palabre 0.3.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.
@@ -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));
@@ -151,17 +151,17 @@ function isAgentDetected(name, config, discovery) {
151
151
  return discovery.opencode.available;
152
152
  return true;
153
153
  }
154
- function agentStatus(_name, config, discovery, detected) {
154
+ function agentStatus(_name, config, discovery, detected, messages) {
155
155
  if (config.type === "ollama") {
156
156
  return detected
157
- ? `ollama/${config.role} détecté (${discovery.ollama.models.length} modèle(s))`
158
- : `ollama/${config.role} non joignable`;
157
+ ? messages.new.detectedOllama(config.role, discovery.ollama.models.length)
158
+ : messages.new.ollamaUnreachable(config.role);
159
159
  }
160
160
  return detected
161
- ? `cli/${config.role} détecté`
162
- : `cli/${config.role} non détecté`;
161
+ ? messages.new.detectedCli(config.role)
162
+ : messages.new.missingCli(config.role);
163
163
  }
164
- async function askAgent(rl, choices, label, defaultName) {
164
+ async function askAgent(rl, choices, label, defaultName, messages) {
165
165
  const fallback = choices.find((choice) => choice.name === defaultName)?.name ?? choices[0]?.name;
166
166
  console.log(label);
167
167
  choices.forEach((choice, index) => {
@@ -182,10 +182,10 @@ async function askAgent(rl, choices, label, defaultName) {
182
182
  if (choices.some((choice) => choice.name === value)) {
183
183
  return value;
184
184
  }
185
- console.log("Choix invalide. Tape un numéro, un nom d'agent, Entrée ou q.");
185
+ console.log(messages.new.invalidAgentChoice);
186
186
  }
187
187
  }
188
- async function askRequiredText(rl, label) {
188
+ async function askRequiredText(rl, label, messages) {
189
189
  while (true) {
190
190
  const answer = await rl.question(`${label}: `);
191
191
  const value = answer.trim();
@@ -193,7 +193,7 @@ async function askRequiredText(rl, label) {
193
193
  return undefined;
194
194
  if (value)
195
195
  return value;
196
- console.log("Ce champ est requis pour lancer un débat.");
196
+ console.log(messages.new.requiredField);
197
197
  }
198
198
  }
199
199
  async function askOptionalText(rl, label) {
@@ -201,7 +201,7 @@ async function askOptionalText(rl, label) {
201
201
  const value = answer.trim();
202
202
  return isQuit(value) ? undefined : value;
203
203
  }
204
- async function askNumber(rl, label, defaultValue) {
204
+ async function askNumber(rl, label, defaultValue, messages) {
205
205
  while (true) {
206
206
  const answer = await rl.question(`${label} [${defaultValue}]: `);
207
207
  const value = answer.trim();
@@ -212,18 +212,18 @@ async function askNumber(rl, label, defaultValue) {
212
212
  const parsed = Number(value);
213
213
  if (Number.isInteger(parsed)) {
214
214
  try {
215
- validateTurns(parsed, "Le nombre de réponses");
215
+ validateTurns(parsed, messages.new.turnsValidationLabel, messages);
216
216
  return parsed;
217
217
  }
218
218
  catch {
219
219
  // Show the user-facing wizard hint below.
220
220
  }
221
221
  }
222
- console.log(`Entre un nombre entier entre 1 et ${MAX_TURNS}, Entrée ou q.`);
222
+ console.log(messages.new.invalidTurns(MAX_TURNS));
223
223
  }
224
224
  }
225
- async function askYesNo(rl, label, defaultValue) {
226
- const suffix = defaultValue ? "Y/n" : "y/N";
225
+ async function askYesNo(rl, label, defaultValue, messages) {
226
+ const suffix = messages.new.yesNoSuffix(defaultValue);
227
227
  while (true) {
228
228
  const answer = await rl.question(`${label} [${suffix}]: `);
229
229
  const value = answer.trim().toLowerCase();
@@ -235,7 +235,7 @@ async function askYesNo(rl, label, defaultValue) {
235
235
  return true;
236
236
  if (["n", "no", "non"].includes(value))
237
237
  return false;
238
- console.log("Réponds par oui, non, Entrée ou q.");
238
+ console.log(messages.new.invalidYesNo);
239
239
  }
240
240
  }
241
241
  function splitPaths(value) {
@@ -254,11 +254,11 @@ function normalizeCommandName(command) {
254
254
  function isQuit(value) {
255
255
  return ["q", "quit", "exit"].includes(value.toLowerCase());
256
256
  }
257
- function printCommandPreview(selection) {
257
+ function printCommandPreview(selection, messages) {
258
258
  const explicitCommand = buildExplicitCommand(selection);
259
259
  const shortCommand = buildShortCommand(selection);
260
260
  console.log("");
261
- console.log("Commandes équivalentes:");
261
+ console.log(messages.new.equivalentCommands);
262
262
  console.log(` ${explicitCommand}`);
263
263
  if (shortCommand) {
264
264
  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
  }
package/dist/presets.js CHANGED
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  const presets = {
2
3
  "codex-claude": {
3
4
  agentA: "codex",
@@ -81,10 +82,11 @@ const presets = {
81
82
  }
82
83
  };
83
84
  /** Retourne la paire d'agents pour `name`. Lève une erreur avec la liste des presets disponibles si inconnu. */
84
- export function resolvePreset(name) {
85
+ export function resolvePreset(name, messages) {
85
86
  const preset = presets[name];
86
87
  if (!preset) {
87
- throw new Error(`Preset inconnu: ${name}. Presets disponibles: ${Object.keys(presets).join(", ")}`);
88
+ const available = Object.keys(presets).join(", ");
89
+ throw new Error(messages?.presets.unknown(name, available) ?? `Preset inconnu: ${name}. Presets disponibles: ${available}`);
88
90
  }
89
91
  return preset;
90
92
  }
@@ -92,7 +94,81 @@ export function resolvePreset(name) {
92
94
  export function listPresetNames() {
93
95
  return Object.keys(presets);
94
96
  }
97
+ /** Retourne les presets complets, dans l'ordre de déclaration. */
98
+ export function listPresets() {
99
+ return Object.entries(presets).map(([name, preset]) => ({
100
+ name,
101
+ agentA: preset.agentA,
102
+ agentB: preset.agentB
103
+ }));
104
+ }
105
+ /**
106
+ * Retourne les presets enrichis par la disponibilité réelle des agents déclarés.
107
+ * Les intégrations peuvent filtrer `available === true` sans réimplémenter la découverte locale.
108
+ */
109
+ export function listPresetsWithAvailability(config, discovery, messages) {
110
+ return listPresets().map((preset) => {
111
+ const checks = [
112
+ checkAgentAvailability(preset.agentA, config, discovery, messages),
113
+ checkAgentAvailability(preset.agentB, config, discovery, messages)
114
+ ];
115
+ const unavailable = checks.filter((check) => !check.available);
116
+ return {
117
+ ...preset,
118
+ available: unavailable.length === 0,
119
+ missingAgents: unavailable.map((check) => check.agent),
120
+ unavailableReasons: unavailable.map((check) => check.reason)
121
+ };
122
+ });
123
+ }
95
124
  /** Recherche inverse : retourne le nom du preset correspondant à une paire `(agentA, agentB)`, ou `undefined`. */
96
125
  export function findPresetNameForPair(agentA, agentB) {
97
126
  return Object.entries(presets).find(([, preset]) => preset.agentA === agentA && preset.agentB === agentB)?.[0];
98
127
  }
128
+ function checkAgentAvailability(agentName, config, discovery, messages) {
129
+ const agent = config.agents[agentName];
130
+ if (!agent) {
131
+ return unavailable(agentName, messages?.presets.missingAgent(agentName) ?? `agent absent de la config: ${agentName}`);
132
+ }
133
+ if (agent.type === "ollama") {
134
+ if (!discovery.ollama.available) {
135
+ return unavailable(agentName, discovery.ollama.commandAvailable
136
+ ? messages?.presets.ollamaUnreachable(agentName) ?? `Ollama non joignable pour ${agentName}`
137
+ : messages?.presets.ollamaNotDetected(agentName) ?? `Ollama non détecté pour ${agentName}`);
138
+ }
139
+ if (!discovery.ollama.models.includes(agent.model)) {
140
+ return unavailable(agentName, messages?.presets.missingOllamaModel(agentName, agent.model) ?? `modèle Ollama absent pour ${agentName}: ${agent.model}`);
141
+ }
142
+ return available(agentName);
143
+ }
144
+ const detection = knownCliDetection(agent, discovery);
145
+ if (!detection) {
146
+ // Les CLIs custom déclarées par l'utilisateur restent considérées utilisables :
147
+ // Palabre ne peut pas connaître leur sémantique sans les lancer.
148
+ return available(agentName);
149
+ }
150
+ return detection.available
151
+ ? available(agentName)
152
+ : unavailable(agentName, messages?.presets.missingCommand(agentName, detection.command) ?? `commande non détectée pour ${agentName}: ${detection.command}`);
153
+ }
154
+ function knownCliDetection(agent, discovery) {
155
+ const command = normalizeCommandName(agent.command);
156
+ if (command === "codex")
157
+ return discovery.codex;
158
+ if (command === "claude")
159
+ return discovery.claude;
160
+ if (command === "gemini")
161
+ return discovery.gemini;
162
+ if (command === "opencode")
163
+ return discovery.opencode;
164
+ return undefined;
165
+ }
166
+ function normalizeCommandName(command) {
167
+ return path.basename(command).toLowerCase().replace(/\.(exe|cmd|ps1|bat)$/i, "");
168
+ }
169
+ function available(agent) {
170
+ return { agent, available: true, reason: "" };
171
+ }
172
+ function unavailable(agent, reason) {
173
+ return { agent, available: false, reason };
174
+ }