@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.
- package/CHANGELOG.md +82 -1
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +54 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +16 -2
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +38 -2
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/src/async/job-manager.ts +78 -20
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +51 -0
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +18 -1
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-runtime.ts +411 -17
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
- package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
- package/src/gjc-runtime/workflow-manifest.ts +4 -68
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -0
- package/src/modes/controllers/selector-controller.ts +45 -0
- package/src/modes/runtime-init.ts +3 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session.ts +88 -9
- package/src/session/agent-session.ts +822 -172
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +86 -55
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- 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({
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
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
|
-
|
|
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 =
|
|
1319
|
-
this.#processedBytes =
|
|
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 =
|
|
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.#
|
|
1337
|
-
|
|
1338
|
-
if (this.#
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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, "
|
|
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.
|
|
214
|
-
const displayPath =
|
|
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(
|
|
328
|
-
const filePath = path.join(
|
|
329
|
-
const displayPath =
|
|
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(
|
|
381
|
-
|
|
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
|
}
|