@kairyou/agent-tools 0.1.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.
- package/README.md +147 -69
- package/README.zh-CN.md +143 -67
- package/dist/vision/cli.mjs +1972 -0
- package/dist/vision/mcp-server.mjs +32858 -0
- package/{statusline/claude/statusline.mjs → integrations/statusline/claude-statusline.mjs} +4 -2
- package/integrations/usage/cli.mjs +27 -0
- package/{hooks/codex/usage-hook.mjs → integrations/usage/codex-hook.mjs} +1 -1
- package/{lib/usage.mjs → integrations/usage/core.mjs} +5 -1
- package/{plugins/opencode/usage-plugin.mjs → integrations/usage/opencode-plugin.mjs} +1 -1
- package/integrations/usage/skills/at-usage/SKILL.md +16 -0
- package/integrations/vision/lib/cli.mjs +137 -0
- package/integrations/vision/lib/config.mjs +159 -0
- package/integrations/vision/lib/errors.mjs +35 -0
- package/integrations/vision/lib/image-source.mjs +273 -0
- package/integrations/vision/lib/inspect.mjs +108 -0
- package/integrations/vision/lib/providers/anthropic-compatible.mjs +59 -0
- package/integrations/vision/lib/providers/openai-compatible.mjs +56 -0
- package/integrations/vision/lib/providers/shared.mjs +251 -0
- package/integrations/vision/lib/rate-limit.mjs +188 -0
- package/integrations/vision/lib/redact.mjs +30 -0
- package/integrations/vision/mcp-server.mjs +96 -0
- package/integrations/vision/skills/at-vision/SKILL.md +66 -0
- package/package.json +15 -10
- package/scripts/build-vision.mjs +35 -0
- package/scripts/capture-codex-tools.mjs +48 -0
- package/scripts/install.mjs +511 -29
- package/scripts/release.mjs +126 -0
- package/skills/integrations/at-zentao/SKILL.md +148 -0
- package/skills/workflow/at-commit/SKILL.md +3 -8
- package/skills/workflow/at-review/SKILL.md +9 -4
- package/skills/workflow/at-simplify/SKILL.md +1 -0
- package/hooks/claude/.gitkeep +0 -1
- package/hooks/codex/.gitkeep +0 -1
- package/hooks/common/.gitkeep +0 -1
- package/hooks/opencode/.gitkeep +0 -1
- package/statusline/.gitkeep +0 -1
- package/statusline/codex/.gitkeep +0 -1
- /package/{plugins/opencode/usage-tui.mjs → integrations/usage/opencode-tui.mjs} +0 -0
package/scripts/install.mjs
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// agent-tools installer: wires statusline / usage into each agent's config.
|
|
3
|
-
//
|
|
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 (
|
|
8
|
+
// usage Active API provider quota/balance (all agents).
|
|
9
|
+
// vision inspect_image MCP server + at-vision skill (all agents).
|
|
10
|
+
//
|
|
11
|
+
// Standalone commands (dispatched before capability parsing):
|
|
12
|
+
// inspect-image <path|url> --question "..." Human diagnostic for vision.
|
|
13
|
+
// mcp-vision Run the vision MCP stdio server.
|
|
8
14
|
//
|
|
9
15
|
// Targets:
|
|
10
16
|
// claude -> ~/.claude/settings.json (statusLine key)
|
|
17
|
+
// + ~/.claude/skills/at-usage
|
|
18
|
+
// ~/.claude.json (vision MCP) + ~/.claude/skills (at-vision)
|
|
11
19
|
// codex -> ~/.codex/hooks.json (standalone hooks file)
|
|
12
|
-
//
|
|
20
|
+
// + ~/.agents/skills/at-usage
|
|
21
|
+
// ~/.codex/config.toml (vision MCP) + ~/.agents/skills (at-vision)
|
|
22
|
+
// opencode -> ~/.config/opencode/ (server + TUI plugins,
|
|
23
|
+
// vision MCP in opencode.json + skills/at-vision)
|
|
13
24
|
// Runtime scripts are copied into ~/.agent-tools so this installer can be
|
|
14
25
|
// run via npx from GitHub without requiring a persistent local clone.
|
|
15
26
|
//
|
|
@@ -25,10 +36,15 @@
|
|
|
25
36
|
// --settings <path> Override the Claude settings.json (for testing).
|
|
26
37
|
// --codex-hooks <path> Override the Codex hooks.json (for testing).
|
|
27
38
|
// --opencode-config-dir <p> Override the opencode config dir (for testing).
|
|
39
|
+
// --claude-json <path> Override ~/.claude.json for vision MCP (for testing).
|
|
40
|
+
// --claude-skills-dir <p> Override ~/.claude/skills (for testing).
|
|
41
|
+
// --codex-config <path> Override ~/.codex/config.toml (for testing).
|
|
42
|
+
// --codex-skills-dir <p> Override ~/.agents/skills (for testing).
|
|
28
43
|
// --uninstall Remove what this installer added, restoring backups.
|
|
29
44
|
// --dry-run Print planned changes without writing anything.
|
|
30
45
|
// -h, --help Show this help.
|
|
31
46
|
|
|
47
|
+
import { spawnSync } from "node:child_process";
|
|
32
48
|
import fs from "node:fs";
|
|
33
49
|
import os from "node:os";
|
|
34
50
|
import path from "node:path";
|
|
@@ -41,28 +57,40 @@ const INSTALL_ROOT =
|
|
|
41
57
|
const META_KEY = "_agentTools";
|
|
42
58
|
const META_VERSION = 1;
|
|
43
59
|
const SOURCE = {
|
|
44
|
-
codexUsageHook: path.join(REPO_ROOT, "
|
|
45
|
-
usageScript: path.join(REPO_ROOT, "
|
|
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"),
|
|
46
63
|
config: path.join(REPO_ROOT, "config.default.jsonc"),
|
|
47
|
-
claudeStatusline: path.join(REPO_ROOT, "
|
|
48
|
-
opencodeUsagePlugin: path.join(REPO_ROOT, "
|
|
49
|
-
opencodeUsageTui: path.join(REPO_ROOT, "
|
|
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"),
|
|
50
67
|
};
|
|
51
68
|
const RUNTIME = {
|
|
52
|
-
codexUsageHook: path.join(INSTALL_ROOT, "
|
|
53
|
-
usageScript: path.join(INSTALL_ROOT, "
|
|
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"),
|
|
54
72
|
config: path.join(INSTALL_ROOT, "config.jsonc"),
|
|
55
|
-
claudeStatusline: path.join(INSTALL_ROOT, "
|
|
56
|
-
opencodeUsagePlugin: path.join(INSTALL_ROOT, "
|
|
57
|
-
opencodeUsageTui: path.join(INSTALL_ROOT, "
|
|
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"),
|
|
58
76
|
};
|
|
59
|
-
const ALL_CAPS = ["statusline", "usage"];
|
|
77
|
+
const ALL_CAPS = ["statusline", "usage", "vision"];
|
|
60
78
|
const ALL_AGENTS = ["claude", "codex", "opencode"];
|
|
61
79
|
const AGENT_CAPS = {
|
|
62
|
-
claude: ["statusline"],
|
|
63
|
-
codex: ["usage"],
|
|
64
|
-
opencode: ["usage"],
|
|
80
|
+
claude: ["statusline", "usage", "vision"],
|
|
81
|
+
codex: ["usage", "vision"],
|
|
82
|
+
opencode: ["usage", "vision"],
|
|
65
83
|
};
|
|
84
|
+
const VISION_MCP_NAME = "agent-tools-vision";
|
|
85
|
+
const VISION_SKILL_NAME = "at-vision";
|
|
86
|
+
// The at-vision skill ships inside the vision capability dir (integrations/vision),
|
|
87
|
+
// not skills/: it is unusable without the MCP server, so it must not surface
|
|
88
|
+
// as an independently installable skill.
|
|
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);
|
|
92
|
+
const VISION_BUNDLED_MCP_SERVER = path.join(REPO_ROOT, "dist", "vision", "mcp-server.mjs");
|
|
93
|
+
const VISION_BUNDLED_CLI = path.join(REPO_ROOT, "dist", "vision", "cli.mjs");
|
|
66
94
|
|
|
67
95
|
function fwd(p) {
|
|
68
96
|
return p.replace(/\\/g, "/");
|
|
@@ -152,7 +180,7 @@ function isOpenCodeUsageTuiEntry(entry) {
|
|
|
152
180
|
const spec = Array.isArray(entry) ? entry[0] : entry;
|
|
153
181
|
return (
|
|
154
182
|
typeof spec === "string" &&
|
|
155
|
-
/\/
|
|
183
|
+
/\/integrations\/usage\/opencode-tui\.mjs$/i.test(spec.replace(/\\/g, "/"))
|
|
156
184
|
);
|
|
157
185
|
}
|
|
158
186
|
|
|
@@ -233,6 +261,7 @@ function installRuntimeAssets(opts) {
|
|
|
233
261
|
addFile(SOURCE.opencodeUsageTui, RUNTIME.opencodeUsageTui);
|
|
234
262
|
}
|
|
235
263
|
addFile(SOURCE.usageScript, RUNTIME.usageScript);
|
|
264
|
+
addFile(SOURCE.usageCli, RUNTIME.usageCli);
|
|
236
265
|
addFile(SOURCE.config, RUNTIME.config, { mergeJsonc: true });
|
|
237
266
|
}
|
|
238
267
|
if (files.length === 0) return;
|
|
@@ -243,10 +272,14 @@ function installRuntimeAssets(opts) {
|
|
|
243
272
|
function parseArgs(argv) {
|
|
244
273
|
const opts = {
|
|
245
274
|
agents: [],
|
|
246
|
-
|
|
275
|
+
integrations: [],
|
|
247
276
|
settings: null,
|
|
248
277
|
codexHooks: null,
|
|
249
278
|
opencodeConfigDir: null,
|
|
279
|
+
claudeJson: null,
|
|
280
|
+
claudeSkillsDir: null,
|
|
281
|
+
codexConfig: null,
|
|
282
|
+
codexSkillsDir: null,
|
|
250
283
|
uninstall: false,
|
|
251
284
|
dryRun: false,
|
|
252
285
|
help: false,
|
|
@@ -277,6 +310,10 @@ function parseArgs(argv) {
|
|
|
277
310
|
case "--settings": opts.settings = argv[++i]; break;
|
|
278
311
|
case "--codex-hooks": opts.codexHooks = argv[++i]; break;
|
|
279
312
|
case "--opencode-config-dir": opts.opencodeConfigDir = argv[++i]; break;
|
|
313
|
+
case "--claude-json": opts.claudeJson = argv[++i]; break;
|
|
314
|
+
case "--claude-skills-dir": opts.claudeSkillsDir = argv[++i]; break;
|
|
315
|
+
case "--codex-config": opts.codexConfig = argv[++i]; break;
|
|
316
|
+
case "--codex-skills-dir": opts.codexSkillsDir = argv[++i]; break;
|
|
280
317
|
case "--uninstall": opts.uninstall = true; break;
|
|
281
318
|
case "--dry-run": opts.dryRun = true; break;
|
|
282
319
|
case "-h":
|
|
@@ -286,15 +323,15 @@ function parseArgs(argv) {
|
|
|
286
323
|
console.error(`Unknown option: ${a}`);
|
|
287
324
|
process.exit(2);
|
|
288
325
|
}
|
|
289
|
-
opts.
|
|
326
|
+
opts.integrations.push(a);
|
|
290
327
|
}
|
|
291
328
|
}
|
|
292
329
|
if (opts.agents.length === 0) opts.agents = ["claude"];
|
|
293
|
-
if (!opts.help && opts.
|
|
330
|
+
if (!opts.help && opts.integrations.length === 0) {
|
|
294
331
|
console.error(`Missing capability (available: ${ALL_CAPS.join(", ")})`);
|
|
295
332
|
process.exit(2);
|
|
296
333
|
}
|
|
297
|
-
for (const name of opts.
|
|
334
|
+
for (const name of opts.integrations) {
|
|
298
335
|
if (!ALL_CAPS.includes(name)) {
|
|
299
336
|
console.error(`Unknown capability: ${name} (available: ${ALL_CAPS.join(", ")})`);
|
|
300
337
|
process.exit(2);
|
|
@@ -304,14 +341,14 @@ function parseArgs(argv) {
|
|
|
304
341
|
}
|
|
305
342
|
|
|
306
343
|
function wants(opts, cap) {
|
|
307
|
-
return opts.
|
|
344
|
+
return opts.integrations.length === 0 || opts.integrations.includes(cap);
|
|
308
345
|
}
|
|
309
346
|
|
|
310
347
|
function validateAgentCapabilities(opts) {
|
|
311
348
|
const invalid = [];
|
|
312
349
|
for (const agent of opts.agents) {
|
|
313
350
|
const supported = AGENT_CAPS[agent] || [];
|
|
314
|
-
for (const cap of opts.
|
|
351
|
+
for (const cap of opts.integrations) {
|
|
315
352
|
if (!supported.includes(cap)) invalid.push(`${cap} -a ${agent}`);
|
|
316
353
|
}
|
|
317
354
|
}
|
|
@@ -322,7 +359,6 @@ function validateAgentCapabilities(opts) {
|
|
|
322
359
|
for (const agent of ALL_AGENTS) {
|
|
323
360
|
console.error(` ${agent}: ${AGENT_CAPS[agent].join(", ")}`);
|
|
324
361
|
}
|
|
325
|
-
console.error("Claude API usage is refreshed by the statusline capability; use `statusline -a claude`.");
|
|
326
362
|
process.exit(2);
|
|
327
363
|
}
|
|
328
364
|
|
|
@@ -380,7 +416,7 @@ function isOurProviderUsageEntry(entry) {
|
|
|
380
416
|
entry.hooks.some(
|
|
381
417
|
(h) =>
|
|
382
418
|
typeof h?.command === "string" &&
|
|
383
|
-
/(?:^|[/\\])
|
|
419
|
+
/(?:^|[/\\])codex-hook\.mjs(?:["\s]|$)/.test(h.command)
|
|
384
420
|
)
|
|
385
421
|
);
|
|
386
422
|
}
|
|
@@ -403,9 +439,309 @@ function applyProviderUsage(cfg, { remove }) {
|
|
|
403
439
|
if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
404
440
|
}
|
|
405
441
|
|
|
442
|
+
// ---- Vision capability helpers. ----
|
|
443
|
+
|
|
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");
|
|
447
|
+
const VISION_RATE_LIMIT_STATE = path.join(INSTALL_ROOT, "cache", "vision-rate-limit.json");
|
|
448
|
+
const VISION_DIST_DIR = path.join(REPO_ROOT, "dist", "vision");
|
|
449
|
+
const VISION_RUNTIME_SERVER = path.join(VISION_RUNTIME_DIR, "mcp-server.mjs");
|
|
450
|
+
const VISION_RUNTIME_CLI = path.join(VISION_RUNTIME_DIR, "cli.mjs");
|
|
451
|
+
const SKILL_MARKER = ".agent-tools-managed.json";
|
|
452
|
+
const VISION_SKILL_MARKER_DATA = Object.freeze({
|
|
453
|
+
owner: "@kairyou/agent-tools",
|
|
454
|
+
capability: "vision",
|
|
455
|
+
artifact: "skill",
|
|
456
|
+
});
|
|
457
|
+
const USAGE_SKILL_MARKER_DATA = Object.freeze({
|
|
458
|
+
owner: "@kairyou/agent-tools",
|
|
459
|
+
capability: "usage",
|
|
460
|
+
artifact: "skill",
|
|
461
|
+
});
|
|
462
|
+
// Vision is bundled at release time. Install atomically swaps two self-contained
|
|
463
|
+
// entry files into ~/.agent-tools/dist/vision, so hosts never run npm and a
|
|
464
|
+
// failed update cannot destroy the previously working runtime.
|
|
465
|
+
function visionMcpCommand() {
|
|
466
|
+
return { command: "node", args: [fwd(VISION_RUNTIME_SERVER)] };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function sameFilePath(left, right) {
|
|
470
|
+
function canonical(value) {
|
|
471
|
+
const resolved = path.resolve(String(value));
|
|
472
|
+
try {
|
|
473
|
+
return fs.realpathSync.native(resolved);
|
|
474
|
+
} catch {
|
|
475
|
+
return resolved;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const a = canonical(left);
|
|
479
|
+
const b = canonical(right);
|
|
480
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function isVisionServerPath(value) {
|
|
484
|
+
return sameFilePath(value, VISION_RUNTIME_SERVER);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function isClaudeVisionMcp(value, mcp = visionMcpCommand()) {
|
|
488
|
+
return Boolean(
|
|
489
|
+
value &&
|
|
490
|
+
value.command === mcp.command &&
|
|
491
|
+
Array.isArray(value.args) &&
|
|
492
|
+
value.args.length === 1 &&
|
|
493
|
+
isVisionServerPath(value.args[0])
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function isOpenCodeVisionMcp(value, mcp = visionMcpCommand()) {
|
|
498
|
+
return Boolean(
|
|
499
|
+
value &&
|
|
500
|
+
value.type === "local" &&
|
|
501
|
+
Array.isArray(value.command) &&
|
|
502
|
+
value.command.length === 2 &&
|
|
503
|
+
value.command[0] === mcp.command &&
|
|
504
|
+
isVisionServerPath(value.command[1])
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function installVisionRuntime(opts) {
|
|
509
|
+
if (opts.uninstall) return;
|
|
510
|
+
console.log(`vision runtime: ${VISION_RUNTIME_DIR}`);
|
|
511
|
+
if (opts.dryRun) {
|
|
512
|
+
console.log(` [dry-run] would atomically install ${VISION_DIST_DIR} -> ${VISION_RUNTIME_DIR}`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
for (const name of ["mcp-server.mjs", "cli.mjs"]) {
|
|
516
|
+
if (!fs.existsSync(path.join(VISION_DIST_DIR, name))) {
|
|
517
|
+
throw new Error(`Missing bundled vision runtime ${path.join(VISION_DIST_DIR, name)}. Run npm run build:vision.`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fs.mkdirSync(path.dirname(VISION_RUNTIME_DIR), { recursive: true });
|
|
522
|
+
const suffix = `${process.pid}-${Date.now()}`;
|
|
523
|
+
const stage = `${VISION_RUNTIME_DIR}.stage-${suffix}`;
|
|
524
|
+
const backup = `${VISION_RUNTIME_DIR}.backup-${suffix}`;
|
|
525
|
+
let movedCurrent = false;
|
|
526
|
+
try {
|
|
527
|
+
fs.cpSync(VISION_DIST_DIR, stage, { recursive: true });
|
|
528
|
+
writeText(
|
|
529
|
+
path.join(stage, "runtime.json"),
|
|
530
|
+
JSON.stringify({ owner: "@kairyou/agent-tools", capability: "vision", version: 1 }, null, 2) + "\n",
|
|
531
|
+
false
|
|
532
|
+
);
|
|
533
|
+
for (const name of ["mcp-server.mjs", "cli.mjs"]) {
|
|
534
|
+
const check = spawnSync(process.execPath, ["--check", path.join(stage, name)], {
|
|
535
|
+
encoding: "utf8",
|
|
536
|
+
});
|
|
537
|
+
if (check.status !== 0) {
|
|
538
|
+
throw new Error(`Bundled vision runtime failed syntax validation (${name}): ${check.stderr || check.stdout}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (fs.existsSync(VISION_RUNTIME_DIR)) {
|
|
543
|
+
fs.renameSync(VISION_RUNTIME_DIR, backup);
|
|
544
|
+
movedCurrent = true;
|
|
545
|
+
}
|
|
546
|
+
if (process.env.AGENT_TOOLS_VISION_TEST_FAIL_SWAP === "1") {
|
|
547
|
+
throw new Error("Simulated vision runtime swap failure");
|
|
548
|
+
}
|
|
549
|
+
fs.renameSync(stage, VISION_RUNTIME_DIR);
|
|
550
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
551
|
+
console.log(" installed bundled vision runtime");
|
|
552
|
+
} catch (error) {
|
|
553
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
554
|
+
if (movedCurrent && !fs.existsSync(VISION_RUNTIME_DIR) && fs.existsSync(backup)) {
|
|
555
|
+
fs.renameSync(backup, VISION_RUNTIME_DIR);
|
|
556
|
+
}
|
|
557
|
+
throw error;
|
|
558
|
+
} finally {
|
|
559
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
560
|
+
if (fs.existsSync(VISION_RUNTIME_DIR)) fs.rmSync(backup, { recursive: true, force: true });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function isManagedSkillDir(dest, markerData) {
|
|
565
|
+
const marker = path.join(dest, SKILL_MARKER);
|
|
566
|
+
if (!fs.existsSync(marker)) return false;
|
|
567
|
+
try {
|
|
568
|
+
const value = JSON.parse(fs.readFileSync(marker, "utf8"));
|
|
569
|
+
return Object.entries(markerData).every(([key, expected]) => value?.[key] === expected);
|
|
570
|
+
} catch {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
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);
|
|
581
|
+
if (remove) {
|
|
582
|
+
if (!fs.existsSync(dest)) return;
|
|
583
|
+
if (!isManaged()) {
|
|
584
|
+
console.log(` kept unmanaged ${dest}`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (dryRun) {
|
|
588
|
+
console.log(` [dry-run] would remove ${dest}`);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
592
|
+
console.log(` removed ${dest}`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
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);
|
|
601
|
+
}
|
|
602
|
+
if (dryRun) {
|
|
603
|
+
console.log(` [dry-run] would copy ${source} -> ${dest}`);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
607
|
+
fs.cpSync(source, dest, { recursive: true });
|
|
608
|
+
const skillFile = path.join(dest, "SKILL.md");
|
|
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);
|
|
616
|
+
}
|
|
617
|
+
fs.writeFileSync(skillFile, skill);
|
|
618
|
+
fs.writeFileSync(
|
|
619
|
+
path.join(dest, SKILL_MARKER),
|
|
620
|
+
JSON.stringify(markerData, null, 2) + "\n"
|
|
621
|
+
);
|
|
622
|
+
console.log(` wrote ${dest}`);
|
|
623
|
+
}
|
|
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
|
+
|
|
650
|
+
const CODEX_VISION_BEGIN = "# >>> agent-tools vision >>>";
|
|
651
|
+
const CODEX_VISION_END = "# <<< agent-tools vision <<<";
|
|
652
|
+
|
|
653
|
+
// Codex config.toml is user-owned free-form TOML; we manage exactly one
|
|
654
|
+
// marker-delimited block so install/update/uninstall never touch other keys.
|
|
655
|
+
function updateCodexVisionBlock(file, { remove, dryRun, mcp }) {
|
|
656
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
657
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
658
|
+
const blockRe = new RegExp(
|
|
659
|
+
`\\r?\\n?${escape(CODEX_VISION_BEGIN)}[\\s\\S]*?${escape(CODEX_VISION_END)}\\r?\\n?`
|
|
660
|
+
);
|
|
661
|
+
let next = current.replace(blockRe, "\n").replace(/^\n+/, "");
|
|
662
|
+
if (!remove) {
|
|
663
|
+
// A same-named table outside our marker block would make the appended
|
|
664
|
+
// TOML a duplicate declaration and break the ENTIRE config.toml parse.
|
|
665
|
+
const manualRe = new RegExp(`^\\s*\\[mcp_servers\\.["']?${escape(VISION_MCP_NAME)}["']?\\]`, "m");
|
|
666
|
+
if (manualRe.test(next)) {
|
|
667
|
+
console.error(
|
|
668
|
+
` Found an existing [mcp_servers.${VISION_MCP_NAME}] entry in ${file} that was not ` +
|
|
669
|
+
"written by this installer. Remove or rename that entry, then re-run the install."
|
|
670
|
+
);
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
const args = mcp.args.map((a) => JSON.stringify(a)).join(", ");
|
|
674
|
+
const block = [
|
|
675
|
+
CODEX_VISION_BEGIN,
|
|
676
|
+
`[mcp_servers.${VISION_MCP_NAME}]`,
|
|
677
|
+
`command = ${JSON.stringify(mcp.command)}`,
|
|
678
|
+
`args = [${args}]`,
|
|
679
|
+
CODEX_VISION_END,
|
|
680
|
+
"",
|
|
681
|
+
].join("\n");
|
|
682
|
+
next = next.trim() === "" ? block : next.replace(/\s*$/, "\n\n") + block;
|
|
683
|
+
}
|
|
684
|
+
if (next === current) {
|
|
685
|
+
console.log(` kept existing ${file}`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (remove && next.trim() === "" && fs.existsSync(file) && current.trim() !== "") {
|
|
689
|
+
// The file only ever contained our block; leave an empty file rather than
|
|
690
|
+
// deleting config.toml, which Codex may expect to exist.
|
|
691
|
+
writeText(file, "", dryRun);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
writeText(file, next, dryRun);
|
|
695
|
+
}
|
|
696
|
+
|
|
406
697
|
// ---- Claude: statusLine backed up in _agentTools. ----
|
|
407
698
|
|
|
699
|
+
function runClaudeVision(opts) {
|
|
700
|
+
const claudeJson = opts.claudeJson || path.join(os.homedir(), ".claude.json");
|
|
701
|
+
const skillsDir = opts.claudeSkillsDir || path.join(os.homedir(), ".claude", "skills");
|
|
702
|
+
console.log(`claude vision: ${claudeJson}`);
|
|
703
|
+
const cfg = readJson(claudeJson);
|
|
704
|
+
const mcp = visionMcpCommand();
|
|
705
|
+
const existing = cfg.mcpServers?.[VISION_MCP_NAME];
|
|
706
|
+
if (opts.uninstall) {
|
|
707
|
+
if (isClaudeVisionMcp(existing, mcp)) {
|
|
708
|
+
delete cfg.mcpServers[VISION_MCP_NAME];
|
|
709
|
+
if (Object.keys(cfg.mcpServers).length === 0) delete cfg.mcpServers;
|
|
710
|
+
writeJson(claudeJson, cfg, opts.dryRun);
|
|
711
|
+
} else if (existing) {
|
|
712
|
+
console.log(` kept unmanaged ${VISION_MCP_NAME} entry in ${claudeJson}`);
|
|
713
|
+
} else {
|
|
714
|
+
console.log(` no ${VISION_MCP_NAME} MCP entry found; nothing to remove.`);
|
|
715
|
+
}
|
|
716
|
+
installVisionSkillDir(skillsDir, { remove: true, dryRun: opts.dryRun });
|
|
717
|
+
console.log(" - vision");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (existing && !isClaudeVisionMcp(existing, mcp)) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
`Found an existing unmanaged ${VISION_MCP_NAME} entry in ${claudeJson}. ` +
|
|
723
|
+
"Remove or rename it, then re-run the install."
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
727
|
+
cfg.mcpServers[VISION_MCP_NAME] = { type: "stdio", command: mcp.command, args: mcp.args };
|
|
728
|
+
writeJson(claudeJson, cfg, opts.dryRun);
|
|
729
|
+
installVisionSkillDir(skillsDir, { remove: false, dryRun: opts.dryRun });
|
|
730
|
+
console.log(" + vision (inspect_image MCP + at-vision skill)");
|
|
731
|
+
}
|
|
732
|
+
|
|
408
733
|
function runClaude(opts) {
|
|
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
|
+
}
|
|
744
|
+
if (!wants(opts, "statusline")) return;
|
|
409
745
|
const settings = opts.settings || path.join(os.homedir(), ".claude", "settings.json");
|
|
410
746
|
console.log(`claude (global): ${settings}`);
|
|
411
747
|
const cfg = readJson(settings);
|
|
@@ -444,11 +780,30 @@ function runClaude(opts) {
|
|
|
444
780
|
// written (Codex validates hooks.json against a schema); our hook is found by
|
|
445
781
|
// command signature instead. ----
|
|
446
782
|
|
|
783
|
+
function runCodexVision(opts) {
|
|
784
|
+
const file = opts.codexConfig || path.join(os.homedir(), ".codex", "config.toml");
|
|
785
|
+
const skillsDir = opts.codexSkillsDir || path.join(os.homedir(), ".agents", "skills");
|
|
786
|
+
console.log(`codex vision: ${file}`);
|
|
787
|
+
updateCodexVisionBlock(file, {
|
|
788
|
+
remove: opts.uninstall,
|
|
789
|
+
dryRun: opts.dryRun,
|
|
790
|
+
mcp: visionMcpCommand(),
|
|
791
|
+
});
|
|
792
|
+
installVisionSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
|
|
793
|
+
console.log(opts.uninstall ? " - vision" : " + vision (inspect_image MCP + at-vision skill)");
|
|
794
|
+
}
|
|
795
|
+
|
|
447
796
|
function runCodex(opts) {
|
|
797
|
+
if (wants(opts, "vision")) runCodexVision(opts);
|
|
448
798
|
if (!wants(opts, "usage")) {
|
|
449
|
-
console.log("codex: nothing to do (supports: usage).");
|
|
450
799
|
return;
|
|
451
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
|
+
});
|
|
452
807
|
const file = opts.codexHooks || path.join(os.homedir(), ".codex", "hooks.json");
|
|
453
808
|
console.log(`codex (global): ${file}`);
|
|
454
809
|
const cfg = readJson(file);
|
|
@@ -491,9 +846,58 @@ function opencodeConfigDir(opts) {
|
|
|
491
846
|
);
|
|
492
847
|
}
|
|
493
848
|
|
|
849
|
+
// opencode.json may be JSONC with user comments — edit only our key in place.
|
|
850
|
+
function updateOpencodeVisionMcp(file, { remove, dryRun, mcp }) {
|
|
851
|
+
const exists = fs.existsSync(file);
|
|
852
|
+
const currentText = exists ? fs.readFileSync(file, "utf8") : "{}\n";
|
|
853
|
+
const errors = [];
|
|
854
|
+
const current = parseJsonc(currentText, errors, { allowTrailingComma: true }) || {};
|
|
855
|
+
if (errors.length > 0 || typeof current !== "object" || Array.isArray(current)) {
|
|
856
|
+
throw new Error(`Cannot parse ${file} as JSONC`);
|
|
857
|
+
}
|
|
858
|
+
const existing = current.mcp && current.mcp[VISION_MCP_NAME];
|
|
859
|
+
if (existing && !isOpenCodeVisionMcp(existing, mcp)) {
|
|
860
|
+
if (remove) {
|
|
861
|
+
console.log(` kept unmanaged ${VISION_MCP_NAME} entry in ${file}`);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
throw new Error(
|
|
865
|
+
`Found an existing unmanaged ${VISION_MCP_NAME} entry in ${file}. ` +
|
|
866
|
+
"Remove or rename it, then re-run the install."
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const desired = remove
|
|
870
|
+
? undefined
|
|
871
|
+
: { type: "local", command: [mcp.command, ...mcp.args], enabled: true };
|
|
872
|
+
if (JSON.stringify(existing) === JSON.stringify(desired)) {
|
|
873
|
+
console.log(` kept existing ${file}`);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const eol = currentText.includes("\r\n") ? "\r\n" : "\n";
|
|
877
|
+
const edits = modify(currentText, ["mcp", VISION_MCP_NAME], desired, {
|
|
878
|
+
formattingOptions: { insertSpaces: true, tabSize: 2, eol },
|
|
879
|
+
});
|
|
880
|
+
const updated = applyEdits(currentText, edits).replace(/\s*$/, "") + eol;
|
|
881
|
+
writeText(file, updated, dryRun);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function runOpencodeVision(opts) {
|
|
885
|
+
const configDir = opencodeConfigDir(opts);
|
|
886
|
+
const file = path.join(configDir, "opencode.json");
|
|
887
|
+
const skillsDir = path.join(configDir, "skills");
|
|
888
|
+
console.log(`opencode vision: ${file}`);
|
|
889
|
+
updateOpencodeVisionMcp(file, {
|
|
890
|
+
remove: opts.uninstall,
|
|
891
|
+
dryRun: opts.dryRun,
|
|
892
|
+
mcp: visionMcpCommand(),
|
|
893
|
+
});
|
|
894
|
+
installVisionSkillDir(skillsDir, { remove: opts.uninstall, dryRun: opts.dryRun });
|
|
895
|
+
console.log(opts.uninstall ? " - vision" : " + vision (inspect_image MCP + at-vision skill)");
|
|
896
|
+
}
|
|
897
|
+
|
|
494
898
|
function runOpencode(opts) {
|
|
899
|
+
if (wants(opts, "vision")) runOpencodeVision(opts);
|
|
495
900
|
if (!wants(opts, "usage")) {
|
|
496
|
-
console.log("opencode: nothing to do (supports: usage).");
|
|
497
901
|
return;
|
|
498
902
|
}
|
|
499
903
|
|
|
@@ -520,6 +924,70 @@ function runOpencode(opts) {
|
|
|
520
924
|
if (!opts.dryRun) console.log(" NOTE: restart opencode to load the agent-tools usage plugin.");
|
|
521
925
|
}
|
|
522
926
|
|
|
927
|
+
function visionRuntimeHasRemainingReference(opts) {
|
|
928
|
+
const removed = new Set(opts.agents);
|
|
929
|
+
if (!removed.has("claude")) {
|
|
930
|
+
const file = opts.claudeJson || path.join(os.homedir(), ".claude.json");
|
|
931
|
+
try {
|
|
932
|
+
if (isClaudeVisionMcp(readJson(file).mcpServers?.[VISION_MCP_NAME])) return true;
|
|
933
|
+
} catch {
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (!removed.has("codex")) {
|
|
938
|
+
const file = opts.codexConfig || path.join(os.homedir(), ".codex", "config.toml");
|
|
939
|
+
try {
|
|
940
|
+
const text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
941
|
+
const start = text.indexOf(CODEX_VISION_BEGIN);
|
|
942
|
+
const end = text.indexOf(CODEX_VISION_END, start + CODEX_VISION_BEGIN.length);
|
|
943
|
+
if (start !== -1 && end !== -1) {
|
|
944
|
+
const block = text.slice(start, end);
|
|
945
|
+
if (block.includes(fwd(VISION_RUNTIME_SERVER))) {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
} catch {
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (!removed.has("opencode")) {
|
|
954
|
+
const file = path.join(opencodeConfigDir(opts), "opencode.json");
|
|
955
|
+
try {
|
|
956
|
+
if (fs.existsSync(file)) {
|
|
957
|
+
const errors = [];
|
|
958
|
+
const config = parseJsonc(fs.readFileSync(file, "utf8"), errors, { allowTrailingComma: true });
|
|
959
|
+
if (errors.length > 0) return true;
|
|
960
|
+
if (isOpenCodeVisionMcp(config?.mcp?.[VISION_MCP_NAME])) return true;
|
|
961
|
+
}
|
|
962
|
+
} catch {
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function cleanupVisionRuntimeIfUnused(opts) {
|
|
970
|
+
if (!opts.uninstall || !wants(opts, "vision") || !fs.existsSync(VISION_RUNTIME_DIR)) return;
|
|
971
|
+
if (visionRuntimeHasRemainingReference(opts)) {
|
|
972
|
+
console.log(` kept shared vision runtime ${VISION_RUNTIME_DIR} (another agent still references it)`);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (opts.dryRun) {
|
|
976
|
+
console.log(` [dry-run] would remove unused vision runtime ${VISION_RUNTIME_DIR}`);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
fs.rmSync(VISION_RUNTIME_DIR, { recursive: true, force: true });
|
|
980
|
+
if (!fs.existsSync(`${VISION_RATE_LIMIT_STATE}.lock`)) {
|
|
981
|
+
fs.rmSync(VISION_RATE_LIMIT_STATE, { force: true });
|
|
982
|
+
try {
|
|
983
|
+
fs.rmdirSync(path.dirname(VISION_RATE_LIMIT_STATE));
|
|
984
|
+
} catch {
|
|
985
|
+
// The shared cache directory may contain state for other integrations.
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
console.log(` removed unused vision runtime ${VISION_RUNTIME_DIR}`);
|
|
989
|
+
}
|
|
990
|
+
|
|
523
991
|
const AGENTS = { claude: runClaude, codex: runCodex, opencode: runOpencode };
|
|
524
992
|
|
|
525
993
|
function main() {
|
|
@@ -538,6 +1006,7 @@ function main() {
|
|
|
538
1006
|
}
|
|
539
1007
|
validateAgentCapabilities(opts);
|
|
540
1008
|
installRuntimeAssets(opts);
|
|
1009
|
+
if (wants(opts, "vision")) installVisionRuntime(opts);
|
|
541
1010
|
for (const name of opts.agents) {
|
|
542
1011
|
const run = AGENTS[name];
|
|
543
1012
|
if (!run) {
|
|
@@ -546,6 +1015,19 @@ function main() {
|
|
|
546
1015
|
}
|
|
547
1016
|
run(opts);
|
|
548
1017
|
}
|
|
1018
|
+
cleanupVisionRuntimeIfUnused(opts);
|
|
549
1019
|
}
|
|
550
1020
|
|
|
551
|
-
|
|
1021
|
+
// Standalone vision commands dispatch before capability parsing so image
|
|
1022
|
+
// paths and questions are never mistaken for integrations.
|
|
1023
|
+
const subcommand = process.argv[2];
|
|
1024
|
+
if (subcommand === "inspect-image") {
|
|
1025
|
+
const { runInspectImageCli } = await import(
|
|
1026
|
+
pathToFileURL(VISION_BUNDLED_CLI).href
|
|
1027
|
+
);
|
|
1028
|
+
process.exitCode = await runInspectImageCli(process.argv.slice(3));
|
|
1029
|
+
} else if (subcommand === "mcp-vision") {
|
|
1030
|
+
await import(pathToFileURL(VISION_BUNDLED_MCP_SERVER).href);
|
|
1031
|
+
} else {
|
|
1032
|
+
main();
|
|
1033
|
+
}
|