@gajae-code/coding-agent 0.5.1 → 0.5.3
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 +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover and import existing Claude Code / Codex CLI credentials.
|
|
3
|
+
*
|
|
4
|
+
* This is the testable core behind `gjc setup credentials` (CLI, primary entry)
|
|
5
|
+
* and the TUI provider-onboarding "import existing credentials" action. It never
|
|
6
|
+
* prints or returns raw tokens: callers receive redacted summaries plus opaque
|
|
7
|
+
* {@link AuthCredential} payloads that go straight into the store.
|
|
8
|
+
*
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "node:fs/promises";
|
|
16
|
+
import * as os from "node:os";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import type { AuthCredential, OAuthCredential } from "@gajae-code/ai";
|
|
19
|
+
import { isEnoent } from "@gajae-code/utils";
|
|
20
|
+
import { redactSecret } from "./provider-onboarding";
|
|
21
|
+
|
|
22
|
+
/** gjc provider ids that external credentials map onto. */
|
|
23
|
+
export type ExternalProvider = "anthropic" | "openai-codex";
|
|
24
|
+
|
|
25
|
+
/** Where a discovered credential came from. */
|
|
26
|
+
export type CredentialOrigin = "claude-code-file" | "claude-code-keychain" | "codex-file";
|
|
27
|
+
|
|
28
|
+
/** Human labels for providers, used in redacted summaries. */
|
|
29
|
+
export const EXTERNAL_PROVIDER_LABELS: Record<ExternalProvider, string> = {
|
|
30
|
+
anthropic: "Claude (Anthropic)",
|
|
31
|
+
"openai-codex": "Codex (ChatGPT)",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** A credential that can be safely imported into gjc's store. */
|
|
35
|
+
export interface ImportableCredential {
|
|
36
|
+
provider: ExternalProvider;
|
|
37
|
+
origin: CredentialOrigin;
|
|
38
|
+
/** Redacted, human-readable description of where this came from. */
|
|
39
|
+
source: string;
|
|
40
|
+
kind: AuthCredential["type"];
|
|
41
|
+
identity?: { email?: string; accountId?: string };
|
|
42
|
+
/** Epoch-ms expiry for OAuth credentials, when known. */
|
|
43
|
+
expiresAt?: number;
|
|
44
|
+
/** Redacted access token / API key — safe to display. */
|
|
45
|
+
redactedToken: string;
|
|
46
|
+
/** Opaque credential payload. Never include this in any summary output. */
|
|
47
|
+
credential: AuthCredential;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A source that was found but could not be imported. */
|
|
51
|
+
export interface SkippedCredential {
|
|
52
|
+
origin: CredentialOrigin;
|
|
53
|
+
source: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Ambient environment-backed auth that is already usable without import. */
|
|
58
|
+
export interface EnvironmentCredentialHint {
|
|
59
|
+
provider: ExternalProvider;
|
|
60
|
+
variable: string;
|
|
61
|
+
redactedValue: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CredentialDiscoveryResult {
|
|
65
|
+
importable: ImportableCredential[];
|
|
66
|
+
skipped: SkippedCredential[];
|
|
67
|
+
environment: EnvironmentCredentialHint[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DiscoveryOptions {
|
|
71
|
+
/** Override the home directory (defaults to `os.homedir()`). */
|
|
72
|
+
homeDir?: string;
|
|
73
|
+
/** Override the environment (defaults to `process.env`). */
|
|
74
|
+
env?: Record<string, string | undefined>;
|
|
75
|
+
/** Override the platform (defaults to `process.platform`). */
|
|
76
|
+
platform?: NodeJS.Platform;
|
|
77
|
+
/**
|
|
78
|
+
* Reader for the macOS Keychain `Claude Code-credentials` entry. Defaults to
|
|
79
|
+
* shelling out to `security`; injected in tests. Returns the raw JSON string,
|
|
80
|
+
* or `null` when no entry exists.
|
|
81
|
+
*/
|
|
82
|
+
readClaudeKeychain?: () => Promise<string | null>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type CredentialUpserter = (provider: string, credential: AuthCredential) => unknown | Promise<unknown>;
|
|
86
|
+
|
|
87
|
+
export interface ImportSummary {
|
|
88
|
+
imported: ImportableCredential[];
|
|
89
|
+
failed: Array<{ credential: ImportableCredential; error: string }>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Source-file shapes ──────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
interface ClaudeCredentialsFile {
|
|
95
|
+
claudeAiOauth?: {
|
|
96
|
+
accessToken?: unknown;
|
|
97
|
+
refreshToken?: unknown;
|
|
98
|
+
expiresAt?: unknown;
|
|
99
|
+
scopes?: unknown;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface CodexAuthFile {
|
|
104
|
+
OPENAI_API_KEY?: unknown;
|
|
105
|
+
tokens?: {
|
|
106
|
+
id_token?: unknown;
|
|
107
|
+
access_token?: unknown;
|
|
108
|
+
refresh_token?: unknown;
|
|
109
|
+
account_id?: unknown;
|
|
110
|
+
};
|
|
111
|
+
last_refresh?: unknown;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ANTHROPIC_ENV_KEYS = ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"] as const;
|
|
115
|
+
const OPENAI_ENV_KEYS = ["OPENAI_API_KEY"] as const;
|
|
116
|
+
|
|
117
|
+
// ─── JWT helpers (best-effort identity/expiry extraction) ────────────────────
|
|
118
|
+
|
|
119
|
+
const OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
|
|
120
|
+
const OPENAI_PROFILE_CLAIM = "https://api.openai.com/profile";
|
|
121
|
+
|
|
122
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
|
123
|
+
try {
|
|
124
|
+
const parts = token.split(".");
|
|
125
|
+
if (parts.length !== 3) return null;
|
|
126
|
+
const payload = parts[1] ?? "";
|
|
127
|
+
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
|
|
128
|
+
const parsed = JSON.parse(decoded) as unknown;
|
|
129
|
+
return typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
136
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build a user-safe `skipped.reason` for a failed credential source.
|
|
141
|
+
*
|
|
142
|
+
* NEVER include the raw exception message: JSON parse errors and filesystem
|
|
143
|
+
* errors can echo back file contents (including bare token-like substrings) or
|
|
144
|
+
* other sensitive material, which then surfaces through CLI text/JSON and TUI
|
|
145
|
+
* discovery summaries. We expose only a generic phrase plus a non-sensitive
|
|
146
|
+
* error class (e.g. `SyntaxError`) or a standard Node syscall code (e.g.
|
|
147
|
+
* `EACCES`), both of which come from fixed, non-secret vocabularies.
|
|
148
|
+
*/
|
|
149
|
+
function sanitizedFailureReason(
|
|
150
|
+
base: "malformed credential file" | "unreadable credential file",
|
|
151
|
+
err: unknown,
|
|
152
|
+
): string {
|
|
153
|
+
if (!(err instanceof Error)) return base;
|
|
154
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
155
|
+
const detail = typeof code === "string" && /^[A-Z][A-Z0-9_]*$/.test(code) ? code : err.constructor.name;
|
|
156
|
+
return detail ? `${base} (${detail})` : base;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Claude Code discovery ───────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function parseClaudeCredentials(
|
|
162
|
+
raw: string,
|
|
163
|
+
origin: CredentialOrigin,
|
|
164
|
+
source: string,
|
|
165
|
+
): ImportableCredential | SkippedCredential {
|
|
166
|
+
let parsed: unknown;
|
|
167
|
+
try {
|
|
168
|
+
parsed = JSON.parse(raw) as ClaudeCredentialsFile;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return { origin, source, reason: sanitizedFailureReason("malformed credential file", err) };
|
|
171
|
+
}
|
|
172
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
173
|
+
return { origin, source, reason: "unsupported shape (root is not an object)" };
|
|
174
|
+
}
|
|
175
|
+
const credentials = parsed as ClaudeCredentialsFile;
|
|
176
|
+
const oauth = credentials.claudeAiOauth;
|
|
177
|
+
if (typeof oauth !== "object" || oauth === null) {
|
|
178
|
+
return { origin, source, reason: "missing claudeAiOauth block (unsupported shape)" };
|
|
179
|
+
}
|
|
180
|
+
const access = nonEmptyString(oauth.accessToken);
|
|
181
|
+
const refresh = nonEmptyString(oauth.refreshToken);
|
|
182
|
+
if (!access || !refresh) {
|
|
183
|
+
return { origin, source, reason: "missing accessToken or refreshToken" };
|
|
184
|
+
}
|
|
185
|
+
const expiresAt =
|
|
186
|
+
typeof oauth.expiresAt === "number" && Number.isFinite(oauth.expiresAt) ? oauth.expiresAt : undefined;
|
|
187
|
+
const credential: OAuthCredential = {
|
|
188
|
+
type: "oauth",
|
|
189
|
+
access,
|
|
190
|
+
refresh,
|
|
191
|
+
expires: expiresAt ?? Date.now(),
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
provider: "anthropic",
|
|
195
|
+
origin,
|
|
196
|
+
source,
|
|
197
|
+
kind: "oauth",
|
|
198
|
+
expiresAt,
|
|
199
|
+
redactedToken: redactSecret(access),
|
|
200
|
+
credential,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function discoverClaudeCode(
|
|
205
|
+
opts: Required<Pick<DiscoveryOptions, "homeDir" | "platform">> & Pick<DiscoveryOptions, "readClaudeKeychain">,
|
|
206
|
+
result: CredentialDiscoveryResult,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const filePath = path.join(opts.homeDir, ".claude", ".credentials.json");
|
|
209
|
+
const displayPath = `~/.claude/.credentials.json`;
|
|
210
|
+
let fileRaw: string | null = null;
|
|
211
|
+
try {
|
|
212
|
+
fileRaw = await fs.readFile(filePath, "utf-8");
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (!isEnoent(err)) {
|
|
215
|
+
result.skipped.push({
|
|
216
|
+
origin: "claude-code-file",
|
|
217
|
+
source: `Claude Code (${displayPath})`,
|
|
218
|
+
reason: sanitizedFailureReason("unreadable credential file", err),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (fileRaw !== null) {
|
|
223
|
+
const outcome = parseClaudeCredentials(fileRaw, "claude-code-file", `Claude Code (${displayPath})`);
|
|
224
|
+
pushOutcome(result, outcome);
|
|
225
|
+
} else if (opts.platform === "darwin") {
|
|
226
|
+
const reader = opts.readClaudeKeychain ?? defaultClaudeKeychainReader;
|
|
227
|
+
let keychainRaw: string | null = null;
|
|
228
|
+
try {
|
|
229
|
+
keychainRaw = await reader();
|
|
230
|
+
} catch (err) {
|
|
231
|
+
result.skipped.push({
|
|
232
|
+
origin: "claude-code-keychain",
|
|
233
|
+
source: "Claude Code (macOS Keychain)",
|
|
234
|
+
reason: sanitizedFailureReason("unreadable credential file", err),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (keychainRaw !== null && keychainRaw.trim().length > 0) {
|
|
238
|
+
const outcome = parseClaudeCredentials(keychainRaw, "claude-code-keychain", "Claude Code (macOS Keychain)");
|
|
239
|
+
pushOutcome(result, outcome);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function defaultClaudeKeychainReader(): Promise<string | null> {
|
|
245
|
+
const { $ } = await import("bun");
|
|
246
|
+
const proc = await $`security find-generic-password -s ${"Claude Code-credentials"} -w`.quiet().nothrow();
|
|
247
|
+
if (proc.exitCode !== 0) return null;
|
|
248
|
+
const out = proc.stdout.toString().trim();
|
|
249
|
+
return out.length > 0 ? out : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── Codex discovery ─────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
function parseCodexAuth(raw: string, source: string): ImportableCredential | SkippedCredential {
|
|
255
|
+
let parsed: unknown;
|
|
256
|
+
try {
|
|
257
|
+
parsed = JSON.parse(raw) as CodexAuthFile;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
return { origin: "codex-file", source, reason: sanitizedFailureReason("malformed credential file", err) };
|
|
260
|
+
}
|
|
261
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
262
|
+
return { origin: "codex-file", source, reason: "unsupported shape (root is not an object)" };
|
|
263
|
+
}
|
|
264
|
+
const auth = parsed as CodexAuthFile;
|
|
265
|
+
const tokens = auth.tokens;
|
|
266
|
+
const access = nonEmptyString(tokens?.access_token);
|
|
267
|
+
const refresh = nonEmptyString(tokens?.refresh_token);
|
|
268
|
+
if (access && refresh) {
|
|
269
|
+
const accessPayload = decodeJwtPayload(access);
|
|
270
|
+
const idPayload = nonEmptyString(tokens?.id_token) ? decodeJwtPayload(tokens?.id_token as string) : null;
|
|
271
|
+
const accountId =
|
|
272
|
+
nonEmptyString(tokens?.account_id) ??
|
|
273
|
+
nonEmptyString((accessPayload?.[OPENAI_AUTH_CLAIM] as { chatgpt_account_id?: unknown })?.chatgpt_account_id) ??
|
|
274
|
+
nonEmptyString((idPayload?.[OPENAI_AUTH_CLAIM] as { chatgpt_account_id?: unknown })?.chatgpt_account_id);
|
|
275
|
+
const email =
|
|
276
|
+
nonEmptyString((accessPayload?.[OPENAI_PROFILE_CLAIM] as { email?: unknown })?.email) ??
|
|
277
|
+
nonEmptyString(idPayload?.email)?.toLowerCase();
|
|
278
|
+
const expSeconds = typeof accessPayload?.exp === "number" ? accessPayload.exp : undefined;
|
|
279
|
+
const expiresAt = expSeconds !== undefined ? expSeconds * 1000 : undefined;
|
|
280
|
+
const credential: OAuthCredential = {
|
|
281
|
+
type: "oauth",
|
|
282
|
+
access,
|
|
283
|
+
refresh,
|
|
284
|
+
expires: expiresAt ?? Date.now(),
|
|
285
|
+
...(accountId ? { accountId } : {}),
|
|
286
|
+
...(email ? { email } : {}),
|
|
287
|
+
};
|
|
288
|
+
const identity =
|
|
289
|
+
accountId || email ? { ...(email ? { email } : {}), ...(accountId ? { accountId } : {}) } : undefined;
|
|
290
|
+
return {
|
|
291
|
+
provider: "openai-codex",
|
|
292
|
+
origin: "codex-file",
|
|
293
|
+
source,
|
|
294
|
+
kind: "oauth",
|
|
295
|
+
...(identity ? { identity } : {}),
|
|
296
|
+
expiresAt,
|
|
297
|
+
redactedToken: redactSecret(access),
|
|
298
|
+
credential,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (tokens && (tokens.access_token !== undefined || tokens.refresh_token !== undefined)) {
|
|
302
|
+
return {
|
|
303
|
+
origin: "codex-file",
|
|
304
|
+
source,
|
|
305
|
+
reason: "incomplete OAuth tokens (missing access_token or refresh_token)",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const apiKey = nonEmptyString(auth.OPENAI_API_KEY);
|
|
309
|
+
if (apiKey) {
|
|
310
|
+
return {
|
|
311
|
+
provider: "openai-codex",
|
|
312
|
+
origin: "codex-file",
|
|
313
|
+
source,
|
|
314
|
+
kind: "api_key",
|
|
315
|
+
redactedToken: redactSecret(apiKey),
|
|
316
|
+
credential: { type: "api_key", key: apiKey },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return { origin: "codex-file", source, reason: "no OAuth tokens or OPENAI_API_KEY present (unsupported shape)" };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function discoverCodex(homeDir: string, result: CredentialDiscoveryResult): Promise<void> {
|
|
323
|
+
const filePath = path.join(homeDir, ".codex", "auth.json");
|
|
324
|
+
const displayPath = "~/.codex/auth.json";
|
|
325
|
+
let raw: string | null = null;
|
|
326
|
+
try {
|
|
327
|
+
raw = await fs.readFile(filePath, "utf-8");
|
|
328
|
+
} catch (err) {
|
|
329
|
+
if (!isEnoent(err)) {
|
|
330
|
+
result.skipped.push({
|
|
331
|
+
origin: "codex-file",
|
|
332
|
+
source: `Codex CLI (${displayPath})`,
|
|
333
|
+
reason: sanitizedFailureReason("unreadable credential file", err),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
pushOutcome(result, parseCodexAuth(raw, `Codex CLI (${displayPath})`));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── Environment hints ───────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
function discoverEnvironment(env: Record<string, string | undefined>, result: CredentialDiscoveryResult): void {
|
|
344
|
+
for (const variable of ANTHROPIC_ENV_KEYS) {
|
|
345
|
+
const value = nonEmptyString(env[variable]);
|
|
346
|
+
if (value) {
|
|
347
|
+
result.environment.push({ provider: "anthropic", variable, redactedValue: redactSecret(value) });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const variable of OPENAI_ENV_KEYS) {
|
|
351
|
+
const value = nonEmptyString(env[variable]);
|
|
352
|
+
if (value) {
|
|
353
|
+
result.environment.push({ provider: "openai-codex", variable, redactedValue: redactSecret(value) });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
function pushOutcome(result: CredentialDiscoveryResult, outcome: ImportableCredential | SkippedCredential): void {
|
|
361
|
+
if ("reason" in outcome) result.skipped.push(outcome);
|
|
362
|
+
else result.importable.push(outcome);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Discover Claude Code and Codex CLI credentials across files, the macOS
|
|
367
|
+
* Keychain, and environment variables. Never throws for individual unreadable or
|
|
368
|
+
* malformed sources — those land in {@link CredentialDiscoveryResult.skipped}.
|
|
369
|
+
*/
|
|
370
|
+
export async function discoverExternalCredentials(options: DiscoveryOptions = {}): Promise<CredentialDiscoveryResult> {
|
|
371
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
372
|
+
const env = options.env ?? process.env;
|
|
373
|
+
const platform = options.platform ?? process.platform;
|
|
374
|
+
const result: CredentialDiscoveryResult = { importable: [], skipped: [], environment: [] };
|
|
375
|
+
await discoverClaudeCode({ homeDir, platform, readClaudeKeychain: options.readClaudeKeychain }, result);
|
|
376
|
+
await discoverCodex(homeDir, result);
|
|
377
|
+
discoverEnvironment(env, result);
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Redacted one-line summary of an importable credential. Never includes secrets. */
|
|
382
|
+
export function formatCredentialSummary(credential: ImportableCredential): string {
|
|
383
|
+
const provider = EXTERNAL_PROVIDER_LABELS[credential.provider];
|
|
384
|
+
const kind = credential.kind === "oauth" ? "OAuth" : "API key";
|
|
385
|
+
const identity = credential.identity?.email ?? credential.identity?.accountId;
|
|
386
|
+
const identityPart = identity ? ` ${identity}` : "";
|
|
387
|
+
let expiry = "";
|
|
388
|
+
if (credential.kind === "oauth" && credential.expiresAt !== undefined) {
|
|
389
|
+
expiry = credential.expiresAt < Date.now() ? " [expired]" : "";
|
|
390
|
+
}
|
|
391
|
+
return `${provider} · ${kind}${identityPart} · token ${credential.redactedToken}${expiry} (from ${credential.source})`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Redacted summary lines for an entire discovery result. Never includes secrets. */
|
|
395
|
+
export function formatDiscoverySummary(result: CredentialDiscoveryResult): string[] {
|
|
396
|
+
const lines: string[] = [];
|
|
397
|
+
for (const credential of result.importable) {
|
|
398
|
+
lines.push(`import ${formatCredentialSummary(credential)}`);
|
|
399
|
+
}
|
|
400
|
+
for (const skip of result.skipped) {
|
|
401
|
+
lines.push(`skip ${skip.source}: ${skip.reason}`);
|
|
402
|
+
}
|
|
403
|
+
for (const hint of result.environment) {
|
|
404
|
+
lines.push(
|
|
405
|
+
`env ${EXTERNAL_PROVIDER_LABELS[hint.provider]} · ${hint.variable}=${hint.redactedValue} (already active via environment)`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return lines;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Persist discovered credentials via `upsert`. Each credential is imported
|
|
413
|
+
* independently; a failure on one is recorded without aborting the rest.
|
|
414
|
+
*/
|
|
415
|
+
export async function importCredentials(
|
|
416
|
+
credentials: readonly ImportableCredential[],
|
|
417
|
+
upsert: CredentialUpserter,
|
|
418
|
+
): Promise<ImportSummary> {
|
|
419
|
+
const summary: ImportSummary = { imported: [], failed: [] };
|
|
420
|
+
for (const credential of credentials) {
|
|
421
|
+
try {
|
|
422
|
+
await upsert(credential.provider, credential.credential);
|
|
423
|
+
summary.imported.push(credential);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
summary.failed.push({ credential, error: err instanceof Error ? err.message : String(err) });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return summary;
|
|
429
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import type { AgentTool } from "@gajae-code/agent-core";
|
|
3
|
+
import { logger } from "@gajae-code/utils";
|
|
3
4
|
import { expandApplyPatchToEntries } from "../edit/modes/apply-patch";
|
|
4
5
|
import { ModeStateSchema } from "../gjc-runtime/state-schema";
|
|
5
6
|
import { LocalProtocolHandler, resolveLocalUrlToPath } from "../internal-urls/local-protocol";
|
|
@@ -78,7 +79,7 @@ function modeStatePath(cwd: string, skill: string, sessionId?: string): string {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function warnInvalidModeState(filePath: string, error: string): void {
|
|
81
|
-
|
|
82
|
+
logger.warn(`gjc skill-state: invalid mode-state at ${filePath}: ${error}`);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
async function readValidatedModeState(filePath: string): Promise<ModeState | null> {
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
4
4
|
import { type Model, modelsAreEqual } from "@gajae-code/ai";
|
|
5
5
|
import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
|
|
6
|
+
import { Spacer, Text } from "@gajae-code/tui";
|
|
6
7
|
import { setProjectDir } from "@gajae-code/utils";
|
|
7
8
|
import { jobElapsedMs } from "../async";
|
|
8
9
|
import {
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
import { extractExplicitThinkingSelector, formatModelSelectorValue, parseModelPattern } from "../config/model-resolver";
|
|
14
15
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../discovery/helpers.js";
|
|
15
16
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
17
|
+
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
18
|
+
import { theme } from "../modes/theme/theme";
|
|
16
19
|
import type { InteractiveModeContext } from "../modes/types";
|
|
17
20
|
import { formatModelOnboardingGuidance } from "../setup/model-onboarding-guidance";
|
|
18
21
|
import {
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
} from "../setup/provider-onboarding";
|
|
24
27
|
import { parseThinkingLevel } from "../thinking";
|
|
25
28
|
import { buildContextReportText } from "./helpers/context-report";
|
|
29
|
+
import { buildFastStatusReport } from "./helpers/fast-status-report";
|
|
26
30
|
import { formatDuration } from "./helpers/format";
|
|
27
31
|
import { commandConsumed, errorMessage, parseSlashCommand, usage } from "./helpers/parse";
|
|
28
32
|
import { handleSshAcp } from "./helpers/ssh";
|
|
@@ -41,6 +45,14 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
|
|
|
41
45
|
/** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
|
|
42
46
|
export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
|
|
43
47
|
|
|
48
|
+
function fastStatusRoleTargets(): Array<{ id: GjcModelAssignmentTargetId; label: string; isSubagentRole: boolean }> {
|
|
49
|
+
return GJC_MODEL_ASSIGNMENT_TARGET_IDS.map(id => ({
|
|
50
|
+
id,
|
|
51
|
+
label: GJC_MODEL_ASSIGNMENT_TARGETS[id].tag ?? id.toUpperCase(),
|
|
52
|
+
isSubagentRole: GJC_MODEL_ASSIGNMENT_TARGETS[id].settingsPath === "task.agentModelOverrides",
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
function parseProviderSetupSlashArgs(args: string): {
|
|
45
57
|
preset?: string;
|
|
46
58
|
compat?: string;
|
|
@@ -357,7 +369,13 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
357
369
|
return commandConsumed();
|
|
358
370
|
}
|
|
359
371
|
if (arg === "status") {
|
|
360
|
-
await runtime.output(
|
|
372
|
+
await runtime.output(
|
|
373
|
+
buildFastStatusReport({
|
|
374
|
+
session: runtime.session,
|
|
375
|
+
roleTargets: fastStatusRoleTargets(),
|
|
376
|
+
iconFast: theme.icon.fast,
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
361
379
|
return commandConsumed();
|
|
362
380
|
}
|
|
363
381
|
return usage("Usage: /fast [on|off|status]", runtime);
|
|
@@ -386,8 +404,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
386
404
|
return;
|
|
387
405
|
}
|
|
388
406
|
if (arg === "status") {
|
|
389
|
-
const
|
|
390
|
-
|
|
407
|
+
const report = buildFastStatusReport({
|
|
408
|
+
session: runtime.ctx.session,
|
|
409
|
+
roleTargets: fastStatusRoleTargets(),
|
|
410
|
+
iconFast: theme.icon.fast,
|
|
411
|
+
formatInactive: text => theme.fg("dim", text),
|
|
412
|
+
});
|
|
413
|
+
runtime.ctx.chatContainer.addChild(new Spacer(1));
|
|
414
|
+
runtime.ctx.chatContainer.addChild(new DynamicBorder());
|
|
415
|
+
runtime.ctx.chatContainer.addChild(new Text(report, 1, 0));
|
|
416
|
+
runtime.ctx.chatContainer.addChild(new DynamicBorder());
|
|
417
|
+
runtime.ctx.ui.requestRender();
|
|
391
418
|
runtime.ctx.editor.setText("");
|
|
392
419
|
return;
|
|
393
420
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Model } from "@gajae-code/ai";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A single line in the `/fast status` report: a labelled model and whether fast
|
|
5
|
+
* mode is effective for it. The `fast` flag is resolved by the caller
|
|
6
|
+
* (`buildFastStatusReport`) so each row can use the correct service tier — the
|
|
7
|
+
* main session tier for the current model / `modelRoles` roles, or the subagent
|
|
8
|
+
* tier (`task.serviceTier`) for `task.agentModelOverrides` roles.
|
|
9
|
+
*/
|
|
10
|
+
export interface FastStatusRow {
|
|
11
|
+
/** Display label, e.g. "현재 모델", "DEFAULT", "EXECUTOR". */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Resolved model for this row, if any. */
|
|
14
|
+
model?: Model;
|
|
15
|
+
/** Whether fast mode is effective for this row's model. */
|
|
16
|
+
fast: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FormatFastStatusReportArgs {
|
|
20
|
+
rows: FastStatusRow[];
|
|
21
|
+
/** The active theme's fast icon token (`theme.icon.fast`). */
|
|
22
|
+
iconFast: string;
|
|
23
|
+
/** Optional decorator for inactive ("off") text, e.g. theme dim in the TUI. */
|
|
24
|
+
formatInactive?: (text: string) => string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Title line of the `/fast status` report. */
|
|
28
|
+
export const FAST_STATUS_TITLE = "Fast 모드 상태";
|
|
29
|
+
|
|
30
|
+
/** The inactive marker shown for rows where fast mode does not apply. */
|
|
31
|
+
export const FAST_STATUS_OFF = "off";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format a multiline `/fast status` report. Pure and shared by the CLI
|
|
35
|
+
* (`handle`) and TUI (`handleTui`) command branches so the two never drift.
|
|
36
|
+
* Each row's fast/off state is decided by the caller (see
|
|
37
|
+
* {@link buildFastStatusReport}) so per-row service-tier differences are honored.
|
|
38
|
+
*/
|
|
39
|
+
export function formatFastStatusReport(args: FormatFastStatusReportArgs): string {
|
|
40
|
+
const { rows, iconFast } = args;
|
|
41
|
+
const formatInactive = args.formatInactive ?? ((text: string) => text);
|
|
42
|
+
const lines: string[] = [FAST_STATUS_TITLE];
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
if (!row.model) {
|
|
45
|
+
lines.push(`${row.label}: ${formatInactive(FAST_STATUS_OFF)}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const ref = `${row.model.provider}/${row.model.id}`;
|
|
49
|
+
lines.push(`${row.label}: ${ref} ${row.fast ? iconFast : formatInactive(FAST_STATUS_OFF)}`);
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Minimal session surface needed to build the `/fast status` report. */
|
|
55
|
+
export interface FastStatusSessionLike {
|
|
56
|
+
readonly model?: Model;
|
|
57
|
+
/** Fast predicate against the main session tier (current model + `modelRoles`). */
|
|
58
|
+
isFastForProvider(provider?: string): boolean;
|
|
59
|
+
/** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
|
|
60
|
+
isFastForSubagentProvider(provider?: string): boolean;
|
|
61
|
+
resolveRoleModelWithThinking(role: string): { model?: Model };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A role to enumerate in the report, with the tier source its subagent runs under. */
|
|
65
|
+
export interface FastStatusRoleTarget {
|
|
66
|
+
id: string;
|
|
67
|
+
label: string;
|
|
68
|
+
/**
|
|
69
|
+
* True for `task.agentModelOverrides` roles (executor/architect/planner/critic)
|
|
70
|
+
* that run under `task.serviceTier`; false for `modelRoles` roles (default)
|
|
71
|
+
* that run under the main session tier.
|
|
72
|
+
*/
|
|
73
|
+
isSubagentRole: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BuildFastStatusReportArgs {
|
|
77
|
+
session: FastStatusSessionLike;
|
|
78
|
+
/** Role targets to enumerate, in display order. */
|
|
79
|
+
roleTargets: ReadonlyArray<FastStatusRoleTarget>;
|
|
80
|
+
/** The active theme's fast icon token (`theme.icon.fast`). */
|
|
81
|
+
iconFast: string;
|
|
82
|
+
/** Optional decorator for inactive ("off") text, e.g. theme dim in the TUI. */
|
|
83
|
+
formatInactive?: (text: string) => string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the `/fast status` report from a live session: the active/current model
|
|
88
|
+
* followed by each assigned role (subagent) model. Unassigned roles are skipped
|
|
89
|
+
* so the report mirrors the `/model` selector, which only badges assigned roles.
|
|
90
|
+
*
|
|
91
|
+
* Subagent roles (`task.agentModelOverrides`) are evaluated against the
|
|
92
|
+
* effective subagent tier (`task.serviceTier`), while the current model and
|
|
93
|
+
* `modelRoles` roles use the main session tier — matching where each model
|
|
94
|
+
* actually runs.
|
|
95
|
+
*/
|
|
96
|
+
export function buildFastStatusReport(args: BuildFastStatusReportArgs): string {
|
|
97
|
+
const { session, roleTargets, iconFast, formatInactive } = args;
|
|
98
|
+
const rows: FastStatusRow[] = [
|
|
99
|
+
{ label: "현재 모델", model: session.model, fast: session.isFastForProvider(session.model?.provider) },
|
|
100
|
+
];
|
|
101
|
+
for (const target of roleTargets) {
|
|
102
|
+
const resolved = session.resolveRoleModelWithThinking(target.id);
|
|
103
|
+
if (resolved.model) {
|
|
104
|
+
const fast = target.isSubagentRole
|
|
105
|
+
? session.isFastForSubagentProvider(resolved.model.provider)
|
|
106
|
+
: session.isFastForProvider(resolved.model.provider);
|
|
107
|
+
rows.push({ label: target.label, model: resolved.model, fast });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return formatFastStatusReport({ rows, iconFast, formatInactive });
|
|
111
|
+
}
|
package/src/task/executor.ts
CHANGED
|
@@ -482,11 +482,17 @@ function getUsageTokens(usage: unknown): number {
|
|
|
482
482
|
return firstNumberField(record, ["totalTokens", "total_tokens"]) ?? 0;
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
function createSubagentSettings(baseSettings: Settings): Settings {
|
|
485
|
+
export function createSubagentSettings(baseSettings: Settings): Settings {
|
|
486
486
|
const snapshot: Partial<Record<SettingPath, unknown>> = {};
|
|
487
487
|
for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
488
488
|
snapshot[key] = baseSettings.get(key);
|
|
489
489
|
}
|
|
490
|
+
// Subagent-scoped service-tier override: "inherit" keeps the snapshotted main
|
|
491
|
+
// session tier; any explicit value applies only to subagent sessions.
|
|
492
|
+
const taskServiceTier = baseSettings.get("task.serviceTier");
|
|
493
|
+
if (taskServiceTier !== "inherit") {
|
|
494
|
+
snapshot.serviceTier = taskServiceTier;
|
|
495
|
+
}
|
|
490
496
|
return Settings.isolated({
|
|
491
497
|
...snapshot,
|
|
492
498
|
"async.enabled": false,
|