nexus-agents 2.26.1 → 2.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +7 -7
  2. package/dist/{chunk-X33QNBGA.js → chunk-E7EX2KQJ.js} +3 -5
  3. package/dist/{chunk-X33QNBGA.js.map → chunk-E7EX2KQJ.js.map} +1 -1
  4. package/dist/{chunk-BOWNZMPH.js → chunk-L2SHSW4T.js} +3017 -1300
  5. package/dist/chunk-L2SHSW4T.js.map +1 -0
  6. package/dist/{chunk-ARNVVQ5W.js → chunk-LKSTILEE.js} +1213 -117
  7. package/dist/chunk-LKSTILEE.js.map +1 -0
  8. package/dist/{chunk-L3LQ3RP5.js → chunk-QZEAD6AG.js} +10339 -6289
  9. package/dist/chunk-QZEAD6AG.js.map +1 -0
  10. package/dist/{chunk-LCHCASB7.js → chunk-UGNLR4NZ.js} +2 -2
  11. package/dist/{chunk-UVQ7R4C4.js → chunk-YSDUVCCZ.js} +137 -717
  12. package/dist/chunk-YSDUVCCZ.js.map +1 -0
  13. package/dist/cli.d.ts +8 -1
  14. package/dist/cli.js +644 -216
  15. package/dist/cli.js.map +1 -1
  16. package/dist/{dist-Y5F6UM2N.js → dist-H5XNXVAV.js} +1384 -1295
  17. package/dist/dist-H5XNXVAV.js.map +1 -0
  18. package/dist/doctor-deep-BDE2PHVX.js +11 -0
  19. package/dist/index.d.ts +4299 -7411
  20. package/dist/index.js +588 -132
  21. package/dist/index.js.map +1 -1
  22. package/dist/{setup-command-VNF3KTCJ.js → setup-command-SS7LMN7Y.js} +5 -6
  23. package/dist/setup-config-DSMOOLVW.js +9 -0
  24. package/dist/workflows/templates/code-review.yaml +1 -1
  25. package/dist/workflows/templates/refactoring.yaml +1 -1
  26. package/dist/workflows/templates/research-review.yaml +19 -4
  27. package/dist/workflows/templates/security-audit.yaml +1 -1
  28. package/dist/workflows/templates/standards-review.yaml +1 -1
  29. package/package.json +12 -12
  30. package/src/workflows/templates/code-review.yaml +1 -1
  31. package/src/workflows/templates/refactoring.yaml +1 -1
  32. package/src/workflows/templates/research-review.yaml +19 -4
  33. package/src/workflows/templates/security-audit.yaml +1 -1
  34. package/src/workflows/templates/standards-review.yaml +1 -1
  35. package/dist/chunk-ARNVVQ5W.js.map +0 -1
  36. package/dist/chunk-BOWNZMPH.js.map +0 -1
  37. package/dist/chunk-L3LQ3RP5.js.map +0 -1
  38. package/dist/chunk-LCDOP543.js +0 -365
  39. package/dist/chunk-LCDOP543.js.map +0 -1
  40. package/dist/chunk-PGNRXCYY.js +0 -776
  41. package/dist/chunk-PGNRXCYY.js.map +0 -1
  42. package/dist/chunk-UVQ7R4C4.js.map +0 -1
  43. package/dist/dist-Y5F6UM2N.js.map +0 -1
  44. package/dist/doctor-deep-I2J5CRFG.js +0 -13
  45. package/dist/setup-config-VQSWWJ5O.js +0 -9
  46. /package/dist/{chunk-LCHCASB7.js.map → chunk-UGNLR4NZ.js.map} +0 -0
  47. /package/dist/{doctor-deep-I2J5CRFG.js.map → doctor-deep-BDE2PHVX.js.map} +0 -0
  48. /package/dist/{setup-command-VNF3KTCJ.js.map → setup-command-SS7LMN7Y.js.map} +0 -0
  49. /package/dist/{setup-config-VQSWWJ5O.js.map → setup-config-DSMOOLVW.js.map} +0 -0
@@ -2,30 +2,37 @@ import {
2
2
  BACKOFF_CONFIG,
3
3
  CLI_SUBPROCESS_TIMEOUTS,
4
4
  CLI_TIMEOUTS,
5
- CODEX_MCP_TIMEOUTS,
6
- getCliTimeout
7
- } from "./chunk-LCDOP543.js";
8
- import {
9
5
  CLI_VERSION_REQUIREMENTS,
6
+ CODEX_MCP_TIMEOUTS,
10
7
  DEFAULT_CAPABILITIES,
11
8
  DEFAULT_MODEL_CAPABILITIES,
12
9
  DEFAULT_MODEL_PER_CLI,
13
10
  ErrorCode,
11
+ LEARNING_DIR,
14
12
  NexusError,
13
+ OUTCOMES_FILE,
14
+ RULES_FILE,
15
15
  buildModelInfo,
16
16
  clampPercent,
17
+ colors,
17
18
  createLogger,
19
+ detectTaskCategory,
18
20
  err,
19
21
  getCliModelName,
22
+ getCliTimeout,
20
23
  getDefaultModelForCli,
21
24
  getErrorMessage,
25
+ getOutcomeStore,
22
26
  getRandomProvider,
23
27
  getTimeProvider,
24
- ok
25
- } from "./chunk-BOWNZMPH.js";
28
+ isPersistenceEnabled,
29
+ ok,
30
+ symbols,
31
+ writeLine
32
+ } from "./chunk-L2SHSW4T.js";
26
33
 
27
34
  // src/version.ts
28
- var VERSION = true ? "2.26.1" : "dev";
35
+ var VERSION = true ? "2.28.0" : "dev";
29
36
 
30
37
  // src/utils/async-utils.ts
31
38
  function sleep(ms) {
@@ -60,7 +67,10 @@ function estimateTaskComplexity(taskDescription) {
60
67
  "entire",
61
68
  "comprehensive",
62
69
  "deep analysis",
63
- "system-wide"
70
+ "system-wide",
71
+ "security",
72
+ "audit",
73
+ "vulnerability"
64
74
  ];
65
75
  if (complexIndicators.some((indicator) => lower.includes(indicator))) {
66
76
  return "complex";
@@ -71,6 +81,30 @@ function estimateTaskComplexity(taskDescription) {
71
81
  }
72
82
  return "standard";
73
83
  }
84
+ var ADAPTIVE_TIMEOUT_MIN_SAMPLES = 10;
85
+ var ADAPTIVE_TIMEOUT_MARGIN = 1.2;
86
+ function computeP95(sorted) {
87
+ if (sorted.length === 0) return 0;
88
+ const idx = Math.ceil(sorted.length * 0.95) - 1;
89
+ return sorted[Math.min(idx, sorted.length - 1)];
90
+ }
91
+ function getAdaptiveTimeout(cli, taskDescription, options) {
92
+ const complexity = estimateTaskComplexity(taskDescription);
93
+ const staticTimeout = getCliTimeout(cli, complexity);
94
+ const match = detectTaskCategory(taskDescription);
95
+ if (match === null) return staticTimeout;
96
+ const store = options?.store ?? getOutcomeStore();
97
+ const outcomes = store.query({
98
+ cli,
99
+ category: match.category,
100
+ success: true
101
+ });
102
+ if (outcomes.length < ADAPTIVE_TIMEOUT_MIN_SAMPLES) return staticTimeout;
103
+ const durations = outcomes.map((o) => o.durationMs).sort((a, b) => a - b);
104
+ const p95 = computeP95(durations);
105
+ const adaptiveTimeout = Math.round(p95 * ADAPTIVE_TIMEOUT_MARGIN);
106
+ return Math.max(staticTimeout, adaptiveTimeout);
107
+ }
74
108
 
75
109
  // src/cli-adapters/cli-timeout-profiles.ts
76
110
  var CLI_TIMEOUT_PROFILES = {
@@ -86,8 +120,7 @@ function estimateTaskComplexity2(taskDescription) {
86
120
  return estimateTaskComplexity(taskDescription);
87
121
  }
88
122
  function getTimeoutForTaskAuto(cli, taskDescription) {
89
- const complexity = estimateTaskComplexity(taskDescription);
90
- return getCliTimeout(cli, complexity);
123
+ return getAdaptiveTimeout(cli, taskDescription);
91
124
  }
92
125
 
93
126
  // src/cli-adapters/base-adapter.ts
@@ -227,6 +260,7 @@ function createCapacityTracker(cli) {
227
260
 
228
261
  // src/cli-adapters/base-adapter.ts
229
262
  var execAsync = promisify(exec);
263
+ var DEFAULT_CAPACITY_FALLBACK = 1e5;
230
264
  var DEFAULT_OPTIONS = {
231
265
  timeoutMs: 6e4,
232
266
  // 1 minute (reduced from 2 minutes per Issue #280)
@@ -242,8 +276,8 @@ var BaseCliAdapter = class {
242
276
  initialized = false;
243
277
  lastHealthCheck;
244
278
  cachedVersion;
245
- constructor(logger2) {
246
- this.logger = logger2 ?? createLogger({ component: "cli-adapter" });
279
+ constructor(logger6) {
280
+ this.logger = logger6 ?? createLogger({ component: "cli-adapter" });
247
281
  }
248
282
  /**
249
283
  * Initializes the capacity tracker.
@@ -385,9 +419,13 @@ var BaseCliAdapter = class {
385
419
  */
386
420
  getCapacity() {
387
421
  if (this.capacityTracker === null) {
422
+ this.logger.warn("Capacity tracker uninitialized, returning default fallback", {
423
+ cli: this.name,
424
+ fallbackTokens: DEFAULT_CAPACITY_FALLBACK
425
+ });
388
426
  return Promise.resolve({
389
- remainingTokens: Number.MAX_SAFE_INTEGER,
390
- remainingRequests: Number.MAX_SAFE_INTEGER,
427
+ remainingTokens: DEFAULT_CAPACITY_FALLBACK,
428
+ remainingRequests: DEFAULT_CAPACITY_FALLBACK,
391
429
  resetTime: new Date(getTimeProvider().now() + 36e5),
392
430
  utilizationPercent: 0,
393
431
  exhausted: false
@@ -493,19 +531,145 @@ function isRateLimitOutput(stdout) {
493
531
  const lower = stdout.toLowerCase();
494
532
  return RATE_LIMIT_PATTERNS.some((pattern) => lower.includes(pattern));
495
533
  }
534
+ var PLAINTEXT_FALLBACK_MIN_LENGTH = 30;
535
+ function tryPlaintextFallback(stdout) {
536
+ const trimmed = stdout.trim();
537
+ if (trimmed.length < PLAINTEXT_FALLBACK_MIN_LENGTH) return null;
538
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return null;
539
+ return trimmed;
540
+ }
541
+ var STDERR_ERROR_PATTERNS = [
542
+ "error:",
543
+ "fatal:",
544
+ "panic:",
545
+ "unhandled",
546
+ "not found",
547
+ "invalid model",
548
+ "authentication",
549
+ "unauthorized",
550
+ "permission denied",
551
+ "connection refused",
552
+ "econnrefused",
553
+ "enotfound",
554
+ "timeout",
555
+ "failed to connect",
556
+ "invalid api key",
557
+ "rate limit",
558
+ "quota exceeded",
559
+ "service unavailable"
560
+ ];
561
+ function looksLikeErrorStderr(stderr) {
562
+ const lower = stderr.toLowerCase();
563
+ return STDERR_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));
564
+ }
565
+ var STDERR_CONNECTION_PATTERNS = [
566
+ "connection refused",
567
+ "econnrefused",
568
+ "enotfound",
569
+ "econnreset",
570
+ "failed to connect",
571
+ "service unavailable",
572
+ "eaddrinuse",
573
+ "address already in use"
574
+ ];
575
+ var STDERR_RATE_LIMIT_PATTERNS = ["rate limit", "quota exceeded", "429", "too many requests"];
576
+ var STDERR_TIMEOUT_PATTERNS = ["timeout", "timed out", "etimedout"];
577
+ function classifyStderrError(stderr) {
578
+ const lower = stderr.toLowerCase();
579
+ if (STDERR_CONNECTION_PATTERNS.some((p) => lower.includes(p))) return "CONNECTION_ERROR";
580
+ if (STDERR_RATE_LIMIT_PATTERNS.some((p) => lower.includes(p))) return "RATE_LIMITED";
581
+ if (STDERR_TIMEOUT_PATTERNS.some((p) => lower.includes(p))) return "TIMEOUT";
582
+ return "EXECUTION_ERROR";
583
+ }
584
+ var subprocessLogger = createLogger({ component: "subprocess-adapter" });
585
+ var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
586
+ var TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set([
587
+ "TIMEOUT",
588
+ "RATE_LIMITED",
589
+ "CONNECTION_ERROR",
590
+ "PARSE_ERROR"
591
+ ]);
592
+ var TRANSIENT_RETRY_DELAYS_MS = [500, 1e3];
593
+ var MAX_TRANSIENT_RETRIES = TRANSIENT_RETRY_DELAYS_MS.length;
594
+ var MAX_PARSE_RETRIES = 1;
595
+ var TIMEOUT_RETRY_MULTIPLIER = 1.5;
596
+ function isTransientError(code) {
597
+ return TRANSIENT_ERROR_CODES.has(code);
598
+ }
599
+ var TRANSIENT_EXIT_CODES = /* @__PURE__ */ new Set([137, 143]);
600
+ var EXIT_CODE_SIGNAL_NAMES = {
601
+ 137: "SIGKILL (OOM or killed)",
602
+ 143: "SIGTERM (terminated)"
603
+ };
604
+ function buildSignalExitMessage(code) {
605
+ const signalName = EXIT_CODE_SIGNAL_NAMES[code];
606
+ if (signalName !== void 0) return `Process killed by ${signalName}, exit code ${String(code)}`;
607
+ return `Process exited with code ${String(code)}`;
608
+ }
609
+ function isTransientExitCode(code) {
610
+ return code !== null && TRANSIENT_EXIT_CODES.has(code);
611
+ }
612
+ function appendBuffered(state, stream, data) {
613
+ const bytesKey = stream === "stdout" ? "stdoutBytes" : "stderrBytes";
614
+ const truncKey = stream === "stdout" ? "stdoutTruncated" : "stderrTruncated";
615
+ if (state[bytesKey] < MAX_BUFFER_BYTES) {
616
+ state[stream] += data.toString();
617
+ state[bytesKey] += data.length;
618
+ } else if (!state[truncKey]) {
619
+ state[truncKey] = true;
620
+ subprocessLogger.warn(`${stream} buffer exceeded 10 MB, truncating`);
621
+ }
622
+ }
496
623
  var SubprocessCliAdapter = class extends BaseCliAdapter {
497
624
  transport = "subprocess";
625
+ /** Transient-error retry config. Override in subclass to enable. */
626
+ transientRetry = { enabled: true };
498
627
  /**
499
- * Executes a task via subprocess using spawn for proper argument handling.
500
- * Using spawn avoids shell escaping issues with multi-line content.
501
- * If stdin is provided in command config, it is written to process stdin.
628
+ * Executes a task via subprocess, with optional transient-error retry.
629
+ * When `transientRetry.enabled` is true, transient errors (timeout,
630
+ * rate_limit, connection, parse) are retried with exponential backoff
631
+ * (500ms, 1000ms). Parse errors get max 1 retry (#1533); others get 2.
502
632
  */
503
633
  async executeTask(task, options) {
634
+ const result = await this.spawnSubprocess(task, options);
635
+ if (result.ok || !this.transientRetry.enabled) return result;
636
+ if (!isTransientError(result.error.code)) return result;
637
+ return this.retryTransient(task, options, result, 0);
638
+ }
639
+ /**
640
+ * Retries a transient error with bounded exponential backoff.
641
+ */
642
+ async retryTransient(task, options, lastResult, attempt) {
643
+ const isParseError = !lastResult.ok && lastResult.error.code === "PARSE_ERROR";
644
+ const maxRetries = isParseError ? MAX_PARSE_RETRIES : MAX_TRANSIENT_RETRIES;
645
+ if (attempt >= maxRetries) return lastResult;
646
+ const delayMs = TRANSIENT_RETRY_DELAYS_MS[attempt];
647
+ const isTimeout = !lastResult.ok && lastResult.error.code === "TIMEOUT";
648
+ subprocessLogger.debug("Retrying transient error", {
649
+ cli: this.name,
650
+ attempt: attempt + 1,
651
+ delayMs,
652
+ errorCode: !lastResult.ok ? lastResult.error.code : void 0
653
+ });
654
+ await this.delay(delayMs);
655
+ const retryOptions = isTimeout ? { ...options, timeoutMs: Math.round(options.timeoutMs * TIMEOUT_RETRY_MULTIPLIER) } : options;
656
+ const result = await this.spawnSubprocess(task, retryOptions);
657
+ if (result.ok) return result;
658
+ if (!isTransientError(result.error.code)) return result;
659
+ return this.retryTransient(task, retryOptions, result, attempt + 1);
660
+ }
661
+ /**
662
+ * Spawns a single subprocess execution (no retry).
663
+ */
664
+ spawnSubprocess(task, options) {
504
665
  const cmdConfig = this.getCommand(task);
505
666
  const startTime = getTimeProvider().now();
506
667
  return new Promise((resolve) => {
668
+ const childEnv = { ...process.env };
669
+ delete childEnv["CLAUDECODE"];
507
670
  const child = spawn(cmdConfig.command, cmdConfig.args, {
508
- stdio: ["pipe", "pipe", "pipe"]
671
+ stdio: ["pipe", "pipe", "pipe"],
672
+ env: childEnv
509
673
  });
510
674
  const onProgress = options.onProgress;
511
675
  const state = this.setupChildProcessHandlers(
@@ -526,7 +690,15 @@ var SubprocessCliAdapter = class extends BaseCliAdapter {
526
690
  * Sets up child process event handlers for output collection and error handling.
527
691
  */
528
692
  setupChildProcessHandlers(child, startTime, timeoutMs, resolve, onProgress) {
529
- const state = { stdout: "", stderr: "", resolved: false };
693
+ const state = {
694
+ stdout: "",
695
+ stderr: "",
696
+ resolved: false,
697
+ stdoutBytes: 0,
698
+ stderrBytes: 0,
699
+ stdoutTruncated: false,
700
+ stderrTruncated: false
701
+ };
530
702
  const resolveOnce = (result) => {
531
703
  if (!state.resolved) {
532
704
  state.resolved = true;
@@ -535,35 +707,47 @@ var SubprocessCliAdapter = class extends BaseCliAdapter {
535
707
  };
536
708
  if (child.stdout !== null) {
537
709
  child.stdout.on("data", (data) => {
538
- state.stdout += data.toString();
710
+ appendBuffered(state, "stdout", data);
539
711
  onProgress?.();
540
712
  });
541
713
  }
542
714
  if (child.stderr !== null) {
543
715
  child.stderr.on("data", (data) => {
544
- state.stderr += data.toString();
716
+ appendBuffered(state, "stderr", data);
545
717
  });
546
718
  }
547
719
  child.on("error", (error) => {
548
720
  resolveOnce(this.handleSubprocessError(error));
549
721
  });
550
- child.on("close", (code) => {
551
- if (code !== 0 && state.stdout === "") {
552
- const msg = state.stderr !== "" ? state.stderr : `Process exited with code ${String(code)}`;
553
- resolveOnce(err(this.createError("EXECUTION_ERROR", msg)));
554
- return;
555
- }
556
- resolveOnce(this.handleSubprocessOutput(state.stdout, state.stderr, startTime));
557
- });
558
722
  const timeoutId = setTimeout(() => {
559
723
  child.kill("SIGTERM");
560
724
  resolveOnce(err(this.createError("TIMEOUT", "Execution timed out")));
561
725
  }, timeoutMs);
562
- child.on("close", () => {
726
+ child.on("close", (code) => {
563
727
  clearTimeout(timeoutId);
728
+ resolveOnce(this.classifyCloseResult(code, state, startTime));
564
729
  });
565
730
  return state;
566
731
  }
732
+ /** Classify a subprocess close event into a Result. */
733
+ classifyCloseResult(code, state, startTime) {
734
+ if (code !== 0 && state.stdout === "") {
735
+ if (state.stderr !== "") {
736
+ const msg2 = state.stderr;
737
+ return err(this.createError(classifyStderrError(state.stderr), msg2));
738
+ }
739
+ const msg = code !== null ? buildSignalExitMessage(code) : "Process exited with unknown code";
740
+ if (isTransientExitCode(code)) {
741
+ return err(this.createError("CONNECTION_ERROR", msg));
742
+ }
743
+ return err(this.createError("EXECUTION_ERROR", msg));
744
+ }
745
+ if (code !== 0 && state.stderr !== "" && looksLikeErrorStderr(state.stderr)) {
746
+ const msg = `Exit code ${String(code)}: ${state.stderr.slice(0, 500).trim()}`;
747
+ return err(this.createError(classifyStderrError(state.stderr), msg));
748
+ }
749
+ return this.handleSubprocessOutput(state.stdout, state.stderr, startTime);
750
+ }
567
751
  /**
568
752
  * Handles successful subprocess output.
569
753
  */
@@ -574,11 +758,24 @@ var SubprocessCliAdapter = class extends BaseCliAdapter {
574
758
  const text = this.parser.extractResponse(stdout);
575
759
  if (text === null) {
576
760
  if (isRateLimitOutput(stdout)) {
577
- const snippet2 = stdout.slice(0, 200).trim();
761
+ const snippet2 = stdout.slice(0, 500).trim();
578
762
  return err(this.createError("RATE_LIMITED", snippet2));
579
763
  }
580
- const snippet = stdout.slice(0, 200).trim();
581
- return err(this.createError("PARSE_ERROR", `Failed to parse response: ${snippet}`));
764
+ const plaintext = tryPlaintextFallback(stdout);
765
+ if (plaintext !== null) {
766
+ subprocessLogger.debug("Using plaintext fallback for unparseable output");
767
+ return ok(
768
+ this.normalizeResponse(plaintext, void 0, {
769
+ durationMs: getTimeProvider().now() - startTime,
770
+ raw: stdout
771
+ })
772
+ );
773
+ }
774
+ const snippet = stdout.slice(0, 500).trim();
775
+ const stderrHint = stderr !== "" ? ` [stderr: ${stderr.slice(0, 300).trim()}]` : "";
776
+ return err(
777
+ this.createError("PARSE_ERROR", `Failed to parse response: ${snippet}${stderrHint}`)
778
+ );
582
779
  }
583
780
  const usage = this.parser.extractUsage(stdout);
584
781
  const sessionId = this.parser.extractSessionId(stdout);
@@ -642,6 +839,7 @@ function extractNumberField(record, key) {
642
839
  }
643
840
 
644
841
  // src/cli-adapters/parsers/claude-parser.ts
842
+ var logger = createLogger({ component: "claude-parser" });
645
843
  var ClaudeResponseParser = class {
646
844
  name = "claude-parser";
647
845
  supportedVersionRange = ">=2.0.0 <3.0.0";
@@ -656,6 +854,7 @@ var ClaudeResponseParser = class {
656
854
  }
657
855
  return data;
658
856
  } catch {
857
+ logger.debug("Skipped malformed output line", { snippet: raw.slice(0, 100) });
659
858
  return null;
660
859
  }
661
860
  }
@@ -677,6 +876,7 @@ var ClaudeResponseParser = class {
677
876
  }
678
877
  return null;
679
878
  } catch {
879
+ logger.debug("Skipped malformed output line", { snippet: raw.slice(0, 100) });
680
880
  return null;
681
881
  }
682
882
  }
@@ -703,6 +903,7 @@ var ClaudeResponseParser = class {
703
903
  ...cachedInputTokens !== null && { cachedInputTokens }
704
904
  };
705
905
  } catch {
906
+ logger.debug("Skipped malformed output line", { snippet: raw.slice(0, 100) });
706
907
  return null;
707
908
  }
708
909
  }
@@ -720,6 +921,7 @@ var ClaudeResponseParser = class {
720
921
  }
721
922
  return null;
722
923
  } catch {
924
+ logger.debug("Skipped malformed output line", { snippet: raw.slice(0, 100) });
723
925
  return null;
724
926
  }
725
927
  }
@@ -743,6 +945,7 @@ function buildClaudeAliasMap() {
743
945
  }
744
946
  }
745
947
  map["claude-sonnet-4"] = "sonnet";
948
+ map["claude-sonnet-4-6"] = "sonnet";
746
949
  map["claude-sonnet-4-5-20250929"] = "sonnet";
747
950
  map["claude-opus-4"] = "opus";
748
951
  map["claude-opus-4-6"] = "opus";
@@ -808,6 +1011,20 @@ var ClaudeCliAdapter = class extends SubprocessCliAdapter {
808
1011
  costPerMillionOutput: CLAUDE_LEGACY_DEFAULTS.outputCosts[this.model] ?? CLAUDE_LEGACY_DEFAULTS.outputCost
809
1012
  };
810
1013
  }
1014
+ /** Appends optional string-type task options to CLI args. */
1015
+ appendTaskOptions(args, task) {
1016
+ const workDir = task.options?.["workDir"];
1017
+ if (typeof workDir === "string" && workDir.length > 0) {
1018
+ args.push("--add-dir", workDir);
1019
+ }
1020
+ const mcpConfigPath = task.options?.["mcpConfigPath"];
1021
+ if (typeof mcpConfigPath === "string" && mcpConfigPath.length > 0) {
1022
+ args.push("--mcp-config", mcpConfigPath);
1023
+ }
1024
+ if (task.options?.["skipPermissions"] === true) {
1025
+ args.push("--dangerously-skip-permissions");
1026
+ }
1027
+ }
811
1028
  /**
812
1029
  * Gets CLI command and arguments for execution.
813
1030
  * Uses stdin for the prompt to avoid argument escaping issues,
@@ -824,10 +1041,7 @@ var ClaudeCliAdapter = class extends SubprocessCliAdapter {
824
1041
  if (task.sessionId !== void 0 && task.sessionId !== "") {
825
1042
  args.push("--resume", task.sessionId);
826
1043
  }
827
- const workDir = task.options?.["workDir"];
828
- if (typeof workDir === "string" && workDir.length > 0) {
829
- args.push("--add-dir", workDir);
830
- }
1044
+ this.appendTaskOptions(args, task);
831
1045
  return { command: "claude", args, stdin: task.content };
832
1046
  }
833
1047
  };
@@ -1133,12 +1347,20 @@ var DEFAULT_CIRCUIT_BREAKER_CONFIG = {
1133
1347
  halfOpenSuccessThreshold: 2,
1134
1348
  countTimeoutsAsFailures: true,
1135
1349
  countAuthFailuresAsFailures: false,
1350
+ countRateLimitsAsFailures: true,
1136
1351
  halfOpenMaxRequests: 3
1137
1352
  };
1138
1353
  var TIMEOUT_PATTERNS = ["timeout", "timed out"];
1139
1354
  var AUTH_PATTERNS = ["auth", "unauthorized", "forbidden", "oauth"];
1140
1355
  var RATE_LIMIT_PATTERNS2 = ["rate limit", "too many requests", "429"];
1141
- var CONNECTION_PATTERNS = ["connection", "econnrefused", "enotfound", "mcp"];
1356
+ var CONNECTION_PATTERNS = [
1357
+ "connection",
1358
+ "econnrefused",
1359
+ "enotfound",
1360
+ "mcp",
1361
+ "eaddrinuse",
1362
+ "address already in use"
1363
+ ];
1142
1364
  var CRASH_PATTERNS = ["crash", "exited", "killed", "sigterm", "sigkill"];
1143
1365
  function matchesPatterns(text, patterns) {
1144
1366
  return patterns.some((pattern) => text.includes(pattern));
@@ -1325,6 +1547,7 @@ var CliCircuitBreaker = class {
1325
1547
  shouldCountFailure(category) {
1326
1548
  if (category === "timeout") return this.config.countTimeoutsAsFailures;
1327
1549
  if (category === "authentication") return this.config.countAuthFailuresAsFailures;
1550
+ if (category === "rate_limit") return this.config.countRateLimitsAsFailures;
1328
1551
  return true;
1329
1552
  }
1330
1553
  createExecutionError(error, category) {
@@ -1678,6 +1901,7 @@ var GeminiCliAdapter = class extends SubprocessCliAdapter {
1678
1901
  };
1679
1902
 
1680
1903
  // src/cli-adapters/parsers/codex-parser.ts
1904
+ var logger2 = createLogger({ component: "codex-parser" });
1681
1905
  var CodexResponseParser = class {
1682
1906
  name = "codex-parser";
1683
1907
  supportedVersionRange = ">=0.70.0 <1.0.0";
@@ -1728,8 +1952,8 @@ var CodexResponseParser = class {
1728
1952
  const usage = this.extractUsageFromEvent(record);
1729
1953
  if (usage !== null) setUsage(usage);
1730
1954
  }
1731
- } catch (lineErr) {
1732
- void lineErr;
1955
+ } catch {
1956
+ logger2.debug("Skipped malformed NDJSON line", { snippet: line.slice(0, 100) });
1733
1957
  }
1734
1958
  }
1735
1959
  /**
@@ -1758,6 +1982,7 @@ var CodexResponseParser = class {
1758
1982
  return this.extractUsageFromEvent(record);
1759
1983
  }
1760
1984
  } catch {
1985
+ logger2.debug("Skipped malformed NDJSON line", { snippet: line.slice(0, 100) });
1761
1986
  continue;
1762
1987
  }
1763
1988
  }
@@ -1781,6 +2006,7 @@ var CodexResponseParser = class {
1781
2006
  }
1782
2007
  }
1783
2008
  } catch {
2009
+ logger2.debug("Skipped malformed NDJSON line", { snippet: line.slice(0, 100) });
1784
2010
  continue;
1785
2011
  }
1786
2012
  }
@@ -2073,7 +2299,18 @@ var CodexMcpAdapter = class extends BaseCliAdapter {
2073
2299
  }
2074
2300
  };
2075
2301
 
2302
+ // src/cli-adapters/adapters/opencode-adapter.ts
2303
+ import { execFile } from "child_process";
2304
+
2076
2305
  // src/cli-adapters/parsers/opencode-parser.ts
2306
+ var logger3 = createLogger({ component: "opencode-parser" });
2307
+ var PLAINTEXT_MIN_LENGTH = 10;
2308
+ function looksLikeNdjson(text) {
2309
+ const lines = text.split("\n").filter((l) => l.trim() !== "");
2310
+ if (lines.length === 0) return false;
2311
+ const jsonLineCount = lines.filter((l) => l.trimStart().startsWith("{")).length;
2312
+ return jsonLineCount > lines.length / 2;
2313
+ }
2077
2314
  var OpenCodeResponseParser = class {
2078
2315
  name = "opencode-parser";
2079
2316
  supportedVersionRange = ">=1.0.0 <2.0.0";
@@ -2082,22 +2319,53 @@ var OpenCodeResponseParser = class {
2082
2319
  */
2083
2320
  parse(raw) {
2084
2321
  const lines = raw.trim().split("\n");
2322
+ const state = this.processAllLines(lines);
2323
+ if (state.contentParts.length === 0) {
2324
+ return this.handleEmptyContent(raw, lines.length, state);
2325
+ }
2326
+ return this.buildResponse(state.contentParts.join(""), state.sessionId, state.usage);
2327
+ }
2328
+ /** Processes all NDJSON lines and returns aggregated state. */
2329
+ processAllLines(lines) {
2085
2330
  let sessionId;
2086
2331
  const contentParts = [];
2087
2332
  let usage;
2088
- for (const line of lines) {
2089
- if (line.trim() === "") continue;
2090
- this.processLine(
2333
+ let hasStepEvents = false;
2334
+ let hasAnyRecognizedEvent = false;
2335
+ for (let idx = 0; idx < lines.length; idx++) {
2336
+ const line = lines[idx];
2337
+ if (line === void 0 || line.trim() === "") continue;
2338
+ const hadEvent = this.processLine(
2091
2339
  line,
2092
2340
  contentParts,
2093
2341
  (id) => sessionId = id,
2094
- (u) => usage = u
2342
+ (u) => usage = u,
2343
+ idx
2344
+ );
2345
+ if (hadEvent) hasStepEvents = true;
2346
+ if (hadEvent || this.isRecognizedLegacyEvent(line)) hasAnyRecognizedEvent = true;
2347
+ }
2348
+ return { sessionId, contentParts, usage, hasStepEvents, hasAnyRecognizedEvent };
2349
+ }
2350
+ /** Handles the case where no text content was extracted from NDJSON. */
2351
+ handleEmptyContent(raw, lineCount, state) {
2352
+ if (state.hasStepEvents) {
2353
+ return this.buildResponse(
2354
+ "[Tool-only response \u2014 no text output]",
2355
+ state.sessionId,
2356
+ state.usage
2095
2357
  );
2096
2358
  }
2097
- if (contentParts.length === 0) {
2098
- return this.parsePlainJson(raw);
2099
- }
2100
- const content = contentParts.join("");
2359
+ logger3.debug("No NDJSON content extracted, trying JSON fallback", {
2360
+ rawLength: raw.length,
2361
+ lineCount,
2362
+ hasStepEvents: state.hasStepEvents,
2363
+ hasAnyRecognizedEvent: state.hasAnyRecognizedEvent
2364
+ });
2365
+ return this.parsePlainJson(raw, state.hasAnyRecognizedEvent);
2366
+ }
2367
+ /** Builds an OpenCodeCliResponse from parsed components. */
2368
+ buildResponse(content, sessionId, usage) {
2101
2369
  return {
2102
2370
  content,
2103
2371
  ...sessionId !== void 0 && { sessionId },
@@ -2110,6 +2378,11 @@ var OpenCodeResponseParser = class {
2110
2378
  extractResponse(raw) {
2111
2379
  const parsed = this.parse(raw);
2112
2380
  if (parsed === null || parsed.content === "") {
2381
+ logger3.debug("extractResponse returned null", {
2382
+ rawLength: raw.length,
2383
+ snippet: raw.slice(0, 100),
2384
+ parsedNull: parsed === null
2385
+ });
2113
2386
  return null;
2114
2387
  }
2115
2388
  return parsed.content;
@@ -2167,41 +2440,73 @@ var OpenCodeResponseParser = class {
2167
2440
  /**
2168
2441
  * Processes a single NDJSON line.
2169
2442
  * Handles both real v1.2.x format and legacy assumed format.
2443
+ * Returns true if a real v1.2.x step event was processed (step_start/text/tool_use/step_finish).
2444
+ * Legacy events return false since they don't indicate tool-only responses.
2170
2445
  */
2171
- processLine(line, contentParts, setSessionId, setUsage) {
2446
+ processLine(line, contentParts, setSessionId, setUsage, lineIndex) {
2172
2447
  try {
2173
2448
  const record = asRecord(JSON.parse(line));
2174
- if (record === null) return;
2175
- switch (record.type) {
2176
- // --- Real opencode v1.2.x event types ---
2177
- case "step_start":
2178
- this.handleRealSessionId(record, setSessionId);
2179
- break;
2180
- case "text":
2181
- this.handleRealSessionId(record, setSessionId);
2182
- this.pushRealTextContent(record, contentParts);
2183
- break;
2184
- case "step_finish":
2185
- this.handleRealSessionId(record, setSessionId);
2186
- this.emitRealUsage(record, setUsage);
2187
- break;
2188
- // --- Legacy assumed event types ---
2189
- case "session.start":
2190
- this.handleLegacySessionStart(record, setSessionId);
2191
- break;
2192
- case "message.delta":
2193
- this.pushLegacyTextContent(record, contentParts);
2194
- break;
2195
- case "message.complete":
2196
- this.pushLegacyTextContent(record, contentParts);
2197
- this.emitLegacyUsage(record, setUsage);
2198
- break;
2199
- case "session.complete":
2200
- this.emitLegacyUsage(record, setUsage);
2201
- break;
2202
- }
2203
- } catch (lineErr) {
2204
- void lineErr;
2449
+ if (record === null) return false;
2450
+ const isReal = this.processRealEvent(record, contentParts, setSessionId, setUsage);
2451
+ if (isReal) return true;
2452
+ this.processLegacyEvent(record, contentParts, setSessionId, setUsage);
2453
+ return false;
2454
+ } catch {
2455
+ logger3.debug("Skipped malformed NDJSON line", {
2456
+ lineNumber: lineIndex + 1,
2457
+ snippet: line.slice(0, 100)
2458
+ });
2459
+ return false;
2460
+ }
2461
+ }
2462
+ /** Processes real opencode v1.2.x event types. Returns true if handled. */
2463
+ processRealEvent(record, contentParts, setSessionId, setUsage) {
2464
+ switch (record.type) {
2465
+ case "step_start":
2466
+ case "tool_use":
2467
+ this.handleRealSessionId(record, setSessionId);
2468
+ return true;
2469
+ case "text":
2470
+ this.handleRealSessionId(record, setSessionId);
2471
+ this.pushRealTextContent(record, contentParts);
2472
+ return true;
2473
+ case "step_finish":
2474
+ this.handleRealSessionId(record, setSessionId);
2475
+ this.emitRealUsage(record, setUsage);
2476
+ return true;
2477
+ case "error":
2478
+ this.handleRealSessionId(record, setSessionId);
2479
+ this.pushErrorContent(record, contentParts);
2480
+ return true;
2481
+ default:
2482
+ return false;
2483
+ }
2484
+ }
2485
+ /** Extracts error message from error event (#1402). */
2486
+ pushErrorContent(record, parts) {
2487
+ const errorObj = asRecord(record.error);
2488
+ if (errorObj === null) return;
2489
+ const data = asRecord(errorObj.data);
2490
+ const message = data !== null && typeof data.message === "string" ? data.message : typeof errorObj.name === "string" ? errorObj.name : "Unknown error";
2491
+ logger3.warn("OpenCode returned error event", { message });
2492
+ parts.push(`[OpenCode error: ${message}]`);
2493
+ }
2494
+ /** Processes legacy assumed event types. */
2495
+ processLegacyEvent(record, contentParts, setSessionId, setUsage) {
2496
+ switch (record.type) {
2497
+ case "session.start":
2498
+ this.handleLegacySessionStart(record, setSessionId);
2499
+ break;
2500
+ case "message.delta":
2501
+ this.pushLegacyTextContent(record, contentParts);
2502
+ break;
2503
+ case "message.complete":
2504
+ this.pushLegacyTextContent(record, contentParts);
2505
+ this.emitLegacyUsage(record, setUsage);
2506
+ break;
2507
+ case "session.complete":
2508
+ this.emitLegacyUsage(record, setUsage);
2509
+ break;
2205
2510
  }
2206
2511
  }
2207
2512
  // --- Real v1.2.x format handlers ---
@@ -2251,26 +2556,65 @@ var OpenCodeResponseParser = class {
2251
2556
  const usage = this.extractUsageFromRecord(record);
2252
2557
  if (usage !== null) setUsage(usage);
2253
2558
  }
2559
+ /** Checks if a line contains a recognized legacy event type (without full parsing). */
2560
+ isRecognizedLegacyEvent(line) {
2561
+ try {
2562
+ const record = asRecord(JSON.parse(line));
2563
+ if (record === null) return false;
2564
+ const t = record.type;
2565
+ return t === "session.start" || t === "message.start" || t === "message.delta" || t === "message.complete" || t === "session.complete";
2566
+ } catch {
2567
+ return false;
2568
+ }
2569
+ }
2254
2570
  /**
2255
2571
  * Fallback parser for plain JSON output (non-streaming).
2572
+ * Falls back to raw plaintext if JSON parsing fails and content is substantial (#1402).
2573
+ * When hasAnyRecognizedEvent is true, NDJSON-like plaintext is rejected (it was
2574
+ * recognized NDJSON that simply had no content — not malformed output).
2256
2575
  */
2257
- parsePlainJson(raw) {
2576
+ parsePlainJson(raw, hasAnyRecognizedEvent) {
2258
2577
  try {
2259
2578
  const data = JSON.parse(raw);
2260
2579
  const record = asRecord(data);
2261
- if (record === null) return null;
2580
+ if (record === null) return this.parsePlaintext(raw, hasAnyRecognizedEvent);
2262
2581
  const content = record.content ?? record.result ?? record.text ?? record.output;
2263
2582
  if (typeof content !== "string") return null;
2264
2583
  const usage = this.extractUsageFromRecord(record);
2265
2584
  const sid = record.session_id ?? record.sessionId;
2266
2585
  return {
2267
2586
  content,
2268
- ...typeof sid === "string" && { sessionId: sid },
2269
- ...usage !== null && { usage }
2587
+ ...typeof sid === "string" ? { sessionId: sid } : {},
2588
+ ...usage !== null ? { usage } : {}
2270
2589
  };
2271
2590
  } catch {
2591
+ return this.parsePlaintext(raw, hasAnyRecognizedEvent);
2592
+ }
2593
+ }
2594
+ /**
2595
+ * Last-resort plaintext fallback for non-JSON CLI output (#1402).
2596
+ * Returns raw text as content when it has substantial length.
2597
+ * Accepts NDJSON-like content only when no recognized events were found
2598
+ * (meaning it's truly unrecognized/malformed output, not valid NDJSON with no text).
2599
+ */
2600
+ parsePlaintext(raw, hasAnyRecognizedEvent) {
2601
+ const trimmed = raw.trim();
2602
+ if (trimmed.length < PLAINTEXT_MIN_LENGTH) {
2603
+ logger3.debug("Plaintext fallback rejected: too short", { length: trimmed.length });
2272
2604
  return null;
2273
2605
  }
2606
+ if (looksLikeNdjson(trimmed)) {
2607
+ if (hasAnyRecognizedEvent) {
2608
+ logger3.debug("Plaintext fallback rejected: recognized NDJSON with no content", {
2609
+ length: trimmed.length
2610
+ });
2611
+ return null;
2612
+ }
2613
+ logger3.debug("Plaintext fallback: accepting malformed NDJSON as text", {
2614
+ length: trimmed.length
2615
+ });
2616
+ }
2617
+ return { content: trimmed };
2274
2618
  }
2275
2619
  /**
2276
2620
  * Extracts usage from a record with usage/token fields (legacy format).
@@ -2290,11 +2634,72 @@ var OpenCodeResponseParser = class {
2290
2634
  };
2291
2635
 
2292
2636
  // src/cli-adapters/adapters/opencode-adapter.ts
2637
+ var logger4 = createLogger({ component: "opencode-adapter" });
2293
2638
  var ALLOWED_VARIANTS = ["high", "max", "minimal"];
2639
+ var MODEL_TO_CLI_NAME = buildOpenCodeAliasMap();
2640
+ function buildOpenCodeAliasMap() {
2641
+ const map = {};
2642
+ for (const model of DEFAULT_MODEL_CAPABILITIES.models) {
2643
+ if (model.cliName === "opencode" && model.cliModelName !== void 0) {
2644
+ map[model.id] = model.cliModelName;
2645
+ if (model.cliAlias !== void 0) {
2646
+ map[model.cliAlias] = model.cliModelName;
2647
+ }
2648
+ map[model.cliModelName] = model.cliModelName;
2649
+ }
2650
+ }
2651
+ return map;
2652
+ }
2653
+ function resolveOpenCodeModel(model) {
2654
+ return MODEL_TO_CLI_NAME[model] ?? model;
2655
+ }
2656
+ var PROBE_TIMEOUT_MS = 1e4;
2657
+ var cachedModels;
2658
+ var probePromise;
2659
+ function probeAvailableModels() {
2660
+ if (cachedModels !== void 0) return Promise.resolve(cachedModels);
2661
+ if (probePromise !== void 0) return probePromise;
2662
+ probePromise = new Promise((resolve) => {
2663
+ execFile("opencode", ["models"], { timeout: PROBE_TIMEOUT_MS }, (error, stdout) => {
2664
+ if (error !== null || stdout.trim() === "") {
2665
+ logger4.debug("Failed to probe OpenCode models, will omit --model flag", {
2666
+ error: error?.message
2667
+ });
2668
+ cachedModels = /* @__PURE__ */ new Set();
2669
+ resolve(cachedModels);
2670
+ return;
2671
+ }
2672
+ const models = new Set(
2673
+ stdout.trim().split("\n").map((l) => l.trim()).filter((l) => l.length > 0)
2674
+ );
2675
+ logger4.debug("Probed OpenCode models", { count: models.size });
2676
+ cachedModels = models;
2677
+ resolve(cachedModels);
2678
+ });
2679
+ }).finally(() => {
2680
+ probePromise = void 0;
2681
+ });
2682
+ return probePromise;
2683
+ }
2684
+ var ANTHROPIC_MODEL_PATTERNS = ["anthropic/", "custom/claude"];
2685
+ function warnIfAnthropicProvider(models) {
2686
+ const anthropicModels = [...models].filter(
2687
+ (m) => ANTHROPIC_MODEL_PATTERNS.some((p) => m.toLowerCase().includes(p))
2688
+ );
2689
+ if (anthropicModels.length > 0) {
2690
+ logger4.warn(
2691
+ "OpenCode has Anthropic/Claude models configured. Ensure these use a SEPARATE API key from console.anthropic.com, NOT a Claude Code subscription key (which is restricted to Claude Code only).",
2692
+ { detectedModels: anthropicModels }
2693
+ );
2694
+ }
2695
+ }
2294
2696
  var OpenCodeCliAdapter = class extends SubprocessCliAdapter {
2295
2697
  name = "opencode";
2296
2698
  parser = new OpenCodeResponseParser();
2699
+ /** Enable transient-error retry for OpenCode (#1456). */
2700
+ transientRetry = { enabled: true };
2297
2701
  model;
2702
+ availableModels;
2298
2703
  constructor(options) {
2299
2704
  super(options?.logger);
2300
2705
  this.model = options?.model ?? getCliModelName(getDefaultModelForCli("opencode"));
@@ -2315,13 +2720,34 @@ var OpenCodeCliAdapter = class extends SubprocessCliAdapter {
2315
2720
  };
2316
2721
  }
2317
2722
  /**
2318
- * Gets CLI command and arguments for execution.
2319
- * Uses `opencode run` with JSON format for stable parsing.
2723
+ * Initializes the adapter probes available models.
2724
+ * Warns if Anthropic provider is configured (#1429 API key boundaries).
2320
2725
  */
2321
- getCommand(task) {
2322
- const args = ["run", "--format", "json"];
2726
+ async initialize() {
2727
+ this.availableModels = await probeAvailableModels();
2728
+ warnIfAnthropicProvider(this.availableModels);
2729
+ await super.initialize();
2730
+ }
2731
+ /** Returns true if the model is available in the OpenCode installation. */
2732
+ isModelAvailable(cliModel) {
2733
+ if (this.availableModels === void 0 || this.availableModels.size === 0) return false;
2734
+ return this.availableModels.has(cliModel);
2735
+ }
2736
+ /** Appends --model if the resolved model is available (#1402). */
2737
+ appendModelArg(args, task) {
2323
2738
  const internalModel = task.model ?? this.model;
2324
- args.push("--model", internalModel);
2739
+ const cliModel = resolveOpenCodeModel(internalModel);
2740
+ if (this.isModelAvailable(cliModel)) {
2741
+ args.push("--model", cliModel);
2742
+ } else {
2743
+ logger4.debug("Model not available, using OpenCode default", {
2744
+ requested: cliModel,
2745
+ available: this.availableModels?.size ?? 0
2746
+ });
2747
+ }
2748
+ }
2749
+ /** Appends optional task flags (workDir, variant, thinking). */
2750
+ appendTaskFlags(args, task) {
2325
2751
  const workDir = task.options?.["workDir"];
2326
2752
  if (typeof workDir === "string" && workDir.length > 0) {
2327
2753
  args.push("--dir", workDir);
@@ -2333,8 +2759,17 @@ var OpenCodeCliAdapter = class extends SubprocessCliAdapter {
2333
2759
  if (task.options?.["thinking"] === true) {
2334
2760
  args.push("--thinking");
2335
2761
  }
2336
- args.push(task.content);
2337
- return { command: "opencode", args };
2762
+ }
2763
+ /**
2764
+ * Gets CLI command and arguments for execution.
2765
+ * Uses `opencode run` with JSON format for stable parsing.
2766
+ * Omits --model when the requested model isn't available (#1402).
2767
+ */
2768
+ getCommand(task) {
2769
+ const args = ["run", "--format", "json"];
2770
+ this.appendModelArg(args, task);
2771
+ this.appendTaskFlags(args, task);
2772
+ return { command: "opencode", args, stdin: task.content };
2338
2773
  }
2339
2774
  };
2340
2775
 
@@ -2344,13 +2779,19 @@ var CliDetectionCacheConfigSchema = z.object({
2344
2779
  ttlMs: z.number().min(1e3).max(36e5).default(3e5)
2345
2780
  });
2346
2781
  var DEFAULT_CACHE_CONFIG = {
2347
- ttlMs: 3e5
2782
+ ttlMs: 3e5,
2348
2783
  // 5 minutes
2784
+ adaptiveTtl: true
2349
2785
  };
2786
+ var ADAPTIVE_MULTIPLIER_HEALTHY = 2;
2787
+ var ADAPTIVE_MULTIPLIER_UNHEALTHY = 0.25;
2788
+ var ADAPTIVE_STREAK_THRESHOLD = 2;
2350
2789
  var CliDetectionCache = class {
2351
2790
  config;
2352
2791
  logger;
2353
2792
  cache = /* @__PURE__ */ new Map();
2793
+ /** Consecutive same-health-status count per CLI (for adaptive TTL). */
2794
+ streaks = /* @__PURE__ */ new Map();
2354
2795
  hits = 0;
2355
2796
  misses = 0;
2356
2797
  lastReset = new Date(getTimeProvider().now());
@@ -2358,7 +2799,11 @@ var CliDetectionCache = class {
2358
2799
  const validated = CliDetectionCacheConfigSchema.parse({
2359
2800
  ttlMs: config?.ttlMs ?? DEFAULT_CACHE_CONFIG.ttlMs
2360
2801
  });
2361
- this.config = { ...validated, logger: config?.logger };
2802
+ this.config = {
2803
+ ...validated,
2804
+ adaptiveTtl: config?.adaptiveTtl ?? DEFAULT_CACHE_CONFIG.adaptiveTtl,
2805
+ logger: config?.logger
2806
+ };
2362
2807
  this.logger = config?.logger ?? createLogger({ component: "CliDetectionCache" });
2363
2808
  this.logger.debug("CliDetectionCache initialized", { ttlMs: this.config.ttlMs });
2364
2809
  }
@@ -2380,6 +2825,7 @@ var CliDetectionCache = class {
2380
2825
  }
2381
2826
  set(cli, result) {
2382
2827
  this.cache.set(cli, result);
2828
+ this.updateStreak(cli, result.healthy);
2383
2829
  this.logger.debug("Cache updated", {
2384
2830
  cli,
2385
2831
  healthy: result.healthy,
@@ -2390,14 +2836,34 @@ var CliDetectionCache = class {
2390
2836
  const result = this.cache.get(cli);
2391
2837
  if (result === void 0) return true;
2392
2838
  const age = getTimeProvider().now() - result.checkedAt.getTime();
2393
- return age > this.config.ttlMs;
2839
+ return age > this.getEffectiveTtl(cli);
2840
+ }
2841
+ /** Returns the effective TTL for a CLI, applying adaptive multiplier if enabled. */
2842
+ getEffectiveTtl(cli) {
2843
+ if (this.config.adaptiveTtl === false) return this.config.ttlMs;
2844
+ const streak = this.streaks.get(cli);
2845
+ if (streak === void 0 || streak.count < ADAPTIVE_STREAK_THRESHOLD) {
2846
+ return this.config.ttlMs;
2847
+ }
2848
+ const multiplier = streak.healthy ? ADAPTIVE_MULTIPLIER_HEALTHY : ADAPTIVE_MULTIPLIER_UNHEALTHY;
2849
+ return this.config.ttlMs * multiplier;
2850
+ }
2851
+ updateStreak(cli, healthy) {
2852
+ const prev = this.streaks.get(cli);
2853
+ if (prev?.healthy === healthy) {
2854
+ this.streaks.set(cli, { healthy, count: prev.count + 1 });
2855
+ } else {
2856
+ this.streaks.set(cli, { healthy, count: 1 });
2857
+ }
2394
2858
  }
2395
2859
  invalidate(cli) {
2396
2860
  if (cli !== void 0) {
2397
2861
  this.cache.delete(cli);
2862
+ this.streaks.delete(cli);
2398
2863
  this.logger.info("Cache invalidated", { cli });
2399
2864
  } else {
2400
2865
  this.cache.clear();
2866
+ this.streaks.clear();
2401
2867
  this.logger.info("Cache cleared");
2402
2868
  }
2403
2869
  }
@@ -2467,9 +2933,9 @@ function createCodexAdapter(transport, options) {
2467
2933
  }
2468
2934
  return new CodexMcpAdapter(options);
2469
2935
  }
2470
- function createAllAdapters(logger2, codexTransport = "mcp") {
2936
+ function createAllAdapters(logger6, codexTransport = "mcp") {
2471
2937
  const adapters = /* @__PURE__ */ new Map();
2472
- const options = logger2 !== void 0 ? { logger: logger2 } : void 0;
2938
+ const options = logger6 !== void 0 ? { logger: logger6 } : void 0;
2473
2939
  adapters.set("claude", new ClaudeCliAdapter(options));
2474
2940
  adapters.set("gemini", new GeminiCliAdapter(options));
2475
2941
  adapters.set("codex", createCodexAdapter(codexTransport, options ?? {}));
@@ -2513,6 +2979,16 @@ async function getAvailableClis(cache) {
2513
2979
  ).map((r) => r.value.cli);
2514
2980
  }
2515
2981
 
2982
+ // src/cli/setup-data-dir.ts
2983
+ import { mkdirSync, existsSync as existsSync2 } from "fs";
2984
+ import { homedir as homedir2 } from "os";
2985
+ import { join as join2 } from "path";
2986
+
2987
+ // src/cli/doctor.ts
2988
+ import { existsSync, readFileSync, accessSync, constants as fsConstants } from "fs";
2989
+ import { homedir } from "os";
2990
+ import { join } from "path";
2991
+
2516
2992
  // src/mcp/server.ts
2517
2993
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2518
2994
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -2523,13 +2999,13 @@ var MAX_TASK_TTL_MS = 6e5;
2523
2999
  var DEFAULT_TASK_TTL_MS = 3e5;
2524
3000
  var MAX_TASK_CAPACITY = 50;
2525
3001
  var CLEANUP_INTERVAL_MS = 6e4;
2526
- var logger = createLogger({ component: "task-store" });
3002
+ var logger5 = createLogger({ component: "task-store" });
2527
3003
  var singletonStore;
2528
3004
  var cleanupTimer;
2529
3005
  function getTaskStore() {
2530
3006
  if (singletonStore === void 0) {
2531
3007
  singletonStore = new InMemoryTaskStore();
2532
- logger.info("Task store created", { maxCapacity: MAX_TASK_CAPACITY });
3008
+ logger5.info("Task store created", { maxCapacity: MAX_TASK_CAPACITY });
2533
3009
  cleanupTimer = setInterval(() => {
2534
3010
  evictExcessTasks();
2535
3011
  }, CLEANUP_INTERVAL_MS);
@@ -2544,7 +3020,7 @@ function clampTaskTtl(requestedTtl) {
2544
3020
  return DEFAULT_TASK_TTL_MS;
2545
3021
  }
2546
3022
  if (requestedTtl > MAX_TASK_TTL_MS) {
2547
- logger.warn("Task TTL clamped to maximum", {
3023
+ logger5.warn("Task TTL clamped to maximum", {
2548
3024
  requested: requestedTtl,
2549
3025
  max: MAX_TASK_TTL_MS
2550
3026
  });
@@ -2564,13 +3040,13 @@ function evictExcessTasks() {
2564
3040
  const task = sorted[i];
2565
3041
  if (task === void 0) continue;
2566
3042
  singletonStore.updateTaskStatus(task.taskId, "cancelled", "Evicted: capacity exceeded").catch((err2) => {
2567
- logger.debug("Failed to evict task", {
3043
+ logger5.debug("Failed to evict task", {
2568
3044
  taskId: task.taskId,
2569
3045
  error: String(err2)
2570
3046
  });
2571
3047
  });
2572
3048
  }
2573
- logger.info("Evicted excess tasks", { evicted: evictCount, total: sorted.length });
3049
+ logger5.info("Evicted excess tasks", { evicted: evictCount, total: sorted.length });
2574
3050
  }
2575
3051
 
2576
3052
  // src/mcp/server.ts
@@ -2585,9 +3061,10 @@ function createServerError(code, message, error) {
2585
3061
  function createServer(config) {
2586
3062
  const serverName = config?.name ?? DEFAULT_SERVER_NAME;
2587
3063
  const serverVersion = config?.version ?? VERSION;
2588
- const logger2 = config?.logger ?? createLogger({ component: "mcp-server" });
3064
+ const logger6 = config?.logger ?? createLogger({ component: "mcp-server" });
2589
3065
  try {
2590
- logger2.info("Creating MCP server", {
3066
+ initDataDirectories();
3067
+ logger6.info("Creating MCP server", {
2591
3068
  name: serverName,
2592
3069
  version: serverVersion
2593
3070
  });
@@ -2606,11 +3083,11 @@ function createServer(config) {
2606
3083
  taskStore: getTaskStore()
2607
3084
  }
2608
3085
  );
2609
- logger2.debug("MCP server created successfully");
2610
- return ok({ server, logger: logger2 });
3086
+ logger6.debug("MCP server created successfully");
3087
+ return ok({ server, logger: logger6 });
2611
3088
  } catch (error) {
2612
3089
  const errorMessage = getErrorMessage(error);
2613
- logger2.error("Failed to create MCP server", error instanceof Error ? error : void 0);
3090
+ logger6.error("Failed to create MCP server", error instanceof Error ? error : void 0);
2614
3091
  return err(
2615
3092
  createServerError(
2616
3093
  "SERVER_CREATION_FAILED",
@@ -2620,8 +3097,8 @@ function createServer(config) {
2620
3097
  );
2621
3098
  }
2622
3099
  }
2623
- async function connectTransport(server, transport, logger2) {
2624
- const log = logger2 ?? createLogger({ component: "mcp-server" });
3100
+ async function connectTransport(server, transport, logger6) {
3101
+ const log = logger6 ?? createLogger({ component: "mcp-server" });
2625
3102
  try {
2626
3103
  log.info("Connecting server to transport");
2627
3104
  await server.connect(transport);
@@ -2644,19 +3121,19 @@ async function startStdioServer(config) {
2644
3121
  if (!serverResult.ok) {
2645
3122
  return serverResult;
2646
3123
  }
2647
- const { server, logger: logger2 } = serverResult.value;
3124
+ const { server, logger: logger6 } = serverResult.value;
2648
3125
  try {
2649
- logger2.info("Starting stdio transport");
3126
+ logger6.info("Starting stdio transport");
2650
3127
  const transport = new StdioServerTransport();
2651
- const connectResult = await connectTransport(server, transport, logger2);
3128
+ const connectResult = await connectTransport(server, transport, logger6);
2652
3129
  if (!connectResult.ok) {
2653
3130
  return connectResult;
2654
3131
  }
2655
- logger2.info("MCP server running with stdio transport");
2656
- return ok({ server, logger: logger2 });
3132
+ logger6.info("MCP server running with stdio transport");
3133
+ return ok({ server, logger: logger6 });
2657
3134
  } catch (error) {
2658
3135
  const errorMessage = getErrorMessage(error);
2659
- logger2.error("Failed to start stdio server", error instanceof Error ? error : void 0);
3136
+ logger6.error("Failed to start stdio server", error instanceof Error ? error : void 0);
2660
3137
  return err(
2661
3138
  createServerError(
2662
3139
  "SERVER_START_FAILED",
@@ -2666,8 +3143,8 @@ async function startStdioServer(config) {
2666
3143
  );
2667
3144
  }
2668
3145
  }
2669
- async function closeServer(server, logger2) {
2670
- const log = logger2 ?? createLogger({ component: "mcp-server" });
3146
+ async function closeServer(server, logger6) {
3147
+ const log = logger6 ?? createLogger({ component: "mcp-server" });
2671
3148
  try {
2672
3149
  log.info("Closing MCP server");
2673
3150
  await server.close();
@@ -2797,6 +3274,623 @@ function truncateSentence(text, maxLength = 150) {
2797
3274
  return text.slice(0, maxLength).trim() + "...";
2798
3275
  }
2799
3276
 
3277
+ // src/cli/doctor-formatting.ts
3278
+ var REQUIRED_NODE_MAJOR = 22;
3279
+ function formatStatus(healthy, warn = false) {
3280
+ if (healthy) return `${colors.green}${symbols.check}${colors.reset}`;
3281
+ if (warn) return `${colors.yellow}${symbols.warn}${colors.reset}`;
3282
+ return `${colors.red}${symbols.cross}${colors.reset}`;
3283
+ }
3284
+ function formatVersionStatus(status) {
3285
+ switch (status) {
3286
+ case "supported":
3287
+ return `${colors.green}supported${colors.reset}`;
3288
+ case "outdated":
3289
+ return `${colors.yellow}outdated${colors.reset}`;
3290
+ case "unsupported":
3291
+ case "breaking":
3292
+ return `${colors.red}${status}${colors.reset}`;
3293
+ default:
3294
+ return status;
3295
+ }
3296
+ }
3297
+ function formatCapacity(capacity) {
3298
+ if (capacity === void 0) return "Unknown";
3299
+ const remaining = 100 - capacity.utilizationPercent;
3300
+ const remainingStr = String(remaining);
3301
+ if (remaining > 80) return `${colors.green}${remainingStr}% remaining${colors.reset}`;
3302
+ if (remaining > 20) return `${colors.yellow}${remainingStr}% remaining${colors.reset}`;
3303
+ return `${colors.red}${remainingStr}% remaining${colors.reset}`;
3304
+ }
3305
+ function printInstalledCliDetails(cli) {
3306
+ writeLine(` Version: ${cli.version} (${formatVersionStatus(cli.versionStatus)})`);
3307
+ const authText = cli.authenticated ? `${colors.green}${cli.authMethod ?? "Authenticated"}${colors.reset}` : `${colors.red}Not authenticated${colors.reset}`;
3308
+ writeLine(` Auth: ${authText}`);
3309
+ if (cli.capacity !== void 0) {
3310
+ writeLine(` Capacity: ${formatCapacity(cli.capacity)}`);
3311
+ }
3312
+ }
3313
+ function printCliResult(cli) {
3314
+ const status = cli.installed && cli.authenticated;
3315
+ const warn = cli.installed && (!cli.authenticated || cli.versionStatus === "outdated");
3316
+ writeLine(
3317
+ `${formatStatus(status, warn)} ${colors.bold}${capitalize(cli.name)} CLI${colors.reset}`
3318
+ );
3319
+ if (cli.installed) {
3320
+ printInstalledCliDetails(cli);
3321
+ } else {
3322
+ const errorText = cli.error ?? "Not installed";
3323
+ writeLine(` ${colors.red}Error: ${errorText}${colors.reset}`);
3324
+ }
3325
+ if (cli.fix !== void 0 && cli.fix !== "") {
3326
+ writeLine(` ${colors.dim}Fix: ${cli.fix}${colors.reset}`);
3327
+ }
3328
+ writeLine("");
3329
+ }
3330
+ function printCapabilities(clis) {
3331
+ const installedClis = clis.filter((c) => c.installed);
3332
+ if (installedClis.length === 0) {
3333
+ writeLine(`${formatStatus(false)} No CLIs installed`);
3334
+ return;
3335
+ }
3336
+ const caps = DEFAULT_CAPABILITIES;
3337
+ const bestReasoning = installedClis.reduce(
3338
+ (best, c) => caps[c.name].reasoning > caps[best.name].reasoning ? c : best
3339
+ );
3340
+ const bestContext = installedClis.reduce(
3341
+ (best, c) => caps[c.name].contextWindow > caps[best.name].contextWindow ? c : best
3342
+ );
3343
+ const bestSpeed = installedClis.reduce(
3344
+ (best, c) => caps[c.name].speed > caps[best.name].speed ? c : best
3345
+ );
3346
+ const contextTokensK = (caps[bestContext.name].contextWindow / 1e3).toFixed(0);
3347
+ writeLine(
3348
+ `${formatStatus(true)} Complex reasoning: ${colors.bold}${capitalize(bestReasoning.name)}${colors.reset}`
3349
+ );
3350
+ writeLine(
3351
+ `${formatStatus(true)} Large context: ${colors.bold}${capitalize(bestContext.name)}${colors.reset} (${contextTokensK}K tokens)`
3352
+ );
3353
+ writeLine(
3354
+ `${formatStatus(true)} Fast execution: ${colors.bold}${capitalize(bestSpeed.name)}${colors.reset}`
3355
+ );
3356
+ }
3357
+ function printNodeVersionCheck(check) {
3358
+ const versionText = check.supported ? `${colors.green}${check.version}${colors.reset}` : `${colors.yellow}${check.version}${colors.reset}`;
3359
+ writeLine(`${formatStatus(check.supported, !check.supported)} Node.js version: ${versionText}`);
3360
+ if (!check.supported) {
3361
+ writeLine(
3362
+ ` ${colors.dim}Warning: Node.js ${String(REQUIRED_NODE_MAJOR)}.x LTS required${colors.reset}`
3363
+ );
3364
+ }
3365
+ }
3366
+ function printApiKeysCheck(keys) {
3367
+ const configuredCount = keys.filter((k) => k.configured).length;
3368
+ const configuredNames = keys.filter((k) => k.configured).map((k) => k.name);
3369
+ const hasAny = configuredCount > 0;
3370
+ writeLine(
3371
+ `${formatStatus(hasAny, !hasAny)} API keys configured: ${String(configuredCount)} of ${String(keys.length)}`
3372
+ );
3373
+ if (hasAny) {
3374
+ writeLine(` ${colors.dim}Keys: ${configuredNames.join(", ")}${colors.reset}`);
3375
+ } else {
3376
+ writeLine(
3377
+ ` ${colors.dim}Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_AI_API_KEY${colors.reset}`
3378
+ );
3379
+ }
3380
+ }
3381
+ function printConfigFileCheck(check) {
3382
+ if (check.found && check.path !== null) {
3383
+ writeLine(`${formatStatus(true)} Configuration loaded: ${check.path}`);
3384
+ } else {
3385
+ writeLine(`${formatStatus(false, true)} Configuration file: Not found`);
3386
+ writeLine(` ${colors.dim}Run: nexus-agents config init${colors.reset}`);
3387
+ }
3388
+ }
3389
+ function printRegistryAdvisory(advisory) {
3390
+ const allAvailable = advisory.unavailableModels === 0;
3391
+ const countText = `${String(advisory.availableModels)} of ${String(advisory.totalModels)}`;
3392
+ writeLine(`${formatStatus(allAvailable, !allAvailable)} Models available: ${countText}`);
3393
+ if (advisory.unavailableModels > 0) {
3394
+ const missing = advisory.models.filter((m) => !m.available);
3395
+ for (const m of missing) {
3396
+ writeLine(` ${colors.dim}${m.displayName} \u2014 ${m.reason}${colors.reset}`);
3397
+ }
3398
+ }
3399
+ const ageText = `${String(advisory.registryAgeDays)} days old`;
3400
+ if (advisory.registryStale) {
3401
+ writeLine(
3402
+ `${colors.yellow}${symbols.warn}${colors.reset} Model registry is ${ageText} \u2014 may have stale model data`
3403
+ );
3404
+ writeLine(` ${colors.dim}Run: npx tsx scripts/probe-models.ts${colors.reset}`);
3405
+ } else {
3406
+ writeLine(`${formatStatus(true)} Model registry: ${ageText}`);
3407
+ }
3408
+ }
3409
+ function printLearningPersistence(check) {
3410
+ if (!check.enabled) {
3411
+ writeLine(`${formatStatus(true)} Learning persistence: ${colors.dim}Disabled${colors.reset}`);
3412
+ writeLine(` ${colors.dim}Set NEXUS_PERSIST_LEARNING=true to enable${colors.reset}`);
3413
+ return;
3414
+ }
3415
+ const healthy = check.dirExists && check.dirWritable && check.error === null;
3416
+ writeLine(`${formatStatus(healthy, !healthy)} Learning persistence: Enabled`);
3417
+ if (check.error !== null) {
3418
+ writeLine(` ${colors.red}Error: ${check.error}${colors.reset}`);
3419
+ return;
3420
+ }
3421
+ const dirStatus = check.dirExists ? check.dirWritable ? `${colors.green}writable${colors.reset}` : `${colors.red}not writable${colors.reset}` : `${colors.yellow}not created yet${colors.reset}`;
3422
+ writeLine(` Data directory: ${dirStatus}`);
3423
+ writeLine(` Outcomes: ${String(check.outcomeCount)} recorded`);
3424
+ writeLine(` Distilled rules: ${String(check.ruleCount)} active`);
3425
+ if (check.rulesLastSaved !== null) {
3426
+ writeLine(` Rules last saved: ${check.rulesLastSaved}`);
3427
+ }
3428
+ }
3429
+ function printSqliteCheck(check) {
3430
+ if (check.available) {
3431
+ writeLine(
3432
+ `${formatStatus(true)} SQLite (better-sqlite3): ${colors.green}Available${colors.reset}`
3433
+ );
3434
+ } else {
3435
+ writeLine(
3436
+ `${formatStatus(false, true)} SQLite (better-sqlite3): ${colors.yellow}Not available${colors.reset}`
3437
+ );
3438
+ writeLine(
3439
+ ` ${colors.dim}Memory backends (agentic, adaptive, typed) require it${colors.reset}`
3440
+ );
3441
+ writeLine(` ${colors.dim}Fix: npm install -g better-sqlite3${colors.reset}`);
3442
+ }
3443
+ }
3444
+ function printDataDirectory(check) {
3445
+ if (check.rootExists) {
3446
+ const existCount = check.subdirectories.filter((d) => d.exists).length;
3447
+ const totalCount = check.subdirectories.length;
3448
+ const allExist = existCount === totalCount;
3449
+ const allWritable = check.subdirectories.every((d) => !d.exists || d.writable);
3450
+ const healthy = allExist && allWritable;
3451
+ writeLine(
3452
+ `${formatStatus(healthy, !healthy)} Data directory: ${check.rootPath} (${String(existCount)}/${String(totalCount)} subdirs)`
3453
+ );
3454
+ if (!allExist) {
3455
+ const missing = check.subdirectories.filter((d) => !d.exists);
3456
+ for (const dir of missing) {
3457
+ writeLine(` ${colors.dim}Missing: ${dir.name}/${colors.reset}`);
3458
+ }
3459
+ writeLine(` ${colors.dim}Fix: nexus-agents setup${colors.reset}`);
3460
+ }
3461
+ if (!allWritable) {
3462
+ const readonly_ = check.subdirectories.filter((d) => d.exists && !d.writable);
3463
+ for (const dir of readonly_) {
3464
+ writeLine(` ${colors.yellow}Not writable: ${dir.name}/${colors.reset}`);
3465
+ }
3466
+ }
3467
+ } else {
3468
+ writeLine(
3469
+ `${formatStatus(false, true)} Data directory: ${colors.yellow}Not created${colors.reset}`
3470
+ );
3471
+ writeLine(` ${colors.dim}Run: nexus-agents setup${colors.reset}`);
3472
+ }
3473
+ }
3474
+ function printDoctorSummary(result) {
3475
+ const unhealthyCount = result.clis.filter((c) => !c.installed || !c.authenticated).length;
3476
+ const nodeIssue = result.nodeVersion.supported ? 0 : 1;
3477
+ const totalIssues = unhealthyCount + nodeIssue + (result.mcpServerReady ? 0 : 1);
3478
+ const summary = result.allHealthy ? `${colors.green}${colors.bold}Status: Ready${colors.reset}` : `${colors.yellow}${colors.bold}Summary: ${String(totalIssues)} issue(s) found${colors.reset}`;
3479
+ writeLine(summary);
3480
+ writeLine("");
3481
+ }
3482
+ function printDoctorResults(result) {
3483
+ writeLine("");
3484
+ writeLine(`${colors.bold}Nexus Agents Doctor${colors.reset}`);
3485
+ writeLine("===================");
3486
+ writeLine("");
3487
+ writeLine(`${colors.cyan}Checking environment...${colors.reset}`);
3488
+ writeLine("");
3489
+ printNodeVersionCheck(result.nodeVersion);
3490
+ printApiKeysCheck(result.apiKeys);
3491
+ printConfigFileCheck(result.configFile);
3492
+ writeLine("");
3493
+ writeLine(`${colors.cyan}Checking CLI installations...${colors.reset}`);
3494
+ writeLine("");
3495
+ for (const cli of result.clis) {
3496
+ printCliResult(cli);
3497
+ }
3498
+ writeLine(`${colors.cyan}Checking MCP configuration...${colors.reset}`);
3499
+ writeLine("");
3500
+ writeLine(
3501
+ `${formatStatus(result.mcpServerReady)} MCP Server mode: ${result.mcpServerReady ? "Ready" : "Not ready"}`
3502
+ );
3503
+ writeLine(
3504
+ `${formatStatus(result.mcpClientReady)} MCP Client mode: ${result.mcpClientReady ? "Ready (Codex mcp-server)" : "Not ready (Codex not installed)"}`
3505
+ );
3506
+ writeLine("");
3507
+ writeLine(`${colors.cyan}Checking capabilities...${colors.reset}`);
3508
+ writeLine("");
3509
+ printCapabilities(result.clis);
3510
+ writeLine("");
3511
+ writeLine(`${colors.cyan}Checking model registry...${colors.reset}`);
3512
+ writeLine("");
3513
+ printRegistryAdvisory(result.registryAdvisory);
3514
+ writeLine("");
3515
+ writeLine(`${colors.cyan}Checking learning subsystem...${colors.reset}`);
3516
+ writeLine("");
3517
+ printLearningPersistence(result.learningPersistence);
3518
+ writeLine("");
3519
+ writeLine(`${colors.cyan}Checking data storage...${colors.reset}`);
3520
+ writeLine("");
3521
+ printSqliteCheck(result.sqliteCheck);
3522
+ printDataDirectory(result.dataDirectory);
3523
+ writeLine("");
3524
+ printDoctorSummary(result);
3525
+ }
3526
+
3527
+ // src/cli/doctor.ts
3528
+ var REQUIRED_NODE_MAJOR2 = 22;
3529
+ var API_KEY_VARS = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_AI_API_KEY"];
3530
+ var CONFIG_FILE_PATHS = ["./nexus-agents.yaml", "./nexus-agents.yml"];
3531
+ var DATA_SUBDIRECTORIES = [
3532
+ "memory",
3533
+ "memory/beliefs",
3534
+ "learning",
3535
+ "sessions",
3536
+ "audit",
3537
+ "voting",
3538
+ "auth",
3539
+ "research",
3540
+ "checkpoints"
3541
+ ];
3542
+ function getFixCommand(name, issue) {
3543
+ const commands = {
3544
+ claude: {
3545
+ install: "npm install -g @anthropic-ai/claude-code",
3546
+ upgrade: "npm update -g @anthropic-ai/claude-code",
3547
+ auth: "claude auth login"
3548
+ },
3549
+ gemini: {
3550
+ install: "npm install -g @google/gemini-cli",
3551
+ upgrade: "npm update -g @google/gemini-cli",
3552
+ auth: "gemini auth login"
3553
+ },
3554
+ codex: {
3555
+ install: "npm install -g @openai/codex",
3556
+ upgrade: "npm update -g @openai/codex",
3557
+ auth: "codex auth login"
3558
+ },
3559
+ opencode: {
3560
+ install: "npm install -g opencode-ai",
3561
+ upgrade: "npm update -g opencode-ai",
3562
+ auth: "opencode auth login"
3563
+ }
3564
+ };
3565
+ return commands[name][issue] ?? "";
3566
+ }
3567
+ function createNotFoundResult(name, errorMsg) {
3568
+ return {
3569
+ name,
3570
+ installed: false,
3571
+ version: "N/A",
3572
+ versionStatus: "unsupported",
3573
+ authenticated: false,
3574
+ error: errorMsg,
3575
+ fix: getFixCommand(name, "install")
3576
+ };
3577
+ }
3578
+ function detectAuthMethod(name) {
3579
+ const authMethods = {
3580
+ claude: "CLI auth",
3581
+ gemini: "ADC/CLI auth",
3582
+ codex: "CLI auth",
3583
+ opencode: "CLI auth"
3584
+ };
3585
+ return authMethods[name];
3586
+ }
3587
+ function createHealthyResult(name, health, capacity) {
3588
+ const authenticated = health.healthy;
3589
+ const result = {
3590
+ name,
3591
+ installed: true,
3592
+ version: health.version,
3593
+ versionStatus: health.versionStatus,
3594
+ authenticated,
3595
+ ...authenticated && { authMethod: detectAuthMethod(name) },
3596
+ ...capacity !== void 0 && { capacity }
3597
+ };
3598
+ if (health.message !== void 0 && health.message !== "") {
3599
+ return { ...result, error: health.message };
3600
+ }
3601
+ if (!authenticated) {
3602
+ return { ...result, fix: getFixCommand(name, "auth") };
3603
+ }
3604
+ if (health.versionStatus === "outdated") {
3605
+ return { ...result, fix: getFixCommand(name, "upgrade") };
3606
+ }
3607
+ return result;
3608
+ }
3609
+ async function checkCli(name) {
3610
+ const adapters = createAllAdapters();
3611
+ const adapter = adapters.get(name);
3612
+ if (!adapter) {
3613
+ return createNotFoundResult(name, "Adapter not available");
3614
+ }
3615
+ try {
3616
+ const health = await adapter.healthCheck();
3617
+ let capacity;
3618
+ try {
3619
+ capacity = await adapter.getCapacity();
3620
+ } catch (capErr) {
3621
+ void capErr;
3622
+ }
3623
+ return createHealthyResult(name, health, capacity);
3624
+ } catch (error) {
3625
+ const message = getErrorMessage(error);
3626
+ const isNotFound = message.includes("ENOENT") || message.includes("not found");
3627
+ return createNotFoundResult(name, isNotFound ? "Not found in PATH" : message);
3628
+ }
3629
+ }
3630
+ function checkNodeVersion() {
3631
+ const version = process.version;
3632
+ const major = Number(version.slice(1).split(".")[0]);
3633
+ return {
3634
+ version,
3635
+ major,
3636
+ supported: major >= REQUIRED_NODE_MAJOR2
3637
+ };
3638
+ }
3639
+ function checkApiKeys() {
3640
+ return API_KEY_VARS.map((name) => ({
3641
+ name,
3642
+ configured: typeof process.env[name] === "string" && process.env[name] !== ""
3643
+ }));
3644
+ }
3645
+ function checkConfigFile() {
3646
+ for (const configPath of CONFIG_FILE_PATHS) {
3647
+ if (existsSync(configPath)) {
3648
+ return { found: true, path: configPath };
3649
+ }
3650
+ }
3651
+ return { found: false, path: null };
3652
+ }
3653
+ function checkMcpServerReady() {
3654
+ try {
3655
+ const result = createServer({ name: "nexus-agents-doctor-check" });
3656
+ return result.ok;
3657
+ } catch {
3658
+ return false;
3659
+ }
3660
+ }
3661
+ function buildRegistryAdvisory(cliResults) {
3662
+ const installedClis = new Set(cliResults.filter((c) => c.installed).map((c) => c.name));
3663
+ const models = DEFAULT_MODEL_CAPABILITIES.models.filter((m) => m.cliName !== void 0).map((m) => {
3664
+ const cliName = m.cliName ?? "";
3665
+ const available = cliName.length > 0 && installedClis.has(cliName);
3666
+ const reason = available ? `${cliName} CLI is installed` : `${cliName} CLI is not installed`;
3667
+ return { modelId: m.id, displayName: m.displayName, cliName, available, reason };
3668
+ });
3669
+ const STALE_THRESHOLD_DAYS = 30;
3670
+ const updatedAt = new Date(DEFAULT_MODEL_CAPABILITIES.updatedAt);
3671
+ const nowMs = getTimeProvider().now();
3672
+ const ageDays = Math.floor((nowMs - updatedAt.getTime()) / (1e3 * 60 * 60 * 24));
3673
+ return {
3674
+ totalModels: models.length,
3675
+ availableModels: models.filter((m) => m.available).length,
3676
+ unavailableModels: models.filter((m) => !m.available).length,
3677
+ models,
3678
+ registryAgeDays: ageDays,
3679
+ registryStale: ageDays > STALE_THRESHOLD_DAYS
3680
+ };
3681
+ }
3682
+ function countJsonlLines(filePath) {
3683
+ if (!existsSync(filePath)) return 0;
3684
+ return readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim().length > 0).length;
3685
+ }
3686
+ function readRulesMetadata(filePath) {
3687
+ if (!existsSync(filePath)) return { count: 0, savedAt: null };
3688
+ try {
3689
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
3690
+ const rules = raw["rules"];
3691
+ const saved = raw["savedAt"];
3692
+ return {
3693
+ count: Array.isArray(rules) ? rules.length : 0,
3694
+ savedAt: typeof saved === "string" ? saved : null
3695
+ };
3696
+ } catch {
3697
+ return { count: 0, savedAt: null };
3698
+ }
3699
+ }
3700
+ function checkDirAccess(dir) {
3701
+ const exists = existsSync(dir);
3702
+ if (!exists) return { exists: false, writable: false };
3703
+ try {
3704
+ accessSync(dir, fsConstants.W_OK);
3705
+ return { exists: true, writable: true };
3706
+ } catch {
3707
+ return { exists: true, writable: false };
3708
+ }
3709
+ }
3710
+ var DISABLED_CHECK = {
3711
+ enabled: false,
3712
+ dirExists: false,
3713
+ dirWritable: false,
3714
+ outcomeCount: 0,
3715
+ ruleCount: 0,
3716
+ rulesLastSaved: null,
3717
+ error: null
3718
+ };
3719
+ function checkLearningPersistence() {
3720
+ if (!isPersistenceEnabled()) return DISABLED_CHECK;
3721
+ try {
3722
+ const { exists: dirExists, writable: dirWritable } = checkDirAccess(LEARNING_DIR);
3723
+ const outcomeCount = countJsonlLines(OUTCOMES_FILE);
3724
+ const { count: ruleCount, savedAt: rulesLastSaved } = readRulesMetadata(RULES_FILE);
3725
+ return {
3726
+ enabled: true,
3727
+ dirExists,
3728
+ dirWritable,
3729
+ outcomeCount,
3730
+ ruleCount,
3731
+ rulesLastSaved,
3732
+ error: null
3733
+ };
3734
+ } catch (error) {
3735
+ return {
3736
+ enabled: true,
3737
+ dirExists: false,
3738
+ dirWritable: false,
3739
+ outcomeCount: 0,
3740
+ ruleCount: 0,
3741
+ rulesLastSaved: null,
3742
+ error: getErrorMessage(error)
3743
+ };
3744
+ }
3745
+ }
3746
+ async function checkSqlite() {
3747
+ try {
3748
+ await import("better-sqlite3");
3749
+ return { available: true, error: null };
3750
+ } catch (error) {
3751
+ const msg = getErrorMessage(error);
3752
+ const isNotFound = msg.includes("Cannot find") || msg.includes("MODULE_NOT_FOUND");
3753
+ return {
3754
+ available: false,
3755
+ error: isNotFound ? "better-sqlite3 not installed \u2014 5 memory backends unavailable" : `better-sqlite3 load error: ${msg}`
3756
+ };
3757
+ }
3758
+ }
3759
+ function checkDataDirectory() {
3760
+ const rootPath = join(homedir(), ".nexus-agents");
3761
+ const rootExists = existsSync(rootPath);
3762
+ const subdirectories = DATA_SUBDIRECTORIES.map((name) => {
3763
+ const fullPath = join(rootPath, name);
3764
+ const exists = existsSync(fullPath);
3765
+ return { name, path: fullPath, exists, writable: exists && isWritable(fullPath) };
3766
+ });
3767
+ return { rootExists, rootPath, subdirectories };
3768
+ }
3769
+ function isWritable(dirPath) {
3770
+ try {
3771
+ accessSync(dirPath, fsConstants.W_OK);
3772
+ return true;
3773
+ } catch {
3774
+ return false;
3775
+ }
3776
+ }
3777
+ async function runDoctor() {
3778
+ const clis = await Promise.all([
3779
+ checkCli("claude"),
3780
+ checkCli("gemini"),
3781
+ checkCli("codex"),
3782
+ checkCli("opencode")
3783
+ ]);
3784
+ const nodeVersion = checkNodeVersion();
3785
+ const apiKeys = checkApiKeys();
3786
+ const configFile = checkConfigFile();
3787
+ const mcpServerReady = checkMcpServerReady();
3788
+ const codexCheck = clis.find((c) => c.name === "codex");
3789
+ const mcpClientReady = codexCheck?.installed ?? false;
3790
+ const registryAdvisory = buildRegistryAdvisory(clis);
3791
+ const learningPersistence = checkLearningPersistence();
3792
+ const sqliteCheck = await checkSqlite();
3793
+ const dataDirectory = checkDataDirectory();
3794
+ const hasAuthMethod = apiKeys.some((k) => k.configured) || clis.some((c) => c.installed && c.authenticated);
3795
+ const allHealthy = nodeVersion.supported && hasAuthMethod && mcpServerReady && clis.every((c) => c.installed && c.authenticated && c.versionStatus !== "unsupported");
3796
+ return {
3797
+ clis,
3798
+ nodeVersion,
3799
+ apiKeys,
3800
+ configFile,
3801
+ mcpServerReady,
3802
+ mcpClientReady,
3803
+ registryAdvisory,
3804
+ learningPersistence,
3805
+ sqliteCheck,
3806
+ dataDirectory,
3807
+ allHealthy,
3808
+ timestamp: new Date(getTimeProvider().now())
3809
+ };
3810
+ }
3811
+ async function doctorCommand(options = {}) {
3812
+ const result = await runDoctor();
3813
+ printDoctorResults(result);
3814
+ if (options.fix === true) {
3815
+ await runDoctorFix(result);
3816
+ }
3817
+ return result.allHealthy ? 0 : 1;
3818
+ }
3819
+ async function runDoctorFix(result) {
3820
+ const writeLine2 = (text) => {
3821
+ process.stdout.write(text + "\n");
3822
+ };
3823
+ writeLine2("");
3824
+ writeLine2("\x1B[1mAuto-fix\x1B[0m");
3825
+ writeLine2("\u2500".repeat(40));
3826
+ let fixCount = 0;
3827
+ if (!result.dataDirectory.rootExists || result.dataDirectory.subdirectories.some((d) => !d.exists || !d.writable)) {
3828
+ const { runSetup } = await import("./setup-command-SS7LMN7Y.js");
3829
+ const setupResult = runSetup({
3830
+ skipMcp: true,
3831
+ skipRules: true,
3832
+ skipHooks: true,
3833
+ skipConfig: true,
3834
+ skipOpencode: true
3835
+ });
3836
+ if (setupResult.success) {
3837
+ writeLine2("\u2713 Created missing data directories");
3838
+ fixCount++;
3839
+ }
3840
+ }
3841
+ if (!result.configFile.found) {
3842
+ const { runConfigInitSync } = await import("./setup-config-DSMOOLVW.js");
3843
+ const configResult = runConfigInitSync(process.cwd(), false, false);
3844
+ if (configResult.success && configResult.created) {
3845
+ writeLine2(`\u2713 Generated config: ${configResult.path}`);
3846
+ fixCount++;
3847
+ }
3848
+ }
3849
+ if (!result.sqliteCheck.available) {
3850
+ writeLine2("");
3851
+ writeLine2("\u26A0 better-sqlite3 not installed (manual step required):");
3852
+ writeLine2(" npm install -g better-sqlite3");
3853
+ }
3854
+ if (fixCount > 0) {
3855
+ writeLine2("");
3856
+ writeLine2(
3857
+ `\x1B[32m${String(fixCount)} issue(s) fixed.\x1B[0m Re-run \x1B[1mnexus-agents doctor\x1B[0m to verify.`
3858
+ );
3859
+ } else {
3860
+ writeLine2("No auto-fixable issues found.");
3861
+ }
3862
+ writeLine2("");
3863
+ }
3864
+
3865
+ // src/cli/setup-data-dir.ts
3866
+ var NEXUS_DATA_DIR = join2(homedir2(), ".nexus-agents");
3867
+ var RESTRICTED_DIRS = /* @__PURE__ */ new Set(["auth"]);
3868
+ function initDataDirectories(dryRun = false) {
3869
+ const created = [];
3870
+ const alreadyExisted = [];
3871
+ try {
3872
+ ensureDir(NEXUS_DATA_DIR, dryRun, created, alreadyExisted);
3873
+ for (const subdir of DATA_SUBDIRECTORIES) {
3874
+ const mode = RESTRICTED_DIRS.has(subdir) ? 448 : void 0;
3875
+ ensureDir(join2(NEXUS_DATA_DIR, subdir), dryRun, created, alreadyExisted, mode);
3876
+ }
3877
+ return { success: true, rootPath: NEXUS_DATA_DIR, created, alreadyExisted, error: null };
3878
+ } catch (error) {
3879
+ const msg = error instanceof Error ? error.message : String(error);
3880
+ return { success: false, rootPath: NEXUS_DATA_DIR, created, alreadyExisted, error: msg };
3881
+ }
3882
+ }
3883
+ function ensureDir(dirPath, dryRun, created, alreadyExisted, mode) {
3884
+ if (existsSync2(dirPath)) {
3885
+ alreadyExisted.push(dirPath);
3886
+ return;
3887
+ }
3888
+ if (!dryRun) {
3889
+ mkdirSync(dirPath, { recursive: true, ...mode !== void 0 ? { mode } : {} });
3890
+ }
3891
+ created.push(dirPath);
3892
+ }
3893
+
2800
3894
  export {
2801
3895
  VERSION,
2802
3896
  sleep,
@@ -2841,9 +3935,11 @@ export {
2841
3935
  truncateSentence,
2842
3936
  DEFAULT_TASK_TTL_MS,
2843
3937
  clampTaskTtl,
3938
+ doctorCommand,
3939
+ initDataDirectories,
2844
3940
  createServer,
2845
3941
  connectTransport,
2846
3942
  startStdioServer,
2847
3943
  closeServer
2848
3944
  };
2849
- //# sourceMappingURL=chunk-ARNVVQ5W.js.map
3945
+ //# sourceMappingURL=chunk-LKSTILEE.js.map