super-dsh 0.1.10 → 0.1.11
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/README.md +18 -2
- package/agent-agy/dist/agent.js +19 -1
- package/agent-claude/dist/adapter.js +106 -31
- package/agent-claude/dist/agent.js +177 -22
- package/agent-claude/dist/claude-client.js +84 -43
- package/agent-claude/dist/claude-launch.js +101 -0
- package/agent-claude/dist/claude-native-config.js +169 -0
- package/agent-claude/dist/claude-provider-gateway.js +340 -0
- package/agent-claude/dist/claude-providers.js +391 -0
- package/agent-claude/dist/claude-route.js +152 -0
- package/agent-claude/dist/index.js +214 -37
- package/agent-claude/dist/models.js +14 -0
- package/agent-claude/dist/sdk-runtime.js +320 -0
- package/agent-claude/package.json +6 -4
- package/agent-codex/README.md +18 -6
- package/agent-codex/dist/adapter.js +3 -2
- package/agent-codex/dist/agent.js +41 -7
- package/agent-codex/dist/app-server-client.js +7 -1
- package/agent-codex/dist/app-server-wire.js +10 -22
- package/agent-codex/dist/codex-launch.js +10 -32
- package/agent-codex/dist/codex-native-config.js +9 -3
- package/agent-codex/dist/codex-oauth.js +153 -0
- package/agent-codex/dist/codex-provider-gateway.js +36 -38
- package/agent-codex/dist/codex-providers.js +20 -7
- package/agent-codex/dist/codex-runtime.js +120 -0
- package/agent-codex/dist/index.js +18 -11
- package/agent-codex/package.json +1 -1
- package/agent-codex-sdk/dist/adapter.js +3 -2
- package/agent-codex-sdk/dist/agent.js +35 -1
- package/agent-codex-sdk/dist/codex-native-config.js +9 -3
- package/agent-codex-sdk/dist/codex-oauth.js +153 -0
- package/agent-codex-sdk/dist/codex-provider-gateway.js +36 -38
- package/agent-codex-sdk/dist/codex-providers.js +20 -7
- package/agent-codex-sdk/dist/codex-sdk-launch.js +5 -38
- package/agent-codex-sdk/dist/index.js +18 -11
- package/agent-codex-sdk/dist/sdk-runtime.js +2 -2
- package/agent-hermes/dist/agent.js +46 -2
- package/agent-hermes/dist/hermes-client.js +49 -1
- package/agent-omp/dist/agent.js +17 -4
- package/agent-omp-sdk/dist/agent.d.ts +11 -0
- package/agent-omp-sdk/dist/agent.js +17 -4
- package/agent-pi/dist/agent.js +33 -1
- package/client/index.js +150 -58
- package/package.json +2 -3
- package/superd-hub/dist/client/agent-settings.d.ts +66 -12
- package/superd-hub/dist/client/agent-settings.js +85 -14
- package/superd-hub/dist/client/runtime-settings-api.js +5 -5
- package/superd-hub/dist/world-mount.d.ts +6 -5
- package/superd-hub/dist/world-mount.js +6 -6
- package/superd-hub/lib/client/index.js +150 -58
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
* methods exist at all), drives the bridge lazily per session, and fans every
|
|
9
9
|
* message out through `projectClaudeEvent` (claude-events.ts).
|
|
10
10
|
*
|
|
11
|
+
* ## The SDK is runtime-self-fetched, not imported
|
|
12
|
+
*
|
|
13
|
+
* The SDK is not a static dependency of this package: the default factory
|
|
14
|
+
* resolves it at the first real prompt via `loadClaudeQuery(worldHome)`
|
|
15
|
+
* (sdk-runtime.ts), which fetches the pinned tree into the agent world's
|
|
16
|
+
* home. `ClaudeFactory` stays SYNCHRONOUS — the await lives in factory
|
|
17
|
+
* RESOLUTION, never between query construction and the `this.query`
|
|
18
|
+
* assignment (see `ensureStarted`).
|
|
19
|
+
*
|
|
11
20
|
* ## The two hard deviations from the codex precedent
|
|
12
21
|
*
|
|
13
22
|
* 1. **No shared singleton.** `agent-codex` multiplexes all clients through one
|
|
@@ -54,38 +63,31 @@
|
|
|
54
63
|
* every push is queued in order and there is no separate follow-up channel.
|
|
55
64
|
* `steer` differs by pushing with `priority: "now"`.
|
|
56
65
|
*/
|
|
57
|
-
import {
|
|
58
|
-
import { homedir } from "node:os";
|
|
59
|
-
import { join } from "node:path";
|
|
60
|
-
import { query as officialQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
66
|
+
import { loadClaudeQuery, resolveClaudeBinary } from "./sdk-runtime.js";
|
|
61
67
|
import { InputQueue } from "./input-queue.js";
|
|
62
68
|
import { projectClaudeEvent } from "./claude-events.js";
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const defaultExecutableResolver = () => process.env.CLAUDE_EXECUTABLE ?? resolveClaudeExecutable();
|
|
72
|
-
/** PATH first, then the common native-installer locations (a service unit
|
|
73
|
-
* PATH may not include ~/.local/bin — dev3 2026-09-22: "native binary not
|
|
74
|
-
* found at claude" mid-session). */
|
|
75
|
-
function resolveClaudeExecutable() {
|
|
76
|
-
for (const dir of (process.env.PATH ?? "").split(":").filter(Boolean)) {
|
|
77
|
-
if (existsSync(join(dir, "claude")))
|
|
78
|
-
return DEFAULT_CLAUDE_EXECUTABLE;
|
|
79
|
-
}
|
|
80
|
-
for (const candidate of [join(homedir(), ".local/bin/claude"), "/usr/local/bin/claude"]) {
|
|
81
|
-
if (existsSync(candidate))
|
|
82
|
-
return candidate;
|
|
83
|
-
}
|
|
84
|
-
return DEFAULT_CLAUDE_EXECUTABLE;
|
|
85
|
-
}
|
|
86
|
-
let factory = defaultFactory;
|
|
69
|
+
import { NATIVE_LAUNCH } from "./claude-launch.js";
|
|
70
|
+
/** The default executable resolution: Task 1's `resolveClaudeBinary` —
|
|
71
|
+
* `$CLAUDE_EXECUTABLE` (only while it exists) > `claude` on PATH >
|
|
72
|
+
* `~/.local/bin/claude` > `/usr/local/bin/claude`. `undefined` means "no
|
|
73
|
+
* local binary": `pathToClaudeCodeExecutable` is left unset and the SDK's
|
|
74
|
+
* bundled platform payload is the CLI (bundled-payload mode). */
|
|
75
|
+
const defaultExecutableResolver = () => resolveClaudeBinary();
|
|
76
|
+
let factory; // test injection only
|
|
87
77
|
let executableResolver = defaultExecutableResolver;
|
|
88
78
|
let refcount = 0;
|
|
79
|
+
/** The factory to construct queries with: a test-injected one wins (the sync
|
|
80
|
+
* seam); otherwise the SDK is self-fetched into `worldHome`'s durable runtime
|
|
81
|
+
* on first use (sdk-runtime.ts). */
|
|
82
|
+
async function resolveFactory(worldHome) {
|
|
83
|
+
if (factory)
|
|
84
|
+
return factory;
|
|
85
|
+
if (worldHome === undefined) {
|
|
86
|
+
throw new Error("agent-claude: client needs worldHome to fetch its SDK runtime (the provider passes dshHomePath)");
|
|
87
|
+
}
|
|
88
|
+
const sdkQuery = await loadClaudeQuery(worldHome);
|
|
89
|
+
return (o) => sdkQuery({ prompt: o.prompt, options: o.options });
|
|
90
|
+
}
|
|
89
91
|
/** Replace the SDK entry point (tests inject a fake query). */
|
|
90
92
|
export function setClaudeFactory(next) {
|
|
91
93
|
factory = next;
|
|
@@ -101,7 +103,7 @@ export function claudeClientRefCount() {
|
|
|
101
103
|
/** Restore module-level seams and refcount (test isolation only). */
|
|
102
104
|
export function resetClaudeClientState() {
|
|
103
105
|
refcount = 0;
|
|
104
|
-
factory =
|
|
106
|
+
factory = undefined;
|
|
105
107
|
executableResolver = defaultExecutableResolver;
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
@@ -114,13 +116,15 @@ export function resetClaudeClientState() {
|
|
|
114
116
|
* is the caller's to trace; the boot catalog stays as-is.
|
|
115
117
|
*/
|
|
116
118
|
export async function probeClaudeModels(config = {}) {
|
|
119
|
+
const queryFactory = await resolveFactory(config.worldHome);
|
|
120
|
+
const executable = executableResolver();
|
|
117
121
|
const queue = new InputQueue();
|
|
118
|
-
const query =
|
|
122
|
+
const query = queryFactory({
|
|
119
123
|
prompt: queue,
|
|
120
124
|
options: {
|
|
121
125
|
cwd: config.cwd ?? process.cwd(),
|
|
122
126
|
env: { ...process.env },
|
|
123
|
-
pathToClaudeCodeExecutable:
|
|
127
|
+
...(executable !== undefined ? { pathToClaudeCodeExecutable: executable } : {}),
|
|
124
128
|
persistSession: false,
|
|
125
129
|
},
|
|
126
130
|
});
|
|
@@ -145,10 +149,13 @@ export async function probeClaudeModels(config = {}) {
|
|
|
145
149
|
}
|
|
146
150
|
export class ClaudeSdkClient {
|
|
147
151
|
cwdPath;
|
|
152
|
+
worldHome;
|
|
148
153
|
claudeSessionId;
|
|
149
154
|
resumeSessionId;
|
|
150
155
|
canUseTool;
|
|
151
156
|
allowDangerouslySkipPermissions;
|
|
157
|
+
/** The provider-pinning launch this client will spawn under (NATIVE_LAUNCH until `setLaunch`). */
|
|
158
|
+
launch;
|
|
152
159
|
/** True while a user turn is in flight (set on push, cleared on `turn_end`/`agent_end`). */
|
|
153
160
|
turnActive = false;
|
|
154
161
|
queue;
|
|
@@ -164,10 +171,12 @@ export class ClaudeSdkClient {
|
|
|
164
171
|
throw new Error("agent-claude: claudeSessionId and resumeSessionId are mutually exclusive");
|
|
165
172
|
}
|
|
166
173
|
this.cwdPath = config.cwd ?? process.cwd();
|
|
174
|
+
this.worldHome = config.worldHome;
|
|
167
175
|
this.claudeSessionId = config.claudeSessionId;
|
|
168
176
|
this.resumeSessionId = config.resumeSessionId;
|
|
169
177
|
this.canUseTool = config.canUseTool;
|
|
170
178
|
this.allowDangerouslySkipPermissions = config.allowDangerouslySkipPermissions;
|
|
179
|
+
this.launch = config.launch ?? NATIVE_LAUNCH;
|
|
171
180
|
refcount += 1;
|
|
172
181
|
}
|
|
173
182
|
/**
|
|
@@ -186,6 +195,21 @@ export class ClaudeSdkClient {
|
|
|
186
195
|
get threadId() {
|
|
187
196
|
return this.threadIdValue;
|
|
188
197
|
}
|
|
198
|
+
/** The provider id this client was last told to spawn under (constructor fact; `null` = native). */
|
|
199
|
+
get appliedProvider() {
|
|
200
|
+
return this.launch.providerId;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Pin the launch this client will spawn under. Only legal before the query
|
|
204
|
+
* exists: once the CLI subprocess is up, a provider move is a different child
|
|
205
|
+
* (the agent swaps the whole client at a turn boundary — see `#reattach`).
|
|
206
|
+
*/
|
|
207
|
+
setLaunch(launch) {
|
|
208
|
+
if (this.query !== undefined) {
|
|
209
|
+
throw new Error("agent-claude: setLaunch after spawn is not allowed (a provider move swaps the client)");
|
|
210
|
+
}
|
|
211
|
+
this.launch = launch;
|
|
212
|
+
}
|
|
189
213
|
/** The client's own running/streaming attestation (idle-TTL revalidation gate). */
|
|
190
214
|
async getState() {
|
|
191
215
|
return { isStreaming: this.turnActive };
|
|
@@ -221,22 +245,27 @@ export class ClaudeSdkClient {
|
|
|
221
245
|
};
|
|
222
246
|
}
|
|
223
247
|
/**
|
|
224
|
-
* Lazily construct the query (zero-IO until this runs).
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* the
|
|
248
|
+
* Lazily construct the query (zero-IO until this runs). The factory is
|
|
249
|
+
* resolved first — an await (a test-injected seam, or the world-home SDK
|
|
250
|
+
* self-fetch) — and a caller that loses the race during that await reuses
|
|
251
|
+
* the winner's query at the re-check. From the construct line through the
|
|
252
|
+
* `this.query = query` assignment everything is synchronous (zero awaits),
|
|
253
|
+
* so concurrent first prompts can never build two queries. The single-query
|
|
254
|
+
* invariant therefore still depends on the `ClaudeFactory` contract staying
|
|
255
|
+
* synchronous (it must return the `Query` without awaiting). The reader loop
|
|
256
|
+
* is started fire-and-forget before any pending control call is replayed, so
|
|
257
|
+
* a throwing control can never leave the CLI undrained.
|
|
232
258
|
*/
|
|
233
259
|
async ensureStarted() {
|
|
234
260
|
if (this.query !== undefined)
|
|
235
261
|
return;
|
|
262
|
+
const f = await resolveFactory(this.worldHome); // await BEFORE the sync section
|
|
263
|
+
if (this.query !== undefined)
|
|
264
|
+
return; // lost the race to a concurrent first prompt — reuse its query
|
|
236
265
|
const queue = new InputQueue();
|
|
237
|
-
const query =
|
|
266
|
+
const query = f({ prompt: queue, options: this.buildOptions() });
|
|
238
267
|
this.queue = queue;
|
|
239
|
-
this.query = query;
|
|
268
|
+
this.query = query; // sync section: zero awaits before this assignment (invariant)
|
|
240
269
|
void this.runReader(query);
|
|
241
270
|
const controls = this.pendingControls;
|
|
242
271
|
this.pendingControls = [];
|
|
@@ -303,11 +332,23 @@ export class ClaudeSdkClient {
|
|
|
303
332
|
}
|
|
304
333
|
// --- internals ------------------------------------------------------------
|
|
305
334
|
buildOptions() {
|
|
335
|
+
const launchExecutable = this.launch.pathToClaudeCodeExecutable;
|
|
336
|
+
const executable = launchExecutable !== undefined && launchExecutable !== "" ? launchExecutable : executableResolver();
|
|
306
337
|
return {
|
|
307
338
|
cwd: this.cwdPath,
|
|
308
|
-
env
|
|
309
|
-
|
|
339
|
+
// The launch env holds ADDITIONS only; spreading process.env first keeps
|
|
340
|
+
// PATH/HOME and the user's own key alive (NATIVE_LAUNCH carries an empty
|
|
341
|
+
// env, so the native shape is exactly `{ ...process.env }`).
|
|
342
|
+
env: { ...process.env, ...this.launch.env },
|
|
343
|
+
// The `--settings` command-line tier (a JSON string) outranks the user's
|
|
344
|
+
// native settings.json env, so a provider-pinned launch rides it here
|
|
345
|
+
// (2026-10-04 F-B: without it the native env pins the endpoint).
|
|
346
|
+
...(this.launch.settings !== undefined ? { settings: this.launch.settings } : {}),
|
|
347
|
+
// undefined (no local binary) leaves the key unset — bundled-payload
|
|
348
|
+
// mode lets the SDK spawn its own platform payload as the CLI.
|
|
349
|
+
...(executable !== undefined ? { pathToClaudeCodeExecutable: executable } : {}),
|
|
310
350
|
persistSession: true,
|
|
351
|
+
...(this.launch.model !== undefined ? { model: this.launch.model } : {}),
|
|
311
352
|
...(this.claudeSessionId !== undefined ? { sessionId: this.claudeSessionId } : {}),
|
|
312
353
|
...(this.resumeSessionId !== undefined ? { resume: this.resumeSessionId } : {}),
|
|
313
354
|
...(this.canUseTool !== undefined ? { canUseTool: this.canUseTool } : {}),
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-launch.ts — how a provider reaches the Claude Code CLI
|
|
3
|
+
* (2026-10-04 multi-provider rework).
|
|
4
|
+
*
|
|
5
|
+
* Claude Code is a single-provider harness: its own `settings.json` env block
|
|
6
|
+
* names exactly one `ANTHROPIC_BASE_URL` with one key, so the LIST of
|
|
7
|
+
* configured providers lives in `claude-providers.ts` instead. This module is
|
|
8
|
+
* the other end of that store: given the route the next turn will run (DSH's
|
|
9
|
+
* own per-session model selection, read by `claude-route.ts`), the resolved
|
|
10
|
+
* provider entry, and the resolved host binary, it produces the slice Task 9
|
|
11
|
+
* merges into the SDK's `Options`.
|
|
12
|
+
*
|
|
13
|
+
* The child's environment is the ONE channel a provider's variables enter the
|
|
14
|
+
* process through (spec §2.2) — AND, for a provider-pinned launch, the same env
|
|
15
|
+
* is re-carried as a `--settings`-tier JSON string so the command-line tier
|
|
16
|
+
* outranks the user's native `~/.claude/settings.json` env (2026-10-04 F-B).
|
|
17
|
+
* Concretely, the env keys are:
|
|
18
|
+
*
|
|
19
|
+
* - `ANTHROPIC_BASE_URL` — always, from the entry (validation guarantees a
|
|
20
|
+
* non-empty endpoint; an entry without one is not an entry).
|
|
21
|
+
* - `ANTHROPIC_AUTH_TOKEN` for `authToken` (Bearer), `ANTHROPIC_API_KEY` for
|
|
22
|
+
* `apiKey` (x-api-key) — exactly one, chosen by the entry's auth KIND, with
|
|
23
|
+
* the stored value verbatim. The kind names the channel, so the variable is
|
|
24
|
+
* present even when the value is empty ("keyed outside Super D"): the CLI
|
|
25
|
+
* reads both names with `.trim() || undefined` (measured in
|
|
26
|
+
* `@anthropic-ai/claude-agent-sdk` 2.1.263), so an empty value is unset
|
|
27
|
+
* rather than a fabricated secret.
|
|
28
|
+
* - `ANTHROPIC_MODEL` — the RAW route model id (`glm-5.2`), never the
|
|
29
|
+
* selector's composite key (`glm/glm-5.2`); the composite is a DSH catalog
|
|
30
|
+
* id and has no meaning on the wire.
|
|
31
|
+
* - `ANTHROPIC_DEFAULT_{SONNET,OPUS,HAIKU}_MODEL` — ONLY for aliases the
|
|
32
|
+
* entry actually declares, and never an empty-string one: an alias the user
|
|
33
|
+
* did not write must not be invented, or the child silently answers on a
|
|
34
|
+
* model they never chose.
|
|
35
|
+
*
|
|
36
|
+
* The returned env holds ADDITIONS only. The caller composes the child
|
|
37
|
+
* environment as `{ ...process.env, ...launch.env }` — the spread AFTER
|
|
38
|
+
* `process.env` is the contract, and it is why this function never reads or
|
|
39
|
+
* mutates `process.env` itself: it is a pure declaration, and the caller's
|
|
40
|
+
* precedence is what makes it effective. `NATIVE_LAUNCH` is the same
|
|
41
|
+
* declaration with nothing in it: `providerId: null` and an empty env, for a
|
|
42
|
+
* route no provider entry answers (a legacy bare-id selection, the native
|
|
43
|
+
* settings-only shape).
|
|
44
|
+
*
|
|
45
|
+
* The same reasoning makes `pathToClaudeCodeExecutable` optional and
|
|
46
|
+
* conditional: it is set only when a host binary was actually resolved, so an
|
|
47
|
+
* unresolved binary omits the key entirely and lets the SDK fall back to its
|
|
48
|
+
* bundled payload (spec §2.1) instead of spawning an empty path.
|
|
49
|
+
*
|
|
50
|
+
* @module agent-claude/claude-launch
|
|
51
|
+
*/
|
|
52
|
+
/** The launch for "no provider injection at all" (the native settings apply). */
|
|
53
|
+
export const NATIVE_LAUNCH = { providerId: null, env: {} };
|
|
54
|
+
/** The variable each auth kind authenticates through. */
|
|
55
|
+
const AUTH_ENV = {
|
|
56
|
+
authToken: "ANTHROPIC_AUTH_TOKEN",
|
|
57
|
+
apiKey: "ANTHROPIC_API_KEY",
|
|
58
|
+
};
|
|
59
|
+
/** The alias slots an entry may declare, in catalog order. */
|
|
60
|
+
const ALIAS_ENV = [
|
|
61
|
+
["sonnet", "ANTHROPIC_DEFAULT_SONNET_MODEL"],
|
|
62
|
+
["opus", "ANTHROPIC_DEFAULT_OPUS_MODEL"],
|
|
63
|
+
["haiku", "ANTHROPIC_DEFAULT_HAIKU_MODEL"],
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Turn one resolved provider into the SDK `Options` slice for one route.
|
|
67
|
+
*
|
|
68
|
+
* Pure: reads no file, no env, no clock, and spawns nothing. The provider id
|
|
69
|
+
* comes from the ENTRY (the caller resolves the entry BY `route.provider`, so
|
|
70
|
+
* the label and the injected data stay one fact); the model comes from the
|
|
71
|
+
* ROUTE, because that is what the session selected.
|
|
72
|
+
*
|
|
73
|
+
* @param route - the route the next turn runs (raw model half).
|
|
74
|
+
* @param entry - the provider entry that route resolved to.
|
|
75
|
+
* @param hostBinary - the resolved Claude Code binary, or `undefined` to let
|
|
76
|
+
* the SDK use its bundled payload.
|
|
77
|
+
* @returns the env additions, the wire model, and the optional binary path.
|
|
78
|
+
*/
|
|
79
|
+
export function claudeLaunchFor(route, entry, hostBinary) {
|
|
80
|
+
const env = {
|
|
81
|
+
ANTHROPIC_BASE_URL: entry.baseUrl,
|
|
82
|
+
[AUTH_ENV[entry.auth.kind]]: entry.auth.value,
|
|
83
|
+
ANTHROPIC_MODEL: route.model,
|
|
84
|
+
};
|
|
85
|
+
// Declared aliases only: `undefined` means the entry never named that slot,
|
|
86
|
+
// and "" is not a model id — emitting either would invent a choice.
|
|
87
|
+
for (const [slot, name] of ALIAS_ENV) {
|
|
88
|
+
const value = entry.aliases?.[slot];
|
|
89
|
+
if (value !== undefined && value !== "")
|
|
90
|
+
env[name] = value;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
providerId: entry.id,
|
|
94
|
+
env,
|
|
95
|
+
settings: JSON.stringify({ env }),
|
|
96
|
+
model: route.model,
|
|
97
|
+
// Spread, not `pathToClaudeCodeExecutable: hostBinary`: an absent key must
|
|
98
|
+
// not exist at all, or it would ride a spread into Options as `undefined`.
|
|
99
|
+
...(hostBinary !== undefined && hostBinary !== "" ? { pathToClaudeCodeExecutable: hostBinary } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-native-config.ts — reading the foreign runtime's own settings, the
|
|
3
|
+
* read-only half of the multi-provider split (2026-10-04).
|
|
4
|
+
*
|
|
5
|
+
* Claude Code is a single-provider harness: its `settings.json` `env` block
|
|
6
|
+
* names exactly ONE `ANTHROPIC_BASE_URL` with at most one key variable, so the
|
|
7
|
+
* LIST of providers a user has configured has nowhere to live natively and
|
|
8
|
+
* lives in the adapter's store instead (`claude-providers.ts`). This module is
|
|
9
|
+
* the other half of that split — the sole authority on what the user's own
|
|
10
|
+
* file actually says.
|
|
11
|
+
*
|
|
12
|
+
* The direction is one-way: `settings.json` is a SOURCE. Nothing here writes
|
|
13
|
+
* it — this module has no write path at all — and cc-switch can still own the
|
|
14
|
+
* file; the sync in `claude-provider-gateway.ts` picks up whatever it leaves
|
|
15
|
+
* there. The `.bak-*` manual-switch layers are never folded (they are not the
|
|
16
|
+
* live file). Compare `agent-codex/src/codex-native-config.ts`, the structural
|
|
17
|
+
* port source; claude needs none of codex's per-provider catalog machinery
|
|
18
|
+
* because a settings env block IS its whole catalog.
|
|
19
|
+
*
|
|
20
|
+
* Two reads live here:
|
|
21
|
+
*
|
|
22
|
+
* - `readNativeProviders` — the `env` block as a provider table, 0 or 1
|
|
23
|
+
* entries. An env block without `ANTHROPIC_BASE_URL` declares no provider:
|
|
24
|
+
* the store's identity IS the endpoint URL, and with nothing to dedupe by
|
|
25
|
+
* there is no entry to make.
|
|
26
|
+
* - `readNativeModel` — `env.ANTHROPIC_MODEL`, the model the user's own CLI
|
|
27
|
+
* runs. Independent of the provider read: a native setup on the default
|
|
28
|
+
* Anthropic endpoint carries no base URL but a perfectly meaningful model.
|
|
29
|
+
*
|
|
30
|
+
* Auth is classified by the VARIABLE NAME, and an explicitly-empty variable
|
|
31
|
+
* reads as unset — the same env semantics the codex port applies to an empty
|
|
32
|
+
* `env_key` — so `ANTHROPIC_AUTH_TOKEN` (Bearer) wins only when it carries a
|
|
33
|
+
* value, and a base URL whose env names no usable key variable folds as the
|
|
34
|
+
* real "keyed outside Super D" state `{kind:"apiKey", value:""}` rather than
|
|
35
|
+
* being rejected.
|
|
36
|
+
*
|
|
37
|
+
* Non-string env values (a hand-edited file may carry numbers or booleans)
|
|
38
|
+
* are read in their string forms — everything downstream of an env block is a
|
|
39
|
+
* string, process-env semantics — and shapes with no string form worth
|
|
40
|
+
* honoring (objects, arrays, null) are skipped.
|
|
41
|
+
*
|
|
42
|
+
* Every read is fail-soft: an absent file, an unreadable file, unparsable
|
|
43
|
+
* JSON, or a wrong shape all degrade to "nothing declared" and never throw.
|
|
44
|
+
*
|
|
45
|
+
* @module agent-claude/claude-native-config
|
|
46
|
+
*/
|
|
47
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
// The home resolver stays canonical in claude-home.ts (it has its own tests
|
|
50
|
+
// and its own callers); this re-export is the non-duplicating way to give the
|
|
51
|
+
// native-config consumers the one-face import the plan's Interfaces pin.
|
|
52
|
+
export { resolveClaudeHome } from "./claude-home.js";
|
|
53
|
+
const SETTINGS_FILE = "settings.json";
|
|
54
|
+
/** Read a file, or `undefined` when it is absent or unreadable. */
|
|
55
|
+
function readIfExists(path) {
|
|
56
|
+
if (!existsSync(path))
|
|
57
|
+
return undefined;
|
|
58
|
+
try {
|
|
59
|
+
return readFileSync(path, "utf8");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One env value as the process would see it. Strings pass verbatim; numbers
|
|
67
|
+
* and booleans coerce to their string forms; objects, arrays, null, and
|
|
68
|
+
* empty/whitespace strings are unusable (an empty credential is not a
|
|
69
|
+
* credential — see the module header).
|
|
70
|
+
*/
|
|
71
|
+
function envString(value) {
|
|
72
|
+
if (typeof value === "string")
|
|
73
|
+
return value.trim() === "" ? undefined : value;
|
|
74
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
75
|
+
return String(value);
|
|
76
|
+
if (typeof value === "boolean")
|
|
77
|
+
return value ? "true" : "false";
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/** The settings.json `env` block, or `undefined` when anything above it fails. */
|
|
81
|
+
function readEnvBlock(claudeHome) {
|
|
82
|
+
const text = readIfExists(join(claudeHome, SETTINGS_FILE));
|
|
83
|
+
if (text === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object")
|
|
93
|
+
return undefined;
|
|
94
|
+
const env = parsed["env"];
|
|
95
|
+
if (env === null || Array.isArray(env) || typeof env !== "object")
|
|
96
|
+
return undefined;
|
|
97
|
+
return env;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse the native settings env into the sync's source tables.
|
|
101
|
+
*
|
|
102
|
+
* The output is 0 or 1 entries, in the only order there is. `models` is the
|
|
103
|
+
* DEDUPLICATED sequence of the alias values (sonnet, opus, haiku) — the only
|
|
104
|
+
* model ids a settings env reveals; `ANTHROPIC_MODEL` deliberately does not
|
|
105
|
+
* become one (it is `readNativeModel`'s own view, and folding it would invent
|
|
106
|
+
* a catalog entry the aliases never named).
|
|
107
|
+
*
|
|
108
|
+
* @param claudeHome - the NATIVE claude home (`~/.claude`).
|
|
109
|
+
* @returns the declared provider table, or `[]` when the env declares none.
|
|
110
|
+
*/
|
|
111
|
+
export function readNativeProviders(claudeHome) {
|
|
112
|
+
const env = readEnvBlock(claudeHome);
|
|
113
|
+
if (env === undefined)
|
|
114
|
+
return [];
|
|
115
|
+
const baseUrl = envString(env["ANTHROPIC_BASE_URL"]);
|
|
116
|
+
// No endpoint is not a provider: there is nothing to dedupe the entry by in
|
|
117
|
+
// a list whose identity IS the endpoint URL.
|
|
118
|
+
if (baseUrl === undefined)
|
|
119
|
+
return [];
|
|
120
|
+
// Key-variable precedence: AUTH_TOKEN (Bearer) over API_KEY (x-api-key),
|
|
121
|
+
// an empty variable counting as unset, and no usable variable at all
|
|
122
|
+
// folding as the keyed-outside-Super-D state rather than an error.
|
|
123
|
+
const token = envString(env["ANTHROPIC_AUTH_TOKEN"]);
|
|
124
|
+
const key = envString(env["ANTHROPIC_API_KEY"]);
|
|
125
|
+
const auth = token !== undefined
|
|
126
|
+
? { kind: "authToken", value: token }
|
|
127
|
+
: key !== undefined
|
|
128
|
+
? { kind: "apiKey", value: key }
|
|
129
|
+
: { kind: "apiKey", value: "" };
|
|
130
|
+
const sonnet = envString(env["ANTHROPIC_DEFAULT_SONNET_MODEL"]);
|
|
131
|
+
const opus = envString(env["ANTHROPIC_DEFAULT_OPUS_MODEL"]);
|
|
132
|
+
const haiku = envString(env["ANTHROPIC_DEFAULT_HAIKU_MODEL"]);
|
|
133
|
+
const models = [];
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
for (const id of [sonnet, opus, haiku]) {
|
|
136
|
+
if (id === undefined || seen.has(id))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(id);
|
|
139
|
+
models.push({ id });
|
|
140
|
+
}
|
|
141
|
+
const table = {
|
|
142
|
+
baseUrl,
|
|
143
|
+
auth,
|
|
144
|
+
models,
|
|
145
|
+
...(sonnet === undefined && opus === undefined && haiku === undefined ? {} : {
|
|
146
|
+
aliases: {
|
|
147
|
+
...(sonnet === undefined ? {} : { sonnet }),
|
|
148
|
+
...(opus === undefined ? {} : { opus }),
|
|
149
|
+
...(haiku === undefined ? {} : { haiku }),
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
return [table];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The model the user's own settings env puts in force (`env.ANTHROPIC_MODEL`).
|
|
157
|
+
*
|
|
158
|
+
* Read independently of the provider tables: it is the panel's passive "your
|
|
159
|
+
* own Claude runs this" line, meaningful even when no endpoint is declared.
|
|
160
|
+
*
|
|
161
|
+
* @param claudeHome - the NATIVE claude home (`~/.claude`).
|
|
162
|
+
* @returns the model id, or `undefined` when the env names none.
|
|
163
|
+
*/
|
|
164
|
+
export function readNativeModel(claudeHome) {
|
|
165
|
+
const env = readEnvBlock(claudeHome);
|
|
166
|
+
if (env === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
return envString(env["ANTHROPIC_MODEL"]);
|
|
169
|
+
}
|