demian-cli 1.0.7 → 1.0.8

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/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
3
3
 
4
4
  // src/cli.ts
5
- import path31 from "node:path";
5
+ import path35 from "node:path";
6
6
 
7
7
  // src/agents/types.ts
8
8
  function normalizeAgent(input2) {
@@ -96,7 +96,7 @@ var claudeCodeAgent = {
96
96
  name: "claudecode",
97
97
  description: "Claude Code external coding agent for focused delegated workspace tasks.",
98
98
  mode: "subagent",
99
- provider: { profile: "claudecode-subagent", permissionProfile: "build" },
99
+ provider: { profile: "claudecode", permissionProfile: "build" },
100
100
  tools: [],
101
101
  systemPrompt: [
102
102
  "You are demian claudecode, a Claude Code external runtime working as a sub agent for Demian.",
@@ -141,7 +141,7 @@ var claudeCodeExplorerAgent = {
141
141
  name: "claudecode-explorer",
142
142
  description: "Claude Code external read-only explorer for cowork repository inspection.",
143
143
  mode: "subagent",
144
- provider: { profile: "claudecode-subagent", permissionProfile: "explore" },
144
+ provider: { profile: "claudecode", permissionProfile: "explore" },
145
145
  tools: [],
146
146
  systemPrompt: [
147
147
  "You are demian claudecode-explorer, a Claude Code external runtime working as a read-only cowork sub agent for Demian.",
@@ -176,7 +176,7 @@ var claudeCodeBuilderAgent = {
176
176
  name: "claudecode-builder",
177
177
  description: "Claude Code-backed builder for bounded cowork implementation tasks.",
178
178
  mode: "subagent",
179
- provider: { profile: "claudecode-subagent", permissionProfile: "build" },
179
+ provider: { profile: "claudecode", permissionProfile: "build" },
180
180
  tools: [],
181
181
  systemPrompt: [
182
182
  "You are demian claudecode-builder, a Claude Code external runtime working as a writer cowork sub agent for Demian.",
@@ -481,7 +481,7 @@ import { readFile as readFile6 } from "node:fs/promises";
481
481
  import crypto4 from "node:crypto";
482
482
  import fs7 from "node:fs";
483
483
  import os7 from "node:os";
484
- import path12 from "node:path";
484
+ import path13 from "node:path";
485
485
 
486
486
  // src/providers/retry.ts
487
487
  var RETRY_STATUS = /* @__PURE__ */ new Set([408, 409, 425, 429, 500, 502, 503, 504]);
@@ -874,12 +874,14 @@ var dynamicImport = new Function("specifier", "return import(specifier)");
874
874
  var AnthropicProvider = class {
875
875
  id = "anthropic";
876
876
  #apiKey;
877
+ #baseURL;
877
878
  #defaultModel;
878
879
  #defaultMaxTokens;
879
880
  #client;
880
881
  #onRetry;
881
882
  constructor(config) {
882
883
  this.#apiKey = config.apiKey;
884
+ this.#baseURL = config.baseURL;
883
885
  this.#defaultModel = config.defaultModel;
884
886
  this.#defaultMaxTokens = config.defaultMaxTokens ?? 4096;
885
887
  this.#client = config.client;
@@ -924,7 +926,7 @@ var AnthropicProvider = class {
924
926
  if (this.#client) return this.#client;
925
927
  if (!this.#apiKey) throw new Error("AnthropicProvider requires apiKey");
926
928
  const Anthropic = await loadAnthropicConstructor();
927
- this.#client = new Anthropic({ apiKey: this.#apiKey });
929
+ this.#client = new Anthropic({ apiKey: this.#apiKey, baseURL: this.#baseURL });
928
930
  return this.#client;
929
931
  }
930
932
  };
@@ -1157,19 +1159,56 @@ import { execFile } from "node:child_process";
1157
1159
  import { promisify } from "node:util";
1158
1160
  import fs2 from "node:fs";
1159
1161
  import { chmod, mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "node:fs/promises";
1160
- import path2 from "node:path";
1162
+ import path3 from "node:path";
1161
1163
 
1162
1164
  // src/providers/codex-state.ts
1163
1165
  import crypto from "node:crypto";
1164
1166
  import fs from "node:fs";
1165
1167
  import { mkdir, readFile, writeFile } from "node:fs/promises";
1168
+ import os2 from "node:os";
1169
+ import path2 from "node:path";
1170
+
1171
+ // src/path-expansion.ts
1166
1172
  import os from "node:os";
1167
1173
  import path from "node:path";
1174
+ function expandPathReferences(value) {
1175
+ let expanded = expandTilde(value);
1176
+ expanded = expanded.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, bare) => envPathValue(braced ?? bare) ?? match);
1177
+ expanded = expanded.replace(/%([A-Za-z_][A-Za-z0-9_]*)%/g, (match, name) => envPathValue(name) ?? match);
1178
+ return expanded;
1179
+ }
1180
+ function resolveExpandedPath(value) {
1181
+ return path.resolve(expandPathReferences(value));
1182
+ }
1183
+ function expandTilde(value) {
1184
+ if (value === "~") return os.homedir();
1185
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(os.homedir(), value.slice(2));
1186
+ return value;
1187
+ }
1188
+ function envPathValue(name) {
1189
+ if (!name) return void 0;
1190
+ const value = processEnvValue(name);
1191
+ if (value !== void 0) return value;
1192
+ const upper = name.toUpperCase();
1193
+ if (upper === "HOME" || upper === "USERPROFILE") return os.homedir();
1194
+ if (process.platform === "win32" && upper === "HOMEDRIVE") return path.win32.parse(os.homedir()).root.replace(/[\\/]$/, "");
1195
+ if (process.platform === "win32" && upper === "HOMEPATH") return os.homedir().replace(/^[A-Za-z]:/, "");
1196
+ return void 0;
1197
+ }
1198
+ function processEnvValue(name) {
1199
+ if (process.env[name] !== void 0) return process.env[name];
1200
+ if (process.platform !== "win32") return void 0;
1201
+ const upper = name.toUpperCase();
1202
+ const key = Object.keys(process.env).find((item) => item.toUpperCase() === upper);
1203
+ return key ? process.env[key] : void 0;
1204
+ }
1205
+
1206
+ // src/providers/codex-state.ts
1168
1207
  function resolveCodexHome(configured) {
1169
- return path.resolve(expandCodexPath(configured ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex")));
1208
+ return resolveExpandedPath(configured ?? process.env.CODEX_HOME ?? path2.join(os2.homedir(), ".codex"));
1170
1209
  }
1171
1210
  async function loadOrCreateInstallationId(codexHome) {
1172
- const filePath = path.join(codexHome, "installation_id");
1211
+ const filePath = path2.join(codexHome, "installation_id");
1173
1212
  try {
1174
1213
  const existing = (await readFile(filePath, "utf8")).trim();
1175
1214
  if (isUuid(existing)) return existing;
@@ -1191,15 +1230,6 @@ function isNodeError(error, code) {
1191
1230
  function fileExists(filePath) {
1192
1231
  return fs.existsSync(filePath);
1193
1232
  }
1194
- function expandCodexPath(value) {
1195
- let expanded = value;
1196
- if (expanded === "~") return os.homedir();
1197
- if (expanded.startsWith("~/")) expanded = path.join(os.homedir(), expanded.slice(2));
1198
- return expanded.replace(/\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/gi, (match, braced, bare) => {
1199
- const name = braced ?? bare;
1200
- return name && process.env[name] !== void 0 ? process.env[name] : match;
1201
- });
1202
- }
1203
1233
 
1204
1234
  // src/providers/codex-auth.ts
1205
1235
  var execFileAsync = promisify(execFile);
@@ -1422,10 +1452,10 @@ var CodexAuthStore = class {
1422
1452
  }
1423
1453
  };
1424
1454
  function authFilePath(codexHome) {
1425
- return path2.join(codexHome, "auth.json");
1455
+ return path3.join(codexHome, "auth.json");
1426
1456
  }
1427
1457
  function codexKeyringAccount(codexHome) {
1428
- const resolved = path2.resolve(codexHome);
1458
+ const resolved = path3.resolve(codexHome);
1429
1459
  const canonical = fs2.existsSync(resolved) ? fs2.realpathSync.native(resolved) : resolved;
1430
1460
  const hash = crypto2.createHash("sha256").update(canonical).digest("hex").slice(0, 16);
1431
1461
  return `cli|${hash}`;
@@ -1854,8 +1884,8 @@ import { randomUUID } from "node:crypto";
1854
1884
  import { execFile as execFile2 } from "node:child_process";
1855
1885
  import fs3 from "node:fs";
1856
1886
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "node:fs/promises";
1857
- import os2 from "node:os";
1858
- import path3 from "node:path";
1887
+ import os3 from "node:os";
1888
+ import path4 from "node:path";
1859
1889
  import { promisify as promisify2 } from "node:util";
1860
1890
  var execFileAsync2 = promisify2(execFile2);
1861
1891
  var CLAUDE_CODE_KEYRING_SERVICE = "Claude Code-credentials";
@@ -1888,7 +1918,7 @@ function getSharedClaudeCodeAuthStore(options = {}) {
1888
1918
  proactiveRefreshMinutes: options.proactiveRefreshMinutes ?? 30,
1889
1919
  refreshCache: options.refreshCache ?? "claude-store",
1890
1920
  refreshTokenURL: options.refreshTokenURL ?? DEFAULT_CLAUDE_CODE_REFRESH_TOKEN_URL,
1891
- keychainAccount: options.keychainAccount ?? os2.userInfo().username
1921
+ keychainAccount: options.keychainAccount ?? os3.userInfo().username
1892
1922
  });
1893
1923
  const existing = sharedAuthStores2.get(key);
1894
1924
  if (existing) return existing;
@@ -1917,7 +1947,7 @@ var ClaudeCodeAuthStore = class {
1917
1947
  this.#proactiveRefreshMinutes = options.proactiveRefreshMinutes ?? 30;
1918
1948
  this.#refreshCache = options.refreshCache ?? "claude-store";
1919
1949
  this.#refreshTokenURL = options.refreshTokenURL ?? DEFAULT_CLAUDE_CODE_REFRESH_TOKEN_URL;
1920
- this.#keychainAccount = options.keychainAccount ?? os2.userInfo().username;
1950
+ this.#keychainAccount = options.keychainAccount ?? os3.userInfo().username;
1921
1951
  this.#fetch = options.fetch ?? fetch;
1922
1952
  this.#keyring = options.keyring ?? new MacOSSecurityKeyring2();
1923
1953
  }
@@ -2118,10 +2148,10 @@ var ClaudeCodeAuthStore = class {
2118
2148
  }
2119
2149
  };
2120
2150
  function resolveClaudeConfigDir(configured) {
2121
- return path3.resolve(expandClaudePath(configured ?? process.env.CLAUDE_CONFIG_DIR ?? path3.join(os2.homedir(), ".claude")));
2151
+ return resolveExpandedPath(configured ?? process.env.CLAUDE_CONFIG_DIR ?? path4.join(os3.homedir(), ".claude"));
2122
2152
  }
2123
2153
  function claudeCodeAuthFilePath(claudeConfigDir) {
2124
- return path3.join(claudeConfigDir, ".credentials.json");
2154
+ return path4.join(claudeConfigDir, ".credentials.json");
2125
2155
  }
2126
2156
  function oauthPayload(auth) {
2127
2157
  const raw = auth.claudeAiOauth;
@@ -2180,15 +2210,6 @@ async function writeClaudeCodeAuthFileAtomic(claudeConfigDir, auth) {
2180
2210
  await chmod2(tempPath, 384);
2181
2211
  await rename2(tempPath, filePath);
2182
2212
  }
2183
- function expandClaudePath(value) {
2184
- let expanded = value;
2185
- if (expanded === "~") return os2.homedir();
2186
- if (expanded.startsWith("~/")) expanded = path3.join(os2.homedir(), expanded.slice(2));
2187
- return expanded.replace(/\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/gi, (match, braced, bare) => {
2188
- const name = braced ?? bare;
2189
- return name && process.env[name] !== void 0 ? process.env[name] : match;
2190
- });
2191
- }
2192
2213
  var MacOSSecurityKeyring2 = class {
2193
2214
  async load(service, account) {
2194
2215
  if (process.platform !== "darwin") return void 0;
@@ -2425,6 +2446,111 @@ function parseClaudeCodeErrorBody(body) {
2425
2446
  }
2426
2447
  }
2427
2448
 
2449
+ // src/providers/ollama.ts
2450
+ var OllamaProvider = class {
2451
+ id = "ollama";
2452
+ #baseURL;
2453
+ #apiKey;
2454
+ #defaultModel;
2455
+ #fetch;
2456
+ #onRetry;
2457
+ constructor(config) {
2458
+ this.#baseURL = config.baseURL.replace(/\/+$/, "");
2459
+ this.#apiKey = config.apiKey;
2460
+ this.#defaultModel = config.defaultModel;
2461
+ this.#fetch = config.fetch ?? fetch;
2462
+ this.#onRetry = config.onRetry;
2463
+ }
2464
+ async chat(req) {
2465
+ return chatWithRetry(() => this.#rawChat(req), {
2466
+ signal: req.signal,
2467
+ onRetry: this.#onRetry
2468
+ });
2469
+ }
2470
+ async #rawChat(req) {
2471
+ const response = await this.#fetch(`${this.#baseURL}/chat`, {
2472
+ method: "POST",
2473
+ headers: {
2474
+ "content-type": "application/json",
2475
+ ...this.#authHeaders()
2476
+ },
2477
+ body: JSON.stringify({
2478
+ model: req.model || this.#defaultModel,
2479
+ messages: req.messages.map(toOllamaMessage),
2480
+ ...req.tools.length > 0 ? { tools: req.tools.map(toOpenAITool) } : {},
2481
+ stream: false,
2482
+ ...req.temperature !== void 0 ? { options: { temperature: req.temperature } } : {}
2483
+ }),
2484
+ signal: req.signal
2485
+ });
2486
+ const text = await response.text();
2487
+ if (!response.ok) throw new ProviderHttpError(response.status, text, parseRetryAfter(response.headers.get("retry-after")));
2488
+ return normalizeOllamaResponse(text ? JSON.parse(text) : {});
2489
+ }
2490
+ #authHeaders() {
2491
+ return this.#apiKey ? { authorization: `Bearer ${this.#apiKey}` } : {};
2492
+ }
2493
+ };
2494
+ function toOllamaMessage(message) {
2495
+ if (message.role === "tool") {
2496
+ return {
2497
+ role: "tool",
2498
+ content: message.content,
2499
+ tool_name: message.name
2500
+ };
2501
+ }
2502
+ const out = {
2503
+ role: message.role,
2504
+ content: typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? "")
2505
+ };
2506
+ if (message.role === "assistant" && message.toolCalls?.length) {
2507
+ out.tool_calls = message.toolCalls.map((call) => ({
2508
+ function: {
2509
+ name: call.name,
2510
+ arguments: call.input ?? {}
2511
+ }
2512
+ }));
2513
+ }
2514
+ return out;
2515
+ }
2516
+ function normalizeOllamaResponse(raw) {
2517
+ const data = raw;
2518
+ const toolCalls = [];
2519
+ for (const call of data.message?.tool_calls ?? []) {
2520
+ const name = call.function?.name;
2521
+ if (!name) continue;
2522
+ toolCalls.push({
2523
+ id: call.id ?? `call_${toolCalls.length + 1}`,
2524
+ name,
2525
+ input: normalizeToolArguments(call.function?.arguments)
2526
+ });
2527
+ }
2528
+ const message = {
2529
+ role: "assistant",
2530
+ content: data.message?.content ?? null,
2531
+ ...toolCalls.length ? { toolCalls } : {}
2532
+ };
2533
+ return {
2534
+ message,
2535
+ toolCalls,
2536
+ stopReason: toolCalls.length ? "tool_use" : data.done_reason === "length" ? "max_tokens" : "end_turn",
2537
+ usage: data.prompt_eval_count !== void 0 || data.eval_count !== void 0 ? {
2538
+ inputTokens: data.prompt_eval_count,
2539
+ outputTokens: data.eval_count,
2540
+ totalTokens: (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0)
2541
+ } : void 0,
2542
+ raw
2543
+ };
2544
+ }
2545
+ function normalizeToolArguments(value) {
2546
+ if (typeof value !== "string") return value ?? {};
2547
+ try {
2548
+ return JSON.parse(value);
2549
+ } catch {
2550
+ return {};
2551
+ }
2552
+ }
2553
+
2428
2554
  // src/external-runtime/claudecode-cli.ts
2429
2555
  import { spawn as spawn3 } from "node:child_process";
2430
2556
  import readline from "node:readline";
@@ -2432,7 +2558,7 @@ import readline from "node:readline";
2432
2558
  // src/external-runtime/claudecode-attachments.ts
2433
2559
  import crypto3 from "node:crypto";
2434
2560
  import fs4 from "node:fs";
2435
- import path4 from "node:path";
2561
+ import path5 from "node:path";
2436
2562
  async function resolveClaudeCodeAttachmentPrompt(runtimeLabel, config, req) {
2437
2563
  const attachments = req.attachments ?? [];
2438
2564
  if (attachments.length === 0) return req.prompt;
@@ -2474,7 +2600,7 @@ function unsupportedAttachmentError(runtimeLabel, count, detail) {
2474
2600
  }
2475
2601
  function formatAttachmentReference(value, cwd) {
2476
2602
  if (isUrl(value)) return imageMimeFromPath(value) ? `[image: ${value} (${imageMimeFromPath(value)}, remote)]` : `[attachment: ${value}]`;
2477
- const target = path4.isAbsolute(value) ? value : path4.resolve(cwd, value);
2603
+ const target = path5.isAbsolute(value) ? value : path5.resolve(cwd, value);
2478
2604
  try {
2479
2605
  const stats = fs4.statSync(target);
2480
2606
  if (!stats.isFile()) return `[attachment: ${value} (${stats.size} bytes)]`;
@@ -2491,7 +2617,7 @@ function isUrl(value) {
2491
2617
  }
2492
2618
  function imageMimeFromPath(value) {
2493
2619
  const pathname = isUrl(value) ? new URL(value).pathname : value;
2494
- const ext = path4.extname(pathname).toLowerCase();
2620
+ const ext = path5.extname(pathname).toLowerCase();
2495
2621
  if (ext === ".png") return "image/png";
2496
2622
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
2497
2623
  if (ext === ".gif") return "image/gif";
@@ -2523,15 +2649,14 @@ function hasAnthropicApiKeyEnv(env = process.env) {
2523
2649
 
2524
2650
  // src/external-runtime/claudecode-paths.ts
2525
2651
  import fs5 from "node:fs";
2526
- import os3 from "node:os";
2527
- import path5 from "node:path";
2652
+ import path6 from "node:path";
2528
2653
  function resolveClaudeCodeCliPath(configured) {
2529
2654
  if (configured) return expandHome(configured);
2530
2655
  if (process.env.CLAUDE_CODE_CLI) return expandHome(process.env.CLAUDE_CODE_CLI);
2531
2656
  const candidates = ["~/.local/bin/claude", "claude"];
2532
2657
  for (const candidate of candidates) {
2533
2658
  const expanded = expandHome(candidate);
2534
- if (path5.isAbsolute(expanded)) {
2659
+ if (path6.isAbsolute(expanded)) {
2535
2660
  if (isExecutableFile(expanded)) return expanded;
2536
2661
  continue;
2537
2662
  }
@@ -2540,9 +2665,7 @@ function resolveClaudeCodeCliPath(configured) {
2540
2665
  return void 0;
2541
2666
  }
2542
2667
  function expandHome(value) {
2543
- if (value === "~") return os3.homedir();
2544
- if (value.startsWith("~/")) return path5.join(os3.homedir(), value.slice(2));
2545
- return value;
2668
+ return expandPathReferences(value);
2546
2669
  }
2547
2670
  function isExecutableFile(filePath) {
2548
2671
  try {
@@ -2808,14 +2931,14 @@ function numberValue2(value) {
2808
2931
  // src/external-runtime/session-lock.ts
2809
2932
  import { mkdir as mkdir4, open, readFile as readFile4, unlink } from "node:fs/promises";
2810
2933
  import os4 from "node:os";
2811
- import path6 from "node:path";
2934
+ import path7 from "node:path";
2812
2935
  var DEFAULT_STALE_MS = 30 * 60 * 1e3;
2813
2936
  async function acquireClaudeCodeSessionLock(sessionId, options = {}) {
2814
- const dir = options.dir ?? path6.join(os4.homedir(), ".demian", "claude-sessions");
2937
+ const dir = options.dir ?? path7.join(os4.homedir(), ".demian", "claude-sessions");
2815
2938
  const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
2816
2939
  const now2 = options.now ?? (() => Date.now());
2817
2940
  await mkdir4(dir, { recursive: true });
2818
- const lockPath = path6.join(dir, `${encodeURIComponent(sessionId)}.lock`);
2941
+ const lockPath = path7.join(dir, `${encodeURIComponent(sessionId)}.lock`);
2819
2942
  for (let attempt = 0; attempt < 2; attempt++) {
2820
2943
  try {
2821
2944
  const handle = await open(lockPath, "wx");
@@ -2879,14 +3002,14 @@ function isAlreadyExists(error) {
2879
3002
  // src/external-runtime/usage-ledger.ts
2880
3003
  import { mkdir as mkdir5, readFile as readFile5, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
2881
3004
  import os5 from "node:os";
2882
- import path7 from "node:path";
3005
+ import path8 from "node:path";
2883
3006
  var processLedger = /* @__PURE__ */ new Map();
2884
3007
  var ClaudeCodeUsageLedger = class {
2885
3008
  #scope;
2886
3009
  #filePath;
2887
3010
  constructor(options = {}) {
2888
3011
  this.#scope = options.scope ?? "process";
2889
- this.#filePath = options.filePath ?? path7.join(os5.homedir(), ".demian", "usage-ledger.json");
3012
+ this.#filePath = options.filePath ?? path8.join(os5.homedir(), ".demian", "usage-ledger.json");
2890
3013
  }
2891
3014
  async spentUsd(key, now2 = /* @__PURE__ */ new Date()) {
2892
3015
  if (this.#scope === "process") return processLedger.get(processKey(key)) ?? 0;
@@ -2917,7 +3040,7 @@ var ClaudeCodeUsageLedger = class {
2917
3040
  return { version: 1, buckets: {} };
2918
3041
  }
2919
3042
  async #write(file) {
2920
- await mkdir5(path7.dirname(this.#filePath), { recursive: true });
3043
+ await mkdir5(path8.dirname(this.#filePath), { recursive: true });
2921
3044
  const temp = `${this.#filePath}.${process.pid}.tmp`;
2922
3045
  await writeFile4(temp, `${JSON.stringify(file, null, 2)}
2923
3046
  `, "utf8");
@@ -22578,10 +22701,10 @@ function now() {
22578
22701
  }
22579
22702
 
22580
22703
  // src/permissions/engine.ts
22581
- import path10 from "node:path";
22704
+ import path11 from "node:path";
22582
22705
 
22583
22706
  // src/permissions/grants.ts
22584
- import path8 from "node:path";
22707
+ import path9 from "node:path";
22585
22708
 
22586
22709
  // src/util.ts
22587
22710
  function stableStringify(value) {
@@ -22636,8 +22759,8 @@ function grantKeyFor(tool, input2, cwd) {
22636
22759
  }
22637
22760
  if (tool === "write_file" || tool === "edit_file") {
22638
22761
  const filePath = typeof object2.path === "string" ? object2.path : "";
22639
- const parent = filePath ? path8.dirname(path8.resolve(cwd, filePath)) : cwd;
22640
- return `${tool}:${path8.relative(cwd, parent) || "."}`;
22762
+ const parent = filePath ? path9.dirname(path9.resolve(cwd, filePath)) : cwd;
22763
+ return `${tool}:${path9.relative(cwd, parent) || "."}`;
22641
22764
  }
22642
22765
  return `${tool}:${stableStringify(input2)}`;
22643
22766
  }
@@ -22664,30 +22787,30 @@ function firstCommandTokens(command, count) {
22664
22787
  }
22665
22788
 
22666
22789
  // src/workspace/paths.ts
22667
- import path9 from "node:path";
22790
+ import path10 from "node:path";
22668
22791
  function normalizePathForMatch(value) {
22669
- return value.split(path9.sep).join("/");
22792
+ return value.split(path10.sep).join("/");
22670
22793
  }
22671
22794
  function isInsidePath(root, candidate) {
22672
- const relative = path9.relative(path9.resolve(root), path9.resolve(candidate));
22673
- return relative === "" || !relative.startsWith("..") && !path9.isAbsolute(relative);
22795
+ const relative = path10.relative(path10.resolve(root), path10.resolve(candidate));
22796
+ return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
22674
22797
  }
22675
22798
  function resolveInsideCwd(cwd, inputPath) {
22676
22799
  if (typeof inputPath !== "string" || inputPath.length === 0) {
22677
22800
  throw new Error("path must be a non-empty string");
22678
22801
  }
22679
- const resolved = path9.resolve(cwd, inputPath);
22802
+ const resolved = path10.resolve(cwd, inputPath);
22680
22803
  if (!isInsidePath(cwd, resolved)) {
22681
22804
  throw new Error(`Path is outside the workspace: ${inputPath}`);
22682
22805
  }
22683
22806
  return resolved;
22684
22807
  }
22685
22808
  function relativeToCwd(cwd, absolutePath) {
22686
- const relative = path9.relative(cwd, absolutePath);
22809
+ const relative = path10.relative(cwd, absolutePath);
22687
22810
  return normalizePathForMatch(relative || ".");
22688
22811
  }
22689
22812
  function isEnvFile(filePath) {
22690
- const base = path9.basename(filePath);
22813
+ const base = path10.basename(filePath);
22691
22814
  if (base === ".env.example") return false;
22692
22815
  return base === ".env" || base.startsWith(".env.");
22693
22816
  }
@@ -22794,7 +22917,7 @@ var PermissionEngine = class {
22794
22917
  #hardDeny(tool, input2, grantKey) {
22795
22918
  const paths = inputPaths(tool, input2);
22796
22919
  for (const item of paths) {
22797
- const resolved = path10.resolve(this.#cwd, item);
22920
+ const resolved = path11.resolve(this.#cwd, item);
22798
22921
  if (!isInsidePath(this.#cwd, resolved)) {
22799
22922
  return {
22800
22923
  decision: "deny",
@@ -22848,7 +22971,7 @@ function matchesRule(rule, tool, input2, cwd) {
22848
22971
  if (rule.match.pathGlob) {
22849
22972
  const paths = inputPaths(tool, input2);
22850
22973
  if (paths.length === 0) return false;
22851
- if (!paths.some((item) => matchesPathRule(rule.match?.pathGlob, relativeToCwd(cwd, path10.resolve(cwd, item))))) {
22974
+ if (!paths.some((item) => matchesPathRule(rule.match?.pathGlob, relativeToCwd(cwd, path11.resolve(cwd, item))))) {
22852
22975
  return false;
22853
22976
  }
22854
22977
  }
@@ -22895,7 +23018,7 @@ function rulePriority(rule, ref) {
22895
23018
  }
22896
23019
  function matchesPathRule(pattern, relativePath) {
22897
23020
  if (!pattern) return true;
22898
- if (path10.basename(relativePath) === ".env.example" && pattern.includes(".env.*")) return false;
23021
+ if (path11.basename(relativePath) === ".env.example" && pattern.includes(".env.*")) return false;
22899
23022
  return matchGlob(pattern, relativePath);
22900
23023
  }
22901
23024
  function mostSpecificRule(rules, input2, cwd, ref) {
@@ -22973,13 +23096,13 @@ function permissionLabel(req) {
22973
23096
  if ((req.tool === "write_file" || req.tool === "edit_file" || req.tool === "read_file") && typeof inputObject.path === "string") {
22974
23097
  return `${req.tool}: ${inputObject.path}`;
22975
23098
  }
22976
- const path32 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
22977
- if (path32) return `${tool}: ${path32}`;
23099
+ const path36 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
23100
+ if (path36) return `${tool}: ${path36}`;
22978
23101
  return tool;
22979
23102
  }
22980
23103
 
22981
23104
  // src/workspace/write-scope.ts
22982
- import path11 from "node:path";
23105
+ import path12 from "node:path";
22983
23106
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "Write", "Edit", "MultiEdit", "NotebookEdit"]);
22984
23107
  var WORKSPACE_SCOPE = "**";
22985
23108
  function normalizeWriteScope(cwd, scope) {
@@ -23001,7 +23124,7 @@ function enforceToolWriteScope(input2) {
23001
23124
  if (paths.length === 0) return { ok: true, paths: [] };
23002
23125
  const checked = [];
23003
23126
  for (const target of paths) {
23004
- const resolved = path11.resolve(input2.cwd, target);
23127
+ const resolved = path12.resolve(input2.cwd, target);
23005
23128
  const relative = relativeToCwd(input2.cwd, resolved);
23006
23129
  checked.push(relative);
23007
23130
  const allowed = isPathAllowedByWriteScope(input2.cwd, target, input2.writeScope);
@@ -23025,7 +23148,7 @@ function outOfScopeDiffFiles(cwd, summary, writeScope) {
23025
23148
  return out;
23026
23149
  }
23027
23150
  function isPathAllowedByWriteScope(cwd, inputPath, writeScope) {
23028
- const resolved = path11.resolve(cwd, inputPath);
23151
+ const resolved = path12.resolve(cwd, inputPath);
23029
23152
  if (!isInsidePath(cwd, resolved)) {
23030
23153
  return { ok: false, error: "path is outside the workspace", paths: [inputPath] };
23031
23154
  }
@@ -23044,9 +23167,9 @@ function normalizeScopePattern(cwd, raw) {
23044
23167
  if (!value) return { ok: false, error: "writeScope entries must be non-empty." };
23045
23168
  if (value.includes("\0")) return { ok: false, error: "writeScope entries must not contain NUL bytes." };
23046
23169
  const hasGlob2 = /[*?[\]{}]/.test(value);
23047
- if (path11.isAbsolute(value)) {
23170
+ if (path12.isAbsolute(value)) {
23048
23171
  if (hasGlob2) return { ok: false, error: `writeScope absolute globs are not supported: ${raw}` };
23049
- const resolved = path11.resolve(value);
23172
+ const resolved = path12.resolve(value);
23050
23173
  if (!isInsidePath(cwd, resolved)) return { ok: false, error: `writeScope path is outside the workspace: ${raw}` };
23051
23174
  value = relativeToCwd(cwd, resolved);
23052
23175
  }
@@ -23423,6 +23546,7 @@ function claudeCodePreviewEnabled(config = {}, env = process.env) {
23423
23546
  }
23424
23547
 
23425
23548
  // src/config.ts
23549
+ var BUILTIN_PROVIDER_ORDER = ["openai", "anthropic", "gemini", "groq", "azure", "lmstudio", "ollama-local", "ollama-cloud", "llamacpp", "vllm", "codex", "claudecode"];
23426
23550
  var CLAUDE_CODE_SONNET_MODEL = "claude-sonnet-4-6";
23427
23551
  var CLAUDE_CODE_OPUS_MODEL = "claude-opus-4-7";
23428
23552
  var CLAUDE_CODE_MODELS = [CLAUDE_CODE_SONNET_MODEL, CLAUDE_CODE_OPUS_MODEL];
@@ -23460,7 +23584,7 @@ var defaultConfig = {
23460
23584
  maxConcurrentExpensive: 1,
23461
23585
  maxConcurrentWriters: 1,
23462
23586
  maxConcurrentPerProvider: {
23463
- "claudecode-subagent": 1
23587
+ claudecode: 1
23464
23588
  },
23465
23589
  tokenBudget: null,
23466
23590
  defaultMergeStrategy: "synthesize",
@@ -23582,73 +23706,102 @@ var defaultConfig = {
23582
23706
  providers: {
23583
23707
  openai: {
23584
23708
  type: "openai-compatible",
23585
- model: "openai-model-name",
23586
23709
  baseURL: "https://api.openai.com/v1",
23587
- apiKeyEnv: "OPENAI_API_KEY"
23710
+ apiKeyEnv: "OPENAI_API_KEY",
23711
+ catalog: {
23712
+ type: "openai-models",
23713
+ endpoint: "https://api.openai.com/v1/models"
23714
+ }
23715
+ },
23716
+ anthropic: {
23717
+ type: "anthropic",
23718
+ baseURL: "https://api.anthropic.com/v1",
23719
+ apiKeyEnv: "ANTHROPIC_API_KEY",
23720
+ catalog: {
23721
+ type: "anthropic-models",
23722
+ endpoint: "https://api.anthropic.com/v1/models"
23723
+ }
23588
23724
  },
23589
23725
  gemini: {
23590
23726
  type: "openai-compatible",
23591
- model: "gemini-model-name",
23592
23727
  baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
23593
- apiKeyEnv: "GOOGLE_API_KEY"
23728
+ apiKeyEnv: "GEMINI_API_KEY",
23729
+ apiKeyEnvAliases: ["GOOGLE_API_KEY"],
23730
+ catalog: {
23731
+ type: "gemini-openai-models",
23732
+ endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models"
23733
+ }
23594
23734
  },
23595
- ollama: {
23735
+ groq: {
23596
23736
  type: "openai-compatible",
23597
- model: "local-coder-model",
23598
- baseURL: "http://localhost:11434/v1",
23599
- apiKey: "ollama",
23600
- quirks: { omitTemperature: true }
23737
+ baseURL: "https://api.groq.com/openai/v1",
23738
+ apiKeyEnv: "GROQ_API_KEY",
23739
+ catalog: {
23740
+ type: "groq-models",
23741
+ endpoint: "https://api.groq.com/openai/v1/models"
23742
+ }
23743
+ },
23744
+ azure: {
23745
+ type: "openai-compatible",
23746
+ auth: { type: "api-key", header: "api-key" },
23747
+ modelProfiles: [
23748
+ {
23749
+ name: "azure-example",
23750
+ displayName: "Azure example",
23751
+ model: "azure-deployment-name",
23752
+ baseURL: "https://example.openai.azure.com/openai/v1",
23753
+ apiKeyEnv: "AZURE_OPENAI_API_KEY"
23754
+ }
23755
+ ]
23601
23756
  },
23602
23757
  lmstudio: {
23603
23758
  type: "openai-compatible",
23604
- model: "local-coder-model",
23605
23759
  baseURL: "http://localhost:1234/v1",
23606
- apiKey: "lm-studio"
23760
+ apiKey: "lm-studio",
23761
+ catalog: {
23762
+ type: "openai-compatible-models",
23763
+ endpoint: "http://localhost:1234/v1/models"
23764
+ }
23607
23765
  },
23608
- vllm: {
23766
+ "ollama-local": {
23609
23767
  type: "openai-compatible",
23610
- model: "local-coder-model",
23611
- baseURL: "http://localhost:8000/v1",
23612
- apiKey: "vllm"
23768
+ baseURL: "http://localhost:11434/v1",
23769
+ apiKey: "ollama",
23770
+ quirks: { omitTemperature: true },
23771
+ catalog: {
23772
+ type: "openai-compatible-models",
23773
+ endpoint: "http://localhost:11434/v1/models"
23774
+ }
23775
+ },
23776
+ "ollama-cloud": {
23777
+ type: "ollama",
23778
+ baseURL: "https://ollama.com/api",
23779
+ apiKeyEnv: "OLLAMA_API_KEY",
23780
+ catalog: {
23781
+ type: "ollama-tags",
23782
+ endpoint: "https://ollama.com/api/tags"
23783
+ }
23613
23784
  },
23614
23785
  llamacpp: {
23615
23786
  type: "openai-compatible",
23616
- model: "local-coder-model",
23617
23787
  baseURL: "http://localhost:8080/v1",
23618
- apiKey: "llama.cpp"
23619
- },
23620
- openrouter: {
23621
- type: "openai-compatible",
23622
- model: "provider/model-name",
23623
- baseURL: "https://openrouter.ai/api/v1",
23624
- apiKeyEnv: "OPENROUTER_API_KEY"
23625
- },
23626
- together: {
23627
- type: "openai-compatible",
23628
- model: "provider/model-name",
23629
- baseURL: "https://api.together.xyz/v1",
23630
- apiKeyEnv: "TOGETHER_API_KEY"
23631
- },
23632
- groq: {
23633
- type: "openai-compatible",
23634
- model: "provider/model-name",
23635
- baseURL: "https://api.groq.com/openai/v1",
23636
- apiKeyEnv: "GROQ_API_KEY"
23788
+ apiKey: "llama.cpp",
23789
+ catalog: {
23790
+ type: "openai-compatible-models",
23791
+ endpoint: "http://localhost:8080/v1/models"
23792
+ }
23637
23793
  },
23638
- azure: {
23794
+ vllm: {
23639
23795
  type: "openai-compatible",
23640
- model: "azure-deployment-name",
23641
- baseURL: "https://example.openai.azure.com/openai/v1",
23642
- apiKeyEnv: "AZURE_OPENAI_API_KEY"
23643
- },
23644
- anthropic: {
23645
- type: "anthropic",
23646
- model: "anthropic-model-name",
23647
- apiKeyEnv: "ANTHROPIC_API_KEY"
23796
+ baseURL: "http://localhost:8000/v1",
23797
+ apiKey: "vllm",
23798
+ catalog: {
23799
+ type: "openai-compatible-models",
23800
+ endpoint: "http://localhost:8000/v1/models"
23801
+ }
23648
23802
  },
23649
23803
  codex: {
23650
23804
  type: "codex",
23651
- model: "gpt-5.1-codex",
23652
23805
  baseURL: "https://chatgpt.com/backend-api/codex",
23653
23806
  authStore: "auto",
23654
23807
  allowApiKeyFallback: false,
@@ -23660,6 +23813,9 @@ var defaultConfig = {
23660
23813
  effort: "medium",
23661
23814
  summary: "auto"
23662
23815
  }
23816
+ },
23817
+ catalog: {
23818
+ type: "codex-oauth-models"
23663
23819
  }
23664
23820
  },
23665
23821
  claudecode: {
@@ -23678,67 +23834,37 @@ var defaultConfig = {
23678
23834
  useBareMode: false,
23679
23835
  usageLedgerScope: "process",
23680
23836
  sessionLock: true,
23681
- abortPolicy: "record-only"
23682
- },
23683
- "claudecode-plan": {
23684
- type: "claudecode",
23685
- runtime: "agent-sdk",
23686
- model: CLAUDE_CODE_SONNET_MODEL,
23687
- models: [...CLAUDE_CODE_MODELS],
23688
- permissionProfile: "plan",
23689
- cliPath: "~/.local/bin/claude",
23690
- cwdMode: "session",
23691
- historyPolicy: "passthrough-resume",
23692
- onInvalidResume: "fresh",
23693
- attachmentFallback: "block",
23694
- allowSubagents: false,
23695
- sanitizeApiKeyEnv: true,
23696
- authPreflight: true,
23697
- useBareMode: false,
23698
- usageLedgerScope: "process",
23699
- sessionLock: true,
23700
- abortPolicy: "record-only",
23701
- hidden: true
23702
- },
23703
- "claudecode-subagent": {
23704
- type: "claudecode",
23705
- runtime: "agent-sdk",
23706
- model: CLAUDE_CODE_SONNET_MODEL,
23707
- models: [...CLAUDE_CODE_MODELS],
23708
- permissionProfile: "build",
23709
- cliPath: "~/.local/bin/claude",
23710
- cwdMode: "session",
23711
- historyPolicy: "passthrough-resume",
23712
- onInvalidResume: "fresh",
23713
- attachmentFallback: "block",
23714
- allowSubagents: false,
23715
- sanitizeApiKeyEnv: true,
23716
- authPreflight: true,
23717
- useBareMode: false,
23718
- usageLedgerScope: "process",
23719
- sessionLock: true,
23720
23837
  abortPolicy: "record-only",
23721
- hidden: true
23838
+ catalog: {
23839
+ type: "claudecode-supported-models"
23840
+ }
23722
23841
  }
23723
23842
  }
23724
23843
  };
23725
23844
  async function loadConfig(options) {
23726
23845
  const configs = [];
23727
- const userConfigDir = path12.join(os7.homedir(), ".demian");
23728
- const projectConfigDir = path12.join(options.cwd, ".demian");
23729
- const configPaths = [
23730
- path12.join(userConfigDir, "config.json"),
23731
- path12.join(userConfigDir, "config.jsond"),
23732
- path12.join(projectConfigDir, "config.json"),
23733
- path12.join(projectConfigDir, "config.jsond"),
23734
- options.configPath
23735
- ].filter(Boolean);
23846
+ const userConfigDir = path13.join(os7.homedir(), ".demian");
23847
+ const configPaths = [path13.join(userConfigDir, "config.json"), path13.join(userConfigDir, "config.jsond"), options.configPath].filter(Boolean);
23736
23848
  for (const filePath of configPaths) {
23737
23849
  if (!fs7.existsSync(filePath)) continue;
23738
- configs.push(parseConfigText(await readFile6(filePath, "utf8"), filePath));
23850
+ const parsed = parseConfigText(await readFile6(filePath, "utf8"), filePath);
23851
+ warnIfV1Config(parsed, filePath);
23852
+ configs.push(parsed);
23739
23853
  }
23740
23854
  return configs.reduce((acc, item) => mergeConfig(acc, item), structuredClone(defaultConfig));
23741
23855
  }
23856
+ function warnIfV1Config(parsed, filePath) {
23857
+ if (parsed.version === 2) return;
23858
+ const providers = parsed.providers ?? {};
23859
+ const hasLegacyShape = Object.values(providers).some((provider) => {
23860
+ const candidate = provider;
23861
+ return (typeof candidate.model === "string" || Array.isArray(candidate.models)) && !Array.isArray(candidate.modelProfiles);
23862
+ });
23863
+ if (!hasLegacyShape && parsed.version === void 0 && Object.keys(providers).length === 0) return;
23864
+ if (!hasLegacyShape) return;
23865
+ process.stderr.write(`[demian] warning: ${filePath} uses the v1 schema (provider.model/models). v2 expects modelProfiles. Run \`demian config init\` to scaffold a v2 template.
23866
+ `);
23867
+ }
23742
23868
  function parseConfigText(text, filePath) {
23743
23869
  try {
23744
23870
  return JSON.parse(filePath.endsWith(".jsond") ? stripJsonD(text) : text);
@@ -23945,7 +24071,127 @@ function mergeConfig(base, overlay) {
23945
24071
  };
23946
24072
  }
23947
24073
  function normalizeProviderAlias(providerName) {
23948
- return providerName === "claudecode-plan" ? "claudecode" : providerName;
24074
+ if (providerName === "claudecode-plan" || providerName === "claudecode-subagent") return "claudecode";
24075
+ if (providerName === "ollama") return "ollama-local";
24076
+ return providerName;
24077
+ }
24078
+ function sortProviderNames(names) {
24079
+ const order = new Map(BUILTIN_PROVIDER_ORDER.map((name, index) => [name, index]));
24080
+ return [...names].sort((left, right) => {
24081
+ const leftIndex = order.get(left);
24082
+ const rightIndex = order.get(right);
24083
+ if (leftIndex !== void 0 && rightIndex !== void 0) return leftIndex - rightIndex;
24084
+ if (leftIndex !== void 0) return -1;
24085
+ if (rightIndex !== void 0) return 1;
24086
+ return left.localeCompare(right);
24087
+ });
24088
+ }
24089
+ function providerModelProfiles(providerName, providerConfig) {
24090
+ const explicit = Array.isArray(providerConfig.modelProfiles) ? providerConfig.modelProfiles.filter((profile) => typeof profile?.model === "string" && profile.model.trim()).map((profile) => ({
24091
+ ...profile,
24092
+ name: profile.name?.trim() || profile.displayName?.trim() || profile.model.trim(),
24093
+ displayName: profile.displayName?.trim() || profile.name?.trim() || profile.model.trim(),
24094
+ model: profile.model.trim()
24095
+ })) : [];
24096
+ const seenNames = /* @__PURE__ */ new Set();
24097
+ const seenDisplayNames = /* @__PURE__ */ new Set();
24098
+ const seenModels = /* @__PURE__ */ new Set();
24099
+ const out = [];
24100
+ for (const profile of explicit) {
24101
+ if (seenNames.has(profile.name)) {
24102
+ process.stderr.write(`[demian] warning: provider ${providerName} has duplicate modelProfile name "${profile.name}". Later entry ignored.
24103
+ `);
24104
+ continue;
24105
+ }
24106
+ if (profile.displayName && seenDisplayNames.has(profile.displayName)) {
24107
+ process.stderr.write(`[demian] warning: provider ${providerName} has duplicate modelProfile displayName "${profile.displayName}". Later entry ignored.
24108
+ `);
24109
+ continue;
24110
+ }
24111
+ seenNames.add(profile.name);
24112
+ if (profile.displayName) seenDisplayNames.add(profile.displayName);
24113
+ seenModels.add(profile.model);
24114
+ out.push(profile);
24115
+ }
24116
+ const legacy = [...typeof providerConfig.model === "string" && providerConfig.model.trim() ? [providerConfig.model.trim()] : [], ...(providerConfig.models ?? []).filter((model) => typeof model === "string" && model.trim())];
24117
+ for (const model of legacy) {
24118
+ const trimmed = model.trim();
24119
+ if (!trimmed || seenModels.has(trimmed)) continue;
24120
+ seenModels.add(trimmed);
24121
+ const name = trimmed;
24122
+ if (!seenNames.has(name)) seenNames.add(name);
24123
+ out.push({ name, displayName: name, model: trimmed });
24124
+ }
24125
+ const fallback = fallbackModelForProvider(providerName, providerConfig);
24126
+ if (out.length === 0 && fallback) out.push({ name: fallback, displayName: fallback, model: fallback });
24127
+ return out;
24128
+ }
24129
+ function providerModelOptions(providerName, providerConfig) {
24130
+ return providerModelProfiles(providerName, providerConfig).filter((profile) => !profile.hidden).map((profile) => profile.displayName ?? profile.name ?? profile.model);
24131
+ }
24132
+ function defaultModelForProvider(providerName, providerConfig) {
24133
+ return providerModelProfiles(providerName, providerConfig)[0]?.model ?? "";
24134
+ }
24135
+ function resolveConfiguredApiKey(providerConfig) {
24136
+ if (providerConfig.apiKey) return { value: providerConfig.apiKey };
24137
+ for (const envName of [providerConfig.apiKeyEnv, ...providerConfig.apiKeyEnvAliases ?? []]) {
24138
+ if (!envName) continue;
24139
+ const value = process.env[envName];
24140
+ if (value) return { value, envName };
24141
+ }
24142
+ return { envName: providerConfig.apiKeyEnv ?? providerConfig.apiKeyEnvAliases?.[0] };
24143
+ }
24144
+ function resolveProviderRuntimeConfig(config, selection) {
24145
+ const providerName = normalizeProviderAlias(selection.providerName);
24146
+ const providerConfig = resolveProviderConfig(config, providerName);
24147
+ const profiles = providerModelProfiles(providerName, providerConfig);
24148
+ let profile;
24149
+ if (selection.modelProfileName) profile = profiles.find((item) => item.name === selection.modelProfileName);
24150
+ if (!profile && selection.model) profile = profiles.find((item) => item.displayName === selection.model);
24151
+ if (!profile && selection.model) profile = profiles.find((item) => item.model === selection.model);
24152
+ if (!profile) profile = profiles[0];
24153
+ if (!profile) {
24154
+ const model = selection.model ?? defaultModelForProvider(providerName, providerConfig);
24155
+ if (!model) throw new Error(`Provider ${providerName} has no model. Run \`demian config models ${providerName} --refresh\` or add a model profile.`);
24156
+ return { providerName, providerConfig, model };
24157
+ }
24158
+ const merged = mergeModelProfile(providerConfig, profile);
24159
+ return {
24160
+ providerName,
24161
+ providerConfig: merged,
24162
+ model: profile.model,
24163
+ modelProfileName: profile.name
24164
+ };
24165
+ }
24166
+ function mergeModelProfile(providerConfig, profile) {
24167
+ const merged = {
24168
+ ...providerConfig,
24169
+ ...definedOnly({
24170
+ baseURL: profile.baseURL,
24171
+ apiKey: profile.apiKey,
24172
+ apiKeyEnv: profile.apiKeyEnv,
24173
+ apiKeyEnvAliases: profile.apiKeyEnvAliases,
24174
+ auth: profile.auth,
24175
+ headers: profile.headers,
24176
+ quirks: profile.quirks,
24177
+ maxTokens: profile.maxTokens
24178
+ }),
24179
+ model: profile.model
24180
+ };
24181
+ return merged;
24182
+ }
24183
+ function definedOnly(input2) {
24184
+ return Object.fromEntries(Object.entries(input2).filter(([, value]) => value !== void 0));
24185
+ }
24186
+ function fallbackModelForProvider(providerName, providerConfig) {
24187
+ if (typeof providerConfig.model === "string" && providerConfig.model.trim()) return providerConfig.model.trim();
24188
+ if (providerName === "openai") return "gpt-5.5";
24189
+ if (providerName === "anthropic") return CLAUDE_CODE_SONNET_MODEL;
24190
+ if (providerName === "codex" || providerConfig.type === "codex") return "gpt-5.5";
24191
+ if (providerName === "claudecode") return CLAUDE_CODE_SONNET_MODEL;
24192
+ if (providerName === "gemini") return "gemini-2.5-pro";
24193
+ if (providerName === "groq") return "openai/gpt-oss-120b";
24194
+ return void 0;
23949
24195
  }
23950
24196
  function migrateProviderConfig(provider, baseProvider) {
23951
24197
  if (isLegacyClaudeCodeShape(provider)) {
@@ -24027,12 +24273,14 @@ function resolveAgentMode(config, flagMode) {
24027
24273
  return "single-agent";
24028
24274
  }
24029
24275
  function resolveProviderConfig(config, providerName) {
24030
- const provider = config.providers[providerName];
24276
+ const normalized = normalizeProviderAlias(providerName);
24277
+ const provider = config.providers[normalized];
24031
24278
  if (!provider) throw new Error(`Unknown provider: ${providerName}`);
24032
24279
  return provider;
24033
24280
  }
24034
24281
  function resolveProvider(providerConfig, options = {}) {
24035
24282
  const model = options.model ?? providerConfig.model;
24283
+ if (!model) throw new Error("Provider has no selected model. Choose a model or add a model profile.");
24036
24284
  if (providerConfig.type === "claudecode") {
24037
24285
  throw new Error("claudecode is an external agent runtime. Use resolveExecutionBackend().");
24038
24286
  }
@@ -24079,12 +24327,28 @@ function resolveProvider(providerConfig, options = {}) {
24079
24327
  };
24080
24328
  }
24081
24329
  if (providerConfig.type === "anthropic") {
24082
- const apiKey2 = providerConfig.apiKey ?? (providerConfig.apiKeyEnv ? process.env[providerConfig.apiKeyEnv] : void 0);
24330
+ const apiKey2 = resolveConfiguredApiKey(providerConfig).value;
24083
24331
  if (!apiKey2) {
24084
24332
  throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24085
24333
  }
24086
24334
  return {
24087
24335
  provider: new AnthropicProvider({
24336
+ apiKey: apiKey2,
24337
+ baseURL: providerConfig.baseURL,
24338
+ defaultModel: model,
24339
+ onRetry: options.onRetry
24340
+ }),
24341
+ model
24342
+ };
24343
+ }
24344
+ if (providerConfig.type === "ollama") {
24345
+ const apiKey2 = resolveConfiguredApiKey(providerConfig).value;
24346
+ if (!apiKey2) {
24347
+ throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24348
+ }
24349
+ return {
24350
+ provider: new OllamaProvider({
24351
+ baseURL: providerConfig.baseURL,
24088
24352
  apiKey: apiKey2,
24089
24353
  defaultModel: model,
24090
24354
  onRetry: options.onRetry
@@ -24092,7 +24356,7 @@ function resolveProvider(providerConfig, options = {}) {
24092
24356
  model
24093
24357
  };
24094
24358
  }
24095
- const apiKey = providerConfig.apiKey ?? (providerConfig.apiKeyEnv ? process.env[providerConfig.apiKeyEnv] : void 0);
24359
+ const apiKey = resolveConfiguredApiKey(providerConfig).value;
24096
24360
  if (!apiKey) {
24097
24361
  throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24098
24362
  }
@@ -24111,6 +24375,7 @@ function resolveProvider(providerConfig, options = {}) {
24111
24375
  }
24112
24376
  function resolveExecutionBackend(providerConfig, options = {}) {
24113
24377
  const model = options.model ?? providerConfig.model;
24378
+ if (!model) throw new Error("Provider has no selected model. Choose a model or add a model profile.");
24114
24379
  if (providerConfig.type === "claudecode") {
24115
24380
  const runtimeConfig = options.config || providerConfig.permissionProfile ? resolveClaudeCodeRuntimeConfig(providerConfig, options.config ?? defaultConfig, options.agent) : providerConfig;
24116
24381
  return {
@@ -24152,44 +24417,1128 @@ function claudeCodeRuntimePolicyHash(config) {
24152
24417
  }
24153
24418
  var CLAUDE_CODE_PERMISSION_PROFILE_KEYS = ["permissionMode", "defaultDecision", "allowedTools", "disallowedTools", "tools", "allowSubagents"];
24154
24419
 
24155
- // src/doctor/policies.ts
24156
- import { existsSync as existsSync2 } from "node:fs";
24157
- import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
24420
+ // src/config-command.ts
24421
+ import { spawn as spawn4 } from "node:child_process";
24422
+ import { stat as stat4 } from "node:fs/promises";
24423
+ import os10 from "node:os";
24424
+ import path16 from "node:path";
24425
+ import readline3 from "node:readline/promises";
24426
+
24427
+ // src/config-scaffold.ts
24428
+ import { chmod as chmod3, mkdir as mkdir6, readFile as readFile7, rename as rename4, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
24158
24429
  import os8 from "node:os";
24159
- import path13 from "node:path";
24160
- function isDoctorCommand(argv) {
24161
- return argv[0] === "doctor";
24162
- }
24163
- async function maybeRunDoctorCommand(argv, options = {}) {
24164
- if (!isDoctorCommand(argv)) return void 0;
24165
- return runDoctorCommand(argv.slice(1), options);
24430
+ import path14 from "node:path";
24431
+ function defaultUserConfigPath() {
24432
+ return path14.join(os8.homedir(), ".demian", "config.json");
24166
24433
  }
24167
- async function runDoctorCommand(argv, options = {}) {
24168
- const stdout = options.stdout ?? process.stdout;
24169
- const stderr = options.stderr ?? process.stderr;
24170
- const flags = parseDoctorFlags(argv);
24171
- if (flags.help) {
24172
- stdout.write(doctorHelp());
24173
- return 0;
24174
- }
24175
- if (argv[0] !== "policies" || !argv.includes("--upgrade-namespaces")) {
24176
- stderr.write("Unsupported doctor command. Run `demian doctor policies --upgrade-namespaces [--write]`.\n");
24177
- return 1;
24178
- }
24179
- const cwd = path13.resolve(flags.cwd ?? options.cwd ?? process.cwd());
24180
- const filePaths = resolvePolicyConfigPaths(cwd, flags.configPaths);
24181
- if (filePaths.length === 0) {
24182
- stdout.write("No demian config files found to inspect.\n");
24434
+ function defaultUserConfig(defaultProvider = detectDefaultProvider()) {
24435
+ return {
24436
+ version: 2,
24437
+ defaultProvider,
24438
+ providers: {
24439
+ openai: {
24440
+ type: "openai-compatible",
24441
+ baseURL: "https://api.openai.com/v1",
24442
+ apiKeyEnv: "OPENAI_API_KEY",
24443
+ catalog: { type: "openai-models", endpoint: "https://api.openai.com/v1/models" }
24444
+ },
24445
+ anthropic: {
24446
+ type: "anthropic",
24447
+ baseURL: "https://api.anthropic.com/v1",
24448
+ apiKeyEnv: "ANTHROPIC_API_KEY",
24449
+ catalog: { type: "anthropic-models", endpoint: "https://api.anthropic.com/v1/models" }
24450
+ },
24451
+ gemini: {
24452
+ type: "openai-compatible",
24453
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
24454
+ apiKeyEnv: "GEMINI_API_KEY",
24455
+ apiKeyEnvAliases: ["GOOGLE_API_KEY"],
24456
+ catalog: { type: "gemini-openai-models", endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models" }
24457
+ },
24458
+ groq: {
24459
+ type: "openai-compatible",
24460
+ baseURL: "https://api.groq.com/openai/v1",
24461
+ apiKeyEnv: "GROQ_API_KEY",
24462
+ catalog: { type: "groq-models", endpoint: "https://api.groq.com/openai/v1/models" }
24463
+ },
24464
+ azure: {
24465
+ type: "openai-compatible",
24466
+ auth: { type: "api-key", header: "api-key" },
24467
+ modelProfiles: [
24468
+ {
24469
+ name: "azure-example",
24470
+ displayName: "Azure example",
24471
+ model: "azure-deployment-name",
24472
+ baseURL: "https://example.openai.azure.com/openai/v1",
24473
+ apiKeyEnv: "AZURE_OPENAI_API_KEY"
24474
+ }
24475
+ ]
24476
+ },
24477
+ lmstudio: {
24478
+ type: "openai-compatible",
24479
+ baseURL: "http://localhost:1234/v1",
24480
+ apiKey: "lm-studio",
24481
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:1234/v1/models" }
24482
+ },
24483
+ "ollama-local": {
24484
+ type: "openai-compatible",
24485
+ baseURL: "http://localhost:11434/v1",
24486
+ apiKey: "ollama",
24487
+ quirks: { omitTemperature: true },
24488
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:11434/v1/models" }
24489
+ },
24490
+ "ollama-cloud": {
24491
+ type: "ollama",
24492
+ baseURL: "https://ollama.com/api",
24493
+ apiKeyEnv: "OLLAMA_API_KEY",
24494
+ catalog: { type: "ollama-tags", endpoint: "https://ollama.com/api/tags" }
24495
+ },
24496
+ llamacpp: {
24497
+ type: "openai-compatible",
24498
+ baseURL: "http://localhost:8080/v1",
24499
+ apiKey: "llama.cpp",
24500
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8080/v1/models" }
24501
+ },
24502
+ vllm: {
24503
+ type: "openai-compatible",
24504
+ baseURL: "http://localhost:8000/v1",
24505
+ apiKey: "vllm",
24506
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8000/v1/models" }
24507
+ },
24508
+ codex: {
24509
+ type: "codex",
24510
+ baseURL: "https://chatgpt.com/backend-api/codex",
24511
+ authStore: "auto",
24512
+ allowApiKeyFallback: false,
24513
+ promptCacheKey: "root-session",
24514
+ catalog: { type: "codex-oauth-models" }
24515
+ },
24516
+ claudecode: {
24517
+ type: "claudecode",
24518
+ runtime: "agent-sdk",
24519
+ cliPath: "~/.local/bin/claude",
24520
+ cwdMode: "session",
24521
+ historyPolicy: "passthrough-resume",
24522
+ onInvalidResume: "fresh",
24523
+ attachmentFallback: "block",
24524
+ allowSubagents: false,
24525
+ sanitizeApiKeyEnv: true,
24526
+ authPreflight: true,
24527
+ useBareMode: false,
24528
+ usageLedgerScope: "process",
24529
+ sessionLock: true,
24530
+ abortPolicy: "record-only",
24531
+ catalog: { type: "claudecode-supported-models" }
24532
+ }
24533
+ }
24534
+ };
24535
+ }
24536
+ async function createUserConfig(options = {}) {
24537
+ const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24538
+ const content = `${JSON.stringify(defaultUserConfig(options.defaultProvider), null, 2)}
24539
+ `;
24540
+ if (options.print) return { path: filePath, created: false, content };
24541
+ const existed = await exists(filePath);
24542
+ if (existed && !options.force) return { path: filePath, created: false, content: await readFile7(filePath, "utf8"), existed: true };
24543
+ await writeJsonAtomic(filePath, content);
24544
+ return { path: filePath, created: true, content, existed };
24545
+ }
24546
+ async function addProvider(options) {
24547
+ const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24548
+ const config = await readConfigObject(filePath);
24549
+ const providers = objectValue(config.providers);
24550
+ const name = options.name;
24551
+ if (providers[name] && !options.force) throw new Error(`Provider ${name} already exists. Use --force to overwrite it.`);
24552
+ providers[name] = providerPreset(options);
24553
+ config.providers = providers;
24554
+ const content = `${JSON.stringify(config, null, 2)}
24555
+ `;
24556
+ await writeJsonAtomic(filePath, content);
24557
+ return { path: filePath, created: true, content };
24558
+ }
24559
+ async function addModelProfile(options) {
24560
+ const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24561
+ const config = await readConfigObject(filePath);
24562
+ const providers = objectValue(config.providers);
24563
+ const provider = objectValue(providers[options.provider]);
24564
+ const profiles = Array.isArray(provider.modelProfiles) ? [...provider.modelProfiles] : [];
24565
+ const displayName = options.displayName ?? options.name;
24566
+ const existingByName = profiles.findIndex((entry) => entry.name === options.name);
24567
+ const existingByDisplay = profiles.findIndex((entry) => entry.displayName === displayName);
24568
+ if (existingByName >= 0 && !options.force) throw new Error(`Profile name "${options.name}" already exists on provider ${options.provider}. Use --force to overwrite it.`);
24569
+ if (existingByDisplay >= 0 && existingByDisplay !== existingByName && !options.force) {
24570
+ throw new Error(`Profile displayName "${displayName}" already exists on provider ${options.provider} (profile name: ${profiles[existingByDisplay].name}). Use --force or pick a different --display-name.`);
24571
+ }
24572
+ const next = {
24573
+ name: options.name,
24574
+ displayName,
24575
+ model: options.model,
24576
+ ...options.baseURL ? { baseURL: options.baseURL } : {},
24577
+ ...options.apiKey ? { apiKey: options.apiKey } : {},
24578
+ ...options.apiKeyEnv ? { apiKeyEnv: options.apiKeyEnv } : {}
24579
+ };
24580
+ if (existingByName >= 0) profiles[existingByName] = next;
24581
+ else profiles.push(next);
24582
+ provider.modelProfiles = profiles;
24583
+ providers[options.provider] = provider;
24584
+ config.providers = providers;
24585
+ const content = `${JSON.stringify(config, null, 2)}
24586
+ `;
24587
+ await writeJsonAtomic(filePath, content);
24588
+ return { path: filePath, created: true, content };
24589
+ }
24590
+ async function readConfigObject(filePath) {
24591
+ if (!await exists(filePath)) await createUserConfig({ path: filePath });
24592
+ const raw = JSON.parse(await readFile7(filePath, "utf8"));
24593
+ raw.version ??= 2;
24594
+ raw.providers = objectValue(raw.providers);
24595
+ return raw;
24596
+ }
24597
+ function providerPreset(options) {
24598
+ const preset = options.preset ?? options.name;
24599
+ const auth = {
24600
+ ...options.apiKey ? { apiKey: options.apiKey } : {},
24601
+ ...options.apiKeyEnv ? { apiKeyEnv: options.apiKeyEnv } : {}
24602
+ };
24603
+ const openAIAuth = options.authHeader ? { auth: { type: "api-key", header: options.authHeader } } : {};
24604
+ if (preset === "openai") return { type: "openai-compatible", baseURL: options.baseURL ?? "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", ...auth, ...openAIAuth, catalog: { type: "openai-models", endpoint: `${(options.baseURL ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/models` } };
24605
+ if (preset === "anthropic") return { type: "anthropic", baseURL: options.baseURL ?? "https://api.anthropic.com/v1", apiKeyEnv: "ANTHROPIC_API_KEY", ...auth, catalog: { type: "anthropic-models", endpoint: `${(options.baseURL ?? "https://api.anthropic.com/v1").replace(/\/+$/, "")}/models` } };
24606
+ if (preset === "gemini") {
24607
+ const geminiBase = options.baseURL ?? "https://generativelanguage.googleapis.com/v1beta/openai/";
24608
+ return { type: "openai-compatible", baseURL: geminiBase, apiKeyEnv: "GEMINI_API_KEY", apiKeyEnvAliases: ["GOOGLE_API_KEY"], ...auth, ...openAIAuth, catalog: { type: "gemini-openai-models", endpoint: `${geminiBase.replace(/\/+$/, "")}/models` } };
24609
+ }
24610
+ if (preset === "groq") return { type: "openai-compatible", baseURL: options.baseURL ?? "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_API_KEY", ...auth, ...openAIAuth, catalog: { type: "groq-models", endpoint: `${(options.baseURL ?? "https://api.groq.com/openai/v1").replace(/\/+$/, "")}/models` } };
24611
+ if (preset === "azure") {
24612
+ return {
24613
+ type: "openai-compatible",
24614
+ auth: { type: "api-key", header: options.authHeader ?? "api-key" },
24615
+ ...auth,
24616
+ modelProfiles: [
24617
+ {
24618
+ name: "azure-example",
24619
+ displayName: "Azure example",
24620
+ model: "azure-deployment-name",
24621
+ baseURL: options.baseURL ?? "https://example.openai.azure.com/openai/v1",
24622
+ apiKeyEnv: options.apiKeyEnv ?? "AZURE_OPENAI_API_KEY"
24623
+ }
24624
+ ]
24625
+ };
24626
+ }
24627
+ if (preset === "lmstudio") return { type: "openai-compatible", baseURL: options.baseURL ?? "http://localhost:1234/v1", apiKey: options.apiKey ?? "lm-studio", catalog: { type: "openai-compatible-models", endpoint: `${(options.baseURL ?? "http://localhost:1234/v1").replace(/\/+$/, "")}/models` } };
24628
+ if (preset === "ollama-local" || preset === "ollama") return { type: "openai-compatible", baseURL: options.baseURL ?? "http://localhost:11434/v1", apiKey: options.apiKey ?? "ollama", quirks: { omitTemperature: true }, catalog: { type: "openai-compatible-models", endpoint: `${(options.baseURL ?? "http://localhost:11434/v1").replace(/\/+$/, "")}/models` } };
24629
+ if (preset === "ollama-cloud") return { type: "ollama", baseURL: options.baseURL ?? "https://ollama.com/api", ...auth, catalog: { type: "ollama-tags", endpoint: `${(options.baseURL ?? "https://ollama.com/api").replace(/\/+$/, "")}/tags` } };
24630
+ if (preset === "llamacpp") return { type: "openai-compatible", baseURL: options.baseURL ?? "http://localhost:8080/v1", apiKey: options.apiKey ?? "llama.cpp", catalog: { type: "openai-compatible-models", endpoint: `${(options.baseURL ?? "http://localhost:8080/v1").replace(/\/+$/, "")}/models` } };
24631
+ if (preset === "vllm") return { type: "openai-compatible", baseURL: options.baseURL ?? "http://localhost:8000/v1", apiKey: options.apiKey ?? "vllm", catalog: { type: "openai-compatible-models", endpoint: `${(options.baseURL ?? "http://localhost:8000/v1").replace(/\/+$/, "")}/models` } };
24632
+ if (preset === "codex") return { type: "codex", baseURL: options.baseURL ?? "https://chatgpt.com/backend-api/codex", authStore: "auto", allowApiKeyFallback: false, promptCacheKey: "root-session", catalog: { type: "codex-oauth-models" } };
24633
+ if (preset === "claudecode") return { type: "claudecode", runtime: "agent-sdk", cliPath: "~/.local/bin/claude", cwdMode: "session", historyPolicy: "passthrough-resume", onInvalidResume: "fresh", attachmentFallback: "block", allowSubagents: false, sanitizeApiKeyEnv: true, authPreflight: true, useBareMode: false, usageLedgerScope: "process", sessionLock: true, abortPolicy: "record-only", catalog: { type: "claudecode-supported-models" } };
24634
+ if ((options.type ?? preset) === "openai-compatible") {
24635
+ const baseURL = options.baseURL;
24636
+ if (!baseURL) throw new Error("openai-compatible provider requires --base-url.");
24637
+ return {
24638
+ type: "openai-compatible",
24639
+ baseURL,
24640
+ ...options.apiKey ? { apiKey: options.apiKey } : {},
24641
+ ...options.apiKeyEnv ? { apiKeyEnv: options.apiKeyEnv } : {},
24642
+ ...openAIAuth,
24643
+ catalog: { type: "openai-compatible-models", endpoint: `${baseURL.replace(/\/+$/, "")}/models` }
24644
+ };
24645
+ }
24646
+ throw new Error(`Unknown provider preset: ${preset}`);
24647
+ }
24648
+ async function writeJsonAtomic(filePath, content) {
24649
+ await mkdir6(path14.dirname(filePath), { recursive: true, mode: 448 });
24650
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
24651
+ await writeFile5(temp, content, { mode: 384 });
24652
+ await rename4(temp, filePath);
24653
+ await chmod3(filePath, 384).catch(() => void 0);
24654
+ }
24655
+ function detectDefaultProvider() {
24656
+ if (process.env.OPENAI_API_KEY) return "openai";
24657
+ if (process.env.ANTHROPIC_API_KEY) return "anthropic";
24658
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return "gemini";
24659
+ if (process.env.GROQ_API_KEY) return "groq";
24660
+ return "openai";
24661
+ }
24662
+ function objectValue(value) {
24663
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
24664
+ }
24665
+ async function exists(filePath) {
24666
+ try {
24667
+ await stat3(filePath);
24668
+ return true;
24669
+ } catch {
24670
+ return false;
24671
+ }
24672
+ }
24673
+ function expandHome2(value) {
24674
+ return resolveExpandedPath(value);
24675
+ }
24676
+
24677
+ // src/models/catalog.ts
24678
+ import crypto5 from "node:crypto";
24679
+ import { mkdir as mkdir7, readFile as readFile8, rename as rename5, writeFile as writeFile6 } from "node:fs/promises";
24680
+ import os9 from "node:os";
24681
+ import path15 from "node:path";
24682
+ var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
24683
+ var PING_TIMEOUT_MS = 1500;
24684
+ var DEFAULT_CODEX_CLIENT_VERSION = "0.0.0";
24685
+ var dynamicImport2 = new Function("specifier", "return import(specifier)");
24686
+ var inflight = /* @__PURE__ */ new Map();
24687
+ var pingCache = /* @__PURE__ */ new Map();
24688
+ var PING_CACHE_TTL_MS = 30 * 1e3;
24689
+ var packageVersionPromise;
24690
+ async function listProviderModelCatalog(providerName, providerConfig, options = {}) {
24691
+ const key = `${catalogCacheKey(providerName, providerConfig)}:${options.refresh ? "refresh" : "default"}`;
24692
+ const existing = inflight.get(key);
24693
+ if (existing) return existing;
24694
+ const promise = doListProviderModelCatalog(providerName, providerConfig, options).finally(() => inflight.delete(key));
24695
+ inflight.set(key, promise);
24696
+ return promise;
24697
+ }
24698
+ async function doListProviderModelCatalog(providerName, providerConfig, options) {
24699
+ const staticModels = staticModelEntries(providerName, providerConfig);
24700
+ const catalog = providerConfig.catalog;
24701
+ if (!catalog || catalog.type === "static") {
24702
+ return { status: staticModels.length ? "ready" : "unavailable", providerName, models: staticModels, source: "config" };
24703
+ }
24704
+ const missingAuthEnv = missingCatalogAuth(providerConfig);
24705
+ if (missingAuthEnv) {
24706
+ return {
24707
+ status: "missing-auth",
24708
+ providerName,
24709
+ models: staticModels,
24710
+ source: staticModels.length ? "config" : "fallback",
24711
+ message: `missing-auth:${missingAuthEnv}`
24712
+ };
24713
+ }
24714
+ const cacheKey = catalogCacheKey(providerName, providerConfig);
24715
+ const ttlMs = catalog.refreshTtlMs ?? DEFAULT_CACHE_TTL_MS;
24716
+ const cached = await readCatalogCache(cacheKey, ttlMs, options.refresh !== true);
24717
+ if (cached && !options.refresh) return { ...cached, models: mergeCatalogWithStatic(staticModels, cached.models) };
24718
+ if (catalog.endpoint) {
24719
+ const reachable = await pingEndpoint(catalog.endpoint, options.fetch);
24720
+ if (!reachable) {
24721
+ const stale = await readCatalogCache(cacheKey, Number.POSITIVE_INFINITY, true);
24722
+ if (stale) return { ...stale, models: mergeCatalogWithStatic(staticModels, stale.models), message: `endpoint ${catalog.endpoint} unreachable; using cached models` };
24723
+ if (staticModels.length) return { status: "ready", providerName, models: staticModels, source: "config", message: `endpoint ${catalog.endpoint} unreachable; using configured profiles` };
24724
+ return { status: "unavailable", providerName, models: [], source: "fallback", message: `endpoint ${catalog.endpoint} unreachable` };
24725
+ }
24726
+ }
24727
+ try {
24728
+ const remote = await fetchCatalog(providerName, providerConfig, options);
24729
+ const merged = mergeCatalogWithStatic(staticModels, remote.models);
24730
+ const result = { ...remote, models: merged };
24731
+ if (result.status === "ready") await writeCatalogCache(cacheKey, result);
24732
+ return result;
24733
+ } catch (error) {
24734
+ const authMissing = isMissingAuthError(error);
24735
+ const stale = await readCatalogCache(cacheKey, Number.POSITIVE_INFINITY, true);
24736
+ if (stale) return { ...stale, status: authMissing ? "missing-auth" : stale.status, models: mergeCatalogWithStatic(staticModels, stale.models), message: errorMessage2(error) };
24737
+ if (staticModels.length) return { status: authMissing ? "missing-auth" : "ready", providerName, models: staticModels, source: "config", message: errorMessage2(error) };
24738
+ return { status: authMissing ? "missing-auth" : "unavailable", providerName, models: [], source: "fallback", message: errorMessage2(error) };
24739
+ }
24740
+ }
24741
+ async function pingEndpoint(endpoint, fetcher) {
24742
+ const now2 = Date.now();
24743
+ const cached = pingCache.get(endpoint);
24744
+ if (cached && cached.expiresAt > now2) return cached.ok;
24745
+ const ok2 = await tryPing(endpoint, fetcher);
24746
+ pingCache.set(endpoint, { ok: ok2, expiresAt: now2 + PING_CACHE_TTL_MS });
24747
+ return ok2;
24748
+ }
24749
+ async function tryPing(endpoint, fetcher) {
24750
+ const fn = fetcher ?? fetch;
24751
+ try {
24752
+ const response = await fn(endpoint, { method: "HEAD", signal: AbortSignal.timeout(PING_TIMEOUT_MS) });
24753
+ if (response.status < 500) return true;
24754
+ } catch {
24755
+ }
24756
+ try {
24757
+ const response = await fn(endpoint, { method: "GET", signal: AbortSignal.timeout(PING_TIMEOUT_MS), headers: { range: "bytes=0-0" } });
24758
+ return response.status < 500;
24759
+ } catch {
24760
+ return false;
24761
+ }
24762
+ }
24763
+ async function fetchCatalog(providerName, providerConfig, options) {
24764
+ if (providerConfig.type === "codex" || providerConfig.catalog?.type === "codex-oauth-models") return fetchCodexCatalog(providerName, providerConfig, options);
24765
+ if (providerConfig.type === "claudecode" || providerConfig.catalog?.type === "claudecode-supported-models") return fetchClaudeCodeCatalog(providerName, providerConfig, options);
24766
+ if (providerConfig.catalog?.type === "anthropic-models") return fetchAnthropicCatalog(providerName, providerConfig, options);
24767
+ if (providerConfig.catalog?.type === "ollama-tags") return fetchOllamaTagsCatalog(providerName, providerConfig, options);
24768
+ return fetchOpenAIStyleCatalog(providerName, providerConfig, options);
24769
+ }
24770
+ async function fetchOpenAIStyleCatalog(providerName, providerConfig, options) {
24771
+ if (providerConfig.type !== "openai-compatible") throw new Error(`Provider ${providerName} is not OpenAI-compatible.`);
24772
+ const apiKey = resolveConfiguredApiKey(providerConfig);
24773
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
24774
+ const endpoint = providerConfig.catalog?.endpoint ?? `${providerConfig.baseURL.replace(/\/+$/, "")}/models`;
24775
+ const response = await fetchJson(endpoint, {
24776
+ fetcher: options.fetch,
24777
+ timeoutMs: options.timeoutMs,
24778
+ headers: {
24779
+ ...authHeadersForOpenAICompatible(providerConfig, apiKey.value),
24780
+ ...providerConfig.headers ?? {}
24781
+ }
24782
+ });
24783
+ return {
24784
+ status: "ready",
24785
+ providerName,
24786
+ models: normalizeOpenAIModels(response).map((entry) => ({ ...entry, source: "api" })),
24787
+ source: "api"
24788
+ };
24789
+ }
24790
+ async function fetchAnthropicCatalog(providerName, providerConfig, options) {
24791
+ if (providerConfig.type !== "anthropic") throw new Error(`Provider ${providerName} is not Anthropic.`);
24792
+ const apiKey = resolveConfiguredApiKey(providerConfig);
24793
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
24794
+ const base = providerConfig.baseURL ?? "https://api.anthropic.com/v1";
24795
+ const endpoint = providerConfig.catalog?.endpoint ?? `${base.replace(/\/+$/, "")}/models`;
24796
+ const models = [];
24797
+ let afterId;
24798
+ for (; ; ) {
24799
+ const url = new URL(endpoint);
24800
+ if (afterId) url.searchParams.set("after_id", afterId);
24801
+ url.searchParams.set("limit", "100");
24802
+ const response = await fetchJson(url.toString(), {
24803
+ fetcher: options.fetch,
24804
+ timeoutMs: options.timeoutMs,
24805
+ headers: {
24806
+ "x-api-key": apiKey.value,
24807
+ "anthropic-version": "2023-06-01"
24808
+ }
24809
+ });
24810
+ const page = normalizeAnthropicModels(response);
24811
+ models.push(...page);
24812
+ if (!response.has_more || !response.last_id) break;
24813
+ afterId = response.last_id;
24814
+ }
24815
+ return { status: "ready", providerName, models, source: "api" };
24816
+ }
24817
+ async function fetchOllamaTagsCatalog(providerName, providerConfig, options) {
24818
+ if (providerConfig.type !== "ollama") throw new Error(`Provider ${providerName} is not Ollama native.`);
24819
+ const apiKey = resolveConfiguredApiKey(providerConfig);
24820
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
24821
+ const endpoint = providerConfig.catalog?.endpoint ?? `${providerConfig.baseURL.replace(/\/+$/, "")}/tags`;
24822
+ const response = await fetchJson(endpoint, {
24823
+ fetcher: options.fetch,
24824
+ timeoutMs: options.timeoutMs,
24825
+ headers: { authorization: `Bearer ${apiKey.value}` }
24826
+ });
24827
+ return {
24828
+ status: "ready",
24829
+ providerName,
24830
+ models: normalizeOllamaTags(response).map((entry) => ({ ...entry, source: "api" })),
24831
+ source: "api"
24832
+ };
24833
+ }
24834
+ async function fetchCodexCatalog(providerName, providerConfig, options) {
24835
+ if (providerConfig.type !== "codex") throw new Error(`Provider ${providerName} is not Codex.`);
24836
+ const fetcher = options.fetch ?? fetch;
24837
+ const baseURL = (providerConfig.baseURL ?? "https://chatgpt.com/backend-api/codex").replace(/\/+$/, "");
24838
+ const installationId = await loadOrCreateInstallationId(resolveCodexHome(providerConfig.codexHome));
24839
+ const auth = getSharedCodexAuthStore({
24840
+ codexHome: providerConfig.codexHome,
24841
+ authStore: providerConfig.authStore,
24842
+ allowApiKeyFallback: providerConfig.allowApiKeyFallback,
24843
+ proactiveRefreshMinutes: providerConfig.refresh?.proactiveRefreshMinutes,
24844
+ refreshCache: providerConfig.refresh?.cache,
24845
+ fetch: fetcher
24846
+ });
24847
+ const headers = await auth.requestHeaders(installationId);
24848
+ const url = new URL(`${baseURL}/models`);
24849
+ url.searchParams.set("client_version", await codexClientVersion(options.clientVersion));
24850
+ const response = await fetchJson(url.toString(), {
24851
+ fetcher,
24852
+ timeoutMs: options.timeoutMs,
24853
+ headers
24854
+ });
24855
+ return {
24856
+ status: "ready",
24857
+ providerName,
24858
+ models: normalizeCodexModels(response).map((entry) => ({ ...entry, source: "oauth" })),
24859
+ source: "oauth"
24860
+ };
24861
+ }
24862
+ async function fetchClaudeCodeCatalog(providerName, providerConfig, options) {
24863
+ if (providerConfig.type !== "claudecode") throw new Error(`Provider ${providerName} is not Claude Code.`);
24864
+ const fallback = staticModelEntries(providerName, providerConfig);
24865
+ try {
24866
+ const module = await dynamicImport2("@anthropic-ai/claude-agent-sdk");
24867
+ if (!module.query) throw new Error("Claude Agent SDK query export is unavailable.");
24868
+ const query = module.query({
24869
+ prompt: "",
24870
+ options: {
24871
+ model: defaultModelForProvider(providerName, providerConfig),
24872
+ maxTurns: 0,
24873
+ pathToClaudeCodeExecutable: resolveClaudeCodeCliPath(providerConfig.cliPath),
24874
+ env: sanitizedClaudeCodeEnv(providerConfig.env, providerConfig.sanitizeApiKeyEnv ?? true)
24875
+ }
24876
+ });
24877
+ try {
24878
+ if (typeof query.supportedModels !== "function") throw new Error("Claude Agent SDK supportedModels is unavailable.");
24879
+ const models = await withTimeout(query.supportedModels(), options.timeoutMs ?? 5e3);
24880
+ return {
24881
+ status: "ready",
24882
+ providerName,
24883
+ models: models.map((model, index) => ({
24884
+ name: model.displayName || model.value,
24885
+ displayName: model.displayName || model.value,
24886
+ model: model.value,
24887
+ description: model.description,
24888
+ isDefault: index === 0,
24889
+ source: "oauth"
24890
+ })),
24891
+ source: "oauth"
24892
+ };
24893
+ } finally {
24894
+ query.close?.();
24895
+ }
24896
+ } catch (error) {
24897
+ if (fallback.length) return { status: "ready", providerName, models: fallback, source: "fallback", message: errorMessage2(error) };
24898
+ throw error;
24899
+ }
24900
+ }
24901
+ function staticModelEntries(providerName, providerConfig) {
24902
+ return providerModelProfiles(providerName, providerConfig).filter((profile) => !profile.hidden).map((profile, index) => ({
24903
+ name: profile.displayName ?? profile.name,
24904
+ displayName: profile.displayName ?? profile.name,
24905
+ model: profile.model,
24906
+ description: profile.description,
24907
+ isDefault: index === 0,
24908
+ source: "config"
24909
+ }));
24910
+ }
24911
+ function mergeCatalogWithStatic(staticModels, remoteModels) {
24912
+ const byModel = /* @__PURE__ */ new Map();
24913
+ for (const model of remoteModels) byModel.set(model.model, model);
24914
+ for (const model of staticModels) byModel.set(model.model, { ...byModel.get(model.model), ...model, source: model.source });
24915
+ return [...byModel.values()];
24916
+ }
24917
+ function normalizeOpenAIModels(raw) {
24918
+ const items = Array.isArray(raw.data) ? raw.data : Array.isArray(raw.models) ? raw.models : [];
24919
+ return items.map((item) => {
24920
+ const object2 = item;
24921
+ const id = stringValue4(object2.id) ?? stringValue4(object2.name) ?? stringValue4(object2.model);
24922
+ if (!id) return void 0;
24923
+ return {
24924
+ name: id,
24925
+ displayName: stringValue4(object2.display_name) ?? stringValue4(object2.displayName) ?? id,
24926
+ model: id,
24927
+ description: stringValue4(object2.description)
24928
+ };
24929
+ }).filter(Boolean);
24930
+ }
24931
+ function normalizeAnthropicModels(raw) {
24932
+ return (raw.data ?? []).map((item) => {
24933
+ const object2 = item;
24934
+ const id = stringValue4(object2.id);
24935
+ if (!id) return void 0;
24936
+ return {
24937
+ name: stringValue4(object2.display_name) ?? id,
24938
+ displayName: stringValue4(object2.display_name) ?? id,
24939
+ model: id,
24940
+ description: stringValue4(object2.description),
24941
+ source: "api"
24942
+ };
24943
+ }).filter(Boolean);
24944
+ }
24945
+ function normalizeOllamaTags(raw) {
24946
+ const items = Array.isArray(raw.models) ? raw.models : [];
24947
+ return items.map((item) => {
24948
+ const object2 = item;
24949
+ const id = stringValue4(object2.name) ?? stringValue4(object2.model);
24950
+ if (!id) return void 0;
24951
+ return { name: id, displayName: id, model: id };
24952
+ }).filter(Boolean);
24953
+ }
24954
+ function normalizeCodexModels(raw) {
24955
+ const items = Array.isArray(raw.models) ? raw.models : [];
24956
+ return items.map((item, index) => {
24957
+ const object2 = item;
24958
+ const id = stringValue4(object2.slug) ?? stringValue4(object2.id) ?? stringValue4(object2.model);
24959
+ if (!id) return void 0;
24960
+ return {
24961
+ name: stringValue4(object2.display_name) ?? id,
24962
+ displayName: stringValue4(object2.display_name) ?? id,
24963
+ model: id,
24964
+ description: stringValue4(object2.description),
24965
+ isDefault: index === 0
24966
+ };
24967
+ }).filter(Boolean);
24968
+ }
24969
+ function authHeadersForOpenAICompatible(providerConfig, apiKey) {
24970
+ const auth = inferOpenAICompatibleAuth(providerConfig.baseURL, providerConfig.auth);
24971
+ const type = auth.type ?? "bearer";
24972
+ const header = auth.header ?? (type === "api-key" ? "api-key" : "authorization");
24973
+ return { [header]: type === "bearer" ? `Bearer ${apiKey}` : apiKey };
24974
+ }
24975
+ async function fetchJson(url, options) {
24976
+ const response = await (options.fetcher ?? fetch)(url, {
24977
+ method: "GET",
24978
+ headers: {
24979
+ accept: "application/json",
24980
+ ...options.headers ?? {}
24981
+ },
24982
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
24983
+ });
24984
+ const text = await response.text();
24985
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
24986
+ return text ? JSON.parse(text) : {};
24987
+ }
24988
+ function cachePath(cacheKey) {
24989
+ return path15.join(os9.homedir(), ".demian", "cache", "models", `${safeCacheName(cacheKey)}.json`);
24990
+ }
24991
+ async function readCatalogCache(cacheKey, ttlMs, allow) {
24992
+ if (!allow) return void 0;
24993
+ try {
24994
+ const raw = JSON.parse(await readFile8(cachePath(cacheKey), "utf8"));
24995
+ if (!raw.result) return void 0;
24996
+ if (ttlMs !== Number.POSITIVE_INFINITY && Date.now() - (raw.savedAt ?? 0) > ttlMs) return void 0;
24997
+ return { ...raw.result, source: "cache", models: raw.result.models.map((model) => ({ ...model, source: "cache" })) };
24998
+ } catch {
24999
+ return void 0;
25000
+ }
25001
+ }
25002
+ async function writeCatalogCache(cacheKey, result) {
25003
+ const filePath = cachePath(cacheKey);
25004
+ await mkdir7(path15.dirname(filePath), { recursive: true, mode: 448 });
25005
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
25006
+ await writeFile6(temp, JSON.stringify({ savedAt: Date.now(), result }, null, 2), { mode: 384 });
25007
+ await rename5(temp, filePath);
25008
+ }
25009
+ function safeCacheName(providerName) {
25010
+ return providerName.replace(/[^a-zA-Z0-9_.-]+/g, "_");
25011
+ }
25012
+ function catalogCacheKey(providerName, providerConfig) {
25013
+ const identity = {
25014
+ providerName,
25015
+ type: providerConfig.type,
25016
+ catalogType: providerConfig.catalog?.type,
25017
+ endpoint: providerConfig.catalog?.endpoint,
25018
+ baseURL: "baseURL" in providerConfig ? providerConfig.baseURL : void 0,
25019
+ apiKeyEnv: "apiKeyEnv" in providerConfig ? providerConfig.apiKeyEnv : void 0,
25020
+ apiKeyEnvAliases: "apiKeyEnvAliases" in providerConfig ? providerConfig.apiKeyEnvAliases : void 0,
25021
+ auth: "auth" in providerConfig ? providerConfig.auth : void 0
25022
+ };
25023
+ const digest = crypto5.createHash("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 16);
25024
+ return `${providerName}-${digest}`;
25025
+ }
25026
+ function missingCatalogAuth(providerConfig) {
25027
+ if (providerConfig.type !== "openai-compatible" && providerConfig.type !== "anthropic" && providerConfig.type !== "ollama") return void 0;
25028
+ if (providerConfig.catalog?.requiresAuth === false) return void 0;
25029
+ const expectsAuth = providerConfig.catalog?.requiresAuth === true || !!providerConfig.apiKey || !!providerConfig.apiKeyEnv || !!providerConfig.apiKeyEnvAliases?.length;
25030
+ if (!expectsAuth) return void 0;
25031
+ const apiKey = resolveConfiguredApiKey(providerConfig);
25032
+ return apiKey.value ? void 0 : apiKey.envName ?? "apiKey";
25033
+ }
25034
+ function stringValue4(value) {
25035
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
25036
+ }
25037
+ function missingAuth(envName) {
25038
+ const error = new Error(`missing-auth:${envName}`);
25039
+ return error;
25040
+ }
25041
+ function isMissingAuthError(error) {
25042
+ if (error instanceof CodexAuthError) {
25043
+ return error.code === "missing_auth" || error.code === "missing_chatgpt_tokens" || error.code === "refresh_unavailable" || error.code === "keyring_write_unsupported" || error.code === "refresh_expired" || error.code === "refresh_invalidated" || error.code === "refresh_reused" || error.code === "refresh_other";
25044
+ }
25045
+ return error instanceof Error && error.message.startsWith("missing-auth:");
25046
+ }
25047
+ function errorMessage2(error) {
25048
+ return error instanceof Error ? error.message : String(error);
25049
+ }
25050
+ async function codexClientVersion(override) {
25051
+ const normalized = semverLike(override);
25052
+ if (normalized) return normalized;
25053
+ packageVersionPromise ??= readPackageVersion();
25054
+ return packageVersionPromise;
25055
+ }
25056
+ async function readPackageVersion() {
25057
+ try {
25058
+ const raw = JSON.parse(await readFile8(new URL("../package.json", import.meta.url), "utf8"));
25059
+ return semverLike(raw.version) ?? DEFAULT_CODEX_CLIENT_VERSION;
25060
+ } catch {
25061
+ return DEFAULT_CODEX_CLIENT_VERSION;
25062
+ }
25063
+ }
25064
+ function semverLike(value) {
25065
+ if (typeof value !== "string") return void 0;
25066
+ const trimmed = value.trim();
25067
+ return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(trimmed) ? trimmed : void 0;
25068
+ }
25069
+ function withTimeout(promise, timeoutMs) {
25070
+ return new Promise((resolve, reject) => {
25071
+ const timer = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
25072
+ promise.then(
25073
+ (value) => {
25074
+ clearTimeout(timer);
25075
+ resolve(value);
25076
+ },
25077
+ (error) => {
25078
+ clearTimeout(timer);
25079
+ reject(error);
25080
+ }
25081
+ );
25082
+ });
25083
+ }
25084
+
25085
+ // src/config-command.ts
25086
+ async function maybeRunConfigCommand(argv) {
25087
+ if (argv[0] === "config") return runConfigCommand(argv.slice(1));
25088
+ if (argv[0] === "auth") return runAuthCommand(argv.slice(1));
25089
+ return void 0;
25090
+ }
25091
+ async function runConfigCommand(argv) {
25092
+ const [command, ...rest] = argv;
25093
+ try {
25094
+ if (!command || command === "help" || command === "--help" || command === "-h") {
25095
+ printConfigHelp();
25096
+ return 0;
25097
+ }
25098
+ if (command === "init") {
25099
+ const flags = parseFlags(rest);
25100
+ const force = boolFlag(flags, "force") || await confirmOverwriteIfExists(stringFlag(flags, "path"), boolFlag(flags, "print"));
25101
+ const result = await createUserConfig({
25102
+ path: stringFlag(flags, "path"),
25103
+ force,
25104
+ print: boolFlag(flags, "print"),
25105
+ defaultProvider: stringFlag(flags, "provider")
25106
+ });
25107
+ if (boolFlag(flags, "print")) {
25108
+ process.stdout.write(result.content);
25109
+ } else if (result.existed && !result.created) {
25110
+ process.stdout.write(`Config already exists: ${result.path}
25111
+ `);
25112
+ } else {
25113
+ process.stdout.write(`Created config: ${result.path}
25114
+ `);
25115
+ }
25116
+ return 0;
25117
+ }
25118
+ if (command === "path") {
25119
+ process.stdout.write(`${defaultUserConfigPath()}
25120
+ `);
25121
+ return 0;
25122
+ }
25123
+ if (command === "setup") {
25124
+ return runSetupWizard();
25125
+ }
25126
+ if (command === "list") {
25127
+ const flags = parseFlags(rest);
25128
+ const config = await loadConfig({ cwd: process.cwd(), configPath: stringFlag(flags, "config") });
25129
+ process.stdout.write(`defaultProvider: ${config.defaultProvider}
25130
+ `);
25131
+ for (const name of Object.keys(config.providers)) {
25132
+ const provider = config.providers[name];
25133
+ if (provider.hidden) continue;
25134
+ process.stdout.write(`- ${name} (${provider.type})
25135
+ `);
25136
+ }
25137
+ return 0;
25138
+ }
25139
+ if (command === "models") {
25140
+ const providerName = rest.find((item) => !item.startsWith("-"));
25141
+ if (!providerName) throw new Error("config models requires a provider name.");
25142
+ const flags = parseFlags(rest.filter((item) => item !== providerName));
25143
+ const config = await loadConfig({ cwd: process.cwd(), configPath: stringFlag(flags, "config") });
25144
+ const provider = resolveProviderConfig(config, providerName);
25145
+ const result = await listProviderModelCatalog(providerName, provider, { refresh: boolFlag(flags, "refresh") });
25146
+ if (result.status !== "ready") {
25147
+ process.stdout.write(`${providerName}: ${result.status}${result.message ? ` (${result.message})` : ""}
25148
+ `);
25149
+ return result.status === "missing-auth" ? 2 : 1;
25150
+ }
25151
+ for (const model of result.models) {
25152
+ const label = model.displayName && model.displayName !== model.model ? `${model.displayName} (${model.model})` : model.model;
25153
+ process.stdout.write(`${model.isDefault ? "* " : " "}${label}
25154
+ `);
25155
+ }
25156
+ return 0;
25157
+ }
25158
+ if (command === "add-provider") {
25159
+ const [presetOrName, ...tail2] = rest;
25160
+ if (!presetOrName) throw new Error("config add-provider requires a provider name or preset.");
25161
+ const flags = parseFlags(tail2);
25162
+ const name = stringFlag(flags, "name") ?? presetOrName;
25163
+ const force = await ensureOverwriteForProvider(stringFlag(flags, "path"), name, boolFlag(flags, "force"));
25164
+ const result = await addProvider({
25165
+ path: stringFlag(flags, "path"),
25166
+ name,
25167
+ preset: presetOrName,
25168
+ type: stringFlag(flags, "type") ?? presetOrName,
25169
+ baseURL: stringFlag(flags, "base-url"),
25170
+ apiKey: await apiKeyFromFlags(flags),
25171
+ apiKeyEnv: stringFlag(flags, "api-key-env"),
25172
+ authHeader: stringFlag(flags, "auth-header"),
25173
+ force
25174
+ });
25175
+ process.stdout.write(`Updated config: ${result.path}
25176
+ `);
25177
+ return 0;
25178
+ }
25179
+ if (command === "add-model") {
25180
+ const [provider, ...tail2] = rest;
25181
+ if (!provider) throw new Error("config add-model requires a provider name.");
25182
+ const flags = parseFlags(tail2);
25183
+ const name = requiredStringFlag(flags, "name");
25184
+ const model = requiredStringFlag(flags, "model");
25185
+ const displayName = stringFlag(flags, "display-name");
25186
+ const force = await ensureOverwriteForModel(stringFlag(flags, "path"), provider, name, displayName, boolFlag(flags, "force"));
25187
+ const result = await addModelProfile({
25188
+ path: stringFlag(flags, "path"),
25189
+ provider,
25190
+ name,
25191
+ displayName,
25192
+ model,
25193
+ baseURL: stringFlag(flags, "base-url"),
25194
+ apiKey: await apiKeyFromFlags(flags),
25195
+ apiKeyEnv: stringFlag(flags, "api-key-env"),
25196
+ force
25197
+ });
25198
+ process.stdout.write(`Updated config: ${result.path}
25199
+ `);
25200
+ return 0;
25201
+ }
25202
+ throw new Error(`Unknown config command: ${command}`);
25203
+ } catch (error) {
25204
+ process.stderr.write(`${errorMessage3(error)}
25205
+ `);
25206
+ return 1;
25207
+ }
25208
+ }
25209
+ async function runAuthCommand(argv) {
25210
+ const [command, provider] = argv;
25211
+ try {
25212
+ if (!command || command === "help" || command === "--help" || command === "-h") {
25213
+ printAuthHelp();
25214
+ return 0;
25215
+ }
25216
+ if (command === "status") {
25217
+ const claude = await preflightClaudeCodeAuth({ sanitizeApiKeyEnv: true }).catch((error) => ({ ok: false, source: "unknown", warning: errorMessage3(error) }));
25218
+ process.stdout.write(`claudecode: ${claude.ok ? "ok" : "not ready"} (${claude.source})${claude.warning ? ` - ${claude.warning}` : ""}
25219
+ `);
25220
+ const codex = await checkCodexAuth();
25221
+ process.stdout.write(`codex: ${codex.ok ? "ok" : "not ready"} (${codex.source})${codex.message ? ` - ${codex.message}` : ""}
25222
+ `);
25223
+ return claude.ok && codex.ok ? 0 : 1;
25224
+ }
25225
+ if (command !== "login") throw new Error(`Unknown auth command: ${command}`);
25226
+ return runProviderLogin(provider);
25227
+ } catch (error) {
25228
+ process.stderr.write(`${errorMessage3(error)}
25229
+ `);
25230
+ return 1;
25231
+ }
25232
+ }
25233
+ async function runProviderLogin(provider) {
25234
+ if (provider === "codex") {
25235
+ if (!await commandExists("codex")) {
25236
+ process.stderr.write("Codex CLI not found in PATH. Install it from https://github.com/openai/codex first, then re-run `demian auth login codex`.\n");
25237
+ return 1;
25238
+ }
25239
+ return runLogin("codex", ["login"]);
25240
+ }
25241
+ if (provider === "claudecode") {
25242
+ const cli = resolveClaudeCodeCliPath();
25243
+ if (!cli && !await commandExists("claude")) {
25244
+ process.stderr.write("Claude Code CLI not found. Install it from https://docs.claude.com/en/docs/agents-and-tools/claude-code first, then re-run `demian auth login claudecode`.\n");
25245
+ return 1;
25246
+ }
25247
+ return runLogin(cli ?? "claude", ["setup-token"]);
25248
+ }
25249
+ throw new Error("auth login requires provider codex or claudecode.");
25250
+ }
25251
+ function runLogin(command, args) {
25252
+ process.stdout.write(`Starting ${[command, ...args].join(" ")}...
25253
+ `);
25254
+ const opened = /* @__PURE__ */ new Set();
25255
+ return new Promise((resolve) => {
25256
+ const child = spawn4(command, args, { stdio: ["inherit", "pipe", "pipe"], env: process.env });
25257
+ const onText = (stream) => (chunk) => {
25258
+ const text = chunk.toString();
25259
+ stream.write(text);
25260
+ for (const url of urlsInText(text)) {
25261
+ if (opened.has(url)) continue;
25262
+ opened.add(url);
25263
+ void openUrl(url);
25264
+ }
25265
+ };
25266
+ child.stdout.on("data", onText(process.stdout));
25267
+ child.stderr.on("data", onText(process.stderr));
25268
+ child.on("error", (error) => {
25269
+ process.stderr.write(`${error.message}
25270
+ `);
25271
+ resolve(1);
25272
+ });
25273
+ child.on("close", (code) => resolve(code ?? 1));
25274
+ });
25275
+ }
25276
+ function parseFlags(argv) {
25277
+ const flags = /* @__PURE__ */ new Map();
25278
+ for (let i = 0; i < argv.length; i++) {
25279
+ const arg = argv[i];
25280
+ if (!arg.startsWith("--")) continue;
25281
+ const key = arg.slice(2);
25282
+ const next = argv[i + 1];
25283
+ if (!next || next.startsWith("--")) {
25284
+ flags.set(key, true);
25285
+ } else {
25286
+ flags.set(key, next);
25287
+ i++;
25288
+ }
25289
+ }
25290
+ return flags;
25291
+ }
25292
+ function stringFlag(flags, name) {
25293
+ const value = flags.get(name);
25294
+ return typeof value === "string" && value.trim() ? value : void 0;
25295
+ }
25296
+ function requiredStringFlag(flags, name) {
25297
+ const value = stringFlag(flags, name);
25298
+ if (!value) throw new Error(`--${name} is required.`);
25299
+ return value;
25300
+ }
25301
+ function boolFlag(flags, name) {
25302
+ return flags.get(name) === true;
25303
+ }
25304
+ async function apiKeyFromFlags(flags) {
25305
+ const inline = stringFlag(flags, "api-key");
25306
+ if (inline) return inline;
25307
+ if (!boolFlag(flags, "api-key-stdin")) return void 0;
25308
+ return (await readStdin()).trim();
25309
+ }
25310
+ async function readStdin() {
25311
+ let value = "";
25312
+ for await (const chunk of process.stdin) value += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
25313
+ return value;
25314
+ }
25315
+ function urlsInText(text) {
25316
+ return [...text.matchAll(/https?:\/\/[^\s)>"']+/g)].map((match) => match[0]);
25317
+ }
25318
+ function openUrl(url) {
25319
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
25320
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
25321
+ return new Promise((resolve) => {
25322
+ const child = spawn4(command, args, { stdio: "ignore", detached: true });
25323
+ child.on("error", () => resolve());
25324
+ child.on("spawn", () => {
25325
+ child.unref();
25326
+ resolve();
25327
+ });
25328
+ });
25329
+ }
25330
+ function printConfigHelp() {
25331
+ process.stdout.write(`demian config
25332
+
25333
+ Usage:
25334
+ demian config init [--path <path>] [--force] [--print] [--provider <name>]
25335
+ demian config setup interactive multi-step provider wizard
25336
+ demian config add-provider <preset|openai-compatible> [--name <name>] [--base-url <url>] [--api-key-env <env>] [--api-key-stdin] [--auth-header <header>] [--force]
25337
+ demian config add-model <provider> --name <name> --model <model> [--display-name <label>] [--base-url <url>] [--api-key-env <env>] [--api-key-stdin] [--force]
25338
+ demian config models <provider> [--refresh] [--config <path>]
25339
+ demian config list [--config <path>]
25340
+ demian config path
25341
+ `);
25342
+ }
25343
+ function printAuthHelp() {
25344
+ process.stdout.write(`demian auth
25345
+
25346
+ Usage:
25347
+ demian auth login codex
25348
+ demian auth login claudecode
25349
+ demian auth status
25350
+ `);
25351
+ }
25352
+ function errorMessage3(error) {
25353
+ return error instanceof Error ? error.message : String(error);
25354
+ }
25355
+ async function confirmOverwriteIfExists(customPath, isPrint) {
25356
+ if (isPrint) return false;
25357
+ const filePath = customPath ?? defaultUserConfigPath();
25358
+ if (!await fileExists2(filePath)) return false;
25359
+ return promptYesNo(`Overwrite existing config at ${filePath}? (y/N): `, false);
25360
+ }
25361
+ async function ensureOverwriteForProvider(customPath, providerName, force) {
25362
+ if (force) return true;
25363
+ const filePath = customPath ?? defaultUserConfigPath();
25364
+ if (!await fileExists2(filePath)) return false;
25365
+ const config = await readJsonSafe(filePath);
25366
+ const provider = config?.providers?.[providerName];
25367
+ if (!provider) return false;
25368
+ return promptYesNo(`Provider "${providerName}" already exists. Overwrite? (y/N): `, false);
25369
+ }
25370
+ async function ensureOverwriteForModel(customPath, providerName, profileName, displayName, force) {
25371
+ if (force) return true;
25372
+ const filePath = customPath ?? defaultUserConfigPath();
25373
+ if (!await fileExists2(filePath)) return false;
25374
+ const config = await readJsonSafe(filePath);
25375
+ const profiles = Array.isArray(config?.providers?.[providerName]?.modelProfiles) ? config.providers[providerName].modelProfiles : [];
25376
+ const sameName = profiles.find((entry) => entry.name === profileName);
25377
+ const sameDisplay = displayName ? profiles.find((entry) => entry.displayName === displayName && entry.name !== profileName) : void 0;
25378
+ if (!sameName && !sameDisplay) return false;
25379
+ const which = sameName ? `name "${profileName}"` : `displayName "${displayName}"`;
25380
+ return promptYesNo(`Profile with ${which} already exists on provider ${providerName}. Overwrite? (y/N): `, false);
25381
+ }
25382
+ async function fileExists2(filePath) {
25383
+ try {
25384
+ await stat4(filePath);
25385
+ return true;
25386
+ } catch {
25387
+ return false;
25388
+ }
25389
+ }
25390
+ async function readJsonSafe(filePath) {
25391
+ try {
25392
+ const { readFile: readFile18 } = await import("node:fs/promises");
25393
+ const text = await readFile18(filePath, "utf8");
25394
+ return JSON.parse(text);
25395
+ } catch {
25396
+ return void 0;
25397
+ }
25398
+ }
25399
+ async function promptYesNo(question, defaultYes) {
25400
+ if (!process.stdin.isTTY) {
25401
+ process.stderr.write(`${question.replace(/\(y\/N\)|\(Y\/n\)/, "").trim()} (non-interactive; pass --force to overwrite).
25402
+ `);
25403
+ return false;
25404
+ }
25405
+ const rl2 = readline3.createInterface({ input: process.stdin, output: process.stdout });
25406
+ try {
25407
+ const answer = (await rl2.question(question)).trim().toLowerCase();
25408
+ if (!answer) return defaultYes;
25409
+ return answer === "y" || answer === "yes";
25410
+ } finally {
25411
+ rl2.close();
25412
+ }
25413
+ }
25414
+ async function commandExists(command) {
25415
+ return new Promise((resolve) => {
25416
+ const child = spawn4(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" });
25417
+ child.on("close", (code) => resolve(code === 0));
25418
+ child.on("error", () => resolve(false));
25419
+ });
25420
+ }
25421
+ async function runFirstRunWizard() {
25422
+ const filePath = defaultUserConfigPath();
25423
+ if (await fileExists2(filePath)) return void 0;
25424
+ if (!process.stdin.isTTY) {
25425
+ process.stderr.write(`No Demian config at ${filePath}. Run \`demian config init\` to create one.
25426
+ `);
25427
+ return void 0;
25428
+ }
25429
+ process.stdout.write(`
25430
+ Welcome to Demian.
25431
+ No config found at ${filePath}.
25432
+ `);
25433
+ const ok2 = await promptYesNo("Create a default v2 config now? (Y/n): ", true);
25434
+ if (!ok2) {
25435
+ process.stdout.write("Skipped. Run `demian config init` later to set up.\n");
25436
+ return void 0;
25437
+ }
25438
+ const result = await createUserConfig({});
25439
+ process.stdout.write(`Created ${result.path}.
25440
+ `);
25441
+ const walkProviders = await promptYesNo("Walk through provider setup now? (y/N): ", false);
25442
+ if (walkProviders) await walkProviderSetup();
25443
+ else {
25444
+ process.stdout.write("Next steps:\n");
25445
+ process.stdout.write(" - Set API key env vars (e.g. OPENAI_API_KEY)\n");
25446
+ process.stdout.write(" - `demian config models <provider> --refresh` to inspect catalogs\n");
25447
+ process.stdout.write(" - `demian auth login codex|claudecode` for OAuth providers\n");
25448
+ process.stdout.write(" - `demian config setup` to re-enter this wizard\n\n");
25449
+ }
25450
+ return { created: true, configPath: result.path };
25451
+ }
25452
+ async function runSetupWizard() {
25453
+ if (!process.stdin.isTTY) {
25454
+ process.stderr.write("config setup requires an interactive terminal.\n");
25455
+ return 1;
25456
+ }
25457
+ const filePath = defaultUserConfigPath();
25458
+ if (!await fileExists2(filePath)) {
25459
+ const wizard = await runFirstRunWizard();
25460
+ return wizard?.created ? 0 : 1;
25461
+ }
25462
+ await walkProviderSetup();
25463
+ return 0;
25464
+ }
25465
+ async function walkProviderSetup() {
25466
+ const builtinPresets = [
25467
+ { name: "openai", description: "OpenAI ChatGPT API", defaultEnv: "OPENAI_API_KEY" },
25468
+ { name: "anthropic", description: "Anthropic Claude API", defaultEnv: "ANTHROPIC_API_KEY" },
25469
+ { name: "gemini", description: "Google Gemini (OpenAI-compatible)", defaultEnv: "GEMINI_API_KEY" },
25470
+ { name: "groq", description: "Groq Cloud", defaultEnv: "GROQ_API_KEY" },
25471
+ { name: "codex", description: "ChatGPT Codex (OAuth)", oauth: "codex" },
25472
+ { name: "claudecode", description: "Claude Code external runtime (OAuth)", oauth: "claudecode" }
25473
+ ];
25474
+ process.stdout.write("\nProvider setup. Press Enter to skip a provider.\n");
25475
+ for (const preset of builtinPresets) {
25476
+ const want = await promptYesNo(`Configure ${preset.name} (${preset.description})? (y/N): `, false);
25477
+ if (!want) continue;
25478
+ if (preset.oauth) {
25479
+ const login = await promptYesNo(`Run \`demian auth login ${preset.oauth}\` now? (Y/n): `, true);
25480
+ if (login) await runProviderLogin(preset.oauth).catch(() => void 0);
25481
+ continue;
25482
+ }
25483
+ if (preset.defaultEnv) {
25484
+ const envSet = !!process.env[preset.defaultEnv];
25485
+ process.stdout.write(` env ${preset.defaultEnv}: ${envSet ? "set" : "not set"}
25486
+ `);
25487
+ if (!envSet) process.stdout.write(` \u2192 export ${preset.defaultEnv}=... before running demian.
25488
+ `);
25489
+ }
25490
+ }
25491
+ process.stdout.write("\nSetup complete. Run `demian` to start a session.\n\n");
25492
+ }
25493
+ async function checkCodexAuth() {
25494
+ const candidates = [path16.join(process.env.HOME ?? os10.homedir(), ".codex", "auth.json"), path16.join(process.env.HOME ?? os10.homedir(), ".codex", "credentials.json")];
25495
+ for (const candidate of candidates) {
25496
+ if (await fileExists2(candidate)) {
25497
+ return { ok: true, source: "codex-store", message: candidate };
25498
+ }
25499
+ }
25500
+ if (process.env.OPENAI_API_KEY) return { ok: true, source: "api-key-fallback", message: "OPENAI_API_KEY set" };
25501
+ return { ok: false, source: "none", message: "no codex auth file or OPENAI_API_KEY; run `demian auth login codex`" };
25502
+ }
25503
+
25504
+ // src/doctor/policies.ts
25505
+ import { existsSync as existsSync2 } from "node:fs";
25506
+ import { readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
25507
+ import os11 from "node:os";
25508
+ import path17 from "node:path";
25509
+ function isDoctorCommand(argv) {
25510
+ return argv[0] === "doctor";
25511
+ }
25512
+ async function maybeRunDoctorCommand(argv, options = {}) {
25513
+ if (!isDoctorCommand(argv)) return void 0;
25514
+ return runDoctorCommand(argv.slice(1), options);
25515
+ }
25516
+ async function runDoctorCommand(argv, options = {}) {
25517
+ const stdout = options.stdout ?? process.stdout;
25518
+ const stderr = options.stderr ?? process.stderr;
25519
+ const flags = parseDoctorFlags(argv);
25520
+ if (flags.help) {
25521
+ stdout.write(doctorHelp());
25522
+ return 0;
25523
+ }
25524
+ if (argv[0] !== "policies" || !argv.includes("--upgrade-namespaces")) {
25525
+ stderr.write("Unsupported doctor command. Run `demian doctor policies --upgrade-namespaces [--write]`.\n");
25526
+ return 1;
25527
+ }
25528
+ const cwd = path17.resolve(flags.cwd ?? options.cwd ?? process.cwd());
25529
+ const filePaths = resolvePolicyConfigPaths(cwd, flags.configPaths);
25530
+ if (filePaths.length === 0) {
25531
+ stdout.write("No demian config files found to inspect.\n");
24183
25532
  return 0;
24184
25533
  }
24185
25534
  const allChanges = [];
24186
25535
  let updatedFiles = 0;
24187
25536
  for (const filePath of filePaths) {
24188
- const original = await readFile7(filePath, "utf8");
25537
+ const original = await readFile9(filePath, "utf8");
24189
25538
  const upgraded = upgradePolicyNamespacesInText(original, filePath);
24190
25539
  allChanges.push(...upgraded.changes);
24191
25540
  if (flags.write && upgraded.changes.length > 0) {
24192
- await writeFile5(filePath, upgraded.text, "utf8");
25541
+ await writeFile7(filePath, upgraded.text, "utf8");
24193
25542
  updatedFiles++;
24194
25543
  }
24195
25544
  }
@@ -24277,11 +25626,11 @@ function parseDoctorFlags(argv) {
24277
25626
  return flags;
24278
25627
  }
24279
25628
  function resolvePolicyConfigPaths(cwd, explicitPaths) {
24280
- const candidates = explicitPaths.length > 0 ? explicitPaths.map((item) => path13.resolve(cwd, item)) : [
24281
- path13.join(os8.homedir(), ".demian", "config.json"),
24282
- path13.join(os8.homedir(), ".demian", "config.jsond"),
24283
- path13.join(cwd, ".demian", "config.json"),
24284
- path13.join(cwd, ".demian", "config.jsond")
25629
+ const candidates = explicitPaths.length > 0 ? explicitPaths.map((item) => path17.resolve(cwd, item)) : [
25630
+ path17.join(os11.homedir(), ".demian", "config.json"),
25631
+ path17.join(os11.homedir(), ".demian", "config.jsond"),
25632
+ path17.join(cwd, ".demian", "config.json"),
25633
+ path17.join(cwd, ".demian", "config.jsond")
24285
25634
  ];
24286
25635
  return [...new Set(candidates)].filter((item) => existsSync2(item));
24287
25636
  }
@@ -24377,8 +25726,9 @@ Flags:
24377
25726
  }
24378
25727
 
24379
25728
  // src/transcript.ts
24380
- import { mkdir as mkdir6, appendFile, writeFile as writeFile6 } from "node:fs/promises";
24381
- import path14 from "node:path";
25729
+ import { mkdir as mkdir8, appendFile, writeFile as writeFile8 } from "node:fs/promises";
25730
+ import os12 from "node:os";
25731
+ import path18 from "node:path";
24382
25732
  var TranscriptWriter = class {
24383
25733
  filePath;
24384
25734
  #enabled;
@@ -24386,9 +25736,9 @@ var TranscriptWriter = class {
24386
25736
  #queue = Promise.resolve();
24387
25737
  constructor(options) {
24388
25738
  this.#enabled = options.enabled !== false;
24389
- const dir = path14.join(options.cwd, ".demian", "transcripts", options.sessionId);
24390
- this.filePath = path14.join(dir, "session.jsonl");
24391
- this.#ready = this.#enabled ? mkdir6(dir, { recursive: true }).then(() => writeFile6(this.filePath, "", { flag: "a" })) : Promise.resolve();
25739
+ const dir = path18.join(options.storageDir ?? defaultDemianStorageDir(), "transcripts", options.sessionId);
25740
+ this.filePath = path18.join(dir, "session.jsonl");
25741
+ this.#ready = this.#enabled ? mkdir8(dir, { recursive: true }).then(() => writeFile8(this.filePath, "", { flag: "a" })) : Promise.resolve();
24392
25742
  }
24393
25743
  write(event) {
24394
25744
  if (!this.#enabled) return Promise.resolve();
@@ -24403,12 +25753,15 @@ var TranscriptWriter = class {
24403
25753
  await this.#queue;
24404
25754
  }
24405
25755
  };
25756
+ function defaultDemianStorageDir() {
25757
+ return path18.join(os12.homedir(), ".demian");
25758
+ }
24406
25759
 
24407
25760
  // src/external-runtime/snapshot-diff.ts
24408
25761
  import { createHash as createHash2 } from "node:crypto";
24409
25762
  import { createReadStream } from "node:fs";
24410
- import { readdir, readFile as readFile8, stat as stat3 } from "node:fs/promises";
24411
- import path15 from "node:path";
25763
+ import { readdir, readFile as readFile10, stat as stat5 } from "node:fs/promises";
25764
+ import path19 from "node:path";
24412
25765
 
24413
25766
  // src/workspace/diff.ts
24414
25767
  var CONTEXT_LINES = 3;
@@ -24618,7 +25971,7 @@ async function diffWorkspaceSnapshot(before) {
24618
25971
  }
24619
25972
  async function walk(root, relativeDir, entries, options) {
24620
25973
  if (entries.size >= options.maxFiles) return;
24621
- const absoluteDir = path15.join(root, relativeDir);
25974
+ const absoluteDir = path19.join(root, relativeDir);
24622
25975
  let items;
24623
25976
  try {
24624
25977
  items = await readdir(absoluteDir, { withFileTypes: true });
@@ -24629,8 +25982,8 @@ async function walk(root, relativeDir, entries, options) {
24629
25982
  if (entries.size >= options.maxFiles) return;
24630
25983
  if (item.name.startsWith(".") && item.name !== ".env.example" && SKIP_DIRS.has(item.name)) continue;
24631
25984
  if (SKIP_DIRS.has(item.name)) continue;
24632
- const relativePath = normalizeRelativePath(path15.join(relativeDir, item.name));
24633
- const absolutePath = path15.join(root, relativePath);
25985
+ const relativePath = normalizeRelativePath(path19.join(relativeDir, item.name));
25986
+ const absolutePath = path19.join(root, relativePath);
24634
25987
  if (item.isDirectory()) {
24635
25988
  await walk(root, relativePath, entries, options);
24636
25989
  continue;
@@ -24641,7 +25994,7 @@ async function walk(root, relativeDir, entries, options) {
24641
25994
  }
24642
25995
  }
24643
25996
  async function snapshotFile(filePath, relativePath, maxTextBytes) {
24644
- const info = await stat3(filePath);
25997
+ const info = await stat5(filePath);
24645
25998
  const sha2562 = await sha256File(filePath);
24646
25999
  const content = info.size <= maxTextBytes ? await readTextContent(filePath) : void 0;
24647
26000
  return { path: relativePath, size: info.size, sha256: sha2562, content };
@@ -24652,7 +26005,7 @@ function snapshotDiffText(entry) {
24652
26005
  `;
24653
26006
  }
24654
26007
  async function readTextContent(filePath) {
24655
- const buffer = await readFile8(filePath);
26008
+ const buffer = await readFile10(filePath);
24656
26009
  if (buffer.includes(0)) return void 0;
24657
26010
  return buffer.toString("utf8");
24658
26011
  }
@@ -24666,7 +26019,7 @@ function sha256File(filePath) {
24666
26019
  });
24667
26020
  }
24668
26021
  function normalizeRelativePath(value) {
24669
- return value.split(path15.sep).join("/");
26022
+ return value.split(path19.sep).join("/");
24670
26023
  }
24671
26024
 
24672
26025
  // src/external-runtime/session-runner.ts
@@ -25101,15 +26454,15 @@ function safeJson(value) {
25101
26454
  }
25102
26455
 
25103
26456
  // src/hooks/dispatcher.ts
25104
- import path17 from "node:path";
26457
+ import path21 from "node:path";
25105
26458
 
25106
26459
  // src/hooks/command.ts
25107
- import { spawn as spawn4 } from "node:child_process";
26460
+ import { spawn as spawn5 } from "node:child_process";
25108
26461
  async function runCommandHook(hook, ctx) {
25109
26462
  if (!hook.command) return void 0;
25110
26463
  const timeoutMs = hook.timeoutMs ?? 1e4;
25111
26464
  return new Promise((resolve, reject) => {
25112
- const child = spawn4(hook.command, {
26465
+ const child = spawn5(hook.command, {
25113
26466
  cwd: ctx.cwd,
25114
26467
  env: process.env,
25115
26468
  shell: true,
@@ -25194,7 +26547,7 @@ var blockDangerousBashHook = {
25194
26547
  };
25195
26548
 
25196
26549
  // src/hooks/builtin/protect-env-files.ts
25197
- import path16 from "node:path";
26550
+ import path20 from "node:path";
25198
26551
  var protectEnvFilesHook = {
25199
26552
  name: "protect-env-files",
25200
26553
  event: "PreToolUse",
@@ -25202,7 +26555,7 @@ var protectEnvFilesHook = {
25202
26555
  run(ctx) {
25203
26556
  if (ctx.toolName !== "write_file" && ctx.toolName !== "edit_file") return { decision: "allow" };
25204
26557
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
25205
- const filePath = typeof input2.path === "string" ? path16.resolve(ctx.cwd, input2.path) : "";
26558
+ const filePath = typeof input2.path === "string" ? path20.resolve(ctx.cwd, input2.path) : "";
25206
26559
  if (filePath && isEnvFile(filePath)) {
25207
26560
  return {
25208
26561
  decision: "block",
@@ -25239,7 +26592,7 @@ var maskSecretsHook = {
25239
26592
  };
25240
26593
 
25241
26594
  // src/hooks/builtin/inject-env-info.ts
25242
- import os9 from "node:os";
26595
+ import os13 from "node:os";
25243
26596
  var injectEnvInfoHook = {
25244
26597
  name: "inject-env-info",
25245
26598
  event: "SessionStart",
@@ -25248,7 +26601,7 @@ var injectEnvInfoHook = {
25248
26601
  decision: "allow",
25249
26602
  patch: {
25250
26603
  systemNote: [
25251
- `Environment: ${os9.type()} ${os9.release()} (${os9.platform()}/${os9.arch()})`,
26604
+ `Environment: ${os13.type()} ${os13.release()} (${os13.platform()}/${os13.arch()})`,
25252
26605
  `cwd: ${ctx.cwd}`,
25253
26606
  `provider: ${ctx.provider ?? "unknown"}`,
25254
26607
  `model: ${ctx.model ?? "unknown"}`,
@@ -25317,14 +26670,14 @@ function matchesHook(match, ctx) {
25317
26670
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
25318
26671
  const filePath = typeof input2.path === "string" ? input2.path : typeof input2.workdir === "string" ? input2.workdir : void 0;
25319
26672
  if (!filePath) return false;
25320
- if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path17.resolve(ctx.cwd, filePath)))) return false;
26673
+ if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path21.resolve(ctx.cwd, filePath)))) return false;
25321
26674
  }
25322
26675
  return true;
25323
26676
  }
25324
26677
 
25325
26678
  // src/multimodal.ts
25326
- import { readFile as readFile9, stat as stat4 } from "node:fs/promises";
25327
- import path18 from "node:path";
26679
+ import { readFile as readFile11, stat as stat6 } from "node:fs/promises";
26680
+ import path22 from "node:path";
25328
26681
  var DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
25329
26682
  async function buildUserContent(prompt, images = [], options) {
25330
26683
  if (images.length === 0) return prompt;
@@ -25343,16 +26696,16 @@ async function buildUserContent(prompt, images = [], options) {
25343
26696
  async function imageToUrl(input2, options) {
25344
26697
  if (/^https?:\/\//i.test(input2) || /^data:image\//i.test(input2)) return input2;
25345
26698
  const filePath = resolveInsideCwd(options.cwd, input2);
25346
- const info = await stat4(filePath);
26699
+ const info = await stat6(filePath);
25347
26700
  if (!info.isFile()) throw new Error(`Image input is not a file: ${input2}`);
25348
26701
  const maxImageBytes = options.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
25349
26702
  if (info.size > maxImageBytes) throw new Error(`Image is larger than ${maxImageBytes} bytes: ${input2}`);
25350
26703
  const mime = mimeFromPath(filePath);
25351
- const bytes = await readFile9(filePath);
26704
+ const bytes = await readFile11(filePath);
25352
26705
  return `data:${mime};base64,${bytes.toString("base64")}`;
25353
26706
  }
25354
26707
  function mimeFromPath(filePath) {
25355
- switch (path18.extname(filePath).toLowerCase()) {
26708
+ switch (path22.extname(filePath).toLowerCase()) {
25356
26709
  case ".jpg":
25357
26710
  case ".jpeg":
25358
26711
  return "image/jpeg";
@@ -25363,15 +26716,15 @@ function mimeFromPath(filePath) {
25363
26716
  case ".webp":
25364
26717
  return "image/webp";
25365
26718
  default:
25366
- throw new Error(`Unsupported image extension: ${path18.extname(filePath) || "(none)"}`);
26719
+ throw new Error(`Unsupported image extension: ${path22.extname(filePath) || "(none)"}`);
25367
26720
  }
25368
26721
  }
25369
26722
 
25370
26723
  // src/permissions/persistent-grants.ts
25371
- import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile7 } from "node:fs/promises";
26724
+ import { mkdir as mkdir9, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
25372
26725
  import fs8 from "node:fs";
25373
- import os10 from "node:os";
25374
- import path19 from "node:path";
26726
+ import os14 from "node:os";
26727
+ import path23 from "node:path";
25375
26728
  var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
25376
26729
  var PersistentGrantStore = class {
25377
26730
  filePath;
@@ -25402,22 +26755,22 @@ var PersistentGrantStore = class {
25402
26755
  }
25403
26756
  async #read() {
25404
26757
  if (!fs8.existsSync(this.filePath)) return { version: 1, grants: [] };
25405
- const data = JSON.parse(await readFile10(this.filePath, "utf8"));
26758
+ const data = JSON.parse(await readFile12(this.filePath, "utf8"));
25406
26759
  return {
25407
26760
  version: 1,
25408
26761
  grants: Array.isArray(data.grants) ? data.grants.filter(isGrantRecord) : []
25409
26762
  };
25410
26763
  }
25411
26764
  async #write(file) {
25412
- await mkdir7(path19.dirname(this.filePath), { recursive: true });
25413
- await writeFile7(this.filePath, `${JSON.stringify(file, null, 2)}
26765
+ await mkdir9(path23.dirname(this.filePath), { recursive: true });
26766
+ await writeFile9(this.filePath, `${JSON.stringify(file, null, 2)}
25414
26767
  `, { mode: 384 });
25415
26768
  }
25416
26769
  };
25417
26770
  function resolveGrantPath(cwd, config) {
25418
- if (config.path) return path19.resolve(cwd, config.path);
25419
- if ((config.scope ?? "project") === "user") return path19.join(os10.homedir(), ".demian", "grants.json");
25420
- return path19.join(cwd, ".demian", "grants.json");
26771
+ if (config.path) return path23.resolve(cwd, config.path);
26772
+ if ((config.scope ?? "project") === "user") return path23.join(os14.homedir(), ".demian", "grants.json");
26773
+ return path23.join(cwd, ".demian", "grants.json");
25421
26774
  }
25422
26775
  function isGrantRecord(value) {
25423
26776
  if (!value || typeof value !== "object") return false;
@@ -25850,25 +27203,25 @@ function fail(content, metadata) {
25850
27203
  }
25851
27204
 
25852
27205
  // src/tools/output.ts
25853
- import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
25854
- import path20 from "node:path";
27206
+ import { mkdir as mkdir10, writeFile as writeFile10 } from "node:fs/promises";
27207
+ import path24 from "node:path";
25855
27208
  var DEFAULT_CAP_BYTES = 32 * 1024;
25856
27209
  var HALF_PREVIEW_BYTES = 16 * 1024;
25857
27210
  async function capToolOutput(result, options) {
25858
27211
  const capBytes = options.capBytes ?? DEFAULT_CAP_BYTES;
25859
27212
  const bytes = Buffer.byteLength(result.content, "utf8");
25860
27213
  if (bytes <= capBytes) return result;
25861
- const dir = path20.join(options.cwd, ".demian", "tmp");
25862
- await mkdir8(dir, { recursive: true });
25863
- const outputPath = path20.join(dir, `output-${safeCallId(options.callId)}.txt`);
25864
- await writeFile8(outputPath, result.content, "utf8");
27214
+ const dir = path24.join(options.cwd, ".demian", "tmp");
27215
+ await mkdir10(dir, { recursive: true });
27216
+ const outputPath = path24.join(dir, `output-${safeCallId(options.callId)}.txt`);
27217
+ await writeFile10(outputPath, result.content, "utf8");
25865
27218
  return {
25866
27219
  ...result,
25867
27220
  content: [
25868
27221
  sliceUtf8(result.content, 0, HALF_PREVIEW_BYTES),
25869
27222
  `
25870
27223
 
25871
- [Full output saved to ${path20.relative(options.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
27224
+ [Full output saved to ${path24.relative(options.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
25872
27225
 
25873
27226
  `,
25874
27227
  sliceUtf8(result.content, Math.max(0, bytes - HALF_PREVIEW_BYTES), bytes)
@@ -25893,8 +27246,8 @@ function sliceUtf8(text, startByte, endByte) {
25893
27246
  }
25894
27247
 
25895
27248
  // src/tools/read-file.ts
25896
- import { open as open2, stat as stat5 } from "node:fs/promises";
25897
- import path21 from "node:path";
27249
+ import { open as open2, stat as stat7 } from "node:fs/promises";
27250
+ import path25 from "node:path";
25898
27251
 
25899
27252
  // src/tools/validation.ts
25900
27253
  function assertObject(input2, toolName) {
@@ -25964,7 +27317,7 @@ var readFileTool = {
25964
27317
  const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
25965
27318
  const offset = positiveInteger(optionalNumberField(object2, "offset"), 1, "offset");
25966
27319
  const limit = positiveInteger(optionalNumberField(object2, "limit"), DEFAULT_LIMIT, "limit");
25967
- const info = await stat5(filePath);
27320
+ const info = await stat7(filePath);
25968
27321
  if (info.isDirectory()) throw new Error(`read_file expected a file but got a directory: ${relativeToCwd(ctx.cwd, filePath)}`);
25969
27322
  if (info.size > MAX_BYTES) throw new Error(`File is larger than ${MAX_BYTES} bytes: ${relativeToCwd(ctx.cwd, filePath)}`);
25970
27323
  const sample = await readBytes(filePath, Math.min(SAMPLE_BYTES, info.size));
@@ -25978,7 +27331,7 @@ var readFileTool = {
25978
27331
  const truncated = start + sliced.length < lines.length;
25979
27332
  return ok(content + (truncated ? `
25980
27333
  ... (${lines.length - start - sliced.length} more lines)` : ""), {
25981
- path: path21.relative(ctx.cwd, filePath),
27334
+ path: path25.relative(ctx.cwd, filePath),
25982
27335
  lines: sliced.length,
25983
27336
  totalLines: lines.length,
25984
27337
  truncated
@@ -26016,8 +27369,8 @@ function looksBinary(buffer) {
26016
27369
  }
26017
27370
 
26018
27371
  // src/tools/write-file.ts
26019
- import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
26020
- import path22 from "node:path";
27372
+ import { mkdir as mkdir11, readFile as readFile13, writeFile as writeFile11 } from "node:fs/promises";
27373
+ import path26 from "node:path";
26021
27374
  var writeFileTool = {
26022
27375
  name: "write_file",
26023
27376
  description: "Create or replace a text file inside the workspace.",
@@ -26035,8 +27388,8 @@ var writeFileTool = {
26035
27388
  const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
26036
27389
  const content = stringField(object2, "content", { allowEmpty: true });
26037
27390
  const before = await readOptionalTextFile(filePath);
26038
- await mkdir9(path22.dirname(filePath), { recursive: true });
26039
- await writeFile9(filePath, content, "utf8");
27391
+ await mkdir11(path26.dirname(filePath), { recursive: true });
27392
+ await writeFile11(filePath, content, "utf8");
26040
27393
  const relative = relativeToCwd(ctx.cwd, filePath);
26041
27394
  const diff = createTextDiff(before, content, relative);
26042
27395
  return ok(`Wrote ${relativeToCwd(ctx.cwd, filePath)} (${Buffer.byteLength(content, "utf8")} bytes).`, {
@@ -26049,7 +27402,7 @@ var writeFileTool = {
26049
27402
  };
26050
27403
  async function readOptionalTextFile(filePath) {
26051
27404
  try {
26052
- return await readFile11(filePath, "utf8");
27405
+ return await readFile13(filePath, "utf8");
26053
27406
  } catch (error) {
26054
27407
  if (isNotFound(error)) return void 0;
26055
27408
  throw error;
@@ -26060,7 +27413,7 @@ function isNotFound(error) {
26060
27413
  }
26061
27414
 
26062
27415
  // src/tools/edit-file.ts
26063
- import { readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
27416
+ import { readFile as readFile14, writeFile as writeFile12 } from "node:fs/promises";
26064
27417
  var editFileTool = {
26065
27418
  name: "edit_file",
26066
27419
  description: "Replace an exact string in a text file inside the workspace.",
@@ -26082,12 +27435,12 @@ var editFileTool = {
26082
27435
  const newString = stringField(object2, "newString", { allowEmpty: true });
26083
27436
  const replaceAll = optionalBooleanField(object2, "replaceAll") ?? false;
26084
27437
  if (oldString === newString) throw new Error("oldString and newString must be different");
26085
- const before = await readFile12(filePath, "utf8");
27438
+ const before = await readFile14(filePath, "utf8");
26086
27439
  const matches = countMatches(before, oldString);
26087
27440
  if (matches === 0) throw new Error("oldString was not found");
26088
27441
  if (matches > 1 && !replaceAll) throw new Error(`oldString matched ${matches} times; set replaceAll=true to replace all matches`);
26089
27442
  const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString);
26090
- await writeFile10(filePath, after, "utf8");
27443
+ await writeFile12(filePath, after, "utf8");
26091
27444
  const relative = relativeToCwd(ctx.cwd, filePath);
26092
27445
  const diff = createTextDiff(before, after, relative);
26093
27446
  return ok(`Edited ${relative} (${replaceAll ? matches : 1} replacement${matches === 1 ? "" : "s"}).`, {
@@ -26110,8 +27463,8 @@ function countMatches(text, needle) {
26110
27463
  }
26111
27464
 
26112
27465
  // src/tools/bash.ts
26113
- import { spawn as spawn5 } from "node:child_process";
26114
- import path24 from "node:path";
27466
+ import { spawn as spawn6 } from "node:child_process";
27467
+ import path28 from "node:path";
26115
27468
 
26116
27469
  // src/sandbox/env-only.ts
26117
27470
  function buildEnvOnlyLaunch(command, config) {
@@ -26171,7 +27524,7 @@ function bwrapPath() {
26171
27524
 
26172
27525
  // src/sandbox/macos.ts
26173
27526
  import { existsSync as existsSync4 } from "node:fs";
26174
- import path23 from "node:path";
27527
+ import path27 from "node:path";
26175
27528
  function canUseMacOSSandbox() {
26176
27529
  return process.platform === "darwin" && existsSync4("/usr/bin/sandbox-exec");
26177
27530
  }
@@ -26197,7 +27550,7 @@ function macosProfile(cwd, config) {
26197
27550
  }
26198
27551
  if (mode === "workspace-write") {
26199
27552
  lines.push("(deny file-write*)");
26200
- lines.push(`(allow file-write* (subpath "${escapeProfilePath(path23.resolve(cwd))}"))`);
27553
+ lines.push(`(allow file-write* (subpath "${escapeProfilePath(path27.resolve(cwd))}"))`);
26201
27554
  lines.push('(allow file-write* (subpath "/tmp"))');
26202
27555
  lines.push('(allow file-write* (subpath "/private/tmp"))');
26203
27556
  lines.push('(allow file-write* (subpath "/private/var/folders"))');
@@ -26259,7 +27612,7 @@ ${result.stderr}` : ""
26259
27612
  ].filter(Boolean).join("\n");
26260
27613
  return ok(content, {
26261
27614
  command,
26262
- workdir: path24.relative(ctx.cwd, workdir) || ".",
27615
+ workdir: path28.relative(ctx.cwd, workdir) || ".",
26263
27616
  exitCode: result.exitCode,
26264
27617
  signal: result.signal,
26265
27618
  timedOut: result.timedOut,
@@ -26270,7 +27623,7 @@ ${result.stderr}` : ""
26270
27623
  function runCommand(command, cwd, timeoutMs, signal, sandbox = { mode: "workspace-write" }) {
26271
27624
  return new Promise((resolve, reject) => {
26272
27625
  const launch = buildSandboxLaunch(command, cwd, sandbox);
26273
- const child = spawn5(launch.command, launch.args, {
27626
+ const child = spawn6(launch.command, launch.args, {
26274
27627
  cwd,
26275
27628
  env: { ...process.env, ...launch.env },
26276
27629
  stdio: ["ignore", "pipe", "pipe"]
@@ -26310,9 +27663,9 @@ function runCommand(command, cwd, timeoutMs, signal, sandbox = { mode: "workspac
26310
27663
  }
26311
27664
 
26312
27665
  // src/tools/grep.ts
26313
- import { spawn as spawn6 } from "node:child_process";
26314
- import { readdir as readdir2, readFile as readFile13, stat as stat6 } from "node:fs/promises";
26315
- import path25 from "node:path";
27666
+ import { spawn as spawn7 } from "node:child_process";
27667
+ import { readdir as readdir2, readFile as readFile15, stat as stat8 } from "node:fs/promises";
27668
+ import path29 from "node:path";
26316
27669
  var MAX_MATCHES = 200;
26317
27670
  var grepTool = {
26318
27671
  name: "grep",
@@ -26362,7 +27715,7 @@ function runRg(cwd, base, pattern, glob, signal) {
26362
27715
  ];
26363
27716
  if (glob) args.push("--glob", glob);
26364
27717
  args.push(pattern, base);
26365
- const child = spawn6("rg", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
27718
+ const child = spawn7("rg", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
26366
27719
  const stdout = [];
26367
27720
  const stderr = [];
26368
27721
  const abort = () => child.kill("SIGTERM");
@@ -26387,7 +27740,7 @@ function runRg(cwd, base, pattern, glob, signal) {
26387
27740
  }
26388
27741
  function rewriteRgPath(cwd, line) {
26389
27742
  const [file, rest] = splitFirst(line, ":");
26390
- if (!path25.isAbsolute(file)) return line;
27743
+ if (!path29.isAbsolute(file)) return line;
26391
27744
  return `${relativeToCwd(cwd, file)}:${rest}`;
26392
27745
  }
26393
27746
  function splitFirst(value, delimiter) {
@@ -26402,13 +27755,13 @@ async function fallbackSearch(cwd, base, pattern, glob) {
26402
27755
  if (out.length > MAX_MATCHES) return;
26403
27756
  const relative = relativeToCwd(cwd, item);
26404
27757
  if (isIgnoredPath(relative)) return;
26405
- const info = await stat6(item);
27758
+ const info = await stat8(item);
26406
27759
  if (info.isDirectory()) {
26407
- for (const entry of await readdir2(item)) await walk2(path25.join(item, entry));
27760
+ for (const entry of await readdir2(item)) await walk2(path29.join(item, entry));
26408
27761
  return;
26409
27762
  }
26410
27763
  if (glob && !matchGlob(glob, relative)) return;
26411
- const buffer = await readFile13(item);
27764
+ const buffer = await readFile15(item);
26412
27765
  if (buffer.includes(0)) return;
26413
27766
  const lines = buffer.toString("utf8").split(/\r?\n/);
26414
27767
  for (let i = 0; i < lines.length; i++) {
@@ -26422,8 +27775,8 @@ async function fallbackSearch(cwd, base, pattern, glob) {
26422
27775
  }
26423
27776
 
26424
27777
  // src/tools/glob.ts
26425
- import { readdir as readdir3, stat as stat7 } from "node:fs/promises";
26426
- import path26 from "node:path";
27778
+ import { readdir as readdir3, stat as stat9 } from "node:fs/promises";
27779
+ import path30 from "node:path";
26427
27780
  var MAX_PATHS = 1e3;
26428
27781
  var globTool = {
26429
27782
  name: "glob",
@@ -26455,10 +27808,10 @@ async function collectPaths(cwd, root, pattern) {
26455
27808
  async function walk2(dir) {
26456
27809
  const entries = await readdir3(dir, { withFileTypes: true });
26457
27810
  for (const entry of entries) {
26458
- const absolute = path26.join(dir, entry.name);
27811
+ const absolute = path30.join(dir, entry.name);
26459
27812
  const relative = relativeToCwd(cwd, absolute);
26460
27813
  if (isIgnoredPath(relative)) continue;
26461
- const info = await stat7(absolute);
27814
+ const info = await stat9(absolute);
26462
27815
  if (matchGlob(pattern, relative)) out.push({ relative, mtimeMs: info.mtimeMs });
26463
27816
  if (entry.isDirectory()) await walk2(absolute);
26464
27817
  }
@@ -26596,7 +27949,7 @@ async function runBraveSearch(config, options) {
26596
27949
  if (config.country) url.searchParams.set("country", config.country);
26597
27950
  if (config.searchLang) url.searchParams.set("search_lang", config.searchLang);
26598
27951
  if (config.safeSearch) url.searchParams.set("safesearch", config.safeSearch);
26599
- const json = await fetchJson(url, {
27952
+ const json = await fetchJson2(url, {
26600
27953
  method: "GET",
26601
27954
  headers: {
26602
27955
  accept: "application/json",
@@ -26615,7 +27968,7 @@ async function runTavilySearch(config, options) {
26615
27968
  if (!apiKey.ok) return apiKey;
26616
27969
  const maxResults = Math.min(positiveInteger(options.maxResults, config.maxResults ?? 5, "maxResults"), TAVILY_MAX_RESULTS);
26617
27970
  const endpoint = config.endpoint ?? defaultConfig.webSearch.providers.tavily.endpoint ?? "https://api.tavily.com/search";
26618
- const json = await fetchJson(endpoint, {
27971
+ const json = await fetchJson2(endpoint, {
26619
27972
  method: "POST",
26620
27973
  headers: {
26621
27974
  "content-type": "application/json",
@@ -26641,7 +27994,7 @@ async function runExaSearch(config, options) {
26641
27994
  if (!apiKey.ok) return apiKey;
26642
27995
  const numResults = Math.min(positiveInteger(options.maxResults, config.numResults ?? 5, "maxResults"), EXA_MAX_RESULTS);
26643
27996
  const endpoint = config.endpoint ?? defaultConfig.webSearch.providers.exa.endpoint ?? "https://api.exa.ai/search";
26644
- const json = await fetchJson(endpoint, {
27997
+ const json = await fetchJson2(endpoint, {
26645
27998
  method: "POST",
26646
27999
  headers: {
26647
28000
  "content-type": "application/json",
@@ -26670,7 +28023,7 @@ function resolveApiKey(provider, config) {
26670
28023
  if (value) return { ok: true, value };
26671
28024
  return toolFail(`Missing API key for web_search provider "${provider}". Set webSearch.providers.${provider}.apiKey or set ${config.apiKeyEnv ?? "the configured apiKeyEnv"}.`);
26672
28025
  }
26673
- async function fetchJson(url, options) {
28026
+ async function fetchJson2(url, options) {
26674
28027
  const { fetcher, provider, ...init } = options;
26675
28028
  const response = await fetcher(url, init);
26676
28029
  const text = await response.text();
@@ -26688,10 +28041,10 @@ function normalizeBraveResults(raw, limit) {
26688
28041
  return (data.web?.results ?? []).slice(0, limit).map((item) => {
26689
28042
  const result = item;
26690
28043
  return {
26691
- title: stringValue4(result.title) ?? "Untitled",
26692
- url: stringValue4(result.url) ?? "",
26693
- snippet: cleanSnippet(stringValue4(result.description) ?? stringValue4(result.snippet)),
26694
- publishedDate: stringValue4(result.age)
28044
+ title: stringValue5(result.title) ?? "Untitled",
28045
+ url: stringValue5(result.url) ?? "",
28046
+ snippet: cleanSnippet(stringValue5(result.description) ?? stringValue5(result.snippet)),
28047
+ publishedDate: stringValue5(result.age)
26695
28048
  };
26696
28049
  }).filter((item) => item.url.length > 0);
26697
28050
  }
@@ -26700,9 +28053,9 @@ function normalizeTavilyResults(raw, limit) {
26700
28053
  return (data.results ?? []).slice(0, limit).map((item) => {
26701
28054
  const result = item;
26702
28055
  return {
26703
- title: stringValue4(result.title) ?? "Untitled",
26704
- url: stringValue4(result.url) ?? "",
26705
- snippet: cleanSnippet(stringValue4(result.content) ?? stringValue4(result.raw_content)),
28056
+ title: stringValue5(result.title) ?? "Untitled",
28057
+ url: stringValue5(result.url) ?? "",
28058
+ snippet: cleanSnippet(stringValue5(result.content) ?? stringValue5(result.raw_content)),
26706
28059
  score: numberValue3(result.score)
26707
28060
  };
26708
28061
  }).filter((item) => item.url.length > 0);
@@ -26712,10 +28065,10 @@ function normalizeExaResults(raw, limit) {
26712
28065
  return (data.results ?? []).slice(0, limit).map((item) => {
26713
28066
  const result = item;
26714
28067
  return {
26715
- title: stringValue4(result.title) ?? "Untitled",
26716
- url: stringValue4(result.url) ?? "",
28068
+ title: stringValue5(result.title) ?? "Untitled",
28069
+ url: stringValue5(result.url) ?? "",
26717
28070
  snippet: exaSnippet(result),
26718
- publishedDate: stringValue4(result.publishedDate),
28071
+ publishedDate: stringValue5(result.publishedDate),
26719
28072
  score: numberValue3(result.score)
26720
28073
  };
26721
28074
  }).filter((item) => item.url.length > 0);
@@ -26726,7 +28079,7 @@ function exaSnippet(result) {
26726
28079
  const joined = highlights.map((item) => typeof item === "string" ? item : "").filter(Boolean).join(" ");
26727
28080
  if (joined) return cleanSnippet(joined);
26728
28081
  }
26729
- return cleanSnippet(stringValue4(result.text) ?? stringValue4(result.summary));
28082
+ return cleanSnippet(stringValue5(result.text) ?? stringValue5(result.summary));
26730
28083
  }
26731
28084
  function searchResult(provider, query, results, raw) {
26732
28085
  const content = [
@@ -26750,7 +28103,7 @@ function formatResult(index, result) {
26750
28103
  if (result.snippet) lines.push(` Snippet: ${result.snippet}`);
26751
28104
  return lines.join("\n");
26752
28105
  }
26753
- function stringValue4(value) {
28106
+ function stringValue5(value) {
26754
28107
  return typeof value === "string" && value.length > 0 ? value : void 0;
26755
28108
  }
26756
28109
  function numberValue3(value) {
@@ -27457,14 +28810,14 @@ async function runExecutionSession(options) {
27457
28810
  }
27458
28811
 
27459
28812
  // src/goals/lock.ts
27460
- import { mkdir as mkdir11, open as open3, rm as rm2 } from "node:fs/promises";
27461
- import path28 from "node:path";
28813
+ import { mkdir as mkdir13, open as open3, rm as rm2 } from "node:fs/promises";
28814
+ import path32 from "node:path";
27462
28815
 
27463
28816
  // src/goals/storage.ts
27464
28817
  import { createHash as createHash3 } from "node:crypto";
27465
- import { mkdir as mkdir10, readFile as readFile14, rename as rename4, writeFile as writeFile11 } from "node:fs/promises";
28818
+ import { mkdir as mkdir12, readFile as readFile16, rename as rename6, writeFile as writeFile13 } from "node:fs/promises";
27466
28819
  import fs9 from "node:fs";
27467
- import path27 from "node:path";
28820
+ import path31 from "node:path";
27468
28821
  var GoalStore = class {
27469
28822
  cwd;
27470
28823
  dir;
@@ -27475,12 +28828,12 @@ var GoalStore = class {
27475
28828
  this.cwd = cwd;
27476
28829
  this.scope = normalizeGoalStoreScope(options.scope);
27477
28830
  this.dir = goalStoreDir(cwd, this.scope);
27478
- this.activePath = path27.join(this.dir, "active.json");
27479
- this.archiveDir = path27.join(this.dir, "archive");
28831
+ this.activePath = path31.join(this.dir, "active.json");
28832
+ this.archiveDir = path31.join(this.dir, "archive");
27480
28833
  }
27481
28834
  async loadActive() {
27482
28835
  if (!fs9.existsSync(this.activePath)) return void 0;
27483
- const text = await readFile14(this.activePath, "utf8");
28836
+ const text = await readFile16(this.activePath, "utf8");
27484
28837
  try {
27485
28838
  return JSON.parse(text);
27486
28839
  } catch {
@@ -27489,36 +28842,36 @@ var GoalStore = class {
27489
28842
  }
27490
28843
  }
27491
28844
  async saveActive(state) {
27492
- await mkdir10(this.dir, { recursive: true });
27493
- const tmp = path27.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
27494
- await writeFile11(tmp, `${JSON.stringify(state, null, 2)}
28845
+ await mkdir12(this.dir, { recursive: true });
28846
+ const tmp = path31.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
28847
+ await writeFile13(tmp, `${JSON.stringify(state, null, 2)}
27495
28848
  `, "utf8");
27496
- await rename4(tmp, this.activePath);
28849
+ await rename6(tmp, this.activePath);
27497
28850
  }
27498
28851
  async archiveActive(status = "cleared") {
27499
28852
  const state = await this.loadActive();
27500
28853
  if (!state) return void 0;
27501
- await mkdir10(this.archiveDir, { recursive: true });
28854
+ await mkdir12(this.archiveDir, { recursive: true });
27502
28855
  const archived = {
27503
28856
  ...state,
27504
28857
  status,
27505
28858
  updatedAt: Date.now()
27506
28859
  };
27507
- await writeFile11(path27.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
28860
+ await writeFile13(path31.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
27508
28861
  `, "utf8");
27509
28862
  await fs9.promises.rm(this.activePath, { force: true });
27510
28863
  return archived;
27511
28864
  }
27512
28865
  async backupCorrupted(text) {
27513
- await mkdir10(this.dir, { recursive: true });
27514
- const backup = path27.join(this.dir, `active.corrupt.${Date.now()}.json`);
27515
- await writeFile11(backup, text, "utf8");
28866
+ await mkdir12(this.dir, { recursive: true });
28867
+ const backup = path31.join(this.dir, `active.corrupt.${Date.now()}.json`);
28868
+ await writeFile13(backup, text, "utf8");
27516
28869
  await fs9.promises.rm(this.activePath, { force: true });
27517
28870
  }
27518
28871
  };
27519
28872
  function goalStoreDir(cwd, scope) {
27520
28873
  const safeScope = normalizeGoalStoreScope(scope);
27521
- return safeScope ? path27.join(cwd, ".demian", "goals", "sessions", safeScope) : path27.join(cwd, ".demian", "goals");
28874
+ return safeScope ? path31.join(cwd, ".demian", "goals", "sessions", safeScope) : path31.join(cwd, ".demian", "goals");
27522
28875
  }
27523
28876
  function normalizeGoalStoreScope(scope) {
27524
28877
  const trimmed = scope?.trim();
@@ -27532,10 +28885,10 @@ function normalizeGoalStoreScope(scope) {
27532
28885
  var GoalLock = class {
27533
28886
  path;
27534
28887
  constructor(cwd, scope) {
27535
- this.path = path28.join(goalStoreDir(cwd, scope), "active.lock");
28888
+ this.path = path32.join(goalStoreDir(cwd, scope), "active.lock");
27536
28889
  }
27537
28890
  async acquire() {
27538
- await mkdir11(path28.dirname(this.path), { recursive: true });
28891
+ await mkdir13(path32.dirname(this.path), { recursive: true });
27539
28892
  const handle = await open3(this.path, "wx").catch((error) => {
27540
28893
  const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
27541
28894
  if (code === "EEXIST") throw new Error("Another goal is already active in this workspace.");
@@ -28065,7 +29418,7 @@ function readString(input2, key) {
28065
29418
  }
28066
29419
 
28067
29420
  // src/external-runtime/session-map.ts
28068
- import crypto5 from "node:crypto";
29421
+ import crypto6 from "node:crypto";
28069
29422
  var ClaudeCodeSessionMap = class {
28070
29423
  #sessions = /* @__PURE__ */ new Map();
28071
29424
  get(key) {
@@ -28118,13 +29471,13 @@ function normalizeInstruction(value) {
28118
29471
  return value.replace(/\r\n/g, "\n").trim().split("\n").map((line) => line.replace(/[ \t]+$/g, "")).join("\n");
28119
29472
  }
28120
29473
  function sha256(value) {
28121
- return crypto5.createHash("sha256").update(value).digest("hex").slice(0, 16);
29474
+ return crypto6.createHash("sha256").update(value).digest("hex").slice(0, 16);
28122
29475
  }
28123
29476
 
28124
29477
  // src/root-session.ts
28125
29478
  import { cp, mkdtemp, rm as rm3 } from "node:fs/promises";
28126
- import os11 from "node:os";
28127
- import path29 from "node:path";
29479
+ import os15 from "node:os";
29480
+ import path33 from "node:path";
28128
29481
 
28129
29482
  // src/permissions/presets.ts
28130
29483
  var PERMISSION_PRESETS = ["deny", "ask", "auto", "full"];
@@ -28968,8 +30321,8 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
28968
30321
  }
28969
30322
  }
28970
30323
  async createCoworkIsolatedWorkspace(groupId, memberId) {
28971
- const root = await mkdtemp(path29.join(os11.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
28972
- const cwd = path29.join(root, "workspace");
30324
+ const root = await mkdtemp(path33.join(os15.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
30325
+ const cwd = path33.join(root, "workspace");
28973
30326
  await cp(this.#options.cwd, cwd, {
28974
30327
  recursive: true,
28975
30328
  filter: (source) => shouldCopyIntoCoworkWorkspace(this.#options.cwd, source)
@@ -29117,7 +30470,7 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
29117
30470
  const denyTools = ["write_file", "edit_file", "bash", "claudecode.Edit", "claudecode.Write", "claudecode.MultiEdit", "claudecode.Bash"];
29118
30471
  return this.withPermissionPreset({
29119
30472
  ...agent,
29120
- provider: agent.provider?.profile === "claudecode-subagent" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
30473
+ provider: agent.provider?.profile === "claudecode" && agent.name === "claudecode-explorer" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
29121
30474
  tools: agent.tools.filter((tool) => readTools.has(tool)),
29122
30475
  permissions: [
29123
30476
  ...agent.permissions.filter((rule) => !denyTools.includes(rule.tool)),
@@ -29164,13 +30517,14 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
29164
30517
  return lines.join("\n");
29165
30518
  }
29166
30519
  resolveInvocationBackend(profileName, modelOverride, agent) {
29167
- const providerConfig = resolveProviderConfig(this.#options.config, profileName);
30520
+ const runtime = resolveProviderRuntimeConfig(this.#options.config, { providerName: profileName, model: modelOverride });
30521
+ const providerConfig = runtime.providerConfig;
29168
30522
  if (this.#options.resolveExecutionBackend) return this.#options.resolveExecutionBackend(profileName, providerConfig, modelOverride);
29169
30523
  if (this.#options.resolveProvider) {
29170
- const resolved = this.#options.resolveProvider(profileName, providerConfig, modelOverride);
30524
+ const resolved = this.#options.resolveProvider(profileName, providerConfig, runtime.model);
29171
30525
  return { kind: "provider", ...resolved };
29172
30526
  }
29173
- return resolveExecutionBackend(providerConfig, { model: modelOverride, config: this.#options.config, agent });
30527
+ return resolveExecutionBackend(providerConfig, { model: runtime.model, config: this.#options.config, agent });
29174
30528
  }
29175
30529
  loadAgentSession(agent, providerProfile, model, sessionScope, cwd = this.#options.cwd) {
29176
30530
  const key = `${this.id}:${sessionScope ?? "delegate"}:${agent.name}:${providerProfile}:${model}:${cwd}`;
@@ -29272,17 +30626,17 @@ function findDependencyCycle(group) {
29272
30626
  const byId = new Map(group.map((member) => [member.memberId, member]));
29273
30627
  const visiting = /* @__PURE__ */ new Set();
29274
30628
  const visited = /* @__PURE__ */ new Set();
29275
- const path32 = [];
30629
+ const path36 = [];
29276
30630
  const visit = (id) => {
29277
- if (visiting.has(id)) return [...path32.slice(path32.indexOf(id)), id];
30631
+ if (visiting.has(id)) return [...path36.slice(path36.indexOf(id)), id];
29278
30632
  if (visited.has(id)) return void 0;
29279
30633
  visiting.add(id);
29280
- path32.push(id);
30634
+ path36.push(id);
29281
30635
  for (const dep of byId.get(id)?.dependsOn ?? []) {
29282
30636
  const cycle = visit(dep);
29283
30637
  if (cycle) return cycle;
29284
30638
  }
29285
- path32.pop();
30639
+ path36.pop();
29286
30640
  visiting.delete(id);
29287
30641
  visited.add(id);
29288
30642
  return void 0;
@@ -29323,7 +30677,7 @@ function scopeStaticPrefix(pattern) {
29323
30677
  return prefix.replace(/\/+$/, "").split("/").filter(Boolean).join("/") || ".";
29324
30678
  }
29325
30679
  function shouldCopyIntoCoworkWorkspace(root, source) {
29326
- const relative = path29.relative(root, source).split(path29.sep).join("/");
30680
+ const relative = path33.relative(root, source).split(path33.sep).join("/");
29327
30681
  if (!relative) return true;
29328
30682
  const parts = relative.split("/");
29329
30683
  return !parts.includes(".git") && !parts.includes("node_modules") && !parts.includes(".demian");
@@ -29718,52 +31072,114 @@ function finitePositiveInteger(value) {
29718
31072
  }
29719
31073
 
29720
31074
  // src/ui/plain/interactive.ts
29721
- import readline3 from "node:readline";
31075
+ import readline4 from "node:readline";
29722
31076
 
29723
31077
  // src/ui/settings.ts
29724
- function providerOptions(config) {
29725
- return Object.entries(config.providers).filter(([, provider]) => !provider.hidden && !(provider.type === "claudecode" && provider.runtime === "cli")).map(([name, provider]) => ({
29726
- name,
29727
- model: provider.model,
29728
- models: provider.models?.length ? [...new Set([provider.model, ...provider.models].filter((item) => typeof item === "string" && item.trim()))] : [provider.model],
29729
- type: provider.type,
29730
- runtime: provider.type === "claudecode" ? provider.runtime : void 0,
29731
- permissionMode: provider.type === "claudecode" ? provider.permissionMode : void 0,
29732
- ga: provider.type === "claudecode" ? claudeCodeGaEnabled(provider) : void 0
29733
- }));
31078
+ function providerOptions(config, catalogs = {}) {
31079
+ const orderedNames = sortProviderNames(Object.keys(config.providers));
31080
+ const order = new Map(orderedNames.map((name, index) => [name, index]));
31081
+ return orderedNames.map((name) => [name, config.providers[name]]).filter(([, provider]) => provider && !provider.hidden && !(provider.type === "claudecode" && provider.runtime === "cli")).map(([name, provider]) => {
31082
+ const catalog = catalogs[name];
31083
+ const available = providerCatalogAvailable(catalog);
31084
+ const profiles = catalog?.models.length ? catalog.models.map((model) => ({
31085
+ name: model.name,
31086
+ displayName: model.displayName ?? model.name,
31087
+ model: model.model,
31088
+ description: model.description,
31089
+ source: model.source,
31090
+ isDefault: model.isDefault
31091
+ })) : providerModelProfiles(name, provider);
31092
+ const modelProfiles = profiles.map((profile) => {
31093
+ const displayName = profile.displayName ?? profile.name ?? profile.model;
31094
+ return {
31095
+ ...profile,
31096
+ label: available ? displayName : unavailableLabel(displayName),
31097
+ available
31098
+ };
31099
+ });
31100
+ const models = modelProfiles.length ? [...new Set(modelProfiles.map((item) => item.model).filter((item) => typeof item === "string" && item.trim()))] : providerModelOptions(name, provider);
31101
+ const defaultModel = defaultModelForProvider(name, provider);
31102
+ return {
31103
+ name,
31104
+ label: available ? name : unavailableLabel(name),
31105
+ model: defaultModel,
31106
+ modelLabel: available || !defaultModel ? defaultModel : unavailableLabel(defaultModel),
31107
+ models,
31108
+ modelProfiles,
31109
+ type: provider.type,
31110
+ runtime: provider.type === "claudecode" ? provider.runtime : void 0,
31111
+ permissionMode: provider.type === "claudecode" ? provider.permissionMode : void 0,
31112
+ ga: provider.type === "claudecode" ? claudeCodeGaEnabled(provider) : void 0,
31113
+ available,
31114
+ catalog: catalog ? { status: catalog.status, source: catalog.source, message: catalog.message } : void 0
31115
+ };
31116
+ }).sort((left, right) => {
31117
+ if (left.available !== right.available) return left.available ? -1 : 1;
31118
+ return (order.get(left.name) ?? 0) - (order.get(right.name) ?? 0);
31119
+ });
29734
31120
  }
31121
+ var MISSING_SELECTION_PLACEHOLDER = "-";
29735
31122
  function createInteractiveModelSelection(config, flags = {}, saved) {
29736
31123
  const savedProviderName = saved?.providerName === "claudecode-plan" ? "claudecode" : saved?.providerName;
29737
- const savedProviderConfig = savedProviderName ? config.providers[savedProviderName] : void 0;
29738
- const savedProvider = savedProviderName && savedProviderConfig && !savedProviderConfig.hidden ? savedProviderName : void 0;
29739
- const providerName = flags.provider ?? savedProvider ?? config.defaultProvider;
29740
- const providerConfig = resolveProviderConfig(config, providerName);
29741
- const usesSavedProvider = !flags.provider && providerName === savedProvider;
31124
+ const savedProviderExists = savedProviderName && config.providers[savedProviderName] && !config.providers[savedProviderName]?.hidden;
31125
+ if (saved?.providerName && !flags.provider && !savedProviderExists) {
31126
+ return {
31127
+ providerName: saved.providerName,
31128
+ providerSource: "saved",
31129
+ model: MISSING_SELECTION_PLACEHOLDER,
31130
+ modelSource: "saved"
31131
+ };
31132
+ }
31133
+ const providerName = flags.provider ?? (savedProviderExists ? savedProviderName : config.defaultProvider);
31134
+ let providerConfig;
31135
+ try {
31136
+ providerConfig = resolveProviderConfig(config, providerName);
31137
+ } catch {
31138
+ return {
31139
+ providerName,
31140
+ providerSource: flags.provider ? "flag" : "config",
31141
+ model: MISSING_SELECTION_PLACEHOLDER,
31142
+ modelSource: "config"
31143
+ };
31144
+ }
31145
+ const usesSavedProvider = !flags.provider && savedProviderExists && providerName === savedProviderName;
29742
31146
  const savedModel = usesSavedProvider && saved?.model ? saved.model : void 0;
31147
+ const model = flags.model ?? savedModel ?? defaultModelForProvider(providerName, providerConfig);
29743
31148
  return {
29744
31149
  providerName,
29745
31150
  providerSource: flags.provider ? "flag" : usesSavedProvider ? "saved" : "config",
29746
- model: flags.model ?? savedModel ?? providerConfig.model,
29747
- modelSource: flags.model ? "flag" : savedModel ? "saved" : "config"
31151
+ model,
31152
+ modelSource: flags.model ? "flag" : savedModel ? "saved" : "config",
31153
+ modelProfileName: providerModelProfiles(providerName, providerConfig).find((profile) => profile.model === model || profile.displayName === model || profile.name === model)?.name
29748
31154
  };
29749
31155
  }
29750
31156
  function selectProvider(config, selection, providerName) {
29751
31157
  const providerConfig = resolveProviderConfig(config, providerName);
31158
+ const profiles = providerModelProfiles(providerName, providerConfig);
31159
+ const profile = profiles[0];
29752
31160
  return {
29753
31161
  ...selection,
29754
31162
  providerName,
29755
31163
  providerSource: "interactive",
29756
- model: providerConfig.model,
29757
- modelSource: "config"
31164
+ model: profile?.model ?? defaultModelForProvider(providerName, providerConfig),
31165
+ modelSource: "config",
31166
+ modelProfileName: profile?.name
29758
31167
  };
29759
31168
  }
29760
31169
  function selectModel(selection, model) {
29761
31170
  return {
29762
31171
  ...selection,
29763
31172
  model,
29764
- modelSource: "interactive"
31173
+ modelSource: "interactive",
31174
+ modelProfileName: void 0
29765
31175
  };
29766
31176
  }
31177
+ function providerCatalogAvailable(catalog) {
31178
+ return catalog ? catalog.status === "ready" && catalog.models.length > 0 : true;
31179
+ }
31180
+ function unavailableLabel(value) {
31181
+ return `(n/a) ${value}`;
31182
+ }
29767
31183
 
29768
31184
  // src/ui/plain/interactive.ts
29769
31185
  var PlainInteractivePrompt = class {
@@ -29775,7 +31191,7 @@ var PlainInteractivePrompt = class {
29775
31191
  #exitRequested = false;
29776
31192
  constructor(input2 = process.stdin, output2 = process.stderr) {
29777
31193
  this.#output = output2;
29778
- this.#rl = readline3.createInterface({ input: input2, output: output2 });
31194
+ this.#rl = readline4.createInterface({ input: input2, output: output2 });
29779
31195
  this.#rl.on("line", (line) => this.#handleLine(line));
29780
31196
  }
29781
31197
  get exitRequested() {
@@ -29872,13 +31288,13 @@ async function askModel(promptUi, selection) {
29872
31288
  }
29873
31289
 
29874
31290
  // src/ui/preferences.ts
29875
- import { mkdir as mkdir12, readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
31291
+ import { mkdir as mkdir14, readFile as readFile17, writeFile as writeFile14 } from "node:fs/promises";
29876
31292
  import fs10 from "node:fs";
29877
- import path30 from "node:path";
31293
+ import path34 from "node:path";
29878
31294
  var UiPreferenceStore = class {
29879
31295
  filePath;
29880
31296
  constructor(cwd, filePath) {
29881
- this.filePath = filePath ? path30.resolve(cwd, filePath) : path30.join(cwd, ".demian", "preferences.json");
31297
+ this.filePath = filePath ? path34.resolve(cwd, filePath) : path34.join(cwd, ".demian", "preferences.json");
29882
31298
  }
29883
31299
  async load() {
29884
31300
  if (!fs10.existsSync(this.filePath)) return void 0;
@@ -29896,13 +31312,13 @@ var UiPreferenceStore = class {
29896
31312
  model: selection.model,
29897
31313
  updatedAt: Date.now()
29898
31314
  };
29899
- await mkdir12(path30.dirname(this.filePath), { recursive: true });
29900
- await writeFile12(this.filePath, `${JSON.stringify(file, null, 2)}
31315
+ await mkdir14(path34.dirname(this.filePath), { recursive: true });
31316
+ await writeFile14(this.filePath, `${JSON.stringify(file, null, 2)}
29901
31317
  `, { mode: 384 });
29902
31318
  }
29903
31319
  async #read() {
29904
31320
  try {
29905
- return JSON.parse(await readFile15(this.filePath, "utf8"));
31321
+ return JSON.parse(await readFile17(this.filePath, "utf8"));
29906
31322
  } catch {
29907
31323
  return void 0;
29908
31324
  }
@@ -29918,6 +31334,8 @@ function preferenceKey(selection) {
29918
31334
 
29919
31335
  // src/cli.ts
29920
31336
  async function main(argv = process.argv.slice(2)) {
31337
+ const configCommandResult = await maybeRunConfigCommand(argv);
31338
+ if (configCommandResult !== void 0) return configCommandResult;
29921
31339
  const doctorResult = await maybeRunDoctorCommand(argv);
29922
31340
  if (doctorResult !== void 0) return doctorResult;
29923
31341
  const flags = parseArgs(argv);
@@ -29925,7 +31343,8 @@ async function main(argv = process.argv.slice(2)) {
29925
31343
  printHelp();
29926
31344
  return 0;
29927
31345
  }
29928
- const cwd = path31.resolve(flags.cwd ?? process.cwd());
31346
+ if (!flags.noWizard) await runFirstRunWizard().catch(() => void 0);
31347
+ const cwd = path35.resolve(flags.cwd ?? process.cwd());
29929
31348
  const config = await loadConfig({ cwd, configPath: flags.configPath });
29930
31349
  const agentName = flags.agent ?? config.defaultAgent;
29931
31350
  const preferenceStore = new UiPreferenceStore(cwd);
@@ -30014,9 +31433,9 @@ async function runPlainTask(options) {
30014
31433
  config: config.goals,
30015
31434
  eventBus,
30016
31435
  titleGenerator: async (input2) => {
30017
- const providerConfig2 = resolveProviderConfig(config, selection.providerName);
30018
- const resolved = resolveExecutionBackend(providerConfig2, {
30019
- model: selection.model,
31436
+ const runtime2 = resolveProviderRuntimeConfig(config, selection);
31437
+ const resolved = resolveExecutionBackend(runtime2.providerConfig, {
31438
+ model: runtime2.model,
30020
31439
  onRetry: (event) => eventBus.emit({
30021
31440
  type: "provider.retry",
30022
31441
  sessionId: "goal-title",
@@ -30106,10 +31525,10 @@ async function runPlainTask(options) {
30106
31525
  `);
30107
31526
  return { history: interactiveHistoryFromRunMessages(result2.messages), lastExternalSessionKey: options.lastExternalSessionKey };
30108
31527
  }
30109
- const providerConfig = resolveProviderConfig(config, providerName);
30110
31528
  const runtimeAgent = activeGoal ? withGoalTools(agent) : agent;
30111
- const backend = resolveExecutionBackend(providerConfig, {
30112
- model: selection.model,
31529
+ const runtime = resolveProviderRuntimeConfig(config, selection);
31530
+ const backend = resolveExecutionBackend(runtime.providerConfig, {
31531
+ model: runtime.model,
30113
31532
  config,
30114
31533
  agent: runtimeAgent,
30115
31534
  onRetry: (event) => eventBus.emit({
@@ -30213,6 +31632,10 @@ function parseArgs(argv) {
30213
31632
  out.transcript = false;
30214
31633
  continue;
30215
31634
  }
31635
+ if (arg === "--no-wizard") {
31636
+ out.noWizard = true;
31637
+ continue;
31638
+ }
30216
31639
  if (arg === "--goal") {
30217
31640
  out.goal = true;
30218
31641
  continue;
@@ -30287,6 +31710,10 @@ function printHelp() {
30287
31710
 
30288
31711
  Usage:
30289
31712
  demian-plain [flags]
31713
+ demian-plain config init [--force]
31714
+ demian-plain config add-provider <preset|openai-compatible> [--name <name>]
31715
+ demian-plain config add-model <provider> --name <name> --model <model>
31716
+ demian-plain auth login <codex|claudecode>
30290
31717
  demian-plain doctor policies --upgrade-namespaces [--write]
30291
31718
 
30292
31719
  Default:
@@ -30305,7 +31732,7 @@ Flags:
30305
31732
  --single-agent force the current single-agent runtime
30306
31733
  --multi-agent enable root-owned delegate_agent runtime
30307
31734
  --agent <name> general, build, plan, execute, or a configured custom agent
30308
- --provider <name> openai, gemini, ollama, lmstudio, vllm, llamacpp, openrouter, together, groq, azure, anthropic, codex, claudecode
31735
+ --provider <name> openai, anthropic, gemini, groq, azure, lmstudio, ollama-local, ollama-cloud, llamacpp, vllm, codex, claudecode
30309
31736
  --model <name> override configured model
30310
31737
  --max-turns <n> maximum model/tool loop turns
30311
31738
  --cwd <path> workspace directory
@@ -30318,7 +31745,8 @@ Flags:
30318
31745
  --persistent-grants persist "always" permission grants
30319
31746
  --no-persistent-grants
30320
31747
  keep "always" grants session-local
30321
- --no-transcript disable .demian transcript writes
31748
+ --no-transcript disable ~/.demian transcript writes
31749
+ --no-wizard skip the first-run config wizard
30322
31750
  --goal run the prompt as an explicit /goal objective
30323
31751
  /ralph-loop --fresh-context is single-agent only
30324
31752
  --config <path> load an additional config file
@@ -30327,6 +31755,13 @@ Flags:
30327
31755
  Doctor:
30328
31756
  doctor policies --upgrade-namespaces
30329
31757
  rewrite prefix-less permission rules as demian.<tool>
31758
+
31759
+ Config:
31760
+ config init create ~/.demian/config.json
31761
+ config models <provider> [--refresh]
31762
+ list configured or remote provider models
31763
+ auth login codex|claudecode
31764
+ start the external OAuth login flow
30330
31765
  `);
30331
31766
  }
30332
31767
  if (import.meta.url === `file://${process.argv[1]}`) {