palabre 0.2.0 → 0.5.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
@@ -2,16 +2,17 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, writeExampleConfig } from "./config.js";
5
+ import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
6
6
  import { loadProjectInputs } from "./context.js";
7
7
  import { discoverLocalTools } from "./discovery.js";
8
8
  import { runDoctor } from "./doctor.js";
9
9
  import { AdapterError, formatAdapterError } from "./errors.js";
10
10
  import { runConfigWizard } from "./configWizard.js";
11
+ import { createTranslator, DEFAULT_LANGUAGE, parseLanguage, resolveLanguage } from "./i18n.js";
11
12
  import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault } from "./limits.js";
12
13
  import { formatAgentPrompt } from "./prompt.js";
13
14
  import { runNewWizard } from "./new.js";
14
- import { listPresetNames, resolvePreset } from "./presets.js";
15
+ import { listPresetNames, listPresetsWithAvailability, resolvePreset } from "./presets.js";
15
16
  import { createConsoleRenderer } from "./renderers/console.js";
16
17
  import { createNdjsonRenderer } from "./renderers/ndjson.js";
17
18
  import { runDebate } from "./orchestrator.js";
@@ -20,17 +21,20 @@ import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./up
20
21
  import { createSessionContext } from "./session.js";
21
22
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
22
23
  async function main() {
23
- const parsed = parseArgs(process.argv.slice(2));
24
+ const rawArgs = process.argv.slice(2);
25
+ const startupLanguage = resolveLanguage({ explicitLanguage: findRawLanguageFlag(rawArgs) });
26
+ const startupMessages = createTranslator(startupLanguage);
27
+ const parsed = parseArgs(rawArgs, startupMessages);
24
28
  if (parsed.command === "version" || parsed.flags.version) {
25
29
  console.log(await getPackageVersion());
26
30
  return;
27
31
  }
28
32
  if (parsed.command === "help" || parsed.flags.help) {
29
- printHelp();
33
+ printHelp(await resolveCommandMessages(parsed.flags), commandHelpTarget(parsed));
30
34
  return;
31
35
  }
32
36
  if (parsed.command === "doctor") {
33
- const result = await runDoctor(optionalString(parsed.flags.config), Boolean(parsed.flags.plain));
37
+ const result = await runDoctor(optionalString(parsed.flags.config), Boolean(parsed.flags.plain), optionalString(parsed.flags.language));
34
38
  console.log(result.output);
35
39
  process.exitCode = result.ok ? 0 : 1;
36
40
  return;
@@ -43,40 +47,69 @@ async function main() {
43
47
  await runAgentsCommand(parsed.flags);
44
48
  return;
45
49
  }
50
+ if (parsed.command === "presets" || parsed.command === "preset") {
51
+ await runPresetsCommand(parsed.flags);
52
+ return;
53
+ }
46
54
  if (parsed.command === "update") {
47
55
  const info = await getUpdateInfo(await getPackageVersion());
56
+ const updateConfigPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
57
+ const updateConfig = await configExists(updateConfigPath)
58
+ ? await loadConfig(updateConfigPath)
59
+ : undefined;
60
+ const updateLanguage = resolveLanguage({
61
+ explicitLanguage: optionalString(parsed.flags.language),
62
+ configLanguage: updateConfig?.language
63
+ });
64
+ const updateMessages = createTranslator(updateLanguage);
48
65
  if (parsed.flags.apply) {
49
- await applySourceUpdate(info);
50
- console.log("PALABRE est a jour.");
66
+ await applySourceUpdate(info, updateMessages);
67
+ console.log(updateMessages.update.upToDate);
51
68
  return;
52
69
  }
53
- console.log(formatUpdateInstructions(info));
70
+ console.log(formatUpdateInstructions(info, updateMessages));
54
71
  return;
55
72
  }
56
73
  if (parsed.command === "init" || parsed.command === "setup") {
57
74
  const initConfigPath = optionalString(parsed.flags.config) ?? (parsed.flags.local ? DEFAULT_CONFIG_PATH : GLOBAL_CONFIG_PATH);
58
75
  if (await configExists(initConfigPath)) {
59
- console.log(`${initConfigPath} existe déjà.`);
76
+ console.log(startupMessages.init.configExists(initConfigPath));
60
77
  return;
61
78
  }
62
79
  const discovery = await discoverLocalTools();
63
80
  const config = createConfigFromDiscovery(discovery);
81
+ config.language = resolveLanguage({
82
+ explicitLanguage: optionalString(parsed.flags.language),
83
+ configLanguage: config.language
84
+ });
85
+ const initMessages = createTranslator(config.language);
64
86
  await writeExampleConfig(initConfigPath, config);
65
- console.log(`${initConfigPath} créé.`);
66
- printInitDiscovery(discovery, config);
87
+ console.log(initMessages.init.configCreated(initConfigPath));
88
+ printInitDiscovery(discovery, config, initMessages);
67
89
  return;
68
90
  }
69
91
  const configPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
70
92
  if (!(await configExists(configPath))) {
71
- await writeExampleConfig(configPath);
72
- console.log(`${configPath} créé. Édite la config puis relance palabre run.`);
93
+ const config = createConfigFromDiscovery(await discoverLocalTools());
94
+ config.language = resolveLanguage({
95
+ explicitLanguage: optionalString(parsed.flags.language),
96
+ configLanguage: config.language
97
+ });
98
+ const messages = createTranslator(config.language);
99
+ await writeExampleConfig(configPath, config);
100
+ console.log(messages.init.editConfigThenRerun(configPath));
73
101
  return;
74
102
  }
75
103
  const config = await loadConfig(configPath);
104
+ const language = resolveLanguage({
105
+ explicitLanguage: optionalString(parsed.flags.language),
106
+ configLanguage: config.language
107
+ });
108
+ const messages = createTranslator(language);
76
109
  if (parsed.command === "new") {
77
- const selection = await runNewWizard(config);
110
+ const selection = await runNewWizard(config, messages);
78
111
  if (!selection) {
79
- console.log("Création de débat annulée.");
112
+ console.log(messages.new.cancelled);
80
113
  return;
81
114
  }
82
115
  parsed.flags["agent-a"] = selection.agentA;
@@ -104,17 +137,18 @@ async function main() {
104
137
  parsed.flags.context = selection.context;
105
138
  }
106
139
  const topic = optionalString(parsed.flags.topic) ?? "";
107
- const context = await loadProjectInputs(getStringListFlag(parsed.flags.files), getStringListFlag(parsed.flags.context));
140
+ const context = await loadProjectInputs(getStringListFlag(parsed.flags.files), getStringListFlag(parsed.flags.context), process.cwd(), messages);
108
141
  const presetName = optionalString(parsed.flags.preset);
109
- const preset = presetName ? resolvePreset(presetName) : undefined;
142
+ const preset = presetName ? resolvePreset(presetName, messages) : undefined;
110
143
  if (!topic) {
111
- throw new Error("Le parametre --topic/--subject est requis.");
144
+ throw new Error(messages.common.topicRequired);
112
145
  }
113
146
  const options = {
147
+ language,
114
148
  topic,
115
- agentA: resolveAgentName("agent A", parsed.flags["agent-a"], preset?.agentA, config.defaults?.agentA),
116
- agentB: resolveAgentName("agent B", parsed.flags["agent-b"], preset?.agentB, config.defaults?.agentB),
117
- turns: parseTurnsFlag(parsed.flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns"),
149
+ agentA: resolveAgentName("agent A", parsed.flags["agent-a"], preset?.agentA, config.defaults?.agentA, messages),
150
+ agentB: resolveAgentName("agent B", parsed.flags["agent-b"], preset?.agentB, config.defaults?.agentB, messages),
151
+ turns: parseTurnsFlag(parsed.flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
118
152
  session: createSessionContext(),
119
153
  files: context.files,
120
154
  modelA: optionalString(parsed.flags["model-a"]),
@@ -127,14 +161,14 @@ async function main() {
127
161
  plainOutput: Boolean(parsed.flags.plain)
128
162
  };
129
163
  if (parsed.flags["show-prompt"]) {
130
- printContextWarnings(context.warnings);
131
- printPromptPreview(config, options);
164
+ printContextWarnings(context.warnings, messages);
165
+ printPromptPreview(config, options, language, messages);
132
166
  return;
133
167
  }
134
- const renderer = createRendererFromFlags(parsed.flags, options.plainOutput);
168
+ const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, messages);
135
169
  context.warnings.forEach((warning) => renderer.warning(warning));
136
- const result = await runDebate(config, options, renderer);
137
- const outputPath = await writeDebateMarkdown(config.outputDir ?? ".", result.options, result.messages, result.summary, result.stopReason);
170
+ const result = await runDebate(config, options, renderer, messages);
171
+ const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages);
138
172
  renderer.done(outputPath);
139
173
  }
140
174
  /**
@@ -144,11 +178,17 @@ async function main() {
144
178
  async function runAgentsCommand(flags) {
145
179
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
146
180
  if (!(await configExists(configPath))) {
147
- throw new Error("Aucune config trouvée. Lance `palabre init`, puis `palabre agents`.");
181
+ const messages = createTranslator(resolveLanguage({ explicitLanguage: optionalString(flags.language) }));
182
+ throw new Error(messages.agents.noConfig);
148
183
  }
149
184
  const config = await loadConfig(configPath);
185
+ const language = resolveLanguage({
186
+ explicitLanguage: optionalString(flags.language),
187
+ configLanguage: config.language
188
+ });
189
+ const messages = createTranslator(language);
150
190
  const discovery = await discoverLocalTools();
151
- printAgents(configPath, config, discovery);
191
+ printAgents(configPath, config, discovery, messages);
152
192
  }
153
193
  /**
154
194
  * Exécute la commande `config` : wizard interactif ou mise à jour directe des paramètres par défaut.
@@ -156,62 +196,76 @@ async function runAgentsCommand(flags) {
156
196
  */
157
197
  async function runConfigCommand(flags) {
158
198
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
199
+ const explicitLanguage = optionalString(flags.language);
159
200
  if (!(await configExists(configPath))) {
201
+ const messages = createTranslator(resolveLanguage({ explicitLanguage }));
160
202
  await writeExampleConfig(configPath);
161
- console.log(`${configPath} créé. Édite la config puis relance palabre config.`);
203
+ console.log(messages.config.createdForConfig(configPath));
162
204
  return;
163
205
  }
164
206
  const config = await loadConfig(configPath);
207
+ const language = resolveLanguage({
208
+ explicitLanguage,
209
+ configLanguage: config.language
210
+ });
211
+ const messages = createTranslator(language);
165
212
  if (flags["sync-agents"]) {
166
213
  const discovery = await discoverLocalTools();
167
214
  const addedAgents = syncDetectedAgents(config, discovery);
168
215
  if (addedAgents.length === 0) {
169
- console.log(`Aucun agent détecté manquant dans ${configPath}.`);
216
+ console.log(messages.config.syncNoMissing(configPath));
170
217
  return;
171
218
  }
172
219
  await writeExampleConfig(configPath, config);
173
- console.log(`Agents ajoutés dans ${configPath}: ${addedAgents.join(", ")}.`);
220
+ console.log(messages.config.syncAdded(configPath, addedAgents.join(", ")));
174
221
  return;
175
222
  }
176
223
  const defaultAgents = getStringListFlag(flags["set-defaults"]);
177
224
  const hasTurnsFlag = flags.turns !== undefined;
178
225
  const summaryAgentValue = optionalString(flags["summary-agent"]);
179
- if (defaultAgents.length > 0 || hasTurnsFlag || summaryAgentValue !== undefined) {
226
+ const languageValue = explicitLanguage;
227
+ const changesDefaults = defaultAgents.length > 0 || hasTurnsFlag || summaryAgentValue !== undefined;
228
+ if (changesDefaults || languageValue !== undefined) {
180
229
  const nextDefaults = { ...(config.defaults ?? {}) };
181
230
  if (defaultAgents.length > 0) {
182
231
  const [agentA, agentB] = defaultAgents;
183
232
  if (!agentA || !agentB) {
184
- throw new Error("L'option --set-defaults attend deux agents: --set-defaults <agentA> <agentB>.");
233
+ throw new Error(messages.common.setDefaultsRequiresTwo);
185
234
  }
186
- assertKnownAgent(config, agentA, "defaults.agentA");
187
- assertKnownAgent(config, agentB, "defaults.agentB");
235
+ assertKnownAgent(config, agentA, "defaults.agentA", messages);
236
+ assertKnownAgent(config, agentB, "defaults.agentB", messages);
188
237
  nextDefaults.agentA = agentA;
189
238
  nextDefaults.agentB = agentB;
190
239
  }
191
240
  if (hasTurnsFlag) {
192
- nextDefaults.turns = parseTurnsFlag(flags.turns, nextDefaults.turns ?? DEFAULT_TURNS, "--turns");
241
+ nextDefaults.turns = parseTurnsFlag(flags.turns, nextDefaults.turns ?? DEFAULT_TURNS, "--turns", messages);
193
242
  }
194
243
  if (summaryAgentValue !== undefined) {
195
244
  if (isNoneValue(summaryAgentValue)) {
196
245
  delete nextDefaults.summaryAgent;
197
246
  }
198
247
  else {
199
- assertKnownAgent(config, summaryAgentValue, "defaults.summaryAgent");
248
+ assertKnownAgent(config, summaryAgentValue, "defaults.summaryAgent", messages);
200
249
  nextDefaults.summaryAgent = summaryAgentValue;
201
250
  }
202
251
  }
203
- config.defaults = nextDefaults;
252
+ if (languageValue !== undefined) {
253
+ config.language = parseLanguage(languageValue, "--language");
254
+ }
255
+ if (changesDefaults) {
256
+ config.defaults = nextDefaults;
257
+ }
204
258
  await writeExampleConfig(configPath, config);
205
- console.log(`Paramètres par défaut mis à jour dans ${configPath}: ${formatDefaultsForMessage(config.defaults)}.`);
259
+ console.log(messages.config.updated(configPath, formatDefaultsForMessage(config.defaults ?? {}, messages), config.language ?? DEFAULT_LANGUAGE));
206
260
  return;
207
261
  }
208
262
  if (flags["clear-defaults"]) {
209
263
  delete config.defaults;
210
264
  await writeExampleConfig(configPath, config);
211
- console.log(`Paramètres par défaut supprimés dans ${configPath}. Utilise maintenant un preset ou --agent-a/--agent-b pour lancer un débat.`);
265
+ console.log(messages.config.cleared(configPath));
212
266
  return;
213
267
  }
214
- await runConfigWizard(configPath, config);
268
+ await runConfigWizard(configPath, config, messages);
215
269
  }
216
270
  /**
217
271
  * Renvoie `true` si la valeur représente une désactivation explicite (ex. "none", "0", "disabled").
@@ -225,12 +279,8 @@ function isNoneValue(value) {
225
279
  * @param defaults - Objet `defaults` de la config Palabre.
226
280
  * @returns Chaîne résumant la paire d'agents, le nombre de réponses et l'agent de synthèse.
227
281
  */
228
- function formatDefaultsForMessage(defaults) {
229
- const pair = defaults.agentA && defaults.agentB
230
- ? `agents: ${defaults.agentA} <-> ${defaults.agentB}`
231
- : "agents: non définis";
232
- const summary = defaults.summaryAgent ? `synthèse: ${defaults.summaryAgent}` : "synthèse: agent B";
233
- return `${pair}, réponses: ${turnsOrDefault(defaults.turns)}, ${summary}`;
282
+ function formatDefaultsForMessage(defaults, messages) {
283
+ return messages.config.defaultsSummary(defaults.agentA, defaults.agentB, turnsOrDefault(defaults.turns), defaults.summaryAgent);
234
284
  }
235
285
  /**
236
286
  * Lève une erreur si `agentName` n'est pas déclaré dans la config.
@@ -238,9 +288,9 @@ function formatDefaultsForMessage(defaults) {
238
288
  * @param agentName - Nom de l'agent à vérifier.
239
289
  * @param fieldName - Nom du champ (utilisé dans le message d'erreur).
240
290
  */
241
- function assertKnownAgent(config, agentName, fieldName) {
291
+ function assertKnownAgent(config, agentName, fieldName, messages) {
242
292
  if (!config.agents[agentName]) {
243
- throw new Error(`Agent inconnu pour ${fieldName}: ${agentName}. Agents disponibles: ${Object.keys(config.agents).join(", ")}.`);
293
+ throw new Error(messages.common.unknownAgentForField(fieldName, agentName, Object.keys(config.agents).join(", ")));
244
294
  }
245
295
  }
246
296
  /**
@@ -252,10 +302,10 @@ function assertKnownAgent(config, agentName, fieldName) {
252
302
  * @param defaultValue - Valeur issue des défauts de la config.
253
303
  * @returns Nom de l'agent résolu.
254
304
  */
255
- function resolveAgentName(label, explicitValue, presetValue, defaultValue) {
305
+ function resolveAgentName(label, explicitValue, presetValue, defaultValue, messages) {
256
306
  const resolved = optionalString(explicitValue) ?? presetValue ?? defaultValue;
257
307
  if (!resolved) {
258
- throw new Error(`Aucun ${label} défini. Utilise --agent-a/--agent-b, un preset, ou lance palabre init pour définir defaults.agentA/defaults.agentB.`);
308
+ throw new Error(messages.common.noAgentDefined(label));
259
309
  }
260
310
  return resolved;
261
311
  }
@@ -264,30 +314,33 @@ function resolveAgentName(label, explicitValue, presetValue, defaultValue) {
264
314
  * @param config - Config chargée.
265
315
  * @param options - Options du débat résolues.
266
316
  */
267
- function printPromptPreview(config, options) {
317
+ function printPromptPreview(config, options, language, messages) {
268
318
  const agentConfig = config.agents[options.agentA];
269
319
  if (!agentConfig) {
270
- throw new Error(`Agent inconnu: ${options.agentA}`);
320
+ throw new Error(messages.common.unknownAgent(options.agentA));
271
321
  }
272
322
  const prompt = formatAgentPrompt({
273
323
  topic: options.topic,
274
324
  turn: 1,
325
+ totalTurns: options.turns,
275
326
  selfName: options.agentA,
276
327
  peerName: options.agentB,
277
328
  selfRole: agentConfig.role,
329
+ language: options.language,
278
330
  session: options.session,
279
331
  files: options.files,
280
332
  transcript: []
281
333
  });
282
- console.log(`# Prompt preview`);
283
- console.log(`Agent: ${options.agentA} (${agentConfig.role})`);
284
- console.log(`Peer: ${options.agentB}`);
285
- console.log(`Pull missing Ollama models: ${options.pullModels ? "yes" : "no"}`);
286
- console.log(`Summary: ${options.summaryEnabled ? options.summaryAgent ?? options.agentB : "disabled"}`);
334
+ console.log(messages.preview.title);
335
+ console.log(messages.preview.agent(options.agentA, agentConfig.role));
336
+ console.log(messages.preview.peer(options.agentB));
337
+ console.log(messages.preview.pullModels(options.pullModels));
338
+ console.log(messages.preview.summary(options.summaryEnabled ? options.summaryAgent ?? options.agentB : messages.preview.disabled));
339
+ console.log(messages.preview.interfaceLanguage(language));
287
340
  console.log("");
288
341
  console.log(prompt);
289
342
  console.log("");
290
- console.log("Note: seuls les prompts du premier tour sont exacts sans exécuter les agents. Les tours suivants incluent le transcript réel.");
343
+ console.log(messages.preview.note);
291
344
  }
292
345
  /**
293
346
  * Extrait une chaîne non vide depuis une valeur de flag, ou renvoie `undefined`.
@@ -296,6 +349,20 @@ function printPromptPreview(config, options) {
296
349
  function optionalString(value) {
297
350
  return typeof value === "string" && value.trim() ? value : undefined;
298
351
  }
352
+ /**
353
+ * Pré-lit seulement `--language`/`--lang` dans les arguments bruts pour localiser
354
+ * les erreurs qui peuvent survenir avant le parsing complet ou le chargement de config.
355
+ */
356
+ function findRawLanguageFlag(args) {
357
+ for (let index = 0; index < args.length; index += 1) {
358
+ const value = args[index];
359
+ if (value === "--language" || value === "--lang") {
360
+ const next = args[index + 1];
361
+ return next && !next.startsWith("-") ? next : undefined;
362
+ }
363
+ }
364
+ return undefined;
365
+ }
299
366
  /** Liste des kinds de renderer acceptés par `--renderer`. */
300
367
  const SUPPORTED_RENDERERS = ["auto", "pretty", "plain", "ndjson"];
301
368
  /**
@@ -309,28 +376,65 @@ const SUPPORTED_RENDERERS = ["auto", "pretty", "plain", "ndjson"];
309
376
  *
310
377
  * Lève si la valeur de `--renderer` n'est pas dans `SUPPORTED_RENDERERS`.
311
378
  */
312
- function createRendererFromFlags(flags, plainOutputFallback) {
379
+ function createRendererFromFlags(flags, plainOutputFallback, messages) {
313
380
  const explicit = optionalString(flags.renderer);
314
381
  if (explicit) {
315
382
  if (!SUPPORTED_RENDERERS.includes(explicit)) {
316
- throw new Error(`Renderer inconnu: ${explicit}. Valeurs supportées: ${SUPPORTED_RENDERERS.join(", ")}.`);
383
+ throw new Error(messages.common.unknownRenderer(explicit, SUPPORTED_RENDERERS.join(", ")));
317
384
  }
318
385
  const kind = explicit;
319
386
  switch (kind) {
320
387
  case "ndjson":
321
388
  return createNdjsonRenderer();
322
389
  case "plain":
323
- return createConsoleRenderer(true);
390
+ return createConsoleRenderer(true, messages);
324
391
  case "pretty":
325
- return createConsoleRenderer(false);
392
+ return createConsoleRenderer(false, messages);
326
393
  case "auto":
327
- return createConsoleRenderer(plainOutputFallback);
394
+ return createConsoleRenderer(plainOutputFallback, messages);
328
395
  }
329
396
  }
330
397
  if (flags.json) {
331
398
  return createNdjsonRenderer();
332
399
  }
333
- return createConsoleRenderer(plainOutputFallback);
400
+ return createConsoleRenderer(plainOutputFallback, messages);
401
+ }
402
+ /**
403
+ * Exécute la commande `palabre presets`.
404
+ *
405
+ * Sortie humaine par défaut (liste alignée), ou JSON avec `--json` pour les
406
+ * intégrations (extension VS Code, scripts shell). Le schéma JSON est versionné
407
+ * via le champ `v` au cas où on enrichirait plus tard (ex : description par
408
+ * preset, tags premium/local).
409
+ *
410
+ * @param flags - Flags parsés depuis la ligne de commande.
411
+ */
412
+ async function runPresetsCommand(flags) {
413
+ const discovery = await discoverLocalTools();
414
+ const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
415
+ const config = await configExists(configPath)
416
+ ? await loadConfig(configPath)
417
+ : createConfigFromDiscovery(discovery);
418
+ const language = resolveLanguage({
419
+ explicitLanguage: optionalString(flags.language),
420
+ configLanguage: config.language
421
+ });
422
+ const messages = createTranslator(language);
423
+ const presets = listPresetsWithAvailability(config, discovery, messages);
424
+ if (flags.json) {
425
+ process.stdout.write(JSON.stringify({ v: 1, presets }) + "\n");
426
+ return;
427
+ }
428
+ console.log(messages.presets.title);
429
+ console.log("");
430
+ for (const preset of presets) {
431
+ const status = preset.available
432
+ ? messages.presets.available
433
+ : messages.presets.unavailable(preset.unavailableReasons.join("; "));
434
+ console.log(` ${preset.name.padEnd(20)} ${preset.agentA} <-> ${preset.agentB} ${status}`);
435
+ }
436
+ console.log("");
437
+ console.log(messages.presets.total(presets.length));
334
438
  }
335
439
  /**
336
440
  * Parse `process.argv` en une structure typée `ParsedArgs`.
@@ -339,22 +443,27 @@ function createRendererFromFlags(flags, plainOutputFallback) {
339
443
  * @param args - Tableau d'arguments (généralement `process.argv.slice(2)`).
340
444
  * @returns Commande détectée, indicateur d'explicitation et map de flags.
341
445
  */
342
- function parseArgs(args) {
446
+ function parseArgs(args, messages) {
343
447
  const flags = {};
344
448
  let command = "run";
345
449
  let commandExplicit = false;
346
450
  const positionals = [];
347
- const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents"]);
451
+ const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents", "preset", "presets"]);
348
452
  const presets = new Set(listPresetNames());
349
453
  for (let index = 0; index < args.length; index += 1) {
350
454
  const value = args[index];
455
+ if (!value.startsWith("-") && !commandExplicit && positionals.length === 0 && commands.has(value)) {
456
+ command = value;
457
+ commandExplicit = true;
458
+ continue;
459
+ }
351
460
  if (!value.startsWith("-") && index === 0) {
352
461
  if (commands.has(value)) {
353
462
  command = value;
354
463
  commandExplicit = true;
355
464
  }
356
465
  else if (isLikelyCommandTypo(value, commands)) {
357
- throw new Error(`Commande inconnue: ${value}. Commandes disponibles: ${Array.from(commands).join(", ")}.`);
466
+ throw new Error(messages.common.unknownCommand(value, Array.from(commands).join(", ")));
358
467
  }
359
468
  else {
360
469
  positionals.push(value);
@@ -381,7 +490,7 @@ function parseArgs(args) {
381
490
  if (value === "-s") {
382
491
  const next = args[index + 1];
383
492
  if (!next || next.startsWith("-")) {
384
- throw new Error("L'option -s attend une valeur.");
493
+ throw new Error(messages.common.optionRequiresValue("-s"));
385
494
  }
386
495
  flags.topic = next;
387
496
  index += 1;
@@ -390,7 +499,7 @@ function parseArgs(args) {
390
499
  if (value === "-t") {
391
500
  const next = args[index + 1];
392
501
  if (!next || next.startsWith("-")) {
393
- throw new Error("L'option -t attend une valeur.");
502
+ throw new Error(messages.common.optionRequiresValue("-t"));
394
503
  }
395
504
  flags.turns = next;
396
505
  index += 1;
@@ -406,7 +515,7 @@ function parseArgs(args) {
406
515
  index += 1;
407
516
  }
408
517
  if (values.length !== 2) {
409
- throw new Error("L'option --set-defaults attend deux agents: --set-defaults <agentA> <agentB>.");
518
+ throw new Error(messages.common.setDefaultsRequiresTwo);
410
519
  }
411
520
  flags[key] = values;
412
521
  continue;
@@ -423,7 +532,7 @@ function parseArgs(args) {
423
532
  const next = args[index + 1];
424
533
  if (!next || next.startsWith("-")) {
425
534
  if (requiresFlagValue(key)) {
426
- throw new Error(`L'option --${rawKey} attend une valeur.`);
535
+ throw new Error(messages.common.optionRequiresValue(`--${rawKey}`));
427
536
  }
428
537
  flags[key] = true;
429
538
  }
@@ -434,7 +543,7 @@ function parseArgs(args) {
434
543
  }
435
544
  }
436
545
  if (command === "run") {
437
- applyRunPositionals(positionals, flags, presets, commandExplicit);
546
+ applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages);
438
547
  }
439
548
  return { command, commandExplicit, flags };
440
549
  }
@@ -482,7 +591,7 @@ function levenshteinDistance(left, right) {
482
591
  * @param presets - Ensemble des noms de presets valides.
483
592
  * @param commandExplicit - `true` si l'utilisateur a tapé `palabre run` explicitement.
484
593
  */
485
- function applyRunPositionals(positionals, flags, presets, commandExplicit) {
594
+ function applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages) {
486
595
  if (positionals.length === 0) {
487
596
  return;
488
597
  }
@@ -495,7 +604,10 @@ function applyRunPositionals(positionals, flags, presets, commandExplicit) {
495
604
  return;
496
605
  }
497
606
  if (!commandExplicit && positionals.length === 1 && !positionals[0]?.includes(" ")) {
498
- throw new Error(`Commande inconnue ou sujet ambigu: ${positionals[0]}. Utilise -s "${positionals[0]}" pour un sujet en un mot, ou palabre help pour voir les commandes.`);
607
+ if (isLikelyCommandTypo(positionals[0], commands)) {
608
+ throw new Error(messages.common.unknownCommand(positionals[0], Array.from(commands).join(", ")));
609
+ }
610
+ throw new Error(messages.common.ambiguousSubject(positionals[0]));
499
611
  }
500
612
  flags.topic ??= positionals.join(" ");
501
613
  }
@@ -505,6 +617,7 @@ function applyRunPositionals(positionals, flags, presets, commandExplicit) {
505
617
  */
506
618
  function normalizeFlagName(value) {
507
619
  const aliases = {
620
+ lang: "language",
508
621
  s: "topic",
509
622
  subject: "topic",
510
623
  t: "turns"
@@ -520,6 +633,7 @@ function requiresFlagValue(value) {
520
633
  "agent-a",
521
634
  "agent-b",
522
635
  "config",
636
+ "language",
523
637
  "model-a",
524
638
  "model-b",
525
639
  "preset",
@@ -555,9 +669,9 @@ function getStringListFlag(value) {
555
669
  * Écrit les avertissements de contexte sur `stderr`.
556
670
  * @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
557
671
  */
558
- function printContextWarnings(warnings) {
672
+ function printContextWarnings(warnings, messages) {
559
673
  for (const warning of warnings) {
560
- process.stderr.write(`Warning: ${warning}\n`);
674
+ process.stderr.write(`${messages.renderers.warningPrefix} ${warning}\n`);
561
675
  }
562
676
  }
563
677
  /**
@@ -596,15 +710,15 @@ function findDetectedMissingAgents(config, discovery) {
596
710
  * @param config - Config Palabre chargée.
597
711
  * @param discovery - Résultat de la découverte locale des outils.
598
712
  */
599
- function printAgents(configPath, config, discovery) {
713
+ function printAgents(configPath, config, discovery, messages) {
600
714
  const entries = Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right));
601
- console.log(`Config: ${configPath}`);
715
+ console.log(messages.agents.config(configPath));
602
716
  console.log("");
603
- console.log("Agents déclarés:");
717
+ console.log(messages.agents.title);
604
718
  for (const [name, agentConfig] of entries) {
605
- const status = formatAgentDetection(name, agentConfig, discovery);
606
- const defaults = formatAgentDefaults(name, config);
607
- const details = formatAgentDetails(agentConfig);
719
+ const status = formatAgentDetection(name, agentConfig, discovery, messages);
720
+ const defaults = formatAgentDefaults(name, config, messages);
721
+ const details = formatAgentDetails(agentConfig, messages);
608
722
  const suffix = defaults ? ` | ${defaults}` : "";
609
723
  console.log(`- ${name.padEnd(13)} ${`${agentConfig.type}/${agentConfig.role}`.padEnd(18)} ${status}${suffix}`);
610
724
  if (details) {
@@ -612,32 +726,32 @@ function printAgents(configPath, config, discovery) {
612
726
  }
613
727
  }
614
728
  console.log("");
615
- console.log(`Défauts: ${config.defaults?.agentA ?? "aucun"} <-> ${config.defaults?.agentB ?? "aucun"}, réponses: ${turnsOrDefault(config.defaults?.turns)}, synthèse: ${config.defaults?.summaryAgent ?? "agent B"}`);
729
+ 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));
616
730
  }
617
731
  /**
618
732
  * Renvoie un libellé indiquant si l'agent est agent A, agent B ou agent de synthèse par défaut.
619
733
  * @param name - Nom de l'agent.
620
734
  * @param config - Config Palabre contenant les défauts.
621
735
  */
622
- function formatAgentDefaults(name, config) {
736
+ function formatAgentDefaults(name, config, messages) {
623
737
  const labels = [];
624
738
  if (config.defaults?.agentA === name)
625
- labels.push("agent A par défaut");
739
+ labels.push(messages.agents.defaultAgentA);
626
740
  if (config.defaults?.agentB === name)
627
- labels.push("agent B par défaut");
741
+ labels.push(messages.agents.defaultAgentB);
628
742
  if (config.defaults?.summaryAgent === name)
629
- labels.push("synthèse par défaut");
743
+ labels.push(messages.agents.defaultSummary);
630
744
  return labels.join(", ");
631
745
  }
632
746
  /**
633
747
  * Renvoie une ligne de détails pour un agent : commande CLI ou modèle Ollama.
634
748
  * @param agentConfig - Configuration de l'agent.
635
749
  */
636
- function formatAgentDetails(agentConfig) {
750
+ function formatAgentDetails(agentConfig, messages) {
637
751
  if (agentConfig.type === "ollama") {
638
- return `modèle: ${agentConfig.model}`;
752
+ return messages.agents.model(agentConfig.model);
639
753
  }
640
- return `commande: ${agentConfig.command}${agentConfig.model ? ` | modèle: ${agentConfig.model}` : ""}`;
754
+ return messages.agents.command(agentConfig.command, agentConfig.model);
641
755
  }
642
756
  /**
643
757
  * Renvoie le statut de détection d'un agent sous forme de chaîne lisible.
@@ -646,17 +760,17 @@ function formatAgentDetails(agentConfig) {
646
760
  * @param agentConfig - Configuration de l'agent.
647
761
  * @param discovery - Résultat de la découverte locale des outils.
648
762
  */
649
- function formatAgentDetection(name, agentConfig, discovery) {
763
+ function formatAgentDetection(name, agentConfig, discovery, messages) {
650
764
  if (agentConfig.type === "ollama") {
651
765
  if (!discovery.ollama.available) {
652
- return discovery.ollama.commandAvailable ? "Ollama non joignable" : "Ollama non détecté";
766
+ return discovery.ollama.commandAvailable ? messages.agents.ollamaUnreachable : messages.agents.ollamaNotDetected;
653
767
  }
654
768
  return discovery.ollama.models.includes(agentConfig.model)
655
- ? "détecté"
656
- : `modèle absent (${agentConfig.model})`;
769
+ ? messages.agents.detected()
770
+ : messages.agents.missingModel(agentConfig.model);
657
771
  }
658
772
  const detection = cliDetectionForAgent(name, agentConfig, discovery);
659
- return detection.available ? `détecté (${detection.command})` : "non détecté";
773
+ return detection.available ? messages.agents.detected(detection.command) : messages.agents.notDetected;
660
774
  }
661
775
  /**
662
776
  * Résout l'entrée de détection correspondant à un agent CLI dans le résultat de découverte.
@@ -689,177 +803,108 @@ function normalizeCommandName(command) {
689
803
  * @param discovery - Résultat de la découverte locale des outils.
690
804
  * @param config - Config générée à partir de la découverte.
691
805
  */
692
- function printInitDiscovery(discovery, config) {
806
+ function printInitDiscovery(discovery, config, messages) {
693
807
  console.log("");
694
- console.log("Détection locale:");
695
- console.log(`- Codex CLI: ${formatCommandDetection(discovery.codex)}`);
696
- console.log(`- Claude CLI: ${formatCommandDetection(discovery.claude)}`);
697
- console.log(`- Gemini CLI: ${formatCommandDetection(discovery.gemini)}`);
698
- console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode)}`);
699
- console.log(`- Ollama API: ${formatOllamaDetection(discovery.ollama)}`);
808
+ console.log(messages.init.localDetectionTitle);
809
+ console.log(`- Codex CLI: ${formatCommandDetection(discovery.codex, messages)}`);
810
+ console.log(`- Claude CLI: ${formatCommandDetection(discovery.claude, messages)}`);
811
+ console.log(`- Gemini CLI: ${formatCommandDetection(discovery.gemini, messages)}`);
812
+ console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode, messages)}`);
813
+ console.log(`- Ollama API: ${formatOllamaDetection(discovery.ollama, messages)}`);
700
814
  console.log("");
701
- console.log(`Défauts: ${config.defaults?.agentA ?? "codex"} <-> ${config.defaults?.agentB ?? "ollama-local"}`);
815
+ console.log(config.defaults?.agentA && config.defaults.agentB
816
+ ? messages.init.defaults(config.defaults.agentA, config.defaults.agentB)
817
+ : messages.init.noDefaultPair(formatDetectedAgentSummary(discovery, config.language ?? DEFAULT_LANGUAGE)));
818
+ console.log(messages.init.languageHint(config.language ?? DEFAULT_LANGUAGE));
819
+ }
820
+ function formatDetectedAgentSummary(discovery, language) {
821
+ const names = [
822
+ discovery.codex.available ? "codex" : undefined,
823
+ discovery.claude.available ? "claude" : undefined,
824
+ discovery.gemini.available ? "gemini" : undefined,
825
+ discovery.opencode.available ? "opencode" : undefined,
826
+ discovery.ollama.available ? "ollama-local" : undefined
827
+ ].filter((name) => Boolean(name));
828
+ if (names.length === 0) {
829
+ return language === "en" ? "no agent detected" : "aucun agent détecté";
830
+ }
831
+ if (names.length === 1) {
832
+ return language === "en"
833
+ ? `only one agent detected (${names[0]})`
834
+ : `un seul agent détecté (${names[0]})`;
835
+ }
836
+ return language === "en"
837
+ ? `no usable pair detected among ${names.join(", ")}`
838
+ : `aucune paire utilisable détectée parmi ${names.join(", ")}`;
702
839
  }
703
840
  /**
704
841
  * Formate le statut de détection d'un outil CLI (disponible ou non).
705
842
  * @param detection - Résultat de détection d'un outil CLI.
706
843
  */
707
- function formatCommandDetection(detection) {
844
+ function formatCommandDetection(detection, messages) {
708
845
  return detection.available
709
- ? `détecté (${detection.command})`
710
- : "non détecté";
846
+ ? messages.init.commandDetected(detection.command)
847
+ : messages.init.commandMissing;
711
848
  }
712
849
  /**
713
850
  * Formate le statut de détection d'Ollama : commande absente, serveur injoignable ou modèles disponibles.
714
851
  * @param detection - Résultat de détection d'Ollama.
715
852
  */
716
- function formatOllamaDetection(detection) {
853
+ function formatOllamaDetection(detection, messages) {
717
854
  if (!detection.available) {
718
855
  return detection.commandAvailable
719
- ? `serveur non joignable (${detection.baseUrl})`
720
- : "non détecté";
856
+ ? messages.init.ollamaServerUnreachable(detection.baseUrl)
857
+ : messages.init.ollamaMissing;
721
858
  }
722
859
  const modelCount = detection.models.length;
723
- return `détectée (${modelCount} modèle${modelCount > 1 ? "s" : ""})`;
860
+ return messages.init.ollamaDetected(modelCount);
724
861
  }
725
862
  /** Affiche le texte d'aide complet sur `stdout`. */
726
- function printHelp() {
727
- console.log(`
728
- PALABRE
729
- _____________________________________________
730
-
731
- Usage rapide:
732
-
733
- palabre init
734
- Crée une config globale et détecte les agents AI disponibles sur la machine.
735
-
736
- palabre agents
737
- Affiche les agents déclarés dans la config.
738
-
739
- palabre config
740
- Assistant pour définir ou supprimer les paramètres par défaut.
741
-
742
- palabre new
743
- Assistant interactif pour choisir les agents, le sujet et les options.
744
-
745
- palabre claude-gemini "Sujet" -t 4
746
- Lance avec un preset et un sujet positionnel.
747
-
748
- palabre "Sujet"
749
- Lance le débat avec paramètres par défaut de la config.
750
-
751
- _____________________________________________
752
-
753
-
754
- Commandes:
755
-
756
- palabre init [--local]
757
- Crée une config locale et détecte Codex, Claude, Gemini, OpenCode et Ollama.
758
-
759
- palabre agents [--config <path>]
760
- Liste les agents déclarés dans la config et leur détection locale.
761
-
762
- palabre config
763
- Assistant pour définir ou supprimer les paramètres par défaut.
764
-
765
- palabre config --set-defaults <agentA> <agentB> [-t <n>] [--summary-agent <name>]
766
- Définit les agents par défaut, et optionnellement les réponses et la synthèse.
767
-
768
- palabre config -t <n>
769
- Définit seulement le nombre de réponses par défaut.
770
-
771
- palabre config --summary-agent <name|none>
772
- Définit ou retire seulement l'agent de synthèse par défaut.
773
-
774
- palabre config --clear-defaults
775
- Supprime les paramètres par défaut.
776
-
777
- palabre doctor [--config <path>]
778
- Vérifie la config et les outils locaux.
779
-
780
- palabre update [--apply]
781
- Affiche ou exécute les étapes de mise à jour d'un checkout git.
782
-
783
- palabre help
784
- Affiche cette aide. Identique à -h ou --help.
785
-
786
- palabre version
787
- Affiche la version. Identique à -v ou --version.
788
-
789
- _____________________________________________
790
-
791
-
792
- Notation:
793
-
794
- [option] signifie facultatif. Ne tape pas les crochets.
795
- <valeur> signifie qu'il faut remplacer ce texte par ta valeur.
796
-
797
- Options générales:
798
-
799
- -h, --help Affiche cette aide
800
- -v, --version Affiche la version
801
- -a, --agents Liste les agents. Identique à palabre agents
802
- --config <path> Chemin vers un fichier de config explicite
803
- --plain Utilise le rendu console simple sans habillage TUI
804
- --json Émet un événement NDJSON par ligne sur stdout (alias de --renderer ndjson)
805
- --renderer <kind> Force le renderer : auto | pretty | plain | ndjson
806
-
807
- Sujet et lancement:
808
-
809
- -s, --subject <text> Sujet du débat, option recommandée
810
- --topic <text> Alias compatible de --subject
811
- --agent-a <name> Premier agent
812
- --agent-b <name> Second agent
813
- --preset <name> Preset de paire d'agents. Exemples: codex-claude, claude-gemini
814
- -t, --turns <number> Nombre total de réponses (1 à 20)
815
- --no-early-stop Désactive l'arrêt anticipé si les agents sont clairement d'accord
816
-
817
- Modèles:
818
-
819
- --model-a <model> Modèle brut transmis à l'agent A
820
- --model-b <model> Modèle brut transmis à l'agent B
821
- --pull-models Autorise Ollama à télécharger un modèle manquant
822
-
823
- Synthèse:
824
-
825
- --summary-agent <name> Agent utilisé pour produire la synthèse finale
826
- --summary-model <model> Modèle brut transmis à l'agent de synthèse
827
- --no-summary Désactive la synthèse finale
828
-
829
- Contexte:
830
-
831
- --files <paths...> Fichiers texte à injecter explicitement dans le contexte
832
- --context <paths...> Scanne fichiers/dossiers texte en respectant les limites de contexte
833
- --show-prompt Affiche le prompt du premier tour sans appeler d'agent
834
-
835
- Configuration:
836
-
837
- --local Avec init/setup, crée ./palabre.config.json
838
- --set-defaults <a b> Avec config, définit les agents par défaut
839
- --summary-agent <name> Avec config, définit l'agent de synthèse par défaut
840
- --summary-agent none Avec config, retire l'agent de synthèse par défaut
841
- --clear-defaults Avec config, supprime les paramètres par défaut
842
- --sync-agents Avec config, ajoute les agents détectés manquants
843
-
844
- Mise à jour:
845
-
846
- --apply Avec update, exécute les étapes de mise à jour
847
-
848
- _____________________________________________
849
-
850
-
851
- Presets disponibles:
852
-
853
- ${listPresetNames().join(", ")}
854
-
855
- _____________________________________________
856
-
857
- `);
863
+ function printHelp(messages, command) {
864
+ const commandHelp = command ? messages.help.renderCommand(command) : undefined;
865
+ console.log(commandHelp ?? messages.help.render(listPresetNames().join(", ")));
866
+ }
867
+ function commandHelpTarget(parsed) {
868
+ if (parsed.command === "help" || parsed.command === "run") {
869
+ return undefined;
870
+ }
871
+ if (parsed.command === "agent")
872
+ return "agents";
873
+ if (parsed.command === "preset")
874
+ return "presets";
875
+ if (parsed.command === "setup")
876
+ return "init";
877
+ return parsed.command;
878
+ }
879
+ /** Résout les messages d'une commande qui peut être affichée avant le flux principal. */
880
+ async function resolveCommandMessages(flags) {
881
+ const explicitLanguage = optionalString(flags.language);
882
+ const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
883
+ let configLanguage;
884
+ try {
885
+ configLanguage = await configExists(configPath)
886
+ ? (await loadConfig(configPath)).language
887
+ : undefined;
888
+ }
889
+ catch {
890
+ configLanguage = undefined;
891
+ }
892
+ return createTranslator(resolveLanguage({ explicitLanguage, configLanguage }));
858
893
  }
859
894
  main().catch((error) => {
895
+ const language = safeStartupLanguage(process.argv.slice(2));
896
+ const messages = createTranslator(language);
860
897
  const message = error instanceof AdapterError
861
- ? formatAdapterError(error)
898
+ ? formatAdapterError(error, messages)
862
899
  : error instanceof Error ? error.message : String(error);
863
- console.error(`Erreur: ${message}`);
900
+ console.error(`${messages.common.errorPrefix}: ${message}`);
864
901
  process.exitCode = 1;
865
902
  });
903
+ function safeStartupLanguage(args) {
904
+ try {
905
+ return resolveLanguage({ explicitLanguage: findRawLanguageFlag(args) });
906
+ }
907
+ catch {
908
+ return DEFAULT_LANGUAGE;
909
+ }
910
+ }