demian-cli 1.1.0 → 1.1.2

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,9 @@
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 mkdir15, rm as rm4, writeFile as writeFile14 } from "node:fs/promises";
5
- import path35 from "node:path";
4
+ import { existsSync as existsSync5 } from "node:fs";
5
+ import { mkdir as mkdir16, rm as rm5, writeFile as writeFile15 } from "node:fs/promises";
6
+ import path36 from "node:path";
6
7
 
7
8
  // src/permissions/presets.ts
8
9
  var PERMISSION_PRESETS = ["deny", "ask", "auto", "full"];
@@ -67,6 +68,26 @@ function isCompactCommand(input2) {
67
68
  function isRetryCommand(input2) {
68
69
  return input2.trim().toLowerCase() === "/retry";
69
70
  }
71
+ function isSessionCommand(input2) {
72
+ return /^\/(?:session|sessions|conversation|conversations|conv)(?:\s|$)/i.test(input2.trim());
73
+ }
74
+ function parseSessionCommand(input2) {
75
+ const raw = input2.trim();
76
+ if (!isSessionCommand(raw)) return void 0;
77
+ const rest = raw.replace(/^\/(?:session|sessions|conversation|conversations|conv)\b/i, "").trim();
78
+ if (!rest) return { action: "help" };
79
+ const tokens = shellishSplit(rest);
80
+ const action = tokens.shift()?.toLowerCase() ?? "help";
81
+ if (action === "help" || action === "-h" || action === "--help") return { action: "help" };
82
+ if (action === "list" || action === "ls") return { action: "list" };
83
+ if (action === "current" || action === "status") return { action: "current" };
84
+ if (action === "new" || action === "create") return { action: "new", title: tokens.join(" ").trim() || void 0 };
85
+ if (action === "switch" || action === "select" || action === "use" || action === "open") return { action: "switch", target: tokens.join(" ").trim() || void 0 };
86
+ if (action === "delete" || action === "remove" || action === "rm") return { action: "delete", target: tokens.join(" ").trim() || void 0 };
87
+ if (action === "rename" || action === "name") return { action: "rename", title: tokens.join(" ").trim() || void 0 };
88
+ if (action === "clear" || action === "reset") return { action: "clear" };
89
+ return { action: "switch", target: [action, ...tokens].join(" ").trim() };
90
+ }
70
91
  function isCoworkCommand(input2) {
71
92
  return /^\/cowork(?:\s|$)/i.test(input2.trim());
72
93
  }
@@ -742,14 +763,40 @@ function isPathLikeKey(key) {
742
763
  }
743
764
 
744
765
  // src/ui/tui/store.ts
766
+ var TUI_CONFIG_ACTION_PREFIX = "demian-config:";
767
+ var TUI_SESSION_ACTION_PREFIX = "demian-session:";
768
+ function parseTuiConfigAction(prompt) {
769
+ if (!prompt.startsWith(TUI_CONFIG_ACTION_PREFIX)) return void 0;
770
+ try {
771
+ return JSON.parse(prompt.slice(TUI_CONFIG_ACTION_PREFIX.length));
772
+ } catch {
773
+ return void 0;
774
+ }
775
+ }
776
+ function parseTuiSessionAction(prompt) {
777
+ if (!prompt.startsWith(TUI_SESSION_ACTION_PREFIX)) return void 0;
778
+ try {
779
+ return JSON.parse(prompt.slice(TUI_SESSION_ACTION_PREFIX.length));
780
+ } catch {
781
+ return void 0;
782
+ }
783
+ }
745
784
  var TuiStore = class {
746
785
  #state = {
747
786
  status: {},
748
787
  blocks: [],
749
788
  activity: "starting",
750
789
  inputMode: "starting",
790
+ sessionOptions: [],
791
+ sessionCursor: 0,
792
+ sessionCancelBehavior: "exit",
751
793
  providerOptions: [],
752
794
  providerCursor: 0,
795
+ configModelCursor: 0,
796
+ configAddProviderCursor: 0,
797
+ configModelInput: "",
798
+ configProviderInput: "",
799
+ configProviderPresets: defaultConfigProviderPresets(),
753
800
  agentOptions: [],
754
801
  agentCursor: 0,
755
802
  permissionPreset: "auto",
@@ -769,6 +816,7 @@ var TuiStore = class {
769
816
  #blockId = 0;
770
817
  #stopActiveTask;
771
818
  #promptResolve;
819
+ #sessionResolve;
772
820
  #queuedPrompt;
773
821
  #exitRequested = false;
774
822
  #promptHistory = [];
@@ -784,12 +832,15 @@ var TuiStore = class {
784
832
  #pendingPermissionTimeout;
785
833
  #syntheticPermissionId = 0;
786
834
  #activeClaudeCodePlan;
835
+ #notifyTimer;
787
836
  snapshot() {
788
837
  return {
789
838
  ...this.#state,
790
839
  status: { ...this.#state.status },
791
840
  selection: this.#state.selection ? { ...this.#state.selection } : void 0,
841
+ sessionOptions: this.#state.sessionOptions.map((option) => ({ ...option })),
792
842
  providerOptions: this.#state.providerOptions.map((option) => ({ ...option })),
843
+ configProviderPresets: this.#state.configProviderPresets.map((option) => ({ ...option })),
793
844
  agentOptions: this.#state.agentOptions.map((option) => ({ ...option })),
794
845
  blocks: this.#state.blocks.map((block) => ({ ...block, lines: [...block.lines], toolDetails: cloneToolRunDetails(block.toolDetails), goalWork: cloneGoalWork(block.goalWork), cowork: cloneCoworkGroup(block.cowork) })),
795
846
  warnings: [...this.#state.warnings],
@@ -827,6 +878,7 @@ var TuiStore = class {
827
878
  requestExit() {
828
879
  this.#exitRequested = true;
829
880
  if (this.#promptResolve) this.#resolvePrompt("");
881
+ if (this.#sessionResolve) this.#resolveSession({ kind: "exit" });
830
882
  this.#stopActiveTask?.();
831
883
  this.#state.inputMode = "done";
832
884
  this.#state.done = true;
@@ -906,6 +958,84 @@ var TuiStore = class {
906
958
  this.#state.activity = `context compacted: ${summary.beforeTokenEstimate} -> ${summary.afterTokenEstimate}`;
907
959
  this.#notify();
908
960
  }
961
+ conversationSnapshot() {
962
+ const snapshot = this.snapshot();
963
+ return {
964
+ ...snapshot,
965
+ inputMode: "prompt",
966
+ promptInput: "",
967
+ promptError: void 0,
968
+ streamingText: "",
969
+ streamingFinalized: true,
970
+ permission: void 0,
971
+ done: false
972
+ };
973
+ }
974
+ restoreConversationSnapshot(snapshot, fallback) {
975
+ this.#resetTransientConversationState();
976
+ const currentSettings = {
977
+ selection: this.#state.selection,
978
+ providerOptions: this.#state.providerOptions,
979
+ providerCursor: this.#state.providerCursor,
980
+ agentOptions: this.#state.agentOptions,
981
+ agentCursor: this.#state.agentCursor,
982
+ selectedAgent: this.#state.selectedAgent,
983
+ permissionPreset: this.#state.permissionPreset,
984
+ permissionPresetCursor: this.#state.permissionPresetCursor,
985
+ contextEfficiency: this.#state.contextEfficiency
986
+ };
987
+ const restoredSelection = snapshot?.selection ? { ...snapshot.selection } : currentSettings.selection;
988
+ const restoredAgent = snapshot?.selectedAgent ?? snapshot?.status?.agent ?? currentSettings.selectedAgent;
989
+ const restoredPermissionPreset = normalizePermissionPreset(snapshot?.permissionPreset, currentSettings.permissionPreset);
990
+ this.#state = {
991
+ ...this.#state,
992
+ ...safeConversationSnapshot(snapshot),
993
+ ...currentSettings,
994
+ selection: restoredSelection,
995
+ providerCursor: Math.max(0, currentSettings.providerOptions.findIndex((option) => option.name === restoredSelection?.providerName)),
996
+ selectedAgent: restoredAgent,
997
+ agentCursor: Math.max(0, currentSettings.agentOptions.findIndex((option) => option.name === restoredAgent)),
998
+ permissionPreset: restoredPermissionPreset,
999
+ permissionPresetCursor: Math.max(0, PERMISSION_PRESETS.indexOf(restoredPermissionPreset)),
1000
+ status: {
1001
+ ...snapshot?.status ?? this.#state.status,
1002
+ sessionId: fallback.sessionId,
1003
+ cwd: fallback.cwd,
1004
+ provider: restoredSelection?.providerName ?? snapshot?.status?.provider ?? this.#state.status.provider,
1005
+ model: restoredSelection?.model ?? snapshot?.status?.model ?? this.#state.status.model,
1006
+ agent: restoredAgent ?? snapshot?.status?.agent ?? this.#state.status.agent
1007
+ },
1008
+ inputMode: "prompt",
1009
+ promptInput: "",
1010
+ promptError: void 0,
1011
+ streamingText: "",
1012
+ streamingFinalized: true,
1013
+ permission: void 0,
1014
+ done: false,
1015
+ activity: `conversation: ${fallback.title}`
1016
+ };
1017
+ this.#blockId = nextBlockId(this.#state.blocks);
1018
+ this.#notify();
1019
+ }
1020
+ clearConversation(title = "New session") {
1021
+ this.#resetTransientConversationState();
1022
+ this.#state.blocks = [];
1023
+ this.#state.streamingText = "";
1024
+ this.#state.streamingFinalized = true;
1025
+ this.#state.permission = void 0;
1026
+ this.#state.warnings = [];
1027
+ this.#state.done = false;
1028
+ this.#state.finalAnswer = void 0;
1029
+ this.#state.progressNotes = [];
1030
+ this.#state.turnDiff = void 0;
1031
+ this.#state.diffExpanded = false;
1032
+ this.#state.workPlan = void 0;
1033
+ this.#state.workPlanExpanded = false;
1034
+ this.#state.goal = void 0;
1035
+ this.#state.pendingClaudeCodePlan = void 0;
1036
+ this.#state.activity = `cleared conversation: ${title}`;
1037
+ this.prepareForPrompt("waiting for next message");
1038
+ }
909
1039
  prepareForPrompt(message = "waiting for next message") {
910
1040
  if (this.#exitRequested) return;
911
1041
  this.#state.inputMode = "prompt";
@@ -932,6 +1062,10 @@ var TuiStore = class {
932
1062
  agent: context.agent,
933
1063
  cwd: context.cwd
934
1064
  };
1065
+ this.#state.configPath = context.configPath;
1066
+ this.#state.configCreated = context.configCreated;
1067
+ this.#state.configDefaultProvider = context.configDefaultProvider;
1068
+ this.#state.configMessage = context.configMessage;
935
1069
  if (context.contextEfficiency) {
936
1070
  this.#state.contextEfficiency = {
937
1071
  ...this.#state.contextEfficiency ?? { selectedTools: [] },
@@ -963,7 +1097,58 @@ var TuiStore = class {
963
1097
  currentPermissionPreset() {
964
1098
  return this.#state.permissionPreset;
965
1099
  }
1100
+ requestSessionSelection(options2, behavior = {}) {
1101
+ if (this.#exitRequested) return Promise.resolve({ kind: "exit" });
1102
+ const normalized = options2.length > 0 ? options2.map((option) => ({ ...option })) : [{ kind: "new", title: "New session", status: "ready", currentWorkspace: true }];
1103
+ return new Promise((resolve) => {
1104
+ this.#sessionResolve = resolve;
1105
+ this.#state.inputMode = "session";
1106
+ this.#state.sessionOptions = normalized;
1107
+ this.#state.sessionCursor = 0;
1108
+ this.#state.sessionCancelBehavior = behavior.cancel ?? "exit";
1109
+ this.#state.promptInput = "";
1110
+ this.#state.promptError = void 0;
1111
+ this.#state.streamingText = "";
1112
+ this.#state.streamingFinalized = true;
1113
+ this.#state.done = false;
1114
+ this.#state.activity = "select session";
1115
+ this.#notify();
1116
+ });
1117
+ }
1118
+ cancelSessionSelection() {
1119
+ if (this.#state.inputMode !== "session") return false;
1120
+ if (this.#state.sessionCancelBehavior === "prompt") {
1121
+ this.#resolveSession({ kind: "cancel" });
1122
+ return false;
1123
+ }
1124
+ return true;
1125
+ }
1126
+ moveSessionCursor(delta) {
1127
+ if (this.#state.inputMode !== "session" || this.#state.sessionOptions.length === 0) return;
1128
+ const count = this.#state.sessionOptions.length;
1129
+ this.#state.sessionCursor = (this.#state.sessionCursor + delta + count) % count;
1130
+ this.#notify();
1131
+ }
1132
+ submitSessionSelection() {
1133
+ if (this.#state.inputMode !== "session") return;
1134
+ const option = this.#state.sessionOptions[this.#state.sessionCursor];
1135
+ if (!option) return;
1136
+ if (option.kind === "new") {
1137
+ this.#resolveSession({ kind: "new" });
1138
+ return;
1139
+ }
1140
+ if (option.id) this.#resolveSession({ kind: "session", id: option.id });
1141
+ }
1142
+ submitNewSessionSelection() {
1143
+ if (this.#state.inputMode !== "session") return;
1144
+ this.#resolveSession({ kind: "new" });
1145
+ }
1146
+ submitSessionSelectionShortcut() {
1147
+ if (this.#state.inputMode !== "prompt" || this.#state.promptInput.trim()) return;
1148
+ this.#resolvePrompt(`${TUI_SESSION_ACTION_PREFIX}${JSON.stringify({ type: "select" })}`);
1149
+ }
966
1150
  requestPrompt(initialPrompt) {
1151
+ if (this.#exitRequested) return Promise.resolve("");
967
1152
  const prompt = (initialPrompt ?? "").trim();
968
1153
  if (prompt) {
969
1154
  this.#rememberPrompt(prompt);
@@ -1004,12 +1189,12 @@ var TuiStore = class {
1004
1189
  this.#notify();
1005
1190
  });
1006
1191
  }
1007
- appendPromptInput(input2) {
1192
+ appendPromptInput(input2, options2 = {}) {
1008
1193
  if (this.#state.inputMode !== "prompt" && this.#state.inputMode !== "running") return;
1009
1194
  if (this.#state.inputMode === "prompt") this.#detachPromptHistory();
1010
1195
  this.#state.promptInput += input2;
1011
1196
  this.#state.promptError = void 0;
1012
- this.#notify();
1197
+ if (options2.notify !== false) this.#notify();
1013
1198
  }
1014
1199
  navigatePromptHistory(delta) {
1015
1200
  if (this.#state.inputMode !== "prompt" || this.#promptHistory.length === 0) return;
@@ -1059,6 +1244,143 @@ var TuiStore = class {
1059
1244
  this.#state.activity = `provider ${option.name} selected`;
1060
1245
  this.#notify();
1061
1246
  }
1247
+ openConfigManager(message) {
1248
+ const canOpenFromConfig = this.#state.inputMode === "config" || this.#state.inputMode === "configModel" || this.#state.inputMode === "configModelInput" || this.#state.inputMode === "configProviderInput" || this.#state.inputMode === "configAddProvider";
1249
+ if (!canOpenFromConfig && (this.#state.inputMode !== "prompt" || this.#state.promptInput.trim())) return;
1250
+ this.#state.inputMode = "config";
1251
+ const current = this.#state.selection?.providerName ?? this.#state.status.provider;
1252
+ this.#state.providerCursor = Math.max(0, this.#state.providerOptions.findIndex((option) => option.name === current));
1253
+ this.#state.configMessage = message ?? this.#state.configMessage;
1254
+ this.#state.settingsError = void 0;
1255
+ this.#state.activity = "configure providers and models";
1256
+ this.#notify();
1257
+ }
1258
+ openConfigProviderInput(field) {
1259
+ if (this.#state.inputMode !== "config") return;
1260
+ const provider = this.#selectedConfigProvider();
1261
+ if (!provider) return;
1262
+ this.#state.inputMode = "configProviderInput";
1263
+ this.#state.configProviderInputField = field;
1264
+ this.#state.configProviderInput = configProviderFieldValue(provider, field);
1265
+ this.#state.settingsError = void 0;
1266
+ this.#state.activity = `edit ${field} for ${provider.name}`;
1267
+ this.#notify();
1268
+ }
1269
+ appendConfigProviderInput(input2) {
1270
+ if (this.#state.inputMode !== "configProviderInput") return;
1271
+ this.#state.configProviderInput += input2;
1272
+ this.#state.settingsError = void 0;
1273
+ this.#notify();
1274
+ }
1275
+ backspaceConfigProviderInput() {
1276
+ if (this.#state.inputMode !== "configProviderInput") return;
1277
+ this.#state.configProviderInput = Array.from(this.#state.configProviderInput).slice(0, -1).join("");
1278
+ this.#state.settingsError = void 0;
1279
+ this.#notify();
1280
+ }
1281
+ submitConfigProviderInput() {
1282
+ if (this.#state.inputMode !== "configProviderInput") return;
1283
+ const provider = this.#selectedConfigProvider();
1284
+ const field = this.#state.configProviderInputField;
1285
+ if (!provider || !field) return;
1286
+ this.#submitConfigAction({ type: "updateProvider", provider: provider.name, field, value: this.#state.configProviderInput });
1287
+ }
1288
+ moveConfigProviderCursor(delta) {
1289
+ if (this.#state.inputMode !== "config" || this.#state.providerOptions.length === 0) return;
1290
+ const count = this.#state.providerOptions.length;
1291
+ this.#state.providerCursor = (this.#state.providerCursor + delta + count) % count;
1292
+ this.#notify();
1293
+ }
1294
+ submitConfigDefaultProvider() {
1295
+ if (this.#state.inputMode !== "config") return;
1296
+ const option = this.#selectedConfigProvider();
1297
+ if (!option) return;
1298
+ this.#submitConfigAction({ type: "setDefaultProvider", provider: option.name });
1299
+ }
1300
+ openConfigModelSelector() {
1301
+ if (this.#state.inputMode !== "config" && this.#state.inputMode !== "configModelInput") return;
1302
+ const option = this.#selectedConfigProvider();
1303
+ if (!option) return;
1304
+ this.#state.inputMode = "configModel";
1305
+ this.#state.configModelCursor = 0;
1306
+ this.#state.settingsError = void 0;
1307
+ this.#state.activity = `choose default model for ${option.name}`;
1308
+ this.#notify();
1309
+ }
1310
+ moveConfigModelCursor(delta) {
1311
+ if (this.#state.inputMode !== "configModel") return;
1312
+ const profiles = this.#selectedConfigProvider()?.modelProfiles ?? [];
1313
+ if (profiles.length === 0) return;
1314
+ this.#state.configModelCursor = (this.#state.configModelCursor + delta + profiles.length) % profiles.length;
1315
+ this.#notify();
1316
+ }
1317
+ submitConfigDefaultModel() {
1318
+ if (this.#state.inputMode !== "configModel") return;
1319
+ const provider = this.#selectedConfigProvider();
1320
+ const profile = provider?.modelProfiles?.[this.#state.configModelCursor];
1321
+ if (!provider || !profile?.model) return;
1322
+ this.#submitConfigAction({
1323
+ type: "setDefaultModel",
1324
+ provider: provider.name,
1325
+ profileName: profile.name,
1326
+ name: profile.name,
1327
+ displayName: profile.displayName,
1328
+ model: profile.model
1329
+ });
1330
+ }
1331
+ openConfigModelInput() {
1332
+ if (this.#state.inputMode !== "configModel" && this.#state.inputMode !== "config") return;
1333
+ if (!this.#selectedConfigProvider()) return;
1334
+ this.#state.inputMode = "configModelInput";
1335
+ this.#state.configModelInput = "";
1336
+ this.#state.settingsError = void 0;
1337
+ this.#state.activity = "add a model profile";
1338
+ this.#notify();
1339
+ }
1340
+ appendConfigModelInput(input2) {
1341
+ if (this.#state.inputMode !== "configModelInput") return;
1342
+ this.#state.configModelInput += input2;
1343
+ this.#state.settingsError = void 0;
1344
+ this.#notify();
1345
+ }
1346
+ backspaceConfigModelInput() {
1347
+ if (this.#state.inputMode !== "configModelInput") return;
1348
+ this.#state.configModelInput = Array.from(this.#state.configModelInput).slice(0, -1).join("");
1349
+ this.#state.settingsError = void 0;
1350
+ this.#notify();
1351
+ }
1352
+ submitConfigModelInput() {
1353
+ if (this.#state.inputMode !== "configModelInput") return;
1354
+ const provider = this.#selectedConfigProvider();
1355
+ const model = this.#state.configModelInput.trim();
1356
+ if (!provider || !model) {
1357
+ this.#state.settingsError = "Model ID is required.";
1358
+ this.#notify();
1359
+ return;
1360
+ }
1361
+ this.#submitConfigAction({ type: "setDefaultModel", provider: provider.name, model, displayName: model });
1362
+ }
1363
+ openConfigAddProviderSelector() {
1364
+ if (this.#state.inputMode !== "config") return;
1365
+ this.#state.inputMode = "configAddProvider";
1366
+ this.#state.configAddProviderCursor = 0;
1367
+ this.#state.settingsError = void 0;
1368
+ this.#state.activity = "add provider preset";
1369
+ this.#notify();
1370
+ }
1371
+ moveConfigAddProviderCursor(delta) {
1372
+ if (this.#state.inputMode !== "configAddProvider") return;
1373
+ const count = this.#state.configProviderPresets.length;
1374
+ if (count === 0) return;
1375
+ this.#state.configAddProviderCursor = (this.#state.configAddProviderCursor + delta + count) % count;
1376
+ this.#notify();
1377
+ }
1378
+ submitConfigAddProvider() {
1379
+ if (this.#state.inputMode !== "configAddProvider") return;
1380
+ const preset = this.#state.configProviderPresets[this.#state.configAddProviderCursor];
1381
+ if (!preset) return;
1382
+ this.#submitConfigAction({ type: "addProvider", preset: preset.preset, name: preset.name, apiKeyEnv: preset.apiKeyEnv });
1383
+ }
1062
1384
  openAgentSelector() {
1063
1385
  if (this.#state.inputMode !== "prompt" || this.#state.promptInput.trim()) return;
1064
1386
  if (this.#state.agentOptions.length === 0) {
@@ -1157,26 +1479,29 @@ var TuiStore = class {
1157
1479
  this.#notify();
1158
1480
  }
1159
1481
  closeSettings() {
1160
- if (this.#state.inputMode !== "provider" && this.#state.inputMode !== "model" && this.#state.inputMode !== "agent" && this.#state.inputMode !== "permissionPreset") return;
1482
+ if (!["provider", "model", "agent", "permissionPreset", "config", "configModel", "configModelInput", "configProviderInput", "configAddProvider"].includes(this.#state.inputMode)) return;
1161
1483
  this.#state.inputMode = "prompt";
1162
1484
  this.#state.modelInput = "";
1485
+ this.#state.configModelInput = "";
1486
+ this.#state.configProviderInput = "";
1487
+ this.#state.configProviderInputField = void 0;
1163
1488
  this.#state.settingsError = void 0;
1164
1489
  this.#state.activity = "waiting for first message";
1165
1490
  this.#notify();
1166
1491
  }
1167
- backspacePromptInput() {
1492
+ backspacePromptInput(options2 = {}) {
1168
1493
  if (this.#state.inputMode !== "prompt" && this.#state.inputMode !== "running") return;
1169
1494
  if (this.#state.inputMode === "prompt") this.#detachPromptHistory();
1170
1495
  this.#state.promptInput = Array.from(this.#state.promptInput).slice(0, -1).join("");
1171
1496
  this.#state.promptError = void 0;
1172
- this.#notify();
1497
+ if (options2.notify !== false) this.#notify();
1173
1498
  }
1174
- clearPromptInput() {
1499
+ clearPromptInput(options2 = {}) {
1175
1500
  if (this.#state.inputMode !== "prompt" && this.#state.inputMode !== "running") return;
1176
1501
  if (this.#state.inputMode === "prompt") this.#detachPromptHistory();
1177
1502
  this.#state.promptInput = "";
1178
1503
  this.#state.promptError = void 0;
1179
- this.#notify();
1504
+ if (options2.notify !== false) this.#notify();
1180
1505
  }
1181
1506
  usePendingClaudeCodePlan() {
1182
1507
  if (this.#state.inputMode !== "prompt") return false;
@@ -1288,9 +1613,12 @@ var TuiStore = class {
1288
1613
  this.#state.streamingFinalized = false;
1289
1614
  this.#activeClaudeCodePlan = this.#isClaudeCodePlanRun(event.provider) ? { text: "", providerName: event.provider, model: event.model, createdAt: event.ts } : void 0;
1290
1615
  }
1291
- if (event.type === "model.text.delta" && !isNestedInvocationEvent(event)) {
1616
+ if (event.type === "model.text.delta") {
1617
+ if (isNestedInvocationEvent(event)) return;
1292
1618
  this.#state.streamingText += event.text;
1293
1619
  this.#state.streamingFinalized = false;
1620
+ this.#notify({ deferMs: 50 });
1621
+ return;
1294
1622
  }
1295
1623
  if (event.type === "model.text" && !isNestedInvocationEvent(event)) {
1296
1624
  this.#state.streamingText = event.text;
@@ -2004,9 +2332,38 @@ var TuiStore = class {
2004
2332
  resolve(prompt);
2005
2333
  this.#notify();
2006
2334
  }
2335
+ #selectedConfigProvider() {
2336
+ return this.#state.providerOptions[this.#state.providerCursor];
2337
+ }
2338
+ #submitConfigAction(action) {
2339
+ this.#state.settingsError = void 0;
2340
+ this.#state.configModelInput = "";
2341
+ this.#state.configProviderInput = "";
2342
+ this.#state.configProviderInputField = void 0;
2343
+ this.#resolvePrompt(`${TUI_CONFIG_ACTION_PREFIX}${JSON.stringify(action)}`);
2344
+ }
2345
+ #resolveSession(selection) {
2346
+ const resolve = this.#sessionResolve;
2347
+ if (!resolve) return;
2348
+ this.#sessionResolve = void 0;
2349
+ this.#state.sessionCancelBehavior = "exit";
2350
+ if (selection.kind === "exit") {
2351
+ this.#state.inputMode = "done";
2352
+ this.#state.done = true;
2353
+ this.#state.activity = "exiting";
2354
+ } else if (selection.kind === "cancel") {
2355
+ this.#state.inputMode = "prompt";
2356
+ this.#state.activity = "waiting for next message";
2357
+ } else {
2358
+ this.#state.inputMode = "starting";
2359
+ this.#state.activity = selection.kind === "new" ? "creating session" : "opening session";
2360
+ }
2361
+ this.#notify();
2362
+ resolve(selection);
2363
+ }
2007
2364
  #rememberPrompt(prompt) {
2008
2365
  const value = prompt.trim();
2009
- if (!value || isExitCommand(value) || isStopCommand(value) || isCompactCommand(value) || isRetryCommand(value)) return;
2366
+ if (!value || isExitCommand(value) || isStopCommand(value) || isCompactCommand(value) || isRetryCommand(value) || isSessionCommand(value)) return;
2010
2367
  if (this.#promptHistory.at(-1) === value) {
2011
2368
  this.#state.canRetryLastPrompt = true;
2012
2369
  return;
@@ -2018,6 +2375,20 @@ var TuiStore = class {
2018
2375
  #lastRetryPrompt() {
2019
2376
  return this.#promptHistory.at(-1);
2020
2377
  }
2378
+ #resetTransientConversationState() {
2379
+ this.#toolRuns.clear();
2380
+ this.#goalWorkBlocks.clear();
2381
+ this.#coworkBlocks.clear();
2382
+ this.#activeGoalWorkBlockId = void 0;
2383
+ this.#currentGoalNarrationId = void 0;
2384
+ this.#timelineProgressIds.clear();
2385
+ this.#pendingPermissionCallId = void 0;
2386
+ if (this.#pendingPermissionTimeout) clearTimeout(this.#pendingPermissionTimeout);
2387
+ this.#pendingPermissionTimeout = void 0;
2388
+ this.#activeClaudeCodePlan = void 0;
2389
+ this.#promptHistoryCursor = void 0;
2390
+ this.#promptDraft = "";
2391
+ }
2021
2392
  #detachPromptHistory() {
2022
2393
  this.#promptHistoryCursor = void 0;
2023
2394
  this.#promptDraft = "";
@@ -2094,7 +2465,23 @@ var TuiStore = class {
2094
2465
  }
2095
2466
  };
2096
2467
  }
2097
- #notify() {
2468
+ #notify(options2 = {}) {
2469
+ const deferMs = options2.deferMs ?? 0;
2470
+ if (deferMs > 0) {
2471
+ if (this.#notifyTimer) return;
2472
+ this.#notifyTimer = setTimeout(() => {
2473
+ this.#notifyTimer = void 0;
2474
+ this.#emitNotify();
2475
+ }, deferMs);
2476
+ return;
2477
+ }
2478
+ if (this.#notifyTimer) {
2479
+ clearTimeout(this.#notifyTimer);
2480
+ this.#notifyTimer = void 0;
2481
+ }
2482
+ this.#emitNotify();
2483
+ }
2484
+ #emitNotify() {
2098
2485
  for (const listener of this.#listeners) listener();
2099
2486
  }
2100
2487
  };
@@ -2257,6 +2644,25 @@ function cloneContextEfficiency(context) {
2257
2644
  lastCompaction: context.lastCompaction ? { ...context.lastCompaction } : void 0
2258
2645
  };
2259
2646
  }
2647
+ function configProviderFieldValue(provider, field) {
2648
+ if (field === "baseURL") return provider.baseURL ?? "";
2649
+ if (field === "apiKeyEnv") return provider.apiKeyEnv ?? "";
2650
+ return provider.auth?.header ?? "";
2651
+ }
2652
+ function defaultConfigProviderPresets() {
2653
+ return [
2654
+ { label: "OpenAI", preset: "openai", name: "openai", apiKeyEnv: "OPENAI_API_KEY", detail: "OpenAI API" },
2655
+ { label: "Anthropic", preset: "anthropic", name: "anthropic", apiKeyEnv: "ANTHROPIC_API_KEY", detail: "Anthropic API" },
2656
+ { label: "Gemini", preset: "gemini", name: "gemini", apiKeyEnv: "GEMINI_API_KEY", detail: "Google Gemini OpenAI-compatible API" },
2657
+ { label: "Groq", preset: "groq", name: "groq", apiKeyEnv: "GROQ_API_KEY", detail: "Groq Cloud" },
2658
+ { label: "Codex", preset: "codex", name: "codex", detail: "ChatGPT Codex OAuth" },
2659
+ { label: "Claude Code", preset: "claudecode", name: "claudecode", detail: "Local Claude Code runtime" },
2660
+ { label: "Ollama local", preset: "ollama-local", name: "ollama-local", detail: "http://localhost:11434" },
2661
+ { label: "LM Studio", preset: "lmstudio", name: "lmstudio", detail: "http://localhost:1234" },
2662
+ { label: "llama.cpp", preset: "llamacpp", name: "llamacpp", detail: "http://localhost:8080" },
2663
+ { label: "vLLM", preset: "vllm", name: "vllm", detail: "http://localhost:8000" }
2664
+ ];
2665
+ }
2260
2666
  function cloneToolRunDetails(details) {
2261
2667
  if (!details) return void 0;
2262
2668
  return {
@@ -2309,6 +2715,36 @@ function cloneGoalToolRun(tool) {
2309
2715
  details: cloneToolRunDetails(tool.details)
2310
2716
  };
2311
2717
  }
2718
+ function safeConversationSnapshot(snapshot) {
2719
+ if (!snapshot || typeof snapshot !== "object") return {};
2720
+ return {
2721
+ status: snapshot.status ? { ...snapshot.status } : void 0,
2722
+ blocks: Array.isArray(snapshot.blocks) ? snapshot.blocks.map((block) => ({ ...block, lines: [...block.lines ?? []], toolDetails: cloneToolRunDetails(block.toolDetails), goalWork: cloneGoalWork(block.goalWork), cowork: cloneCoworkGroup(block.cowork) })) : [],
2723
+ activity: typeof snapshot.activity === "string" ? snapshot.activity : "restored conversation",
2724
+ warnings: Array.isArray(snapshot.warnings) ? [...snapshot.warnings] : [],
2725
+ finalAnswer: snapshot.finalAnswer,
2726
+ progressNotes: Array.isArray(snapshot.progressNotes) ? snapshot.progressNotes.map((note) => ({ ...note })) : [],
2727
+ turnDiff: snapshot.turnDiff ? cloneDiffSummary(snapshot.turnDiff) : void 0,
2728
+ diffExpanded: snapshot.diffExpanded === true,
2729
+ workPlan: snapshot.workPlan ? {
2730
+ ...snapshot.workPlan,
2731
+ steps: snapshot.workPlan.steps.map((step) => ({ ...step }))
2732
+ } : void 0,
2733
+ workPlanExpanded: snapshot.workPlanExpanded === true,
2734
+ goal: snapshot.goal ? { ...snapshot.goal } : void 0,
2735
+ contextEfficiency: snapshot.contextEfficiency ? cloneContextEfficiency(snapshot.contextEfficiency) : void 0,
2736
+ canRetryLastPrompt: snapshot.canRetryLastPrompt === true,
2737
+ pendingClaudeCodePlan: snapshot.pendingClaudeCodePlan ? { ...snapshot.pendingClaudeCodePlan } : void 0
2738
+ };
2739
+ }
2740
+ function nextBlockId(blocks) {
2741
+ let max = 0;
2742
+ for (const block of blocks ?? []) {
2743
+ const match = /^block_(\d+)$/.exec(block.id);
2744
+ if (match) max = Math.max(max, Number(match[1]));
2745
+ }
2746
+ return max;
2747
+ }
2312
2748
  function formatDuration2(durationMs) {
2313
2749
  if (durationMs < 1e3) return `${durationMs}ms`;
2314
2750
  return `${Math.round(durationMs / 1e3)}s`;
@@ -2327,7 +2763,8 @@ function buildClaudeCodePlanExecutionPrompt(planText, requestText) {
2327
2763
  }
2328
2764
 
2329
2765
  // src/ui/tui/controller.ts
2330
- import path32 from "node:path";
2766
+ import path34 from "node:path";
2767
+ import { existsSync as existsSync4 } from "node:fs";
2331
2768
 
2332
2769
  // src/agents/types.ts
2333
2770
  function normalizeAgent(input2) {
@@ -25421,8 +25858,8 @@ function permissionLabel(req) {
25421
25858
  if ((req.tool === "write_file" || req.tool === "edit_file" || req.tool === "read_file") && typeof inputObject.path === "string") {
25422
25859
  return `${req.tool}: ${inputObject.path}`;
25423
25860
  }
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}`;
25861
+ const path37 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
25862
+ if (path37) return `${tool}: ${path37}`;
25426
25863
  return tool;
25427
25864
  }
25428
25865
 
@@ -26766,10 +27203,380 @@ function claudeCodeRuntimePolicyHash(config) {
26766
27203
  }
26767
27204
  var CLAUDE_CODE_PERMISSION_PROFILE_KEYS = ["permissionMode", "defaultDecision", "allowedTools", "disallowedTools", "tools", "allowSubagents"];
26768
27205
 
26769
- // src/transcript.ts
26770
- import { mkdir as mkdir6, appendFile, writeFile as writeFile5 } from "node:fs/promises";
27206
+ // src/config-scaffold.ts
27207
+ import { chmod as chmod3, mkdir as mkdir6, readFile as readFile7, rename as rename4, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
26771
27208
  import os8 from "node:os";
26772
27209
  import path14 from "node:path";
27210
+ function defaultUserConfigPath() {
27211
+ return path14.join(os8.homedir(), ".demian", "config.json");
27212
+ }
27213
+ function defaultUserConfig(defaultProvider = detectDefaultProvider()) {
27214
+ return {
27215
+ version: 2,
27216
+ defaultProvider,
27217
+ providers: {
27218
+ openai: {
27219
+ type: "openai-compatible",
27220
+ baseURL: "https://api.openai.com/v1",
27221
+ apiKey: "",
27222
+ apiKeyEnv: "OPENAI_API_KEY",
27223
+ catalog: { type: "openai-models", endpoint: "https://api.openai.com/v1/models" }
27224
+ },
27225
+ anthropic: {
27226
+ type: "anthropic",
27227
+ baseURL: "https://api.anthropic.com/v1",
27228
+ apiKey: "",
27229
+ apiKeyEnv: "ANTHROPIC_API_KEY",
27230
+ catalog: { type: "anthropic-models", endpoint: "https://api.anthropic.com/v1/models" }
27231
+ },
27232
+ gemini: {
27233
+ type: "openai-compatible",
27234
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
27235
+ apiKey: "",
27236
+ apiKeyEnv: "GEMINI_API_KEY",
27237
+ apiKeyEnvAliases: ["GOOGLE_API_KEY"],
27238
+ catalog: { type: "gemini-openai-models", endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models" }
27239
+ },
27240
+ groq: {
27241
+ type: "openai-compatible",
27242
+ baseURL: "https://api.groq.com/openai/v1",
27243
+ apiKey: "",
27244
+ apiKeyEnv: "GROQ_API_KEY",
27245
+ catalog: { type: "groq-models", endpoint: "https://api.groq.com/openai/v1/models" }
27246
+ },
27247
+ azure: {
27248
+ type: "openai-compatible",
27249
+ auth: { type: "api-key", header: "api-key" },
27250
+ modelProfiles: [
27251
+ {
27252
+ name: "azure-example",
27253
+ displayName: "Azure example",
27254
+ model: "azure-deployment-name",
27255
+ baseURL: "https://example.openai.azure.com/openai/v1",
27256
+ apiKey: "",
27257
+ apiKeyEnv: "AZURE_OPENAI_API_KEY"
27258
+ }
27259
+ ]
27260
+ },
27261
+ lmstudio: {
27262
+ type: "openai-compatible",
27263
+ baseURL: "http://localhost:1234/v1",
27264
+ apiKey: "lm-studio",
27265
+ modelProfiles: [],
27266
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:1234/v1/models" }
27267
+ },
27268
+ "ollama-local": {
27269
+ type: "openai-compatible",
27270
+ baseURL: "http://localhost:11434/v1",
27271
+ apiKey: "ollama",
27272
+ modelProfiles: [],
27273
+ quirks: { omitTemperature: true },
27274
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:11434/v1/models" }
27275
+ },
27276
+ "ollama-cloud": {
27277
+ type: "ollama",
27278
+ baseURL: "https://ollama.com/api",
27279
+ apiKey: "",
27280
+ apiKeyEnv: "OLLAMA_API_KEY",
27281
+ modelProfiles: [],
27282
+ catalog: { type: "ollama-tags", endpoint: "https://ollama.com/api/tags" }
27283
+ },
27284
+ llamacpp: {
27285
+ type: "openai-compatible",
27286
+ baseURL: "http://localhost:8080/v1",
27287
+ apiKey: "llama.cpp",
27288
+ modelProfiles: [],
27289
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8080/v1/models" }
27290
+ },
27291
+ vllm: {
27292
+ type: "openai-compatible",
27293
+ baseURL: "http://localhost:8000/v1",
27294
+ apiKey: "vllm",
27295
+ modelProfiles: [],
27296
+ catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8000/v1/models" }
27297
+ },
27298
+ codex: {
27299
+ type: "codex",
27300
+ baseURL: "https://chatgpt.com/backend-api/codex",
27301
+ authStore: "auto",
27302
+ allowApiKeyFallback: false,
27303
+ promptCacheKey: "root-session",
27304
+ catalog: { type: "codex-oauth-models" }
27305
+ },
27306
+ claudecode: {
27307
+ type: "claudecode",
27308
+ runtime: "agent-sdk",
27309
+ cliPath: "~/.local/bin/claude",
27310
+ cwdMode: "session",
27311
+ historyPolicy: "passthrough-resume",
27312
+ onInvalidResume: "fresh",
27313
+ attachmentFallback: "block",
27314
+ allowSubagents: false,
27315
+ sanitizeApiKeyEnv: true,
27316
+ authPreflight: true,
27317
+ useBareMode: false,
27318
+ usageLedgerScope: "process",
27319
+ sessionLock: true,
27320
+ abortPolicy: "record-only",
27321
+ catalog: { type: "claudecode-supported-models" }
27322
+ }
27323
+ }
27324
+ };
27325
+ }
27326
+ async function createUserConfig(options2 = {}) {
27327
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
27328
+ const content = `${JSON.stringify(defaultUserConfig(options2.defaultProvider), null, 2)}
27329
+ `;
27330
+ if (options2.print) return { path: filePath, created: false, content };
27331
+ const existed = await exists(filePath);
27332
+ if (existed && !options2.force) return { path: filePath, created: false, content: await readFile7(filePath, "utf8"), existed: true };
27333
+ await writeJsonAtomic(filePath, content);
27334
+ return { path: filePath, created: true, content, existed };
27335
+ }
27336
+ async function addProvider(options2) {
27337
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
27338
+ const config = await readConfigObject(filePath);
27339
+ const providers = objectValue(config.providers);
27340
+ const name = options2.name;
27341
+ if (providers[name] && !options2.force) throw new Error(`Provider ${name} already exists. Use --force to overwrite it.`);
27342
+ providers[name] = providerPreset(options2);
27343
+ config.providers = providers;
27344
+ const content = `${JSON.stringify(config, null, 2)}
27345
+ `;
27346
+ await writeJsonAtomic(filePath, content);
27347
+ return { path: filePath, created: true, content };
27348
+ }
27349
+ async function addModelProfile(options2) {
27350
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
27351
+ const config = await readConfigObject(filePath);
27352
+ const providers = objectValue(config.providers);
27353
+ const provider = objectValue(providers[options2.provider]);
27354
+ const profiles = Array.isArray(provider.modelProfiles) ? [...provider.modelProfiles] : [];
27355
+ const displayName = options2.displayName ?? options2.name;
27356
+ const previousName = normalizeOptionalName(options2.previousName);
27357
+ const existingByPreviousName = previousName ? profiles.findIndex((entry) => entry.name === previousName) : -1;
27358
+ const existingByName = profiles.findIndex((entry) => entry.name === options2.name);
27359
+ const existingByDisplay = profiles.findIndex((entry) => entry.displayName === displayName);
27360
+ const updateIndex = existingByPreviousName >= 0 ? existingByPreviousName : existingByName;
27361
+ if (existingByName >= 0 && existingByName !== updateIndex) throw new Error(`Profile name "${options2.name}" already exists on provider ${options2.provider}. Pick a different profile name.`);
27362
+ if (existingByName >= 0 && existingByPreviousName < 0 && !options2.force) throw new Error(`Profile name "${options2.name}" already exists on provider ${options2.provider}. Use --force to overwrite it.`);
27363
+ if (existingByDisplay >= 0 && existingByDisplay !== updateIndex && !options2.force) {
27364
+ 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.`);
27365
+ }
27366
+ const next = {
27367
+ name: options2.name,
27368
+ displayName,
27369
+ model: options2.model,
27370
+ ...options2.baseURL ? { baseURL: options2.baseURL } : {},
27371
+ ...options2.apiKey !== void 0 || options2.apiKeyEnv ? { apiKey: options2.apiKey ?? "" } : {},
27372
+ ...options2.apiKeyEnv ? { apiKeyEnv: options2.apiKeyEnv } : {}
27373
+ };
27374
+ if (updateIndex >= 0) profiles[updateIndex] = next;
27375
+ else profiles.push(next);
27376
+ provider.modelProfiles = profiles;
27377
+ providers[options2.provider] = provider;
27378
+ config.providers = providers;
27379
+ const content = `${JSON.stringify(config, null, 2)}
27380
+ `;
27381
+ await writeJsonAtomic(filePath, content);
27382
+ return { path: filePath, created: true, content };
27383
+ }
27384
+ async function updateConfigDefaults(options2) {
27385
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
27386
+ const config = await readConfigObject(filePath);
27387
+ const defaultProvider = normalizeOptionalName(options2.defaultProvider);
27388
+ const defaultAgent = normalizeOptionalName(options2.defaultAgent);
27389
+ if (!defaultProvider && !defaultAgent) throw new Error("At least one of defaultProvider or defaultAgent is required.");
27390
+ if (defaultProvider) config.defaultProvider = defaultProvider;
27391
+ if (defaultAgent) config.defaultAgent = defaultAgent;
27392
+ const content = `${JSON.stringify(config, null, 2)}
27393
+ `;
27394
+ await writeJsonAtomic(filePath, content);
27395
+ return { path: filePath, created: true, content };
27396
+ }
27397
+ async function setDefaultModelProfile(options2) {
27398
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
27399
+ const config = await readConfigObject(filePath);
27400
+ const providers = objectValue(config.providers);
27401
+ const provider = providers[options2.provider];
27402
+ if (!provider || typeof provider !== "object" || Array.isArray(provider)) throw new Error(`Provider ${options2.provider} does not exist.`);
27403
+ const providerConfig = { ...provider };
27404
+ const profiles = Array.isArray(providerConfig.modelProfiles) ? [...providerConfig.modelProfiles] : [];
27405
+ const requestedName = normalizeOptionalName(options2.profileName ?? options2.name);
27406
+ const requestedModel = normalizeOptionalName(options2.model);
27407
+ const requestedDisplayName = normalizeOptionalName(options2.displayName) ?? requestedModel ?? requestedName;
27408
+ let index = profiles.findIndex((entry) => requestedName && entry.name === requestedName);
27409
+ if (index < 0 && requestedModel) index = profiles.findIndex((entry) => entry.model === requestedModel);
27410
+ if (index < 0 && requestedDisplayName) index = profiles.findIndex((entry) => entry.displayName === requestedDisplayName);
27411
+ const profile = index >= 0 ? {
27412
+ ...profiles[index],
27413
+ ...requestedName ? { name: requestedName } : {},
27414
+ ...requestedDisplayName ? { displayName: requestedDisplayName } : {},
27415
+ ...requestedModel ? { model: requestedModel } : {},
27416
+ ...options2.baseURL ? { baseURL: options2.baseURL } : {},
27417
+ ...options2.apiKey !== void 0 || options2.apiKeyEnv ? { apiKey: options2.apiKey ?? "" } : {},
27418
+ ...options2.apiKeyEnv ? { apiKeyEnv: options2.apiKeyEnv } : {}
27419
+ } : newModelProfile({
27420
+ name: requestedName,
27421
+ displayName: requestedDisplayName,
27422
+ model: requestedModel ?? requestedName,
27423
+ baseURL: options2.baseURL,
27424
+ apiKey: options2.apiKey,
27425
+ apiKeyEnv: options2.apiKeyEnv
27426
+ });
27427
+ if (!profile.name || typeof profile.name !== "string") throw new Error("Model profile name is required.");
27428
+ if (!profile.model || typeof profile.model !== "string") throw new Error("Model ID is required.");
27429
+ if (index >= 0) profiles.splice(index, 1);
27430
+ profiles.unshift(profile);
27431
+ providerConfig.modelProfiles = profiles;
27432
+ providers[options2.provider] = providerConfig;
27433
+ config.providers = providers;
27434
+ const content = `${JSON.stringify(config, null, 2)}
27435
+ `;
27436
+ await writeJsonAtomic(filePath, content);
27437
+ return { path: filePath, created: true, content };
27438
+ }
27439
+ async function updateProviderSettings(options2) {
27440
+ const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
27441
+ const config = await readConfigObject(filePath);
27442
+ const providers = objectValue(config.providers);
27443
+ const provider = providers[options2.provider];
27444
+ if (!provider || typeof provider !== "object" || Array.isArray(provider)) throw new Error(`Provider ${options2.provider} does not exist.`);
27445
+ const providerConfig = { ...provider };
27446
+ if (options2.baseURL !== void 0) setOrDelete(providerConfig, "baseURL", options2.baseURL);
27447
+ if (options2.apiKey !== void 0) setOrDelete(providerConfig, "apiKey", options2.apiKey);
27448
+ if (options2.apiKeyEnv !== void 0) setOrDelete(providerConfig, "apiKeyEnv", options2.apiKeyEnv);
27449
+ if (options2.authHeader !== void 0) {
27450
+ const header = options2.authHeader.trim();
27451
+ if (header) providerConfig.auth = { type: "api-key", header };
27452
+ else delete providerConfig.auth;
27453
+ }
27454
+ providers[options2.provider] = providerConfig;
27455
+ config.providers = providers;
27456
+ const content = `${JSON.stringify(config, null, 2)}
27457
+ `;
27458
+ await writeJsonAtomic(filePath, content);
27459
+ return { path: filePath, created: true, content };
27460
+ }
27461
+ async function readConfigObject(filePath) {
27462
+ if (!await exists(filePath)) await createUserConfig({ path: filePath });
27463
+ const raw = JSON.parse(await readFile7(filePath, "utf8"));
27464
+ raw.version ??= 2;
27465
+ raw.providers = objectValue(raw.providers);
27466
+ return raw;
27467
+ }
27468
+ function providerPreset(options2) {
27469
+ const preset = options2.preset ?? options2.name;
27470
+ const auth = apiKeyAuthFields(options2);
27471
+ const openAIAuth = options2.authHeader ? { auth: { type: "api-key", header: options2.authHeader } } : {};
27472
+ if (preset === "openai") return { type: "openai-compatible", baseURL: options2.baseURL ?? "https://api.openai.com/v1", ...apiKeyAuthFields(options2, "OPENAI_API_KEY"), ...openAIAuth, catalog: { type: "openai-models", endpoint: `${(options2.baseURL ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/models` } };
27473
+ if (preset === "anthropic") return { type: "anthropic", baseURL: options2.baseURL ?? "https://api.anthropic.com/v1", ...apiKeyAuthFields(options2, "ANTHROPIC_API_KEY"), catalog: { type: "anthropic-models", endpoint: `${(options2.baseURL ?? "https://api.anthropic.com/v1").replace(/\/+$/, "")}/models` } };
27474
+ if (preset === "gemini") {
27475
+ const geminiBase = options2.baseURL ?? "https://generativelanguage.googleapis.com/v1beta/openai/";
27476
+ return { type: "openai-compatible", baseURL: geminiBase, ...apiKeyAuthFields(options2, "GEMINI_API_KEY"), apiKeyEnvAliases: ["GOOGLE_API_KEY"], ...openAIAuth, catalog: { type: "gemini-openai-models", endpoint: `${geminiBase.replace(/\/+$/, "")}/models` } };
27477
+ }
27478
+ if (preset === "groq") return { type: "openai-compatible", baseURL: options2.baseURL ?? "https://api.groq.com/openai/v1", ...apiKeyAuthFields(options2, "GROQ_API_KEY"), ...openAIAuth, catalog: { type: "groq-models", endpoint: `${(options2.baseURL ?? "https://api.groq.com/openai/v1").replace(/\/+$/, "")}/models` } };
27479
+ if (preset === "azure") {
27480
+ return {
27481
+ type: "openai-compatible",
27482
+ auth: { type: "api-key", header: options2.authHeader ?? "api-key" },
27483
+ ...auth,
27484
+ modelProfiles: [
27485
+ {
27486
+ name: "azure-example",
27487
+ displayName: "Azure example",
27488
+ model: "azure-deployment-name",
27489
+ baseURL: options2.baseURL ?? "https://example.openai.azure.com/openai/v1",
27490
+ ...options2.apiKey ? {} : { apiKey: "" },
27491
+ apiKeyEnv: options2.apiKeyEnv ?? "AZURE_OPENAI_API_KEY"
27492
+ }
27493
+ ]
27494
+ };
27495
+ }
27496
+ if (preset === "lmstudio") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:1234/v1", apiKey: options2.apiKey ?? "lm-studio", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:1234/v1").replace(/\/+$/, "")}/models` } };
27497
+ if (preset === "ollama-local" || preset === "ollama") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:11434/v1", apiKey: options2.apiKey ?? "ollama", modelProfiles: [], quirks: { omitTemperature: true }, catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:11434/v1").replace(/\/+$/, "")}/models` } };
27498
+ if (preset === "ollama-cloud") return { type: "ollama", baseURL: options2.baseURL ?? "https://ollama.com/api", ...apiKeyAuthFields(options2, "OLLAMA_API_KEY"), modelProfiles: [], catalog: { type: "ollama-tags", endpoint: `${(options2.baseURL ?? "https://ollama.com/api").replace(/\/+$/, "")}/tags` } };
27499
+ if (preset === "llamacpp") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:8080/v1", apiKey: options2.apiKey ?? "llama.cpp", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:8080/v1").replace(/\/+$/, "")}/models` } };
27500
+ if (preset === "vllm") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:8000/v1", apiKey: options2.apiKey ?? "vllm", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:8000/v1").replace(/\/+$/, "")}/models` } };
27501
+ 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" } };
27502
+ 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" } };
27503
+ if ((options2.type ?? preset) === "openai-compatible") {
27504
+ const baseURL = options2.baseURL;
27505
+ if (!baseURL) throw new Error("openai-compatible provider requires --base-url.");
27506
+ return {
27507
+ type: "openai-compatible",
27508
+ baseURL,
27509
+ ...auth,
27510
+ ...openAIAuth,
27511
+ catalog: { type: "openai-compatible-models", endpoint: `${baseURL.replace(/\/+$/, "")}/models` }
27512
+ };
27513
+ }
27514
+ throw new Error(`Unknown provider preset: ${preset}`);
27515
+ }
27516
+ function apiKeyAuthFields(options2, defaultApiKeyEnv) {
27517
+ const apiKeyEnv = options2.apiKeyEnv ?? defaultApiKeyEnv;
27518
+ return {
27519
+ ...options2.apiKey !== void 0 || apiKeyEnv ? { apiKey: options2.apiKey ?? "" } : {},
27520
+ ...apiKeyEnv ? { apiKeyEnv } : {}
27521
+ };
27522
+ }
27523
+ async function writeJsonAtomic(filePath, content) {
27524
+ await mkdir6(path14.dirname(filePath), { recursive: true, mode: 448 });
27525
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
27526
+ await writeFile5(temp, content, { mode: 384 });
27527
+ await rename4(temp, filePath);
27528
+ await chmod3(filePath, 384).catch(() => void 0);
27529
+ }
27530
+ function detectDefaultProvider() {
27531
+ if (process.env.OPENAI_API_KEY) return "openai";
27532
+ if (process.env.ANTHROPIC_API_KEY) return "anthropic";
27533
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return "gemini";
27534
+ if (process.env.GROQ_API_KEY) return "groq";
27535
+ return "openai";
27536
+ }
27537
+ function objectValue(value) {
27538
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
27539
+ }
27540
+ function normalizeOptionalName(value) {
27541
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
27542
+ }
27543
+ function newModelProfile(options2) {
27544
+ const model = options2.model?.trim();
27545
+ if (!model) throw new Error("Model ID is required.");
27546
+ const name = options2.name?.trim() || modelProfileNameFromModel(model);
27547
+ return {
27548
+ name,
27549
+ displayName: options2.displayName?.trim() || name,
27550
+ model,
27551
+ ...options2.baseURL ? { baseURL: options2.baseURL } : {},
27552
+ ...options2.apiKey !== void 0 || options2.apiKeyEnv ? { apiKey: options2.apiKey ?? "" } : {},
27553
+ ...options2.apiKeyEnv ? { apiKeyEnv: options2.apiKeyEnv } : {}
27554
+ };
27555
+ }
27556
+ function modelProfileNameFromModel(model) {
27557
+ return model.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "model";
27558
+ }
27559
+ function setOrDelete(target, key, value) {
27560
+ const trimmed = value.trim();
27561
+ if (trimmed) target[key] = trimmed;
27562
+ else delete target[key];
27563
+ }
27564
+ async function exists(filePath) {
27565
+ try {
27566
+ await stat3(filePath);
27567
+ return true;
27568
+ } catch {
27569
+ return false;
27570
+ }
27571
+ }
27572
+ function expandHome2(value) {
27573
+ return resolveExpandedPath(value);
27574
+ }
27575
+
27576
+ // src/transcript.ts
27577
+ import { mkdir as mkdir7, appendFile, writeFile as writeFile6 } from "node:fs/promises";
27578
+ import os9 from "node:os";
27579
+ import path15 from "node:path";
26773
27580
  var TranscriptWriter = class {
26774
27581
  filePath;
26775
27582
  #enabled;
@@ -26777,9 +27584,9 @@ var TranscriptWriter = class {
26777
27584
  #queue = Promise.resolve();
26778
27585
  constructor(options2) {
26779
27586
  this.#enabled = options2.enabled !== false;
26780
- const dir = path14.join(options2.storageDir ?? defaultDemianStorageDir(), "transcripts", options2.sessionId);
26781
- this.filePath = path14.join(dir, "session.jsonl");
26782
- this.#ready = this.#enabled ? mkdir6(dir, { recursive: true }).then(() => writeFile5(this.filePath, "", { flag: "a" })) : Promise.resolve();
27587
+ const dir = path15.join(options2.storageDir ?? defaultDemianStorageDir(), "transcripts", options2.sessionId);
27588
+ this.filePath = path15.join(dir, "session.jsonl");
27589
+ this.#ready = this.#enabled ? mkdir7(dir, { recursive: true }).then(() => writeFile6(this.filePath, "", { flag: "a" })) : Promise.resolve();
26783
27590
  }
26784
27591
  write(event) {
26785
27592
  if (!this.#enabled) return Promise.resolve();
@@ -26795,14 +27602,14 @@ var TranscriptWriter = class {
26795
27602
  }
26796
27603
  };
26797
27604
  function defaultDemianStorageDir() {
26798
- return path14.join(os8.homedir(), ".demian");
27605
+ return path15.join(os9.homedir(), ".demian");
26799
27606
  }
26800
27607
 
26801
27608
  // src/external-runtime/snapshot-diff.ts
26802
27609
  import { createHash as createHash2 } from "node:crypto";
26803
27610
  import { createReadStream } from "node:fs";
26804
- import { readdir, readFile as readFile7, stat as stat3 } from "node:fs/promises";
26805
- import path15 from "node:path";
27611
+ import { readdir, readFile as readFile8, stat as stat4 } from "node:fs/promises";
27612
+ import path16 from "node:path";
26806
27613
 
26807
27614
  // src/workspace/diff.ts
26808
27615
  var CONTEXT_LINES = 3;
@@ -27012,7 +27819,7 @@ async function diffWorkspaceSnapshot(before) {
27012
27819
  }
27013
27820
  async function walk(root, relativeDir, entries, options2) {
27014
27821
  if (entries.size >= options2.maxFiles) return;
27015
- const absoluteDir = path15.join(root, relativeDir);
27822
+ const absoluteDir = path16.join(root, relativeDir);
27016
27823
  let items;
27017
27824
  try {
27018
27825
  items = await readdir(absoluteDir, { withFileTypes: true });
@@ -27023,8 +27830,8 @@ async function walk(root, relativeDir, entries, options2) {
27023
27830
  if (entries.size >= options2.maxFiles) return;
27024
27831
  if (item.name.startsWith(".") && item.name !== ".env.example" && SKIP_DIRS.has(item.name)) continue;
27025
27832
  if (SKIP_DIRS.has(item.name)) continue;
27026
- const relativePath = normalizeRelativePath(path15.join(relativeDir, item.name));
27027
- const absolutePath = path15.join(root, relativePath);
27833
+ const relativePath = normalizeRelativePath(path16.join(relativeDir, item.name));
27834
+ const absolutePath = path16.join(root, relativePath);
27028
27835
  if (item.isDirectory()) {
27029
27836
  await walk(root, relativePath, entries, options2);
27030
27837
  continue;
@@ -27035,7 +27842,7 @@ async function walk(root, relativeDir, entries, options2) {
27035
27842
  }
27036
27843
  }
27037
27844
  async function snapshotFile(filePath, relativePath, maxTextBytes) {
27038
- const info = await stat3(filePath);
27845
+ const info = await stat4(filePath);
27039
27846
  const sha2562 = await sha256File(filePath);
27040
27847
  const content = info.size <= maxTextBytes ? await readTextContent(filePath) : void 0;
27041
27848
  return { path: relativePath, size: info.size, sha256: sha2562, content };
@@ -27046,7 +27853,7 @@ function snapshotDiffText(entry) {
27046
27853
  `;
27047
27854
  }
27048
27855
  async function readTextContent(filePath) {
27049
- const buffer = await readFile7(filePath);
27856
+ const buffer = await readFile8(filePath);
27050
27857
  if (buffer.includes(0)) return void 0;
27051
27858
  return buffer.toString("utf8");
27052
27859
  }
@@ -27060,7 +27867,7 @@ function sha256File(filePath) {
27060
27867
  });
27061
27868
  }
27062
27869
  function normalizeRelativePath(value) {
27063
- return value.split(path15.sep).join("/");
27870
+ return value.split(path16.sep).join("/");
27064
27871
  }
27065
27872
 
27066
27873
  // src/external-runtime/session-runner.ts
@@ -27495,7 +28302,7 @@ function safeJson(value) {
27495
28302
  }
27496
28303
 
27497
28304
  // src/hooks/dispatcher.ts
27498
- import path17 from "node:path";
28305
+ import path18 from "node:path";
27499
28306
 
27500
28307
  // src/hooks/command.ts
27501
28308
  import { spawn as spawn4 } from "node:child_process";
@@ -27588,7 +28395,7 @@ var blockDangerousBashHook = {
27588
28395
  };
27589
28396
 
27590
28397
  // src/hooks/builtin/protect-env-files.ts
27591
- import path16 from "node:path";
28398
+ import path17 from "node:path";
27592
28399
  var protectEnvFilesHook = {
27593
28400
  name: "protect-env-files",
27594
28401
  event: "PreToolUse",
@@ -27596,7 +28403,7 @@ var protectEnvFilesHook = {
27596
28403
  run(ctx) {
27597
28404
  if (ctx.toolName !== "write_file" && ctx.toolName !== "edit_file") return { decision: "allow" };
27598
28405
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
27599
- const filePath = typeof input2.path === "string" ? path16.resolve(ctx.cwd, input2.path) : "";
28406
+ const filePath = typeof input2.path === "string" ? path17.resolve(ctx.cwd, input2.path) : "";
27600
28407
  if (filePath && isEnvFile(filePath)) {
27601
28408
  return {
27602
28409
  decision: "block",
@@ -27633,7 +28440,7 @@ var maskSecretsHook = {
27633
28440
  };
27634
28441
 
27635
28442
  // src/hooks/builtin/inject-env-info.ts
27636
- import os9 from "node:os";
28443
+ import os10 from "node:os";
27637
28444
  var injectEnvInfoHook = {
27638
28445
  name: "inject-env-info",
27639
28446
  event: "SessionStart",
@@ -27642,7 +28449,7 @@ var injectEnvInfoHook = {
27642
28449
  decision: "allow",
27643
28450
  patch: {
27644
28451
  systemNote: [
27645
- `Environment: ${os9.type()} ${os9.release()} (${os9.platform()}/${os9.arch()})`,
28452
+ `Environment: ${os10.type()} ${os10.release()} (${os10.platform()}/${os10.arch()})`,
27646
28453
  `cwd: ${ctx.cwd}`,
27647
28454
  `provider: ${ctx.provider ?? "unknown"}`,
27648
28455
  `model: ${ctx.model ?? "unknown"}`,
@@ -27711,14 +28518,14 @@ function matchesHook(match, ctx) {
27711
28518
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
27712
28519
  const filePath = typeof input2.path === "string" ? input2.path : typeof input2.workdir === "string" ? input2.workdir : void 0;
27713
28520
  if (!filePath) return false;
27714
- if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path17.resolve(ctx.cwd, filePath)))) return false;
28521
+ if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path18.resolve(ctx.cwd, filePath)))) return false;
27715
28522
  }
27716
28523
  return true;
27717
28524
  }
27718
28525
 
27719
28526
  // src/multimodal.ts
27720
- import { readFile as readFile8, stat as stat4 } from "node:fs/promises";
27721
- import path18 from "node:path";
28527
+ import { readFile as readFile9, stat as stat5 } from "node:fs/promises";
28528
+ import path19 from "node:path";
27722
28529
  var DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
27723
28530
  async function buildUserContent(prompt, images = [], options2) {
27724
28531
  if (images.length === 0) return prompt;
@@ -27737,16 +28544,16 @@ async function buildUserContent(prompt, images = [], options2) {
27737
28544
  async function imageToUrl(input2, options2) {
27738
28545
  if (/^https?:\/\//i.test(input2) || /^data:image\//i.test(input2)) return input2;
27739
28546
  const filePath = resolveInsideCwd(options2.cwd, input2);
27740
- const info = await stat4(filePath);
28547
+ const info = await stat5(filePath);
27741
28548
  if (!info.isFile()) throw new Error(`Image input is not a file: ${input2}`);
27742
28549
  const maxImageBytes = options2.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
27743
28550
  if (info.size > maxImageBytes) throw new Error(`Image is larger than ${maxImageBytes} bytes: ${input2}`);
27744
28551
  const mime = mimeFromPath(filePath);
27745
- const bytes = await readFile8(filePath);
28552
+ const bytes = await readFile9(filePath);
27746
28553
  return `data:${mime};base64,${bytes.toString("base64")}`;
27747
28554
  }
27748
28555
  function mimeFromPath(filePath) {
27749
- switch (path18.extname(filePath).toLowerCase()) {
28556
+ switch (path19.extname(filePath).toLowerCase()) {
27750
28557
  case ".jpg":
27751
28558
  case ".jpeg":
27752
28559
  return "image/jpeg";
@@ -27757,15 +28564,15 @@ function mimeFromPath(filePath) {
27757
28564
  case ".webp":
27758
28565
  return "image/webp";
27759
28566
  default:
27760
- throw new Error(`Unsupported image extension: ${path18.extname(filePath) || "(none)"}`);
28567
+ throw new Error(`Unsupported image extension: ${path19.extname(filePath) || "(none)"}`);
27761
28568
  }
27762
28569
  }
27763
28570
 
27764
28571
  // src/permissions/persistent-grants.ts
27765
- import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
28572
+ import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile7 } from "node:fs/promises";
27766
28573
  import fs8 from "node:fs";
27767
- import os10 from "node:os";
27768
- import path19 from "node:path";
28574
+ import os11 from "node:os";
28575
+ import path20 from "node:path";
27769
28576
  var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
27770
28577
  var PersistentGrantStore = class {
27771
28578
  filePath;
@@ -27796,22 +28603,22 @@ var PersistentGrantStore = class {
27796
28603
  }
27797
28604
  async #read() {
27798
28605
  if (!fs8.existsSync(this.filePath)) return { version: 1, grants: [] };
27799
- const data = JSON.parse(await readFile9(this.filePath, "utf8"));
28606
+ const data = JSON.parse(await readFile10(this.filePath, "utf8"));
27800
28607
  return {
27801
28608
  version: 1,
27802
28609
  grants: Array.isArray(data.grants) ? data.grants.filter(isGrantRecord) : []
27803
28610
  };
27804
28611
  }
27805
28612
  async #write(file) {
27806
- await mkdir7(path19.dirname(this.filePath), { recursive: true });
27807
- await writeFile6(this.filePath, `${JSON.stringify(file, null, 2)}
28613
+ await mkdir8(path20.dirname(this.filePath), { recursive: true });
28614
+ await writeFile7(this.filePath, `${JSON.stringify(file, null, 2)}
27808
28615
  `, { mode: 384 });
27809
28616
  }
27810
28617
  };
27811
28618
  function resolveGrantPath(cwd, config) {
27812
- if (config.path) return path19.resolve(cwd, config.path);
27813
- if ((config.scope ?? "project") === "user") return path19.join(os10.homedir(), ".demian", "grants.json");
27814
- return path19.join(cwd, ".demian", "grants.json");
28619
+ if (config.path) return path20.resolve(cwd, config.path);
28620
+ if ((config.scope ?? "project") === "user") return path20.join(os11.homedir(), ".demian", "grants.json");
28621
+ return path20.join(cwd, ".demian", "grants.json");
27815
28622
  }
27816
28623
  function isGrantRecord(value) {
27817
28624
  if (!value || typeof value !== "object") return false;
@@ -28244,25 +29051,25 @@ function fail(content, metadata) {
28244
29051
  }
28245
29052
 
28246
29053
  // src/tools/output.ts
28247
- import { mkdir as mkdir8, writeFile as writeFile7 } from "node:fs/promises";
28248
- import path20 from "node:path";
29054
+ import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
29055
+ import path21 from "node:path";
28249
29056
  var DEFAULT_CAP_BYTES = 32 * 1024;
28250
29057
  var HALF_PREVIEW_BYTES = 16 * 1024;
28251
29058
  async function capToolOutput(result, options2) {
28252
29059
  const capBytes = options2.capBytes ?? DEFAULT_CAP_BYTES;
28253
29060
  const bytes = Buffer.byteLength(result.content, "utf8");
28254
29061
  if (bytes <= capBytes) return result;
28255
- const dir = path20.join(options2.cwd, ".demian", "tmp");
28256
- await mkdir8(dir, { recursive: true });
28257
- const outputPath = path20.join(dir, `output-${safeCallId(options2.callId)}.txt`);
28258
- await writeFile7(outputPath, result.content, "utf8");
29062
+ const dir = path21.join(options2.cwd, ".demian", "tmp");
29063
+ await mkdir9(dir, { recursive: true });
29064
+ const outputPath = path21.join(dir, `output-${safeCallId(options2.callId)}.txt`);
29065
+ await writeFile8(outputPath, result.content, "utf8");
28259
29066
  return {
28260
29067
  ...result,
28261
29068
  content: [
28262
29069
  sliceUtf8(result.content, 0, HALF_PREVIEW_BYTES),
28263
29070
  `
28264
29071
 
28265
- [Full output saved to ${path20.relative(options2.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
29072
+ [Full output saved to ${path21.relative(options2.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
28266
29073
 
28267
29074
  `,
28268
29075
  sliceUtf8(result.content, Math.max(0, bytes - HALF_PREVIEW_BYTES), bytes)
@@ -28287,8 +29094,8 @@ function sliceUtf8(text, startByte, endByte) {
28287
29094
  }
28288
29095
 
28289
29096
  // src/tools/read-file.ts
28290
- import { open as open2, stat as stat5 } from "node:fs/promises";
28291
- import path21 from "node:path";
29097
+ import { open as open2, stat as stat6 } from "node:fs/promises";
29098
+ import path22 from "node:path";
28292
29099
 
28293
29100
  // src/tools/validation.ts
28294
29101
  function assertObject(input2, toolName) {
@@ -28358,7 +29165,7 @@ var readFileTool = {
28358
29165
  const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
28359
29166
  const offset = positiveInteger(optionalNumberField(object2, "offset"), 1, "offset");
28360
29167
  const limit = positiveInteger(optionalNumberField(object2, "limit"), DEFAULT_LIMIT, "limit");
28361
- const info = await stat5(filePath);
29168
+ const info = await stat6(filePath);
28362
29169
  if (info.isDirectory()) throw new Error(`read_file expected a file but got a directory: ${relativeToCwd(ctx.cwd, filePath)}`);
28363
29170
  if (info.size > MAX_BYTES) throw new Error(`File is larger than ${MAX_BYTES} bytes: ${relativeToCwd(ctx.cwd, filePath)}`);
28364
29171
  const sample = await readBytes(filePath, Math.min(SAMPLE_BYTES, info.size));
@@ -28372,7 +29179,7 @@ var readFileTool = {
28372
29179
  const truncated = start + sliced.length < lines.length;
28373
29180
  return ok(content + (truncated ? `
28374
29181
  ... (${lines.length - start - sliced.length} more lines)` : ""), {
28375
- path: path21.relative(ctx.cwd, filePath),
29182
+ path: path22.relative(ctx.cwd, filePath),
28376
29183
  lines: sliced.length,
28377
29184
  totalLines: lines.length,
28378
29185
  truncated
@@ -28410,8 +29217,8 @@ function looksBinary(buffer) {
28410
29217
  }
28411
29218
 
28412
29219
  // src/tools/write-file.ts
28413
- import { mkdir as mkdir9, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
28414
- import path22 from "node:path";
29220
+ import { mkdir as mkdir10, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
29221
+ import path23 from "node:path";
28415
29222
  var writeFileTool = {
28416
29223
  name: "write_file",
28417
29224
  description: "Create or replace a text file inside the workspace.",
@@ -28429,8 +29236,8 @@ var writeFileTool = {
28429
29236
  const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
28430
29237
  const content = stringField(object2, "content", { allowEmpty: true });
28431
29238
  const before = await readOptionalTextFile(filePath);
28432
- await mkdir9(path22.dirname(filePath), { recursive: true });
28433
- await writeFile8(filePath, content, "utf8");
29239
+ await mkdir10(path23.dirname(filePath), { recursive: true });
29240
+ await writeFile9(filePath, content, "utf8");
28434
29241
  const relative = relativeToCwd(ctx.cwd, filePath);
28435
29242
  const diff = createTextDiff(before, content, relative);
28436
29243
  return ok(`Wrote ${relativeToCwd(ctx.cwd, filePath)} (${Buffer.byteLength(content, "utf8")} bytes).`, {
@@ -28443,7 +29250,7 @@ var writeFileTool = {
28443
29250
  };
28444
29251
  async function readOptionalTextFile(filePath) {
28445
29252
  try {
28446
- return await readFile10(filePath, "utf8");
29253
+ return await readFile11(filePath, "utf8");
28447
29254
  } catch (error) {
28448
29255
  if (isNotFound(error)) return void 0;
28449
29256
  throw error;
@@ -28454,7 +29261,7 @@ function isNotFound(error) {
28454
29261
  }
28455
29262
 
28456
29263
  // src/tools/edit-file.ts
28457
- import { readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
29264
+ import { readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
28458
29265
  var editFileTool = {
28459
29266
  name: "edit_file",
28460
29267
  description: "Replace an exact string in a text file inside the workspace.",
@@ -28476,12 +29283,12 @@ var editFileTool = {
28476
29283
  const newString = stringField(object2, "newString", { allowEmpty: true });
28477
29284
  const replaceAll = optionalBooleanField(object2, "replaceAll") ?? false;
28478
29285
  if (oldString === newString) throw new Error("oldString and newString must be different");
28479
- const before = await readFile11(filePath, "utf8");
29286
+ const before = await readFile12(filePath, "utf8");
28480
29287
  const matches = countMatches(before, oldString);
28481
29288
  if (matches === 0) throw new Error("oldString was not found");
28482
29289
  if (matches > 1 && !replaceAll) throw new Error(`oldString matched ${matches} times; set replaceAll=true to replace all matches`);
28483
29290
  const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString);
28484
- await writeFile9(filePath, after, "utf8");
29291
+ await writeFile10(filePath, after, "utf8");
28485
29292
  const relative = relativeToCwd(ctx.cwd, filePath);
28486
29293
  const diff = createTextDiff(before, after, relative);
28487
29294
  return ok(`Edited ${relative} (${replaceAll ? matches : 1} replacement${matches === 1 ? "" : "s"}).`, {
@@ -28505,7 +29312,7 @@ function countMatches(text, needle) {
28505
29312
 
28506
29313
  // src/tools/bash.ts
28507
29314
  import { spawn as spawn5 } from "node:child_process";
28508
- import path24 from "node:path";
29315
+ import path25 from "node:path";
28509
29316
 
28510
29317
  // src/sandbox/env-only.ts
28511
29318
  function buildEnvOnlyLaunch(command, config) {
@@ -28565,7 +29372,7 @@ function bwrapPath() {
28565
29372
 
28566
29373
  // src/sandbox/macos.ts
28567
29374
  import { existsSync as existsSync3 } from "node:fs";
28568
- import path23 from "node:path";
29375
+ import path24 from "node:path";
28569
29376
  function canUseMacOSSandbox() {
28570
29377
  return process.platform === "darwin" && existsSync3("/usr/bin/sandbox-exec");
28571
29378
  }
@@ -28591,7 +29398,7 @@ function macosProfile(cwd, config) {
28591
29398
  }
28592
29399
  if (mode === "workspace-write") {
28593
29400
  lines.push("(deny file-write*)");
28594
- lines.push(`(allow file-write* (subpath "${escapeProfilePath(path23.resolve(cwd))}"))`);
29401
+ lines.push(`(allow file-write* (subpath "${escapeProfilePath(path24.resolve(cwd))}"))`);
28595
29402
  lines.push('(allow file-write* (subpath "/tmp"))');
28596
29403
  lines.push('(allow file-write* (subpath "/private/tmp"))');
28597
29404
  lines.push('(allow file-write* (subpath "/private/var/folders"))');
@@ -28653,7 +29460,7 @@ ${result.stderr}` : ""
28653
29460
  ].filter(Boolean).join("\n");
28654
29461
  return ok(content, {
28655
29462
  command,
28656
- workdir: path24.relative(ctx.cwd, workdir) || ".",
29463
+ workdir: path25.relative(ctx.cwd, workdir) || ".",
28657
29464
  exitCode: result.exitCode,
28658
29465
  signal: result.signal,
28659
29466
  timedOut: result.timedOut,
@@ -28705,8 +29512,8 @@ function runCommand(command, cwd, timeoutMs, signal, sandbox = { mode: "workspac
28705
29512
 
28706
29513
  // src/tools/grep.ts
28707
29514
  import { spawn as spawn6 } from "node:child_process";
28708
- import { readdir as readdir2, readFile as readFile12, stat as stat6 } from "node:fs/promises";
28709
- import path25 from "node:path";
29515
+ import { readdir as readdir2, readFile as readFile13, stat as stat7 } from "node:fs/promises";
29516
+ import path26 from "node:path";
28710
29517
  var MAX_MATCHES = 200;
28711
29518
  var grepTool = {
28712
29519
  name: "grep",
@@ -28781,7 +29588,7 @@ function runRg(cwd, base, pattern, glob, signal) {
28781
29588
  }
28782
29589
  function rewriteRgPath(cwd, line) {
28783
29590
  const [file, rest] = splitFirst(line, ":");
28784
- if (!path25.isAbsolute(file)) return line;
29591
+ if (!path26.isAbsolute(file)) return line;
28785
29592
  return `${relativeToCwd(cwd, file)}:${rest}`;
28786
29593
  }
28787
29594
  function splitFirst(value, delimiter) {
@@ -28796,13 +29603,13 @@ async function fallbackSearch(cwd, base, pattern, glob) {
28796
29603
  if (out.length > MAX_MATCHES) return;
28797
29604
  const relative = relativeToCwd(cwd, item);
28798
29605
  if (isIgnoredPath(relative)) return;
28799
- const info = await stat6(item);
29606
+ const info = await stat7(item);
28800
29607
  if (info.isDirectory()) {
28801
- for (const entry of await readdir2(item)) await walk2(path25.join(item, entry));
29608
+ for (const entry of await readdir2(item)) await walk2(path26.join(item, entry));
28802
29609
  return;
28803
29610
  }
28804
29611
  if (glob && !matchGlob(glob, relative)) return;
28805
- const buffer = await readFile12(item);
29612
+ const buffer = await readFile13(item);
28806
29613
  if (buffer.includes(0)) return;
28807
29614
  const lines = buffer.toString("utf8").split(/\r?\n/);
28808
29615
  for (let i = 0; i < lines.length; i++) {
@@ -28816,8 +29623,8 @@ async function fallbackSearch(cwd, base, pattern, glob) {
28816
29623
  }
28817
29624
 
28818
29625
  // src/tools/glob.ts
28819
- import { readdir as readdir3, stat as stat7 } from "node:fs/promises";
28820
- import path26 from "node:path";
29626
+ import { readdir as readdir3, stat as stat8 } from "node:fs/promises";
29627
+ import path27 from "node:path";
28821
29628
  var MAX_PATHS = 1e3;
28822
29629
  var globTool = {
28823
29630
  name: "glob",
@@ -28849,10 +29656,10 @@ async function collectPaths(cwd, root, pattern) {
28849
29656
  async function walk2(dir) {
28850
29657
  const entries = await readdir3(dir, { withFileTypes: true });
28851
29658
  for (const entry of entries) {
28852
- const absolute = path26.join(dir, entry.name);
29659
+ const absolute = path27.join(dir, entry.name);
28853
29660
  const relative = relativeToCwd(cwd, absolute);
28854
29661
  if (isIgnoredPath(relative)) continue;
28855
- const info = await stat7(absolute);
29662
+ const info = await stat8(absolute);
28856
29663
  if (matchGlob(pattern, relative)) out.push({ relative, mtimeMs: info.mtimeMs });
28857
29664
  if (entry.isDirectory()) await walk2(absolute);
28858
29665
  }
@@ -29851,14 +30658,14 @@ async function runExecutionSession(options2) {
29851
30658
  }
29852
30659
 
29853
30660
  // src/goals/lock.ts
29854
- import { mkdir as mkdir11, open as open3, rm as rm2 } from "node:fs/promises";
29855
- import path28 from "node:path";
30661
+ import { mkdir as mkdir12, open as open3, rm as rm2 } from "node:fs/promises";
30662
+ import path29 from "node:path";
29856
30663
 
29857
30664
  // src/goals/storage.ts
29858
30665
  import { createHash as createHash3 } from "node:crypto";
29859
- import { mkdir as mkdir10, readFile as readFile13, rename as rename4, writeFile as writeFile10 } from "node:fs/promises";
30666
+ import { mkdir as mkdir11, readFile as readFile14, rename as rename5, writeFile as writeFile11 } from "node:fs/promises";
29860
30667
  import fs9 from "node:fs";
29861
- import path27 from "node:path";
30668
+ import path28 from "node:path";
29862
30669
  var GoalStore = class {
29863
30670
  cwd;
29864
30671
  dir;
@@ -29869,12 +30676,12 @@ var GoalStore = class {
29869
30676
  this.cwd = cwd;
29870
30677
  this.scope = normalizeGoalStoreScope(options2.scope);
29871
30678
  this.dir = goalStoreDir(cwd, this.scope);
29872
- this.activePath = path27.join(this.dir, "active.json");
29873
- this.archiveDir = path27.join(this.dir, "archive");
30679
+ this.activePath = path28.join(this.dir, "active.json");
30680
+ this.archiveDir = path28.join(this.dir, "archive");
29874
30681
  }
29875
30682
  async loadActive() {
29876
30683
  if (!fs9.existsSync(this.activePath)) return void 0;
29877
- const text = await readFile13(this.activePath, "utf8");
30684
+ const text = await readFile14(this.activePath, "utf8");
29878
30685
  try {
29879
30686
  return JSON.parse(text);
29880
30687
  } catch {
@@ -29883,36 +30690,36 @@ var GoalStore = class {
29883
30690
  }
29884
30691
  }
29885
30692
  async saveActive(state) {
29886
- await mkdir10(this.dir, { recursive: true });
29887
- const tmp = path27.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
29888
- await writeFile10(tmp, `${JSON.stringify(state, null, 2)}
30693
+ await mkdir11(this.dir, { recursive: true });
30694
+ const tmp = path28.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
30695
+ await writeFile11(tmp, `${JSON.stringify(state, null, 2)}
29889
30696
  `, "utf8");
29890
- await rename4(tmp, this.activePath);
30697
+ await rename5(tmp, this.activePath);
29891
30698
  }
29892
30699
  async archiveActive(status = "cleared") {
29893
30700
  const state = await this.loadActive();
29894
30701
  if (!state) return void 0;
29895
- await mkdir10(this.archiveDir, { recursive: true });
30702
+ await mkdir11(this.archiveDir, { recursive: true });
29896
30703
  const archived = {
29897
30704
  ...state,
29898
30705
  status,
29899
30706
  updatedAt: Date.now()
29900
30707
  };
29901
- await writeFile10(path27.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
30708
+ await writeFile11(path28.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
29902
30709
  `, "utf8");
29903
30710
  await fs9.promises.rm(this.activePath, { force: true });
29904
30711
  return archived;
29905
30712
  }
29906
30713
  async backupCorrupted(text) {
29907
- await mkdir10(this.dir, { recursive: true });
29908
- const backup = path27.join(this.dir, `active.corrupt.${Date.now()}.json`);
29909
- await writeFile10(backup, text, "utf8");
30714
+ await mkdir11(this.dir, { recursive: true });
30715
+ const backup = path28.join(this.dir, `active.corrupt.${Date.now()}.json`);
30716
+ await writeFile11(backup, text, "utf8");
29910
30717
  await fs9.promises.rm(this.activePath, { force: true });
29911
30718
  }
29912
30719
  };
29913
30720
  function goalStoreDir(cwd, scope) {
29914
30721
  const safeScope = normalizeGoalStoreScope(scope);
29915
- return safeScope ? path27.join(cwd, ".demian", "goals", "sessions", safeScope) : path27.join(cwd, ".demian", "goals");
30722
+ return safeScope ? path28.join(cwd, ".demian", "goals", "sessions", safeScope) : path28.join(cwd, ".demian", "goals");
29916
30723
  }
29917
30724
  function normalizeGoalStoreScope(scope) {
29918
30725
  const trimmed = scope?.trim();
@@ -29926,10 +30733,10 @@ function normalizeGoalStoreScope(scope) {
29926
30733
  var GoalLock = class {
29927
30734
  path;
29928
30735
  constructor(cwd, scope) {
29929
- this.path = path28.join(goalStoreDir(cwd, scope), "active.lock");
30736
+ this.path = path29.join(goalStoreDir(cwd, scope), "active.lock");
29930
30737
  }
29931
30738
  async acquire() {
29932
- await mkdir11(path28.dirname(this.path), { recursive: true });
30739
+ await mkdir12(path29.dirname(this.path), { recursive: true });
29933
30740
  const handle = await open3(this.path, "wx").catch((error) => {
29934
30741
  const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
29935
30742
  if (code === "EEXIST") throw new Error("Another goal is already active in this workspace.");
@@ -30046,8 +30853,8 @@ async function summarizeGoalTitleWithProvider(options2) {
30046
30853
  }
30047
30854
  function normalizeGoalTitle(value, objective) {
30048
30855
  const raw = typeof value === "string" ? value : "";
30049
- const firstLine = raw.split(/\r?\n/).find((line) => line.trim()) ?? "";
30050
- const cleaned = stripDecorations(firstLine);
30856
+ const firstLine2 = raw.split(/\r?\n/).find((line) => line.trim()) ?? "";
30857
+ const cleaned = stripDecorations(firstLine2);
30051
30858
  return truncateGoalTitle(cleaned || fallbackGoalTitle(objective));
30052
30859
  }
30053
30860
  function fallbackGoalTitle(objective) {
@@ -30517,8 +31324,8 @@ function sha256(value) {
30517
31324
 
30518
31325
  // src/root-session.ts
30519
31326
  import { cp, mkdtemp, rm as rm3 } from "node:fs/promises";
30520
- import os11 from "node:os";
30521
- import path29 from "node:path";
31327
+ import os12 from "node:os";
31328
+ import path30 from "node:path";
30522
31329
 
30523
31330
  // src/tools/cowork.ts
30524
31331
  function createCoworkTool(options2) {
@@ -31341,8 +32148,8 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
31341
32148
  }
31342
32149
  }
31343
32150
  async createCoworkIsolatedWorkspace(groupId, memberId) {
31344
- const root = await mkdtemp(path29.join(os11.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
31345
- const cwd = path29.join(root, "workspace");
32151
+ const root = await mkdtemp(path30.join(os12.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
32152
+ const cwd = path30.join(root, "workspace");
31346
32153
  await cp(this.#options.cwd, cwd, {
31347
32154
  recursive: true,
31348
32155
  filter: (source) => shouldCopyIntoCoworkWorkspace(this.#options.cwd, source)
@@ -31646,17 +32453,17 @@ function findDependencyCycle(group) {
31646
32453
  const byId = new Map(group.map((member) => [member.memberId, member]));
31647
32454
  const visiting = /* @__PURE__ */ new Set();
31648
32455
  const visited = /* @__PURE__ */ new Set();
31649
- const path36 = [];
32456
+ const path37 = [];
31650
32457
  const visit = (id) => {
31651
- if (visiting.has(id)) return [...path36.slice(path36.indexOf(id)), id];
32458
+ if (visiting.has(id)) return [...path37.slice(path37.indexOf(id)), id];
31652
32459
  if (visited.has(id)) return void 0;
31653
32460
  visiting.add(id);
31654
- path36.push(id);
32461
+ path37.push(id);
31655
32462
  for (const dep of byId.get(id)?.dependsOn ?? []) {
31656
32463
  const cycle = visit(dep);
31657
32464
  if (cycle) return cycle;
31658
32465
  }
31659
- path36.pop();
32466
+ path37.pop();
31660
32467
  visiting.delete(id);
31661
32468
  visited.add(id);
31662
32469
  return void 0;
@@ -31697,7 +32504,7 @@ function scopeStaticPrefix(pattern) {
31697
32504
  return prefix.replace(/\/+$/, "").split("/").filter(Boolean).join("/") || ".";
31698
32505
  }
31699
32506
  function shouldCopyIntoCoworkWorkspace(root, source) {
31700
- const relative = path29.relative(root, source).split(path29.sep).join("/");
32507
+ const relative = path30.relative(root, source).split(path30.sep).join("/");
31701
32508
  if (!relative) return true;
31702
32509
  const parts = relative.split("/");
31703
32510
  return !parts.includes(".git") && !parts.includes("node_modules") && !parts.includes(".demian");
@@ -31909,13 +32716,13 @@ function finitePositiveInteger(value) {
31909
32716
  }
31910
32717
 
31911
32718
  // src/ui/preferences.ts
31912
- import { mkdir as mkdir12, readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
32719
+ import { mkdir as mkdir13, readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
31913
32720
  import fs10 from "node:fs";
31914
- import path30 from "node:path";
32721
+ import path31 from "node:path";
31915
32722
  var UiPreferenceStore = class {
31916
32723
  filePath;
31917
32724
  constructor(cwd, filePath) {
31918
- this.filePath = filePath ? path30.resolve(cwd, filePath) : path30.join(cwd, ".demian", "preferences.json");
32725
+ this.filePath = filePath ? path31.resolve(cwd, filePath) : path31.join(cwd, ".demian", "preferences.json");
31919
32726
  }
31920
32727
  async load() {
31921
32728
  if (!fs10.existsSync(this.filePath)) return void 0;
@@ -31933,13 +32740,13 @@ var UiPreferenceStore = class {
31933
32740
  model: selection.model,
31934
32741
  updatedAt: Date.now()
31935
32742
  };
31936
- await mkdir12(path30.dirname(this.filePath), { recursive: true });
31937
- await writeFile11(this.filePath, `${JSON.stringify(file, null, 2)}
32743
+ await mkdir13(path31.dirname(this.filePath), { recursive: true });
32744
+ await writeFile12(this.filePath, `${JSON.stringify(file, null, 2)}
31938
32745
  `, { mode: 384 });
31939
32746
  }
31940
32747
  async #read() {
31941
32748
  try {
31942
- return JSON.parse(await readFile14(this.filePath, "utf8"));
32749
+ return JSON.parse(await readFile15(this.filePath, "utf8"));
31943
32750
  } catch {
31944
32751
  return void 0;
31945
32752
  }
@@ -31955,9 +32762,9 @@ function preferenceKey(selection) {
31955
32762
 
31956
32763
  // src/models/catalog.ts
31957
32764
  import crypto6 from "node:crypto";
31958
- import { mkdir as mkdir13, readFile as readFile15, rename as rename5, writeFile as writeFile12 } from "node:fs/promises";
31959
- import os12 from "node:os";
31960
- import path31 from "node:path";
32765
+ import { mkdir as mkdir14, readFile as readFile16, rename as rename6, writeFile as writeFile13 } from "node:fs/promises";
32766
+ import os13 from "node:os";
32767
+ import path32 from "node:path";
31961
32768
  var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
31962
32769
  var PING_TIMEOUT_MS = 1500;
31963
32770
  var DEFAULT_CODEX_CLIENT_VERSION = "0.0.0";
@@ -32269,12 +33076,12 @@ async function fetchJson2(url, options2) {
32269
33076
  return text ? JSON.parse(text) : {};
32270
33077
  }
32271
33078
  function cachePath(cacheKey) {
32272
- return path31.join(os12.homedir(), ".demian", "cache", "models", `${safeCacheName(cacheKey)}.json`);
33079
+ return path32.join(os13.homedir(), ".demian", "cache", "models", `${safeCacheName(cacheKey)}.json`);
32273
33080
  }
32274
33081
  async function readCatalogCache(cacheKey, ttlMs, allow) {
32275
33082
  if (!allow) return void 0;
32276
33083
  try {
32277
- const raw = JSON.parse(await readFile15(cachePath(cacheKey), "utf8"));
33084
+ const raw = JSON.parse(await readFile16(cachePath(cacheKey), "utf8"));
32278
33085
  if (!raw.result) return void 0;
32279
33086
  if (ttlMs !== Number.POSITIVE_INFINITY && Date.now() - (raw.savedAt ?? 0) > ttlMs) return void 0;
32280
33087
  return { ...raw.result, source: "cache", models: raw.result.models.map((model) => ({ ...model, source: "cache" })) };
@@ -32284,10 +33091,10 @@ async function readCatalogCache(cacheKey, ttlMs, allow) {
32284
33091
  }
32285
33092
  async function writeCatalogCache(cacheKey, result) {
32286
33093
  const filePath = cachePath(cacheKey);
32287
- await mkdir13(path31.dirname(filePath), { recursive: true, mode: 448 });
33094
+ await mkdir14(path32.dirname(filePath), { recursive: true, mode: 448 });
32288
33095
  const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
32289
- await writeFile12(temp, JSON.stringify({ savedAt: Date.now(), result }, null, 2), { mode: 384 });
32290
- await rename5(temp, filePath);
33096
+ await writeFile13(temp, JSON.stringify({ savedAt: Date.now(), result }, null, 2), { mode: 384 });
33097
+ await rename6(temp, filePath);
32291
33098
  }
32292
33099
  function safeCacheName(providerName) {
32293
33100
  return providerName.replace(/[^a-zA-Z0-9_.-]+/g, "_");
@@ -32338,7 +33145,7 @@ async function codexClientVersion(override) {
32338
33145
  }
32339
33146
  async function readPackageVersion() {
32340
33147
  try {
32341
- const raw = JSON.parse(await readFile15(new URL("../package.json", import.meta.url), "utf8"));
33148
+ const raw = JSON.parse(await readFile16(new URL("../package.json", import.meta.url), "utf8"));
32342
33149
  return semverLike(raw.version) ?? DEFAULT_CODEX_CLIENT_VERSION;
32343
33150
  } catch {
32344
33151
  return DEFAULT_CODEX_CLIENT_VERSION;
@@ -32402,6 +33209,9 @@ function providerOptions(config, catalogs = {}) {
32402
33209
  permissionMode: provider.type === "claudecode" ? provider.permissionMode : void 0,
32403
33210
  ga: provider.type === "claudecode" ? claudeCodeGaEnabled(provider) : void 0,
32404
33211
  available,
33212
+ baseURL: typeof provider.baseURL === "string" ? provider.baseURL : void 0,
33213
+ apiKeyEnv: typeof provider.apiKeyEnv === "string" ? provider.apiKeyEnv : void 0,
33214
+ auth: sanitizeProviderAuth(provider.auth),
32405
33215
  catalog: catalog ? { status: catalog.status, source: catalog.source, message: catalog.message } : void 0
32406
33216
  };
32407
33217
  }).sort((left, right) => {
@@ -32473,17 +33283,228 @@ function providerCatalogAvailable(catalog) {
32473
33283
  function unavailableLabel(value) {
32474
33284
  return `(n/a) ${value}`;
32475
33285
  }
33286
+ function sanitizeProviderAuth(auth) {
33287
+ if (!auth || typeof auth !== "object" || Array.isArray(auth)) return void 0;
33288
+ const record = auth;
33289
+ const type = typeof record.type === "string" ? record.type : void 0;
33290
+ const header = typeof record.header === "string" ? record.header : void 0;
33291
+ return type || header ? { type, header } : void 0;
33292
+ }
32476
33293
  function errorMessage3(error) {
32477
33294
  return error instanceof Error ? error.message : String(error);
32478
33295
  }
32479
33296
 
33297
+ // src/ui/conversations.ts
33298
+ import { mkdir as mkdir15, readFile as readFile17, readdir as readdir4, rm as rm4, writeFile as writeFile14 } from "node:fs/promises";
33299
+ import path33 from "node:path";
33300
+ var CONVERSATIONS_DIR = "conversations";
33301
+ var CONVERSATION_INDEX_FILE = "conversations.json";
33302
+ var CONVERSATION_FILE = "conversation.json";
33303
+ var MAX_STORED_CONVERSATIONS = 40;
33304
+ var MAX_MODEL_HISTORY = 80;
33305
+ function createConversationRecord(input2) {
33306
+ const timestamp = input2.now ?? Date.now();
33307
+ return {
33308
+ id: sanitizeConversationId(input2.id),
33309
+ title: input2.title?.trim() || "New session",
33310
+ createdAt: timestamp,
33311
+ updatedAt: timestamp,
33312
+ cwd: path33.resolve(input2.cwd),
33313
+ modelHistory: cleanModelHistory(input2.history ?? []),
33314
+ snapshot: input2.snapshot
33315
+ };
33316
+ }
33317
+ async function loadConversationIndex(storageDir = defaultDemianStorageDir()) {
33318
+ const index = await readJson(path33.join(storageDir, CONVERSATION_INDEX_FILE));
33319
+ const conversations = Array.isArray(index?.conversations) ? index.conversations.map((item) => {
33320
+ if (!item || typeof item !== "object") return void 0;
33321
+ const object2 = item;
33322
+ const id = typeof object2.id === "string" ? sanitizeConversationId(object2.id) : "";
33323
+ if (!id) return void 0;
33324
+ return {
33325
+ id,
33326
+ title: typeof object2.title === "string" && object2.title.trim() ? object2.title.trim() : "New session",
33327
+ updatedAt: numberOrNow(object2.updatedAt),
33328
+ cwd: typeof object2.cwd === "string" ? object2.cwd : void 0
33329
+ };
33330
+ }).filter((item) => Boolean(item)) : [];
33331
+ return {
33332
+ selectedSessionId: typeof index?.selectedSessionId === "string" ? sanitizeConversationId(index.selectedSessionId) : void 0,
33333
+ conversations
33334
+ };
33335
+ }
33336
+ async function loadConversationRecords(storageDir = defaultDemianStorageDir()) {
33337
+ const index = await loadConversationIndex(storageDir);
33338
+ const ids = new Set(index.conversations.map((item) => item.id));
33339
+ for (const id of await listConversationRecordIds(storageDir)) ids.add(id);
33340
+ const records = (await Promise.all(
33341
+ [...ids].map(async (id) => {
33342
+ const item = await readJson(conversationRecordPath(storageDir, id));
33343
+ return normalizeConversationRecord(item, id);
33344
+ })
33345
+ )).filter((item) => Boolean(item));
33346
+ records.sort((a, b2) => b2.updatedAt - a.updatedAt);
33347
+ return { selectedSessionId: index.selectedSessionId, records };
33348
+ }
33349
+ async function saveConversationRecords(storageDir, records, selectedSessionId) {
33350
+ const root = storageDir ?? defaultDemianStorageDir();
33351
+ const kept = [...records].sort((a, b2) => b2.updatedAt - a.updatedAt).slice(0, MAX_STORED_CONVERSATIONS).map((record) => ({ ...record, id: sanitizeConversationId(record.id), modelHistory: cleanModelHistory(record.modelHistory) }));
33352
+ await mkdir15(path33.join(root, CONVERSATIONS_DIR), { recursive: true });
33353
+ for (const record of kept) {
33354
+ await writeJson(conversationRecordPath(root, record.id), {
33355
+ version: 1,
33356
+ id: record.id,
33357
+ title: record.title,
33358
+ createdAt: record.createdAt,
33359
+ updatedAt: record.updatedAt,
33360
+ cwd: record.cwd,
33361
+ modelHistory: record.modelHistory,
33362
+ snapshot: record.snapshot
33363
+ });
33364
+ }
33365
+ await writeJson(path33.join(root, CONVERSATION_INDEX_FILE), {
33366
+ version: 1,
33367
+ selectedSessionId,
33368
+ conversations: kept.map((record) => ({
33369
+ id: record.id,
33370
+ title: record.title,
33371
+ updatedAt: record.updatedAt,
33372
+ cwd: record.cwd,
33373
+ path: conversationDir(root, record.id)
33374
+ }))
33375
+ });
33376
+ }
33377
+ async function deleteConversationRecord(storageDir, id) {
33378
+ const root = storageDir ?? defaultDemianStorageDir();
33379
+ await rm4(conversationDir(root, id), { recursive: true, force: true });
33380
+ }
33381
+ function findConversation(records, target) {
33382
+ const value = target?.trim();
33383
+ if (!value) return void 0;
33384
+ const index = Number(value);
33385
+ if (Number.isInteger(index) && index >= 1 && index <= records.length) return records[index - 1];
33386
+ const normalized = value.toLowerCase();
33387
+ return records.find((record) => record.id === value || record.id.startsWith(value)) ?? records.find((record) => record.title.toLowerCase() === normalized) ?? records.find((record) => record.title.toLowerCase().includes(normalized));
33388
+ }
33389
+ function sortConversationRecords(records, cwd) {
33390
+ const resolvedCwd = path33.resolve(cwd);
33391
+ return [...records].sort((a, b2) => {
33392
+ const aCurrent = isConversationForCwd(a, resolvedCwd) ? 0 : 1;
33393
+ const bCurrent = isConversationForCwd(b2, resolvedCwd) ? 0 : 1;
33394
+ if (aCurrent !== bCurrent) return aCurrent - bCurrent;
33395
+ const statusDelta = conversationStatusRank(conversationRuntimeStatus(a, resolvedCwd)) - conversationStatusRank(conversationRuntimeStatus(b2, resolvedCwd));
33396
+ if (statusDelta !== 0) return statusDelta;
33397
+ return b2.updatedAt - a.updatedAt;
33398
+ });
33399
+ }
33400
+ function conversationRuntimeStatus(record, cwd) {
33401
+ if (record.snapshot?.inputMode === "running") return "running";
33402
+ return isConversationForCwd(record, cwd) ? "ready" : "stored";
33403
+ }
33404
+ function isConversationForCwd(record, cwd) {
33405
+ return path33.resolve(record.cwd) === path33.resolve(cwd);
33406
+ }
33407
+ function conversationSummaryLine(record, index, activeId) {
33408
+ const active = record.id === activeId ? "*" : " ";
33409
+ const age = formatRelativeAge(Date.now() - record.updatedAt);
33410
+ return `${active} ${index + 1}. ${record.title} ${record.id} ${age} ${record.cwd}`;
33411
+ }
33412
+ function conversationStatusRank(status) {
33413
+ if (status === "running") return 0;
33414
+ if (status === "ready") return 1;
33415
+ return 2;
33416
+ }
33417
+ function titleFromHistory(history, fallback = "New session") {
33418
+ const firstUser = history.find((message) => message.role === "user" && typeof message.content === "string");
33419
+ return firstLine(firstUser?.content ?? fallback);
33420
+ }
33421
+ function cleanModelHistory(history) {
33422
+ return history.filter((message) => !isInternalModelHistoryMessage(message)).slice(-MAX_MODEL_HISTORY);
33423
+ }
33424
+ function conversationDir(storageDir, id) {
33425
+ return path33.join(storageDir, CONVERSATIONS_DIR, sanitizeConversationId(id));
33426
+ }
33427
+ function conversationRecordPath(storageDir, id) {
33428
+ return path33.join(conversationDir(storageDir, id), CONVERSATION_FILE);
33429
+ }
33430
+ async function listConversationRecordIds(storageDir) {
33431
+ try {
33432
+ const entries = await readdir4(path33.join(storageDir, CONVERSATIONS_DIR), { withFileTypes: true });
33433
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => sanitizeConversationId(entry.name)).filter(Boolean);
33434
+ } catch {
33435
+ return [];
33436
+ }
33437
+ }
33438
+ async function readJson(filePath) {
33439
+ try {
33440
+ return JSON.parse(await readFile17(filePath, "utf8"));
33441
+ } catch {
33442
+ return void 0;
33443
+ }
33444
+ }
33445
+ async function writeJson(filePath, value) {
33446
+ await mkdir15(path33.dirname(filePath), { recursive: true });
33447
+ await writeFile14(filePath, `${JSON.stringify(value, null, 2)}
33448
+ `, "utf8");
33449
+ }
33450
+ function normalizeConversationRecord(value, fallbackId) {
33451
+ if (!value || typeof value !== "object") return void 0;
33452
+ const object2 = value;
33453
+ const id = sanitizeConversationId(typeof object2.id === "string" ? object2.id : fallbackId);
33454
+ if (!id) return void 0;
33455
+ const history = Array.isArray(object2.modelHistory) ? object2.modelHistory.filter(isMessage) : [];
33456
+ const snapshot = object2.snapshot && typeof object2.snapshot === "object" && !Array.isArray(object2.snapshot) ? object2.snapshot : void 0;
33457
+ return {
33458
+ id,
33459
+ title: typeof object2.title === "string" && object2.title.trim() ? object2.title.trim() : titleFromHistory(history),
33460
+ createdAt: numberOrNow(object2.createdAt),
33461
+ updatedAt: numberOrNow(object2.updatedAt),
33462
+ cwd: typeof object2.cwd === "string" && object2.cwd.trim() ? object2.cwd : process.cwd(),
33463
+ modelHistory: cleanModelHistory(history),
33464
+ snapshot
33465
+ };
33466
+ }
33467
+ function isMessage(value) {
33468
+ if (!value || typeof value !== "object") return false;
33469
+ const message = value;
33470
+ if (message.role === "user") return typeof message.content === "string" || Array.isArray(message.content);
33471
+ if (message.role === "assistant") return message.content === void 0 || message.content === null || typeof message.content === "string" || Array.isArray(message.toolCalls);
33472
+ if (message.role === "tool") return typeof message.toolCallId === "string" && typeof message.name === "string" && typeof message.content === "string";
33473
+ if (message.role === "system") return typeof message.content === "string";
33474
+ return false;
33475
+ }
33476
+ function isInternalModelHistoryMessage(message) {
33477
+ return message.role === "user" && typeof message.content === "string" && /^You are running as sub agent:/i.test(message.content.trim());
33478
+ }
33479
+ function sanitizeConversationId(id) {
33480
+ return String(id || "conversation").replace(/[^a-zA-Z0-9._-]+/g, "_") || "conversation";
33481
+ }
33482
+ function numberOrNow(value) {
33483
+ return typeof value === "number" && Number.isFinite(value) ? value : Date.now();
33484
+ }
33485
+ function firstLine(value) {
33486
+ const line = value.replace(/\s+/g, " ").trim();
33487
+ if (!line) return "New session";
33488
+ return line.length > 60 ? `${line.slice(0, 57)}...` : line;
33489
+ }
33490
+ function formatRelativeAge(ms2) {
33491
+ const seconds = Math.max(0, Math.floor(ms2 / 1e3));
33492
+ if (seconds < 60) return `${seconds}s ago`;
33493
+ const minutes = Math.floor(seconds / 60);
33494
+ if (minutes < 60) return `${minutes}m ago`;
33495
+ const hours = Math.floor(minutes / 60);
33496
+ if (hours < 48) return `${hours}h ago`;
33497
+ return `${Math.floor(hours / 24)}d ago`;
33498
+ }
33499
+
32480
33500
  // src/ui/tui/controller.ts
32481
33501
  async function runTuiSession(flags, store2) {
32482
- const cwd = path32.resolve(flags.cwd ?? process.cwd());
33502
+ const cwd = path34.resolve(flags.cwd ?? process.cwd());
32483
33503
  const eventBus = new EventBus();
32484
33504
  eventBus.subscribe((event) => store2.handleEvent(event));
32485
- const loadedConfig2 = await loadConfig({ cwd, configPath: flags.configPath });
32486
- const config = flags.context ? mergeConfig(loadedConfig2, {
33505
+ const configInit = await ensureTuiConfigExists(flags);
33506
+ let loadedConfig2 = await loadConfig({ cwd, configPath: flags.configPath });
33507
+ let config = flags.context ? mergeConfig(loadedConfig2, {
32487
33508
  context: {
32488
33509
  main: flags.context
32489
33510
  }
@@ -32503,6 +33524,10 @@ async function runTuiSession(flags, store2) {
32503
33524
  store2.configureSettings(initialSelection, await providerOptionsWithCatalog(config, { timeoutMs: 1500 }), {
32504
33525
  agent: agentName,
32505
33526
  cwd,
33527
+ configPath: configMutationPath(flags),
33528
+ configCreated: configInit.created,
33529
+ configDefaultProvider: config.defaultProvider,
33530
+ configMessage: configInit.created ? `Created config at ${configInit.path}` : void 0,
32506
33531
  agentOptions,
32507
33532
  contextEfficiency: {
32508
33533
  maxContextTokens: config.context.main.maxContextTokens,
@@ -32511,10 +33536,21 @@ async function runTuiSession(flags, store2) {
32511
33536
  }
32512
33537
  });
32513
33538
  let nextPrompt = flags.prompt;
32514
- let history = flags.initialHistory ? [...flags.initialHistory] : [];
33539
+ let openConfigOnFirstPrompt = configInit.created && !flags.prompt.trim();
33540
+ const conversationsEnabled = flags.conversationManagement === true;
33541
+ const conversationState = conversationsEnabled ? await initializeConversationState(cwd, flags) : void 0;
33542
+ let currentConversation = conversationState?.current;
33543
+ const conversationRecords = conversationState?.records ?? /* @__PURE__ */ new Map();
33544
+ const shouldShowStartupSessionSelector = conversationsEnabled && !flags.prompt.trim() && !flags.sessionId;
33545
+ let history = flags.initialHistory ? [...flags.initialHistory] : currentConversation?.modelHistory ? [...currentConversation.modelHistory] : [];
32515
33546
  let lastExternalSessionKey;
32516
- const externalSessions = new ClaudeCodeSessionMap();
32517
- const rootSessionId = flags.sessionId ?? createRootSessionId();
33547
+ let externalSessions = new ClaudeCodeSessionMap();
33548
+ const runtimeByConversation = /* @__PURE__ */ new Map();
33549
+ let rootSessionId = currentConversation?.id ?? flags.sessionId ?? createRootSessionId();
33550
+ if (currentConversation && !shouldShowStartupSessionSelector) {
33551
+ store2.restoreConversationSnapshot(currentConversation.snapshot, { sessionId: currentConversation.id, title: currentConversation.title, cwd: currentConversation.cwd });
33552
+ runtimeByConversation.set(currentConversation.id, { externalSessions, lastExternalSessionKey });
33553
+ }
32518
33554
  const goalTitleGenerator = async (input2) => {
32519
33555
  const selection = store2.currentSelection();
32520
33556
  const runtime = resolveProviderRuntimeConfig(config, selection);
@@ -32532,19 +33568,48 @@ async function runTuiSession(flags, store2) {
32532
33568
  if (resolved.kind === "external-agent") return input2.objective.trim().split(/\s+/).slice(0, 8).join(" ");
32533
33569
  return summarizeGoalTitleWithProvider({ ...input2, provider: resolved.provider, model: resolved.model });
32534
33570
  };
33571
+ if (shouldShowStartupSessionSelector) {
33572
+ const selected = await store2.requestSessionSelection(startupSessionOptions());
33573
+ if (selected.kind === "exit" || store2.exitRequested()) {
33574
+ await saveCurrentSelection();
33575
+ return 0;
33576
+ }
33577
+ await applyStartupSessionSelection(selected);
33578
+ }
32535
33579
  for (; ; ) {
32536
- const prompt = await store2.requestPrompt(nextPrompt);
33580
+ const promptPromise = store2.requestPrompt(nextPrompt);
33581
+ if (openConfigOnFirstPrompt) {
33582
+ openConfigOnFirstPrompt = false;
33583
+ store2.openConfigManager(`Created config at ${configInit.path}`);
33584
+ }
33585
+ const prompt = await promptPromise;
32537
33586
  nextPrompt = void 0;
32538
33587
  if (store2.exitRequested()) {
32539
33588
  await saveCurrentSelection();
32540
33589
  return 0;
32541
33590
  }
32542
33591
  if (!prompt) return 0;
33592
+ const configAction = parseTuiConfigAction(prompt);
33593
+ if (configAction) {
33594
+ await handleConfigAction(configAction);
33595
+ continue;
33596
+ }
33597
+ const sessionAction = parseTuiSessionAction(prompt);
33598
+ if (sessionAction) {
33599
+ await handleSessionSelectionShortcut();
33600
+ continue;
33601
+ }
33602
+ const sessionCommand = parseSessionCommand(prompt);
33603
+ if (sessionCommand) {
33604
+ await handleSessionCommand(sessionCommand);
33605
+ continue;
33606
+ }
32543
33607
  if (isCompactCommand(prompt)) {
32544
33608
  const result = compactInteractiveHistory(history, config.context.main);
32545
33609
  history = result.messages;
32546
33610
  store2.prepareForPrompt("waiting for next message");
32547
33611
  store2.markHistoryCompacted(result);
33612
+ await persistCurrentConversation();
32548
33613
  continue;
32549
33614
  }
32550
33615
  const mode = resolveAgentMode(config, flags.mode);
@@ -32696,6 +33761,7 @@ async function runTuiSession(flags, store2) {
32696
33761
  history = interactiveHistoryFromRunMessages(result.messages);
32697
33762
  lastExternalSessionKey = externalSessionKey ?? lastExternalSessionKey;
32698
33763
  }
33764
+ await persistCurrentConversation();
32699
33765
  } catch (error) {
32700
33766
  store2.setStopTask(void 0);
32701
33767
  if (activeAbort?.signal.aborted) {
@@ -32708,6 +33774,7 @@ async function runTuiSession(flags, store2) {
32708
33774
  store2.setStopTask(void 0);
32709
33775
  }
32710
33776
  if (store2.exitRequested()) {
33777
+ await persistCurrentConversation();
32711
33778
  await saveCurrentSelection();
32712
33779
  return 0;
32713
33780
  }
@@ -32716,6 +33783,184 @@ async function runTuiSession(flags, store2) {
32716
33783
  async function saveCurrentSelection() {
32717
33784
  await saveSelection(store2.currentSelection());
32718
33785
  }
33786
+ async function handleSessionCommand(command) {
33787
+ if (!conversationsEnabled) {
33788
+ store2.showSystemMessage("Sessions", "Session management is available in demian-cli TUI, but this embedded runtime lets the host manage sessions.");
33789
+ store2.prepareForPrompt("waiting for next message");
33790
+ return;
33791
+ }
33792
+ if (!currentConversation) {
33793
+ currentConversation = createConversationRecord({ id: flags.sessionId ?? createRootSessionId(), cwd });
33794
+ conversationRecords.set(currentConversation.id, currentConversation);
33795
+ }
33796
+ if (command.action === "help") {
33797
+ store2.showSystemMessage("Sessions", sessionUsage());
33798
+ store2.prepareForPrompt("waiting for next message");
33799
+ return;
33800
+ }
33801
+ if (command.action === "list") {
33802
+ store2.showSystemMessage("Sessions", sessionListMessage());
33803
+ store2.prepareForPrompt("waiting for next message");
33804
+ return;
33805
+ }
33806
+ if (command.action === "current") {
33807
+ store2.showSystemMessage("Current Session", conversationSummaryLine(currentConversation, sortedConversations().findIndex((item) => item.id === currentConversation?.id), currentConversation.id));
33808
+ store2.prepareForPrompt("waiting for next message");
33809
+ return;
33810
+ }
33811
+ if (command.action === "new") {
33812
+ await persistCurrentConversation();
33813
+ const next = createConversationRecord({ id: createRootSessionId(), cwd, title: command.title });
33814
+ conversationRecords.set(next.id, next);
33815
+ await switchConversation(next);
33816
+ store2.prepareForPrompt(`new session: ${next.title}`);
33817
+ await persistCurrentConversation();
33818
+ return;
33819
+ }
33820
+ if (command.action === "switch") {
33821
+ const target = findConversation(sortedConversations(), command.target);
33822
+ if (!target) {
33823
+ store2.showSystemMessage("Sessions", `No session matched "${command.target ?? ""}".
33824
+
33825
+ ${sessionListMessage()}`);
33826
+ store2.prepareForPrompt("waiting for next message");
33827
+ return;
33828
+ }
33829
+ await persistCurrentConversation();
33830
+ await switchConversation(target);
33831
+ store2.prepareForPrompt(`switched session: ${target.title}`);
33832
+ await persistCurrentConversation();
33833
+ return;
33834
+ }
33835
+ if (command.action === "delete") {
33836
+ const target = findConversation(sortedConversations(), command.target);
33837
+ if (!target) {
33838
+ store2.showSystemMessage("Sessions", `No session matched "${command.target ?? ""}".`);
33839
+ store2.prepareForPrompt("waiting for next message");
33840
+ return;
33841
+ }
33842
+ conversationRecords.delete(target.id);
33843
+ runtimeByConversation.delete(target.id);
33844
+ await deleteConversationRecord(flags.conversationStorageDir, target.id);
33845
+ if (currentConversation?.id === target.id) {
33846
+ const next = sortedConversations()[0] ?? createConversationRecord({ id: createRootSessionId(), cwd });
33847
+ conversationRecords.set(next.id, next);
33848
+ await switchConversation(next);
33849
+ }
33850
+ store2.prepareForPrompt(`deleted session: ${target.title}`);
33851
+ await persistCurrentConversation();
33852
+ return;
33853
+ }
33854
+ if (command.action === "rename") {
33855
+ const title = command.title?.trim();
33856
+ if (!title) {
33857
+ store2.showSystemMessage("Sessions", "Usage: /session rename <title>");
33858
+ store2.prepareForPrompt("waiting for next message");
33859
+ return;
33860
+ }
33861
+ currentConversation.title = title;
33862
+ currentConversation.updatedAt = Date.now();
33863
+ store2.prepareForPrompt(`renamed session: ${title}`);
33864
+ await persistCurrentConversation();
33865
+ return;
33866
+ }
33867
+ if (command.action === "clear") {
33868
+ history = [];
33869
+ currentConversation.modelHistory = [];
33870
+ currentConversation.snapshot = void 0;
33871
+ currentConversation.title = "New session";
33872
+ currentConversation.updatedAt = Date.now();
33873
+ store2.clearConversation(currentConversation.title);
33874
+ store2.prepareForPrompt("cleared current conversation");
33875
+ await persistCurrentConversation();
33876
+ }
33877
+ }
33878
+ async function handleSessionSelectionShortcut() {
33879
+ if (!conversationsEnabled) {
33880
+ store2.showSystemMessage("Sessions", "Session management is available in demian-cli TUI, but this embedded runtime lets the host manage sessions.");
33881
+ store2.prepareForPrompt("waiting for next message");
33882
+ return;
33883
+ }
33884
+ if (!currentConversation) {
33885
+ currentConversation = createConversationRecord({ id: flags.sessionId ?? createRootSessionId(), cwd });
33886
+ conversationRecords.set(currentConversation.id, currentConversation);
33887
+ }
33888
+ await persistCurrentConversation();
33889
+ const selection = await store2.requestSessionSelection(startupSessionOptions(), { cancel: "prompt" });
33890
+ if (selection.kind === "cancel") {
33891
+ store2.prepareForPrompt("waiting for next message");
33892
+ return;
33893
+ }
33894
+ if (selection.kind === "exit" || store2.exitRequested()) return;
33895
+ await applyStartupSessionSelection(selection);
33896
+ }
33897
+ function startupSessionOptions() {
33898
+ const records = sortedConversations();
33899
+ const sessionOptions = records.map((record) => ({
33900
+ kind: "session",
33901
+ id: record.id,
33902
+ title: record.title,
33903
+ cwd: record.cwd,
33904
+ status: conversationRuntimeStatus(record, cwd),
33905
+ updatedAt: record.updatedAt,
33906
+ currentWorkspace: path34.resolve(record.cwd) === cwd
33907
+ }));
33908
+ return [...sessionOptions, { kind: "new", title: "New session", cwd, status: "ready", currentWorkspace: true }];
33909
+ }
33910
+ async function applyStartupSessionSelection(selection) {
33911
+ if (selection.kind === "cancel") {
33912
+ store2.prepareForPrompt("waiting for next message");
33913
+ return;
33914
+ }
33915
+ if (selection.kind === "new") {
33916
+ const next = createConversationRecord({ id: createRootSessionId(), cwd });
33917
+ conversationRecords.set(next.id, next);
33918
+ await switchConversation(next);
33919
+ store2.prepareForPrompt("new session ready");
33920
+ await saveConversationRecords(flags.conversationStorageDir, [...conversationRecords.values()], next.id);
33921
+ return;
33922
+ }
33923
+ const target = conversationRecords.get(selection.id) ?? findConversation(sortedConversations(), selection.id);
33924
+ if (!target) {
33925
+ const next = createConversationRecord({ id: createRootSessionId(), cwd });
33926
+ conversationRecords.set(next.id, next);
33927
+ await switchConversation(next);
33928
+ store2.prepareForPrompt("new session ready");
33929
+ await saveConversationRecords(flags.conversationStorageDir, [...conversationRecords.values()], next.id);
33930
+ return;
33931
+ }
33932
+ await switchConversation(target);
33933
+ store2.prepareForPrompt(`session ready: ${target.title}`);
33934
+ await saveConversationRecords(flags.conversationStorageDir, [...conversationRecords.values()], target.id);
33935
+ }
33936
+ async function switchConversation(record) {
33937
+ currentConversation = record;
33938
+ rootSessionId = record.id;
33939
+ history = [...record.modelHistory];
33940
+ const runtime = runtimeByConversation.get(record.id) ?? { externalSessions: new ClaudeCodeSessionMap(), lastExternalSessionKey: void 0 };
33941
+ externalSessions = runtime.externalSessions;
33942
+ lastExternalSessionKey = runtime.lastExternalSessionKey;
33943
+ runtimeByConversation.set(record.id, runtime);
33944
+ store2.restoreConversationSnapshot(record.snapshot, { sessionId: record.id, title: record.title, cwd: record.cwd || cwd });
33945
+ }
33946
+ async function persistCurrentConversation() {
33947
+ if (!conversationsEnabled || !currentConversation) return;
33948
+ currentConversation.modelHistory = cleanModelHistory(history);
33949
+ if (currentConversation.title === "New session" && currentConversation.modelHistory.length > 0) currentConversation.title = titleFromHistory(currentConversation.modelHistory);
33950
+ currentConversation.snapshot = store2.conversationSnapshot();
33951
+ currentConversation.updatedAt = Date.now();
33952
+ conversationRecords.set(currentConversation.id, currentConversation);
33953
+ runtimeByConversation.set(currentConversation.id, { externalSessions, lastExternalSessionKey });
33954
+ await saveConversationRecords(flags.conversationStorageDir, [...conversationRecords.values()], currentConversation.id);
33955
+ }
33956
+ function sortedConversations() {
33957
+ return sortConversationRecords([...conversationRecords.values()], cwd);
33958
+ }
33959
+ function sessionListMessage() {
33960
+ const records = sortedConversations();
33961
+ if (!records.length) return "No saved sessions.";
33962
+ return ["Saved sessions:", ...records.map((record, index) => conversationSummaryLine(record, index, currentConversation?.id)), "", "Use /session switch <number|id|title> to open one."].join("\n");
33963
+ }
32719
33964
  async function saveSelection(selection) {
32720
33965
  if (!shouldPersistSelection(selection)) return;
32721
33966
  const key = preferenceKey(selection);
@@ -32723,298 +33968,124 @@ async function runTuiSession(flags, store2) {
32723
33968
  await preferenceStore.save(selection);
32724
33969
  savedSelectionKey = key;
32725
33970
  }
32726
- }
32727
- function isGoalStateClearingAction(action) {
32728
- return action === "status" || action === "pause" || action === "resume";
32729
- }
32730
-
32731
- // src/config-watcher.ts
32732
- import { EventEmitter } from "node:events";
32733
- import fs11 from "node:fs";
32734
- import path34 from "node:path";
32735
-
32736
- // src/config-scaffold.ts
32737
- import { chmod as chmod3, mkdir as mkdir14, readFile as readFile16, rename as rename6, stat as stat8, writeFile as writeFile13 } from "node:fs/promises";
32738
- import os13 from "node:os";
32739
- import path33 from "node:path";
32740
- function defaultUserConfigPath() {
32741
- return path33.join(os13.homedir(), ".demian", "config.json");
32742
- }
32743
- function defaultUserConfig(defaultProvider = detectDefaultProvider()) {
32744
- return {
32745
- version: 2,
32746
- defaultProvider,
32747
- providers: {
32748
- openai: {
32749
- type: "openai-compatible",
32750
- baseURL: "https://api.openai.com/v1",
32751
- apiKey: "",
32752
- apiKeyEnv: "OPENAI_API_KEY",
32753
- catalog: { type: "openai-models", endpoint: "https://api.openai.com/v1/models" }
32754
- },
32755
- anthropic: {
32756
- type: "anthropic",
32757
- baseURL: "https://api.anthropic.com/v1",
32758
- apiKey: "",
32759
- apiKeyEnv: "ANTHROPIC_API_KEY",
32760
- catalog: { type: "anthropic-models", endpoint: "https://api.anthropic.com/v1/models" }
32761
- },
32762
- gemini: {
32763
- type: "openai-compatible",
32764
- baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
32765
- apiKey: "",
32766
- apiKeyEnv: "GEMINI_API_KEY",
32767
- apiKeyEnvAliases: ["GOOGLE_API_KEY"],
32768
- catalog: { type: "gemini-openai-models", endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/models" }
32769
- },
32770
- groq: {
32771
- type: "openai-compatible",
32772
- baseURL: "https://api.groq.com/openai/v1",
32773
- apiKey: "",
32774
- apiKeyEnv: "GROQ_API_KEY",
32775
- catalog: { type: "groq-models", endpoint: "https://api.groq.com/openai/v1/models" }
32776
- },
32777
- azure: {
32778
- type: "openai-compatible",
32779
- auth: { type: "api-key", header: "api-key" },
32780
- modelProfiles: [
32781
- {
32782
- name: "azure-example",
32783
- displayName: "Azure example",
32784
- model: "azure-deployment-name",
32785
- baseURL: "https://example.openai.azure.com/openai/v1",
32786
- apiKey: "",
32787
- apiKeyEnv: "AZURE_OPENAI_API_KEY"
32788
- }
32789
- ]
32790
- },
32791
- lmstudio: {
32792
- type: "openai-compatible",
32793
- baseURL: "http://localhost:1234/v1",
32794
- apiKey: "lm-studio",
32795
- modelProfiles: [],
32796
- catalog: { type: "openai-compatible-models", endpoint: "http://localhost:1234/v1/models" }
32797
- },
32798
- "ollama-local": {
32799
- type: "openai-compatible",
32800
- baseURL: "http://localhost:11434/v1",
32801
- apiKey: "ollama",
32802
- modelProfiles: [],
32803
- quirks: { omitTemperature: true },
32804
- catalog: { type: "openai-compatible-models", endpoint: "http://localhost:11434/v1/models" }
32805
- },
32806
- "ollama-cloud": {
32807
- type: "ollama",
32808
- baseURL: "https://ollama.com/api",
32809
- apiKey: "",
32810
- apiKeyEnv: "OLLAMA_API_KEY",
32811
- modelProfiles: [],
32812
- catalog: { type: "ollama-tags", endpoint: "https://ollama.com/api/tags" }
32813
- },
32814
- llamacpp: {
32815
- type: "openai-compatible",
32816
- baseURL: "http://localhost:8080/v1",
32817
- apiKey: "llama.cpp",
32818
- modelProfiles: [],
32819
- catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8080/v1/models" }
32820
- },
32821
- vllm: {
32822
- type: "openai-compatible",
32823
- baseURL: "http://localhost:8000/v1",
32824
- apiKey: "vllm",
32825
- modelProfiles: [],
32826
- catalog: { type: "openai-compatible-models", endpoint: "http://localhost:8000/v1/models" }
32827
- },
32828
- codex: {
32829
- type: "codex",
32830
- baseURL: "https://chatgpt.com/backend-api/codex",
32831
- authStore: "auto",
32832
- allowApiKeyFallback: false,
32833
- promptCacheKey: "root-session",
32834
- catalog: { type: "codex-oauth-models" }
32835
- },
32836
- claudecode: {
32837
- type: "claudecode",
32838
- runtime: "agent-sdk",
32839
- cliPath: "~/.local/bin/claude",
32840
- cwdMode: "session",
32841
- historyPolicy: "passthrough-resume",
32842
- onInvalidResume: "fresh",
32843
- attachmentFallback: "block",
32844
- allowSubagents: false,
32845
- sanitizeApiKeyEnv: true,
32846
- authPreflight: true,
32847
- useBareMode: false,
32848
- usageLedgerScope: "process",
32849
- sessionLock: true,
32850
- abortPolicy: "record-only",
32851
- catalog: { type: "claudecode-supported-models" }
33971
+ async function handleConfigAction(action) {
33972
+ const path37 = configMutationPath(flags);
33973
+ try {
33974
+ let message = "Config updated.";
33975
+ let nextSelection = {};
33976
+ if (action.type === "setDefaultProvider") {
33977
+ await updateConfigDefaults({ path: path37, defaultProvider: action.provider });
33978
+ message = `Default provider saved: ${action.provider}`;
33979
+ nextSelection = { provider: action.provider };
33980
+ } else if (action.type === "setDefaultModel") {
33981
+ await setDefaultModelProfile({
33982
+ path: path37,
33983
+ provider: action.provider,
33984
+ profileName: action.profileName,
33985
+ name: action.name,
33986
+ displayName: action.displayName,
33987
+ model: action.model
33988
+ });
33989
+ message = `Default model saved for ${action.provider}: ${action.displayName ?? action.model}`;
33990
+ nextSelection = { provider: action.provider, model: action.displayName ?? action.model };
33991
+ } else if (action.type === "addProvider") {
33992
+ await addProvider({ path: path37, name: action.name, preset: action.preset, apiKeyEnv: action.apiKeyEnv });
33993
+ message = `Provider added: ${action.name}`;
33994
+ nextSelection = { provider: action.name };
33995
+ } else if (action.type === "updateProvider") {
33996
+ await updateProviderSettings({
33997
+ path: path37,
33998
+ provider: action.provider,
33999
+ ...action.field === "baseURL" ? { baseURL: action.value } : {},
34000
+ ...action.field === "apiKeyEnv" ? { apiKeyEnv: action.value } : {},
34001
+ ...action.field === "authHeader" ? { authHeader: action.value } : {}
34002
+ });
34003
+ message = `Provider ${action.field} saved: ${action.provider}`;
34004
+ nextSelection = { provider: action.provider };
32852
34005
  }
34006
+ await reloadConfigSettings(message, nextSelection);
34007
+ } catch (error) {
34008
+ store2.prepareForPrompt("config action failed");
34009
+ store2.showSystemMessage("Config", errorMessage(error));
34010
+ store2.openConfigManager(errorMessage(error));
32853
34011
  }
32854
- };
32855
- }
32856
- async function createUserConfig(options2 = {}) {
32857
- const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
32858
- const content = `${JSON.stringify(defaultUserConfig(options2.defaultProvider), null, 2)}
32859
- `;
32860
- if (options2.print) return { path: filePath, created: false, content };
32861
- const existed = await exists(filePath);
32862
- if (existed && !options2.force) return { path: filePath, created: false, content: await readFile16(filePath, "utf8"), existed: true };
32863
- await writeJsonAtomic(filePath, content);
32864
- return { path: filePath, created: true, content, existed };
32865
- }
32866
- async function addProvider(options2) {
32867
- const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
32868
- const config = await readConfigObject(filePath);
32869
- const providers = objectValue(config.providers);
32870
- const name = options2.name;
32871
- if (providers[name] && !options2.force) throw new Error(`Provider ${name} already exists. Use --force to overwrite it.`);
32872
- providers[name] = providerPreset(options2);
32873
- config.providers = providers;
32874
- const content = `${JSON.stringify(config, null, 2)}
32875
- `;
32876
- await writeJsonAtomic(filePath, content);
32877
- return { path: filePath, created: true, content };
32878
- }
32879
- async function addModelProfile(options2) {
32880
- const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
32881
- const config = await readConfigObject(filePath);
32882
- const providers = objectValue(config.providers);
32883
- const provider = objectValue(providers[options2.provider]);
32884
- const profiles = Array.isArray(provider.modelProfiles) ? [...provider.modelProfiles] : [];
32885
- const displayName = options2.displayName ?? options2.name;
32886
- const existingByName = profiles.findIndex((entry) => entry.name === options2.name);
32887
- const existingByDisplay = profiles.findIndex((entry) => entry.displayName === displayName);
32888
- if (existingByName >= 0 && !options2.force) throw new Error(`Profile name "${options2.name}" already exists on provider ${options2.provider}. Use --force to overwrite it.`);
32889
- if (existingByDisplay >= 0 && existingByDisplay !== existingByName && !options2.force) {
32890
- 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.`);
32891
- }
32892
- const next = {
32893
- name: options2.name,
32894
- displayName,
32895
- model: options2.model,
32896
- ...options2.baseURL ? { baseURL: options2.baseURL } : {},
32897
- ...options2.apiKey !== void 0 || options2.apiKeyEnv ? { apiKey: options2.apiKey ?? "" } : {},
32898
- ...options2.apiKeyEnv ? { apiKeyEnv: options2.apiKeyEnv } : {}
32899
- };
32900
- if (existingByName >= 0) profiles[existingByName] = next;
32901
- else profiles.push(next);
32902
- provider.modelProfiles = profiles;
32903
- providers[options2.provider] = provider;
32904
- config.providers = providers;
32905
- const content = `${JSON.stringify(config, null, 2)}
32906
- `;
32907
- await writeJsonAtomic(filePath, content);
32908
- return { path: filePath, created: true, content };
32909
- }
32910
- async function updateConfigDefaults(options2) {
32911
- const filePath = expandHome2(options2.path ?? defaultUserConfigPath());
32912
- const config = await readConfigObject(filePath);
32913
- const defaultProvider = normalizeOptionalName(options2.defaultProvider);
32914
- const defaultAgent = normalizeOptionalName(options2.defaultAgent);
32915
- if (!defaultProvider && !defaultAgent) throw new Error("At least one of defaultProvider or defaultAgent is required.");
32916
- if (defaultProvider) config.defaultProvider = defaultProvider;
32917
- if (defaultAgent) config.defaultAgent = defaultAgent;
32918
- const content = `${JSON.stringify(config, null, 2)}
32919
- `;
32920
- await writeJsonAtomic(filePath, content);
32921
- return { path: filePath, created: true, content };
32922
- }
32923
- async function readConfigObject(filePath) {
32924
- if (!await exists(filePath)) await createUserConfig({ path: filePath });
32925
- const raw = JSON.parse(await readFile16(filePath, "utf8"));
32926
- raw.version ??= 2;
32927
- raw.providers = objectValue(raw.providers);
32928
- return raw;
32929
- }
32930
- function providerPreset(options2) {
32931
- const preset = options2.preset ?? options2.name;
32932
- const auth = apiKeyAuthFields(options2);
32933
- const openAIAuth = options2.authHeader ? { auth: { type: "api-key", header: options2.authHeader } } : {};
32934
- if (preset === "openai") return { type: "openai-compatible", baseURL: options2.baseURL ?? "https://api.openai.com/v1", ...apiKeyAuthFields(options2, "OPENAI_API_KEY"), ...openAIAuth, catalog: { type: "openai-models", endpoint: `${(options2.baseURL ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/models` } };
32935
- if (preset === "anthropic") return { type: "anthropic", baseURL: options2.baseURL ?? "https://api.anthropic.com/v1", ...apiKeyAuthFields(options2, "ANTHROPIC_API_KEY"), catalog: { type: "anthropic-models", endpoint: `${(options2.baseURL ?? "https://api.anthropic.com/v1").replace(/\/+$/, "")}/models` } };
32936
- if (preset === "gemini") {
32937
- const geminiBase = options2.baseURL ?? "https://generativelanguage.googleapis.com/v1beta/openai/";
32938
- return { type: "openai-compatible", baseURL: geminiBase, ...apiKeyAuthFields(options2, "GEMINI_API_KEY"), apiKeyEnvAliases: ["GOOGLE_API_KEY"], ...openAIAuth, catalog: { type: "gemini-openai-models", endpoint: `${geminiBase.replace(/\/+$/, "")}/models` } };
32939
34012
  }
32940
- if (preset === "groq") return { type: "openai-compatible", baseURL: options2.baseURL ?? "https://api.groq.com/openai/v1", ...apiKeyAuthFields(options2, "GROQ_API_KEY"), ...openAIAuth, catalog: { type: "groq-models", endpoint: `${(options2.baseURL ?? "https://api.groq.com/openai/v1").replace(/\/+$/, "")}/models` } };
32941
- if (preset === "azure") {
32942
- return {
32943
- type: "openai-compatible",
32944
- auth: { type: "api-key", header: options2.authHeader ?? "api-key" },
32945
- ...auth,
32946
- modelProfiles: [
32947
- {
32948
- name: "azure-example",
32949
- displayName: "Azure example",
32950
- model: "azure-deployment-name",
32951
- baseURL: options2.baseURL ?? "https://example.openai.azure.com/openai/v1",
32952
- ...options2.apiKey ? {} : { apiKey: "" },
32953
- apiKeyEnv: options2.apiKeyEnv ?? "AZURE_OPENAI_API_KEY"
32954
- }
32955
- ]
32956
- };
32957
- }
32958
- if (preset === "lmstudio") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:1234/v1", apiKey: options2.apiKey ?? "lm-studio", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:1234/v1").replace(/\/+$/, "")}/models` } };
32959
- if (preset === "ollama-local" || preset === "ollama") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:11434/v1", apiKey: options2.apiKey ?? "ollama", modelProfiles: [], quirks: { omitTemperature: true }, catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:11434/v1").replace(/\/+$/, "")}/models` } };
32960
- if (preset === "ollama-cloud") return { type: "ollama", baseURL: options2.baseURL ?? "https://ollama.com/api", ...apiKeyAuthFields(options2, "OLLAMA_API_KEY"), modelProfiles: [], catalog: { type: "ollama-tags", endpoint: `${(options2.baseURL ?? "https://ollama.com/api").replace(/\/+$/, "")}/tags` } };
32961
- if (preset === "llamacpp") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:8080/v1", apiKey: options2.apiKey ?? "llama.cpp", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:8080/v1").replace(/\/+$/, "")}/models` } };
32962
- if (preset === "vllm") return { type: "openai-compatible", baseURL: options2.baseURL ?? "http://localhost:8000/v1", apiKey: options2.apiKey ?? "vllm", modelProfiles: [], catalog: { type: "openai-compatible-models", endpoint: `${(options2.baseURL ?? "http://localhost:8000/v1").replace(/\/+$/, "")}/models` } };
32963
- 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" } };
32964
- 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" } };
32965
- if ((options2.type ?? preset) === "openai-compatible") {
32966
- const baseURL = options2.baseURL;
32967
- if (!baseURL) throw new Error("openai-compatible provider requires --base-url.");
32968
- return {
32969
- type: "openai-compatible",
32970
- baseURL,
32971
- ...auth,
32972
- ...openAIAuth,
32973
- catalog: { type: "openai-compatible-models", endpoint: `${baseURL.replace(/\/+$/, "")}/models` }
32974
- };
34013
+ async function reloadConfigSettings(message, selectionFlags = {}) {
34014
+ loadedConfig2 = await loadConfig({ cwd, configPath: flags.configPath });
34015
+ config = flags.context ? mergeConfig(loadedConfig2, {
34016
+ context: {
34017
+ main: flags.context
34018
+ }
34019
+ }) : loadedConfig2;
34020
+ const currentSelection = store2.currentSelection();
34021
+ const selection = createInteractiveModelSelection(
34022
+ config,
34023
+ selectionFlags.provider || selectionFlags.model ? selectionFlags : { provider: currentSelection.providerName, model: currentSelection.model }
34024
+ );
34025
+ store2.configureSettings(selection, await providerOptionsWithCatalog(config, { timeoutMs: 1500 }), {
34026
+ agent: store2.currentAgentName(),
34027
+ cwd,
34028
+ configPath: configMutationPath(flags),
34029
+ configDefaultProvider: config.defaultProvider,
34030
+ configMessage: message,
34031
+ agentOptions,
34032
+ contextEfficiency: {
34033
+ maxContextTokens: config.context.main.maxContextTokens,
34034
+ compactAtRatio: config.context.main.compactAtRatio ?? config.context.main.compressAtRatio ?? 0.5,
34035
+ compactAtTokens: config.context.main.maxContextTokens ? Math.floor(config.context.main.maxContextTokens * (config.context.main.compactAtRatio ?? config.context.main.compressAtRatio ?? 0.5)) : void 0
34036
+ }
34037
+ });
34038
+ store2.openConfigManager(message);
32975
34039
  }
32976
- throw new Error(`Unknown provider preset: ${preset}`);
32977
34040
  }
32978
- function apiKeyAuthFields(options2, defaultApiKeyEnv) {
32979
- const apiKeyEnv = options2.apiKeyEnv ?? defaultApiKeyEnv;
32980
- return {
32981
- ...options2.apiKey !== void 0 || apiKeyEnv ? { apiKey: options2.apiKey ?? "" } : {},
32982
- ...apiKeyEnv ? { apiKeyEnv } : {}
32983
- };
32984
- }
32985
- async function writeJsonAtomic(filePath, content) {
32986
- await mkdir14(path33.dirname(filePath), { recursive: true, mode: 448 });
32987
- const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
32988
- await writeFile13(temp, content, { mode: 384 });
32989
- await rename6(temp, filePath);
32990
- await chmod3(filePath, 384).catch(() => void 0);
32991
- }
32992
- function detectDefaultProvider() {
32993
- if (process.env.OPENAI_API_KEY) return "openai";
32994
- if (process.env.ANTHROPIC_API_KEY) return "anthropic";
32995
- if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return "gemini";
32996
- if (process.env.GROQ_API_KEY) return "groq";
32997
- return "openai";
34041
+ async function ensureTuiConfigExists(flags) {
34042
+ const target = configMutationPath(flags);
34043
+ if (!flags.configPath) {
34044
+ const jsondPath = target.replace(/\.json$/i, ".jsond");
34045
+ if (existsSync4(target) || existsSync4(jsondPath)) return { path: target, created: false };
34046
+ }
34047
+ const result = await createUserConfig({ path: target });
34048
+ return { path: result.path, created: result.created };
32998
34049
  }
32999
- function objectValue(value) {
33000
- return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
34050
+ function configMutationPath(flags) {
34051
+ return flags.configPath ? path34.resolve(flags.configPath) : defaultUserConfigPath();
33001
34052
  }
33002
- function normalizeOptionalName(value) {
33003
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
34053
+ function isGoalStateClearingAction(action) {
34054
+ return action === "status" || action === "pause" || action === "resume";
33004
34055
  }
33005
- async function exists(filePath) {
33006
- try {
33007
- await stat8(filePath);
33008
- return true;
33009
- } catch {
33010
- return false;
33011
- }
34056
+ async function initializeConversationState(cwd, flags) {
34057
+ const loaded = await loadConversationRecords(flags.conversationStorageDir);
34058
+ const records = new Map(loaded.records.map((record) => [record.id, record]));
34059
+ const shouldResumeSelected = !flags.prompt.trim();
34060
+ const orderedRecords = sortConversationRecords([...records.values()], cwd);
34061
+ const preferred = (flags.sessionId ? findConversation(orderedRecords, flags.sessionId) : void 0) ?? (shouldResumeSelected && loaded.selectedSessionId ? findConversation(orderedRecords, loaded.selectedSessionId) : void 0) ?? (shouldResumeSelected ? orderedRecords[0] : void 0);
34062
+ const current = preferred ?? (flags.prompt.trim() || flags.sessionId || flags.initialHistory?.length ? createConversationRecord({
34063
+ id: flags.sessionId ?? createRootSessionId(),
34064
+ cwd,
34065
+ history: flags.initialHistory
34066
+ }) : void 0);
34067
+ if (current && flags.initialHistory?.length) current.modelHistory = cleanModelHistory(flags.initialHistory);
34068
+ if (current) records.set(current.id, current);
34069
+ return { records, current };
33012
34070
  }
33013
- function expandHome2(value) {
33014
- return resolveExpandedPath(value);
34071
+ function sessionUsage() {
34072
+ return [
34073
+ "Usage:",
34074
+ " /session list",
34075
+ " /session new [title]",
34076
+ " /session switch <number|id|title>",
34077
+ " /session rename <title>",
34078
+ " /session delete <number|id|title>",
34079
+ " /session clear",
34080
+ "",
34081
+ "Aliases: /sessions, /conversation, /conversations, /conv"
34082
+ ].join("\n");
33015
34083
  }
33016
34084
 
33017
34085
  // src/config-watcher.ts
34086
+ import { EventEmitter } from "node:events";
34087
+ import fs11 from "node:fs";
34088
+ import path35 from "node:path";
33018
34089
  var ConfigWatcher = class extends EventEmitter {
33019
34090
  #filePath;
33020
34091
  #debounceMs;
@@ -33036,9 +34107,9 @@ var ConfigWatcher = class extends EventEmitter {
33036
34107
  this.#started = true;
33037
34108
  this.#lastSeen = this.#snapshot();
33038
34109
  try {
33039
- fs11.mkdirSync(path34.dirname(this.#filePath), { recursive: true, mode: 448 });
33040
- this.#watcher = fs11.watch(path34.dirname(this.#filePath), { persistent: false }, (_eventType, filename) => {
33041
- if (filename && filename !== path34.basename(this.#filePath)) return;
34110
+ fs11.mkdirSync(path35.dirname(this.#filePath), { recursive: true, mode: 448 });
34111
+ this.#watcher = fs11.watch(path35.dirname(this.#filePath), { persistent: false }, (_eventType, filename) => {
34112
+ if (filename && filename !== path35.basename(this.#filePath)) return;
33042
34113
  this.#schedule();
33043
34114
  });
33044
34115
  this.#watcher.on("error", () => void 0);
@@ -33103,6 +34174,20 @@ if (process.argv.includes("--config-template")) {
33103
34174
  `);
33104
34175
  process.exit(0);
33105
34176
  }
34177
+ if (process.argv.includes("--config-read")) {
34178
+ await readConfigMeta(jsonArg("--config-read")).then(
34179
+ (result) => {
34180
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}
34181
+ `);
34182
+ process.exit(0);
34183
+ },
34184
+ (error) => {
34185
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
34186
+ `);
34187
+ process.exit(1);
34188
+ }
34189
+ );
34190
+ }
33106
34191
  if (process.argv.includes("--config-init")) {
33107
34192
  const force = process.argv.includes("--force");
33108
34193
  await createUserConfig({ force }).then(
@@ -33160,6 +34245,34 @@ if (process.argv.includes("--config-set-defaults")) {
33160
34245
  }
33161
34246
  );
33162
34247
  }
34248
+ if (process.argv.includes("--config-set-default-model")) {
34249
+ await setDefaultModelProfile(jsonArg("--config-set-default-model")).then(
34250
+ (result) => {
34251
+ process.stdout.write(`${JSON.stringify({ ok: true, path: result.path })}
34252
+ `);
34253
+ process.exit(0);
34254
+ },
34255
+ (error) => {
34256
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
34257
+ `);
34258
+ process.exit(1);
34259
+ }
34260
+ );
34261
+ }
34262
+ if (process.argv.includes("--config-update-provider")) {
34263
+ await updateProviderSettings(jsonArg("--config-update-provider")).then(
34264
+ (result) => {
34265
+ process.stdout.write(`${JSON.stringify({ ok: true, path: result.path })}
34266
+ `);
34267
+ process.exit(0);
34268
+ },
34269
+ (error) => {
34270
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
34271
+ `);
34272
+ process.exit(1);
34273
+ }
34274
+ );
34275
+ }
33163
34276
  if (process.argv.includes("--provider-auth-check")) {
33164
34277
  await checkProviderAuth(jsonArg("--provider-auth-check")).then(
33165
34278
  (result) => {
@@ -33306,14 +34419,14 @@ async function undoDiffAction(actionId) {
33306
34419
  if (!action) throw new Error("No diff action is available to undo.");
33307
34420
  if (!canUndoDiffAction(diffState, action.id)) throw new Error("Only the latest change for a file can be undone.");
33308
34421
  if (!hasBeforeSnapshot(action)) throw new Error("This diff was not recorded with enough content to undo safely.");
33309
- const target = path35.resolve(options.cwd, action.path);
33310
- const relative = path35.relative(options.cwd, target);
33311
- if (relative.startsWith("..") || path35.isAbsolute(relative)) throw new Error("Refusing to undo a file outside the workspace.");
34422
+ const target = path36.resolve(options.cwd, action.path);
34423
+ const relative = path36.relative(options.cwd, target);
34424
+ if (relative.startsWith("..") || path36.isAbsolute(relative)) throw new Error("Refusing to undo a file outside the workspace.");
33312
34425
  if (!action.beforeExists) {
33313
- await rm4(target, { force: true });
34426
+ await rm5(target, { force: true });
33314
34427
  } else {
33315
- await mkdir15(path35.dirname(target), { recursive: true });
33316
- await writeFile14(target, action.beforeContent ?? "", "utf8");
34428
+ await mkdir16(path36.dirname(target), { recursive: true });
34429
+ await writeFile15(target, action.beforeContent ?? "", "utf8");
33317
34430
  }
33318
34431
  markDiffActionUndone(action.id);
33319
34432
  send({ type: "diffUndoCompleted", actionId: action.id, path: action.path });
@@ -33734,30 +34847,43 @@ function setApiKey(providerName, apiKey, opts = {}) {
33734
34847
  }
33735
34848
  async function sendConfigMeta() {
33736
34849
  if (!loadedConfig) return;
34850
+ send({ type: "configMeta", config: await buildConfigMeta(loadedConfig) });
34851
+ }
34852
+ async function readConfigMeta(options2) {
34853
+ const cwd = typeof options2.cwd === "string" && options2.cwd.trim() ? options2.cwd : process.cwd();
34854
+ const configPath = typeof options2.configPath === "string" && options2.configPath.trim() ? options2.configPath : defaultUserConfigPath();
34855
+ let created = false;
34856
+ if (options2.create && !existsSync5(configPath)) {
34857
+ const result = await createUserConfig({ path: configPath });
34858
+ created = result.created;
34859
+ }
34860
+ const config = await loadConfig({ cwd, configPath: options2.configPath });
34861
+ return { path: configPath, created, config: await buildConfigMeta(config) };
34862
+ }
34863
+ async function buildConfigMeta(config) {
33737
34864
  const registry = new AgentRegistry();
33738
- for (const agentDefinition of Object.values(loadedConfig.agents ?? {})) registry.register(agentDefinition, { scope: "user", trusted: true });
33739
- const providerEntries = sortProviderNames(Object.keys(loadedConfig.providers)).map((name) => [name, loadedConfig.providers[name]]).filter(([, provider]) => provider && !provider.hidden);
34865
+ for (const agentDefinition of Object.values(config.agents ?? {})) registry.register(agentDefinition, { scope: "user", trusted: true });
34866
+ const providerEntries = sortProviderNames(Object.keys(config.providers)).map((name) => [name, config.providers[name]]).filter(([, provider]) => provider && !provider.hidden);
33740
34867
  const providerOrder = new Map(providerEntries.map(([name], index) => [name, index]));
33741
34868
  const providers = (await Promise.all(providerEntries.map(([name, provider]) => providerMeta(name, provider)))).sort((left, right) => {
33742
34869
  if (left.available !== right.available) return left.available ? -1 : 1;
33743
34870
  return (providerOrder.get(left.name) ?? 0) - (providerOrder.get(right.name) ?? 0);
33744
34871
  });
33745
- send({
33746
- type: "configMeta",
33747
- config: {
33748
- defaultProvider: loadedConfig.defaultProvider,
33749
- defaultAgent: loadedConfig.defaultAgent,
33750
- context: {
33751
- main: loadedConfig.context?.main
33752
- },
33753
- providerOrder: providers.map((provider) => provider.name),
33754
- providers,
33755
- agents: registry.list().filter((agent) => !agent.hidden && isPrimaryAgent(agent)).map((agent) => ({ name: agent.name, description: agent.description }))
33756
- }
33757
- });
34872
+ return {
34873
+ defaultProvider: config.defaultProvider,
34874
+ defaultAgent: config.defaultAgent,
34875
+ context: {
34876
+ main: config.context?.main
34877
+ },
34878
+ providerOrder: providers.map((provider) => provider.name),
34879
+ providers,
34880
+ agents: registry.list().filter((agent) => !agent.hidden && isPrimaryAgent(agent)).map((agent) => ({ name: agent.name, description: agent.description }))
34881
+ };
33758
34882
  }
33759
34883
  async function providerMeta(name, provider) {
33760
34884
  const apiKeyEnv = typeof provider.apiKeyEnv === "string" ? provider.apiKeyEnv : void 0;
34885
+ const configuredProfiles = configuredModelProfiles(provider);
34886
+ const auth = sanitizeProviderAuth2(provider.auth);
33761
34887
  const catalog = await listProviderModelCatalog(name, provider).catch((error) => ({ status: "unavailable", providerName: name, models: [], source: "fallback", message: errorMessage4(error) }));
33762
34888
  const available = providerCatalogAvailable2(catalog);
33763
34889
  const modelProfiles = catalog.models.length ? catalog.models.map((model) => ({
@@ -33790,7 +34916,16 @@ async function providerMeta(name, provider) {
33790
34916
  modelLabel: available || !defaultModel ? defaultModel : unavailableLabel2(defaultModel),
33791
34917
  models,
33792
34918
  modelProfiles: labeledProfiles,
34919
+ configuredModelProfiles: configuredProfiles,
34920
+ configured: {
34921
+ baseURL: provider.baseURL,
34922
+ apiKeyEnv,
34923
+ apiKeyEnvAliases: asStringArray(provider.apiKeyEnvAliases),
34924
+ auth,
34925
+ hasInlineApiKey: typeof provider.apiKey === "string" && provider.apiKey.length > 0
34926
+ },
33793
34927
  baseURL: provider.baseURL,
34928
+ auth,
33794
34929
  apiKeyEnv,
33795
34930
  apiKeyEnvAliases: asStringArray(provider.apiKeyEnvAliases),
33796
34931
  catalog: { status: catalog.status, source: catalog.source, message: catalog.message },
@@ -33799,6 +34934,26 @@ async function providerMeta(name, provider) {
33799
34934
  requiresApiKey: provider.type === "openai-compatible" || provider.type === "anthropic" || provider.type === "ollama"
33800
34935
  };
33801
34936
  }
34937
+ function configuredModelProfiles(provider) {
34938
+ if (!Array.isArray(provider.modelProfiles)) return [];
34939
+ return provider.modelProfiles.filter((profile) => profile && typeof profile === "object" && typeof profile.model === "string" && profile.model.trim()).map((profile, index) => {
34940
+ const name = typeof profile.name === "string" && profile.name.trim() ? profile.name.trim() : typeof profile.displayName === "string" && profile.displayName.trim() ? profile.displayName.trim() : profile.model.trim();
34941
+ const displayName = typeof profile.displayName === "string" && profile.displayName.trim() ? profile.displayName.trim() : name;
34942
+ return {
34943
+ name,
34944
+ displayName,
34945
+ model: profile.model.trim(),
34946
+ description: typeof profile.description === "string" ? profile.description : void 0,
34947
+ baseURL: typeof profile.baseURL === "string" ? profile.baseURL : void 0,
34948
+ apiKeyEnv: typeof profile.apiKeyEnv === "string" ? profile.apiKeyEnv : void 0,
34949
+ apiKeyEnvAliases: asStringArray(profile.apiKeyEnvAliases),
34950
+ auth: sanitizeProviderAuth2(profile.auth),
34951
+ hasInlineApiKey: typeof profile.apiKey === "string" && profile.apiKey.length > 0,
34952
+ source: "config",
34953
+ isDefault: index === 0
34954
+ };
34955
+ });
34956
+ }
33802
34957
  function providerCatalogAvailable2(catalog) {
33803
34958
  return catalog?.status === "ready" && Array.isArray(catalog.models) && catalog.models.length > 0;
33804
34959
  }
@@ -33808,6 +34963,12 @@ function unavailableLabel2(value) {
33808
34963
  function asStringArray(value) {
33809
34964
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
33810
34965
  }
34966
+ function sanitizeProviderAuth2(auth) {
34967
+ if (!auth || typeof auth !== "object" || Array.isArray(auth)) return void 0;
34968
+ const type = typeof auth.type === "string" ? auth.type : void 0;
34969
+ const header = typeof auth.header === "string" ? auth.header : void 0;
34970
+ return type || header ? { type, header } : void 0;
34971
+ }
33811
34972
  function contextOptions(value) {
33812
34973
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
33813
34974
  const context = {};