@kairyou/agent-tools 0.2.0 → 0.4.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.
Files changed (40) hide show
  1. package/README.md +112 -115
  2. package/README.zh-CN.md +108 -115
  3. package/dist/statusline/claude-statusline.mjs +355 -0
  4. package/dist/usage/cli.mjs +25 -0
  5. package/dist/usage/codex-hook.mjs +144 -0
  6. package/dist/usage/core.mjs +1054 -0
  7. package/dist/usage/opencode-plugin.mjs +80 -0
  8. package/dist/usage/opencode-tui.mjs +46 -0
  9. package/dist/vision/cli.mjs +13 -13
  10. package/dist/vision/mcp-server.mjs +12 -12
  11. package/{statusline/claude/statusline.mjs → integrations/statusline/claude-statusline.mjs} +33 -2
  12. package/integrations/usage/cli.mjs +27 -0
  13. package/{hooks/codex/usage-hook.mjs → integrations/usage/codex-hook.mjs} +1 -1
  14. package/{lib/usage.mjs → integrations/usage/core.mjs} +34 -1
  15. package/{plugins/opencode/usage-plugin.mjs → integrations/usage/opencode-plugin.mjs} +1 -1
  16. package/integrations/usage/skills/at-usage/SKILL.md +16 -0
  17. package/{plugins → integrations}/vision/mcp-server.mjs +4 -4
  18. package/package.json +8 -12
  19. package/scripts/build.mjs +65 -0
  20. package/scripts/install.mjs +135 -90
  21. package/scripts/release.mjs +75 -5
  22. package/hooks/claude/.gitkeep +0 -1
  23. package/hooks/codex/.gitkeep +0 -1
  24. package/hooks/common/.gitkeep +0 -1
  25. package/hooks/opencode/.gitkeep +0 -1
  26. package/scripts/build-vision.mjs +0 -35
  27. package/statusline/.gitkeep +0 -1
  28. package/statusline/codex/.gitkeep +0 -1
  29. /package/{plugins/opencode/usage-tui.mjs → integrations/usage/opencode-tui.mjs} +0 -0
  30. /package/{lib/vision → integrations/vision/lib}/cli.mjs +0 -0
  31. /package/{lib/vision → integrations/vision/lib}/config.mjs +0 -0
  32. /package/{lib/vision → integrations/vision/lib}/errors.mjs +0 -0
  33. /package/{lib/vision → integrations/vision/lib}/image-source.mjs +0 -0
  34. /package/{lib/vision → integrations/vision/lib}/inspect.mjs +0 -0
  35. /package/{lib/vision → integrations/vision/lib}/providers/anthropic-compatible.mjs +0 -0
  36. /package/{lib/vision → integrations/vision/lib}/providers/openai-compatible.mjs +0 -0
  37. /package/{lib/vision → integrations/vision/lib}/providers/shared.mjs +0 -0
  38. /package/{lib/vision → integrations/vision/lib}/rate-limit.mjs +0 -0
  39. /package/{lib/vision → integrations/vision/lib}/redact.mjs +0 -0
  40. /package/{plugins → integrations}/vision/skills/at-vision/SKILL.md +0 -0
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // agent-tools installer: wires statusline / usage into each agent's config.
3
- // Skills are NOT handled here — install those with `npx skills add`.
3
+ // Runtime-dependent skills are installed with their capability. Standalone
4
+ // workflow/integration skills are still handled by `npx skills add`.
4
5
  //
5
6
  // Capabilities (all global for now — they target the user-level config):
6
7
  // statusline Claude Code statusLine script (claude only).
7
- // usage Active API provider quota/balance (codex + opencode).
8
+ // usage Active API provider quota/balance (all agents).
8
9
  // vision inspect_image MCP server + at-vision skill (all agents).
9
10
  //
10
11
  // Standalone commands (dispatched before capability parsing):
@@ -13,8 +14,10 @@
13
14
  //
14
15
  // Targets:
15
16
  // claude -> ~/.claude/settings.json (statusLine key)
17
+ // + ~/.claude/skills/at-usage
16
18
  // ~/.claude.json (vision MCP) + ~/.claude/skills (at-vision)
17
19
  // codex -> ~/.codex/hooks.json (standalone hooks file)
20
+ // + ~/.agents/skills/at-usage
18
21
  // ~/.codex/config.toml (vision MCP) + ~/.agents/skills (at-vision)
19
22
  // opencode -> ~/.config/opencode/ (server + TUI plugins,
20
23
  // vision MCP in opencode.json + skills/at-vision)
@@ -53,35 +56,41 @@ const INSTALL_ROOT =
53
56
  process.env.AGENT_TOOLS_HOME || path.join(os.homedir(), ".agent-tools");
54
57
  const META_KEY = "_agentTools";
55
58
  const META_VERSION = 1;
59
+ // Everything copied into ~/.agent-tools is built output from dist/ (see
60
+ // scripts/build.mjs); integrations/ holds the sources.
56
61
  const SOURCE = {
57
- codexUsageHook: path.join(REPO_ROOT, "hooks", "codex", "usage-hook.mjs"),
58
- usageScript: path.join(REPO_ROOT, "lib", "usage.mjs"),
62
+ codexUsageHook: path.join(REPO_ROOT, "dist", "usage", "codex-hook.mjs"),
63
+ usageScript: path.join(REPO_ROOT, "dist", "usage", "core.mjs"),
64
+ usageCli: path.join(REPO_ROOT, "dist", "usage", "cli.mjs"),
59
65
  config: path.join(REPO_ROOT, "config.default.jsonc"),
60
- claudeStatusline: path.join(REPO_ROOT, "statusline", "claude", "statusline.mjs"),
61
- opencodeUsagePlugin: path.join(REPO_ROOT, "plugins", "opencode", "usage-plugin.mjs"),
62
- opencodeUsageTui: path.join(REPO_ROOT, "plugins", "opencode", "usage-tui.mjs"),
66
+ claudeStatusline: path.join(REPO_ROOT, "dist", "statusline", "claude-statusline.mjs"),
67
+ opencodeUsagePlugin: path.join(REPO_ROOT, "dist", "usage", "opencode-plugin.mjs"),
68
+ opencodeUsageTui: path.join(REPO_ROOT, "dist", "usage", "opencode-tui.mjs"),
63
69
  };
64
70
  const RUNTIME = {
65
- codexUsageHook: path.join(INSTALL_ROOT, "hooks", "codex", "usage-hook.mjs"),
66
- usageScript: path.join(INSTALL_ROOT, "lib", "usage.mjs"),
71
+ codexUsageHook: path.join(INSTALL_ROOT, "dist", "usage", "codex-hook.mjs"),
72
+ usageScript: path.join(INSTALL_ROOT, "dist", "usage", "core.mjs"),
73
+ usageCli: path.join(INSTALL_ROOT, "dist", "usage", "cli.mjs"),
67
74
  config: path.join(INSTALL_ROOT, "config.jsonc"),
68
- claudeStatusline: path.join(INSTALL_ROOT, "statusline", "claude", "statusline.mjs"),
69
- opencodeUsagePlugin: path.join(INSTALL_ROOT, "plugins", "opencode", "usage-plugin.mjs"),
70
- opencodeUsageTui: path.join(INSTALL_ROOT, "plugins", "opencode", "usage-tui.mjs"),
75
+ claudeStatusline: path.join(INSTALL_ROOT, "dist", "statusline", "claude-statusline.mjs"),
76
+ opencodeUsagePlugin: path.join(INSTALL_ROOT, "dist", "usage", "opencode-plugin.mjs"),
77
+ opencodeUsageTui: path.join(INSTALL_ROOT, "dist", "usage", "opencode-tui.mjs"),
71
78
  };
72
79
  const ALL_CAPS = ["statusline", "usage", "vision"];
73
80
  const ALL_AGENTS = ["claude", "codex", "opencode"];
74
81
  const AGENT_CAPS = {
75
- claude: ["statusline", "vision"],
82
+ claude: ["statusline", "usage", "vision"],
76
83
  codex: ["usage", "vision"],
77
84
  opencode: ["usage", "vision"],
78
85
  };
79
86
  const VISION_MCP_NAME = "agent-tools-vision";
80
87
  const VISION_SKILL_NAME = "at-vision";
81
- // The at-vision skill ships inside the vision feature dir (plugins/vision),
88
+ // The at-vision skill ships inside the vision capability dir (integrations/vision),
82
89
  // not skills/: it is unusable without the MCP server, so it must not surface
83
90
  // as an independently installable skill.
84
- const VISION_SKILL_SRC = path.join(REPO_ROOT, "plugins", "vision", "skills", VISION_SKILL_NAME);
91
+ const VISION_SKILL_SRC = path.join(REPO_ROOT, "integrations", "vision", "skills", VISION_SKILL_NAME);
92
+ const USAGE_SKILL_NAME = "at-usage";
93
+ const USAGE_SKILL_SRC = path.join(REPO_ROOT, "integrations", "usage", "skills", USAGE_SKILL_NAME);
85
94
  const VISION_BUNDLED_MCP_SERVER = path.join(REPO_ROOT, "dist", "vision", "mcp-server.mjs");
86
95
  const VISION_BUNDLED_CLI = path.join(REPO_ROOT, "dist", "vision", "cli.mjs");
87
96
 
@@ -130,7 +139,11 @@ function stripJsonComments(input) {
130
139
  function readJsonc(file) {
131
140
  if (!fs.existsSync(file)) return {};
132
141
  const raw = fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "");
133
- return raw.trim() ? JSON.parse(stripJsonComments(raw)) : {};
142
+ if (!raw.trim()) return {};
143
+ const errors = [];
144
+ const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
145
+ if (errors.length > 0) throw new Error(`Cannot parse ${file} as JSONC`);
146
+ return parsed ?? {};
134
147
  }
135
148
 
136
149
  function headerComments(text) {
@@ -173,7 +186,7 @@ function isOpenCodeUsageTuiEntry(entry) {
173
186
  const spec = Array.isArray(entry) ? entry[0] : entry;
174
187
  return (
175
188
  typeof spec === "string" &&
176
- /\/plugins\/opencode\/usage-tui\.mjs$/i.test(spec.replace(/\\/g, "/"))
189
+ /\/dist\/usage\/opencode-tui\.mjs$/i.test(spec.replace(/\\/g, "/"))
177
190
  );
178
191
  }
179
192
 
@@ -254,9 +267,15 @@ function installRuntimeAssets(opts) {
254
267
  addFile(SOURCE.opencodeUsageTui, RUNTIME.opencodeUsageTui);
255
268
  }
256
269
  addFile(SOURCE.usageScript, RUNTIME.usageScript);
270
+ addFile(SOURCE.usageCli, RUNTIME.usageCli);
257
271
  addFile(SOURCE.config, RUNTIME.config, { mergeJsonc: true });
258
272
  }
259
273
  if (files.length === 0) return;
274
+ for (const [src] of files) {
275
+ if (!fs.existsSync(src)) {
276
+ throw new Error(`Missing built artifact ${src}. Run npm run build.`);
277
+ }
278
+ }
260
279
  console.log(`runtime: ${INSTALL_ROOT}`);
261
280
  for (const [src, dest, options] of files) copyRuntimeFile(src, dest, opts.dryRun, options);
262
281
  }
@@ -264,7 +283,7 @@ function installRuntimeAssets(opts) {
264
283
  function parseArgs(argv) {
265
284
  const opts = {
266
285
  agents: [],
267
- capabilities: [],
286
+ integrations: [],
268
287
  settings: null,
269
288
  codexHooks: null,
270
289
  opencodeConfigDir: null,
@@ -315,15 +334,15 @@ function parseArgs(argv) {
315
334
  console.error(`Unknown option: ${a}`);
316
335
  process.exit(2);
317
336
  }
318
- opts.capabilities.push(a);
337
+ opts.integrations.push(a);
319
338
  }
320
339
  }
321
340
  if (opts.agents.length === 0) opts.agents = ["claude"];
322
- if (!opts.help && opts.capabilities.length === 0) {
341
+ if (!opts.help && opts.integrations.length === 0) {
323
342
  console.error(`Missing capability (available: ${ALL_CAPS.join(", ")})`);
324
343
  process.exit(2);
325
344
  }
326
- for (const name of opts.capabilities) {
345
+ for (const name of opts.integrations) {
327
346
  if (!ALL_CAPS.includes(name)) {
328
347
  console.error(`Unknown capability: ${name} (available: ${ALL_CAPS.join(", ")})`);
329
348
  process.exit(2);
@@ -333,14 +352,14 @@ function parseArgs(argv) {
333
352
  }
334
353
 
335
354
  function wants(opts, cap) {
336
- return opts.capabilities.length === 0 || opts.capabilities.includes(cap);
355
+ return opts.integrations.length === 0 || opts.integrations.includes(cap);
337
356
  }
338
357
 
339
358
  function validateAgentCapabilities(opts) {
340
359
  const invalid = [];
341
360
  for (const agent of opts.agents) {
342
361
  const supported = AGENT_CAPS[agent] || [];
343
- for (const cap of opts.capabilities) {
362
+ for (const cap of opts.integrations) {
344
363
  if (!supported.includes(cap)) invalid.push(`${cap} -a ${agent}`);
345
364
  }
346
365
  }
@@ -351,7 +370,6 @@ function validateAgentCapabilities(opts) {
351
370
  for (const agent of ALL_AGENTS) {
352
371
  console.error(` ${agent}: ${AGENT_CAPS[agent].join(", ")}`);
353
372
  }
354
- console.error("Claude API usage is refreshed by the statusline capability; use `statusline -a claude`.");
355
373
  process.exit(2);
356
374
  }
357
375
 
@@ -409,7 +427,7 @@ function isOurProviderUsageEntry(entry) {
409
427
  entry.hooks.some(
410
428
  (h) =>
411
429
  typeof h?.command === "string" &&
412
- /(?:^|[/\\])(?:usage-hook|usage)\.mjs(?:["\s]|$)/.test(h.command)
430
+ /(?:^|[/\\])codex-hook\.mjs(?:["\s]|$)/.test(h.command)
413
431
  )
414
432
  );
415
433
  }
@@ -434,25 +452,26 @@ function applyProviderUsage(cfg, { remove }) {
434
452
 
435
453
  // ---- Vision capability helpers. ----
436
454
 
437
- const VISION_RUNTIME_DIR = path.join(INSTALL_ROOT, "vision-runtime");
455
+ // The installed tree mirrors the package tree, so the bundled runtime lands in
456
+ // dist/vision just like in the package.
457
+ const VISION_RUNTIME_DIR = path.join(INSTALL_ROOT, "dist", "vision");
438
458
  const VISION_RATE_LIMIT_STATE = path.join(INSTALL_ROOT, "cache", "vision-rate-limit.json");
439
459
  const VISION_DIST_DIR = path.join(REPO_ROOT, "dist", "vision");
440
460
  const VISION_RUNTIME_SERVER = path.join(VISION_RUNTIME_DIR, "mcp-server.mjs");
441
461
  const VISION_RUNTIME_CLI = path.join(VISION_RUNTIME_DIR, "cli.mjs");
442
- const VISION_LEGACY_RUNTIME_SERVER = path.join(
443
- VISION_RUNTIME_DIR,
444
- "plugins",
445
- "vision",
446
- "mcp-server.mjs"
447
- );
448
- const VISION_SKILL_MARKER = ".agent-tools-managed.json";
462
+ const SKILL_MARKER = ".agent-tools-managed.json";
449
463
  const VISION_SKILL_MARKER_DATA = Object.freeze({
450
464
  owner: "@kairyou/agent-tools",
451
465
  capability: "vision",
452
466
  artifact: "skill",
453
467
  });
468
+ const USAGE_SKILL_MARKER_DATA = Object.freeze({
469
+ owner: "@kairyou/agent-tools",
470
+ capability: "usage",
471
+ artifact: "skill",
472
+ });
454
473
  // Vision is bundled at release time. Install atomically swaps two self-contained
455
- // entry files into ~/.agent-tools/vision-runtime, so hosts never run npm and a
474
+ // entry files into ~/.agent-tools/dist/vision, so hosts never run npm and a
456
475
  // failed update cannot destroy the previously working runtime.
457
476
  function visionMcpCommand() {
458
477
  return { command: "node", args: [fwd(VISION_RUNTIME_SERVER)] };
@@ -472,14 +491,17 @@ function sameFilePath(left, right) {
472
491
  return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
473
492
  }
474
493
 
494
+ function isVisionServerPath(value) {
495
+ return sameFilePath(value, VISION_RUNTIME_SERVER);
496
+ }
497
+
475
498
  function isClaudeVisionMcp(value, mcp = visionMcpCommand()) {
476
499
  return Boolean(
477
500
  value &&
478
501
  value.command === mcp.command &&
479
502
  Array.isArray(value.args) &&
480
503
  value.args.length === 1 &&
481
- (sameFilePath(value.args[0], mcp.args[0]) ||
482
- sameFilePath(value.args[0], VISION_LEGACY_RUNTIME_SERVER))
504
+ isVisionServerPath(value.args[0])
483
505
  );
484
506
  }
485
507
 
@@ -490,8 +512,7 @@ function isOpenCodeVisionMcp(value, mcp = visionMcpCommand()) {
490
512
  Array.isArray(value.command) &&
491
513
  value.command.length === 2 &&
492
514
  value.command[0] === mcp.command &&
493
- (sameFilePath(value.command[1], mcp.args[0]) ||
494
- sameFilePath(value.command[1], VISION_LEGACY_RUNTIME_SERVER))
515
+ isVisionServerPath(value.command[1])
495
516
  );
496
517
  }
497
518
 
@@ -504,7 +525,7 @@ function installVisionRuntime(opts) {
504
525
  }
505
526
  for (const name of ["mcp-server.mjs", "cli.mjs"]) {
506
527
  if (!fs.existsSync(path.join(VISION_DIST_DIR, name))) {
507
- throw new Error(`Missing bundled vision runtime ${path.join(VISION_DIST_DIR, name)}. Run npm run build:vision.`);
528
+ throw new Error(`Missing bundled vision runtime ${path.join(VISION_DIST_DIR, name)}. Run npm run build.`);
508
529
  }
509
530
  }
510
531
 
@@ -551,37 +572,26 @@ function installVisionRuntime(opts) {
551
572
  }
552
573
  }
553
574
 
554
- function isManagedVisionSkillDir(dest) {
555
- const marker = path.join(dest, VISION_SKILL_MARKER);
575
+ function isManagedSkillDir(dest, markerData) {
576
+ const marker = path.join(dest, SKILL_MARKER);
556
577
  if (!fs.existsSync(marker)) return false;
557
578
  try {
558
579
  const value = JSON.parse(fs.readFileSync(marker, "utf8"));
559
- return Object.entries(VISION_SKILL_MARKER_DATA).every(([key, expected]) => value?.[key] === expected);
580
+ return Object.entries(markerData).every(([key, expected]) => value?.[key] === expected);
560
581
  } catch {
561
582
  return false;
562
583
  }
563
584
  }
564
585
 
565
- function isLegacyManagedVisionSkillDir(dest) {
566
- try {
567
- const entries = fs.readdirSync(dest);
568
- if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
569
- const skill = fs.readFileSync(path.join(dest, "SKILL.md"), "utf8");
570
- return (
571
- /^---\s*[\s\S]*?^name:\s*at-vision\s*$/m.test(skill) &&
572
- skill.includes("# Visual Reasoning Policy") &&
573
- skill.includes("`inspect_image` MCP tool (server `agent-tools-vision`)")
574
- );
575
- } catch {
576
- return false;
577
- }
578
- }
579
-
580
- function installSkillDir(skillsRoot, { remove, dryRun }) {
581
- const dest = path.join(skillsRoot, VISION_SKILL_NAME);
586
+ function installManagedSkillDir(
587
+ skillsRoot,
588
+ { name, source, markerData, replacements, remove, dryRun }
589
+ ) {
590
+ const dest = path.join(skillsRoot, name);
591
+ const isManaged = () => isManagedSkillDir(dest, markerData);
582
592
  if (remove) {
583
593
  if (!fs.existsSync(dest)) return;
584
- if (!isManagedVisionSkillDir(dest)) {
594
+ if (!isManaged()) {
585
595
  console.log(` kept unmanaged ${dest}`);
586
596
  return;
587
597
  }
@@ -593,38 +603,61 @@ function installSkillDir(skillsRoot, { remove, dryRun }) {
593
603
  console.log(` removed ${dest}`);
594
604
  return;
595
605
  }
596
- if (fs.existsSync(dest) && !isManagedVisionSkillDir(dest)) {
597
- if (isLegacyManagedVisionSkillDir(dest)) {
598
- console.log(` adopting legacy managed skill ${dest}`);
599
- } else {
600
- console.error(
601
- ` Refusing to overwrite existing unowned skill directory ${dest}. ` +
602
- `Move or remove it, then re-run the install.`
603
- );
604
- process.exit(1);
605
- }
606
+ if (fs.existsSync(dest) && !isManaged()) {
607
+ console.error(
608
+ ` Refusing to overwrite existing unowned skill directory ${dest}. ` +
609
+ `Move or remove it, then re-run the install.`
610
+ );
611
+ process.exit(1);
606
612
  }
607
613
  if (dryRun) {
608
- console.log(` [dry-run] would copy ${VISION_SKILL_SRC} -> ${dest}`);
614
+ console.log(` [dry-run] would copy ${source} -> ${dest}`);
609
615
  return;
610
616
  }
611
617
  fs.rmSync(dest, { recursive: true, force: true });
612
- fs.cpSync(VISION_SKILL_SRC, dest, { recursive: true });
618
+ fs.cpSync(source, dest, { recursive: true });
613
619
  const skillFile = path.join(dest, "SKILL.md");
614
- const skill = fs.readFileSync(skillFile, "utf8");
615
- const token = "{{VISION_CLI_PATH}}";
616
- if (!skill.includes(token)) {
617
- fs.rmSync(dest, { recursive: true, force: true });
618
- throw new Error(`Vision skill template is missing ${token}`);
620
+ let skill = fs.readFileSync(skillFile, "utf8");
621
+ for (const [token, value] of Object.entries(replacements)) {
622
+ if (!skill.includes(token)) {
623
+ fs.rmSync(dest, { recursive: true, force: true });
624
+ throw new Error(`${name} skill template is missing ${token}`);
625
+ }
626
+ skill = skill.replaceAll(token, value);
619
627
  }
620
- fs.writeFileSync(skillFile, skill.replaceAll(token, fwd(VISION_RUNTIME_CLI)));
628
+ fs.writeFileSync(skillFile, skill);
621
629
  fs.writeFileSync(
622
- path.join(dest, VISION_SKILL_MARKER),
623
- JSON.stringify(VISION_SKILL_MARKER_DATA, null, 2) + "\n"
630
+ path.join(dest, SKILL_MARKER),
631
+ JSON.stringify(markerData, null, 2) + "\n"
624
632
  );
625
633
  console.log(` wrote ${dest}`);
626
634
  }
627
635
 
636
+ function installVisionSkillDir(skillsRoot, { remove, dryRun }) {
637
+ installManagedSkillDir(skillsRoot, {
638
+ name: VISION_SKILL_NAME,
639
+ source: VISION_SKILL_SRC,
640
+ markerData: VISION_SKILL_MARKER_DATA,
641
+ replacements: { "{{VISION_CLI_PATH}}": fwd(VISION_RUNTIME_CLI) },
642
+ remove,
643
+ dryRun,
644
+ });
645
+ }
646
+
647
+ function installUsageSkillDir(skillsRoot, agent, { remove, dryRun }) {
648
+ installManagedSkillDir(skillsRoot, {
649
+ name: USAGE_SKILL_NAME,
650
+ source: USAGE_SKILL_SRC,
651
+ markerData: USAGE_SKILL_MARKER_DATA,
652
+ replacements: {
653
+ "{{USAGE_CLI_PATH}}": fwd(RUNTIME.usageCli),
654
+ "{{USAGE_AGENT}}": agent,
655
+ },
656
+ remove,
657
+ dryRun,
658
+ });
659
+ }
660
+
628
661
  const CODEX_VISION_BEGIN = "# >>> agent-tools vision >>>";
629
662
  const CODEX_VISION_END = "# <<< agent-tools vision <<<";
630
663
 
@@ -691,7 +724,7 @@ function runClaudeVision(opts) {
691
724
  } else {
692
725
  console.log(` no ${VISION_MCP_NAME} MCP entry found; nothing to remove.`);
693
726
  }
694
- installSkillDir(skillsDir, { remove: true, dryRun: opts.dryRun });
727
+ installVisionSkillDir(skillsDir, { remove: true, dryRun: opts.dryRun });
695
728
  console.log(" - vision");
696
729
  return;
697
730
  }
@@ -704,12 +737,21 @@ function runClaudeVision(opts) {
704
737
  cfg.mcpServers = cfg.mcpServers || {};
705
738
  cfg.mcpServers[VISION_MCP_NAME] = { type: "stdio", command: mcp.command, args: mcp.args };
706
739
  writeJson(claudeJson, cfg, opts.dryRun);
707
- installSkillDir(skillsDir, { remove: false, dryRun: opts.dryRun });
740
+ installVisionSkillDir(skillsDir, { remove: false, dryRun: opts.dryRun });
708
741
  console.log(" + vision (inspect_image MCP + at-vision skill)");
709
742
  }
710
743
 
711
744
  function runClaude(opts) {
712
745
  if (wants(opts, "vision")) runClaudeVision(opts);
746
+ if (wants(opts, "usage")) {
747
+ const skillsDir = opts.claudeSkillsDir || path.join(os.homedir(), ".claude", "skills");
748
+ console.log(`claude usage: ${skillsDir}`);
749
+ installUsageSkillDir(skillsDir, "claude", {
750
+ remove: opts.uninstall,
751
+ dryRun: opts.dryRun,
752
+ });
753
+ console.log(opts.uninstall ? " - usage skill" : " + usage skill (at-usage)");
754
+ }
713
755
  if (!wants(opts, "statusline")) return;
714
756
  const settings = opts.settings || path.join(os.homedir(), ".claude", "settings.json");
715
757
  console.log(`claude (global): ${settings}`);
@@ -758,7 +800,7 @@ function runCodexVision(opts) {
758
800
  dryRun: opts.dryRun,
759
801
  mcp: visionMcpCommand(),
760
802
  });
761
- installSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
803
+ installVisionSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
762
804
  console.log(opts.uninstall ? " - vision" : " + vision (inspect_image MCP + at-vision skill)");
763
805
  }
764
806
 
@@ -767,6 +809,12 @@ function runCodex(opts) {
767
809
  if (!wants(opts, "usage")) {
768
810
  return;
769
811
  }
812
+ const skillsDir = opts.codexSkillsDir || path.join(os.homedir(), ".agents", "skills");
813
+ console.log(`codex usage skill: ${skillsDir}`);
814
+ installUsageSkillDir(skillsDir, "codex", {
815
+ remove: opts.uninstall,
816
+ dryRun: opts.dryRun,
817
+ });
770
818
  const file = opts.codexHooks || path.join(os.homedir(), ".codex", "hooks.json");
771
819
  console.log(`codex (global): ${file}`);
772
820
  const cfg = readJson(file);
@@ -854,7 +902,7 @@ function runOpencodeVision(opts) {
854
902
  dryRun: opts.dryRun,
855
903
  mcp: visionMcpCommand(),
856
904
  });
857
- installSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
905
+ installVisionSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
858
906
  console.log(opts.uninstall ? " - vision" : " + vision (inspect_image MCP + at-vision skill)");
859
907
  }
860
908
 
@@ -905,10 +953,7 @@ function visionRuntimeHasRemainingReference(opts) {
905
953
  const end = text.indexOf(CODEX_VISION_END, start + CODEX_VISION_BEGIN.length);
906
954
  if (start !== -1 && end !== -1) {
907
955
  const block = text.slice(start, end);
908
- if (
909
- block.includes(fwd(VISION_RUNTIME_SERVER)) ||
910
- block.includes(fwd(VISION_LEGACY_RUNTIME_SERVER))
911
- ) {
956
+ if (block.includes(fwd(VISION_RUNTIME_SERVER))) {
912
957
  return true;
913
958
  }
914
959
  }
@@ -948,7 +993,7 @@ function cleanupVisionRuntimeIfUnused(opts) {
948
993
  try {
949
994
  fs.rmdirSync(path.dirname(VISION_RATE_LIMIT_STATE));
950
995
  } catch {
951
- // The shared cache directory may contain state for other capabilities.
996
+ // The shared cache directory may contain state for other integrations.
952
997
  }
953
998
  }
954
999
  console.log(` removed unused vision runtime ${VISION_RUNTIME_DIR}`);
@@ -985,7 +1030,7 @@ function main() {
985
1030
  }
986
1031
 
987
1032
  // Standalone vision commands dispatch before capability parsing so image
988
- // paths and questions are never mistaken for capabilities.
1033
+ // paths and questions are never mistaken for integrations.
989
1034
  const subcommand = process.argv[2];
990
1035
  if (subcommand === "inspect-image") {
991
1036
  const { runInspectImageCli } = await import(
@@ -1,19 +1,55 @@
1
1
  #!/usr/bin/env node
2
- // Release helper for @kairyou/agent-tools.
2
+ // Release helper for this package.
3
3
  //
4
4
  // Usage:
5
5
  // npm run release [-- minor|major] # default: patch
6
6
  // npm run release -- --dry-run # npm test + preview tarball, no bump/publish
7
7
  //
8
- // Steps: clean-tree check -> npm test -> npm version -> npm publish -> git push --follow-tags
8
+ // Steps: clean-tree check -> npm auth check -> npm test -> npm version -> npm publish -> git push
9
9
  //
10
- // Release when scripts/ lib/ plugins/ hooks/ statusline/ config.default.jsonc or
10
+ // Release when scripts/ integrations/ config.default.jsonc or
11
11
  // package.json deps change — npx users only get these from the published package.
12
12
  // (skills/ ships from GitHub via `npx skills add`; no release needed.)
13
13
 
14
14
  import { spawnSync } from "node:child_process";
15
+ import { readFileSync } from "node:fs";
15
16
 
16
17
  const BUMPS = ["patch", "minor", "major"];
18
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
19
+ const PACKAGE_NAME = packageJson.name;
20
+
21
+ if (typeof PACKAGE_NAME !== "string" || PACKAGE_NAME.trim() === "") {
22
+ console.error("package.json must define a non-empty name before releasing.");
23
+ process.exit(2);
24
+ }
25
+
26
+ function readNpmConfig(key) {
27
+ const npmExecPath = process.env.npm_execpath;
28
+ const result = npmExecPath
29
+ ? spawnSync(process.execPath, [npmExecPath, "config", "get", key], { encoding: "utf8" })
30
+ : spawnSync("npm", ["config", "get", key], { encoding: "utf8", shell: true });
31
+
32
+ if (result.status !== 0) {
33
+ const detail = result.stderr?.trim() || result.error?.message || "npm config exited unexpectedly";
34
+ console.error(`Unable to read npm config ${key}:\n${detail}`);
35
+ process.exit(result.status ?? 2);
36
+ }
37
+
38
+ const value = result.stdout?.trim();
39
+ return value && value !== "undefined" && value !== "null" ? value : undefined;
40
+ }
41
+
42
+ const packageScope = PACKAGE_NAME.match(/^(@[^/]+)\//)?.[1];
43
+ const REGISTRY = (packageScope && readNpmConfig(`${packageScope}:registry`)) || readNpmConfig("registry");
44
+
45
+ try {
46
+ const registryUrl = new URL(REGISTRY);
47
+ if (!["http:", "https:"].includes(registryUrl.protocol)) throw new Error("unsupported protocol");
48
+ } catch {
49
+ console.error("npm registry config must be a valid HTTP(S) URL before releasing.");
50
+ process.exit(2);
51
+ }
52
+
17
53
  let bump = "patch";
18
54
  let dryRun = false;
19
55
  for (const arg of process.argv.slice(2)) {
@@ -26,20 +62,40 @@ for (const arg of process.argv.slice(2)) {
26
62
  }
27
63
 
28
64
  function run(command, opts = {}) {
65
+ const { onFailure, ...spawnOptions } = opts;
29
66
  console.log(`\n> ${command}`);
30
- const result = spawnSync(command, { stdio: "inherit", shell: true, ...opts });
67
+ const result = spawnSync(command, { stdio: "inherit", shell: true, ...spawnOptions });
31
68
  if (result.status !== 0) {
32
69
  console.error(`\nAborted: \`${command}\` exited with ${result.status}.`);
70
+ onFailure?.();
33
71
  process.exit(result.status ?? 1);
34
72
  }
35
73
  }
36
74
 
75
+ function printCommands(message, commands) {
76
+ console.error(`\n${message}\n`);
77
+ for (const command of commands) console.error(` ${command}`);
78
+ }
79
+
37
80
  const status = spawnSync("git status --porcelain", { shell: true, encoding: "utf8" });
38
81
  if (!dryRun && status.stdout.trim() !== "") {
39
82
  console.error("Working tree is not clean — commit or stash first:\n" + status.stdout);
40
83
  process.exit(1);
41
84
  }
42
85
 
86
+ if (!dryRun) {
87
+ // Fail before npm version creates a commit and tag when the saved token is stale.
88
+ run(`npm whoami --registry=${REGISTRY}`, {
89
+ onFailure() {
90
+ printCommands("npm authentication is missing or expired. Log in, verify the account, then retry:", [
91
+ `npm login --registry=${REGISTRY}`,
92
+ `npm whoami --registry=${REGISTRY}`,
93
+ `npm run release -- ${bump}`,
94
+ ]);
95
+ },
96
+ });
97
+ }
98
+
43
99
  run("npm test");
44
100
 
45
101
  if (dryRun) {
@@ -50,7 +106,21 @@ if (dryRun) {
50
106
  }
51
107
 
52
108
  run(`npm version ${bump}`);
53
- run("npm publish");
109
+ run("npm publish", {
110
+ onFailure() {
111
+ printCommands(
112
+ "npm publish failed after the version commit and tag were created. Do not run the release command again. Fix authentication/permissions, then resume:",
113
+ [
114
+ `npm login --registry=${REGISTRY}`,
115
+ `npm whoami --registry=${REGISTRY}`,
116
+ `npm owner ls ${PACKAGE_NAME}`,
117
+ "npm publish",
118
+ `npm view ${PACKAGE_NAME} version`,
119
+ "git push --follow-tags",
120
+ ],
121
+ );
122
+ },
123
+ });
54
124
  run("git push --follow-tags");
55
125
 
56
126
  console.log("\nRelease complete.");
@@ -1 +0,0 @@
1
-
@@ -1 +0,0 @@
1
-
@@ -1 +0,0 @@
1
-
@@ -1 +0,0 @@
1
-
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { build } from "esbuild";
7
-
8
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
- const OUT_DIR = path.join(ROOT, "dist", "vision");
10
- const STAGE_DIR = path.join(ROOT, "dist", `.vision-build-${process.pid}-${Date.now()}`);
11
-
12
- try {
13
- await build({
14
- entryPoints: {
15
- "mcp-server": path.join(ROOT, "plugins", "vision", "mcp-server.mjs"),
16
- cli: path.join(ROOT, "lib", "vision", "cli.mjs"),
17
- },
18
- outdir: STAGE_DIR,
19
- bundle: true,
20
- platform: "node",
21
- format: "esm",
22
- target: "node22",
23
- mainFields: ["module", "main"],
24
- outExtension: { ".js": ".mjs" },
25
- packages: "bundle",
26
- sourcemap: false,
27
- legalComments: "none",
28
- });
29
-
30
- fs.rmSync(OUT_DIR, { recursive: true, force: true });
31
- fs.renameSync(STAGE_DIR, OUT_DIR);
32
- console.log(`built ${path.relative(ROOT, OUT_DIR)}`);
33
- } finally {
34
- fs.rmSync(STAGE_DIR, { recursive: true, force: true });
35
- }
@@ -1 +0,0 @@
1
-
@@ -1 +0,0 @@
1
-
File without changes