Files
getcargohq__cargo-skills/.github/scripts/generate-llms-txt.ts
Auree Aubert fb95217c52 Make the cookbook menu a local file, and route on one-off vs standing (#110)
* 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>
2026-08-18 17:57:26 -07:00

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}`);
}