@arp-protocol/cli 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,805 @@
1
+ #!/usr/bin/env node
2
+ import"./cli-g5zvr10v.js";
3
+
4
+ // src/install/commands.ts
5
+ import {
6
+ chmodSync,
7
+ copyFileSync,
8
+ existsSync as existsSync2,
9
+ mkdirSync,
10
+ readFileSync as readFileSync2,
11
+ renameSync,
12
+ rmSync,
13
+ statSync,
14
+ writeFileSync
15
+ } from "node:fs";
16
+ import { dirname, join as join2 } from "node:path";
17
+
18
+ // src/install/targets.ts
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+
22
+ // src/install/edits.ts
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ function parseJsonConfig(text) {
27
+ if (text === null || text.trim().length === 0)
28
+ return {};
29
+ try {
30
+ const parsed = JSON.parse(text);
31
+ return isRecord(parsed) ? parsed : new Error("not a JSON object");
32
+ } catch (error) {
33
+ return error instanceof Error ? error : new Error(String(error));
34
+ }
35
+ }
36
+ function formatJson(value) {
37
+ return `${JSON.stringify(value, null, 2)}
38
+ `;
39
+ }
40
+ function withMcpServer(config, name, entry) {
41
+ const servers = isRecord(config.mcpServers) ? { ...config.mcpServers } : {};
42
+ if (entry === null)
43
+ delete servers[name];
44
+ else
45
+ servers[name] = entry;
46
+ const next = { ...config, mcpServers: servers };
47
+ if (entry === null && Object.keys(servers).length === 0)
48
+ delete next.mcpServers;
49
+ return next;
50
+ }
51
+ var CLAUDE_HOOKS = {
52
+ shape: "nested",
53
+ events: [
54
+ { name: "PostToolUse", matcher: "*" },
55
+ { name: "PostToolUseFailure", matcher: "*" }
56
+ ],
57
+ entry: (command) => ({ type: "command", command, async: true, timeout: 10 })
58
+ };
59
+ function isOurs(hook, marker) {
60
+ return isRecord(hook) && typeof hook.command === "string" && hook.command.includes(marker);
61
+ }
62
+ function strip(list, spec, marker) {
63
+ if (!Array.isArray(list))
64
+ return [];
65
+ if (spec.shape === "flat")
66
+ return list.filter((entry) => !isOurs(entry, marker));
67
+ const kept = [];
68
+ for (const group of list) {
69
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
70
+ kept.push(group);
71
+ continue;
72
+ }
73
+ const rest = group.hooks.filter((hook) => !isOurs(hook, marker));
74
+ if (rest.length > 0)
75
+ kept.push({ ...group, hooks: rest });
76
+ }
77
+ return kept;
78
+ }
79
+ function ours(list, spec, marker) {
80
+ if (!Array.isArray(list))
81
+ return [];
82
+ if (spec.shape === "flat")
83
+ return list.filter((entry) => isOurs(entry, marker));
84
+ return list.flatMap((group) => isRecord(group) && Array.isArray(group.hooks) ? group.hooks.filter((hook) => isOurs(hook, marker)) : []);
85
+ }
86
+ function withHooks(settings, spec, command, marker) {
87
+ const hooks = isRecord(settings.hooks) ? { ...settings.hooks } : {};
88
+ for (const event of spec.events) {
89
+ const kept = strip(hooks[event.name], spec, marker);
90
+ if (command !== null) {
91
+ const entry = spec.entry(command);
92
+ kept.push(spec.shape === "flat" ? entry : { ...event.matcher === undefined ? {} : { matcher: event.matcher }, hooks: [entry] });
93
+ }
94
+ if (kept.length > 0)
95
+ hooks[event.name] = kept;
96
+ else
97
+ delete hooks[event.name];
98
+ }
99
+ const next = { ...settings, hooks };
100
+ if (Object.keys(hooks).length === 0) {
101
+ delete next.hooks;
102
+ if (spec.shape === "flat" && Object.keys(next).length === 1 && "version" in next)
103
+ return {};
104
+ } else if (spec.shape === "flat" && next.version === undefined) {
105
+ return { version: 1, ...next };
106
+ }
107
+ return next;
108
+ }
109
+ function hooksOf(settings, spec, marker) {
110
+ if (!isRecord(settings.hooks))
111
+ return {};
112
+ const out = {};
113
+ for (const event of spec.events) {
114
+ const found = ours(settings.hooks[event.name], spec, marker);
115
+ if (found.length > 0)
116
+ out[event.name] = found;
117
+ }
118
+ return out;
119
+ }
120
+ function hasHooks(settings, spec, marker) {
121
+ const found = hooksOf(settings, spec, marker);
122
+ return spec.events.every((event) => (event.name in found));
123
+ }
124
+ var TOML_BEGIN = "# >>> arp — managed by `arp init`, removed by `arp uninstall` >>>";
125
+ var TOML_END = "# <<< arp <<<";
126
+ function tomlString(value) {
127
+ return JSON.stringify(value);
128
+ }
129
+ function withoutCodexTable(text) {
130
+ let inside = false;
131
+ return text.split(`
132
+ `).filter((line) => {
133
+ if (/^\s*\[\[?[^\]]+\]\]?\s*(#.*)?$/.test(line)) {
134
+ inside = /^\s*\[mcp_servers\.arp(\.[^\]]+)?\]/.test(line);
135
+ }
136
+ return !inside;
137
+ }).join(`
138
+ `);
139
+ }
140
+ function withCodexServer(text, entry, force = false) {
141
+ const current = text ?? "";
142
+ const begin = current.indexOf(TOML_BEGIN);
143
+ const end = current.indexOf(TOML_END);
144
+ const hasBlock = begin !== -1 && end > begin;
145
+ const kept = hasBlock ? current.slice(0, begin) + current.slice(end + TOML_END.length) : current;
146
+ const foreign = /^\s*\[mcp_servers\.arp\]/m.test(kept);
147
+ if (foreign && !force) {
148
+ return new Error("an [mcp_servers.arp] table exists that `arp init` did not write");
149
+ }
150
+ const outside = foreign ? withoutCodexTable(kept) : kept;
151
+ let base = outside.replace(/\n{3,}/g, `
152
+
153
+ `).replace(/\s+$/, "");
154
+ if (entry === null)
155
+ return base.length === 0 ? "" : `${base}
156
+ `;
157
+ const block = [
158
+ TOML_BEGIN,
159
+ "[mcp_servers.arp]",
160
+ `command = ${tomlString(entry.command)}`,
161
+ `args = [${entry.args.map(tomlString).join(", ")}]`,
162
+ `env = { ${Object.entries(entry.env).map(([key, value]) => `${key} = ${tomlString(value)}`).join(", ")} }`,
163
+ TOML_END
164
+ ].join(`
165
+ `);
166
+ base = base.length === 0 ? "" : `${base}
167
+
168
+ `;
169
+ return `${base}${block}
170
+ `;
171
+ }
172
+ function codexBlockOf(text) {
173
+ if (text === null)
174
+ return [];
175
+ const begin = text.indexOf(TOML_BEGIN);
176
+ const end = text.indexOf(TOML_END);
177
+ return begin === -1 || end < begin ? [] : text.slice(begin, end + TOML_END.length).split(`
178
+ `);
179
+ }
180
+ function hasCodexServer(text) {
181
+ return text !== null && text.includes(TOML_BEGIN) && text.includes(TOML_END);
182
+ }
183
+
184
+ // src/install/targets.ts
185
+ var RUNTIME_IDS = [
186
+ "claude",
187
+ "grok",
188
+ "cursor",
189
+ "gemini",
190
+ "codex",
191
+ "copilot"
192
+ ];
193
+ var SERVER_NAME = "arp";
194
+ function namesOurServer(text, cliPath) {
195
+ return text.includes("@arp-protocol/cli") || text.includes(cliPath) || /apps[\\/]cli[\\/]dist[\\/]cli\.js/.test(text) || /[\\/]dist[\\/]cli\.js/.test(text) && text.includes("ARP_TARGET") || /(?:packaging[\\/]claude[\\/]arp|plugins[\\/]cache[\\/]arp[\\/]arp[\\/][^\\/]+)[\\/]bin[\\/]arp\.js/.test(text);
196
+ }
197
+ function foreignServerStep(where) {
198
+ return {
199
+ kind: "skip",
200
+ summary: `MCP server "arp" in ${where} is not one ARP wrote; left as it is`
201
+ };
202
+ }
203
+ function foreignRefusal(where) {
204
+ return {
205
+ kind: "skip",
206
+ failed: true,
207
+ summary: `MCP server "arp" in ${where} is not one ARP wrote; left as it is. Rename it, or run arp init --force to replace it`
208
+ };
209
+ }
210
+ function read(path) {
211
+ try {
212
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+ function npxLaunch(pkg) {
218
+ return {
219
+ command: "sh",
220
+ args: ["-c", `export ARP_PROJECT_DIR="$PWD"; cd / && exec npx --prefix / -y --fetch-retries=0 --fetch-timeout=5000 ${pkg} mcp`]
221
+ };
222
+ }
223
+ var SERVER_ENV = { ARP_TARGET: "testnet" };
224
+ var SPONSORED_ENV = {
225
+ ...SERVER_ENV,
226
+ ARP_TOOLS: "sponsored",
227
+ ARP_SKILLS_DIR: ".claude/skills"
228
+ };
229
+ function commandsFor(env, cliPath, capture = false, sponsored = false, force = false) {
230
+ const hookFile = join(env.home, ".arp", "bin", "hook.js");
231
+ const fromNpx = /[\\/]_npx[\\/]/.test(cliPath);
232
+ const mcp = {
233
+ ...fromNpx ? {
234
+ ...npxLaunch(`@arp-protocol/cli@${env.version}`)
235
+ } : { command: env.node, args: [cliPath, "mcp"] },
236
+ env: sponsored ? SPONSORED_ENV : SERVER_ENV
237
+ };
238
+ const hook = `"${env.node}" "${hookFile}"${capture ? " --capture" : ""}`;
239
+ return { mcp, hook, hookFile, cliPath, capture, force };
240
+ }
241
+ function diffLines(before, after) {
242
+ const lines = (value, sign) => value === undefined || typeof value === "object" && value !== null && Object.keys(value).length === 0 ? [] : JSON.stringify(value, null, 2).split(`
243
+ `).map((line) => `${sign} ${line}`);
244
+ return [...lines(before, "-"), ...lines(after, "+")];
245
+ }
246
+ function serversOf(config) {
247
+ return typeof config.mcpServers === "object" && config.mcpServers !== null && !Array.isArray(config.mcpServers) ? config.mcpServers : {};
248
+ }
249
+ function jsonStep(path, edit, summary, view) {
250
+ const before = read(path);
251
+ const parsed = parseJsonConfig(before);
252
+ if (parsed instanceof Error) {
253
+ return {
254
+ kind: "skip",
255
+ failed: true,
256
+ summary: `${path} is not valid JSON (${parsed.message}); left as it is`
257
+ };
258
+ }
259
+ const next = edit(parsed);
260
+ const after = Object.keys(next).length === 0 ? null : formatJson(next);
261
+ return { kind: "file", path, before, after, summary, diff: diffLines(view(parsed), view(next)) };
262
+ }
263
+ function hasForeignServer(path, commands) {
264
+ const config = parseJsonConfig(read(path));
265
+ if (config instanceof Error)
266
+ return false;
267
+ const entry = serversOf(config)[SERVER_NAME];
268
+ return entry !== undefined && !namesOurServer(JSON.stringify(entry), commands.cliPath);
269
+ }
270
+ function claudeFiles(env, project) {
271
+ const root = project ? env.cwd : env.home;
272
+ return {
273
+ mcp: project ? join(env.cwd, ".mcp.json") : join(env.home, ".claude.json"),
274
+ settings: join(root, ".claude", "settings.json")
275
+ };
276
+ }
277
+ function mcpEntry(server, typed) {
278
+ return typed ? { type: "stdio", command: server.command, args: server.args, env: server.env } : { command: server.command, args: server.args, env: server.env };
279
+ }
280
+ function claudeMcpSteps(env, commands, project, remove, allowCli) {
281
+ const files = claudeFiles(env, project);
282
+ if (allowCli && !project && env.which("claude") !== null) {
283
+ const got = env.run(["claude", "mcp", "get", SERVER_NAME]);
284
+ const present = got.code === 0;
285
+ const same = present && [
286
+ commands.mcp.command,
287
+ ...commands.mcp.args,
288
+ ...Object.entries(commands.mcp.env).map(([key, value]) => `${key}=${value}`)
289
+ ].every((part) => got.stdout.includes(part));
290
+ const removeStep = {
291
+ kind: "command",
292
+ argv: ["claude", "mcp", "remove", SERVER_NAME, "--scope", "user"],
293
+ summary: "claude mcp remove arp --scope user",
294
+ diff: present ? got.stdout.trim().split(`
295
+ `).map((line) => `- ${line.replace(/=\S+/g, "=…")}`) : []
296
+ };
297
+ const entry = mcpEntry(commands.mcp, true);
298
+ const addStep = {
299
+ kind: "command",
300
+ argv: ["claude", "mcp", "add-json", SERVER_NAME, JSON.stringify(entry), "--scope", "user"],
301
+ summary: "claude mcp add-json arp … --scope user",
302
+ diff: diffLines(undefined, entry)
303
+ };
304
+ if (remove) {
305
+ if (!present)
306
+ return [{ kind: "skip", summary: 'MCP server "arp" not declared (claude mcp get arp)' }];
307
+ return namesOurServer(got.stdout, commands.cliPath) ? [removeStep] : [foreignServerStep("claude mcp get arp")];
308
+ }
309
+ if (same)
310
+ return [
311
+ {
312
+ kind: "skip",
313
+ summary: 'MCP server "arp" already declared with this command (claude mcp get arp)'
314
+ }
315
+ ];
316
+ if (present && !commands.force && !namesOurServer(got.stdout, commands.cliPath)) {
317
+ return [foreignRefusal("claude mcp get arp")];
318
+ }
319
+ return present ? [removeStep, addStep] : [addStep];
320
+ }
321
+ if (remove && hasForeignServer(files.mcp, commands))
322
+ return [foreignServerStep(files.mcp)];
323
+ if (!remove && !commands.force && hasForeignServer(files.mcp, commands)) {
324
+ return [foreignRefusal(files.mcp)];
325
+ }
326
+ return [
327
+ jsonStep(files.mcp, (config) => withMcpServer(config, SERVER_NAME, remove ? null : mcpEntry(commands.mcp, false)), remove ? "mcpServers.arp removed" : `mcpServers.arp → ${commands.mcp.command} ${commands.mcp.args.join(" ")}`, (config) => serversOf(config)[SERVER_NAME])
328
+ ];
329
+ }
330
+ function claudeHookStep(env, commands, project, remove) {
331
+ return jsonStep(claudeFiles(env, project).settings, (settings) => withHooks(settings, CLAUDE_HOOKS, remove ? null : commands.hook, commands.hookFile), remove ? "hooks: arp removed from PostToolUse and PostToolUseFailure" : "hooks: arp on PostToolUse and PostToolUseFailure, async", (settings) => hooksOf(settings, CLAUDE_HOOKS, commands.hookFile));
332
+ }
333
+ var CLAUDE_HOOKS_SEEN_FROM = [2, 1, 316];
334
+ function claudeVersionCheck(env) {
335
+ if (env.which("claude") === null)
336
+ return [];
337
+ const found = /(\d+)\.(\d+)\.(\d+)/.exec(env.run(["claude", "--version"]).stdout);
338
+ const seen = CLAUDE_HOOKS_SEEN_FROM.join(".");
339
+ if (found === null)
340
+ return [[`claude --version answers (hooks seen working from ${seen})`, false]];
341
+ const version = found.slice(1, 4).map(Number);
342
+ const newer = (() => {
343
+ for (const [i, floor] of CLAUDE_HOOKS_SEEN_FROM.entries()) {
344
+ const part = version[i] ?? 0;
345
+ if (part !== floor)
346
+ return part > floor;
347
+ }
348
+ return true;
349
+ })();
350
+ return [[`claude ${found[0]} runs hooks (seen working from ${seen}; claude update)`, newer]];
351
+ }
352
+ function claudeChecks(env, commands, project) {
353
+ const files = claudeFiles(env, project);
354
+ const mcp = parseJsonConfig(read(files.mcp));
355
+ const settings = parseJsonConfig(read(files.settings));
356
+ const servers = mcp instanceof Error ? {} : serversOf(mcp);
357
+ const viaCli = !project && env.which("claude") !== null ? env.run(["claude", "mcp", "get", SERVER_NAME]).code === 0 : false;
358
+ return [
359
+ [
360
+ `MCP server "arp" declared (${viaCli ? "claude mcp get" : files.mcp})`,
361
+ viaCli || SERVER_NAME in servers
362
+ ],
363
+ [
364
+ `hook on PostToolUse and PostToolUseFailure (${files.settings})`,
365
+ !(settings instanceof Error) && hasHooks(settings, CLAUDE_HOOKS, commands.hookFile)
366
+ ],
367
+ ...claudeVersionCheck(env)
368
+ ];
369
+ }
370
+ var claude = {
371
+ id: "claude",
372
+ name: "Claude Code",
373
+ detect: (env) => env.which("claude") !== null || existsSync(join(env.home, ".claude")),
374
+ install: (env, commands, project) => [
375
+ ...claudeMcpSteps(env, commands, project, false, true),
376
+ claudeHookStep(env, commands, project, false)
377
+ ],
378
+ remove: (env, commands, project) => [
379
+ ...claudeMcpSteps(env, commands, project, true, true),
380
+ claudeHookStep(env, commands, project, true)
381
+ ],
382
+ check: claudeChecks
383
+ };
384
+ var grok = {
385
+ id: "grok",
386
+ name: "Grok Build",
387
+ detect: (env) => env.which("grok") !== null || existsSync(join(env.home, ".grok")),
388
+ install: (env, commands, project) => claude.detect(env) ? [{ kind: "skip", summary: "reads Claude Code's files, written above" }] : [
389
+ ...claudeMcpSteps(env, commands, project, false, false),
390
+ claudeHookStep(env, commands, project, false)
391
+ ],
392
+ remove: (env, commands, project) => claude.detect(env) ? [{ kind: "skip", summary: "reads Claude Code's files, handled above" }] : [
393
+ ...claudeMcpSteps(env, commands, project, true, false),
394
+ claudeHookStep(env, commands, project, true)
395
+ ],
396
+ check: claudeChecks
397
+ };
398
+ var CODEX_HOOKS = {
399
+ shape: "nested",
400
+ events: [{ name: "PostToolUse", matcher: "mcp__.*" }],
401
+ entry: (command) => ({ type: "command", command, timeout: 10 })
402
+ };
403
+ var GEMINI_HOOKS = {
404
+ shape: "nested",
405
+ events: [{ name: "AfterTool", matcher: "mcp_.*" }],
406
+ entry: (command) => ({ type: "command", command, timeout: 1e4 })
407
+ };
408
+ var CURSOR_HOOKS = {
409
+ shape: "flat",
410
+ events: [{ name: "postToolUse" }, { name: "postToolUseFailure" }, { name: "afterMCPExecution" }],
411
+ entry: (command) => ({ command, timeout: 10 })
412
+ };
413
+ var COPILOT_HOOKS = {
414
+ shape: "flat",
415
+ events: [{ name: "postToolUse" }],
416
+ entry: (command) => ({ type: "command", command, timeoutSec: 10 })
417
+ };
418
+ var NO_HOOK = {
419
+ kind: "skip",
420
+ summary: "no hook: it cannot read this runtime yet (`arp init --capture` helps, task 33i)"
421
+ };
422
+ function noHook(path, spec, commands) {
423
+ const removal = hookStep(path, spec, commands, true);
424
+ return removal.kind === "file" && removal.before !== removal.after ? removal : NO_HOOK;
425
+ }
426
+ function hookStep(path, spec, commands, remove) {
427
+ return jsonStep(path, (settings) => withHooks(settings, spec, remove ? null : commands.hook, commands.hookFile), remove ? `hooks: arp removed (${spec.events.map((e) => e.name).join(", ")})` : `hooks: arp on ${spec.events.map((e) => e.name).join(", ")}${commands.capture ? ", capturing" : ""}`, (settings) => hooksOf(settings, spec, commands.hookFile));
428
+ }
429
+ function jsonMcpRuntime(options) {
430
+ const { id, name, dir, file, typed, binary, hook } = options;
431
+ const root = (env, project) => project ? env.cwd : env.home;
432
+ const path = (env, project) => join(root(env, project), dir, file);
433
+ const hookPath = (env, project) => join(root(env, project), dir, hook.file);
434
+ const mcpEdit = (commands) => (config) => withMcpServer(config, SERVER_NAME, commands === null ? null : mcpEntry(commands.mcp, typed));
435
+ const steps = (env, commands, project, remove) => {
436
+ const withHook = remove || commands.capture;
437
+ const foreignHere = hasForeignServer(path(env, project), commands);
438
+ if (!remove && !commands.force && foreignHere) {
439
+ const refusal = foreignRefusal(path(env, project));
440
+ if (hook.file === file || !withHook)
441
+ return [refusal];
442
+ return [refusal, hookStep(hookPath(env, project), hook.spec, commands, false)];
443
+ }
444
+ const foreign = remove && foreignHere;
445
+ const serverEdit = foreign ? (config) => config : mcpEdit(remove ? null : commands);
446
+ if (hook.file === file) {
447
+ return [
448
+ jsonStep(path(env, project), (config) => withHooks(serverEdit(config), hook.spec, withHook && !remove ? commands.hook : null, commands.hookFile), foreign ? 'the arp hook removed; MCP server "arp" is not one ARP wrote, and stays' : remove ? "mcpServers.arp and the arp hook removed" : `mcpServers.arp${commands.capture ? " and the hook, capturing" : ""}`, (config) => ({
449
+ mcpServers: { arp: serversOf(config)[SERVER_NAME] },
450
+ hooks: hooksOf(config, hook.spec, commands.hookFile)
451
+ }))
452
+ ];
453
+ }
454
+ return [
455
+ foreign ? foreignServerStep(path(env, project)) : jsonStep(path(env, project), serverEdit, remove ? "mcpServers.arp removed" : `mcpServers.arp → ${commands.mcp.command} ${commands.mcp.args.join(" ")}`, (config) => serversOf(config)[SERVER_NAME]),
456
+ withHook ? hookStep(hookPath(env, project), hook.spec, commands, remove) : noHook(hookPath(env, project), hook.spec, commands)
457
+ ];
458
+ };
459
+ return {
460
+ id,
461
+ name,
462
+ detect: (env) => env.which(binary) !== null || existsSync(join(env.home, dir)),
463
+ install: (env, commands, project) => steps(env, commands, project, false),
464
+ remove: (env, commands, project) => steps(env, commands, project, true),
465
+ check: (env, _commands, project) => {
466
+ const config = parseJsonConfig(read(path(env, project)));
467
+ const present = !(config instanceof Error) && SERVER_NAME in serversOf(config);
468
+ return [[`MCP server "arp" declared (${path(env, project)})`, present]];
469
+ }
470
+ };
471
+ }
472
+ function codexStep(env, entry, project, force = false) {
473
+ const path = join(project ? env.cwd : env.home, ".codex", "config.toml");
474
+ const before = read(path);
475
+ const after = withCodexServer(before, entry, force);
476
+ if (after instanceof Error) {
477
+ const way = entry === null ? "" : ". Rename it, or run arp init --force to replace it";
478
+ return { kind: "skip", failed: true, summary: `${path}: ${after.message}; left as it is${way}` };
479
+ }
480
+ return {
481
+ kind: "file",
482
+ path,
483
+ before,
484
+ after: after.length === 0 ? null : after,
485
+ summary: entry === null ? "[mcp_servers.arp] removed" : `[mcp_servers.arp] → ${entry.command} ${entry.args.join(" ")}`,
486
+ diff: [
487
+ ...codexBlockOf(before).map((line) => `- ${line}`),
488
+ ...codexBlockOf(after).map((line) => `+ ${line}`)
489
+ ]
490
+ };
491
+ }
492
+ var codex = {
493
+ id: "codex",
494
+ name: "Codex",
495
+ detect: (env) => env.which("codex") !== null || existsSync(join(env.home, ".codex")),
496
+ install: (env, commands, project) => [
497
+ codexStep(env, commands.mcp, project, commands.force),
498
+ commands.capture ? hookStep(join(project ? env.cwd : env.home, ".codex", "hooks.json"), CODEX_HOOKS, commands, false) : noHook(join(project ? env.cwd : env.home, ".codex", "hooks.json"), CODEX_HOOKS, commands)
499
+ ],
500
+ remove: (env, commands, project) => [
501
+ codexStep(env, null, project),
502
+ hookStep(join(project ? env.cwd : env.home, ".codex", "hooks.json"), CODEX_HOOKS, commands, true)
503
+ ],
504
+ check: (env, _commands, project) => {
505
+ const path = join(project ? env.cwd : env.home, ".codex", "config.toml");
506
+ return [[`MCP server "arp" declared (${path})`, hasCodexServer(read(path))]];
507
+ }
508
+ };
509
+ var RUNTIMES = [
510
+ claude,
511
+ grok,
512
+ jsonMcpRuntime({
513
+ id: "cursor",
514
+ name: "Cursor",
515
+ dir: ".cursor",
516
+ file: "mcp.json",
517
+ typed: true,
518
+ binary: "cursor",
519
+ hook: { file: "hooks.json", spec: CURSOR_HOOKS }
520
+ }),
521
+ jsonMcpRuntime({
522
+ id: "gemini",
523
+ name: "Gemini CLI",
524
+ dir: ".gemini",
525
+ file: "settings.json",
526
+ typed: false,
527
+ binary: "gemini",
528
+ hook: { file: "settings.json", spec: GEMINI_HOOKS }
529
+ }),
530
+ codex,
531
+ jsonMcpRuntime({
532
+ id: "copilot",
533
+ name: "Copilot (VS Code, CLI)",
534
+ dir: ".copilot",
535
+ file: "mcp-config.json",
536
+ typed: true,
537
+ binary: "copilot",
538
+ hook: { file: join("hooks", "arp.json"), spec: COPILOT_HOOKS }
539
+ })
540
+ ];
541
+
542
+ // src/install/commands.ts
543
+ var EXIT = {
544
+ ok: 0,
545
+ problems: 1,
546
+ usage: 2,
547
+ refused: 3,
548
+ nothingFound: 4,
549
+ writeFailed: 5,
550
+ partly: 6
551
+ };
552
+ function parseFlags(args) {
553
+ const flags = {
554
+ yes: false,
555
+ noInput: false,
556
+ dryRun: false,
557
+ project: false,
558
+ print: false,
559
+ capture: false,
560
+ sponsored: false,
561
+ force: false,
562
+ runtimes: null
563
+ };
564
+ for (let i = 0;i < args.length; i += 1) {
565
+ const arg = args[i];
566
+ if (arg === "--yes" || arg === "-y")
567
+ flags.yes = true;
568
+ else if (arg === "--no-input")
569
+ flags.noInput = true;
570
+ else if (arg === "--dry-run" || arg === "-n")
571
+ flags.dryRun = true;
572
+ else if (arg === "--project")
573
+ flags.project = true;
574
+ else if (arg === "--print")
575
+ flags.print = true;
576
+ else if (arg === "--capture")
577
+ flags.capture = true;
578
+ else if (arg === "--sponsored")
579
+ flags.sponsored = true;
580
+ else if (arg === "--force")
581
+ flags.force = true;
582
+ else if (arg === "--runtime") {
583
+ const list = (args[i + 1] ?? "").split(",").filter((name) => name.length > 0);
584
+ i += 1;
585
+ const unknown = list.filter((name) => !RUNTIME_IDS.includes(name));
586
+ if (list.length === 0 || unknown.length > 0) {
587
+ return new Error(`--runtime takes a comma list of: ${RUNTIME_IDS.join(", ")}`);
588
+ }
589
+ flags.runtimes = list;
590
+ } else
591
+ return new Error(`unknown option ${String(arg)}`);
592
+ }
593
+ return flags;
594
+ }
595
+ function selected(env, flags) {
596
+ return RUNTIMES.filter((runtime) => flags.runtimes === null ? runtime.detect(env) : flags.runtimes.includes(runtime.id));
597
+ }
598
+ function installsHook(runtimes, capture) {
599
+ return capture ? runtimes.length > 0 : runtimes.some((runtime) => runtime.id === "claude" || runtime.id === "grok");
600
+ }
601
+ function read2(path) {
602
+ try {
603
+ return existsSync2(path) ? readFileSync2(path, "utf8") : null;
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
608
+ function changes(step) {
609
+ return step.kind === "command" || step.kind === "file" && step.before !== step.after;
610
+ }
611
+ function stamp(date) {
612
+ return date.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
613
+ }
614
+ function apply(env, step) {
615
+ try {
616
+ if (step.kind === "command") {
617
+ const result = env.run(step.argv);
618
+ return result.code === 0 ? null : `${step.argv.join(" ")}: ${result.stderr.trim() || `exit ${String(result.code)}`}`;
619
+ }
620
+ if (step.kind !== "file")
621
+ return null;
622
+ if (step.before !== null)
623
+ copyFileSync(step.path, `${step.path}.arp-backup-${stamp(env.now())}`);
624
+ if (step.after === null) {
625
+ rmSync(step.path, { force: true });
626
+ return null;
627
+ }
628
+ mkdirSync(dirname(step.path), { recursive: true });
629
+ const mode = step.before === null ? undefined : statSync(step.path).mode & 511;
630
+ const temp = `${step.path}.arp-tmp`;
631
+ writeFileSync(temp, step.after, mode === undefined ? undefined : { mode });
632
+ if (mode !== undefined)
633
+ chmodSync(temp, mode);
634
+ renameSync(temp, step.path);
635
+ return null;
636
+ } catch (error) {
637
+ return `${step.kind === "file" ? step.path : ""}: ${String(error)}`;
638
+ }
639
+ }
640
+ function hookFileStep(env, commands, remove) {
641
+ const before = read2(commands.hookFile);
642
+ const after = remove ? null : read2(join2(env.dist, "hook.js"));
643
+ if (!remove && after === null) {
644
+ return {
645
+ kind: "skip",
646
+ failed: true,
647
+ summary: `no hook.js next to this arp (${env.dist}); build it first`
648
+ };
649
+ }
650
+ return {
651
+ kind: "file",
652
+ path: commands.hookFile,
653
+ before,
654
+ after,
655
+ summary: remove ? "removed" : `the hook, ${after?.length ?? 0} bytes, run by ${env.node}`,
656
+ diff: []
657
+ };
658
+ }
659
+ async function consent(env, flags, question) {
660
+ if (flags.yes)
661
+ return true;
662
+ if (flags.noInput || !env.interactive) {
663
+ env.err("arp: changes needed and no terminal to ask. Re-run with --yes, or --dry-run to look first.");
664
+ return false;
665
+ }
666
+ return env.ask(question);
667
+ }
668
+ async function change(env, flags, plan, verb) {
669
+ const pending = plan.flatMap((group) => group.steps.filter(changes));
670
+ const leftOut = plan.flatMap((group) => group.steps.flatMap((step) => step.kind === "skip" && step.failed === true ? [`${group.runtime}: ${step.summary}`] : []));
671
+ const finish = () => {
672
+ if (leftOut.length === 0)
673
+ return EXIT.ok;
674
+ env.err(`
675
+ Not done in ${leftOut.length} place(s):`);
676
+ for (const line of leftOut)
677
+ env.err(` ${line}`);
678
+ return EXIT.partly;
679
+ };
680
+ for (const group of plan) {
681
+ env.out(group.runtime);
682
+ for (const step of group.steps) {
683
+ if (step.kind === "skip")
684
+ env.out(` -- ${step.summary}`);
685
+ else if (step.kind === "command")
686
+ env.out(` run ${step.summary}`);
687
+ else
688
+ env.out(` ${changes(step) ? step.after === null ? "del" : "set" : "ok "} ${step.path}
689
+ ${step.summary}`);
690
+ if (step.kind !== "skip" && changes(step))
691
+ for (const line of step.diff)
692
+ env.out(` ${line}`);
693
+ }
694
+ }
695
+ if (pending.length === 0) {
696
+ env.out(`
697
+ Nothing to change.`);
698
+ return finish();
699
+ }
700
+ if (flags.dryRun) {
701
+ env.out(`
702
+ --dry-run: ${pending.length} change(s) shown, nothing written.`);
703
+ return finish();
704
+ }
705
+ env.out(`
706
+ Each file is backed up as <file>.arp-backup-<date> before it is written. A change through the \`claude\` command has no backup: what it removes is printed above.`);
707
+ if (!await consent(env, flags, `${verb} ${pending.length} change(s)?`))
708
+ return EXIT.refused;
709
+ const failures = pending.map((step) => apply(env, step)).filter((f) => f !== null);
710
+ for (const failure of failures)
711
+ env.err(`arp: ${failure}`);
712
+ if (failures.length > 0)
713
+ return EXIT.writeFailed;
714
+ env.out(`Done. ${verb === "Install" ? "Restart your agent runtime to load the MCP server; hooks load on their own." : "Removed."}`);
715
+ return finish();
716
+ }
717
+ async function init(env, flags, cliPath) {
718
+ const commands = commandsFor(env, cliPath, flags.capture, flags.sponsored, flags.force);
719
+ if (flags.print) {
720
+ env.out(JSON.stringify({ mcpServers: { arp: commands.mcp } }, null, 2));
721
+ return EXIT.ok;
722
+ }
723
+ const runtimes = selected(env, flags);
724
+ if (runtimes.length === 0) {
725
+ env.err(`arp: no supported runtime found (${RUNTIME_IDS.join(", ")}).`);
726
+ env.err("For any other MCP client, `arp init --print` prints the entry to paste.");
727
+ return EXIT.nothingFound;
728
+ }
729
+ const plan = runtimes.map((runtime) => ({
730
+ runtime: runtime.name,
731
+ steps: runtime.install(env, commands, flags.project)
732
+ }));
733
+ if (installsHook(runtimes, flags.capture)) {
734
+ plan.push({ runtime: "hook", steps: [hookFileStep(env, commands, false)] });
735
+ }
736
+ return change(env, flags, plan, "Install");
737
+ }
738
+ async function uninstall(env, flags, cliPath) {
739
+ const commands = commandsFor(env, cliPath);
740
+ const runtimes = selected(env, flags);
741
+ const plan = runtimes.map((runtime) => ({
742
+ runtime: runtime.name,
743
+ steps: runtime.remove(env, commands, flags.project)
744
+ }));
745
+ plan.push({ runtime: "hook", steps: [hookFileStep(env, commands, true)] });
746
+ const code = await change(env, flags, plan, "Remove");
747
+ const captures = join2(env.home, ".arp", "captures");
748
+ if (existsSync2(captures))
749
+ env.out(`Captures kept in ${captures}. Delete them when you no longer need them.`);
750
+ return code;
751
+ }
752
+ var INITIALIZE = `${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} })}
753
+ `;
754
+ function doctor(env, flags, cliPath) {
755
+ const commands = commandsFor(env, cliPath);
756
+ const runtimes = selected(env, flags);
757
+ const rows = [];
758
+ if (runtimes.length === 0)
759
+ rows.push(["runtime", `none of ${RUNTIME_IDS.join(", ")} found`, false]);
760
+ for (const runtime of runtimes) {
761
+ for (const [what, ok] of runtime.check(env, commands, flags.project))
762
+ rows.push([runtime.name, what, ok]);
763
+ }
764
+ if (installsHook(runtimes, false)) {
765
+ const installed = read2(commands.hookFile);
766
+ const packaged = read2(join2(env.dist, "hook.js"));
767
+ rows.push(["hook", `${commands.hookFile} present`, installed !== null]);
768
+ if (installed !== null && packaged !== null) {
769
+ rows.push([
770
+ "hook",
771
+ "same version as this arp (else run arp init again)",
772
+ installed === packaged
773
+ ]);
774
+ }
775
+ rows.push(["hook", `node at ${env.node}`, existsSync2(env.node)]);
776
+ }
777
+ const answer = env.run([commands.mcp.command, ...commands.mcp.args], INITIALIZE, 30000);
778
+ rows.push([
779
+ "MCP server",
780
+ `answers initialize (${commands.mcp.command} ${commands.mcp.args.join(" ")})`,
781
+ answer.stdout.includes('"name":"ARP"')
782
+ ]);
783
+ const log = read2(join2(env.cwd, ".arp", "session.jsonl"));
784
+ const last = log?.trim().split(`
785
+ `).pop();
786
+ let lastAt = null;
787
+ try {
788
+ const parsed = last === undefined ? null : JSON.parse(last);
789
+ if (typeof parsed === "object" && parsed !== null && "at" in parsed && typeof parsed.at === "string")
790
+ lastAt = parsed.at;
791
+ } catch {
792
+ lastAt = null;
793
+ }
794
+ for (const [where, what, ok] of rows)
795
+ env.out(`${ok ? "ok " : "FAIL"} ${where.padEnd(12)} ${what}`);
796
+ env.out(`-- session log ${lastAt === null ? `no call recorded in ${join2(env.cwd, ".arp")}` : `last call recorded ${lastAt}`}`);
797
+ return rows.every(([, , ok]) => ok) ? EXIT.ok : EXIT.problems;
798
+ }
799
+ export {
800
+ uninstall,
801
+ parseFlags,
802
+ init,
803
+ doctor,
804
+ EXIT
805
+ };