palabre 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaModel, syncDetectedAgents, syncOllamaModel, writeExampleConfig } from "./config.js";
2
+ import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeExampleConfig } from "./config.js";
3
3
  import { loadProjectInputs } from "./context.js";
4
4
  import { buildContextScan } from "./contextScan.js";
5
5
  import { discoverLocalTools } from "./discovery.js";
@@ -11,14 +11,16 @@ import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault, validateTurns } from "./
11
11
  import { formatAgentPrompt } from "./prompt.js";
12
12
  import { runNewWizard } from "./new.js";
13
13
  import { listPresetNames, listPresetsWithAvailability, resolvePreset } from "./presets.js";
14
+ import { listHistoryEntries } from "./history.js";
14
15
  import { createConsoleRenderer } from "./renderers/console.js";
15
16
  import { createNdjsonRenderer } from "./renderers/ndjson.js";
16
- import { createTuiRenderer, promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiHomeTopic, promptTuiRolesWizard, renderTuiConfig, renderTuiHelp, renderTuiHome } from "./renderers/tui.js";
17
+ import { createTuiRenderer, promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiHomeTopic, promptTuiRolesWizard, renderTuiConfig, renderTuiHelp, renderTuiHistory, renderTuiHome } from "./renderers/tui.js";
17
18
  import { MAX_ASK_AGENTS, runAsk, runDebate } from "./orchestrator.js";
18
19
  import { writeDebateMarkdown } from "./output.js";
19
20
  import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./update.js";
20
21
  import { createSessionContext } from "./session.js";
21
22
  import { getStringListFlag, parseArgs } from "./args.js";
23
+ import { askAgentSeedsForMode, clearTuiRunOverrides } from "./tuiState.js";
22
24
  import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
23
25
  import { getPackageVersion } from "./version.js";
24
26
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
@@ -53,6 +55,10 @@ async function main() {
53
55
  await runPresetsCommand(parsed.flags);
54
56
  return;
55
57
  }
58
+ if (parsed.command === "history" || parsed.command === "historique") {
59
+ await runHistoryCommand(parsed.flags);
60
+ return;
61
+ }
56
62
  if (parsed.command === "context") {
57
63
  await runContextCommand(parsed.flags, parsed.positionals);
58
64
  return;
@@ -95,200 +101,235 @@ async function main() {
95
101
  return;
96
102
  }
97
103
  const configPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
104
+ let config;
105
+ let tuiNotice;
98
106
  if (!(await configExists(configPath))) {
99
- const config = createConfigFromDiscovery(await discoverLocalTools());
107
+ config = createConfigFromDiscovery(await discoverLocalTools());
100
108
  config.language = resolveLanguage({
101
109
  explicitLanguage: optionalString(parsed.flags.language),
102
110
  configLanguage: config.language
103
111
  });
104
112
  const messages = createTranslator(config.language);
105
113
  await writeExampleConfig(configPath, config);
106
- console.log(messages.init.editConfigThenRerun(configPath));
107
- return;
114
+ if (!shouldOpenTuiHome(parsed)) {
115
+ console.log(messages.init.editConfigThenRerun(configPath));
116
+ return;
117
+ }
118
+ tuiNotice = messages.init.configCreated(configPath);
119
+ }
120
+ else {
121
+ config = await loadConfig(configPath);
108
122
  }
109
- const config = await loadConfig(configPath);
110
123
  let language = resolveLanguage({
111
124
  explicitLanguage: optionalString(parsed.flags.language),
112
125
  configLanguage: config.language
113
126
  });
114
127
  let messages = createTranslator(language);
115
128
  assertRunnableConfig(config, messages, configPath);
129
+ let stayInTuiAfterSession = false;
130
+ let hasCompletedTuiSession = false;
131
+ let resetTuiRunOverridesOnNextTopic = false;
132
+ let tuiMode = config.defaults?.mode ?? "debate";
133
+ let tuiVersion = "";
134
+ const handleTuiHomeInput = async (tuiInput) => {
135
+ if (!tuiInput) {
136
+ return "quit";
137
+ }
138
+ if (tuiInput.kind === "help") {
139
+ renderTuiHelp(messages);
140
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages);
141
+ return handleTuiHomeInput(nextInput);
142
+ }
143
+ if (tuiInput.kind === "history") {
144
+ renderTuiHistory(await listHistoryEntries(resolveOutputDir(config.outputDir)), messages);
145
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages);
146
+ return handleTuiHomeInput(nextInput);
147
+ }
148
+ if (tuiInput.kind === "home") {
149
+ return "continue";
150
+ }
151
+ if (tuiInput.kind === "roles") {
152
+ const result = await runTuiRolesWizard(configPath, config, messages, tuiMode, tuiInput.roles);
153
+ if (result.quit)
154
+ return "quit";
155
+ tuiNotice = result.notice;
156
+ return "continue";
157
+ }
158
+ if (tuiInput.kind === "agents") {
159
+ const result = await runTuiAgentsWizard(configPath, config, messages, tuiMode, tuiInput.agents);
160
+ if (result.quit)
161
+ return "quit";
162
+ tuiNotice = result.notice;
163
+ resetTuiRunOverridesOnNextTopic ||= Boolean(result.changedRunDefaults);
164
+ return "continue";
165
+ }
166
+ if (tuiInput.kind === "mode") {
167
+ tuiMode = tuiInput.mode;
168
+ return "continue";
169
+ }
170
+ if (tuiInput.kind === "config") {
171
+ const result = await runTuiConfigLoop(configPath, config, messages, tuiMode);
172
+ if (result.quit)
173
+ return "quit";
174
+ tuiMode = result.mode;
175
+ resetTuiRunOverridesOnNextTopic ||= result.changedRunDefaults;
176
+ language = resolveLanguage({ explicitLanguage: optionalString(parsed.flags.language), configLanguage: config.language });
177
+ messages = createTranslator(language);
178
+ return "continue";
179
+ }
180
+ if (tuiInput.kind === "new") {
181
+ parsed.command = "new";
182
+ parsed.commandExplicit = true;
183
+ delete parsed.flags.topic;
184
+ return "run";
185
+ }
186
+ if (tuiInput.kind === "retry") {
187
+ if (!optionalString(parsed.flags.topic)) {
188
+ tuiNotice = messages.tui.retryUnavailable;
189
+ return "continue";
190
+ }
191
+ return "retry";
192
+ }
193
+ parsed.command = "";
194
+ parsed.commandExplicit = false;
195
+ if (hasCompletedTuiSession || resetTuiRunOverridesOnNextTopic) {
196
+ clearTuiRunOverrides(parsed.flags);
197
+ resetTuiRunOverridesOnNextTopic = false;
198
+ }
199
+ parsed.flags.topic = tuiInput.topic;
200
+ return "run";
201
+ };
116
202
  if (shouldOpenTuiHome(parsed)) {
117
- let tuiMode = config.defaults?.mode ?? "debate";
118
- const tuiVersion = await getPackageVersion();
119
- let tuiNotice;
203
+ const syncResult = await syncInteractiveDetectedAgents(configPath, config);
204
+ if (!tuiNotice && syncResult.addedAgents.length > 0) {
205
+ tuiNotice = messages.config.syncAdded(configPath, syncResult.addedAgents.join(", "));
206
+ }
207
+ stayInTuiAfterSession = true;
208
+ tuiVersion = await getPackageVersion();
120
209
  for (;;) {
121
210
  renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion });
122
211
  const tuiInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
123
212
  tuiNotice = undefined;
124
- if (!tuiInput) {
213
+ const action = await handleTuiHomeInput(tuiInput);
214
+ if (action === "quit")
125
215
  return;
126
- }
127
- if (tuiInput.kind === "help") {
128
- renderTuiHelp(messages);
129
- const nextInput = await promptTuiHomeTopic(tuiMode, messages);
130
- if (!nextInput) {
131
- return;
132
- }
133
- if (nextInput.kind === "help") {
134
- continue;
135
- }
136
- if (nextInput.kind === "roles") {
137
- const result = await runTuiRolesWizard(configPath, config, messages, tuiMode, nextInput.roles);
138
- if (result.quit)
139
- return;
140
- tuiNotice = result.notice;
141
- continue;
142
- }
143
- if (nextInput.kind === "agents") {
144
- const result = await runTuiAgentsWizard(configPath, config, messages, tuiMode, nextInput.agents);
145
- if (result.quit)
146
- return;
147
- tuiNotice = result.notice;
148
- continue;
149
- }
150
- if (nextInput.kind === "mode") {
151
- tuiMode = nextInput.mode;
152
- continue;
153
- }
154
- if (nextInput.kind === "config") {
155
- const result = await runTuiConfigLoop(configPath, config, messages, tuiMode);
156
- if (result.quit)
157
- return;
158
- tuiMode = result.mode;
159
- language = resolveLanguage({ explicitLanguage: optionalString(parsed.flags.language), configLanguage: config.language });
160
- messages = createTranslator(language);
161
- continue;
162
- }
163
- if (nextInput.kind === "new") {
164
- parsed.command = "new";
165
- parsed.commandExplicit = true;
166
- }
167
- else {
168
- parsed.flags.topic = nextInput.topic;
169
- }
170
- }
171
- else if (tuiInput.kind === "mode") {
172
- tuiMode = tuiInput.mode;
173
- continue;
174
- }
175
- else if (tuiInput.kind === "roles") {
176
- const result = await runTuiRolesWizard(configPath, config, messages, tuiMode, tuiInput.roles);
177
- if (result.quit)
178
- return;
179
- tuiNotice = result.notice;
180
- continue;
181
- }
182
- else if (tuiInput.kind === "agents") {
183
- const result = await runTuiAgentsWizard(configPath, config, messages, tuiMode, tuiInput.agents);
184
- if (result.quit)
185
- return;
186
- tuiNotice = result.notice;
216
+ if (action === "continue")
187
217
  continue;
218
+ parsed.flags.mode = tuiMode;
219
+ parsed.flags.renderer = "tui";
220
+ break;
221
+ }
222
+ }
223
+ for (;;) {
224
+ if (parsed.command === "new") {
225
+ const selection = await runNewWizard(config, messages);
226
+ if (!selection) {
227
+ console.log(messages.new.cancelled);
228
+ return;
188
229
  }
189
- else if (tuiInput.kind === "config") {
190
- const result = await runTuiConfigLoop(configPath, config, messages, tuiMode);
191
- if (result.quit)
192
- return;
193
- tuiMode = result.mode;
194
- language = resolveLanguage({ explicitLanguage: optionalString(parsed.flags.language), configLanguage: config.language });
195
- messages = createTranslator(language);
230
+ parsed.flags["agent-a"] = selection.agentA;
231
+ parsed.flags["agent-b"] = selection.agentB;
232
+ parsed.flags.topic = selection.topic;
233
+ if (selection.modelA)
234
+ parsed.flags["model-a"] = selection.modelA;
235
+ if (selection.modelB)
236
+ parsed.flags["model-b"] = selection.modelB;
237
+ if (selection.turns)
238
+ parsed.flags.turns = String(selection.turns);
239
+ if (selection.summaryAgent)
240
+ parsed.flags["summary-agent"] = selection.summaryAgent;
241
+ if (selection.summaryModel)
242
+ parsed.flags["summary-model"] = selection.summaryModel;
243
+ if (selection.summaryEnabled === false)
244
+ parsed.flags["no-summary"] = true;
245
+ if (selection.showPrompt)
246
+ parsed.flags["show-prompt"] = true;
247
+ if (selection.plainOutput)
248
+ parsed.flags.plain = true;
249
+ if (selection.files.length > 0)
250
+ parsed.flags.files = selection.files;
251
+ if (selection.context.length > 0)
252
+ parsed.flags.context = selection.context;
253
+ if (selection.mode)
254
+ parsed.flags.mode = selection.mode;
255
+ if (selection.askAgents && selection.askAgents.length > 0)
256
+ parsed.flags.agents = selection.askAgents;
257
+ parsed.command = "";
258
+ parsed.commandExplicit = false;
259
+ }
260
+ const topic = optionalString(parsed.flags.topic) ?? "";
261
+ const context = await loadProjectInputs(getStringListFlag(parsed.flags.files), getStringListFlag(parsed.flags.context), process.cwd(), messages);
262
+ const presetName = optionalString(parsed.flags.preset);
263
+ const preset = presetName ? resolvePreset(presetName, messages) : undefined;
264
+ if (!topic) {
265
+ throw new Error(messages.common.topicRequired);
266
+ }
267
+ const mode = parseModeFlag(optionalString(parsed.flags.mode) ?? config.defaults?.mode, messages);
268
+ const explicitAskAgents = getStringListFlag(parsed.flags.agents);
269
+ const askAgentSeeds = askAgentSeedsForMode(mode, explicitAskAgents, config.defaults?.askAgents);
270
+ const agentA = resolveAgentName("agent A", parsed.flags["agent-a"], preset?.agentA, askAgentSeeds[0] ?? config.defaults?.agentA, messages);
271
+ const agentB = resolveAgentName("agent B", parsed.flags["agent-b"], preset?.agentB, askAgentSeeds[1] ?? askAgentSeeds[0] ?? config.defaults?.agentB, messages);
272
+ const askAgents = mode === "ask" ? resolveAskAgents(explicitAskAgents, config.defaults?.askAgents, [agentA, agentB], messages) : undefined;
273
+ const options = {
274
+ mode,
275
+ language,
276
+ topic,
277
+ agentA,
278
+ agentB,
279
+ askAgents,
280
+ turns: parseTurnsFlag(parsed.flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
281
+ session: createSessionContext(),
282
+ files: context.files,
283
+ modelA: optionalString(parsed.flags["model-a"]),
284
+ modelB: optionalString(parsed.flags["model-b"]),
285
+ pullModels: Boolean(parsed.flags["pull-models"]),
286
+ summaryAgent: resolveSummaryAgentOption(parsed.flags["summary-agent"], config.defaults, mode),
287
+ summaryModel: optionalString(parsed.flags["summary-model"]),
288
+ summaryEnabled: !parsed.flags["no-summary"],
289
+ earlyStopOnAgreement: !parsed.flags["no-early-stop"],
290
+ plainOutput: Boolean(parsed.flags.plain || parsed.flags.terminal),
291
+ signal: debateAbortSignal()
292
+ };
293
+ if (parsed.flags["show-prompt"]) {
294
+ printContextWarnings(context.warnings, messages);
295
+ printPromptPreview(config, options, language, messages);
296
+ return;
297
+ }
298
+ process.exitCode = undefined;
299
+ const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, config.defaults?.interface, messages);
300
+ context.warnings.forEach((warning) => renderer.warning(warning));
301
+ const result = options.mode === "ask"
302
+ ? await runAsk(config, options, renderer, messages)
303
+ : await runDebate(config, options, renderer, messages);
304
+ const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages, result.failure);
305
+ renderer.done(outputPath);
306
+ hasCompletedTuiSession = stayInTuiAfterSession;
307
+ if (result.failure) {
308
+ process.exitCode = result.failure.kind === "cancelled" ? 130 : 1;
309
+ }
310
+ if (!stayInTuiAfterSession) {
311
+ return;
312
+ }
313
+ tuiMode = mode;
314
+ for (;;) {
315
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
316
+ tuiNotice = undefined;
317
+ const action = await handleTuiHomeInput(nextInput);
318
+ if (action === "quit")
319
+ return;
320
+ if (action === "continue") {
321
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion });
196
322
  continue;
197
323
  }
198
- else if (tuiInput.kind === "new") {
199
- parsed.command = "new";
200
- parsed.commandExplicit = true;
201
- }
202
- else {
203
- parsed.flags.topic = tuiInput.topic;
324
+ if (action === "retry") {
325
+ parsed.flags.renderer = "tui";
326
+ break;
204
327
  }
205
328
  parsed.flags.mode = tuiMode;
206
329
  parsed.flags.renderer = "tui";
207
330
  break;
208
331
  }
209
332
  }
210
- if (parsed.command === "new") {
211
- const selection = await runNewWizard(config, messages);
212
- if (!selection) {
213
- console.log(messages.new.cancelled);
214
- return;
215
- }
216
- parsed.flags["agent-a"] = selection.agentA;
217
- parsed.flags["agent-b"] = selection.agentB;
218
- parsed.flags.topic = selection.topic;
219
- if (selection.modelA)
220
- parsed.flags["model-a"] = selection.modelA;
221
- if (selection.modelB)
222
- parsed.flags["model-b"] = selection.modelB;
223
- if (selection.turns)
224
- parsed.flags.turns = String(selection.turns);
225
- if (selection.summaryAgent)
226
- parsed.flags["summary-agent"] = selection.summaryAgent;
227
- if (selection.summaryModel)
228
- parsed.flags["summary-model"] = selection.summaryModel;
229
- if (selection.summaryEnabled === false)
230
- parsed.flags["no-summary"] = true;
231
- if (selection.showPrompt)
232
- parsed.flags["show-prompt"] = true;
233
- if (selection.plainOutput)
234
- parsed.flags.plain = true;
235
- if (selection.files.length > 0)
236
- parsed.flags.files = selection.files;
237
- if (selection.context.length > 0)
238
- parsed.flags.context = selection.context;
239
- if (selection.mode)
240
- parsed.flags.mode = selection.mode;
241
- if (selection.askAgents && selection.askAgents.length > 0)
242
- parsed.flags.agents = selection.askAgents;
243
- }
244
- const topic = optionalString(parsed.flags.topic) ?? "";
245
- const context = await loadProjectInputs(getStringListFlag(parsed.flags.files), getStringListFlag(parsed.flags.context), process.cwd(), messages);
246
- const presetName = optionalString(parsed.flags.preset);
247
- const preset = presetName ? resolvePreset(presetName, messages) : undefined;
248
- if (!topic) {
249
- throw new Error(messages.common.topicRequired);
250
- }
251
- const mode = parseModeFlag(optionalString(parsed.flags.mode) ?? config.defaults?.mode, messages);
252
- const explicitAskAgents = getStringListFlag(parsed.flags.agents);
253
- const askAgentSeeds = explicitAskAgents.length > 0 ? explicitAskAgents : config.defaults?.askAgents ?? [];
254
- const agentA = resolveAgentName("agent A", parsed.flags["agent-a"], preset?.agentA, askAgentSeeds[0] ?? config.defaults?.agentA, messages);
255
- const agentB = resolveAgentName("agent B", parsed.flags["agent-b"], preset?.agentB, askAgentSeeds[1] ?? askAgentSeeds[0] ?? config.defaults?.agentB, messages);
256
- const askAgents = mode === "ask" ? resolveAskAgents(explicitAskAgents, config.defaults?.askAgents, [agentA, agentB], messages) : undefined;
257
- const options = {
258
- mode,
259
- language,
260
- topic,
261
- agentA,
262
- agentB,
263
- askAgents,
264
- turns: parseTurnsFlag(parsed.flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
265
- session: createSessionContext(),
266
- files: context.files,
267
- modelA: optionalString(parsed.flags["model-a"]),
268
- modelB: optionalString(parsed.flags["model-b"]),
269
- pullModels: Boolean(parsed.flags["pull-models"]),
270
- summaryAgent: resolveSummaryAgentOption(parsed.flags["summary-agent"], config.defaults, mode),
271
- summaryModel: optionalString(parsed.flags["summary-model"]),
272
- summaryEnabled: !parsed.flags["no-summary"],
273
- earlyStopOnAgreement: !parsed.flags["no-early-stop"],
274
- plainOutput: Boolean(parsed.flags.plain || parsed.flags.terminal),
275
- signal: debateAbortSignal()
276
- };
277
- if (parsed.flags["show-prompt"]) {
278
- printContextWarnings(context.warnings, messages);
279
- printPromptPreview(config, options, language, messages);
280
- return;
281
- }
282
- const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, config.defaults?.interface, messages);
283
- context.warnings.forEach((warning) => renderer.warning(warning));
284
- const result = options.mode === "ask"
285
- ? await runAsk(config, options, renderer, messages)
286
- : await runDebate(config, options, renderer, messages);
287
- const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages, result.failure);
288
- renderer.done(outputPath);
289
- if (result.failure) {
290
- process.exitCode = result.failure.kind === "cancelled" ? 130 : 1;
291
- }
292
333
  }
293
334
  function debateAbortSignal() {
294
335
  const controller = new AbortController();
@@ -354,13 +395,15 @@ async function runConfigCommand(flags) {
354
395
  }
355
396
  if (flags["sync-agents"]) {
356
397
  const discovery = await discoverLocalTools();
357
- const addedAgents = syncDetectedAgents(config, discovery);
358
- if (addedAgents.length === 0) {
398
+ const result = syncDetectedAgentsDetailed(config, discovery);
399
+ if (!result.changed) {
359
400
  console.log(messages.config.syncNoMissing(configPath));
360
401
  return;
361
402
  }
362
403
  await writeExampleConfig(configPath, config);
363
- console.log(messages.config.syncAdded(configPath, addedAgents.join(", ")));
404
+ console.log(result.addedAgents.length > 0
405
+ ? messages.config.syncAdded(configPath, result.addedAgents.join(", "))
406
+ : messages.config.syncRefreshed(configPath));
364
407
  return;
365
408
  }
366
409
  const defaultAgents = getStringListFlag(flags["set-defaults"]);
@@ -442,15 +485,16 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
442
485
  let mode = initialMode;
443
486
  let notice;
444
487
  let currentMessages = messages;
488
+ let changedRunDefaults = false;
445
489
  for (;;) {
446
490
  renderTuiConfig(config, configPath, mode, currentMessages, { message: notice });
447
491
  notice = undefined;
448
492
  const input = await promptTuiConfigCommand(mode, currentMessages);
449
493
  if (input.kind === "quit") {
450
- return { mode, quit: true };
494
+ return { mode, quit: true, changedRunDefaults };
451
495
  }
452
496
  if (input.kind === "back") {
453
- return { mode, quit: false };
497
+ return { mode, quit: false, changedRunDefaults };
454
498
  }
455
499
  if (input.kind === "unknown") {
456
500
  notice = input.message;
@@ -458,12 +502,16 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
458
502
  }
459
503
  if (input.kind === "mode") {
460
504
  mode = mode === "ask" ? "debate" : "ask";
461
- notice = mode === "ask" ? currentMessages.tui.askConfigMode : currentMessages.tui.debateConfigMode;
505
+ config.defaults = { ...(config.defaults ?? {}), mode };
506
+ await writeExampleConfig(configPath, config);
507
+ changedRunDefaults = true;
508
+ notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
462
509
  continue;
463
510
  }
464
511
  if (input.kind === "default-mode") {
465
512
  config.defaults = { ...(config.defaults ?? {}), mode };
466
513
  await writeExampleConfig(configPath, config);
514
+ changedRunDefaults = true;
467
515
  notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
468
516
  continue;
469
517
  }
@@ -486,7 +534,7 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
486
534
  ? { kind: "agents", agents: input.agents }
487
535
  : await promptTuiAgentsWizard(config, mode, currentMessages);
488
536
  if (agentsInput.kind === "quit") {
489
- return { mode, quit: true };
537
+ return { mode, quit: true, changedRunDefaults };
490
538
  }
491
539
  if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
492
540
  notice = currentMessages.tui.agentsUnchanged;
@@ -496,12 +544,14 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
496
544
  const agents = normalizeTuiAskAgents(config, agentsInput.agents, currentMessages);
497
545
  config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
498
546
  await writeExampleConfig(configPath, config);
547
+ changedRunDefaults = true;
499
548
  notice = currentMessages.tui.askAgentsUpdated(agents.join(", "));
500
549
  }
501
550
  else {
502
551
  const [agentA, agentB] = normalizeTuiDebateAgents(config, agentsInput.agents, currentMessages);
503
552
  config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
504
553
  await writeExampleConfig(configPath, config);
554
+ changedRunDefaults = true;
505
555
  notice = currentMessages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
506
556
  }
507
557
  }
@@ -516,7 +566,7 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
516
566
  ? { kind: "roles", roles: input.roles }
517
567
  : await promptTuiRolesWizard(config, mode, currentMessages);
518
568
  if (rolesInput.kind === "quit") {
519
- return { mode, quit: true };
569
+ return { mode, quit: true, changedRunDefaults };
520
570
  }
521
571
  if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
522
572
  notice = currentMessages.tui.rolesUnchanged;
@@ -539,6 +589,7 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
539
589
  validateTurns(input.turns, "--turns", currentMessages);
540
590
  config.defaults = { ...(config.defaults ?? {}), turns: input.turns };
541
591
  await writeExampleConfig(configPath, config);
592
+ changedRunDefaults = true;
542
593
  notice = currentMessages.tui.turnsUpdated(input.turns);
543
594
  }
544
595
  catch (error) {
@@ -572,12 +623,103 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
572
623
  }
573
624
  config.defaults = nextDefaults;
574
625
  await writeExampleConfig(configPath, config);
626
+ changedRunDefaults = true;
575
627
  }
576
628
  catch (error) {
577
629
  notice = error instanceof Error ? error.message : String(error);
578
630
  }
631
+ continue;
579
632
  }
633
+ if (input.kind === "ollama-info") {
634
+ try {
635
+ notice = await formatTuiOllamaInfo(config, currentMessages);
636
+ }
637
+ catch (error) {
638
+ notice = error instanceof Error ? error.message : String(error);
639
+ }
640
+ continue;
641
+ }
642
+ if (input.kind === "ollama-model") {
643
+ try {
644
+ notice = await setTuiOllamaModel(configPath, config, input.model, currentMessages);
645
+ changedRunDefaults = true;
646
+ }
647
+ catch (error) {
648
+ notice = error instanceof Error ? error.message : String(error);
649
+ }
650
+ continue;
651
+ }
652
+ if (input.kind === "ollama-sync") {
653
+ try {
654
+ notice = await syncTuiOllamaModel(configPath, config, currentMessages);
655
+ changedRunDefaults = true;
656
+ }
657
+ catch (error) {
658
+ notice = error instanceof Error ? error.message : String(error);
659
+ }
660
+ continue;
661
+ }
662
+ }
663
+ }
664
+ async function formatTuiOllamaInfo(config, messages) {
665
+ const discovery = await discoverLocalTools();
666
+ const agent = config.agents["ollama-local"];
667
+ if (agent?.type !== "ollama") {
668
+ throw new Error(messages.config.ollamaModelNoAgent);
669
+ }
670
+ if (!discovery.ollama.available) {
671
+ return messages.tui.ollamaUnavailable(discovery.ollama.baseUrl);
672
+ }
673
+ const installed = discovery.ollama.models.length > 0
674
+ ? discovery.ollama.models.join(", ")
675
+ : messages.config.ollamaModelNoInstalledModels;
676
+ const api = `${discovery.ollama.baseUrl}`;
677
+ return messages.tui.ollamaInfo(agent.model, installed, api);
678
+ }
679
+ async function setTuiOllamaModel(configPath, config, model, messages) {
680
+ const trimmed = model.trim();
681
+ if (!trimmed) {
682
+ throw new Error(messages.tui.ollamaModelUsage);
683
+ }
684
+ const discovery = await discoverLocalTools();
685
+ const agent = config.agents["ollama-local"];
686
+ if (agent?.type !== "ollama") {
687
+ throw new Error(messages.config.ollamaModelNoAgent);
688
+ }
689
+ if (!discovery.ollama.models.includes(trimmed)) {
690
+ throw new Error(messages.config.ollamaModelUnavailable(trimmed));
691
+ }
692
+ const result = setOllamaModel(config, trimmed);
693
+ await writeExampleConfig(configPath, config);
694
+ return result
695
+ ? messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel)
696
+ : messages.config.ollamaModelNoChange(configPath, agent.model);
697
+ }
698
+ async function syncTuiOllamaModel(configPath, config, messages) {
699
+ const discovery = await discoverLocalTools();
700
+ const agent = config.agents["ollama-local"];
701
+ if (agent?.type !== "ollama") {
702
+ throw new Error(messages.config.ollamaModelNoAgent);
703
+ }
704
+ if (discovery.ollama.models.length === 0) {
705
+ throw new Error(messages.config.ollamaModelNoInstalledModels);
580
706
  }
707
+ const result = syncOllamaModel(config, discovery);
708
+ if (!result) {
709
+ return messages.config.ollamaModelNoChange(configPath, agent.model);
710
+ }
711
+ await writeExampleConfig(configPath, config);
712
+ return messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel);
713
+ }
714
+ async function syncInteractiveDetectedAgents(configPath, config) {
715
+ const discovery = await discoverLocalTools();
716
+ const result = syncDetectedAgentsDetailed(config, discovery);
717
+ if (result.changed) {
718
+ await writeExampleConfig(configPath, config);
719
+ }
720
+ return {
721
+ addedAgents: result.addedAgents
722
+ };
581
723
  }
582
724
  function normalizeTuiDebateAgents(config, agents, messages) {
583
725
  const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
@@ -605,17 +747,17 @@ async function runTuiAgentsWizard(configPath, config, messages, mode, inlineAgen
605
747
  ? { kind: "agents", agents: inlineAgents }
606
748
  : await promptTuiAgentsWizard(config, mode, messages);
607
749
  if (agentsInput.kind === "quit") {
608
- return { quit: true };
750
+ return { quit: true, changedRunDefaults: false };
609
751
  }
610
752
  if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
611
- return { quit: false };
753
+ return { quit: false, changedRunDefaults: false };
612
754
  }
613
755
  const notice = applyTuiAgents(config, mode, agentsInput.agents, messages);
614
756
  await writeExampleConfig(configPath, config);
615
- return { notice, quit: false };
757
+ return { notice, quit: false, changedRunDefaults: true };
616
758
  }
617
759
  catch (error) {
618
- return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false };
760
+ return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false, changedRunDefaults: false };
619
761
  }
620
762
  }
621
763
  async function runTuiRolesWizard(configPath, config, messages, mode, inlineRoles = []) {
@@ -1005,6 +1147,32 @@ async function runPresetsCommand(flags) {
1005
1147
  console.log("");
1006
1148
  console.log(messages.presets.total(presets.length));
1007
1149
  }
1150
+ async function runHistoryCommand(flags) {
1151
+ const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
1152
+ const config = await configExists(configPath)
1153
+ ? await loadConfig(configPath)
1154
+ : undefined;
1155
+ const language = resolveLanguage({
1156
+ explicitLanguage: optionalString(flags.language),
1157
+ configLanguage: config?.language
1158
+ });
1159
+ const messages = createTranslator(language);
1160
+ const entries = await listHistoryEntries(resolveOutputDir(config?.outputDir));
1161
+ if (flags.json) {
1162
+ process.stdout.write(JSON.stringify({ v: 1, history: entries }) + "\n");
1163
+ return;
1164
+ }
1165
+ console.log(messages.tui.historyTitle);
1166
+ console.log("");
1167
+ if (entries.length === 0) {
1168
+ console.log(messages.tui.historyEmpty);
1169
+ return;
1170
+ }
1171
+ for (const entry of entries) {
1172
+ console.log(`- ${entry.date || entry.fileName} | ${entry.mode} | ${entry.topic}`);
1173
+ console.log(` ${entry.path}`);
1174
+ }
1175
+ }
1008
1176
  async function runContextCommand(flags, positionals) {
1009
1177
  const language = resolveLanguage({ explicitLanguage: optionalString(flags.language) });
1010
1178
  const messages = createTranslator(language);
@@ -1137,7 +1305,6 @@ function printInitDiscovery(discovery, config, messages) {
1137
1305
  console.log(messages.init.localDetectionTitle);
1138
1306
  console.log(`- Codex CLI: ${formatCommandDetection(discovery.codex, messages)}`);
1139
1307
  console.log(`- Claude CLI: ${formatCommandDetection(discovery.claude, messages)}`);
1140
- console.log(`- Gemini CLI: ${formatCommandDetection(discovery.gemini, messages)}`);
1141
1308
  console.log(`- Antigravity CLI: ${formatCommandDetection(discovery.antigravity, messages)}`);
1142
1309
  console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode, messages)}`);
1143
1310
  console.log(`- Mistral Vibe CLI: ${formatCommandDetection(discovery.vibe, messages)}`);