palabre 0.6.4 → 0.8.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,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
2
+ import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaModel, syncDetectedAgents, syncOllamaModel, writeExampleConfig } from "./config.js";
6
3
  import { loadProjectInputs } from "./context.js";
7
4
  import { buildContextScan } from "./contextScan.js";
8
5
  import { discoverLocalTools } from "./discovery.js";
@@ -10,18 +7,20 @@ import { runDoctor } from "./doctor.js";
10
7
  import { AdapterError, formatAdapterError } from "./errors.js";
11
8
  import { runConfigWizard } from "./configWizard.js";
12
9
  import { createTranslator, DEFAULT_LANGUAGE, parseLanguage, resolveLanguage } from "./i18n.js";
13
- import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault } from "./limits.js";
10
+ import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault, validateTurns } from "./limits.js";
14
11
  import { formatAgentPrompt } from "./prompt.js";
15
12
  import { runNewWizard } from "./new.js";
16
13
  import { listPresetNames, listPresetsWithAvailability, resolvePreset } from "./presets.js";
17
14
  import { createConsoleRenderer } from "./renderers/console.js";
18
15
  import { createNdjsonRenderer } from "./renderers/ndjson.js";
19
- import { runDebate } from "./orchestrator.js";
16
+ import { createTuiRenderer, promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiHomeTopic, promptTuiRolesWizard, renderTuiConfig, renderTuiHelp, renderTuiHome } from "./renderers/tui.js";
17
+ import { MAX_ASK_AGENTS, runAsk, runDebate } from "./orchestrator.js";
20
18
  import { writeDebateMarkdown } from "./output.js";
21
19
  import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./update.js";
22
20
  import { createSessionContext } from "./session.js";
23
21
  import { getStringListFlag, parseArgs } from "./args.js";
24
22
  import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
23
+ import { getPackageVersion } from "./version.js";
25
24
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
26
25
  async function main() {
27
26
  const rawArgs = process.argv.slice(2);
@@ -37,7 +36,7 @@ async function main() {
37
36
  return;
38
37
  }
39
38
  if (parsed.command === "doctor") {
40
- const result = await runDoctor(optionalString(parsed.flags.config), Boolean(parsed.flags.plain), optionalString(parsed.flags.language));
39
+ const result = await runDoctor(optionalString(parsed.flags.config), Boolean(parsed.flags.plain || parsed.flags.terminal), optionalString(parsed.flags.language));
41
40
  console.log(result.output);
42
41
  process.exitCode = result.ok ? 0 : 1;
43
42
  return;
@@ -108,12 +107,106 @@ async function main() {
108
107
  return;
109
108
  }
110
109
  const config = await loadConfig(configPath);
111
- const language = resolveLanguage({
110
+ let language = resolveLanguage({
112
111
  explicitLanguage: optionalString(parsed.flags.language),
113
112
  configLanguage: config.language
114
113
  });
115
- const messages = createTranslator(language);
114
+ let messages = createTranslator(language);
116
115
  assertRunnableConfig(config, messages, configPath);
116
+ if (shouldOpenTuiHome(parsed)) {
117
+ let tuiMode = config.defaults?.mode ?? "debate";
118
+ const tuiVersion = await getPackageVersion();
119
+ let tuiNotice;
120
+ for (;;) {
121
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion });
122
+ const tuiInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
123
+ tuiNotice = undefined;
124
+ if (!tuiInput) {
125
+ 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;
187
+ continue;
188
+ }
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);
196
+ continue;
197
+ }
198
+ else if (tuiInput.kind === "new") {
199
+ parsed.command = "new";
200
+ parsed.commandExplicit = true;
201
+ }
202
+ else {
203
+ parsed.flags.topic = tuiInput.topic;
204
+ }
205
+ parsed.flags.mode = tuiMode;
206
+ parsed.flags.renderer = "tui";
207
+ break;
208
+ }
209
+ }
117
210
  if (parsed.command === "new") {
118
211
  const selection = await runNewWizard(config, messages);
119
212
  if (!selection) {
@@ -143,6 +236,10 @@ async function main() {
143
236
  parsed.flags.files = selection.files;
144
237
  if (selection.context.length > 0)
145
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;
146
243
  }
147
244
  const topic = optionalString(parsed.flags.topic) ?? "";
148
245
  const context = await loadProjectInputs(getStringListFlag(parsed.flags.files), getStringListFlag(parsed.flags.context), process.cwd(), messages);
@@ -151,37 +248,59 @@ async function main() {
151
248
  if (!topic) {
152
249
  throw new Error(messages.common.topicRequired);
153
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;
154
257
  const options = {
258
+ mode,
155
259
  language,
156
260
  topic,
157
- agentA: resolveAgentName("agent A", parsed.flags["agent-a"], preset?.agentA, config.defaults?.agentA, messages),
158
- agentB: resolveAgentName("agent B", parsed.flags["agent-b"], preset?.agentB, config.defaults?.agentB, messages),
261
+ agentA,
262
+ agentB,
263
+ askAgents,
159
264
  turns: parseTurnsFlag(parsed.flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
160
265
  session: createSessionContext(),
161
266
  files: context.files,
162
267
  modelA: optionalString(parsed.flags["model-a"]),
163
268
  modelB: optionalString(parsed.flags["model-b"]),
164
269
  pullModels: Boolean(parsed.flags["pull-models"]),
165
- summaryAgent: optionalString(parsed.flags["summary-agent"]) ?? config.defaults?.summaryAgent,
270
+ summaryAgent: resolveSummaryAgentOption(parsed.flags["summary-agent"], config.defaults, mode),
166
271
  summaryModel: optionalString(parsed.flags["summary-model"]),
167
272
  summaryEnabled: !parsed.flags["no-summary"],
168
273
  earlyStopOnAgreement: !parsed.flags["no-early-stop"],
169
- plainOutput: Boolean(parsed.flags.plain)
274
+ plainOutput: Boolean(parsed.flags.plain || parsed.flags.terminal),
275
+ signal: debateAbortSignal()
170
276
  };
171
277
  if (parsed.flags["show-prompt"]) {
172
278
  printContextWarnings(context.warnings, messages);
173
279
  printPromptPreview(config, options, language, messages);
174
280
  return;
175
281
  }
176
- const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, messages);
282
+ const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, config.defaults?.interface, messages);
177
283
  context.warnings.forEach((warning) => renderer.warning(warning));
178
- const result = await runDebate(config, options, renderer, messages);
284
+ const result = options.mode === "ask"
285
+ ? await runAsk(config, options, renderer, messages)
286
+ : await runDebate(config, options, renderer, messages);
179
287
  const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages, result.failure);
180
288
  renderer.done(outputPath);
181
289
  if (result.failure) {
182
- process.exitCode = 1;
290
+ process.exitCode = result.failure.kind === "cancelled" ? 130 : 1;
183
291
  }
184
292
  }
293
+ function debateAbortSignal() {
294
+ const controller = new AbortController();
295
+ const abort = () => {
296
+ if (!controller.signal.aborted) {
297
+ controller.abort();
298
+ }
299
+ };
300
+ process.once("SIGINT", abort);
301
+ process.once("SIGTERM", abort);
302
+ return controller.signal;
303
+ }
185
304
  /**
186
305
  * Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
187
306
  * @param flags - Flags parsés depuis la ligne de commande.
@@ -220,6 +339,19 @@ async function runConfigCommand(flags) {
220
339
  configLanguage: config.language
221
340
  });
222
341
  const messages = createTranslator(language);
342
+ if (flags["ollama-models"]) {
343
+ await runOllamaModelsCommand(config, Boolean(flags.json));
344
+ return;
345
+ }
346
+ const setOllamaModelValue = optionalString(flags["set-ollama-model"]);
347
+ if (setOllamaModelValue !== undefined) {
348
+ await runSetOllamaModelCommand(configPath, config, setOllamaModelValue, messages);
349
+ return;
350
+ }
351
+ if (flags["sync-ollama-model"]) {
352
+ await runSyncOllamaModelCommand(configPath, config, messages);
353
+ return;
354
+ }
223
355
  if (flags["sync-agents"]) {
224
356
  const discovery = await discoverLocalTools();
225
357
  const addedAgents = syncDetectedAgents(config, discovery);
@@ -234,8 +366,18 @@ async function runConfigCommand(flags) {
234
366
  const defaultAgents = getStringListFlag(flags["set-defaults"]);
235
367
  const hasTurnsFlag = flags.turns !== undefined;
236
368
  const summaryAgentValue = optionalString(flags["summary-agent"]);
369
+ const askSummaryAgentValue = optionalString(flags["ask-summary-agent"]);
370
+ const interfaceValue = optionalString(flags.interface);
371
+ const modeValue = optionalString(flags.mode);
372
+ const askAgentsValue = getStringListFlag(flags["ask-agents"]);
237
373
  const languageValue = explicitLanguage;
238
- const changesDefaults = defaultAgents.length > 0 || hasTurnsFlag || summaryAgentValue !== undefined;
374
+ const changesDefaults = defaultAgents.length > 0
375
+ || hasTurnsFlag
376
+ || summaryAgentValue !== undefined
377
+ || askSummaryAgentValue !== undefined
378
+ || interfaceValue !== undefined
379
+ || modeValue !== undefined
380
+ || askAgentsValue.length > 0;
239
381
  if (changesDefaults || languageValue !== undefined) {
240
382
  const nextDefaults = { ...(config.defaults ?? {}) };
241
383
  if (defaultAgents.length > 0) {
@@ -260,6 +402,24 @@ async function runConfigCommand(flags) {
260
402
  nextDefaults.summaryAgent = summaryAgentValue;
261
403
  }
262
404
  }
405
+ if (askSummaryAgentValue !== undefined) {
406
+ if (isNoneValue(askSummaryAgentValue)) {
407
+ delete nextDefaults.askSummaryAgent;
408
+ }
409
+ else {
410
+ assertKnownAgent(config, askSummaryAgentValue, "defaults.askSummaryAgent", messages);
411
+ nextDefaults.askSummaryAgent = askSummaryAgentValue;
412
+ }
413
+ }
414
+ if (interfaceValue !== undefined) {
415
+ nextDefaults.interface = parseInterfaceFlag(interfaceValue, messages);
416
+ }
417
+ if (modeValue !== undefined) {
418
+ nextDefaults.mode = parseModeFlag(modeValue, messages);
419
+ }
420
+ if (askAgentsValue.length > 0) {
421
+ nextDefaults.askAgents = normalizeAskAgentsForConfig(config, askAgentsValue, messages);
422
+ }
263
423
  if (languageValue !== undefined) {
264
424
  config.language = parseLanguage(languageValue, "--language");
265
425
  }
@@ -278,6 +438,315 @@ async function runConfigCommand(flags) {
278
438
  }
279
439
  await runConfigWizard(configPath, config, messages);
280
440
  }
441
+ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
442
+ let mode = initialMode;
443
+ let notice;
444
+ let currentMessages = messages;
445
+ for (;;) {
446
+ renderTuiConfig(config, configPath, mode, currentMessages, { message: notice });
447
+ notice = undefined;
448
+ const input = await promptTuiConfigCommand(mode, currentMessages);
449
+ if (input.kind === "quit") {
450
+ return { mode, quit: true };
451
+ }
452
+ if (input.kind === "back") {
453
+ return { mode, quit: false };
454
+ }
455
+ if (input.kind === "unknown") {
456
+ notice = input.message;
457
+ continue;
458
+ }
459
+ if (input.kind === "mode") {
460
+ mode = mode === "ask" ? "debate" : "ask";
461
+ notice = mode === "ask" ? currentMessages.tui.askConfigMode : currentMessages.tui.debateConfigMode;
462
+ continue;
463
+ }
464
+ if (input.kind === "default-mode") {
465
+ config.defaults = { ...(config.defaults ?? {}), mode };
466
+ await writeExampleConfig(configPath, config);
467
+ notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
468
+ continue;
469
+ }
470
+ if (input.kind === "interface") {
471
+ config.defaults = { ...(config.defaults ?? {}), interface: input.interfaceName };
472
+ await writeExampleConfig(configPath, config);
473
+ notice = currentMessages.tui.interfaceDefault(input.interfaceName);
474
+ continue;
475
+ }
476
+ if (input.kind === "language") {
477
+ config.language = parseLanguage(input.language, "--language");
478
+ await writeExampleConfig(configPath, config);
479
+ currentMessages = createTranslator(config.language ?? DEFAULT_LANGUAGE);
480
+ notice = currentMessages.tui.languageUpdated(input.language);
481
+ continue;
482
+ }
483
+ if (input.kind === "agents") {
484
+ try {
485
+ const agentsInput = input.agents.length > 0
486
+ ? { kind: "agents", agents: input.agents }
487
+ : await promptTuiAgentsWizard(config, mode, currentMessages);
488
+ if (agentsInput.kind === "quit") {
489
+ return { mode, quit: true };
490
+ }
491
+ if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
492
+ notice = currentMessages.tui.agentsUnchanged;
493
+ continue;
494
+ }
495
+ if (mode === "ask") {
496
+ const agents = normalizeTuiAskAgents(config, agentsInput.agents, currentMessages);
497
+ config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
498
+ await writeExampleConfig(configPath, config);
499
+ notice = currentMessages.tui.askAgentsUpdated(agents.join(", "));
500
+ }
501
+ else {
502
+ const [agentA, agentB] = normalizeTuiDebateAgents(config, agentsInput.agents, currentMessages);
503
+ config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
504
+ await writeExampleConfig(configPath, config);
505
+ notice = currentMessages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
506
+ }
507
+ }
508
+ catch (error) {
509
+ notice = error instanceof Error ? error.message : String(error);
510
+ }
511
+ continue;
512
+ }
513
+ if (input.kind === "roles") {
514
+ try {
515
+ const rolesInput = input.roles.length > 0
516
+ ? { kind: "roles", roles: input.roles }
517
+ : await promptTuiRolesWizard(config, mode, currentMessages);
518
+ if (rolesInput.kind === "quit") {
519
+ return { mode, quit: true };
520
+ }
521
+ if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
522
+ notice = currentMessages.tui.rolesUnchanged;
523
+ continue;
524
+ }
525
+ notice = applyTuiRoles(config, mode, rolesInput.roles, currentMessages);
526
+ await writeExampleConfig(configPath, config);
527
+ }
528
+ catch (error) {
529
+ notice = error instanceof Error ? error.message : String(error);
530
+ }
531
+ continue;
532
+ }
533
+ if (input.kind === "turns") {
534
+ if (mode === "ask") {
535
+ notice = currentMessages.tui.askTurnsNotice;
536
+ continue;
537
+ }
538
+ try {
539
+ validateTurns(input.turns, "--turns", currentMessages);
540
+ config.defaults = { ...(config.defaults ?? {}), turns: input.turns };
541
+ await writeExampleConfig(configPath, config);
542
+ notice = currentMessages.tui.turnsUpdated(input.turns);
543
+ }
544
+ catch (error) {
545
+ notice = error instanceof Error ? error.message : String(error);
546
+ }
547
+ continue;
548
+ }
549
+ if (input.kind === "summary") {
550
+ try {
551
+ const nextDefaults = { ...(config.defaults ?? {}) };
552
+ if (input.agent !== undefined) {
553
+ assertKnownAgent(config, input.agent, mode === "ask" ? "defaults.askSummaryAgent" : "defaults.summaryAgent", currentMessages);
554
+ }
555
+ if (mode === "ask") {
556
+ if (input.agent === undefined) {
557
+ delete nextDefaults.askSummaryAgent;
558
+ notice = currentMessages.tui.askSummaryFallback;
559
+ }
560
+ else {
561
+ nextDefaults.askSummaryAgent = input.agent;
562
+ notice = currentMessages.tui.askSummaryAgent(input.agent);
563
+ }
564
+ }
565
+ else if (input.agent === undefined) {
566
+ delete nextDefaults.summaryAgent;
567
+ notice = currentMessages.tui.debateSummaryFallback;
568
+ }
569
+ else {
570
+ nextDefaults.summaryAgent = input.agent;
571
+ notice = currentMessages.tui.debateSummaryAgent(input.agent);
572
+ }
573
+ config.defaults = nextDefaults;
574
+ await writeExampleConfig(configPath, config);
575
+ }
576
+ catch (error) {
577
+ notice = error instanceof Error ? error.message : String(error);
578
+ }
579
+ }
580
+ }
581
+ }
582
+ function normalizeTuiDebateAgents(config, agents, messages) {
583
+ const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
584
+ if (unique.length !== 2) {
585
+ throw new Error(messages.tui.debateAgentsUsage);
586
+ }
587
+ assertKnownAgent(config, unique[0], "defaults.agentA", messages);
588
+ assertKnownAgent(config, unique[1], "defaults.agentB", messages);
589
+ return [unique[0], unique[1]];
590
+ }
591
+ function normalizeTuiAskAgents(config, agents, messages) {
592
+ const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
593
+ if (unique.length === 0) {
594
+ throw new Error(messages.tui.askAgentsUsage);
595
+ }
596
+ if (unique.length > MAX_ASK_AGENTS) {
597
+ throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
598
+ }
599
+ unique.forEach((agent) => assertKnownAgent(config, agent, "defaults.askAgents", messages));
600
+ return unique;
601
+ }
602
+ async function runTuiAgentsWizard(configPath, config, messages, mode, inlineAgents = []) {
603
+ try {
604
+ const agentsInput = inlineAgents.length > 0
605
+ ? { kind: "agents", agents: inlineAgents }
606
+ : await promptTuiAgentsWizard(config, mode, messages);
607
+ if (agentsInput.kind === "quit") {
608
+ return { quit: true };
609
+ }
610
+ if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
611
+ return { quit: false };
612
+ }
613
+ const notice = applyTuiAgents(config, mode, agentsInput.agents, messages);
614
+ await writeExampleConfig(configPath, config);
615
+ return { notice, quit: false };
616
+ }
617
+ catch (error) {
618
+ return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false };
619
+ }
620
+ }
621
+ async function runTuiRolesWizard(configPath, config, messages, mode, inlineRoles = []) {
622
+ try {
623
+ const rolesInput = inlineRoles.length > 0
624
+ ? { kind: "roles", roles: inlineRoles }
625
+ : await promptTuiRolesWizard(config, mode, messages);
626
+ if (rolesInput.kind === "quit") {
627
+ return { quit: true };
628
+ }
629
+ if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
630
+ return { quit: false };
631
+ }
632
+ const notice = applyTuiRoles(config, mode, rolesInput.roles, messages);
633
+ await writeExampleConfig(configPath, config);
634
+ return { notice, quit: false };
635
+ }
636
+ catch (error) {
637
+ return { notice: messages.tui.rolesError(error instanceof Error ? error.message : String(error)), quit: false };
638
+ }
639
+ }
640
+ function applyTuiAgents(config, mode, agentNames, messages) {
641
+ if (mode === "ask") {
642
+ const agents = normalizeTuiAskAgents(config, agentNames, messages);
643
+ config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
644
+ return messages.tui.askAgentsUpdated(agents.join(", "));
645
+ }
646
+ const [agentA, agentB] = normalizeTuiDebateAgents(config, agentNames, messages);
647
+ config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
648
+ return messages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
649
+ }
650
+ function applyTuiRoles(config, mode, roleNames, messages) {
651
+ const agents = activeAgentsForMode(config, mode);
652
+ if (agents.length === 0) {
653
+ throw new Error(mode === "ask" ? messages.tui.noAskAgentsConfigured : messages.tui.noDebateAgentsConfigured);
654
+ }
655
+ const roles = normalizeTuiRoles(roleNames, agents, mode, messages);
656
+ agents.forEach((agent, index) => {
657
+ config.agents[agent].role = roles[index];
658
+ });
659
+ return mode === "ask"
660
+ ? messages.tui.askRolesUpdated(roles.join(", "))
661
+ : messages.tui.debateRolesUpdated(roles.join(" <-> "));
662
+ }
663
+ function activeAgentsForMode(config, mode) {
664
+ const defaults = config.defaults ?? {};
665
+ if (mode === "ask") {
666
+ if (defaults.askAgents && defaults.askAgents.length > 0) {
667
+ return defaults.askAgents.filter((agent) => Boolean(config.agents[agent]));
668
+ }
669
+ return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
670
+ }
671
+ return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
672
+ }
673
+ function normalizeTuiRoles(roleNames, agents, mode, messages) {
674
+ const roles = roleNames.map((role) => role.trim().toLowerCase()).filter(Boolean);
675
+ const expectedCount = agents.length;
676
+ if (roles.length < expectedCount) {
677
+ const agentLabel = mode === "ask"
678
+ ? agents.join(", ")
679
+ : agents.join(" <-> ");
680
+ throw new Error(messages.tui.rolesCountError(roles.length, expectedCount, agentLabel));
681
+ }
682
+ return roles.slice(0, expectedCount).map((role) => {
683
+ if (isAgentRole(role)) {
684
+ return role;
685
+ }
686
+ throw new Error(messages.tui.unknownRole(role, VALID_AGENT_ROLES.join(", ")));
687
+ });
688
+ }
689
+ function isAgentRole(value) {
690
+ return VALID_AGENT_ROLES.includes(value);
691
+ }
692
+ const VALID_AGENT_ROLES = ["implementer", "reviewer", "architect", "scout", "critic", "summarizer"];
693
+ async function runOllamaModelsCommand(config, json) {
694
+ const discovery = await discoverLocalTools();
695
+ const agent = config.agents["ollama-local"];
696
+ const currentModel = agent?.type === "ollama" ? agent.model : null;
697
+ const payload = {
698
+ v: 1,
699
+ agent: "ollama-local",
700
+ available: discovery.ollama.available,
701
+ baseUrl: discovery.ollama.baseUrl,
702
+ currentModel,
703
+ currentModelInstalled: currentModel ? discovery.ollama.models.includes(currentModel) : false,
704
+ installedModels: discovery.ollama.models
705
+ };
706
+ if (json) {
707
+ console.log(JSON.stringify(payload, null, 2));
708
+ return;
709
+ }
710
+ console.log(`ollama-local: ${currentModel ?? "(non configuré)"}`);
711
+ console.log(`Ollama API: ${discovery.ollama.available ? "joignable" : "indisponible"} (${discovery.ollama.baseUrl})`);
712
+ console.log(`Modèles installés: ${discovery.ollama.models.length > 0 ? discovery.ollama.models.join(", ") : "(aucun)"}`);
713
+ }
714
+ async function runSetOllamaModelCommand(configPath, config, model, messages) {
715
+ const trimmed = model.trim();
716
+ if (!trimmed) {
717
+ throw new Error(messages.common.optionRequiresValue("--set-ollama-model"));
718
+ }
719
+ const discovery = await discoverLocalTools();
720
+ const agent = config.agents["ollama-local"];
721
+ if (agent?.type !== "ollama") {
722
+ throw new Error(messages.config.ollamaModelNoAgent);
723
+ }
724
+ if (!discovery.ollama.models.includes(trimmed)) {
725
+ throw new Error(messages.config.ollamaModelUnavailable(trimmed));
726
+ }
727
+ const result = setOllamaModel(config, trimmed);
728
+ await writeExampleConfig(configPath, config);
729
+ console.log(result
730
+ ? messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel)
731
+ : messages.config.ollamaModelNoChange(configPath, agent.model));
732
+ }
733
+ async function runSyncOllamaModelCommand(configPath, config, messages) {
734
+ const discovery = await discoverLocalTools();
735
+ const agent = config.agents["ollama-local"];
736
+ if (agent?.type !== "ollama") {
737
+ throw new Error(messages.config.ollamaModelNoAgent);
738
+ }
739
+ if (discovery.ollama.models.length === 0) {
740
+ throw new Error(messages.config.ollamaModelNoInstalledModels);
741
+ }
742
+ const result = syncOllamaModel(config, discovery);
743
+ if (!result) {
744
+ console.log(messages.config.ollamaModelNoChange(configPath, agent.model));
745
+ return;
746
+ }
747
+ await writeExampleConfig(configPath, config);
748
+ console.log(messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel));
749
+ }
281
750
  /**
282
751
  * Renvoie `true` si la valeur représente une désactivation explicite (ex. "none", "0", "disabled").
283
752
  * @param value - Chaîne saisie par l'utilisateur.
@@ -291,7 +760,19 @@ function isNoneValue(value) {
291
760
  * @returns Chaîne résumant la paire d'agents, le nombre de réponses et l'agent de synthèse.
292
761
  */
293
762
  function formatDefaultsForMessage(defaults, messages) {
294
- return messages.config.defaultsSummary(defaults.agentA, defaults.agentB, turnsOrDefault(defaults.turns), defaults.summaryAgent);
763
+ return messages.config.defaultsSummary(defaults.agentA, defaults.agentB, turnsOrDefault(defaults.turns), defaults.summaryAgent, defaults.askSummaryAgent, defaults.mode, defaults.askAgents, defaults.interface);
764
+ }
765
+ function normalizeAskAgentsForConfig(config, agents, messages) {
766
+ const unique = agents
767
+ .map((agent) => agent.trim())
768
+ .filter((agent, index, list) => agent && list.indexOf(agent) === index);
769
+ if (unique.length > MAX_ASK_AGENTS) {
770
+ throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
771
+ }
772
+ for (const agent of unique) {
773
+ assertKnownAgent(config, agent, "defaults.askAgents", messages);
774
+ }
775
+ return unique;
295
776
  }
296
777
  /**
297
778
  * Lève une erreur si `agentName` n'est pas déclaré dans la config.
@@ -320,38 +801,88 @@ function resolveAgentName(label, explicitValue, presetValue, defaultValue, messa
320
801
  }
321
802
  return resolved;
322
803
  }
804
+ function resolveSummaryAgentOption(explicitValue, defaults, mode) {
805
+ const explicit = optionalString(explicitValue);
806
+ if (explicit) {
807
+ return explicit;
808
+ }
809
+ if (mode === "ask") {
810
+ return defaults?.askSummaryAgent ?? defaults?.summaryAgent;
811
+ }
812
+ return defaults?.summaryAgent;
813
+ }
814
+ function parseModeFlag(value, messages) {
815
+ if (!value) {
816
+ return "debate";
817
+ }
818
+ if (value === "debate" || value === "ask") {
819
+ return value;
820
+ }
821
+ throw new Error(messages.common.unknownMode(value, "debate, ask"));
822
+ }
823
+ function parseInterfaceFlag(value, messages) {
824
+ if (!value) {
825
+ return "tui";
826
+ }
827
+ if (value === "tui" || value === "terminal") {
828
+ return value;
829
+ }
830
+ throw new Error(messages.common.unknownMode(value, "tui, terminal"));
831
+ }
832
+ function resolveAskAgents(explicitAgents, defaultAgents, fallbackAgents, messages) {
833
+ const selected = explicitAgents.length > 0
834
+ ? explicitAgents
835
+ : defaultAgents && defaultAgents.length > 0 ? defaultAgents : fallbackAgents;
836
+ const unique = selected.filter((agent, index) => agent.trim() && selected.indexOf(agent) === index);
837
+ if (unique.length > MAX_ASK_AGENTS) {
838
+ throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
839
+ }
840
+ return unique;
841
+ }
323
842
  /**
324
843
  * Affiche un aperçu du prompt du premier tour sans appeler aucun agent (flag `--show-prompt`).
325
844
  * @param config - Config chargée.
326
845
  * @param options - Options du débat résolues.
327
846
  */
328
847
  function printPromptPreview(config, options, language, messages) {
329
- const agentConfig = config.agents[options.agentA];
848
+ const previewAgent = options.mode === "ask" ? options.askAgents?.[0] ?? options.agentA : options.agentA;
849
+ const peerName = options.mode === "ask" ? "independent-agents" : options.agentB;
850
+ const agentConfig = config.agents[previewAgent];
330
851
  if (!agentConfig) {
331
- throw new Error(messages.common.unknownAgent(options.agentA));
852
+ throw new Error(messages.common.unknownAgent(previewAgent));
332
853
  }
333
854
  const prompt = formatAgentPrompt({
334
855
  topic: options.topic,
335
856
  turn: 1,
336
- totalTurns: options.turns,
337
- selfName: options.agentA,
338
- peerName: options.agentB,
857
+ totalTurns: options.mode === "ask" ? options.askAgents?.length ?? 1 : options.turns,
858
+ selfName: previewAgent,
859
+ peerName,
339
860
  selfRole: agentConfig.role,
861
+ mode: options.mode === "ask" ? "ask" : "debate",
340
862
  language: options.language,
341
863
  session: options.session,
342
864
  files: options.files,
343
865
  transcript: []
344
866
  });
345
867
  console.log(messages.preview.title);
346
- console.log(messages.preview.agent(options.agentA, agentConfig.role));
347
- console.log(messages.preview.peer(options.agentB));
868
+ console.log(messages.preview.agent(previewAgent, agentConfig.role));
869
+ console.log(messages.preview.peer(peerName));
348
870
  console.log(messages.preview.pullModels(options.pullModels));
349
- console.log(messages.preview.summary(options.summaryEnabled ? options.summaryAgent ?? options.agentB : messages.preview.disabled));
871
+ console.log(messages.preview.summary(options.summaryEnabled ? previewSummaryAgent(options) : messages.preview.disabled));
350
872
  console.log(messages.preview.interfaceLanguage(language));
351
873
  console.log("");
352
874
  console.log(prompt);
353
875
  console.log("");
354
- console.log(messages.preview.note);
876
+ console.log(options.mode === "ask" ? messages.preview.askNote : messages.preview.note);
877
+ }
878
+ function previewSummaryAgent(options) {
879
+ if (options.summaryAgent) {
880
+ return options.summaryAgent;
881
+ }
882
+ if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
883
+ return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
884
+ }
885
+ return options.agentB;
355
886
  }
356
887
  /**
357
888
  * Extrait une chaîne non vide depuis une valeur de flag, ou renvoie `undefined`.
@@ -375,7 +906,7 @@ function findRawLanguageFlag(args) {
375
906
  return undefined;
376
907
  }
377
908
  /** Liste des kinds de renderer acceptés par `--renderer`. */
378
- const SUPPORTED_RENDERERS = ["auto", "pretty", "plain", "ndjson"];
909
+ const SUPPORTED_RENDERERS = ["auto", "pretty", "plain", "tui", "ndjson"];
379
910
  /**
380
911
  * Instancie le renderer en fonction des flags CLI.
381
912
  *
@@ -387,7 +918,7 @@ const SUPPORTED_RENDERERS = ["auto", "pretty", "plain", "ndjson"];
387
918
  *
388
919
  * Lève si la valeur de `--renderer` n'est pas dans `SUPPORTED_RENDERERS`.
389
920
  */
390
- function createRendererFromFlags(flags, plainOutputFallback, messages) {
921
+ function createRendererFromFlags(flags, plainOutputFallback, defaultInterface, messages) {
391
922
  const explicit = optionalString(flags.renderer);
392
923
  if (explicit) {
393
924
  if (!SUPPORTED_RENDERERS.includes(explicit)) {
@@ -401,14 +932,41 @@ function createRendererFromFlags(flags, plainOutputFallback, messages) {
401
932
  return createConsoleRenderer(true, messages);
402
933
  case "pretty":
403
934
  return createConsoleRenderer(false, messages);
935
+ case "tui":
936
+ return createTuiRenderer(messages);
404
937
  case "auto":
405
- return createConsoleRenderer(plainOutputFallback, messages);
938
+ return createAutoRenderer(flags, plainOutputFallback, defaultInterface, messages);
406
939
  }
407
940
  }
408
941
  if (flags.json) {
409
942
  return createNdjsonRenderer();
410
943
  }
411
- return createConsoleRenderer(plainOutputFallback, messages);
944
+ if (flags.tui) {
945
+ return createTuiRenderer(messages);
946
+ }
947
+ if (flags.terminal || flags.plain || plainOutputFallback || defaultInterface === "terminal") {
948
+ return createConsoleRenderer(true, messages);
949
+ }
950
+ return createAutoRenderer(flags, plainOutputFallback, defaultInterface, messages);
951
+ }
952
+ function createAutoRenderer(flags, plainOutputFallback, defaultInterface, messages) {
953
+ if (flags.tui) {
954
+ return createTuiRenderer(messages);
955
+ }
956
+ if (flags.terminal || flags.plain || plainOutputFallback || defaultInterface === "terminal") {
957
+ return createConsoleRenderer(true, messages);
958
+ }
959
+ return process.stdout.isTTY ? createTuiRenderer(messages) : createConsoleRenderer(true, messages);
960
+ }
961
+ function shouldOpenTuiHome(parsed) {
962
+ return parsed.command === "run"
963
+ && !parsed.commandExplicit
964
+ && parsed.positionals.length === 0
965
+ && optionalString(parsed.flags.topic) === undefined
966
+ && optionalString(parsed.flags.renderer) === undefined
967
+ && parsed.flags.json !== true
968
+ && parsed.flags.plain !== true
969
+ && parsed.flags.terminal !== true;
412
970
  }
413
971
  /**
414
972
  * Exécute la commande `palabre presets`.
@@ -472,13 +1030,6 @@ async function runContextCommand(flags, positionals) {
472
1030
  console.error(`${messages.renderers.warningPrefix} ${warning}`);
473
1031
  }
474
1032
  }
475
- /** Lit la version depuis `package.json` adjacent au bundle compilé. */
476
- async function getPackageVersion() {
477
- const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
478
- const raw = await readFile(packageJsonPath, "utf8");
479
- const packageJson = JSON.parse(raw);
480
- return packageJson.version ?? "0.0.0";
481
- }
482
1033
  /**
483
1034
  * Écrit les avertissements de contexte sur `stderr`.
484
1035
  * @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
@@ -495,22 +1046,6 @@ function printContextWarnings(warnings, messages) {
495
1046
  * @param discovery - Résultat de la découverte locale des outils.
496
1047
  * @returns Noms des agents nouvellement ajoutés.
497
1048
  */
498
- function syncDetectedAgents(config, discovery) {
499
- const discoveredConfig = createConfigFromDiscovery(discovery);
500
- const missingAgents = findDetectedMissingAgents(config, discovery);
501
- for (const agentName of missingAgents) {
502
- config.agents[agentName] = discoveredConfig.agents[agentName];
503
- }
504
- return missingAgents;
505
- }
506
- /**
507
- * Renvoie les noms des agents détectés localement qui ne sont pas encore dans `config.agents`.
508
- * @param config - Config Palabre existante.
509
- * @param discovery - Résultat de la découverte locale des outils.
510
- */
511
- function findDetectedMissingAgents(config, discovery) {
512
- return detectedAgentNames(discovery).filter((agentName) => !config.agents[agentName]);
513
- }
514
1049
  /**
515
1050
  * Affiche la liste des agents déclarés avec leur type, rôle, état de détection et défauts.
516
1051
  * @param configPath - Chemin du fichier de config (affiché en en-tête).
@@ -533,7 +1068,7 @@ function printAgents(configPath, config, discovery, messages) {
533
1068
  }
534
1069
  }
535
1070
  console.log("");
536
- 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));
1071
+ 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));
537
1072
  }
538
1073
  /**
539
1074
  * Renvoie un libellé indiquant si l'agent est agent A, agent B ou agent de synthèse par défaut.
@@ -548,6 +1083,8 @@ function formatAgentDefaults(name, config, messages) {
548
1083
  labels.push(messages.agents.defaultAgentB);
549
1084
  if (config.defaults?.summaryAgent === name)
550
1085
  labels.push(messages.agents.defaultSummary);
1086
+ if (config.defaults?.askSummaryAgent === name)
1087
+ labels.push(messages.agents.defaultAskSummary);
551
1088
  return labels.join(", ");
552
1089
  }
553
1090
  /**
@@ -603,6 +1140,7 @@ function printInitDiscovery(discovery, config, messages) {
603
1140
  console.log(`- Gemini CLI: ${formatCommandDetection(discovery.gemini, messages)}`);
604
1141
  console.log(`- Antigravity CLI: ${formatCommandDetection(discovery.antigravity, messages)}`);
605
1142
  console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode, messages)}`);
1143
+ console.log(`- Mistral Vibe CLI: ${formatCommandDetection(discovery.vibe, messages)}`);
606
1144
  console.log(`- Ollama API: ${formatOllamaDetection(discovery.ollama, messages)}`);
607
1145
  console.log("");
608
1146
  console.log(config.defaults?.agentA && config.defaults.agentB