demian-cli 1.0.7 → 1.0.9

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",
@@ -23515,6 +23639,7 @@ var defaultConfig = {
23515
23639
  defaultProvider: "brave",
23516
23640
  providers: {
23517
23641
  brave: {
23642
+ apiKey: "",
23518
23643
  apiKeyEnv: "BRAVE_SEARCH_API_KEY",
23519
23644
  endpoint: "https://api.search.brave.com/res/v1/web/search",
23520
23645
  count: 10,
@@ -23523,12 +23648,14 @@ var defaultConfig = {
23523
23648
  safeSearch: "moderate"
23524
23649
  },
23525
23650
  tavily: {
23651
+ apiKey: "",
23526
23652
  apiKeyEnv: "TAVILY_API_KEY",
23527
23653
  endpoint: "https://api.tavily.com/search",
23528
23654
  maxResults: 5,
23529
23655
  searchDepth: "basic"
23530
23656
  },
23531
23657
  exa: {
23658
+ apiKey: "",
23532
23659
  apiKeyEnv: "EXA_API_KEY",
23533
23660
  endpoint: "https://api.exa.ai/search",
23534
23661
  numResults: 5,
@@ -23582,73 +23709,113 @@ var defaultConfig = {
23582
23709
  providers: {
23583
23710
  openai: {
23584
23711
  type: "openai-compatible",
23585
- model: "openai-model-name",
23586
23712
  baseURL: "https://api.openai.com/v1",
23587
- apiKeyEnv: "OPENAI_API_KEY"
23713
+ apiKey: "",
23714
+ apiKeyEnv: "OPENAI_API_KEY",
23715
+ catalog: {
23716
+ type: "openai-models",
23717
+ endpoint: "https://api.openai.com/v1/models"
23718
+ }
23719
+ },
23720
+ anthropic: {
23721
+ type: "anthropic",
23722
+ baseURL: "https://api.anthropic.com/v1",
23723
+ apiKey: "",
23724
+ apiKeyEnv: "ANTHROPIC_API_KEY",
23725
+ catalog: {
23726
+ type: "anthropic-models",
23727
+ endpoint: "https://api.anthropic.com/v1/models"
23728
+ }
23588
23729
  },
23589
23730
  gemini: {
23590
23731
  type: "openai-compatible",
23591
- model: "gemini-model-name",
23592
23732
  baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
23593
- apiKeyEnv: "GOOGLE_API_KEY"
23733
+ apiKey: "",
23734
+ apiKeyEnv: "GEMINI_API_KEY",
23735
+ apiKeyEnvAliases: ["GOOGLE_API_KEY"],
23736
+ catalog: {
23737
+ type: "gemini-openai-models",
23738
+ endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models"
23739
+ }
23594
23740
  },
23595
- ollama: {
23741
+ groq: {
23596
23742
  type: "openai-compatible",
23597
- model: "local-coder-model",
23598
- baseURL: "http://localhost:11434/v1",
23599
- apiKey: "ollama",
23600
- quirks: { omitTemperature: true }
23743
+ baseURL: "https://api.groq.com/openai/v1",
23744
+ apiKey: "",
23745
+ apiKeyEnv: "GROQ_API_KEY",
23746
+ catalog: {
23747
+ type: "groq-models",
23748
+ endpoint: "https://api.groq.com/openai/v1/models"
23749
+ }
23750
+ },
23751
+ azure: {
23752
+ type: "openai-compatible",
23753
+ auth: { type: "api-key", header: "api-key" },
23754
+ modelProfiles: [
23755
+ {
23756
+ name: "azure-example",
23757
+ displayName: "Azure example",
23758
+ model: "azure-deployment-name",
23759
+ baseURL: "https://example.openai.azure.com/openai/v1",
23760
+ apiKey: "",
23761
+ apiKeyEnv: "AZURE_OPENAI_API_KEY"
23762
+ }
23763
+ ]
23601
23764
  },
23602
23765
  lmstudio: {
23603
23766
  type: "openai-compatible",
23604
- model: "local-coder-model",
23605
23767
  baseURL: "http://localhost:1234/v1",
23606
- apiKey: "lm-studio"
23768
+ apiKey: "lm-studio",
23769
+ modelProfiles: [],
23770
+ catalog: {
23771
+ type: "openai-compatible-models",
23772
+ endpoint: "http://localhost:1234/v1/models"
23773
+ }
23607
23774
  },
23608
- vllm: {
23775
+ "ollama-local": {
23609
23776
  type: "openai-compatible",
23610
- model: "local-coder-model",
23611
- baseURL: "http://localhost:8000/v1",
23612
- apiKey: "vllm"
23777
+ baseURL: "http://localhost:11434/v1",
23778
+ apiKey: "ollama",
23779
+ modelProfiles: [],
23780
+ quirks: { omitTemperature: true },
23781
+ catalog: {
23782
+ type: "openai-compatible-models",
23783
+ endpoint: "http://localhost:11434/v1/models"
23784
+ }
23785
+ },
23786
+ "ollama-cloud": {
23787
+ type: "ollama",
23788
+ baseURL: "https://ollama.com/api",
23789
+ apiKey: "",
23790
+ apiKeyEnv: "OLLAMA_API_KEY",
23791
+ modelProfiles: [],
23792
+ catalog: {
23793
+ type: "ollama-tags",
23794
+ endpoint: "https://ollama.com/api/tags"
23795
+ }
23613
23796
  },
23614
23797
  llamacpp: {
23615
23798
  type: "openai-compatible",
23616
- model: "local-coder-model",
23617
23799
  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"
23800
+ apiKey: "llama.cpp",
23801
+ modelProfiles: [],
23802
+ catalog: {
23803
+ type: "openai-compatible-models",
23804
+ endpoint: "http://localhost:8080/v1/models"
23805
+ }
23637
23806
  },
23638
- azure: {
23807
+ vllm: {
23639
23808
  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"
23809
+ baseURL: "http://localhost:8000/v1",
23810
+ apiKey: "vllm",
23811
+ modelProfiles: [],
23812
+ catalog: {
23813
+ type: "openai-compatible-models",
23814
+ endpoint: "http://localhost:8000/v1/models"
23815
+ }
23648
23816
  },
23649
23817
  codex: {
23650
23818
  type: "codex",
23651
- model: "gpt-5.1-codex",
23652
23819
  baseURL: "https://chatgpt.com/backend-api/codex",
23653
23820
  authStore: "auto",
23654
23821
  allowApiKeyFallback: false,
@@ -23660,6 +23827,9 @@ var defaultConfig = {
23660
23827
  effort: "medium",
23661
23828
  summary: "auto"
23662
23829
  }
23830
+ },
23831
+ catalog: {
23832
+ type: "codex-oauth-models"
23663
23833
  }
23664
23834
  },
23665
23835
  claudecode: {
@@ -23678,67 +23848,37 @@ var defaultConfig = {
23678
23848
  useBareMode: false,
23679
23849
  usageLedgerScope: "process",
23680
23850
  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
23851
  abortPolicy: "record-only",
23721
- hidden: true
23852
+ catalog: {
23853
+ type: "claudecode-supported-models"
23854
+ }
23722
23855
  }
23723
23856
  }
23724
23857
  };
23725
23858
  async function loadConfig(options) {
23726
23859
  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);
23860
+ const userConfigDir = path13.join(os7.homedir(), ".demian");
23861
+ const configPaths = [path13.join(userConfigDir, "config.json"), path13.join(userConfigDir, "config.jsond"), options.configPath].filter(Boolean);
23736
23862
  for (const filePath of configPaths) {
23737
23863
  if (!fs7.existsSync(filePath)) continue;
23738
- configs.push(parseConfigText(await readFile6(filePath, "utf8"), filePath));
23864
+ const parsed = parseConfigText(await readFile6(filePath, "utf8"), filePath);
23865
+ warnIfV1Config(parsed, filePath);
23866
+ configs.push(parsed);
23739
23867
  }
23740
23868
  return configs.reduce((acc, item) => mergeConfig(acc, item), structuredClone(defaultConfig));
23741
23869
  }
23870
+ function warnIfV1Config(parsed, filePath) {
23871
+ if (parsed.version === 2) return;
23872
+ const providers = parsed.providers ?? {};
23873
+ const hasLegacyShape = Object.values(providers).some((provider) => {
23874
+ const candidate = provider;
23875
+ return (typeof candidate.model === "string" || Array.isArray(candidate.models)) && !Array.isArray(candidate.modelProfiles);
23876
+ });
23877
+ if (!hasLegacyShape && parsed.version === void 0 && Object.keys(providers).length === 0) return;
23878
+ if (!hasLegacyShape) return;
23879
+ 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.
23880
+ `);
23881
+ }
23742
23882
  function parseConfigText(text, filePath) {
23743
23883
  try {
23744
23884
  return JSON.parse(filePath.endsWith(".jsond") ? stripJsonD(text) : text);
@@ -23945,7 +24085,137 @@ function mergeConfig(base, overlay) {
23945
24085
  };
23946
24086
  }
23947
24087
  function normalizeProviderAlias(providerName) {
23948
- return providerName === "claudecode-plan" ? "claudecode" : providerName;
24088
+ if (providerName === "claudecode-plan" || providerName === "claudecode-subagent") return "claudecode";
24089
+ if (providerName === "ollama") return "ollama-local";
24090
+ return providerName;
24091
+ }
24092
+ function sortProviderNames(names) {
24093
+ const order = new Map(BUILTIN_PROVIDER_ORDER.map((name, index) => [name, index]));
24094
+ return [...names].sort((left, right) => {
24095
+ const leftIndex = order.get(left);
24096
+ const rightIndex = order.get(right);
24097
+ if (leftIndex !== void 0 && rightIndex !== void 0) return leftIndex - rightIndex;
24098
+ if (leftIndex !== void 0) return -1;
24099
+ if (rightIndex !== void 0) return 1;
24100
+ return left.localeCompare(right);
24101
+ });
24102
+ }
24103
+ function providerModelProfiles(providerName, providerConfig) {
24104
+ const explicit = Array.isArray(providerConfig.modelProfiles) ? providerConfig.modelProfiles.filter((profile) => typeof profile?.model === "string" && profile.model.trim()).map((profile) => ({
24105
+ ...profile,
24106
+ name: profile.name?.trim() || profile.displayName?.trim() || profile.model.trim(),
24107
+ displayName: profile.displayName?.trim() || profile.name?.trim() || profile.model.trim(),
24108
+ model: profile.model.trim()
24109
+ })) : [];
24110
+ const seenNames = /* @__PURE__ */ new Set();
24111
+ const seenDisplayNames = /* @__PURE__ */ new Set();
24112
+ const seenModels = /* @__PURE__ */ new Set();
24113
+ const out = [];
24114
+ for (const profile of explicit) {
24115
+ if (seenNames.has(profile.name)) {
24116
+ process.stderr.write(`[demian] warning: provider ${providerName} has duplicate modelProfile name "${profile.name}". Later entry ignored.
24117
+ `);
24118
+ continue;
24119
+ }
24120
+ if (profile.displayName && seenDisplayNames.has(profile.displayName)) {
24121
+ process.stderr.write(`[demian] warning: provider ${providerName} has duplicate modelProfile displayName "${profile.displayName}". Later entry ignored.
24122
+ `);
24123
+ continue;
24124
+ }
24125
+ seenNames.add(profile.name);
24126
+ if (profile.displayName) seenDisplayNames.add(profile.displayName);
24127
+ seenModels.add(profile.model);
24128
+ out.push(profile);
24129
+ }
24130
+ const legacy = [...typeof providerConfig.model === "string" && providerConfig.model.trim() ? [providerConfig.model.trim()] : [], ...(providerConfig.models ?? []).filter((model) => typeof model === "string" && model.trim())];
24131
+ for (const model of legacy) {
24132
+ const trimmed = model.trim();
24133
+ if (!trimmed || seenModels.has(trimmed)) continue;
24134
+ seenModels.add(trimmed);
24135
+ const name = trimmed;
24136
+ if (!seenNames.has(name)) seenNames.add(name);
24137
+ out.push({ name, displayName: name, model: trimmed });
24138
+ }
24139
+ const fallback = fallbackModelForProvider(providerName, providerConfig);
24140
+ if (out.length === 0 && fallback) out.push({ name: fallback, displayName: fallback, model: fallback });
24141
+ return out;
24142
+ }
24143
+ function providerModelOptions(providerName, providerConfig) {
24144
+ return providerModelProfiles(providerName, providerConfig).filter((profile) => !profile.hidden).map((profile) => displayModelForProfile(profile));
24145
+ }
24146
+ function defaultModelForProvider(providerName, providerConfig) {
24147
+ return providerModelProfiles(providerName, providerConfig)[0]?.model ?? "";
24148
+ }
24149
+ function displayModelForProfile(profile) {
24150
+ return profile.displayName?.trim() || profile.name?.trim() || profile.model;
24151
+ }
24152
+ function defaultDisplayModelForProvider(providerName, providerConfig) {
24153
+ const profile = providerModelProfiles(providerName, providerConfig)[0];
24154
+ return profile ? displayModelForProfile(profile) : defaultModelForProvider(providerName, providerConfig);
24155
+ }
24156
+ function resolveConfiguredApiKey(providerConfig) {
24157
+ if (providerConfig.apiKey) return { value: providerConfig.apiKey };
24158
+ for (const envName of [providerConfig.apiKeyEnv, ...providerConfig.apiKeyEnvAliases ?? []]) {
24159
+ if (!envName) continue;
24160
+ const value = process.env[envName];
24161
+ if (value) return { value, envName };
24162
+ }
24163
+ return { envName: providerConfig.apiKeyEnv ?? providerConfig.apiKeyEnvAliases?.[0] };
24164
+ }
24165
+ function resolveProviderRuntimeConfig(config, selection) {
24166
+ const providerName = normalizeProviderAlias(selection.providerName);
24167
+ const providerConfig = resolveProviderConfig(config, providerName);
24168
+ const profiles = providerModelProfiles(providerName, providerConfig);
24169
+ let profile;
24170
+ if (selection.modelProfileName) profile = profiles.find((item) => item.name === selection.modelProfileName);
24171
+ if (!profile && selection.model) profile = profiles.find((item) => item.displayName === selection.model);
24172
+ if (!profile && selection.model) profile = profiles.find((item) => item.model === selection.model);
24173
+ if (!profile) profile = profiles[0];
24174
+ if (!profile) {
24175
+ const model = selection.model ?? defaultModelForProvider(providerName, providerConfig);
24176
+ if (!model) throw new Error(`Provider ${providerName} has no model. Run \`demian config models ${providerName} --refresh\` or add a model profile.`);
24177
+ return { providerName, providerConfig, model };
24178
+ }
24179
+ const merged = mergeModelProfile(providerConfig, profile);
24180
+ return {
24181
+ providerName,
24182
+ providerConfig: merged,
24183
+ model: profile.model,
24184
+ modelProfileName: profile.name
24185
+ };
24186
+ }
24187
+ function mergeModelProfile(providerConfig, profile) {
24188
+ const merged = {
24189
+ ...providerConfig,
24190
+ ...definedOnly({
24191
+ baseURL: profile.baseURL,
24192
+ apiKey: nonEmptyString(profile.apiKey),
24193
+ apiKeyEnv: profile.apiKeyEnv,
24194
+ apiKeyEnvAliases: profile.apiKeyEnvAliases,
24195
+ auth: profile.auth,
24196
+ headers: profile.headers,
24197
+ quirks: profile.quirks,
24198
+ maxTokens: profile.maxTokens
24199
+ }),
24200
+ model: profile.model
24201
+ };
24202
+ return merged;
24203
+ }
24204
+ function definedOnly(input2) {
24205
+ return Object.fromEntries(Object.entries(input2).filter(([, value]) => value !== void 0));
24206
+ }
24207
+ function nonEmptyString(value) {
24208
+ return typeof value === "string" && value.length > 0 ? value : void 0;
24209
+ }
24210
+ function fallbackModelForProvider(providerName, providerConfig) {
24211
+ if (typeof providerConfig.model === "string" && providerConfig.model.trim()) return providerConfig.model.trim();
24212
+ if (providerName === "openai") return "gpt-5.5";
24213
+ if (providerName === "anthropic") return CLAUDE_CODE_SONNET_MODEL;
24214
+ if (providerName === "codex" || providerConfig.type === "codex") return "gpt-5.5";
24215
+ if (providerName === "claudecode") return CLAUDE_CODE_SONNET_MODEL;
24216
+ if (providerName === "gemini") return "gemini-2.5-pro";
24217
+ if (providerName === "groq") return "openai/gpt-oss-120b";
24218
+ return void 0;
23949
24219
  }
23950
24220
  function migrateProviderConfig(provider, baseProvider) {
23951
24221
  if (isLegacyClaudeCodeShape(provider)) {
@@ -24027,12 +24297,14 @@ function resolveAgentMode(config, flagMode) {
24027
24297
  return "single-agent";
24028
24298
  }
24029
24299
  function resolveProviderConfig(config, providerName) {
24030
- const provider = config.providers[providerName];
24300
+ const normalized = normalizeProviderAlias(providerName);
24301
+ const provider = config.providers[normalized];
24031
24302
  if (!provider) throw new Error(`Unknown provider: ${providerName}`);
24032
24303
  return provider;
24033
24304
  }
24034
24305
  function resolveProvider(providerConfig, options = {}) {
24035
24306
  const model = options.model ?? providerConfig.model;
24307
+ if (!model) throw new Error("Provider has no selected model. Choose a model or add a model profile.");
24036
24308
  if (providerConfig.type === "claudecode") {
24037
24309
  throw new Error("claudecode is an external agent runtime. Use resolveExecutionBackend().");
24038
24310
  }
@@ -24079,12 +24351,28 @@ function resolveProvider(providerConfig, options = {}) {
24079
24351
  };
24080
24352
  }
24081
24353
  if (providerConfig.type === "anthropic") {
24082
- const apiKey2 = providerConfig.apiKey ?? (providerConfig.apiKeyEnv ? process.env[providerConfig.apiKeyEnv] : void 0);
24354
+ const apiKey2 = resolveConfiguredApiKey(providerConfig).value;
24083
24355
  if (!apiKey2) {
24084
24356
  throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24085
24357
  }
24086
24358
  return {
24087
24359
  provider: new AnthropicProvider({
24360
+ apiKey: apiKey2,
24361
+ baseURL: providerConfig.baseURL,
24362
+ defaultModel: model,
24363
+ onRetry: options.onRetry
24364
+ }),
24365
+ model
24366
+ };
24367
+ }
24368
+ if (providerConfig.type === "ollama") {
24369
+ const apiKey2 = resolveConfiguredApiKey(providerConfig).value;
24370
+ if (!apiKey2) {
24371
+ throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24372
+ }
24373
+ return {
24374
+ provider: new OllamaProvider({
24375
+ baseURL: providerConfig.baseURL,
24088
24376
  apiKey: apiKey2,
24089
24377
  defaultModel: model,
24090
24378
  onRetry: options.onRetry
@@ -24092,7 +24380,7 @@ function resolveProvider(providerConfig, options = {}) {
24092
24380
  model
24093
24381
  };
24094
24382
  }
24095
- const apiKey = providerConfig.apiKey ?? (providerConfig.apiKeyEnv ? process.env[providerConfig.apiKeyEnv] : void 0);
24383
+ const apiKey = resolveConfiguredApiKey(providerConfig).value;
24096
24384
  if (!apiKey) {
24097
24385
  throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24098
24386
  }
@@ -24111,6 +24399,7 @@ function resolveProvider(providerConfig, options = {}) {
24111
24399
  }
24112
24400
  function resolveExecutionBackend(providerConfig, options = {}) {
24113
24401
  const model = options.model ?? providerConfig.model;
24402
+ if (!model) throw new Error("Provider has no selected model. Choose a model or add a model profile.");
24114
24403
  if (providerConfig.type === "claudecode") {
24115
24404
  const runtimeConfig = options.config || providerConfig.permissionProfile ? resolveClaudeCodeRuntimeConfig(providerConfig, options.config ?? defaultConfig, options.agent) : providerConfig;
24116
24405
  return {
@@ -24152,31 +24441,1130 @@ function claudeCodeRuntimePolicyHash(config) {
24152
24441
  }
24153
24442
  var CLAUDE_CODE_PERMISSION_PROFILE_KEYS = ["permissionMode", "defaultDecision", "allowedTools", "disallowedTools", "tools", "allowSubagents"];
24154
24443
 
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";
24444
+ // src/config-command.ts
24445
+ import { spawn as spawn4 } from "node:child_process";
24446
+ import { stat as stat4 } from "node:fs/promises";
24447
+ import os10 from "node:os";
24448
+ import path16 from "node:path";
24449
+ import readline3 from "node:readline/promises";
24450
+
24451
+ // src/config-scaffold.ts
24452
+ import { chmod as chmod3, mkdir as mkdir6, readFile as readFile7, rename as rename4, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
24158
24453
  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);
24454
+ import path14 from "node:path";
24455
+ function defaultUserConfigPath() {
24456
+ return path14.join(os8.homedir(), ".demian", "config.json");
24166
24457
  }
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")) {
24458
+ function defaultUserConfig(defaultProvider = detectDefaultProvider()) {
24459
+ return {
24460
+ version: 2,
24461
+ defaultProvider,
24462
+ providers: {
24463
+ openai: {
24464
+ type: "openai-compatible",
24465
+ baseURL: "https://api.openai.com/v1",
24466
+ apiKey: "",
24467
+ apiKeyEnv: "OPENAI_API_KEY",
24468
+ catalog: { type: "openai-models", endpoint: "https://api.openai.com/v1/models" }
24469
+ },
24470
+ anthropic: {
24471
+ type: "anthropic",
24472
+ baseURL: "https://api.anthropic.com/v1",
24473
+ apiKey: "",
24474
+ apiKeyEnv: "ANTHROPIC_API_KEY",
24475
+ catalog: { type: "anthropic-models", endpoint: "https://api.anthropic.com/v1/models" }
24476
+ },
24477
+ gemini: {
24478
+ type: "openai-compatible",
24479
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
24480
+ apiKey: "",
24481
+ apiKeyEnv: "GEMINI_API_KEY",
24482
+ apiKeyEnvAliases: ["GOOGLE_API_KEY"],
24483
+ catalog: { type: "gemini-openai-models", endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models" }
24484
+ },
24485
+ groq: {
24486
+ type: "openai-compatible",
24487
+ baseURL: "https://api.groq.com/openai/v1",
24488
+ apiKey: "",
24489
+ apiKeyEnv: "GROQ_API_KEY",
24490
+ catalog: { type: "groq-models", endpoint: "https://api.groq.com/openai/v1/models" }
24491
+ },
24492
+ azure: {
24493
+ type: "openai-compatible",
24494
+ auth: { type: "api-key", header: "api-key" },
24495
+ modelProfiles: [
24496
+ {
24497
+ name: "azure-example",
24498
+ displayName: "Azure example",
24499
+ model: "azure-deployment-name",
24500
+ baseURL: "https://example.openai.azure.com/openai/v1",
24501
+ apiKey: "",
24502
+ apiKeyEnv: "AZURE_OPENAI_API_KEY"
24503
+ }
24504
+ ]
24505
+ },
24506
+ lmstudio: {
24507
+ type: "openai-compatible",
24508
+ baseURL: "http://localhost:1234/v1",
24509
+ apiKey: "lm-studio",
24510
+ modelProfiles: [],
24511
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:1234/v1/models" }
24512
+ },
24513
+ "ollama-local": {
24514
+ type: "openai-compatible",
24515
+ baseURL: "http://localhost:11434/v1",
24516
+ apiKey: "ollama",
24517
+ modelProfiles: [],
24518
+ quirks: { omitTemperature: true },
24519
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:11434/v1/models" }
24520
+ },
24521
+ "ollama-cloud": {
24522
+ type: "ollama",
24523
+ baseURL: "https://ollama.com/api",
24524
+ apiKey: "",
24525
+ apiKeyEnv: "OLLAMA_API_KEY",
24526
+ modelProfiles: [],
24527
+ catalog: { type: "ollama-tags", endpoint: "https://ollama.com/api/tags" }
24528
+ },
24529
+ llamacpp: {
24530
+ type: "openai-compatible",
24531
+ baseURL: "http://localhost:8080/v1",
24532
+ apiKey: "llama.cpp",
24533
+ modelProfiles: [],
24534
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8080/v1/models" }
24535
+ },
24536
+ vllm: {
24537
+ type: "openai-compatible",
24538
+ baseURL: "http://localhost:8000/v1",
24539
+ apiKey: "vllm",
24540
+ modelProfiles: [],
24541
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8000/v1/models" }
24542
+ },
24543
+ codex: {
24544
+ type: "codex",
24545
+ baseURL: "https://chatgpt.com/backend-api/codex",
24546
+ authStore: "auto",
24547
+ allowApiKeyFallback: false,
24548
+ promptCacheKey: "root-session",
24549
+ catalog: { type: "codex-oauth-models" }
24550
+ },
24551
+ claudecode: {
24552
+ type: "claudecode",
24553
+ runtime: "agent-sdk",
24554
+ cliPath: "~/.local/bin/claude",
24555
+ cwdMode: "session",
24556
+ historyPolicy: "passthrough-resume",
24557
+ onInvalidResume: "fresh",
24558
+ attachmentFallback: "block",
24559
+ allowSubagents: false,
24560
+ sanitizeApiKeyEnv: true,
24561
+ authPreflight: true,
24562
+ useBareMode: false,
24563
+ usageLedgerScope: "process",
24564
+ sessionLock: true,
24565
+ abortPolicy: "record-only",
24566
+ catalog: { type: "claudecode-supported-models" }
24567
+ }
24568
+ }
24569
+ };
24570
+ }
24571
+ async function createUserConfig(options = {}) {
24572
+ const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24573
+ const content = `${JSON.stringify(defaultUserConfig(options.defaultProvider), null, 2)}
24574
+ `;
24575
+ if (options.print) return { path: filePath, created: false, content };
24576
+ const existed = await exists(filePath);
24577
+ if (existed && !options.force) return { path: filePath, created: false, content: await readFile7(filePath, "utf8"), existed: true };
24578
+ await writeJsonAtomic(filePath, content);
24579
+ return { path: filePath, created: true, content, existed };
24580
+ }
24581
+ async function addProvider(options) {
24582
+ const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24583
+ const config = await readConfigObject(filePath);
24584
+ const providers = objectValue(config.providers);
24585
+ const name = options.name;
24586
+ if (providers[name] && !options.force) throw new Error(`Provider ${name} already exists. Use --force to overwrite it.`);
24587
+ providers[name] = providerPreset(options);
24588
+ config.providers = providers;
24589
+ const content = `${JSON.stringify(config, null, 2)}
24590
+ `;
24591
+ await writeJsonAtomic(filePath, content);
24592
+ return { path: filePath, created: true, content };
24593
+ }
24594
+ async function addModelProfile(options) {
24595
+ const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24596
+ const config = await readConfigObject(filePath);
24597
+ const providers = objectValue(config.providers);
24598
+ const provider = objectValue(providers[options.provider]);
24599
+ const profiles = Array.isArray(provider.modelProfiles) ? [...provider.modelProfiles] : [];
24600
+ const displayName = options.displayName ?? options.name;
24601
+ const existingByName = profiles.findIndex((entry) => entry.name === options.name);
24602
+ const existingByDisplay = profiles.findIndex((entry) => entry.displayName === displayName);
24603
+ if (existingByName >= 0 && !options.force) throw new Error(`Profile name "${options.name}" already exists on provider ${options.provider}. Use --force to overwrite it.`);
24604
+ if (existingByDisplay >= 0 && existingByDisplay !== existingByName && !options.force) {
24605
+ 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.`);
24606
+ }
24607
+ const next = {
24608
+ name: options.name,
24609
+ displayName,
24610
+ model: options.model,
24611
+ ...options.baseURL ? { baseURL: options.baseURL } : {},
24612
+ ...options.apiKey !== void 0 || options.apiKeyEnv ? { apiKey: options.apiKey ?? "" } : {},
24613
+ ...options.apiKeyEnv ? { apiKeyEnv: options.apiKeyEnv } : {}
24614
+ };
24615
+ if (existingByName >= 0) profiles[existingByName] = next;
24616
+ else profiles.push(next);
24617
+ provider.modelProfiles = profiles;
24618
+ providers[options.provider] = provider;
24619
+ config.providers = providers;
24620
+ const content = `${JSON.stringify(config, null, 2)}
24621
+ `;
24622
+ await writeJsonAtomic(filePath, content);
24623
+ return { path: filePath, created: true, content };
24624
+ }
24625
+ async function readConfigObject(filePath) {
24626
+ if (!await exists(filePath)) await createUserConfig({ path: filePath });
24627
+ const raw = JSON.parse(await readFile7(filePath, "utf8"));
24628
+ raw.version ??= 2;
24629
+ raw.providers = objectValue(raw.providers);
24630
+ return raw;
24631
+ }
24632
+ function providerPreset(options) {
24633
+ const preset = options.preset ?? options.name;
24634
+ const auth = apiKeyAuthFields(options);
24635
+ const openAIAuth = options.authHeader ? { auth: { type: "api-key", header: options.authHeader } } : {};
24636
+ if (preset === "openai") return { type: "openai-compatible", baseURL: options.baseURL ?? "https://api.openai.com/v1", ...apiKeyAuthFields(options, "OPENAI_API_KEY"), ...openAIAuth, catalog: { type: "openai-models", endpoint: `${(options.baseURL ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/models` } };
24637
+ if (preset === "anthropic") return { type: "anthropic", baseURL: options.baseURL ?? "https://api.anthropic.com/v1", ...apiKeyAuthFields(options, "ANTHROPIC_API_KEY"), catalog: { type: "anthropic-models", endpoint: `${(options.baseURL ?? "https://api.anthropic.com/v1").replace(/\/+$/, "")}/models` } };
24638
+ if (preset === "gemini") {
24639
+ const geminiBase = options.baseURL ?? "https://generativelanguage.googleapis.com/v1beta/openai/";
24640
+ return { type: "openai-compatible", baseURL: geminiBase, ...apiKeyAuthFields(options, "GEMINI_API_KEY"), apiKeyEnvAliases: ["GOOGLE_API_KEY"], ...openAIAuth, catalog: { type: "gemini-openai-models", endpoint: `${geminiBase.replace(/\/+$/, "")}/models` } };
24641
+ }
24642
+ if (preset === "groq") return { type: "openai-compatible", baseURL: options.baseURL ?? "https://api.groq.com/openai/v1", ...apiKeyAuthFields(options, "GROQ_API_KEY"), ...openAIAuth, catalog: { type: "groq-models", endpoint: `${(options.baseURL ?? "https://api.groq.com/openai/v1").replace(/\/+$/, "")}/models` } };
24643
+ if (preset === "azure") {
24644
+ return {
24645
+ type: "openai-compatible",
24646
+ auth: { type: "api-key", header: options.authHeader ?? "api-key" },
24647
+ ...auth,
24648
+ modelProfiles: [
24649
+ {
24650
+ name: "azure-example",
24651
+ displayName: "Azure example",
24652
+ model: "azure-deployment-name",
24653
+ baseURL: options.baseURL ?? "https://example.openai.azure.com/openai/v1",
24654
+ ...options.apiKey ? {} : { apiKey: "" },
24655
+ apiKeyEnv: options.apiKeyEnv ?? "AZURE_OPENAI_API_KEY"
24656
+ }
24657
+ ]
24658
+ };
24659
+ }
24660
+ if (preset === "lmstudio") return { type: "openai-compatible", baseURL: options.baseURL ?? "http://localhost:1234/v1", apiKey: options.apiKey ?? "lm-studio", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options.baseURL ?? "http://localhost:1234/v1").replace(/\/+$/, "")}/models` } };
24661
+ if (preset === "ollama-local" || preset === "ollama") return { type: "openai-compatible", baseURL: options.baseURL ?? "http://localhost:11434/v1", apiKey: options.apiKey ?? "ollama", modelProfiles: [], quirks: { omitTemperature: true }, catalog: { type: "openai-compatible-models", endpoint: `${(options.baseURL ?? "http://localhost:11434/v1").replace(/\/+$/, "")}/models` } };
24662
+ if (preset === "ollama-cloud") return { type: "ollama", baseURL: options.baseURL ?? "https://ollama.com/api", ...apiKeyAuthFields(options, "OLLAMA_API_KEY"), modelProfiles: [], catalog: { type: "ollama-tags", endpoint: `${(options.baseURL ?? "https://ollama.com/api").replace(/\/+$/, "")}/tags` } };
24663
+ if (preset === "llamacpp") return { type: "openai-compatible", baseURL: options.baseURL ?? "http://localhost:8080/v1", apiKey: options.apiKey ?? "llama.cpp", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options.baseURL ?? "http://localhost:8080/v1").replace(/\/+$/, "")}/models` } };
24664
+ if (preset === "vllm") return { type: "openai-compatible", baseURL: options.baseURL ?? "http://localhost:8000/v1", apiKey: options.apiKey ?? "vllm", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options.baseURL ?? "http://localhost:8000/v1").replace(/\/+$/, "")}/models` } };
24665
+ 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" } };
24666
+ 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" } };
24667
+ if ((options.type ?? preset) === "openai-compatible") {
24668
+ const baseURL = options.baseURL;
24669
+ if (!baseURL) throw new Error("openai-compatible provider requires --base-url.");
24670
+ return {
24671
+ type: "openai-compatible",
24672
+ baseURL,
24673
+ ...auth,
24674
+ ...openAIAuth,
24675
+ catalog: { type: "openai-compatible-models", endpoint: `${baseURL.replace(/\/+$/, "")}/models` }
24676
+ };
24677
+ }
24678
+ throw new Error(`Unknown provider preset: ${preset}`);
24679
+ }
24680
+ function apiKeyAuthFields(options, defaultApiKeyEnv) {
24681
+ const apiKeyEnv = options.apiKeyEnv ?? defaultApiKeyEnv;
24682
+ return {
24683
+ ...options.apiKey !== void 0 || apiKeyEnv ? { apiKey: options.apiKey ?? "" } : {},
24684
+ ...apiKeyEnv ? { apiKeyEnv } : {}
24685
+ };
24686
+ }
24687
+ async function writeJsonAtomic(filePath, content) {
24688
+ await mkdir6(path14.dirname(filePath), { recursive: true, mode: 448 });
24689
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
24690
+ await writeFile5(temp, content, { mode: 384 });
24691
+ await rename4(temp, filePath);
24692
+ await chmod3(filePath, 384).catch(() => void 0);
24693
+ }
24694
+ function detectDefaultProvider() {
24695
+ if (process.env.OPENAI_API_KEY) return "openai";
24696
+ if (process.env.ANTHROPIC_API_KEY) return "anthropic";
24697
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return "gemini";
24698
+ if (process.env.GROQ_API_KEY) return "groq";
24699
+ return "openai";
24700
+ }
24701
+ function objectValue(value) {
24702
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
24703
+ }
24704
+ async function exists(filePath) {
24705
+ try {
24706
+ await stat3(filePath);
24707
+ return true;
24708
+ } catch {
24709
+ return false;
24710
+ }
24711
+ }
24712
+ function expandHome2(value) {
24713
+ return resolveExpandedPath(value);
24714
+ }
24715
+
24716
+ // src/models/catalog.ts
24717
+ import crypto5 from "node:crypto";
24718
+ import { mkdir as mkdir7, readFile as readFile8, rename as rename5, writeFile as writeFile6 } from "node:fs/promises";
24719
+ import os9 from "node:os";
24720
+ import path15 from "node:path";
24721
+ var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
24722
+ var PING_TIMEOUT_MS = 1500;
24723
+ var DEFAULT_CODEX_CLIENT_VERSION = "0.0.0";
24724
+ var dynamicImport2 = new Function("specifier", "return import(specifier)");
24725
+ var inflight = /* @__PURE__ */ new Map();
24726
+ var pingCache = /* @__PURE__ */ new Map();
24727
+ var PING_CACHE_TTL_MS = 30 * 1e3;
24728
+ var packageVersionPromise;
24729
+ async function listProviderModelCatalog(providerName, providerConfig, options = {}) {
24730
+ const key = `${catalogCacheKey(providerName, providerConfig)}:${options.refresh ? "refresh" : "default"}`;
24731
+ const existing = inflight.get(key);
24732
+ if (existing) return existing;
24733
+ const promise = doListProviderModelCatalog(providerName, providerConfig, options).finally(() => inflight.delete(key));
24734
+ inflight.set(key, promise);
24735
+ return promise;
24736
+ }
24737
+ async function doListProviderModelCatalog(providerName, providerConfig, options) {
24738
+ const staticModels = staticModelEntries(providerName, providerConfig);
24739
+ const catalog = providerConfig.catalog;
24740
+ if (!catalog || catalog.type === "static") {
24741
+ return { status: staticModels.length ? "ready" : "unavailable", providerName, models: staticModels, source: "config" };
24742
+ }
24743
+ const missingAuthEnv = missingCatalogAuth(providerConfig);
24744
+ if (missingAuthEnv) {
24745
+ return {
24746
+ status: "missing-auth",
24747
+ providerName,
24748
+ models: staticModels,
24749
+ source: staticModels.length ? "config" : "fallback",
24750
+ message: `missing-auth:${missingAuthEnv}`
24751
+ };
24752
+ }
24753
+ const cacheKey = catalogCacheKey(providerName, providerConfig);
24754
+ const ttlMs = catalog.refreshTtlMs ?? DEFAULT_CACHE_TTL_MS;
24755
+ const cached = await readCatalogCache(cacheKey, ttlMs, options.refresh !== true);
24756
+ if (cached && !options.refresh) return { ...cached, models: mergeCatalogWithStatic(staticModels, cached.models) };
24757
+ if (catalog.endpoint) {
24758
+ const reachable = await pingEndpoint(catalog.endpoint, options.fetch);
24759
+ if (!reachable) {
24760
+ const stale = await readCatalogCache(cacheKey, Number.POSITIVE_INFINITY, true);
24761
+ if (stale) return { ...stale, models: mergeCatalogWithStatic(staticModels, stale.models), message: `endpoint ${catalog.endpoint} unreachable; using cached models` };
24762
+ if (staticModels.length) return { status: "ready", providerName, models: staticModels, source: "config", message: `endpoint ${catalog.endpoint} unreachable; using configured profiles` };
24763
+ return { status: "unavailable", providerName, models: [], source: "fallback", message: `endpoint ${catalog.endpoint} unreachable` };
24764
+ }
24765
+ }
24766
+ try {
24767
+ const remote = await fetchCatalog(providerName, providerConfig, options);
24768
+ const merged = mergeCatalogWithStatic(staticModels, remote.models);
24769
+ const result = { ...remote, models: merged };
24770
+ if (result.status === "ready") await writeCatalogCache(cacheKey, result);
24771
+ return result;
24772
+ } catch (error) {
24773
+ const authMissing = isMissingAuthError(error);
24774
+ const stale = await readCatalogCache(cacheKey, Number.POSITIVE_INFINITY, true);
24775
+ if (stale) return { ...stale, status: authMissing ? "missing-auth" : stale.status, models: mergeCatalogWithStatic(staticModels, stale.models), message: errorMessage2(error) };
24776
+ if (staticModels.length) return { status: authMissing ? "missing-auth" : "ready", providerName, models: staticModels, source: "config", message: errorMessage2(error) };
24777
+ return { status: authMissing ? "missing-auth" : "unavailable", providerName, models: [], source: "fallback", message: errorMessage2(error) };
24778
+ }
24779
+ }
24780
+ async function pingEndpoint(endpoint, fetcher) {
24781
+ const now2 = Date.now();
24782
+ const cached = pingCache.get(endpoint);
24783
+ if (cached && cached.expiresAt > now2) return cached.ok;
24784
+ const ok2 = await tryPing(endpoint, fetcher);
24785
+ pingCache.set(endpoint, { ok: ok2, expiresAt: now2 + PING_CACHE_TTL_MS });
24786
+ return ok2;
24787
+ }
24788
+ async function tryPing(endpoint, fetcher) {
24789
+ const fn = fetcher ?? fetch;
24790
+ try {
24791
+ const response = await fn(endpoint, { method: "HEAD", signal: AbortSignal.timeout(PING_TIMEOUT_MS) });
24792
+ if (response.status < 500) return true;
24793
+ } catch {
24794
+ }
24795
+ try {
24796
+ const response = await fn(endpoint, { method: "GET", signal: AbortSignal.timeout(PING_TIMEOUT_MS), headers: { range: "bytes=0-0" } });
24797
+ return response.status < 500;
24798
+ } catch {
24799
+ return false;
24800
+ }
24801
+ }
24802
+ async function fetchCatalog(providerName, providerConfig, options) {
24803
+ if (providerConfig.type === "codex" || providerConfig.catalog?.type === "codex-oauth-models") return fetchCodexCatalog(providerName, providerConfig, options);
24804
+ if (providerConfig.type === "claudecode" || providerConfig.catalog?.type === "claudecode-supported-models") return fetchClaudeCodeCatalog(providerName, providerConfig, options);
24805
+ if (providerConfig.catalog?.type === "anthropic-models") return fetchAnthropicCatalog(providerName, providerConfig, options);
24806
+ if (providerConfig.catalog?.type === "ollama-tags") return fetchOllamaTagsCatalog(providerName, providerConfig, options);
24807
+ return fetchOpenAIStyleCatalog(providerName, providerConfig, options);
24808
+ }
24809
+ async function fetchOpenAIStyleCatalog(providerName, providerConfig, options) {
24810
+ if (providerConfig.type !== "openai-compatible") throw new Error(`Provider ${providerName} is not OpenAI-compatible.`);
24811
+ const apiKey = resolveConfiguredApiKey(providerConfig);
24812
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
24813
+ const endpoint = providerConfig.catalog?.endpoint ?? `${providerConfig.baseURL.replace(/\/+$/, "")}/models`;
24814
+ const response = await fetchJson(endpoint, {
24815
+ fetcher: options.fetch,
24816
+ timeoutMs: options.timeoutMs,
24817
+ headers: {
24818
+ ...authHeadersForOpenAICompatible(providerConfig, apiKey.value),
24819
+ ...providerConfig.headers ?? {}
24820
+ }
24821
+ });
24822
+ return {
24823
+ status: "ready",
24824
+ providerName,
24825
+ models: normalizeOpenAIModels(response).map((entry) => ({ ...entry, source: "api" })),
24826
+ source: "api"
24827
+ };
24828
+ }
24829
+ async function fetchAnthropicCatalog(providerName, providerConfig, options) {
24830
+ if (providerConfig.type !== "anthropic") throw new Error(`Provider ${providerName} is not Anthropic.`);
24831
+ const apiKey = resolveConfiguredApiKey(providerConfig);
24832
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
24833
+ const base = providerConfig.baseURL ?? "https://api.anthropic.com/v1";
24834
+ const endpoint = providerConfig.catalog?.endpoint ?? `${base.replace(/\/+$/, "")}/models`;
24835
+ const models = [];
24836
+ let afterId;
24837
+ for (; ; ) {
24838
+ const url = new URL(endpoint);
24839
+ if (afterId) url.searchParams.set("after_id", afterId);
24840
+ url.searchParams.set("limit", "100");
24841
+ const response = await fetchJson(url.toString(), {
24842
+ fetcher: options.fetch,
24843
+ timeoutMs: options.timeoutMs,
24844
+ headers: {
24845
+ "x-api-key": apiKey.value,
24846
+ "anthropic-version": "2023-06-01"
24847
+ }
24848
+ });
24849
+ const page = normalizeAnthropicModels(response);
24850
+ models.push(...page);
24851
+ if (!response.has_more || !response.last_id) break;
24852
+ afterId = response.last_id;
24853
+ }
24854
+ return { status: "ready", providerName, models, source: "api" };
24855
+ }
24856
+ async function fetchOllamaTagsCatalog(providerName, providerConfig, options) {
24857
+ if (providerConfig.type !== "ollama") throw new Error(`Provider ${providerName} is not Ollama native.`);
24858
+ const apiKey = resolveConfiguredApiKey(providerConfig);
24859
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
24860
+ const endpoint = providerConfig.catalog?.endpoint ?? `${providerConfig.baseURL.replace(/\/+$/, "")}/tags`;
24861
+ const response = await fetchJson(endpoint, {
24862
+ fetcher: options.fetch,
24863
+ timeoutMs: options.timeoutMs,
24864
+ headers: { authorization: `Bearer ${apiKey.value}` }
24865
+ });
24866
+ return {
24867
+ status: "ready",
24868
+ providerName,
24869
+ models: normalizeOllamaTags(response).map((entry) => ({ ...entry, source: "api" })),
24870
+ source: "api"
24871
+ };
24872
+ }
24873
+ async function fetchCodexCatalog(providerName, providerConfig, options) {
24874
+ if (providerConfig.type !== "codex") throw new Error(`Provider ${providerName} is not Codex.`);
24875
+ const fetcher = options.fetch ?? fetch;
24876
+ const baseURL = (providerConfig.baseURL ?? "https://chatgpt.com/backend-api/codex").replace(/\/+$/, "");
24877
+ const installationId = await loadOrCreateInstallationId(resolveCodexHome(providerConfig.codexHome));
24878
+ const auth = getSharedCodexAuthStore({
24879
+ codexHome: providerConfig.codexHome,
24880
+ authStore: providerConfig.authStore,
24881
+ allowApiKeyFallback: providerConfig.allowApiKeyFallback,
24882
+ proactiveRefreshMinutes: providerConfig.refresh?.proactiveRefreshMinutes,
24883
+ refreshCache: providerConfig.refresh?.cache,
24884
+ fetch: fetcher
24885
+ });
24886
+ const headers = await auth.requestHeaders(installationId);
24887
+ const url = new URL(`${baseURL}/models`);
24888
+ url.searchParams.set("client_version", await codexClientVersion(options.clientVersion));
24889
+ const response = await fetchJson(url.toString(), {
24890
+ fetcher,
24891
+ timeoutMs: options.timeoutMs,
24892
+ headers
24893
+ });
24894
+ return {
24895
+ status: "ready",
24896
+ providerName,
24897
+ models: normalizeCodexModels(response).map((entry) => ({ ...entry, source: "oauth" })),
24898
+ source: "oauth"
24899
+ };
24900
+ }
24901
+ async function fetchClaudeCodeCatalog(providerName, providerConfig, options) {
24902
+ if (providerConfig.type !== "claudecode") throw new Error(`Provider ${providerName} is not Claude Code.`);
24903
+ const fallback = staticModelEntries(providerName, providerConfig);
24904
+ try {
24905
+ const module = await dynamicImport2("@anthropic-ai/claude-agent-sdk");
24906
+ if (!module.query) throw new Error("Claude Agent SDK query export is unavailable.");
24907
+ const query = module.query({
24908
+ prompt: "",
24909
+ options: {
24910
+ model: defaultModelForProvider(providerName, providerConfig),
24911
+ maxTurns: 0,
24912
+ pathToClaudeCodeExecutable: resolveClaudeCodeCliPath(providerConfig.cliPath),
24913
+ env: sanitizedClaudeCodeEnv(providerConfig.env, providerConfig.sanitizeApiKeyEnv ?? true)
24914
+ }
24915
+ });
24916
+ try {
24917
+ if (typeof query.supportedModels !== "function") throw new Error("Claude Agent SDK supportedModels is unavailable.");
24918
+ const models = await withTimeout(query.supportedModels(), options.timeoutMs ?? 5e3);
24919
+ return {
24920
+ status: "ready",
24921
+ providerName,
24922
+ models: models.map((model, index) => ({
24923
+ name: model.displayName || model.value,
24924
+ displayName: model.displayName || model.value,
24925
+ model: model.value,
24926
+ description: model.description,
24927
+ isDefault: index === 0,
24928
+ source: "oauth"
24929
+ })),
24930
+ source: "oauth"
24931
+ };
24932
+ } finally {
24933
+ query.close?.();
24934
+ }
24935
+ } catch (error) {
24936
+ if (fallback.length) return { status: "ready", providerName, models: fallback, source: "fallback", message: errorMessage2(error) };
24937
+ throw error;
24938
+ }
24939
+ }
24940
+ function staticModelEntries(providerName, providerConfig) {
24941
+ return providerModelProfiles(providerName, providerConfig).filter((profile) => !profile.hidden).map((profile, index) => ({
24942
+ name: profile.displayName ?? profile.name,
24943
+ displayName: profile.displayName ?? profile.name,
24944
+ model: profile.model,
24945
+ description: profile.description,
24946
+ isDefault: index === 0,
24947
+ source: "config"
24948
+ }));
24949
+ }
24950
+ function mergeCatalogWithStatic(staticModels, remoteModels) {
24951
+ const byModel = /* @__PURE__ */ new Map();
24952
+ for (const model of remoteModels) byModel.set(model.model, model);
24953
+ for (const model of staticModels) byModel.set(model.model, { ...byModel.get(model.model), ...model, source: model.source });
24954
+ return [...byModel.values()];
24955
+ }
24956
+ function normalizeOpenAIModels(raw) {
24957
+ const items = Array.isArray(raw.data) ? raw.data : Array.isArray(raw.models) ? raw.models : [];
24958
+ return items.map((item) => {
24959
+ const object2 = item;
24960
+ const id = stringValue4(object2.id) ?? stringValue4(object2.name) ?? stringValue4(object2.model);
24961
+ if (!id) return void 0;
24962
+ return {
24963
+ name: id,
24964
+ displayName: stringValue4(object2.display_name) ?? stringValue4(object2.displayName) ?? id,
24965
+ model: id,
24966
+ description: stringValue4(object2.description)
24967
+ };
24968
+ }).filter(Boolean);
24969
+ }
24970
+ function normalizeAnthropicModels(raw) {
24971
+ return (raw.data ?? []).map((item) => {
24972
+ const object2 = item;
24973
+ const id = stringValue4(object2.id);
24974
+ if (!id) return void 0;
24975
+ return {
24976
+ name: stringValue4(object2.display_name) ?? id,
24977
+ displayName: stringValue4(object2.display_name) ?? id,
24978
+ model: id,
24979
+ description: stringValue4(object2.description),
24980
+ source: "api"
24981
+ };
24982
+ }).filter(Boolean);
24983
+ }
24984
+ function normalizeOllamaTags(raw) {
24985
+ const items = Array.isArray(raw.models) ? raw.models : [];
24986
+ return items.map((item) => {
24987
+ const object2 = item;
24988
+ const id = stringValue4(object2.name) ?? stringValue4(object2.model);
24989
+ if (!id) return void 0;
24990
+ return { name: id, displayName: id, model: id };
24991
+ }).filter(Boolean);
24992
+ }
24993
+ function normalizeCodexModels(raw) {
24994
+ const items = Array.isArray(raw.models) ? raw.models : [];
24995
+ return items.map((item, index) => {
24996
+ const object2 = item;
24997
+ const id = stringValue4(object2.slug) ?? stringValue4(object2.id) ?? stringValue4(object2.model);
24998
+ if (!id) return void 0;
24999
+ return {
25000
+ name: stringValue4(object2.display_name) ?? id,
25001
+ displayName: stringValue4(object2.display_name) ?? id,
25002
+ model: id,
25003
+ description: stringValue4(object2.description),
25004
+ isDefault: index === 0
25005
+ };
25006
+ }).filter(Boolean);
25007
+ }
25008
+ function authHeadersForOpenAICompatible(providerConfig, apiKey) {
25009
+ const auth = inferOpenAICompatibleAuth(providerConfig.baseURL, providerConfig.auth);
25010
+ const type = auth.type ?? "bearer";
25011
+ const header = auth.header ?? (type === "api-key" ? "api-key" : "authorization");
25012
+ return { [header]: type === "bearer" ? `Bearer ${apiKey}` : apiKey };
25013
+ }
25014
+ async function fetchJson(url, options) {
25015
+ const response = await (options.fetcher ?? fetch)(url, {
25016
+ method: "GET",
25017
+ headers: {
25018
+ accept: "application/json",
25019
+ ...options.headers ?? {}
25020
+ },
25021
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
25022
+ });
25023
+ const text = await response.text();
25024
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
25025
+ return text ? JSON.parse(text) : {};
25026
+ }
25027
+ function cachePath(cacheKey) {
25028
+ return path15.join(os9.homedir(), ".demian", "cache", "models", `${safeCacheName(cacheKey)}.json`);
25029
+ }
25030
+ async function readCatalogCache(cacheKey, ttlMs, allow) {
25031
+ if (!allow) return void 0;
25032
+ try {
25033
+ const raw = JSON.parse(await readFile8(cachePath(cacheKey), "utf8"));
25034
+ if (!raw.result) return void 0;
25035
+ if (ttlMs !== Number.POSITIVE_INFINITY && Date.now() - (raw.savedAt ?? 0) > ttlMs) return void 0;
25036
+ return { ...raw.result, source: "cache", models: raw.result.models.map((model) => ({ ...model, source: "cache" })) };
25037
+ } catch {
25038
+ return void 0;
25039
+ }
25040
+ }
25041
+ async function writeCatalogCache(cacheKey, result) {
25042
+ const filePath = cachePath(cacheKey);
25043
+ await mkdir7(path15.dirname(filePath), { recursive: true, mode: 448 });
25044
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
25045
+ await writeFile6(temp, JSON.stringify({ savedAt: Date.now(), result }, null, 2), { mode: 384 });
25046
+ await rename5(temp, filePath);
25047
+ }
25048
+ function safeCacheName(providerName) {
25049
+ return providerName.replace(/[^a-zA-Z0-9_.-]+/g, "_");
25050
+ }
25051
+ function catalogCacheKey(providerName, providerConfig) {
25052
+ const identity = {
25053
+ providerName,
25054
+ type: providerConfig.type,
25055
+ catalogType: providerConfig.catalog?.type,
25056
+ endpoint: providerConfig.catalog?.endpoint,
25057
+ baseURL: "baseURL" in providerConfig ? providerConfig.baseURL : void 0,
25058
+ apiKeyEnv: "apiKeyEnv" in providerConfig ? providerConfig.apiKeyEnv : void 0,
25059
+ apiKeyEnvAliases: "apiKeyEnvAliases" in providerConfig ? providerConfig.apiKeyEnvAliases : void 0,
25060
+ auth: "auth" in providerConfig ? providerConfig.auth : void 0
25061
+ };
25062
+ const digest = crypto5.createHash("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 16);
25063
+ return `${providerName}-${digest}`;
25064
+ }
25065
+ function missingCatalogAuth(providerConfig) {
25066
+ if (providerConfig.type !== "openai-compatible" && providerConfig.type !== "anthropic" && providerConfig.type !== "ollama") return void 0;
25067
+ if (providerConfig.catalog?.requiresAuth === false) return void 0;
25068
+ const expectsAuth = providerConfig.catalog?.requiresAuth === true || !!providerConfig.apiKey || !!providerConfig.apiKeyEnv || !!providerConfig.apiKeyEnvAliases?.length;
25069
+ if (!expectsAuth) return void 0;
25070
+ const apiKey = resolveConfiguredApiKey(providerConfig);
25071
+ return apiKey.value ? void 0 : apiKey.envName ?? "apiKey";
25072
+ }
25073
+ function stringValue4(value) {
25074
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
25075
+ }
25076
+ function missingAuth(envName) {
25077
+ const error = new Error(`missing-auth:${envName}`);
25078
+ return error;
25079
+ }
25080
+ function isMissingAuthError(error) {
25081
+ if (error instanceof CodexAuthError) {
25082
+ 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";
25083
+ }
25084
+ return error instanceof Error && error.message.startsWith("missing-auth:");
25085
+ }
25086
+ function errorMessage2(error) {
25087
+ return error instanceof Error ? error.message : String(error);
25088
+ }
25089
+ async function codexClientVersion(override) {
25090
+ const normalized = semverLike(override);
25091
+ if (normalized) return normalized;
25092
+ packageVersionPromise ??= readPackageVersion();
25093
+ return packageVersionPromise;
25094
+ }
25095
+ async function readPackageVersion() {
25096
+ try {
25097
+ const raw = JSON.parse(await readFile8(new URL("../package.json", import.meta.url), "utf8"));
25098
+ return semverLike(raw.version) ?? DEFAULT_CODEX_CLIENT_VERSION;
25099
+ } catch {
25100
+ return DEFAULT_CODEX_CLIENT_VERSION;
25101
+ }
25102
+ }
25103
+ function semverLike(value) {
25104
+ if (typeof value !== "string") return void 0;
25105
+ const trimmed = value.trim();
25106
+ return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(trimmed) ? trimmed : void 0;
25107
+ }
25108
+ function withTimeout(promise, timeoutMs) {
25109
+ return new Promise((resolve, reject) => {
25110
+ const timer = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
25111
+ promise.then(
25112
+ (value) => {
25113
+ clearTimeout(timer);
25114
+ resolve(value);
25115
+ },
25116
+ (error) => {
25117
+ clearTimeout(timer);
25118
+ reject(error);
25119
+ }
25120
+ );
25121
+ });
25122
+ }
25123
+
25124
+ // src/config-command.ts
25125
+ async function maybeRunConfigCommand(argv) {
25126
+ if (argv[0] === "config") return runConfigCommand(argv.slice(1));
25127
+ if (argv[0] === "auth") return runAuthCommand(argv.slice(1));
25128
+ return void 0;
25129
+ }
25130
+ async function runConfigCommand(argv) {
25131
+ const [command, ...rest] = argv;
25132
+ try {
25133
+ if (!command || command === "help" || command === "--help" || command === "-h") {
25134
+ printConfigHelp();
25135
+ return 0;
25136
+ }
25137
+ if (command === "init") {
25138
+ const flags = parseFlags(rest);
25139
+ const force = boolFlag(flags, "force") || await confirmOverwriteIfExists(stringFlag(flags, "path"), boolFlag(flags, "print"));
25140
+ const result = await createUserConfig({
25141
+ path: stringFlag(flags, "path"),
25142
+ force,
25143
+ print: boolFlag(flags, "print"),
25144
+ defaultProvider: stringFlag(flags, "provider")
25145
+ });
25146
+ if (boolFlag(flags, "print")) {
25147
+ process.stdout.write(result.content);
25148
+ } else if (result.existed && !result.created) {
25149
+ process.stdout.write(`Config already exists: ${result.path}
25150
+ `);
25151
+ } else {
25152
+ process.stdout.write(`Created config: ${result.path}
25153
+ `);
25154
+ }
25155
+ return 0;
25156
+ }
25157
+ if (command === "path") {
25158
+ process.stdout.write(`${defaultUserConfigPath()}
25159
+ `);
25160
+ return 0;
25161
+ }
25162
+ if (command === "setup") {
25163
+ return runSetupWizard();
25164
+ }
25165
+ if (command === "list") {
25166
+ const flags = parseFlags(rest);
25167
+ const config = await loadConfig({ cwd: process.cwd(), configPath: stringFlag(flags, "config") });
25168
+ process.stdout.write(`defaultProvider: ${config.defaultProvider}
25169
+ `);
25170
+ for (const name of Object.keys(config.providers)) {
25171
+ const provider = config.providers[name];
25172
+ if (provider.hidden) continue;
25173
+ process.stdout.write(`- ${name} (${provider.type})
25174
+ `);
25175
+ }
25176
+ return 0;
25177
+ }
25178
+ if (command === "models") {
25179
+ const providerName = rest.find((item) => !item.startsWith("-"));
25180
+ if (!providerName) throw new Error("config models requires a provider name.");
25181
+ const flags = parseFlags(rest.filter((item) => item !== providerName));
25182
+ const config = await loadConfig({ cwd: process.cwd(), configPath: stringFlag(flags, "config") });
25183
+ const provider = resolveProviderConfig(config, providerName);
25184
+ const result = await listProviderModelCatalog(providerName, provider, { refresh: boolFlag(flags, "refresh") });
25185
+ if (result.status !== "ready") {
25186
+ process.stdout.write(`${providerName}: ${result.status}${result.message ? ` (${result.message})` : ""}
25187
+ `);
25188
+ return result.status === "missing-auth" ? 2 : 1;
25189
+ }
25190
+ for (const model of result.models) {
25191
+ const label = model.displayName && model.displayName !== model.model ? `${model.displayName} (${model.model})` : model.model;
25192
+ process.stdout.write(`${model.isDefault ? "* " : " "}${label}
25193
+ `);
25194
+ }
25195
+ return 0;
25196
+ }
25197
+ if (command === "add-provider") {
25198
+ const [presetOrName, ...tail2] = rest;
25199
+ if (!presetOrName) throw new Error("config add-provider requires a provider name or preset.");
25200
+ const flags = parseFlags(tail2);
25201
+ const name = stringFlag(flags, "name") ?? presetOrName;
25202
+ const force = await ensureOverwriteForProvider(stringFlag(flags, "path"), name, boolFlag(flags, "force"));
25203
+ const result = await addProvider({
25204
+ path: stringFlag(flags, "path"),
25205
+ name,
25206
+ preset: presetOrName,
25207
+ type: stringFlag(flags, "type") ?? presetOrName,
25208
+ baseURL: stringFlag(flags, "base-url"),
25209
+ apiKey: await apiKeyFromFlags(flags),
25210
+ apiKeyEnv: stringFlag(flags, "api-key-env"),
25211
+ authHeader: stringFlag(flags, "auth-header"),
25212
+ force
25213
+ });
25214
+ process.stdout.write(`Updated config: ${result.path}
25215
+ `);
25216
+ return 0;
25217
+ }
25218
+ if (command === "add-model") {
25219
+ const [provider, ...tail2] = rest;
25220
+ if (!provider) throw new Error("config add-model requires a provider name.");
25221
+ const flags = parseFlags(tail2);
25222
+ const name = requiredStringFlag(flags, "name");
25223
+ const model = requiredStringFlag(flags, "model");
25224
+ const displayName = stringFlag(flags, "display-name");
25225
+ const force = await ensureOverwriteForModel(stringFlag(flags, "path"), provider, name, displayName, boolFlag(flags, "force"));
25226
+ const result = await addModelProfile({
25227
+ path: stringFlag(flags, "path"),
25228
+ provider,
25229
+ name,
25230
+ displayName,
25231
+ model,
25232
+ baseURL: stringFlag(flags, "base-url"),
25233
+ apiKey: await apiKeyFromFlags(flags),
25234
+ apiKeyEnv: stringFlag(flags, "api-key-env"),
25235
+ force
25236
+ });
25237
+ process.stdout.write(`Updated config: ${result.path}
25238
+ `);
25239
+ return 0;
25240
+ }
25241
+ throw new Error(`Unknown config command: ${command}`);
25242
+ } catch (error) {
25243
+ process.stderr.write(`${errorMessage3(error)}
25244
+ `);
25245
+ return 1;
25246
+ }
25247
+ }
25248
+ async function runAuthCommand(argv) {
25249
+ const [command, provider] = argv;
25250
+ try {
25251
+ if (!command || command === "help" || command === "--help" || command === "-h") {
25252
+ printAuthHelp();
25253
+ return 0;
25254
+ }
25255
+ if (command === "status") {
25256
+ const claude = await preflightClaudeCodeAuth({ sanitizeApiKeyEnv: true }).catch((error) => ({ ok: false, source: "unknown", warning: errorMessage3(error) }));
25257
+ process.stdout.write(`claudecode: ${claude.ok ? "ok" : "not ready"} (${claude.source})${claude.warning ? ` - ${claude.warning}` : ""}
25258
+ `);
25259
+ const codex = await checkCodexAuth();
25260
+ process.stdout.write(`codex: ${codex.ok ? "ok" : "not ready"} (${codex.source})${codex.message ? ` - ${codex.message}` : ""}
25261
+ `);
25262
+ return claude.ok && codex.ok ? 0 : 1;
25263
+ }
25264
+ if (command !== "login") throw new Error(`Unknown auth command: ${command}`);
25265
+ return runProviderLogin(provider);
25266
+ } catch (error) {
25267
+ process.stderr.write(`${errorMessage3(error)}
25268
+ `);
25269
+ return 1;
25270
+ }
25271
+ }
25272
+ async function runProviderLogin(provider) {
25273
+ if (provider === "codex") {
25274
+ if (!await commandExists("codex")) {
25275
+ 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");
25276
+ return 1;
25277
+ }
25278
+ return runLogin("codex", ["login"]);
25279
+ }
25280
+ if (provider === "claudecode") {
25281
+ const cli = resolveClaudeCodeCliPath();
25282
+ if (!cli && !await commandExists("claude")) {
25283
+ 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");
25284
+ return 1;
25285
+ }
25286
+ return runLogin(cli ?? "claude", ["setup-token"]);
25287
+ }
25288
+ throw new Error("auth login requires provider codex or claudecode.");
25289
+ }
25290
+ function runLogin(command, args) {
25291
+ process.stdout.write(`Starting ${[command, ...args].join(" ")}...
25292
+ `);
25293
+ const opened = /* @__PURE__ */ new Set();
25294
+ return new Promise((resolve) => {
25295
+ const child = spawn4(command, args, { stdio: ["inherit", "pipe", "pipe"], env: process.env });
25296
+ const onText = (stream) => (chunk) => {
25297
+ const text = chunk.toString();
25298
+ stream.write(text);
25299
+ for (const url of urlsInText(text)) {
25300
+ if (opened.has(url)) continue;
25301
+ opened.add(url);
25302
+ void openUrl(url);
25303
+ }
25304
+ };
25305
+ child.stdout.on("data", onText(process.stdout));
25306
+ child.stderr.on("data", onText(process.stderr));
25307
+ child.on("error", (error) => {
25308
+ process.stderr.write(`${error.message}
25309
+ `);
25310
+ resolve(1);
25311
+ });
25312
+ child.on("close", (code) => resolve(code ?? 1));
25313
+ });
25314
+ }
25315
+ function parseFlags(argv) {
25316
+ const flags = /* @__PURE__ */ new Map();
25317
+ for (let i = 0; i < argv.length; i++) {
25318
+ const arg = argv[i];
25319
+ if (!arg.startsWith("--")) continue;
25320
+ const key = arg.slice(2);
25321
+ const next = argv[i + 1];
25322
+ if (!next || next.startsWith("--")) {
25323
+ flags.set(key, true);
25324
+ } else {
25325
+ flags.set(key, next);
25326
+ i++;
25327
+ }
25328
+ }
25329
+ return flags;
25330
+ }
25331
+ function stringFlag(flags, name) {
25332
+ const value = flags.get(name);
25333
+ return typeof value === "string" && value.trim() ? value : void 0;
25334
+ }
25335
+ function requiredStringFlag(flags, name) {
25336
+ const value = stringFlag(flags, name);
25337
+ if (!value) throw new Error(`--${name} is required.`);
25338
+ return value;
25339
+ }
25340
+ function boolFlag(flags, name) {
25341
+ return flags.get(name) === true;
25342
+ }
25343
+ async function apiKeyFromFlags(flags) {
25344
+ const inline = stringFlag(flags, "api-key");
25345
+ if (inline) return inline;
25346
+ if (!boolFlag(flags, "api-key-stdin")) return void 0;
25347
+ return (await readStdin()).trim();
25348
+ }
25349
+ async function readStdin() {
25350
+ let value = "";
25351
+ for await (const chunk of process.stdin) value += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
25352
+ return value;
25353
+ }
25354
+ function urlsInText(text) {
25355
+ return [...text.matchAll(/https?:\/\/[^\s)>"']+/g)].map((match) => match[0]);
25356
+ }
25357
+ function openUrl(url) {
25358
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
25359
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
25360
+ return new Promise((resolve) => {
25361
+ const child = spawn4(command, args, { stdio: "ignore", detached: true });
25362
+ child.on("error", () => resolve());
25363
+ child.on("spawn", () => {
25364
+ child.unref();
25365
+ resolve();
25366
+ });
25367
+ });
25368
+ }
25369
+ function printConfigHelp() {
25370
+ process.stdout.write(`demian config
25371
+
25372
+ Usage:
25373
+ demian config init [--path <path>] [--force] [--print] [--provider <name>]
25374
+ demian config setup interactive multi-step provider wizard
25375
+ demian config add-provider <preset|openai-compatible> [--name <name>] [--base-url <url>] [--api-key-env <env>] [--api-key-stdin] [--auth-header <header>] [--force]
25376
+ demian config add-model <provider> --name <name> --model <model> [--display-name <label>] [--base-url <url>] [--api-key-env <env>] [--api-key-stdin] [--force]
25377
+ demian config models <provider> [--refresh] [--config <path>]
25378
+ demian config list [--config <path>]
25379
+ demian config path
25380
+ `);
25381
+ }
25382
+ function printAuthHelp() {
25383
+ process.stdout.write(`demian auth
25384
+
25385
+ Usage:
25386
+ demian auth login codex
25387
+ demian auth login claudecode
25388
+ demian auth status
25389
+ `);
25390
+ }
25391
+ function errorMessage3(error) {
25392
+ return error instanceof Error ? error.message : String(error);
25393
+ }
25394
+ async function confirmOverwriteIfExists(customPath, isPrint) {
25395
+ if (isPrint) return false;
25396
+ const filePath = customPath ?? defaultUserConfigPath();
25397
+ if (!await fileExists2(filePath)) return false;
25398
+ return promptYesNo(`Overwrite existing config at ${filePath}? (y/N): `, false);
25399
+ }
25400
+ async function ensureOverwriteForProvider(customPath, providerName, force) {
25401
+ if (force) return true;
25402
+ const filePath = customPath ?? defaultUserConfigPath();
25403
+ if (!await fileExists2(filePath)) return false;
25404
+ const config = await readJsonSafe(filePath);
25405
+ const provider = config?.providers?.[providerName];
25406
+ if (!provider) return false;
25407
+ return promptYesNo(`Provider "${providerName}" already exists. Overwrite? (y/N): `, false);
25408
+ }
25409
+ async function ensureOverwriteForModel(customPath, providerName, profileName, displayName, force) {
25410
+ if (force) return true;
25411
+ const filePath = customPath ?? defaultUserConfigPath();
25412
+ if (!await fileExists2(filePath)) return false;
25413
+ const config = await readJsonSafe(filePath);
25414
+ const profiles = Array.isArray(config?.providers?.[providerName]?.modelProfiles) ? config.providers[providerName].modelProfiles : [];
25415
+ const sameName = profiles.find((entry) => entry.name === profileName);
25416
+ const sameDisplay = displayName ? profiles.find((entry) => entry.displayName === displayName && entry.name !== profileName) : void 0;
25417
+ if (!sameName && !sameDisplay) return false;
25418
+ const which = sameName ? `name "${profileName}"` : `displayName "${displayName}"`;
25419
+ return promptYesNo(`Profile with ${which} already exists on provider ${providerName}. Overwrite? (y/N): `, false);
25420
+ }
25421
+ async function fileExists2(filePath) {
25422
+ try {
25423
+ await stat4(filePath);
25424
+ return true;
25425
+ } catch {
25426
+ return false;
25427
+ }
25428
+ }
25429
+ async function readJsonSafe(filePath) {
25430
+ try {
25431
+ const { readFile: readFile18 } = await import("node:fs/promises");
25432
+ const text = await readFile18(filePath, "utf8");
25433
+ return JSON.parse(text);
25434
+ } catch {
25435
+ return void 0;
25436
+ }
25437
+ }
25438
+ async function promptYesNo(question, defaultYes) {
25439
+ if (!process.stdin.isTTY) {
25440
+ process.stderr.write(`${question.replace(/\(y\/N\)|\(Y\/n\)/, "").trim()} (non-interactive; pass --force to overwrite).
25441
+ `);
25442
+ return false;
25443
+ }
25444
+ const rl2 = readline3.createInterface({ input: process.stdin, output: process.stdout });
25445
+ try {
25446
+ const answer = (await rl2.question(question)).trim().toLowerCase();
25447
+ if (!answer) return defaultYes;
25448
+ return answer === "y" || answer === "yes";
25449
+ } finally {
25450
+ rl2.close();
25451
+ }
25452
+ }
25453
+ async function commandExists(command) {
25454
+ return new Promise((resolve) => {
25455
+ const child = spawn4(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" });
25456
+ child.on("close", (code) => resolve(code === 0));
25457
+ child.on("error", () => resolve(false));
25458
+ });
25459
+ }
25460
+ async function runFirstRunWizard() {
25461
+ const filePath = defaultUserConfigPath();
25462
+ if (await fileExists2(filePath)) return void 0;
25463
+ if (!process.stdin.isTTY) {
25464
+ process.stderr.write(`No Demian config at ${filePath}. Run \`demian config init\` to create one.
25465
+ `);
25466
+ return void 0;
25467
+ }
25468
+ process.stdout.write(`
25469
+ Welcome to Demian.
25470
+ No config found at ${filePath}.
25471
+ `);
25472
+ const ok2 = await promptYesNo("Create a default v2 config now? (Y/n): ", true);
25473
+ if (!ok2) {
25474
+ process.stdout.write("Skipped. Run `demian config init` later to set up.\n");
25475
+ return void 0;
25476
+ }
25477
+ const result = await createUserConfig({});
25478
+ process.stdout.write(`Created ${result.path}.
25479
+ `);
25480
+ const walkProviders = await promptYesNo("Walk through provider setup now? (y/N): ", false);
25481
+ if (walkProviders) await walkProviderSetup();
25482
+ else {
25483
+ process.stdout.write("Next steps:\n");
25484
+ process.stdout.write(" - Set API key env vars (e.g. OPENAI_API_KEY)\n");
25485
+ process.stdout.write(" - `demian config models <provider> --refresh` to inspect catalogs\n");
25486
+ process.stdout.write(" - `demian auth login codex|claudecode` for OAuth providers\n");
25487
+ process.stdout.write(" - `demian config setup` to re-enter this wizard\n\n");
25488
+ }
25489
+ return { created: true, configPath: result.path };
25490
+ }
25491
+ async function runSetupWizard() {
25492
+ if (!process.stdin.isTTY) {
25493
+ process.stderr.write("config setup requires an interactive terminal.\n");
25494
+ return 1;
25495
+ }
25496
+ const filePath = defaultUserConfigPath();
25497
+ if (!await fileExists2(filePath)) {
25498
+ const wizard = await runFirstRunWizard();
25499
+ return wizard?.created ? 0 : 1;
25500
+ }
25501
+ await walkProviderSetup();
25502
+ return 0;
25503
+ }
25504
+ async function walkProviderSetup() {
25505
+ const builtinPresets = [
25506
+ { name: "openai", description: "OpenAI ChatGPT API", defaultEnv: "OPENAI_API_KEY" },
25507
+ { name: "anthropic", description: "Anthropic Claude API", defaultEnv: "ANTHROPIC_API_KEY" },
25508
+ { name: "gemini", description: "Google Gemini (OpenAI-compatible)", defaultEnv: "GEMINI_API_KEY" },
25509
+ { name: "groq", description: "Groq Cloud", defaultEnv: "GROQ_API_KEY" },
25510
+ { name: "codex", description: "ChatGPT Codex (OAuth)", oauth: "codex" },
25511
+ { name: "claudecode", description: "Claude Code external runtime (OAuth)", oauth: "claudecode" }
25512
+ ];
25513
+ process.stdout.write("\nProvider setup. Press Enter to skip a provider.\n");
25514
+ for (const preset of builtinPresets) {
25515
+ const want = await promptYesNo(`Configure ${preset.name} (${preset.description})? (y/N): `, false);
25516
+ if (!want) continue;
25517
+ if (preset.oauth) {
25518
+ const login = await promptYesNo(`Run \`demian auth login ${preset.oauth}\` now? (Y/n): `, true);
25519
+ if (login) await runProviderLogin(preset.oauth).catch(() => void 0);
25520
+ continue;
25521
+ }
25522
+ if (preset.defaultEnv) {
25523
+ const envSet = !!process.env[preset.defaultEnv];
25524
+ process.stdout.write(` env ${preset.defaultEnv}: ${envSet ? "set" : "not set"}
25525
+ `);
25526
+ if (!envSet) process.stdout.write(` \u2192 export ${preset.defaultEnv}=... before running demian.
25527
+ `);
25528
+ }
25529
+ }
25530
+ process.stdout.write("\nSetup complete. Run `demian` to start a session.\n\n");
25531
+ }
25532
+ async function checkCodexAuth() {
25533
+ const candidates = [path16.join(process.env.HOME ?? os10.homedir(), ".codex", "auth.json"), path16.join(process.env.HOME ?? os10.homedir(), ".codex", "credentials.json")];
25534
+ for (const candidate of candidates) {
25535
+ if (await fileExists2(candidate)) {
25536
+ return { ok: true, source: "codex-store", message: candidate };
25537
+ }
25538
+ }
25539
+ if (process.env.OPENAI_API_KEY) return { ok: true, source: "api-key-fallback", message: "OPENAI_API_KEY set" };
25540
+ return { ok: false, source: "none", message: "no codex auth file or OPENAI_API_KEY; run `demian auth login codex`" };
25541
+ }
25542
+
25543
+ // src/doctor/policies.ts
25544
+ import { existsSync as existsSync2 } from "node:fs";
25545
+ import { readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
25546
+ import os11 from "node:os";
25547
+ import path17 from "node:path";
25548
+ function isDoctorCommand(argv) {
25549
+ return argv[0] === "doctor";
25550
+ }
25551
+ async function maybeRunDoctorCommand(argv, options = {}) {
25552
+ if (!isDoctorCommand(argv)) return void 0;
25553
+ return runDoctorCommand(argv.slice(1), options);
25554
+ }
25555
+ async function runDoctorCommand(argv, options = {}) {
25556
+ const stdout = options.stdout ?? process.stdout;
25557
+ const stderr = options.stderr ?? process.stderr;
25558
+ const flags = parseDoctorFlags(argv);
25559
+ if (flags.help) {
25560
+ stdout.write(doctorHelp());
25561
+ return 0;
25562
+ }
25563
+ if (argv[0] !== "policies" || !argv.includes("--upgrade-namespaces")) {
24176
25564
  stderr.write("Unsupported doctor command. Run `demian doctor policies --upgrade-namespaces [--write]`.\n");
24177
25565
  return 1;
24178
25566
  }
24179
- const cwd = path13.resolve(flags.cwd ?? options.cwd ?? process.cwd());
25567
+ const cwd = path17.resolve(flags.cwd ?? options.cwd ?? process.cwd());
24180
25568
  const filePaths = resolvePolicyConfigPaths(cwd, flags.configPaths);
24181
25569
  if (filePaths.length === 0) {
24182
25570
  stdout.write("No demian config files found to inspect.\n");
@@ -24185,11 +25573,11 @@ async function runDoctorCommand(argv, options = {}) {
24185
25573
  const allChanges = [];
24186
25574
  let updatedFiles = 0;
24187
25575
  for (const filePath of filePaths) {
24188
- const original = await readFile7(filePath, "utf8");
25576
+ const original = await readFile9(filePath, "utf8");
24189
25577
  const upgraded = upgradePolicyNamespacesInText(original, filePath);
24190
25578
  allChanges.push(...upgraded.changes);
24191
25579
  if (flags.write && upgraded.changes.length > 0) {
24192
- await writeFile5(filePath, upgraded.text, "utf8");
25580
+ await writeFile7(filePath, upgraded.text, "utf8");
24193
25581
  updatedFiles++;
24194
25582
  }
24195
25583
  }
@@ -24277,11 +25665,11 @@ function parseDoctorFlags(argv) {
24277
25665
  return flags;
24278
25666
  }
24279
25667
  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")
25668
+ const candidates = explicitPaths.length > 0 ? explicitPaths.map((item) => path17.resolve(cwd, item)) : [
25669
+ path17.join(os11.homedir(), ".demian", "config.json"),
25670
+ path17.join(os11.homedir(), ".demian", "config.jsond"),
25671
+ path17.join(cwd, ".demian", "config.json"),
25672
+ path17.join(cwd, ".demian", "config.jsond")
24285
25673
  ];
24286
25674
  return [...new Set(candidates)].filter((item) => existsSync2(item));
24287
25675
  }
@@ -24377,8 +25765,9 @@ Flags:
24377
25765
  }
24378
25766
 
24379
25767
  // src/transcript.ts
24380
- import { mkdir as mkdir6, appendFile, writeFile as writeFile6 } from "node:fs/promises";
24381
- import path14 from "node:path";
25768
+ import { mkdir as mkdir8, appendFile, writeFile as writeFile8 } from "node:fs/promises";
25769
+ import os12 from "node:os";
25770
+ import path18 from "node:path";
24382
25771
  var TranscriptWriter = class {
24383
25772
  filePath;
24384
25773
  #enabled;
@@ -24386,9 +25775,9 @@ var TranscriptWriter = class {
24386
25775
  #queue = Promise.resolve();
24387
25776
  constructor(options) {
24388
25777
  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();
25778
+ const dir = path18.join(options.storageDir ?? defaultDemianStorageDir(), "transcripts", options.sessionId);
25779
+ this.filePath = path18.join(dir, "session.jsonl");
25780
+ this.#ready = this.#enabled ? mkdir8(dir, { recursive: true }).then(() => writeFile8(this.filePath, "", { flag: "a" })) : Promise.resolve();
24392
25781
  }
24393
25782
  write(event) {
24394
25783
  if (!this.#enabled) return Promise.resolve();
@@ -24403,12 +25792,15 @@ var TranscriptWriter = class {
24403
25792
  await this.#queue;
24404
25793
  }
24405
25794
  };
25795
+ function defaultDemianStorageDir() {
25796
+ return path18.join(os12.homedir(), ".demian");
25797
+ }
24406
25798
 
24407
25799
  // src/external-runtime/snapshot-diff.ts
24408
25800
  import { createHash as createHash2 } from "node:crypto";
24409
25801
  import { createReadStream } from "node:fs";
24410
- import { readdir, readFile as readFile8, stat as stat3 } from "node:fs/promises";
24411
- import path15 from "node:path";
25802
+ import { readdir, readFile as readFile10, stat as stat5 } from "node:fs/promises";
25803
+ import path19 from "node:path";
24412
25804
 
24413
25805
  // src/workspace/diff.ts
24414
25806
  var CONTEXT_LINES = 3;
@@ -24618,7 +26010,7 @@ async function diffWorkspaceSnapshot(before) {
24618
26010
  }
24619
26011
  async function walk(root, relativeDir, entries, options) {
24620
26012
  if (entries.size >= options.maxFiles) return;
24621
- const absoluteDir = path15.join(root, relativeDir);
26013
+ const absoluteDir = path19.join(root, relativeDir);
24622
26014
  let items;
24623
26015
  try {
24624
26016
  items = await readdir(absoluteDir, { withFileTypes: true });
@@ -24629,8 +26021,8 @@ async function walk(root, relativeDir, entries, options) {
24629
26021
  if (entries.size >= options.maxFiles) return;
24630
26022
  if (item.name.startsWith(".") && item.name !== ".env.example" && SKIP_DIRS.has(item.name)) continue;
24631
26023
  if (SKIP_DIRS.has(item.name)) continue;
24632
- const relativePath = normalizeRelativePath(path15.join(relativeDir, item.name));
24633
- const absolutePath = path15.join(root, relativePath);
26024
+ const relativePath = normalizeRelativePath(path19.join(relativeDir, item.name));
26025
+ const absolutePath = path19.join(root, relativePath);
24634
26026
  if (item.isDirectory()) {
24635
26027
  await walk(root, relativePath, entries, options);
24636
26028
  continue;
@@ -24641,7 +26033,7 @@ async function walk(root, relativeDir, entries, options) {
24641
26033
  }
24642
26034
  }
24643
26035
  async function snapshotFile(filePath, relativePath, maxTextBytes) {
24644
- const info = await stat3(filePath);
26036
+ const info = await stat5(filePath);
24645
26037
  const sha2562 = await sha256File(filePath);
24646
26038
  const content = info.size <= maxTextBytes ? await readTextContent(filePath) : void 0;
24647
26039
  return { path: relativePath, size: info.size, sha256: sha2562, content };
@@ -24652,7 +26044,7 @@ function snapshotDiffText(entry) {
24652
26044
  `;
24653
26045
  }
24654
26046
  async function readTextContent(filePath) {
24655
- const buffer = await readFile8(filePath);
26047
+ const buffer = await readFile10(filePath);
24656
26048
  if (buffer.includes(0)) return void 0;
24657
26049
  return buffer.toString("utf8");
24658
26050
  }
@@ -24666,7 +26058,7 @@ function sha256File(filePath) {
24666
26058
  });
24667
26059
  }
24668
26060
  function normalizeRelativePath(value) {
24669
- return value.split(path15.sep).join("/");
26061
+ return value.split(path19.sep).join("/");
24670
26062
  }
24671
26063
 
24672
26064
  // src/external-runtime/session-runner.ts
@@ -25101,15 +26493,15 @@ function safeJson(value) {
25101
26493
  }
25102
26494
 
25103
26495
  // src/hooks/dispatcher.ts
25104
- import path17 from "node:path";
26496
+ import path21 from "node:path";
25105
26497
 
25106
26498
  // src/hooks/command.ts
25107
- import { spawn as spawn4 } from "node:child_process";
26499
+ import { spawn as spawn5 } from "node:child_process";
25108
26500
  async function runCommandHook(hook, ctx) {
25109
26501
  if (!hook.command) return void 0;
25110
26502
  const timeoutMs = hook.timeoutMs ?? 1e4;
25111
26503
  return new Promise((resolve, reject) => {
25112
- const child = spawn4(hook.command, {
26504
+ const child = spawn5(hook.command, {
25113
26505
  cwd: ctx.cwd,
25114
26506
  env: process.env,
25115
26507
  shell: true,
@@ -25194,7 +26586,7 @@ var blockDangerousBashHook = {
25194
26586
  };
25195
26587
 
25196
26588
  // src/hooks/builtin/protect-env-files.ts
25197
- import path16 from "node:path";
26589
+ import path20 from "node:path";
25198
26590
  var protectEnvFilesHook = {
25199
26591
  name: "protect-env-files",
25200
26592
  event: "PreToolUse",
@@ -25202,7 +26594,7 @@ var protectEnvFilesHook = {
25202
26594
  run(ctx) {
25203
26595
  if (ctx.toolName !== "write_file" && ctx.toolName !== "edit_file") return { decision: "allow" };
25204
26596
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
25205
- const filePath = typeof input2.path === "string" ? path16.resolve(ctx.cwd, input2.path) : "";
26597
+ const filePath = typeof input2.path === "string" ? path20.resolve(ctx.cwd, input2.path) : "";
25206
26598
  if (filePath && isEnvFile(filePath)) {
25207
26599
  return {
25208
26600
  decision: "block",
@@ -25239,7 +26631,7 @@ var maskSecretsHook = {
25239
26631
  };
25240
26632
 
25241
26633
  // src/hooks/builtin/inject-env-info.ts
25242
- import os9 from "node:os";
26634
+ import os13 from "node:os";
25243
26635
  var injectEnvInfoHook = {
25244
26636
  name: "inject-env-info",
25245
26637
  event: "SessionStart",
@@ -25248,7 +26640,7 @@ var injectEnvInfoHook = {
25248
26640
  decision: "allow",
25249
26641
  patch: {
25250
26642
  systemNote: [
25251
- `Environment: ${os9.type()} ${os9.release()} (${os9.platform()}/${os9.arch()})`,
26643
+ `Environment: ${os13.type()} ${os13.release()} (${os13.platform()}/${os13.arch()})`,
25252
26644
  `cwd: ${ctx.cwd}`,
25253
26645
  `provider: ${ctx.provider ?? "unknown"}`,
25254
26646
  `model: ${ctx.model ?? "unknown"}`,
@@ -25317,14 +26709,14 @@ function matchesHook(match, ctx) {
25317
26709
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
25318
26710
  const filePath = typeof input2.path === "string" ? input2.path : typeof input2.workdir === "string" ? input2.workdir : void 0;
25319
26711
  if (!filePath) return false;
25320
- if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path17.resolve(ctx.cwd, filePath)))) return false;
26712
+ if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path21.resolve(ctx.cwd, filePath)))) return false;
25321
26713
  }
25322
26714
  return true;
25323
26715
  }
25324
26716
 
25325
26717
  // src/multimodal.ts
25326
- import { readFile as readFile9, stat as stat4 } from "node:fs/promises";
25327
- import path18 from "node:path";
26718
+ import { readFile as readFile11, stat as stat6 } from "node:fs/promises";
26719
+ import path22 from "node:path";
25328
26720
  var DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
25329
26721
  async function buildUserContent(prompt, images = [], options) {
25330
26722
  if (images.length === 0) return prompt;
@@ -25343,16 +26735,16 @@ async function buildUserContent(prompt, images = [], options) {
25343
26735
  async function imageToUrl(input2, options) {
25344
26736
  if (/^https?:\/\//i.test(input2) || /^data:image\//i.test(input2)) return input2;
25345
26737
  const filePath = resolveInsideCwd(options.cwd, input2);
25346
- const info = await stat4(filePath);
26738
+ const info = await stat6(filePath);
25347
26739
  if (!info.isFile()) throw new Error(`Image input is not a file: ${input2}`);
25348
26740
  const maxImageBytes = options.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
25349
26741
  if (info.size > maxImageBytes) throw new Error(`Image is larger than ${maxImageBytes} bytes: ${input2}`);
25350
26742
  const mime = mimeFromPath(filePath);
25351
- const bytes = await readFile9(filePath);
26743
+ const bytes = await readFile11(filePath);
25352
26744
  return `data:${mime};base64,${bytes.toString("base64")}`;
25353
26745
  }
25354
26746
  function mimeFromPath(filePath) {
25355
- switch (path18.extname(filePath).toLowerCase()) {
26747
+ switch (path22.extname(filePath).toLowerCase()) {
25356
26748
  case ".jpg":
25357
26749
  case ".jpeg":
25358
26750
  return "image/jpeg";
@@ -25363,15 +26755,15 @@ function mimeFromPath(filePath) {
25363
26755
  case ".webp":
25364
26756
  return "image/webp";
25365
26757
  default:
25366
- throw new Error(`Unsupported image extension: ${path18.extname(filePath) || "(none)"}`);
26758
+ throw new Error(`Unsupported image extension: ${path22.extname(filePath) || "(none)"}`);
25367
26759
  }
25368
26760
  }
25369
26761
 
25370
26762
  // src/permissions/persistent-grants.ts
25371
- import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile7 } from "node:fs/promises";
26763
+ import { mkdir as mkdir9, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
25372
26764
  import fs8 from "node:fs";
25373
- import os10 from "node:os";
25374
- import path19 from "node:path";
26765
+ import os14 from "node:os";
26766
+ import path23 from "node:path";
25375
26767
  var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
25376
26768
  var PersistentGrantStore = class {
25377
26769
  filePath;
@@ -25402,22 +26794,22 @@ var PersistentGrantStore = class {
25402
26794
  }
25403
26795
  async #read() {
25404
26796
  if (!fs8.existsSync(this.filePath)) return { version: 1, grants: [] };
25405
- const data = JSON.parse(await readFile10(this.filePath, "utf8"));
26797
+ const data = JSON.parse(await readFile12(this.filePath, "utf8"));
25406
26798
  return {
25407
26799
  version: 1,
25408
26800
  grants: Array.isArray(data.grants) ? data.grants.filter(isGrantRecord) : []
25409
26801
  };
25410
26802
  }
25411
26803
  async #write(file) {
25412
- await mkdir7(path19.dirname(this.filePath), { recursive: true });
25413
- await writeFile7(this.filePath, `${JSON.stringify(file, null, 2)}
26804
+ await mkdir9(path23.dirname(this.filePath), { recursive: true });
26805
+ await writeFile9(this.filePath, `${JSON.stringify(file, null, 2)}
25414
26806
  `, { mode: 384 });
25415
26807
  }
25416
26808
  };
25417
26809
  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");
26810
+ if (config.path) return path23.resolve(cwd, config.path);
26811
+ if ((config.scope ?? "project") === "user") return path23.join(os14.homedir(), ".demian", "grants.json");
26812
+ return path23.join(cwd, ".demian", "grants.json");
25421
26813
  }
25422
26814
  function isGrantRecord(value) {
25423
26815
  if (!value || typeof value !== "object") return false;
@@ -25850,25 +27242,25 @@ function fail(content, metadata) {
25850
27242
  }
25851
27243
 
25852
27244
  // src/tools/output.ts
25853
- import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
25854
- import path20 from "node:path";
27245
+ import { mkdir as mkdir10, writeFile as writeFile10 } from "node:fs/promises";
27246
+ import path24 from "node:path";
25855
27247
  var DEFAULT_CAP_BYTES = 32 * 1024;
25856
27248
  var HALF_PREVIEW_BYTES = 16 * 1024;
25857
27249
  async function capToolOutput(result, options) {
25858
27250
  const capBytes = options.capBytes ?? DEFAULT_CAP_BYTES;
25859
27251
  const bytes = Buffer.byteLength(result.content, "utf8");
25860
27252
  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");
27253
+ const dir = path24.join(options.cwd, ".demian", "tmp");
27254
+ await mkdir10(dir, { recursive: true });
27255
+ const outputPath = path24.join(dir, `output-${safeCallId(options.callId)}.txt`);
27256
+ await writeFile10(outputPath, result.content, "utf8");
25865
27257
  return {
25866
27258
  ...result,
25867
27259
  content: [
25868
27260
  sliceUtf8(result.content, 0, HALF_PREVIEW_BYTES),
25869
27261
  `
25870
27262
 
25871
- [Full output saved to ${path20.relative(options.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
27263
+ [Full output saved to ${path24.relative(options.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
25872
27264
 
25873
27265
  `,
25874
27266
  sliceUtf8(result.content, Math.max(0, bytes - HALF_PREVIEW_BYTES), bytes)
@@ -25893,8 +27285,8 @@ function sliceUtf8(text, startByte, endByte) {
25893
27285
  }
25894
27286
 
25895
27287
  // src/tools/read-file.ts
25896
- import { open as open2, stat as stat5 } from "node:fs/promises";
25897
- import path21 from "node:path";
27288
+ import { open as open2, stat as stat7 } from "node:fs/promises";
27289
+ import path25 from "node:path";
25898
27290
 
25899
27291
  // src/tools/validation.ts
25900
27292
  function assertObject(input2, toolName) {
@@ -25964,7 +27356,7 @@ var readFileTool = {
25964
27356
  const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
25965
27357
  const offset = positiveInteger(optionalNumberField(object2, "offset"), 1, "offset");
25966
27358
  const limit = positiveInteger(optionalNumberField(object2, "limit"), DEFAULT_LIMIT, "limit");
25967
- const info = await stat5(filePath);
27359
+ const info = await stat7(filePath);
25968
27360
  if (info.isDirectory()) throw new Error(`read_file expected a file but got a directory: ${relativeToCwd(ctx.cwd, filePath)}`);
25969
27361
  if (info.size > MAX_BYTES) throw new Error(`File is larger than ${MAX_BYTES} bytes: ${relativeToCwd(ctx.cwd, filePath)}`);
25970
27362
  const sample = await readBytes(filePath, Math.min(SAMPLE_BYTES, info.size));
@@ -25978,7 +27370,7 @@ var readFileTool = {
25978
27370
  const truncated = start + sliced.length < lines.length;
25979
27371
  return ok(content + (truncated ? `
25980
27372
  ... (${lines.length - start - sliced.length} more lines)` : ""), {
25981
- path: path21.relative(ctx.cwd, filePath),
27373
+ path: path25.relative(ctx.cwd, filePath),
25982
27374
  lines: sliced.length,
25983
27375
  totalLines: lines.length,
25984
27376
  truncated
@@ -26016,8 +27408,8 @@ function looksBinary(buffer) {
26016
27408
  }
26017
27409
 
26018
27410
  // 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";
27411
+ import { mkdir as mkdir11, readFile as readFile13, writeFile as writeFile11 } from "node:fs/promises";
27412
+ import path26 from "node:path";
26021
27413
  var writeFileTool = {
26022
27414
  name: "write_file",
26023
27415
  description: "Create or replace a text file inside the workspace.",
@@ -26035,8 +27427,8 @@ var writeFileTool = {
26035
27427
  const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
26036
27428
  const content = stringField(object2, "content", { allowEmpty: true });
26037
27429
  const before = await readOptionalTextFile(filePath);
26038
- await mkdir9(path22.dirname(filePath), { recursive: true });
26039
- await writeFile9(filePath, content, "utf8");
27430
+ await mkdir11(path26.dirname(filePath), { recursive: true });
27431
+ await writeFile11(filePath, content, "utf8");
26040
27432
  const relative = relativeToCwd(ctx.cwd, filePath);
26041
27433
  const diff = createTextDiff(before, content, relative);
26042
27434
  return ok(`Wrote ${relativeToCwd(ctx.cwd, filePath)} (${Buffer.byteLength(content, "utf8")} bytes).`, {
@@ -26049,7 +27441,7 @@ var writeFileTool = {
26049
27441
  };
26050
27442
  async function readOptionalTextFile(filePath) {
26051
27443
  try {
26052
- return await readFile11(filePath, "utf8");
27444
+ return await readFile13(filePath, "utf8");
26053
27445
  } catch (error) {
26054
27446
  if (isNotFound(error)) return void 0;
26055
27447
  throw error;
@@ -26060,7 +27452,7 @@ function isNotFound(error) {
26060
27452
  }
26061
27453
 
26062
27454
  // src/tools/edit-file.ts
26063
- import { readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
27455
+ import { readFile as readFile14, writeFile as writeFile12 } from "node:fs/promises";
26064
27456
  var editFileTool = {
26065
27457
  name: "edit_file",
26066
27458
  description: "Replace an exact string in a text file inside the workspace.",
@@ -26082,12 +27474,12 @@ var editFileTool = {
26082
27474
  const newString = stringField(object2, "newString", { allowEmpty: true });
26083
27475
  const replaceAll = optionalBooleanField(object2, "replaceAll") ?? false;
26084
27476
  if (oldString === newString) throw new Error("oldString and newString must be different");
26085
- const before = await readFile12(filePath, "utf8");
27477
+ const before = await readFile14(filePath, "utf8");
26086
27478
  const matches = countMatches(before, oldString);
26087
27479
  if (matches === 0) throw new Error("oldString was not found");
26088
27480
  if (matches > 1 && !replaceAll) throw new Error(`oldString matched ${matches} times; set replaceAll=true to replace all matches`);
26089
27481
  const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString);
26090
- await writeFile10(filePath, after, "utf8");
27482
+ await writeFile12(filePath, after, "utf8");
26091
27483
  const relative = relativeToCwd(ctx.cwd, filePath);
26092
27484
  const diff = createTextDiff(before, after, relative);
26093
27485
  return ok(`Edited ${relative} (${replaceAll ? matches : 1} replacement${matches === 1 ? "" : "s"}).`, {
@@ -26110,8 +27502,8 @@ function countMatches(text, needle) {
26110
27502
  }
26111
27503
 
26112
27504
  // src/tools/bash.ts
26113
- import { spawn as spawn5 } from "node:child_process";
26114
- import path24 from "node:path";
27505
+ import { spawn as spawn6 } from "node:child_process";
27506
+ import path28 from "node:path";
26115
27507
 
26116
27508
  // src/sandbox/env-only.ts
26117
27509
  function buildEnvOnlyLaunch(command, config) {
@@ -26171,7 +27563,7 @@ function bwrapPath() {
26171
27563
 
26172
27564
  // src/sandbox/macos.ts
26173
27565
  import { existsSync as existsSync4 } from "node:fs";
26174
- import path23 from "node:path";
27566
+ import path27 from "node:path";
26175
27567
  function canUseMacOSSandbox() {
26176
27568
  return process.platform === "darwin" && existsSync4("/usr/bin/sandbox-exec");
26177
27569
  }
@@ -26197,7 +27589,7 @@ function macosProfile(cwd, config) {
26197
27589
  }
26198
27590
  if (mode === "workspace-write") {
26199
27591
  lines.push("(deny file-write*)");
26200
- lines.push(`(allow file-write* (subpath "${escapeProfilePath(path23.resolve(cwd))}"))`);
27592
+ lines.push(`(allow file-write* (subpath "${escapeProfilePath(path27.resolve(cwd))}"))`);
26201
27593
  lines.push('(allow file-write* (subpath "/tmp"))');
26202
27594
  lines.push('(allow file-write* (subpath "/private/tmp"))');
26203
27595
  lines.push('(allow file-write* (subpath "/private/var/folders"))');
@@ -26259,7 +27651,7 @@ ${result.stderr}` : ""
26259
27651
  ].filter(Boolean).join("\n");
26260
27652
  return ok(content, {
26261
27653
  command,
26262
- workdir: path24.relative(ctx.cwd, workdir) || ".",
27654
+ workdir: path28.relative(ctx.cwd, workdir) || ".",
26263
27655
  exitCode: result.exitCode,
26264
27656
  signal: result.signal,
26265
27657
  timedOut: result.timedOut,
@@ -26270,7 +27662,7 @@ ${result.stderr}` : ""
26270
27662
  function runCommand(command, cwd, timeoutMs, signal, sandbox = { mode: "workspace-write" }) {
26271
27663
  return new Promise((resolve, reject) => {
26272
27664
  const launch = buildSandboxLaunch(command, cwd, sandbox);
26273
- const child = spawn5(launch.command, launch.args, {
27665
+ const child = spawn6(launch.command, launch.args, {
26274
27666
  cwd,
26275
27667
  env: { ...process.env, ...launch.env },
26276
27668
  stdio: ["ignore", "pipe", "pipe"]
@@ -26310,9 +27702,9 @@ function runCommand(command, cwd, timeoutMs, signal, sandbox = { mode: "workspac
26310
27702
  }
26311
27703
 
26312
27704
  // 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";
27705
+ import { spawn as spawn7 } from "node:child_process";
27706
+ import { readdir as readdir2, readFile as readFile15, stat as stat8 } from "node:fs/promises";
27707
+ import path29 from "node:path";
26316
27708
  var MAX_MATCHES = 200;
26317
27709
  var grepTool = {
26318
27710
  name: "grep",
@@ -26362,7 +27754,7 @@ function runRg(cwd, base, pattern, glob, signal) {
26362
27754
  ];
26363
27755
  if (glob) args.push("--glob", glob);
26364
27756
  args.push(pattern, base);
26365
- const child = spawn6("rg", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
27757
+ const child = spawn7("rg", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
26366
27758
  const stdout = [];
26367
27759
  const stderr = [];
26368
27760
  const abort = () => child.kill("SIGTERM");
@@ -26387,7 +27779,7 @@ function runRg(cwd, base, pattern, glob, signal) {
26387
27779
  }
26388
27780
  function rewriteRgPath(cwd, line) {
26389
27781
  const [file, rest] = splitFirst(line, ":");
26390
- if (!path25.isAbsolute(file)) return line;
27782
+ if (!path29.isAbsolute(file)) return line;
26391
27783
  return `${relativeToCwd(cwd, file)}:${rest}`;
26392
27784
  }
26393
27785
  function splitFirst(value, delimiter) {
@@ -26402,13 +27794,13 @@ async function fallbackSearch(cwd, base, pattern, glob) {
26402
27794
  if (out.length > MAX_MATCHES) return;
26403
27795
  const relative = relativeToCwd(cwd, item);
26404
27796
  if (isIgnoredPath(relative)) return;
26405
- const info = await stat6(item);
27797
+ const info = await stat8(item);
26406
27798
  if (info.isDirectory()) {
26407
- for (const entry of await readdir2(item)) await walk2(path25.join(item, entry));
27799
+ for (const entry of await readdir2(item)) await walk2(path29.join(item, entry));
26408
27800
  return;
26409
27801
  }
26410
27802
  if (glob && !matchGlob(glob, relative)) return;
26411
- const buffer = await readFile13(item);
27803
+ const buffer = await readFile15(item);
26412
27804
  if (buffer.includes(0)) return;
26413
27805
  const lines = buffer.toString("utf8").split(/\r?\n/);
26414
27806
  for (let i = 0; i < lines.length; i++) {
@@ -26422,8 +27814,8 @@ async function fallbackSearch(cwd, base, pattern, glob) {
26422
27814
  }
26423
27815
 
26424
27816
  // src/tools/glob.ts
26425
- import { readdir as readdir3, stat as stat7 } from "node:fs/promises";
26426
- import path26 from "node:path";
27817
+ import { readdir as readdir3, stat as stat9 } from "node:fs/promises";
27818
+ import path30 from "node:path";
26427
27819
  var MAX_PATHS = 1e3;
26428
27820
  var globTool = {
26429
27821
  name: "glob",
@@ -26455,10 +27847,10 @@ async function collectPaths(cwd, root, pattern) {
26455
27847
  async function walk2(dir) {
26456
27848
  const entries = await readdir3(dir, { withFileTypes: true });
26457
27849
  for (const entry of entries) {
26458
- const absolute = path26.join(dir, entry.name);
27850
+ const absolute = path30.join(dir, entry.name);
26459
27851
  const relative = relativeToCwd(cwd, absolute);
26460
27852
  if (isIgnoredPath(relative)) continue;
26461
- const info = await stat7(absolute);
27853
+ const info = await stat9(absolute);
26462
27854
  if (matchGlob(pattern, relative)) out.push({ relative, mtimeMs: info.mtimeMs });
26463
27855
  if (entry.isDirectory()) await walk2(absolute);
26464
27856
  }
@@ -26596,7 +27988,7 @@ async function runBraveSearch(config, options) {
26596
27988
  if (config.country) url.searchParams.set("country", config.country);
26597
27989
  if (config.searchLang) url.searchParams.set("search_lang", config.searchLang);
26598
27990
  if (config.safeSearch) url.searchParams.set("safesearch", config.safeSearch);
26599
- const json = await fetchJson(url, {
27991
+ const json = await fetchJson2(url, {
26600
27992
  method: "GET",
26601
27993
  headers: {
26602
27994
  accept: "application/json",
@@ -26615,7 +28007,7 @@ async function runTavilySearch(config, options) {
26615
28007
  if (!apiKey.ok) return apiKey;
26616
28008
  const maxResults = Math.min(positiveInteger(options.maxResults, config.maxResults ?? 5, "maxResults"), TAVILY_MAX_RESULTS);
26617
28009
  const endpoint = config.endpoint ?? defaultConfig.webSearch.providers.tavily.endpoint ?? "https://api.tavily.com/search";
26618
- const json = await fetchJson(endpoint, {
28010
+ const json = await fetchJson2(endpoint, {
26619
28011
  method: "POST",
26620
28012
  headers: {
26621
28013
  "content-type": "application/json",
@@ -26641,7 +28033,7 @@ async function runExaSearch(config, options) {
26641
28033
  if (!apiKey.ok) return apiKey;
26642
28034
  const numResults = Math.min(positiveInteger(options.maxResults, config.numResults ?? 5, "maxResults"), EXA_MAX_RESULTS);
26643
28035
  const endpoint = config.endpoint ?? defaultConfig.webSearch.providers.exa.endpoint ?? "https://api.exa.ai/search";
26644
- const json = await fetchJson(endpoint, {
28036
+ const json = await fetchJson2(endpoint, {
26645
28037
  method: "POST",
26646
28038
  headers: {
26647
28039
  "content-type": "application/json",
@@ -26666,11 +28058,11 @@ async function runExaSearch(config, options) {
26666
28058
  return searchResult("exa", options.query, results, json.value);
26667
28059
  }
26668
28060
  function resolveApiKey(provider, config) {
26669
- const value = config.apiKey ?? (config.apiKeyEnv ? process.env[config.apiKeyEnv] : void 0);
28061
+ const value = config.apiKey || (config.apiKeyEnv ? process.env[config.apiKeyEnv] : void 0);
26670
28062
  if (value) return { ok: true, value };
26671
28063
  return toolFail(`Missing API key for web_search provider "${provider}". Set webSearch.providers.${provider}.apiKey or set ${config.apiKeyEnv ?? "the configured apiKeyEnv"}.`);
26672
28064
  }
26673
- async function fetchJson(url, options) {
28065
+ async function fetchJson2(url, options) {
26674
28066
  const { fetcher, provider, ...init } = options;
26675
28067
  const response = await fetcher(url, init);
26676
28068
  const text = await response.text();
@@ -26688,10 +28080,10 @@ function normalizeBraveResults(raw, limit) {
26688
28080
  return (data.web?.results ?? []).slice(0, limit).map((item) => {
26689
28081
  const result = item;
26690
28082
  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)
28083
+ title: stringValue5(result.title) ?? "Untitled",
28084
+ url: stringValue5(result.url) ?? "",
28085
+ snippet: cleanSnippet(stringValue5(result.description) ?? stringValue5(result.snippet)),
28086
+ publishedDate: stringValue5(result.age)
26695
28087
  };
26696
28088
  }).filter((item) => item.url.length > 0);
26697
28089
  }
@@ -26700,9 +28092,9 @@ function normalizeTavilyResults(raw, limit) {
26700
28092
  return (data.results ?? []).slice(0, limit).map((item) => {
26701
28093
  const result = item;
26702
28094
  return {
26703
- title: stringValue4(result.title) ?? "Untitled",
26704
- url: stringValue4(result.url) ?? "",
26705
- snippet: cleanSnippet(stringValue4(result.content) ?? stringValue4(result.raw_content)),
28095
+ title: stringValue5(result.title) ?? "Untitled",
28096
+ url: stringValue5(result.url) ?? "",
28097
+ snippet: cleanSnippet(stringValue5(result.content) ?? stringValue5(result.raw_content)),
26706
28098
  score: numberValue3(result.score)
26707
28099
  };
26708
28100
  }).filter((item) => item.url.length > 0);
@@ -26712,10 +28104,10 @@ function normalizeExaResults(raw, limit) {
26712
28104
  return (data.results ?? []).slice(0, limit).map((item) => {
26713
28105
  const result = item;
26714
28106
  return {
26715
- title: stringValue4(result.title) ?? "Untitled",
26716
- url: stringValue4(result.url) ?? "",
28107
+ title: stringValue5(result.title) ?? "Untitled",
28108
+ url: stringValue5(result.url) ?? "",
26717
28109
  snippet: exaSnippet(result),
26718
- publishedDate: stringValue4(result.publishedDate),
28110
+ publishedDate: stringValue5(result.publishedDate),
26719
28111
  score: numberValue3(result.score)
26720
28112
  };
26721
28113
  }).filter((item) => item.url.length > 0);
@@ -26726,7 +28118,7 @@ function exaSnippet(result) {
26726
28118
  const joined = highlights.map((item) => typeof item === "string" ? item : "").filter(Boolean).join(" ");
26727
28119
  if (joined) return cleanSnippet(joined);
26728
28120
  }
26729
- return cleanSnippet(stringValue4(result.text) ?? stringValue4(result.summary));
28121
+ return cleanSnippet(stringValue5(result.text) ?? stringValue5(result.summary));
26730
28122
  }
26731
28123
  function searchResult(provider, query, results, raw) {
26732
28124
  const content = [
@@ -26750,7 +28142,7 @@ function formatResult(index, result) {
26750
28142
  if (result.snippet) lines.push(` Snippet: ${result.snippet}`);
26751
28143
  return lines.join("\n");
26752
28144
  }
26753
- function stringValue4(value) {
28145
+ function stringValue5(value) {
26754
28146
  return typeof value === "string" && value.length > 0 ? value : void 0;
26755
28147
  }
26756
28148
  function numberValue3(value) {
@@ -27457,14 +28849,14 @@ async function runExecutionSession(options) {
27457
28849
  }
27458
28850
 
27459
28851
  // 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";
28852
+ import { mkdir as mkdir13, open as open3, rm as rm2 } from "node:fs/promises";
28853
+ import path32 from "node:path";
27462
28854
 
27463
28855
  // src/goals/storage.ts
27464
28856
  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";
28857
+ import { mkdir as mkdir12, readFile as readFile16, rename as rename6, writeFile as writeFile13 } from "node:fs/promises";
27466
28858
  import fs9 from "node:fs";
27467
- import path27 from "node:path";
28859
+ import path31 from "node:path";
27468
28860
  var GoalStore = class {
27469
28861
  cwd;
27470
28862
  dir;
@@ -27475,12 +28867,12 @@ var GoalStore = class {
27475
28867
  this.cwd = cwd;
27476
28868
  this.scope = normalizeGoalStoreScope(options.scope);
27477
28869
  this.dir = goalStoreDir(cwd, this.scope);
27478
- this.activePath = path27.join(this.dir, "active.json");
27479
- this.archiveDir = path27.join(this.dir, "archive");
28870
+ this.activePath = path31.join(this.dir, "active.json");
28871
+ this.archiveDir = path31.join(this.dir, "archive");
27480
28872
  }
27481
28873
  async loadActive() {
27482
28874
  if (!fs9.existsSync(this.activePath)) return void 0;
27483
- const text = await readFile14(this.activePath, "utf8");
28875
+ const text = await readFile16(this.activePath, "utf8");
27484
28876
  try {
27485
28877
  return JSON.parse(text);
27486
28878
  } catch {
@@ -27489,36 +28881,36 @@ var GoalStore = class {
27489
28881
  }
27490
28882
  }
27491
28883
  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)}
28884
+ await mkdir12(this.dir, { recursive: true });
28885
+ const tmp = path31.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
28886
+ await writeFile13(tmp, `${JSON.stringify(state, null, 2)}
27495
28887
  `, "utf8");
27496
- await rename4(tmp, this.activePath);
28888
+ await rename6(tmp, this.activePath);
27497
28889
  }
27498
28890
  async archiveActive(status = "cleared") {
27499
28891
  const state = await this.loadActive();
27500
28892
  if (!state) return void 0;
27501
- await mkdir10(this.archiveDir, { recursive: true });
28893
+ await mkdir12(this.archiveDir, { recursive: true });
27502
28894
  const archived = {
27503
28895
  ...state,
27504
28896
  status,
27505
28897
  updatedAt: Date.now()
27506
28898
  };
27507
- await writeFile11(path27.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
28899
+ await writeFile13(path31.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
27508
28900
  `, "utf8");
27509
28901
  await fs9.promises.rm(this.activePath, { force: true });
27510
28902
  return archived;
27511
28903
  }
27512
28904
  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");
28905
+ await mkdir12(this.dir, { recursive: true });
28906
+ const backup = path31.join(this.dir, `active.corrupt.${Date.now()}.json`);
28907
+ await writeFile13(backup, text, "utf8");
27516
28908
  await fs9.promises.rm(this.activePath, { force: true });
27517
28909
  }
27518
28910
  };
27519
28911
  function goalStoreDir(cwd, scope) {
27520
28912
  const safeScope = normalizeGoalStoreScope(scope);
27521
- return safeScope ? path27.join(cwd, ".demian", "goals", "sessions", safeScope) : path27.join(cwd, ".demian", "goals");
28913
+ return safeScope ? path31.join(cwd, ".demian", "goals", "sessions", safeScope) : path31.join(cwd, ".demian", "goals");
27522
28914
  }
27523
28915
  function normalizeGoalStoreScope(scope) {
27524
28916
  const trimmed = scope?.trim();
@@ -27532,10 +28924,10 @@ function normalizeGoalStoreScope(scope) {
27532
28924
  var GoalLock = class {
27533
28925
  path;
27534
28926
  constructor(cwd, scope) {
27535
- this.path = path28.join(goalStoreDir(cwd, scope), "active.lock");
28927
+ this.path = path32.join(goalStoreDir(cwd, scope), "active.lock");
27536
28928
  }
27537
28929
  async acquire() {
27538
- await mkdir11(path28.dirname(this.path), { recursive: true });
28930
+ await mkdir13(path32.dirname(this.path), { recursive: true });
27539
28931
  const handle = await open3(this.path, "wx").catch((error) => {
27540
28932
  const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
27541
28933
  if (code === "EEXIST") throw new Error("Another goal is already active in this workspace.");
@@ -28065,7 +29457,7 @@ function readString(input2, key) {
28065
29457
  }
28066
29458
 
28067
29459
  // src/external-runtime/session-map.ts
28068
- import crypto5 from "node:crypto";
29460
+ import crypto6 from "node:crypto";
28069
29461
  var ClaudeCodeSessionMap = class {
28070
29462
  #sessions = /* @__PURE__ */ new Map();
28071
29463
  get(key) {
@@ -28118,13 +29510,13 @@ function normalizeInstruction(value) {
28118
29510
  return value.replace(/\r\n/g, "\n").trim().split("\n").map((line) => line.replace(/[ \t]+$/g, "")).join("\n");
28119
29511
  }
28120
29512
  function sha256(value) {
28121
- return crypto5.createHash("sha256").update(value).digest("hex").slice(0, 16);
29513
+ return crypto6.createHash("sha256").update(value).digest("hex").slice(0, 16);
28122
29514
  }
28123
29515
 
28124
29516
  // src/root-session.ts
28125
29517
  import { cp, mkdtemp, rm as rm3 } from "node:fs/promises";
28126
- import os11 from "node:os";
28127
- import path29 from "node:path";
29518
+ import os15 from "node:os";
29519
+ import path33 from "node:path";
28128
29520
 
28129
29521
  // src/permissions/presets.ts
28130
29522
  var PERMISSION_PRESETS = ["deny", "ask", "auto", "full"];
@@ -28968,8 +30360,8 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
28968
30360
  }
28969
30361
  }
28970
30362
  async createCoworkIsolatedWorkspace(groupId, memberId) {
28971
- const root = await mkdtemp(path29.join(os11.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
28972
- const cwd = path29.join(root, "workspace");
30363
+ const root = await mkdtemp(path33.join(os15.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
30364
+ const cwd = path33.join(root, "workspace");
28973
30365
  await cp(this.#options.cwd, cwd, {
28974
30366
  recursive: true,
28975
30367
  filter: (source) => shouldCopyIntoCoworkWorkspace(this.#options.cwd, source)
@@ -29117,7 +30509,7 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
29117
30509
  const denyTools = ["write_file", "edit_file", "bash", "claudecode.Edit", "claudecode.Write", "claudecode.MultiEdit", "claudecode.Bash"];
29118
30510
  return this.withPermissionPreset({
29119
30511
  ...agent,
29120
- provider: agent.provider?.profile === "claudecode-subagent" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
30512
+ provider: agent.provider?.profile === "claudecode" && agent.name === "claudecode-explorer" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
29121
30513
  tools: agent.tools.filter((tool) => readTools.has(tool)),
29122
30514
  permissions: [
29123
30515
  ...agent.permissions.filter((rule) => !denyTools.includes(rule.tool)),
@@ -29164,13 +30556,14 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
29164
30556
  return lines.join("\n");
29165
30557
  }
29166
30558
  resolveInvocationBackend(profileName, modelOverride, agent) {
29167
- const providerConfig = resolveProviderConfig(this.#options.config, profileName);
30559
+ const runtime = resolveProviderRuntimeConfig(this.#options.config, { providerName: profileName, model: modelOverride });
30560
+ const providerConfig = runtime.providerConfig;
29168
30561
  if (this.#options.resolveExecutionBackend) return this.#options.resolveExecutionBackend(profileName, providerConfig, modelOverride);
29169
30562
  if (this.#options.resolveProvider) {
29170
- const resolved = this.#options.resolveProvider(profileName, providerConfig, modelOverride);
30563
+ const resolved = this.#options.resolveProvider(profileName, providerConfig, runtime.model);
29171
30564
  return { kind: "provider", ...resolved };
29172
30565
  }
29173
- return resolveExecutionBackend(providerConfig, { model: modelOverride, config: this.#options.config, agent });
30566
+ return resolveExecutionBackend(providerConfig, { model: runtime.model, config: this.#options.config, agent });
29174
30567
  }
29175
30568
  loadAgentSession(agent, providerProfile, model, sessionScope, cwd = this.#options.cwd) {
29176
30569
  const key = `${this.id}:${sessionScope ?? "delegate"}:${agent.name}:${providerProfile}:${model}:${cwd}`;
@@ -29272,17 +30665,17 @@ function findDependencyCycle(group) {
29272
30665
  const byId = new Map(group.map((member) => [member.memberId, member]));
29273
30666
  const visiting = /* @__PURE__ */ new Set();
29274
30667
  const visited = /* @__PURE__ */ new Set();
29275
- const path32 = [];
30668
+ const path36 = [];
29276
30669
  const visit = (id) => {
29277
- if (visiting.has(id)) return [...path32.slice(path32.indexOf(id)), id];
30670
+ if (visiting.has(id)) return [...path36.slice(path36.indexOf(id)), id];
29278
30671
  if (visited.has(id)) return void 0;
29279
30672
  visiting.add(id);
29280
- path32.push(id);
30673
+ path36.push(id);
29281
30674
  for (const dep of byId.get(id)?.dependsOn ?? []) {
29282
30675
  const cycle = visit(dep);
29283
30676
  if (cycle) return cycle;
29284
30677
  }
29285
- path32.pop();
30678
+ path36.pop();
29286
30679
  visiting.delete(id);
29287
30680
  visited.add(id);
29288
30681
  return void 0;
@@ -29323,7 +30716,7 @@ function scopeStaticPrefix(pattern) {
29323
30716
  return prefix.replace(/\/+$/, "").split("/").filter(Boolean).join("/") || ".";
29324
30717
  }
29325
30718
  function shouldCopyIntoCoworkWorkspace(root, source) {
29326
- const relative = path29.relative(root, source).split(path29.sep).join("/");
30719
+ const relative = path33.relative(root, source).split(path33.sep).join("/");
29327
30720
  if (!relative) return true;
29328
30721
  const parts = relative.split("/");
29329
30722
  return !parts.includes(".git") && !parts.includes("node_modules") && !parts.includes(".demian");
@@ -29718,52 +31111,119 @@ function finitePositiveInteger(value) {
29718
31111
  }
29719
31112
 
29720
31113
  // src/ui/plain/interactive.ts
29721
- import readline3 from "node:readline";
31114
+ import readline4 from "node:readline";
29722
31115
 
29723
31116
  // 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
- }));
31117
+ function providerOptions(config, catalogs = {}) {
31118
+ const orderedNames = sortProviderNames(Object.keys(config.providers));
31119
+ const order = new Map(orderedNames.map((name, index) => [name, index]));
31120
+ return orderedNames.map((name) => [name, config.providers[name]]).filter(([, provider]) => provider && !provider.hidden && !(provider.type === "claudecode" && provider.runtime === "cli")).map(([name, provider]) => {
31121
+ const catalog = catalogs[name];
31122
+ const available = providerCatalogAvailable(catalog);
31123
+ const profiles = catalog?.models.length ? catalog.models.map((model) => ({
31124
+ name: model.name,
31125
+ displayName: model.displayName ?? model.name,
31126
+ model: model.model,
31127
+ description: model.description,
31128
+ source: model.source,
31129
+ isDefault: model.isDefault
31130
+ })) : providerModelProfiles(name, provider);
31131
+ const modelProfiles = profiles.map((profile) => {
31132
+ const displayName = displayModelForProfile(profile);
31133
+ return {
31134
+ ...profile,
31135
+ label: available ? displayName : unavailableLabel(displayName),
31136
+ available
31137
+ };
31138
+ });
31139
+ const models = modelProfiles.length ? [...new Set(modelProfiles.map((item) => item.model).filter((item) => typeof item === "string" && item.trim()))] : providerModelOptions(name, provider);
31140
+ const defaultModel = defaultDisplayModelForProvider(name, provider);
31141
+ return {
31142
+ name,
31143
+ label: available ? name : unavailableLabel(name),
31144
+ model: defaultModel,
31145
+ modelLabel: available || !defaultModel ? defaultModel : unavailableLabel(defaultModel),
31146
+ models,
31147
+ modelProfiles,
31148
+ type: provider.type,
31149
+ runtime: provider.type === "claudecode" ? provider.runtime : void 0,
31150
+ permissionMode: provider.type === "claudecode" ? provider.permissionMode : void 0,
31151
+ ga: provider.type === "claudecode" ? claudeCodeGaEnabled(provider) : void 0,
31152
+ available,
31153
+ catalog: catalog ? { status: catalog.status, source: catalog.source, message: catalog.message } : void 0
31154
+ };
31155
+ }).sort((left, right) => {
31156
+ if (left.available !== right.available) return left.available ? -1 : 1;
31157
+ return (order.get(left.name) ?? 0) - (order.get(right.name) ?? 0);
31158
+ });
29734
31159
  }
31160
+ var MISSING_SELECTION_PLACEHOLDER = "-";
29735
31161
  function createInteractiveModelSelection(config, flags = {}, saved) {
29736
31162
  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;
31163
+ const savedProviderExists = savedProviderName && config.providers[savedProviderName] && !config.providers[savedProviderName]?.hidden;
31164
+ if (saved?.providerName && !flags.provider && !savedProviderExists) {
31165
+ return {
31166
+ providerName: saved.providerName,
31167
+ providerSource: "saved",
31168
+ model: MISSING_SELECTION_PLACEHOLDER,
31169
+ modelSource: "saved"
31170
+ };
31171
+ }
31172
+ const providerName = flags.provider ?? (savedProviderExists ? savedProviderName : config.defaultProvider);
31173
+ let providerConfig;
31174
+ try {
31175
+ providerConfig = resolveProviderConfig(config, providerName);
31176
+ } catch {
31177
+ return {
31178
+ providerName,
31179
+ providerSource: flags.provider ? "flag" : "config",
31180
+ model: MISSING_SELECTION_PLACEHOLDER,
31181
+ modelSource: "config"
31182
+ };
31183
+ }
31184
+ const usesSavedProvider = !flags.provider && savedProviderExists && providerName === savedProviderName;
29742
31185
  const savedModel = usesSavedProvider && saved?.model ? saved.model : void 0;
31186
+ const profiles = providerModelProfiles(providerName, providerConfig);
31187
+ const savedProfile = savedModel ? profiles.find((profile) => profile.model === savedModel || profile.displayName === savedModel || profile.name === savedModel) : void 0;
31188
+ const defaultModel = displayModelForProfile(profiles[0] ?? { name: "", displayName: "", model: "" }) || defaultDisplayModelForProvider(providerName, providerConfig);
31189
+ const flagProfile = flags.model ? profiles.find((profile) => profile.model === flags.model || profile.displayName === flags.model || profile.name === flags.model) : void 0;
31190
+ const selectedProfile = flags.model ? flagProfile : savedModel ? savedProfile : profiles[0];
31191
+ const model = flags.model ?? (savedProfile ? displayModelForProfile(savedProfile) : savedModel) ?? defaultModel;
29743
31192
  return {
29744
31193
  providerName,
29745
31194
  providerSource: flags.provider ? "flag" : usesSavedProvider ? "saved" : "config",
29746
- model: flags.model ?? savedModel ?? providerConfig.model,
29747
- modelSource: flags.model ? "flag" : savedModel ? "saved" : "config"
31195
+ model,
31196
+ modelSource: flags.model ? "flag" : savedModel ? "saved" : "config",
31197
+ modelProfileName: selectedProfile?.name
29748
31198
  };
29749
31199
  }
29750
31200
  function selectProvider(config, selection, providerName) {
29751
31201
  const providerConfig = resolveProviderConfig(config, providerName);
31202
+ const profiles = providerModelProfiles(providerName, providerConfig);
31203
+ const profile = profiles[0];
29752
31204
  return {
29753
31205
  ...selection,
29754
31206
  providerName,
29755
31207
  providerSource: "interactive",
29756
- model: providerConfig.model,
29757
- modelSource: "config"
31208
+ model: profile ? displayModelForProfile(profile) : defaultDisplayModelForProvider(providerName, providerConfig),
31209
+ modelSource: "config",
31210
+ modelProfileName: profile?.name
29758
31211
  };
29759
31212
  }
29760
31213
  function selectModel(selection, model) {
29761
31214
  return {
29762
31215
  ...selection,
29763
31216
  model,
29764
- modelSource: "interactive"
31217
+ modelSource: "interactive",
31218
+ modelProfileName: void 0
29765
31219
  };
29766
31220
  }
31221
+ function providerCatalogAvailable(catalog) {
31222
+ return catalog ? catalog.status === "ready" && catalog.models.length > 0 : true;
31223
+ }
31224
+ function unavailableLabel(value) {
31225
+ return `(n/a) ${value}`;
31226
+ }
29767
31227
 
29768
31228
  // src/ui/plain/interactive.ts
29769
31229
  var PlainInteractivePrompt = class {
@@ -29775,7 +31235,7 @@ var PlainInteractivePrompt = class {
29775
31235
  #exitRequested = false;
29776
31236
  constructor(input2 = process.stdin, output2 = process.stderr) {
29777
31237
  this.#output = output2;
29778
- this.#rl = readline3.createInterface({ input: input2, output: output2 });
31238
+ this.#rl = readline4.createInterface({ input: input2, output: output2 });
29779
31239
  this.#rl.on("line", (line) => this.#handleLine(line));
29780
31240
  }
29781
31241
  get exitRequested() {
@@ -29872,13 +31332,13 @@ async function askModel(promptUi, selection) {
29872
31332
  }
29873
31333
 
29874
31334
  // src/ui/preferences.ts
29875
- import { mkdir as mkdir12, readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
31335
+ import { mkdir as mkdir14, readFile as readFile17, writeFile as writeFile14 } from "node:fs/promises";
29876
31336
  import fs10 from "node:fs";
29877
- import path30 from "node:path";
31337
+ import path34 from "node:path";
29878
31338
  var UiPreferenceStore = class {
29879
31339
  filePath;
29880
31340
  constructor(cwd, filePath) {
29881
- this.filePath = filePath ? path30.resolve(cwd, filePath) : path30.join(cwd, ".demian", "preferences.json");
31341
+ this.filePath = filePath ? path34.resolve(cwd, filePath) : path34.join(cwd, ".demian", "preferences.json");
29882
31342
  }
29883
31343
  async load() {
29884
31344
  if (!fs10.existsSync(this.filePath)) return void 0;
@@ -29896,13 +31356,13 @@ var UiPreferenceStore = class {
29896
31356
  model: selection.model,
29897
31357
  updatedAt: Date.now()
29898
31358
  };
29899
- await mkdir12(path30.dirname(this.filePath), { recursive: true });
29900
- await writeFile12(this.filePath, `${JSON.stringify(file, null, 2)}
31359
+ await mkdir14(path34.dirname(this.filePath), { recursive: true });
31360
+ await writeFile14(this.filePath, `${JSON.stringify(file, null, 2)}
29901
31361
  `, { mode: 384 });
29902
31362
  }
29903
31363
  async #read() {
29904
31364
  try {
29905
- return JSON.parse(await readFile15(this.filePath, "utf8"));
31365
+ return JSON.parse(await readFile17(this.filePath, "utf8"));
29906
31366
  } catch {
29907
31367
  return void 0;
29908
31368
  }
@@ -29918,6 +31378,8 @@ function preferenceKey(selection) {
29918
31378
 
29919
31379
  // src/cli.ts
29920
31380
  async function main(argv = process.argv.slice(2)) {
31381
+ const configCommandResult = await maybeRunConfigCommand(argv);
31382
+ if (configCommandResult !== void 0) return configCommandResult;
29921
31383
  const doctorResult = await maybeRunDoctorCommand(argv);
29922
31384
  if (doctorResult !== void 0) return doctorResult;
29923
31385
  const flags = parseArgs(argv);
@@ -29925,7 +31387,8 @@ async function main(argv = process.argv.slice(2)) {
29925
31387
  printHelp();
29926
31388
  return 0;
29927
31389
  }
29928
- const cwd = path31.resolve(flags.cwd ?? process.cwd());
31390
+ if (!flags.noWizard) await runFirstRunWizard().catch(() => void 0);
31391
+ const cwd = path35.resolve(flags.cwd ?? process.cwd());
29929
31392
  const config = await loadConfig({ cwd, configPath: flags.configPath });
29930
31393
  const agentName = flags.agent ?? config.defaultAgent;
29931
31394
  const preferenceStore = new UiPreferenceStore(cwd);
@@ -30014,9 +31477,9 @@ async function runPlainTask(options) {
30014
31477
  config: config.goals,
30015
31478
  eventBus,
30016
31479
  titleGenerator: async (input2) => {
30017
- const providerConfig2 = resolveProviderConfig(config, selection.providerName);
30018
- const resolved = resolveExecutionBackend(providerConfig2, {
30019
- model: selection.model,
31480
+ const runtime2 = resolveProviderRuntimeConfig(config, selection);
31481
+ const resolved = resolveExecutionBackend(runtime2.providerConfig, {
31482
+ model: runtime2.model,
30020
31483
  onRetry: (event) => eventBus.emit({
30021
31484
  type: "provider.retry",
30022
31485
  sessionId: "goal-title",
@@ -30106,10 +31569,10 @@ async function runPlainTask(options) {
30106
31569
  `);
30107
31570
  return { history: interactiveHistoryFromRunMessages(result2.messages), lastExternalSessionKey: options.lastExternalSessionKey };
30108
31571
  }
30109
- const providerConfig = resolveProviderConfig(config, providerName);
30110
31572
  const runtimeAgent = activeGoal ? withGoalTools(agent) : agent;
30111
- const backend = resolveExecutionBackend(providerConfig, {
30112
- model: selection.model,
31573
+ const runtime = resolveProviderRuntimeConfig(config, selection);
31574
+ const backend = resolveExecutionBackend(runtime.providerConfig, {
31575
+ model: runtime.model,
30113
31576
  config,
30114
31577
  agent: runtimeAgent,
30115
31578
  onRetry: (event) => eventBus.emit({
@@ -30213,6 +31676,10 @@ function parseArgs(argv) {
30213
31676
  out.transcript = false;
30214
31677
  continue;
30215
31678
  }
31679
+ if (arg === "--no-wizard") {
31680
+ out.noWizard = true;
31681
+ continue;
31682
+ }
30216
31683
  if (arg === "--goal") {
30217
31684
  out.goal = true;
30218
31685
  continue;
@@ -30287,6 +31754,10 @@ function printHelp() {
30287
31754
 
30288
31755
  Usage:
30289
31756
  demian-plain [flags]
31757
+ demian-plain config init [--force]
31758
+ demian-plain config add-provider <preset|openai-compatible> [--name <name>]
31759
+ demian-plain config add-model <provider> --name <name> --model <model>
31760
+ demian-plain auth login <codex|claudecode>
30290
31761
  demian-plain doctor policies --upgrade-namespaces [--write]
30291
31762
 
30292
31763
  Default:
@@ -30305,7 +31776,7 @@ Flags:
30305
31776
  --single-agent force the current single-agent runtime
30306
31777
  --multi-agent enable root-owned delegate_agent runtime
30307
31778
  --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
31779
+ --provider <name> openai, anthropic, gemini, groq, azure, lmstudio, ollama-local, ollama-cloud, llamacpp, vllm, codex, claudecode
30309
31780
  --model <name> override configured model
30310
31781
  --max-turns <n> maximum model/tool loop turns
30311
31782
  --cwd <path> workspace directory
@@ -30318,7 +31789,8 @@ Flags:
30318
31789
  --persistent-grants persist "always" permission grants
30319
31790
  --no-persistent-grants
30320
31791
  keep "always" grants session-local
30321
- --no-transcript disable .demian transcript writes
31792
+ --no-transcript disable ~/.demian transcript writes
31793
+ --no-wizard skip the first-run config wizard
30322
31794
  --goal run the prompt as an explicit /goal objective
30323
31795
  /ralph-loop --fresh-context is single-agent only
30324
31796
  --config <path> load an additional config file
@@ -30327,6 +31799,13 @@ Flags:
30327
31799
  Doctor:
30328
31800
  doctor policies --upgrade-namespaces
30329
31801
  rewrite prefix-less permission rules as demian.<tool>
31802
+
31803
+ Config:
31804
+ config init create ~/.demian/config.json
31805
+ config models <provider> [--refresh]
31806
+ list configured or remote provider models
31807
+ auth login codex|claudecode
31808
+ start the external OAuth login flow
30330
31809
  `);
30331
31810
  }
30332
31811
  if (import.meta.url === `file://${process.argv[1]}`) {