@gajae-code/coding-agent 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +54 -0
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
  16. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  18. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  19. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  21. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  22. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  23. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  24. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  25. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  26. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  27. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  28. package/dist/types/reminders/star-reminder.d.ts +3 -13
  29. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  30. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  31. package/dist/types/sdk/broker/transport.d.ts +2 -0
  32. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  33. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  34. package/dist/types/sdk/bus/config.d.ts +6 -2
  35. package/dist/types/sdk/bus/index.d.ts +29 -0
  36. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  37. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  38. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  39. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  40. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  41. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  42. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  43. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  44. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  45. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  46. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  47. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  48. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  49. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  50. package/dist/types/sdk/prompt-status.d.ts +23 -3
  51. package/dist/types/sdk/session.d.ts +6 -0
  52. package/dist/types/session/agent-session.d.ts +38 -2
  53. package/dist/types/session/artifacts.d.ts +8 -0
  54. package/dist/types/session/blob-store.d.ts +57 -4
  55. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  56. package/dist/types/session/session-manager.d.ts +23 -6
  57. package/dist/types/session/session-storage.d.ts +4 -0
  58. package/dist/types/session/streaming-output.d.ts +36 -7
  59. package/dist/types/setup/credential-import.d.ts +12 -0
  60. package/dist/types/skill-state/active-state.d.ts +12 -0
  61. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  62. package/dist/types/task/executor.d.ts +3 -2
  63. package/dist/types/task/index.d.ts +3 -1
  64. package/dist/types/task/provider-retry-status.d.ts +8 -0
  65. package/dist/types/task/receipt.d.ts +7 -1
  66. package/dist/types/task/types.d.ts +23 -0
  67. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  68. package/dist/types/tools/bash-interactive.d.ts +4 -1
  69. package/dist/types/tools/bash.d.ts +3 -1
  70. package/dist/types/tools/image-gen.d.ts +4 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/output-meta.d.ts +23 -2
  73. package/dist/types/tools/path-utils.d.ts +1 -0
  74. package/dist/types/tools/read.d.ts +0 -6
  75. package/dist/types/tools/review.d.ts +5 -7
  76. package/dist/types/tools/subagent.d.ts +2 -0
  77. package/dist/types/utils/gh.d.ts +13 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  80. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  81. package/dist/types/web/search/providers/xai.d.ts +2 -0
  82. package/package.json +10 -8
  83. package/scripts/acp-conformance-agent.ts +218 -0
  84. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  85. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  86. package/scripts/resident-memory-bench.ts +1341 -0
  87. package/scripts/run-acp-conformance.ts +285 -0
  88. package/src/async/job-manager.ts +78 -20
  89. package/src/cli/fast-help.ts +1 -1
  90. package/src/cli/plugin-cli.ts +176 -18
  91. package/src/commands/team.ts +19 -1
  92. package/src/config/file-lock.ts +11 -2
  93. package/src/config/settings-schema.ts +51 -0
  94. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  95. package/src/coordinator-mcp/server.ts +5 -12
  96. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  97. package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
  98. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  99. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
  100. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  101. package/src/defaults/gjc-defaults.ts +4 -4
  102. package/src/exa/mcp-client.ts +12 -3
  103. package/src/exec/bash-executor.ts +144 -27
  104. package/src/extensibility/extensions/runner.ts +15 -0
  105. package/src/extensibility/extensions/types.ts +18 -1
  106. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  107. package/src/extensibility/gjc-plugins/index.ts +19 -2
  108. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  109. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  110. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  111. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  112. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  113. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  114. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  115. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  116. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  117. package/src/extensibility/gjc-plugins/types.ts +141 -0
  118. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  119. package/src/gjc-runtime/launch-tmux.ts +25 -6
  120. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  121. package/src/gjc-runtime/ralplan-runtime.ts +411 -17
  122. package/src/gjc-runtime/team-launch.ts +8 -0
  123. package/src/gjc-runtime/team-runtime.ts +140 -32
  124. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  127. package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
  128. package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
  129. package/src/gjc-runtime/workflow-manifest.ts +4 -68
  130. package/src/internal-urls/docs-index.generated.ts +15 -15
  131. package/src/modes/DESIGN.md +51 -0
  132. package/src/modes/acp/acp-agent.ts +298 -186
  133. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  134. package/src/modes/components/notifications-settings-editor.ts +12 -1
  135. package/src/modes/components/oauth-selector.ts +68 -30
  136. package/src/modes/components/session-observer-overlay.ts +305 -86
  137. package/src/modes/components/settings-selector.ts +56 -5
  138. package/src/modes/components/status-line/gh.ts +16 -0
  139. package/src/modes/components/tool-status-header.ts +3 -14
  140. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  141. package/src/modes/controllers/command-controller.ts +4 -0
  142. package/src/modes/controllers/extension-ui-controller.ts +7 -0
  143. package/src/modes/controllers/selector-controller.ts +45 -0
  144. package/src/modes/runtime-init.ts +3 -0
  145. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  146. package/src/prompts/system/eager-todo.md +8 -4
  147. package/src/prompts/system/system-prompt.md +11 -4
  148. package/src/prompts/tools/bash.md +5 -5
  149. package/src/prompts/tools/search-tool-bm25.md +5 -0
  150. package/src/prompts/tools/task.md +1 -1
  151. package/src/reminders/star-reminder.ts +4 -42
  152. package/src/runtime-mcp/manager.ts +19 -4
  153. package/src/sdk/acp/adapter.ts +22 -6
  154. package/src/sdk/broker/transport.ts +35 -3
  155. package/src/sdk/bus/attachment-registry.ts +5 -1
  156. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  157. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  158. package/src/sdk/bus/config.ts +18 -2
  159. package/src/sdk/bus/index.ts +661 -126
  160. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  161. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  162. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  163. package/src/sdk/bus/reconciliation-store.ts +95 -16
  164. package/src/sdk/bus/rich-draft.ts +1 -1
  165. package/src/sdk/bus/rich-render.ts +2 -2
  166. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  167. package/src/sdk/bus/telegram-cli.ts +33 -8
  168. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  169. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  170. package/src/sdk/bus/telegram-daemon.ts +916 -23
  171. package/src/sdk/bus/telegram-reference.ts +40 -3
  172. package/src/sdk/bus/threaded-render.ts +48 -3
  173. package/src/sdk/bus/topic-registry.ts +20 -0
  174. package/src/sdk/host/query/revision-store.ts +36 -2
  175. package/src/sdk/host/reverse-leases.ts +44 -7
  176. package/src/sdk/prompt-status.ts +16 -3
  177. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  178. package/src/sdk/session.ts +88 -9
  179. package/src/session/agent-session.ts +822 -172
  180. package/src/session/artifacts.ts +22 -0
  181. package/src/session/auth-broker-config.ts +10 -3
  182. package/src/session/blob-store.ts +915 -24
  183. package/src/session/internal/managed-session-scope.ts +44 -6
  184. package/src/session/internal/managed-session-storage.ts +48 -6
  185. package/src/session/session-manager.ts +1614 -435
  186. package/src/session/session-storage.ts +9 -1
  187. package/src/session/streaming-output.ts +238 -68
  188. package/src/setup/credential-import.ts +75 -13
  189. package/src/skill-state/active-state.ts +32 -0
  190. package/src/skill-state/workflow-hud.ts +17 -0
  191. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  192. package/src/task/executor.ts +122 -39
  193. package/src/task/index.ts +23 -14
  194. package/src/task/provider-retry-status.ts +16 -0
  195. package/src/task/receipt.ts +23 -7
  196. package/src/task/render.ts +6 -2
  197. package/src/task/types.ts +108 -0
  198. package/src/tools/ask-answer-registry.ts +12 -6
  199. package/src/tools/ask.ts +86 -55
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash-interactive.ts +8 -3
  203. package/src/tools/bash.ts +503 -60
  204. package/src/tools/find.ts +1 -0
  205. package/src/tools/image-gen.ts +29 -5
  206. package/src/tools/index.ts +4 -0
  207. package/src/tools/output-meta.ts +108 -14
  208. package/src/tools/path-utils.ts +3 -2
  209. package/src/tools/read.ts +18 -0
  210. package/src/tools/review.ts +5 -7
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/subagent-render.ts +6 -2
  213. package/src/tools/subagent.ts +6 -0
  214. package/src/tools/telegram-send.ts +6 -3
  215. package/src/utils/gh.ts +39 -0
  216. package/src/utils/image-resize.ts +3 -0
  217. package/src/web/search/providers/anthropic.ts +4 -4
  218. package/src/web/search/providers/kimi.ts +15 -5
  219. package/src/web/search/providers/searxng.ts +47 -4
  220. package/src/web/search/providers/xai.ts +7 -2
  221. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  222. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  223. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -106,6 +106,8 @@ export interface SessionStorageWriterOpenOptions {
106
106
  export interface ManagedSessionSecurityContext {
107
107
  readonly kind: "managed";
108
108
  readonly agentDir: string;
109
+ /** Logical profile root for process-local caches; distinct from managed pathname authority. */
110
+ readonly profileAgentDir: string;
109
111
  readonly sessionsRoot: string;
110
112
  readonly sessionDir: string;
111
113
  readonly rootAuthority: ManagedDirectoryRoot;
@@ -117,12 +119,18 @@ const managedSecurityContexts = new WeakSet<ManagedSessionSecurityContext>();
117
119
  /** @internal Create the only accepted managed writer authority object. */
118
120
  export function createManagedSessionSecurityContext(input: {
119
121
  agentDir: string;
122
+ /** Optional for compatibility with existing authority-only callers. */
123
+ profileAgentDir?: string;
120
124
  sessionsRoot: string;
121
125
  sessionDir: string;
122
126
  rootAuthority: ManagedDirectoryRoot;
123
127
  retainedAuthority?: native.RecoveryFsRoot;
124
128
  }): ManagedSessionSecurityContext {
125
- const context = Object.freeze({ kind: "managed" as const, ...input });
129
+ const context = Object.freeze({
130
+ kind: "managed" as const,
131
+ ...input,
132
+ profileAgentDir: input.profileAgentDir ?? input.agentDir,
133
+ });
126
134
  managedSecurityContexts.add(context);
127
135
  return context;
128
136
  }
@@ -28,6 +28,27 @@ const NL = "\n";
28
28
 
29
29
  const ELLIPSIS = "…";
30
30
 
31
+ const ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES = 256;
32
+
33
+ /**
34
+ * Terminal publisher used when managed artifact storage exposes an ID but no
35
+ * writable pathname. The sink supplies raw content retained up to its artifact
36
+ * hard cap; `omittedBytes` is the source-byte count already omitted by that cap.
37
+ * A successful publisher may report additional omitted bytes (for example, a
38
+ * downstream storage cap); those counts are added to the sink summary. Failed
39
+ * and unavailable results are surfaced through the bounded artifact diagnostic
40
+ * without exposing an unresolvable artifact URL.
41
+ */
42
+ export type TerminalArtifactPublishResult =
43
+ | { status: "published"; artifactId: string; omittedBytes?: number }
44
+ | { status: "unavailable" }
45
+ | { status: "failed"; diagnostic: string };
46
+
47
+ export type TerminalArtifactPublisher = (
48
+ content: string,
49
+ info: { totalBytes: number; omittedBytes: number },
50
+ ) => Promise<TerminalArtifactPublishResult>;
51
+
31
52
  // =============================================================================
32
53
  // Interfaces
33
54
  // =============================================================================
@@ -47,10 +68,12 @@ export interface OutputSummary {
47
68
  columnDroppedBytes?: number;
48
69
  /** Number of distinct lines that hit the per-line column cap. */
49
70
  columnTruncatedLines?: number;
50
- /** Artifact ID for internal URL access (artifact://<id>) when truncated */
71
+ /** Artifact ID for internal URL access (artifact://<id>) when output was persisted. */
51
72
  artifactId?: string;
52
73
  /** Bytes omitted from artifact storage after the artifact hard cap was reached. */
53
74
  artifactTruncatedBytes?: number;
75
+ /** Bounded diagnostic when artifact writer or terminal publisher creation, write, finalization, or publication failed. */
76
+ artifactFailureDiagnostic?: string;
54
77
  }
55
78
 
56
79
  export interface OutputSinkOptions {
@@ -60,6 +83,12 @@ export interface OutputSinkOptions {
60
83
  */
61
84
  artifactPath?: string;
62
85
  artifactId?: string;
86
+ /**
87
+ * Optional terminal publisher for managed artifact stores that do not expose a
88
+ * writable path. It is invoked only when visible spill/truncation requires an
89
+ * artifact, and receives raw content bounded by `artifactMaxBytes`.
90
+ */
91
+ artifactPublisher?: TerminalArtifactPublisher;
63
92
  /** Tail buffer budget (bytes). Default DEFAULT_MAX_BYTES. */
64
93
  spillThreshold?: number;
65
94
  /**
@@ -913,6 +942,9 @@ export class OutputSink {
913
942
  #artifactBytes = 0;
914
943
  #artifactTruncatedBytes = 0;
915
944
  #artifactTruncationNoticeWritten = false;
945
+ #artifactFailureDiagnostic?: string;
946
+ #publishedArtifactId?: string;
947
+ #artifactFinalizationFailed = false;
916
948
 
917
949
  // Per-line column cap streaming state (persists across `push` calls so a
918
950
  // long line split across chunks still trips the same trigger).
@@ -927,9 +959,13 @@ export class OutputSink {
927
959
  };
928
960
 
929
961
  // Raw prefix chunks not yet confirmed written to the file sink. This queue is
930
- // the only artifact replay source; retained head/tail windows are lossy views.
962
+ // the pathname-backed artifact replay source; retained head/tail windows are lossy views.
931
963
  #pendingFileWrites?: string[];
932
964
  #pendingFileWriteBytes = 0;
965
+ // Raw chunks retained for a terminal publisher. This queue is capped by
966
+ // `#artifactMaxBytes` and is never used by pathname-backed sinks.
967
+ #pendingArtifactContent?: string[];
968
+ #artifactPublishAttempted = false;
933
969
  #finalized = false;
934
970
 
935
971
  #fileReady = false;
@@ -943,6 +979,7 @@ export class OutputSink {
943
979
  readonly #chunkThrottleMs: number;
944
980
  readonly #maxColumns: number;
945
981
  readonly #artifactMaxBytes: number;
982
+ readonly #artifactPublisher?: TerminalArtifactPublisher;
946
983
  readonly #coalesceSanitize: boolean;
947
984
  #coalesceBuf = "";
948
985
 
@@ -957,6 +994,7 @@ export class OutputSink {
957
994
  chunkThrottleMs = 0,
958
995
  onRawChunk,
959
996
  artifactMaxBytes = DEFAULT_ARTIFACT_MAX_BYTES,
997
+ artifactPublisher,
960
998
  coalesceSanitize = process.env.PI_OUTPUT_SANITIZE_COALESCE === "1",
961
999
  } = options ?? {};
962
1000
  // Managed callers omit artifactPath at the allocation boundary; explicit callers
@@ -970,6 +1008,7 @@ export class OutputSink {
970
1008
  this.#onRawChunk = onRawChunk;
971
1009
  this.#chunkThrottleMs = chunkThrottleMs;
972
1010
  this.#artifactMaxBytes = Math.max(0, artifactMaxBytes);
1011
+ this.#artifactPublisher = artifactPublisher;
973
1012
  this.#coalesceSanitize = coalesceSanitize;
974
1013
  }
975
1014
 
@@ -1060,11 +1099,14 @@ export class OutputSink {
1060
1099
  // open, keep an independent raw replay prefix because retained head/tail
1061
1100
  // windows are trimmed and cannot reconstruct byte-correct artifacts.
1062
1101
  if (this.#artifactPath && this.#maxColumns === 0) this.#enqueueFileWrite(rawChunk, rawBytes);
1102
+ else if (this.#artifactPublisher && this.#maxColumns === 0)
1103
+ this.#enqueueTerminalArtifactChunk(rawChunk, rawBytes);
1063
1104
 
1064
1105
  if (rawBytes === 0) return;
1065
1106
 
1066
1107
  const visibleChunk = this.#maxColumns > 0 ? this.#applyColumnCap(sanitizedChunk) : sanitizedChunk;
1067
1108
  if (this.#artifactPath && this.#maxColumns > 0) this.#enqueueFileWrite(rawChunk, rawBytes);
1109
+ else if (this.#artifactPublisher && this.#maxColumns > 0) this.#enqueueTerminalArtifactChunk(rawChunk, rawBytes);
1068
1110
  if (this.#columnDroppedBytes > 0) this.#createFileSink();
1069
1111
  const visibleBytes = Buffer.byteLength(visibleChunk, "utf-8");
1070
1112
  if (visibleChunk.length > 0) {
@@ -1186,6 +1228,45 @@ export class OutputSink {
1186
1228
  return `\n[artifact truncated after ${this.#artifactBytes} bytes; omitted at least ${droppedBytes} bytes]\n`;
1187
1229
  }
1188
1230
 
1231
+ #recordArtifactFailure(phase: "create" | "write" | "end", error: unknown): void {
1232
+ if (this.#artifactFailureDiagnostic) return;
1233
+ const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
1234
+ const normalized = message || "unknown storage error";
1235
+ this.#artifactFailureDiagnostic = truncateHeadBytes(
1236
+ `${phase}: ${normalized}`,
1237
+ ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES,
1238
+ ).text;
1239
+ }
1240
+
1241
+ #recordTerminalArtifactDiagnostic(kind: "unavailable" | "failed", error?: unknown): void {
1242
+ if (this.#artifactFailureDiagnostic) return;
1243
+ const message =
1244
+ error === undefined
1245
+ ? "terminal artifact publisher unavailable"
1246
+ : (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim() ||
1247
+ "unknown publication error";
1248
+ this.#artifactFailureDiagnostic = truncateHeadBytes(
1249
+ `${kind}: ${message}`,
1250
+ ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES,
1251
+ ).text;
1252
+ }
1253
+
1254
+ // A failed write may have committed only a prefix, so the replay queue is no longer trustworthy.
1255
+ #failArtifact(phase: "create" | "write" | "end", error: unknown, sink?: Bun.FileSink): void {
1256
+ this.#recordArtifactFailure(phase, error);
1257
+ this.#artifactFinalizationFailed = true;
1258
+ this.#file = undefined;
1259
+ this.#fileReady = false;
1260
+ this.#pendingFileWrites = undefined;
1261
+ this.#pendingFileWriteBytes = 0;
1262
+ if (!sink) return;
1263
+ try {
1264
+ void Promise.resolve(sink.end()).catch(endError => this.#recordArtifactFailure("end", endError));
1265
+ } catch (endError) {
1266
+ this.#recordArtifactFailure("end", endError);
1267
+ }
1268
+ }
1269
+
1189
1270
  #capArtifactChunk(chunk: string, bytes: number): { chunk: string; bytes: number } | null {
1190
1271
  if (bytes === 0) return null;
1191
1272
  if (this.#artifactMaxBytes <= 0 || this.#artifactBytes >= this.#artifactMaxBytes) {
@@ -1201,18 +1282,23 @@ export class OutputSink {
1201
1282
  return kept.bytes > 0 ? { chunk: kept.text, bytes: kept.bytes } : null;
1202
1283
  }
1203
1284
 
1204
- #writeArtifactTruncationNotice(): void {
1205
- if (this.#artifactTruncatedBytes <= 0 || this.#artifactTruncationNoticeWritten) return;
1285
+ async #writeArtifactTruncationNotice(): Promise<void> {
1286
+ if (
1287
+ this.#artifactFinalizationFailed ||
1288
+ this.#artifactTruncatedBytes <= 0 ||
1289
+ this.#artifactTruncationNoticeWritten
1290
+ )
1291
+ return;
1206
1292
  const notice = this.#artifactTruncationNotice(this.#artifactTruncatedBytes);
1207
1293
  try {
1208
1294
  if (this.#fileReady && this.#file) {
1209
- this.#file.sink.write(notice);
1295
+ await this.#file.sink.write(notice);
1210
1296
  } else {
1211
1297
  this.#queuePendingFileWrite(notice, Buffer.byteLength(notice, "utf-8"));
1212
1298
  }
1213
1299
  this.#artifactTruncationNoticeWritten = true;
1214
- } catch {
1215
- /* ignore */
1300
+ } catch (error) {
1301
+ this.#failArtifact("write", error, this.#file?.sink);
1216
1302
  }
1217
1303
  }
1218
1304
 
@@ -1224,6 +1310,7 @@ export class OutputSink {
1224
1310
  }
1225
1311
 
1226
1312
  #enqueueFileWrite(chunk: string, bytes: number): void {
1313
+ if (this.#artifactFinalizationFailed || this.#finalized) return;
1227
1314
  const capped = this.#capArtifactChunk(chunk, bytes);
1228
1315
  if (!capped) return;
1229
1316
  this.#artifactBytes += capped.bytes;
@@ -1235,51 +1322,113 @@ export class OutputSink {
1235
1322
 
1236
1323
  try {
1237
1324
  this.#file.sink.write(capped.chunk);
1238
- } catch {
1239
- try {
1240
- void this.#file.sink.end();
1241
- } catch {
1242
- /* ignore */
1243
- }
1244
- this.#file = undefined;
1245
- this.#fileReady = false;
1246
- this.#queuePendingFileWrite(capped.chunk, capped.bytes);
1247
- this.#createFileSink();
1325
+ } catch (error) {
1326
+ this.#failArtifact("write", error, this.#file.sink);
1327
+ }
1328
+ }
1329
+
1330
+ #enqueueTerminalArtifactChunk(chunk: string, bytes: number): void {
1331
+ if (this.#artifactFinalizationFailed || this.#finalized) return;
1332
+ const capped = this.#capArtifactChunk(chunk, bytes);
1333
+ if (!capped) return;
1334
+ this.#artifactBytes += capped.bytes;
1335
+ if (!this.#pendingArtifactContent) this.#pendingArtifactContent = [capped.chunk];
1336
+ else this.#pendingArtifactContent.push(capped.chunk);
1337
+ const pending = this.#pendingArtifactContent;
1338
+ if (!pending) return;
1339
+ if (pending.length > MAX_PENDING) {
1340
+ pending[0] = pending.join("");
1341
+ pending.length = 1;
1248
1342
  }
1249
1343
  }
1250
1344
 
1251
1345
  #createFileSink(): boolean {
1252
- if (this.#finalized) return false;
1346
+ if (this.#finalized || this.#artifactFinalizationFailed) return false;
1253
1347
 
1254
1348
  if (!this.#artifactPath) return false;
1255
1349
  if (this.#fileReady) return this.#file != null;
1350
+
1351
+ let sink: Bun.FileSink;
1256
1352
  try {
1257
- const sink = Bun.file(this.#artifactPath).writer();
1258
- this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
1353
+ sink = Bun.file(this.#artifactPath).writer();
1354
+ } catch (error) {
1355
+ this.#recordArtifactFailure("create", error);
1356
+ return false;
1357
+ }
1358
+ this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
1259
1359
 
1360
+ try {
1260
1361
  const pending = this.#pendingFileWrites;
1261
1362
  if (pending) {
1262
1363
  for (const chunk of pending) sink.write(chunk);
1263
1364
  }
1365
+ } catch (error) {
1366
+ this.#failArtifact("write", error, sink);
1367
+ return false;
1368
+ }
1264
1369
 
1265
- this.#fileReady = true;
1266
- this.#pendingFileWrites = undefined;
1267
- this.#pendingFileWriteBytes = 0;
1370
+ this.#fileReady = true;
1371
+ this.#pendingFileWrites = undefined;
1372
+ this.#pendingFileWriteBytes = 0;
1373
+ return true;
1374
+ }
1375
+
1376
+ #shouldPublishTerminalArtifact(): boolean {
1377
+ return (
1378
+ this.#artifactPublisher !== undefined &&
1379
+ this.#artifactPath === undefined &&
1380
+ (this.#truncated || this.#columnDroppedBytes > 0 || this.#artifactTruncatedBytes > 0)
1381
+ );
1382
+ }
1268
1383
 
1269
- return true;
1270
- } catch {
1271
- try {
1272
- void this.#file?.sink?.end();
1273
- } catch {
1274
- /* ignore */
1384
+ async #publishTerminalArtifact(): Promise<string | undefined> {
1385
+ if (
1386
+ !this.#shouldPublishTerminalArtifact() ||
1387
+ this.#artifactPublishAttempted ||
1388
+ this.#artifactFinalizationFailed
1389
+ ) {
1390
+ return this.#publishedArtifactId;
1391
+ }
1392
+ this.#artifactPublishAttempted = true;
1393
+ const content = this.#pendingArtifactContent?.join("") ?? "";
1394
+ const omittedBytes = this.#artifactTruncatedBytes;
1395
+ try {
1396
+ const published = await this.#artifactPublisher!(content, {
1397
+ totalBytes: this.#totalBytes,
1398
+ omittedBytes,
1399
+ });
1400
+ if (published.status === "unavailable") {
1401
+ this.#recordTerminalArtifactDiagnostic("unavailable");
1402
+ this.#artifactFinalizationFailed = true;
1403
+ return undefined;
1275
1404
  }
1276
- this.#file = undefined;
1277
- // Keep #pendingFileWriteBytes in sync with the preserved queue so
1278
- // later retry/threshold decisions don't undercount retained bytes.
1279
- this.#pendingFileWriteBytes = this.#pendingFileWrites
1280
- ? this.#pendingFileWrites.reduce((sum, chunk) => sum + Buffer.byteLength(chunk), 0)
1281
- : 0;
1282
- return false;
1405
+ if (published.status === "failed") {
1406
+ this.#recordTerminalArtifactDiagnostic("failed", published.diagnostic);
1407
+ this.#artifactFinalizationFailed = true;
1408
+ return undefined;
1409
+ }
1410
+ if (published.artifactId.length === 0) {
1411
+ this.#recordTerminalArtifactDiagnostic("failed", "storage returned no artifact id");
1412
+ this.#artifactFinalizationFailed = true;
1413
+ return undefined;
1414
+ }
1415
+ if (published.omittedBytes !== undefined) {
1416
+ if (!Number.isSafeInteger(published.omittedBytes) || published.omittedBytes < 0) {
1417
+ this.#recordTerminalArtifactDiagnostic("failed", "publisher returned invalid omitted-byte accounting");
1418
+ this.#artifactFinalizationFailed = true;
1419
+ return undefined;
1420
+ }
1421
+ this.#artifactTruncatedBytes += published.omittedBytes;
1422
+ }
1423
+ this.#publishedArtifactId = published.artifactId;
1424
+ this.#finalized = true;
1425
+ return this.#publishedArtifactId;
1426
+ } catch (error) {
1427
+ this.#recordTerminalArtifactDiagnostic("failed", error);
1428
+ this.#artifactFinalizationFailed = true;
1429
+ return undefined;
1430
+ } finally {
1431
+ this.#pendingArtifactContent = undefined;
1283
1432
  }
1284
1433
  }
1285
1434
 
@@ -1302,25 +1451,34 @@ export class OutputSink {
1302
1451
  * minimizer rewrites the captured output after the raw bytes have already
1303
1452
  * been streamed.
1304
1453
  *
1305
- * After this call the buffer is authoritative: streaming counters realign
1306
- * to the replacement, the retained head window is cleared, and head
1307
- * retention is disabled so subsequent `push()` calls append directly to the
1308
- * tail buffer instead of repopulating the (now meaningless) head window
1309
- * — which would otherwise reorder content and trip the middle-elision
1310
- * branch in `dump()` against stale totals.
1454
+ * After this call the replacement is authoritative: counters reflect its full
1455
+ * size, the configured head+tail byte windows are retained with UTF-8-safe
1456
+ * boundaries, and future head accumulation is disabled so later `push()` calls
1457
+ * append directly to the tail without reordering the replacement.
1311
1458
  */
1312
1459
  replace(text: string): void {
1313
1460
  this.#coalesceBuf = "";
1314
- this.#setTail(text);
1461
+ const replacementBytes = Buffer.byteLength(text, "utf-8");
1315
1462
  this.#head = "";
1316
1463
  this.#headBytes = 0;
1464
+ this.#setTail("");
1465
+
1466
+ if (this.#headLimit > 0) {
1467
+ const head = truncateHeadBytes(text, this.#headLimit);
1468
+ this.#appendHead(head.text, head.bytes);
1469
+ const tailText = text.substring(head.text.length);
1470
+ this.#setTail(tailText, replacementBytes - head.bytes);
1471
+ } else {
1472
+ this.#setTail(text, replacementBytes);
1473
+ }
1474
+ this.#trimTailTo(this.#spillThreshold);
1317
1475
  this.#headRetentionDisabled = true;
1318
- this.#totalBytes = this.#bufferBytes;
1319
- this.#processedBytes = this.#bufferBytes;
1476
+ this.#totalBytes = replacementBytes;
1477
+ this.#processedBytes = replacementBytes;
1320
1478
  this.#totalLines = countNewlines(text);
1321
1479
  this.#processedLines = this.#totalLines;
1322
1480
  this.#sawData = text.length > 0;
1323
- this.#truncated = false;
1481
+ this.#truncated = replacementBytes > this.#headBytes + this.#bufferBytes;
1324
1482
  this.#currentLineBytes = 0;
1325
1483
  this.#columnEllipsisAdded = false;
1326
1484
  this.#columnDroppedBytes = 0;
@@ -1333,18 +1491,35 @@ export class OutputSink {
1333
1491
  const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1334
1492
 
1335
1493
  let artifactId: string | undefined;
1336
- if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
1337
- this.#writeArtifactTruncationNotice();
1338
- if (this.#file) {
1339
- artifactId = this.#file.artifactId;
1340
- await this.#file.sink.end();
1341
- this.#finalized = true;
1494
+ if (this.#finalized) {
1495
+ artifactId = this.#publishedArtifactId ?? this.#artifactId;
1496
+ } else if (!this.#artifactFinalizationFailed) {
1497
+ if (this.#artifactPublisher && !this.#artifactPath) {
1498
+ artifactId = await this.#publishTerminalArtifact();
1499
+ }
1500
+ if (!this.#artifactFinalizationFailed && this.#artifactPath) {
1501
+ if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
1502
+ await this.#writeArtifactTruncationNotice();
1503
+ if (this.#file) {
1504
+ try {
1505
+ await this.#file.sink.end();
1506
+ artifactId = this.#file.artifactId;
1507
+ this.#finalized = true;
1508
+ } catch (error) {
1509
+ this.#recordArtifactFailure("end", error);
1510
+ this.#artifactFinalizationFailed = true;
1511
+ this.#file = undefined;
1512
+ this.#fileReady = false;
1513
+ }
1514
+ }
1515
+ }
1342
1516
  }
1343
1517
  if (this.#finalized) {
1344
1518
  // Terminal: the artifact is closed; replay state is no longer needed.
1345
1519
  this.#pendingFileWrites = undefined;
1346
1520
  this.#pendingFileWriteBytes = 0;
1347
1521
  this.#fileReady = false;
1522
+ this.#pendingArtifactContent = undefined;
1348
1523
  }
1349
1524
  // Non-finalized dumps (no artifact sink ever opened) keep the raw replay
1350
1525
  // queue so a later post-dump push that spills produces a CUMULATIVE
@@ -1370,24 +1545,18 @@ export class OutputSink {
1370
1545
  let elidedLines: number | undefined;
1371
1546
 
1372
1547
  if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
1373
- // Middle was elided. Emit head + marker + tail.
1548
+ // Middle was elided. Emit both explicit windows, including when they are
1549
+ // fragments of one source line (the marker still carries byte evidence).
1374
1550
  elidedBytes = Math.max(0, effectiveTotalBytes - headBytes - tailBytes);
1375
1551
  elidedLines = Math.max(0, processedTotalLines - headLines - tailLines);
1376
- if (elidedLines === 0) {
1377
- body = headText;
1378
- outputBytes = headBytes;
1379
- outputLines = headLines;
1380
- this.#truncated = true;
1381
- } else {
1382
- const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
1383
- const markerBytes = Buffer.byteLength(marker, "utf-8");
1384
- const headSep = headText.endsWith("\n") ? "" : "\n";
1385
- const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
1386
- body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
1387
- outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
1388
- outputLines = headLines + 1 + tailLines;
1389
- this.#truncated = true;
1390
- }
1552
+ const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
1553
+ const markerBytes = Buffer.byteLength(marker, "utf-8");
1554
+ const headSep = headText.endsWith("\n") ? "" : "\n";
1555
+ const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
1556
+ body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
1557
+ outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
1558
+ outputLines = headLines + 1 + tailLines;
1559
+ this.#truncated = true;
1391
1560
  } else if (headBytes > 0) {
1392
1561
  // Head + tail combine into the full buffered output (no overlap or elision).
1393
1562
  body = `${headText}${tailBuf}`;
@@ -1412,6 +1581,7 @@ export class OutputSink {
1412
1581
  columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
1413
1582
  columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
1414
1583
  artifactTruncatedBytes: this.#artifactTruncatedBytes > 0 ? this.#artifactTruncatedBytes : undefined,
1584
+ artifactFailureDiagnostic: this.#artifactFailureDiagnostic,
1415
1585
  artifactId,
1416
1586
  };
1417
1587
  }
@@ -7,16 +7,17 @@
7
7
  * {@link AuthCredential} payloads that go straight into the store.
8
8
  *
9
9
  * Sources:
10
- * - Claude Code: `~/.claude/.credentials.json` (Linux/WSL/Windows native),
11
- * the macOS Keychain (`Claude Code-credentials`), and env vars.
12
- * - Codex CLI: `~/.codex/auth.json` (OAuth `tokens` block or stored
13
- * `OPENAI_API_KEY`), and env vars.
10
+ * - Claude Code: `$CLAUDE_CONFIG_DIR/.credentials.json` (defaults to
11
+ * `~/.claude/.credentials.json`) on Linux/WSL/Windows native, the macOS
12
+ * Keychain (`Claude Code-credentials`), and env vars.
13
+ * - Codex CLI: `$CODEX_HOME/auth.json` (defaults to `~/.codex/auth.json`;
14
+ * OAuth `tokens` block or stored `OPENAI_API_KEY`), and env vars.
14
15
  */
15
16
  import * as fs from "node:fs/promises";
16
17
  import * as os from "node:os";
17
18
  import * as path from "node:path";
18
19
  import type { AuthCredential, OAuthCredential } from "@gajae-code/ai";
19
- import { isEnoent } from "@gajae-code/utils";
20
+ import { $credentialEnv, isEnoent } from "@gajae-code/utils";
20
21
  import { redactSecret } from "./provider-onboarding";
21
22
 
22
23
  /** gjc provider ids that external credentials map onto. */
@@ -79,6 +80,18 @@ export interface DiscoveryOptions {
79
80
  env?: Record<string, string | undefined>;
80
81
  /** Override the platform (defaults to `process.platform`). */
81
82
  platform?: NodeJS.Platform;
83
+ /**
84
+ * Claude Code config directory holding `.credentials.json`.
85
+ *
86
+ * Defaults to the trusted `CLAUDE_CONFIG_DIR` value, else `<homeDir>/.claude`.
87
+ */
88
+ claudeConfigDir?: string;
89
+ /**
90
+ * Codex CLI home directory holding `auth.json`.
91
+ *
92
+ * Defaults to the trusted `CODEX_HOME` value, else `<homeDir>/.codex`.
93
+ */
94
+ codexHome?: string;
82
95
  /**
83
96
  * Reader for the macOS Keychain `Claude Code-credentials` entry. Defaults to
84
97
  * shelling out to `security`; injected in tests. Returns the raw JSON string,
@@ -206,12 +219,24 @@ function parseClaudeCredentials(
206
219
  };
207
220
  }
208
221
 
222
+ /**
223
+ * A discovery root plus the leak-free label used in redacted summaries.
224
+ *
225
+ * `display` never contains a resolved filesystem path: it is either the
226
+ * home-relative default or the env-var spelling that redirected the lookup.
227
+ */
228
+ interface ResolvedExternalDir {
229
+ dir: string;
230
+ display: string;
231
+ }
232
+
209
233
  async function discoverClaudeCode(
210
- opts: Required<Pick<DiscoveryOptions, "homeDir" | "platform">> & Pick<DiscoveryOptions, "readClaudeKeychain">,
234
+ opts: Required<Pick<DiscoveryOptions, "platform">> &
235
+ Pick<DiscoveryOptions, "readClaudeKeychain"> & { configDir: ResolvedExternalDir },
211
236
  result: CredentialDiscoveryResult,
212
237
  ): Promise<void> {
213
- const filePath = path.join(opts.homeDir, ".claude", ".credentials.json");
214
- const displayPath = `~/.claude/.credentials.json`;
238
+ const filePath = path.join(opts.configDir.dir, ".credentials.json");
239
+ const displayPath = `${opts.configDir.display}/.credentials.json`;
215
240
  let fileRaw: string | null = null;
216
241
  try {
217
242
  fileRaw = await fs.readFile(filePath, "utf-8");
@@ -324,9 +349,9 @@ function parseCodexAuth(raw: string, source: string): ImportableCredential | Ski
324
349
  return { origin: "codex-file", source, reason: "no OAuth tokens or OPENAI_API_KEY present (unsupported shape)" };
325
350
  }
326
351
 
327
- async function discoverCodex(homeDir: string, result: CredentialDiscoveryResult): Promise<void> {
328
- const filePath = path.join(homeDir, ".codex", "auth.json");
329
- const displayPath = "~/.codex/auth.json";
352
+ async function discoverCodex(home: ResolvedExternalDir, result: CredentialDiscoveryResult): Promise<void> {
353
+ const filePath = path.join(home.dir, "auth.json");
354
+ const displayPath = `${home.display}/auth.json`;
330
355
  let raw: string | null = null;
331
356
  try {
332
357
  raw = await fs.readFile(filePath, "utf-8");
@@ -367,6 +392,34 @@ function pushOutcome(result: CredentialDiscoveryResult, outcome: ImportableCrede
367
392
  else result.importable.push(outcome);
368
393
  }
369
394
 
395
+ /**
396
+ * Resolve a discovery root that the external CLI itself relocates through the
397
+ * environment (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`).
398
+ *
399
+ * Both variables are read through `$credentialEnv`, so the caller's project
400
+ * `.env` cannot redirect discovery at a credential file a repository ships —
401
+ * the same boundary provider authentication already uses.
402
+ *
403
+ * Relative values are rejected. Claude Code and Codex resolve them against the
404
+ * process cwd, which for gjc is the user's project directory; honouring that
405
+ * would reintroduce the project-controlled redirect the trust boundary exists
406
+ * to prevent. An explicit `options.env` (tests, embedders) is consulted verbatim
407
+ * instead of the ambient environment.
408
+ */
409
+ function resolveExternalDir(
410
+ name: "CLAUDE_CONFIG_DIR" | "CODEX_HOME",
411
+ envOverride: Record<string, string | undefined> | undefined,
412
+ fallbackDir: string,
413
+ fallbackDisplay: string,
414
+ ): ResolvedExternalDir {
415
+ const raw = envOverride ? envOverride[name] : $credentialEnv(name);
416
+ const trimmed = raw?.trim();
417
+ if (trimmed && path.isAbsolute(trimmed)) {
418
+ return { dir: trimmed, display: `$${name}` };
419
+ }
420
+ return { dir: fallbackDir, display: fallbackDisplay };
421
+ }
422
+
370
423
  /**
371
424
  * Discover Claude Code and Codex CLI credentials across files, the macOS
372
425
  * Keychain, and environment variables. Never throws for individual unreadable or
@@ -376,9 +429,18 @@ export async function discoverExternalCredentials(options: DiscoveryOptions = {}
376
429
  const homeDir = options.homeDir ?? os.homedir();
377
430
  const env = options.env ?? process.env;
378
431
  const platform = options.platform ?? process.platform;
432
+ const claudeConfigDir: ResolvedExternalDir = options.claudeConfigDir
433
+ ? { dir: options.claudeConfigDir, display: "$CLAUDE_CONFIG_DIR" }
434
+ : resolveExternalDir("CLAUDE_CONFIG_DIR", options.env, path.join(homeDir, ".claude"), "~/.claude");
435
+ const codexHome: ResolvedExternalDir = options.codexHome
436
+ ? { dir: options.codexHome, display: "$CODEX_HOME" }
437
+ : resolveExternalDir("CODEX_HOME", options.env, path.join(homeDir, ".codex"), "~/.codex");
379
438
  const result: CredentialDiscoveryResult = { importable: [], skipped: [], environment: [] };
380
- await discoverClaudeCode({ homeDir, platform, readClaudeKeychain: options.readClaudeKeychain }, result);
381
- await discoverCodex(homeDir, result);
439
+ await discoverClaudeCode(
440
+ { configDir: claudeConfigDir, platform, readClaudeKeychain: options.readClaudeKeychain },
441
+ result,
442
+ );
443
+ await discoverCodex(codexHome, result);
382
444
  discoverEnvironment(env, result);
383
445
  return result;
384
446
  }