demian-cli 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
2
2
 
3
3
  // src/vscode-worker.ts
4
- import { mkdir as mkdir13, rm as rm4, writeFile as writeFile12 } from "node:fs/promises";
5
- import path31 from "node:path";
4
+ import { mkdir as mkdir15, rm as rm4, writeFile as writeFile14 } from "node:fs/promises";
5
+ import path35 from "node:path";
6
6
 
7
7
  // src/permissions/presets.ts
8
8
  var PERMISSION_PRESETS = ["deny", "ask", "auto", "full"];
@@ -61,7 +61,7 @@ function isExitCommand(input2) {
61
61
  function isStopCommand(input2) {
62
62
  return input2.trim().toLowerCase() === "/stop";
63
63
  }
64
- function isCompactCommand2(input2) {
64
+ function isCompactCommand(input2) {
65
65
  return input2.trim().toLowerCase() === "/compact";
66
66
  }
67
67
  function isRetryCommand(input2) {
@@ -971,7 +971,7 @@ var TuiStore = class {
971
971
  this.#state.promptInput = "";
972
972
  this.#state.promptError = void 0;
973
973
  this.#state.activity = "starting session";
974
- if (!isCompactCommand2(prompt)) {
974
+ if (!isCompactCommand(prompt)) {
975
975
  this.#clearTurnDiff();
976
976
  this.#clearResolvedWorkPlan();
977
977
  }
@@ -986,7 +986,7 @@ var TuiStore = class {
986
986
  this.#state.promptInput = "";
987
987
  this.#state.promptError = void 0;
988
988
  this.#state.activity = "starting session";
989
- if (!isCompactCommand2(queued)) {
989
+ if (!isCompactCommand(queued)) {
990
990
  this.#clearTurnDiff();
991
991
  this.#clearResolvedWorkPlan();
992
992
  }
@@ -1234,7 +1234,7 @@ var TuiStore = class {
1234
1234
  this.stopActiveTask();
1235
1235
  return;
1236
1236
  }
1237
- if (isCompactCommand2(prompt)) {
1237
+ if (isCompactCommand(prompt)) {
1238
1238
  this.#resolvePrompt(prompt);
1239
1239
  return;
1240
1240
  }
@@ -1996,7 +1996,7 @@ var TuiStore = class {
1996
1996
  this.#state.promptInput = "";
1997
1997
  this.#state.promptError = void 0;
1998
1998
  this.#state.activity = prompt ? "starting session" : "cancelled";
1999
- if (prompt && !isCompactCommand2(prompt)) {
1999
+ if (prompt && !isCompactCommand(prompt)) {
2000
2000
  this.#clearTurnDiff();
2001
2001
  this.#clearResolvedWorkPlan();
2002
2002
  }
@@ -2006,7 +2006,7 @@ var TuiStore = class {
2006
2006
  }
2007
2007
  #rememberPrompt(prompt) {
2008
2008
  const value = prompt.trim();
2009
- if (!value || isExitCommand(value) || isStopCommand(value) || isCompactCommand2(value) || isRetryCommand(value)) return;
2009
+ if (!value || isExitCommand(value) || isStopCommand(value) || isCompactCommand(value) || isRetryCommand(value)) return;
2010
2010
  if (this.#promptHistory.at(-1) === value) {
2011
2011
  this.#state.canRetryLastPrompt = true;
2012
2012
  return;
@@ -2327,7 +2327,7 @@ function buildClaudeCodePlanExecutionPrompt(planText, requestText) {
2327
2327
  }
2328
2328
 
2329
2329
  // src/ui/tui/controller.ts
2330
- import path30 from "node:path";
2330
+ import path32 from "node:path";
2331
2331
 
2332
2332
  // src/agents/types.ts
2333
2333
  function normalizeAgent(input2) {
@@ -2421,7 +2421,7 @@ var claudeCodeAgent = {
2421
2421
  name: "claudecode",
2422
2422
  description: "Claude Code external coding agent for focused delegated workspace tasks.",
2423
2423
  mode: "subagent",
2424
- provider: { profile: "claudecode-subagent", permissionProfile: "build" },
2424
+ provider: { profile: "claudecode", permissionProfile: "build" },
2425
2425
  tools: [],
2426
2426
  systemPrompt: [
2427
2427
  "You are demian claudecode, a Claude Code external runtime working as a sub agent for Demian.",
@@ -2466,7 +2466,7 @@ var claudeCodeExplorerAgent = {
2466
2466
  name: "claudecode-explorer",
2467
2467
  description: "Claude Code external read-only explorer for cowork repository inspection.",
2468
2468
  mode: "subagent",
2469
- provider: { profile: "claudecode-subagent", permissionProfile: "explore" },
2469
+ provider: { profile: "claudecode", permissionProfile: "explore" },
2470
2470
  tools: [],
2471
2471
  systemPrompt: [
2472
2472
  "You are demian claudecode-explorer, a Claude Code external runtime working as a read-only cowork sub agent for Demian.",
@@ -2501,7 +2501,7 @@ var claudeCodeBuilderAgent = {
2501
2501
  name: "claudecode-builder",
2502
2502
  description: "Claude Code-backed builder for bounded cowork implementation tasks.",
2503
2503
  mode: "subagent",
2504
- provider: { profile: "claudecode-subagent", permissionProfile: "build" },
2504
+ provider: { profile: "claudecode", permissionProfile: "build" },
2505
2505
  tools: [],
2506
2506
  systemPrompt: [
2507
2507
  "You are demian claudecode-builder, a Claude Code external runtime working as a writer cowork sub agent for Demian.",
@@ -2806,7 +2806,7 @@ import { readFile as readFile6 } from "node:fs/promises";
2806
2806
  import crypto4 from "node:crypto";
2807
2807
  import fs7 from "node:fs";
2808
2808
  import os7 from "node:os";
2809
- import path12 from "node:path";
2809
+ import path13 from "node:path";
2810
2810
 
2811
2811
  // src/providers/retry.ts
2812
2812
  var RETRY_STATUS = /* @__PURE__ */ new Set([408, 409, 425, 429, 500, 502, 503, 504]);
@@ -3199,12 +3199,14 @@ var dynamicImport2 = new Function("specifier", "return import(specifier)");
3199
3199
  var AnthropicProvider = class {
3200
3200
  id = "anthropic";
3201
3201
  #apiKey;
3202
+ #baseURL;
3202
3203
  #defaultModel;
3203
3204
  #defaultMaxTokens;
3204
3205
  #client;
3205
3206
  #onRetry;
3206
3207
  constructor(config) {
3207
3208
  this.#apiKey = config.apiKey;
3209
+ this.#baseURL = config.baseURL;
3208
3210
  this.#defaultModel = config.defaultModel;
3209
3211
  this.#defaultMaxTokens = config.defaultMaxTokens ?? 4096;
3210
3212
  this.#client = config.client;
@@ -3249,7 +3251,7 @@ var AnthropicProvider = class {
3249
3251
  if (this.#client) return this.#client;
3250
3252
  if (!this.#apiKey) throw new Error("AnthropicProvider requires apiKey");
3251
3253
  const Anthropic = await loadAnthropicConstructor();
3252
- this.#client = new Anthropic({ apiKey: this.#apiKey });
3254
+ this.#client = new Anthropic({ apiKey: this.#apiKey, baseURL: this.#baseURL });
3253
3255
  return this.#client;
3254
3256
  }
3255
3257
  };
@@ -3482,19 +3484,56 @@ import { execFile } from "node:child_process";
3482
3484
  import { promisify } from "node:util";
3483
3485
  import fs2 from "node:fs";
3484
3486
  import { chmod, mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "node:fs/promises";
3485
- import path2 from "node:path";
3487
+ import path3 from "node:path";
3486
3488
 
3487
3489
  // src/providers/codex-state.ts
3488
3490
  import crypto from "node:crypto";
3489
3491
  import fs from "node:fs";
3490
3492
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3493
+ import os2 from "node:os";
3494
+ import path2 from "node:path";
3495
+
3496
+ // src/path-expansion.ts
3491
3497
  import os from "node:os";
3492
3498
  import path from "node:path";
3499
+ function expandPathReferences(value) {
3500
+ let expanded = expandTilde(value);
3501
+ 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);
3502
+ expanded = expanded.replace(/%([A-Za-z_][A-Za-z0-9_]*)%/g, (match, name) => envPathValue(name) ?? match);
3503
+ return expanded;
3504
+ }
3505
+ function resolveExpandedPath(value) {
3506
+ return path.resolve(expandPathReferences(value));
3507
+ }
3508
+ function expandTilde(value) {
3509
+ if (value === "~") return os.homedir();
3510
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(os.homedir(), value.slice(2));
3511
+ return value;
3512
+ }
3513
+ function envPathValue(name) {
3514
+ if (!name) return void 0;
3515
+ const value = processEnvValue(name);
3516
+ if (value !== void 0) return value;
3517
+ const upper = name.toUpperCase();
3518
+ if (upper === "HOME" || upper === "USERPROFILE") return os.homedir();
3519
+ if (process.platform === "win32" && upper === "HOMEDRIVE") return path.win32.parse(os.homedir()).root.replace(/[\\/]$/, "");
3520
+ if (process.platform === "win32" && upper === "HOMEPATH") return os.homedir().replace(/^[A-Za-z]:/, "");
3521
+ return void 0;
3522
+ }
3523
+ function processEnvValue(name) {
3524
+ if (process.env[name] !== void 0) return process.env[name];
3525
+ if (process.platform !== "win32") return void 0;
3526
+ const upper = name.toUpperCase();
3527
+ const key = Object.keys(process.env).find((item) => item.toUpperCase() === upper);
3528
+ return key ? process.env[key] : void 0;
3529
+ }
3530
+
3531
+ // src/providers/codex-state.ts
3493
3532
  function resolveCodexHome(configured) {
3494
- return path.resolve(expandCodexPath(configured ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex")));
3533
+ return resolveExpandedPath(configured ?? process.env.CODEX_HOME ?? path2.join(os2.homedir(), ".codex"));
3495
3534
  }
3496
3535
  async function loadOrCreateInstallationId(codexHome) {
3497
- const filePath = path.join(codexHome, "installation_id");
3536
+ const filePath = path2.join(codexHome, "installation_id");
3498
3537
  try {
3499
3538
  const existing = (await readFile(filePath, "utf8")).trim();
3500
3539
  if (isUuid(existing)) return existing;
@@ -3516,15 +3555,6 @@ function isNodeError(error, code) {
3516
3555
  function fileExists(filePath) {
3517
3556
  return fs.existsSync(filePath);
3518
3557
  }
3519
- function expandCodexPath(value) {
3520
- let expanded = value;
3521
- if (expanded === "~") return os.homedir();
3522
- if (expanded.startsWith("~/")) expanded = path.join(os.homedir(), expanded.slice(2));
3523
- return expanded.replace(/\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/gi, (match, braced, bare) => {
3524
- const name = braced ?? bare;
3525
- return name && process.env[name] !== void 0 ? process.env[name] : match;
3526
- });
3527
- }
3528
3558
 
3529
3559
  // src/providers/codex-auth.ts
3530
3560
  var execFileAsync = promisify(execFile);
@@ -3747,10 +3777,10 @@ var CodexAuthStore = class {
3747
3777
  }
3748
3778
  };
3749
3779
  function authFilePath(codexHome) {
3750
- return path2.join(codexHome, "auth.json");
3780
+ return path3.join(codexHome, "auth.json");
3751
3781
  }
3752
3782
  function codexKeyringAccount(codexHome) {
3753
- const resolved = path2.resolve(codexHome);
3783
+ const resolved = path3.resolve(codexHome);
3754
3784
  const canonical = fs2.existsSync(resolved) ? fs2.realpathSync.native(resolved) : resolved;
3755
3785
  const hash = crypto2.createHash("sha256").update(canonical).digest("hex").slice(0, 16);
3756
3786
  return `cli|${hash}`;
@@ -4179,8 +4209,8 @@ import { randomUUID } from "node:crypto";
4179
4209
  import { execFile as execFile2 } from "node:child_process";
4180
4210
  import fs3 from "node:fs";
4181
4211
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "node:fs/promises";
4182
- import os2 from "node:os";
4183
- import path3 from "node:path";
4212
+ import os3 from "node:os";
4213
+ import path4 from "node:path";
4184
4214
  import { promisify as promisify2 } from "node:util";
4185
4215
  var execFileAsync2 = promisify2(execFile2);
4186
4216
  var CLAUDE_CODE_KEYRING_SERVICE = "Claude Code-credentials";
@@ -4213,7 +4243,7 @@ function getSharedClaudeCodeAuthStore(options2 = {}) {
4213
4243
  proactiveRefreshMinutes: options2.proactiveRefreshMinutes ?? 30,
4214
4244
  refreshCache: options2.refreshCache ?? "claude-store",
4215
4245
  refreshTokenURL: options2.refreshTokenURL ?? DEFAULT_CLAUDE_CODE_REFRESH_TOKEN_URL,
4216
- keychainAccount: options2.keychainAccount ?? os2.userInfo().username
4246
+ keychainAccount: options2.keychainAccount ?? os3.userInfo().username
4217
4247
  });
4218
4248
  const existing = sharedAuthStores2.get(key);
4219
4249
  if (existing) return existing;
@@ -4242,7 +4272,7 @@ var ClaudeCodeAuthStore = class {
4242
4272
  this.#proactiveRefreshMinutes = options2.proactiveRefreshMinutes ?? 30;
4243
4273
  this.#refreshCache = options2.refreshCache ?? "claude-store";
4244
4274
  this.#refreshTokenURL = options2.refreshTokenURL ?? DEFAULT_CLAUDE_CODE_REFRESH_TOKEN_URL;
4245
- this.#keychainAccount = options2.keychainAccount ?? os2.userInfo().username;
4275
+ this.#keychainAccount = options2.keychainAccount ?? os3.userInfo().username;
4246
4276
  this.#fetch = options2.fetch ?? fetch;
4247
4277
  this.#keyring = options2.keyring ?? new MacOSSecurityKeyring2();
4248
4278
  }
@@ -4443,10 +4473,10 @@ var ClaudeCodeAuthStore = class {
4443
4473
  }
4444
4474
  };
4445
4475
  function resolveClaudeConfigDir(configured) {
4446
- return path3.resolve(expandClaudePath(configured ?? process.env.CLAUDE_CONFIG_DIR ?? path3.join(os2.homedir(), ".claude")));
4476
+ return resolveExpandedPath(configured ?? process.env.CLAUDE_CONFIG_DIR ?? path4.join(os3.homedir(), ".claude"));
4447
4477
  }
4448
4478
  function claudeCodeAuthFilePath(claudeConfigDir) {
4449
- return path3.join(claudeConfigDir, ".credentials.json");
4479
+ return path4.join(claudeConfigDir, ".credentials.json");
4450
4480
  }
4451
4481
  function oauthPayload(auth) {
4452
4482
  const raw = auth.claudeAiOauth;
@@ -4505,15 +4535,6 @@ async function writeClaudeCodeAuthFileAtomic(claudeConfigDir, auth) {
4505
4535
  await chmod2(tempPath, 384);
4506
4536
  await rename2(tempPath, filePath);
4507
4537
  }
4508
- function expandClaudePath(value) {
4509
- let expanded = value;
4510
- if (expanded === "~") return os2.homedir();
4511
- if (expanded.startsWith("~/")) expanded = path3.join(os2.homedir(), expanded.slice(2));
4512
- return expanded.replace(/\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/gi, (match, braced, bare) => {
4513
- const name = braced ?? bare;
4514
- return name && process.env[name] !== void 0 ? process.env[name] : match;
4515
- });
4516
- }
4517
4538
  var MacOSSecurityKeyring2 = class {
4518
4539
  async load(service, account) {
4519
4540
  if (process.platform !== "darwin") return void 0;
@@ -4750,6 +4771,111 @@ function parseClaudeCodeErrorBody(body) {
4750
4771
  }
4751
4772
  }
4752
4773
 
4774
+ // src/providers/ollama.ts
4775
+ var OllamaProvider = class {
4776
+ id = "ollama";
4777
+ #baseURL;
4778
+ #apiKey;
4779
+ #defaultModel;
4780
+ #fetch;
4781
+ #onRetry;
4782
+ constructor(config) {
4783
+ this.#baseURL = config.baseURL.replace(/\/+$/, "");
4784
+ this.#apiKey = config.apiKey;
4785
+ this.#defaultModel = config.defaultModel;
4786
+ this.#fetch = config.fetch ?? fetch;
4787
+ this.#onRetry = config.onRetry;
4788
+ }
4789
+ async chat(req) {
4790
+ return chatWithRetry(() => this.#rawChat(req), {
4791
+ signal: req.signal,
4792
+ onRetry: this.#onRetry
4793
+ });
4794
+ }
4795
+ async #rawChat(req) {
4796
+ const response = await this.#fetch(`${this.#baseURL}/chat`, {
4797
+ method: "POST",
4798
+ headers: {
4799
+ "content-type": "application/json",
4800
+ ...this.#authHeaders()
4801
+ },
4802
+ body: JSON.stringify({
4803
+ model: req.model || this.#defaultModel,
4804
+ messages: req.messages.map(toOllamaMessage),
4805
+ ...req.tools.length > 0 ? { tools: req.tools.map(toOpenAITool) } : {},
4806
+ stream: false,
4807
+ ...req.temperature !== void 0 ? { options: { temperature: req.temperature } } : {}
4808
+ }),
4809
+ signal: req.signal
4810
+ });
4811
+ const text = await response.text();
4812
+ if (!response.ok) throw new ProviderHttpError(response.status, text, parseRetryAfter(response.headers.get("retry-after")));
4813
+ return normalizeOllamaResponse(text ? JSON.parse(text) : {});
4814
+ }
4815
+ #authHeaders() {
4816
+ return this.#apiKey ? { authorization: `Bearer ${this.#apiKey}` } : {};
4817
+ }
4818
+ };
4819
+ function toOllamaMessage(message) {
4820
+ if (message.role === "tool") {
4821
+ return {
4822
+ role: "tool",
4823
+ content: message.content,
4824
+ tool_name: message.name
4825
+ };
4826
+ }
4827
+ const out = {
4828
+ role: message.role,
4829
+ content: typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? "")
4830
+ };
4831
+ if (message.role === "assistant" && message.toolCalls?.length) {
4832
+ out.tool_calls = message.toolCalls.map((call) => ({
4833
+ function: {
4834
+ name: call.name,
4835
+ arguments: call.input ?? {}
4836
+ }
4837
+ }));
4838
+ }
4839
+ return out;
4840
+ }
4841
+ function normalizeOllamaResponse(raw) {
4842
+ const data = raw;
4843
+ const toolCalls = [];
4844
+ for (const call of data.message?.tool_calls ?? []) {
4845
+ const name = call.function?.name;
4846
+ if (!name) continue;
4847
+ toolCalls.push({
4848
+ id: call.id ?? `call_${toolCalls.length + 1}`,
4849
+ name,
4850
+ input: normalizeToolArguments(call.function?.arguments)
4851
+ });
4852
+ }
4853
+ const message = {
4854
+ role: "assistant",
4855
+ content: data.message?.content ?? null,
4856
+ ...toolCalls.length ? { toolCalls } : {}
4857
+ };
4858
+ return {
4859
+ message,
4860
+ toolCalls,
4861
+ stopReason: toolCalls.length ? "tool_use" : data.done_reason === "length" ? "max_tokens" : "end_turn",
4862
+ usage: data.prompt_eval_count !== void 0 || data.eval_count !== void 0 ? {
4863
+ inputTokens: data.prompt_eval_count,
4864
+ outputTokens: data.eval_count,
4865
+ totalTokens: (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0)
4866
+ } : void 0,
4867
+ raw
4868
+ };
4869
+ }
4870
+ function normalizeToolArguments(value) {
4871
+ if (typeof value !== "string") return value ?? {};
4872
+ try {
4873
+ return JSON.parse(value);
4874
+ } catch {
4875
+ return {};
4876
+ }
4877
+ }
4878
+
4753
4879
  // src/external-runtime/claudecode-cli.ts
4754
4880
  import { spawn as spawn3 } from "node:child_process";
4755
4881
  import readline from "node:readline";
@@ -4757,7 +4883,7 @@ import readline from "node:readline";
4757
4883
  // src/external-runtime/claudecode-attachments.ts
4758
4884
  import crypto3 from "node:crypto";
4759
4885
  import fs4 from "node:fs";
4760
- import path4 from "node:path";
4886
+ import path5 from "node:path";
4761
4887
  async function resolveClaudeCodeAttachmentPrompt(runtimeLabel, config, req) {
4762
4888
  const attachments = req.attachments ?? [];
4763
4889
  if (attachments.length === 0) return req.prompt;
@@ -4799,7 +4925,7 @@ function unsupportedAttachmentError(runtimeLabel, count, detail) {
4799
4925
  }
4800
4926
  function formatAttachmentReference(value, cwd) {
4801
4927
  if (isUrl(value)) return imageMimeFromPath(value) ? `[image: ${value} (${imageMimeFromPath(value)}, remote)]` : `[attachment: ${value}]`;
4802
- const target = path4.isAbsolute(value) ? value : path4.resolve(cwd, value);
4928
+ const target = path5.isAbsolute(value) ? value : path5.resolve(cwd, value);
4803
4929
  try {
4804
4930
  const stats = fs4.statSync(target);
4805
4931
  if (!stats.isFile()) return `[attachment: ${value} (${stats.size} bytes)]`;
@@ -4816,7 +4942,7 @@ function isUrl(value) {
4816
4942
  }
4817
4943
  function imageMimeFromPath(value) {
4818
4944
  const pathname = isUrl(value) ? new URL(value).pathname : value;
4819
- const ext = path4.extname(pathname).toLowerCase();
4945
+ const ext = path5.extname(pathname).toLowerCase();
4820
4946
  if (ext === ".png") return "image/png";
4821
4947
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
4822
4948
  if (ext === ".gif") return "image/gif";
@@ -4848,15 +4974,14 @@ function hasAnthropicApiKeyEnv(env = process.env) {
4848
4974
 
4849
4975
  // src/external-runtime/claudecode-paths.ts
4850
4976
  import fs5 from "node:fs";
4851
- import os3 from "node:os";
4852
- import path5 from "node:path";
4977
+ import path6 from "node:path";
4853
4978
  function resolveClaudeCodeCliPath(configured) {
4854
4979
  if (configured) return expandHome(configured);
4855
4980
  if (process.env.CLAUDE_CODE_CLI) return expandHome(process.env.CLAUDE_CODE_CLI);
4856
4981
  const candidates = ["~/.local/bin/claude", "claude"];
4857
4982
  for (const candidate of candidates) {
4858
4983
  const expanded = expandHome(candidate);
4859
- if (path5.isAbsolute(expanded)) {
4984
+ if (path6.isAbsolute(expanded)) {
4860
4985
  if (isExecutableFile(expanded)) return expanded;
4861
4986
  continue;
4862
4987
  }
@@ -4865,9 +4990,7 @@ function resolveClaudeCodeCliPath(configured) {
4865
4990
  return void 0;
4866
4991
  }
4867
4992
  function expandHome(value) {
4868
- if (value === "~") return os3.homedir();
4869
- if (value.startsWith("~/")) return path5.join(os3.homedir(), value.slice(2));
4870
- return value;
4993
+ return expandPathReferences(value);
4871
4994
  }
4872
4995
  function isExecutableFile(filePath) {
4873
4996
  try {
@@ -5133,14 +5256,14 @@ function numberValue2(value) {
5133
5256
  // src/external-runtime/session-lock.ts
5134
5257
  import { mkdir as mkdir4, open, readFile as readFile4, unlink } from "node:fs/promises";
5135
5258
  import os4 from "node:os";
5136
- import path6 from "node:path";
5259
+ import path7 from "node:path";
5137
5260
  var DEFAULT_STALE_MS = 30 * 60 * 1e3;
5138
5261
  async function acquireClaudeCodeSessionLock(sessionId, options2 = {}) {
5139
- const dir = options2.dir ?? path6.join(os4.homedir(), ".demian", "claude-sessions");
5262
+ const dir = options2.dir ?? path7.join(os4.homedir(), ".demian", "claude-sessions");
5140
5263
  const staleMs = options2.staleMs ?? DEFAULT_STALE_MS;
5141
5264
  const now2 = options2.now ?? (() => Date.now());
5142
5265
  await mkdir4(dir, { recursive: true });
5143
- const lockPath = path6.join(dir, `${encodeURIComponent(sessionId)}.lock`);
5266
+ const lockPath = path7.join(dir, `${encodeURIComponent(sessionId)}.lock`);
5144
5267
  for (let attempt = 0; attempt < 2; attempt++) {
5145
5268
  try {
5146
5269
  const handle = await open(lockPath, "wx");
@@ -5204,14 +5327,14 @@ function isAlreadyExists(error) {
5204
5327
  // src/external-runtime/usage-ledger.ts
5205
5328
  import { mkdir as mkdir5, readFile as readFile5, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
5206
5329
  import os5 from "node:os";
5207
- import path7 from "node:path";
5330
+ import path8 from "node:path";
5208
5331
  var processLedger = /* @__PURE__ */ new Map();
5209
5332
  var ClaudeCodeUsageLedger = class {
5210
5333
  #scope;
5211
5334
  #filePath;
5212
5335
  constructor(options2 = {}) {
5213
5336
  this.#scope = options2.scope ?? "process";
5214
- this.#filePath = options2.filePath ?? path7.join(os5.homedir(), ".demian", "usage-ledger.json");
5337
+ this.#filePath = options2.filePath ?? path8.join(os5.homedir(), ".demian", "usage-ledger.json");
5215
5338
  }
5216
5339
  async spentUsd(key, now2 = /* @__PURE__ */ new Date()) {
5217
5340
  if (this.#scope === "process") return processLedger.get(processKey(key)) ?? 0;
@@ -5242,7 +5365,7 @@ var ClaudeCodeUsageLedger = class {
5242
5365
  return { version: 1, buckets: {} };
5243
5366
  }
5244
5367
  async #write(file) {
5245
- await mkdir5(path7.dirname(this.#filePath), { recursive: true });
5368
+ await mkdir5(path8.dirname(this.#filePath), { recursive: true });
5246
5369
  const temp = `${this.#filePath}.${process.pid}.tmp`;
5247
5370
  await writeFile4(temp, `${JSON.stringify(file, null, 2)}
5248
5371
  `, "utf8");
@@ -24903,10 +25026,10 @@ function now() {
24903
25026
  }
24904
25027
 
24905
25028
  // src/permissions/engine.ts
24906
- import path10 from "node:path";
25029
+ import path11 from "node:path";
24907
25030
 
24908
25031
  // src/permissions/grants.ts
24909
- import path8 from "node:path";
25032
+ import path9 from "node:path";
24910
25033
 
24911
25034
  // src/util.ts
24912
25035
  function stableStringify(value) {
@@ -24961,8 +25084,8 @@ function grantKeyFor(tool, input2, cwd) {
24961
25084
  }
24962
25085
  if (tool === "write_file" || tool === "edit_file") {
24963
25086
  const filePath = typeof object2.path === "string" ? object2.path : "";
24964
- const parent = filePath ? path8.dirname(path8.resolve(cwd, filePath)) : cwd;
24965
- return `${tool}:${path8.relative(cwd, parent) || "."}`;
25087
+ const parent = filePath ? path9.dirname(path9.resolve(cwd, filePath)) : cwd;
25088
+ return `${tool}:${path9.relative(cwd, parent) || "."}`;
24966
25089
  }
24967
25090
  return `${tool}:${stableStringify(input2)}`;
24968
25091
  }
@@ -24989,30 +25112,30 @@ function firstCommandTokens(command, count) {
24989
25112
  }
24990
25113
 
24991
25114
  // src/workspace/paths.ts
24992
- import path9 from "node:path";
25115
+ import path10 from "node:path";
24993
25116
  function normalizePathForMatch(value) {
24994
- return value.split(path9.sep).join("/");
25117
+ return value.split(path10.sep).join("/");
24995
25118
  }
24996
25119
  function isInsidePath(root, candidate) {
24997
- const relative = path9.relative(path9.resolve(root), path9.resolve(candidate));
24998
- return relative === "" || !relative.startsWith("..") && !path9.isAbsolute(relative);
25120
+ const relative = path10.relative(path10.resolve(root), path10.resolve(candidate));
25121
+ return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
24999
25122
  }
25000
25123
  function resolveInsideCwd(cwd, inputPath) {
25001
25124
  if (typeof inputPath !== "string" || inputPath.length === 0) {
25002
25125
  throw new Error("path must be a non-empty string");
25003
25126
  }
25004
- const resolved = path9.resolve(cwd, inputPath);
25127
+ const resolved = path10.resolve(cwd, inputPath);
25005
25128
  if (!isInsidePath(cwd, resolved)) {
25006
25129
  throw new Error(`Path is outside the workspace: ${inputPath}`);
25007
25130
  }
25008
25131
  return resolved;
25009
25132
  }
25010
25133
  function relativeToCwd(cwd, absolutePath) {
25011
- const relative = path9.relative(cwd, absolutePath);
25134
+ const relative = path10.relative(cwd, absolutePath);
25012
25135
  return normalizePathForMatch(relative || ".");
25013
25136
  }
25014
25137
  function isEnvFile(filePath) {
25015
- const base = path9.basename(filePath);
25138
+ const base = path10.basename(filePath);
25016
25139
  if (base === ".env.example") return false;
25017
25140
  return base === ".env" || base.startsWith(".env.");
25018
25141
  }
@@ -25119,7 +25242,7 @@ var PermissionEngine = class {
25119
25242
  #hardDeny(tool, input2, grantKey) {
25120
25243
  const paths = inputPaths(tool, input2);
25121
25244
  for (const item of paths) {
25122
- const resolved = path10.resolve(this.#cwd, item);
25245
+ const resolved = path11.resolve(this.#cwd, item);
25123
25246
  if (!isInsidePath(this.#cwd, resolved)) {
25124
25247
  return {
25125
25248
  decision: "deny",
@@ -25173,7 +25296,7 @@ function matchesRule(rule, tool, input2, cwd) {
25173
25296
  if (rule.match.pathGlob) {
25174
25297
  const paths = inputPaths(tool, input2);
25175
25298
  if (paths.length === 0) return false;
25176
- if (!paths.some((item) => matchesPathRule(rule.match?.pathGlob, relativeToCwd(cwd, path10.resolve(cwd, item))))) {
25299
+ if (!paths.some((item) => matchesPathRule(rule.match?.pathGlob, relativeToCwd(cwd, path11.resolve(cwd, item))))) {
25177
25300
  return false;
25178
25301
  }
25179
25302
  }
@@ -25220,7 +25343,7 @@ function rulePriority(rule, ref) {
25220
25343
  }
25221
25344
  function matchesPathRule(pattern, relativePath) {
25222
25345
  if (!pattern) return true;
25223
- if (path10.basename(relativePath) === ".env.example" && pattern.includes(".env.*")) return false;
25346
+ if (path11.basename(relativePath) === ".env.example" && pattern.includes(".env.*")) return false;
25224
25347
  return matchGlob(pattern, relativePath);
25225
25348
  }
25226
25349
  function mostSpecificRule(rules, input2, cwd, ref) {
@@ -25298,13 +25421,13 @@ function permissionLabel(req) {
25298
25421
  if ((req.tool === "write_file" || req.tool === "edit_file" || req.tool === "read_file") && typeof inputObject.path === "string") {
25299
25422
  return `${req.tool}: ${inputObject.path}`;
25300
25423
  }
25301
- const path32 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
25302
- if (path32) return `${tool}: ${path32}`;
25424
+ const path36 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
25425
+ if (path36) return `${tool}: ${path36}`;
25303
25426
  return tool;
25304
25427
  }
25305
25428
 
25306
25429
  // src/workspace/write-scope.ts
25307
- import path11 from "node:path";
25430
+ import path12 from "node:path";
25308
25431
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "Write", "Edit", "MultiEdit", "NotebookEdit"]);
25309
25432
  var WORKSPACE_SCOPE = "**";
25310
25433
  function normalizeWriteScope(cwd, scope) {
@@ -25326,7 +25449,7 @@ function enforceToolWriteScope(input2) {
25326
25449
  if (paths.length === 0) return { ok: true, paths: [] };
25327
25450
  const checked = [];
25328
25451
  for (const target of paths) {
25329
- const resolved = path11.resolve(input2.cwd, target);
25452
+ const resolved = path12.resolve(input2.cwd, target);
25330
25453
  const relative = relativeToCwd(input2.cwd, resolved);
25331
25454
  checked.push(relative);
25332
25455
  const allowed = isPathAllowedByWriteScope(input2.cwd, target, input2.writeScope);
@@ -25350,7 +25473,7 @@ function outOfScopeDiffFiles(cwd, summary, writeScope) {
25350
25473
  return out;
25351
25474
  }
25352
25475
  function isPathAllowedByWriteScope(cwd, inputPath, writeScope) {
25353
- const resolved = path11.resolve(cwd, inputPath);
25476
+ const resolved = path12.resolve(cwd, inputPath);
25354
25477
  if (!isInsidePath(cwd, resolved)) {
25355
25478
  return { ok: false, error: "path is outside the workspace", paths: [inputPath] };
25356
25479
  }
@@ -25369,9 +25492,9 @@ function normalizeScopePattern(cwd, raw) {
25369
25492
  if (!value) return { ok: false, error: "writeScope entries must be non-empty." };
25370
25493
  if (value.includes("\0")) return { ok: false, error: "writeScope entries must not contain NUL bytes." };
25371
25494
  const hasGlob2 = /[*?[\]{}]/.test(value);
25372
- if (path11.isAbsolute(value)) {
25495
+ if (path12.isAbsolute(value)) {
25373
25496
  if (hasGlob2) return { ok: false, error: `writeScope absolute globs are not supported: ${raw}` };
25374
- const resolved = path11.resolve(value);
25497
+ const resolved = path12.resolve(value);
25375
25498
  if (!isInsidePath(cwd, resolved)) return { ok: false, error: `writeScope path is outside the workspace: ${raw}` };
25376
25499
  value = relativeToCwd(cwd, resolved);
25377
25500
  }
@@ -25748,6 +25871,7 @@ function claudeCodePreviewEnabled(config = {}, env = process.env) {
25748
25871
  }
25749
25872
 
25750
25873
  // src/config.ts
25874
+ var BUILTIN_PROVIDER_ORDER = ["openai", "anthropic", "gemini", "groq", "azure", "lmstudio", "ollama-local", "ollama-cloud", "llamacpp", "vllm", "codex", "claudecode"];
25751
25875
  var CLAUDE_CODE_SONNET_MODEL = "claude-sonnet-4-6";
25752
25876
  var CLAUDE_CODE_OPUS_MODEL = "claude-opus-4-7";
25753
25877
  var CLAUDE_CODE_MODELS = [CLAUDE_CODE_SONNET_MODEL, CLAUDE_CODE_OPUS_MODEL];
@@ -25785,7 +25909,7 @@ var defaultConfig = {
25785
25909
  maxConcurrentExpensive: 1,
25786
25910
  maxConcurrentWriters: 1,
25787
25911
  maxConcurrentPerProvider: {
25788
- "claudecode-subagent": 1
25912
+ claudecode: 1
25789
25913
  },
25790
25914
  tokenBudget: null,
25791
25915
  defaultMergeStrategy: "synthesize",
@@ -25907,73 +26031,102 @@ var defaultConfig = {
25907
26031
  providers: {
25908
26032
  openai: {
25909
26033
  type: "openai-compatible",
25910
- model: "openai-model-name",
25911
26034
  baseURL: "https://api.openai.com/v1",
25912
- apiKeyEnv: "OPENAI_API_KEY"
26035
+ apiKeyEnv: "OPENAI_API_KEY",
26036
+ catalog: {
26037
+ type: "openai-models",
26038
+ endpoint: "https://api.openai.com/v1/models"
26039
+ }
26040
+ },
26041
+ anthropic: {
26042
+ type: "anthropic",
26043
+ baseURL: "https://api.anthropic.com/v1",
26044
+ apiKeyEnv: "ANTHROPIC_API_KEY",
26045
+ catalog: {
26046
+ type: "anthropic-models",
26047
+ endpoint: "https://api.anthropic.com/v1/models"
26048
+ }
25913
26049
  },
25914
26050
  gemini: {
25915
26051
  type: "openai-compatible",
25916
- model: "gemini-model-name",
25917
26052
  baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
25918
- apiKeyEnv: "GOOGLE_API_KEY"
26053
+ apiKeyEnv: "GEMINI_API_KEY",
26054
+ apiKeyEnvAliases: ["GOOGLE_API_KEY"],
26055
+ catalog: {
26056
+ type: "gemini-openai-models",
26057
+ endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models"
26058
+ }
26059
+ },
26060
+ groq: {
26061
+ type: "openai-compatible",
26062
+ baseURL: "https://api.groq.com/openai/v1",
26063
+ apiKeyEnv: "GROQ_API_KEY",
26064
+ catalog: {
26065
+ type: "groq-models",
26066
+ endpoint: "https://api.groq.com/openai/v1/models"
26067
+ }
25919
26068
  },
25920
- ollama: {
26069
+ azure: {
25921
26070
  type: "openai-compatible",
25922
- model: "local-coder-model",
25923
- baseURL: "http://localhost:11434/v1",
25924
- apiKey: "ollama",
25925
- quirks: { omitTemperature: true }
26071
+ auth: { type: "api-key", header: "api-key" },
26072
+ modelProfiles: [
26073
+ {
26074
+ name: "azure-example",
26075
+ displayName: "Azure example",
26076
+ model: "azure-deployment-name",
26077
+ baseURL: "https://example.openai.azure.com/openai/v1",
26078
+ apiKeyEnv: "AZURE_OPENAI_API_KEY"
26079
+ }
26080
+ ]
25926
26081
  },
25927
26082
  lmstudio: {
25928
26083
  type: "openai-compatible",
25929
- model: "local-coder-model",
25930
26084
  baseURL: "http://localhost:1234/v1",
25931
- apiKey: "lm-studio"
26085
+ apiKey: "lm-studio",
26086
+ catalog: {
26087
+ type: "openai-compatible-models",
26088
+ endpoint: "http://localhost:1234/v1/models"
26089
+ }
25932
26090
  },
25933
- vllm: {
26091
+ "ollama-local": {
25934
26092
  type: "openai-compatible",
25935
- model: "local-coder-model",
25936
- baseURL: "http://localhost:8000/v1",
25937
- apiKey: "vllm"
26093
+ baseURL: "http://localhost:11434/v1",
26094
+ apiKey: "ollama",
26095
+ quirks: { omitTemperature: true },
26096
+ catalog: {
26097
+ type: "openai-compatible-models",
26098
+ endpoint: "http://localhost:11434/v1/models"
26099
+ }
26100
+ },
26101
+ "ollama-cloud": {
26102
+ type: "ollama",
26103
+ baseURL: "https://ollama.com/api",
26104
+ apiKeyEnv: "OLLAMA_API_KEY",
26105
+ catalog: {
26106
+ type: "ollama-tags",
26107
+ endpoint: "https://ollama.com/api/tags"
26108
+ }
25938
26109
  },
25939
26110
  llamacpp: {
25940
26111
  type: "openai-compatible",
25941
- model: "local-coder-model",
25942
26112
  baseURL: "http://localhost:8080/v1",
25943
- apiKey: "llama.cpp"
25944
- },
25945
- openrouter: {
25946
- type: "openai-compatible",
25947
- model: "provider/model-name",
25948
- baseURL: "https://openrouter.ai/api/v1",
25949
- apiKeyEnv: "OPENROUTER_API_KEY"
25950
- },
25951
- together: {
25952
- type: "openai-compatible",
25953
- model: "provider/model-name",
25954
- baseURL: "https://api.together.xyz/v1",
25955
- apiKeyEnv: "TOGETHER_API_KEY"
25956
- },
25957
- groq: {
25958
- type: "openai-compatible",
25959
- model: "provider/model-name",
25960
- baseURL: "https://api.groq.com/openai/v1",
25961
- apiKeyEnv: "GROQ_API_KEY"
26113
+ apiKey: "llama.cpp",
26114
+ catalog: {
26115
+ type: "openai-compatible-models",
26116
+ endpoint: "http://localhost:8080/v1/models"
26117
+ }
25962
26118
  },
25963
- azure: {
26119
+ vllm: {
25964
26120
  type: "openai-compatible",
25965
- model: "azure-deployment-name",
25966
- baseURL: "https://example.openai.azure.com/openai/v1",
25967
- apiKeyEnv: "AZURE_OPENAI_API_KEY"
25968
- },
25969
- anthropic: {
25970
- type: "anthropic",
25971
- model: "anthropic-model-name",
25972
- apiKeyEnv: "ANTHROPIC_API_KEY"
26121
+ baseURL: "http://localhost:8000/v1",
26122
+ apiKey: "vllm",
26123
+ catalog: {
26124
+ type: "openai-compatible-models",
26125
+ endpoint: "http://localhost:8000/v1/models"
26126
+ }
25973
26127
  },
25974
26128
  codex: {
25975
26129
  type: "codex",
25976
- model: "gpt-5.1-codex",
25977
26130
  baseURL: "https://chatgpt.com/backend-api/codex",
25978
26131
  authStore: "auto",
25979
26132
  allowApiKeyFallback: false,
@@ -25985,6 +26138,9 @@ var defaultConfig = {
25985
26138
  effort: "medium",
25986
26139
  summary: "auto"
25987
26140
  }
26141
+ },
26142
+ catalog: {
26143
+ type: "codex-oauth-models"
25988
26144
  }
25989
26145
  },
25990
26146
  claudecode: {
@@ -26003,67 +26159,37 @@ var defaultConfig = {
26003
26159
  useBareMode: false,
26004
26160
  usageLedgerScope: "process",
26005
26161
  sessionLock: true,
26006
- abortPolicy: "record-only"
26007
- },
26008
- "claudecode-plan": {
26009
- type: "claudecode",
26010
- runtime: "agent-sdk",
26011
- model: CLAUDE_CODE_SONNET_MODEL,
26012
- models: [...CLAUDE_CODE_MODELS],
26013
- permissionProfile: "plan",
26014
- cliPath: "~/.local/bin/claude",
26015
- cwdMode: "session",
26016
- historyPolicy: "passthrough-resume",
26017
- onInvalidResume: "fresh",
26018
- attachmentFallback: "block",
26019
- allowSubagents: false,
26020
- sanitizeApiKeyEnv: true,
26021
- authPreflight: true,
26022
- useBareMode: false,
26023
- usageLedgerScope: "process",
26024
- sessionLock: true,
26025
- abortPolicy: "record-only",
26026
- hidden: true
26027
- },
26028
- "claudecode-subagent": {
26029
- type: "claudecode",
26030
- runtime: "agent-sdk",
26031
- model: CLAUDE_CODE_SONNET_MODEL,
26032
- models: [...CLAUDE_CODE_MODELS],
26033
- permissionProfile: "build",
26034
- cliPath: "~/.local/bin/claude",
26035
- cwdMode: "session",
26036
- historyPolicy: "passthrough-resume",
26037
- onInvalidResume: "fresh",
26038
- attachmentFallback: "block",
26039
- allowSubagents: false,
26040
- sanitizeApiKeyEnv: true,
26041
- authPreflight: true,
26042
- useBareMode: false,
26043
- usageLedgerScope: "process",
26044
- sessionLock: true,
26045
26162
  abortPolicy: "record-only",
26046
- hidden: true
26163
+ catalog: {
26164
+ type: "claudecode-supported-models"
26165
+ }
26047
26166
  }
26048
26167
  }
26049
26168
  };
26050
26169
  async function loadConfig(options2) {
26051
26170
  const configs = [];
26052
- const userConfigDir = path12.join(os7.homedir(), ".demian");
26053
- const projectConfigDir = path12.join(options2.cwd, ".demian");
26054
- const configPaths = [
26055
- path12.join(userConfigDir, "config.json"),
26056
- path12.join(userConfigDir, "config.jsond"),
26057
- path12.join(projectConfigDir, "config.json"),
26058
- path12.join(projectConfigDir, "config.jsond"),
26059
- options2.configPath
26060
- ].filter(Boolean);
26171
+ const userConfigDir = path13.join(os7.homedir(), ".demian");
26172
+ const configPaths = [path13.join(userConfigDir, "config.json"), path13.join(userConfigDir, "config.jsond"), options2.configPath].filter(Boolean);
26061
26173
  for (const filePath of configPaths) {
26062
26174
  if (!fs7.existsSync(filePath)) continue;
26063
- configs.push(parseConfigText(await readFile6(filePath, "utf8"), filePath));
26175
+ const parsed = parseConfigText(await readFile6(filePath, "utf8"), filePath);
26176
+ warnIfV1Config(parsed, filePath);
26177
+ configs.push(parsed);
26064
26178
  }
26065
26179
  return configs.reduce((acc, item) => mergeConfig(acc, item), structuredClone(defaultConfig));
26066
26180
  }
26181
+ function warnIfV1Config(parsed, filePath) {
26182
+ if (parsed.version === 2) return;
26183
+ const providers = parsed.providers ?? {};
26184
+ const hasLegacyShape = Object.values(providers).some((provider) => {
26185
+ const candidate = provider;
26186
+ return (typeof candidate.model === "string" || Array.isArray(candidate.models)) && !Array.isArray(candidate.modelProfiles);
26187
+ });
26188
+ if (!hasLegacyShape && parsed.version === void 0 && Object.keys(providers).length === 0) return;
26189
+ if (!hasLegacyShape) return;
26190
+ 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.
26191
+ `);
26192
+ }
26067
26193
  function parseConfigText(text, filePath) {
26068
26194
  try {
26069
26195
  return JSON.parse(filePath.endsWith(".jsond") ? stripJsonD(text) : text);
@@ -26270,7 +26396,127 @@ function mergeConfig(base, overlay) {
26270
26396
  };
26271
26397
  }
26272
26398
  function normalizeProviderAlias(providerName) {
26273
- return providerName === "claudecode-plan" ? "claudecode" : providerName;
26399
+ if (providerName === "claudecode-plan" || providerName === "claudecode-subagent") return "claudecode";
26400
+ if (providerName === "ollama") return "ollama-local";
26401
+ return providerName;
26402
+ }
26403
+ function sortProviderNames(names) {
26404
+ const order = new Map(BUILTIN_PROVIDER_ORDER.map((name, index) => [name, index]));
26405
+ return [...names].sort((left, right) => {
26406
+ const leftIndex = order.get(left);
26407
+ const rightIndex = order.get(right);
26408
+ if (leftIndex !== void 0 && rightIndex !== void 0) return leftIndex - rightIndex;
26409
+ if (leftIndex !== void 0) return -1;
26410
+ if (rightIndex !== void 0) return 1;
26411
+ return left.localeCompare(right);
26412
+ });
26413
+ }
26414
+ function providerModelProfiles(providerName, providerConfig) {
26415
+ const explicit = Array.isArray(providerConfig.modelProfiles) ? providerConfig.modelProfiles.filter((profile) => typeof profile?.model === "string" && profile.model.trim()).map((profile) => ({
26416
+ ...profile,
26417
+ name: profile.name?.trim() || profile.displayName?.trim() || profile.model.trim(),
26418
+ displayName: profile.displayName?.trim() || profile.name?.trim() || profile.model.trim(),
26419
+ model: profile.model.trim()
26420
+ })) : [];
26421
+ const seenNames = /* @__PURE__ */ new Set();
26422
+ const seenDisplayNames = /* @__PURE__ */ new Set();
26423
+ const seenModels = /* @__PURE__ */ new Set();
26424
+ const out = [];
26425
+ for (const profile of explicit) {
26426
+ if (seenNames.has(profile.name)) {
26427
+ process.stderr.write(`[demian] warning: provider ${providerName} has duplicate modelProfile name "${profile.name}". Later entry ignored.
26428
+ `);
26429
+ continue;
26430
+ }
26431
+ if (profile.displayName && seenDisplayNames.has(profile.displayName)) {
26432
+ process.stderr.write(`[demian] warning: provider ${providerName} has duplicate modelProfile displayName "${profile.displayName}". Later entry ignored.
26433
+ `);
26434
+ continue;
26435
+ }
26436
+ seenNames.add(profile.name);
26437
+ if (profile.displayName) seenDisplayNames.add(profile.displayName);
26438
+ seenModels.add(profile.model);
26439
+ out.push(profile);
26440
+ }
26441
+ const legacy = [...typeof providerConfig.model === "string" && providerConfig.model.trim() ? [providerConfig.model.trim()] : [], ...(providerConfig.models ?? []).filter((model) => typeof model === "string" && model.trim())];
26442
+ for (const model of legacy) {
26443
+ const trimmed = model.trim();
26444
+ if (!trimmed || seenModels.has(trimmed)) continue;
26445
+ seenModels.add(trimmed);
26446
+ const name = trimmed;
26447
+ if (!seenNames.has(name)) seenNames.add(name);
26448
+ out.push({ name, displayName: name, model: trimmed });
26449
+ }
26450
+ const fallback = fallbackModelForProvider(providerName, providerConfig);
26451
+ if (out.length === 0 && fallback) out.push({ name: fallback, displayName: fallback, model: fallback });
26452
+ return out;
26453
+ }
26454
+ function providerModelOptions(providerName, providerConfig) {
26455
+ return providerModelProfiles(providerName, providerConfig).filter((profile) => !profile.hidden).map((profile) => profile.displayName ?? profile.name ?? profile.model);
26456
+ }
26457
+ function defaultModelForProvider(providerName, providerConfig) {
26458
+ return providerModelProfiles(providerName, providerConfig)[0]?.model ?? "";
26459
+ }
26460
+ function resolveConfiguredApiKey(providerConfig) {
26461
+ if (providerConfig.apiKey) return { value: providerConfig.apiKey };
26462
+ for (const envName of [providerConfig.apiKeyEnv, ...providerConfig.apiKeyEnvAliases ?? []]) {
26463
+ if (!envName) continue;
26464
+ const value = process.env[envName];
26465
+ if (value) return { value, envName };
26466
+ }
26467
+ return { envName: providerConfig.apiKeyEnv ?? providerConfig.apiKeyEnvAliases?.[0] };
26468
+ }
26469
+ function resolveProviderRuntimeConfig(config, selection) {
26470
+ const providerName = normalizeProviderAlias(selection.providerName);
26471
+ const providerConfig = resolveProviderConfig(config, providerName);
26472
+ const profiles = providerModelProfiles(providerName, providerConfig);
26473
+ let profile;
26474
+ if (selection.modelProfileName) profile = profiles.find((item) => item.name === selection.modelProfileName);
26475
+ if (!profile && selection.model) profile = profiles.find((item) => item.displayName === selection.model);
26476
+ if (!profile && selection.model) profile = profiles.find((item) => item.model === selection.model);
26477
+ if (!profile) profile = profiles[0];
26478
+ if (!profile) {
26479
+ const model = selection.model ?? defaultModelForProvider(providerName, providerConfig);
26480
+ if (!model) throw new Error(`Provider ${providerName} has no model. Run \`demian config models ${providerName} --refresh\` or add a model profile.`);
26481
+ return { providerName, providerConfig, model };
26482
+ }
26483
+ const merged = mergeModelProfile(providerConfig, profile);
26484
+ return {
26485
+ providerName,
26486
+ providerConfig: merged,
26487
+ model: profile.model,
26488
+ modelProfileName: profile.name
26489
+ };
26490
+ }
26491
+ function mergeModelProfile(providerConfig, profile) {
26492
+ const merged = {
26493
+ ...providerConfig,
26494
+ ...definedOnly({
26495
+ baseURL: profile.baseURL,
26496
+ apiKey: profile.apiKey,
26497
+ apiKeyEnv: profile.apiKeyEnv,
26498
+ apiKeyEnvAliases: profile.apiKeyEnvAliases,
26499
+ auth: profile.auth,
26500
+ headers: profile.headers,
26501
+ quirks: profile.quirks,
26502
+ maxTokens: profile.maxTokens
26503
+ }),
26504
+ model: profile.model
26505
+ };
26506
+ return merged;
26507
+ }
26508
+ function definedOnly(input2) {
26509
+ return Object.fromEntries(Object.entries(input2).filter(([, value]) => value !== void 0));
26510
+ }
26511
+ function fallbackModelForProvider(providerName, providerConfig) {
26512
+ if (typeof providerConfig.model === "string" && providerConfig.model.trim()) return providerConfig.model.trim();
26513
+ if (providerName === "openai") return "gpt-5.5";
26514
+ if (providerName === "anthropic") return CLAUDE_CODE_SONNET_MODEL;
26515
+ if (providerName === "codex" || providerConfig.type === "codex") return "gpt-5.5";
26516
+ if (providerName === "claudecode") return CLAUDE_CODE_SONNET_MODEL;
26517
+ if (providerName === "gemini") return "gemini-2.5-pro";
26518
+ if (providerName === "groq") return "openai/gpt-oss-120b";
26519
+ return void 0;
26274
26520
  }
26275
26521
  function migrateProviderConfig(provider, baseProvider) {
26276
26522
  if (isLegacyClaudeCodeShape(provider)) {
@@ -26352,12 +26598,14 @@ function resolveAgentMode(config, flagMode) {
26352
26598
  return "single-agent";
26353
26599
  }
26354
26600
  function resolveProviderConfig(config, providerName) {
26355
- const provider = config.providers[providerName];
26601
+ const normalized = normalizeProviderAlias(providerName);
26602
+ const provider = config.providers[normalized];
26356
26603
  if (!provider) throw new Error(`Unknown provider: ${providerName}`);
26357
26604
  return provider;
26358
26605
  }
26359
26606
  function resolveProvider(providerConfig, options2 = {}) {
26360
26607
  const model = options2.model ?? providerConfig.model;
26608
+ if (!model) throw new Error("Provider has no selected model. Choose a model or add a model profile.");
26361
26609
  if (providerConfig.type === "claudecode") {
26362
26610
  throw new Error("claudecode is an external agent runtime. Use resolveExecutionBackend().");
26363
26611
  }
@@ -26404,12 +26652,28 @@ function resolveProvider(providerConfig, options2 = {}) {
26404
26652
  };
26405
26653
  }
26406
26654
  if (providerConfig.type === "anthropic") {
26407
- const apiKey2 = providerConfig.apiKey ?? (providerConfig.apiKeyEnv ? process.env[providerConfig.apiKeyEnv] : void 0);
26655
+ const apiKey2 = resolveConfiguredApiKey(providerConfig).value;
26408
26656
  if (!apiKey2) {
26409
26657
  throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
26410
26658
  }
26411
26659
  return {
26412
26660
  provider: new AnthropicProvider({
26661
+ apiKey: apiKey2,
26662
+ baseURL: providerConfig.baseURL,
26663
+ defaultModel: model,
26664
+ onRetry: options2.onRetry
26665
+ }),
26666
+ model
26667
+ };
26668
+ }
26669
+ if (providerConfig.type === "ollama") {
26670
+ const apiKey2 = resolveConfiguredApiKey(providerConfig).value;
26671
+ if (!apiKey2) {
26672
+ throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
26673
+ }
26674
+ return {
26675
+ provider: new OllamaProvider({
26676
+ baseURL: providerConfig.baseURL,
26413
26677
  apiKey: apiKey2,
26414
26678
  defaultModel: model,
26415
26679
  onRetry: options2.onRetry
@@ -26417,7 +26681,7 @@ function resolveProvider(providerConfig, options2 = {}) {
26417
26681
  model
26418
26682
  };
26419
26683
  }
26420
- const apiKey = providerConfig.apiKey ?? (providerConfig.apiKeyEnv ? process.env[providerConfig.apiKeyEnv] : void 0);
26684
+ const apiKey = resolveConfiguredApiKey(providerConfig).value;
26421
26685
  if (!apiKey) {
26422
26686
  throw new Error(`Missing API key for provider. Set ${providerConfig.apiKeyEnv ?? "apiKey"} or configure apiKey.`);
26423
26687
  }
@@ -26436,6 +26700,7 @@ function resolveProvider(providerConfig, options2 = {}) {
26436
26700
  }
26437
26701
  function resolveExecutionBackend(providerConfig, options2 = {}) {
26438
26702
  const model = options2.model ?? providerConfig.model;
26703
+ if (!model) throw new Error("Provider has no selected model. Choose a model or add a model profile.");
26439
26704
  if (providerConfig.type === "claudecode") {
26440
26705
  const runtimeConfig = options2.config || providerConfig.permissionProfile ? resolveClaudeCodeRuntimeConfig(providerConfig, options2.config ?? defaultConfig, options2.agent) : providerConfig;
26441
26706
  return {
@@ -26479,7 +26744,8 @@ var CLAUDE_CODE_PERMISSION_PROFILE_KEYS = ["permissionMode", "defaultDecision",
26479
26744
 
26480
26745
  // src/transcript.ts
26481
26746
  import { mkdir as mkdir6, appendFile, writeFile as writeFile5 } from "node:fs/promises";
26482
- import path13 from "node:path";
26747
+ import os8 from "node:os";
26748
+ import path14 from "node:path";
26483
26749
  var TranscriptWriter = class {
26484
26750
  filePath;
26485
26751
  #enabled;
@@ -26487,8 +26753,8 @@ var TranscriptWriter = class {
26487
26753
  #queue = Promise.resolve();
26488
26754
  constructor(options2) {
26489
26755
  this.#enabled = options2.enabled !== false;
26490
- const dir = path13.join(options2.cwd, ".demian", "transcripts", options2.sessionId);
26491
- this.filePath = path13.join(dir, "session.jsonl");
26756
+ const dir = path14.join(options2.storageDir ?? defaultDemianStorageDir(), "transcripts", options2.sessionId);
26757
+ this.filePath = path14.join(dir, "session.jsonl");
26492
26758
  this.#ready = this.#enabled ? mkdir6(dir, { recursive: true }).then(() => writeFile5(this.filePath, "", { flag: "a" })) : Promise.resolve();
26493
26759
  }
26494
26760
  write(event) {
@@ -26504,12 +26770,15 @@ var TranscriptWriter = class {
26504
26770
  await this.#queue;
26505
26771
  }
26506
26772
  };
26773
+ function defaultDemianStorageDir() {
26774
+ return path14.join(os8.homedir(), ".demian");
26775
+ }
26507
26776
 
26508
26777
  // src/external-runtime/snapshot-diff.ts
26509
26778
  import { createHash as createHash2 } from "node:crypto";
26510
26779
  import { createReadStream } from "node:fs";
26511
26780
  import { readdir, readFile as readFile7, stat as stat3 } from "node:fs/promises";
26512
- import path14 from "node:path";
26781
+ import path15 from "node:path";
26513
26782
 
26514
26783
  // src/workspace/diff.ts
26515
26784
  var CONTEXT_LINES = 3;
@@ -26719,7 +26988,7 @@ async function diffWorkspaceSnapshot(before) {
26719
26988
  }
26720
26989
  async function walk(root, relativeDir, entries, options2) {
26721
26990
  if (entries.size >= options2.maxFiles) return;
26722
- const absoluteDir = path14.join(root, relativeDir);
26991
+ const absoluteDir = path15.join(root, relativeDir);
26723
26992
  let items;
26724
26993
  try {
26725
26994
  items = await readdir(absoluteDir, { withFileTypes: true });
@@ -26730,8 +26999,8 @@ async function walk(root, relativeDir, entries, options2) {
26730
26999
  if (entries.size >= options2.maxFiles) return;
26731
27000
  if (item.name.startsWith(".") && item.name !== ".env.example" && SKIP_DIRS.has(item.name)) continue;
26732
27001
  if (SKIP_DIRS.has(item.name)) continue;
26733
- const relativePath = normalizeRelativePath(path14.join(relativeDir, item.name));
26734
- const absolutePath = path14.join(root, relativePath);
27002
+ const relativePath = normalizeRelativePath(path15.join(relativeDir, item.name));
27003
+ const absolutePath = path15.join(root, relativePath);
26735
27004
  if (item.isDirectory()) {
26736
27005
  await walk(root, relativePath, entries, options2);
26737
27006
  continue;
@@ -26767,7 +27036,7 @@ function sha256File(filePath) {
26767
27036
  });
26768
27037
  }
26769
27038
  function normalizeRelativePath(value) {
26770
- return value.split(path14.sep).join("/");
27039
+ return value.split(path15.sep).join("/");
26771
27040
  }
26772
27041
 
26773
27042
  // src/external-runtime/session-runner.ts
@@ -27202,7 +27471,7 @@ function safeJson(value) {
27202
27471
  }
27203
27472
 
27204
27473
  // src/hooks/dispatcher.ts
27205
- import path16 from "node:path";
27474
+ import path17 from "node:path";
27206
27475
 
27207
27476
  // src/hooks/command.ts
27208
27477
  import { spawn as spawn4 } from "node:child_process";
@@ -27295,7 +27564,7 @@ var blockDangerousBashHook = {
27295
27564
  };
27296
27565
 
27297
27566
  // src/hooks/builtin/protect-env-files.ts
27298
- import path15 from "node:path";
27567
+ import path16 from "node:path";
27299
27568
  var protectEnvFilesHook = {
27300
27569
  name: "protect-env-files",
27301
27570
  event: "PreToolUse",
@@ -27303,7 +27572,7 @@ var protectEnvFilesHook = {
27303
27572
  run(ctx) {
27304
27573
  if (ctx.toolName !== "write_file" && ctx.toolName !== "edit_file") return { decision: "allow" };
27305
27574
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
27306
- const filePath = typeof input2.path === "string" ? path15.resolve(ctx.cwd, input2.path) : "";
27575
+ const filePath = typeof input2.path === "string" ? path16.resolve(ctx.cwd, input2.path) : "";
27307
27576
  if (filePath && isEnvFile(filePath)) {
27308
27577
  return {
27309
27578
  decision: "block",
@@ -27340,7 +27609,7 @@ var maskSecretsHook = {
27340
27609
  };
27341
27610
 
27342
27611
  // src/hooks/builtin/inject-env-info.ts
27343
- import os8 from "node:os";
27612
+ import os9 from "node:os";
27344
27613
  var injectEnvInfoHook = {
27345
27614
  name: "inject-env-info",
27346
27615
  event: "SessionStart",
@@ -27349,7 +27618,7 @@ var injectEnvInfoHook = {
27349
27618
  decision: "allow",
27350
27619
  patch: {
27351
27620
  systemNote: [
27352
- `Environment: ${os8.type()} ${os8.release()} (${os8.platform()}/${os8.arch()})`,
27621
+ `Environment: ${os9.type()} ${os9.release()} (${os9.platform()}/${os9.arch()})`,
27353
27622
  `cwd: ${ctx.cwd}`,
27354
27623
  `provider: ${ctx.provider ?? "unknown"}`,
27355
27624
  `model: ${ctx.model ?? "unknown"}`,
@@ -27418,14 +27687,14 @@ function matchesHook(match, ctx) {
27418
27687
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
27419
27688
  const filePath = typeof input2.path === "string" ? input2.path : typeof input2.workdir === "string" ? input2.workdir : void 0;
27420
27689
  if (!filePath) return false;
27421
- if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path16.resolve(ctx.cwd, filePath)))) return false;
27690
+ if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path17.resolve(ctx.cwd, filePath)))) return false;
27422
27691
  }
27423
27692
  return true;
27424
27693
  }
27425
27694
 
27426
27695
  // src/multimodal.ts
27427
27696
  import { readFile as readFile8, stat as stat4 } from "node:fs/promises";
27428
- import path17 from "node:path";
27697
+ import path18 from "node:path";
27429
27698
  var DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
27430
27699
  async function buildUserContent(prompt, images = [], options2) {
27431
27700
  if (images.length === 0) return prompt;
@@ -27453,7 +27722,7 @@ async function imageToUrl(input2, options2) {
27453
27722
  return `data:${mime};base64,${bytes.toString("base64")}`;
27454
27723
  }
27455
27724
  function mimeFromPath(filePath) {
27456
- switch (path17.extname(filePath).toLowerCase()) {
27725
+ switch (path18.extname(filePath).toLowerCase()) {
27457
27726
  case ".jpg":
27458
27727
  case ".jpeg":
27459
27728
  return "image/jpeg";
@@ -27464,15 +27733,15 @@ function mimeFromPath(filePath) {
27464
27733
  case ".webp":
27465
27734
  return "image/webp";
27466
27735
  default:
27467
- throw new Error(`Unsupported image extension: ${path17.extname(filePath) || "(none)"}`);
27736
+ throw new Error(`Unsupported image extension: ${path18.extname(filePath) || "(none)"}`);
27468
27737
  }
27469
27738
  }
27470
27739
 
27471
27740
  // src/permissions/persistent-grants.ts
27472
27741
  import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
27473
27742
  import fs8 from "node:fs";
27474
- import os9 from "node:os";
27475
- import path18 from "node:path";
27743
+ import os10 from "node:os";
27744
+ import path19 from "node:path";
27476
27745
  var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
27477
27746
  var PersistentGrantStore = class {
27478
27747
  filePath;
@@ -27510,15 +27779,15 @@ var PersistentGrantStore = class {
27510
27779
  };
27511
27780
  }
27512
27781
  async #write(file) {
27513
- await mkdir7(path18.dirname(this.filePath), { recursive: true });
27782
+ await mkdir7(path19.dirname(this.filePath), { recursive: true });
27514
27783
  await writeFile6(this.filePath, `${JSON.stringify(file, null, 2)}
27515
27784
  `, { mode: 384 });
27516
27785
  }
27517
27786
  };
27518
27787
  function resolveGrantPath(cwd, config) {
27519
- if (config.path) return path18.resolve(cwd, config.path);
27520
- if ((config.scope ?? "project") === "user") return path18.join(os9.homedir(), ".demian", "grants.json");
27521
- return path18.join(cwd, ".demian", "grants.json");
27788
+ if (config.path) return path19.resolve(cwd, config.path);
27789
+ if ((config.scope ?? "project") === "user") return path19.join(os10.homedir(), ".demian", "grants.json");
27790
+ return path19.join(cwd, ".demian", "grants.json");
27522
27791
  }
27523
27792
  function isGrantRecord(value) {
27524
27793
  if (!value || typeof value !== "object") return false;
@@ -27952,16 +28221,16 @@ function fail(content, metadata) {
27952
28221
 
27953
28222
  // src/tools/output.ts
27954
28223
  import { mkdir as mkdir8, writeFile as writeFile7 } from "node:fs/promises";
27955
- import path19 from "node:path";
28224
+ import path20 from "node:path";
27956
28225
  var DEFAULT_CAP_BYTES = 32 * 1024;
27957
28226
  var HALF_PREVIEW_BYTES = 16 * 1024;
27958
28227
  async function capToolOutput(result, options2) {
27959
28228
  const capBytes = options2.capBytes ?? DEFAULT_CAP_BYTES;
27960
28229
  const bytes = Buffer.byteLength(result.content, "utf8");
27961
28230
  if (bytes <= capBytes) return result;
27962
- const dir = path19.join(options2.cwd, ".demian", "tmp");
28231
+ const dir = path20.join(options2.cwd, ".demian", "tmp");
27963
28232
  await mkdir8(dir, { recursive: true });
27964
- const outputPath = path19.join(dir, `output-${safeCallId(options2.callId)}.txt`);
28233
+ const outputPath = path20.join(dir, `output-${safeCallId(options2.callId)}.txt`);
27965
28234
  await writeFile7(outputPath, result.content, "utf8");
27966
28235
  return {
27967
28236
  ...result,
@@ -27969,7 +28238,7 @@ async function capToolOutput(result, options2) {
27969
28238
  sliceUtf8(result.content, 0, HALF_PREVIEW_BYTES),
27970
28239
  `
27971
28240
 
27972
- [Full output saved to ${path19.relative(options2.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
28241
+ [Full output saved to ${path20.relative(options2.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
27973
28242
 
27974
28243
  `,
27975
28244
  sliceUtf8(result.content, Math.max(0, bytes - HALF_PREVIEW_BYTES), bytes)
@@ -27995,7 +28264,7 @@ function sliceUtf8(text, startByte, endByte) {
27995
28264
 
27996
28265
  // src/tools/read-file.ts
27997
28266
  import { open as open2, stat as stat5 } from "node:fs/promises";
27998
- import path20 from "node:path";
28267
+ import path21 from "node:path";
27999
28268
 
28000
28269
  // src/tools/validation.ts
28001
28270
  function assertObject(input2, toolName) {
@@ -28079,7 +28348,7 @@ var readFileTool = {
28079
28348
  const truncated = start + sliced.length < lines.length;
28080
28349
  return ok(content + (truncated ? `
28081
28350
  ... (${lines.length - start - sliced.length} more lines)` : ""), {
28082
- path: path20.relative(ctx.cwd, filePath),
28351
+ path: path21.relative(ctx.cwd, filePath),
28083
28352
  lines: sliced.length,
28084
28353
  totalLines: lines.length,
28085
28354
  truncated
@@ -28118,7 +28387,7 @@ function looksBinary(buffer) {
28118
28387
 
28119
28388
  // src/tools/write-file.ts
28120
28389
  import { mkdir as mkdir9, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
28121
- import path21 from "node:path";
28390
+ import path22 from "node:path";
28122
28391
  var writeFileTool = {
28123
28392
  name: "write_file",
28124
28393
  description: "Create or replace a text file inside the workspace.",
@@ -28136,7 +28405,7 @@ var writeFileTool = {
28136
28405
  const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
28137
28406
  const content = stringField(object2, "content", { allowEmpty: true });
28138
28407
  const before = await readOptionalTextFile(filePath);
28139
- await mkdir9(path21.dirname(filePath), { recursive: true });
28408
+ await mkdir9(path22.dirname(filePath), { recursive: true });
28140
28409
  await writeFile8(filePath, content, "utf8");
28141
28410
  const relative = relativeToCwd(ctx.cwd, filePath);
28142
28411
  const diff = createTextDiff(before, content, relative);
@@ -28212,7 +28481,7 @@ function countMatches(text, needle) {
28212
28481
 
28213
28482
  // src/tools/bash.ts
28214
28483
  import { spawn as spawn5 } from "node:child_process";
28215
- import path23 from "node:path";
28484
+ import path24 from "node:path";
28216
28485
 
28217
28486
  // src/sandbox/env-only.ts
28218
28487
  function buildEnvOnlyLaunch(command, config) {
@@ -28272,7 +28541,7 @@ function bwrapPath() {
28272
28541
 
28273
28542
  // src/sandbox/macos.ts
28274
28543
  import { existsSync as existsSync3 } from "node:fs";
28275
- import path22 from "node:path";
28544
+ import path23 from "node:path";
28276
28545
  function canUseMacOSSandbox() {
28277
28546
  return process.platform === "darwin" && existsSync3("/usr/bin/sandbox-exec");
28278
28547
  }
@@ -28298,7 +28567,7 @@ function macosProfile(cwd, config) {
28298
28567
  }
28299
28568
  if (mode === "workspace-write") {
28300
28569
  lines.push("(deny file-write*)");
28301
- lines.push(`(allow file-write* (subpath "${escapeProfilePath(path22.resolve(cwd))}"))`);
28570
+ lines.push(`(allow file-write* (subpath "${escapeProfilePath(path23.resolve(cwd))}"))`);
28302
28571
  lines.push('(allow file-write* (subpath "/tmp"))');
28303
28572
  lines.push('(allow file-write* (subpath "/private/tmp"))');
28304
28573
  lines.push('(allow file-write* (subpath "/private/var/folders"))');
@@ -28360,7 +28629,7 @@ ${result.stderr}` : ""
28360
28629
  ].filter(Boolean).join("\n");
28361
28630
  return ok(content, {
28362
28631
  command,
28363
- workdir: path23.relative(ctx.cwd, workdir) || ".",
28632
+ workdir: path24.relative(ctx.cwd, workdir) || ".",
28364
28633
  exitCode: result.exitCode,
28365
28634
  signal: result.signal,
28366
28635
  timedOut: result.timedOut,
@@ -28413,7 +28682,7 @@ function runCommand(command, cwd, timeoutMs, signal, sandbox = { mode: "workspac
28413
28682
  // src/tools/grep.ts
28414
28683
  import { spawn as spawn6 } from "node:child_process";
28415
28684
  import { readdir as readdir2, readFile as readFile12, stat as stat6 } from "node:fs/promises";
28416
- import path24 from "node:path";
28685
+ import path25 from "node:path";
28417
28686
  var MAX_MATCHES = 200;
28418
28687
  var grepTool = {
28419
28688
  name: "grep",
@@ -28488,7 +28757,7 @@ function runRg(cwd, base, pattern, glob, signal) {
28488
28757
  }
28489
28758
  function rewriteRgPath(cwd, line) {
28490
28759
  const [file, rest] = splitFirst(line, ":");
28491
- if (!path24.isAbsolute(file)) return line;
28760
+ if (!path25.isAbsolute(file)) return line;
28492
28761
  return `${relativeToCwd(cwd, file)}:${rest}`;
28493
28762
  }
28494
28763
  function splitFirst(value, delimiter) {
@@ -28505,7 +28774,7 @@ async function fallbackSearch(cwd, base, pattern, glob) {
28505
28774
  if (isIgnoredPath(relative)) return;
28506
28775
  const info = await stat6(item);
28507
28776
  if (info.isDirectory()) {
28508
- for (const entry of await readdir2(item)) await walk2(path24.join(item, entry));
28777
+ for (const entry of await readdir2(item)) await walk2(path25.join(item, entry));
28509
28778
  return;
28510
28779
  }
28511
28780
  if (glob && !matchGlob(glob, relative)) return;
@@ -28524,7 +28793,7 @@ async function fallbackSearch(cwd, base, pattern, glob) {
28524
28793
 
28525
28794
  // src/tools/glob.ts
28526
28795
  import { readdir as readdir3, stat as stat7 } from "node:fs/promises";
28527
- import path25 from "node:path";
28796
+ import path26 from "node:path";
28528
28797
  var MAX_PATHS = 1e3;
28529
28798
  var globTool = {
28530
28799
  name: "glob",
@@ -28556,7 +28825,7 @@ async function collectPaths(cwd, root, pattern) {
28556
28825
  async function walk2(dir) {
28557
28826
  const entries = await readdir3(dir, { withFileTypes: true });
28558
28827
  for (const entry of entries) {
28559
- const absolute = path25.join(dir, entry.name);
28828
+ const absolute = path26.join(dir, entry.name);
28560
28829
  const relative = relativeToCwd(cwd, absolute);
28561
28830
  if (isIgnoredPath(relative)) continue;
28562
28831
  const info = await stat7(absolute);
@@ -29559,13 +29828,13 @@ async function runExecutionSession(options2) {
29559
29828
 
29560
29829
  // src/goals/lock.ts
29561
29830
  import { mkdir as mkdir11, open as open3, rm as rm2 } from "node:fs/promises";
29562
- import path27 from "node:path";
29831
+ import path28 from "node:path";
29563
29832
 
29564
29833
  // src/goals/storage.ts
29565
29834
  import { createHash as createHash3 } from "node:crypto";
29566
29835
  import { mkdir as mkdir10, readFile as readFile13, rename as rename4, writeFile as writeFile10 } from "node:fs/promises";
29567
29836
  import fs9 from "node:fs";
29568
- import path26 from "node:path";
29837
+ import path27 from "node:path";
29569
29838
  var GoalStore = class {
29570
29839
  cwd;
29571
29840
  dir;
@@ -29576,8 +29845,8 @@ var GoalStore = class {
29576
29845
  this.cwd = cwd;
29577
29846
  this.scope = normalizeGoalStoreScope(options2.scope);
29578
29847
  this.dir = goalStoreDir(cwd, this.scope);
29579
- this.activePath = path26.join(this.dir, "active.json");
29580
- this.archiveDir = path26.join(this.dir, "archive");
29848
+ this.activePath = path27.join(this.dir, "active.json");
29849
+ this.archiveDir = path27.join(this.dir, "archive");
29581
29850
  }
29582
29851
  async loadActive() {
29583
29852
  if (!fs9.existsSync(this.activePath)) return void 0;
@@ -29591,7 +29860,7 @@ var GoalStore = class {
29591
29860
  }
29592
29861
  async saveActive(state) {
29593
29862
  await mkdir10(this.dir, { recursive: true });
29594
- const tmp = path26.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
29863
+ const tmp = path27.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
29595
29864
  await writeFile10(tmp, `${JSON.stringify(state, null, 2)}
29596
29865
  `, "utf8");
29597
29866
  await rename4(tmp, this.activePath);
@@ -29605,21 +29874,21 @@ var GoalStore = class {
29605
29874
  status,
29606
29875
  updatedAt: Date.now()
29607
29876
  };
29608
- await writeFile10(path26.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
29877
+ await writeFile10(path27.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
29609
29878
  `, "utf8");
29610
29879
  await fs9.promises.rm(this.activePath, { force: true });
29611
29880
  return archived;
29612
29881
  }
29613
29882
  async backupCorrupted(text) {
29614
29883
  await mkdir10(this.dir, { recursive: true });
29615
- const backup = path26.join(this.dir, `active.corrupt.${Date.now()}.json`);
29884
+ const backup = path27.join(this.dir, `active.corrupt.${Date.now()}.json`);
29616
29885
  await writeFile10(backup, text, "utf8");
29617
29886
  await fs9.promises.rm(this.activePath, { force: true });
29618
29887
  }
29619
29888
  };
29620
29889
  function goalStoreDir(cwd, scope) {
29621
29890
  const safeScope = normalizeGoalStoreScope(scope);
29622
- return safeScope ? path26.join(cwd, ".demian", "goals", "sessions", safeScope) : path26.join(cwd, ".demian", "goals");
29891
+ return safeScope ? path27.join(cwd, ".demian", "goals", "sessions", safeScope) : path27.join(cwd, ".demian", "goals");
29623
29892
  }
29624
29893
  function normalizeGoalStoreScope(scope) {
29625
29894
  const trimmed = scope?.trim();
@@ -29633,10 +29902,10 @@ function normalizeGoalStoreScope(scope) {
29633
29902
  var GoalLock = class {
29634
29903
  path;
29635
29904
  constructor(cwd, scope) {
29636
- this.path = path27.join(goalStoreDir(cwd, scope), "active.lock");
29905
+ this.path = path28.join(goalStoreDir(cwd, scope), "active.lock");
29637
29906
  }
29638
29907
  async acquire() {
29639
- await mkdir11(path27.dirname(this.path), { recursive: true });
29908
+ await mkdir11(path28.dirname(this.path), { recursive: true });
29640
29909
  const handle = await open3(this.path, "wx").catch((error) => {
29641
29910
  const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
29642
29911
  if (code === "EEXIST") throw new Error("Another goal is already active in this workspace.");
@@ -30224,8 +30493,8 @@ function sha256(value) {
30224
30493
 
30225
30494
  // src/root-session.ts
30226
30495
  import { cp, mkdtemp, rm as rm3 } from "node:fs/promises";
30227
- import os10 from "node:os";
30228
- import path28 from "node:path";
30496
+ import os11 from "node:os";
30497
+ import path29 from "node:path";
30229
30498
 
30230
30499
  // src/tools/cowork.ts
30231
30500
  function createCoworkTool(options2) {
@@ -31048,8 +31317,8 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
31048
31317
  }
31049
31318
  }
31050
31319
  async createCoworkIsolatedWorkspace(groupId, memberId) {
31051
- const root = await mkdtemp(path28.join(os10.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
31052
- const cwd = path28.join(root, "workspace");
31320
+ const root = await mkdtemp(path29.join(os11.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
31321
+ const cwd = path29.join(root, "workspace");
31053
31322
  await cp(this.#options.cwd, cwd, {
31054
31323
  recursive: true,
31055
31324
  filter: (source) => shouldCopyIntoCoworkWorkspace(this.#options.cwd, source)
@@ -31197,7 +31466,7 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
31197
31466
  const denyTools = ["write_file", "edit_file", "bash", "claudecode.Edit", "claudecode.Write", "claudecode.MultiEdit", "claudecode.Bash"];
31198
31467
  return this.withPermissionPreset({
31199
31468
  ...agent,
31200
- provider: agent.provider?.profile === "claudecode-subagent" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
31469
+ provider: agent.provider?.profile === "claudecode" && agent.name === "claudecode-explorer" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
31201
31470
  tools: agent.tools.filter((tool) => readTools.has(tool)),
31202
31471
  permissions: [
31203
31472
  ...agent.permissions.filter((rule) => !denyTools.includes(rule.tool)),
@@ -31244,13 +31513,14 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
31244
31513
  return lines.join("\n");
31245
31514
  }
31246
31515
  resolveInvocationBackend(profileName, modelOverride, agent) {
31247
- const providerConfig = resolveProviderConfig(this.#options.config, profileName);
31516
+ const runtime = resolveProviderRuntimeConfig(this.#options.config, { providerName: profileName, model: modelOverride });
31517
+ const providerConfig = runtime.providerConfig;
31248
31518
  if (this.#options.resolveExecutionBackend) return this.#options.resolveExecutionBackend(profileName, providerConfig, modelOverride);
31249
31519
  if (this.#options.resolveProvider) {
31250
- const resolved = this.#options.resolveProvider(profileName, providerConfig, modelOverride);
31520
+ const resolved = this.#options.resolveProvider(profileName, providerConfig, runtime.model);
31251
31521
  return { kind: "provider", ...resolved };
31252
31522
  }
31253
- return resolveExecutionBackend(providerConfig, { model: modelOverride, config: this.#options.config, agent });
31523
+ return resolveExecutionBackend(providerConfig, { model: runtime.model, config: this.#options.config, agent });
31254
31524
  }
31255
31525
  loadAgentSession(agent, providerProfile, model, sessionScope, cwd = this.#options.cwd) {
31256
31526
  const key = `${this.id}:${sessionScope ?? "delegate"}:${agent.name}:${providerProfile}:${model}:${cwd}`;
@@ -31352,17 +31622,17 @@ function findDependencyCycle(group) {
31352
31622
  const byId = new Map(group.map((member) => [member.memberId, member]));
31353
31623
  const visiting = /* @__PURE__ */ new Set();
31354
31624
  const visited = /* @__PURE__ */ new Set();
31355
- const path32 = [];
31625
+ const path36 = [];
31356
31626
  const visit = (id) => {
31357
- if (visiting.has(id)) return [...path32.slice(path32.indexOf(id)), id];
31627
+ if (visiting.has(id)) return [...path36.slice(path36.indexOf(id)), id];
31358
31628
  if (visited.has(id)) return void 0;
31359
31629
  visiting.add(id);
31360
- path32.push(id);
31630
+ path36.push(id);
31361
31631
  for (const dep of byId.get(id)?.dependsOn ?? []) {
31362
31632
  const cycle = visit(dep);
31363
31633
  if (cycle) return cycle;
31364
31634
  }
31365
- path32.pop();
31635
+ path36.pop();
31366
31636
  visiting.delete(id);
31367
31637
  visited.add(id);
31368
31638
  return void 0;
@@ -31403,7 +31673,7 @@ function scopeStaticPrefix(pattern) {
31403
31673
  return prefix.replace(/\/+$/, "").split("/").filter(Boolean).join("/") || ".";
31404
31674
  }
31405
31675
  function shouldCopyIntoCoworkWorkspace(root, source) {
31406
- const relative = path28.relative(root, source).split(path28.sep).join("/");
31676
+ const relative = path29.relative(root, source).split(path29.sep).join("/");
31407
31677
  if (!relative) return true;
31408
31678
  const parts = relative.split("/");
31409
31679
  return !parts.includes(".git") && !parts.includes("node_modules") && !parts.includes(".demian");
@@ -31617,11 +31887,11 @@ function finitePositiveInteger(value) {
31617
31887
  // src/ui/preferences.ts
31618
31888
  import { mkdir as mkdir12, readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
31619
31889
  import fs10 from "node:fs";
31620
- import path29 from "node:path";
31890
+ import path30 from "node:path";
31621
31891
  var UiPreferenceStore = class {
31622
31892
  filePath;
31623
31893
  constructor(cwd, filePath) {
31624
- this.filePath = filePath ? path29.resolve(cwd, filePath) : path29.join(cwd, ".demian", "preferences.json");
31894
+ this.filePath = filePath ? path30.resolve(cwd, filePath) : path30.join(cwd, ".demian", "preferences.json");
31625
31895
  }
31626
31896
  async load() {
31627
31897
  if (!fs10.existsSync(this.filePath)) return void 0;
@@ -31639,7 +31909,7 @@ var UiPreferenceStore = class {
31639
31909
  model: selection.model,
31640
31910
  updatedAt: Date.now()
31641
31911
  };
31642
- await mkdir12(path29.dirname(this.filePath), { recursive: true });
31912
+ await mkdir12(path30.dirname(this.filePath), { recursive: true });
31643
31913
  await writeFile11(this.filePath, `${JSON.stringify(file, null, 2)}
31644
31914
  `, { mode: 384 });
31645
31915
  }
@@ -31659,37 +31929,528 @@ function preferenceKey(selection) {
31659
31929
  return `${selection.providerName}\0${selection.model}`;
31660
31930
  }
31661
31931
 
31662
- // src/ui/settings.ts
31663
- function providerOptions(config) {
31664
- return Object.entries(config.providers).filter(([, provider]) => !provider.hidden && !(provider.type === "claudecode" && provider.runtime === "cli")).map(([name, provider]) => ({
31665
- name,
31666
- model: provider.model,
31667
- models: provider.models?.length ? [...new Set([provider.model, ...provider.models].filter((item) => typeof item === "string" && item.trim()))] : [provider.model],
31668
- type: provider.type,
31669
- runtime: provider.type === "claudecode" ? provider.runtime : void 0,
31670
- permissionMode: provider.type === "claudecode" ? provider.permissionMode : void 0,
31671
- ga: provider.type === "claudecode" ? claudeCodeGaEnabled(provider) : void 0
31932
+ // src/models/catalog.ts
31933
+ import crypto6 from "node:crypto";
31934
+ import { mkdir as mkdir13, readFile as readFile15, rename as rename5, writeFile as writeFile12 } from "node:fs/promises";
31935
+ import os12 from "node:os";
31936
+ import path31 from "node:path";
31937
+ var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
31938
+ var PING_TIMEOUT_MS = 1500;
31939
+ var DEFAULT_CODEX_CLIENT_VERSION = "0.0.0";
31940
+ var dynamicImport3 = new Function("specifier", "return import(specifier)");
31941
+ var inflight = /* @__PURE__ */ new Map();
31942
+ var pingCache = /* @__PURE__ */ new Map();
31943
+ var PING_CACHE_TTL_MS = 30 * 1e3;
31944
+ var packageVersionPromise;
31945
+ async function listProviderModelCatalog(providerName, providerConfig, options2 = {}) {
31946
+ const key = `${catalogCacheKey(providerName, providerConfig)}:${options2.refresh ? "refresh" : "default"}`;
31947
+ const existing = inflight.get(key);
31948
+ if (existing) return existing;
31949
+ const promise = doListProviderModelCatalog(providerName, providerConfig, options2).finally(() => inflight.delete(key));
31950
+ inflight.set(key, promise);
31951
+ return promise;
31952
+ }
31953
+ async function doListProviderModelCatalog(providerName, providerConfig, options2) {
31954
+ const staticModels = staticModelEntries(providerName, providerConfig);
31955
+ const catalog = providerConfig.catalog;
31956
+ if (!catalog || catalog.type === "static") {
31957
+ return { status: staticModels.length ? "ready" : "unavailable", providerName, models: staticModels, source: "config" };
31958
+ }
31959
+ const missingAuthEnv = missingCatalogAuth(providerConfig);
31960
+ if (missingAuthEnv) {
31961
+ return {
31962
+ status: "missing-auth",
31963
+ providerName,
31964
+ models: staticModels,
31965
+ source: staticModels.length ? "config" : "fallback",
31966
+ message: `missing-auth:${missingAuthEnv}`
31967
+ };
31968
+ }
31969
+ const cacheKey = catalogCacheKey(providerName, providerConfig);
31970
+ const ttlMs = catalog.refreshTtlMs ?? DEFAULT_CACHE_TTL_MS;
31971
+ const cached = await readCatalogCache(cacheKey, ttlMs, options2.refresh !== true);
31972
+ if (cached && !options2.refresh) return { ...cached, models: mergeCatalogWithStatic(staticModels, cached.models) };
31973
+ if (catalog.endpoint) {
31974
+ const reachable = await pingEndpoint(catalog.endpoint, options2.fetch);
31975
+ if (!reachable) {
31976
+ const stale = await readCatalogCache(cacheKey, Number.POSITIVE_INFINITY, true);
31977
+ if (stale) return { ...stale, models: mergeCatalogWithStatic(staticModels, stale.models), message: `endpoint ${catalog.endpoint} unreachable; using cached models` };
31978
+ if (staticModels.length) return { status: "ready", providerName, models: staticModels, source: "config", message: `endpoint ${catalog.endpoint} unreachable; using configured profiles` };
31979
+ return { status: "unavailable", providerName, models: [], source: "fallback", message: `endpoint ${catalog.endpoint} unreachable` };
31980
+ }
31981
+ }
31982
+ try {
31983
+ const remote = await fetchCatalog(providerName, providerConfig, options2);
31984
+ const merged = mergeCatalogWithStatic(staticModels, remote.models);
31985
+ const result = { ...remote, models: merged };
31986
+ if (result.status === "ready") await writeCatalogCache(cacheKey, result);
31987
+ return result;
31988
+ } catch (error) {
31989
+ const authMissing = isMissingAuthError(error);
31990
+ const stale = await readCatalogCache(cacheKey, Number.POSITIVE_INFINITY, true);
31991
+ if (stale) return { ...stale, status: authMissing ? "missing-auth" : stale.status, models: mergeCatalogWithStatic(staticModels, stale.models), message: errorMessage2(error) };
31992
+ if (staticModels.length) return { status: authMissing ? "missing-auth" : "ready", providerName, models: staticModels, source: "config", message: errorMessage2(error) };
31993
+ return { status: authMissing ? "missing-auth" : "unavailable", providerName, models: [], source: "fallback", message: errorMessage2(error) };
31994
+ }
31995
+ }
31996
+ async function pingEndpoint(endpoint, fetcher) {
31997
+ const now2 = Date.now();
31998
+ const cached = pingCache.get(endpoint);
31999
+ if (cached && cached.expiresAt > now2) return cached.ok;
32000
+ const ok2 = await tryPing(endpoint, fetcher);
32001
+ pingCache.set(endpoint, { ok: ok2, expiresAt: now2 + PING_CACHE_TTL_MS });
32002
+ return ok2;
32003
+ }
32004
+ async function tryPing(endpoint, fetcher) {
32005
+ const fn = fetcher ?? fetch;
32006
+ try {
32007
+ const response = await fn(endpoint, { method: "HEAD", signal: AbortSignal.timeout(PING_TIMEOUT_MS) });
32008
+ if (response.status < 500) return true;
32009
+ } catch {
32010
+ }
32011
+ try {
32012
+ const response = await fn(endpoint, { method: "GET", signal: AbortSignal.timeout(PING_TIMEOUT_MS), headers: { range: "bytes=0-0" } });
32013
+ return response.status < 500;
32014
+ } catch {
32015
+ return false;
32016
+ }
32017
+ }
32018
+ function invalidateCatalogPingCache(endpoint) {
32019
+ if (endpoint) pingCache.delete(endpoint);
32020
+ else pingCache.clear();
32021
+ }
32022
+ async function fetchCatalog(providerName, providerConfig, options2) {
32023
+ if (providerConfig.type === "codex" || providerConfig.catalog?.type === "codex-oauth-models") return fetchCodexCatalog(providerName, providerConfig, options2);
32024
+ if (providerConfig.type === "claudecode" || providerConfig.catalog?.type === "claudecode-supported-models") return fetchClaudeCodeCatalog(providerName, providerConfig, options2);
32025
+ if (providerConfig.catalog?.type === "anthropic-models") return fetchAnthropicCatalog(providerName, providerConfig, options2);
32026
+ if (providerConfig.catalog?.type === "ollama-tags") return fetchOllamaTagsCatalog(providerName, providerConfig, options2);
32027
+ return fetchOpenAIStyleCatalog(providerName, providerConfig, options2);
32028
+ }
32029
+ async function fetchOpenAIStyleCatalog(providerName, providerConfig, options2) {
32030
+ if (providerConfig.type !== "openai-compatible") throw new Error(`Provider ${providerName} is not OpenAI-compatible.`);
32031
+ const apiKey = resolveConfiguredApiKey(providerConfig);
32032
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
32033
+ const endpoint = providerConfig.catalog?.endpoint ?? `${providerConfig.baseURL.replace(/\/+$/, "")}/models`;
32034
+ const response = await fetchJson2(endpoint, {
32035
+ fetcher: options2.fetch,
32036
+ timeoutMs: options2.timeoutMs,
32037
+ headers: {
32038
+ ...authHeadersForOpenAICompatible(providerConfig, apiKey.value),
32039
+ ...providerConfig.headers ?? {}
32040
+ }
32041
+ });
32042
+ return {
32043
+ status: "ready",
32044
+ providerName,
32045
+ models: normalizeOpenAIModels(response).map((entry) => ({ ...entry, source: "api" })),
32046
+ source: "api"
32047
+ };
32048
+ }
32049
+ async function fetchAnthropicCatalog(providerName, providerConfig, options2) {
32050
+ if (providerConfig.type !== "anthropic") throw new Error(`Provider ${providerName} is not Anthropic.`);
32051
+ const apiKey = resolveConfiguredApiKey(providerConfig);
32052
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
32053
+ const base = providerConfig.baseURL ?? "https://api.anthropic.com/v1";
32054
+ const endpoint = providerConfig.catalog?.endpoint ?? `${base.replace(/\/+$/, "")}/models`;
32055
+ const models = [];
32056
+ let afterId;
32057
+ for (; ; ) {
32058
+ const url = new URL(endpoint);
32059
+ if (afterId) url.searchParams.set("after_id", afterId);
32060
+ url.searchParams.set("limit", "100");
32061
+ const response = await fetchJson2(url.toString(), {
32062
+ fetcher: options2.fetch,
32063
+ timeoutMs: options2.timeoutMs,
32064
+ headers: {
32065
+ "x-api-key": apiKey.value,
32066
+ "anthropic-version": "2023-06-01"
32067
+ }
32068
+ });
32069
+ const page = normalizeAnthropicModels(response);
32070
+ models.push(...page);
32071
+ if (!response.has_more || !response.last_id) break;
32072
+ afterId = response.last_id;
32073
+ }
32074
+ return { status: "ready", providerName, models, source: "api" };
32075
+ }
32076
+ async function fetchOllamaTagsCatalog(providerName, providerConfig, options2) {
32077
+ if (providerConfig.type !== "ollama") throw new Error(`Provider ${providerName} is not Ollama native.`);
32078
+ const apiKey = resolveConfiguredApiKey(providerConfig);
32079
+ if (!apiKey.value) throw missingAuth(providerConfig.apiKeyEnv ?? "apiKey");
32080
+ const endpoint = providerConfig.catalog?.endpoint ?? `${providerConfig.baseURL.replace(/\/+$/, "")}/tags`;
32081
+ const response = await fetchJson2(endpoint, {
32082
+ fetcher: options2.fetch,
32083
+ timeoutMs: options2.timeoutMs,
32084
+ headers: { authorization: `Bearer ${apiKey.value}` }
32085
+ });
32086
+ return {
32087
+ status: "ready",
32088
+ providerName,
32089
+ models: normalizeOllamaTags(response).map((entry) => ({ ...entry, source: "api" })),
32090
+ source: "api"
32091
+ };
32092
+ }
32093
+ async function fetchCodexCatalog(providerName, providerConfig, options2) {
32094
+ if (providerConfig.type !== "codex") throw new Error(`Provider ${providerName} is not Codex.`);
32095
+ const fetcher = options2.fetch ?? fetch;
32096
+ const baseURL = (providerConfig.baseURL ?? "https://chatgpt.com/backend-api/codex").replace(/\/+$/, "");
32097
+ const installationId = await loadOrCreateInstallationId(resolveCodexHome(providerConfig.codexHome));
32098
+ const auth = getSharedCodexAuthStore({
32099
+ codexHome: providerConfig.codexHome,
32100
+ authStore: providerConfig.authStore,
32101
+ allowApiKeyFallback: providerConfig.allowApiKeyFallback,
32102
+ proactiveRefreshMinutes: providerConfig.refresh?.proactiveRefreshMinutes,
32103
+ refreshCache: providerConfig.refresh?.cache,
32104
+ fetch: fetcher
32105
+ });
32106
+ const headers = await auth.requestHeaders(installationId);
32107
+ const url = new URL(`${baseURL}/models`);
32108
+ url.searchParams.set("client_version", await codexClientVersion(options2.clientVersion));
32109
+ const response = await fetchJson2(url.toString(), {
32110
+ fetcher,
32111
+ timeoutMs: options2.timeoutMs,
32112
+ headers
32113
+ });
32114
+ return {
32115
+ status: "ready",
32116
+ providerName,
32117
+ models: normalizeCodexModels(response).map((entry) => ({ ...entry, source: "oauth" })),
32118
+ source: "oauth"
32119
+ };
32120
+ }
32121
+ async function fetchClaudeCodeCatalog(providerName, providerConfig, options2) {
32122
+ if (providerConfig.type !== "claudecode") throw new Error(`Provider ${providerName} is not Claude Code.`);
32123
+ const fallback = staticModelEntries(providerName, providerConfig);
32124
+ try {
32125
+ const module = await dynamicImport3("@anthropic-ai/claude-agent-sdk");
32126
+ if (!module.query) throw new Error("Claude Agent SDK query export is unavailable.");
32127
+ const query = module.query({
32128
+ prompt: "",
32129
+ options: {
32130
+ model: defaultModelForProvider(providerName, providerConfig),
32131
+ maxTurns: 0,
32132
+ pathToClaudeCodeExecutable: resolveClaudeCodeCliPath(providerConfig.cliPath),
32133
+ env: sanitizedClaudeCodeEnv(providerConfig.env, providerConfig.sanitizeApiKeyEnv ?? true)
32134
+ }
32135
+ });
32136
+ try {
32137
+ if (typeof query.supportedModels !== "function") throw new Error("Claude Agent SDK supportedModels is unavailable.");
32138
+ const models = await withTimeout(query.supportedModels(), options2.timeoutMs ?? 5e3);
32139
+ return {
32140
+ status: "ready",
32141
+ providerName,
32142
+ models: models.map((model, index) => ({
32143
+ name: model.displayName || model.value,
32144
+ displayName: model.displayName || model.value,
32145
+ model: model.value,
32146
+ description: model.description,
32147
+ isDefault: index === 0,
32148
+ source: "oauth"
32149
+ })),
32150
+ source: "oauth"
32151
+ };
32152
+ } finally {
32153
+ query.close?.();
32154
+ }
32155
+ } catch (error) {
32156
+ if (fallback.length) return { status: "ready", providerName, models: fallback, source: "fallback", message: errorMessage2(error) };
32157
+ throw error;
32158
+ }
32159
+ }
32160
+ function staticModelEntries(providerName, providerConfig) {
32161
+ return providerModelProfiles(providerName, providerConfig).filter((profile) => !profile.hidden).map((profile, index) => ({
32162
+ name: profile.displayName ?? profile.name,
32163
+ displayName: profile.displayName ?? profile.name,
32164
+ model: profile.model,
32165
+ description: profile.description,
32166
+ isDefault: index === 0,
32167
+ source: "config"
31672
32168
  }));
31673
32169
  }
32170
+ function mergeCatalogWithStatic(staticModels, remoteModels) {
32171
+ const byModel = /* @__PURE__ */ new Map();
32172
+ for (const model of remoteModels) byModel.set(model.model, model);
32173
+ for (const model of staticModels) byModel.set(model.model, { ...byModel.get(model.model), ...model, source: model.source });
32174
+ return [...byModel.values()];
32175
+ }
32176
+ function normalizeOpenAIModels(raw) {
32177
+ const items = Array.isArray(raw.data) ? raw.data : Array.isArray(raw.models) ? raw.models : [];
32178
+ return items.map((item) => {
32179
+ const object2 = item;
32180
+ const id = stringValue5(object2.id) ?? stringValue5(object2.name) ?? stringValue5(object2.model);
32181
+ if (!id) return void 0;
32182
+ return {
32183
+ name: id,
32184
+ displayName: stringValue5(object2.display_name) ?? stringValue5(object2.displayName) ?? id,
32185
+ model: id,
32186
+ description: stringValue5(object2.description)
32187
+ };
32188
+ }).filter(Boolean);
32189
+ }
32190
+ function normalizeAnthropicModels(raw) {
32191
+ return (raw.data ?? []).map((item) => {
32192
+ const object2 = item;
32193
+ const id = stringValue5(object2.id);
32194
+ if (!id) return void 0;
32195
+ return {
32196
+ name: stringValue5(object2.display_name) ?? id,
32197
+ displayName: stringValue5(object2.display_name) ?? id,
32198
+ model: id,
32199
+ description: stringValue5(object2.description),
32200
+ source: "api"
32201
+ };
32202
+ }).filter(Boolean);
32203
+ }
32204
+ function normalizeOllamaTags(raw) {
32205
+ const items = Array.isArray(raw.models) ? raw.models : [];
32206
+ return items.map((item) => {
32207
+ const object2 = item;
32208
+ const id = stringValue5(object2.name) ?? stringValue5(object2.model);
32209
+ if (!id) return void 0;
32210
+ return { name: id, displayName: id, model: id };
32211
+ }).filter(Boolean);
32212
+ }
32213
+ function normalizeCodexModels(raw) {
32214
+ const items = Array.isArray(raw.models) ? raw.models : [];
32215
+ return items.map((item, index) => {
32216
+ const object2 = item;
32217
+ const id = stringValue5(object2.slug) ?? stringValue5(object2.id) ?? stringValue5(object2.model);
32218
+ if (!id) return void 0;
32219
+ return {
32220
+ name: stringValue5(object2.display_name) ?? id,
32221
+ displayName: stringValue5(object2.display_name) ?? id,
32222
+ model: id,
32223
+ description: stringValue5(object2.description),
32224
+ isDefault: index === 0
32225
+ };
32226
+ }).filter(Boolean);
32227
+ }
32228
+ function authHeadersForOpenAICompatible(providerConfig, apiKey) {
32229
+ const auth = inferOpenAICompatibleAuth(providerConfig.baseURL, providerConfig.auth);
32230
+ const type = auth.type ?? "bearer";
32231
+ const header = auth.header ?? (type === "api-key" ? "api-key" : "authorization");
32232
+ return { [header]: type === "bearer" ? `Bearer ${apiKey}` : apiKey };
32233
+ }
32234
+ async function fetchJson2(url, options2) {
32235
+ const response = await (options2.fetcher ?? fetch)(url, {
32236
+ method: "GET",
32237
+ headers: {
32238
+ accept: "application/json",
32239
+ ...options2.headers ?? {}
32240
+ },
32241
+ signal: AbortSignal.timeout(options2.timeoutMs ?? 5e3)
32242
+ });
32243
+ const text = await response.text();
32244
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
32245
+ return text ? JSON.parse(text) : {};
32246
+ }
32247
+ function cachePath(cacheKey) {
32248
+ return path31.join(os12.homedir(), ".demian", "cache", "models", `${safeCacheName(cacheKey)}.json`);
32249
+ }
32250
+ async function readCatalogCache(cacheKey, ttlMs, allow) {
32251
+ if (!allow) return void 0;
32252
+ try {
32253
+ const raw = JSON.parse(await readFile15(cachePath(cacheKey), "utf8"));
32254
+ if (!raw.result) return void 0;
32255
+ if (ttlMs !== Number.POSITIVE_INFINITY && Date.now() - (raw.savedAt ?? 0) > ttlMs) return void 0;
32256
+ return { ...raw.result, source: "cache", models: raw.result.models.map((model) => ({ ...model, source: "cache" })) };
32257
+ } catch {
32258
+ return void 0;
32259
+ }
32260
+ }
32261
+ async function writeCatalogCache(cacheKey, result) {
32262
+ const filePath = cachePath(cacheKey);
32263
+ await mkdir13(path31.dirname(filePath), { recursive: true, mode: 448 });
32264
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
32265
+ await writeFile12(temp, JSON.stringify({ savedAt: Date.now(), result }, null, 2), { mode: 384 });
32266
+ await rename5(temp, filePath);
32267
+ }
32268
+ function safeCacheName(providerName) {
32269
+ return providerName.replace(/[^a-zA-Z0-9_.-]+/g, "_");
32270
+ }
32271
+ function catalogCacheKey(providerName, providerConfig) {
32272
+ const identity = {
32273
+ providerName,
32274
+ type: providerConfig.type,
32275
+ catalogType: providerConfig.catalog?.type,
32276
+ endpoint: providerConfig.catalog?.endpoint,
32277
+ baseURL: "baseURL" in providerConfig ? providerConfig.baseURL : void 0,
32278
+ apiKeyEnv: "apiKeyEnv" in providerConfig ? providerConfig.apiKeyEnv : void 0,
32279
+ apiKeyEnvAliases: "apiKeyEnvAliases" in providerConfig ? providerConfig.apiKeyEnvAliases : void 0,
32280
+ auth: "auth" in providerConfig ? providerConfig.auth : void 0
32281
+ };
32282
+ const digest = crypto6.createHash("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 16);
32283
+ return `${providerName}-${digest}`;
32284
+ }
32285
+ function missingCatalogAuth(providerConfig) {
32286
+ if (providerConfig.type !== "openai-compatible" && providerConfig.type !== "anthropic" && providerConfig.type !== "ollama") return void 0;
32287
+ if (providerConfig.catalog?.requiresAuth === false) return void 0;
32288
+ const expectsAuth = providerConfig.catalog?.requiresAuth === true || !!providerConfig.apiKey || !!providerConfig.apiKeyEnv || !!providerConfig.apiKeyEnvAliases?.length;
32289
+ if (!expectsAuth) return void 0;
32290
+ const apiKey = resolveConfiguredApiKey(providerConfig);
32291
+ return apiKey.value ? void 0 : apiKey.envName ?? "apiKey";
32292
+ }
32293
+ function stringValue5(value) {
32294
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
32295
+ }
32296
+ function missingAuth(envName) {
32297
+ const error = new Error(`missing-auth:${envName}`);
32298
+ return error;
32299
+ }
32300
+ function isMissingAuthError(error) {
32301
+ if (error instanceof CodexAuthError) {
32302
+ 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";
32303
+ }
32304
+ return error instanceof Error && error.message.startsWith("missing-auth:");
32305
+ }
32306
+ function errorMessage2(error) {
32307
+ return error instanceof Error ? error.message : String(error);
32308
+ }
32309
+ async function codexClientVersion(override) {
32310
+ const normalized = semverLike(override);
32311
+ if (normalized) return normalized;
32312
+ packageVersionPromise ??= readPackageVersion();
32313
+ return packageVersionPromise;
32314
+ }
32315
+ async function readPackageVersion() {
32316
+ try {
32317
+ const raw = JSON.parse(await readFile15(new URL("../package.json", import.meta.url), "utf8"));
32318
+ return semverLike(raw.version) ?? DEFAULT_CODEX_CLIENT_VERSION;
32319
+ } catch {
32320
+ return DEFAULT_CODEX_CLIENT_VERSION;
32321
+ }
32322
+ }
32323
+ function semverLike(value) {
32324
+ if (typeof value !== "string") return void 0;
32325
+ const trimmed = value.trim();
32326
+ return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(trimmed) ? trimmed : void 0;
32327
+ }
32328
+ function withTimeout(promise, timeoutMs) {
32329
+ return new Promise((resolve, reject) => {
32330
+ const timer = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
32331
+ promise.then(
32332
+ (value) => {
32333
+ clearTimeout(timer);
32334
+ resolve(value);
32335
+ },
32336
+ (error) => {
32337
+ clearTimeout(timer);
32338
+ reject(error);
32339
+ }
32340
+ );
32341
+ });
32342
+ }
32343
+
32344
+ // src/ui/settings.ts
32345
+ function providerOptions(config, catalogs = {}) {
32346
+ const orderedNames = sortProviderNames(Object.keys(config.providers));
32347
+ const order = new Map(orderedNames.map((name, index) => [name, index]));
32348
+ return orderedNames.map((name) => [name, config.providers[name]]).filter(([, provider]) => provider && !provider.hidden && !(provider.type === "claudecode" && provider.runtime === "cli")).map(([name, provider]) => {
32349
+ const catalog = catalogs[name];
32350
+ const available = providerCatalogAvailable(catalog);
32351
+ const profiles = catalog?.models.length ? catalog.models.map((model) => ({
32352
+ name: model.name,
32353
+ displayName: model.displayName ?? model.name,
32354
+ model: model.model,
32355
+ description: model.description,
32356
+ source: model.source,
32357
+ isDefault: model.isDefault
32358
+ })) : providerModelProfiles(name, provider);
32359
+ const modelProfiles = profiles.map((profile) => {
32360
+ const displayName = profile.displayName ?? profile.name ?? profile.model;
32361
+ return {
32362
+ ...profile,
32363
+ label: available ? displayName : unavailableLabel(displayName),
32364
+ available
32365
+ };
32366
+ });
32367
+ const models = modelProfiles.length ? [...new Set(modelProfiles.map((item) => item.model).filter((item) => typeof item === "string" && item.trim()))] : providerModelOptions(name, provider);
32368
+ const defaultModel = defaultModelForProvider(name, provider);
32369
+ return {
32370
+ name,
32371
+ label: available ? name : unavailableLabel(name),
32372
+ model: defaultModel,
32373
+ modelLabel: available || !defaultModel ? defaultModel : unavailableLabel(defaultModel),
32374
+ models,
32375
+ modelProfiles,
32376
+ type: provider.type,
32377
+ runtime: provider.type === "claudecode" ? provider.runtime : void 0,
32378
+ permissionMode: provider.type === "claudecode" ? provider.permissionMode : void 0,
32379
+ ga: provider.type === "claudecode" ? claudeCodeGaEnabled(provider) : void 0,
32380
+ available,
32381
+ catalog: catalog ? { status: catalog.status, source: catalog.source, message: catalog.message } : void 0
32382
+ };
32383
+ }).sort((left, right) => {
32384
+ if (left.available !== right.available) return left.available ? -1 : 1;
32385
+ return (order.get(left.name) ?? 0) - (order.get(right.name) ?? 0);
32386
+ });
32387
+ }
32388
+ async function providerOptionsWithCatalog(config, options2 = {}) {
32389
+ const entries = sortProviderNames(Object.keys(config.providers)).map((name) => [name, config.providers[name]]).filter(([, provider]) => provider && !provider.hidden && !(provider.type === "claudecode" && provider.runtime === "cli"));
32390
+ const catalogs = Object.fromEntries(
32391
+ await Promise.all(
32392
+ entries.map(async ([name, provider]) => {
32393
+ const catalog = await listProviderModelCatalog(name, provider, { timeoutMs: options2.timeoutMs }).catch((error) => ({
32394
+ status: "unavailable",
32395
+ providerName: name,
32396
+ models: [],
32397
+ source: "fallback",
32398
+ message: errorMessage3(error)
32399
+ }));
32400
+ return [name, catalog];
32401
+ })
32402
+ )
32403
+ );
32404
+ return providerOptions(config, catalogs);
32405
+ }
32406
+ var MISSING_SELECTION_PLACEHOLDER = "-";
31674
32407
  function createInteractiveModelSelection(config, flags = {}, saved) {
31675
32408
  const savedProviderName = saved?.providerName === "claudecode-plan" ? "claudecode" : saved?.providerName;
31676
- const savedProviderConfig = savedProviderName ? config.providers[savedProviderName] : void 0;
31677
- const savedProvider = savedProviderName && savedProviderConfig && !savedProviderConfig.hidden ? savedProviderName : void 0;
31678
- const providerName = flags.provider ?? savedProvider ?? config.defaultProvider;
31679
- const providerConfig = resolveProviderConfig(config, providerName);
31680
- const usesSavedProvider = !flags.provider && providerName === savedProvider;
32409
+ const savedProviderExists = savedProviderName && config.providers[savedProviderName] && !config.providers[savedProviderName]?.hidden;
32410
+ if (saved?.providerName && !flags.provider && !savedProviderExists) {
32411
+ return {
32412
+ providerName: saved.providerName,
32413
+ providerSource: "saved",
32414
+ model: MISSING_SELECTION_PLACEHOLDER,
32415
+ modelSource: "saved"
32416
+ };
32417
+ }
32418
+ const providerName = flags.provider ?? (savedProviderExists ? savedProviderName : config.defaultProvider);
32419
+ let providerConfig;
32420
+ try {
32421
+ providerConfig = resolveProviderConfig(config, providerName);
32422
+ } catch {
32423
+ return {
32424
+ providerName,
32425
+ providerSource: flags.provider ? "flag" : "config",
32426
+ model: MISSING_SELECTION_PLACEHOLDER,
32427
+ modelSource: "config"
32428
+ };
32429
+ }
32430
+ const usesSavedProvider = !flags.provider && savedProviderExists && providerName === savedProviderName;
31681
32431
  const savedModel = usesSavedProvider && saved?.model ? saved.model : void 0;
32432
+ const model = flags.model ?? savedModel ?? defaultModelForProvider(providerName, providerConfig);
31682
32433
  return {
31683
32434
  providerName,
31684
32435
  providerSource: flags.provider ? "flag" : usesSavedProvider ? "saved" : "config",
31685
- model: flags.model ?? savedModel ?? providerConfig.model,
31686
- modelSource: flags.model ? "flag" : savedModel ? "saved" : "config"
32436
+ model,
32437
+ modelSource: flags.model ? "flag" : savedModel ? "saved" : "config",
32438
+ modelProfileName: providerModelProfiles(providerName, providerConfig).find((profile) => profile.model === model || profile.displayName === model || profile.name === model)?.name
31687
32439
  };
31688
32440
  }
32441
+ function providerCatalogAvailable(catalog) {
32442
+ return catalog ? catalog.status === "ready" && catalog.models.length > 0 : true;
32443
+ }
32444
+ function unavailableLabel(value) {
32445
+ return `(n/a) ${value}`;
32446
+ }
32447
+ function errorMessage3(error) {
32448
+ return error instanceof Error ? error.message : String(error);
32449
+ }
31689
32450
 
31690
32451
  // src/ui/tui/controller.ts
31691
32452
  async function runTuiSession(flags, store2) {
31692
- const cwd = path30.resolve(flags.cwd ?? process.cwd());
32453
+ const cwd = path32.resolve(flags.cwd ?? process.cwd());
31693
32454
  const eventBus = new EventBus();
31694
32455
  eventBus.subscribe((event) => store2.handleEvent(event));
31695
32456
  const loadedConfig2 = await loadConfig({ cwd, configPath: flags.configPath });
@@ -31710,7 +32471,7 @@ async function runTuiSession(flags, store2) {
31710
32471
  const current = agentRegistry.get(agentName);
31711
32472
  agentOptions.unshift({ name: current.name, description: current.description });
31712
32473
  }
31713
- store2.configureSettings(initialSelection, providerOptions(config), {
32474
+ store2.configureSettings(initialSelection, await providerOptionsWithCatalog(config, { timeoutMs: 1500 }), {
31714
32475
  agent: agentName,
31715
32476
  cwd,
31716
32477
  agentOptions,
@@ -31727,9 +32488,9 @@ async function runTuiSession(flags, store2) {
31727
32488
  const rootSessionId = flags.sessionId ?? createRootSessionId();
31728
32489
  const goalTitleGenerator = async (input2) => {
31729
32490
  const selection = store2.currentSelection();
31730
- const providerConfig = resolveProviderConfig(config, selection.providerName);
31731
- const resolved = resolveExecutionBackend(providerConfig, {
31732
- model: selection.model,
32491
+ const runtime = resolveProviderRuntimeConfig(config, selection);
32492
+ const resolved = resolveExecutionBackend(runtime.providerConfig, {
32493
+ model: runtime.model,
31733
32494
  onRetry: (event) => eventBus.emit({
31734
32495
  type: "provider.retry",
31735
32496
  sessionId: "goal-title",
@@ -31750,7 +32511,7 @@ async function runTuiSession(flags, store2) {
31750
32511
  return 0;
31751
32512
  }
31752
32513
  if (!prompt) return 0;
31753
- if (isCompactCommand2(prompt)) {
32514
+ if (isCompactCommand(prompt)) {
31754
32515
  const result = compactInteractiveHistory(history, config.context.main);
31755
32516
  history = result.messages;
31756
32517
  store2.prepareForPrompt("waiting for next message");
@@ -31787,6 +32548,7 @@ async function runTuiSession(flags, store2) {
31787
32548
  if (goalCommand.kind === "message") {
31788
32549
  store2.showSystemMessage("Goal", goalCommand.message);
31789
32550
  if (parsedGoalCommand?.action === "clear") store2.clearGoalState();
32551
+ else if (!goalCommand.state && isGoalStateClearingAction(parsedGoalCommand?.action)) store2.clearGoalState();
31790
32552
  else if (goalCommand.state) store2.showGoalState(goalCommand.state);
31791
32553
  store2.prepareForPrompt("waiting for next message");
31792
32554
  continue;
@@ -31838,10 +32600,10 @@ async function runTuiSession(flags, store2) {
31838
32600
  if (goalState) await goalRuntime.finishIteration({ state: goalState, finalAnswer: result.finalAnswer, reason: result.endReason, usage: result.usage });
31839
32601
  history = interactiveHistoryFromRunMessages(result.messages);
31840
32602
  } else {
31841
- const providerConfig = resolveProviderConfig(runConfig, selection.providerName);
31842
32603
  const runtimeAgent = goalState ? withGoalTools(agent) : agent;
31843
- const backend = resolveExecutionBackend(providerConfig, {
31844
- model: selection.model,
32604
+ const runtime = resolveProviderRuntimeConfig(runConfig, selection);
32605
+ const backend = resolveExecutionBackend(runtime.providerConfig, {
32606
+ model: runtime.model,
31845
32607
  config: runConfig,
31846
32608
  agent: runtimeAgent,
31847
32609
  onRetry: (event) => eventBus.emit({
@@ -31933,8 +32695,446 @@ async function runTuiSession(flags, store2) {
31933
32695
  savedSelectionKey = key;
31934
32696
  }
31935
32697
  }
32698
+ function isGoalStateClearingAction(action) {
32699
+ return action === "status" || action === "pause" || action === "resume";
32700
+ }
32701
+
32702
+ // src/config-watcher.ts
32703
+ import { EventEmitter } from "node:events";
32704
+ import fs11 from "node:fs";
32705
+ import path34 from "node:path";
32706
+
32707
+ // src/config-scaffold.ts
32708
+ import { chmod as chmod3, mkdir as mkdir14, readFile as readFile16, rename as rename6, stat as stat8, writeFile as writeFile13 } from "node:fs/promises";
32709
+ import os13 from "node:os";
32710
+ import path33 from "node:path";
32711
+ function defaultUserConfigPath() {
32712
+ return path33.join(os13.homedir(), ".demian", "config.json");
32713
+ }
32714
+ function defaultUserConfig(defaultProvider = detectDefaultProvider()) {
32715
+ return {
32716
+ version: 2,
32717
+ defaultProvider,
32718
+ providers: {
32719
+ openai: {
32720
+ type: "openai-compatible",
32721
+ baseURL: "https://api.openai.com/v1",
32722
+ apiKeyEnv: "OPENAI_API_KEY",
32723
+ catalog: { type: "openai-models", endpoint: "https://api.openai.com/v1/models" }
32724
+ },
32725
+ anthropic: {
32726
+ type: "anthropic",
32727
+ baseURL: "https://api.anthropic.com/v1",
32728
+ apiKeyEnv: "ANTHROPIC_API_KEY",
32729
+ catalog: { type: "anthropic-models", endpoint: "https://api.anthropic.com/v1/models" }
32730
+ },
32731
+ gemini: {
32732
+ type: "openai-compatible",
32733
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
32734
+ apiKeyEnv: "GEMINI_API_KEY",
32735
+ apiKeyEnvAliases: ["GOOGLE_API_KEY"],
32736
+ catalog: { type: "gemini-openai-models", endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models" }
32737
+ },
32738
+ groq: {
32739
+ type: "openai-compatible",
32740
+ baseURL: "https://api.groq.com/openai/v1",
32741
+ apiKeyEnv: "GROQ_API_KEY",
32742
+ catalog: { type: "groq-models", endpoint: "https://api.groq.com/openai/v1/models" }
32743
+ },
32744
+ azure: {
32745
+ type: "openai-compatible",
32746
+ auth: { type: "api-key", header: "api-key" },
32747
+ modelProfiles: [
32748
+ {
32749
+ name: "azure-example",
32750
+ displayName: "Azure example",
32751
+ model: "azure-deployment-name",
32752
+ baseURL: "https://example.openai.azure.com/openai/v1",
32753
+ apiKeyEnv: "AZURE_OPENAI_API_KEY"
32754
+ }
32755
+ ]
32756
+ },
32757
+ lmstudio: {
32758
+ type: "openai-compatible",
32759
+ baseURL: "http://localhost:1234/v1",
32760
+ apiKey: "lm-studio",
32761
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:1234/v1/models" }
32762
+ },
32763
+ "ollama-local": {
32764
+ type: "openai-compatible",
32765
+ baseURL: "http://localhost:11434/v1",
32766
+ apiKey: "ollama",
32767
+ quirks: { omitTemperature: true },
32768
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:11434/v1/models" }
32769
+ },
32770
+ "ollama-cloud": {
32771
+ type: "ollama",
32772
+ baseURL: "https://ollama.com/api",
32773
+ apiKeyEnv: "OLLAMA_API_KEY",
32774
+ catalog: { type: "ollama-tags", endpoint: "https://ollama.com/api/tags" }
32775
+ },
32776
+ llamacpp: {
32777
+ type: "openai-compatible",
32778
+ baseURL: "http://localhost:8080/v1",
32779
+ apiKey: "llama.cpp",
32780
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8080/v1/models" }
32781
+ },
32782
+ vllm: {
32783
+ type: "openai-compatible",
32784
+ baseURL: "http://localhost:8000/v1",
32785
+ apiKey: "vllm",
32786
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8000/v1/models" }
32787
+ },
32788
+ codex: {
32789
+ type: "codex",
32790
+ baseURL: "https://chatgpt.com/backend-api/codex",
32791
+ authStore: "auto",
32792
+ allowApiKeyFallback: false,
32793
+ promptCacheKey: "root-session",
32794
+ catalog: { type: "codex-oauth-models" }
32795
+ },
32796
+ claudecode: {
32797
+ type: "claudecode",
32798
+ runtime: "agent-sdk",
32799
+ cliPath: "~/.local/bin/claude",
32800
+ cwdMode: "session",
32801
+ historyPolicy: "passthrough-resume",
32802
+ onInvalidResume: "fresh",
32803
+ attachmentFallback: "block",
32804
+ allowSubagents: false,
32805
+ sanitizeApiKeyEnv: true,
32806
+ authPreflight: true,
32807
+ useBareMode: false,
32808
+ usageLedgerScope: "process",
32809
+ sessionLock: true,
32810
+ abortPolicy: "record-only",
32811
+ catalog: { type: "claudecode-supported-models" }
32812
+ }
32813
+ }
32814
+ };
32815
+ }
32816
+ async function createUserConfig(options2 = {}) {
32817
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
32818
+ const content = `${JSON.stringify(defaultUserConfig(options2.defaultProvider), null, 2)}
32819
+ `;
32820
+ if (options2.print) return { path: filePath, created: false, content };
32821
+ const existed = await exists(filePath);
32822
+ if (existed && !options2.force) return { path: filePath, created: false, content: await readFile16(filePath, "utf8"), existed: true };
32823
+ await writeJsonAtomic(filePath, content);
32824
+ return { path: filePath, created: true, content, existed };
32825
+ }
32826
+ async function addProvider(options2) {
32827
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
32828
+ const config = await readConfigObject(filePath);
32829
+ const providers = objectValue(config.providers);
32830
+ const name = options2.name;
32831
+ if (providers[name] && !options2.force) throw new Error(`Provider ${name} already exists. Use --force to overwrite it.`);
32832
+ providers[name] = providerPreset(options2);
32833
+ config.providers = providers;
32834
+ const content = `${JSON.stringify(config, null, 2)}
32835
+ `;
32836
+ await writeJsonAtomic(filePath, content);
32837
+ return { path: filePath, created: true, content };
32838
+ }
32839
+ async function addModelProfile(options2) {
32840
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
32841
+ const config = await readConfigObject(filePath);
32842
+ const providers = objectValue(config.providers);
32843
+ const provider = objectValue(providers[options2.provider]);
32844
+ const profiles = Array.isArray(provider.modelProfiles) ? [...provider.modelProfiles] : [];
32845
+ const displayName = options2.displayName ?? options2.name;
32846
+ const existingByName = profiles.findIndex((entry) => entry.name === options2.name);
32847
+ const existingByDisplay = profiles.findIndex((entry) => entry.displayName === displayName);
32848
+ if (existingByName >= 0 && !options2.force) throw new Error(`Profile name "${options2.name}" already exists on provider ${options2.provider}. Use --force to overwrite it.`);
32849
+ if (existingByDisplay >= 0 && existingByDisplay !== existingByName && !options2.force) {
32850
+ throw new Error(`Profile displayName "${displayName}" already exists on provider ${options2.provider} (profile name: ${profiles[existingByDisplay].name}). Use --force or pick a different --display-name.`);
32851
+ }
32852
+ const next = {
32853
+ name: options2.name,
32854
+ displayName,
32855
+ model: options2.model,
32856
+ ...options2.baseURL ? { baseURL: options2.baseURL } : {},
32857
+ ...options2.apiKey ? { apiKey: options2.apiKey } : {},
32858
+ ...options2.apiKeyEnv ? { apiKeyEnv: options2.apiKeyEnv } : {}
32859
+ };
32860
+ if (existingByName >= 0) profiles[existingByName] = next;
32861
+ else profiles.push(next);
32862
+ provider.modelProfiles = profiles;
32863
+ providers[options2.provider] = provider;
32864
+ config.providers = providers;
32865
+ const content = `${JSON.stringify(config, null, 2)}
32866
+ `;
32867
+ await writeJsonAtomic(filePath, content);
32868
+ return { path: filePath, created: true, content };
32869
+ }
32870
+ async function updateConfigDefaults(options2) {
32871
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
32872
+ const config = await readConfigObject(filePath);
32873
+ const defaultProvider = normalizeOptionalName(options2.defaultProvider);
32874
+ const defaultAgent = normalizeOptionalName(options2.defaultAgent);
32875
+ if (!defaultProvider && !defaultAgent) throw new Error("At least one of defaultProvider or defaultAgent is required.");
32876
+ if (defaultProvider) config.defaultProvider = defaultProvider;
32877
+ if (defaultAgent) config.defaultAgent = defaultAgent;
32878
+ const content = `${JSON.stringify(config, null, 2)}
32879
+ `;
32880
+ await writeJsonAtomic(filePath, content);
32881
+ return { path: filePath, created: true, content };
32882
+ }
32883
+ async function readConfigObject(filePath) {
32884
+ if (!await exists(filePath)) await createUserConfig({ path: filePath });
32885
+ const raw = JSON.parse(await readFile16(filePath, "utf8"));
32886
+ raw.version ??= 2;
32887
+ raw.providers = objectValue(raw.providers);
32888
+ return raw;
32889
+ }
32890
+ function providerPreset(options2) {
32891
+ const preset = options2.preset ?? options2.name;
32892
+ const auth = {
32893
+ ...options2.apiKey ? { apiKey: options2.apiKey } : {},
32894
+ ...options2.apiKeyEnv ? { apiKeyEnv: options2.apiKeyEnv } : {}
32895
+ };
32896
+ const openAIAuth = options2.authHeader ? { auth: { type: "api-key", header: options2.authHeader } } : {};
32897
+ if (preset === "openai") return { type: "openai-compatible", baseURL: options2.baseURL ?? "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", ...auth, ...openAIAuth, catalog: { type: "openai-models", endpoint: `${(options2.baseURL ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/models` } };
32898
+ if (preset === "anthropic") return { type: "anthropic", baseURL: options2.baseURL ?? "https://api.anthropic.com/v1", apiKeyEnv: "ANTHROPIC_API_KEY", ...auth, catalog: { type: "anthropic-models", endpoint: `${(options2.baseURL ?? "https://api.anthropic.com/v1").replace(/\/+$/, "")}/models` } };
32899
+ if (preset === "gemini") {
32900
+ const geminiBase = options2.baseURL ?? "https://generativelanguage.googleapis.com/v1beta/openai/";
32901
+ return { type: "openai-compatible", baseURL: geminiBase, apiKeyEnv: "GEMINI_API_KEY", apiKeyEnvAliases: ["GOOGLE_API_KEY"], ...auth, ...openAIAuth, catalog: { type: "gemini-openai-models", endpoint: `${geminiBase.replace(/\/+$/, "")}/models` } };
32902
+ }
32903
+ if (preset === "groq") return { type: "openai-compatible", baseURL: options2.baseURL ?? "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_API_KEY", ...auth, ...openAIAuth, catalog: { type: "groq-models", endpoint: `${(options2.baseURL ?? "https://api.groq.com/openai/v1").replace(/\/+$/, "")}/models` } };
32904
+ if (preset === "azure") {
32905
+ return {
32906
+ type: "openai-compatible",
32907
+ auth: { type: "api-key", header: options2.authHeader ?? "api-key" },
32908
+ ...auth,
32909
+ modelProfiles: [
32910
+ {
32911
+ name: "azure-example",
32912
+ displayName: "Azure example",
32913
+ model: "azure-deployment-name",
32914
+ baseURL: options2.baseURL ?? "https://example.openai.azure.com/openai/v1",
32915
+ apiKeyEnv: options2.apiKeyEnv ?? "AZURE_OPENAI_API_KEY"
32916
+ }
32917
+ ]
32918
+ };
32919
+ }
32920
+ if (preset === "lmstudio") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:1234/v1", apiKey: options2.apiKey ?? "lm-studio", catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:1234/v1").replace(/\/+$/, "")}/models` } };
32921
+ if (preset === "ollama-local" || preset === "ollama") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:11434/v1", apiKey: options2.apiKey ?? "ollama", quirks: { omitTemperature: true }, catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:11434/v1").replace(/\/+$/, "")}/models` } };
32922
+ if (preset === "ollama-cloud") return { type: "ollama", baseURL: options2.baseURL ?? "https://ollama.com/api", ...auth, catalog: { type: "ollama-tags", endpoint: `${(options2.baseURL ?? "https://ollama.com/api").replace(/\/+$/, "")}/tags` } };
32923
+ if (preset === "llamacpp") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:8080/v1", apiKey: options2.apiKey ?? "llama.cpp", catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:8080/v1").replace(/\/+$/, "")}/models` } };
32924
+ if (preset === "vllm") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:8000/v1", apiKey: options2.apiKey ?? "vllm", catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:8000/v1").replace(/\/+$/, "")}/models` } };
32925
+ if (preset === "codex") return { type: "codex", baseURL: options2.baseURL ?? "https://chatgpt.com/backend-api/codex", authStore: "auto", allowApiKeyFallback: false, promptCacheKey: "root-session", catalog: { type: "codex-oauth-models" } };
32926
+ if (preset === "claudecode") return { type: "claudecode", runtime: "agent-sdk", cliPath: "~/.local/bin/claude", cwdMode: "session", historyPolicy: "passthrough-resume", onInvalidResume: "fresh", attachmentFallback: "block", allowSubagents: false, sanitizeApiKeyEnv: true, authPreflight: true, useBareMode: false, usageLedgerScope: "process", sessionLock: true, abortPolicy: "record-only", catalog: { type: "claudecode-supported-models" } };
32927
+ if ((options2.type ?? preset) === "openai-compatible") {
32928
+ const baseURL = options2.baseURL;
32929
+ if (!baseURL) throw new Error("openai-compatible provider requires --base-url.");
32930
+ return {
32931
+ type: "openai-compatible",
32932
+ baseURL,
32933
+ ...options2.apiKey ? { apiKey: options2.apiKey } : {},
32934
+ ...options2.apiKeyEnv ? { apiKeyEnv: options2.apiKeyEnv } : {},
32935
+ ...openAIAuth,
32936
+ catalog: { type: "openai-compatible-models", endpoint: `${baseURL.replace(/\/+$/, "")}/models` }
32937
+ };
32938
+ }
32939
+ throw new Error(`Unknown provider preset: ${preset}`);
32940
+ }
32941
+ async function writeJsonAtomic(filePath, content) {
32942
+ await mkdir14(path33.dirname(filePath), { recursive: true, mode: 448 });
32943
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
32944
+ await writeFile13(temp, content, { mode: 384 });
32945
+ await rename6(temp, filePath);
32946
+ await chmod3(filePath, 384).catch(() => void 0);
32947
+ }
32948
+ function detectDefaultProvider() {
32949
+ if (process.env.OPENAI_API_KEY) return "openai";
32950
+ if (process.env.ANTHROPIC_API_KEY) return "anthropic";
32951
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return "gemini";
32952
+ if (process.env.GROQ_API_KEY) return "groq";
32953
+ return "openai";
32954
+ }
32955
+ function objectValue(value) {
32956
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
32957
+ }
32958
+ function normalizeOptionalName(value) {
32959
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
32960
+ }
32961
+ async function exists(filePath) {
32962
+ try {
32963
+ await stat8(filePath);
32964
+ return true;
32965
+ } catch {
32966
+ return false;
32967
+ }
32968
+ }
32969
+ function expandHome2(value) {
32970
+ return resolveExpandedPath(value);
32971
+ }
32972
+
32973
+ // src/config-watcher.ts
32974
+ var ConfigWatcher = class extends EventEmitter {
32975
+ #filePath;
32976
+ #debounceMs;
32977
+ #watcher;
32978
+ #pollIntervalMs = 1e3;
32979
+ #lastSeen;
32980
+ #pending;
32981
+ #started = false;
32982
+ constructor(options2 = {}) {
32983
+ super();
32984
+ this.#filePath = options2.path ?? defaultUserConfigPath();
32985
+ this.#debounceMs = options2.debounceMs ?? 300;
32986
+ }
32987
+ get filePath() {
32988
+ return this.#filePath;
32989
+ }
32990
+ start() {
32991
+ if (this.#started) return;
32992
+ this.#started = true;
32993
+ this.#lastSeen = this.#snapshot();
32994
+ try {
32995
+ fs11.mkdirSync(path34.dirname(this.#filePath), { recursive: true, mode: 448 });
32996
+ this.#watcher = fs11.watch(path34.dirname(this.#filePath), { persistent: false }, (_eventType, filename) => {
32997
+ if (filename && filename !== path34.basename(this.#filePath)) return;
32998
+ this.#schedule();
32999
+ });
33000
+ this.#watcher.on("error", () => void 0);
33001
+ } catch {
33002
+ this.#watcher = void 0;
33003
+ }
33004
+ fs11.watchFile(this.#filePath, { persistent: false, interval: this.#pollIntervalMs }, () => this.#schedule());
33005
+ }
33006
+ stop() {
33007
+ this.#started = false;
33008
+ if (this.#pending) clearTimeout(this.#pending);
33009
+ this.#pending = void 0;
33010
+ this.#watcher?.close();
33011
+ this.#watcher = void 0;
33012
+ try {
33013
+ fs11.unwatchFile(this.#filePath);
33014
+ } catch {
33015
+ }
33016
+ }
33017
+ #schedule() {
33018
+ if (this.#pending) clearTimeout(this.#pending);
33019
+ this.#pending = setTimeout(() => this.#emit(), this.#debounceMs);
33020
+ }
33021
+ #emit() {
33022
+ this.#pending = void 0;
33023
+ const snapshot = this.#snapshot();
33024
+ const previous = this.#lastSeen ?? { size: 0, mtimeMs: 0, exists: false };
33025
+ this.#lastSeen = snapshot;
33026
+ if (previous.exists && !snapshot.exists) {
33027
+ this.emit("unlink", this.#filePath);
33028
+ this.emit("changed", this.#filePath, "unlink");
33029
+ return;
33030
+ }
33031
+ if (!previous.exists && snapshot.exists) {
33032
+ this.emit("create", this.#filePath);
33033
+ this.emit("changed", this.#filePath, "create");
33034
+ return;
33035
+ }
33036
+ if (snapshot.exists && (snapshot.mtimeMs !== previous.mtimeMs || snapshot.size !== previous.size)) {
33037
+ this.emit("change", this.#filePath);
33038
+ this.emit("changed", this.#filePath, "change");
33039
+ }
33040
+ }
33041
+ #snapshot() {
33042
+ try {
33043
+ const stat9 = fs11.statSync(this.#filePath);
33044
+ return { size: stat9.size, mtimeMs: stat9.mtimeMs, exists: true };
33045
+ } catch {
33046
+ return { size: 0, mtimeMs: 0, exists: false };
33047
+ }
33048
+ }
33049
+ };
33050
+ function createConfigWatcher(options2) {
33051
+ const watcher = new ConfigWatcher(options2);
33052
+ watcher.start();
33053
+ return watcher;
33054
+ }
31936
33055
 
31937
33056
  // src/vscode-worker.ts
33057
+ if (process.argv.includes("--config-template")) {
33058
+ process.stdout.write(`${JSON.stringify(defaultUserConfig(), null, 2)}
33059
+ `);
33060
+ process.exit(0);
33061
+ }
33062
+ if (process.argv.includes("--config-init")) {
33063
+ const force = process.argv.includes("--force");
33064
+ await createUserConfig({ force }).then(
33065
+ (result) => {
33066
+ process.stdout.write(`${JSON.stringify({ ok: true, path: result.path, created: result.created, existed: result.existed ?? false })}
33067
+ `);
33068
+ process.exit(0);
33069
+ },
33070
+ (error) => {
33071
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
33072
+ `);
33073
+ process.exit(1);
33074
+ }
33075
+ );
33076
+ }
33077
+ if (process.argv.includes("--config-add-provider")) {
33078
+ await addProvider(jsonArg("--config-add-provider")).then(
33079
+ (result) => {
33080
+ process.stdout.write(`${JSON.stringify({ ok: true, path: result.path })}
33081
+ `);
33082
+ process.exit(0);
33083
+ },
33084
+ (error) => {
33085
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
33086
+ `);
33087
+ process.exit(1);
33088
+ }
33089
+ );
33090
+ }
33091
+ if (process.argv.includes("--config-add-model")) {
33092
+ await addModelProfile(jsonArg("--config-add-model")).then(
33093
+ (result) => {
33094
+ process.stdout.write(`${JSON.stringify({ ok: true, path: result.path })}
33095
+ `);
33096
+ process.exit(0);
33097
+ },
33098
+ (error) => {
33099
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
33100
+ `);
33101
+ process.exit(1);
33102
+ }
33103
+ );
33104
+ }
33105
+ if (process.argv.includes("--config-set-defaults")) {
33106
+ await updateConfigDefaults(jsonArg("--config-set-defaults")).then(
33107
+ (result) => {
33108
+ process.stdout.write(`${JSON.stringify({ ok: true, path: result.path })}
33109
+ `);
33110
+ process.exit(0);
33111
+ },
33112
+ (error) => {
33113
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
33114
+ `);
33115
+ process.exit(1);
33116
+ }
33117
+ );
33118
+ }
33119
+ if (process.argv.includes("--provider-auth-check")) {
33120
+ await checkProviderAuth(jsonArg("--provider-auth-check")).then(
33121
+ (result) => {
33122
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}
33123
+ `);
33124
+ process.exit(0);
33125
+ },
33126
+ (error) => {
33127
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
33128
+ `);
33129
+ process.exit(1);
33130
+ }
33131
+ );
33132
+ }
33133
+ if (process.argv.includes("--config-path")) {
33134
+ process.stdout.write(`${defaultUserConfigPath()}
33135
+ `);
33136
+ process.exit(0);
33137
+ }
31938
33138
  var options = parseOptions();
31939
33139
  var store = new TuiStore();
31940
33140
  var diffState = normalizeDiffState(options.initialSnapshot?.diff);
@@ -31956,7 +33156,7 @@ store.handleEvent = (event) => {
31956
33156
  };
31957
33157
  store.subscribe(scheduleSnapshot);
31958
33158
  process.on("message", (message) => {
31959
- void handleMessage(message).catch((error) => send({ type: "error", message: errorMessage2(error) }));
33159
+ void handleMessage(message).catch((error) => send({ type: "error", message: errorMessage4(error) }));
31960
33160
  });
31961
33161
  process.on("disconnect", () => {
31962
33162
  closed = true;
@@ -31998,7 +33198,7 @@ async function main() {
31998
33198
  },
31999
33199
  (error) => {
32000
33200
  sendSnapshotNow();
32001
- send({ type: "error", message: errorMessage2(error) });
33201
+ send({ type: "error", message: errorMessage4(error) });
32002
33202
  process.exit(1);
32003
33203
  }
32004
33204
  );
@@ -32061,21 +33261,21 @@ async function undoDiffAction(actionId) {
32061
33261
  if (!action) throw new Error("No diff action is available to undo.");
32062
33262
  if (!canUndoDiffAction(diffState, action.id)) throw new Error("Only the latest change for a file can be undone.");
32063
33263
  if (!hasBeforeSnapshot(action)) throw new Error("This diff was not recorded with enough content to undo safely.");
32064
- const target = path31.resolve(options.cwd, action.path);
32065
- const relative = path31.relative(options.cwd, target);
32066
- if (relative.startsWith("..") || path31.isAbsolute(relative)) throw new Error("Refusing to undo a file outside the workspace.");
33264
+ const target = path35.resolve(options.cwd, action.path);
33265
+ const relative = path35.relative(options.cwd, target);
33266
+ if (relative.startsWith("..") || path35.isAbsolute(relative)) throw new Error("Refusing to undo a file outside the workspace.");
32067
33267
  if (!action.beforeExists) {
32068
33268
  await rm4(target, { force: true });
32069
33269
  } else {
32070
- await mkdir13(path31.dirname(target), { recursive: true });
32071
- await writeFile12(target, action.beforeContent ?? "", "utf8");
33270
+ await mkdir15(path35.dirname(target), { recursive: true });
33271
+ await writeFile14(target, action.beforeContent ?? "", "utf8");
32072
33272
  }
32073
33273
  markDiffActionUndone(action.id);
32074
33274
  send({ type: "diffUndoCompleted", actionId: action.id, path: action.path });
32075
33275
  sendSnapshotNow();
32076
33276
  }
32077
33277
  function submitPrompt(prompt, displayPrompt, images = []) {
32078
- if (isGoalClearPrompt(prompt)) clearRestoredGoal();
33278
+ if (isGoalCommandPrompt(prompt)) clearRestoredGoal();
32079
33279
  if (!isCompactCommand(prompt)) clearResolvedRestoredWorkPlan();
32080
33280
  if (displayPrompt && displayPrompt !== prompt) displayPromptOverrides.push({ prompt, displayPrompt });
32081
33281
  pendingImages = images;
@@ -32089,6 +33289,32 @@ function submitGoalCommand(command) {
32089
33289
  if (store.snapshot().inputMode !== "prompt") return;
32090
33290
  submitPrompt(`/goal ${normalized}`);
32091
33291
  }
33292
+ function jsonArg(flag) {
33293
+ const index = process.argv.indexOf(flag);
33294
+ const raw = index >= 0 ? process.argv[index + 1] : void 0;
33295
+ if (!raw) throw new Error(`${flag} requires a JSON payload.`);
33296
+ const parsed = JSON.parse(raw);
33297
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${flag} payload must be an object.`);
33298
+ return parsed;
33299
+ }
33300
+ async function checkProviderAuth(options2) {
33301
+ const config = await loadConfig({ cwd: process.cwd(), configPath: options2.configPath });
33302
+ const providerName = typeof options2.provider === "string" && options2.provider.trim() ? options2.provider.trim() : config.defaultProvider;
33303
+ const provider = config.providers[providerName];
33304
+ if (!provider) throw new Error(`Unknown provider: ${providerName}`);
33305
+ const catalog = await listProviderModelCatalog(providerName, provider, {
33306
+ refresh: options2.refresh !== false,
33307
+ timeoutMs: typeof options2.timeoutMs === "number" && options2.timeoutMs > 0 ? options2.timeoutMs : 5e3
33308
+ });
33309
+ return {
33310
+ providerName,
33311
+ providerType: provider.type,
33312
+ status: catalog.status,
33313
+ source: catalog.source,
33314
+ message: catalog.message,
33315
+ models: catalog.models.map((model) => ({ name: model.name, displayName: model.displayName, model: model.model, source: model.source, isDefault: model.isDefault }))
33316
+ };
33317
+ }
32092
33318
  function displayUserMessageEvent(event) {
32093
33319
  if (isNestedInvocationEvent2(event)) return event;
32094
33320
  const index = displayPromptOverrides.findIndex((item) => item.prompt === event.text);
@@ -32183,8 +33409,8 @@ function clearResolvedRestoredWorkPlan() {
32183
33409
  progressNotes: []
32184
33410
  };
32185
33411
  }
32186
- function isGoalClearPrompt(prompt) {
32187
- return /^\/(?:goal|ralph-loop)\s+(?:clear|reset)\b/i.test(String(prompt || "").trim());
33412
+ function isGoalCommandPrompt(prompt) {
33413
+ return /^\/(?:goal|ralph-loop)\b/i.test(String(prompt || "").trim());
32188
33414
  }
32189
33415
  function normalizeGoalCommand(command) {
32190
33416
  return command === "pause" || command === "resume" || command === "clear" ? command : void 0;
@@ -32401,7 +33627,7 @@ function numberOrZero(value) {
32401
33627
  }
32402
33628
  function compactEvent(event) {
32403
33629
  if (!event || typeof event !== "object") return event;
32404
- if (event.type === "model.text.delta") return { ...event, text: void 0, textLength: event.text?.length ?? 0 };
33630
+ if (event.type === "model.text.delta") return { ...event, textLength: event.text?.length ?? 0 };
32405
33631
  return event;
32406
33632
  }
32407
33633
  function send(message) {
@@ -32440,11 +33666,16 @@ async function loadAndSendConfigMeta() {
32440
33666
  }
32441
33667
  }) : config;
32442
33668
  applyInitialApiKeys();
32443
- sendConfigMeta();
33669
+ void sendConfigMeta();
32444
33670
  } catch (error) {
32445
- send({ type: "error", message: errorMessage2(error) });
33671
+ send({ type: "error", message: errorMessage4(error) });
32446
33672
  }
32447
33673
  }
33674
+ var configWatcher = createConfigWatcher();
33675
+ configWatcher.on("changed", () => {
33676
+ invalidateCatalogPingCache();
33677
+ void loadAndSendConfigMeta();
33678
+ });
32448
33679
  function applyInitialApiKeys() {
32449
33680
  for (const [provider, apiKey] of Object.entries(options.apiKeys ?? {})) {
32450
33681
  if (typeof apiKey === "string") setApiKey(provider, apiKey, { quiet: true });
@@ -32454,12 +33685,18 @@ function setApiKey(providerName, apiKey, opts = {}) {
32454
33685
  const provider = loadedConfig?.providers?.[providerName];
32455
33686
  const envName = provider?.apiKeyEnv;
32456
33687
  if (envName && apiKey.trim()) process.env[envName] = apiKey.trim();
32457
- if (!opts.quiet) sendConfigMeta();
33688
+ if (!opts.quiet) void sendConfigMeta();
32458
33689
  }
32459
- function sendConfigMeta() {
33690
+ async function sendConfigMeta() {
32460
33691
  if (!loadedConfig) return;
32461
33692
  const registry = new AgentRegistry();
32462
33693
  for (const agentDefinition of Object.values(loadedConfig.agents ?? {})) registry.register(agentDefinition, { scope: "user", trusted: true });
33694
+ const providerEntries = sortProviderNames(Object.keys(loadedConfig.providers)).map((name) => [name, loadedConfig.providers[name]]).filter(([, provider]) => provider && !provider.hidden);
33695
+ const providerOrder = new Map(providerEntries.map(([name], index) => [name, index]));
33696
+ const providers = (await Promise.all(providerEntries.map(([name, provider]) => providerMeta(name, provider)))).sort((left, right) => {
33697
+ if (left.available !== right.available) return left.available ? -1 : 1;
33698
+ return (providerOrder.get(left.name) ?? 0) - (providerOrder.get(right.name) ?? 0);
33699
+ });
32463
33700
  send({
32464
33701
  type: "configMeta",
32465
33702
  config: {
@@ -32468,30 +33705,61 @@ function sendConfigMeta() {
32468
33705
  context: {
32469
33706
  main: loadedConfig.context?.main
32470
33707
  },
32471
- providers: Object.entries(loadedConfig.providers).filter(([, provider]) => !provider.hidden).map(([name, provider]) => providerMeta(name, provider)),
33708
+ providerOrder: providers.map((provider) => provider.name),
33709
+ providers,
32472
33710
  agents: registry.list().filter((agent) => !agent.hidden && isPrimaryAgent(agent)).map((agent) => ({ name: agent.name, description: agent.description }))
32473
33711
  }
32474
33712
  });
32475
33713
  }
32476
- function providerMeta(name, provider) {
33714
+ async function providerMeta(name, provider) {
32477
33715
  const apiKeyEnv = typeof provider.apiKeyEnv === "string" ? provider.apiKeyEnv : void 0;
32478
- const extraModels = [...asStringArray(provider.models), ...asStringArray(provider.modelOptions), ...asStringArray(provider.deployments)];
32479
- const models = [...new Set([provider.model, ...extraModels].filter((item) => typeof item === "string" && item.trim()))];
33716
+ const catalog = await listProviderModelCatalog(name, provider).catch((error) => ({ status: "unavailable", providerName: name, models: [], source: "fallback", message: errorMessage4(error) }));
33717
+ const available = providerCatalogAvailable2(catalog);
33718
+ const modelProfiles = catalog.models.length ? catalog.models.map((model) => ({
33719
+ name: model.name,
33720
+ displayName: model.displayName ?? model.name,
33721
+ model: model.model,
33722
+ description: model.description,
33723
+ source: model.source,
33724
+ isDefault: model.isDefault
33725
+ })) : providerModelProfiles(name, provider);
33726
+ const labeledProfiles = modelProfiles.map((profile) => {
33727
+ const displayName = profile.displayName ?? profile.name ?? profile.model;
33728
+ return {
33729
+ ...profile,
33730
+ label: available ? displayName : unavailableLabel2(displayName),
33731
+ available
33732
+ };
33733
+ });
33734
+ const models = modelProfiles.length ? [...new Set(modelProfiles.map((item) => item.model).filter((item) => typeof item === "string" && item.trim()))] : providerModelOptions(name, provider);
33735
+ const defaultModel = defaultModelForProvider(name, provider);
32480
33736
  return {
32481
33737
  name,
33738
+ label: available ? name : unavailableLabel2(name),
33739
+ available,
32482
33740
  type: provider.type,
32483
33741
  runtime: provider.runtime,
32484
33742
  permissionMode: provider.permissionMode,
32485
33743
  ga: provider.ga === true || process.env.DEMIAN_CLAUDECODE_GA === "1",
32486
- model: provider.model,
33744
+ model: defaultModel,
33745
+ modelLabel: available || !defaultModel ? defaultModel : unavailableLabel2(defaultModel),
32487
33746
  models,
33747
+ modelProfiles: labeledProfiles,
32488
33748
  baseURL: provider.baseURL,
32489
33749
  apiKeyEnv,
33750
+ apiKeyEnvAliases: asStringArray(provider.apiKeyEnvAliases),
33751
+ catalog: { status: catalog.status, source: catalog.source, message: catalog.message },
32490
33752
  hasInlineApiKey: typeof provider.apiKey === "string" && provider.apiKey.length > 0,
32491
- hasEnvApiKey: !!(apiKeyEnv && process.env[apiKeyEnv]),
32492
- requiresApiKey: provider.type === "openai-compatible" || provider.type === "anthropic"
33753
+ hasEnvApiKey: !![apiKeyEnv, ...asStringArray(provider.apiKeyEnvAliases)].find((envName) => envName && process.env[envName]),
33754
+ requiresApiKey: provider.type === "openai-compatible" || provider.type === "anthropic" || provider.type === "ollama"
32493
33755
  };
32494
33756
  }
33757
+ function providerCatalogAvailable2(catalog) {
33758
+ return catalog?.status === "ready" && Array.isArray(catalog.models) && catalog.models.length > 0;
33759
+ }
33760
+ function unavailableLabel2(value) {
33761
+ return `(n/a) ${value}`;
33762
+ }
32495
33763
  function asStringArray(value) {
32496
33764
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
32497
33765
  }
@@ -32502,6 +33770,6 @@ function contextOptions(value) {
32502
33770
  if (typeof value.compactAtRatio === "number" && Number.isFinite(value.compactAtRatio) && value.compactAtRatio > 0 && value.compactAtRatio <= 1) context.compactAtRatio = value.compactAtRatio;
32503
33771
  return Object.keys(context).length ? context : void 0;
32504
33772
  }
32505
- function errorMessage2(error) {
33773
+ function errorMessage4(error) {
32506
33774
  return error instanceof Error ? error.message : String(error);
32507
33775
  }