mirror of
https://github.com/getcargohq/cargo-skills.git
synced 2026-09-14 19:17:15 +08:00
010ae38779
* Playbooks for parallel and exa, and a 20x price error in the stage map Section 11 claims every credits-based provider in the catalog has a playbook. Nine do not: brightData, builtwith, datachimp, exa, openRouter, parallel, proxycurl, sillage and x. This adds the two that matter most for research and leaves the claim to be squared with the other seven separately. Both are worth having because both beat what the recipes currently reach for. parallel.extract reads a known URL for 0.025, half of firecrawl.scrape. parallel.createTask is the only action in the catalog that fills a caller-supplied output schema, and at processor lite it is 0.125 against linkup.instruct at 1. exa.search is the only rung with a document-type category filter and publication-date bounds, which is what makes it worth 0.175 fixed plus 0.025 an item over a 0.05 flat search. Every action, input and price here was read from the live integration catalog rather than from documentation, per the lesson this pack keeps relearning: a capability claim needs the implementation, not the docs index. That is also how the error surfaced. references/stage-action-map.md priced serper.search at 1. It is 0.05, and provider-playbooks/serper.md had it right throughout, so the map was steering agents away from the cheapest search rung by a factor of twenty. alternatives.md carried the same wrong number for serper.searchPlaces. Both corrected, and the correction is stated in the table rather than made silently. The Web research table is rebuilt around the actual decision: known URL means parallel.extract, plain keyword means serper, document-type or date filter means exa, structured output means parallel.createTask at lite. The createTask ladder gets its own table and a warning. processor is required with no default, and the top rung is 60 credits per record, which is more than a full contact waterfall. Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com> Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> * Ban datachimp in code, and close three of the playbook gaps Two things, both about the same claim: section 11 said every credits-based provider has a playbook, and nine did not. Datachimp is removed everywhere and cannot come back. Aurelien's instruction, 2026-08-15. It appeared in the generated catalog and in two lines of cargo-storage prose. Deleting those would have lasted until the next --refresh, so the removal is enforced in code instead: a DENIED_INTEGRATIONS set filters it out of the catalog on both write and read, and skills-lint fails on the string anywhere in the repo, which is what catches the hand-written half no generator touches. It was also an integration with zero callable actions, so nothing was lost. Three playbooks added, each earning its place on price rather than on coverage. builtwith's getDomainSummary is FREE and answers most stack questions, so the playbook's whole argument is that it runs in front of enrichDomain (1); the current recipes reach straight for a paid rung. sillage's searchLeads is also free and is the only free signal rung in the catalog, which makes it first on any signal question by definition. x is fourteen actions at 0.02, the cheapest signal surface there is, and the playbook spends most of its length on why that is dangerous rather than on how to use it. Three are deliberately not written and the claim now names them instead of overstating. openRouter exposes a model lister and no credits-based actions, so there is nothing to document. proxycurl is excluded on instruction. brightData is excluded because its entire action set is consumer social-platform scraping (Instagram, TikTok, Facebook), which acceptable-use already refuses for person targeting: documenting it as a GTM provider would contradict the gate two files away. Every action, input and price read from the live integration catalog. Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com> Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> * x: a cost table a validator can read, not a grouping a human can The action list was grouped by input shape, which reads well and parses to nothing. Every other playbook here carries `| action | cost | inputs | use for |` and the downstream validator in getcargohq/gtm-skills asserts a skill's quoted price against that column, so a skill calling any x action would have failed with "not in that provider's cost table" and sent someone hunting for a missing price rather than a missing table shape. The four engagement-list actions keep their warning in the Use for column instead of only in the anti-patterns, since that column is what a reader scans. Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com> Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> * Install the whole bundle by default, and stop hand-counting installs skills.sh counts per skill, and one `skills add` sends a single event carrying every skill it installed. So the command in the README decides how many counters a real install moves. `--all` is now the documented form: it is shorthand for `--skill '*' --agent '*' -y` and behaves the same in a terminal, in CI, and inside an agent, where a bare add would drop into an interactive picker. Two stale hand-maintained facts removed rather than corrected, which is the same call this repo made about "sixteen skills" in the CLI help. The install total read "installed 54,885 times". The live figure is 58,245 across the seventeen skills. A number in prose that only moves when somebody remembers to move it will be wrong again next week, and this one is a public claim about traction, which is the worst kind to be casually wrong about. The gtm-skills section listed twelve skills by name. That repo has grown past twelve, so the list now points at its README instead: a hand-maintained copy of someone else's list is a copy that goes stale. Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com> Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> --------- Co-authored-by: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> Co-authored-by: Auree Aubert <16732193+aureeaubert@users.noreply.github.com>
205 lines
7.0 KiB
TypeScript
205 lines
7.0 KiB
TypeScript
/**
|
|
* Keeps the integration/provider name lists inside SKILL.md descriptions in
|
|
* sync with what the workspace actually supports.
|
|
*
|
|
* Proper nouns are the highest-precision routing tokens an agent has: a user
|
|
* who says "enrich this with FullEnrich" or "connect my HubSpot" names
|
|
* something Cargo supports, and the description is the only text the agent
|
|
* weighs before loading a skill. Hand-maintaining those lists guarantees
|
|
* drift, so they are generated:
|
|
*
|
|
* cargo-gtm `Providers: …` ← cargo-gtm/provider-playbooks/*.md
|
|
* cargo-connection `Integrations: …` ← .github/data/integrations.json
|
|
* (systems-of-record categories)
|
|
*
|
|
* Usage:
|
|
* node .github/scripts/sync-trigger-slugs.ts # rewrite descriptions
|
|
* node .github/scripts/sync-trigger-slugs.ts --check # CI: fail if stale
|
|
* node .github/scripts/sync-trigger-slugs.ts --refresh # re-pull the catalog
|
|
* (needs a logged-in
|
|
* cargo-ai CLI)
|
|
*
|
|
* Requires Node >= 22.18 (run as .ts via native type-stripping).
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
import { execFileSync } from "node:child_process";
|
|
import { join, dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const catalogPath = join(repoRoot, ".github/data/integrations.json");
|
|
|
|
/**
|
|
* Integrations that must never reach a skill, a description, or the committed
|
|
* catalog, whatever the API returns.
|
|
*
|
|
* This is a product decision, not a technical one, so it is enforced in code
|
|
* rather than left to whoever next runs `--refresh`. The catalog is generated,
|
|
* so deleting an entry by hand lasts exactly until the next refresh puts it
|
|
* back; the filter below is what makes the removal stick.
|
|
*
|
|
* `datachimp` — excluded on Aurelien's instruction, 2026-08-15. It is also an
|
|
* integration with **zero callable actions**, so nothing was lost.
|
|
*/
|
|
const DENIED_INTEGRATIONS = new Set(["datachimp"]);
|
|
|
|
/**
|
|
* Categories whose integrations are systems a user *names* when they want to
|
|
* connect Cargo to something they already run — CRMs, sequencers, warehouses,
|
|
* ad platforms, support desks. The `enrichment` category is deliberately
|
|
* excluded: those are routed by cargo-gtm via provider playbooks, and listing
|
|
* ~73 of them twice would blunt both descriptions.
|
|
*/
|
|
const OPS_CATEGORIES = new Set([
|
|
"sales",
|
|
"marketing",
|
|
"storage",
|
|
"analytics",
|
|
"support",
|
|
"finance",
|
|
]);
|
|
|
|
/** Freeform-category integrations that are still recognizable destinations. */
|
|
const OPS_EXTRAS = ["slack", "microsoftTeams", "notionMcp", "http"];
|
|
|
|
interface Integration {
|
|
slug: string;
|
|
name: string;
|
|
category: string;
|
|
actions: number;
|
|
credits: boolean;
|
|
}
|
|
|
|
function refreshCatalog(): void {
|
|
const raw = execFileSync("cargo-ai", ["connection", "integration", "list"], {
|
|
encoding: "utf8",
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
});
|
|
const parsed = JSON.parse(raw) as { integrations?: unknown[] };
|
|
const list = parsed.integrations ?? [];
|
|
const integrations: Integration[] = list
|
|
.filter((raw) => !DENIED_INTEGRATIONS.has(String((raw as Record<string, any>).slug)))
|
|
.map((raw) => {
|
|
const i = raw as Record<string, any>;
|
|
return {
|
|
slug: String(i.slug),
|
|
name: String(i.name ?? i.slug),
|
|
category: String(i.category ?? "other"),
|
|
actions: Object.keys(i.actions ?? {}).length,
|
|
credits: Boolean(i.connector?.credits?.isCompatible),
|
|
};
|
|
})
|
|
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
|
|
writeFileSync(
|
|
catalogPath,
|
|
JSON.stringify(
|
|
{
|
|
$comment:
|
|
"Generated by .github/scripts/sync-trigger-slugs.ts --refresh — do not hand-edit.",
|
|
generatedFrom: "cargo-ai connection integration list",
|
|
count: integrations.length,
|
|
creditsBasedCount: integrations.filter((i) => i.credits).length,
|
|
integrations,
|
|
},
|
|
null,
|
|
2,
|
|
) + "\n",
|
|
);
|
|
console.log(`Wrote ${catalogPath} (${integrations.length} integrations)`);
|
|
}
|
|
|
|
function loadCatalog(): Integration[] {
|
|
if (!existsSync(catalogPath)) {
|
|
throw new Error(
|
|
`${catalogPath} is missing — regenerate it with: node .github/scripts/sync-trigger-slugs.ts --refresh`,
|
|
);
|
|
}
|
|
// Filtered on read as well as on refresh: a hand-edited catalog is still a
|
|
// catalog, and this list must hold however the file got there.
|
|
return (JSON.parse(readFileSync(catalogPath, "utf8")) as { integrations: Integration[] })
|
|
.integrations;
|
|
}
|
|
|
|
function gtmProviderSlugs(): string[] {
|
|
const dir = join(repoRoot, "cargo-gtm/provider-playbooks");
|
|
return readdirSync(dir)
|
|
.filter((f) => f.endsWith(".md"))
|
|
.map((f) => f.replace(/\.md$/, ""))
|
|
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
}
|
|
|
|
function opsIntegrationSlugs(catalog: Integration[]): string[] {
|
|
const slugs = catalog
|
|
.filter((i) => OPS_CATEGORIES.has(i.category))
|
|
.map((i) => i.slug);
|
|
for (const extra of OPS_EXTRAS) {
|
|
if (catalog.some((i) => i.slug === extra)) slugs.push(extra);
|
|
}
|
|
return [...new Set(slugs)].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
}
|
|
|
|
/** Replace `<label>: a, b, c.` inside a SKILL.md description line. */
|
|
function rewriteDescription(
|
|
skillDir: string,
|
|
label: string,
|
|
slugs: string[],
|
|
suffix = "",
|
|
): { path: string; before: string; after: string } {
|
|
const path = join(repoRoot, skillDir, "SKILL.md");
|
|
const before = readFileSync(path, "utf8");
|
|
const list = slugs.join(", ") + suffix;
|
|
const pattern = new RegExp(`(^description:.*?${label}: )[^.]*\\.`, "m");
|
|
if (!pattern.test(before)) {
|
|
throw new Error(
|
|
`${skillDir}/SKILL.md description has no \`${label}: … .\` sentence for the generator to fill.`,
|
|
);
|
|
}
|
|
const after = before.replace(pattern, `$1${list}.`);
|
|
return { path, before, after };
|
|
}
|
|
|
|
function main(): void {
|
|
if (process.argv.includes("--refresh")) {
|
|
refreshCatalog();
|
|
}
|
|
|
|
const catalog = loadCatalog();
|
|
const ops = opsIntegrationSlugs(catalog);
|
|
const edits = [
|
|
rewriteDescription("cargo-gtm", "Providers", gtmProviderSlugs()),
|
|
rewriteDescription(
|
|
"cargo-connection",
|
|
"Integrations",
|
|
ops,
|
|
`, and ${catalog.length - ops.length} more`,
|
|
),
|
|
];
|
|
|
|
const stale = edits.filter((e) => e.before !== e.after);
|
|
if (process.argv.includes("--check")) {
|
|
if (stale.length) {
|
|
for (const e of stale) console.error(`stale slug list: ${e.path}`);
|
|
console.error(
|
|
"Regenerate with: node .github/scripts/sync-trigger-slugs.ts",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log(
|
|
`Trigger slug lists are up to date (${gtmProviderSlugs().length} providers, ${ops.length} integrations).`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
for (const e of stale) {
|
|
writeFileSync(e.path, e.after);
|
|
console.log(`Updated ${e.path}`);
|
|
}
|
|
console.log(
|
|
`${gtmProviderSlugs().length} GTM providers, ${ops.length} named integrations (of ${catalog.length} in the catalog).`,
|
|
);
|
|
}
|
|
|
|
main();
|