@tonyclaw/agent-inspector 2.0.38 → 2.0.40
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/.output/cli.js +528 -24
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-hc5OoAls.js → CompareDrawer-p0uYxW7_.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-BMKBs8Qz.js → ProxyViewerContainer-BGqeiYKD.js} +26 -26
- package/.output/public/assets/{ReplayDialog-CN-KM2nu.js → ReplayDialog-TTTVCMe9.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-B1kTlAUk.js → RequestAnatomy-c8ru0pS-.js} +1 -1
- package/.output/public/assets/{ResponseView-BchUjKjt.js → ResponseView-D59jTMy4.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-BQOxI9Hi.js → StreamingChunkSequence-UlAMVF9j.js} +1 -1
- package/.output/public/assets/_sessionId-EHPexdPl.js +1 -0
- package/.output/public/assets/index-CZP-XfpB.js +1 -0
- package/.output/public/assets/{main-Blykwzsn.js → main-CKYe6UUv.js} +2 -2
- package/.output/server/{_sessionId-BUNZz2zZ.mjs → _sessionId-CkwhOzdS.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-VaDYPM5G.mjs → CompareDrawer-BHDiuYMw.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-CpdWs7Pk.mjs → ProxyViewerContainer-0c2_DBMX.mjs} +57 -6
- package/.output/server/_ssr/{ReplayDialog-DbfIjJhb.mjs → ReplayDialog-CTjwzDDH.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-CrD8h6As.mjs → RequestAnatomy-D36oujv2.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-BalvDwtU.mjs → ResponseView-ClZ3oODE.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-RxatDbPV.mjs → StreamingChunkSequence-Ck5l4p1m.mjs} +2 -2
- package/.output/server/_ssr/{index-11qAtV9d.mjs → index-DKBYH0TC.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-C4OdRnqX.mjs → router-D4I3ZyVS.mjs} +434 -6
- package/.output/server/{_tanstack-start-manifest_v-F6bwcl33.mjs → _tanstack-start-manifest_v-D4vSn_wD.mjs} +1 -1
- package/.output/server/index.mjs +54 -54
- package/README.md +7 -0
- package/package.json +1 -1
- package/src/cli/onboard.ts +629 -21
- package/src/components/providers/SettingsDialog.tsx +30 -0
- package/src/components/proxy-viewer/LogEntry.tsx +15 -1
- package/src/components/proxy-viewer/LogEntryHeader.tsx +20 -0
- package/src/contracts/log.ts +1 -0
- package/src/proxy/logFinalizer.ts +58 -0
- package/src/proxy/logSearch.ts +1 -0
- package/src/proxy/toolSchemaWarnings.ts +515 -0
- package/.output/public/assets/_sessionId-CVw5e8TK.js +0 -1
- package/.output/public/assets/index-BdUTMQL6.js +0 -1
package/src/cli/onboard.ts
CHANGED
|
@@ -36,6 +36,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
36
36
|
const __dirname = dirname(__filename);
|
|
37
37
|
|
|
38
38
|
const DEFAULT_PORT = 25947;
|
|
39
|
+
const DEFAULT_MCP_URL = `http://localhost:${DEFAULT_PORT}/api/mcp`;
|
|
39
40
|
const SKILL_DIR_NAME = "agent-inspector-onboard";
|
|
40
41
|
const SKILL_FILE_NAME = "SKILL.md";
|
|
41
42
|
// Windows reserves `:` for NTFS alternate data streams, so a file named
|
|
@@ -57,6 +58,11 @@ export type OnboardFlags = {
|
|
|
57
58
|
skipToolWire: boolean;
|
|
58
59
|
codexOnly: boolean;
|
|
59
60
|
skipCodexSkill: boolean;
|
|
61
|
+
opencode: boolean;
|
|
62
|
+
opencodeOnly: boolean;
|
|
63
|
+
opencodeConfig: string | null;
|
|
64
|
+
opencodeTransport: OpenCodeTransport;
|
|
65
|
+
opencodeMcpUrl: string;
|
|
60
66
|
uninstall: boolean;
|
|
61
67
|
status: boolean;
|
|
62
68
|
json: boolean;
|
|
@@ -64,6 +70,40 @@ export type OnboardFlags = {
|
|
|
64
70
|
codexSkillDir: string | null;
|
|
65
71
|
};
|
|
66
72
|
|
|
73
|
+
type OpenCodeTransport = "local" | "remote";
|
|
74
|
+
type OpenCodeStatusTransport = OpenCodeTransport | "unknown";
|
|
75
|
+
type OpenCodeConfigState = "missing" | "configured" | "custom" | "disabled" | "invalid";
|
|
76
|
+
|
|
77
|
+
type OpenCodeConfigStatus = {
|
|
78
|
+
label: "OpenCode MCP config";
|
|
79
|
+
path: string;
|
|
80
|
+
state: OpenCodeConfigState;
|
|
81
|
+
transport: OpenCodeStatusTransport;
|
|
82
|
+
endpoint: string;
|
|
83
|
+
action: string;
|
|
84
|
+
detail: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type OpenCodeConfigPlan = {
|
|
88
|
+
status: OpenCodeConfigStatus;
|
|
89
|
+
body: string | null;
|
|
90
|
+
shouldWrite: boolean;
|
|
91
|
+
shouldRemove: boolean;
|
|
92
|
+
blockedMessage: string | null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type OpenCodeReadResult =
|
|
96
|
+
| {
|
|
97
|
+
ok: true;
|
|
98
|
+
exists: boolean;
|
|
99
|
+
config: Record<string, unknown>;
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
ok: false;
|
|
103
|
+
exists: boolean;
|
|
104
|
+
message: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
67
107
|
type OnboardTargets = {
|
|
68
108
|
claudeSkillFile: string;
|
|
69
109
|
claudeCommandFile: string;
|
|
@@ -126,6 +166,11 @@ function parseFlags(argv: readonly string[]): OnboardFlags {
|
|
|
126
166
|
skipToolWire: false,
|
|
127
167
|
codexOnly: false,
|
|
128
168
|
skipCodexSkill: false,
|
|
169
|
+
opencode: false,
|
|
170
|
+
opencodeOnly: false,
|
|
171
|
+
opencodeConfig: null,
|
|
172
|
+
opencodeTransport: "local",
|
|
173
|
+
opencodeMcpUrl: DEFAULT_MCP_URL,
|
|
129
174
|
uninstall: false,
|
|
130
175
|
status: false,
|
|
131
176
|
json: false,
|
|
@@ -155,6 +200,57 @@ function parseFlags(argv: readonly string[]): OnboardFlags {
|
|
|
155
200
|
case "--skip-codex-skill":
|
|
156
201
|
flags.skipCodexSkill = true;
|
|
157
202
|
break;
|
|
203
|
+
case "--opencode":
|
|
204
|
+
flags.opencode = true;
|
|
205
|
+
break;
|
|
206
|
+
case "--opencode-only":
|
|
207
|
+
flags.opencode = true;
|
|
208
|
+
flags.opencodeOnly = true;
|
|
209
|
+
break;
|
|
210
|
+
case "--opencode-config": {
|
|
211
|
+
const next = argv[i + 1];
|
|
212
|
+
if (next === undefined) {
|
|
213
|
+
process.stderr.write(
|
|
214
|
+
"agent-inspector onboard: --opencode-config requires a path argument\n",
|
|
215
|
+
);
|
|
216
|
+
process.exit(2);
|
|
217
|
+
}
|
|
218
|
+
flags.opencodeConfig = next;
|
|
219
|
+
i++;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "--opencode-transport": {
|
|
223
|
+
const next = argv[i + 1];
|
|
224
|
+
if (next === undefined) {
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
"agent-inspector onboard: --opencode-transport requires local or remote\n",
|
|
227
|
+
);
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
switch (next) {
|
|
231
|
+
case "local":
|
|
232
|
+
case "remote":
|
|
233
|
+
flags.opencodeTransport = next;
|
|
234
|
+
break;
|
|
235
|
+
default:
|
|
236
|
+
process.stderr.write(
|
|
237
|
+
`agent-inspector onboard: invalid --opencode-transport: ${next}\n`,
|
|
238
|
+
);
|
|
239
|
+
process.exit(2);
|
|
240
|
+
}
|
|
241
|
+
i++;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "--opencode-mcp-url": {
|
|
245
|
+
const next = argv[i + 1];
|
|
246
|
+
if (next === undefined) {
|
|
247
|
+
process.stderr.write("agent-inspector onboard: --opencode-mcp-url requires a URL\n");
|
|
248
|
+
process.exit(2);
|
|
249
|
+
}
|
|
250
|
+
flags.opencodeMcpUrl = next;
|
|
251
|
+
i++;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
158
254
|
case "--uninstall":
|
|
159
255
|
flags.uninstall = true;
|
|
160
256
|
break;
|
|
@@ -212,6 +308,12 @@ Options:
|
|
|
212
308
|
--skip-tool-wire Skip the wire-tool phase in the Claude skill body
|
|
213
309
|
--skip-codex-skill Install only the Claude Code skill and slash command
|
|
214
310
|
--codex-only Install only the Codex skill
|
|
311
|
+
--opencode Also configure OpenCode MCP in opencode.json/jsonc
|
|
312
|
+
--opencode-only Configure only OpenCode MCP
|
|
313
|
+
--opencode-config <path> Override OpenCode config path (default: ~/.config/opencode/opencode.json)
|
|
314
|
+
--opencode-transport <local|remote>
|
|
315
|
+
OpenCode MCP transport to write (default: local)
|
|
316
|
+
--opencode-mcp-url <url> Agent Inspector MCP endpoint for OpenCode
|
|
215
317
|
--uninstall Remove generated files whose version matches this package
|
|
216
318
|
--status Show installed onboarding file versions and suggested action
|
|
217
319
|
--json Emit machine-readable JSON with --status
|
|
@@ -243,6 +345,429 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
243
345
|
return typeof value === "object" && value !== null;
|
|
244
346
|
}
|
|
245
347
|
|
|
348
|
+
function isOpenCodeEnabled(flags: OnboardFlags): boolean {
|
|
349
|
+
return flags.opencode || flags.opencodeOnly;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function resolveOpenCodeConfigPath(flags: OnboardFlags): string {
|
|
353
|
+
if (flags.opencodeConfig !== null) {
|
|
354
|
+
return flags.opencodeConfig;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const configDir = join(homedir(), ".config", "opencode");
|
|
358
|
+
const jsoncPath = join(configDir, "opencode.jsonc");
|
|
359
|
+
if (existsSync(jsoncPath)) {
|
|
360
|
+
return jsoncPath;
|
|
361
|
+
}
|
|
362
|
+
return join(configDir, "opencode.json");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function stripJsoncComments(raw: string): string {
|
|
366
|
+
let result = "";
|
|
367
|
+
let index = 0;
|
|
368
|
+
let inString = false;
|
|
369
|
+
let stringChar = "";
|
|
370
|
+
|
|
371
|
+
while (index < raw.length) {
|
|
372
|
+
const ch = raw[index];
|
|
373
|
+
const next = raw[index + 1];
|
|
374
|
+
|
|
375
|
+
if (inString) {
|
|
376
|
+
if (ch === "\\" && next !== undefined) {
|
|
377
|
+
result += ch + next;
|
|
378
|
+
index += 2;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (ch === stringChar) {
|
|
382
|
+
inString = false;
|
|
383
|
+
stringChar = "";
|
|
384
|
+
}
|
|
385
|
+
result += ch;
|
|
386
|
+
index += 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (ch === '"' || ch === "'") {
|
|
391
|
+
inString = true;
|
|
392
|
+
stringChar = ch;
|
|
393
|
+
result += ch;
|
|
394
|
+
index += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (ch === "/" && next === "/") {
|
|
399
|
+
while (index < raw.length && raw[index] !== "\n") {
|
|
400
|
+
index += 1;
|
|
401
|
+
}
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (ch === "/" && next === "*") {
|
|
406
|
+
index += 2;
|
|
407
|
+
while (index < raw.length - 1 && !(raw[index] === "*" && raw[index + 1] === "/")) {
|
|
408
|
+
index += 1;
|
|
409
|
+
}
|
|
410
|
+
index += 2;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
result += ch;
|
|
415
|
+
index += 1;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function readOpenCodeConfig(path: string): OpenCodeReadResult {
|
|
422
|
+
if (!existsSync(path)) {
|
|
423
|
+
return { ok: true, exists: false, config: {} };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const raw = readFileSync(path, "utf8");
|
|
428
|
+
if (raw.trim().length === 0) {
|
|
429
|
+
return { ok: true, exists: true, config: {} };
|
|
430
|
+
}
|
|
431
|
+
const parsed: unknown = JSON.parse(stripJsoncComments(raw));
|
|
432
|
+
if (isObject(parsed)) {
|
|
433
|
+
return { ok: true, exists: true, config: parsed };
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
exists: true,
|
|
438
|
+
message: "OpenCode config root must be a JSON object",
|
|
439
|
+
};
|
|
440
|
+
} catch (err) {
|
|
441
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
442
|
+
return {
|
|
443
|
+
ok: false,
|
|
444
|
+
exists: true,
|
|
445
|
+
message,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function buildOpenCodeMcpEntry(
|
|
451
|
+
transport: OpenCodeTransport,
|
|
452
|
+
endpoint: string,
|
|
453
|
+
): Record<string, unknown> {
|
|
454
|
+
switch (transport) {
|
|
455
|
+
case "local":
|
|
456
|
+
return {
|
|
457
|
+
type: "local",
|
|
458
|
+
command: ["agent-inspector-mcp", "stdio", "--url", endpoint],
|
|
459
|
+
enabled: true,
|
|
460
|
+
};
|
|
461
|
+
case "remote":
|
|
462
|
+
return {
|
|
463
|
+
type: "remote",
|
|
464
|
+
url: endpoint,
|
|
465
|
+
enabled: true,
|
|
466
|
+
};
|
|
467
|
+
default:
|
|
468
|
+
return {
|
|
469
|
+
type: "local",
|
|
470
|
+
command: ["agent-inspector-mcp", "stdio", "--url", endpoint],
|
|
471
|
+
enabled: true,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function stringArrayEquals(value: unknown, expected: readonly string[]): boolean {
|
|
477
|
+
if (!Array.isArray(value) || value.length !== expected.length) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return value.every((item, index) => item === expected[index]);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function isOpenCodeLocalEntry(value: unknown, endpoint: string, ignoreEnabled: boolean): boolean {
|
|
485
|
+
if (!isObject(value)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const enabled = value["enabled"];
|
|
490
|
+
if (!ignoreEnabled && enabled === false) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const env = value["environment"];
|
|
495
|
+
const usesUrlArg = stringArrayEquals(value["command"], [
|
|
496
|
+
"agent-inspector-mcp",
|
|
497
|
+
"stdio",
|
|
498
|
+
"--url",
|
|
499
|
+
endpoint,
|
|
500
|
+
]);
|
|
501
|
+
const usesEnv =
|
|
502
|
+
stringArrayEquals(value["command"], ["agent-inspector-mcp", "stdio"]) &&
|
|
503
|
+
isObject(env) &&
|
|
504
|
+
env["AGENT_INSPECTOR_MCP_URL"] === endpoint;
|
|
505
|
+
|
|
506
|
+
return value["type"] === "local" && (usesUrlArg || usesEnv);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function isOpenCodeRemoteEntry(value: unknown, endpoint: string, ignoreEnabled: boolean): boolean {
|
|
510
|
+
if (!isObject(value)) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const enabled = value["enabled"];
|
|
515
|
+
if (!ignoreEnabled && enabled === false) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return value["type"] === "remote" && value["url"] === endpoint;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function isKnownOpenCodeEntry(value: unknown, endpoint: string, ignoreEnabled: boolean): boolean {
|
|
523
|
+
return (
|
|
524
|
+
isOpenCodeLocalEntry(value, endpoint, ignoreEnabled) ||
|
|
525
|
+
isOpenCodeRemoteEntry(value, endpoint, ignoreEnabled)
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function detectOpenCodeTransport(value: unknown): OpenCodeStatusTransport {
|
|
530
|
+
if (!isObject(value)) {
|
|
531
|
+
return "unknown";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
switch (value["type"]) {
|
|
535
|
+
case "local":
|
|
536
|
+
return "local";
|
|
537
|
+
case "remote":
|
|
538
|
+
return "remote";
|
|
539
|
+
default:
|
|
540
|
+
return "unknown";
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function openCodeActionForStatus(status: OpenCodeConfigStatus): string {
|
|
545
|
+
switch (status.state) {
|
|
546
|
+
case "missing":
|
|
547
|
+
return "agent-inspector onboard --opencode-only";
|
|
548
|
+
case "configured":
|
|
549
|
+
return "opencode mcp list";
|
|
550
|
+
case "disabled":
|
|
551
|
+
case "custom":
|
|
552
|
+
return "agent-inspector onboard --opencode-only --force";
|
|
553
|
+
case "invalid":
|
|
554
|
+
return "fix OpenCode JSON/JSONC, then rerun agent-inspector onboard --opencode-only";
|
|
555
|
+
default:
|
|
556
|
+
return "agent-inspector onboard --opencode-only";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function makeOpenCodeStatus(
|
|
561
|
+
path: string,
|
|
562
|
+
state: OpenCodeConfigState,
|
|
563
|
+
transport: OpenCodeStatusTransport,
|
|
564
|
+
endpoint: string,
|
|
565
|
+
detail: string,
|
|
566
|
+
): OpenCodeConfigStatus {
|
|
567
|
+
const status: OpenCodeConfigStatus = {
|
|
568
|
+
label: "OpenCode MCP config",
|
|
569
|
+
path,
|
|
570
|
+
state,
|
|
571
|
+
transport,
|
|
572
|
+
endpoint,
|
|
573
|
+
action: "agent-inspector onboard --opencode-only",
|
|
574
|
+
detail,
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
...status,
|
|
579
|
+
action: openCodeActionForStatus(status),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function getOpenCodeEntry(config: Record<string, unknown>): unknown {
|
|
584
|
+
const mcp = config["mcp"];
|
|
585
|
+
if (!isObject(mcp)) {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
return mcp["agent-inspector"];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function buildOpenCodeConfig(
|
|
592
|
+
config: Record<string, unknown>,
|
|
593
|
+
transport: OpenCodeTransport,
|
|
594
|
+
endpoint: string,
|
|
595
|
+
): Record<string, unknown> {
|
|
596
|
+
const schema =
|
|
597
|
+
typeof config["$schema"] === "string" ? config["$schema"] : "https://opencode.ai/config.json";
|
|
598
|
+
const nextConfig: Record<string, unknown> = {
|
|
599
|
+
$schema: schema,
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
for (const [key, value] of Object.entries(config)) {
|
|
603
|
+
if (key !== "$schema" && key !== "mcp") {
|
|
604
|
+
nextConfig[key] = value;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const currentMcp = config["mcp"];
|
|
609
|
+
const nextMcp: Record<string, unknown> = isObject(currentMcp) ? { ...currentMcp } : {};
|
|
610
|
+
nextMcp["agent-inspector"] = buildOpenCodeMcpEntry(transport, endpoint);
|
|
611
|
+
nextConfig["mcp"] = nextMcp;
|
|
612
|
+
return nextConfig;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function formatOpenCodeConfig(config: Record<string, unknown>): string {
|
|
616
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function removeOpenCodeEntry(config: Record<string, unknown>): Record<string, unknown> {
|
|
620
|
+
const nextConfig: Record<string, unknown> = {};
|
|
621
|
+
for (const [key, value] of Object.entries(config)) {
|
|
622
|
+
if (key !== "mcp") {
|
|
623
|
+
nextConfig[key] = value;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const currentMcp = config["mcp"];
|
|
628
|
+
if (!isObject(currentMcp)) {
|
|
629
|
+
return nextConfig;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const nextMcp: Record<string, unknown> = {};
|
|
633
|
+
for (const [key, value] of Object.entries(currentMcp)) {
|
|
634
|
+
if (key !== "agent-inspector") {
|
|
635
|
+
nextMcp[key] = value;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (Object.keys(nextMcp).length > 0) {
|
|
640
|
+
nextConfig["mcp"] = nextMcp;
|
|
641
|
+
}
|
|
642
|
+
return nextConfig;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function readOpenCodeStatus(flags: OnboardFlags): OpenCodeConfigStatus {
|
|
646
|
+
const path = resolveOpenCodeConfigPath(flags);
|
|
647
|
+
const read = readOpenCodeConfig(path);
|
|
648
|
+
if (!read.ok) {
|
|
649
|
+
return makeOpenCodeStatus(path, "invalid", "unknown", flags.opencodeMcpUrl, read.message);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const entry = getOpenCodeEntry(read.config);
|
|
653
|
+
if (entry === undefined) {
|
|
654
|
+
const detail = read.exists
|
|
655
|
+
? "OpenCode config has no mcp.agent-inspector entry"
|
|
656
|
+
: "Missing file";
|
|
657
|
+
return makeOpenCodeStatus(path, "missing", "unknown", flags.opencodeMcpUrl, detail);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const transport = detectOpenCodeTransport(entry);
|
|
661
|
+
if (isObject(entry) && entry["enabled"] === false) {
|
|
662
|
+
return makeOpenCodeStatus(
|
|
663
|
+
path,
|
|
664
|
+
"disabled",
|
|
665
|
+
transport,
|
|
666
|
+
flags.opencodeMcpUrl,
|
|
667
|
+
"mcp.agent-inspector is present but disabled",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (isKnownOpenCodeEntry(entry, flags.opencodeMcpUrl, false)) {
|
|
672
|
+
return makeOpenCodeStatus(
|
|
673
|
+
path,
|
|
674
|
+
"configured",
|
|
675
|
+
transport,
|
|
676
|
+
flags.opencodeMcpUrl,
|
|
677
|
+
"mcp.agent-inspector points at Agent Inspector MCP",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return makeOpenCodeStatus(
|
|
682
|
+
path,
|
|
683
|
+
"custom",
|
|
684
|
+
transport,
|
|
685
|
+
flags.opencodeMcpUrl,
|
|
686
|
+
"mcp.agent-inspector exists but does not match Agent Inspector defaults",
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function buildOpenCodePlan(flags: OnboardFlags): OpenCodeConfigPlan {
|
|
691
|
+
const path = resolveOpenCodeConfigPath(flags);
|
|
692
|
+
const read = readOpenCodeConfig(path);
|
|
693
|
+
if (!read.ok) {
|
|
694
|
+
return {
|
|
695
|
+
status: makeOpenCodeStatus(path, "invalid", "unknown", flags.opencodeMcpUrl, read.message),
|
|
696
|
+
body: null,
|
|
697
|
+
shouldWrite: false,
|
|
698
|
+
shouldRemove: false,
|
|
699
|
+
blockedMessage: `OpenCode config is invalid: ${read.message}`,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const status = readOpenCodeStatus(flags);
|
|
704
|
+
const entry = getOpenCodeEntry(read.config);
|
|
705
|
+
const nextBody = formatOpenCodeConfig(
|
|
706
|
+
buildOpenCodeConfig(read.config, flags.opencodeTransport, flags.opencodeMcpUrl),
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
if (flags.uninstall) {
|
|
710
|
+
const shouldRemove =
|
|
711
|
+
entry !== undefined && isKnownOpenCodeEntry(entry, flags.opencodeMcpUrl, true);
|
|
712
|
+
const removeBody = shouldRemove ? formatOpenCodeConfig(removeOpenCodeEntry(read.config)) : null;
|
|
713
|
+
return {
|
|
714
|
+
status,
|
|
715
|
+
body: removeBody,
|
|
716
|
+
shouldWrite: false,
|
|
717
|
+
shouldRemove,
|
|
718
|
+
blockedMessage: shouldRemove
|
|
719
|
+
? null
|
|
720
|
+
: "OpenCode mcp.agent-inspector is missing or custom; preserved",
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
switch (status.state) {
|
|
725
|
+
case "configured":
|
|
726
|
+
return {
|
|
727
|
+
status,
|
|
728
|
+
body: nextBody,
|
|
729
|
+
shouldWrite: flags.force,
|
|
730
|
+
shouldRemove: false,
|
|
731
|
+
blockedMessage: null,
|
|
732
|
+
};
|
|
733
|
+
case "missing":
|
|
734
|
+
return {
|
|
735
|
+
status,
|
|
736
|
+
body: nextBody,
|
|
737
|
+
shouldWrite: true,
|
|
738
|
+
shouldRemove: false,
|
|
739
|
+
blockedMessage: null,
|
|
740
|
+
};
|
|
741
|
+
case "custom":
|
|
742
|
+
case "disabled":
|
|
743
|
+
return {
|
|
744
|
+
status,
|
|
745
|
+
body: nextBody,
|
|
746
|
+
shouldWrite: flags.force,
|
|
747
|
+
shouldRemove: false,
|
|
748
|
+
blockedMessage: flags.force
|
|
749
|
+
? null
|
|
750
|
+
: "OpenCode mcp.agent-inspector is custom or disabled; use --force to replace",
|
|
751
|
+
};
|
|
752
|
+
case "invalid":
|
|
753
|
+
return {
|
|
754
|
+
status,
|
|
755
|
+
body: null,
|
|
756
|
+
shouldWrite: false,
|
|
757
|
+
shouldRemove: false,
|
|
758
|
+
blockedMessage: "OpenCode config is invalid",
|
|
759
|
+
};
|
|
760
|
+
default:
|
|
761
|
+
return {
|
|
762
|
+
status,
|
|
763
|
+
body: null,
|
|
764
|
+
shouldWrite: false,
|
|
765
|
+
shouldRemove: false,
|
|
766
|
+
blockedMessage: "OpenCode config state is unknown",
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
246
771
|
function buildDetectedSummary(): string {
|
|
247
772
|
const entries = detectAll();
|
|
248
773
|
const present = entries.filter((e) => e.result.found);
|
|
@@ -448,8 +973,8 @@ function buildPlannedFiles(
|
|
|
448
973
|
targets: OnboardTargets,
|
|
449
974
|
version: string,
|
|
450
975
|
): PlannedFile[] {
|
|
451
|
-
const installClaude = !flags.codexOnly;
|
|
452
|
-
const installCodex = !flags.skipCodexSkill;
|
|
976
|
+
const installClaude = !flags.codexOnly && !flags.opencodeOnly;
|
|
977
|
+
const installCodex = !flags.skipCodexSkill && !flags.opencodeOnly;
|
|
453
978
|
const detectedSummary = installClaude ? buildDetectedSummary() : "";
|
|
454
979
|
const claudeSkillBody = installClaude
|
|
455
980
|
? renderSkillOnboard({
|
|
@@ -525,6 +1050,7 @@ function runUninstall(
|
|
|
525
1050
|
plannedFiles: readonly PlannedFile[],
|
|
526
1051
|
currentVersion: string,
|
|
527
1052
|
dryRun: boolean,
|
|
1053
|
+
openCodePlan: OpenCodeConfigPlan | null,
|
|
528
1054
|
): number {
|
|
529
1055
|
const enabledFiles = plannedFiles.filter((file) => file.enabled);
|
|
530
1056
|
const filesToRemove = enabledFiles.filter((file) => shouldUninstall(file, currentVersion));
|
|
@@ -535,14 +1061,27 @@ function runUninstall(
|
|
|
535
1061
|
const status = shouldUninstall(file, currentVersion) ? "matching" : "skipped";
|
|
536
1062
|
process.stdout.write(`${file.label}: ${file.path} (${status})\n`);
|
|
537
1063
|
}
|
|
1064
|
+
if (openCodePlan !== null) {
|
|
1065
|
+
const status = openCodePlan.shouldRemove ? "matching" : "skipped";
|
|
1066
|
+
process.stdout.write(
|
|
1067
|
+
`${openCodePlan.status.label}: ${openCodePlan.status.path} (${status})\n`,
|
|
1068
|
+
);
|
|
1069
|
+
if (openCodePlan.blockedMessage !== null) {
|
|
1070
|
+
process.stdout.write(` ${openCodePlan.blockedMessage}\n`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
538
1073
|
process.stdout.write(`\nNo files were removed.\n`);
|
|
539
1074
|
return 0;
|
|
540
1075
|
}
|
|
541
1076
|
|
|
542
|
-
|
|
1077
|
+
const removeOpenCode = openCodePlan !== null && openCodePlan.shouldRemove;
|
|
1078
|
+
if (filesToRemove.length === 0 && !removeOpenCode) {
|
|
543
1079
|
process.stdout.write(
|
|
544
1080
|
"agent-inspector onboard: no generated onboarding files match this package version\n",
|
|
545
1081
|
);
|
|
1082
|
+
if (openCodePlan !== null && openCodePlan.blockedMessage !== null) {
|
|
1083
|
+
process.stdout.write(`${openCodePlan.blockedMessage}\n`);
|
|
1084
|
+
}
|
|
546
1085
|
return 0;
|
|
547
1086
|
}
|
|
548
1087
|
|
|
@@ -552,6 +1091,11 @@ function runUninstall(
|
|
|
552
1091
|
removeEmptyDirectory(dirname(file.path));
|
|
553
1092
|
process.stdout.write(`Removed ${file.label}: ${file.path}\n`);
|
|
554
1093
|
}
|
|
1094
|
+
if (openCodePlan !== null && openCodePlan.shouldRemove && openCodePlan.body !== null) {
|
|
1095
|
+
mkdirSync(dirname(openCodePlan.status.path), { recursive: true });
|
|
1096
|
+
writeFileSync(openCodePlan.status.path, openCodePlan.body, "utf8");
|
|
1097
|
+
process.stdout.write(`Removed OpenCode MCP entry from: ${openCodePlan.status.path}\n`);
|
|
1098
|
+
}
|
|
555
1099
|
} catch (err) {
|
|
556
1100
|
const msg = err instanceof Error ? err.message : String(err);
|
|
557
1101
|
process.stderr.write(`agent-inspector onboard: failed to remove files: ${msg}\n`);
|
|
@@ -565,13 +1109,28 @@ function formatStatusVersion(status: GeneratedFileStatus): string {
|
|
|
565
1109
|
return status.version ?? "-";
|
|
566
1110
|
}
|
|
567
1111
|
|
|
568
|
-
function summarizeStatusAction(
|
|
1112
|
+
function summarizeStatusAction(
|
|
1113
|
+
statuses: readonly GeneratedFileStatus[],
|
|
1114
|
+
openCodeStatus: OpenCodeConfigStatus | null,
|
|
1115
|
+
): string {
|
|
569
1116
|
const actionable = statuses.filter(
|
|
570
1117
|
(status) => status.state === "missing" || status.state === "outdated",
|
|
571
1118
|
);
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1119
|
+
const actions = actionable.map((status) => status.action);
|
|
1120
|
+
if (openCodeStatus !== null && openCodeStatus.state === "missing") {
|
|
1121
|
+
actions.push(openCodeStatus.action);
|
|
1122
|
+
}
|
|
1123
|
+
if (actions.length > 0) {
|
|
1124
|
+
const uniqueActions = Array.from(new Set(actions));
|
|
1125
|
+
return `Run: ${uniqueActions.join(" && ")}`;
|
|
1126
|
+
}
|
|
1127
|
+
if (
|
|
1128
|
+
openCodeStatus !== null &&
|
|
1129
|
+
(openCodeStatus.state === "custom" ||
|
|
1130
|
+
openCodeStatus.state === "disabled" ||
|
|
1131
|
+
openCodeStatus.state === "invalid")
|
|
1132
|
+
) {
|
|
1133
|
+
return `OpenCode config needs review: ${openCodeStatus.action}`;
|
|
575
1134
|
}
|
|
576
1135
|
if (statuses.some((status) => status.state === "custom")) {
|
|
577
1136
|
return "Custom files are preserved. Use --force only if you want to replace them.";
|
|
@@ -586,10 +1145,11 @@ function runStatus(
|
|
|
586
1145
|
plannedFiles: readonly PlannedFile[],
|
|
587
1146
|
currentVersion: string,
|
|
588
1147
|
json: boolean,
|
|
1148
|
+
openCodeStatus: OpenCodeConfigStatus | null,
|
|
589
1149
|
): number {
|
|
590
1150
|
const enabledFiles = plannedFiles.filter((file) => file.enabled);
|
|
591
1151
|
const statuses = enabledFiles.map((file) => readGeneratedFileStatus(file, currentVersion));
|
|
592
|
-
const summary = summarizeStatusAction(statuses);
|
|
1152
|
+
const summary = summarizeStatusAction(statuses, openCodeStatus);
|
|
593
1153
|
|
|
594
1154
|
if (json) {
|
|
595
1155
|
process.stdout.write(
|
|
@@ -597,6 +1157,7 @@ function runStatus(
|
|
|
597
1157
|
{
|
|
598
1158
|
packageVersion: currentVersion,
|
|
599
1159
|
files: statuses,
|
|
1160
|
+
opencodeConfig: openCodeStatus,
|
|
600
1161
|
suggestedAction: summary,
|
|
601
1162
|
},
|
|
602
1163
|
null,
|
|
@@ -616,6 +1177,15 @@ function runStatus(
|
|
|
616
1177
|
process.stdout.write(` Action: ${status.action}\n`);
|
|
617
1178
|
}
|
|
618
1179
|
|
|
1180
|
+
if (openCodeStatus !== null) {
|
|
1181
|
+
process.stdout.write(`${openCodeStatus.label}: ${openCodeStatus.state}`);
|
|
1182
|
+
process.stdout.write(`, transport ${openCodeStatus.transport}\n`);
|
|
1183
|
+
process.stdout.write(` ${openCodeStatus.path}\n`);
|
|
1184
|
+
process.stdout.write(` Endpoint: ${openCodeStatus.endpoint}\n`);
|
|
1185
|
+
process.stdout.write(` Detail: ${openCodeStatus.detail}\n`);
|
|
1186
|
+
process.stdout.write(` Action: ${openCodeStatus.action}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
619
1189
|
process.stdout.write(`\nSuggested action: ${summary}\n`);
|
|
620
1190
|
return 0;
|
|
621
1191
|
}
|
|
@@ -638,23 +1208,25 @@ function runOnboardSync(argv: readonly string[]): number {
|
|
|
638
1208
|
const version = readPackageVersion();
|
|
639
1209
|
const plannedFiles = buildPlannedFiles(flags, targets, version);
|
|
640
1210
|
const enabledFiles = plannedFiles.filter((file) => file.enabled);
|
|
1211
|
+
const openCodePlan = isOpenCodeEnabled(flags) ? buildOpenCodePlan(flags) : null;
|
|
1212
|
+
const openCodeStatus = openCodePlan === null ? null : openCodePlan.status;
|
|
641
1213
|
|
|
642
1214
|
if (flags.json && !flags.status) {
|
|
643
1215
|
process.stderr.write("agent-inspector onboard: --json is only supported with --status\n");
|
|
644
1216
|
return 2;
|
|
645
1217
|
}
|
|
646
1218
|
|
|
647
|
-
if (enabledFiles.length === 0) {
|
|
1219
|
+
if (enabledFiles.length === 0 && openCodePlan === null) {
|
|
648
1220
|
process.stderr.write("agent-inspector onboard: no onboarding target selected\n");
|
|
649
1221
|
return 2;
|
|
650
1222
|
}
|
|
651
1223
|
|
|
652
1224
|
if (flags.status) {
|
|
653
|
-
return runStatus(plannedFiles, version, flags.json);
|
|
1225
|
+
return runStatus(plannedFiles, version, flags.json, openCodeStatus);
|
|
654
1226
|
}
|
|
655
1227
|
|
|
656
1228
|
if (flags.uninstall) {
|
|
657
|
-
return runUninstall(plannedFiles, version, flags.dryRun);
|
|
1229
|
+
return runUninstall(plannedFiles, version, flags.dryRun, openCodePlan);
|
|
658
1230
|
}
|
|
659
1231
|
|
|
660
1232
|
if (flags.dryRun) {
|
|
@@ -663,23 +1235,34 @@ function runOnboardSync(argv: readonly string[]): number {
|
|
|
663
1235
|
const status = existsSync(file.path) ? "exists" : "missing";
|
|
664
1236
|
process.stdout.write(`${file.label}: ${file.path} (${status})\n`);
|
|
665
1237
|
}
|
|
1238
|
+
if (openCodePlan !== null) {
|
|
1239
|
+
process.stdout.write(
|
|
1240
|
+
`${openCodePlan.status.label}: ${openCodePlan.status.path} (${openCodePlan.status.state})\n`,
|
|
1241
|
+
);
|
|
1242
|
+
process.stdout.write(` Transport: ${flags.opencodeTransport}\n`);
|
|
1243
|
+
process.stdout.write(` Endpoint: ${openCodePlan.status.endpoint}\n`);
|
|
1244
|
+
process.stdout.write(` Action: ${openCodePlan.status.action}\n`);
|
|
1245
|
+
if (openCodePlan.blockedMessage !== null) {
|
|
1246
|
+
process.stdout.write(` ${openCodePlan.blockedMessage}\n`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
666
1249
|
process.stdout.write(`\nClaude skill headings:\n`);
|
|
667
|
-
if (!flags.codexOnly) {
|
|
1250
|
+
if (!flags.codexOnly && !flags.opencodeOnly) {
|
|
668
1251
|
for (const heading of REQUIRED_PHASE_HEADINGS) {
|
|
669
1252
|
process.stdout.write(` - ${heading}\n`);
|
|
670
1253
|
}
|
|
671
1254
|
} else {
|
|
672
|
-
process.stdout.write(` (disabled by
|
|
1255
|
+
process.stdout.write(` (disabled by selected targets)\n`);
|
|
673
1256
|
}
|
|
674
1257
|
process.stdout.write(`\nCodex skill headings:\n`);
|
|
675
|
-
if (!flags.skipCodexSkill) {
|
|
1258
|
+
if (!flags.skipCodexSkill && !flags.opencodeOnly) {
|
|
676
1259
|
for (const heading of REQUIRED_CODEX_PHASE_HEADINGS) {
|
|
677
1260
|
process.stdout.write(` - ${heading}\n`);
|
|
678
1261
|
}
|
|
679
1262
|
} else {
|
|
680
|
-
process.stdout.write(` (disabled by
|
|
1263
|
+
process.stdout.write(` (disabled by selected targets)\n`);
|
|
681
1264
|
}
|
|
682
|
-
if (!flags.codexOnly) {
|
|
1265
|
+
if (!flags.codexOnly && !flags.opencodeOnly) {
|
|
683
1266
|
process.stdout.write(`\nDetected tools:\n${buildDetectedSummary()}\n`);
|
|
684
1267
|
}
|
|
685
1268
|
process.stdout.write(`\nNo files were written.\n`);
|
|
@@ -687,10 +1270,19 @@ function runOnboardSync(argv: readonly string[]): number {
|
|
|
687
1270
|
}
|
|
688
1271
|
|
|
689
1272
|
const filesToWrite = plannedFiles.filter((file) => shouldWrite(file, flags.force, version));
|
|
690
|
-
|
|
1273
|
+
const shouldWriteOpenCode =
|
|
1274
|
+
openCodePlan !== null && openCodePlan.shouldWrite && openCodePlan.body !== null;
|
|
1275
|
+
if (openCodePlan !== null && openCodePlan.status.state === "invalid") {
|
|
1276
|
+
process.stderr.write(`agent-inspector onboard: ${openCodePlan.blockedMessage ?? "invalid"}\n`);
|
|
1277
|
+
return 1;
|
|
1278
|
+
}
|
|
1279
|
+
if (filesToWrite.length === 0 && !shouldWriteOpenCode) {
|
|
691
1280
|
process.stdout.write(
|
|
692
1281
|
"agent-inspector onboard: all selected onboarding files are already up to date\n",
|
|
693
1282
|
);
|
|
1283
|
+
if (openCodePlan !== null && openCodePlan.blockedMessage !== null) {
|
|
1284
|
+
process.stdout.write(`${openCodePlan.blockedMessage}\n`);
|
|
1285
|
+
}
|
|
694
1286
|
process.stdout.write("Re-run with --force to refresh.\n");
|
|
695
1287
|
return 0;
|
|
696
1288
|
}
|
|
@@ -700,6 +1292,10 @@ function runOnboardSync(argv: readonly string[]): number {
|
|
|
700
1292
|
mkdirSync(dirname(file.path), { recursive: true });
|
|
701
1293
|
writeFileSync(file.path, file.body, "utf8");
|
|
702
1294
|
}
|
|
1295
|
+
if (shouldWriteOpenCode && openCodePlan !== null && openCodePlan.body !== null) {
|
|
1296
|
+
mkdirSync(dirname(openCodePlan.status.path), { recursive: true });
|
|
1297
|
+
writeFileSync(openCodePlan.status.path, openCodePlan.body, "utf8");
|
|
1298
|
+
}
|
|
703
1299
|
} catch (err) {
|
|
704
1300
|
const msg = err instanceof Error ? err.message : String(err);
|
|
705
1301
|
process.stderr.write(`agent-inspector onboard: failed to write files: ${msg}\n`);
|
|
@@ -712,14 +1308,26 @@ function runOnboardSync(argv: readonly string[]): number {
|
|
|
712
1308
|
for (const file of filesToWrite) {
|
|
713
1309
|
process.stdout.write(`Installed/updated ${file.label} to: ${file.path}\n`);
|
|
714
1310
|
}
|
|
1311
|
+
if (shouldWriteOpenCode && openCodePlan !== null) {
|
|
1312
|
+
process.stdout.write(`Installed/updated OpenCode MCP config: ${openCodePlan.status.path}\n`);
|
|
1313
|
+
}
|
|
715
1314
|
process.stdout.write(`\nNext steps:\n`);
|
|
716
|
-
if (!flags.codexOnly) {
|
|
1315
|
+
if (!flags.codexOnly && !flags.opencodeOnly) {
|
|
717
1316
|
process.stdout.write(` - Open Claude Code and run: /agent-inspector:onboard\n`);
|
|
718
1317
|
}
|
|
719
|
-
if (!flags.skipCodexSkill) {
|
|
1318
|
+
if (!flags.skipCodexSkill && !flags.opencodeOnly) {
|
|
720
1319
|
process.stdout.write(` - Open Codex and ask to use the agent-inspector-onboard skill\n`);
|
|
721
1320
|
}
|
|
722
|
-
|
|
723
|
-
|
|
1321
|
+
if (openCodePlan !== null) {
|
|
1322
|
+
process.stdout.write(` - Verify OpenCode MCP: opencode mcp list\n`);
|
|
1323
|
+
}
|
|
1324
|
+
if (flags.opencodeOnly) {
|
|
1325
|
+
process.stdout.write(
|
|
1326
|
+
` - Refresh OpenCode later: agent-inspector onboard --opencode-only --force\n`,
|
|
1327
|
+
);
|
|
1328
|
+
} else {
|
|
1329
|
+
process.stdout.write(` - Refresh later: agent-inspector onboard --force\n`);
|
|
1330
|
+
process.stdout.write(` - Detected primary tool: ${toolHint}\n`);
|
|
1331
|
+
}
|
|
724
1332
|
return 0;
|
|
725
1333
|
}
|