palabre 0.3.0 → 0.6.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 (41) hide show
  1. package/README.md +6 -4
  2. package/dist/adapters/cli-pty.js +183 -0
  3. package/dist/adapters/cli.js +6 -6
  4. package/dist/adapters/index.js +3 -0
  5. package/dist/adapters/terminal.js +13 -0
  6. package/dist/config.js +55 -8
  7. package/dist/configWizard.js +45 -40
  8. package/dist/context.js +16 -14
  9. package/dist/discovery.js +3 -1
  10. package/dist/doctor.js +147 -137
  11. package/dist/errors.js +4 -31
  12. package/dist/i18n.js +30 -0
  13. package/dist/index.js +275 -258
  14. package/dist/limits.js +11 -10
  15. package/dist/messages/adapter-errors.js +36 -0
  16. package/dist/messages/agents.js +38 -0
  17. package/dist/messages/common.js +28 -0
  18. package/dist/messages/config.js +88 -0
  19. package/dist/messages/context.js +24 -0
  20. package/dist/messages/doctor.js +126 -0
  21. package/dist/messages/help.js +280 -0
  22. package/dist/messages/index.js +38 -0
  23. package/dist/messages/init.js +30 -0
  24. package/dist/messages/limits.js +12 -0
  25. package/dist/messages/new.js +66 -0
  26. package/dist/messages/orchestrator.js +14 -0
  27. package/dist/messages/output.js +64 -0
  28. package/dist/messages/presets.js +26 -0
  29. package/dist/messages/preview.js +22 -0
  30. package/dist/messages/prompt.js +102 -0
  31. package/dist/messages/renderers.js +38 -0
  32. package/dist/messages/update.js +40 -0
  33. package/dist/new.js +46 -42
  34. package/dist/orchestrator.js +23 -18
  35. package/dist/output.js +34 -33
  36. package/dist/presets.js +122 -2
  37. package/dist/prompt.js +43 -58
  38. package/dist/renderers/console.js +33 -27
  39. package/dist/update.js +10 -21
  40. package/package.json +4 -1
  41. package/palabre.config.example.json +1 -0
@@ -0,0 +1,38 @@
1
+ export const rendererMessages = {
2
+ fr: {
3
+ subject: (topic) => `Sujet: ${topic}`,
4
+ agents: (pair) => `Agents: ${pair}`,
5
+ responsesSummaryContext: (turns, summary, context) => `Réponses: ${turns} | Synthèse: ${summary} | Contexte: ${context}`,
6
+ responsesSummary: (turns, summary) => `Réponses: ${turns} | Synthèse: ${summary}`,
7
+ context: (context) => `Contexte: ${context}`,
8
+ options: (earlyStop, pullModels) => `Options: arrêt anticipé ${earlyStop ? "activé" : "désactivé"}, auto-pull Ollama ${pullModels ? "activé" : "désactivé"}`,
9
+ enabled: "activé",
10
+ disabled: "désactivée",
11
+ warningPrefix: "Warning:",
12
+ infoPrefix: "Info:",
13
+ turn: (turn, totalTurns) => `tour ${turn}/${totalTurns}`,
14
+ thinking: (agent, role) => `${agent} (${role}) reflechit`,
15
+ summaryTitle: "Synthese",
16
+ exported: (path) => `Debat exporte: ${path}`,
17
+ noInjectedFiles: "aucun fichier injecté",
18
+ injectedFiles: (count) => `${count} fichier${count > 1 ? "s" : ""} injecté${count > 1 ? "s" : ""}`
19
+ },
20
+ en: {
21
+ subject: (topic) => `Subject: ${topic}`,
22
+ agents: (pair) => `Agents: ${pair}`,
23
+ responsesSummaryContext: (turns, summary, context) => `Responses: ${turns} | Summary: ${summary} | Context: ${context}`,
24
+ responsesSummary: (turns, summary) => `Responses: ${turns} | Summary: ${summary}`,
25
+ context: (context) => `Context: ${context}`,
26
+ options: (earlyStop, pullModels) => `Options: early stop ${earlyStop ? "enabled" : "disabled"}, Ollama auto-pull ${pullModels ? "enabled" : "disabled"}`,
27
+ enabled: "enabled",
28
+ disabled: "disabled",
29
+ warningPrefix: "Warning:",
30
+ infoPrefix: "Info:",
31
+ turn: (turn, totalTurns) => `turn ${turn}/${totalTurns}`,
32
+ thinking: (agent, role) => `${agent} (${role}) is thinking`,
33
+ summaryTitle: "Summary",
34
+ exported: (path) => `Debate exported: ${path}`,
35
+ noInjectedFiles: "no injected files",
36
+ injectedFiles: (count) => `${count} injected file${count > 1 ? "s" : ""}`
37
+ }
38
+ };
@@ -0,0 +1,40 @@
1
+ export const updateMessages = {
2
+ fr: {
3
+ upToDate: "PALABRE est a jour.",
4
+ automaticSourceOnly: "Mise a jour automatique disponible seulement depuis un checkout git. Utilise pnpm add --global palabre@latest.",
5
+ stepFailed: (command, args, exitCode) => `${command} ${args} a echoue avec le code ${exitCode}.`,
6
+ instructions: (options) => {
7
+ const lines = [
8
+ `PALABRE ${options.version}`,
9
+ "",
10
+ "Mise a jour recommandee:"
11
+ ];
12
+ if (options.sourceCheckout) {
13
+ lines.push("", "Installation depuis le repo source detectee.", "", ` cd "${options.projectRoot}"`, " git pull --ff-only", " pnpm install", " pnpm build", " pnpm link --global", "", "Pour executer ces etapes automatiquement:", "", " palabre update --apply");
14
+ }
15
+ else {
16
+ lines.push("", "Installation package detectee.", "", " pnpm add --global palabre@latest", "", "Si tu utilises npm:", "", " npm install --global palabre@latest");
17
+ }
18
+ return lines.join("\n");
19
+ }
20
+ },
21
+ en: {
22
+ upToDate: "PALABRE is up to date.",
23
+ automaticSourceOnly: "Automatic update is only available from a git checkout. Use pnpm add --global palabre@latest.",
24
+ stepFailed: (command, args, exitCode) => `${command} ${args} failed with exit code ${exitCode}.`,
25
+ instructions: (options) => {
26
+ const lines = [
27
+ `PALABRE ${options.version}`,
28
+ "",
29
+ "Recommended update:"
30
+ ];
31
+ if (options.sourceCheckout) {
32
+ lines.push("", "Source repository installation detected.", "", ` cd "${options.projectRoot}"`, " git pull --ff-only", " pnpm install", " pnpm build", " pnpm link --global", "", "To run these steps automatically:", "", " palabre update --apply");
33
+ }
34
+ else {
35
+ lines.push("", "Package installation detected.", "", " pnpm add --global palabre@latest", "", "If you use npm:", "", " npm install --global palabre@latest");
36
+ }
37
+ return lines.join("\n");
38
+ }
39
+ }
40
+ };
package/dist/new.js CHANGED
@@ -8,30 +8,30 @@ import { MAX_TURNS, turnsOrDefault, validateTurns } from "./limits.js";
8
8
  * Détecte les outils locaux, liste les agents de la config et guide la composition du débat.
9
9
  * Retourne `undefined` si l'utilisateur annule (q/quit/exit ou Ctrl+C).
10
10
  */
11
- export async function runNewWizard(config) {
11
+ export async function runNewWizard(config, messages) {
12
12
  const discovery = await discoverLocalTools();
13
- const choices = buildAgentChoices(config, discovery);
13
+ const choices = buildAgentChoices(config, discovery, messages);
14
14
  if (choices.length < 2) {
15
- throw new Error("palabre new a besoin d'au moins deux agents dans la config. Lance `palabre init` ou edite ta config.");
15
+ throw new Error(messages.new.needsTwoAgents);
16
16
  }
17
17
  const rl = await createQuestioner();
18
18
  try {
19
- console.log("PALABRE - ASSISTANT DE CONFIGURATION");
20
- console.log("À tout moment: Ctrl+C pour interrompre, ou tape q, quit ou exit dans un prompt pour quitter.");
21
- console.log("Appuie sur Entrée pour accepter un choix par défaut (*).");
19
+ console.log(messages.new.title);
20
+ console.log(messages.new.quitHint);
21
+ console.log(messages.new.defaultHint);
22
22
  console.log("");
23
- const agentA = await askAgent(rl, choices, "Agent A", config.defaults?.agentA);
23
+ const agentA = await askAgent(rl, choices, messages.new.agentA, config.defaults?.agentA, messages);
24
24
  if (!agentA)
25
25
  return undefined;
26
- const agentB = await askAgent(rl, choices.filter((choice) => choice.name !== agentA), "Agent B", config.defaults?.agentB === agentA ? undefined : config.defaults?.agentB);
26
+ const agentB = await askAgent(rl, choices.filter((choice) => choice.name !== agentA), messages.new.agentB, config.defaults?.agentB === agentA ? undefined : config.defaults?.agentB, messages);
27
27
  if (!agentB)
28
28
  return undefined;
29
- const topic = await askRequiredText(rl, "Sujet");
29
+ const topic = await askRequiredText(rl, messages.new.topic, messages);
30
30
  if (!topic)
31
31
  return undefined;
32
- printCommandPreview({ agentA, agentB, topic, turns: turnsOrDefault(config.defaults?.turns) });
33
- console.log("Réponds non pour choisir le nombre de réponses, les modèles, la synthèse et le contexte.");
34
- const launchMinimal = await askYesNo(rl, "Lancer maintenant avec les options par défaut ?", true);
32
+ printCommandPreview({ agentA, agentB, topic, turns: turnsOrDefault(config.defaults?.turns) }, messages);
33
+ console.log(messages.new.advancedHint);
34
+ const launchMinimal = await askYesNo(rl, messages.new.launchMinimal, true, messages);
35
35
  if (launchMinimal === undefined)
36
36
  return undefined;
37
37
  if (launchMinimal) {
@@ -45,34 +45,34 @@ export async function runNewWizard(config) {
45
45
  plainOutput: false
46
46
  };
47
47
  }
48
- const turns = await askNumber(rl, "Nombre de réponses", turnsOrDefault(config.defaults?.turns));
48
+ const turns = await askNumber(rl, messages.new.turns, turnsOrDefault(config.defaults?.turns), messages);
49
49
  if (turns === undefined)
50
50
  return undefined;
51
- const modelA = await askOptionalText(rl, `Modèle pour ${agentA} (optionnel)`);
51
+ const modelA = await askOptionalText(rl, messages.new.modelFor(agentA));
52
52
  if (modelA === undefined)
53
53
  return undefined;
54
- const modelB = await askOptionalText(rl, `Modèle pour ${agentB} (optionnel)`);
54
+ const modelB = await askOptionalText(rl, messages.new.modelFor(agentB));
55
55
  if (modelB === undefined)
56
56
  return undefined;
57
- const summaryEnabled = await askYesNo(rl, "Synthèse finale ?", true);
57
+ const summaryEnabled = await askYesNo(rl, messages.new.summaryEnabled, true, messages);
58
58
  if (summaryEnabled === undefined)
59
59
  return undefined;
60
60
  let summaryAgent;
61
61
  let summaryModel;
62
62
  if (summaryEnabled) {
63
- summaryAgent = await askAgent(rl, choices, "Agent de synthèse", config.defaults?.summaryAgent ?? agentB);
63
+ summaryAgent = await askAgent(rl, choices, messages.new.summaryAgent, config.defaults?.summaryAgent ?? agentB, messages);
64
64
  if (!summaryAgent)
65
65
  return undefined;
66
- summaryModel = await askOptionalText(rl, `Modèle de synthèse pour ${summaryAgent} (optionnel)`);
66
+ summaryModel = await askOptionalText(rl, messages.new.summaryModelFor(summaryAgent));
67
67
  if (summaryModel === undefined)
68
68
  return undefined;
69
69
  }
70
- const context = splitPaths(await askOptionalText(rl, "Contexte dossier/fichier via --context (optionnel)"));
71
- const files = splitPaths(await askOptionalText(rl, "Fichiers stricts via --files (optionnel)"));
72
- const showPrompt = await askYesNo(rl, "Afficher seulement le prompt ?", false);
70
+ const context = splitPaths(await askOptionalText(rl, messages.new.contextPaths));
71
+ const files = splitPaths(await askOptionalText(rl, messages.new.filesPaths));
72
+ const showPrompt = await askYesNo(rl, messages.new.showPrompt, false, messages);
73
73
  if (showPrompt === undefined)
74
74
  return undefined;
75
- const plainOutput = await askYesNo(rl, "Rendu plain ?", false);
75
+ const plainOutput = await askYesNo(rl, messages.new.plainOutput, false, messages);
76
76
  if (plainOutput === undefined)
77
77
  return undefined;
78
78
  const selection = {
@@ -90,7 +90,7 @@ export async function runNewWizard(config) {
90
90
  showPrompt,
91
91
  plainOutput
92
92
  };
93
- printCommandPreview(selection);
93
+ printCommandPreview(selection, messages);
94
94
  return selection;
95
95
  }
96
96
  finally {
@@ -123,7 +123,7 @@ async function readPipedLines() {
123
123
  }
124
124
  return raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
125
125
  }
126
- function buildAgentChoices(config, discovery) {
126
+ function buildAgentChoices(config, discovery, messages) {
127
127
  return Object.entries(config.agents)
128
128
  .map(([name, agentConfig]) => {
129
129
  const detected = isAgentDetected(name, agentConfig, discovery);
@@ -131,7 +131,7 @@ function buildAgentChoices(config, discovery) {
131
131
  name,
132
132
  config: agentConfig,
133
133
  detected,
134
- status: agentStatus(name, agentConfig, discovery, detected)
134
+ status: agentStatus(name, agentConfig, discovery, detected, messages)
135
135
  };
136
136
  })
137
137
  .sort((left, right) => Number(right.detected) - Number(left.detected) || left.name.localeCompare(right.name));
@@ -147,21 +147,25 @@ function isAgentDetected(name, config, discovery) {
147
147
  return discovery.claude.available;
148
148
  if (normalized === "gemini")
149
149
  return discovery.gemini.available;
150
+ if (normalized === "agy")
151
+ return discovery.antigravity.available;
152
+ if (normalized === "antigravity")
153
+ return discovery.antigravity.available;
150
154
  if (normalized === "opencode")
151
155
  return discovery.opencode.available;
152
156
  return true;
153
157
  }
154
- function agentStatus(_name, config, discovery, detected) {
158
+ function agentStatus(_name, config, discovery, detected, messages) {
155
159
  if (config.type === "ollama") {
156
160
  return detected
157
- ? `ollama/${config.role} détecté (${discovery.ollama.models.length} modèle(s))`
158
- : `ollama/${config.role} non joignable`;
161
+ ? messages.new.detectedOllama(config.role, discovery.ollama.models.length)
162
+ : messages.new.ollamaUnreachable(config.role);
159
163
  }
160
164
  return detected
161
- ? `cli/${config.role} détecté`
162
- : `cli/${config.role} non détecté`;
165
+ ? messages.new.detectedCli(config.role)
166
+ : messages.new.missingCli(config.role);
163
167
  }
164
- async function askAgent(rl, choices, label, defaultName) {
168
+ async function askAgent(rl, choices, label, defaultName, messages) {
165
169
  const fallback = choices.find((choice) => choice.name === defaultName)?.name ?? choices[0]?.name;
166
170
  console.log(label);
167
171
  choices.forEach((choice, index) => {
@@ -182,10 +186,10 @@ async function askAgent(rl, choices, label, defaultName) {
182
186
  if (choices.some((choice) => choice.name === value)) {
183
187
  return value;
184
188
  }
185
- console.log("Choix invalide. Tape un numéro, un nom d'agent, Entrée ou q.");
189
+ console.log(messages.new.invalidAgentChoice);
186
190
  }
187
191
  }
188
- async function askRequiredText(rl, label) {
192
+ async function askRequiredText(rl, label, messages) {
189
193
  while (true) {
190
194
  const answer = await rl.question(`${label}: `);
191
195
  const value = answer.trim();
@@ -193,7 +197,7 @@ async function askRequiredText(rl, label) {
193
197
  return undefined;
194
198
  if (value)
195
199
  return value;
196
- console.log("Ce champ est requis pour lancer un débat.");
200
+ console.log(messages.new.requiredField);
197
201
  }
198
202
  }
199
203
  async function askOptionalText(rl, label) {
@@ -201,7 +205,7 @@ async function askOptionalText(rl, label) {
201
205
  const value = answer.trim();
202
206
  return isQuit(value) ? undefined : value;
203
207
  }
204
- async function askNumber(rl, label, defaultValue) {
208
+ async function askNumber(rl, label, defaultValue, messages) {
205
209
  while (true) {
206
210
  const answer = await rl.question(`${label} [${defaultValue}]: `);
207
211
  const value = answer.trim();
@@ -212,18 +216,18 @@ async function askNumber(rl, label, defaultValue) {
212
216
  const parsed = Number(value);
213
217
  if (Number.isInteger(parsed)) {
214
218
  try {
215
- validateTurns(parsed, "Le nombre de réponses");
219
+ validateTurns(parsed, messages.new.turnsValidationLabel, messages);
216
220
  return parsed;
217
221
  }
218
222
  catch {
219
223
  // Show the user-facing wizard hint below.
220
224
  }
221
225
  }
222
- console.log(`Entre un nombre entier entre 1 et ${MAX_TURNS}, Entrée ou q.`);
226
+ console.log(messages.new.invalidTurns(MAX_TURNS));
223
227
  }
224
228
  }
225
- async function askYesNo(rl, label, defaultValue) {
226
- const suffix = defaultValue ? "Y/n" : "y/N";
229
+ async function askYesNo(rl, label, defaultValue, messages) {
230
+ const suffix = messages.new.yesNoSuffix(defaultValue);
227
231
  while (true) {
228
232
  const answer = await rl.question(`${label} [${suffix}]: `);
229
233
  const value = answer.trim().toLowerCase();
@@ -235,7 +239,7 @@ async function askYesNo(rl, label, defaultValue) {
235
239
  return true;
236
240
  if (["n", "no", "non"].includes(value))
237
241
  return false;
238
- console.log("Réponds par oui, non, Entrée ou q.");
242
+ console.log(messages.new.invalidYesNo);
239
243
  }
240
244
  }
241
245
  function splitPaths(value) {
@@ -254,11 +258,11 @@ function normalizeCommandName(command) {
254
258
  function isQuit(value) {
255
259
  return ["q", "quit", "exit"].includes(value.toLowerCase());
256
260
  }
257
- function printCommandPreview(selection) {
261
+ function printCommandPreview(selection, messages) {
258
262
  const explicitCommand = buildExplicitCommand(selection);
259
263
  const shortCommand = buildShortCommand(selection);
260
264
  console.log("");
261
- console.log("Commandes équivalentes:");
265
+ console.log(messages.new.equivalentCommands);
262
266
  console.log(` ${explicitCommand}`);
263
267
  if (shortCommand) {
264
268
  console.log(` ${shortCommand}`);
@@ -1,4 +1,5 @@
1
1
  import { createAgent } from "./adapters/index.js";
2
+ import { createTranslator } from "./i18n.js";
2
3
  /**
3
4
  * Point d'entrée de l'orchestration.
4
5
  * Lance le ping-pong entre `agentA` et `agentB` pendant `options.turns` tours,
@@ -6,19 +7,19 @@ import { createAgent } from "./adapters/index.js";
6
7
  *
7
8
  * @throws {Error} si un agent référencé dans `options` est absent de `config.agents`.
8
9
  */
9
- export async function runDebate(config, options, renderer) {
10
+ export async function runDebate(config, options, renderer, messages = createTranslator("fr")) {
10
11
  const agentAConfig = withRuntimeOverrides(config.agents[options.agentA], options.modelA, options.pullModels);
11
12
  const agentBConfig = withRuntimeOverrides(config.agents[options.agentB], options.modelB, options.pullModels);
12
13
  if (!agentAConfig) {
13
- throw new Error(`Agent inconnu: ${options.agentA}`);
14
+ throw new Error(messages.common.unknownAgent(options.agentA));
14
15
  }
15
16
  if (!agentBConfig) {
16
- throw new Error(`Agent inconnu: ${options.agentB}`);
17
+ throw new Error(messages.common.unknownAgent(options.agentB));
17
18
  }
18
19
  warnIfOllamaHasNoContext(options, [
19
20
  [options.agentA, agentAConfig],
20
21
  [options.agentB, agentBConfig]
21
- ], renderer);
22
+ ], renderer, messages);
22
23
  renderer?.start(options, [
23
24
  { name: options.agentA, role: agentAConfig.role, type: agentAConfig.type },
24
25
  { name: options.agentB, role: agentBConfig.role, type: agentBConfig.type }
@@ -27,7 +28,7 @@ export async function runDebate(config, options, renderer) {
27
28
  createAgent(options.agentA, agentAConfig),
28
29
  createAgent(options.agentB, agentBConfig)
29
30
  ];
30
- const messages = [];
31
+ const transcript = [];
31
32
  let stopReason;
32
33
  for (let index = 0; index < options.turns; index += 1) {
33
34
  const current = agents[index % agents.length];
@@ -38,12 +39,14 @@ export async function runDebate(config, options, renderer) {
38
39
  const response = await current.generate({
39
40
  topic: options.topic,
40
41
  turn,
42
+ totalTurns: options.turns,
41
43
  selfName: current.name,
42
44
  peerName: peer.name,
43
45
  selfRole: current.role,
46
+ language: options.language,
44
47
  session: options.session,
45
48
  files: options.files,
46
- transcript: messages
49
+ transcript
47
50
  }).finally(() => renderer?.thinkingEnd());
48
51
  const message = {
49
52
  agent: current.name,
@@ -51,20 +54,20 @@ export async function runDebate(config, options, renderer) {
51
54
  content: response.content,
52
55
  createdAt: new Date().toISOString()
53
56
  };
54
- messages.push(message);
57
+ transcript.push(message);
55
58
  renderer?.message(message.content);
56
- if (shouldStopOnAgreement(options, messages)) {
57
- stopReason = "Accord clair detecte apres un tour complet.";
58
- renderer?.notice(`Arret anticipe: ${stopReason}`);
59
+ if (shouldStopOnAgreement(options, transcript)) {
60
+ stopReason = messages.orchestrator.agreementStopReason;
61
+ renderer?.notice(messages.orchestrator.earlyStop(stopReason));
59
62
  break;
60
63
  }
61
64
  }
62
65
  const summary = options.summaryEnabled
63
- ? await generateSummary(config, options, messages, renderer)
66
+ ? await generateSummary(config, options, transcript, renderer, messages)
64
67
  : undefined;
65
68
  return {
66
69
  options,
67
- messages,
70
+ messages: transcript,
68
71
  summary,
69
72
  stopReason
70
73
  };
@@ -111,7 +114,7 @@ function normalizeForAgreement(value) {
111
114
  * Émet un avertissement si un agent Ollama participe sans contexte fichier.
112
115
  * L'adapter Ollama ne lit pas le filesystem : sans `--files` ou `--context`, il ne voit pas le projet.
113
116
  */
114
- function warnIfOllamaHasNoContext(options, agents, renderer) {
117
+ function warnIfOllamaHasNoContext(options, agents, renderer, messages = createTranslator("fr")) {
115
118
  if (options.files.length > 0) {
116
119
  return;
117
120
  }
@@ -122,33 +125,35 @@ function warnIfOllamaHasNoContext(options, agents, renderer) {
122
125
  if (ollamaAgents.length === 0) {
123
126
  return;
124
127
  }
125
- renderer?.warning(`${ollamaAgents.join(", ")} ne lit pas le filesystem. Ajoute --files ou --context pour fournir un contexte projet.`);
128
+ renderer?.warning(messages.orchestrator.ollamaNoContext(ollamaAgents.join(", ")));
126
129
  }
127
130
  /**
128
131
  * Phase de synthèse post-débat. Utilise `options.summaryAgent` quand il est défini, sinon `agentB`.
129
132
  *
130
133
  * @throws {Error} si l'agent de synthèse est absent de `config.agents`.
131
134
  */
132
- async function generateSummary(config, options, messages, renderer) {
135
+ async function generateSummary(config, options, transcript, renderer, messages = createTranslator("fr")) {
133
136
  const summaryAgentName = options.summaryAgent ?? options.agentB;
134
137
  const summaryModel = options.summaryModel ?? modelForAgent(options, summaryAgentName);
135
138
  const summaryConfig = withRuntimeOverrides(config.agents[summaryAgentName], summaryModel, options.pullModels);
136
139
  if (!summaryConfig) {
137
- throw new Error(`Agent de synthese inconnu: ${summaryAgentName}`);
140
+ throw new Error(messages.orchestrator.unknownSummaryAgent(summaryAgentName));
138
141
  }
139
142
  const summaryAgent = createAgent(summaryAgentName, summaryConfig);
140
143
  renderer?.summaryStart(summaryAgent.name, summaryAgent.role);
141
144
  renderer?.thinkingStart(summaryAgent.name, summaryAgent.role);
142
145
  const response = await summaryAgent.generate({
143
146
  topic: options.topic,
144
- turn: messages.length + 1,
147
+ turn: transcript.length + 1,
148
+ totalTurns: options.turns,
145
149
  selfName: summaryAgent.name,
146
150
  peerName: "transcript",
147
151
  selfRole: summaryAgent.role,
148
152
  mode: "summary",
153
+ language: options.language,
149
154
  session: options.session,
150
155
  files: options.files,
151
- transcript: messages
156
+ transcript
152
157
  }).finally(() => renderer?.thinkingEnd());
153
158
  const summary = {
154
159
  agent: summaryAgent.name,
package/dist/output.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { createTranslator } from "./i18n.js";
3
4
  /**
4
5
  * Écrit le débat au format Markdown dans `outputDir`.
5
6
  * Crée le répertoire si absent. Retourne le chemin absolu du fichier créé.
6
7
  */
7
- export async function writeDebateMarkdown(outputDir, options, messages, summary, stopReason) {
8
+ export async function writeDebateMarkdown(outputDir, options, debateMessages, summary, stopReason, messages = createTranslator("fr")) {
8
9
  const safeDate = new Date().toISOString().replace(/[:.]/g, "-");
9
10
  const fileName = `palabre-${slugifyTopic(options.topic)}-${safeDate}.debate.md`;
10
11
  const filePath = path.resolve(outputDir, fileName);
11
12
  await mkdir(path.dirname(filePath), { recursive: true });
12
- await writeFile(filePath, renderDebateMarkdown(options, messages, summary, stopReason), "utf8");
13
+ await writeFile(filePath, renderDebateMarkdown(options, debateMessages, summary, stopReason, messages), "utf8");
13
14
  return filePath;
14
15
  }
15
16
  function slugifyTopic(topic) {
@@ -27,33 +28,33 @@ function slugifyTopic(topic) {
27
28
  * Produit la représentation Markdown complète du débat.
28
29
  * Fonction pure : aucun effet de bord sur le filesystem.
29
30
  */
30
- export function renderDebateMarkdown(options, messages, summary, stopReason) {
31
+ export function renderDebateMarkdown(options, debateMessages, summary, stopReason, messages = createTranslator("fr")) {
31
32
  const lines = [
32
- "# PALABRE Debate",
33
+ messages.output.title,
33
34
  "",
34
- ...renderSessionHeader(options, messages, stopReason),
35
+ ...renderSessionHeader(options, debateMessages, stopReason, messages),
35
36
  "",
36
- "## Contexte",
37
+ messages.output.contextTitle,
37
38
  "",
38
- ...renderFileList(options.files),
39
+ ...renderFileList(options.files, messages),
39
40
  "",
40
- "## Echanges",
41
+ messages.output.exchangesTitle,
41
42
  ""
42
43
  ];
43
- for (const message of messages) {
44
+ for (const message of debateMessages) {
44
45
  lines.push(`### ${message.agent} (${message.role})`, "", normalizeMarkdownForWindowsPreview(message.content.trim()), "");
45
46
  }
46
- lines.push("---", "", "## Synthese finale", "", ...renderSummaryBlock(options, summary));
47
+ lines.push("---", "", messages.output.finalSummaryTitle, "", ...renderSummaryBlock(options, summary, messages));
47
48
  return `${lines.join("\n")}\n`;
48
49
  }
49
- function renderSummaryBlock(options, summary) {
50
+ function renderSummaryBlock(options, summary, messages) {
50
51
  if (summary) {
51
52
  return [
52
- "| Champ | Valeur |",
53
+ `| ${messages.output.tableField} | ${messages.output.tableValue} |`,
53
54
  "| --- | --- |",
54
- `| Agent | ${escapeTableCell(summary.agent)} |`,
55
- `| Role | ${escapeTableCell(summary.role)} |`,
56
- `| Date | ${escapeTableCell(summary.createdAt)} |`,
55
+ `| ${messages.output.fields.agent} | ${escapeTableCell(summary.agent)} |`,
56
+ `| ${messages.output.fields.role} | ${escapeTableCell(summary.role)} |`,
57
+ `| ${messages.output.fields.date} | ${escapeTableCell(summary.createdAt)} |`,
57
58
  "",
58
59
  normalizeMarkdownForWindowsPreview(summary.content.trim()),
59
60
  ""
@@ -61,30 +62,30 @@ function renderSummaryBlock(options, summary) {
61
62
  }
62
63
  return [
63
64
  options.summaryEnabled
64
- ? "_Synthese finale demandee mais non disponible._"
65
- : "_Synthese desactivee._",
65
+ ? messages.output.summaryMissing
66
+ : messages.output.summaryDisabled,
66
67
  ""
67
68
  ];
68
69
  }
69
70
  function normalizeMarkdownForWindowsPreview(content) {
70
71
  return content.replace(/:\*\*/g, "&#58;**");
71
72
  }
72
- function renderSessionHeader(options, messages, stopReason) {
73
+ function renderSessionHeader(options, debateMessages, stopReason, messages) {
73
74
  const rows = [
74
- ["Sujet", options.topic],
75
- ["Agents", `${options.agentA} <-> ${options.agentB}`],
76
- ["Auto-pull Ollama", options.pullModels ? "yes" : "no"],
77
- ["Synthese", options.summaryEnabled ? options.summaryAgent ?? options.agentB : "disabled"],
78
- ["Tours demandes", String(options.turns)],
79
- ["Tours joues", String(messages.length)],
80
- ["Arret anticipe", stopReason ?? "no"],
81
- ["Date locale", options.session.localDate],
82
- ["Fuseau horaire", options.session.timeZone],
83
- ["Dossier courant", options.session.cwd],
84
- ["Session demarree a", options.session.startedAt]
75
+ [messages.output.fields.subject, options.topic],
76
+ [messages.output.fields.agents, `${options.agentA} <-> ${options.agentB}`],
77
+ [messages.output.fields.autoPullOllama, options.pullModels ? messages.output.yes : messages.output.no],
78
+ [messages.output.fields.summary, options.summaryEnabled ? options.summaryAgent ?? options.agentB : messages.output.disabled],
79
+ [messages.output.fields.requestedTurns, String(options.turns)],
80
+ [messages.output.fields.playedTurns, String(debateMessages.length)],
81
+ [messages.output.fields.earlyStop, stopReason ?? messages.output.no],
82
+ [messages.output.fields.localDate, options.session.localDate],
83
+ [messages.output.fields.timeZone, options.session.timeZone],
84
+ [messages.output.fields.cwd, options.session.cwd],
85
+ [messages.output.fields.sessionStartedAt, options.session.startedAt]
85
86
  ];
86
87
  return [
87
- "| Champ | Valeur |",
88
+ `| ${messages.output.tableField} | ${messages.output.tableValue} |`,
88
89
  "| --- | --- |",
89
90
  ...rows.map(([label, value]) => `| ${escapeTableCell(label)} | ${escapeTableCell(value)} |`)
90
91
  ];
@@ -92,9 +93,9 @@ function renderSessionHeader(options, messages, stopReason) {
92
93
  function escapeTableCell(value) {
93
94
  return value.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
94
95
  }
95
- function renderFileList(files) {
96
+ function renderFileList(files, messages) {
96
97
  if (files.length === 0) {
97
- return ["Aucun contexte fichier injecte."];
98
+ return [messages.output.noFileContext];
98
99
  }
99
- return files.map((file) => `- \`${file.path}\` (${file.sizeBytes} bytes)`);
100
+ return files.map((file) => `- \`${file.path}\` (${file.sizeBytes} ${messages.output.fileSizeUnit})`);
100
101
  }