mirror of
https://github.com/getcargohq/cargo-skills.git
synced 2026-09-14 19:17:15 +08:00
fb95217c52
* Make the cookbook menu a local file, and route on one-off vs standing cargo-cdk told the agent to "read the cookbook menu (the repo README's table)" — a network fetch to another GitHub repo, mid-task, which an agent may simply not make. When it does not, it authors from scratch a GTM outcome that was already sitting there written. A menu you have to go and get is a menu that gets skipped. So the menu becomes a local reference, generated from the cookbook repo: .github/data/cookbooks.json committed snapshot .github/scripts/sync-cookbooks.ts --refresh to re-pull, --check in CI cargo-cdk/references/cookbooks.md generated prose Same shape as sync-trigger-slugs.ts, deliberately: CI reads the snapshot and never the network, so a change in the cookbook repo cannot turn this build red on its own. Staleness is fixed on purpose, with --refresh. Two things the pointers got wrong, beyond being remote: Routing. cargo-cdk's description triggers on "as code", "in git", "Terraform for Cargo". Nobody says that when what they want is a rep cockpit. The real seam is one-off versus standing: "build our TAM" is cargo-gtm when the user wants a list today and a cookbook when they want a pipeline that keeps producing it. The words are identical, so both skills now say so explicitly and the description carries the outcome vocabulary. What a cookbook is. Both pointers called them "scaffoldable outcome templates", which invites copy-and-fill. The code is a worked example: each cookbook declares what may be reshaped, what must hold, and what must be answered, and installing one means adapting it until it is this company's code. Also carries the --force warning into both, because the damage is silent: `cdk init --force` into a non-empty directory replaces the project's package.json and reverts adapted cookbook code while cargo.state.json survives, so the next plan diffs a live workspace against code nobody wrote. Note: the snapshot includes `variations`, which land in cargo-cookbooks#3. Re-run --refresh once that merges. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Read the menu from SKILL.md frontmatter; leave "install a cookbook" to deploy-cookbook cargo-cookbooks folded cookbook.json into SKILL.md (every reader of the contract is an agent, and agents read markdown), so sync-cookbooks.ts now reads frontmatter (outcome, state, chain) plus `kind` and `requires` from cargo.scaffold.json, and the variations table under "## What you can change". Foundations take their one-liner from the README's first paragraph. No new dependency: the frontmatter is validated as real YAML in its own repo, so four scalar fields do not need a parser here. Two corrections to the previous commit on this branch: - cargo-cdk's description carried "install a cookbook" verbatim, which is deploy-cookbook's own trigger. With both installed the tie went to cargo-cdk, so the skill that merely points at cookbooks beat the one that installs them. cargo-cdk now carries discovery vocabulary ("is there a cookbook for this", "start from a cookbook") and leaves the install phrase to the skill that does the work. - `cargo-ai manifest add` is the way into an existing project (copy-in, existing files kept, every file hashed in manifest.json). It replaces the "scaffold to a temp dir and copy across" advice, which was a workaround for a command that already existed. skills-lint learns the `manifest` domain, shipped in 1.0.57. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * List every scaffoldable cookbook in the menu, not only the ones with a skill The menu hid 16 of 20 cookbooks because they had no SKILL.md yet. The old pointer sent the agent to a README that listed all 20, so this was a regression: a user asking for contact sourcing was told nothing existed while the code sat there and cdk init --from scaffolded it fine. Every outcome is listed now; the Skill column says whether the adaptation contract exists or the README and PLACEHOLDER comments are the interview. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * cargo-cdk: no session reload needed to reach a cookbook skill mid-task A cargo-skills user who asks for a cookbook outcome is one hop from the skill that installs it. Say exactly what to do in that hop: skills add, then read the two SKILL.md files directly; or `npx skills use` to read one without installing. And name the --dir default, which lands a cookbook in infra/ where a plain CDK project imports nothing from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * menu: manifest add, not scaffold-to-temp, into an existing project Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * regenerate cargo-cdk metadata Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * menu: read state from the cookbook manifest, not from customer-facing frontmatter cargo-cookbooks moved state, approval and chain out of SKILL.md frontmatter into cargo.scaffold.json, because SKILL.md is what a customer installs and Cargo's bookkeeping is not their concern. The menu now takes everything from the manifest and each README's first paragraph; SKILL.md contributes only the variations table. One code path for foundations, unconverted and converted outcomes alike. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * regenerate metadata Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Point at gtm-skills' catalog.json: the CDK examples are skills there now cargo-cookbooks merged into gtm-skills as flat, self-contained skills, and that repo publishes one validated catalog.json. This script fetches it and parses no markdown; the menu lists the skills that carry a CDK example and the folders whose skill is not written yet. cargo-cdk and the router say what changed in the model: the agent copies the folder in and reconciles it, there is no scaffolder, and the shell comes from cdk init --template blank. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * menu: gtm-skills keeps only skills at the root, so no 'no skill yet' section Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address review: GTM_SKILLS_ROOT accepts a dir or the catalog file; routing cases for cargo-cdk's new vocabulary Bugbot's third finding (foundation variations dropped from the menu) is moot: gtm-skills dissolved base-gtm and crm-sync into the skills that used them, so there are no foundations and nothing to render. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Name things what they are: CDK examples, not cookbooks The script, snapshot, reference file and type were named after the repo the examples came from before the merge. That repo is being archived and the examples are skills in gtm-skills now, so the names follow: sync-cdk-examples, .github/data/cdk-examples.json, references/cdk-examples.md, CdkExample. The llms.txt link to cargo-cookbooks goes; the linter's note that the CLI's manifest domain includes 'manifest add' goes too, ahead of its removal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * cookbook is the name of the kind: rename the menu, snapshot and sync back to it gtm-skills#12 fixes metadata.source as one-off | cookbook. So the menu of gtm-skills that carry a worked CDK example is the cookbook menu, and the file, snapshot, script and type are named for the kind, not for the repo they once came from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
225 lines
7.9 KiB
TypeScript
225 lines
7.9 KiB
TypeScript
/**
|
|
* Generates the repo-root llms.txt from the actual skill tree, so the index
|
|
* can never drift from the skills that really ship (a stale hand-written
|
|
* llms.txt advertises skills that no longer exist).
|
|
*
|
|
* node .github/scripts/generate-llms-txt.ts # write llms.txt
|
|
* node .github/scripts/generate-llms-txt.ts --check # CI: fail if stale
|
|
*
|
|
* Requires Node >= 22.18 (run as .ts via native type-stripping, like the QA
|
|
* scripts in cargo-gtm/scripts/).
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, writeFileSync, existsSync, statSync } from "node:fs";
|
|
import { join, dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const outPath = join(repoRoot, "llms.txt");
|
|
const repoUrl = "https://github.com/getcargohq/cargo-skills";
|
|
/** Standalone single-job slices of this pack, for people who want one thing. */
|
|
const gtmSkillsUrl = "https://github.com/getcargohq/gtm-skills";
|
|
|
|
interface Skill {
|
|
name: string;
|
|
description: string;
|
|
dir: string;
|
|
}
|
|
|
|
function parseFrontmatter(skillMdPath: string): Skill {
|
|
const lines = readFileSync(skillMdPath, "utf8").split("\n");
|
|
if (lines[0]?.trim() !== "---") {
|
|
throw new Error(`${skillMdPath}: no frontmatter block`);
|
|
}
|
|
let name = "";
|
|
let description = "";
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (line.trim() === "---") break;
|
|
const match = /^(name|description):\s*(.+)$/.exec(line);
|
|
if (!match) continue;
|
|
let value = match[2].trim();
|
|
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
|
|
if (match[1] === "name") name = value;
|
|
else description = value;
|
|
}
|
|
if (!name || !description) {
|
|
throw new Error(`${skillMdPath}: missing name or description in frontmatter`);
|
|
}
|
|
return { name, description, dir: dirname(skillMdPath) };
|
|
}
|
|
|
|
function collectSkills(): Skill[] {
|
|
const skills: Skill[] = [];
|
|
for (const entry of readdirSync(repoRoot)) {
|
|
const skillMd = join(repoRoot, entry, "SKILL.md");
|
|
if (statSync(join(repoRoot, entry), { throwIfNoEntry: false })?.isDirectory() && existsSync(skillMd)) {
|
|
skills.push(parseFrontmatter(skillMd));
|
|
}
|
|
}
|
|
// Router first, then onboarding, then the outcome skill, then the rest A→Z —
|
|
// same priority order the router itself teaches.
|
|
const pinned = ["cargo", "cargo-quickstart", "cargo-gtm"];
|
|
skills.sort((a, b) => {
|
|
const pa = pinned.indexOf(a.name);
|
|
const pb = pinned.indexOf(b.name);
|
|
if (pa !== -1 || pb !== -1) {
|
|
return (pa === -1 ? pinned.length : pa) - (pb === -1 ? pinned.length : pb);
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
return skills;
|
|
}
|
|
|
|
/**
|
|
* Recipes and provider playbooks are the job-named, addressable layer under the
|
|
* skills — "build-tam", "job-change-monitoring", "linkedin-url-lookup". Skill
|
|
* names alone are the wrong granularity for anyone (human or crawler) searching
|
|
* by the job they want done, so the index lists them too, with the one-line
|
|
* summary each file carries.
|
|
*/
|
|
interface Doc {
|
|
slug: string;
|
|
path: string;
|
|
summary: string;
|
|
}
|
|
|
|
/** First meaningful prose line of a markdown file, trimmed to one sentence. */
|
|
function summarize(path: string, fallback: string): string {
|
|
const lines = readFileSync(path, "utf8").split("\n");
|
|
let inFrontmatter = false;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
if (i === 0 && line === "---") {
|
|
inFrontmatter = true;
|
|
continue;
|
|
}
|
|
if (inFrontmatter) {
|
|
if (line === "---") inFrontmatter = false;
|
|
continue;
|
|
}
|
|
if (!line) continue;
|
|
if (line.startsWith("#")) continue;
|
|
if (line.startsWith(">") || line.startsWith("|") || line.startsWith("```")) continue;
|
|
// Strip markdown emphasis/links down to plain prose.
|
|
const plain = line
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
.replace(/[*_`]/g, "")
|
|
.trim();
|
|
if (plain.length < 20) continue;
|
|
const sentence = plain.split(/(?<=\.)\s/)[0];
|
|
return sentence.length > 220 ? sentence.slice(0, 217).trimEnd() + "…" : sentence;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function collectDocs(skillDir: string, subdir: string, fallback: string): Doc[] {
|
|
const dir = join(repoRoot, skillDir, subdir);
|
|
if (!existsSync(dir)) return [];
|
|
return readdirSync(dir)
|
|
.filter((f) => f.endsWith(".md"))
|
|
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
|
.map((f) => ({
|
|
slug: f.replace(/\.md$/, ""),
|
|
path: `${skillDir}/${subdir}/${f}`,
|
|
summary: summarize(join(dir, f), fallback),
|
|
}));
|
|
}
|
|
|
|
function render(skills: Skill[]): string {
|
|
const skillLines = skills
|
|
.map((s) => `- [${s.name}](${repoUrl}/blob/main/${s.name}/SKILL.md): ${s.description}`)
|
|
.join("\n");
|
|
|
|
const recipes = [
|
|
...collectDocs("cargo-gtm", "recipes", "A step-by-step GTM playbook."),
|
|
...collectDocs("cargo-cdk", "recipes", "A step-by-step workspace-as-code playbook."),
|
|
];
|
|
const recipeLines = recipes
|
|
.map((r) => `- [${r.slug}](${repoUrl}/blob/main/${r.path}): ${r.summary}`)
|
|
.join("\n");
|
|
|
|
const playbooks = collectDocs(
|
|
"cargo-gtm",
|
|
"provider-playbooks",
|
|
"Action slugs, config shapes, costs, and cost traps for this provider.",
|
|
);
|
|
const playbookLines = playbooks
|
|
.map((p) => `- [${p.slug}](${repoUrl}/blob/main/${p.path}): ${p.summary}`)
|
|
.join("\n");
|
|
|
|
return `# Cargo Agent Skills
|
|
|
|
> ${skills.length} agent skills that teach AI coding agents the Cargo CLI — sourcing, enrichment, verification, signal monitoring, workflow orchestration, workspace-as-code (CDK), diagnostics, and billing for GTM automation on [Cargo](https://getcargo.ai).
|
|
|
|
## Install
|
|
|
|
Paste into Claude Code, Codex, or Cursor:
|
|
|
|
\`\`\`
|
|
Install Cargo by following every step in https://api.getcargo.io/agent-install.txt
|
|
\`\`\`
|
|
|
|
Or manually:
|
|
|
|
\`\`\`bash
|
|
npx skills add getcargohq/cargo-skills
|
|
npm install -g @cargo-ai/cli
|
|
cargo-ai login --email you@company.com # emailed code, no browser; creates the account on first use
|
|
\`\`\`
|
|
|
|
A new account starts with **100 free credits, no card** — roughly 5,000 leads sourced or 1,000 verified-email enrichments. The two-minute quickstart demo spends about 0.5 of them, so an agent can install, sign the user up, and return a real deliverable in one turn.
|
|
|
|
## Skills
|
|
|
|
${skillLines}
|
|
|
|
## Recipes
|
|
|
|
Step-by-step playbooks for a specific job. An agent loads the parent skill, then follows the matching recipe as its execution plan.
|
|
|
|
${recipeLines}
|
|
|
|
## Provider playbooks
|
|
|
|
Per-provider action slugs, config shapes, credit costs, cost traps, and recurring-use cadence. Read the playbook before calling a paid action from that provider.
|
|
|
|
${playbookLines}
|
|
|
|
## Single-job skills
|
|
|
|
Want one job rather than the whole bundle? [\`getcargohq/gtm-skills\`](${gtmSkillsUrl}) packages twelve of these outcomes as standalone skills, named after the job — \`find-work-email\`, \`find-b2b-leads\`, \`enrich-linkedin-profile\`, \`find-stakeholders\`, \`track-job-changes\`, \`verify-email-list\`, and more. Each installs on its own and runs inside the 100 free credits.
|
|
|
|
\`\`\`bash
|
|
npx skills add getcargohq/gtm-skills/<skill-name>
|
|
\`\`\`
|
|
|
|
Install those **or** this pack, not both: each standalone skill defers to \`cargo-gtm\` when the pack is present, so routing stays unambiguous.
|
|
|
|
## Documentation
|
|
|
|
- [Cargo](https://getcargo.ai)
|
|
- [Cargo API docs](https://docs.getcargo.ai/api-reference/introduction)
|
|
- [GitHub repository](${repoUrl})
|
|
- [Standalone GTM skills, run-once and worked CDK examples](${gtmSkillsUrl})
|
|
|
|
<!-- Generated by .github/scripts/generate-llms-txt.ts — do not edit by hand. -->
|
|
`;
|
|
}
|
|
|
|
const generated = render(collectSkills());
|
|
|
|
if (process.argv.includes("--check")) {
|
|
const current = existsSync(outPath) ? readFileSync(outPath, "utf8") : "";
|
|
if (current !== generated) {
|
|
console.error(
|
|
"llms.txt is stale. Regenerate with: node .github/scripts/generate-llms-txt.ts",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log("llms.txt is up to date.");
|
|
} else {
|
|
writeFileSync(outPath, generated);
|
|
console.log(`Wrote ${outPath}`);
|
|
}
|