palabre 0.8.0 → 0.9.0

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, setOllamaBaseUrl, 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";
@@ -10,17 +10,20 @@ import { createTranslator, DEFAULT_LANGUAGE, parseLanguage, resolveLanguage } fr
10
10
  import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault, validateTurns } from "./limits.js";
11
11
  import { formatAgentPrompt } from "./prompt.js";
12
12
  import { runNewWizard } from "./new.js";
13
- import { listPresetNames, listPresetsWithAvailability, resolvePreset } from "./presets.js";
13
+ import { listAgentsWithAvailability, 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, renderTuiUpdate } 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";
22
- import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
23
- import { getPackageVersion } from "./version.js";
23
+ import { askAgentSeedsForMode, clearTuiRunOverrides } from "./tuiState.js";
24
+ import { detectedAgentNames, detectionForCommand, isRetiredAgentName } from "./agentRegistry.js";
25
+ import { configuredOllamaTargets, DEFAULT_OLLAMA_BASE_URL, normalizeOllamaBaseUrl, OllamaUrlError, resolveOllamaBaseUrl } from "./ollamaUrl.js";
26
+ import { compareSemver, getLatestPackageVersion, getPackageVersion } from "./version.js";
24
27
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
25
28
  async function main() {
26
29
  const rawArgs = process.argv.slice(2);
@@ -53,6 +56,10 @@ async function main() {
53
56
  await runPresetsCommand(parsed.flags);
54
57
  return;
55
58
  }
59
+ if (parsed.command === "history" || parsed.command === "historique") {
60
+ await runHistoryCommand(parsed.flags);
61
+ return;
62
+ }
56
63
  if (parsed.command === "context") {
57
64
  await runContextCommand(parsed.flags, parsed.positionals);
58
65
  return;
@@ -82,7 +89,9 @@ async function main() {
82
89
  console.log(startupMessages.init.configExists(initConfigPath));
83
90
  return;
84
91
  }
85
- const discovery = await discoverLocalTools();
92
+ const discovery = await discoverLocalTools({
93
+ ollamaUrl: optionalString(parsed.flags["ollama-url"])
94
+ });
86
95
  const config = createConfigFromDiscovery(discovery);
87
96
  config.language = resolveLanguage({
88
97
  explicitLanguage: optionalString(parsed.flags.language),
@@ -95,200 +104,254 @@ async function main() {
95
104
  return;
96
105
  }
97
106
  const configPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
107
+ let config;
108
+ let tuiNotice;
98
109
  if (!(await configExists(configPath))) {
99
- const config = createConfigFromDiscovery(await discoverLocalTools());
110
+ config = createConfigFromDiscovery(await discoverLocalTools({
111
+ ollamaUrl: optionalString(parsed.flags["ollama-url"])
112
+ }));
100
113
  config.language = resolveLanguage({
101
114
  explicitLanguage: optionalString(parsed.flags.language),
102
115
  configLanguage: config.language
103
116
  });
104
117
  const messages = createTranslator(config.language);
105
118
  await writeExampleConfig(configPath, config);
106
- console.log(messages.init.editConfigThenRerun(configPath));
107
- return;
119
+ if (!shouldOpenTuiHome(parsed)) {
120
+ console.log(messages.init.editConfigThenRerun(configPath));
121
+ return;
122
+ }
123
+ tuiNotice = messages.init.configCreated(configPath);
124
+ }
125
+ else {
126
+ config = await loadConfig(configPath);
108
127
  }
109
- const config = await loadConfig(configPath);
110
128
  let language = resolveLanguage({
111
129
  explicitLanguage: optionalString(parsed.flags.language),
112
130
  configLanguage: config.language
113
131
  });
114
132
  let messages = createTranslator(language);
115
133
  assertRunnableConfig(config, messages, configPath);
134
+ let stayInTuiAfterSession = false;
135
+ let hasCompletedTuiSession = false;
136
+ let resetTuiRunOverridesOnNextTopic = false;
137
+ let tuiMode = config.defaults?.mode ?? "debate";
138
+ let tuiVersion = "";
139
+ let tuiLatestVersion;
140
+ const handleTuiHomeInput = async (tuiInput) => {
141
+ if (!tuiInput) {
142
+ return "quit";
143
+ }
144
+ if (tuiInput.kind === "help") {
145
+ renderTuiHelp(messages);
146
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages);
147
+ return handleTuiHomeInput(nextInput);
148
+ }
149
+ if (tuiInput.kind === "history") {
150
+ renderTuiHistory(await listHistoryEntries(resolveOutputDir(config.outputDir)), messages);
151
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages);
152
+ return handleTuiHomeInput(nextInput);
153
+ }
154
+ if (tuiInput.kind === "update") {
155
+ const info = await getUpdateInfo(tuiVersion);
156
+ renderTuiUpdate(formatUpdateInstructions(info, messages), messages);
157
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages);
158
+ return handleTuiHomeInput(nextInput);
159
+ }
160
+ if (tuiInput.kind === "home") {
161
+ return "continue";
162
+ }
163
+ if (tuiInput.kind === "roles") {
164
+ const result = await runTuiRolesWizard(configPath, config, messages, tuiMode, tuiInput.roles);
165
+ if (result.quit)
166
+ return "quit";
167
+ tuiNotice = result.notice;
168
+ return "continue";
169
+ }
170
+ if (tuiInput.kind === "agents") {
171
+ const result = await runTuiAgentsWizard(configPath, config, messages, tuiMode, tuiInput.agents);
172
+ if (result.quit)
173
+ return "quit";
174
+ tuiNotice = result.notice;
175
+ resetTuiRunOverridesOnNextTopic ||= Boolean(result.changedRunDefaults);
176
+ return "continue";
177
+ }
178
+ if (tuiInput.kind === "mode") {
179
+ tuiMode = tuiInput.mode;
180
+ return "continue";
181
+ }
182
+ if (tuiInput.kind === "config") {
183
+ const result = await runTuiConfigLoop(configPath, config, messages, tuiMode);
184
+ if (result.quit)
185
+ return "quit";
186
+ tuiMode = result.mode;
187
+ resetTuiRunOverridesOnNextTopic ||= result.changedRunDefaults;
188
+ language = resolveLanguage({ explicitLanguage: optionalString(parsed.flags.language), configLanguage: config.language });
189
+ messages = createTranslator(language);
190
+ return "continue";
191
+ }
192
+ if (tuiInput.kind === "new") {
193
+ parsed.command = "new";
194
+ parsed.commandExplicit = true;
195
+ delete parsed.flags.topic;
196
+ return "run";
197
+ }
198
+ if (tuiInput.kind === "retry") {
199
+ if (!optionalString(parsed.flags.topic)) {
200
+ tuiNotice = messages.tui.retryUnavailable;
201
+ return "continue";
202
+ }
203
+ return "retry";
204
+ }
205
+ parsed.command = "";
206
+ parsed.commandExplicit = false;
207
+ if (hasCompletedTuiSession || resetTuiRunOverridesOnNextTopic) {
208
+ clearTuiRunOverrides(parsed.flags);
209
+ resetTuiRunOverridesOnNextTopic = false;
210
+ }
211
+ parsed.flags.topic = tuiInput.topic;
212
+ return "run";
213
+ };
116
214
  if (shouldOpenTuiHome(parsed)) {
117
- let tuiMode = config.defaults?.mode ?? "debate";
118
- const tuiVersion = await getPackageVersion();
119
- let tuiNotice;
215
+ const [syncResult, currentVersion, latestVersion] = await Promise.all([
216
+ syncInteractiveDetectedAgents(configPath, config),
217
+ getPackageVersion(),
218
+ getLatestPackageVersion()
219
+ ]);
220
+ if (!tuiNotice && syncResult.addedAgents.length > 0) {
221
+ tuiNotice = messages.config.syncAdded(configPath, syncResult.addedAgents.join(", "));
222
+ }
223
+ stayInTuiAfterSession = true;
224
+ tuiVersion = currentVersion;
225
+ tuiLatestVersion = latestVersion && compareSemver(currentVersion, latestVersion) < 0
226
+ ? latestVersion
227
+ : undefined;
120
228
  for (;;) {
121
- renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion });
229
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion, latestVersion: tuiLatestVersion });
122
230
  const tuiInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
123
231
  tuiNotice = undefined;
124
- if (!tuiInput) {
232
+ const action = await handleTuiHomeInput(tuiInput);
233
+ if (action === "quit")
125
234
  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;
235
+ if (action === "continue")
187
236
  continue;
237
+ parsed.flags.mode = tuiMode;
238
+ parsed.flags.renderer = "tui";
239
+ break;
240
+ }
241
+ }
242
+ for (;;) {
243
+ if (parsed.command === "new") {
244
+ const selection = await runNewWizard(config, messages);
245
+ if (!selection) {
246
+ console.log(messages.new.cancelled);
247
+ return;
188
248
  }
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);
249
+ parsed.flags["agent-a"] = selection.agentA;
250
+ parsed.flags["agent-b"] = selection.agentB;
251
+ parsed.flags.topic = selection.topic;
252
+ if (selection.modelA)
253
+ parsed.flags["model-a"] = selection.modelA;
254
+ if (selection.modelB)
255
+ parsed.flags["model-b"] = selection.modelB;
256
+ if (selection.turns)
257
+ parsed.flags.turns = String(selection.turns);
258
+ if (selection.summaryAgent)
259
+ parsed.flags["summary-agent"] = selection.summaryAgent;
260
+ if (selection.summaryModel)
261
+ parsed.flags["summary-model"] = selection.summaryModel;
262
+ if (selection.summaryEnabled === false)
263
+ parsed.flags["no-summary"] = true;
264
+ if (selection.showPrompt)
265
+ parsed.flags["show-prompt"] = true;
266
+ if (selection.plainOutput)
267
+ parsed.flags.plain = true;
268
+ if (selection.files.length > 0)
269
+ parsed.flags.files = selection.files;
270
+ if (selection.context.length > 0)
271
+ parsed.flags.context = selection.context;
272
+ if (selection.mode)
273
+ parsed.flags.mode = selection.mode;
274
+ if (selection.askAgents && selection.askAgents.length > 0)
275
+ parsed.flags.agents = selection.askAgents;
276
+ parsed.command = "";
277
+ parsed.commandExplicit = false;
278
+ }
279
+ const topic = optionalString(parsed.flags.topic) ?? "";
280
+ const context = await loadProjectInputs(getStringListFlag(parsed.flags.files), getStringListFlag(parsed.flags.context), process.cwd(), messages);
281
+ const presetName = optionalString(parsed.flags.preset);
282
+ const preset = presetName ? resolvePreset(presetName, messages) : undefined;
283
+ if (!topic) {
284
+ throw new Error(messages.common.topicRequired);
285
+ }
286
+ const mode = parseModeFlag(optionalString(parsed.flags.mode) ?? config.defaults?.mode, messages);
287
+ const explicitAskAgents = getStringListFlag(parsed.flags.agents);
288
+ const askAgentSeeds = askAgentSeedsForMode(mode, explicitAskAgents, config.defaults?.askAgents);
289
+ const agentA = resolveAgentName("agent A", parsed.flags["agent-a"], preset?.agentA, askAgentSeeds[0] ?? config.defaults?.agentA, messages);
290
+ const agentB = resolveAgentName("agent B", parsed.flags["agent-b"], preset?.agentB, askAgentSeeds[1] ?? askAgentSeeds[0] ?? config.defaults?.agentB, messages);
291
+ const askAgents = mode === "ask" ? resolveAskAgents(explicitAskAgents, config.defaults?.askAgents, [agentA, agentB], messages) : undefined;
292
+ const options = {
293
+ mode,
294
+ language,
295
+ topic,
296
+ agentA,
297
+ agentB,
298
+ askAgents,
299
+ turns: parseTurnsFlag(parsed.flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
300
+ session: createSessionContext(),
301
+ files: context.files,
302
+ modelA: optionalString(parsed.flags["model-a"]),
303
+ modelB: optionalString(parsed.flags["model-b"]),
304
+ ollamaUrl: optionalString(parsed.flags["ollama-url"])
305
+ ? normalizeOllamaBaseUrl(optionalString(parsed.flags["ollama-url"]))
306
+ : undefined,
307
+ pullModels: Boolean(parsed.flags["pull-models"]),
308
+ summaryAgent: resolveSummaryAgentOption(parsed.flags["summary-agent"], config.defaults, mode),
309
+ summaryModel: optionalString(parsed.flags["summary-model"]),
310
+ summaryEnabled: !parsed.flags["no-summary"],
311
+ earlyStopOnAgreement: !parsed.flags["no-early-stop"],
312
+ plainOutput: Boolean(parsed.flags.plain || parsed.flags.terminal),
313
+ signal: debateAbortSignal()
314
+ };
315
+ if (parsed.flags["show-prompt"]) {
316
+ printContextWarnings(context.warnings, messages);
317
+ printPromptPreview(config, options, language, messages);
318
+ return;
319
+ }
320
+ process.exitCode = undefined;
321
+ const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, config.defaults?.interface, messages);
322
+ context.warnings.forEach((warning) => renderer.warning(warning));
323
+ const result = options.mode === "ask"
324
+ ? await runAsk(config, options, renderer, messages)
325
+ : await runDebate(config, options, renderer, messages);
326
+ const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages, result.failure);
327
+ renderer.done(outputPath);
328
+ hasCompletedTuiSession = stayInTuiAfterSession;
329
+ if (result.failure) {
330
+ process.exitCode = result.failure.kind === "cancelled" ? 130 : 1;
331
+ }
332
+ if (!stayInTuiAfterSession) {
333
+ return;
334
+ }
335
+ tuiMode = mode;
336
+ for (;;) {
337
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
338
+ tuiNotice = undefined;
339
+ const action = await handleTuiHomeInput(nextInput);
340
+ if (action === "quit")
341
+ return;
342
+ if (action === "continue") {
343
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion, latestVersion: tuiLatestVersion });
196
344
  continue;
197
345
  }
198
- else if (tuiInput.kind === "new") {
199
- parsed.command = "new";
200
- parsed.commandExplicit = true;
201
- }
202
- else {
203
- parsed.flags.topic = tuiInput.topic;
346
+ if (action === "retry") {
347
+ parsed.flags.renderer = "tui";
348
+ break;
204
349
  }
205
350
  parsed.flags.mode = tuiMode;
206
351
  parsed.flags.renderer = "tui";
207
352
  break;
208
353
  }
209
354
  }
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
355
  }
293
356
  function debateAbortSignal() {
294
357
  const controller = new AbortController();
@@ -317,7 +380,21 @@ async function runAgentsCommand(flags) {
317
380
  configLanguage: config.language
318
381
  });
319
382
  const messages = createTranslator(language);
320
- const discovery = await discoverLocalTools();
383
+ const discovery = await discoverLocalToolsForConfig(config, optionalString(flags["ollama-url"]));
384
+ if (flags.json) {
385
+ const fallbackAskAgents = [config.defaults?.agentA, config.defaults?.agentB]
386
+ .filter((name) => typeof name === "string" && !isRetiredAgentName(name));
387
+ process.stdout.write(JSON.stringify({
388
+ v: 1,
389
+ agents: listAgentsWithAvailability(config, discovery, messages),
390
+ defaults: {
391
+ askAgents: config.defaults?.askAgents?.length
392
+ ? config.defaults.askAgents.filter((name) => !isRetiredAgentName(name))
393
+ : fallbackAskAgents
394
+ }
395
+ }) + "\n");
396
+ return;
397
+ }
321
398
  printAgents(configPath, config, discovery, messages);
322
399
  }
323
400
  /**
@@ -353,14 +430,16 @@ async function runConfigCommand(flags) {
353
430
  return;
354
431
  }
355
432
  if (flags["sync-agents"]) {
356
- const discovery = await discoverLocalTools();
357
- const addedAgents = syncDetectedAgents(config, discovery);
358
- if (addedAgents.length === 0) {
433
+ const discovery = await discoverLocalToolsForConfig(config, optionalString(flags["ollama-url"]));
434
+ const result = syncDetectedAgentsDetailed(config, discovery);
435
+ if (!result.changed) {
359
436
  console.log(messages.config.syncNoMissing(configPath));
360
437
  return;
361
438
  }
362
439
  await writeExampleConfig(configPath, config);
363
- console.log(messages.config.syncAdded(configPath, addedAgents.join(", ")));
440
+ console.log(result.addedAgents.length > 0
441
+ ? messages.config.syncAdded(configPath, result.addedAgents.join(", "))
442
+ : messages.config.syncRefreshed(configPath));
364
443
  return;
365
444
  }
366
445
  const defaultAgents = getStringListFlag(flags["set-defaults"]);
@@ -442,15 +521,16 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
442
521
  let mode = initialMode;
443
522
  let notice;
444
523
  let currentMessages = messages;
524
+ let changedRunDefaults = false;
445
525
  for (;;) {
446
526
  renderTuiConfig(config, configPath, mode, currentMessages, { message: notice });
447
527
  notice = undefined;
448
528
  const input = await promptTuiConfigCommand(mode, currentMessages);
449
529
  if (input.kind === "quit") {
450
- return { mode, quit: true };
530
+ return { mode, quit: true, changedRunDefaults };
451
531
  }
452
532
  if (input.kind === "back") {
453
- return { mode, quit: false };
533
+ return { mode, quit: false, changedRunDefaults };
454
534
  }
455
535
  if (input.kind === "unknown") {
456
536
  notice = input.message;
@@ -458,12 +538,16 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
458
538
  }
459
539
  if (input.kind === "mode") {
460
540
  mode = mode === "ask" ? "debate" : "ask";
461
- notice = mode === "ask" ? currentMessages.tui.askConfigMode : currentMessages.tui.debateConfigMode;
541
+ config.defaults = { ...(config.defaults ?? {}), mode };
542
+ await writeExampleConfig(configPath, config);
543
+ changedRunDefaults = true;
544
+ notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
462
545
  continue;
463
546
  }
464
547
  if (input.kind === "default-mode") {
465
548
  config.defaults = { ...(config.defaults ?? {}), mode };
466
549
  await writeExampleConfig(configPath, config);
550
+ changedRunDefaults = true;
467
551
  notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
468
552
  continue;
469
553
  }
@@ -486,7 +570,7 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
486
570
  ? { kind: "agents", agents: input.agents }
487
571
  : await promptTuiAgentsWizard(config, mode, currentMessages);
488
572
  if (agentsInput.kind === "quit") {
489
- return { mode, quit: true };
573
+ return { mode, quit: true, changedRunDefaults };
490
574
  }
491
575
  if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
492
576
  notice = currentMessages.tui.agentsUnchanged;
@@ -496,12 +580,14 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
496
580
  const agents = normalizeTuiAskAgents(config, agentsInput.agents, currentMessages);
497
581
  config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
498
582
  await writeExampleConfig(configPath, config);
583
+ changedRunDefaults = true;
499
584
  notice = currentMessages.tui.askAgentsUpdated(agents.join(", "));
500
585
  }
501
586
  else {
502
587
  const [agentA, agentB] = normalizeTuiDebateAgents(config, agentsInput.agents, currentMessages);
503
588
  config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
504
589
  await writeExampleConfig(configPath, config);
590
+ changedRunDefaults = true;
505
591
  notice = currentMessages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
506
592
  }
507
593
  }
@@ -516,7 +602,7 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
516
602
  ? { kind: "roles", roles: input.roles }
517
603
  : await promptTuiRolesWizard(config, mode, currentMessages);
518
604
  if (rolesInput.kind === "quit") {
519
- return { mode, quit: true };
605
+ return { mode, quit: true, changedRunDefaults };
520
606
  }
521
607
  if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
522
608
  notice = currentMessages.tui.rolesUnchanged;
@@ -539,6 +625,7 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
539
625
  validateTurns(input.turns, "--turns", currentMessages);
540
626
  config.defaults = { ...(config.defaults ?? {}), turns: input.turns };
541
627
  await writeExampleConfig(configPath, config);
628
+ changedRunDefaults = true;
542
629
  notice = currentMessages.tui.turnsUpdated(input.turns);
543
630
  }
544
631
  catch (error) {
@@ -572,13 +659,129 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
572
659
  }
573
660
  config.defaults = nextDefaults;
574
661
  await writeExampleConfig(configPath, config);
662
+ changedRunDefaults = true;
663
+ }
664
+ catch (error) {
665
+ notice = error instanceof Error ? error.message : String(error);
666
+ }
667
+ continue;
668
+ }
669
+ if (input.kind === "ollama-info") {
670
+ try {
671
+ notice = await formatTuiOllamaInfo(config, currentMessages);
672
+ }
673
+ catch (error) {
674
+ notice = error instanceof Error ? error.message : String(error);
675
+ }
676
+ continue;
677
+ }
678
+ if (input.kind === "ollama-url") {
679
+ try {
680
+ notice = await setTuiOllamaUrl(configPath, config, input.url, currentMessages);
681
+ changedRunDefaults = true;
682
+ }
683
+ catch (error) {
684
+ notice = formatRuntimeError(error, currentMessages);
685
+ }
686
+ continue;
687
+ }
688
+ if (input.kind === "ollama-model") {
689
+ try {
690
+ notice = await setTuiOllamaModel(configPath, config, input.model, currentMessages);
691
+ changedRunDefaults = true;
575
692
  }
576
693
  catch (error) {
577
694
  notice = error instanceof Error ? error.message : String(error);
578
695
  }
696
+ continue;
697
+ }
698
+ if (input.kind === "ollama-sync") {
699
+ try {
700
+ notice = await syncTuiOllamaModel(configPath, config, currentMessages);
701
+ changedRunDefaults = true;
702
+ }
703
+ catch (error) {
704
+ notice = error instanceof Error ? error.message : String(error);
705
+ }
706
+ continue;
579
707
  }
580
708
  }
581
709
  }
710
+ async function setTuiOllamaUrl(configPath, config, value, messages) {
711
+ if (!Object.values(config.agents).some((agent) => agent.type === "ollama")) {
712
+ throw new Error(messages.config.ollamaModelNoAgent);
713
+ }
714
+ const normalized = isDefaultOllamaUrl(value)
715
+ ? DEFAULT_OLLAMA_BASE_URL
716
+ : normalizeOllamaBaseUrl(value);
717
+ const effective = resolveOllamaBaseUrl({ configUrl: normalized });
718
+ setOllamaBaseUrl(config, normalized);
719
+ await writeExampleConfig(configPath, config);
720
+ return messages.tui.ollamaUrlUpdated(normalized, effective);
721
+ }
722
+ function isDefaultOllamaUrl(value) {
723
+ return ["default", "defaut", "défaut", "local", "localhost"].includes(value.trim().toLowerCase());
724
+ }
725
+ async function formatTuiOllamaInfo(config, messages) {
726
+ const discovery = await discoverLocalToolsForConfig(config);
727
+ const agent = config.agents["ollama-local"];
728
+ if (agent?.type !== "ollama") {
729
+ throw new Error(messages.config.ollamaModelNoAgent);
730
+ }
731
+ if (!discovery.ollama.available) {
732
+ return messages.tui.ollamaUnavailable(discovery.ollama.baseUrl);
733
+ }
734
+ const installed = discovery.ollama.models.length > 0
735
+ ? discovery.ollama.models.join(", ")
736
+ : messages.config.ollamaModelNoInstalledModels;
737
+ const api = `${discovery.ollama.baseUrl}`;
738
+ return messages.tui.ollamaInfo(agent.model, installed, api);
739
+ }
740
+ async function setTuiOllamaModel(configPath, config, model, messages) {
741
+ const trimmed = model.trim();
742
+ if (!trimmed) {
743
+ throw new Error(messages.tui.ollamaModelUsage);
744
+ }
745
+ const discovery = await discoverLocalToolsForConfig(config);
746
+ const agent = config.agents["ollama-local"];
747
+ if (agent?.type !== "ollama") {
748
+ throw new Error(messages.config.ollamaModelNoAgent);
749
+ }
750
+ if (!discovery.ollama.models.includes(trimmed)) {
751
+ throw new Error(messages.config.ollamaModelUnavailable(trimmed));
752
+ }
753
+ const result = setOllamaModel(config, trimmed);
754
+ await writeExampleConfig(configPath, config);
755
+ return result
756
+ ? messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel)
757
+ : messages.config.ollamaModelNoChange(configPath, agent.model);
758
+ }
759
+ async function syncTuiOllamaModel(configPath, config, messages) {
760
+ const discovery = await discoverLocalToolsForConfig(config);
761
+ const agent = config.agents["ollama-local"];
762
+ if (agent?.type !== "ollama") {
763
+ throw new Error(messages.config.ollamaModelNoAgent);
764
+ }
765
+ if (discovery.ollama.models.length === 0) {
766
+ throw new Error(messages.config.ollamaModelNoInstalledModels);
767
+ }
768
+ const result = syncOllamaModel(config, discovery);
769
+ if (!result) {
770
+ return messages.config.ollamaModelNoChange(configPath, agent.model);
771
+ }
772
+ await writeExampleConfig(configPath, config);
773
+ return messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel);
774
+ }
775
+ async function syncInteractiveDetectedAgents(configPath, config) {
776
+ const discovery = await discoverLocalToolsForConfig(config);
777
+ const result = syncDetectedAgentsDetailed(config, discovery);
778
+ if (result.changed) {
779
+ await writeExampleConfig(configPath, config);
780
+ }
781
+ return {
782
+ addedAgents: result.addedAgents
783
+ };
784
+ }
582
785
  function normalizeTuiDebateAgents(config, agents, messages) {
583
786
  const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
584
787
  if (unique.length !== 2) {
@@ -605,17 +808,17 @@ async function runTuiAgentsWizard(configPath, config, messages, mode, inlineAgen
605
808
  ? { kind: "agents", agents: inlineAgents }
606
809
  : await promptTuiAgentsWizard(config, mode, messages);
607
810
  if (agentsInput.kind === "quit") {
608
- return { quit: true };
811
+ return { quit: true, changedRunDefaults: false };
609
812
  }
610
813
  if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
611
- return { quit: false };
814
+ return { quit: false, changedRunDefaults: false };
612
815
  }
613
816
  const notice = applyTuiAgents(config, mode, agentsInput.agents, messages);
614
817
  await writeExampleConfig(configPath, config);
615
- return { notice, quit: false };
818
+ return { notice, quit: false, changedRunDefaults: true };
616
819
  }
617
820
  catch (error) {
618
- return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false };
821
+ return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false, changedRunDefaults: false };
619
822
  }
620
823
  }
621
824
  async function runTuiRolesWizard(configPath, config, messages, mode, inlineRoles = []) {
@@ -691,7 +894,7 @@ function isAgentRole(value) {
691
894
  }
692
895
  const VALID_AGENT_ROLES = ["implementer", "reviewer", "architect", "scout", "critic", "summarizer"];
693
896
  async function runOllamaModelsCommand(config, json) {
694
- const discovery = await discoverLocalTools();
897
+ const discovery = await discoverLocalToolsForConfig(config);
695
898
  const agent = config.agents["ollama-local"];
696
899
  const currentModel = agent?.type === "ollama" ? agent.model : null;
697
900
  const payload = {
@@ -716,7 +919,7 @@ async function runSetOllamaModelCommand(configPath, config, model, messages) {
716
919
  if (!trimmed) {
717
920
  throw new Error(messages.common.optionRequiresValue("--set-ollama-model"));
718
921
  }
719
- const discovery = await discoverLocalTools();
922
+ const discovery = await discoverLocalToolsForConfig(config);
720
923
  const agent = config.agents["ollama-local"];
721
924
  if (agent?.type !== "ollama") {
722
925
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -731,7 +934,7 @@ async function runSetOllamaModelCommand(configPath, config, model, messages) {
731
934
  : messages.config.ollamaModelNoChange(configPath, agent.model));
732
935
  }
733
936
  async function runSyncOllamaModelCommand(configPath, config, messages) {
734
- const discovery = await discoverLocalTools();
937
+ const discovery = await discoverLocalToolsForConfig(config);
735
938
  const agent = config.agents["ollama-local"];
736
939
  if (agent?.type !== "ollama") {
737
940
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -968,28 +1171,32 @@ function shouldOpenTuiHome(parsed) {
968
1171
  && parsed.flags.plain !== true
969
1172
  && parsed.flags.terminal !== true;
970
1173
  }
1174
+ /** Lance la discovery avec la même adresse Ollama effective que la config et les overrides globaux. */
1175
+ async function discoverLocalToolsForConfig(config, ollamaUrl) {
1176
+ return discoverLocalTools({
1177
+ ollamaUrl,
1178
+ ollamaTargets: configuredOllamaTargets(config)
1179
+ });
1180
+ }
971
1181
  /**
972
- * Exécute la commande `palabre presets`.
973
- *
974
- * Sortie humaine par défaut (liste alignée), ou JSON avec `--json` pour les
975
- * intégrations (extension VS Code, scripts shell). Le schéma JSON est versionné
976
- * via le champ `v` au cas où on enrichirait plus tard (ex : description par
977
- * preset, tags premium/local).
978
- *
979
- * @param flags - Flags parsés depuis la ligne de commande.
1182
+ * Exécute la commande `palabre presets` en sortie humaine ou JSON versionné.
980
1183
  */
981
1184
  async function runPresetsCommand(flags) {
982
- const discovery = await discoverLocalTools();
983
1185
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
1186
+ const ollamaUrl = optionalString(flags["ollama-url"]);
984
1187
  const config = await configExists(configPath)
985
1188
  ? await loadConfig(configPath)
986
- : createConfigFromDiscovery(discovery);
1189
+ : undefined;
1190
+ const discovery = config
1191
+ ? await discoverLocalToolsForConfig(config, ollamaUrl)
1192
+ : await discoverLocalTools({ ollamaUrl });
1193
+ const resolvedConfig = config ?? createConfigFromDiscovery(discovery);
987
1194
  const language = resolveLanguage({
988
1195
  explicitLanguage: optionalString(flags.language),
989
- configLanguage: config.language
1196
+ configLanguage: resolvedConfig.language
990
1197
  });
991
1198
  const messages = createTranslator(language);
992
- const presets = listPresetsWithAvailability(config, discovery, messages);
1199
+ const presets = listPresetsWithAvailability(resolvedConfig, discovery, messages);
993
1200
  if (flags.json) {
994
1201
  process.stdout.write(JSON.stringify({ v: 1, presets }) + "\n");
995
1202
  return;
@@ -1005,6 +1212,32 @@ async function runPresetsCommand(flags) {
1005
1212
  console.log("");
1006
1213
  console.log(messages.presets.total(presets.length));
1007
1214
  }
1215
+ async function runHistoryCommand(flags) {
1216
+ const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
1217
+ const config = await configExists(configPath)
1218
+ ? await loadConfig(configPath)
1219
+ : undefined;
1220
+ const language = resolveLanguage({
1221
+ explicitLanguage: optionalString(flags.language),
1222
+ configLanguage: config?.language
1223
+ });
1224
+ const messages = createTranslator(language);
1225
+ const entries = await listHistoryEntries(resolveOutputDir(config?.outputDir));
1226
+ if (flags.json) {
1227
+ process.stdout.write(JSON.stringify({ v: 1, history: entries }) + "\n");
1228
+ return;
1229
+ }
1230
+ console.log(messages.tui.historyTitle);
1231
+ console.log("");
1232
+ if (entries.length === 0) {
1233
+ console.log(messages.tui.historyEmpty);
1234
+ return;
1235
+ }
1236
+ for (const entry of entries) {
1237
+ console.log(`- ${entry.date || entry.fileName} | ${entry.mode} | ${entry.topic}`);
1238
+ console.log(` ${entry.path}`);
1239
+ }
1240
+ }
1008
1241
  async function runContextCommand(flags, positionals) {
1009
1242
  const language = resolveLanguage({ explicitLanguage: optionalString(flags.language) });
1010
1243
  const messages = createTranslator(language);
@@ -1053,7 +1286,9 @@ function printContextWarnings(warnings, messages) {
1053
1286
  * @param discovery - Résultat de la découverte locale des outils.
1054
1287
  */
1055
1288
  function printAgents(configPath, config, discovery, messages) {
1056
- const entries = Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right));
1289
+ const entries = Object.entries(config.agents)
1290
+ .filter(([name]) => !isRetiredAgentName(name))
1291
+ .sort(([left], [right]) => left.localeCompare(right));
1057
1292
  console.log(messages.agents.config(configPath));
1058
1293
  console.log("");
1059
1294
  console.log(messages.agents.title);
@@ -1137,7 +1372,6 @@ function printInitDiscovery(discovery, config, messages) {
1137
1372
  console.log(messages.init.localDetectionTitle);
1138
1373
  console.log(`- Codex CLI: ${formatCommandDetection(discovery.codex, messages)}`);
1139
1374
  console.log(`- Claude CLI: ${formatCommandDetection(discovery.claude, messages)}`);
1140
- console.log(`- Gemini CLI: ${formatCommandDetection(discovery.gemini, messages)}`);
1141
1375
  console.log(`- Antigravity CLI: ${formatCommandDetection(discovery.antigravity, messages)}`);
1142
1376
  console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode, messages)}`);
1143
1377
  console.log(`- Mistral Vibe CLI: ${formatCommandDetection(discovery.vibe, messages)}`);
@@ -1216,12 +1450,23 @@ async function resolveCommandMessages(flags) {
1216
1450
  }
1217
1451
  return createTranslator(resolveLanguage({ explicitLanguage, configLanguage }));
1218
1452
  }
1453
+ function formatRuntimeError(error, messages) {
1454
+ if (error instanceof AdapterError) {
1455
+ return formatAdapterError(error, messages);
1456
+ }
1457
+ if (error instanceof OllamaUrlError) {
1458
+ if (error.kind === "empty")
1459
+ return messages.common.ollamaUrlEmpty;
1460
+ if (error.kind === "protocol")
1461
+ return messages.common.ollamaUrlProtocol(error.protocol ?? "");
1462
+ return messages.common.ollamaUrlInvalid(error.value);
1463
+ }
1464
+ return error instanceof Error ? error.message : String(error);
1465
+ }
1219
1466
  main().catch((error) => {
1220
1467
  const language = safeStartupLanguage(process.argv.slice(2));
1221
1468
  const messages = createTranslator(language);
1222
- const message = error instanceof AdapterError
1223
- ? formatAdapterError(error, messages)
1224
- : error instanceof Error ? error.message : String(error);
1469
+ const message = formatRuntimeError(error, messages);
1225
1470
  console.error(`${messages.common.errorPrefix}: ${message}`);
1226
1471
  process.exitCode = 1;
1227
1472
  });