palabre 0.9.0 → 0.10.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.
Files changed (53) hide show
  1. package/README.md +2 -0
  2. package/dist/adapters/cli-pty.js +30 -10
  3. package/dist/adapters/cli-shared.js +73 -0
  4. package/dist/adapters/cli.js +40 -77
  5. package/dist/adapters/index.js +1 -0
  6. package/dist/adapters/ollama.js +32 -32
  7. package/dist/adapters/terminal.js +1 -0
  8. package/dist/args.js +1 -0
  9. package/dist/commands/agents.js +91 -0
  10. package/dist/commands/context.js +31 -0
  11. package/dist/commands/history.js +33 -0
  12. package/dist/commands/init.js +64 -0
  13. package/dist/commands/presets.js +39 -0
  14. package/dist/commands/shared.js +8 -0
  15. package/dist/commands/update.js +26 -0
  16. package/dist/config.js +17 -1
  17. package/dist/configWizard.js +5 -4
  18. package/dist/context.js +1 -0
  19. package/dist/contextScan.js +4 -3
  20. package/dist/discovery.js +9 -1
  21. package/dist/doctor.js +1 -0
  22. package/dist/errors.js +4 -0
  23. package/dist/exec.js +1 -0
  24. package/dist/history.js +10 -0
  25. package/dist/i18n.js +2 -0
  26. package/dist/index.js +174 -879
  27. package/dist/limits.js +5 -0
  28. package/dist/messages/adapter-errors.js +26 -2
  29. package/dist/messages/config.js +6 -0
  30. package/dist/messages/index.js +1 -0
  31. package/dist/messages/renderers.js +10 -2
  32. package/dist/messages/tui.js +8 -2
  33. package/dist/new.js +1 -0
  34. package/dist/ollamaUrl.js +20 -0
  35. package/dist/orchestrator.js +106 -161
  36. package/dist/output.js +2 -10
  37. package/dist/presets.js +1 -0
  38. package/dist/prompt.js +1 -0
  39. package/dist/renderers/console.js +2 -8
  40. package/dist/renderers/ndjson.js +1 -10
  41. package/dist/renderers/tui-prompts.js +339 -0
  42. package/dist/renderers/tui-renderer.js +224 -0
  43. package/dist/renderers/tui-screens.js +352 -0
  44. package/dist/renderers/tui-theme.js +356 -0
  45. package/dist/renderers/tui.js +7 -1095
  46. package/dist/runOptions.js +97 -0
  47. package/dist/session.js +1 -0
  48. package/dist/tuiController.js +445 -0
  49. package/dist/tuiState.js +4 -0
  50. package/dist/types.js +1 -0
  51. package/dist/update.js +1 -0
  52. package/dist/version.js +1 -0
  53. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,29 +1,41 @@
1
1
  #!/usr/bin/env node
2
- import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaBaseUrl, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeExampleConfig } from "./config.js";
2
+ /**
3
+ * @file Point d'entrée CLI de Palabre : parsing des arguments, dispatch vers les
4
+ * commandes leaf (`agents`, `presets`, `history`, `context`, `update`, `init`, `config`),
5
+ * résolution de la config/langue, boucle TUI d'accueil et lancement d'un débat ou d'une
6
+ * requête `ask` via l'orchestrateur.
7
+ */
8
+ import { assertRunnableConfig, configExists, createConfigFromDiscovery, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeConfig } from "./config.js";
3
9
  import { loadProjectInputs } from "./context.js";
4
- import { buildContextScan } from "./contextScan.js";
5
- import { discoverLocalTools } from "./discovery.js";
10
+ import { discoverLocalTools, discoverLocalToolsForConfig } from "./discovery.js";
6
11
  import { runDoctor } from "./doctor.js";
7
12
  import { AdapterError, formatAdapterError } from "./errors.js";
8
13
  import { runConfigWizard } from "./configWizard.js";
9
14
  import { createTranslator, DEFAULT_LANGUAGE, parseLanguage, resolveLanguage } from "./i18n.js";
10
- import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault, validateTurns } from "./limits.js";
15
+ import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault } from "./limits.js";
11
16
  import { formatAgentPrompt } from "./prompt.js";
12
17
  import { runNewWizard } from "./new.js";
13
- import { listAgentsWithAvailability, listPresetNames, listPresetsWithAvailability, resolvePreset } from "./presets.js";
18
+ import { listPresetNames, resolvePreset } from "./presets.js";
14
19
  import { listHistoryEntries } from "./history.js";
15
20
  import { createConsoleRenderer } from "./renderers/console.js";
16
21
  import { createNdjsonRenderer } from "./renderers/ndjson.js";
17
- import { createTuiRenderer, promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiHomeTopic, promptTuiRolesWizard, renderTuiConfig, renderTuiHelp, renderTuiHistory, renderTuiHome, renderTuiUpdate } from "./renderers/tui.js";
22
+ import { createTuiRenderer, promptTuiHomeTopic, renderTuiHelp, renderTuiHistory, renderTuiHome, renderTuiUpdate } from "./renderers/tui.js";
18
23
  import { MAX_ASK_AGENTS, runAsk, runDebate } from "./orchestrator.js";
19
24
  import { writeDebateMarkdown } from "./output.js";
20
- import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./update.js";
21
- import { createSessionContext } from "./session.js";
25
+ import { formatUpdateInstructions, getUpdateInfo } from "./update.js";
22
26
  import { getStringListFlag, parseArgs } from "./args.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";
27
+ import { clearTuiRunOverrides } from "./tuiState.js";
28
+ import { formatOllamaUrlError, OllamaUrlError } from "./ollamaUrl.js";
26
29
  import { compareSemver, getLatestPackageVersion, getPackageVersion } from "./version.js";
30
+ import { runAgentsCommand } from "./commands/agents.js";
31
+ import { runContextCommand } from "./commands/context.js";
32
+ import { runHistoryCommand } from "./commands/history.js";
33
+ import { runInitCommand } from "./commands/init.js";
34
+ import { runPresetsCommand } from "./commands/presets.js";
35
+ import { runUpdateCommand } from "./commands/update.js";
36
+ import { optionalString } from "./commands/shared.js";
37
+ import { runTuiAgentsWizard, runTuiConfigLoop, runTuiRolesWizard, syncInteractiveDetectedAgents } from "./tuiController.js";
38
+ import { parseInterfaceFlag, parseModeFlag, resolveRunOptions } from "./runOptions.js";
27
39
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
28
40
  async function main() {
29
41
  const rawArgs = process.argv.slice(2);
@@ -65,42 +77,11 @@ async function main() {
65
77
  return;
66
78
  }
67
79
  if (parsed.command === "update") {
68
- const info = await getUpdateInfo(await getPackageVersion());
69
- const updateConfigPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
70
- const updateConfig = await configExists(updateConfigPath)
71
- ? await loadConfig(updateConfigPath)
72
- : undefined;
73
- const updateLanguage = resolveLanguage({
74
- explicitLanguage: optionalString(parsed.flags.language),
75
- configLanguage: updateConfig?.language
76
- });
77
- const updateMessages = createTranslator(updateLanguage);
78
- if (parsed.flags.apply) {
79
- await applySourceUpdate(info, updateMessages);
80
- console.log(updateMessages.update.upToDate);
81
- return;
82
- }
83
- console.log(formatUpdateInstructions(info, updateMessages));
80
+ await runUpdateCommand(parsed.flags);
84
81
  return;
85
82
  }
86
83
  if (parsed.command === "init" || parsed.command === "setup") {
87
- const initConfigPath = optionalString(parsed.flags.config) ?? (parsed.flags.local ? DEFAULT_CONFIG_PATH : GLOBAL_CONFIG_PATH);
88
- if (await configExists(initConfigPath)) {
89
- console.log(startupMessages.init.configExists(initConfigPath));
90
- return;
91
- }
92
- const discovery = await discoverLocalTools({
93
- ollamaUrl: optionalString(parsed.flags["ollama-url"])
94
- });
95
- const config = createConfigFromDiscovery(discovery);
96
- config.language = resolveLanguage({
97
- explicitLanguage: optionalString(parsed.flags.language),
98
- configLanguage: config.language
99
- });
100
- const initMessages = createTranslator(config.language);
101
- await writeExampleConfig(initConfigPath, config);
102
- console.log(initMessages.init.configCreated(initConfigPath));
103
- printInitDiscovery(discovery, config, initMessages);
84
+ await runInitCommand(parsed.flags);
104
85
  return;
105
86
  }
106
87
  const configPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
@@ -115,7 +96,7 @@ async function main() {
115
96
  configLanguage: config.language
116
97
  });
117
98
  const messages = createTranslator(config.language);
118
- await writeExampleConfig(configPath, config);
99
+ await writeConfig(configPath, config);
119
100
  if (!shouldOpenTuiHome(parsed)) {
120
101
  console.log(messages.init.editConfigThenRerun(configPath));
121
102
  return;
@@ -138,78 +119,94 @@ async function main() {
138
119
  let tuiVersion = "";
139
120
  let tuiLatestVersion;
140
121
  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)
122
+ let input = tuiInput;
123
+ // Les vues informatives (/help, /history, /update) réaffichent le prompt d'accueil
124
+ // puis reprennent le traitement avec la nouvelle saisie, d'où la boucle.
125
+ for (;;) {
126
+ if (!input) {
185
127
  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;
128
+ }
129
+ if (input.kind === "help") {
130
+ renderTuiHelp(messages);
131
+ input = await promptTuiHomeTopic(tuiMode, messages);
132
+ continue;
133
+ }
134
+ if (input.kind === "history") {
135
+ renderTuiHistory(await listHistoryEntries(resolveOutputDir(config.outputDir)), messages);
136
+ input = await promptTuiHomeTopic(tuiMode, messages);
137
+ continue;
138
+ }
139
+ if (input.kind === "update") {
140
+ const info = await getUpdateInfo(tuiVersion);
141
+ renderTuiUpdate(formatUpdateInstructions(info, messages), messages);
142
+ input = await promptTuiHomeTopic(tuiMode, messages);
143
+ continue;
144
+ }
145
+ if (input.kind === "home") {
201
146
  return "continue";
202
147
  }
203
- return "retry";
204
- }
205
- parsed.command = "";
206
- parsed.commandExplicit = false;
207
- if (hasCompletedTuiSession || resetTuiRunOverridesOnNextTopic) {
208
- clearTuiRunOverrides(parsed.flags);
209
- resetTuiRunOverridesOnNextTopic = false;
148
+ if (input.kind === "roles") {
149
+ const result = await runTuiRolesWizard(configPath, config, messages, tuiMode, input.roles);
150
+ if (result.quit)
151
+ return "quit";
152
+ tuiNotice = result.notice;
153
+ return "continue";
154
+ }
155
+ if (input.kind === "agents") {
156
+ const result = await runTuiAgentsWizard(configPath, config, messages, tuiMode, input.agents);
157
+ if (result.quit)
158
+ return "quit";
159
+ tuiNotice = result.notice;
160
+ resetTuiRunOverridesOnNextTopic ||= Boolean(result.changedRunDefaults);
161
+ return "continue";
162
+ }
163
+ if (input.kind === "mode") {
164
+ tuiMode = input.mode;
165
+ return "continue";
166
+ }
167
+ if (input.kind === "config") {
168
+ const result = await runTuiConfigLoop(configPath, config, messages, tuiMode);
169
+ if (result.quit)
170
+ return "quit";
171
+ tuiMode = result.mode;
172
+ resetTuiRunOverridesOnNextTopic ||= result.changedRunDefaults;
173
+ language = resolveLanguage({ explicitLanguage: optionalString(parsed.flags.language), configLanguage: config.language });
174
+ messages = createTranslator(language);
175
+ return "continue";
176
+ }
177
+ if (input.kind === "new") {
178
+ parsed.command = "new";
179
+ parsed.commandExplicit = true;
180
+ delete parsed.flags.topic;
181
+ return "run";
182
+ }
183
+ if (input.kind === "retry") {
184
+ if (!optionalString(parsed.flags.topic)) {
185
+ tuiNotice = messages.tui.retryUnavailable;
186
+ return "continue";
187
+ }
188
+ return "retry";
189
+ }
190
+ if (!input.topic) {
191
+ // Saisie réduite à des flags inline (ex. "--context src") : re-prompter au lieu de sortir en erreur.
192
+ tuiNotice = messages.common.topicRequired;
193
+ return "continue";
194
+ }
195
+ parsed.command = "";
196
+ parsed.commandExplicit = false;
197
+ if (hasCompletedTuiSession || resetTuiRunOverridesOnNextTopic) {
198
+ clearTuiRunOverrides(parsed.flags);
199
+ resetTuiRunOverridesOnNextTopic = false;
200
+ }
201
+ parsed.flags.topic = input.topic;
202
+ if (input.files && input.files.length > 0) {
203
+ parsed.flags.files = input.files;
204
+ }
205
+ if (input.context && input.context.length > 0) {
206
+ parsed.flags.context = input.context;
207
+ }
208
+ return "run";
210
209
  }
211
- parsed.flags.topic = tuiInput.topic;
212
- return "run";
213
210
  };
214
211
  if (shouldOpenTuiHome(parsed)) {
215
212
  const [syncResult, currentVersion, latestVersion] = await Promise.all([
@@ -283,35 +280,15 @@ async function main() {
283
280
  if (!topic) {
284
281
  throw new Error(messages.common.topicRequired);
285
282
  }
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,
283
+ const options = resolveRunOptions({
284
+ flags: parsed.flags,
285
+ config,
294
286
  language,
295
287
  topic,
296
- agentA,
297
- agentB,
298
- askAgents,
299
- turns: parseTurnsFlag(parsed.flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
300
- session: createSessionContext(),
301
288
  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),
289
+ preset,
313
290
  signal: debateAbortSignal()
314
- };
291
+ }, messages);
315
292
  if (parsed.flags["show-prompt"]) {
316
293
  printContextWarnings(context.warnings, messages);
317
294
  printPromptPreview(config, options, language, messages);
@@ -332,7 +309,7 @@ async function main() {
332
309
  if (!stayInTuiAfterSession) {
333
310
  return;
334
311
  }
335
- tuiMode = mode;
312
+ tuiMode = options.mode;
336
313
  for (;;) {
337
314
  const nextInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
338
315
  tuiNotice = undefined;
@@ -353,6 +330,7 @@ async function main() {
353
330
  }
354
331
  }
355
332
  }
333
+ /** Construit un signal d'annulation déclenché sur `SIGINT`/`SIGTERM` pour interrompre un débat en cours. */
356
334
  function debateAbortSignal() {
357
335
  const controller = new AbortController();
358
336
  const abort = () => {
@@ -364,39 +342,6 @@ function debateAbortSignal() {
364
342
  process.once("SIGTERM", abort);
365
343
  return controller.signal;
366
344
  }
367
- /**
368
- * Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
369
- * @param flags - Flags parsés depuis la ligne de commande.
370
- */
371
- async function runAgentsCommand(flags) {
372
- const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
373
- if (!(await configExists(configPath))) {
374
- const messages = createTranslator(resolveLanguage({ explicitLanguage: optionalString(flags.language) }));
375
- throw new Error(messages.agents.noConfig);
376
- }
377
- const config = await loadConfig(configPath);
378
- const language = resolveLanguage({
379
- explicitLanguage: optionalString(flags.language),
380
- configLanguage: config.language
381
- });
382
- const messages = createTranslator(language);
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
- }
398
- printAgents(configPath, config, discovery, messages);
399
- }
400
345
  /**
401
346
  * Exécute la commande `config` : wizard interactif ou mise à jour directe des paramètres par défaut.
402
347
  * @param flags - Flags parsés depuis la ligne de commande.
@@ -406,7 +351,7 @@ async function runConfigCommand(flags) {
406
351
  const explicitLanguage = optionalString(flags.language);
407
352
  if (!(await configExists(configPath))) {
408
353
  const messages = createTranslator(resolveLanguage({ explicitLanguage }));
409
- await writeExampleConfig(configPath);
354
+ await writeConfig(configPath);
410
355
  console.log(messages.config.createdForConfig(configPath));
411
356
  return;
412
357
  }
@@ -417,7 +362,7 @@ async function runConfigCommand(flags) {
417
362
  });
418
363
  const messages = createTranslator(language);
419
364
  if (flags["ollama-models"]) {
420
- await runOllamaModelsCommand(config, Boolean(flags.json));
365
+ await runOllamaModelsCommand(config, Boolean(flags.json), messages);
421
366
  return;
422
367
  }
423
368
  const setOllamaModelValue = optionalString(flags["set-ollama-model"]);
@@ -436,7 +381,7 @@ async function runConfigCommand(flags) {
436
381
  console.log(messages.config.syncNoMissing(configPath));
437
382
  return;
438
383
  }
439
- await writeExampleConfig(configPath, config);
384
+ await writeConfig(configPath, config);
440
385
  console.log(result.addedAgents.length > 0
441
386
  ? messages.config.syncAdded(configPath, result.addedAgents.join(", "))
442
387
  : messages.config.syncRefreshed(configPath));
@@ -505,395 +450,25 @@ async function runConfigCommand(flags) {
505
450
  if (changesDefaults) {
506
451
  config.defaults = nextDefaults;
507
452
  }
508
- await writeExampleConfig(configPath, config);
453
+ await writeConfig(configPath, config);
509
454
  console.log(messages.config.updated(configPath, formatDefaultsForMessage(config.defaults ?? {}, messages), config.language ?? DEFAULT_LANGUAGE));
510
455
  return;
511
456
  }
512
457
  if (flags["clear-defaults"]) {
513
458
  delete config.defaults;
514
- await writeExampleConfig(configPath, config);
459
+ await writeConfig(configPath, config);
515
460
  console.log(messages.config.cleared(configPath));
516
461
  return;
517
462
  }
518
463
  await runConfigWizard(configPath, config, messages);
519
464
  }
520
- async function runTuiConfigLoop(configPath, config, messages, initialMode) {
521
- let mode = initialMode;
522
- let notice;
523
- let currentMessages = messages;
524
- let changedRunDefaults = false;
525
- for (;;) {
526
- renderTuiConfig(config, configPath, mode, currentMessages, { message: notice });
527
- notice = undefined;
528
- const input = await promptTuiConfigCommand(mode, currentMessages);
529
- if (input.kind === "quit") {
530
- return { mode, quit: true, changedRunDefaults };
531
- }
532
- if (input.kind === "back") {
533
- return { mode, quit: false, changedRunDefaults };
534
- }
535
- if (input.kind === "unknown") {
536
- notice = input.message;
537
- continue;
538
- }
539
- if (input.kind === "mode") {
540
- mode = mode === "ask" ? "debate" : "ask";
541
- config.defaults = { ...(config.defaults ?? {}), mode };
542
- await writeExampleConfig(configPath, config);
543
- changedRunDefaults = true;
544
- notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
545
- continue;
546
- }
547
- if (input.kind === "default-mode") {
548
- config.defaults = { ...(config.defaults ?? {}), mode };
549
- await writeExampleConfig(configPath, config);
550
- changedRunDefaults = true;
551
- notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
552
- continue;
553
- }
554
- if (input.kind === "interface") {
555
- config.defaults = { ...(config.defaults ?? {}), interface: input.interfaceName };
556
- await writeExampleConfig(configPath, config);
557
- notice = currentMessages.tui.interfaceDefault(input.interfaceName);
558
- continue;
559
- }
560
- if (input.kind === "language") {
561
- config.language = parseLanguage(input.language, "--language");
562
- await writeExampleConfig(configPath, config);
563
- currentMessages = createTranslator(config.language ?? DEFAULT_LANGUAGE);
564
- notice = currentMessages.tui.languageUpdated(input.language);
565
- continue;
566
- }
567
- if (input.kind === "agents") {
568
- try {
569
- const agentsInput = input.agents.length > 0
570
- ? { kind: "agents", agents: input.agents }
571
- : await promptTuiAgentsWizard(config, mode, currentMessages);
572
- if (agentsInput.kind === "quit") {
573
- return { mode, quit: true, changedRunDefaults };
574
- }
575
- if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
576
- notice = currentMessages.tui.agentsUnchanged;
577
- continue;
578
- }
579
- if (mode === "ask") {
580
- const agents = normalizeTuiAskAgents(config, agentsInput.agents, currentMessages);
581
- config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
582
- await writeExampleConfig(configPath, config);
583
- changedRunDefaults = true;
584
- notice = currentMessages.tui.askAgentsUpdated(agents.join(", "));
585
- }
586
- else {
587
- const [agentA, agentB] = normalizeTuiDebateAgents(config, agentsInput.agents, currentMessages);
588
- config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
589
- await writeExampleConfig(configPath, config);
590
- changedRunDefaults = true;
591
- notice = currentMessages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
592
- }
593
- }
594
- catch (error) {
595
- notice = error instanceof Error ? error.message : String(error);
596
- }
597
- continue;
598
- }
599
- if (input.kind === "roles") {
600
- try {
601
- const rolesInput = input.roles.length > 0
602
- ? { kind: "roles", roles: input.roles }
603
- : await promptTuiRolesWizard(config, mode, currentMessages);
604
- if (rolesInput.kind === "quit") {
605
- return { mode, quit: true, changedRunDefaults };
606
- }
607
- if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
608
- notice = currentMessages.tui.rolesUnchanged;
609
- continue;
610
- }
611
- notice = applyTuiRoles(config, mode, rolesInput.roles, currentMessages);
612
- await writeExampleConfig(configPath, config);
613
- }
614
- catch (error) {
615
- notice = error instanceof Error ? error.message : String(error);
616
- }
617
- continue;
618
- }
619
- if (input.kind === "turns") {
620
- if (mode === "ask") {
621
- notice = currentMessages.tui.askTurnsNotice;
622
- continue;
623
- }
624
- try {
625
- validateTurns(input.turns, "--turns", currentMessages);
626
- config.defaults = { ...(config.defaults ?? {}), turns: input.turns };
627
- await writeExampleConfig(configPath, config);
628
- changedRunDefaults = true;
629
- notice = currentMessages.tui.turnsUpdated(input.turns);
630
- }
631
- catch (error) {
632
- notice = error instanceof Error ? error.message : String(error);
633
- }
634
- continue;
635
- }
636
- if (input.kind === "summary") {
637
- try {
638
- const nextDefaults = { ...(config.defaults ?? {}) };
639
- if (input.agent !== undefined) {
640
- assertKnownAgent(config, input.agent, mode === "ask" ? "defaults.askSummaryAgent" : "defaults.summaryAgent", currentMessages);
641
- }
642
- if (mode === "ask") {
643
- if (input.agent === undefined) {
644
- delete nextDefaults.askSummaryAgent;
645
- notice = currentMessages.tui.askSummaryFallback;
646
- }
647
- else {
648
- nextDefaults.askSummaryAgent = input.agent;
649
- notice = currentMessages.tui.askSummaryAgent(input.agent);
650
- }
651
- }
652
- else if (input.agent === undefined) {
653
- delete nextDefaults.summaryAgent;
654
- notice = currentMessages.tui.debateSummaryFallback;
655
- }
656
- else {
657
- nextDefaults.summaryAgent = input.agent;
658
- notice = currentMessages.tui.debateSummaryAgent(input.agent);
659
- }
660
- config.defaults = nextDefaults;
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;
692
- }
693
- catch (error) {
694
- notice = error instanceof Error ? error.message : String(error);
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;
707
- }
708
- }
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
- }
785
- function normalizeTuiDebateAgents(config, agents, messages) {
786
- const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
787
- if (unique.length !== 2) {
788
- throw new Error(messages.tui.debateAgentsUsage);
789
- }
790
- assertKnownAgent(config, unique[0], "defaults.agentA", messages);
791
- assertKnownAgent(config, unique[1], "defaults.agentB", messages);
792
- return [unique[0], unique[1]];
793
- }
794
- function normalizeTuiAskAgents(config, agents, messages) {
795
- const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
796
- if (unique.length === 0) {
797
- throw new Error(messages.tui.askAgentsUsage);
798
- }
799
- if (unique.length > MAX_ASK_AGENTS) {
800
- throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
801
- }
802
- unique.forEach((agent) => assertKnownAgent(config, agent, "defaults.askAgents", messages));
803
- return unique;
804
- }
805
- async function runTuiAgentsWizard(configPath, config, messages, mode, inlineAgents = []) {
806
- try {
807
- const agentsInput = inlineAgents.length > 0
808
- ? { kind: "agents", agents: inlineAgents }
809
- : await promptTuiAgentsWizard(config, mode, messages);
810
- if (agentsInput.kind === "quit") {
811
- return { quit: true, changedRunDefaults: false };
812
- }
813
- if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
814
- return { quit: false, changedRunDefaults: false };
815
- }
816
- const notice = applyTuiAgents(config, mode, agentsInput.agents, messages);
817
- await writeExampleConfig(configPath, config);
818
- return { notice, quit: false, changedRunDefaults: true };
819
- }
820
- catch (error) {
821
- return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false, changedRunDefaults: false };
822
- }
823
- }
824
- async function runTuiRolesWizard(configPath, config, messages, mode, inlineRoles = []) {
825
- try {
826
- const rolesInput = inlineRoles.length > 0
827
- ? { kind: "roles", roles: inlineRoles }
828
- : await promptTuiRolesWizard(config, mode, messages);
829
- if (rolesInput.kind === "quit") {
830
- return { quit: true };
831
- }
832
- if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
833
- return { quit: false };
834
- }
835
- const notice = applyTuiRoles(config, mode, rolesInput.roles, messages);
836
- await writeExampleConfig(configPath, config);
837
- return { notice, quit: false };
838
- }
839
- catch (error) {
840
- return { notice: messages.tui.rolesError(error instanceof Error ? error.message : String(error)), quit: false };
841
- }
842
- }
843
- function applyTuiAgents(config, mode, agentNames, messages) {
844
- if (mode === "ask") {
845
- const agents = normalizeTuiAskAgents(config, agentNames, messages);
846
- config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
847
- return messages.tui.askAgentsUpdated(agents.join(", "));
848
- }
849
- const [agentA, agentB] = normalizeTuiDebateAgents(config, agentNames, messages);
850
- config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
851
- return messages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
852
- }
853
- function applyTuiRoles(config, mode, roleNames, messages) {
854
- const agents = activeAgentsForMode(config, mode);
855
- if (agents.length === 0) {
856
- throw new Error(mode === "ask" ? messages.tui.noAskAgentsConfigured : messages.tui.noDebateAgentsConfigured);
857
- }
858
- const roles = normalizeTuiRoles(roleNames, agents, mode, messages);
859
- agents.forEach((agent, index) => {
860
- config.agents[agent].role = roles[index];
861
- });
862
- return mode === "ask"
863
- ? messages.tui.askRolesUpdated(roles.join(", "))
864
- : messages.tui.debateRolesUpdated(roles.join(" <-> "));
865
- }
866
- function activeAgentsForMode(config, mode) {
867
- const defaults = config.defaults ?? {};
868
- if (mode === "ask") {
869
- if (defaults.askAgents && defaults.askAgents.length > 0) {
870
- return defaults.askAgents.filter((agent) => Boolean(config.agents[agent]));
871
- }
872
- return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
873
- }
874
- return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
875
- }
876
- function normalizeTuiRoles(roleNames, agents, mode, messages) {
877
- const roles = roleNames.map((role) => role.trim().toLowerCase()).filter(Boolean);
878
- const expectedCount = agents.length;
879
- if (roles.length < expectedCount) {
880
- const agentLabel = mode === "ask"
881
- ? agents.join(", ")
882
- : agents.join(" <-> ");
883
- throw new Error(messages.tui.rolesCountError(roles.length, expectedCount, agentLabel));
884
- }
885
- return roles.slice(0, expectedCount).map((role) => {
886
- if (isAgentRole(role)) {
887
- return role;
888
- }
889
- throw new Error(messages.tui.unknownRole(role, VALID_AGENT_ROLES.join(", ")));
890
- });
891
- }
892
- function isAgentRole(value) {
893
- return VALID_AGENT_ROLES.includes(value);
894
- }
895
- const VALID_AGENT_ROLES = ["implementer", "reviewer", "architect", "scout", "critic", "summarizer"];
896
- async function runOllamaModelsCommand(config, json) {
465
+ /**
466
+ * Affiche l'état de l'agent Ollama local (modèle courant, disponibilité de l'API, modèles installés).
467
+ * @param config - Config chargée.
468
+ * @param json - Si `true`, affiche le résultat en JSON plutôt qu'en texte lisible.
469
+ * @param messages - Dictionnaire localisé pour la sortie texte.
470
+ */
471
+ async function runOllamaModelsCommand(config, json, messages) {
897
472
  const discovery = await discoverLocalToolsForConfig(config);
898
473
  const agent = config.agents["ollama-local"];
899
474
  const currentModel = agent?.type === "ollama" ? agent.model : null;
@@ -910,10 +485,16 @@ async function runOllamaModelsCommand(config, json) {
910
485
  console.log(JSON.stringify(payload, null, 2));
911
486
  return;
912
487
  }
913
- console.log(`ollama-local: ${currentModel ?? "(non configuré)"}`);
914
- console.log(`Ollama API: ${discovery.ollama.available ? "joignable" : "indisponible"} (${discovery.ollama.baseUrl})`);
915
- console.log(`Modèles installés: ${discovery.ollama.models.length > 0 ? discovery.ollama.models.join(", ") : "(aucun)"}`);
488
+ console.log(messages.config.ollamaModelsCurrent(currentModel));
489
+ console.log(messages.config.ollamaModelsApi(discovery.ollama.available, discovery.ollama.baseUrl));
490
+ console.log(messages.config.ollamaModelsInstalled(discovery.ollama.models));
916
491
  }
492
+ /**
493
+ * Change le modèle configuré pour l'agent `ollama-local` après vérification qu'il est bien installé.
494
+ * @param configPath - Chemin du fichier de config à mettre à jour.
495
+ * @param config - Config chargée.
496
+ * @param model - Nom du modèle Ollama à définir.
497
+ */
917
498
  async function runSetOllamaModelCommand(configPath, config, model, messages) {
918
499
  const trimmed = model.trim();
919
500
  if (!trimmed) {
@@ -928,11 +509,16 @@ async function runSetOllamaModelCommand(configPath, config, model, messages) {
928
509
  throw new Error(messages.config.ollamaModelUnavailable(trimmed));
929
510
  }
930
511
  const result = setOllamaModel(config, trimmed);
931
- await writeExampleConfig(configPath, config);
512
+ await writeConfig(configPath, config);
932
513
  console.log(result
933
514
  ? messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel)
934
515
  : messages.config.ollamaModelNoChange(configPath, agent.model));
935
516
  }
517
+ /**
518
+ * Aligne automatiquement le modèle de l'agent `ollama-local` sur un modèle réellement installé.
519
+ * @param configPath - Chemin du fichier de config à mettre à jour.
520
+ * @param config - Config chargée.
521
+ */
936
522
  async function runSyncOllamaModelCommand(configPath, config, messages) {
937
523
  const discovery = await discoverLocalToolsForConfig(config);
938
524
  const agent = config.agents["ollama-local"];
@@ -947,7 +533,7 @@ async function runSyncOllamaModelCommand(configPath, config, messages) {
947
533
  console.log(messages.config.ollamaModelNoChange(configPath, agent.model));
948
534
  return;
949
535
  }
950
- await writeExampleConfig(configPath, config);
536
+ await writeConfig(configPath, config);
951
537
  console.log(messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel));
952
538
  }
953
539
  /**
@@ -965,6 +551,11 @@ function isNoneValue(value) {
965
551
  function formatDefaultsForMessage(defaults, messages) {
966
552
  return messages.config.defaultsSummary(defaults.agentA, defaults.agentB, turnsOrDefault(defaults.turns), defaults.summaryAgent, defaults.askSummaryAgent, defaults.mode, defaults.askAgents, defaults.interface);
967
553
  }
554
+ /**
555
+ * Déduplique et valide une liste d'agents `ask` fournie via `--ask-agents`.
556
+ * @param agents - Noms d'agents bruts, éventuellement en doublon.
557
+ * @throws Si la liste dépasse `MAX_ASK_AGENTS` ou référence un agent inconnu de la config.
558
+ */
968
559
  function normalizeAskAgentsForConfig(config, agents, messages) {
969
560
  const unique = agents
970
561
  .map((agent) => agent.trim())
@@ -988,60 +579,6 @@ function assertKnownAgent(config, agentName, fieldName, messages) {
988
579
  throw new Error(messages.common.unknownAgentForField(fieldName, agentName, Object.keys(config.agents).join(", ")));
989
580
  }
990
581
  }
991
- /**
992
- * Résout le nom d'un agent selon la priorité : flag CLI > preset > défaut config.
993
- * Lève une erreur si aucune source ne fournit de valeur.
994
- * @param label - Libellé humain utilisé dans le message d'erreur (ex. "agent A").
995
- * @param explicitValue - Valeur passée via flag CLI.
996
- * @param presetValue - Valeur issue du preset sélectionné.
997
- * @param defaultValue - Valeur issue des défauts de la config.
998
- * @returns Nom de l'agent résolu.
999
- */
1000
- function resolveAgentName(label, explicitValue, presetValue, defaultValue, messages) {
1001
- const resolved = optionalString(explicitValue) ?? presetValue ?? defaultValue;
1002
- if (!resolved) {
1003
- throw new Error(messages.common.noAgentDefined(label));
1004
- }
1005
- return resolved;
1006
- }
1007
- function resolveSummaryAgentOption(explicitValue, defaults, mode) {
1008
- const explicit = optionalString(explicitValue);
1009
- if (explicit) {
1010
- return explicit;
1011
- }
1012
- if (mode === "ask") {
1013
- return defaults?.askSummaryAgent ?? defaults?.summaryAgent;
1014
- }
1015
- return defaults?.summaryAgent;
1016
- }
1017
- function parseModeFlag(value, messages) {
1018
- if (!value) {
1019
- return "debate";
1020
- }
1021
- if (value === "debate" || value === "ask") {
1022
- return value;
1023
- }
1024
- throw new Error(messages.common.unknownMode(value, "debate, ask"));
1025
- }
1026
- function parseInterfaceFlag(value, messages) {
1027
- if (!value) {
1028
- return "tui";
1029
- }
1030
- if (value === "tui" || value === "terminal") {
1031
- return value;
1032
- }
1033
- throw new Error(messages.common.unknownMode(value, "tui, terminal"));
1034
- }
1035
- function resolveAskAgents(explicitAgents, defaultAgents, fallbackAgents, messages) {
1036
- const selected = explicitAgents.length > 0
1037
- ? explicitAgents
1038
- : defaultAgents && defaultAgents.length > 0 ? defaultAgents : fallbackAgents;
1039
- const unique = selected.filter((agent, index) => agent.trim() && selected.indexOf(agent) === index);
1040
- if (unique.length > MAX_ASK_AGENTS) {
1041
- throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
1042
- }
1043
- return unique;
1044
- }
1045
582
  /**
1046
583
  * Affiche un aperçu du prompt du premier tour sans appeler aucun agent (flag `--show-prompt`).
1047
584
  * @param config - Config chargée.
@@ -1071,29 +608,13 @@ function printPromptPreview(config, options, language, messages) {
1071
608
  console.log(messages.preview.agent(previewAgent, agentConfig.role));
1072
609
  console.log(messages.preview.peer(peerName));
1073
610
  console.log(messages.preview.pullModels(options.pullModels));
1074
- console.log(messages.preview.summary(options.summaryEnabled ? previewSummaryAgent(options) : messages.preview.disabled));
611
+ console.log(messages.preview.summary(options.summaryEnabled ? options.summaryAgent : messages.preview.disabled));
1075
612
  console.log(messages.preview.interfaceLanguage(language));
1076
613
  console.log("");
1077
614
  console.log(prompt);
1078
615
  console.log("");
1079
616
  console.log(options.mode === "ask" ? messages.preview.askNote : messages.preview.note);
1080
617
  }
1081
- function previewSummaryAgent(options) {
1082
- if (options.summaryAgent) {
1083
- return options.summaryAgent;
1084
- }
1085
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
1086
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
1087
- }
1088
- return options.agentB;
1089
- }
1090
- /**
1091
- * Extrait une chaîne non vide depuis une valeur de flag, ou renvoie `undefined`.
1092
- * @param value - Valeur brute issue du parseur de flags.
1093
- */
1094
- function optionalString(value) {
1095
- return typeof value === "string" && value.trim() ? value : undefined;
1096
- }
1097
618
  /**
1098
619
  * Pré-lit seulement `--language`/`--lang` dans les arguments bruts pour localiser
1099
620
  * les erreurs qui peuvent survenir avant le parsing complet ou le chargement de config.
@@ -1144,14 +665,12 @@ function createRendererFromFlags(flags, plainOutputFallback, defaultInterface, m
1144
665
  if (flags.json) {
1145
666
  return createNdjsonRenderer();
1146
667
  }
1147
- if (flags.tui) {
1148
- return createTuiRenderer(messages);
1149
- }
1150
- if (flags.terminal || flags.plain || plainOutputFallback || defaultInterface === "terminal") {
1151
- return createConsoleRenderer(true, messages);
1152
- }
1153
668
  return createAutoRenderer(flags, plainOutputFallback, defaultInterface, messages);
1154
669
  }
670
+ /**
671
+ * Choix du renderer par défaut (`--renderer auto`) : TUI si les flags/config le demandent
672
+ * ou si stdout est un TTY, rendu console plain sinon.
673
+ */
1155
674
  function createAutoRenderer(flags, plainOutputFallback, defaultInterface, messages) {
1156
675
  if (flags.tui) {
1157
676
  return createTuiRenderer(messages);
@@ -1161,6 +680,11 @@ function createAutoRenderer(flags, plainOutputFallback, defaultInterface, messag
1161
680
  }
1162
681
  return process.stdout.isTTY ? createTuiRenderer(messages) : createConsoleRenderer(true, messages);
1163
682
  }
683
+ /**
684
+ * Détermine si l'accueil TUI doit s'ouvrir : commande `run` implicite, sans sujet, preset
685
+ * ni flag de rendu déjà fourni. Toute intention explicite de lancer directement un débat
686
+ * (topic, `--renderer`, `--json`, `--plain`, `--terminal`) désactive l'accueil.
687
+ */
1164
688
  function shouldOpenTuiHome(parsed) {
1165
689
  return parsed.command === "run"
1166
690
  && !parsed.commandExplicit
@@ -1171,98 +695,6 @@ function shouldOpenTuiHome(parsed) {
1171
695
  && parsed.flags.plain !== true
1172
696
  && parsed.flags.terminal !== true;
1173
697
  }
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
- }
1181
- /**
1182
- * Exécute la commande `palabre presets` en sortie humaine ou JSON versionné.
1183
- */
1184
- async function runPresetsCommand(flags) {
1185
- const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
1186
- const ollamaUrl = optionalString(flags["ollama-url"]);
1187
- const config = await configExists(configPath)
1188
- ? await loadConfig(configPath)
1189
- : undefined;
1190
- const discovery = config
1191
- ? await discoverLocalToolsForConfig(config, ollamaUrl)
1192
- : await discoverLocalTools({ ollamaUrl });
1193
- const resolvedConfig = config ?? createConfigFromDiscovery(discovery);
1194
- const language = resolveLanguage({
1195
- explicitLanguage: optionalString(flags.language),
1196
- configLanguage: resolvedConfig.language
1197
- });
1198
- const messages = createTranslator(language);
1199
- const presets = listPresetsWithAvailability(resolvedConfig, discovery, messages);
1200
- if (flags.json) {
1201
- process.stdout.write(JSON.stringify({ v: 1, presets }) + "\n");
1202
- return;
1203
- }
1204
- console.log(messages.presets.title);
1205
- console.log("");
1206
- for (const preset of presets) {
1207
- const status = preset.available
1208
- ? messages.presets.available
1209
- : messages.presets.unavailable(preset.unavailableReasons.join("; "));
1210
- console.log(` ${preset.name.padEnd(20)} ${preset.agentA} <-> ${preset.agentB} ${status}`);
1211
- }
1212
- console.log("");
1213
- console.log(messages.presets.total(presets.length));
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
- }
1241
- async function runContextCommand(flags, positionals) {
1242
- const language = resolveLanguage({ explicitLanguage: optionalString(flags.language) });
1243
- const messages = createTranslator(language);
1244
- const subcommand = positionals[0] ?? "scan";
1245
- if (subcommand !== "scan") {
1246
- throw new Error(messages.common.unknownCommand(`context ${subcommand}`, "context scan"));
1247
- }
1248
- const paths = positionals.slice(1);
1249
- const result = await buildContextScan(paths, process.cwd(), messages);
1250
- const folders = result.items.filter((item) => item.kind === "folder");
1251
- const files = result.items.filter((item) => item.kind === "file");
1252
- if (flags.json) {
1253
- console.log(JSON.stringify(result, null, 2));
1254
- return;
1255
- }
1256
- for (const folder of folders) {
1257
- console.log(`[folder] ${folder.path}`);
1258
- }
1259
- for (const file of files) {
1260
- console.log(`[file] ${file.path} (${file.sizeBytes} bytes)`);
1261
- }
1262
- for (const warning of result.warnings) {
1263
- console.error(`${messages.renderers.warningPrefix} ${warning}`);
1264
- }
1265
- }
1266
698
  /**
1267
699
  * Écrit les avertissements de contexte sur `stderr`.
1268
700
  * @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
@@ -1272,157 +704,16 @@ function printContextWarnings(warnings, messages) {
1272
704
  process.stderr.write(`${messages.renderers.warningPrefix} ${warning}\n`);
1273
705
  }
1274
706
  }
1275
- /**
1276
- * Ajoute dans `config.agents` les agents détectés localement mais absents de la config.
1277
- * Mute `config` directement ; l'appelant est responsable de persister la config.
1278
- * @param config - Config Palabre à compléter.
1279
- * @param discovery - Résultat de la découverte locale des outils.
1280
- * @returns Noms des agents nouvellement ajoutés.
1281
- */
1282
- /**
1283
- * Affiche la liste des agents déclarés avec leur type, rôle, état de détection et défauts.
1284
- * @param configPath - Chemin du fichier de config (affiché en en-tête).
1285
- * @param config - Config Palabre chargée.
1286
- * @param discovery - Résultat de la découverte locale des outils.
1287
- */
1288
- function printAgents(configPath, config, discovery, messages) {
1289
- const entries = Object.entries(config.agents)
1290
- .filter(([name]) => !isRetiredAgentName(name))
1291
- .sort(([left], [right]) => left.localeCompare(right));
1292
- console.log(messages.agents.config(configPath));
1293
- console.log("");
1294
- console.log(messages.agents.title);
1295
- for (const [name, agentConfig] of entries) {
1296
- const status = formatAgentDetection(name, agentConfig, discovery, messages);
1297
- const defaults = formatAgentDefaults(name, config, messages);
1298
- const details = formatAgentDetails(agentConfig, messages);
1299
- const suffix = defaults ? ` | ${defaults}` : "";
1300
- console.log(`- ${name.padEnd(13)} ${`${agentConfig.type}/${agentConfig.role}`.padEnd(18)} ${status}${suffix}`);
1301
- if (details) {
1302
- console.log(` ${details}`);
1303
- }
1304
- }
1305
- console.log("");
1306
- console.log(messages.agents.defaults(config.defaults?.agentA ?? messages.agents.none, config.defaults?.agentB ?? messages.agents.none, turnsOrDefault(config.defaults?.turns), config.defaults?.summaryAgent ?? messages.agents.summaryAgentB, config.defaults?.askSummaryAgent));
1307
- }
1308
- /**
1309
- * Renvoie un libellé indiquant si l'agent est agent A, agent B ou agent de synthèse par défaut.
1310
- * @param name - Nom de l'agent.
1311
- * @param config - Config Palabre contenant les défauts.
1312
- */
1313
- function formatAgentDefaults(name, config, messages) {
1314
- const labels = [];
1315
- if (config.defaults?.agentA === name)
1316
- labels.push(messages.agents.defaultAgentA);
1317
- if (config.defaults?.agentB === name)
1318
- labels.push(messages.agents.defaultAgentB);
1319
- if (config.defaults?.summaryAgent === name)
1320
- labels.push(messages.agents.defaultSummary);
1321
- if (config.defaults?.askSummaryAgent === name)
1322
- labels.push(messages.agents.defaultAskSummary);
1323
- return labels.join(", ");
1324
- }
1325
- /**
1326
- * Renvoie une ligne de détails pour un agent : commande CLI ou modèle Ollama.
1327
- * @param agentConfig - Configuration de l'agent.
1328
- */
1329
- function formatAgentDetails(agentConfig, messages) {
1330
- if (agentConfig.type === "ollama") {
1331
- return messages.agents.model(agentConfig.model);
1332
- }
1333
- return messages.agents.command(agentConfig.command, agentConfig.model);
1334
- }
1335
- /**
1336
- * Renvoie le statut de détection d'un agent sous forme de chaîne lisible.
1337
- * Pour Ollama, vérifie la disponibilité du serveur et la présence du modèle.
1338
- * @param name - Nom de l'agent dans la config.
1339
- * @param agentConfig - Configuration de l'agent.
1340
- * @param discovery - Résultat de la découverte locale des outils.
1341
- */
1342
- function formatAgentDetection(name, agentConfig, discovery, messages) {
1343
- if (agentConfig.type === "ollama") {
1344
- if (!discovery.ollama.available) {
1345
- return discovery.ollama.commandAvailable ? messages.agents.ollamaUnreachable : messages.agents.ollamaNotDetected;
1346
- }
1347
- return discovery.ollama.models.includes(agentConfig.model)
1348
- ? messages.agents.detected()
1349
- : messages.agents.missingModel(agentConfig.model);
1350
- }
1351
- const detection = cliDetectionForAgent(name, agentConfig, discovery);
1352
- return detection.available ? messages.agents.detected(detection.command) : messages.agents.notDetected;
1353
- }
1354
- /**
1355
- * Résout l'entrée de détection correspondant à un agent CLI.
1356
- * Renvoie un objet `{ available: true }` pour les agents CLI non reconnus (considérés disponibles).
1357
- * @param name - Nom de l'agent dans la config.
1358
- * @param agentConfig - Configuration de l'agent.
1359
- * @param discovery - Résultat de la découverte locale des outils.
1360
- */
1361
- function cliDetectionForAgent(name, agentConfig, discovery) {
1362
- const command = agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name;
1363
- return detectionForCommand(command, discovery) ?? { available: true, command };
1364
- }
1365
- /**
1366
- * Affiche le récapitulatif de détection locale après `palabre init`.
1367
- * @param discovery - Résultat de la découverte locale des outils.
1368
- * @param config - Config générée à partir de la découverte.
1369
- */
1370
- function printInitDiscovery(discovery, config, messages) {
1371
- console.log("");
1372
- console.log(messages.init.localDetectionTitle);
1373
- console.log(`- Codex CLI: ${formatCommandDetection(discovery.codex, messages)}`);
1374
- console.log(`- Claude CLI: ${formatCommandDetection(discovery.claude, messages)}`);
1375
- console.log(`- Antigravity CLI: ${formatCommandDetection(discovery.antigravity, messages)}`);
1376
- console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode, messages)}`);
1377
- console.log(`- Mistral Vibe CLI: ${formatCommandDetection(discovery.vibe, messages)}`);
1378
- console.log(`- Ollama API: ${formatOllamaDetection(discovery.ollama, messages)}`);
1379
- console.log("");
1380
- console.log(config.defaults?.agentA && config.defaults.agentB
1381
- ? messages.init.defaults(config.defaults.agentA, config.defaults.agentB)
1382
- : messages.init.noDefaultPair(formatDetectedAgentSummary(discovery, config.language ?? DEFAULT_LANGUAGE)));
1383
- console.log(messages.init.languageHint(config.language ?? DEFAULT_LANGUAGE));
1384
- }
1385
- function formatDetectedAgentSummary(discovery, language) {
1386
- const names = detectedAgentNames(discovery);
1387
- if (names.length === 0) {
1388
- return language === "en" ? "no agent detected" : "aucun agent détecté";
1389
- }
1390
- if (names.length === 1) {
1391
- return language === "en"
1392
- ? `only one agent detected (${names[0]})`
1393
- : `un seul agent détecté (${names[0]})`;
1394
- }
1395
- return language === "en"
1396
- ? `no usable pair detected among ${names.join(", ")}`
1397
- : `aucune paire utilisable détectée parmi ${names.join(", ")}`;
1398
- }
1399
- /**
1400
- * Formate le statut de détection d'un outil CLI (disponible ou non).
1401
- * @param detection - Résultat de détection d'un outil CLI.
1402
- */
1403
- function formatCommandDetection(detection, messages) {
1404
- return detection.available
1405
- ? messages.init.commandDetected(detection.command)
1406
- : messages.init.commandMissing;
1407
- }
1408
- /**
1409
- * Formate le statut de détection d'Ollama : commande absente, serveur injoignable ou modèles disponibles.
1410
- * @param detection - Résultat de détection d'Ollama.
1411
- */
1412
- function formatOllamaDetection(detection, messages) {
1413
- if (!detection.available) {
1414
- return detection.commandAvailable
1415
- ? messages.init.ollamaServerUnreachable(detection.baseUrl)
1416
- : messages.init.ollamaMissing;
1417
- }
1418
- const modelCount = detection.models.length;
1419
- return messages.init.ollamaDetected(modelCount);
1420
- }
1421
707
  /** Affiche le texte d'aide complet sur `stdout`. */
1422
708
  function printHelp(messages, command) {
1423
709
  const commandHelp = command ? messages.help.renderCommand(command) : undefined;
1424
710
  console.log(commandHelp ?? messages.help.render(listPresetNames().join(", ")));
1425
711
  }
712
+ /**
713
+ * Résout la commande cible pour l'aide contextuelle (`palabre <cmd> --help`), en normalisant
714
+ * les alias (`agent` -> `agents`, `preset` -> `presets`, `setup` -> `init`).
715
+ * @returns `undefined` pour `help`/`run`, qui utilisent l'aide générale.
716
+ */
1426
717
  function commandHelpTarget(parsed) {
1427
718
  if (parsed.command === "help" || parsed.command === "run") {
1428
719
  return undefined;
@@ -1450,16 +741,16 @@ async function resolveCommandMessages(flags) {
1450
741
  }
1451
742
  return createTranslator(resolveLanguage({ explicitLanguage, configLanguage }));
1452
743
  }
744
+ /**
745
+ * Formate une erreur non gérée remontée jusqu'au point d'entrée en message lisible,
746
+ * en spécialisant `AdapterError` et `OllamaUrlError` pour rester actionnable.
747
+ */
1453
748
  function formatRuntimeError(error, messages) {
1454
749
  if (error instanceof AdapterError) {
1455
750
  return formatAdapterError(error, messages);
1456
751
  }
1457
752
  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);
753
+ return formatOllamaUrlError(error, messages);
1463
754
  }
1464
755
  return error instanceof Error ? error.message : String(error);
1465
756
  }
@@ -1470,6 +761,10 @@ main().catch((error) => {
1470
761
  console.error(`${messages.common.errorPrefix}: ${message}`);
1471
762
  process.exitCode = 1;
1472
763
  });
764
+ /**
765
+ * Variante de `findRawLanguageFlag` + `resolveLanguage` qui ne peut pas lever, utilisée dans
766
+ * le gestionnaire d'erreur global où la config n'est pas forcément chargée.
767
+ */
1473
768
  function safeStartupLanguage(args) {
1474
769
  try {
1475
770
  return resolveLanguage({ explicitLanguage: findRawLanguageFlag(args) });