claude-setup 1.1.9 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ import { updateManifest } from "../manifest.js";
6
6
  import { buildAddCommand } from "../builder.js";
7
7
  import { estimateTokens, estimateCost } from "../tokens.js";
8
8
  import { c } from "../output.js";
9
+ import { installMarketplaceFetcher } from "./init.js";
9
10
  function ensureDir(dir) {
10
11
  if (!existsSync(dir))
11
12
  mkdirSync(dir, { recursive: true });
@@ -46,6 +47,7 @@ capabilities that need documentation, MCP servers, skills, and hooks together.
46
47
  const tokens = estimateTokens(content);
47
48
  const cost = estimateCost(tokens);
48
49
  ensureDir(".claude/commands");
50
+ installMarketplaceFetcher();
49
51
  writeFileSync(".claude/commands/stack-add.md", content, "utf8");
50
52
  await updateManifest("add", collected, {
51
53
  input: userInput,
@@ -1,3 +1,5 @@
1
+ /** Install the marketplace-fetcher subagent so /stack-add can spawn it */
2
+ export declare function installMarketplaceFetcher(cwd?: string): void;
1
3
  export declare function runInit(opts?: {
2
4
  dryRun?: boolean;
3
5
  template?: string;
@@ -71,6 +71,71 @@ No further action needed — the output IS the status.
71
71
  writeFileSync(filepath, content, "utf8");
72
72
  }
73
73
  }
74
+ /** Install the marketplace-fetcher subagent so /stack-add can spawn it */
75
+ export function installMarketplaceFetcher(cwd = process.cwd()) {
76
+ const agentsDir = join(cwd, ".claude", "agents");
77
+ ensureDir(agentsDir);
78
+ const filepath = join(agentsDir, "marketplace-fetcher.md");
79
+ // Always overwrite — this is a system agent, not user-authored
80
+ writeFileSync(filepath, `---
81
+ name: marketplace-fetcher
82
+ description: Fetches skills and agents from all 4 marketplace catalogs. Spawned automatically by stack-add. Runs in isolation, writes to disk, returns only the install confirmation.
83
+ tools: Bash
84
+ model: haiku
85
+ ---
86
+
87
+ You are a marketplace fetch agent. You receive a prompt containing marketplace instructions with bash code blocks. Your job is to RUN those bash commands, use the output to find matching files, download them, and confirm the install.
88
+
89
+ ## CRITICAL RULES — read before doing anything
90
+
91
+ 1. **You MUST run curl commands.** Your entire purpose is to fetch from remote catalogs. If you finish without running at least 3 curl commands, you have failed.
92
+ 2. **NEVER skip to custom creation early.** You may only create custom content after you have attempted ALL catalog steps with real curl calls and each one returned empty or errored.
93
+ 3. **Use relative paths only.** Never prepend \`cd /some/path &&\` to commands. You are already in the project directory.
94
+ 4. **Pipe curl output through node parsers.** Never let raw JSON/README enter the context. Every curl MUST pipe through \`| node -e "..."\` to extract only the lines you need.
95
+ 5. **A curl failure or empty result means: try the next step.** It does NOT mean stop. Log what failed in one line, then continue to the next step.
96
+ 6. **Fill in template variables.** The instructions contain placeholder variables like \`CATEGORY="09-meta-orchestration"\` and \`SKILL_DIR="matched-directory"\`. Replace these with ACTUAL values from the previous step's output.
97
+ 7. **Verify downloads.** After every curl -o, check the file exists and has >50 characters of content. If empty or stub: delete it, log failure, continue.
98
+
99
+ ## How to execute the instructions
100
+
101
+ Your prompt contains numbered STEPs with bash code blocks. For each STEP:
102
+
103
+ 1. Run the first bash command (the catalog listing/search)
104
+ 2. Read the output — it shows available files/directories
105
+ 3. Pick the entry that best matches the user's request
106
+ 4. Substitute that entry name into the download command
107
+ 5. Run the download command
108
+ 6. Verify the downloaded file has real content
109
+ 7. If it works → return the success line. If not → go to next STEP.
110
+
111
+ ## Return format
112
+
113
+ Return exactly ONE line:
114
+ - \`INSTALLED .claude/agents/<file> <bytes>b\` or
115
+ - \`INSTALLED .claude/skills/<dir>/SKILL.md <bytes>b\` or
116
+ - \`CREATED .claude/agents/<file> <bytes>b\` (only after ALL catalogs exhausted) or
117
+ - \`CREATED .claude/skills/<dir>/SKILL.md <bytes>b\` or
118
+ - \`FAILED no match in any catalog and custom creation not possible\`
119
+
120
+ ## Example of correct behavior
121
+
122
+ If the instructions say:
123
+ \`\`\`bash
124
+ curl -sf "https://api.github.com/repos/X/Y/contents/categories/03-infrastructure" | node -e "..."
125
+ \`\`\`
126
+ And the output shows: \`docker-compose-agent.md\`, \`kubernetes-agent.md\`, \`terraform-agent.md\`
127
+
128
+ Then for the download command that says \`AGENT_FILE="matched-agent.md"\`, you substitute:
129
+ \`\`\`bash
130
+ AGENT_FILE="kubernetes-agent.md"
131
+ curl -sf "https://raw.githubusercontent.com/X/Y/main/categories/03-infrastructure/kubernetes-agent.md" -o ".claude/agents/kubernetes-agent.md"
132
+ \`\`\`
133
+
134
+ Then verify: \`wc -c ".claude/agents/kubernetes-agent.md"\`
135
+
136
+ Now execute the instructions in your prompt. Start with STEP 1 immediately.
137
+ `, "utf8");
138
+ }
74
139
  function installTokenHook(cwd = process.cwd()) {
75
140
  // Write the hook script
76
141
  const hooksDir = join(cwd, ".claude", "hooks");
@@ -139,6 +204,7 @@ export async function runInit(opts = {}) {
139
204
  writeFileSync(".claude/commands/stack-init.md", content, "utf8");
140
205
  writeFileSync(".claude/commands/stack-sync.md", buildBootstrapSync(), "utf8");
141
206
  installBootstrapCommands(".claude/commands");
207
+ installMarketplaceFetcher();
142
208
  await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
143
209
  installTokenHook();
144
210
  // Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
@@ -181,6 +247,7 @@ Claude Code will ask 3 questions, then set up your environment.
181
247
  writeFileSync(".claude/commands/stack-init.md", orchestrator, "utf8");
182
248
  writeFileSync(".claude/commands/stack-sync.md", buildBootstrapSync(), "utf8");
183
249
  installBootstrapCommands(".claude/commands");
250
+ installMarketplaceFetcher();
184
251
  await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
185
252
  installTokenHook();
186
253
  // Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
@@ -48,6 +48,33 @@ export const SAAS_PACKS = [
48
48
  "Supabase", "Vercel", "OpenRouter", "GitHub", "Azure", "MongoDB",
49
49
  "Playwright", "Tavily", "Stripe", "Slack", "Linear", "Notion",
50
50
  ];
51
+ // ── Adjacent category map (for fallback when primary SKIP) ─────────────
52
+ const ADJACENT_CATEGORIES = {
53
+ "01-core-development": ["02-language-specialists", "06-developer-experience"],
54
+ "02-language-specialists": ["01-core-development", "06-developer-experience"],
55
+ "03-infrastructure": ["04-quality-security", "09-meta-orchestration"],
56
+ "04-quality-security": ["03-infrastructure", "01-core-development"],
57
+ "05-data-ai": ["10-research-analysis", "07-specialized-domains"],
58
+ "06-developer-experience": ["01-core-development", "02-language-specialists"],
59
+ "07-specialized-domains": ["05-data-ai", "08-business-product"],
60
+ "08-business-product": ["07-specialized-domains", "09-meta-orchestration"],
61
+ "09-meta-orchestration": ["03-infrastructure", "04-quality-security"],
62
+ "10-research-analysis": ["05-data-ai", "07-specialized-domains"],
63
+ };
64
+ /** Expand target categories with their adjacent neighbors, deduplicating */
65
+ function expandWithAdjacent(targets) {
66
+ const seen = new Set(targets);
67
+ const adjacent = [];
68
+ for (const cat of targets) {
69
+ for (const adj of (ADJACENT_CATEGORIES[cat] ?? [])) {
70
+ if (!seen.has(adj)) {
71
+ seen.add(adj);
72
+ adjacent.push(adj);
73
+ }
74
+ }
75
+ }
76
+ return adjacent;
77
+ }
51
78
  // ── Agent detection keywords ────────────────────────────────────────────
52
79
  const AGENT_KEYWORDS = [
53
80
  "agent", "subagent", "sub-agent", "orchestrat", "multi-agent",
@@ -208,6 +235,49 @@ export function classifyRequest(input) {
208
235
  agentCategories: matchAgentCategories(input),
209
236
  };
210
237
  }
238
+ // ── Query keyword extraction (for inline filtering in node parsers) ─────
239
+ /** Extract meaningful search keywords from user input for inline node filtering */
240
+ function buildQueryRegex(input) {
241
+ const stopWords = new Set(["a", "an", "the", "and", "or", "for", "to", "in", "of", "with", "add", "install", "setup", "get", "find", "need", "want", "skill", "agent", "plugin", "tool"]);
242
+ const words = input.toLowerCase()
243
+ .replace(/[^a-z0-9\s-]/g, "")
244
+ .split(/\s+/)
245
+ .filter(w => w.length > 2 && !stopWords.has(w));
246
+ if (words.length === 0)
247
+ return "."; // match everything if no keywords
248
+ return words.join("|");
249
+ }
250
+ /**
251
+ * Build a universal section-aware README parser as an inline node -e script.
252
+ * Rules (no hardcoding):
253
+ * 1. Split README into sections by any heading (## / ### / **Bold**)
254
+ * 2. Find sections whose heading matches the query keywords
255
+ * 3. Extract ALL links from matching sections (relative ./ AND absolute github.com)
256
+ * 4. If no section matches, fall back to all links in the entire README
257
+ * This works for any README structure — ComposioHQ, VoltAgent, or anything else.
258
+ */
259
+ function buildSectionAwareParser(queryRegex) {
260
+ // Each part is carefully escaped for: TypeScript template → bash double-quotes → node -e
261
+ return (`const t=require('fs').readFileSync(0,'utf8');` +
262
+ `const q=/${queryRegex}/i;` +
263
+ // Split by ## headings, ### headings, and **Bold** subsection headers
264
+ `const secs=t.split(/\\n(?=#{2,3}\\s|\\*\\*[A-Z])/);` +
265
+ // 1st pass: sections whose heading matches query
266
+ `let hit=secs.filter(s=>q.test(s.split('\\n')[0]));` +
267
+ // 2nd pass: if no heading matched, find sections that have a link whose name matches
268
+ `if(hit.length===0)hit=secs.filter(s=>{const lk=/\\[([^\\]]+)\\]/g;let x;while((x=lk.exec(s))!=null){if(q.test(x[1]))return true}return false});` +
269
+ // Use matching sections, or fall back to entire README
270
+ `const src=hit.length>0?hit.join('\\n'):t;` +
271
+ // Extract all links — relative (./dir/) and absolute (github.com) and SKILL.md
272
+ `const re=/\\[([^\\]]+)\\]\\(([^)]+)\\)/g;let m;const r=[];` +
273
+ `while((m=re.exec(src))!=null){` +
274
+ `const u=m[2];` +
275
+ `if(u.startsWith('./')||u.includes('github.com')||u.includes('SKILL.md'))` +
276
+ `r.push(m[1]+' | '+u)` +
277
+ `}` +
278
+ `r.slice(0,10).forEach(x=>console.log(x));` +
279
+ `if(r.length===0)console.log('NO_README_MATCHES')`);
280
+ }
211
281
  // ── Marketplace instruction builder ─────────────────────────────────────
212
282
  // Implements Rule 6 (4-catalog exhaustion) and Rule 7 (3-stage fetch).
213
283
  // Agent requests route to VoltAgent subagents first.
@@ -266,10 +336,12 @@ export function buildMarketplaceInstructions(input) {
266
336
  // ── Agent pipeline (VoltAgent subagents → skills fallback) ──────────
267
337
  function buildAgentPipeline(lines, input, agentCategories, categoryFilter) {
268
338
  const safeName = input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
339
+ const queryRegex = buildQueryRegex(input);
269
340
  const targetCategories = agentCategories.length > 0
270
341
  ? agentCategories
271
342
  : ["09-meta-orchestration"];
272
343
  // ── STEP 1: VoltAgent/awesome-claude-code-subagents ──────────────
344
+ const adjacentCategories = expandWithAdjacent(targetCategories);
273
345
  lines.push(`---`);
274
346
  lines.push(``);
275
347
  lines.push(`### STEP 1 — VoltAgent subagents (PRIMARY source for agents — 127+ specialized agents)`);
@@ -277,52 +349,74 @@ function buildAgentPipeline(lines, input, agentCategories, categoryFilter) {
277
349
  lines.push(`This is the preferred source for anything agent-shaped. 10 categories, 127+ agents.`);
278
350
  lines.push(`If a match is found here, install to \`.claude/agents/\` (NOT .claude/skills/).`);
279
351
  lines.push(``);
280
- lines.push(`**1a. List agent files in the matched categories:**`);
281
- lines.push(`If any curl below fails, skip to the next category or STEP 2 immediately.`);
352
+ lines.push(`**1a. List agent files in the primary categories:**`);
282
353
  lines.push(``);
283
354
  for (const cat of targetCategories) {
284
355
  lines.push(`\`\`\`bash`);
285
- lines.push(`# Category: ${cat}`);
356
+ lines.push(`# Primary category: ${cat}`);
286
357
  lines.push(`curl -sf \${GITHUB_TOKEN:+-H "Authorization: token \$GITHUB_TOKEN"} "${VOLTAGENT_SUBAGENTS_API}/${cat}" \\`);
287
358
  lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));` +
288
- `if(!Array.isArray(d)){console.log('SKIP');process.exit(0)}` +
289
- `d.filter(x=>x.name.endsWith('.md')&&x.name!=='README.md').forEach(x=>console.log(x.name))"`);
359
+ `if(Array.isArray(d)===false){console.log('SKIP');process.exit(0)}` +
360
+ `d.filter(x=>x.name.endsWith('.md')&&(x.name==='README.md')===false).forEach(x=>console.log(x.name))"`);
290
361
  lines.push(`\`\`\``);
291
362
  lines.push(``);
292
363
  }
293
- lines.push(`**1b. From the file list above, pick the BEST match for "${input}".**`);
294
- lines.push(`Match by name and relevance. If multiple candidates exist, pick the closest one.`);
364
+ if (adjacentCategories.length > 0) {
365
+ lines.push(`**1b. If ALL primary categories above returned SKIP, try adjacent categories:**`);
366
+ lines.push(`Do NOT skip this step. A SKIP in the primary category does NOT mean VoltAgent has no match.`);
367
+ lines.push(``);
368
+ for (const cat of adjacentCategories) {
369
+ lines.push(`\`\`\`bash`);
370
+ lines.push(`# Adjacent category: ${cat}`);
371
+ lines.push(`curl -sf \${GITHUB_TOKEN:+-H "Authorization: token \$GITHUB_TOKEN"} "${VOLTAGENT_SUBAGENTS_API}/${cat}" \\`);
372
+ lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));` +
373
+ `if(Array.isArray(d)===false){console.log('SKIP');process.exit(0)}` +
374
+ `d.filter(x=>x.name.endsWith('.md')&&(x.name==='README.md')===false).forEach(x=>console.log(x.name))"`);
375
+ lines.push(`\`\`\``);
376
+ lines.push(``);
377
+ }
378
+ }
379
+ lines.push(`**1c. From ALL file lists above (primary + adjacent), pick the BEST match for "${input}".**`);
380
+ lines.push(`Pick the filename closest to the request. If multiple candidates: pick the first relevant one.`);
295
381
  lines.push(``);
296
- lines.push(`**1c. Download the matched agent file (3-stage resolution — this is stage 3):**`);
382
+ lines.push(`**1d. Download and verify the matched agent file (single step):**`);
297
383
  lines.push(`\`\`\`bash`);
298
- lines.push(`# Replace CATEGORY and AGENT_FILE with actual values from 1a/1b`);
384
+ lines.push(`# Replace CATEGORY and AGENT_FILE with actual values from 1a-1c`);
299
385
  lines.push(`CATEGORY="${targetCategories[0]}"`);
300
386
  lines.push(`AGENT_FILE="matched-agent.md"`);
301
387
  lines.push(`mkdir -p ".claude/agents"`);
302
388
  lines.push(`curl -sf "${VOLTAGENT_SUBAGENTS_RAW}/\${CATEGORY}/\${AGENT_FILE}" \\`);
303
- lines.push(` -o ".claude/agents/\${AGENT_FILE}"`);
389
+ lines.push(` -o ".claude/agents/\${AGENT_FILE}" && wc -c ".claude/agents/\${AGENT_FILE}"`);
304
390
  lines.push(`\`\`\``);
391
+ lines.push(`If >50 bytes: agent is installed. Return the success line.`);
392
+ lines.push(`If empty or 0 bytes: delete it, log the failure, try README fallback below.`);
393
+ lines.push(``);
394
+ lines.push(`**1f. README-driven fallback — if ALL category listings (primary + adjacent) returned SKIP:**`);
395
+ lines.push(`You MUST run this before moving to STEP 2. The README contains the full agent listing.`);
305
396
  lines.push(``);
306
- lines.push(`**1d. Verify the file has real content (Rule 7 — must not be empty):**`);
307
397
  lines.push(`\`\`\`bash`);
308
- lines.push(`# File must have frontmatter (---) and body content`);
309
- lines.push(`head -3 ".claude/agents/\${AGENT_FILE}"`);
310
- lines.push(`wc -l ".claude/agents/\${AGENT_FILE}"`);
398
+ lines.push(`# Fetch README parser finds sections matching your query, extracts all links`);
399
+ lines.push(`curl -sf "https://raw.githubusercontent.com/${VOLTAGENT_SUBAGENTS_REPO}/main/README.md" \\`);
400
+ lines.push(` | node -e "${buildSectionAwareParser(queryRegex)}"`);
311
401
  lines.push(`\`\`\``);
312
- lines.push(`If the file is empty or just frontmatter with no body: delete it, log the failure, continue to STEP 2.`);
313
- lines.push(`If the file has real content: agent is installed. Skip to "Install result format" below.`);
314
402
  lines.push(``);
315
- lines.push(`**README-driven fallback:** If a category listing returns a README.md instead of agent files,`);
316
- lines.push(`parse the README for entries with links. Entries may point to external repos follow them`);
317
- lines.push(`using the universal URL resolution rules (extract {owner}/{repo} dynamically from the link).`);
318
- lines.push(`Navigate the foreign repo to locate the agent .md file and download it.`);
403
+ lines.push(`From the output, pick the FIRST entry. Extract the URL (after " | "). Resolve it:`);
404
+ lines.push(`- If URL starts with \`./\`: it is relative to the repo root. Prepend \`https://raw.githubusercontent.com/${VOLTAGENT_SUBAGENTS_REPO}/main/\``);
405
+ lines.push(`- If URL contains \`github.com/OWNER/REPO/blob/BRANCH/\`: replace with \`raw.githubusercontent.com/OWNER/REPO/BRANCH/\``);
406
+ lines.push(`- If URL contains \`github.com/OWNER/REPO/tree/BRANCH/\`: use API \`api.github.com/repos/OWNER/REPO/contents/PATH\` to list files, then download the .md file`);
407
+ lines.push(`Then download:`);
408
+ lines.push(`\`\`\`bash`);
409
+ lines.push(`mkdir -p ".claude/agents"`);
410
+ lines.push(`curl -sf "RESOLVED_URL" -o ".claude/agents/AGENT_FILE.md" && wc -c ".claude/agents/AGENT_FILE.md"`);
411
+ lines.push(`\`\`\``);
412
+ lines.push(`If >50 bytes: installed. If empty or NO_README_MATCHES: continue to STEP 2.`);
319
413
  lines.push(``);
320
414
  // ── STEP 2: jeremylongshore community skills (fallback for agents) ─
321
415
  lines.push(`---`);
322
416
  lines.push(``);
323
417
  lines.push(`### STEP 2 — Community skills catalog (fallback — 416 plugins)`);
324
418
  lines.push(``);
325
- lines.push(`Only reach here if STEP 1 found no match.`);
419
+ lines.push(`**Before continuing:** Document in one line why STEP 1 produced no result.`);
326
420
  lines.push(`Search for agent-like skills in the community catalog.`);
327
421
  lines.push(`If curl fails, skip to STEP 3.`);
328
422
  lines.push(``);
@@ -332,7 +426,7 @@ function buildAgentPipeline(lines, input, agentCategories, categoryFilter) {
332
426
  lines.push(``);
333
427
  lines.push(`### STEP 3 — ComposioHQ service integrations (1000+ skills — fallback)`);
334
428
  lines.push(``);
335
- lines.push(`Only reach here if STEP 1 and 2 found no match.`);
429
+ lines.push(`**Before continuing:** Document in one line why STEPs 1 and 2 produced no result.`);
336
430
  lines.push(`Strong for API and SaaS automation. Skills live in per-directory SKILL.md files.`);
337
431
  lines.push(`If curl fails, skip to STEP 4.`);
338
432
  lines.push(``);
@@ -365,6 +459,7 @@ function buildAgentPipeline(lines, input, agentCategories, categoryFilter) {
365
459
  // ── Skill pipeline (community → VoltAgent skills → ComposioHQ) ──────
366
460
  function buildSkillPipeline(lines, input, categoryFilter, saasMatches) {
367
461
  const safeName = input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
462
+ const queryRegex = buildQueryRegex(input);
368
463
  // ── STEP 1: Official Anthropic plugins ────────────────────────────
369
464
  lines.push(`---`);
370
465
  lines.push(``);
@@ -392,35 +487,55 @@ function buildSkillPipeline(lines, input, categoryFilter, saasMatches) {
392
487
  lines.push(``);
393
488
  lines.push(`### STEP 3 — VoltAgent curated agent skills (production-proven)`);
394
489
  lines.push(``);
395
- lines.push(`Only reach here if STEP 2 found no match.`);
490
+ lines.push(`**Before continuing:** Document in one line why STEP 2 produced no result.`);
396
491
  lines.push(`Curated real-world skills from engineering teams. If curl fails, skip to STEP 4.`);
397
492
  lines.push(``);
398
493
  lines.push(`\`\`\`bash`);
399
494
  lines.push(`curl -sf \${GITHUB_TOKEN:+-H "Authorization: token \$GITHUB_TOKEN"} "${VOLTAGENT_SKILLS_API}" \\`);
400
495
  lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));` +
401
- `if(!Array.isArray(d)){console.log('SKIP');process.exit(0)}` +
402
- `d.filter(x=>x.type==='dir'&&!x.name.startsWith('.')).forEach(x=>console.log(x.name))"`);
496
+ `if(Array.isArray(d)===false){console.log('SKIP');process.exit(0)}` +
497
+ `const q=/${queryRegex}/i;` +
498
+ `const all=d.filter(x=>x.type==='dir'&&(x.name[0]==='.')===false).map(x=>x.name);` +
499
+ `const matched=all.filter(n=>q.test(n));` +
500
+ `const out=matched.length>0?matched:all;` +
501
+ `out.slice(0,10).forEach(x=>console.log(x))"`);
403
502
  lines.push(`\`\`\``);
404
- lines.push(`If any directory name matches "${input}", fetch its SKILL.md:`);
503
+ lines.push(`Pick the FIRST directory from output that matches "${input}" and download:`);
405
504
  lines.push(`\`\`\`bash`);
505
+ lines.push(`# Replace SKILL_DIR with the actual directory name from the listing above`);
406
506
  lines.push(`SKILL_DIR="matched-skill"`);
407
507
  lines.push(`mkdir -p ".claude/skills/\${SKILL_DIR}"`);
408
508
  lines.push(`curl -sf "https://raw.githubusercontent.com/${VOLTAGENT_SKILLS_REPO}/main/\${SKILL_DIR}/SKILL.md" \\`);
409
509
  lines.push(` -o ".claude/skills/\${SKILL_DIR}/SKILL.md"`);
510
+ lines.push(`wc -c ".claude/skills/\${SKILL_DIR}/SKILL.md"`);
511
+ lines.push(`\`\`\``);
512
+ lines.push(`If >50 bytes: installed. If empty: delete and try README fallback below.`);
513
+ lines.push(``);
514
+ lines.push(`**README-driven fallback:** If the API listing above returned SKIP or no match:`);
515
+ lines.push(``);
516
+ lines.push(`\`\`\`bash`);
517
+ lines.push(`# Fetch README — parser finds sections matching your query, extracts all links`);
518
+ lines.push(`curl -sf "https://raw.githubusercontent.com/${VOLTAGENT_SKILLS_REPO}/main/README.md" \\`);
519
+ lines.push(` | node -e "${buildSectionAwareParser(queryRegex)}"`);
410
520
  lines.push(`\`\`\``);
411
- lines.push(`Verify content is real (not empty). If empty, delete and continue.`);
412
521
  lines.push(``);
413
- lines.push(`**README-driven fallback:** If the API returns a single file (README.md) instead of a directory listing,`);
414
- lines.push(`parse its markdown sections for entries matching "${input}". Entries may link to external repos owned by`);
415
- lines.push(`different authors — follow those links using the universal URL resolution rules above (extract {owner}/{repo}`);
416
- lines.push(`dynamically, never hardcode). Navigate the foreign repo to find the installable .md file and download it.`);
522
+ lines.push(`From the output, pick the FIRST entry. Extract the URL (after " | "). Resolve it:`);
523
+ lines.push(`- If URL starts with \`./\`: prepend \`https://raw.githubusercontent.com/${VOLTAGENT_SKILLS_REPO}/main/\``);
524
+ lines.push(`- If URL contains \`github.com/OWNER/REPO/blob/BRANCH/\`: replace with \`raw.githubusercontent.com/OWNER/REPO/BRANCH/\``);
525
+ lines.push(`- If URL contains \`github.com/OWNER/REPO/tree/BRANCH/\`: use API to list files, then download the SKILL.md`);
526
+ lines.push(`\`\`\`bash`);
527
+ lines.push(`SKILL_DIR="matched-skill"`);
528
+ lines.push(`mkdir -p ".claude/skills/\${SKILL_DIR}"`);
529
+ lines.push(`curl -sf "RESOLVED_URL" -o ".claude/skills/\${SKILL_DIR}/SKILL.md" && wc -c ".claude/skills/\${SKILL_DIR}/SKILL.md"`);
530
+ lines.push(`\`\`\``);
531
+ lines.push(`If >50 bytes: installed. If empty or NO_README_MATCHES: continue to next STEP.`);
417
532
  lines.push(``);
418
533
  // ── STEP 4: ComposioHQ service integrations ───────────────────────
419
534
  lines.push(`---`);
420
535
  lines.push(``);
421
536
  lines.push(`### STEP 4 — ComposioHQ service integrations (1000+ skills)`);
422
537
  lines.push(``);
423
- lines.push(`Only reach here if STEPs 2+3 found no match.`);
538
+ lines.push(`**Before continuing:** Document in one line why STEPs 2 and 3 produced no result.`);
424
539
  lines.push(`Strong for API/SaaS automation: Gmail, Slack, GitHub, Notion, Stripe, Shopify, etc.`);
425
540
  lines.push(`If curl fails, skip to STEP 5.`);
426
541
  lines.push(``);
@@ -482,7 +597,16 @@ function buildUniversalRulesBlock(lines) {
482
597
  lines.push(`- **Uniqueness**: does it duplicate something already installed in .claude/skills/ or .claude/agents/? Deprioritize duplicates.`);
483
598
  lines.push(`Install only the highest-scoring candidate. One high-quality tool per task.`);
484
599
  lines.push(``);
485
- lines.push(`**5. Content verification — never keep stubs:**`);
600
+ lines.push(`**5. No absolute paths — never \`cd\` before commands:**`);
601
+ lines.push(`Claude Code already runs in the project working directory. Do NOT prepend \`cd /absolute/path &&\` to any command.`);
602
+ lines.push(`Use relative paths only (e.g., \`.claude/agents/\`, \`.claude/skills/\`). Absolute paths with backslashes break bash on Windows.`);
603
+ lines.push(``);
604
+ lines.push(`**6. Output filtering — cap context cost:**`);
605
+ lines.push(`Every curl result MUST be piped through a node parser that returns at most 5–15 lines of structured data.`);
606
+ lines.push(`The raw JSON/README response must NEVER enter the context unfiltered.`);
607
+ lines.push(`Chain multiple sequential fetch decisions inside a single bash script so they execute as one tool call.`);
608
+ lines.push(``);
609
+ lines.push(`**7. Content verification — never keep stubs:**`);
486
610
  lines.push(`After downloading any file, verify ALL of these before accepting:`);
487
611
  lines.push(`- Not empty (0 bytes)`);
488
612
  lines.push(`- Not only YAML frontmatter with no body (just \`---\\nname: x\\n---\` and nothing after)`);
@@ -492,68 +616,83 @@ function buildUniversalRulesBlock(lines) {
492
616
  }
493
617
  // ── Shared fetch blocks ─────────────────────────────────────────────────
494
618
  function buildCommunitySkillsFetchBlock(lines, categoryFilter) {
495
- lines.push(`**Stage 1 — Fetch catalog and find matching plugin:**`);
619
+ lines.push(`**Stage 1 — Fetch catalog and find matching plugins (compact output — one line per match):**`);
496
620
  lines.push(`\`\`\`bash`);
497
621
  lines.push(`curl -sf "${MARKETPLACE_CATALOG_URL}" \\`);
498
622
  lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));` +
499
- `const q='${categoryFilter}';` +
500
- `const r=d.plugins.filter(p=>(q===''||p.category.includes(q))&&p.name&&p.source).slice(0,10)` +
501
- `.map(p=>({name:p.name,source:p.source,desc:p.description}));` +
502
- `console.log(JSON.stringify(r,null,2));"`);
623
+ `const q='${categoryFilter}'.replace(/^\\\\d+-/,'');` +
624
+ `const r=d.plugins.filter(p=>(q===''||p.category.includes(q)||q.includes(p.category))&&p.name&&p.source).slice(0,10);` +
625
+ `r.forEach(p=>console.log(p.name+' | '+p.source));` +
626
+ `if(r.length===0)console.log('NO_PLUGINS')"`);
503
627
  lines.push(`\`\`\``);
504
- lines.push(`If this returns an error or empty array, skip to the next STEP.`);
628
+ lines.push(`If NO_PLUGINS or error: skip to next STEP.`);
505
629
  lines.push(``);
506
- lines.push(`**Stage 2 — Score candidates and pick the best match (see rule 4: Relevance > Scope > Uniqueness):**`);
630
+ lines.push(`**Stage 2 — Pick the FIRST match from output. Extract the source path (after " | "). List its skills:**`);
507
631
  lines.push(`\`\`\`bash`);
508
632
  lines.push(`# Replace PLUGIN_SOURCE_PATH with the "source" value from Stage 1`);
509
633
  lines.push(`PLUGIN_SOURCE_PATH="plugins/category/plugin-name"`);
510
634
  lines.push(`curl -sf \${GITHUB_TOKEN:+-H "Authorization: token \$GITHUB_TOKEN"} "https://api.github.com/repos/${MARKETPLACE_REPO}/contents/\${PLUGIN_SOURCE_PATH}/skills" \\`);
511
635
  lines.push(` | node -e "const a=JSON.parse(require('fs').readFileSync(0,'utf8'));` +
512
- `if(!Array.isArray(a)){console.log('NO_SKILLS_DIR');process.exit(0)}` +
636
+ `if(Array.isArray(a)===false){console.log('NO_SKILLS_DIR');process.exit(0)}` +
513
637
  `a.forEach(x=>console.log(x.name))"`);
514
638
  lines.push(`\`\`\``);
515
- lines.push(`If this fails or shows NO_SKILLS_DIR, the plugin has no installable skills skip.`);
639
+ lines.push(`If NO_SKILLS_DIR: try the next plugin from Stage 1, or skip to next STEP.`);
516
640
  lines.push(``);
517
- lines.push(`**Stage 3 — Download each skill file and install:**`);
641
+ lines.push(`**Stage 3 — Download the skill file:**`);
518
642
  lines.push(`\`\`\`bash`);
519
- lines.push(`# Replace PLUGIN_SOURCE_PATH and SKILL_NAME with actual values`);
643
+ lines.push(`# Replace PLUGIN_SOURCE_PATH and SKILL_NAME with actual values from above`);
520
644
  lines.push(`PLUGIN_SOURCE_PATH="plugins/category/plugin-name"`);
521
645
  lines.push(`SKILL_NAME="skill-directory-name"`);
522
646
  lines.push(`mkdir -p ".claude/skills/\${SKILL_NAME}"`);
523
647
  lines.push(`curl -sf "https://raw.githubusercontent.com/${MARKETPLACE_REPO}/main/\${PLUGIN_SOURCE_PATH}/skills/\${SKILL_NAME}/SKILL.md" \\`);
524
648
  lines.push(` -o ".claude/skills/\${SKILL_NAME}/SKILL.md"`);
649
+ lines.push(`wc -c ".claude/skills/\${SKILL_NAME}/SKILL.md"`);
525
650
  lines.push(`\`\`\``);
526
- lines.push(`Verify the file has real content (not empty, not just frontmatter). If empty: delete and move on.`);
651
+ lines.push(`If >50 bytes: installed. If empty: delete and move on to next plugin or next STEP.`);
527
652
  lines.push(``);
528
653
  }
529
654
  function buildComposioFetchBlock(lines, input) {
530
- lines.push(`**Stage 1 List available skill directories:**`);
655
+ const queryRegex = buildQueryRegex(input);
656
+ lines.push(`**Stage 1 — List available skill directories and filter by query:**`);
531
657
  lines.push(`\`\`\`bash`);
532
658
  lines.push(`curl -sf \${GITHUB_TOKEN:+-H "Authorization: token \$GITHUB_TOKEN"} "${COMPOSIO_API}" \\`);
533
659
  lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));` +
534
- `if(!Array.isArray(d)){console.log('SKIP');process.exit(0)}` +
535
- `d.filter(x=>x.type==='dir'&&!x.name.startsWith('.')).forEach(x=>console.log(x.name))"`);
660
+ `if(Array.isArray(d)===false){console.log('SKIP');process.exit(0)}` +
661
+ `const q=/${queryRegex}/i;` +
662
+ `const all=d.filter(x=>x.type==='dir'&&(x.name[0]==='.')===false).map(x=>x.name);` +
663
+ `const matched=all.filter(n=>q.test(n));` +
664
+ `const out=matched.length>0?matched:all;` +
665
+ `out.slice(0,10).forEach(x=>console.log(x))"`);
536
666
  lines.push(`\`\`\``);
537
667
  lines.push(``);
538
- lines.push(`**Stage 2 — Pick the directory that best matches "${input}":**`);
539
- lines.push(`Match by name similarity. If no directory name is relevant, skip.`);
540
- lines.push(``);
541
- lines.push(`**Stage 3 — Download the SKILL.md from the matched directory:**`);
668
+ lines.push(`**Stage 2 — Pick the FIRST directory from the output that matches "${input}" and download:**`);
542
669
  lines.push(`\`\`\`bash`);
670
+ lines.push(`# Replace SKILL_DIR with the actual directory name from Stage 1 output`);
543
671
  lines.push(`SKILL_DIR="matched-directory"`);
544
672
  lines.push(`mkdir -p ".claude/skills/\${SKILL_DIR}"`);
545
673
  lines.push(`curl -sf "${COMPOSIO_RAW}/\${SKILL_DIR}/SKILL.md" \\`);
546
674
  lines.push(` -o ".claude/skills/\${SKILL_DIR}/SKILL.md"`);
675
+ lines.push(`wc -c ".claude/skills/\${SKILL_DIR}/SKILL.md"`);
676
+ lines.push(`\`\`\``);
677
+ lines.push(`If >50 bytes: installed. If empty: delete and try README fallback below.`);
678
+ lines.push(``);
679
+ lines.push(`**README-driven fallback:** If the directory listing returned SKIP or no match:`);
680
+ lines.push(``);
681
+ lines.push(`\`\`\`bash`);
682
+ lines.push(`# Fetch README — parser finds sections matching your query, extracts all links`);
683
+ lines.push(`curl -sf "https://raw.githubusercontent.com/${COMPOSIO_REPO}/master/README.md" \\`);
684
+ lines.push(` | node -e "${buildSectionAwareParser(queryRegex)}"`);
547
685
  lines.push(`\`\`\``);
548
- lines.push(`Verify the file has real content (rule 5 above). If empty: delete and move on.`);
549
686
  lines.push(``);
550
- lines.push(`**README-driven fallback:** If the directory listing above fails or returns unexpected content,`);
551
- lines.push(`fetch the repo's README.md and parse it for sections matching "${input}":`);
687
+ lines.push(`From the output, pick the FIRST entry. Extract the URL (after " | "). Resolve it:`);
688
+ lines.push(`- If URL starts with \`./\`: prepend \`https://raw.githubusercontent.com/${COMPOSIO_REPO}/master/\` and append \`SKILL.md\` if the URL is a directory`);
689
+ lines.push(`- If URL contains \`github.com/OWNER/REPO/blob/BRANCH/\`: replace with \`raw.githubusercontent.com/OWNER/REPO/BRANCH/\``);
690
+ lines.push(`- If URL contains \`github.com/OWNER/REPO/tree/BRANCH/\`: use API to list files, then download the SKILL.md`);
552
691
  lines.push(`\`\`\`bash`);
553
- lines.push(`# Derive owner/repo dynamically from the catalog source — never hardcode`);
554
- lines.push(`curl -sf "https://raw.githubusercontent.com/${COMPOSIO_REPO}/master/README.md" | head -300`);
692
+ lines.push(`SKILL_DIR="matched-skill"`);
693
+ lines.push(`mkdir -p ".claude/skills/\${SKILL_DIR}"`);
694
+ lines.push(`curl -sf "RESOLVED_URL" -o ".claude/skills/\${SKILL_DIR}/SKILL.md" && wc -c ".claude/skills/\${SKILL_DIR}/SKILL.md"`);
555
695
  lines.push(`\`\`\``);
556
- lines.push(`Scan sections for entries with external links. Follow those links using universal URL resolution`);
557
- lines.push(`(rule 2+3 above) to download from the linked repo, regardless of its owner.`);
696
+ lines.push(`If >50 bytes: installed. If empty or NO_README_MATCHES: continue to next STEP.`);
558
697
  lines.push(``);
559
698
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-setup",
3
- "version": "1.1.9",
3
+ "version": "2.0.3",
4
4
  "description": "Setup layer for Claude Code — reads your project, writes command files, Claude Code does the rest",
5
5
  "type": "module",
6
6
  "bin": {
package/templates/add.md CHANGED
@@ -30,8 +30,32 @@ Skills: {{SKILLS_LIST}} | Commands: {{COMMANDS_LIST}}
30
30
 
31
31
  ---
32
32
 
33
+ ## Marketplace search — AUTOMATICALLY spawn the marketplace-fetcher subagent
34
+
35
+ **MANDATORY:** For any request that involves installing a skill, agent, plugin, or integration,
36
+ you MUST use the Agent tool to spawn the `marketplace-fetcher` subagent. Do NOT run catalog
37
+ fetches in your own context. Do NOT ask the user whether to spawn — just do it.
38
+
39
+ **How to spawn:** Use the Agent tool with these parameters:
40
+ - `description`: "Fetch and install: {{USER_INPUT}}"
41
+ - `prompt`: Copy the ENTIRE marketplace instructions block below (between the `BEGIN` and `END` markers) as the subagent's task prompt.
42
+
43
+ The subagent runs in isolation on Haiku: it fetches catalogs, parses READMEs, downloads files to disk,
44
+ and returns exactly one confirmation line. Its context dies when it finishes — zero catalog bloat
45
+ enters your session. This cuts API calls from ~113 to ~6 and cache-read tokens from ~4.8M to ~180K.
46
+
47
+ **Only skip the subagent** if the request is purely a single-file edit (add one MCP server to .mcp.json,
48
+ add a hook to settings.json, or append to CLAUDE.md) with no catalog search needed.
49
+
50
+ --- BEGIN MARKETPLACE INSTRUCTIONS (pass this entire block to the subagent) ---
51
+
33
52
  {{MARKETPLACE_INSTRUCTIONS}}
34
53
 
54
+ --- END MARKETPLACE INSTRUCTIONS ---
55
+
56
+ After the subagent returns its one-line result, continue to the post-install steps below.
57
+ If the subagent returned `FAILED`, create a production-quality custom skill/agent yourself.
58
+
35
59
  ---
36
60
 
37
61
  ## What you must actually do