agent-toolbox 0.2.0

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.
@@ -0,0 +1,2700 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/main.ts
4
+ import { readFile as readFile9 } from "node:fs/promises";
5
+ import { dirname as dirname11, resolve as resolve3 } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ // src/cli/build-target.ts
9
+ import { readFile as readFile3 } from "node:fs/promises";
10
+ import { join as join8 } from "node:path";
11
+
12
+ // src/catalog/scanner.ts
13
+ import { readFile, readdir } from "node:fs/promises";
14
+ import { join } from "node:path";
15
+
16
+ // src/catalog/frontmatter.ts
17
+ import { parse as parseYaml } from "yaml";
18
+ var FRONTMATTER_REGEX = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
19
+ function parseFrontmatter(content) {
20
+ const normalized = content.startsWith("\uFEFF") ? content.slice(1) : content;
21
+ const match = FRONTMATTER_REGEX.exec(normalized);
22
+ if (!match) {
23
+ throw new Error("Missing YAML frontmatter. Expected content to start with '---'.");
24
+ }
25
+ const [, yamlBlock] = match;
26
+ const body = normalized.slice(match[0].length);
27
+ let parsed;
28
+ try {
29
+ parsed = parseYaml(yamlBlock);
30
+ } catch (error) {
31
+ const reason = error instanceof Error ? error.message : String(error);
32
+ throw new Error(`Malformed YAML frontmatter: ${reason}`, { cause: error });
33
+ }
34
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
35
+ throw new Error("Malformed YAML frontmatter: expected a key-value object.");
36
+ }
37
+ return {
38
+ frontmatter: parsed,
39
+ body
40
+ };
41
+ }
42
+
43
+ // src/schemas/catalog.ts
44
+ import { z as z2 } from "zod";
45
+
46
+ // src/schemas/common.ts
47
+ import { z } from "zod";
48
+ var SkillName = z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).max(64);
49
+ var HoloceneDate = z.string().regex(/^1\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/);
50
+ var Provenance = z.enum([
51
+ "ported",
52
+ "adapted",
53
+ "synthesized",
54
+ "original"
55
+ ]);
56
+ var CommaSeparatedList = z.string().transform((s) => s.split(",").map((t) => t.trim()).filter(Boolean));
57
+ var AuthorField = z.string().min(1);
58
+ var LicenseField = z.string().default("Sustainable Use License 1.0");
59
+ var TargetTool = z.enum([
60
+ "claude-code",
61
+ "opencode",
62
+ "cursor",
63
+ "codex",
64
+ "gemini"
65
+ ]);
66
+
67
+ // src/schemas/catalog.ts
68
+ var SkillMetadata = z2.object({
69
+ domain: z2.string(),
70
+ subdomain: z2.string().optional(),
71
+ tags: CommaSeparatedList.optional(),
72
+ frameworks: CommaSeparatedList.optional(),
73
+ author: AuthorField,
74
+ lastUpdated: HoloceneDate,
75
+ provenance: Provenance
76
+ });
77
+ var SkillFrontmatter = z2.object({
78
+ name: SkillName,
79
+ description: z2.string().max(1024),
80
+ license: LicenseField,
81
+ metadata: SkillMetadata
82
+ });
83
+ var ParsedSkill = z2.object({
84
+ frontmatter: SkillFrontmatter,
85
+ body: z2.string(),
86
+ dirName: z2.string(),
87
+ filePath: z2.string(),
88
+ hasNotice: z2.boolean(),
89
+ hasReferences: z2.boolean(),
90
+ hasScripts: z2.boolean(),
91
+ hasAssets: z2.boolean(),
92
+ additionalEntries: z2.array(z2.string()).default([])
93
+ });
94
+ var AgentMetadata = z2.object({
95
+ domain: z2.string(),
96
+ subdomain: z2.string().optional(),
97
+ tags: CommaSeparatedList.optional(),
98
+ author: AuthorField,
99
+ lastUpdated: HoloceneDate,
100
+ provenance: Provenance
101
+ });
102
+ var AgentFrontmatter = z2.object({
103
+ name: SkillName,
104
+ description: z2.string().max(1024),
105
+ license: LicenseField,
106
+ metadata: AgentMetadata
107
+ });
108
+ var CommandMetadata = z2.object({
109
+ domain: z2.string(),
110
+ subdomain: z2.string().optional(),
111
+ tags: CommaSeparatedList.optional(),
112
+ author: AuthorField,
113
+ lastUpdated: HoloceneDate,
114
+ provenance: Provenance
115
+ });
116
+ var CommandFrontmatter = z2.object({
117
+ name: SkillName,
118
+ description: z2.string().max(1024),
119
+ license: LicenseField,
120
+ trigger: z2.string().regex(/^\/[a-z0-9-]+$/),
121
+ metadata: CommandMetadata
122
+ });
123
+ var HookEvent = z2.enum([
124
+ "SessionStart",
125
+ "SessionEnd",
126
+ "BeforeTool",
127
+ "AfterTool",
128
+ "BeforeModel",
129
+ "AfterModel",
130
+ "Notification",
131
+ "Stop",
132
+ "SubagentStart",
133
+ "SubagentEnd",
134
+ "PreToolExecution"
135
+ ]);
136
+ var HookMetadata = z2.object({
137
+ domain: z2.string(),
138
+ subdomain: z2.string().optional(),
139
+ tags: CommaSeparatedList.optional(),
140
+ author: AuthorField,
141
+ lastUpdated: HoloceneDate,
142
+ provenance: Provenance
143
+ });
144
+ var HookFrontmatter = z2.object({
145
+ name: SkillName,
146
+ description: z2.string().max(1024),
147
+ license: LicenseField,
148
+ events: z2.array(HookEvent),
149
+ type: z2.enum(["command", "script", "module"]),
150
+ metadata: HookMetadata
151
+ });
152
+ var McpTransport = z2.enum(["stdio", "sse", "streamable-http"]);
153
+ var McpMetadata = z2.object({
154
+ domain: z2.string(),
155
+ subdomain: z2.string().optional(),
156
+ tags: CommaSeparatedList.optional(),
157
+ author: AuthorField,
158
+ lastUpdated: HoloceneDate,
159
+ provenance: Provenance
160
+ });
161
+ var McpServerFrontmatter = z2.object({
162
+ name: SkillName,
163
+ description: z2.string().max(1024),
164
+ license: LicenseField,
165
+ transport: McpTransport,
166
+ command: z2.string(),
167
+ args: z2.array(z2.string()).default([]),
168
+ env: z2.record(z2.string(), z2.string()).optional(),
169
+ metadata: McpMetadata
170
+ });
171
+ var CatalogItemType = z2.enum([
172
+ "skill",
173
+ "agent",
174
+ "command",
175
+ "hook",
176
+ "mcp"
177
+ ]);
178
+ var SkillIndexEntry = z2.object({
179
+ name: z2.string(),
180
+ description: z2.string(),
181
+ domain: z2.string(),
182
+ subdomain: z2.string().optional(),
183
+ provenance: Provenance,
184
+ author: z2.string(),
185
+ lastUpdated: z2.string(),
186
+ license: z2.string()
187
+ });
188
+ var SkillIndex = z2.object({
189
+ version: z2.literal(2),
190
+ generatedAt: z2.string(),
191
+ skills: z2.array(SkillIndexEntry),
192
+ tags: z2.record(z2.string(), z2.array(z2.string())).default({}),
193
+ frameworks: z2.record(z2.string(), z2.array(z2.string())).default({})
194
+ });
195
+
196
+ // src/catalog/scanner.ts
197
+ async function scanSkills(catalogDir) {
198
+ const skills = [];
199
+ const errors = [];
200
+ const skillsRoot = join(catalogDir, "skills");
201
+ let skillDirs;
202
+ try {
203
+ const entries = await readdir(skillsRoot, { withFileTypes: true });
204
+ skillDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
205
+ } catch (error) {
206
+ const reason = error instanceof Error ? error.message : String(error);
207
+ return {
208
+ skills,
209
+ errors: [
210
+ {
211
+ path: skillsRoot,
212
+ error: `Failed to scan skills directory: ${reason}`
213
+ }
214
+ ]
215
+ };
216
+ }
217
+ for (const dirName of skillDirs) {
218
+ const skillDir = join(skillsRoot, dirName);
219
+ const skillPath = join(skillDir, "SKILL.md");
220
+ try {
221
+ const skillContent = await readFile(skillPath, "utf8");
222
+ const { frontmatter, body } = parseFrontmatter(skillContent);
223
+ const parsedFrontmatter = SkillFrontmatter.safeParse(frontmatter);
224
+ if (!parsedFrontmatter.success) {
225
+ for (const issue of parsedFrontmatter.error.issues) {
226
+ const field = issue.path.length > 0 ? issue.path.join(".") : "frontmatter";
227
+ errors.push({
228
+ path: skillPath,
229
+ error: `Frontmatter validation failed at '${field}': ${issue.message}`
230
+ });
231
+ }
232
+ continue;
233
+ }
234
+ const entryNames = await readdir(skillDir);
235
+ const entrySet = new Set(entryNames);
236
+ const hasNotice = entrySet.has("NOTICE.md");
237
+ const hasReferences = entrySet.has("references");
238
+ const hasScripts = entrySet.has("scripts");
239
+ const hasAssets = entrySet.has("assets");
240
+ const additionalEntries = entryNames.filter((name) => ![
241
+ "SKILL.md",
242
+ "NOTICE.md",
243
+ "references",
244
+ "scripts",
245
+ "assets"
246
+ ].includes(name)).sort((a, b) => a.localeCompare(b));
247
+ const parsedSkill = {
248
+ frontmatter: parsedFrontmatter.data,
249
+ body,
250
+ dirName,
251
+ filePath: skillPath,
252
+ hasNotice,
253
+ hasReferences,
254
+ hasScripts,
255
+ hasAssets,
256
+ additionalEntries
257
+ };
258
+ skills.push(parsedSkill);
259
+ } catch (error) {
260
+ const reason = error instanceof Error ? error.message : String(error);
261
+ errors.push({
262
+ path: skillPath,
263
+ error: reason
264
+ });
265
+ }
266
+ }
267
+ return { skills, errors };
268
+ }
269
+
270
+ // src/generators/claude-code/generator.ts
271
+ import { mkdir as mkdir2, rm, writeFile as writeFile2 } from "node:fs/promises";
272
+ import { join as join3 } from "node:path";
273
+
274
+ // src/generators/copy-utils.ts
275
+ import { mkdir, readFile as readFile2, readdir as readdir2, writeFile } from "node:fs/promises";
276
+ import { join as join2 } from "node:path";
277
+ async function copyDirectoryRecursive(src, dest) {
278
+ await mkdir(dest, { recursive: true });
279
+ const entries = await readdir2(src, { withFileTypes: true });
280
+ for (const entry of entries) {
281
+ const srcPath = join2(src, entry.name);
282
+ const destPath = join2(dest, entry.name);
283
+ if (entry.isDirectory()) {
284
+ await copyDirectoryRecursive(srcPath, destPath);
285
+ } else {
286
+ const content = await readFile2(srcPath);
287
+ await writeFile(destPath, content);
288
+ }
289
+ }
290
+ }
291
+
292
+ // src/generators/claude-code/generator.ts
293
+ class ClaudeCodeGenerator {
294
+ target = "claude-code";
295
+ async generate(options) {
296
+ const { skills, outputDir, catalogDir, version } = options;
297
+ const artifacts = [];
298
+ const warnings = [];
299
+ await rm(outputDir, { recursive: true, force: true });
300
+ await mkdir2(outputDir, { recursive: true });
301
+ const pluginDir = join3(outputDir, ".claude-plugin");
302
+ await mkdir2(pluginDir, { recursive: true });
303
+ const pluginManifest = {
304
+ name: "agent-toolbox",
305
+ version,
306
+ description: "Cross-tool distribution system for agent skills, plugins, and MCP servers"
307
+ };
308
+ await writeFile2(join3(pluginDir, "plugin.json"), JSON.stringify(pluginManifest, null, 2) + `
309
+ `, "utf8");
310
+ artifacts.push(".claude-plugin/plugin.json");
311
+ const skillsDir = join3(outputDir, "skills");
312
+ await mkdir2(skillsDir, { recursive: true });
313
+ for (const skill of skills) {
314
+ const srcDir = join3(catalogDir, "skills", skill.dirName);
315
+ const destDir = join3(skillsDir, skill.dirName);
316
+ await copyDirectoryRecursive(srcDir, destDir);
317
+ artifacts.push(`skills/${skill.dirName}/`);
318
+ }
319
+ await mkdir2(join3(outputDir, "agents"), { recursive: true });
320
+ await writeFile2(join3(outputDir, "agents", ".gitkeep"), "", "utf8");
321
+ artifacts.push("agents/");
322
+ await mkdir2(join3(outputDir, "commands"), { recursive: true });
323
+ await writeFile2(join3(outputDir, "commands", ".gitkeep"), "", "utf8");
324
+ artifacts.push("commands/");
325
+ const hooksDir = join3(outputDir, "hooks");
326
+ await mkdir2(hooksDir, { recursive: true });
327
+ await writeFile2(join3(hooksDir, "hooks.json"), JSON.stringify({ hooks: {} }, null, 2) + `
328
+ `, "utf8");
329
+ artifacts.push("hooks/hooks.json");
330
+ return {
331
+ target: "claude-code",
332
+ skillCount: skills.length,
333
+ outputDir,
334
+ artifacts,
335
+ warnings
336
+ };
337
+ }
338
+ }
339
+
340
+ // src/generators/codex/generator.ts
341
+ import { mkdir as mkdir3, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
342
+ import { join as join4 } from "node:path";
343
+ import { stringify as stringifyYaml } from "yaml";
344
+ function generateInstallMd() {
345
+ return [
346
+ "# Codex Installation",
347
+ "",
348
+ "## Prerequisites",
349
+ "- Codex CLI installed (OpenAI)",
350
+ "",
351
+ "## Installation",
352
+ "",
353
+ "### macOS/Linux",
354
+ "```bash",
355
+ "# Symlink skills directory",
356
+ "ln -sf /path/to/dist/targets/codex/skills ~/.agents/skills",
357
+ "```",
358
+ "",
359
+ "### Windows",
360
+ "```cmd",
361
+ ":: Use junctions (no Developer Mode required)",
362
+ 'mklink /J "%USERPROFILE%\\.agents\\skills" "\\path\\to\\dist\\targets\\codex\\skills"',
363
+ "```",
364
+ ""
365
+ ].join(`
366
+ `);
367
+ }
368
+
369
+ class CodexGenerator {
370
+ target = "codex";
371
+ async generate(options) {
372
+ const { skills, outputDir, catalogDir } = options;
373
+ const artifacts = [];
374
+ const warnings = [];
375
+ await rm2(outputDir, { recursive: true, force: true });
376
+ await mkdir3(outputDir, { recursive: true });
377
+ const skillsDir = join4(outputDir, "skills");
378
+ await mkdir3(skillsDir, { recursive: true });
379
+ for (const skill of skills) {
380
+ const srcDir = join4(catalogDir, "skills", skill.dirName);
381
+ const destDir = join4(skillsDir, skill.dirName);
382
+ await copyDirectoryRecursive(srcDir, destDir);
383
+ artifacts.push(`skills/${skill.dirName}/`);
384
+ }
385
+ const agentsDir = join4(outputDir, "agents");
386
+ await mkdir3(agentsDir, { recursive: true });
387
+ const openAiMetadata = {
388
+ display_name: "Awesome Agent Toolbox",
389
+ icon: "toolbox",
390
+ brand_color: "#2563eb",
391
+ policy: "You have access to skills from the agent-toolbox catalog. Read the relevant SKILL.md when a user's request matches a skill's description."
392
+ };
393
+ await writeFile3(join4(agentsDir, "openai.yaml"), stringifyYaml(openAiMetadata), "utf8");
394
+ artifacts.push("agents/openai.yaml");
395
+ await writeFile3(join4(outputDir, "INSTALL.md"), generateInstallMd(), "utf8");
396
+ artifacts.push("INSTALL.md");
397
+ return {
398
+ target: "codex",
399
+ skillCount: skills.length,
400
+ outputDir,
401
+ artifacts,
402
+ warnings
403
+ };
404
+ }
405
+ }
406
+
407
+ // src/generators/cursor/generator.ts
408
+ import { mkdir as mkdir4, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
409
+ import { join as join5 } from "node:path";
410
+ class CursorGenerator {
411
+ target = "cursor";
412
+ async generate(options) {
413
+ const { skills, outputDir, catalogDir, version } = options;
414
+ const artifacts = [];
415
+ const warnings = [];
416
+ await rm3(outputDir, { recursive: true, force: true });
417
+ await mkdir4(outputDir, { recursive: true });
418
+ const pluginDir = join5(outputDir, ".cursor-plugin");
419
+ await mkdir4(pluginDir, { recursive: true });
420
+ const pluginManifest = {
421
+ name: "agent-toolbox",
422
+ version,
423
+ description: "Cross-tool distribution system for agent skills, plugins, and MCP servers",
424
+ skills: "./skills/",
425
+ agents: "./agents/",
426
+ commands: "./commands/",
427
+ hooks: "./hooks/hooks.json"
428
+ };
429
+ await writeFile4(join5(pluginDir, "plugin.json"), `${JSON.stringify(pluginManifest, null, 2)}
430
+ `, "utf8");
431
+ artifacts.push(".cursor-plugin/plugin.json");
432
+ const skillsDir = join5(outputDir, "skills");
433
+ await mkdir4(skillsDir, { recursive: true });
434
+ for (const skill of skills) {
435
+ const srcDir = join5(catalogDir, "skills", skill.dirName);
436
+ const destDir = join5(skillsDir, skill.dirName);
437
+ await copyDirectoryRecursive(srcDir, destDir);
438
+ artifacts.push(`skills/${skill.dirName}/`);
439
+ }
440
+ const agentsDir = join5(outputDir, "agents");
441
+ await mkdir4(agentsDir, { recursive: true });
442
+ await writeFile4(join5(agentsDir, ".gitkeep"), "", "utf8");
443
+ artifacts.push("agents/");
444
+ const commandsDir = join5(outputDir, "commands");
445
+ await mkdir4(commandsDir, { recursive: true });
446
+ await writeFile4(join5(commandsDir, ".gitkeep"), "", "utf8");
447
+ artifacts.push("commands/");
448
+ const hooksDir = join5(outputDir, "hooks");
449
+ await mkdir4(hooksDir, { recursive: true });
450
+ await writeFile4(join5(hooksDir, "hooks.json"), `${JSON.stringify({ hooks: {} }, null, 2)}
451
+ `, "utf8");
452
+ artifacts.push("hooks/hooks.json");
453
+ return {
454
+ target: "cursor",
455
+ skillCount: skills.length,
456
+ outputDir,
457
+ artifacts,
458
+ warnings
459
+ };
460
+ }
461
+ }
462
+
463
+ // src/generators/gemini/generator.ts
464
+ import { mkdir as mkdir5, rm as rm4, writeFile as writeFile5 } from "node:fs/promises";
465
+ import { join as join6 } from "node:path";
466
+ var GEMINI_HOOK_EVENTS = [
467
+ "SessionStart",
468
+ "SessionEnd",
469
+ "BeforeTool",
470
+ "AfterTool",
471
+ "BeforeModel",
472
+ "AfterModel",
473
+ "Notification",
474
+ "Stop",
475
+ "SubagentStart",
476
+ "SubagentEnd",
477
+ "PreToolExecution"
478
+ ];
479
+ function groupSkillsByDomain(skills) {
480
+ const byDomain = new Map;
481
+ for (const skill of skills) {
482
+ const domain = skill.frontmatter.metadata.domain;
483
+ const existing = byDomain.get(domain);
484
+ if (existing) {
485
+ existing.push(skill);
486
+ } else {
487
+ byDomain.set(domain, [skill]);
488
+ }
489
+ }
490
+ return byDomain;
491
+ }
492
+ function generateGeminiContext(skills) {
493
+ const byDomain = groupSkillsByDomain(skills);
494
+ const sortedDomains = [...byDomain.keys()].sort((a, b) => a.localeCompare(b));
495
+ const lines = [
496
+ "# agent-toolbox Skills",
497
+ "",
498
+ "You have access to the following skills. Read the relevant SKILL.md when a user's request matches.",
499
+ "",
500
+ "## Skills by Domain",
501
+ ""
502
+ ];
503
+ for (const domain of sortedDomains) {
504
+ const domainSkills = byDomain.get(domain) ?? [];
505
+ const sortedSkills = [...domainSkills].sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name));
506
+ lines.push(`### ${domain} (${sortedSkills.length} skills)`);
507
+ for (const skill of sortedSkills) {
508
+ const description = skill.frontmatter.description.replace(/\s+/g, " ").trim();
509
+ lines.push(`- ${skill.frontmatter.name}: ${description}`);
510
+ }
511
+ lines.push("");
512
+ }
513
+ return lines.join(`
514
+ `);
515
+ }
516
+ function generateHooksConfig() {
517
+ const hooks = {};
518
+ for (const event of GEMINI_HOOK_EVENTS) {
519
+ hooks[event] = [];
520
+ }
521
+ return { hooks };
522
+ }
523
+
524
+ class GeminiGenerator {
525
+ target = "gemini";
526
+ async generate(options) {
527
+ const { skills, outputDir, catalogDir, version } = options;
528
+ const artifacts = [];
529
+ const warnings = [];
530
+ await rm4(outputDir, { recursive: true, force: true });
531
+ await mkdir5(outputDir, { recursive: true });
532
+ const extensionManifest = {
533
+ name: "agent-toolbox",
534
+ version,
535
+ description: "Cross-tool distribution system for agent skills, plugins, and MCP servers",
536
+ contextFileName: "GEMINI.md",
537
+ skills: "./skills/",
538
+ commands: "./commands/",
539
+ hooks: "./hooks/hooks.json"
540
+ };
541
+ await writeFile5(join6(outputDir, "gemini-extension.json"), `${JSON.stringify(extensionManifest, null, 2)}
542
+ `, "utf8");
543
+ artifacts.push("gemini-extension.json");
544
+ const skillsDir = join6(outputDir, "skills");
545
+ await mkdir5(skillsDir, { recursive: true });
546
+ for (const skill of skills) {
547
+ const srcDir = join6(catalogDir, "skills", skill.dirName);
548
+ const destDir = join6(skillsDir, skill.dirName);
549
+ await copyDirectoryRecursive(srcDir, destDir);
550
+ artifacts.push(`skills/${skill.dirName}/`);
551
+ }
552
+ const commandsDir = join6(outputDir, "commands");
553
+ await mkdir5(commandsDir, { recursive: true });
554
+ await writeFile5(join6(commandsDir, ".gitkeep"), "", "utf8");
555
+ artifacts.push("commands/");
556
+ const hooksDir = join6(outputDir, "hooks");
557
+ await mkdir5(hooksDir, { recursive: true });
558
+ await writeFile5(join6(hooksDir, "hooks.json"), `${JSON.stringify(generateHooksConfig(), null, 2)}
559
+ `, "utf8");
560
+ artifacts.push("hooks/hooks.json");
561
+ await writeFile5(join6(outputDir, "GEMINI.md"), `${generateGeminiContext(skills)}
562
+ `, "utf8");
563
+ artifacts.push("GEMINI.md");
564
+ return {
565
+ target: "gemini",
566
+ skillCount: skills.length,
567
+ outputDir,
568
+ artifacts,
569
+ warnings
570
+ };
571
+ }
572
+ }
573
+
574
+ // src/generators/opencode/generator.ts
575
+ import { mkdir as mkdir6, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
576
+ import { join as join7 } from "node:path";
577
+ function generateOpenCodePlugin(skillList) {
578
+ return [
579
+ `// agent-toolbox.js - OpenCode plugin bootstrap`,
580
+ `// Injects skill catalog awareness into the system prompt`,
581
+ ``,
582
+ `import path from "path";`,
583
+ `import { fileURLToPath } from "url";`,
584
+ ``,
585
+ `const __dirname = path.dirname(fileURLToPath(import.meta.url));`,
586
+ ``,
587
+ `const SKILL_CATALOG = ${JSON.stringify([
588
+ "## Available Skills (agent-toolbox)",
589
+ "",
590
+ "You have access to skills from agent-toolbox. Each skill has a SKILL.md",
591
+ "with instructions. Read the relevant SKILL.md when a user's request matches a skill's",
592
+ "description.",
593
+ "",
594
+ "### Installed Skills:",
595
+ skillList,
596
+ ""
597
+ ].join(`
598
+ `))};`,
599
+ ``,
600
+ `export const AwesomeAgentToolboxPlugin = async ({ directory }) => {`,
601
+ ` return {`,
602
+ ` "experimental.chat.system.transform": async (_input, output) => {`,
603
+ ` (output.system ||= []).push(SKILL_CATALOG);`,
604
+ ` },`,
605
+ ` };`,
606
+ `};`,
607
+ ``
608
+ ].join(`
609
+ `);
610
+ }
611
+ function generateInstallMd2() {
612
+ return [
613
+ "# OpenCode Installation",
614
+ "",
615
+ "## Prerequisites",
616
+ "- OpenCode installed (https://github.com/anomalyco/opencode)",
617
+ "",
618
+ "## Installation",
619
+ "",
620
+ "### 1. Link skills",
621
+ "```bash",
622
+ "# macOS/Linux",
623
+ "mkdir -p ~/.config/opencode/skills",
624
+ "ln -sf /path/to/dist/targets/opencode/skills ~/.config/opencode/skills/agent-toolbox",
625
+ "",
626
+ "# Windows (PowerShell as Admin)",
627
+ 'New-Item -ItemType Junction -Path "$env:APPDATA\\opencode\\skills\\agent-toolbox" -Target "\\path\\to\\dist\\targets\\opencode\\skills"',
628
+ "```",
629
+ "",
630
+ "### 2. Install plugin",
631
+ "```bash",
632
+ "# macOS/Linux",
633
+ "mkdir -p ~/.config/opencode/plugins",
634
+ "ln -sf /path/to/dist/targets/opencode/plugins/agent-toolbox.js ~/.config/opencode/plugins/agent-toolbox.js",
635
+ "",
636
+ "# Windows (copy)",
637
+ 'copy "\\path\\to\\dist\\targets\\opencode\\plugins\\agent-toolbox.js" "%APPDATA%\\opencode\\plugins\\"',
638
+ "```",
639
+ ""
640
+ ].join(`
641
+ `);
642
+ }
643
+
644
+ class OpenCodeGenerator {
645
+ target = "opencode";
646
+ async generate(options) {
647
+ const { skills, outputDir, catalogDir } = options;
648
+ const artifacts = [];
649
+ const warnings = [];
650
+ await rm5(outputDir, { recursive: true, force: true });
651
+ await mkdir6(outputDir, { recursive: true });
652
+ const skillsDir = join7(outputDir, "skills");
653
+ await mkdir6(skillsDir, { recursive: true });
654
+ for (const skill of skills) {
655
+ const srcDir = join7(catalogDir, "skills", skill.dirName);
656
+ const destDir = join7(skillsDir, skill.dirName);
657
+ await copyDirectoryRecursive(srcDir, destDir);
658
+ artifacts.push(`skills/${skill.dirName}/`);
659
+ }
660
+ const skillListLines = [...skills].sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name)).map((skill) => `- **${skill.frontmatter.name}** (${skill.frontmatter.metadata.domain}): ${skill.frontmatter.description}`).join(`
661
+ `);
662
+ const pluginsDir = join7(outputDir, "plugins");
663
+ await mkdir6(pluginsDir, { recursive: true });
664
+ const pluginJs = generateOpenCodePlugin(skillListLines);
665
+ await writeFile6(join7(pluginsDir, "agent-toolbox.js"), pluginJs, "utf8");
666
+ artifacts.push("plugins/agent-toolbox.js");
667
+ const installMd = generateInstallMd2();
668
+ await writeFile6(join7(outputDir, "INSTALL.md"), installMd, "utf8");
669
+ artifacts.push("INSTALL.md");
670
+ return {
671
+ target: "opencode",
672
+ skillCount: skills.length,
673
+ outputDir,
674
+ artifacts,
675
+ warnings
676
+ };
677
+ }
678
+ }
679
+
680
+ // src/cli/utils.ts
681
+ var RESET = "\x1B[0m";
682
+ var GREEN = "\x1B[32m";
683
+ var RED = "\x1B[31m";
684
+ var YELLOW = "\x1B[33m";
685
+ var DIM = "\x1B[2m";
686
+ var BOLD = "\x1B[1m";
687
+ var CYAN = "\x1B[36m";
688
+ function green(text) {
689
+ return `${GREEN}${text}${RESET}`;
690
+ }
691
+ function red(text) {
692
+ return `${RED}${text}${RESET}`;
693
+ }
694
+ function yellow(text) {
695
+ return `${YELLOW}${text}${RESET}`;
696
+ }
697
+ function dim(text) {
698
+ return `${DIM}${text}${RESET}`;
699
+ }
700
+ function bold(text) {
701
+ return `${BOLD}${text}${RESET}`;
702
+ }
703
+ function cyan(text) {
704
+ return `${CYAN}${text}${RESET}`;
705
+ }
706
+ function parseArgs(argv) {
707
+ const args = {};
708
+ for (let i = 0;i < argv.length; i += 1) {
709
+ const token = argv[i];
710
+ if (!token.startsWith("--")) {
711
+ continue;
712
+ }
713
+ const key = token.slice(2);
714
+ const nextToken = argv[i + 1];
715
+ const value = nextToken && !nextToken.startsWith("--") ? nextToken : "true";
716
+ if (key === "skill") {
717
+ const current = args[key];
718
+ if (!Array.isArray(current)) {
719
+ args[key] = current ? [String(current)] : [];
720
+ }
721
+ if (value !== "true") {
722
+ const arr = args[key];
723
+ arr.push(value);
724
+ }
725
+ } else {
726
+ args[key] = value;
727
+ }
728
+ if (value !== "true") {
729
+ i += 1;
730
+ }
731
+ }
732
+ return args;
733
+ }
734
+ function stripAnsi(text) {
735
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
736
+ }
737
+ function truncate(text, maxWidth) {
738
+ if (text.length <= maxWidth) {
739
+ return text;
740
+ }
741
+ return text.slice(0, maxWidth - 3) + "...";
742
+ }
743
+ function formatTable(headers, rows, options) {
744
+ const maxColWidth = options?.maxColWidth ?? 60;
745
+ const padding = options?.padding ?? 2;
746
+ const truncatedHeaders = headers.map((h) => truncate(h, maxColWidth));
747
+ const truncatedRows = rows.map((row) => row.map((cell) => truncate(cell, maxColWidth)));
748
+ const colWidths = truncatedHeaders.map((h) => stripAnsi(h).length);
749
+ for (const row of truncatedRows) {
750
+ for (let i = 0;i < row.length; i += 1) {
751
+ const cellWidth = stripAnsi(row[i]).length;
752
+ if (i < colWidths.length && cellWidth > colWidths[i]) {
753
+ colWidths[i] = cellWidth;
754
+ }
755
+ }
756
+ }
757
+ const pad = " ".repeat(padding);
758
+ const lines = [];
759
+ const headerLine = truncatedHeaders.map((h, i) => {
760
+ const visible = stripAnsi(h).length;
761
+ return h + " ".repeat(Math.max(0, colWidths[i] - visible));
762
+ }).join(pad);
763
+ lines.push(bold(headerLine));
764
+ const separator = colWidths.map((w) => "─".repeat(w)).join(pad);
765
+ lines.push(dim(separator));
766
+ for (const row of truncatedRows) {
767
+ const rowLine = row.map((cell, i) => {
768
+ const visible = stripAnsi(cell).length;
769
+ const width = i < colWidths.length ? colWidths[i] : visible;
770
+ return cell + " ".repeat(Math.max(0, width - visible));
771
+ }).join(pad);
772
+ lines.push(rowLine);
773
+ }
774
+ return lines.join(`
775
+ `);
776
+ }
777
+
778
+ // src/cli/build-target.ts
779
+ var ALL_TARGETS = [
780
+ "claude-code",
781
+ "opencode",
782
+ "cursor",
783
+ "codex",
784
+ "gemini"
785
+ ];
786
+ function getGenerator(target) {
787
+ const generators = {
788
+ "claude-code": new ClaudeCodeGenerator,
789
+ opencode: new OpenCodeGenerator,
790
+ cursor: new CursorGenerator,
791
+ codex: new CodexGenerator,
792
+ gemini: new GeminiGenerator
793
+ };
794
+ const generator = generators[target];
795
+ if (!generator) {
796
+ throw new Error(`Generator for '${target}' not yet implemented`);
797
+ }
798
+ return generator;
799
+ }
800
+ async function runBuildTarget(rootDir, target) {
801
+ const catalogDir = join8(rootDir, "catalog");
802
+ const targetParse = TargetTool.safeParse(target);
803
+ if (!targetParse.success) {
804
+ console.error(`Invalid target: ${target}. Valid targets: claude-code, opencode, cursor, codex, gemini`);
805
+ process.exit(1);
806
+ }
807
+ const generator = getGenerator(targetParse.data);
808
+ console.log("Scanning catalog...");
809
+ const { skills, errors } = await scanSkills(catalogDir);
810
+ if (errors.length > 0) {
811
+ console.error(`Found ${errors.length} scan errors:`);
812
+ for (const err of errors) {
813
+ console.error(` ${err.path}: ${err.error}`);
814
+ }
815
+ }
816
+ if (skills.length === 0) {
817
+ console.error("No skills found in catalog");
818
+ process.exit(1);
819
+ }
820
+ const pkg = JSON.parse(await readFile3(join8(rootDir, "package.json"), "utf8"));
821
+ const version = pkg.version;
822
+ const outputDir = join8(rootDir, "dist", "targets", targetParse.data);
823
+ console.log(`Generating ${targetParse.data} artifacts...`);
824
+ const result = await generator.generate({
825
+ skills,
826
+ outputDir,
827
+ catalogDir,
828
+ version
829
+ });
830
+ console.log(`${green("✓")} Generated ${result.skillCount} skills for ${result.target}`);
831
+ console.log(` Output: ${result.outputDir}`);
832
+ console.log(` Artifacts: ${result.artifacts.length} items`);
833
+ if (result.warnings.length > 0) {
834
+ console.log(`
835
+ Warnings:`);
836
+ for (const warning of result.warnings) {
837
+ console.log(` ${yellow("!")} ${warning}`);
838
+ }
839
+ }
840
+ }
841
+ async function runBuildAll(rootDir) {
842
+ for (const target of ALL_TARGETS) {
843
+ await runBuildTarget(rootDir, target);
844
+ console.log("");
845
+ }
846
+ }
847
+ if (false) {}
848
+
849
+ // src/cli/build-index.ts
850
+ import { resolve } from "node:path";
851
+
852
+ // src/catalog/index-builder.ts
853
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
854
+ import { dirname as dirname2 } from "node:path";
855
+ import { DELIMITERS, encode } from "@toon-format/toon";
856
+ function buildSkillIndex(skills) {
857
+ const skillEntries = skills.map((skill) => ({
858
+ name: skill.frontmatter.name,
859
+ description: skill.frontmatter.description,
860
+ domain: skill.frontmatter.metadata.domain,
861
+ subdomain: skill.frontmatter.metadata.subdomain,
862
+ provenance: skill.frontmatter.metadata.provenance,
863
+ author: skill.frontmatter.metadata.author,
864
+ lastUpdated: skill.frontmatter.metadata.lastUpdated,
865
+ license: skill.frontmatter.license
866
+ })).sort((a, b) => a.name.localeCompare(b.name));
867
+ const tags = {};
868
+ const frameworks = {};
869
+ for (const skill of skills) {
870
+ const skillTags = skill.frontmatter.metadata.tags;
871
+ if (skillTags && skillTags.length > 0) {
872
+ tags[skill.frontmatter.name] = skillTags;
873
+ }
874
+ const skillFrameworks = skill.frontmatter.metadata.frameworks;
875
+ if (skillFrameworks && skillFrameworks.length > 0) {
876
+ frameworks[skill.frontmatter.name] = skillFrameworks;
877
+ }
878
+ }
879
+ return SkillIndex.parse({
880
+ version: 2,
881
+ generatedAt: new Date().toISOString(),
882
+ skills: skillEntries,
883
+ tags,
884
+ frameworks
885
+ });
886
+ }
887
+ async function writeSkillIndex(index, outputPath) {
888
+ await mkdir7(dirname2(outputPath), { recursive: true });
889
+ const json = `${JSON.stringify(index, null, 2)}
890
+ `;
891
+ await writeFile7(outputPath, json, "utf8");
892
+ }
893
+ async function writeSkillIndexToon(index, outputPath) {
894
+ await mkdir7(dirname2(outputPath), { recursive: true });
895
+ const toon = encode(index, { delimiter: DELIMITERS.tab });
896
+ await writeFile7(outputPath, toon, "utf8");
897
+ }
898
+
899
+ // src/cli/build-index.ts
900
+ async function runBuildIndex(rootDir) {
901
+ const catalogDir = resolve(rootDir, "catalog");
902
+ const outputPath = resolve(catalogDir, "metadata/skill-index.json");
903
+ const toonOutputPath = resolve(catalogDir, "metadata/skill-index.toon");
904
+ const scanResult = await scanSkills(catalogDir);
905
+ if (scanResult.errors.length > 0) {
906
+ for (const error of scanResult.errors) {
907
+ console.error(`${red("✗")} ${error.path}: ${error.error}`);
908
+ }
909
+ process.exit(1);
910
+ }
911
+ const index = buildSkillIndex(scanResult.skills);
912
+ await writeSkillIndex(index, outputPath);
913
+ await writeSkillIndexToon(index, toonOutputPath);
914
+ console.log(`${green("✓")} Generated skill-index.json with ${index.skills.length} skills`);
915
+ console.log(`${green("✓")} Generated skill-index.toon with ${index.skills.length} skills`);
916
+ }
917
+ if (false) {}
918
+
919
+ // src/cli/check.ts
920
+ import { existsSync as existsSync2 } from "node:fs";
921
+ import { readdir as readdir4 } from "node:fs/promises";
922
+ import { join as join12 } from "node:path";
923
+
924
+ // src/install/checker.ts
925
+ import { join as join11 } from "node:path";
926
+
927
+ // src/catalog/provider.ts
928
+ import { execFile as execFileCb } from "node:child_process";
929
+ import { existsSync } from "node:fs";
930
+ import {
931
+ mkdir as mkdir8,
932
+ readFile as readFile4,
933
+ readdir as readdir3,
934
+ rename,
935
+ rm as rm6,
936
+ writeFile as writeFile8
937
+ } from "node:fs/promises";
938
+ import { join as join9 } from "node:path";
939
+ import { promisify } from "node:util";
940
+ import { z as z3 } from "zod";
941
+ var DEFAULT_SOURCE = {
942
+ owner: "yunseo-kim",
943
+ repo: "agent-toolbox",
944
+ branch: "main"
945
+ };
946
+ var USER_AGENT = "agent-toolbox-cli";
947
+ var execFileAsync = promisify(execFileCb);
948
+ var CatalogSourceSchema = z3.object({
949
+ owner: z3.string().min(1),
950
+ repo: z3.string().min(1),
951
+ branch: z3.string().min(1)
952
+ });
953
+ var CatalogResolveOptionsSchema = z3.object({
954
+ rootDir: z3.string().min(1),
955
+ remote: z3.boolean().optional(),
956
+ refresh: z3.boolean().optional(),
957
+ offline: z3.boolean().optional(),
958
+ source: CatalogSourceSchema.partial().optional()
959
+ });
960
+ var CommitShaSchema = z3.string().regex(/^[0-9a-f]{40}$/i, "Invalid commit SHA");
961
+ var CacheMetaSchema = z3.object({
962
+ commitSha: CommitShaSchema,
963
+ etag: z3.string().nullable(),
964
+ fetchedAt: z3.iso.datetime({ offset: true }),
965
+ source: CatalogSourceSchema
966
+ });
967
+ function getCacheDir() {
968
+ const xdgCacheHome = process.env.XDG_CACHE_HOME;
969
+ if (xdgCacheHome) {
970
+ return join9(xdgCacheHome, "agent-toolbox");
971
+ }
972
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
973
+ if (process.platform === "win32") {
974
+ const localAppData = process.env.LOCALAPPDATA ?? join9(home, "AppData", "Local");
975
+ return join9(localAppData, "agent-toolbox");
976
+ }
977
+ return join9(home, ".cache", "agent-toolbox");
978
+ }
979
+ async function fetchWithETag(url, cachedEtag, headers) {
980
+ const requestHeaders = {
981
+ "User-Agent": USER_AGENT,
982
+ ...headers
983
+ };
984
+ if (cachedEtag) {
985
+ requestHeaders["If-None-Match"] = cachedEtag;
986
+ }
987
+ const response = await fetch(url, {
988
+ headers: requestHeaders,
989
+ redirect: "follow"
990
+ });
991
+ if (response.status === 304) {
992
+ return {
993
+ status: 304,
994
+ body: null,
995
+ etag: cachedEtag
996
+ };
997
+ }
998
+ if (!response.ok) {
999
+ throw new Error(`HTTP ${response.status} fetching ${url}: ${response.statusText}`);
1000
+ }
1001
+ return {
1002
+ status: response.status,
1003
+ body: await response.text(),
1004
+ etag: response.headers.get("etag")
1005
+ };
1006
+ }
1007
+ async function fetchCommitShaFromApi(source, cachedEtag) {
1008
+ const url = `https://api.github.com/repos/${source.owner}/${source.repo}/commits/${source.branch}`;
1009
+ const result = await fetchWithETag(url, cachedEtag, {
1010
+ Accept: "application/vnd.github.sha",
1011
+ "X-GitHub-Api-Version": "2022-11-28"
1012
+ });
1013
+ if (result.status === 304) {
1014
+ return {
1015
+ notModified: true,
1016
+ etag: result.etag
1017
+ };
1018
+ }
1019
+ const sha = CommitShaSchema.parse((result.body ?? "").trim());
1020
+ return {
1021
+ notModified: false,
1022
+ sha,
1023
+ etag: result.etag
1024
+ };
1025
+ }
1026
+ async function checkCatalogFreshness(source, cachedEtag) {
1027
+ const rawUrl = `https://raw.githubusercontent.com/${source.owner}/${source.repo}/${source.branch}/catalog/metadata/skill-index.json`;
1028
+ try {
1029
+ const rawResult = await fetchWithETag(rawUrl, cachedEtag, {
1030
+ Accept: "application/json"
1031
+ });
1032
+ if (rawResult.status === 304) {
1033
+ return {
1034
+ notModified: true,
1035
+ etag: rawResult.etag
1036
+ };
1037
+ }
1038
+ return await fetchCommitShaFromApi(source, null);
1039
+ } catch (primaryError) {
1040
+ try {
1041
+ return await fetchCommitShaFromApi(source, cachedEtag);
1042
+ } catch (secondaryError) {
1043
+ const primaryReason = primaryError instanceof Error ? primaryError.message : String(primaryError);
1044
+ const secondaryReason = secondaryError instanceof Error ? secondaryError.message : String(secondaryError);
1045
+ throw new Error(`Failed to check catalog updates via raw.githubusercontent.com (${primaryReason}) and GitHub API (${secondaryReason})`, { cause: secondaryError });
1046
+ }
1047
+ }
1048
+ }
1049
+ async function fetchLatestCommitSha(source) {
1050
+ const latest = await fetchCommitShaFromApi(source, null);
1051
+ if (latest.notModified) {
1052
+ throw new Error("Unexpected 304 response while fetching latest commit SHA");
1053
+ }
1054
+ return latest.sha;
1055
+ }
1056
+ async function downloadAndExtractCatalog(source, sha, cacheRoot) {
1057
+ const tarUrl = `https://codeload.github.com/${source.owner}/${source.repo}/tar.gz/${sha}`;
1058
+ const response = await fetch(tarUrl, {
1059
+ headers: { "User-Agent": USER_AGENT },
1060
+ redirect: "follow"
1061
+ });
1062
+ if (!response.ok) {
1063
+ throw new Error(`Failed to download catalog archive: HTTP ${response.status}`);
1064
+ }
1065
+ const tmpRoot = join9(cacheRoot, ".tmp-catalog");
1066
+ const archivePath = join9(tmpRoot, "catalog.tar.gz");
1067
+ const stageDir = join9(cacheRoot, "catalog.next");
1068
+ const catalogDir = join9(cacheRoot, "catalog");
1069
+ await rm6(tmpRoot, { recursive: true, force: true });
1070
+ await rm6(stageDir, { recursive: true, force: true });
1071
+ await mkdir8(tmpRoot, { recursive: true });
1072
+ const archive = await response.arrayBuffer();
1073
+ await writeFile8(archivePath, Buffer.from(archive));
1074
+ try {
1075
+ await execFileAsync("tar", ["xzf", archivePath, "-C", tmpRoot]);
1076
+ } catch (error) {
1077
+ const execError = error;
1078
+ throw new Error(`Catalog archive extraction failed (exit ${execError.code ?? 1}): ${(execError.stderr ?? "").trim()}`, { cause: error });
1079
+ }
1080
+ const extractedEntries = await readdir3(tmpRoot, { withFileTypes: true });
1081
+ const repoDir = extractedEntries.find((entry) => entry.isDirectory())?.name;
1082
+ if (!repoDir) {
1083
+ throw new Error("Catalog archive did not contain repository directory");
1084
+ }
1085
+ const extractedCatalogDir = join9(tmpRoot, repoDir, "catalog");
1086
+ if (!existsSync(extractedCatalogDir)) {
1087
+ throw new Error("Catalog archive does not contain catalog/ directory");
1088
+ }
1089
+ await rename(extractedCatalogDir, stageDir);
1090
+ await rm6(catalogDir, { recursive: true, force: true });
1091
+ await rename(stageDir, catalogDir);
1092
+ await rm6(tmpRoot, { recursive: true, force: true });
1093
+ }
1094
+ async function readCacheMeta(cacheRoot) {
1095
+ const path = join9(cacheRoot, "cache-meta.json");
1096
+ try {
1097
+ const content = await readFile4(path, "utf8");
1098
+ const raw = JSON.parse(content);
1099
+ return CacheMetaSchema.parse(raw);
1100
+ } catch {
1101
+ return null;
1102
+ }
1103
+ }
1104
+ async function writeCacheMeta(cacheRoot, meta) {
1105
+ const path = join9(cacheRoot, "cache-meta.json");
1106
+ await mkdir8(cacheRoot, { recursive: true });
1107
+ await writeFile8(path, JSON.stringify(meta, null, 2), "utf8");
1108
+ }
1109
+ function isRunningFromPackageManager() {
1110
+ const npmExecPath = process.env.npm_execpath ?? "";
1111
+ const npmUserAgent = process.env.npm_config_user_agent ?? "";
1112
+ const bunxMode = process.env.BUN_INSTALL_BIN ?? "";
1113
+ return npmExecPath.includes("npx") || npmExecPath.includes("npm-cli") || npmExecPath.includes("bunx") || npmUserAgent.includes("npm/") || npmUserAgent.includes("bun/") || bunxMode.length > 0;
1114
+ }
1115
+ async function resolveCatalogDir(options) {
1116
+ const parsedOptions = CatalogResolveOptionsSchema.parse(options);
1117
+ const source = CatalogSourceSchema.parse({
1118
+ ...DEFAULT_SOURCE,
1119
+ ...parsedOptions.source
1120
+ });
1121
+ const shouldUseRemote = parsedOptions.remote ?? isRunningFromPackageManager();
1122
+ if (!shouldUseRemote) {
1123
+ return join9(parsedOptions.rootDir, "catalog");
1124
+ }
1125
+ const cacheRoot = getCacheDir();
1126
+ const catalogDir = join9(cacheRoot, "catalog");
1127
+ const hasCachedCatalog = existsSync(catalogDir);
1128
+ if (parsedOptions.offline) {
1129
+ if (!hasCachedCatalog) {
1130
+ throw new Error("No cached catalog found. Run once without --offline to download catalog data.");
1131
+ }
1132
+ return catalogDir;
1133
+ }
1134
+ const cacheMeta = await readCacheMeta(cacheRoot);
1135
+ if (!parsedOptions.refresh && hasCachedCatalog && cacheMeta) {
1136
+ try {
1137
+ const freshness = await checkCatalogFreshness(source, cacheMeta.etag);
1138
+ if (freshness.notModified) {
1139
+ return catalogDir;
1140
+ }
1141
+ if (freshness.sha === cacheMeta.commitSha) {
1142
+ await writeCacheMeta(cacheRoot, {
1143
+ ...cacheMeta,
1144
+ etag: freshness.etag ?? cacheMeta.etag,
1145
+ fetchedAt: new Date().toISOString(),
1146
+ source
1147
+ });
1148
+ return catalogDir;
1149
+ }
1150
+ } catch (error) {
1151
+ if (hasCachedCatalog) {
1152
+ const reason = error instanceof Error ? error.message : String(error);
1153
+ console.warn(`Could not refresh catalog metadata (${reason}). Using cached catalog.`);
1154
+ return catalogDir;
1155
+ }
1156
+ throw error;
1157
+ }
1158
+ }
1159
+ try {
1160
+ const sha = await fetchLatestCommitSha(source);
1161
+ if (!parsedOptions.refresh && cacheMeta?.commitSha === sha && hasCachedCatalog) {
1162
+ await writeCacheMeta(cacheRoot, {
1163
+ ...cacheMeta,
1164
+ commitSha: sha,
1165
+ fetchedAt: new Date().toISOString(),
1166
+ source
1167
+ });
1168
+ return catalogDir;
1169
+ }
1170
+ await mkdir8(cacheRoot, { recursive: true });
1171
+ await downloadAndExtractCatalog(source, sha, cacheRoot);
1172
+ await writeCacheMeta(cacheRoot, {
1173
+ commitSha: sha,
1174
+ etag: cacheMeta?.etag ?? null,
1175
+ fetchedAt: new Date().toISOString(),
1176
+ source
1177
+ });
1178
+ return catalogDir;
1179
+ } catch (error) {
1180
+ if (hasCachedCatalog) {
1181
+ const reason = error instanceof Error ? error.message : String(error);
1182
+ console.warn(`Failed to update catalog (${reason}). Using cached catalog.`);
1183
+ return catalogDir;
1184
+ }
1185
+ throw error;
1186
+ }
1187
+ }
1188
+
1189
+ // src/schemas/install.ts
1190
+ import { z as z4 } from "zod";
1191
+ var InstallFilters = z4.object({
1192
+ target: TargetTool,
1193
+ domain: z4.string().optional(),
1194
+ subdomain: z4.string().optional(),
1195
+ framework: z4.string().optional(),
1196
+ tag: z4.string().optional(),
1197
+ preset: z4.string().optional(),
1198
+ skill: z4.array(z4.string()).optional(),
1199
+ interactive: z4.boolean().default(false),
1200
+ dryRun: z4.boolean().default(false),
1201
+ refresh: z4.boolean().optional().default(false),
1202
+ offline: z4.boolean().optional().default(false)
1203
+ });
1204
+
1205
+ // src/install/filter.ts
1206
+ function filterSkills(skills, rawFilters) {
1207
+ const filters = InstallFilters.parse(rawFilters);
1208
+ let matched = [...skills];
1209
+ const appliedFilters = [];
1210
+ const total = skills.length;
1211
+ if (filters.domain) {
1212
+ matched = matched.filter((skill) => skill.frontmatter.metadata.domain === filters.domain);
1213
+ appliedFilters.push(`domain=${filters.domain}`);
1214
+ }
1215
+ if (filters.subdomain) {
1216
+ matched = matched.filter((skill) => skill.frontmatter.metadata.subdomain === filters.subdomain);
1217
+ appliedFilters.push(`subdomain=${filters.subdomain}`);
1218
+ }
1219
+ if (filters.framework) {
1220
+ const framework = filters.framework.toLowerCase();
1221
+ matched = matched.filter((skill) => {
1222
+ const frameworks = skill.frontmatter.metadata.frameworks;
1223
+ if (!frameworks) {
1224
+ return false;
1225
+ }
1226
+ return frameworks.some((entry) => entry.toLowerCase() === framework);
1227
+ });
1228
+ appliedFilters.push(`framework=${filters.framework}`);
1229
+ }
1230
+ if (filters.tag) {
1231
+ const tag = filters.tag.toLowerCase();
1232
+ matched = matched.filter((skill) => {
1233
+ const tags = skill.frontmatter.metadata.tags;
1234
+ if (!tags) {
1235
+ return false;
1236
+ }
1237
+ return tags.some((entry) => entry.toLowerCase() === tag);
1238
+ });
1239
+ appliedFilters.push(`tag=${filters.tag}`);
1240
+ }
1241
+ if (filters.skill && filters.skill.length > 0) {
1242
+ const names = new Set(filters.skill.map((name) => name.toLowerCase()));
1243
+ matched = matched.filter((skill) => names.has(skill.frontmatter.name.toLowerCase()));
1244
+ appliedFilters.push(`skill=${filters.skill.join(",")}`);
1245
+ }
1246
+ return { matched, total, appliedFilters };
1247
+ }
1248
+
1249
+ // src/install/manifest.ts
1250
+ import { mkdir as mkdir9, readFile as readFile5, writeFile as writeFile9 } from "node:fs/promises";
1251
+ import { dirname as dirname4, join as join10 } from "node:path";
1252
+
1253
+ // src/schemas/manifest.ts
1254
+ import { z as z5 } from "zod";
1255
+ var InstallManifestSkill = z5.object({
1256
+ name: z5.string(),
1257
+ domain: z5.string(),
1258
+ subdomain: z5.string().optional(),
1259
+ lastUpdated: z5.string()
1260
+ });
1261
+ var InstallManifestFilters = z5.object({
1262
+ domain: z5.string().optional(),
1263
+ subdomain: z5.string().optional(),
1264
+ framework: z5.string().optional(),
1265
+ tag: z5.string().optional(),
1266
+ preset: z5.string().optional(),
1267
+ skill: z5.array(z5.string()).optional()
1268
+ });
1269
+ var InstallManifest = z5.object({
1270
+ version: z5.literal(1),
1271
+ target: TargetTool,
1272
+ installedAt: z5.string(),
1273
+ catalogSource: z5.enum(["local", "remote"]),
1274
+ filters: InstallManifestFilters,
1275
+ skills: z5.array(InstallManifestSkill)
1276
+ });
1277
+
1278
+ // src/install/manifest.ts
1279
+ var MANIFEST_FILENAME = ".install-manifest.json";
1280
+ async function readManifest(targetDir) {
1281
+ const path = join10(targetDir, MANIFEST_FILENAME);
1282
+ try {
1283
+ const content = await readFile5(path, "utf8");
1284
+ const raw = JSON.parse(content);
1285
+ return InstallManifest.parse(raw);
1286
+ } catch {
1287
+ return null;
1288
+ }
1289
+ }
1290
+ async function writeManifest(targetDir, manifest) {
1291
+ const path = join10(targetDir, MANIFEST_FILENAME);
1292
+ await mkdir9(dirname4(path), { recursive: true });
1293
+ await writeFile9(path, JSON.stringify(manifest, null, 2) + `
1294
+ `, "utf8");
1295
+ }
1296
+ async function removeSkillsFromManifest(targetDir, skillNames) {
1297
+ const manifest = await readManifest(targetDir);
1298
+ if (!manifest) {
1299
+ return null;
1300
+ }
1301
+ const namesToRemove = new Set(skillNames.map((n) => n.toLowerCase()));
1302
+ const updated = {
1303
+ ...manifest,
1304
+ skills: manifest.skills.filter((s) => !namesToRemove.has(s.name.toLowerCase()))
1305
+ };
1306
+ await writeManifest(targetDir, updated);
1307
+ return updated;
1308
+ }
1309
+
1310
+ // src/install/checker.ts
1311
+ async function checkInstallStatus(rootDir, target, options = {}) {
1312
+ const targetDir = join11(rootDir, "dist", "targets", target);
1313
+ const manifest = await readManifest(targetDir);
1314
+ if (!manifest) {
1315
+ throw new Error(`No install manifest found for '${target}'. Run 'agent-toolbox install --target ${target}' first.`);
1316
+ }
1317
+ const catalogDir = await resolveCatalogDir({
1318
+ rootDir,
1319
+ refresh: options.refresh,
1320
+ offline: options.offline
1321
+ });
1322
+ const { skills: catalogSkills } = await scanSkills(catalogDir);
1323
+ const currentMatch = filterSkills(catalogSkills, {
1324
+ target: manifest.target,
1325
+ domain: manifest.filters.domain,
1326
+ subdomain: manifest.filters.subdomain,
1327
+ framework: manifest.filters.framework,
1328
+ tag: manifest.filters.tag,
1329
+ skill: manifest.filters.skill
1330
+ });
1331
+ const catalogMap = new Map;
1332
+ for (const skill of currentMatch.matched) {
1333
+ catalogMap.set(skill.frontmatter.name.toLowerCase(), skill);
1334
+ }
1335
+ const installedMap = new Map;
1336
+ for (const skill of manifest.skills) {
1337
+ installedMap.set(skill.name.toLowerCase(), skill);
1338
+ }
1339
+ const entries = [];
1340
+ const summary = {
1341
+ upToDate: 0,
1342
+ outdated: 0,
1343
+ removedFromCatalog: 0,
1344
+ newInCatalog: 0
1345
+ };
1346
+ for (const installed of manifest.skills) {
1347
+ const catalogSkill = catalogMap.get(installed.name.toLowerCase());
1348
+ if (!catalogSkill) {
1349
+ entries.push({
1350
+ name: installed.name,
1351
+ status: "removed-from-catalog",
1352
+ installedDate: installed.lastUpdated
1353
+ });
1354
+ summary.removedFromCatalog += 1;
1355
+ } else if (catalogSkill.frontmatter.metadata.lastUpdated !== installed.lastUpdated) {
1356
+ entries.push({
1357
+ name: installed.name,
1358
+ status: "outdated",
1359
+ installedDate: installed.lastUpdated,
1360
+ catalogDate: catalogSkill.frontmatter.metadata.lastUpdated
1361
+ });
1362
+ summary.outdated += 1;
1363
+ } else {
1364
+ entries.push({
1365
+ name: installed.name,
1366
+ status: "up-to-date",
1367
+ installedDate: installed.lastUpdated,
1368
+ catalogDate: catalogSkill.frontmatter.metadata.lastUpdated
1369
+ });
1370
+ summary.upToDate += 1;
1371
+ }
1372
+ }
1373
+ for (const [name, catalogSkill] of catalogMap) {
1374
+ if (!installedMap.has(name)) {
1375
+ entries.push({
1376
+ name: catalogSkill.frontmatter.name,
1377
+ status: "new-in-catalog",
1378
+ catalogDate: catalogSkill.frontmatter.metadata.lastUpdated
1379
+ });
1380
+ summary.newInCatalog += 1;
1381
+ }
1382
+ }
1383
+ const statusOrder = {
1384
+ outdated: 0,
1385
+ "new-in-catalog": 1,
1386
+ "removed-from-catalog": 2,
1387
+ "up-to-date": 3
1388
+ };
1389
+ entries.sort((a, b) => (statusOrder[a.status] ?? 99) - (statusOrder[b.status] ?? 99));
1390
+ return {
1391
+ target: manifest.target,
1392
+ installedAt: manifest.installedAt,
1393
+ entries,
1394
+ summary
1395
+ };
1396
+ }
1397
+
1398
+ // src/cli/check.ts
1399
+ var NAME = "agent-toolbox";
1400
+ function printCheckHelp() {
1401
+ console.log(`
1402
+ USAGE
1403
+ ${NAME} check [options]
1404
+
1405
+ DESCRIPTION
1406
+ Check if installed skills are outdated compared to the current catalog.
1407
+ Without --target, checks all targets that have an install manifest.
1408
+
1409
+ OPTIONS
1410
+ --target <tool> Check specific target only
1411
+ --json Output as JSON
1412
+ --refresh Force re-download catalog for comparison
1413
+ --offline Use cached catalog only
1414
+
1415
+ EXAMPLES
1416
+ ${NAME} check
1417
+ ${NAME} check --target claude-code
1418
+ ${NAME} check --refresh
1419
+ ${NAME} check --json
1420
+ `.trimStart());
1421
+ }
1422
+ async function discoverTargets(rootDir) {
1423
+ const targetsDir = join12(rootDir, "dist", "targets");
1424
+ if (!existsSync2(targetsDir)) {
1425
+ return [];
1426
+ }
1427
+ const entries = await readdir4(targetsDir, { withFileTypes: true });
1428
+ const targets = [];
1429
+ for (const entry of entries) {
1430
+ if (!entry.isDirectory()) {
1431
+ continue;
1432
+ }
1433
+ const manifestPath = join12(targetsDir, entry.name, MANIFEST_FILENAME);
1434
+ if (existsSync2(manifestPath)) {
1435
+ targets.push(entry.name);
1436
+ }
1437
+ }
1438
+ return targets;
1439
+ }
1440
+ async function runCheck(rootDir, argv) {
1441
+ const args = parseArgs(argv);
1442
+ if (args.help === "true" || args.h === "true") {
1443
+ printCheckHelp();
1444
+ process.exit(0);
1445
+ }
1446
+ const refresh = args.refresh === "true";
1447
+ const offline = args.offline === "true";
1448
+ const jsonMode = args.json === "true";
1449
+ let targets;
1450
+ if (typeof args.target === "string" && args.target !== "true") {
1451
+ const targetParse = TargetTool.safeParse(args.target);
1452
+ if (!targetParse.success) {
1453
+ console.error(`${red("Error:")} Invalid target: ${args.target}. Valid: claude-code, opencode, cursor, codex, gemini`);
1454
+ process.exit(1);
1455
+ }
1456
+ targets = [targetParse.data];
1457
+ } else {
1458
+ targets = await discoverTargets(rootDir);
1459
+ if (targets.length === 0) {
1460
+ console.log(`No install manifests found. Run '${NAME} install --target <tool>' first.`);
1461
+ process.exit(0);
1462
+ }
1463
+ }
1464
+ const allResults = [];
1465
+ let hasOutdated = false;
1466
+ for (const target of targets) {
1467
+ try {
1468
+ const result = await checkInstallStatus(rootDir, target, {
1469
+ refresh,
1470
+ offline
1471
+ });
1472
+ allResults.push(result);
1473
+ if (result.summary.outdated > 0 || result.summary.newInCatalog > 0) {
1474
+ hasOutdated = true;
1475
+ }
1476
+ } catch (error) {
1477
+ const message = error instanceof Error ? error.message : String(error);
1478
+ if (!jsonMode) {
1479
+ console.error(`${yellow("Warning:")} ${target}: ${message}`);
1480
+ }
1481
+ }
1482
+ }
1483
+ if (jsonMode) {
1484
+ console.log(JSON.stringify(allResults, null, 2));
1485
+ process.exit(hasOutdated ? 1 : 0);
1486
+ }
1487
+ for (const result of allResults) {
1488
+ const { summary } = result;
1489
+ console.log(`
1490
+ ${bold(result.target)} ${dim(`(installed: ${result.installedAt})`)}`);
1491
+ if (summary.upToDate > 0) {
1492
+ console.log(` ${green("✓")} ${summary.upToDate} skill(s) up to date`);
1493
+ }
1494
+ if (summary.outdated > 0) {
1495
+ const outdatedNames = result.entries.filter((e) => e.status === "outdated").map((e) => e.name);
1496
+ console.log(` ${yellow("↑")} ${summary.outdated} skill(s) outdated: ${outdatedNames.join(", ")}`);
1497
+ }
1498
+ if (summary.newInCatalog > 0) {
1499
+ const newNames = result.entries.filter((e) => e.status === "new-in-catalog").map((e) => e.name);
1500
+ console.log(` ${cyan("+")} ${summary.newInCatalog} new skill(s) available: ${newNames.join(", ")}`);
1501
+ }
1502
+ if (summary.removedFromCatalog > 0) {
1503
+ const removedNames = result.entries.filter((e) => e.status === "removed-from-catalog").map((e) => e.name);
1504
+ console.log(` ${red("-")} ${summary.removedFromCatalog} skill(s) removed from catalog: ${removedNames.join(", ")}`);
1505
+ }
1506
+ }
1507
+ if (hasOutdated) {
1508
+ const targetHint = targets.length === 1 ? ` --target ${targets[0]}` : "";
1509
+ console.log(`
1510
+ Run '${NAME} update${targetHint}' to update.`);
1511
+ } else if (allResults.length > 0) {
1512
+ console.log(`
1513
+ ${green("✓")} All installed skills are up to date.`);
1514
+ }
1515
+ process.exit(hasOutdated ? 1 : 0);
1516
+ }
1517
+ if (false) {}
1518
+
1519
+ // src/catalog/search.ts
1520
+ function searchSkills(skills, query) {
1521
+ if (!query || query.trim() === "") {
1522
+ return skills.map((skill) => ({ skill, score: 0, matchedFields: [] }));
1523
+ }
1524
+ const q = query.toLowerCase().trim();
1525
+ const results = [];
1526
+ for (const skill of skills) {
1527
+ let score = 0;
1528
+ const matchedFields = [];
1529
+ const name = skill.frontmatter.name.toLowerCase();
1530
+ const description = skill.frontmatter.description.toLowerCase();
1531
+ const domain = skill.frontmatter.metadata.domain.toLowerCase();
1532
+ const subdomain = skill.frontmatter.metadata.subdomain?.toLowerCase() ?? "";
1533
+ const tags = skill.frontmatter.metadata.tags ?? [];
1534
+ const frameworks = skill.frontmatter.metadata.frameworks ?? [];
1535
+ if (name === q) {
1536
+ score += 100;
1537
+ matchedFields.push("name");
1538
+ } else if (name.includes(q)) {
1539
+ score += 50;
1540
+ matchedFields.push("name");
1541
+ }
1542
+ for (const tag of tags) {
1543
+ if (tag.toLowerCase() === q) {
1544
+ score += 30;
1545
+ matchedFields.push("tags");
1546
+ break;
1547
+ }
1548
+ }
1549
+ for (const fw of frameworks) {
1550
+ if (fw.toLowerCase() === q) {
1551
+ score += 30;
1552
+ matchedFields.push("frameworks");
1553
+ break;
1554
+ }
1555
+ }
1556
+ if (domain === q || domain.includes(q)) {
1557
+ score += 20;
1558
+ matchedFields.push("domain");
1559
+ }
1560
+ if (subdomain && (subdomain === q || subdomain.includes(q))) {
1561
+ score += 20;
1562
+ matchedFields.push("subdomain");
1563
+ }
1564
+ if (description.includes(q)) {
1565
+ score += 10;
1566
+ matchedFields.push("description");
1567
+ }
1568
+ if (score > 0) {
1569
+ results.push({ skill, score, matchedFields });
1570
+ }
1571
+ }
1572
+ return results.sort((a, b) => b.score - a.score);
1573
+ }
1574
+
1575
+ // src/cli/find.ts
1576
+ var NAME2 = "agent-toolbox";
1577
+ var DEFAULT_LIMIT = 20;
1578
+ function printFindHelp() {
1579
+ console.log(`
1580
+ USAGE
1581
+ ${NAME2} find <query> [options]
1582
+
1583
+ DESCRIPTION
1584
+ Search catalog skills by keyword. Matches against name, description,
1585
+ domain, subdomain, tags, and frameworks with weighted scoring.
1586
+
1587
+ OPTIONS
1588
+ --domain <d> Pre-filter by domain before searching
1589
+ --limit <n> Max results (default: ${DEFAULT_LIMIT})
1590
+ --json Output as JSON
1591
+ --refresh Force re-download catalog from remote
1592
+ --offline Use cached catalog only
1593
+
1594
+ EXAMPLES
1595
+ ${NAME2} find git
1596
+ ${NAME2} find react --domain development
1597
+ ${NAME2} find "ci-cd" --json
1598
+ ${NAME2} find testing --limit 5
1599
+ `.trimStart());
1600
+ }
1601
+ async function runFind(rootDir, argv) {
1602
+ const args = parseArgs(argv);
1603
+ if (args.help === "true" || args.h === "true") {
1604
+ printFindHelp();
1605
+ process.exit(0);
1606
+ }
1607
+ const query = argv.find((arg, idx) => !arg.startsWith("--") && (idx === 0 || !argv[idx - 1]?.startsWith("--")));
1608
+ if (!query) {
1609
+ console.error(`${red("Error:")} Missing search query.`);
1610
+ console.error(`Usage: ${NAME2} find <query> [options]`);
1611
+ process.exit(1);
1612
+ }
1613
+ const catalogDir = await resolveCatalogDir({
1614
+ rootDir,
1615
+ refresh: args.refresh === "true",
1616
+ offline: args.offline === "true"
1617
+ });
1618
+ const { skills, errors } = await scanSkills(catalogDir);
1619
+ if (errors.length > 0) {
1620
+ for (const err of errors) {
1621
+ console.error(`${yellow("Warning:")} ${err.path}: ${err.error}`);
1622
+ }
1623
+ }
1624
+ let searchPool = skills;
1625
+ if (typeof args.domain === "string" && args.domain !== "true") {
1626
+ const result = filterSkills(skills, {
1627
+ target: "claude-code",
1628
+ domain: args.domain
1629
+ });
1630
+ searchPool = result.matched;
1631
+ }
1632
+ const results = searchSkills(searchPool, query);
1633
+ const limitStr = typeof args.limit === "string" ? args.limit : undefined;
1634
+ const limit = limitStr ? parseInt(limitStr, 10) : DEFAULT_LIMIT;
1635
+ const limited = results.slice(0, limit);
1636
+ if (args.json === "true") {
1637
+ const entries = limited.map((r) => ({
1638
+ name: r.skill.frontmatter.name,
1639
+ description: r.skill.frontmatter.description,
1640
+ domain: r.skill.frontmatter.metadata.domain,
1641
+ subdomain: r.skill.frontmatter.metadata.subdomain ?? null,
1642
+ score: r.score,
1643
+ matchedFields: r.matchedFields
1644
+ }));
1645
+ console.log(JSON.stringify(entries, null, 2));
1646
+ process.exit(0);
1647
+ }
1648
+ if (results.length === 0) {
1649
+ console.log(`No skills found matching ${bold(`"${query}"`)}.`);
1650
+ process.exit(0);
1651
+ }
1652
+ const domainNote = typeof args.domain === "string" && args.domain !== "true" ? ` in ${cyan(args.domain)}` : "";
1653
+ console.log(`
1654
+ ${bold("Search results")} for ${cyan(`"${query}"`)}${domainNote} ${dim(`(${results.length} found, showing ${limited.length})`)}`);
1655
+ console.log();
1656
+ const headers = ["Name", "Domain", "Score", "Matched", "Description"];
1657
+ const rows = limited.map((r) => [
1658
+ r.skill.frontmatter.name,
1659
+ r.skill.frontmatter.metadata.domain,
1660
+ String(r.score),
1661
+ r.matchedFields.join(", "),
1662
+ r.skill.frontmatter.description
1663
+ ]);
1664
+ console.log(formatTable(headers, rows, { maxColWidth: 45 }));
1665
+ if (results.length > limit) {
1666
+ console.log(`
1667
+ ${dim(`Showing ${limit} of ${results.length} results. Use --limit to see more.`)}`);
1668
+ }
1669
+ }
1670
+ if (false) {}
1671
+
1672
+ // src/install/installer.ts
1673
+ import { readFile as readFile7 } from "node:fs/promises";
1674
+ import { join as join13 } from "node:path";
1675
+
1676
+ // src/install/presets.ts
1677
+ import { readFile as readFile6 } from "node:fs/promises";
1678
+ import { parse as parseYaml2 } from "yaml";
1679
+
1680
+ // src/schemas/presets.ts
1681
+ import { z as z6 } from "zod";
1682
+ var PresetEntry = z6.object({
1683
+ name: z6.string(),
1684
+ description: z6.string(),
1685
+ items: z6.array(z6.string())
1686
+ });
1687
+ var PresetsSchema = z6.object({
1688
+ presets: z6.array(PresetEntry)
1689
+ });
1690
+
1691
+ // src/install/presets.ts
1692
+ async function loadPresets(presetsPath) {
1693
+ const content = await readFile6(presetsPath, "utf8");
1694
+ const raw = parseYaml2(content);
1695
+ return PresetsSchema.parse(raw);
1696
+ }
1697
+ function resolvePreset(config, presetName) {
1698
+ const preset = config.presets.find((entry) => entry.name === presetName);
1699
+ return preset ? preset.items : null;
1700
+ }
1701
+
1702
+ // src/install/installer.ts
1703
+ var generatorMap = {
1704
+ "claude-code": () => new ClaudeCodeGenerator,
1705
+ opencode: () => new OpenCodeGenerator,
1706
+ cursor: () => new CursorGenerator,
1707
+ codex: () => new CodexGenerator,
1708
+ gemini: () => new GeminiGenerator
1709
+ };
1710
+ async function install(rootDir, rawFilters) {
1711
+ const filters = InstallFilters.parse(rawFilters);
1712
+ const catalogResolveOptions = {
1713
+ rootDir,
1714
+ remote: undefined,
1715
+ refresh: filters.refresh,
1716
+ offline: filters.offline
1717
+ };
1718
+ const catalogDir = await resolveCatalogDir(catalogResolveOptions);
1719
+ const isRemoteCatalog = !catalogDir.startsWith(rootDir);
1720
+ const presetsPath = join13(catalogDir, "metadata", "presets.yaml");
1721
+ const { skills, errors } = await scanSkills(catalogDir);
1722
+ if (errors.length > 0) {
1723
+ console.error(`Scan errors: ${errors.length}`);
1724
+ for (const err of errors) {
1725
+ console.error(` ${err.path}: ${err.error}`);
1726
+ }
1727
+ }
1728
+ const resolvedFilters = { ...filters };
1729
+ if (filters.preset) {
1730
+ const presets = await loadPresets(presetsPath);
1731
+ const presetSkills = resolvePreset(presets, filters.preset);
1732
+ if (!presetSkills) {
1733
+ throw new Error(`Preset '${filters.preset}' not found in presets.yaml`);
1734
+ }
1735
+ resolvedFilters.skill = [...resolvedFilters.skill ?? [], ...presetSkills];
1736
+ }
1737
+ const filterResult = filterSkills(skills, resolvedFilters);
1738
+ if (filters.dryRun) {
1739
+ return { filterResult, generatorResult: null, dryRun: true };
1740
+ }
1741
+ const createGenerator = generatorMap[filters.target];
1742
+ if (!createGenerator) {
1743
+ throw new Error(`Generator for '${filters.target}' not yet implemented. Available: ${Object.keys(generatorMap).join(", ")}`);
1744
+ }
1745
+ const generator = createGenerator();
1746
+ const outputDir = join13(rootDir, "dist", "targets", filters.target);
1747
+ const pkg = JSON.parse(await readFile7(join13(rootDir, "package.json"), "utf8"));
1748
+ const generatorResult = await generator.generate({
1749
+ skills: filterResult.matched,
1750
+ outputDir,
1751
+ catalogDir,
1752
+ version: pkg.version
1753
+ });
1754
+ await writeManifest(outputDir, {
1755
+ version: 1,
1756
+ target: filters.target,
1757
+ installedAt: new Date().toISOString(),
1758
+ catalogSource: isRemoteCatalog ? "remote" : "local",
1759
+ filters: {
1760
+ domain: filters.domain,
1761
+ subdomain: filters.subdomain,
1762
+ framework: filters.framework,
1763
+ tag: filters.tag,
1764
+ preset: filters.preset,
1765
+ skill: filters.skill
1766
+ },
1767
+ skills: filterResult.matched.map((s) => ({
1768
+ name: s.frontmatter.name,
1769
+ domain: s.frontmatter.metadata.domain,
1770
+ subdomain: s.frontmatter.metadata.subdomain,
1771
+ lastUpdated: s.frontmatter.metadata.lastUpdated
1772
+ }))
1773
+ });
1774
+ return { filterResult, generatorResult, dryRun: false };
1775
+ }
1776
+
1777
+ // src/cli/install.ts
1778
+ async function runInstall(rootDir, argv) {
1779
+ const args = parseArgs(argv);
1780
+ if (!args.target || args.target === "true") {
1781
+ console.error("Usage: agent-toolbox install --target <tool> [options]");
1782
+ console.error("");
1783
+ console.error("Options:");
1784
+ console.error(" --target <tool> Required. claude-code, opencode, cursor, codex, gemini");
1785
+ console.error(" --domain <domain> Filter by domain");
1786
+ console.error(" --subdomain <sub> Filter by subdomain");
1787
+ console.error(" --framework <fw> Filter by framework");
1788
+ console.error(" --tag <tag> Filter by tag");
1789
+ console.error(" --preset <name> Install preset bundle");
1790
+ console.error(" --skill <name> Install specific skill(s) (repeatable)");
1791
+ console.error(" --dry-run Preview without installing");
1792
+ console.error(" --interactive Interactive selection (future)");
1793
+ console.error(" --refresh Force re-download catalog from remote");
1794
+ console.error(" --offline Use cached catalog only, no network");
1795
+ process.exit(1);
1796
+ }
1797
+ const targetParse = TargetTool.safeParse(args.target);
1798
+ if (!targetParse.success) {
1799
+ console.error(`Invalid target: ${String(args.target)}. Valid: claude-code, opencode, cursor, codex, gemini`);
1800
+ process.exit(1);
1801
+ }
1802
+ const filters = {
1803
+ target: targetParse.data,
1804
+ domain: typeof args.domain === "string" && args.domain !== "true" ? args.domain : undefined,
1805
+ subdomain: typeof args.subdomain === "string" && args.subdomain !== "true" ? args.subdomain : undefined,
1806
+ framework: typeof args.framework === "string" && args.framework !== "true" ? args.framework : undefined,
1807
+ tag: typeof args.tag === "string" && args.tag !== "true" ? args.tag : undefined,
1808
+ preset: typeof args.preset === "string" && args.preset !== "true" ? args.preset : undefined,
1809
+ skill: Array.isArray(args.skill) ? args.skill : undefined,
1810
+ dryRun: args["dry-run"] === "true",
1811
+ interactive: args.interactive === "true",
1812
+ refresh: args.refresh === "true",
1813
+ offline: args.offline === "true"
1814
+ };
1815
+ const result = await install(rootDir, {
1816
+ target: filters.target,
1817
+ domain: filters.domain,
1818
+ subdomain: filters.subdomain,
1819
+ framework: filters.framework,
1820
+ tag: filters.tag,
1821
+ preset: filters.preset,
1822
+ skill: filters.skill,
1823
+ dryRun: filters.dryRun,
1824
+ interactive: filters.interactive,
1825
+ refresh: filters.refresh,
1826
+ offline: filters.offline
1827
+ });
1828
+ if (result.filterResult.appliedFilters.length > 0) {
1829
+ console.log(`Filters: ${result.filterResult.appliedFilters.join(", ")}`);
1830
+ }
1831
+ console.log(`Matched: ${result.filterResult.matched.length}/${result.filterResult.total} skills`);
1832
+ if (result.dryRun) {
1833
+ console.log(`
1834
+ ${yellow("[DRY RUN]")} Would install these skills:`);
1835
+ for (const skill of result.filterResult.matched) {
1836
+ console.log(` - ${skill.frontmatter.name} (${skill.frontmatter.metadata.domain})`);
1837
+ }
1838
+ } else if (result.generatorResult) {
1839
+ console.log(`
1840
+ ${green("✓")} Installed ${result.generatorResult.skillCount} skills for ${result.generatorResult.target}`);
1841
+ console.log(` Output: ${result.generatorResult.outputDir}`);
1842
+ }
1843
+ }
1844
+ if (false) {}
1845
+
1846
+ // src/cli/list.ts
1847
+ import { join as join14 } from "node:path";
1848
+ var NAME3 = "agent-toolbox";
1849
+ function printListHelp() {
1850
+ console.log(`
1851
+ USAGE
1852
+ ${NAME3} list [options]
1853
+
1854
+ DESCRIPTION
1855
+ List skills in the agent-toolbox catalog. By default lists all catalog skills.
1856
+ Use --installed --target <tool> to list skills installed for a specific target.
1857
+
1858
+ OPTIONS
1859
+ --domain <d> Filter by domain
1860
+ --subdomain <s> Filter by subdomain
1861
+ --framework <fw> Filter by framework
1862
+ --tag <tag> Filter by tag
1863
+ --provenance <p> Filter by provenance (ported|adapted|synthesized|original)
1864
+ --installed List installed skills (requires --target)
1865
+ --target <tool> Target tool for --installed mode
1866
+ --json Output as JSON
1867
+ --count Show count only
1868
+ --refresh Force re-download catalog from remote
1869
+ --offline Use cached catalog only
1870
+
1871
+ EXAMPLES
1872
+ ${NAME3} list
1873
+ ${NAME3} list --domain devops
1874
+ ${NAME3} list --provenance ported --count
1875
+ ${NAME3} list --installed --target claude-code
1876
+ ${NAME3} list --json
1877
+ `.trimStart());
1878
+ }
1879
+ async function runList(rootDir, argv) {
1880
+ const args = parseArgs(argv);
1881
+ if (args.help === "true" || args.h === "true") {
1882
+ printListHelp();
1883
+ process.exit(0);
1884
+ }
1885
+ const isInstalled = args.installed === "true";
1886
+ if (isInstalled) {
1887
+ if (!args.target || args.target === "true") {
1888
+ console.error(`${red("Error:")} --installed requires --target <tool>`);
1889
+ process.exit(1);
1890
+ }
1891
+ const targetParse = TargetTool.safeParse(args.target);
1892
+ if (!targetParse.success) {
1893
+ console.error(`${red("Error:")} Invalid target: ${String(args.target)}. Valid: claude-code, opencode, cursor, codex, gemini`);
1894
+ process.exit(1);
1895
+ }
1896
+ const targetDir = join14(rootDir, "dist", "targets", targetParse.data);
1897
+ const manifest = await readManifest(targetDir);
1898
+ if (!manifest) {
1899
+ console.log(`No skills installed for ${bold(targetParse.data)}. Run '${NAME3} install --target ${targetParse.data}' first.`);
1900
+ process.exit(0);
1901
+ }
1902
+ if (args.count === "true") {
1903
+ console.log(manifest.skills.length);
1904
+ process.exit(0);
1905
+ }
1906
+ if (args.json === "true") {
1907
+ console.log(JSON.stringify(manifest.skills, null, 2));
1908
+ process.exit(0);
1909
+ }
1910
+ console.log(`
1911
+ ${bold("Installed skills")} for ${cyan(targetParse.data)} ${dim(`(${manifest.installedAt})`)}`);
1912
+ console.log();
1913
+ const headers2 = ["Name", "Domain", "Subdomain", "Last Updated"];
1914
+ const rows2 = manifest.skills.map((s) => [
1915
+ s.name,
1916
+ s.domain,
1917
+ s.subdomain ?? "",
1918
+ s.lastUpdated
1919
+ ]);
1920
+ console.log(formatTable(headers2, rows2));
1921
+ console.log(`
1922
+ ${dim(`Total: ${manifest.skills.length} skills`)}`);
1923
+ return;
1924
+ }
1925
+ const catalogDir = await resolveCatalogDir({
1926
+ rootDir,
1927
+ refresh: args.refresh === "true",
1928
+ offline: args.offline === "true"
1929
+ });
1930
+ const { skills, errors } = await scanSkills(catalogDir);
1931
+ if (errors.length > 0) {
1932
+ for (const err of errors) {
1933
+ console.error(`${yellow("Warning:")} ${err.path}: ${err.error}`);
1934
+ }
1935
+ }
1936
+ const filterResult = filterSkills(skills, {
1937
+ target: "claude-code",
1938
+ domain: typeof args.domain === "string" && args.domain !== "true" ? args.domain : undefined,
1939
+ subdomain: typeof args.subdomain === "string" && args.subdomain !== "true" ? args.subdomain : undefined,
1940
+ framework: typeof args.framework === "string" && args.framework !== "true" ? args.framework : undefined,
1941
+ tag: typeof args.tag === "string" && args.tag !== "true" ? args.tag : undefined
1942
+ });
1943
+ let matched = filterResult.matched;
1944
+ if (typeof args.provenance === "string" && args.provenance !== "true") {
1945
+ const prov = args.provenance.toLowerCase();
1946
+ matched = matched.filter((s) => s.frontmatter.metadata.provenance === prov);
1947
+ }
1948
+ matched.sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name));
1949
+ if (args.count === "true") {
1950
+ console.log(matched.length);
1951
+ process.exit(0);
1952
+ }
1953
+ if (args.json === "true") {
1954
+ const entries = matched.map((s) => ({
1955
+ name: s.frontmatter.name,
1956
+ description: s.frontmatter.description,
1957
+ domain: s.frontmatter.metadata.domain,
1958
+ subdomain: s.frontmatter.metadata.subdomain ?? null,
1959
+ provenance: s.frontmatter.metadata.provenance,
1960
+ author: s.frontmatter.metadata.author,
1961
+ lastUpdated: s.frontmatter.metadata.lastUpdated
1962
+ }));
1963
+ console.log(JSON.stringify(entries, null, 2));
1964
+ process.exit(0);
1965
+ }
1966
+ const appliedFilters = [...filterResult.appliedFilters];
1967
+ if (typeof args.provenance === "string" && args.provenance !== "true") {
1968
+ appliedFilters.push(`provenance=${args.provenance}`);
1969
+ }
1970
+ if (appliedFilters.length > 0) {
1971
+ console.log(`${dim("Filters:")} ${appliedFilters.join(", ")}`);
1972
+ }
1973
+ console.log(`
1974
+ ${bold("Catalog Skills")} ${dim(`(${matched.length}/${filterResult.total})`)}`);
1975
+ console.log();
1976
+ const headers = ["Name", "Domain", "Provenance", "Description"];
1977
+ const rows = matched.map((s) => [
1978
+ s.frontmatter.name,
1979
+ s.frontmatter.metadata.domain,
1980
+ s.frontmatter.metadata.provenance,
1981
+ s.frontmatter.description
1982
+ ]);
1983
+ console.log(formatTable(headers, rows, { maxColWidth: 50 }));
1984
+ const domainCounts = new Map;
1985
+ for (const s of matched) {
1986
+ const d = s.frontmatter.metadata.domain;
1987
+ domainCounts.set(d, (domainCounts.get(d) ?? 0) + 1);
1988
+ }
1989
+ const domainSummary = [...domainCounts.entries()].sort((a, b) => b[1] - a[1]).map(([d, c]) => `${d} ${dim(`(${c})`)}`).join(" ");
1990
+ console.log(`
1991
+ ${dim("Domains:")} ${domainSummary}`);
1992
+ console.log(`${dim("Total:")} ${cyan(String(matched.length))} skills`);
1993
+ }
1994
+ if (false) {}
1995
+
1996
+ // src/cli/remove.ts
1997
+ import { existsSync as existsSync3 } from "node:fs";
1998
+ import { readdir as readdir5, rm as rm7 } from "node:fs/promises";
1999
+ import { join as join15 } from "node:path";
2000
+ var NAME4 = "agent-toolbox";
2001
+ function printRemoveHelp() {
2002
+ console.log(`
2003
+ USAGE
2004
+ ${NAME4} remove --target <tool> [options]
2005
+
2006
+ DESCRIPTION
2007
+ Remove installed skills from a target's generated output directory.
2008
+ Removed skills will be restored on the next 'install' run.
2009
+
2010
+ OPTIONS
2011
+ --target <tool> Required. claude-code | opencode | cursor | codex | gemini
2012
+ --skill <name> Skill(s) to remove (repeatable)
2013
+ --all Remove all installed skills for target
2014
+ --dry-run Preview without removing
2015
+
2016
+ EXAMPLES
2017
+ ${NAME4} remove --target claude-code --skill git-master
2018
+ ${NAME4} remove --target gemini --skill docs-writer --skill mcp-builder
2019
+ ${NAME4} remove --target opencode --all
2020
+ ${NAME4} remove --target cursor --all --dry-run
2021
+ `.trimStart());
2022
+ }
2023
+ async function runRemove(rootDir, argv) {
2024
+ const args = parseArgs(argv);
2025
+ if (args.help === "true" || args.h === "true") {
2026
+ printRemoveHelp();
2027
+ process.exit(0);
2028
+ }
2029
+ if (!args.target || args.target === "true") {
2030
+ console.error(`${red("Error:")} --target is required.`);
2031
+ printRemoveHelp();
2032
+ process.exit(1);
2033
+ }
2034
+ const targetParse = TargetTool.safeParse(args.target);
2035
+ if (!targetParse.success) {
2036
+ console.error(`${red("Error:")} Invalid target: ${String(args.target)}. Valid: claude-code, opencode, cursor, codex, gemini`);
2037
+ process.exit(1);
2038
+ }
2039
+ const removeAll = args.all === "true";
2040
+ const skillNames = Array.isArray(args.skill) ? args.skill : [];
2041
+ const dryRun = args["dry-run"] === "true";
2042
+ if (!removeAll && skillNames.length === 0) {
2043
+ console.error(`${red("Error:")} Specify --skill <name> or --all to remove skills.`);
2044
+ process.exit(1);
2045
+ }
2046
+ const target = targetParse.data;
2047
+ const targetDir = join15(rootDir, "dist", "targets", target);
2048
+ const skillsDir = join15(targetDir, "skills");
2049
+ if (!existsSync3(skillsDir)) {
2050
+ console.log(`No skills installed for ${bold(target)}. Nothing to remove.`);
2051
+ process.exit(0);
2052
+ }
2053
+ const entries = await readdir5(skillsDir, { withFileTypes: true });
2054
+ const installedSkills = entries.filter((e) => e.isDirectory()).map((e) => e.name);
2055
+ let toRemove;
2056
+ if (removeAll) {
2057
+ toRemove = installedSkills;
2058
+ } else {
2059
+ const installed = new Set(installedSkills.map((n) => n.toLowerCase()));
2060
+ const notFound = skillNames.filter((n) => !installed.has(n.toLowerCase()));
2061
+ if (notFound.length > 0) {
2062
+ console.error(`${red("Error:")} Skills not found in ${target}: ${notFound.join(", ")}`);
2063
+ console.error(`${dim("Installed:")} ${installedSkills.join(", ") || "(none)"}`);
2064
+ process.exit(1);
2065
+ }
2066
+ toRemove = skillNames;
2067
+ }
2068
+ if (toRemove.length === 0) {
2069
+ console.log(`No skills to remove for ${bold(target)}.`);
2070
+ process.exit(0);
2071
+ }
2072
+ if (dryRun) {
2073
+ console.log(`
2074
+ ${yellow("[DRY RUN]")} Would remove ${toRemove.length} skill(s) from ${cyan(target)}:`);
2075
+ for (const name of toRemove) {
2076
+ console.log(` - ${name}`);
2077
+ }
2078
+ return;
2079
+ }
2080
+ let removedCount = 0;
2081
+ for (const name of toRemove) {
2082
+ const skillDir = join15(skillsDir, name);
2083
+ if (existsSync3(skillDir)) {
2084
+ await rm7(skillDir, { recursive: true, force: true });
2085
+ removedCount += 1;
2086
+ }
2087
+ }
2088
+ await removeSkillsFromManifest(targetDir, toRemove);
2089
+ console.log(`
2090
+ ${green("✓")} Removed ${removedCount} skill(s) from ${cyan(target)}`);
2091
+ for (const name of toRemove) {
2092
+ console.log(` - ${name}`);
2093
+ }
2094
+ console.log(`
2095
+ ${dim(`Note: Removed skills will be restored on next '${NAME4} install --target ${target}'.`)}`);
2096
+ }
2097
+ if (false) {}
2098
+
2099
+ // src/cli/update.ts
2100
+ import { existsSync as existsSync4 } from "node:fs";
2101
+ import { readdir as readdir6 } from "node:fs/promises";
2102
+ import { join as join16 } from "node:path";
2103
+ var NAME5 = "agent-toolbox";
2104
+ function printUpdateHelp() {
2105
+ console.log(`
2106
+ USAGE
2107
+ ${NAME5} update [options]
2108
+
2109
+ DESCRIPTION
2110
+ Update installed skills to the latest catalog versions.
2111
+ Re-installs using the original install filters from the manifest.
2112
+ Without --target, updates all targets that have an install manifest.
2113
+
2114
+ OPTIONS
2115
+ --target <tool> Update specific target only
2116
+ --dry-run Preview what would be updated
2117
+ --refresh Force re-download catalog
2118
+ --offline Use cached catalog only
2119
+
2120
+ EXAMPLES
2121
+ ${NAME5} update
2122
+ ${NAME5} update --target claude-code
2123
+ ${NAME5} update --dry-run
2124
+ ${NAME5} update --refresh
2125
+ `.trimStart());
2126
+ }
2127
+ async function discoverTargets2(rootDir) {
2128
+ const targetsDir = join16(rootDir, "dist", "targets");
2129
+ if (!existsSync4(targetsDir)) {
2130
+ return [];
2131
+ }
2132
+ const entries = await readdir6(targetsDir, { withFileTypes: true });
2133
+ const targets = [];
2134
+ for (const entry of entries) {
2135
+ if (!entry.isDirectory()) {
2136
+ continue;
2137
+ }
2138
+ const manifestPath = join16(targetsDir, entry.name, MANIFEST_FILENAME);
2139
+ if (existsSync4(manifestPath)) {
2140
+ targets.push(entry.name);
2141
+ }
2142
+ }
2143
+ return targets;
2144
+ }
2145
+ async function runUpdate(rootDir, argv) {
2146
+ const args = parseArgs(argv);
2147
+ if (args.help === "true" || args.h === "true") {
2148
+ printUpdateHelp();
2149
+ process.exit(0);
2150
+ }
2151
+ const dryRun = args["dry-run"] === "true";
2152
+ const refresh = args.refresh === "true";
2153
+ const offline = args.offline === "true";
2154
+ let targets;
2155
+ if (typeof args.target === "string" && args.target !== "true") {
2156
+ const targetParse = TargetTool.safeParse(args.target);
2157
+ if (!targetParse.success) {
2158
+ console.error(`${red("Error:")} Invalid target: ${args.target}. Valid: claude-code, opencode, cursor, codex, gemini`);
2159
+ process.exit(1);
2160
+ }
2161
+ targets = [targetParse.data];
2162
+ } else {
2163
+ targets = await discoverTargets2(rootDir);
2164
+ if (targets.length === 0) {
2165
+ console.log(`No install manifests found. Run '${NAME5} install --target <tool>' first.`);
2166
+ process.exit(0);
2167
+ }
2168
+ }
2169
+ for (const target of targets) {
2170
+ const targetDir = join16(rootDir, "dist", "targets", target);
2171
+ const manifest = await readManifest(targetDir);
2172
+ if (!manifest) {
2173
+ console.error(`${yellow("Warning:")} No manifest for ${target}. Skipping.`);
2174
+ continue;
2175
+ }
2176
+ let checkResult;
2177
+ try {
2178
+ checkResult = await checkInstallStatus(rootDir, target, {
2179
+ refresh,
2180
+ offline
2181
+ });
2182
+ } catch (error) {
2183
+ const message = error instanceof Error ? error.message : String(error);
2184
+ console.error(`${yellow("Warning:")} ${target}: ${message}`);
2185
+ continue;
2186
+ }
2187
+ const { summary } = checkResult;
2188
+ const hasChanges = summary.outdated > 0 || summary.newInCatalog > 0 || summary.removedFromCatalog > 0;
2189
+ if (!hasChanges) {
2190
+ console.log(`${green("✓")} ${bold(target)}: All ${summary.upToDate} skill(s) up to date.`);
2191
+ continue;
2192
+ }
2193
+ console.log(`
2194
+ ${bold(target)}:`);
2195
+ if (summary.outdated > 0) {
2196
+ const names = checkResult.entries.filter((e) => e.status === "outdated").map((e) => e.name);
2197
+ console.log(` ${yellow("↑")} ${summary.outdated} outdated: ${names.join(", ")}`);
2198
+ }
2199
+ if (summary.newInCatalog > 0) {
2200
+ const names = checkResult.entries.filter((e) => e.status === "new-in-catalog").map((e) => e.name);
2201
+ console.log(` ${cyan("+")} ${summary.newInCatalog} new: ${names.join(", ")}`);
2202
+ }
2203
+ if (summary.removedFromCatalog > 0) {
2204
+ const names = checkResult.entries.filter((e) => e.status === "removed-from-catalog").map((e) => e.name);
2205
+ console.log(` ${red("-")} ${summary.removedFromCatalog} removed from catalog: ${names.join(", ")}`);
2206
+ }
2207
+ if (dryRun) {
2208
+ console.log(` ${yellow("[DRY RUN]")} Would re-install with original filters.`);
2209
+ continue;
2210
+ }
2211
+ const result = await install(rootDir, {
2212
+ target: manifest.target,
2213
+ domain: manifest.filters.domain,
2214
+ subdomain: manifest.filters.subdomain,
2215
+ framework: manifest.filters.framework,
2216
+ tag: manifest.filters.tag,
2217
+ skill: manifest.filters.skill,
2218
+ preset: manifest.filters.preset,
2219
+ dryRun: false,
2220
+ interactive: false,
2221
+ refresh: true,
2222
+ offline
2223
+ });
2224
+ if (result.generatorResult) {
2225
+ console.log(` ${green("✓")} Updated ${result.generatorResult.skillCount} skills for ${target}`);
2226
+ }
2227
+ }
2228
+ }
2229
+ if (false) {}
2230
+
2231
+ // src/cli/validate.ts
2232
+ import { basename as basename2, dirname as dirname10, resolve as resolve2 } from "node:path";
2233
+
2234
+ // src/catalog/validator.ts
2235
+ import { readdir as readdir7 } from "node:fs/promises";
2236
+ import { basename, dirname as dirname9, join as join17 } from "node:path";
2237
+
2238
+ // src/catalog/taxonomy.ts
2239
+ import { readFile as readFile8 } from "node:fs/promises";
2240
+ import { parse as parseYaml3 } from "yaml";
2241
+
2242
+ // src/schemas/taxonomy.ts
2243
+ import { z as z7 } from "zod";
2244
+ var SubdomainList = z7.array(z7.string());
2245
+ var DomainEntry = z7.object({
2246
+ description: z7.string(),
2247
+ subdomains: SubdomainList
2248
+ });
2249
+ var TaxonomySchema = z7.object({
2250
+ domains: z7.record(z7.string(), DomainEntry)
2251
+ });
2252
+
2253
+ // src/catalog/taxonomy.ts
2254
+ async function loadTaxonomy(taxonomyPath) {
2255
+ let content;
2256
+ try {
2257
+ content = await readFile8(taxonomyPath, "utf8");
2258
+ } catch (error) {
2259
+ const reason = error instanceof Error ? error.message : String(error);
2260
+ throw new Error(`Failed to read taxonomy file '${taxonomyPath}': ${reason}`, { cause: error });
2261
+ }
2262
+ let rawTaxonomy;
2263
+ try {
2264
+ rawTaxonomy = parseYaml3(content);
2265
+ } catch (error) {
2266
+ const reason = error instanceof Error ? error.message : String(error);
2267
+ throw new Error(`Malformed taxonomy YAML at '${taxonomyPath}': ${reason}`, {
2268
+ cause: error
2269
+ });
2270
+ }
2271
+ try {
2272
+ return TaxonomySchema.parse(rawTaxonomy);
2273
+ } catch (error) {
2274
+ const reason = error instanceof Error ? error.message : String(error);
2275
+ throw new Error(`Invalid taxonomy schema at '${taxonomyPath}': ${reason}`, {
2276
+ cause: error
2277
+ });
2278
+ }
2279
+ }
2280
+ function validateDomain(taxonomy, domain) {
2281
+ return Object.hasOwn(taxonomy.domains, domain);
2282
+ }
2283
+ function validateSubdomain(taxonomy, domain, subdomain) {
2284
+ const domainEntry = taxonomy.domains[domain];
2285
+ if (!domainEntry) {
2286
+ return false;
2287
+ }
2288
+ return domainEntry.subdomains.includes(subdomain);
2289
+ }
2290
+
2291
+ // src/catalog/validator.ts
2292
+ var HOLOCENE_DATE_REGEX = /^1\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
2293
+ var VALID_PROVENANCE = new Set([
2294
+ "ported",
2295
+ "adapted",
2296
+ "synthesized",
2297
+ "original"
2298
+ ]);
2299
+ function skillKeyFromPath(path) {
2300
+ if (path.endsWith("SKILL.md")) {
2301
+ return basename(dirname9(path));
2302
+ }
2303
+ return basename(path);
2304
+ }
2305
+ async function validateCatalog(catalogDir, taxonomyPath) {
2306
+ const errors = [];
2307
+ const warnings = [];
2308
+ const invalidSkillKeys = new Set;
2309
+ const domainCounts = {};
2310
+ const taxonomy = await loadTaxonomy(taxonomyPath);
2311
+ const scanResult = await scanSkills(catalogDir);
2312
+ for (const scanError of scanResult.errors) {
2313
+ errors.push({
2314
+ path: scanError.path,
2315
+ message: scanError.error
2316
+ });
2317
+ invalidSkillKeys.add(skillKeyFromPath(scanError.path));
2318
+ }
2319
+ for (const skill of scanResult.skills) {
2320
+ const skillPath = join17(catalogDir, "skills", skill.dirName);
2321
+ const metadata = skill.frontmatter.metadata;
2322
+ const description = skill.frontmatter.description;
2323
+ domainCounts[metadata.domain] = (domainCounts[metadata.domain] ?? 0) + 1;
2324
+ if (!skill.frontmatter.name) {
2325
+ errors.push({
2326
+ path: skillPath,
2327
+ field: "name",
2328
+ message: "Missing required field: name"
2329
+ });
2330
+ invalidSkillKeys.add(skill.dirName);
2331
+ }
2332
+ if (!skill.frontmatter.description) {
2333
+ errors.push({
2334
+ path: skillPath,
2335
+ field: "description",
2336
+ message: "Missing required field: description"
2337
+ });
2338
+ invalidSkillKeys.add(skill.dirName);
2339
+ }
2340
+ if (!skill.frontmatter.license) {
2341
+ errors.push({
2342
+ path: skillPath,
2343
+ field: "license",
2344
+ message: "Missing required field: license"
2345
+ });
2346
+ invalidSkillKeys.add(skill.dirName);
2347
+ }
2348
+ if (!metadata.domain) {
2349
+ errors.push({
2350
+ path: skillPath,
2351
+ field: "metadata.domain",
2352
+ message: "Missing required field: metadata.domain"
2353
+ });
2354
+ invalidSkillKeys.add(skill.dirName);
2355
+ }
2356
+ if (!metadata.author) {
2357
+ errors.push({
2358
+ path: skillPath,
2359
+ field: "metadata.author",
2360
+ message: "Missing required field: metadata.author"
2361
+ });
2362
+ invalidSkillKeys.add(skill.dirName);
2363
+ }
2364
+ if (!metadata.lastUpdated) {
2365
+ errors.push({
2366
+ path: skillPath,
2367
+ field: "metadata.lastUpdated",
2368
+ message: "Missing required field: metadata.lastUpdated"
2369
+ });
2370
+ invalidSkillKeys.add(skill.dirName);
2371
+ }
2372
+ if (!metadata.provenance) {
2373
+ errors.push({
2374
+ path: skillPath,
2375
+ field: "metadata.provenance",
2376
+ message: "Missing required field: metadata.provenance"
2377
+ });
2378
+ invalidSkillKeys.add(skill.dirName);
2379
+ }
2380
+ if (!validateDomain(taxonomy, metadata.domain)) {
2381
+ errors.push({
2382
+ path: skillPath,
2383
+ field: "metadata.domain",
2384
+ message: `Invalid domain '${metadata.domain}'`
2385
+ });
2386
+ invalidSkillKeys.add(skill.dirName);
2387
+ }
2388
+ if (metadata.subdomain) {
2389
+ if (!validateSubdomain(taxonomy, metadata.domain, metadata.subdomain)) {
2390
+ errors.push({
2391
+ path: skillPath,
2392
+ field: "metadata.subdomain",
2393
+ message: `Invalid subdomain '${metadata.subdomain}' for domain '${metadata.domain}'`
2394
+ });
2395
+ invalidSkillKeys.add(skill.dirName);
2396
+ }
2397
+ } else {
2398
+ warnings.push({
2399
+ path: skillPath,
2400
+ field: "metadata.subdomain",
2401
+ message: "Missing optional field: metadata.subdomain"
2402
+ });
2403
+ }
2404
+ if (!VALID_PROVENANCE.has(metadata.provenance)) {
2405
+ errors.push({
2406
+ path: skillPath,
2407
+ field: "metadata.provenance",
2408
+ message: `Invalid provenance '${metadata.provenance}'`
2409
+ });
2410
+ invalidSkillKeys.add(skill.dirName);
2411
+ }
2412
+ if (!skill.hasNotice) {
2413
+ errors.push({
2414
+ path: skillPath,
2415
+ field: "NOTICE.md",
2416
+ message: "Missing required file: NOTICE.md"
2417
+ });
2418
+ invalidSkillKeys.add(skill.dirName);
2419
+ }
2420
+ if (skill.dirName !== skill.frontmatter.name) {
2421
+ errors.push({
2422
+ path: skillPath,
2423
+ field: "name",
2424
+ message: `Skill directory '${skill.dirName}' does not match name '${skill.frontmatter.name}'`
2425
+ });
2426
+ invalidSkillKeys.add(skill.dirName);
2427
+ }
2428
+ if (!HOLOCENE_DATE_REGEX.test(metadata.lastUpdated)) {
2429
+ errors.push({
2430
+ path: skillPath,
2431
+ field: "metadata.lastUpdated",
2432
+ message: `Invalid Holocene date '${metadata.lastUpdated}'`
2433
+ });
2434
+ invalidSkillKeys.add(skill.dirName);
2435
+ }
2436
+ if (description.length > 1024) {
2437
+ errors.push({
2438
+ path: skillPath,
2439
+ field: "description",
2440
+ message: "Description exceeds 1024 characters"
2441
+ });
2442
+ invalidSkillKeys.add(skill.dirName);
2443
+ }
2444
+ if (description.trim().length === 0) {
2445
+ warnings.push({
2446
+ path: skillPath,
2447
+ field: "description",
2448
+ message: "Description is empty"
2449
+ });
2450
+ }
2451
+ if (metadata.tags === undefined) {
2452
+ warnings.push({
2453
+ path: skillPath,
2454
+ field: "metadata.tags",
2455
+ message: "Missing optional field: metadata.tags"
2456
+ });
2457
+ }
2458
+ if (metadata.frameworks === undefined) {
2459
+ warnings.push({
2460
+ path: skillPath,
2461
+ field: "metadata.frameworks",
2462
+ message: "Missing optional field: metadata.frameworks"
2463
+ });
2464
+ }
2465
+ }
2466
+ let totalSkills;
2467
+ try {
2468
+ const skillRoot = join17(catalogDir, "skills");
2469
+ const entries = await readdir7(skillRoot, { withFileTypes: true });
2470
+ totalSkills = entries.filter((entry) => entry.isDirectory()).length;
2471
+ } catch {
2472
+ totalSkills = scanResult.skills.length;
2473
+ }
2474
+ const invalidSkills = invalidSkillKeys.size;
2475
+ const validSkills = Math.max(totalSkills - invalidSkills, 0);
2476
+ return {
2477
+ valid: errors.length === 0,
2478
+ errors,
2479
+ warnings,
2480
+ stats: {
2481
+ totalSkills,
2482
+ validSkills,
2483
+ invalidSkills,
2484
+ domains: Object.fromEntries(Object.entries(domainCounts).sort(([a], [b]) => a.localeCompare(b)))
2485
+ }
2486
+ };
2487
+ }
2488
+
2489
+ // src/cli/validate.ts
2490
+ function skillKeyFromPath2(path) {
2491
+ if (path.endsWith("SKILL.md")) {
2492
+ return basename2(dirname10(path));
2493
+ }
2494
+ return basename2(path);
2495
+ }
2496
+ async function runValidate(rootDir) {
2497
+ const catalogDir = resolve2(rootDir, "catalog");
2498
+ const taxonomyPath = resolve2(catalogDir, "metadata/taxonomy.yaml");
2499
+ console.log(`Validating catalog...
2500
+ `);
2501
+ const result = await validateCatalog(catalogDir, taxonomyPath);
2502
+ const scan = await scanSkills(catalogDir);
2503
+ const invalidMessages = new Map;
2504
+ for (const error of result.errors) {
2505
+ const key = skillKeyFromPath2(error.path);
2506
+ if (!invalidMessages.has(key)) {
2507
+ invalidMessages.set(key, error.message);
2508
+ }
2509
+ }
2510
+ const parsedSkillNames = new Set(scan.skills.map((skill) => skill.dirName));
2511
+ for (const skill of [...scan.skills].sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name))) {
2512
+ const failure = invalidMessages.get(skill.dirName);
2513
+ if (failure) {
2514
+ console.log(`${red("✗")} ${skill.frontmatter.name}: ${failure}`);
2515
+ } else {
2516
+ console.log(`${green("✓")} ${skill.frontmatter.name} (${skill.frontmatter.metadata.domain})`);
2517
+ }
2518
+ }
2519
+ for (const [skillName, message] of [...invalidMessages.entries()].sort(([a], [b]) => a.localeCompare(b))) {
2520
+ if (!parsedSkillNames.has(skillName)) {
2521
+ console.log(`${red("✗")} ${skillName}: ${message}`);
2522
+ }
2523
+ }
2524
+ if (result.warnings.length > 0) {
2525
+ console.log(`
2526
+ Warnings:`);
2527
+ for (const warning of result.warnings) {
2528
+ const fieldSuffix = warning.field ? ` (${warning.field})` : "";
2529
+ console.log(` ${yellow("!")} ${skillKeyFromPath2(warning.path)}${fieldSuffix}: ${warning.message}`);
2530
+ }
2531
+ }
2532
+ console.log(`
2533
+ Results:`);
2534
+ console.log(` Total: ${result.stats.totalSkills}`);
2535
+ console.log(` Valid: ${result.stats.validSkills}`);
2536
+ console.log(` Invalid: ${result.stats.invalidSkills}`);
2537
+ console.log(`
2538
+ Domains:`);
2539
+ for (const [domain, count] of Object.entries(result.stats.domains).sort(([a], [b]) => a.localeCompare(b))) {
2540
+ console.log(` ${domain}: ${count}`);
2541
+ }
2542
+ process.exit(result.valid ? 0 : 1);
2543
+ }
2544
+ if (false) {}
2545
+
2546
+ // src/cli/main.ts
2547
+ var NAME6 = "agent-toolbox";
2548
+ async function getVersion(rootDir) {
2549
+ try {
2550
+ const pkg = JSON.parse(await readFile9(resolve3(rootDir, "package.json"), "utf8"));
2551
+ return pkg.version ?? "0.0.0";
2552
+ } catch {
2553
+ return "0.0.0";
2554
+ }
2555
+ }
2556
+ function printHelp() {
2557
+ console.log(`
2558
+ ${NAME6} — Cross-tool distribution system for agent skills
2559
+
2560
+ USAGE
2561
+ ${NAME6} <command> [options]
2562
+
2563
+ COMMANDS
2564
+ install Install skills to a target tool
2565
+ list List skills in the catalog
2566
+ find Search catalog skills by keyword
2567
+ remove Remove installed skills from a target
2568
+ check Check for outdated installed skills
2569
+ update Update installed skills to latest catalog
2570
+ build Build target artifacts
2571
+ build-index Generate skill-index.json and skill-index.toon
2572
+ validate Validate catalog against taxonomy
2573
+
2574
+ --help, -h Show help
2575
+ --version, -v Show version
2576
+
2577
+ BUILD OPTIONS
2578
+ --target <tool> claude-code | opencode | cursor | codex | gemini | all
2579
+
2580
+ INSTALL OPTIONS
2581
+ --target <tool> Required. claude-code | opencode | cursor | codex | gemini
2582
+ --domain <d> Filter by domain
2583
+ --subdomain <s> Filter by subdomain
2584
+ --framework <fw> Filter by framework
2585
+ --tag <tag> Filter by tag
2586
+ --preset <name> Install preset bundle
2587
+ --skill <name> Specific skill(s), repeatable
2588
+ --dry-run Preview without installing
2589
+ --interactive Interactive selection (future)
2590
+ --refresh Force re-download catalog from remote
2591
+ --offline Use cached catalog only, no network
2592
+
2593
+ EXAMPLES
2594
+ ${NAME6} install --target opencode --domain devops
2595
+ ${NAME6} install --target claude-code --dry-run
2596
+ ${NAME6} list
2597
+ ${NAME6} list --domain devops
2598
+ ${NAME6} find git
2599
+ ${NAME6} check --target claude-code
2600
+ ${NAME6} update --target opencode
2601
+ ${NAME6} remove --target cursor --skill git-master
2602
+ ${NAME6} build --target all
2603
+ ${NAME6} build --target opencode
2604
+ ${NAME6} validate
2605
+ ${NAME6} build-index
2606
+ `.trimStart());
2607
+ }
2608
+ function printBuildHelp() {
2609
+ console.log(`
2610
+ USAGE
2611
+ ${NAME6} build --target <tool>
2612
+
2613
+ TARGETS
2614
+ claude-code Generate Claude Code plugin artifacts
2615
+ opencode Generate OpenCode plugin + skills
2616
+ cursor Generate Cursor plugin artifacts
2617
+ codex Generate Codex skill directories
2618
+ gemini Generate Gemini CLI extension
2619
+ all Build all targets sequentially
2620
+
2621
+ EXAMPLES
2622
+ ${NAME6} build --target opencode
2623
+ ${NAME6} build --target all
2624
+ `.trimStart());
2625
+ }
2626
+ async function main() {
2627
+ const rootDir = resolve3(dirname11(fileURLToPath(import.meta.url)), "../..");
2628
+ const args = process.argv.slice(2);
2629
+ const command = args[0];
2630
+ if (!command || command === "--help" || command === "-h") {
2631
+ printHelp();
2632
+ process.exit(0);
2633
+ }
2634
+ if (command === "--version" || command === "-v") {
2635
+ const version = await getVersion(rootDir);
2636
+ console.log(version);
2637
+ process.exit(0);
2638
+ }
2639
+ switch (command) {
2640
+ case "validate":
2641
+ await runValidate(rootDir);
2642
+ break;
2643
+ case "build-index":
2644
+ await runBuildIndex(rootDir);
2645
+ break;
2646
+ case "build": {
2647
+ const subArgs = args.slice(1);
2648
+ if (subArgs[0] === "index") {
2649
+ await runBuildIndex(rootDir);
2650
+ break;
2651
+ }
2652
+ if (subArgs[0] === "--help" || subArgs[0] === "-h") {
2653
+ printBuildHelp();
2654
+ process.exit(0);
2655
+ }
2656
+ const targetIdx = subArgs.indexOf("--target");
2657
+ const target = targetIdx >= 0 ? subArgs[targetIdx + 1] : undefined;
2658
+ if (!target) {
2659
+ printBuildHelp();
2660
+ process.exit(1);
2661
+ }
2662
+ if (target === "all") {
2663
+ await runBuildAll(rootDir);
2664
+ } else {
2665
+ await runBuildTarget(rootDir, target);
2666
+ }
2667
+ break;
2668
+ }
2669
+ case "install":
2670
+ await runInstall(rootDir, args.slice(1));
2671
+ break;
2672
+ case "list":
2673
+ case "ls":
2674
+ await runList(rootDir, args.slice(1));
2675
+ break;
2676
+ case "find":
2677
+ case "search":
2678
+ await runFind(rootDir, args.slice(1));
2679
+ break;
2680
+ case "remove":
2681
+ case "rm":
2682
+ await runRemove(rootDir, args.slice(1));
2683
+ break;
2684
+ case "check":
2685
+ await runCheck(rootDir, args.slice(1));
2686
+ break;
2687
+ case "update":
2688
+ await runUpdate(rootDir, args.slice(1));
2689
+ break;
2690
+ default:
2691
+ console.error(`${red("Error:")} Unknown command '${command}'`);
2692
+ console.error(`Run '${NAME6} --help' for available commands.`);
2693
+ process.exit(1);
2694
+ }
2695
+ }
2696
+ main().catch((error) => {
2697
+ const reason = error instanceof Error ? error.message : String(error);
2698
+ console.error(`${red("Error:")} ${reason}`);
2699
+ process.exit(1);
2700
+ });