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/index.mjs CHANGED
@@ -92,7 +92,7 @@ var claudeCodeAgent = {
92
92
  name: "claudecode",
93
93
  description: "Claude Code external coding agent for focused delegated workspace tasks.",
94
94
  mode: "subagent",
95
- provider: { profile: "claudecode-subagent", permissionProfile: "build" },
95
+ provider: { profile: "claudecode", permissionProfile: "build" },
96
96
  tools: [],
97
97
  systemPrompt: [
98
98
  "You are demian claudecode, a Claude Code external runtime working as a sub agent for Demian.",
@@ -137,7 +137,7 @@ var claudeCodeExplorerAgent = {
137
137
  name: "claudecode-explorer",
138
138
  description: "Claude Code external read-only explorer for cowork repository inspection.",
139
139
  mode: "subagent",
140
- provider: { profile: "claudecode-subagent", permissionProfile: "explore" },
140
+ provider: { profile: "claudecode", permissionProfile: "explore" },
141
141
  tools: [],
142
142
  systemPrompt: [
143
143
  "You are demian claudecode-explorer, a Claude Code external runtime working as a read-only cowork sub agent for Demian.",
@@ -172,7 +172,7 @@ var claudeCodeBuilderAgent = {
172
172
  name: "claudecode-builder",
173
173
  description: "Claude Code-backed builder for bounded cowork implementation tasks.",
174
174
  mode: "subagent",
175
- provider: { profile: "claudecode-subagent", permissionProfile: "build" },
175
+ provider: { profile: "claudecode", permissionProfile: "build" },
176
176
  tools: [],
177
177
  systemPrompt: [
178
178
  "You are demian claudecode-builder, a Claude Code external runtime working as a writer cowork sub agent for Demian.",
@@ -477,7 +477,7 @@ import { readFile as readFile6 } from "node:fs/promises";
477
477
  import crypto4 from "node:crypto";
478
478
  import fs7 from "node:fs";
479
479
  import os7 from "node:os";
480
- import path12 from "node:path";
480
+ import path13 from "node:path";
481
481
 
482
482
  // src/providers/retry.ts
483
483
  var RETRY_STATUS = /* @__PURE__ */ new Set([408, 409, 425, 429, 500, 502, 503, 504]);
@@ -870,12 +870,14 @@ var dynamicImport = new Function("specifier", "return import(specifier)");
870
870
  var AnthropicProvider = class {
871
871
  id = "anthropic";
872
872
  #apiKey;
873
+ #baseURL;
873
874
  #defaultModel;
874
875
  #defaultMaxTokens;
875
876
  #client;
876
877
  #onRetry;
877
878
  constructor(config) {
878
879
  this.#apiKey = config.apiKey;
880
+ this.#baseURL = config.baseURL;
879
881
  this.#defaultModel = config.defaultModel;
880
882
  this.#defaultMaxTokens = config.defaultMaxTokens ?? 4096;
881
883
  this.#client = config.client;
@@ -920,7 +922,7 @@ var AnthropicProvider = class {
920
922
  if (this.#client) return this.#client;
921
923
  if (!this.#apiKey) throw new Error("AnthropicProvider requires apiKey");
922
924
  const Anthropic = await loadAnthropicConstructor();
923
- this.#client = new Anthropic({ apiKey: this.#apiKey });
925
+ this.#client = new Anthropic({ apiKey: this.#apiKey, baseURL: this.#baseURL });
924
926
  return this.#client;
925
927
  }
926
928
  };
@@ -1153,19 +1155,56 @@ import { execFile } from "node:child_process";
1153
1155
  import { promisify } from "node:util";
1154
1156
  import fs2 from "node:fs";
1155
1157
  import { chmod, mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "node:fs/promises";
1156
- import path2 from "node:path";
1158
+ import path3 from "node:path";
1157
1159
 
1158
1160
  // src/providers/codex-state.ts
1159
1161
  import crypto from "node:crypto";
1160
1162
  import fs from "node:fs";
1161
1163
  import { mkdir, readFile, writeFile } from "node:fs/promises";
1164
+ import os2 from "node:os";
1165
+ import path2 from "node:path";
1166
+
1167
+ // src/path-expansion.ts
1162
1168
  import os from "node:os";
1163
1169
  import path from "node:path";
1170
+ function expandPathReferences(value) {
1171
+ let expanded = expandTilde(value);
1172
+ 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);
1173
+ expanded = expanded.replace(/%([A-Za-z_][A-Za-z0-9_]*)%/g, (match, name) => envPathValue(name) ?? match);
1174
+ return expanded;
1175
+ }
1176
+ function resolveExpandedPath(value) {
1177
+ return path.resolve(expandPathReferences(value));
1178
+ }
1179
+ function expandTilde(value) {
1180
+ if (value === "~") return os.homedir();
1181
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(os.homedir(), value.slice(2));
1182
+ return value;
1183
+ }
1184
+ function envPathValue(name) {
1185
+ if (!name) return void 0;
1186
+ const value = processEnvValue(name);
1187
+ if (value !== void 0) return value;
1188
+ const upper = name.toUpperCase();
1189
+ if (upper === "HOME" || upper === "USERPROFILE") return os.homedir();
1190
+ if (process.platform === "win32" && upper === "HOMEDRIVE") return path.win32.parse(os.homedir()).root.replace(/[\\/]$/, "");
1191
+ if (process.platform === "win32" && upper === "HOMEPATH") return os.homedir().replace(/^[A-Za-z]:/, "");
1192
+ return void 0;
1193
+ }
1194
+ function processEnvValue(name) {
1195
+ if (process.env[name] !== void 0) return process.env[name];
1196
+ if (process.platform !== "win32") return void 0;
1197
+ const upper = name.toUpperCase();
1198
+ const key = Object.keys(process.env).find((item) => item.toUpperCase() === upper);
1199
+ return key ? process.env[key] : void 0;
1200
+ }
1201
+
1202
+ // src/providers/codex-state.ts
1164
1203
  function resolveCodexHome(configured) {
1165
- return path.resolve(expandCodexPath(configured ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex")));
1204
+ return resolveExpandedPath(configured ?? process.env.CODEX_HOME ?? path2.join(os2.homedir(), ".codex"));
1166
1205
  }
1167
1206
  async function loadOrCreateInstallationId(codexHome) {
1168
- const filePath = path.join(codexHome, "installation_id");
1207
+ const filePath = path2.join(codexHome, "installation_id");
1169
1208
  try {
1170
1209
  const existing = (await readFile(filePath, "utf8")).trim();
1171
1210
  if (isUuid(existing)) return existing;
@@ -1187,15 +1226,6 @@ function isNodeError(error, code) {
1187
1226
  function fileExists(filePath) {
1188
1227
  return fs.existsSync(filePath);
1189
1228
  }
1190
- function expandCodexPath(value) {
1191
- let expanded = value;
1192
- if (expanded === "~") return os.homedir();
1193
- if (expanded.startsWith("~/")) expanded = path.join(os.homedir(), expanded.slice(2));
1194
- return expanded.replace(/\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/gi, (match, braced, bare) => {
1195
- const name = braced ?? bare;
1196
- return name && process.env[name] !== void 0 ? process.env[name] : match;
1197
- });
1198
- }
1199
1229
 
1200
1230
  // src/providers/codex-auth.ts
1201
1231
  var execFileAsync = promisify(execFile);
@@ -1418,10 +1448,10 @@ var CodexAuthStore = class {
1418
1448
  }
1419
1449
  };
1420
1450
  function authFilePath(codexHome) {
1421
- return path2.join(codexHome, "auth.json");
1451
+ return path3.join(codexHome, "auth.json");
1422
1452
  }
1423
1453
  function codexKeyringAccount(codexHome) {
1424
- const resolved = path2.resolve(codexHome);
1454
+ const resolved = path3.resolve(codexHome);
1425
1455
  const canonical = fs2.existsSync(resolved) ? fs2.realpathSync.native(resolved) : resolved;
1426
1456
  const hash = crypto2.createHash("sha256").update(canonical).digest("hex").slice(0, 16);
1427
1457
  return `cli|${hash}`;
@@ -1856,8 +1886,8 @@ import { randomUUID } from "node:crypto";
1856
1886
  import { execFile as execFile2 } from "node:child_process";
1857
1887
  import fs3 from "node:fs";
1858
1888
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "node:fs/promises";
1859
- import os2 from "node:os";
1860
- import path3 from "node:path";
1889
+ import os3 from "node:os";
1890
+ import path4 from "node:path";
1861
1891
  import { promisify as promisify2 } from "node:util";
1862
1892
  var execFileAsync2 = promisify2(execFile2);
1863
1893
  var CLAUDE_CODE_KEYRING_SERVICE = "Claude Code-credentials";
@@ -1890,7 +1920,7 @@ function getSharedClaudeCodeAuthStore(options = {}) {
1890
1920
  proactiveRefreshMinutes: options.proactiveRefreshMinutes ?? 30,
1891
1921
  refreshCache: options.refreshCache ?? "claude-store",
1892
1922
  refreshTokenURL: options.refreshTokenURL ?? DEFAULT_CLAUDE_CODE_REFRESH_TOKEN_URL,
1893
- keychainAccount: options.keychainAccount ?? os2.userInfo().username
1923
+ keychainAccount: options.keychainAccount ?? os3.userInfo().username
1894
1924
  });
1895
1925
  const existing = sharedAuthStores2.get(key);
1896
1926
  if (existing) return existing;
@@ -1919,7 +1949,7 @@ var ClaudeCodeAuthStore = class {
1919
1949
  this.#proactiveRefreshMinutes = options.proactiveRefreshMinutes ?? 30;
1920
1950
  this.#refreshCache = options.refreshCache ?? "claude-store";
1921
1951
  this.#refreshTokenURL = options.refreshTokenURL ?? DEFAULT_CLAUDE_CODE_REFRESH_TOKEN_URL;
1922
- this.#keychainAccount = options.keychainAccount ?? os2.userInfo().username;
1952
+ this.#keychainAccount = options.keychainAccount ?? os3.userInfo().username;
1923
1953
  this.#fetch = options.fetch ?? fetch;
1924
1954
  this.#keyring = options.keyring ?? new MacOSSecurityKeyring2();
1925
1955
  }
@@ -2120,10 +2150,10 @@ var ClaudeCodeAuthStore = class {
2120
2150
  }
2121
2151
  };
2122
2152
  function resolveClaudeConfigDir(configured) {
2123
- return path3.resolve(expandClaudePath(configured ?? process.env.CLAUDE_CONFIG_DIR ?? path3.join(os2.homedir(), ".claude")));
2153
+ return resolveExpandedPath(configured ?? process.env.CLAUDE_CONFIG_DIR ?? path4.join(os3.homedir(), ".claude"));
2124
2154
  }
2125
2155
  function claudeCodeAuthFilePath(claudeConfigDir) {
2126
- return path3.join(claudeConfigDir, ".credentials.json");
2156
+ return path4.join(claudeConfigDir, ".credentials.json");
2127
2157
  }
2128
2158
  function oauthPayload(auth) {
2129
2159
  const raw = auth.claudeAiOauth;
@@ -2182,15 +2212,6 @@ async function writeClaudeCodeAuthFileAtomic(claudeConfigDir, auth) {
2182
2212
  await chmod2(tempPath, 384);
2183
2213
  await rename2(tempPath, filePath);
2184
2214
  }
2185
- function expandClaudePath(value) {
2186
- let expanded = value;
2187
- if (expanded === "~") return os2.homedir();
2188
- if (expanded.startsWith("~/")) expanded = path3.join(os2.homedir(), expanded.slice(2));
2189
- return expanded.replace(/\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/gi, (match, braced, bare) => {
2190
- const name = braced ?? bare;
2191
- return name && process.env[name] !== void 0 ? process.env[name] : match;
2192
- });
2193
- }
2194
2215
  var MacOSSecurityKeyring2 = class {
2195
2216
  async load(service, account) {
2196
2217
  if (process.platform !== "darwin") return void 0;
@@ -2427,6 +2448,111 @@ function parseClaudeCodeErrorBody(body) {
2427
2448
  }
2428
2449
  }
2429
2450
 
2451
+ // src/providers/ollama.ts
2452
+ var OllamaProvider = class {
2453
+ id = "ollama";
2454
+ #baseURL;
2455
+ #apiKey;
2456
+ #defaultModel;
2457
+ #fetch;
2458
+ #onRetry;
2459
+ constructor(config) {
2460
+ this.#baseURL = config.baseURL.replace(/\/+$/, "");
2461
+ this.#apiKey = config.apiKey;
2462
+ this.#defaultModel = config.defaultModel;
2463
+ this.#fetch = config.fetch ?? fetch;
2464
+ this.#onRetry = config.onRetry;
2465
+ }
2466
+ async chat(req) {
2467
+ return chatWithRetry(() => this.#rawChat(req), {
2468
+ signal: req.signal,
2469
+ onRetry: this.#onRetry
2470
+ });
2471
+ }
2472
+ async #rawChat(req) {
2473
+ const response = await this.#fetch(`${this.#baseURL}/chat`, {
2474
+ method: "POST",
2475
+ headers: {
2476
+ "content-type": "application/json",
2477
+ ...this.#authHeaders()
2478
+ },
2479
+ body: JSON.stringify({
2480
+ model: req.model || this.#defaultModel,
2481
+ messages: req.messages.map(toOllamaMessage),
2482
+ ...req.tools.length > 0 ? { tools: req.tools.map(toOpenAITool) } : {},
2483
+ stream: false,
2484
+ ...req.temperature !== void 0 ? { options: { temperature: req.temperature } } : {}
2485
+ }),
2486
+ signal: req.signal
2487
+ });
2488
+ const text = await response.text();
2489
+ if (!response.ok) throw new ProviderHttpError(response.status, text, parseRetryAfter(response.headers.get("retry-after")));
2490
+ return normalizeOllamaResponse(text ? JSON.parse(text) : {});
2491
+ }
2492
+ #authHeaders() {
2493
+ return this.#apiKey ? { authorization: `Bearer ${this.#apiKey}` } : {};
2494
+ }
2495
+ };
2496
+ function toOllamaMessage(message) {
2497
+ if (message.role === "tool") {
2498
+ return {
2499
+ role: "tool",
2500
+ content: message.content,
2501
+ tool_name: message.name
2502
+ };
2503
+ }
2504
+ const out = {
2505
+ role: message.role,
2506
+ content: typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? "")
2507
+ };
2508
+ if (message.role === "assistant" && message.toolCalls?.length) {
2509
+ out.tool_calls = message.toolCalls.map((call) => ({
2510
+ function: {
2511
+ name: call.name,
2512
+ arguments: call.input ?? {}
2513
+ }
2514
+ }));
2515
+ }
2516
+ return out;
2517
+ }
2518
+ function normalizeOllamaResponse(raw) {
2519
+ const data = raw;
2520
+ const toolCalls = [];
2521
+ for (const call of data.message?.tool_calls ?? []) {
2522
+ const name = call.function?.name;
2523
+ if (!name) continue;
2524
+ toolCalls.push({
2525
+ id: call.id ?? `call_${toolCalls.length + 1}`,
2526
+ name,
2527
+ input: normalizeToolArguments(call.function?.arguments)
2528
+ });
2529
+ }
2530
+ const message = {
2531
+ role: "assistant",
2532
+ content: data.message?.content ?? null,
2533
+ ...toolCalls.length ? { toolCalls } : {}
2534
+ };
2535
+ return {
2536
+ message,
2537
+ toolCalls,
2538
+ stopReason: toolCalls.length ? "tool_use" : data.done_reason === "length" ? "max_tokens" : "end_turn",
2539
+ usage: data.prompt_eval_count !== void 0 || data.eval_count !== void 0 ? {
2540
+ inputTokens: data.prompt_eval_count,
2541
+ outputTokens: data.eval_count,
2542
+ totalTokens: (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0)
2543
+ } : void 0,
2544
+ raw
2545
+ };
2546
+ }
2547
+ function normalizeToolArguments(value) {
2548
+ if (typeof value !== "string") return value ?? {};
2549
+ try {
2550
+ return JSON.parse(value);
2551
+ } catch {
2552
+ return {};
2553
+ }
2554
+ }
2555
+
2430
2556
  // src/external-runtime/claudecode-cli.ts
2431
2557
  import { spawn as spawn3 } from "node:child_process";
2432
2558
  import readline from "node:readline";
@@ -2434,7 +2560,7 @@ import readline from "node:readline";
2434
2560
  // src/external-runtime/claudecode-attachments.ts
2435
2561
  import crypto3 from "node:crypto";
2436
2562
  import fs4 from "node:fs";
2437
- import path4 from "node:path";
2563
+ import path5 from "node:path";
2438
2564
  async function resolveClaudeCodeAttachmentPrompt(runtimeLabel, config, req) {
2439
2565
  const attachments = req.attachments ?? [];
2440
2566
  if (attachments.length === 0) return req.prompt;
@@ -2476,7 +2602,7 @@ function unsupportedAttachmentError(runtimeLabel, count, detail) {
2476
2602
  }
2477
2603
  function formatAttachmentReference(value, cwd) {
2478
2604
  if (isUrl(value)) return imageMimeFromPath(value) ? `[image: ${value} (${imageMimeFromPath(value)}, remote)]` : `[attachment: ${value}]`;
2479
- const target = path4.isAbsolute(value) ? value : path4.resolve(cwd, value);
2605
+ const target = path5.isAbsolute(value) ? value : path5.resolve(cwd, value);
2480
2606
  try {
2481
2607
  const stats = fs4.statSync(target);
2482
2608
  if (!stats.isFile()) return `[attachment: ${value} (${stats.size} bytes)]`;
@@ -2493,7 +2619,7 @@ function isUrl(value) {
2493
2619
  }
2494
2620
  function imageMimeFromPath(value) {
2495
2621
  const pathname = isUrl(value) ? new URL(value).pathname : value;
2496
- const ext = path4.extname(pathname).toLowerCase();
2622
+ const ext = path5.extname(pathname).toLowerCase();
2497
2623
  if (ext === ".png") return "image/png";
2498
2624
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
2499
2625
  if (ext === ".gif") return "image/gif";
@@ -2525,15 +2651,14 @@ function hasAnthropicApiKeyEnv(env = process.env) {
2525
2651
 
2526
2652
  // src/external-runtime/claudecode-paths.ts
2527
2653
  import fs5 from "node:fs";
2528
- import os3 from "node:os";
2529
- import path5 from "node:path";
2654
+ import path6 from "node:path";
2530
2655
  function resolveClaudeCodeCliPath(configured) {
2531
2656
  if (configured) return expandHome(configured);
2532
2657
  if (process.env.CLAUDE_CODE_CLI) return expandHome(process.env.CLAUDE_CODE_CLI);
2533
2658
  const candidates = ["~/.local/bin/claude", "claude"];
2534
2659
  for (const candidate of candidates) {
2535
2660
  const expanded = expandHome(candidate);
2536
- if (path5.isAbsolute(expanded)) {
2661
+ if (path6.isAbsolute(expanded)) {
2537
2662
  if (isExecutableFile(expanded)) return expanded;
2538
2663
  continue;
2539
2664
  }
@@ -2542,9 +2667,7 @@ function resolveClaudeCodeCliPath(configured) {
2542
2667
  return void 0;
2543
2668
  }
2544
2669
  function expandHome(value) {
2545
- if (value === "~") return os3.homedir();
2546
- if (value.startsWith("~/")) return path5.join(os3.homedir(), value.slice(2));
2547
- return value;
2670
+ return expandPathReferences(value);
2548
2671
  }
2549
2672
  function isExecutableFile(filePath) {
2550
2673
  try {
@@ -2810,14 +2933,14 @@ function numberValue2(value) {
2810
2933
  // src/external-runtime/session-lock.ts
2811
2934
  import { mkdir as mkdir4, open, readFile as readFile4, unlink } from "node:fs/promises";
2812
2935
  import os4 from "node:os";
2813
- import path6 from "node:path";
2936
+ import path7 from "node:path";
2814
2937
  var DEFAULT_STALE_MS = 30 * 60 * 1e3;
2815
2938
  async function acquireClaudeCodeSessionLock(sessionId, options = {}) {
2816
- const dir = options.dir ?? path6.join(os4.homedir(), ".demian", "claude-sessions");
2939
+ const dir = options.dir ?? path7.join(os4.homedir(), ".demian", "claude-sessions");
2817
2940
  const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
2818
2941
  const now2 = options.now ?? (() => Date.now());
2819
2942
  await mkdir4(dir, { recursive: true });
2820
- const lockPath = path6.join(dir, `${encodeURIComponent(sessionId)}.lock`);
2943
+ const lockPath = path7.join(dir, `${encodeURIComponent(sessionId)}.lock`);
2821
2944
  for (let attempt = 0; attempt < 2; attempt++) {
2822
2945
  try {
2823
2946
  const handle = await open(lockPath, "wx");
@@ -2881,14 +3004,14 @@ function isAlreadyExists(error) {
2881
3004
  // src/external-runtime/usage-ledger.ts
2882
3005
  import { mkdir as mkdir5, readFile as readFile5, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
2883
3006
  import os5 from "node:os";
2884
- import path7 from "node:path";
3007
+ import path8 from "node:path";
2885
3008
  var processLedger = /* @__PURE__ */ new Map();
2886
3009
  var ClaudeCodeUsageLedger = class {
2887
3010
  #scope;
2888
3011
  #filePath;
2889
3012
  constructor(options = {}) {
2890
3013
  this.#scope = options.scope ?? "process";
2891
- this.#filePath = options.filePath ?? path7.join(os5.homedir(), ".demian", "usage-ledger.json");
3014
+ this.#filePath = options.filePath ?? path8.join(os5.homedir(), ".demian", "usage-ledger.json");
2892
3015
  }
2893
3016
  async spentUsd(key, now2 = /* @__PURE__ */ new Date()) {
2894
3017
  if (this.#scope === "process") return processLedger.get(processKey(key)) ?? 0;
@@ -2919,7 +3042,7 @@ var ClaudeCodeUsageLedger = class {
2919
3042
  return { version: 1, buckets: {} };
2920
3043
  }
2921
3044
  async #write(file) {
2922
- await mkdir5(path7.dirname(this.#filePath), { recursive: true });
3045
+ await mkdir5(path8.dirname(this.#filePath), { recursive: true });
2923
3046
  const temp = `${this.#filePath}.${process.pid}.tmp`;
2924
3047
  await writeFile4(temp, `${JSON.stringify(file, null, 2)}
2925
3048
  `, "utf8");
@@ -22580,10 +22703,10 @@ function now() {
22580
22703
  }
22581
22704
 
22582
22705
  // src/permissions/engine.ts
22583
- import path10 from "node:path";
22706
+ import path11 from "node:path";
22584
22707
 
22585
22708
  // src/permissions/grants.ts
22586
- import path8 from "node:path";
22709
+ import path9 from "node:path";
22587
22710
 
22588
22711
  // src/util.ts
22589
22712
  function stableStringify(value) {
@@ -22638,8 +22761,8 @@ function grantKeyFor(tool, input2, cwd) {
22638
22761
  }
22639
22762
  if (tool === "write_file" || tool === "edit_file") {
22640
22763
  const filePath = typeof object2.path === "string" ? object2.path : "";
22641
- const parent = filePath ? path8.dirname(path8.resolve(cwd, filePath)) : cwd;
22642
- return `${tool}:${path8.relative(cwd, parent) || "."}`;
22764
+ const parent = filePath ? path9.dirname(path9.resolve(cwd, filePath)) : cwd;
22765
+ return `${tool}:${path9.relative(cwd, parent) || "."}`;
22643
22766
  }
22644
22767
  return `${tool}:${stableStringify(input2)}`;
22645
22768
  }
@@ -22666,30 +22789,30 @@ function firstCommandTokens(command, count) {
22666
22789
  }
22667
22790
 
22668
22791
  // src/workspace/paths.ts
22669
- import path9 from "node:path";
22792
+ import path10 from "node:path";
22670
22793
  function normalizePathForMatch(value) {
22671
- return value.split(path9.sep).join("/");
22794
+ return value.split(path10.sep).join("/");
22672
22795
  }
22673
22796
  function isInsidePath(root, candidate) {
22674
- const relative = path9.relative(path9.resolve(root), path9.resolve(candidate));
22675
- return relative === "" || !relative.startsWith("..") && !path9.isAbsolute(relative);
22797
+ const relative = path10.relative(path10.resolve(root), path10.resolve(candidate));
22798
+ return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
22676
22799
  }
22677
22800
  function resolveInsideCwd(cwd, inputPath) {
22678
22801
  if (typeof inputPath !== "string" || inputPath.length === 0) {
22679
22802
  throw new Error("path must be a non-empty string");
22680
22803
  }
22681
- const resolved = path9.resolve(cwd, inputPath);
22804
+ const resolved = path10.resolve(cwd, inputPath);
22682
22805
  if (!isInsidePath(cwd, resolved)) {
22683
22806
  throw new Error(`Path is outside the workspace: ${inputPath}`);
22684
22807
  }
22685
22808
  return resolved;
22686
22809
  }
22687
22810
  function relativeToCwd(cwd, absolutePath) {
22688
- const relative = path9.relative(cwd, absolutePath);
22811
+ const relative = path10.relative(cwd, absolutePath);
22689
22812
  return normalizePathForMatch(relative || ".");
22690
22813
  }
22691
22814
  function isEnvFile(filePath) {
22692
- const base = path9.basename(filePath);
22815
+ const base = path10.basename(filePath);
22693
22816
  if (base === ".env.example") return false;
22694
22817
  return base === ".env" || base.startsWith(".env.");
22695
22818
  }
@@ -22796,7 +22919,7 @@ var PermissionEngine = class {
22796
22919
  #hardDeny(tool, input2, grantKey) {
22797
22920
  const paths = inputPaths(tool, input2);
22798
22921
  for (const item of paths) {
22799
- const resolved = path10.resolve(this.#cwd, item);
22922
+ const resolved = path11.resolve(this.#cwd, item);
22800
22923
  if (!isInsidePath(this.#cwd, resolved)) {
22801
22924
  return {
22802
22925
  decision: "deny",
@@ -22850,7 +22973,7 @@ function matchesRule(rule, tool, input2, cwd) {
22850
22973
  if (rule.match.pathGlob) {
22851
22974
  const paths = inputPaths(tool, input2);
22852
22975
  if (paths.length === 0) return false;
22853
- if (!paths.some((item) => matchesPathRule(rule.match?.pathGlob, relativeToCwd(cwd, path10.resolve(cwd, item))))) {
22976
+ if (!paths.some((item) => matchesPathRule(rule.match?.pathGlob, relativeToCwd(cwd, path11.resolve(cwd, item))))) {
22854
22977
  return false;
22855
22978
  }
22856
22979
  }
@@ -22897,7 +23020,7 @@ function rulePriority(rule, ref) {
22897
23020
  }
22898
23021
  function matchesPathRule(pattern, relativePath) {
22899
23022
  if (!pattern) return true;
22900
- if (path10.basename(relativePath) === ".env.example" && pattern.includes(".env.*")) return false;
23023
+ if (path11.basename(relativePath) === ".env.example" && pattern.includes(".env.*")) return false;
22901
23024
  return matchGlob(pattern, relativePath);
22902
23025
  }
22903
23026
  function mostSpecificRule(rules, input2, cwd, ref) {
@@ -22975,13 +23098,13 @@ function permissionLabel(req) {
22975
23098
  if ((req.tool === "write_file" || req.tool === "edit_file" || req.tool === "read_file") && typeof inputObject.path === "string") {
22976
23099
  return `${req.tool}: ${inputObject.path}`;
22977
23100
  }
22978
- const path27 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
22979
- if (path27) return `${tool}: ${path27}`;
23101
+ const path29 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
23102
+ if (path29) return `${tool}: ${path29}`;
22980
23103
  return tool;
22981
23104
  }
22982
23105
 
22983
23106
  // src/workspace/write-scope.ts
22984
- import path11 from "node:path";
23107
+ import path12 from "node:path";
22985
23108
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "Write", "Edit", "MultiEdit", "NotebookEdit"]);
22986
23109
  var WORKSPACE_SCOPE = "**";
22987
23110
  function normalizeWriteScope(cwd, scope) {
@@ -23003,7 +23126,7 @@ function enforceToolWriteScope(input2) {
23003
23126
  if (paths.length === 0) return { ok: true, paths: [] };
23004
23127
  const checked = [];
23005
23128
  for (const target of paths) {
23006
- const resolved = path11.resolve(input2.cwd, target);
23129
+ const resolved = path12.resolve(input2.cwd, target);
23007
23130
  const relative = relativeToCwd(input2.cwd, resolved);
23008
23131
  checked.push(relative);
23009
23132
  const allowed = isPathAllowedByWriteScope(input2.cwd, target, input2.writeScope);
@@ -23027,7 +23150,7 @@ function outOfScopeDiffFiles(cwd, summary, writeScope) {
23027
23150
  return out;
23028
23151
  }
23029
23152
  function isPathAllowedByWriteScope(cwd, inputPath, writeScope) {
23030
- const resolved = path11.resolve(cwd, inputPath);
23153
+ const resolved = path12.resolve(cwd, inputPath);
23031
23154
  if (!isInsidePath(cwd, resolved)) {
23032
23155
  return { ok: false, error: "path is outside the workspace", paths: [inputPath] };
23033
23156
  }
@@ -23046,9 +23169,9 @@ function normalizeScopePattern(cwd, raw) {
23046
23169
  if (!value) return { ok: false, error: "writeScope entries must be non-empty." };
23047
23170
  if (value.includes("\0")) return { ok: false, error: "writeScope entries must not contain NUL bytes." };
23048
23171
  const hasGlob2 = /[*?[\]{}]/.test(value);
23049
- if (path11.isAbsolute(value)) {
23172
+ if (path12.isAbsolute(value)) {
23050
23173
  if (hasGlob2) return { ok: false, error: `writeScope absolute globs are not supported: ${raw}` };
23051
- const resolved = path11.resolve(value);
23174
+ const resolved = path12.resolve(value);
23052
23175
  if (!isInsidePath(cwd, resolved)) return { ok: false, error: `writeScope path is outside the workspace: ${raw}` };
23053
23176
  value = relativeToCwd(cwd, resolved);
23054
23177
  }
@@ -23425,6 +23548,7 @@ function claudeCodePreviewEnabled(config = {}, env = process.env) {
23425
23548
  }
23426
23549
 
23427
23550
  // src/config.ts
23551
+ var BUILTIN_PROVIDER_ORDER = ["openai", "anthropic", "gemini", "groq", "azure", "lmstudio", "ollama-local", "ollama-cloud", "llamacpp", "vllm", "codex", "claudecode"];
23428
23552
  var CLAUDE_CODE_SONNET_MODEL = "claude-sonnet-4-6";
23429
23553
  var CLAUDE_CODE_OPUS_MODEL = "claude-opus-4-7";
23430
23554
  var CLAUDE_CODE_MODELS = [CLAUDE_CODE_SONNET_MODEL, CLAUDE_CODE_OPUS_MODEL];
@@ -23462,7 +23586,7 @@ var defaultConfig = {
23462
23586
  maxConcurrentExpensive: 1,
23463
23587
  maxConcurrentWriters: 1,
23464
23588
  maxConcurrentPerProvider: {
23465
- "claudecode-subagent": 1
23589
+ claudecode: 1
23466
23590
  },
23467
23591
  tokenBudget: null,
23468
23592
  defaultMergeStrategy: "synthesize",
@@ -23517,6 +23641,7 @@ var defaultConfig = {
23517
23641
  defaultProvider: "brave",
23518
23642
  providers: {
23519
23643
  brave: {
23644
+ apiKey: "",
23520
23645
  apiKeyEnv: "BRAVE_SEARCH_API_KEY",
23521
23646
  endpoint: "https://api.search.brave.com/res/v1/web/search",
23522
23647
  count: 10,
@@ -23525,12 +23650,14 @@ var defaultConfig = {
23525
23650
  safeSearch: "moderate"
23526
23651
  },
23527
23652
  tavily: {
23653
+ apiKey: "",
23528
23654
  apiKeyEnv: "TAVILY_API_KEY",
23529
23655
  endpoint: "https://api.tavily.com/search",
23530
23656
  maxResults: 5,
23531
23657
  searchDepth: "basic"
23532
23658
  },
23533
23659
  exa: {
23660
+ apiKey: "",
23534
23661
  apiKeyEnv: "EXA_API_KEY",
23535
23662
  endpoint: "https://api.exa.ai/search",
23536
23663
  numResults: 5,
@@ -23584,73 +23711,113 @@ var defaultConfig = {
23584
23711
  providers: {
23585
23712
  openai: {
23586
23713
  type: "openai-compatible",
23587
- model: "openai-model-name",
23588
23714
  baseURL: "https://api.openai.com/v1",
23589
- apiKeyEnv: "OPENAI_API_KEY"
23715
+ apiKey: "",
23716
+ apiKeyEnv: "OPENAI_API_KEY",
23717
+ catalog: {
23718
+ type: "openai-models",
23719
+ endpoint: "https://api.openai.com/v1/models"
23720
+ }
23721
+ },
23722
+ anthropic: {
23723
+ type: "anthropic",
23724
+ baseURL: "https://api.anthropic.com/v1",
23725
+ apiKey: "",
23726
+ apiKeyEnv: "ANTHROPIC_API_KEY",
23727
+ catalog: {
23728
+ type: "anthropic-models",
23729
+ endpoint: "https://api.anthropic.com/v1/models"
23730
+ }
23590
23731
  },
23591
23732
  gemini: {
23592
23733
  type: "openai-compatible",
23593
- model: "gemini-model-name",
23594
23734
  baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
23595
- apiKeyEnv: "GOOGLE_API_KEY"
23735
+ apiKey: "",
23736
+ apiKeyEnv: "GEMINI_API_KEY",
23737
+ apiKeyEnvAliases: ["GOOGLE_API_KEY"],
23738
+ catalog: {
23739
+ type: "gemini-openai-models",
23740
+ endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models"
23741
+ }
23596
23742
  },
23597
- ollama: {
23743
+ groq: {
23598
23744
  type: "openai-compatible",
23599
- model: "local-coder-model",
23600
- baseURL: "http://localhost:11434/v1",
23601
- apiKey: "ollama",
23602
- quirks: { omitTemperature: true }
23745
+ baseURL: "https://api.groq.com/openai/v1",
23746
+ apiKey: "",
23747
+ apiKeyEnv: "GROQ_API_KEY",
23748
+ catalog: {
23749
+ type: "groq-models",
23750
+ endpoint: "https://api.groq.com/openai/v1/models"
23751
+ }
23752
+ },
23753
+ azure: {
23754
+ type: "openai-compatible",
23755
+ auth: { type: "api-key", header: "api-key" },
23756
+ modelProfiles: [
23757
+ {
23758
+ name: "azure-example",
23759
+ displayName: "Azure example",
23760
+ model: "azure-deployment-name",
23761
+ baseURL: "https://example.openai.azure.com/openai/v1",
23762
+ apiKey: "",
23763
+ apiKeyEnv: "AZURE_OPENAI_API_KEY"
23764
+ }
23765
+ ]
23603
23766
  },
23604
23767
  lmstudio: {
23605
23768
  type: "openai-compatible",
23606
- model: "local-coder-model",
23607
23769
  baseURL: "http://localhost:1234/v1",
23608
- apiKey: "lm-studio"
23770
+ apiKey: "lm-studio",
23771
+ modelProfiles: [],
23772
+ catalog: {
23773
+ type: "openai-compatible-models",
23774
+ endpoint: "http://localhost:1234/v1/models"
23775
+ }
23609
23776
  },
23610
- vllm: {
23777
+ "ollama-local": {
23611
23778
  type: "openai-compatible",
23612
- model: "local-coder-model",
23613
- baseURL: "http://localhost:8000/v1",
23614
- apiKey: "vllm"
23779
+ baseURL: "http://localhost:11434/v1",
23780
+ apiKey: "ollama",
23781
+ modelProfiles: [],
23782
+ quirks: { omitTemperature: true },
23783
+ catalog: {
23784
+ type: "openai-compatible-models",
23785
+ endpoint: "http://localhost:11434/v1/models"
23786
+ }
23787
+ },
23788
+ "ollama-cloud": {
23789
+ type: "ollama",
23790
+ baseURL: "https://ollama.com/api",
23791
+ apiKey: "",
23792
+ apiKeyEnv: "OLLAMA_API_KEY",
23793
+ modelProfiles: [],
23794
+ catalog: {
23795
+ type: "ollama-tags",
23796
+ endpoint: "https://ollama.com/api/tags"
23797
+ }
23615
23798
  },
23616
23799
  llamacpp: {
23617
23800
  type: "openai-compatible",
23618
- model: "local-coder-model",
23619
23801
  baseURL: "http://localhost:8080/v1",
23620
- apiKey: "llama.cpp"
23621
- },
23622
- openrouter: {
23623
- type: "openai-compatible",
23624
- model: "provider/model-name",
23625
- baseURL: "https://openrouter.ai/api/v1",
23626
- apiKeyEnv: "OPENROUTER_API_KEY"
23627
- },
23628
- together: {
23629
- type: "openai-compatible",
23630
- model: "provider/model-name",
23631
- baseURL: "https://api.together.xyz/v1",
23632
- apiKeyEnv: "TOGETHER_API_KEY"
23633
- },
23634
- groq: {
23635
- type: "openai-compatible",
23636
- model: "provider/model-name",
23637
- baseURL: "https://api.groq.com/openai/v1",
23638
- apiKeyEnv: "GROQ_API_KEY"
23802
+ apiKey: "llama.cpp",
23803
+ modelProfiles: [],
23804
+ catalog: {
23805
+ type: "openai-compatible-models",
23806
+ endpoint: "http://localhost:8080/v1/models"
23807
+ }
23639
23808
  },
23640
- azure: {
23809
+ vllm: {
23641
23810
  type: "openai-compatible",
23642
- model: "azure-deployment-name",
23643
- baseURL: "https://example.openai.azure.com/openai/v1",
23644
- apiKeyEnv: "AZURE_OPENAI_API_KEY"
23645
- },
23646
- anthropic: {
23647
- type: "anthropic",
23648
- model: "anthropic-model-name",
23649
- apiKeyEnv: "ANTHROPIC_API_KEY"
23811
+ baseURL: "http://localhost:8000/v1",
23812
+ apiKey: "vllm",
23813
+ modelProfiles: [],
23814
+ catalog: {
23815
+ type: "openai-compatible-models",
23816
+ endpoint: "http://localhost:8000/v1/models"
23817
+ }
23650
23818
  },
23651
23819
  codex: {
23652
23820
  type: "codex",
23653
- model: "gpt-5.1-codex",
23654
23821
  baseURL: "https://chatgpt.com/backend-api/codex",
23655
23822
  authStore: "auto",
23656
23823
  allowApiKeyFallback: false,
@@ -23662,6 +23829,9 @@ var defaultConfig = {
23662
23829
  effort: "medium",
23663
23830
  summary: "auto"
23664
23831
  }
23832
+ },
23833
+ catalog: {
23834
+ type: "codex-oauth-models"
23665
23835
  }
23666
23836
  },
23667
23837
  claudecode: {
@@ -23680,67 +23850,37 @@ var defaultConfig = {
23680
23850
  useBareMode: false,
23681
23851
  usageLedgerScope: "process",
23682
23852
  sessionLock: true,
23683
- abortPolicy: "record-only"
23684
- },
23685
- "claudecode-plan": {
23686
- type: "claudecode",
23687
- runtime: "agent-sdk",
23688
- model: CLAUDE_CODE_SONNET_MODEL,
23689
- models: [...CLAUDE_CODE_MODELS],
23690
- permissionProfile: "plan",
23691
- cliPath: "~/.local/bin/claude",
23692
- cwdMode: "session",
23693
- historyPolicy: "passthrough-resume",
23694
- onInvalidResume: "fresh",
23695
- attachmentFallback: "block",
23696
- allowSubagents: false,
23697
- sanitizeApiKeyEnv: true,
23698
- authPreflight: true,
23699
- useBareMode: false,
23700
- usageLedgerScope: "process",
23701
- sessionLock: true,
23702
- abortPolicy: "record-only",
23703
- hidden: true
23704
- },
23705
- "claudecode-subagent": {
23706
- type: "claudecode",
23707
- runtime: "agent-sdk",
23708
- model: CLAUDE_CODE_SONNET_MODEL,
23709
- models: [...CLAUDE_CODE_MODELS],
23710
- permissionProfile: "build",
23711
- cliPath: "~/.local/bin/claude",
23712
- cwdMode: "session",
23713
- historyPolicy: "passthrough-resume",
23714
- onInvalidResume: "fresh",
23715
- attachmentFallback: "block",
23716
- allowSubagents: false,
23717
- sanitizeApiKeyEnv: true,
23718
- authPreflight: true,
23719
- useBareMode: false,
23720
- usageLedgerScope: "process",
23721
- sessionLock: true,
23722
23853
  abortPolicy: "record-only",
23723
- hidden: true
23854
+ catalog: {
23855
+ type: "claudecode-supported-models"
23856
+ }
23724
23857
  }
23725
23858
  }
23726
23859
  };
23727
23860
  async function loadConfig(options) {
23728
23861
  const configs = [];
23729
- const userConfigDir = path12.join(os7.homedir(), ".demian");
23730
- const projectConfigDir = path12.join(options.cwd, ".demian");
23731
- const configPaths = [
23732
- path12.join(userConfigDir, "config.json"),
23733
- path12.join(userConfigDir, "config.jsond"),
23734
- path12.join(projectConfigDir, "config.json"),
23735
- path12.join(projectConfigDir, "config.jsond"),
23736
- options.configPath
23737
- ].filter(Boolean);
23862
+ const userConfigDir = path13.join(os7.homedir(), ".demian");
23863
+ const configPaths = [path13.join(userConfigDir, "config.json"), path13.join(userConfigDir, "config.jsond"), options.configPath].filter(Boolean);
23738
23864
  for (const filePath of configPaths) {
23739
23865
  if (!fs7.existsSync(filePath)) continue;
23740
- configs.push(parseConfigText(await readFile6(filePath, "utf8"), filePath));
23866
+ const parsed = parseConfigText(await readFile6(filePath, "utf8"), filePath);
23867
+ warnIfV1Config(parsed, filePath);
23868
+ configs.push(parsed);
23741
23869
  }
23742
23870
  return configs.reduce((acc, item) => mergeConfig(acc, item), structuredClone(defaultConfig));
23743
23871
  }
23872
+ function warnIfV1Config(parsed, filePath) {
23873
+ if (parsed.version === 2) return;
23874
+ const providers = parsed.providers ?? {};
23875
+ const hasLegacyShape = Object.values(providers).some((provider) => {
23876
+ const candidate = provider;
23877
+ return (typeof candidate.model === "string" || Array.isArray(candidate.models)) && !Array.isArray(candidate.modelProfiles);
23878
+ });
23879
+ if (!hasLegacyShape && parsed.version === void 0 && Object.keys(providers).length === 0) return;
23880
+ if (!hasLegacyShape) return;
23881
+ 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.
23882
+ `);
23883
+ }
23744
23884
  function parseConfigText(text, filePath) {
23745
23885
  try {
23746
23886
  return JSON.parse(filePath.endsWith(".jsond") ? stripJsonD(text) : text);
@@ -23947,7 +24087,178 @@ function mergeConfig(base, overlay) {
23947
24087
  };
23948
24088
  }
23949
24089
  function normalizeProviderAlias(providerName) {
23950
- return providerName === "claudecode-plan" ? "claudecode" : providerName;
24090
+ if (providerName === "claudecode-plan" || providerName === "claudecode-subagent") return "claudecode";
24091
+ if (providerName === "ollama") return "ollama-local";
24092
+ return providerName;
24093
+ }
24094
+ function sortProviderNames(names) {
24095
+ const order = new Map(BUILTIN_PROVIDER_ORDER.map((name, index) => [name, index]));
24096
+ return [...names].sort((left, right) => {
24097
+ const leftIndex = order.get(left);
24098
+ const rightIndex = order.get(right);
24099
+ if (leftIndex !== void 0 && rightIndex !== void 0) return leftIndex - rightIndex;
24100
+ if (leftIndex !== void 0) return -1;
24101
+ if (rightIndex !== void 0) return 1;
24102
+ return left.localeCompare(right);
24103
+ });
24104
+ }
24105
+ function providerModelProfiles(providerName, providerConfig) {
24106
+ const explicit = Array.isArray(providerConfig.modelProfiles) ? providerConfig.modelProfiles.filter((profile) => typeof profile?.model === "string" && profile.model.trim()).map((profile) => ({
24107
+ ...profile,
24108
+ name: profile.name?.trim() || profile.displayName?.trim() || profile.model.trim(),
24109
+ displayName: profile.displayName?.trim() || profile.name?.trim() || profile.model.trim(),
24110
+ model: profile.model.trim()
24111
+ })) : [];
24112
+ const seenNames = /* @__PURE__ */ new Set();
24113
+ const seenDisplayNames = /* @__PURE__ */ new Set();
24114
+ const seenModels = /* @__PURE__ */ new Set();
24115
+ const out = [];
24116
+ for (const profile of explicit) {
24117
+ if (seenNames.has(profile.name)) {
24118
+ process.stderr.write(`[demian] warning: provider ${providerName} has duplicate modelProfile name "${profile.name}". Later entry ignored.
24119
+ `);
24120
+ continue;
24121
+ }
24122
+ if (profile.displayName && seenDisplayNames.has(profile.displayName)) {
24123
+ process.stderr.write(`[demian] warning: provider ${providerName} has duplicate modelProfile displayName "${profile.displayName}". Later entry ignored.
24124
+ `);
24125
+ continue;
24126
+ }
24127
+ seenNames.add(profile.name);
24128
+ if (profile.displayName) seenDisplayNames.add(profile.displayName);
24129
+ seenModels.add(profile.model);
24130
+ out.push(profile);
24131
+ }
24132
+ const legacy = [...typeof providerConfig.model === "string" && providerConfig.model.trim() ? [providerConfig.model.trim()] : [], ...(providerConfig.models ?? []).filter((model) => typeof model === "string" && model.trim())];
24133
+ for (const model of legacy) {
24134
+ const trimmed = model.trim();
24135
+ if (!trimmed || seenModels.has(trimmed)) continue;
24136
+ seenModels.add(trimmed);
24137
+ const name = trimmed;
24138
+ if (!seenNames.has(name)) seenNames.add(name);
24139
+ out.push({ name, displayName: name, model: trimmed });
24140
+ }
24141
+ const fallback = fallbackModelForProvider(providerName, providerConfig);
24142
+ if (out.length === 0 && fallback) out.push({ name: fallback, displayName: fallback, model: fallback });
24143
+ return out;
24144
+ }
24145
+ function providerModelOptions(providerName, providerConfig) {
24146
+ return providerModelProfiles(providerName, providerConfig).filter((profile) => !profile.hidden).map((profile) => displayModelForProfile(profile));
24147
+ }
24148
+ function defaultModelForProvider(providerName, providerConfig) {
24149
+ return providerModelProfiles(providerName, providerConfig)[0]?.model ?? "";
24150
+ }
24151
+ function displayModelForProfile(profile) {
24152
+ return profile.displayName?.trim() || profile.name?.trim() || profile.model;
24153
+ }
24154
+ function defaultDisplayModelForProvider(providerName, providerConfig) {
24155
+ const profile = providerModelProfiles(providerName, providerConfig)[0];
24156
+ return profile ? displayModelForProfile(profile) : defaultModelForProvider(providerName, providerConfig);
24157
+ }
24158
+ function resolveConfiguredApiKey(providerConfig) {
24159
+ if (providerConfig.apiKey) return { value: providerConfig.apiKey };
24160
+ for (const envName of [providerConfig.apiKeyEnv, ...providerConfig.apiKeyEnvAliases ?? []]) {
24161
+ if (!envName) continue;
24162
+ const value = process.env[envName];
24163
+ if (value) return { value, envName };
24164
+ }
24165
+ return { envName: providerConfig.apiKeyEnv ?? providerConfig.apiKeyEnvAliases?.[0] };
24166
+ }
24167
+ function resolveProviderRuntimeConfig(config, selection) {
24168
+ const providerName = normalizeProviderAlias(selection.providerName);
24169
+ const providerConfig = resolveProviderConfig(config, providerName);
24170
+ const profiles = providerModelProfiles(providerName, providerConfig);
24171
+ let profile;
24172
+ if (selection.modelProfileName) profile = profiles.find((item) => item.name === selection.modelProfileName);
24173
+ if (!profile && selection.model) profile = profiles.find((item) => item.displayName === selection.model);
24174
+ if (!profile && selection.model) profile = profiles.find((item) => item.model === selection.model);
24175
+ if (!profile) profile = profiles[0];
24176
+ if (!profile) {
24177
+ const model = selection.model ?? defaultModelForProvider(providerName, providerConfig);
24178
+ if (!model) throw new Error(`Provider ${providerName} has no model. Run \`demian config models ${providerName} --refresh\` or add a model profile.`);
24179
+ return { providerName, providerConfig, model };
24180
+ }
24181
+ const merged = mergeModelProfile(providerConfig, profile);
24182
+ return {
24183
+ providerName,
24184
+ providerConfig: merged,
24185
+ model: profile.model,
24186
+ modelProfileName: profile.name
24187
+ };
24188
+ }
24189
+ function resolveInternalAgentBackend(config, options = {}) {
24190
+ const tried = [];
24191
+ const candidates = uniqueProviderCandidates([options.preferredProvider, config.defaultProvider, ...Object.keys(config.providers)]);
24192
+ let lastError;
24193
+ for (const candidate of candidates) {
24194
+ tried.push(candidate);
24195
+ const provider = config.providers[normalizeProviderAlias(candidate)];
24196
+ if (!provider || provider.hidden) continue;
24197
+ try {
24198
+ const runtime = resolveProviderRuntimeConfig(config, {
24199
+ providerName: candidate,
24200
+ modelProfileName: candidate === options.preferredProvider ? options.modelProfileName : void 0,
24201
+ model: candidate === options.preferredProvider ? options.model : void 0
24202
+ });
24203
+ const source = candidate === options.preferredProvider ? "preferred" : candidate === config.defaultProvider ? "default" : "first-available";
24204
+ return {
24205
+ providerName: runtime.providerName,
24206
+ providerConfig: runtime.providerConfig,
24207
+ model: runtime.model,
24208
+ modelProfileName: runtime.modelProfileName,
24209
+ source,
24210
+ fallbackReason: source !== "preferred" && options.preferredProvider ? `preferred provider ${options.preferredProvider} unavailable` : void 0
24211
+ };
24212
+ } catch (error) {
24213
+ lastError = error instanceof Error ? error : new Error(String(error));
24214
+ }
24215
+ }
24216
+ throw new Error(`No usable provider for internal agent. Tried: ${tried.join(", ") || "(none)"}. Last error: ${lastError?.message ?? "no providers configured"}.`);
24217
+ }
24218
+ function uniqueProviderCandidates(values) {
24219
+ const seen = /* @__PURE__ */ new Set();
24220
+ const out = [];
24221
+ for (const value of values) {
24222
+ if (!value) continue;
24223
+ const normalized = normalizeProviderAlias(value);
24224
+ if (seen.has(normalized)) continue;
24225
+ seen.add(normalized);
24226
+ out.push(normalized);
24227
+ }
24228
+ return out;
24229
+ }
24230
+ function mergeModelProfile(providerConfig, profile) {
24231
+ const merged = {
24232
+ ...providerConfig,
24233
+ ...definedOnly({
24234
+ baseURL: profile.baseURL,
24235
+ apiKey: nonEmptyString(profile.apiKey),
24236
+ apiKeyEnv: profile.apiKeyEnv,
24237
+ apiKeyEnvAliases: profile.apiKeyEnvAliases,
24238
+ auth: profile.auth,
24239
+ headers: profile.headers,
24240
+ quirks: profile.quirks,
24241
+ maxTokens: profile.maxTokens
24242
+ }),
24243
+ model: profile.model
24244
+ };
24245
+ return merged;
24246
+ }
24247
+ function definedOnly(input2) {
24248
+ return Object.fromEntries(Object.entries(input2).filter(([, value]) => value !== void 0));
24249
+ }
24250
+ function nonEmptyString(value) {
24251
+ return typeof value === "string" && value.length > 0 ? value : void 0;
24252
+ }
24253
+ function fallbackModelForProvider(providerName, providerConfig) {
24254
+ if (typeof providerConfig.model === "string" && providerConfig.model.trim()) return providerConfig.model.trim();
24255
+ if (providerName === "openai") return "gpt-5.5";
24256
+ if (providerName === "anthropic") return CLAUDE_CODE_SONNET_MODEL;
24257
+ if (providerName === "codex" || providerConfig.type === "codex") return "gpt-5.5";
24258
+ if (providerName === "claudecode") return CLAUDE_CODE_SONNET_MODEL;
24259
+ if (providerName === "gemini") return "gemini-2.5-pro";
24260
+ if (providerName === "groq") return "openai/gpt-oss-120b";
24261
+ return void 0;
23951
24262
  }
23952
24263
  function migrateProviderConfig(provider, baseProvider) {
23953
24264
  if (isLegacyClaudeCodeShape(provider)) {
@@ -24029,12 +24340,14 @@ function resolveAgentMode(config, flagMode) {
24029
24340
  return "single-agent";
24030
24341
  }
24031
24342
  function resolveProviderConfig(config, providerName) {
24032
- const provider = config.providers[providerName];
24343
+ const normalized = normalizeProviderAlias(providerName);
24344
+ const provider = config.providers[normalized];
24033
24345
  if (!provider) throw new Error(`Unknown provider: ${providerName}`);
24034
24346
  return provider;
24035
24347
  }
24036
24348
  function resolveProvider(providerConfig, options = {}) {
24037
24349
  const model = options.model ?? providerConfig.model;
24350
+ if (!model) throw new Error("Provider has no selected model. Choose a model or add a model profile.");
24038
24351
  if (providerConfig.type === "claudecode") {
24039
24352
  throw new Error("claudecode is an external agent runtime. Use resolveExecutionBackend().");
24040
24353
  }
@@ -24081,12 +24394,28 @@ function resolveProvider(providerConfig, options = {}) {
24081
24394
  };
24082
24395
  }
24083
24396
  if (providerConfig.type === "anthropic") {
24084
- const apiKey2 = providerConfig.apiKey ?? (providerConfig.apiKeyEnv ? process.env[providerConfig.apiKeyEnv] : void 0);
24397
+ const apiKey2 = resolveConfiguredApiKey(providerConfig).value;
24085
24398
  if (!apiKey2) {
24086
24399
  throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24087
24400
  }
24088
24401
  return {
24089
24402
  provider: new AnthropicProvider({
24403
+ apiKey: apiKey2,
24404
+ baseURL: providerConfig.baseURL,
24405
+ defaultModel: model,
24406
+ onRetry: options.onRetry
24407
+ }),
24408
+ model
24409
+ };
24410
+ }
24411
+ if (providerConfig.type === "ollama") {
24412
+ const apiKey2 = resolveConfiguredApiKey(providerConfig).value;
24413
+ if (!apiKey2) {
24414
+ throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24415
+ }
24416
+ return {
24417
+ provider: new OllamaProvider({
24418
+ baseURL: providerConfig.baseURL,
24090
24419
  apiKey: apiKey2,
24091
24420
  defaultModel: model,
24092
24421
  onRetry: options.onRetry
@@ -24094,7 +24423,7 @@ function resolveProvider(providerConfig, options = {}) {
24094
24423
  model
24095
24424
  };
24096
24425
  }
24097
- const apiKey = providerConfig.apiKey ?? (providerConfig.apiKeyEnv ? process.env[providerConfig.apiKeyEnv] : void 0);
24426
+ const apiKey = resolveConfiguredApiKey(providerConfig).value;
24098
24427
  if (!apiKey) {
24099
24428
  throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
24100
24429
  }
@@ -24113,6 +24442,7 @@ function resolveProvider(providerConfig, options = {}) {
24113
24442
  }
24114
24443
  function resolveExecutionBackend(providerConfig, options = {}) {
24115
24444
  const model = options.model ?? providerConfig.model;
24445
+ if (!model) throw new Error("Provider has no selected model. Choose a model or add a model profile.");
24116
24446
  if (providerConfig.type === "claudecode") {
24117
24447
  const runtimeConfig = options.config || providerConfig.permissionProfile ? resolveClaudeCodeRuntimeConfig(providerConfig, options.config ?? defaultConfig, options.agent) : providerConfig;
24118
24448
  return {
@@ -24321,7 +24651,7 @@ function safeJson(value) {
24321
24651
  }
24322
24652
 
24323
24653
  // src/hooks/dispatcher.ts
24324
- import path14 from "node:path";
24654
+ import path15 from "node:path";
24325
24655
 
24326
24656
  // src/hooks/command.ts
24327
24657
  import { spawn as spawn4 } from "node:child_process";
@@ -24414,7 +24744,7 @@ var blockDangerousBashHook = {
24414
24744
  };
24415
24745
 
24416
24746
  // src/hooks/builtin/protect-env-files.ts
24417
- import path13 from "node:path";
24747
+ import path14 from "node:path";
24418
24748
  var protectEnvFilesHook = {
24419
24749
  name: "protect-env-files",
24420
24750
  event: "PreToolUse",
@@ -24422,7 +24752,7 @@ var protectEnvFilesHook = {
24422
24752
  run(ctx) {
24423
24753
  if (ctx.toolName !== "write_file" && ctx.toolName !== "edit_file") return { decision: "allow" };
24424
24754
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
24425
- const filePath = typeof input2.path === "string" ? path13.resolve(ctx.cwd, input2.path) : "";
24755
+ const filePath = typeof input2.path === "string" ? path14.resolve(ctx.cwd, input2.path) : "";
24426
24756
  if (filePath && isEnvFile(filePath)) {
24427
24757
  return {
24428
24758
  decision: "block",
@@ -24537,14 +24867,426 @@ function matchesHook(match, ctx) {
24537
24867
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
24538
24868
  const filePath = typeof input2.path === "string" ? input2.path : typeof input2.workdir === "string" ? input2.workdir : void 0;
24539
24869
  if (!filePath) return false;
24540
- if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path14.resolve(ctx.cwd, filePath)))) return false;
24870
+ if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path15.resolve(ctx.cwd, filePath)))) return false;
24541
24871
  }
24542
24872
  return true;
24543
24873
  }
24544
24874
 
24875
+ // src/models/catalog.ts
24876
+ import crypto5 from "node:crypto";
24877
+ import { mkdir as mkdir6, readFile as readFile7, rename as rename4, writeFile as writeFile5 } from "node:fs/promises";
24878
+ import os9 from "node:os";
24879
+ import path16 from "node:path";
24880
+ var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
24881
+ var PING_TIMEOUT_MS = 1500;
24882
+ var DEFAULT_CODEX_CLIENT_VERSION = "0.0.0";
24883
+ var dynamicImport2 = new Function("specifier", "return import(specifier)");
24884
+ var inflight = /* @__PURE__ */ new Map();
24885
+ var pingCache = /* @__PURE__ */ new Map();
24886
+ var PING_CACHE_TTL_MS = 30 * 1e3;
24887
+ var packageVersionPromise;
24888
+ async function listProviderModelCatalog(providerName, providerConfig, options = {}) {
24889
+ const key = `${catalogCacheKey(providerName, providerConfig)}:${options.refresh ? "refresh" : "default"}`;
24890
+ const existing = inflight.get(key);
24891
+ if (existing) return existing;
24892
+ const promise = doListProviderModelCatalog(providerName, providerConfig, options).finally(() => inflight.delete(key));
24893
+ inflight.set(key, promise);
24894
+ return promise;
24895
+ }
24896
+ async function doListProviderModelCatalog(providerName, providerConfig, options) {
24897
+ const staticModels = staticModelEntries(providerName, providerConfig);
24898
+ const catalog = providerConfig.catalog;
24899
+ if (!catalog || catalog.type === "static") {
24900
+ return { status: staticModels.length ? "ready" : "unavailable", providerName, models: staticModels, source: "config" };
24901
+ }
24902
+ const missingAuthEnv = missingCatalogAuth(providerConfig);
24903
+ if (missingAuthEnv) {
24904
+ return {
24905
+ status: "missing-auth",
24906
+ providerName,
24907
+ models: staticModels,
24908
+ source: staticModels.length ? "config" : "fallback",
24909
+ message: `missing-auth:${missingAuthEnv}`
24910
+ };
24911
+ }
24912
+ const cacheKey = catalogCacheKey(providerName, providerConfig);
24913
+ const ttlMs = catalog.refreshTtlMs ?? DEFAULT_CACHE_TTL_MS;
24914
+ const cached = await readCatalogCache(cacheKey, ttlMs, options.refresh !== true);
24915
+ if (cached && !options.refresh) return { ...cached, models: mergeCatalogWithStatic(staticModels, cached.models) };
24916
+ if (catalog.endpoint) {
24917
+ const reachable = await pingEndpoint(catalog.endpoint, options.fetch);
24918
+ if (!reachable) {
24919
+ const stale = await readCatalogCache(cacheKey, Number.POSITIVE_INFINITY, true);
24920
+ if (stale) return { ...stale, models: mergeCatalogWithStatic(staticModels, stale.models), message: `endpoint ${catalog.endpoint} unreachable; using cached models` };
24921
+ if (staticModels.length) return { status: "ready", providerName, models: staticModels, source: "config", message: `endpoint ${catalog.endpoint} unreachable; using configured profiles` };
24922
+ return { status: "unavailable", providerName, models: [], source: "fallback", message: `endpoint ${catalog.endpoint} unreachable` };
24923
+ }
24924
+ }
24925
+ try {
24926
+ const remote = await fetchCatalog(providerName, providerConfig, options);
24927
+ const merged = mergeCatalogWithStatic(staticModels, remote.models);
24928
+ const result = { ...remote, models: merged };
24929
+ if (result.status === "ready") await writeCatalogCache(cacheKey, result);
24930
+ return result;
24931
+ } catch (error) {
24932
+ const authMissing = isMissingAuthError(error);
24933
+ const stale = await readCatalogCache(cacheKey, Number.POSITIVE_INFINITY, true);
24934
+ if (stale) return { ...stale, status: authMissing ? "missing-auth" : stale.status, models: mergeCatalogWithStatic(staticModels, stale.models), message: errorMessage2(error) };
24935
+ if (staticModels.length) return { status: authMissing ? "missing-auth" : "ready", providerName, models: staticModels, source: "config", message: errorMessage2(error) };
24936
+ return { status: authMissing ? "missing-auth" : "unavailable", providerName, models: [], source: "fallback", message: errorMessage2(error) };
24937
+ }
24938
+ }
24939
+ async function pingEndpoint(endpoint, fetcher) {
24940
+ const now2 = Date.now();
24941
+ const cached = pingCache.get(endpoint);
24942
+ if (cached && cached.expiresAt > now2) return cached.ok;
24943
+ const ok2 = await tryPing(endpoint, fetcher);
24944
+ pingCache.set(endpoint, { ok: ok2, expiresAt: now2 + PING_CACHE_TTL_MS });
24945
+ return ok2;
24946
+ }
24947
+ async function tryPing(endpoint, fetcher) {
24948
+ const fn = fetcher ?? fetch;
24949
+ try {
24950
+ const response = await fn(endpoint, { method: "HEAD", signal: AbortSignal.timeout(PING_TIMEOUT_MS) });
24951
+ if (response.status < 500) return true;
24952
+ } catch {
24953
+ }
24954
+ try {
24955
+ const response = await fn(endpoint, { method: "GET", signal: AbortSignal.timeout(PING_TIMEOUT_MS), headers: { range: "bytes=0-0" } });
24956
+ return response.status < 500;
24957
+ } catch {
24958
+ return false;
24959
+ }
24960
+ }
24961
+ function invalidateCatalogPingCache(endpoint) {
24962
+ if (endpoint) pingCache.delete(endpoint);
24963
+ else pingCache.clear();
24964
+ }
24965
+ async function fetchCatalog(providerName, providerConfig, options) {
24966
+ if (providerConfig.type === "codex" || providerConfig.catalog?.type === "codex-oauth-models") return fetchCodexCatalog(providerName, providerConfig, options);
24967
+ if (providerConfig.type === "claudecode" || providerConfig.catalog?.type === "claudecode-supported-models") return fetchClaudeCodeCatalog(providerName, providerConfig, options);
24968
+ if (providerConfig.catalog?.type === "anthropic-models") return fetchAnthropicCatalog(providerName, providerConfig, options);
24969
+ if (providerConfig.catalog?.type === "ollama-tags") return fetchOllamaTagsCatalog(providerName, providerConfig, options);
24970
+ return fetchOpenAIStyleCatalog(providerName, providerConfig, options);
24971
+ }
24972
+ async function fetchOpenAIStyleCatalog(providerName, providerConfig, options) {
24973
+ if (providerConfig.type !== "openai-compatible") throw new Error(`Provider ${providerName} is not OpenAI-compatible.`);
24974
+ const apiKey = resolveConfiguredApiKey(providerConfig);
24975
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
24976
+ const endpoint = providerConfig.catalog?.endpoint ?? `${providerConfig.baseURL.replace(/\/+$/, "")}/models`;
24977
+ const response = await fetchJson(endpoint, {
24978
+ fetcher: options.fetch,
24979
+ timeoutMs: options.timeoutMs,
24980
+ headers: {
24981
+ ...authHeadersForOpenAICompatible(providerConfig, apiKey.value),
24982
+ ...providerConfig.headers ?? {}
24983
+ }
24984
+ });
24985
+ return {
24986
+ status: "ready",
24987
+ providerName,
24988
+ models: normalizeOpenAIModels(response).map((entry) => ({ ...entry, source: "api" })),
24989
+ source: "api"
24990
+ };
24991
+ }
24992
+ async function fetchAnthropicCatalog(providerName, providerConfig, options) {
24993
+ if (providerConfig.type !== "anthropic") throw new Error(`Provider ${providerName} is not Anthropic.`);
24994
+ const apiKey = resolveConfiguredApiKey(providerConfig);
24995
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
24996
+ const base = providerConfig.baseURL ?? "https://api.anthropic.com/v1";
24997
+ const endpoint = providerConfig.catalog?.endpoint ?? `${base.replace(/\/+$/, "")}/models`;
24998
+ const models = [];
24999
+ let afterId;
25000
+ for (; ; ) {
25001
+ const url = new URL(endpoint);
25002
+ if (afterId) url.searchParams.set("after_id", afterId);
25003
+ url.searchParams.set("limit", "100");
25004
+ const response = await fetchJson(url.toString(), {
25005
+ fetcher: options.fetch,
25006
+ timeoutMs: options.timeoutMs,
25007
+ headers: {
25008
+ "x-api-key": apiKey.value,
25009
+ "anthropic-version": "2023-06-01"
25010
+ }
25011
+ });
25012
+ const page = normalizeAnthropicModels(response);
25013
+ models.push(...page);
25014
+ if (!response.has_more || !response.last_id) break;
25015
+ afterId = response.last_id;
25016
+ }
25017
+ return { status: "ready", providerName, models, source: "api" };
25018
+ }
25019
+ async function fetchOllamaTagsCatalog(providerName, providerConfig, options) {
25020
+ if (providerConfig.type !== "ollama") throw new Error(`Provider ${providerName} is not Ollama native.`);
25021
+ const apiKey = resolveConfiguredApiKey(providerConfig);
25022
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
25023
+ const endpoint = providerConfig.catalog?.endpoint ?? `${providerConfig.baseURL.replace(/\/+$/, "")}/tags`;
25024
+ const response = await fetchJson(endpoint, {
25025
+ fetcher: options.fetch,
25026
+ timeoutMs: options.timeoutMs,
25027
+ headers: { authorization: `Bearer ${apiKey.value}` }
25028
+ });
25029
+ return {
25030
+ status: "ready",
25031
+ providerName,
25032
+ models: normalizeOllamaTags(response).map((entry) => ({ ...entry, source: "api" })),
25033
+ source: "api"
25034
+ };
25035
+ }
25036
+ async function fetchCodexCatalog(providerName, providerConfig, options) {
25037
+ if (providerConfig.type !== "codex") throw new Error(`Provider ${providerName} is not Codex.`);
25038
+ const fetcher = options.fetch ?? fetch;
25039
+ const baseURL = (providerConfig.baseURL ?? "https://chatgpt.com/backend-api/codex").replace(/\/+$/, "");
25040
+ const installationId = await loadOrCreateInstallationId(resolveCodexHome(providerConfig.codexHome));
25041
+ const auth = getSharedCodexAuthStore({
25042
+ codexHome: providerConfig.codexHome,
25043
+ authStore: providerConfig.authStore,
25044
+ allowApiKeyFallback: providerConfig.allowApiKeyFallback,
25045
+ proactiveRefreshMinutes: providerConfig.refresh?.proactiveRefreshMinutes,
25046
+ refreshCache: providerConfig.refresh?.cache,
25047
+ fetch: fetcher
25048
+ });
25049
+ const headers = await auth.requestHeaders(installationId);
25050
+ const url = new URL(`${baseURL}/models`);
25051
+ url.searchParams.set("client_version", await codexClientVersion(options.clientVersion));
25052
+ const response = await fetchJson(url.toString(), {
25053
+ fetcher,
25054
+ timeoutMs: options.timeoutMs,
25055
+ headers
25056
+ });
25057
+ return {
25058
+ status: "ready",
25059
+ providerName,
25060
+ models: normalizeCodexModels(response).map((entry) => ({ ...entry, source: "oauth" })),
25061
+ source: "oauth"
25062
+ };
25063
+ }
25064
+ async function fetchClaudeCodeCatalog(providerName, providerConfig, options) {
25065
+ if (providerConfig.type !== "claudecode") throw new Error(`Provider ${providerName} is not Claude Code.`);
25066
+ const fallback = staticModelEntries(providerName, providerConfig);
25067
+ try {
25068
+ const module = await dynamicImport2("@anthropic-ai/claude-agent-sdk");
25069
+ if (!module.query) throw new Error("Claude Agent SDK query export is unavailable.");
25070
+ const query = module.query({
25071
+ prompt: "",
25072
+ options: {
25073
+ model: defaultModelForProvider(providerName, providerConfig),
25074
+ maxTurns: 0,
25075
+ pathToClaudeCodeExecutable: resolveClaudeCodeCliPath(providerConfig.cliPath),
25076
+ env: sanitizedClaudeCodeEnv(providerConfig.env, providerConfig.sanitizeApiKeyEnv ?? true)
25077
+ }
25078
+ });
25079
+ try {
25080
+ if (typeof query.supportedModels !== "function") throw new Error("Claude Agent SDK supportedModels is unavailable.");
25081
+ const models = await withTimeout(query.supportedModels(), options.timeoutMs ?? 5e3);
25082
+ return {
25083
+ status: "ready",
25084
+ providerName,
25085
+ models: models.map((model, index) => ({
25086
+ name: model.displayName || model.value,
25087
+ displayName: model.displayName || model.value,
25088
+ model: model.value,
25089
+ description: model.description,
25090
+ isDefault: index === 0,
25091
+ source: "oauth"
25092
+ })),
25093
+ source: "oauth"
25094
+ };
25095
+ } finally {
25096
+ query.close?.();
25097
+ }
25098
+ } catch (error) {
25099
+ if (fallback.length) return { status: "ready", providerName, models: fallback, source: "fallback", message: errorMessage2(error) };
25100
+ throw error;
25101
+ }
25102
+ }
25103
+ function staticModelEntries(providerName, providerConfig) {
25104
+ return providerModelProfiles(providerName, providerConfig).filter((profile) => !profile.hidden).map((profile, index) => ({
25105
+ name: profile.displayName ?? profile.name,
25106
+ displayName: profile.displayName ?? profile.name,
25107
+ model: profile.model,
25108
+ description: profile.description,
25109
+ isDefault: index === 0,
25110
+ source: "config"
25111
+ }));
25112
+ }
25113
+ function mergeCatalogWithStatic(staticModels, remoteModels) {
25114
+ const byModel = /* @__PURE__ */ new Map();
25115
+ for (const model of remoteModels) byModel.set(model.model, model);
25116
+ for (const model of staticModels) byModel.set(model.model, { ...byModel.get(model.model), ...model, source: model.source });
25117
+ return [...byModel.values()];
25118
+ }
25119
+ function normalizeOpenAIModels(raw) {
25120
+ const items = Array.isArray(raw.data) ? raw.data : Array.isArray(raw.models) ? raw.models : [];
25121
+ return items.map((item) => {
25122
+ const object2 = item;
25123
+ const id = stringValue4(object2.id) ?? stringValue4(object2.name) ?? stringValue4(object2.model);
25124
+ if (!id) return void 0;
25125
+ return {
25126
+ name: id,
25127
+ displayName: stringValue4(object2.display_name) ?? stringValue4(object2.displayName) ?? id,
25128
+ model: id,
25129
+ description: stringValue4(object2.description)
25130
+ };
25131
+ }).filter(Boolean);
25132
+ }
25133
+ function normalizeAnthropicModels(raw) {
25134
+ return (raw.data ?? []).map((item) => {
25135
+ const object2 = item;
25136
+ const id = stringValue4(object2.id);
25137
+ if (!id) return void 0;
25138
+ return {
25139
+ name: stringValue4(object2.display_name) ?? id,
25140
+ displayName: stringValue4(object2.display_name) ?? id,
25141
+ model: id,
25142
+ description: stringValue4(object2.description),
25143
+ source: "api"
25144
+ };
25145
+ }).filter(Boolean);
25146
+ }
25147
+ function normalizeOllamaTags(raw) {
25148
+ const items = Array.isArray(raw.models) ? raw.models : [];
25149
+ return items.map((item) => {
25150
+ const object2 = item;
25151
+ const id = stringValue4(object2.name) ?? stringValue4(object2.model);
25152
+ if (!id) return void 0;
25153
+ return { name: id, displayName: id, model: id };
25154
+ }).filter(Boolean);
25155
+ }
25156
+ function normalizeCodexModels(raw) {
25157
+ const items = Array.isArray(raw.models) ? raw.models : [];
25158
+ return items.map((item, index) => {
25159
+ const object2 = item;
25160
+ const id = stringValue4(object2.slug) ?? stringValue4(object2.id) ?? stringValue4(object2.model);
25161
+ if (!id) return void 0;
25162
+ return {
25163
+ name: stringValue4(object2.display_name) ?? id,
25164
+ displayName: stringValue4(object2.display_name) ?? id,
25165
+ model: id,
25166
+ description: stringValue4(object2.description),
25167
+ isDefault: index === 0
25168
+ };
25169
+ }).filter(Boolean);
25170
+ }
25171
+ function authHeadersForOpenAICompatible(providerConfig, apiKey) {
25172
+ const auth = inferOpenAICompatibleAuth(providerConfig.baseURL, providerConfig.auth);
25173
+ const type = auth.type ?? "bearer";
25174
+ const header = auth.header ?? (type === "api-key" ? "api-key" : "authorization");
25175
+ return { [header]: type === "bearer" ? `Bearer ${apiKey}` : apiKey };
25176
+ }
25177
+ async function fetchJson(url, options) {
25178
+ const response = await (options.fetcher ?? fetch)(url, {
25179
+ method: "GET",
25180
+ headers: {
25181
+ accept: "application/json",
25182
+ ...options.headers ?? {}
25183
+ },
25184
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
25185
+ });
25186
+ const text = await response.text();
25187
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
25188
+ return text ? JSON.parse(text) : {};
25189
+ }
25190
+ function cachePath(cacheKey) {
25191
+ return path16.join(os9.homedir(), ".demian", "cache", "models", `${safeCacheName(cacheKey)}.json`);
25192
+ }
25193
+ async function readCatalogCache(cacheKey, ttlMs, allow) {
25194
+ if (!allow) return void 0;
25195
+ try {
25196
+ const raw = JSON.parse(await readFile7(cachePath(cacheKey), "utf8"));
25197
+ if (!raw.result) return void 0;
25198
+ if (ttlMs !== Number.POSITIVE_INFINITY && Date.now() - (raw.savedAt ?? 0) > ttlMs) return void 0;
25199
+ return { ...raw.result, source: "cache", models: raw.result.models.map((model) => ({ ...model, source: "cache" })) };
25200
+ } catch {
25201
+ return void 0;
25202
+ }
25203
+ }
25204
+ async function writeCatalogCache(cacheKey, result) {
25205
+ const filePath = cachePath(cacheKey);
25206
+ await mkdir6(path16.dirname(filePath), { recursive: true, mode: 448 });
25207
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
25208
+ await writeFile5(temp, JSON.stringify({ savedAt: Date.now(), result }, null, 2), { mode: 384 });
25209
+ await rename4(temp, filePath);
25210
+ }
25211
+ function safeCacheName(providerName) {
25212
+ return providerName.replace(/[^a-zA-Z0-9_.-]+/g, "_");
25213
+ }
25214
+ function catalogCacheKey(providerName, providerConfig) {
25215
+ const identity = {
25216
+ providerName,
25217
+ type: providerConfig.type,
25218
+ catalogType: providerConfig.catalog?.type,
25219
+ endpoint: providerConfig.catalog?.endpoint,
25220
+ baseURL: "baseURL" in providerConfig ? providerConfig.baseURL : void 0,
25221
+ apiKeyEnv: "apiKeyEnv" in providerConfig ? providerConfig.apiKeyEnv : void 0,
25222
+ apiKeyEnvAliases: "apiKeyEnvAliases" in providerConfig ? providerConfig.apiKeyEnvAliases : void 0,
25223
+ auth: "auth" in providerConfig ? providerConfig.auth : void 0
25224
+ };
25225
+ const digest = crypto5.createHash("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 16);
25226
+ return `${providerName}-${digest}`;
25227
+ }
25228
+ function missingCatalogAuth(providerConfig) {
25229
+ if (providerConfig.type !== "openai-compatible" && providerConfig.type !== "anthropic" && providerConfig.type !== "ollama") return void 0;
25230
+ if (providerConfig.catalog?.requiresAuth === false) return void 0;
25231
+ const expectsAuth = providerConfig.catalog?.requiresAuth === true || !!providerConfig.apiKey || !!providerConfig.apiKeyEnv || !!providerConfig.apiKeyEnvAliases?.length;
25232
+ if (!expectsAuth) return void 0;
25233
+ const apiKey = resolveConfiguredApiKey(providerConfig);
25234
+ return apiKey.value ? void 0 : apiKey.envName ?? "apiKey";
25235
+ }
25236
+ function stringValue4(value) {
25237
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
25238
+ }
25239
+ function missingAuth(envName) {
25240
+ const error = new Error(`missing-auth:${envName}`);
25241
+ return error;
25242
+ }
25243
+ function isMissingAuthError(error) {
25244
+ if (error instanceof CodexAuthError) {
25245
+ 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";
25246
+ }
25247
+ return error instanceof Error && error.message.startsWith("missing-auth:");
25248
+ }
25249
+ function errorMessage2(error) {
25250
+ return error instanceof Error ? error.message : String(error);
25251
+ }
25252
+ async function codexClientVersion(override) {
25253
+ const normalized = semverLike(override);
25254
+ if (normalized) return normalized;
25255
+ packageVersionPromise ??= readPackageVersion();
25256
+ return packageVersionPromise;
25257
+ }
25258
+ async function readPackageVersion() {
25259
+ try {
25260
+ const raw = JSON.parse(await readFile7(new URL("../package.json", import.meta.url), "utf8"));
25261
+ return semverLike(raw.version) ?? DEFAULT_CODEX_CLIENT_VERSION;
25262
+ } catch {
25263
+ return DEFAULT_CODEX_CLIENT_VERSION;
25264
+ }
25265
+ }
25266
+ function semverLike(value) {
25267
+ if (typeof value !== "string") return void 0;
25268
+ const trimmed = value.trim();
25269
+ return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(trimmed) ? trimmed : void 0;
25270
+ }
25271
+ function withTimeout(promise, timeoutMs) {
25272
+ return new Promise((resolve, reject) => {
25273
+ const timer = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
25274
+ promise.then(
25275
+ (value) => {
25276
+ clearTimeout(timer);
25277
+ resolve(value);
25278
+ },
25279
+ (error) => {
25280
+ clearTimeout(timer);
25281
+ reject(error);
25282
+ }
25283
+ );
25284
+ });
25285
+ }
25286
+
24545
25287
  // src/multimodal.ts
24546
- import { readFile as readFile7, stat as stat3 } from "node:fs/promises";
24547
- import path15 from "node:path";
25288
+ import { readFile as readFile8, stat as stat3 } from "node:fs/promises";
25289
+ import path17 from "node:path";
24548
25290
  var DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
24549
25291
  async function buildUserContent(prompt, images = [], options) {
24550
25292
  if (images.length === 0) return prompt;
@@ -24568,11 +25310,11 @@ async function imageToUrl(input2, options) {
24568
25310
  const maxImageBytes = options.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
24569
25311
  if (info.size > maxImageBytes) throw new Error(`Image is larger than ${maxImageBytes} bytes: ${input2}`);
24570
25312
  const mime = mimeFromPath(filePath);
24571
- const bytes = await readFile7(filePath);
25313
+ const bytes = await readFile8(filePath);
24572
25314
  return `data:${mime};base64,${bytes.toString("base64")}`;
24573
25315
  }
24574
25316
  function mimeFromPath(filePath) {
24575
- switch (path15.extname(filePath).toLowerCase()) {
25317
+ switch (path17.extname(filePath).toLowerCase()) {
24576
25318
  case ".jpg":
24577
25319
  case ".jpeg":
24578
25320
  return "image/jpeg";
@@ -24583,15 +25325,15 @@ function mimeFromPath(filePath) {
24583
25325
  case ".webp":
24584
25326
  return "image/webp";
24585
25327
  default:
24586
- throw new Error(`Unsupported image extension: ${path15.extname(filePath) || "(none)"}`);
25328
+ throw new Error(`Unsupported image extension: ${path17.extname(filePath) || "(none)"}`);
24587
25329
  }
24588
25330
  }
24589
25331
 
24590
25332
  // src/permissions/persistent-grants.ts
24591
- import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
25333
+ import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
24592
25334
  import fs8 from "node:fs";
24593
- import os9 from "node:os";
24594
- import path16 from "node:path";
25335
+ import os10 from "node:os";
25336
+ import path18 from "node:path";
24595
25337
  var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
24596
25338
  var PersistentGrantStore = class {
24597
25339
  filePath;
@@ -24622,22 +25364,22 @@ var PersistentGrantStore = class {
24622
25364
  }
24623
25365
  async #read() {
24624
25366
  if (!fs8.existsSync(this.filePath)) return { version: 1, grants: [] };
24625
- const data = JSON.parse(await readFile8(this.filePath, "utf8"));
25367
+ const data = JSON.parse(await readFile9(this.filePath, "utf8"));
24626
25368
  return {
24627
25369
  version: 1,
24628
25370
  grants: Array.isArray(data.grants) ? data.grants.filter(isGrantRecord) : []
24629
25371
  };
24630
25372
  }
24631
25373
  async #write(file) {
24632
- await mkdir6(path16.dirname(this.filePath), { recursive: true });
24633
- await writeFile5(this.filePath, `${JSON.stringify(file, null, 2)}
25374
+ await mkdir7(path18.dirname(this.filePath), { recursive: true });
25375
+ await writeFile6(this.filePath, `${JSON.stringify(file, null, 2)}
24634
25376
  `, { mode: 384 });
24635
25377
  }
24636
25378
  };
24637
25379
  function resolveGrantPath(cwd, config) {
24638
- if (config.path) return path16.resolve(cwd, config.path);
24639
- if ((config.scope ?? "project") === "user") return path16.join(os9.homedir(), ".demian", "grants.json");
24640
- return path16.join(cwd, ".demian", "grants.json");
25380
+ if (config.path) return path18.resolve(cwd, config.path);
25381
+ if ((config.scope ?? "project") === "user") return path18.join(os10.homedir(), ".demian", "grants.json");
25382
+ return path18.join(cwd, ".demian", "grants.json");
24641
25383
  }
24642
25384
  function isGrantRecord(value) {
24643
25385
  if (!value || typeof value !== "object") return false;
@@ -24696,12 +25438,13 @@ function applyPermissionPresetToConfig(config, preset) {
24696
25438
 
24697
25439
  // src/root-session.ts
24698
25440
  import { cp, mkdtemp, rm as rm2 } from "node:fs/promises";
24699
- import os10 from "node:os";
24700
- import path26 from "node:path";
25441
+ import os12 from "node:os";
25442
+ import path28 from "node:path";
24701
25443
 
24702
25444
  // src/transcript.ts
24703
- import { mkdir as mkdir7, appendFile, writeFile as writeFile6 } from "node:fs/promises";
24704
- import path17 from "node:path";
25445
+ import { mkdir as mkdir8, appendFile, writeFile as writeFile7 } from "node:fs/promises";
25446
+ import os11 from "node:os";
25447
+ import path19 from "node:path";
24705
25448
  var TranscriptWriter = class {
24706
25449
  filePath;
24707
25450
  #enabled;
@@ -24709,9 +25452,9 @@ var TranscriptWriter = class {
24709
25452
  #queue = Promise.resolve();
24710
25453
  constructor(options) {
24711
25454
  this.#enabled = options.enabled !== false;
24712
- const dir = path17.join(options.cwd, ".demian", "transcripts", options.sessionId);
24713
- this.filePath = path17.join(dir, "session.jsonl");
24714
- this.#ready = this.#enabled ? mkdir7(dir, { recursive: true }).then(() => writeFile6(this.filePath, "", { flag: "a" })) : Promise.resolve();
25455
+ const dir = path19.join(options.storageDir ?? defaultDemianStorageDir(), "transcripts", options.sessionId);
25456
+ this.filePath = path19.join(dir, "session.jsonl");
25457
+ this.#ready = this.#enabled ? mkdir8(dir, { recursive: true }).then(() => writeFile7(this.filePath, "", { flag: "a" })) : Promise.resolve();
24715
25458
  }
24716
25459
  write(event) {
24717
25460
  if (!this.#enabled) return Promise.resolve();
@@ -24726,12 +25469,15 @@ var TranscriptWriter = class {
24726
25469
  await this.#queue;
24727
25470
  }
24728
25471
  };
25472
+ function defaultDemianStorageDir() {
25473
+ return path19.join(os11.homedir(), ".demian");
25474
+ }
24729
25475
 
24730
25476
  // src/external-runtime/snapshot-diff.ts
24731
25477
  import { createHash as createHash2 } from "node:crypto";
24732
25478
  import { createReadStream } from "node:fs";
24733
- import { readdir, readFile as readFile9, stat as stat4 } from "node:fs/promises";
24734
- import path18 from "node:path";
25479
+ import { readdir, readFile as readFile10, stat as stat4 } from "node:fs/promises";
25480
+ import path20 from "node:path";
24735
25481
 
24736
25482
  // src/workspace/diff.ts
24737
25483
  var CONTEXT_LINES = 3;
@@ -24941,7 +25687,7 @@ async function diffWorkspaceSnapshot(before) {
24941
25687
  }
24942
25688
  async function walk(root, relativeDir, entries, options) {
24943
25689
  if (entries.size >= options.maxFiles) return;
24944
- const absoluteDir = path18.join(root, relativeDir);
25690
+ const absoluteDir = path20.join(root, relativeDir);
24945
25691
  let items;
24946
25692
  try {
24947
25693
  items = await readdir(absoluteDir, { withFileTypes: true });
@@ -24952,8 +25698,8 @@ async function walk(root, relativeDir, entries, options) {
24952
25698
  if (entries.size >= options.maxFiles) return;
24953
25699
  if (item.name.startsWith(".") && item.name !== ".env.example" && SKIP_DIRS.has(item.name)) continue;
24954
25700
  if (SKIP_DIRS.has(item.name)) continue;
24955
- const relativePath = normalizeRelativePath(path18.join(relativeDir, item.name));
24956
- const absolutePath = path18.join(root, relativePath);
25701
+ const relativePath = normalizeRelativePath(path20.join(relativeDir, item.name));
25702
+ const absolutePath = path20.join(root, relativePath);
24957
25703
  if (item.isDirectory()) {
24958
25704
  await walk(root, relativePath, entries, options);
24959
25705
  continue;
@@ -24975,7 +25721,7 @@ function snapshotDiffText(entry) {
24975
25721
  `;
24976
25722
  }
24977
25723
  async function readTextContent(filePath) {
24978
- const buffer = await readFile9(filePath);
25724
+ const buffer = await readFile10(filePath);
24979
25725
  if (buffer.includes(0)) return void 0;
24980
25726
  return buffer.toString("utf8");
24981
25727
  }
@@ -24989,7 +25735,7 @@ function sha256File(filePath) {
24989
25735
  });
24990
25736
  }
24991
25737
  function normalizeRelativePath(value) {
24992
- return value.split(path18.sep).join("/");
25738
+ return value.split(path20.sep).join("/");
24993
25739
  }
24994
25740
 
24995
25741
  // src/external-runtime/session-runner.ts
@@ -25682,25 +26428,25 @@ function fail(content, metadata) {
25682
26428
  }
25683
26429
 
25684
26430
  // src/tools/output.ts
25685
- import { mkdir as mkdir8, writeFile as writeFile7 } from "node:fs/promises";
25686
- import path19 from "node:path";
26431
+ import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
26432
+ import path21 from "node:path";
25687
26433
  var DEFAULT_CAP_BYTES = 32 * 1024;
25688
26434
  var HALF_PREVIEW_BYTES = 16 * 1024;
25689
26435
  async function capToolOutput(result, options) {
25690
26436
  const capBytes = options.capBytes ?? DEFAULT_CAP_BYTES;
25691
26437
  const bytes = Buffer.byteLength(result.content, "utf8");
25692
26438
  if (bytes <= capBytes) return result;
25693
- const dir = path19.join(options.cwd, ".demian", "tmp");
25694
- await mkdir8(dir, { recursive: true });
25695
- const outputPath = path19.join(dir, `output-${safeCallId(options.callId)}.txt`);
25696
- await writeFile7(outputPath, result.content, "utf8");
26439
+ const dir = path21.join(options.cwd, ".demian", "tmp");
26440
+ await mkdir9(dir, { recursive: true });
26441
+ const outputPath = path21.join(dir, `output-${safeCallId(options.callId)}.txt`);
26442
+ await writeFile8(outputPath, result.content, "utf8");
25697
26443
  return {
25698
26444
  ...result,
25699
26445
  content: [
25700
26446
  sliceUtf8(result.content, 0, HALF_PREVIEW_BYTES),
25701
26447
  `
25702
26448
 
25703
- [Full output saved to ${path19.relative(options.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
26449
+ [Full output saved to ${path21.relative(options.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
25704
26450
 
25705
26451
  `,
25706
26452
  sliceUtf8(result.content, Math.max(0, bytes - HALF_PREVIEW_BYTES), bytes)
@@ -25726,7 +26472,7 @@ function sliceUtf8(text, startByte, endByte) {
25726
26472
 
25727
26473
  // src/tools/read-file.ts
25728
26474
  import { open as open2, stat as stat5 } from "node:fs/promises";
25729
- import path20 from "node:path";
26475
+ import path22 from "node:path";
25730
26476
 
25731
26477
  // src/tools/validation.ts
25732
26478
  function assertObject(input2, toolName) {
@@ -25810,7 +26556,7 @@ var readFileTool = {
25810
26556
  const truncated = start + sliced.length < lines.length;
25811
26557
  return ok(content + (truncated ? `
25812
26558
  ... (${lines.length - start - sliced.length} more lines)` : ""), {
25813
- path: path20.relative(ctx.cwd, filePath),
26559
+ path: path22.relative(ctx.cwd, filePath),
25814
26560
  lines: sliced.length,
25815
26561
  totalLines: lines.length,
25816
26562
  truncated
@@ -25848,8 +26594,8 @@ function looksBinary(buffer) {
25848
26594
  }
25849
26595
 
25850
26596
  // src/tools/write-file.ts
25851
- import { mkdir as mkdir9, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
25852
- import path21 from "node:path";
26597
+ import { mkdir as mkdir10, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
26598
+ import path23 from "node:path";
25853
26599
  var writeFileTool = {
25854
26600
  name: "write_file",
25855
26601
  description: "Create or replace a text file inside the workspace.",
@@ -25867,8 +26613,8 @@ var writeFileTool = {
25867
26613
  const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
25868
26614
  const content = stringField(object2, "content", { allowEmpty: true });
25869
26615
  const before = await readOptionalTextFile(filePath);
25870
- await mkdir9(path21.dirname(filePath), { recursive: true });
25871
- await writeFile8(filePath, content, "utf8");
26616
+ await mkdir10(path23.dirname(filePath), { recursive: true });
26617
+ await writeFile9(filePath, content, "utf8");
25872
26618
  const relative = relativeToCwd(ctx.cwd, filePath);
25873
26619
  const diff = createTextDiff(before, content, relative);
25874
26620
  return ok(`Wrote ${relativeToCwd(ctx.cwd, filePath)} (${Buffer.byteLength(content, "utf8")} bytes).`, {
@@ -25881,7 +26627,7 @@ var writeFileTool = {
25881
26627
  };
25882
26628
  async function readOptionalTextFile(filePath) {
25883
26629
  try {
25884
- return await readFile10(filePath, "utf8");
26630
+ return await readFile11(filePath, "utf8");
25885
26631
  } catch (error) {
25886
26632
  if (isNotFound(error)) return void 0;
25887
26633
  throw error;
@@ -25892,7 +26638,7 @@ function isNotFound(error) {
25892
26638
  }
25893
26639
 
25894
26640
  // src/tools/edit-file.ts
25895
- import { readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
26641
+ import { readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
25896
26642
  var editFileTool = {
25897
26643
  name: "edit_file",
25898
26644
  description: "Replace an exact string in a text file inside the workspace.",
@@ -25914,12 +26660,12 @@ var editFileTool = {
25914
26660
  const newString = stringField(object2, "newString", { allowEmpty: true });
25915
26661
  const replaceAll = optionalBooleanField(object2, "replaceAll") ?? false;
25916
26662
  if (oldString === newString) throw new Error("oldString and newString must be different");
25917
- const before = await readFile11(filePath, "utf8");
26663
+ const before = await readFile12(filePath, "utf8");
25918
26664
  const matches = countMatches(before, oldString);
25919
26665
  if (matches === 0) throw new Error("oldString was not found");
25920
26666
  if (matches > 1 && !replaceAll) throw new Error(`oldString matched ${matches} times; set replaceAll=true to replace all matches`);
25921
26667
  const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString);
25922
- await writeFile9(filePath, after, "utf8");
26668
+ await writeFile10(filePath, after, "utf8");
25923
26669
  const relative = relativeToCwd(ctx.cwd, filePath);
25924
26670
  const diff = createTextDiff(before, after, relative);
25925
26671
  return ok(`Edited ${relative} (${replaceAll ? matches : 1} replacement${matches === 1 ? "" : "s"}).`, {
@@ -25943,7 +26689,7 @@ function countMatches(text, needle) {
25943
26689
 
25944
26690
  // src/tools/bash.ts
25945
26691
  import { spawn as spawn5 } from "node:child_process";
25946
- import path23 from "node:path";
26692
+ import path25 from "node:path";
25947
26693
 
25948
26694
  // src/sandbox/env-only.ts
25949
26695
  function buildEnvOnlyLaunch(command, config) {
@@ -26003,7 +26749,7 @@ function bwrapPath() {
26003
26749
 
26004
26750
  // src/sandbox/macos.ts
26005
26751
  import { existsSync as existsSync3 } from "node:fs";
26006
- import path22 from "node:path";
26752
+ import path24 from "node:path";
26007
26753
  function canUseMacOSSandbox() {
26008
26754
  return process.platform === "darwin" && existsSync3("/usr/bin/sandbox-exec");
26009
26755
  }
@@ -26029,7 +26775,7 @@ function macosProfile(cwd, config) {
26029
26775
  }
26030
26776
  if (mode === "workspace-write") {
26031
26777
  lines.push("(deny file-write*)");
26032
- lines.push(`(allow file-write* (subpath "${escapeProfilePath(path22.resolve(cwd))}"))`);
26778
+ lines.push(`(allow file-write* (subpath "${escapeProfilePath(path24.resolve(cwd))}"))`);
26033
26779
  lines.push('(allow file-write* (subpath "/tmp"))');
26034
26780
  lines.push('(allow file-write* (subpath "/private/tmp"))');
26035
26781
  lines.push('(allow file-write* (subpath "/private/var/folders"))');
@@ -26091,7 +26837,7 @@ ${result.stderr}` : ""
26091
26837
  ].filter(Boolean).join("\n");
26092
26838
  return ok(content, {
26093
26839
  command,
26094
- workdir: path23.relative(ctx.cwd, workdir) || ".",
26840
+ workdir: path25.relative(ctx.cwd, workdir) || ".",
26095
26841
  exitCode: result.exitCode,
26096
26842
  signal: result.signal,
26097
26843
  timedOut: result.timedOut,
@@ -26143,8 +26889,8 @@ function runCommand(command, cwd, timeoutMs, signal, sandbox = { mode: "workspac
26143
26889
 
26144
26890
  // src/tools/grep.ts
26145
26891
  import { spawn as spawn6 } from "node:child_process";
26146
- import { readdir as readdir2, readFile as readFile12, stat as stat6 } from "node:fs/promises";
26147
- import path24 from "node:path";
26892
+ import { readdir as readdir2, readFile as readFile13, stat as stat6 } from "node:fs/promises";
26893
+ import path26 from "node:path";
26148
26894
  var MAX_MATCHES = 200;
26149
26895
  var grepTool = {
26150
26896
  name: "grep",
@@ -26219,7 +26965,7 @@ function runRg(cwd, base, pattern, glob, signal) {
26219
26965
  }
26220
26966
  function rewriteRgPath(cwd, line) {
26221
26967
  const [file, rest] = splitFirst(line, ":");
26222
- if (!path24.isAbsolute(file)) return line;
26968
+ if (!path26.isAbsolute(file)) return line;
26223
26969
  return `${relativeToCwd(cwd, file)}:${rest}`;
26224
26970
  }
26225
26971
  function splitFirst(value, delimiter) {
@@ -26236,11 +26982,11 @@ async function fallbackSearch(cwd, base, pattern, glob) {
26236
26982
  if (isIgnoredPath(relative)) return;
26237
26983
  const info = await stat6(item);
26238
26984
  if (info.isDirectory()) {
26239
- for (const entry of await readdir2(item)) await walk2(path24.join(item, entry));
26985
+ for (const entry of await readdir2(item)) await walk2(path26.join(item, entry));
26240
26986
  return;
26241
26987
  }
26242
26988
  if (glob && !matchGlob(glob, relative)) return;
26243
- const buffer = await readFile12(item);
26989
+ const buffer = await readFile13(item);
26244
26990
  if (buffer.includes(0)) return;
26245
26991
  const lines = buffer.toString("utf8").split(/\r?\n/);
26246
26992
  for (let i = 0; i < lines.length; i++) {
@@ -26255,7 +27001,7 @@ async function fallbackSearch(cwd, base, pattern, glob) {
26255
27001
 
26256
27002
  // src/tools/glob.ts
26257
27003
  import { readdir as readdir3, stat as stat7 } from "node:fs/promises";
26258
- import path25 from "node:path";
27004
+ import path27 from "node:path";
26259
27005
  var MAX_PATHS = 1e3;
26260
27006
  var globTool = {
26261
27007
  name: "glob",
@@ -26287,7 +27033,7 @@ async function collectPaths(cwd, root, pattern) {
26287
27033
  async function walk2(dir) {
26288
27034
  const entries = await readdir3(dir, { withFileTypes: true });
26289
27035
  for (const entry of entries) {
26290
- const absolute = path25.join(dir, entry.name);
27036
+ const absolute = path27.join(dir, entry.name);
26291
27037
  const relative = relativeToCwd(cwd, absolute);
26292
27038
  if (isIgnoredPath(relative)) continue;
26293
27039
  const info = await stat7(absolute);
@@ -26428,7 +27174,7 @@ async function runBraveSearch(config, options) {
26428
27174
  if (config.country) url.searchParams.set("country", config.country);
26429
27175
  if (config.searchLang) url.searchParams.set("search_lang", config.searchLang);
26430
27176
  if (config.safeSearch) url.searchParams.set("safesearch", config.safeSearch);
26431
- const json = await fetchJson(url, {
27177
+ const json = await fetchJson2(url, {
26432
27178
  method: "GET",
26433
27179
  headers: {
26434
27180
  accept: "application/json",
@@ -26447,7 +27193,7 @@ async function runTavilySearch(config, options) {
26447
27193
  if (!apiKey.ok) return apiKey;
26448
27194
  const maxResults = Math.min(positiveInteger(options.maxResults, config.maxResults ?? 5, "maxResults"), TAVILY_MAX_RESULTS);
26449
27195
  const endpoint = config.endpoint ?? defaultConfig.webSearch.providers.tavily.endpoint ?? "https://api.tavily.com/search";
26450
- const json = await fetchJson(endpoint, {
27196
+ const json = await fetchJson2(endpoint, {
26451
27197
  method: "POST",
26452
27198
  headers: {
26453
27199
  "content-type": "application/json",
@@ -26473,7 +27219,7 @@ async function runExaSearch(config, options) {
26473
27219
  if (!apiKey.ok) return apiKey;
26474
27220
  const numResults = Math.min(positiveInteger(options.maxResults, config.numResults ?? 5, "maxResults"), EXA_MAX_RESULTS);
26475
27221
  const endpoint = config.endpoint ?? defaultConfig.webSearch.providers.exa.endpoint ?? "https://api.exa.ai/search";
26476
- const json = await fetchJson(endpoint, {
27222
+ const json = await fetchJson2(endpoint, {
26477
27223
  method: "POST",
26478
27224
  headers: {
26479
27225
  "content-type": "application/json",
@@ -26498,11 +27244,11 @@ async function runExaSearch(config, options) {
26498
27244
  return searchResult("exa", options.query, results, json.value);
26499
27245
  }
26500
27246
  function resolveApiKey(provider, config) {
26501
- const value = config.apiKey ?? (config.apiKeyEnv ? process.env[config.apiKeyEnv] : void 0);
27247
+ const value = config.apiKey || (config.apiKeyEnv ? process.env[config.apiKeyEnv] : void 0);
26502
27248
  if (value) return { ok: true, value };
26503
27249
  return toolFail(`Missing API key for web_search provider "${provider}". Set webSearch.providers.${provider}.apiKey or set ${config.apiKeyEnv ?? "the configured apiKeyEnv"}.`);
26504
27250
  }
26505
- async function fetchJson(url, options) {
27251
+ async function fetchJson2(url, options) {
26506
27252
  const { fetcher, provider, ...init } = options;
26507
27253
  const response = await fetcher(url, init);
26508
27254
  const text = await response.text();
@@ -26520,10 +27266,10 @@ function normalizeBraveResults(raw, limit) {
26520
27266
  return (data.web?.results ?? []).slice(0, limit).map((item) => {
26521
27267
  const result = item;
26522
27268
  return {
26523
- title: stringValue4(result.title) ?? "Untitled",
26524
- url: stringValue4(result.url) ?? "",
26525
- snippet: cleanSnippet(stringValue4(result.description) ?? stringValue4(result.snippet)),
26526
- publishedDate: stringValue4(result.age)
27269
+ title: stringValue5(result.title) ?? "Untitled",
27270
+ url: stringValue5(result.url) ?? "",
27271
+ snippet: cleanSnippet(stringValue5(result.description) ?? stringValue5(result.snippet)),
27272
+ publishedDate: stringValue5(result.age)
26527
27273
  };
26528
27274
  }).filter((item) => item.url.length > 0);
26529
27275
  }
@@ -26532,9 +27278,9 @@ function normalizeTavilyResults(raw, limit) {
26532
27278
  return (data.results ?? []).slice(0, limit).map((item) => {
26533
27279
  const result = item;
26534
27280
  return {
26535
- title: stringValue4(result.title) ?? "Untitled",
26536
- url: stringValue4(result.url) ?? "",
26537
- snippet: cleanSnippet(stringValue4(result.content) ?? stringValue4(result.raw_content)),
27281
+ title: stringValue5(result.title) ?? "Untitled",
27282
+ url: stringValue5(result.url) ?? "",
27283
+ snippet: cleanSnippet(stringValue5(result.content) ?? stringValue5(result.raw_content)),
26538
27284
  score: numberValue3(result.score)
26539
27285
  };
26540
27286
  }).filter((item) => item.url.length > 0);
@@ -26544,10 +27290,10 @@ function normalizeExaResults(raw, limit) {
26544
27290
  return (data.results ?? []).slice(0, limit).map((item) => {
26545
27291
  const result = item;
26546
27292
  return {
26547
- title: stringValue4(result.title) ?? "Untitled",
26548
- url: stringValue4(result.url) ?? "",
27293
+ title: stringValue5(result.title) ?? "Untitled",
27294
+ url: stringValue5(result.url) ?? "",
26549
27295
  snippet: exaSnippet(result),
26550
- publishedDate: stringValue4(result.publishedDate),
27296
+ publishedDate: stringValue5(result.publishedDate),
26551
27297
  score: numberValue3(result.score)
26552
27298
  };
26553
27299
  }).filter((item) => item.url.length > 0);
@@ -26558,7 +27304,7 @@ function exaSnippet(result) {
26558
27304
  const joined = highlights.map((item) => typeof item === "string" ? item : "").filter(Boolean).join(" ");
26559
27305
  if (joined) return cleanSnippet(joined);
26560
27306
  }
26561
- return cleanSnippet(stringValue4(result.text) ?? stringValue4(result.summary));
27307
+ return cleanSnippet(stringValue5(result.text) ?? stringValue5(result.summary));
26562
27308
  }
26563
27309
  function searchResult(provider, query, results, raw) {
26564
27310
  const content = [
@@ -26582,7 +27328,7 @@ function formatResult(index, result) {
26582
27328
  if (result.snippet) lines.push(` Snippet: ${result.snippet}`);
26583
27329
  return lines.join("\n");
26584
27330
  }
26585
- function stringValue4(value) {
27331
+ function stringValue5(value) {
26586
27332
  return typeof value === "string" && value.length > 0 ? value : void 0;
26587
27333
  }
26588
27334
  function numberValue3(value) {
@@ -27462,7 +28208,7 @@ function stringArray2(value) {
27462
28208
  }
27463
28209
 
27464
28210
  // src/external-runtime/session-map.ts
27465
- import crypto5 from "node:crypto";
28211
+ import crypto6 from "node:crypto";
27466
28212
  var ClaudeCodeSessionMap = class {
27467
28213
  #sessions = /* @__PURE__ */ new Map();
27468
28214
  get(key) {
@@ -27515,7 +28261,7 @@ function normalizeInstruction(value) {
27515
28261
  return value.replace(/\r\n/g, "\n").trim().split("\n").map((line) => line.replace(/[ \t]+$/g, "")).join("\n");
27516
28262
  }
27517
28263
  function sha256(value) {
27518
- return crypto5.createHash("sha256").update(value).digest("hex").slice(0, 16);
28264
+ return crypto6.createHash("sha256").update(value).digest("hex").slice(0, 16);
27519
28265
  }
27520
28266
 
27521
28267
  // src/root-session.ts
@@ -28154,8 +28900,8 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
28154
28900
  }
28155
28901
  }
28156
28902
  async createCoworkIsolatedWorkspace(groupId, memberId) {
28157
- const root = await mkdtemp(path26.join(os10.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
28158
- const cwd = path26.join(root, "workspace");
28903
+ const root = await mkdtemp(path28.join(os12.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
28904
+ const cwd = path28.join(root, "workspace");
28159
28905
  await cp(this.#options.cwd, cwd, {
28160
28906
  recursive: true,
28161
28907
  filter: (source) => shouldCopyIntoCoworkWorkspace(this.#options.cwd, source)
@@ -28303,7 +29049,7 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
28303
29049
  const denyTools = ["write_file", "edit_file", "bash", "claudecode.Edit", "claudecode.Write", "claudecode.MultiEdit", "claudecode.Bash"];
28304
29050
  return this.withPermissionPreset({
28305
29051
  ...agent,
28306
- provider: agent.provider?.profile === "claudecode-subagent" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
29052
+ provider: agent.provider?.profile === "claudecode" && agent.name === "claudecode-explorer" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
28307
29053
  tools: agent.tools.filter((tool) => readTools.has(tool)),
28308
29054
  permissions: [
28309
29055
  ...agent.permissions.filter((rule) => !denyTools.includes(rule.tool)),
@@ -28350,13 +29096,14 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
28350
29096
  return lines.join("\n");
28351
29097
  }
28352
29098
  resolveInvocationBackend(profileName, modelOverride, agent) {
28353
- const providerConfig = resolveProviderConfig(this.#options.config, profileName);
29099
+ const runtime = resolveProviderRuntimeConfig(this.#options.config, { providerName: profileName, model: modelOverride });
29100
+ const providerConfig = runtime.providerConfig;
28354
29101
  if (this.#options.resolveExecutionBackend) return this.#options.resolveExecutionBackend(profileName, providerConfig, modelOverride);
28355
29102
  if (this.#options.resolveProvider) {
28356
- const resolved = this.#options.resolveProvider(profileName, providerConfig, modelOverride);
29103
+ const resolved = this.#options.resolveProvider(profileName, providerConfig, runtime.model);
28357
29104
  return { kind: "provider", ...resolved };
28358
29105
  }
28359
- return resolveExecutionBackend(providerConfig, { model: modelOverride, config: this.#options.config, agent });
29106
+ return resolveExecutionBackend(providerConfig, { model: runtime.model, config: this.#options.config, agent });
28360
29107
  }
28361
29108
  loadAgentSession(agent, providerProfile, model, sessionScope, cwd = this.#options.cwd) {
28362
29109
  const key = `${this.id}:${sessionScope ?? "delegate"}:${agent.name}:${providerProfile}:${model}:${cwd}`;
@@ -28458,17 +29205,17 @@ function findDependencyCycle(group) {
28458
29205
  const byId = new Map(group.map((member) => [member.memberId, member]));
28459
29206
  const visiting = /* @__PURE__ */ new Set();
28460
29207
  const visited = /* @__PURE__ */ new Set();
28461
- const path27 = [];
29208
+ const path29 = [];
28462
29209
  const visit = (id) => {
28463
- if (visiting.has(id)) return [...path27.slice(path27.indexOf(id)), id];
29210
+ if (visiting.has(id)) return [...path29.slice(path29.indexOf(id)), id];
28464
29211
  if (visited.has(id)) return void 0;
28465
29212
  visiting.add(id);
28466
- path27.push(id);
29213
+ path29.push(id);
28467
29214
  for (const dep of byId.get(id)?.dependsOn ?? []) {
28468
29215
  const cycle = visit(dep);
28469
29216
  if (cycle) return cycle;
28470
29217
  }
28471
- path27.pop();
29218
+ path29.pop();
28472
29219
  visiting.delete(id);
28473
29220
  visited.add(id);
28474
29221
  return void 0;
@@ -28509,7 +29256,7 @@ function scopeStaticPrefix(pattern) {
28509
29256
  return prefix.replace(/\/+$/, "").split("/").filter(Boolean).join("/") || ".";
28510
29257
  }
28511
29258
  function shouldCopyIntoCoworkWorkspace(root, source) {
28512
- const relative = path26.relative(root, source).split(path26.sep).join("/");
29259
+ const relative = path28.relative(root, source).split(path28.sep).join("/");
28513
29260
  if (!relative) return true;
28514
29261
  const parts = relative.split("/");
28515
29262
  return !parts.includes(".git") && !parts.includes("node_modules") && !parts.includes(".demian");
@@ -28703,6 +29450,7 @@ function sumUsage(outcomes) {
28703
29450
  export {
28704
29451
  AgentRegistry,
28705
29452
  AnthropicProvider,
29453
+ BUILTIN_PROVIDER_ORDER,
28706
29454
  CLAUDE_CODE_KEYRING_SERVICE,
28707
29455
  CLAUDE_CODE_MODELS,
28708
29456
  CLAUDE_CODE_OAUTH_BETA_HEADER,
@@ -28730,6 +29478,7 @@ export {
28730
29478
  DEFAULT_CODEX_BASE_URL,
28731
29479
  EventBus,
28732
29480
  HookDispatcher,
29481
+ OllamaProvider,
28733
29482
  OpenAIProvider,
28734
29483
  PERMISSION_PRESETS,
28735
29484
  PermissionEngine,
@@ -28764,6 +29513,10 @@ export {
28764
29513
  decodeJwtPayload,
28765
29514
  defaultConfig,
28766
29515
  defaultDecisionForPermissionPreset,
29516
+ defaultDemianStorageDir,
29517
+ defaultDisplayModelForProvider,
29518
+ defaultModelForProvider,
29519
+ displayModelForProfile,
28767
29520
  estimateMessageTokens,
28768
29521
  estimateMessagesTokens,
28769
29522
  estimateToolSchemaTokens,
@@ -28780,6 +29533,7 @@ export {
28780
29533
  imageToUrl,
28781
29534
  inferOpenAICompatibleAuth,
28782
29535
  injectClaudeCodeSystemPrefix,
29536
+ invalidateCatalogPingCache,
28783
29537
  isAzureOpenAIEndpoint,
28784
29538
  isCallableAgent,
28785
29539
  isCoworkableAgent,
@@ -28788,6 +29542,7 @@ export {
28788
29542
  isInsidePath,
28789
29543
  isPermissionPreset,
28790
29544
  isPrimaryAgent,
29545
+ listProviderModelCatalog,
28791
29546
  loadConfig,
28792
29547
  loadOrCreateInstallationId,
28793
29548
  matchGlob,
@@ -28796,6 +29551,7 @@ export {
28796
29551
  normalizeAgent,
28797
29552
  normalizeAnthropicResponse,
28798
29553
  normalizeCodexResponse,
29554
+ normalizeOllamaResponse,
28799
29555
  normalizeOpenAIResponse,
28800
29556
  normalizePathForMatch,
28801
29557
  normalizePermissionPreset,
@@ -28807,16 +29563,22 @@ export {
28807
29563
  parseOpenAIStream,
28808
29564
  parseRetryAfter,
28809
29565
  permissionDecisionLine,
29566
+ providerModelOptions,
29567
+ providerModelProfiles,
28810
29568
  relativeToCwd,
28811
29569
  resolveAgentMode,
28812
29570
  resolveClaudeCodeRuntimeConfig,
28813
29571
  resolveClaudeConfigDir,
28814
29572
  resolveCodexHome,
29573
+ resolveConfiguredApiKey,
28815
29574
  resolveExecutionBackend,
28816
29575
  resolveGrantPath,
28817
29576
  resolveInsideCwd,
29577
+ resolveInternalAgentBackend,
28818
29578
  resolveProvider,
28819
29579
  resolveProviderConfig,
29580
+ resolveProviderRuntimeConfig,
29581
+ sortProviderNames,
28820
29582
  toClaudeCodeMessagesPayload,
28821
29583
  toCodexResponsesPayload,
28822
29584
  toCodexResponsesTool,