@kairyou/agent-tools 0.2.0 → 0.3.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 (33) hide show
  1. package/README.md +75 -95
  2. package/README.zh-CN.md +71 -92
  3. package/dist/vision/cli.mjs +13 -13
  4. package/dist/vision/mcp-server.mjs +12 -12
  5. package/{statusline/claude/statusline.mjs → integrations/statusline/claude-statusline.mjs} +3 -1
  6. package/integrations/usage/cli.mjs +27 -0
  7. package/{hooks/codex/usage-hook.mjs → integrations/usage/codex-hook.mjs} +1 -1
  8. package/{lib/usage.mjs → integrations/usage/core.mjs} +4 -0
  9. package/{plugins/opencode/usage-plugin.mjs → integrations/usage/opencode-plugin.mjs} +1 -1
  10. package/integrations/usage/skills/at-usage/SKILL.md +16 -0
  11. package/{plugins → integrations}/vision/mcp-server.mjs +4 -4
  12. package/package.json +5 -9
  13. package/scripts/build-vision.mjs +2 -2
  14. package/scripts/install.mjs +122 -88
  15. package/scripts/release.mjs +75 -5
  16. package/hooks/claude/.gitkeep +0 -1
  17. package/hooks/codex/.gitkeep +0 -1
  18. package/hooks/common/.gitkeep +0 -1
  19. package/hooks/opencode/.gitkeep +0 -1
  20. package/statusline/.gitkeep +0 -1
  21. package/statusline/codex/.gitkeep +0 -1
  22. /package/{plugins/opencode/usage-tui.mjs → integrations/usage/opencode-tui.mjs} +0 -0
  23. /package/{lib/vision → integrations/vision/lib}/cli.mjs +0 -0
  24. /package/{lib/vision → integrations/vision/lib}/config.mjs +0 -0
  25. /package/{lib/vision → integrations/vision/lib}/errors.mjs +0 -0
  26. /package/{lib/vision → integrations/vision/lib}/image-source.mjs +0 -0
  27. /package/{lib/vision → integrations/vision/lib}/inspect.mjs +0 -0
  28. /package/{lib/vision → integrations/vision/lib}/providers/anthropic-compatible.mjs +0 -0
  29. /package/{lib/vision → integrations/vision/lib}/providers/openai-compatible.mjs +0 -0
  30. /package/{lib/vision → integrations/vision/lib}/providers/shared.mjs +0 -0
  31. /package/{lib/vision → integrations/vision/lib}/rate-limit.mjs +0 -0
  32. /package/{lib/vision → integrations/vision/lib}/redact.mjs +0 -0
  33. /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)
@@ -54,34 +57,38 @@ const INSTALL_ROOT =
54
57
  const META_KEY = "_agentTools";
55
58
  const META_VERSION = 1;
56
59
  const SOURCE = {
57
- codexUsageHook: path.join(REPO_ROOT, "hooks", "codex", "usage-hook.mjs"),
58
- usageScript: path.join(REPO_ROOT, "lib", "usage.mjs"),
60
+ codexUsageHook: path.join(REPO_ROOT, "integrations", "usage", "codex-hook.mjs"),
61
+ usageScript: path.join(REPO_ROOT, "integrations", "usage", "core.mjs"),
62
+ usageCli: path.join(REPO_ROOT, "integrations", "usage", "cli.mjs"),
59
63
  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"),
64
+ claudeStatusline: path.join(REPO_ROOT, "integrations", "statusline", "claude-statusline.mjs"),
65
+ opencodeUsagePlugin: path.join(REPO_ROOT, "integrations", "usage", "opencode-plugin.mjs"),
66
+ opencodeUsageTui: path.join(REPO_ROOT, "integrations", "usage", "opencode-tui.mjs"),
63
67
  };
64
68
  const RUNTIME = {
65
- codexUsageHook: path.join(INSTALL_ROOT, "hooks", "codex", "usage-hook.mjs"),
66
- usageScript: path.join(INSTALL_ROOT, "lib", "usage.mjs"),
69
+ codexUsageHook: path.join(INSTALL_ROOT, "integrations", "usage", "codex-hook.mjs"),
70
+ usageScript: path.join(INSTALL_ROOT, "integrations", "usage", "core.mjs"),
71
+ usageCli: path.join(INSTALL_ROOT, "integrations", "usage", "cli.mjs"),
67
72
  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"),
73
+ claudeStatusline: path.join(INSTALL_ROOT, "integrations", "statusline", "claude-statusline.mjs"),
74
+ opencodeUsagePlugin: path.join(INSTALL_ROOT, "integrations", "usage", "opencode-plugin.mjs"),
75
+ opencodeUsageTui: path.join(INSTALL_ROOT, "integrations", "usage", "opencode-tui.mjs"),
71
76
  };
72
77
  const ALL_CAPS = ["statusline", "usage", "vision"];
73
78
  const ALL_AGENTS = ["claude", "codex", "opencode"];
74
79
  const AGENT_CAPS = {
75
- claude: ["statusline", "vision"],
80
+ claude: ["statusline", "usage", "vision"],
76
81
  codex: ["usage", "vision"],
77
82
  opencode: ["usage", "vision"],
78
83
  };
79
84
  const VISION_MCP_NAME = "agent-tools-vision";
80
85
  const VISION_SKILL_NAME = "at-vision";
81
- // The at-vision skill ships inside the vision feature dir (plugins/vision),
86
+ // The at-vision skill ships inside the vision capability dir (integrations/vision),
82
87
  // not skills/: it is unusable without the MCP server, so it must not surface
83
88
  // as an independently installable skill.
84
- const VISION_SKILL_SRC = path.join(REPO_ROOT, "plugins", "vision", "skills", VISION_SKILL_NAME);
89
+ const VISION_SKILL_SRC = path.join(REPO_ROOT, "integrations", "vision", "skills", VISION_SKILL_NAME);
90
+ const USAGE_SKILL_NAME = "at-usage";
91
+ const USAGE_SKILL_SRC = path.join(REPO_ROOT, "integrations", "usage", "skills", USAGE_SKILL_NAME);
85
92
  const VISION_BUNDLED_MCP_SERVER = path.join(REPO_ROOT, "dist", "vision", "mcp-server.mjs");
86
93
  const VISION_BUNDLED_CLI = path.join(REPO_ROOT, "dist", "vision", "cli.mjs");
87
94
 
@@ -173,7 +180,7 @@ function isOpenCodeUsageTuiEntry(entry) {
173
180
  const spec = Array.isArray(entry) ? entry[0] : entry;
174
181
  return (
175
182
  typeof spec === "string" &&
176
- /\/plugins\/opencode\/usage-tui\.mjs$/i.test(spec.replace(/\\/g, "/"))
183
+ /\/integrations\/usage\/opencode-tui\.mjs$/i.test(spec.replace(/\\/g, "/"))
177
184
  );
178
185
  }
179
186
 
@@ -254,6 +261,7 @@ function installRuntimeAssets(opts) {
254
261
  addFile(SOURCE.opencodeUsageTui, RUNTIME.opencodeUsageTui);
255
262
  }
256
263
  addFile(SOURCE.usageScript, RUNTIME.usageScript);
264
+ addFile(SOURCE.usageCli, RUNTIME.usageCli);
257
265
  addFile(SOURCE.config, RUNTIME.config, { mergeJsonc: true });
258
266
  }
259
267
  if (files.length === 0) return;
@@ -264,7 +272,7 @@ function installRuntimeAssets(opts) {
264
272
  function parseArgs(argv) {
265
273
  const opts = {
266
274
  agents: [],
267
- capabilities: [],
275
+ integrations: [],
268
276
  settings: null,
269
277
  codexHooks: null,
270
278
  opencodeConfigDir: null,
@@ -315,15 +323,15 @@ function parseArgs(argv) {
315
323
  console.error(`Unknown option: ${a}`);
316
324
  process.exit(2);
317
325
  }
318
- opts.capabilities.push(a);
326
+ opts.integrations.push(a);
319
327
  }
320
328
  }
321
329
  if (opts.agents.length === 0) opts.agents = ["claude"];
322
- if (!opts.help && opts.capabilities.length === 0) {
330
+ if (!opts.help && opts.integrations.length === 0) {
323
331
  console.error(`Missing capability (available: ${ALL_CAPS.join(", ")})`);
324
332
  process.exit(2);
325
333
  }
326
- for (const name of opts.capabilities) {
334
+ for (const name of opts.integrations) {
327
335
  if (!ALL_CAPS.includes(name)) {
328
336
  console.error(`Unknown capability: ${name} (available: ${ALL_CAPS.join(", ")})`);
329
337
  process.exit(2);
@@ -333,14 +341,14 @@ function parseArgs(argv) {
333
341
  }
334
342
 
335
343
  function wants(opts, cap) {
336
- return opts.capabilities.length === 0 || opts.capabilities.includes(cap);
344
+ return opts.integrations.length === 0 || opts.integrations.includes(cap);
337
345
  }
338
346
 
339
347
  function validateAgentCapabilities(opts) {
340
348
  const invalid = [];
341
349
  for (const agent of opts.agents) {
342
350
  const supported = AGENT_CAPS[agent] || [];
343
- for (const cap of opts.capabilities) {
351
+ for (const cap of opts.integrations) {
344
352
  if (!supported.includes(cap)) invalid.push(`${cap} -a ${agent}`);
345
353
  }
346
354
  }
@@ -351,7 +359,6 @@ function validateAgentCapabilities(opts) {
351
359
  for (const agent of ALL_AGENTS) {
352
360
  console.error(` ${agent}: ${AGENT_CAPS[agent].join(", ")}`);
353
361
  }
354
- console.error("Claude API usage is refreshed by the statusline capability; use `statusline -a claude`.");
355
362
  process.exit(2);
356
363
  }
357
364
 
@@ -409,7 +416,7 @@ function isOurProviderUsageEntry(entry) {
409
416
  entry.hooks.some(
410
417
  (h) =>
411
418
  typeof h?.command === "string" &&
412
- /(?:^|[/\\])(?:usage-hook|usage)\.mjs(?:["\s]|$)/.test(h.command)
419
+ /(?:^|[/\\])codex-hook\.mjs(?:["\s]|$)/.test(h.command)
413
420
  )
414
421
  );
415
422
  }
@@ -434,25 +441,26 @@ function applyProviderUsage(cfg, { remove }) {
434
441
 
435
442
  // ---- Vision capability helpers. ----
436
443
 
437
- const VISION_RUNTIME_DIR = path.join(INSTALL_ROOT, "vision-runtime");
444
+ // The installed tree mirrors the package tree, so the bundled runtime lands in
445
+ // dist/vision just like in the package.
446
+ const VISION_RUNTIME_DIR = path.join(INSTALL_ROOT, "dist", "vision");
438
447
  const VISION_RATE_LIMIT_STATE = path.join(INSTALL_ROOT, "cache", "vision-rate-limit.json");
439
448
  const VISION_DIST_DIR = path.join(REPO_ROOT, "dist", "vision");
440
449
  const VISION_RUNTIME_SERVER = path.join(VISION_RUNTIME_DIR, "mcp-server.mjs");
441
450
  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";
451
+ const SKILL_MARKER = ".agent-tools-managed.json";
449
452
  const VISION_SKILL_MARKER_DATA = Object.freeze({
450
453
  owner: "@kairyou/agent-tools",
451
454
  capability: "vision",
452
455
  artifact: "skill",
453
456
  });
457
+ const USAGE_SKILL_MARKER_DATA = Object.freeze({
458
+ owner: "@kairyou/agent-tools",
459
+ capability: "usage",
460
+ artifact: "skill",
461
+ });
454
462
  // 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
463
+ // entry files into ~/.agent-tools/dist/vision, so hosts never run npm and a
456
464
  // failed update cannot destroy the previously working runtime.
457
465
  function visionMcpCommand() {
458
466
  return { command: "node", args: [fwd(VISION_RUNTIME_SERVER)] };
@@ -472,14 +480,17 @@ function sameFilePath(left, right) {
472
480
  return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
473
481
  }
474
482
 
483
+ function isVisionServerPath(value) {
484
+ return sameFilePath(value, VISION_RUNTIME_SERVER);
485
+ }
486
+
475
487
  function isClaudeVisionMcp(value, mcp = visionMcpCommand()) {
476
488
  return Boolean(
477
489
  value &&
478
490
  value.command === mcp.command &&
479
491
  Array.isArray(value.args) &&
480
492
  value.args.length === 1 &&
481
- (sameFilePath(value.args[0], mcp.args[0]) ||
482
- sameFilePath(value.args[0], VISION_LEGACY_RUNTIME_SERVER))
493
+ isVisionServerPath(value.args[0])
483
494
  );
484
495
  }
485
496
 
@@ -490,8 +501,7 @@ function isOpenCodeVisionMcp(value, mcp = visionMcpCommand()) {
490
501
  Array.isArray(value.command) &&
491
502
  value.command.length === 2 &&
492
503
  value.command[0] === mcp.command &&
493
- (sameFilePath(value.command[1], mcp.args[0]) ||
494
- sameFilePath(value.command[1], VISION_LEGACY_RUNTIME_SERVER))
504
+ isVisionServerPath(value.command[1])
495
505
  );
496
506
  }
497
507
 
@@ -551,37 +561,26 @@ function installVisionRuntime(opts) {
551
561
  }
552
562
  }
553
563
 
554
- function isManagedVisionSkillDir(dest) {
555
- const marker = path.join(dest, VISION_SKILL_MARKER);
564
+ function isManagedSkillDir(dest, markerData) {
565
+ const marker = path.join(dest, SKILL_MARKER);
556
566
  if (!fs.existsSync(marker)) return false;
557
567
  try {
558
568
  const value = JSON.parse(fs.readFileSync(marker, "utf8"));
559
- return Object.entries(VISION_SKILL_MARKER_DATA).every(([key, expected]) => value?.[key] === expected);
560
- } catch {
561
- return false;
562
- }
563
- }
564
-
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
- );
569
+ return Object.entries(markerData).every(([key, expected]) => value?.[key] === expected);
575
570
  } catch {
576
571
  return false;
577
572
  }
578
573
  }
579
574
 
580
- function installSkillDir(skillsRoot, { remove, dryRun }) {
581
- const dest = path.join(skillsRoot, VISION_SKILL_NAME);
575
+ function installManagedSkillDir(
576
+ skillsRoot,
577
+ { name, source, markerData, replacements, remove, dryRun }
578
+ ) {
579
+ const dest = path.join(skillsRoot, name);
580
+ const isManaged = () => isManagedSkillDir(dest, markerData);
582
581
  if (remove) {
583
582
  if (!fs.existsSync(dest)) return;
584
- if (!isManagedVisionSkillDir(dest)) {
583
+ if (!isManaged()) {
585
584
  console.log(` kept unmanaged ${dest}`);
586
585
  return;
587
586
  }
@@ -593,38 +592,61 @@ function installSkillDir(skillsRoot, { remove, dryRun }) {
593
592
  console.log(` removed ${dest}`);
594
593
  return;
595
594
  }
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
- }
595
+ if (fs.existsSync(dest) && !isManaged()) {
596
+ console.error(
597
+ ` Refusing to overwrite existing unowned skill directory ${dest}. ` +
598
+ `Move or remove it, then re-run the install.`
599
+ );
600
+ process.exit(1);
606
601
  }
607
602
  if (dryRun) {
608
- console.log(` [dry-run] would copy ${VISION_SKILL_SRC} -> ${dest}`);
603
+ console.log(` [dry-run] would copy ${source} -> ${dest}`);
609
604
  return;
610
605
  }
611
606
  fs.rmSync(dest, { recursive: true, force: true });
612
- fs.cpSync(VISION_SKILL_SRC, dest, { recursive: true });
607
+ fs.cpSync(source, dest, { recursive: true });
613
608
  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}`);
609
+ let skill = fs.readFileSync(skillFile, "utf8");
610
+ for (const [token, value] of Object.entries(replacements)) {
611
+ if (!skill.includes(token)) {
612
+ fs.rmSync(dest, { recursive: true, force: true });
613
+ throw new Error(`${name} skill template is missing ${token}`);
614
+ }
615
+ skill = skill.replaceAll(token, value);
619
616
  }
620
- fs.writeFileSync(skillFile, skill.replaceAll(token, fwd(VISION_RUNTIME_CLI)));
617
+ fs.writeFileSync(skillFile, skill);
621
618
  fs.writeFileSync(
622
- path.join(dest, VISION_SKILL_MARKER),
623
- JSON.stringify(VISION_SKILL_MARKER_DATA, null, 2) + "\n"
619
+ path.join(dest, SKILL_MARKER),
620
+ JSON.stringify(markerData, null, 2) + "\n"
624
621
  );
625
622
  console.log(` wrote ${dest}`);
626
623
  }
627
624
 
625
+ function installVisionSkillDir(skillsRoot, { remove, dryRun }) {
626
+ installManagedSkillDir(skillsRoot, {
627
+ name: VISION_SKILL_NAME,
628
+ source: VISION_SKILL_SRC,
629
+ markerData: VISION_SKILL_MARKER_DATA,
630
+ replacements: { "{{VISION_CLI_PATH}}": fwd(VISION_RUNTIME_CLI) },
631
+ remove,
632
+ dryRun,
633
+ });
634
+ }
635
+
636
+ function installUsageSkillDir(skillsRoot, agent, { remove, dryRun }) {
637
+ installManagedSkillDir(skillsRoot, {
638
+ name: USAGE_SKILL_NAME,
639
+ source: USAGE_SKILL_SRC,
640
+ markerData: USAGE_SKILL_MARKER_DATA,
641
+ replacements: {
642
+ "{{USAGE_CLI_PATH}}": fwd(RUNTIME.usageCli),
643
+ "{{USAGE_AGENT}}": agent,
644
+ },
645
+ remove,
646
+ dryRun,
647
+ });
648
+ }
649
+
628
650
  const CODEX_VISION_BEGIN = "# >>> agent-tools vision >>>";
629
651
  const CODEX_VISION_END = "# <<< agent-tools vision <<<";
630
652
 
@@ -691,7 +713,7 @@ function runClaudeVision(opts) {
691
713
  } else {
692
714
  console.log(` no ${VISION_MCP_NAME} MCP entry found; nothing to remove.`);
693
715
  }
694
- installSkillDir(skillsDir, { remove: true, dryRun: opts.dryRun });
716
+ installVisionSkillDir(skillsDir, { remove: true, dryRun: opts.dryRun });
695
717
  console.log(" - vision");
696
718
  return;
697
719
  }
@@ -704,12 +726,21 @@ function runClaudeVision(opts) {
704
726
  cfg.mcpServers = cfg.mcpServers || {};
705
727
  cfg.mcpServers[VISION_MCP_NAME] = { type: "stdio", command: mcp.command, args: mcp.args };
706
728
  writeJson(claudeJson, cfg, opts.dryRun);
707
- installSkillDir(skillsDir, { remove: false, dryRun: opts.dryRun });
729
+ installVisionSkillDir(skillsDir, { remove: false, dryRun: opts.dryRun });
708
730
  console.log(" + vision (inspect_image MCP + at-vision skill)");
709
731
  }
710
732
 
711
733
  function runClaude(opts) {
712
734
  if (wants(opts, "vision")) runClaudeVision(opts);
735
+ if (wants(opts, "usage")) {
736
+ const skillsDir = opts.claudeSkillsDir || path.join(os.homedir(), ".claude", "skills");
737
+ console.log(`claude usage: ${skillsDir}`);
738
+ installUsageSkillDir(skillsDir, "claude", {
739
+ remove: opts.uninstall,
740
+ dryRun: opts.dryRun,
741
+ });
742
+ console.log(opts.uninstall ? " - usage skill" : " + usage skill (at-usage)");
743
+ }
713
744
  if (!wants(opts, "statusline")) return;
714
745
  const settings = opts.settings || path.join(os.homedir(), ".claude", "settings.json");
715
746
  console.log(`claude (global): ${settings}`);
@@ -758,7 +789,7 @@ function runCodexVision(opts) {
758
789
  dryRun: opts.dryRun,
759
790
  mcp: visionMcpCommand(),
760
791
  });
761
- installSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
792
+ installVisionSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
762
793
  console.log(opts.uninstall ? " - vision" : " + vision (inspect_image MCP + at-vision skill)");
763
794
  }
764
795
 
@@ -767,6 +798,12 @@ function runCodex(opts) {
767
798
  if (!wants(opts, "usage")) {
768
799
  return;
769
800
  }
801
+ const skillsDir = opts.codexSkillsDir || path.join(os.homedir(), ".agents", "skills");
802
+ console.log(`codex usage skill: ${skillsDir}`);
803
+ installUsageSkillDir(skillsDir, "codex", {
804
+ remove: opts.uninstall,
805
+ dryRun: opts.dryRun,
806
+ });
770
807
  const file = opts.codexHooks || path.join(os.homedir(), ".codex", "hooks.json");
771
808
  console.log(`codex (global): ${file}`);
772
809
  const cfg = readJson(file);
@@ -854,7 +891,7 @@ function runOpencodeVision(opts) {
854
891
  dryRun: opts.dryRun,
855
892
  mcp: visionMcpCommand(),
856
893
  });
857
- installSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
894
+ installVisionSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
858
895
  console.log(opts.uninstall ? " - vision" : " + vision (inspect_image MCP + at-vision skill)");
859
896
  }
860
897
 
@@ -905,10 +942,7 @@ function visionRuntimeHasRemainingReference(opts) {
905
942
  const end = text.indexOf(CODEX_VISION_END, start + CODEX_VISION_BEGIN.length);
906
943
  if (start !== -1 && end !== -1) {
907
944
  const block = text.slice(start, end);
908
- if (
909
- block.includes(fwd(VISION_RUNTIME_SERVER)) ||
910
- block.includes(fwd(VISION_LEGACY_RUNTIME_SERVER))
911
- ) {
945
+ if (block.includes(fwd(VISION_RUNTIME_SERVER))) {
912
946
  return true;
913
947
  }
914
948
  }
@@ -948,7 +982,7 @@ function cleanupVisionRuntimeIfUnused(opts) {
948
982
  try {
949
983
  fs.rmdirSync(path.dirname(VISION_RATE_LIMIT_STATE));
950
984
  } catch {
951
- // The shared cache directory may contain state for other capabilities.
985
+ // The shared cache directory may contain state for other integrations.
952
986
  }
953
987
  }
954
988
  console.log(` removed unused vision runtime ${VISION_RUNTIME_DIR}`);
@@ -985,7 +1019,7 @@ function main() {
985
1019
  }
986
1020
 
987
1021
  // Standalone vision commands dispatch before capability parsing so image
988
- // paths and questions are never mistaken for capabilities.
1022
+ // paths and questions are never mistaken for integrations.
989
1023
  const subcommand = process.argv[2];
990
1024
  if (subcommand === "inspect-image") {
991
1025
  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 +0,0 @@
1
-
@@ -1 +0,0 @@
1
-
File without changes