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.
Files changed (50) hide show
  1. package/README.md +18 -2
  2. package/agent-agy/dist/agent.js +19 -1
  3. package/agent-claude/dist/adapter.js +106 -31
  4. package/agent-claude/dist/agent.js +177 -22
  5. package/agent-claude/dist/claude-client.js +84 -43
  6. package/agent-claude/dist/claude-launch.js +101 -0
  7. package/agent-claude/dist/claude-native-config.js +169 -0
  8. package/agent-claude/dist/claude-provider-gateway.js +340 -0
  9. package/agent-claude/dist/claude-providers.js +391 -0
  10. package/agent-claude/dist/claude-route.js +152 -0
  11. package/agent-claude/dist/index.js +214 -37
  12. package/agent-claude/dist/models.js +14 -0
  13. package/agent-claude/dist/sdk-runtime.js +320 -0
  14. package/agent-claude/package.json +6 -4
  15. package/agent-codex/README.md +18 -6
  16. package/agent-codex/dist/adapter.js +3 -2
  17. package/agent-codex/dist/agent.js +41 -7
  18. package/agent-codex/dist/app-server-client.js +7 -1
  19. package/agent-codex/dist/app-server-wire.js +10 -22
  20. package/agent-codex/dist/codex-launch.js +10 -32
  21. package/agent-codex/dist/codex-native-config.js +9 -3
  22. package/agent-codex/dist/codex-oauth.js +153 -0
  23. package/agent-codex/dist/codex-provider-gateway.js +36 -38
  24. package/agent-codex/dist/codex-providers.js +20 -7
  25. package/agent-codex/dist/codex-runtime.js +120 -0
  26. package/agent-codex/dist/index.js +18 -11
  27. package/agent-codex/package.json +1 -1
  28. package/agent-codex-sdk/dist/adapter.js +3 -2
  29. package/agent-codex-sdk/dist/agent.js +35 -1
  30. package/agent-codex-sdk/dist/codex-native-config.js +9 -3
  31. package/agent-codex-sdk/dist/codex-oauth.js +153 -0
  32. package/agent-codex-sdk/dist/codex-provider-gateway.js +36 -38
  33. package/agent-codex-sdk/dist/codex-providers.js +20 -7
  34. package/agent-codex-sdk/dist/codex-sdk-launch.js +5 -38
  35. package/agent-codex-sdk/dist/index.js +18 -11
  36. package/agent-codex-sdk/dist/sdk-runtime.js +2 -2
  37. package/agent-hermes/dist/agent.js +46 -2
  38. package/agent-hermes/dist/hermes-client.js +49 -1
  39. package/agent-omp/dist/agent.js +17 -4
  40. package/agent-omp-sdk/dist/agent.d.ts +11 -0
  41. package/agent-omp-sdk/dist/agent.js +17 -4
  42. package/agent-pi/dist/agent.js +33 -1
  43. package/client/index.js +150 -58
  44. package/package.json +2 -3
  45. package/superd-hub/dist/client/agent-settings.d.ts +66 -12
  46. package/superd-hub/dist/client/agent-settings.js +85 -14
  47. package/superd-hub/dist/client/runtime-settings-api.js +5 -5
  48. package/superd-hub/dist/world-mount.d.ts +6 -5
  49. package/superd-hub/dist/world-mount.js +6 -6
  50. 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 { existsSync } from "node:fs";
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
- /** Host Claude Code binary driven through the SDK bridge (spec ruling R5).
64
- * PATH-resolved like any host CLI: $CLAUDE_EXECUTABLE wins, else a bare
65
- * "claude" the SDK spawns through PATH. No machine-specific path ships. */
66
- export const DEFAULT_CLAUDE_EXECUTABLE = "claude";
67
- const defaultFactory = (o) => officialQuery({
68
- prompt: o.prompt,
69
- options: o.options,
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 = defaultFactory;
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 = factory({
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: executableResolver(),
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). Everything up to the
225
- * `this.query = query` assignment is synchronous, so concurrent first prompts
226
- * can never build two queries: the first caller assigns `this.query` before
227
- * any `await` yields control. This single-query invariant therefore depends
228
- * on the `ClaudeFactory` contract staying synchronous (it must return the
229
- * `Query` without awaiting). The reader loop is started fire-and-forget before
230
- * any pending control call is replayed, so a throwing control can never leave
231
- * the CLI undrained.
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 = factory({ prompt: queue, options: this.buildOptions() });
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: { ...process.env },
309
- pathToClaudeCodeExecutable: executableResolver(),
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
+ }