@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.
Files changed (35) hide show
  1. package/.output/cli.js +528 -24
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-hc5OoAls.js → CompareDrawer-p0uYxW7_.js} +1 -1
  4. package/.output/public/assets/{ProxyViewerContainer-BMKBs8Qz.js → ProxyViewerContainer-BGqeiYKD.js} +26 -26
  5. package/.output/public/assets/{ReplayDialog-CN-KM2nu.js → ReplayDialog-TTTVCMe9.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-B1kTlAUk.js → RequestAnatomy-c8ru0pS-.js} +1 -1
  7. package/.output/public/assets/{ResponseView-BchUjKjt.js → ResponseView-D59jTMy4.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-BQOxI9Hi.js → StreamingChunkSequence-UlAMVF9j.js} +1 -1
  9. package/.output/public/assets/_sessionId-EHPexdPl.js +1 -0
  10. package/.output/public/assets/index-CZP-XfpB.js +1 -0
  11. package/.output/public/assets/{main-Blykwzsn.js → main-CKYe6UUv.js} +2 -2
  12. package/.output/server/{_sessionId-BUNZz2zZ.mjs → _sessionId-CkwhOzdS.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-VaDYPM5G.mjs → CompareDrawer-BHDiuYMw.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-CpdWs7Pk.mjs → ProxyViewerContainer-0c2_DBMX.mjs} +57 -6
  15. package/.output/server/_ssr/{ReplayDialog-DbfIjJhb.mjs → ReplayDialog-CTjwzDDH.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-CrD8h6As.mjs → RequestAnatomy-D36oujv2.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-BalvDwtU.mjs → ResponseView-ClZ3oODE.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-RxatDbPV.mjs → StreamingChunkSequence-Ck5l4p1m.mjs} +2 -2
  19. package/.output/server/_ssr/{index-11qAtV9d.mjs → index-DKBYH0TC.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-C4OdRnqX.mjs → router-D4I3ZyVS.mjs} +434 -6
  22. package/.output/server/{_tanstack-start-manifest_v-F6bwcl33.mjs → _tanstack-start-manifest_v-D4vSn_wD.mjs} +1 -1
  23. package/.output/server/index.mjs +54 -54
  24. package/README.md +7 -0
  25. package/package.json +1 -1
  26. package/src/cli/onboard.ts +629 -21
  27. package/src/components/providers/SettingsDialog.tsx +30 -0
  28. package/src/components/proxy-viewer/LogEntry.tsx +15 -1
  29. package/src/components/proxy-viewer/LogEntryHeader.tsx +20 -0
  30. package/src/contracts/log.ts +1 -0
  31. package/src/proxy/logFinalizer.ts +58 -0
  32. package/src/proxy/logSearch.ts +1 -0
  33. package/src/proxy/toolSchemaWarnings.ts +515 -0
  34. package/.output/public/assets/_sessionId-CVw5e8TK.js +0 -1
  35. package/.output/public/assets/index-BdUTMQL6.js +0 -1
@@ -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
- if (filesToRemove.length === 0) {
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(statuses: readonly GeneratedFileStatus[]): string {
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
- if (actionable.length > 0) {
573
- const actions = Array.from(new Set(actionable.map((status) => status.action)));
574
- return `Run: ${actions.join(" && ")}`;
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 --codex-only)\n`);
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 --skip-codex-skill)\n`);
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
- if (filesToWrite.length === 0) {
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
- process.stdout.write(` - Refresh later: agent-inspector onboard --force\n`);
723
- process.stdout.write(` - Detected primary tool: ${toolHint}\n`);
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
  }