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
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  ## Français
15
15
 
16
- PALABRE est un orchestrateur CLI qui fait dialoguer plusieurs agents IA installés sur votre machine : Claude Code, Codex CLI, Gemini CLI, OpenCode et Ollama.
16
+ PALABRE est un orchestrateur CLI qui fait dialoguer plusieurs agents IA installés sur votre machine : Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode et Ollama.
17
17
 
18
18
  Il ne remplace pas vos outils : il les pilote. Vous gardez vos abonnements, vos modèles par défaut, vos habitudes de terminal et vos fichiers en local. PALABRE exporte ensuite le débat en Markdown.
19
19
 
@@ -57,6 +57,7 @@ palabre codex-claude "Preview" --context src --show-prompt
57
57
  - Claude Code via `claude --print`
58
58
  - Codex CLI via `codex exec`
59
59
  - Gemini CLI via `gemini --prompt -`
60
+ - Antigravity CLI via `agy --print` en pseudo-terminal
60
61
  - OpenCode via `opencode run`
61
62
  - Ollama via l'API locale HTTP
62
63
 
@@ -64,7 +65,7 @@ PALABRE ne liste pas les modèles : ils changent souvent et dépendent de chaque
64
65
 
65
66
  ### Confidentialité
66
67
 
67
- PALABRE tourne localement et n'envoie aucune donnée à un serveur appartenant à PALABRE. Les données envoyées aux agents dépendent des outils que vous utilisez : vérifiez les politiques de confidentialité de Claude Code, Codex CLI, Gemini CLI, OpenCode, Ollama ou de tout autre agent configuré.
68
+ PALABRE tourne localement et n'envoie aucune donnée à un serveur appartenant à PALABRE. Les données envoyées aux agents dépendent des outils que vous utilisez : vérifiez les politiques de confidentialité de Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Ollama ou de tout autre agent configuré.
68
69
 
69
70
  ### Développement local
70
71
 
@@ -87,7 +88,7 @@ MIT. Voir [LICENSE](./LICENSE).
87
88
 
88
89
  ## English
89
90
 
90
- PALABRE is a CLI orchestrator that lets multiple AI agents installed on your machine talk to each other: Claude Code, Codex CLI, Gemini CLI, OpenCode, and Ollama.
91
+ PALABRE is a CLI orchestrator that lets multiple AI agents installed on your machine talk to each other: Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, and Ollama.
91
92
 
92
93
  It does not replace your tools: it drives them. You keep your subscriptions, default models, terminal habits, and local files. PALABRE then exports the debate as Markdown.
93
94
 
@@ -131,6 +132,7 @@ palabre codex-claude "Preview" --context src --show-prompt
131
132
  - Claude Code via `claude --print`
132
133
  - Codex CLI via `codex exec`
133
134
  - Gemini CLI via `gemini --prompt -`
135
+ - Antigravity CLI via `agy --print` in a pseudo-terminal
134
136
  - OpenCode via `opencode run`
135
137
  - Ollama via the local HTTP API
136
138
 
@@ -138,7 +140,7 @@ PALABRE does not list models: they change often and depend on each CLI or user a
138
140
 
139
141
  ### Privacy
140
142
 
141
- PALABRE runs locally and does not send data to a PALABRE-owned server. Data sent to agents depends on the tools you use: check the privacy policies of Claude Code, Codex CLI, Gemini CLI, OpenCode, Ollama, or any custom agent you configure.
143
+ PALABRE runs locally and does not send data to a PALABRE-owned server. Data sent to agents depends on the tools you use: check the privacy policies of Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Ollama, or any custom agent you configure.
142
144
 
143
145
  ### Local Development
144
146
 
@@ -0,0 +1,183 @@
1
+ import { spawn as spawnPty } from "node-pty";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { AdapterError } from "../errors.js";
5
+ import { formatAgentPrompt } from "../prompt.js";
6
+ import { cleanTerminalOutput } from "./terminal.js";
7
+ /**
8
+ * Adapter pour les CLIs qui exigent un vrai terminal.
9
+ * Contrairement à `CliAdapter`, stdout/stderr sont fusionnés dans le flux PTY.
10
+ */
11
+ export class CliPtyAdapter {
12
+ name;
13
+ config;
14
+ role;
15
+ contract;
16
+ constructor(name, config) {
17
+ this.name = name;
18
+ this.config = config;
19
+ this.role = config.role;
20
+ this.contract = {
21
+ name,
22
+ kind: "cli-pty",
23
+ capabilities: {
24
+ mode: "pty",
25
+ supportsModelOverride: true,
26
+ supportsFilesystemAccess: true,
27
+ supportsStreaming: false,
28
+ supportsProcessExitCode: true,
29
+ supportsStderr: false
30
+ },
31
+ guarantees: {
32
+ rejectsEmptyOutput: !config.allowEmptyOutput,
33
+ rejectsNonZeroExit: true,
34
+ rejectsTimeout: true,
35
+ returnsRawOutput: true
36
+ }
37
+ };
38
+ }
39
+ async generate(prompt) {
40
+ const renderedPrompt = formatAgentPrompt(prompt);
41
+ const promptMode = this.config.promptMode ?? "stdin";
42
+ const baseArgs = withModelArgs(this.config.args ?? [], this.config.model, this.config.modelArg ?? "--model");
43
+ const args = promptMode === "argument"
44
+ ? [...baseArgs, renderedPrompt]
45
+ : baseArgs;
46
+ return new Promise((resolve, reject) => {
47
+ let output = "";
48
+ let settled = false;
49
+ let hardTimer;
50
+ let term;
51
+ let dataSubscription;
52
+ let exitSubscription;
53
+ const finish = (error, exitCode, kill = true) => {
54
+ if (settled)
55
+ return;
56
+ settled = true;
57
+ clearTimeout(hardTimer);
58
+ dataSubscription?.dispose();
59
+ exitSubscription?.dispose();
60
+ if (kill) {
61
+ try {
62
+ term.kill();
63
+ }
64
+ catch {
65
+ // The PTY may already be closed.
66
+ }
67
+ }
68
+ cleanupPty(term);
69
+ if (error) {
70
+ reject(error);
71
+ return;
72
+ }
73
+ const content = cleanTerminalOutput(output);
74
+ if (exitCode && exitCode !== 0 && !content) {
75
+ reject(createPtyExitError(this.name, exitCode, output));
76
+ return;
77
+ }
78
+ if (!content && !this.config.allowEmptyOutput) {
79
+ reject(new AdapterError("empty-output", this.name, `${this.name} produced empty PTY output.`, {
80
+ raw: output
81
+ }));
82
+ return;
83
+ }
84
+ resolve({
85
+ content,
86
+ raw: output
87
+ });
88
+ };
89
+ try {
90
+ term = spawnPty(resolveExecutable(this.config.command), args, {
91
+ name: "xterm-256color",
92
+ cols: this.config.cols ?? 120,
93
+ rows: this.config.rows ?? 40,
94
+ cwd: process.cwd(),
95
+ env: process.env,
96
+ ...(process.platform !== "win32" ? { encoding: "utf8" } : {}),
97
+ ...(process.platform === "win32" ? { useConpty: true } : {})
98
+ });
99
+ }
100
+ catch (error) {
101
+ reject(new AdapterError("spawn-failed", this.name, `${this.name} failed to start PTY command "${this.config.command}": ${error instanceof Error ? error.message : String(error)}`, {
102
+ command: this.config.command
103
+ }));
104
+ return;
105
+ }
106
+ hardTimer = setTimeout(() => {
107
+ finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ?? 180_000}ms`, {
108
+ timeoutMs: this.config.timeoutMs ?? 180_000
109
+ }));
110
+ }, this.config.timeoutMs ?? 180_000);
111
+ dataSubscription = term.onData((chunk) => {
112
+ output += chunk;
113
+ });
114
+ exitSubscription = term.onExit(({ exitCode }) => {
115
+ finish(undefined, exitCode, false);
116
+ });
117
+ if (promptMode === "stdin") {
118
+ term.write(`${renderedPrompt}\r`);
119
+ }
120
+ });
121
+ }
122
+ }
123
+ function resolveExecutable(command) {
124
+ if (path.isAbsolute(command) || command.includes("\\") || command.includes("/")) {
125
+ return command;
126
+ }
127
+ for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
128
+ const trimmed = directory.trim();
129
+ if (!trimmed)
130
+ continue;
131
+ for (const extension of executableExtensions(command)) {
132
+ const candidate = path.join(trimmed, `${command}${extension}`);
133
+ if (existsSync(candidate)) {
134
+ return candidate;
135
+ }
136
+ }
137
+ }
138
+ return command;
139
+ }
140
+ function cleanupPty(term) {
141
+ const maybeTerm = term;
142
+ try {
143
+ maybeTerm._agent?._cleanUpProcess?.();
144
+ maybeTerm._agent?._conoutSocketWorker?._worker?.terminate?.();
145
+ }
146
+ catch {
147
+ // Best-effort cleanup for Windows ConPTY internals.
148
+ }
149
+ }
150
+ function executableExtensions(command) {
151
+ if (path.extname(command) || process.platform !== "win32") {
152
+ return [""];
153
+ }
154
+ return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
155
+ .split(";")
156
+ .map((extension) => extension.toLowerCase())
157
+ .concat(".ps1", "");
158
+ }
159
+ function withModelArgs(args, model, modelArg) {
160
+ if (!model) {
161
+ return [...args];
162
+ }
163
+ const promptStdinIndex = args.lastIndexOf("-");
164
+ if (promptStdinIndex === args.length - 1) {
165
+ return [
166
+ ...args.slice(0, promptStdinIndex),
167
+ modelArg,
168
+ model,
169
+ ...args.slice(promptStdinIndex)
170
+ ];
171
+ }
172
+ return [...args, modelArg, model];
173
+ }
174
+ function createPtyExitError(adapterName, exitCode, raw) {
175
+ return new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizePtyOutput(raw)}`, {
176
+ exitCode,
177
+ raw
178
+ });
179
+ }
180
+ function summarizePtyOutput(output) {
181
+ const cleaned = cleanTerminalOutput(output);
182
+ return cleaned ? cleaned.slice(-1_200) : "aucune sortie PTY capturee.";
183
+ }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { AdapterError } from "../errors.js";
3
3
  import { formatAgentPrompt } from "../prompt.js";
4
+ import { cleanTerminalOutput } from "./terminal.js";
4
5
  /**
5
6
  * Adapter pour les CLIs batch (Codex, Claude, Gemini…).
6
7
  * Lance un sous-processus, injecte le prompt via stdin ou argument, capture stdout.
@@ -107,17 +108,18 @@ export class CliAdapter {
107
108
  command: this.config.command
108
109
  }));
109
110
  });
110
- child.on("close", (code) => {
111
+ const finishFromExitCode = (code) => {
111
112
  if (code && code !== 0 && !stdout.trim()) {
112
113
  finish(createCliExitError(this.name, code, stderr));
113
114
  return;
114
115
  }
115
116
  finish();
116
- });
117
+ };
118
+ child.on("close", finishFromExitCode);
117
119
  if (promptMode === "stdin") {
118
120
  child.stdin.write(renderedPrompt);
119
- child.stdin.end();
120
121
  }
122
+ child.stdin.end();
121
123
  });
122
124
  }
123
125
  }
@@ -142,9 +144,7 @@ function withModelArgs(args, model, modelArg) {
142
144
  }
143
145
  /** Retire les séquences ANSI et les espaces en tête/fin. */
144
146
  function cleanCliOutput(output) {
145
- return output
146
- .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "")
147
- .trim();
147
+ return cleanTerminalOutput(output);
148
148
  }
149
149
  /**
150
150
  * Construit une `AdapterError` typée depuis un exit code non nul.
@@ -1,10 +1,13 @@
1
1
  import { CliAdapter } from "./cli.js";
2
+ import { CliPtyAdapter } from "./cli-pty.js";
2
3
  import { OllamaAdapter } from "./ollama.js";
3
4
  /** Factory qui instancie l'adapter approprié selon `config.type`. Exhaustive : tout `AgentConfig` valide produit un adapter. */
4
5
  export function createAgent(name, config) {
5
6
  switch (config.type) {
6
7
  case "cli":
7
8
  return new CliAdapter(name, config);
9
+ case "cli-pty":
10
+ return new CliPtyAdapter(name, config);
8
11
  case "ollama":
9
12
  return new OllamaAdapter(name, config);
10
13
  }
@@ -0,0 +1,13 @@
1
+ /** Retire les séquences de contrôle ANSI/OSC et normalise les retours ligne d'une sortie terminal. */
2
+ export function cleanTerminalOutput(output) {
3
+ return output
4
+ .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
5
+ .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "")
6
+ .replace(/\u001b[()][A-Za-z0-9]/g, "")
7
+ .replace(/\u001b[=>]/g, "")
8
+ .replace(/\r\n/g, "\n")
9
+ .replace(/\r/g, "\n")
10
+ .replace(/\u0007/g, "")
11
+ .replace(/\u0000/g, "")
12
+ .trim();
13
+ }
package/dist/config.js CHANGED
@@ -6,8 +6,11 @@ export const LEGACY_CONFIG_PATH = "chicane.config.json";
6
6
  export const CONFIG_DIR_NAME = ".palabre";
7
7
  export const GLOBAL_CONFIG_PATH = path.join(os.homedir(), CONFIG_DIR_NAME, DEFAULT_CONFIG_PATH);
8
8
  export const GLOBAL_LEGACY_CONFIG_PATH = path.join(os.homedir(), CONFIG_DIR_NAME, LEGACY_CONFIG_PATH);
9
+ export const DEFAULT_OLLAMA_MODEL = "nemotron-3-nano:4b";
10
+ export const DEFAULT_OUTPUT_DIR = ".palabre";
9
11
  export const exampleConfig = {
10
- outputDir: ".",
12
+ language: "fr",
13
+ outputDir: DEFAULT_OUTPUT_DIR,
11
14
  defaults: {
12
15
  agentA: "codex",
13
16
  agentB: "claude",
@@ -63,6 +66,19 @@ export const exampleConfig = {
63
66
  role: "reviewer",
64
67
  tier: "primary"
65
68
  },
69
+ antigravity: {
70
+ type: "cli-pty",
71
+ command: "agy",
72
+ args: [
73
+ "--print-timeout",
74
+ "5m0s",
75
+ "--print"
76
+ ],
77
+ promptMode: "argument",
78
+ role: "reviewer",
79
+ tier: "primary",
80
+ timeoutMs: 300_000
81
+ },
66
82
  opencode: {
67
83
  type: "cli",
68
84
  command: "opencode",
@@ -78,7 +94,7 @@ export const exampleConfig = {
78
94
  "ollama-local": {
79
95
  type: "ollama",
80
96
  baseUrl: "http://localhost:11434",
81
- model: "nemotron-3-nano:4b",
97
+ model: DEFAULT_OLLAMA_MODEL,
82
98
  role: "critic",
83
99
  tier: "local",
84
100
  temperature: 0.2,
@@ -87,6 +103,15 @@ export const exampleConfig = {
87
103
  }
88
104
  }
89
105
  };
106
+ /**
107
+ * Résout le dossier d'export effectif.
108
+ * `.` est traité comme l'ancien défaut historique afin de regrouper les exports
109
+ * dans un dossier dédié sans demander de migration manuelle aux utilisateurs.
110
+ */
111
+ export function resolveOutputDir(outputDir) {
112
+ const normalized = outputDir?.trim();
113
+ return !normalized || normalized === "." ? DEFAULT_OUTPUT_DIR : normalized;
114
+ }
90
115
  /** Charge et parse la config depuis `configPath`. Lance une erreur si le fichier est absent ou invalide. */
91
116
  export async function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
92
117
  const resolved = path.resolve(configPath);
@@ -126,7 +151,7 @@ export async function resolveDefaultConfigPath() {
126
151
  /**
127
152
  * Construit une `PalabreConfig` complète à partir des outils détectés localement.
128
153
  * Ajuste `defaults.agentA/agentB/summaryAgent` en fonction de la paire disponible.
129
- * Si aucune paire n'est détectée, `defaults` reste celui de `exampleConfig`.
154
+ * Si aucune paire n'est détectée, seuls les defaults sans agent sont conservés.
130
155
  */
131
156
  export function createConfigFromDiscovery(discovery) {
132
157
  const config = cloneConfig(exampleConfig);
@@ -143,14 +168,26 @@ export function createConfigFromDiscovery(discovery) {
143
168
  ...config.agents.gemini,
144
169
  ...(discovery.gemini.available ? { command: discovery.gemini.command } : {})
145
170
  };
171
+ config.agents.antigravity = {
172
+ ...config.agents.antigravity,
173
+ ...(discovery.antigravity.available ? { command: discovery.antigravity.command } : {})
174
+ };
146
175
  config.agents.opencode = {
147
176
  ...config.agents.opencode,
148
177
  ...(discovery.opencode.available ? { command: discovery.opencode.command } : {})
149
178
  };
150
- config.defaults = {
151
- ...config.defaults,
152
- ...(pair ? { agentA: pair[0], agentB: pair[1], summaryAgent: chooseDefaultSummaryAgent(pair) } : {})
153
- };
179
+ const ollamaAgent = config.agents["ollama-local"];
180
+ if (ollamaAgent?.type === "ollama") {
181
+ ollamaAgent.model = chooseDefaultOllamaModel(discovery);
182
+ }
183
+ config.defaults = pair
184
+ ? {
185
+ ...config.defaults,
186
+ agentA: pair[0],
187
+ agentB: pair[1],
188
+ summaryAgent: chooseDefaultSummaryAgent(pair)
189
+ }
190
+ : { turns: config.defaults?.turns };
154
191
  return config;
155
192
  }
156
193
  /** Écrit `config` sérialisé en JSON dans `configPath`. Crée le répertoire parent si nécessaire. */
@@ -159,8 +196,14 @@ export async function writeExampleConfig(configPath = DEFAULT_CONFIG_PATH, confi
159
196
  await mkdir(path.dirname(resolved), { recursive: true });
160
197
  await writeFile(resolved, `${JSON.stringify(config, null, 2)}\n`, "utf8");
161
198
  }
199
+ function chooseDefaultOllamaModel(discovery) {
200
+ if (discovery.ollama.models.includes(DEFAULT_OLLAMA_MODEL)) {
201
+ return DEFAULT_OLLAMA_MODEL;
202
+ }
203
+ return discovery.ollama.models[0] ?? DEFAULT_OLLAMA_MODEL;
204
+ }
162
205
  function chooseDefaultSummaryAgent(pair) {
163
- for (const preferred of ["claude", "codex", "gemini"]) {
206
+ for (const preferred of ["claude", "codex", "antigravity", "gemini"]) {
164
207
  if (pair.includes(preferred)) {
165
208
  return preferred;
166
209
  }
@@ -180,12 +223,16 @@ function chooseDefaultPair(discovery) {
180
223
  if (discovery.opencode.available && discovery.ollama.available) {
181
224
  return ["opencode", "ollama-local"];
182
225
  }
226
+ if (discovery.antigravity.available && discovery.ollama.available) {
227
+ return ["antigravity", "ollama-local"];
228
+ }
183
229
  if (discovery.gemini.available && discovery.ollama.available) {
184
230
  return ["gemini", "ollama-local"];
185
231
  }
186
232
  const cliAgents = [
187
233
  discovery.codex.available ? "codex" : undefined,
188
234
  discovery.claude.available ? "claude" : undefined,
235
+ discovery.antigravity.available ? "antigravity" : undefined,
189
236
  discovery.opencode.available ? "opencode" : undefined,
190
237
  discovery.gemini.available ? "gemini" : undefined
191
238
  ].filter((agent) => Boolean(agent));
@@ -7,47 +7,47 @@ import { DEFAULT_TURNS, MAX_TURNS, turnsOrDefault, validateTurns } from "./limit
7
7
  * Fonctionne en mode TTY (readline) et en mode piped (stdin lu en avance).
8
8
  * Écrit la config sur disque si l'utilisateur confirme ; sort sans modifier si l'utilisateur quitte.
9
9
  */
10
- export async function runConfigWizard(configPath, config) {
10
+ export async function runConfigWizard(configPath, config, messages) {
11
11
  const choices = Object.entries(config.agents).map(([name, agentConfig]) => ({ name, config: agentConfig }));
12
12
  if (choices.length < 2) {
13
- throw new Error("La config doit contenir au moins deux agents pour définir des paramètres par défaut.");
13
+ throw new Error(messages.config.wizardNeedsTwoAgents);
14
14
  }
15
15
  const rl = await createQuestioner();
16
16
  try {
17
- console.log("PALABRE - Configuration");
18
- console.log("À tout moment: Ctrl+C pour interrompre, ou tape q, quit ou exit dans un prompt pour quitter.");
17
+ console.log(messages.config.wizardTitle);
18
+ console.log(messages.config.wizardQuitHint);
19
19
  console.log("");
20
- console.log("Fichier de configuration :");
20
+ console.log(messages.config.wizardConfigFile);
21
21
  console.log(` ${configPath}`);
22
22
  console.log("");
23
- console.log("Paramètres par défaut actuels :");
24
- console.log(` ${config.defaults ? formatDefaults(config.defaults) : "Aucun"}`);
23
+ console.log(messages.config.wizardCurrentDefaults);
24
+ console.log(` ${config.defaults ? formatDefaults(config.defaults, messages) : messages.config.wizardNoDefaults}`);
25
25
  console.log("");
26
- console.log("Que veux-tu faire ?");
27
- console.log(" 1) Définir des paramètres par défaut");
28
- console.log(" 2) Supprimer les paramètres par défaut");
29
- console.log(" 3) Quitter sans modifier");
30
- const action = await askChoice(rl, "Tape le numéro de ton choix", "1", ["1", "2", "3"]);
26
+ console.log(messages.config.wizardActionQuestion);
27
+ console.log(` 1) ${messages.config.wizardActionSetDefaults}`);
28
+ console.log(` 2) ${messages.config.wizardActionClearDefaults}`);
29
+ console.log(` 3) ${messages.config.wizardActionExit}`);
30
+ const action = await askChoice(rl, messages.config.wizardChoicePrompt, "1", ["1", "2", "3"], messages);
31
31
  if (!action || action === "3") {
32
- console.log("Config inchangée.");
32
+ console.log(messages.config.wizardUnchanged);
33
33
  return;
34
34
  }
35
35
  if (action === "2") {
36
36
  delete config.defaults;
37
37
  await writeExampleConfig(configPath, config);
38
- console.log(`Paramètres par défaut supprimés dans ${configPath}.`);
38
+ console.log(messages.config.wizardCleared(configPath));
39
39
  return;
40
40
  }
41
- const agentA = await askAgent(rl, choices, "Agent A", "Choisis l'agent A, celui qui répondra en premier.", config.defaults?.agentA);
41
+ const agentA = await askAgent(rl, choices, messages.config.wizardAgentADescription, config.defaults?.agentA, messages);
42
42
  if (!agentA)
43
43
  return;
44
- const agentB = await askAgent(rl, choices.filter((choice) => choice.name !== agentA), "Agent B", "Choisis l'agent B, celui qui répondra en second.", config.defaults?.agentB === agentA ? undefined : config.defaults?.agentB);
44
+ const agentB = await askAgent(rl, choices.filter((choice) => choice.name !== agentA), messages.config.wizardAgentBDescription, config.defaults?.agentB === agentA ? undefined : config.defaults?.agentB, messages);
45
45
  if (!agentB)
46
46
  return;
47
- const turns = await askNumber(rl, "Nombre de réponses par défaut", turnsOrDefault(config.defaults?.turns), Boolean(config.defaults?.turns));
47
+ const turns = await askNumber(rl, messages.config.wizardTurnsLabel, turnsOrDefault(config.defaults?.turns), Boolean(config.defaults?.turns), messages);
48
48
  if (turns === undefined)
49
49
  return;
50
- const summaryAgent = await askSummaryAgent(rl, choices, config.defaults?.summaryAgent ?? agentB, Boolean(config.defaults?.summaryAgent), agentB);
50
+ const summaryAgent = await askSummaryAgent(rl, choices, config.defaults?.summaryAgent ?? agentB, Boolean(config.defaults?.summaryAgent), agentB, messages);
51
51
  if (summaryAgent === undefined)
52
52
  return;
53
53
  config.defaults = {
@@ -57,7 +57,7 @@ export async function runConfigWizard(configPath, config) {
57
57
  turns
58
58
  };
59
59
  await writeExampleConfig(configPath, config);
60
- console.log(`Paramètres par défaut définis dans ${configPath}: ${formatDefaults(config.defaults)}.`);
60
+ console.log(messages.config.wizardDefaultsSet(configPath, formatDefaults(config.defaults, messages)));
61
61
  }
62
62
  finally {
63
63
  rl.close();
@@ -89,9 +89,9 @@ async function readPipedLines() {
89
89
  }
90
90
  return raw ? raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n") : [];
91
91
  }
92
- async function askAgent(rl, choices, _label, description, defaultName) {
92
+ async function askAgent(rl, choices, description, defaultName, messages) {
93
93
  const fallback = choices.find((choice) => choice.name === defaultName)?.name ?? choices[0]?.name;
94
- const fallbackLabel = defaultName ? "Actuel" : "Suggestion";
94
+ const fallbackLabel = defaultName ? messages.config.wizardCurrent : messages.config.wizardSuggestion;
95
95
  console.log("");
96
96
  console.log(description);
97
97
  console.log(`${fallbackLabel} : ${fallback}`);
@@ -100,7 +100,7 @@ async function askAgent(rl, choices, _label, description, defaultName) {
100
100
  console.log(` ${index + 1}) ${formatAgentLine(choice)}`);
101
101
  });
102
102
  while (true) {
103
- const answer = await rl.question(`Tape un numéro ou un nom d'agent (Entrée = ${fallback}) : `);
103
+ const answer = await rl.question(messages.config.wizardAgentPrompt(fallback));
104
104
  const value = answer.trim();
105
105
  if (isQuit(value))
106
106
  return undefined;
@@ -113,22 +113,22 @@ async function askAgent(rl, choices, _label, description, defaultName) {
113
113
  if (choices.some((choice) => choice.name === value)) {
114
114
  return value;
115
115
  }
116
- console.log("Choix invalide. Tape un numéro, un nom d'agent, Entrée ou q.");
116
+ console.log(messages.config.wizardInvalidAgentChoice);
117
117
  }
118
118
  }
119
- async function askSummaryAgent(rl, choices, defaultName, hasCurrentDefault, agentB) {
119
+ async function askSummaryAgent(rl, choices, defaultName, hasCurrentDefault, agentB, messages) {
120
120
  const fallback = choices.some((choice) => choice.name === defaultName) ? defaultName : choices[0]?.name;
121
- const fallbackLabel = hasCurrentDefault ? "Actuel" : "Suggestion";
121
+ const fallbackLabel = hasCurrentDefault ? messages.config.wizardCurrent : messages.config.wizardSuggestion;
122
122
  console.log("");
123
- console.log("Agent de synthèse par défaut");
124
- console.log(`${fallbackLabel} : ${fallback}${!hasCurrentDefault && fallback === agentB ? " (agent B)" : ""}`);
123
+ console.log(messages.config.wizardSummaryTitle);
124
+ console.log(`${fallbackLabel} : ${fallback}${!hasCurrentDefault && fallback === agentB ? ` (${messages.config.wizardAgentBHint})` : ""}`);
125
125
  console.log("");
126
- console.log(" 0) Aucun agent de synthèse par défaut");
126
+ console.log(` 0) ${messages.config.wizardNoSummary}`);
127
127
  choices.forEach((choice, index) => {
128
128
  console.log(` ${index + 1}) ${formatAgentLine(choice)}`);
129
129
  });
130
130
  while (true) {
131
- const answer = await rl.question(`Tape un numéro, un nom d'agent, ou 0 pour aucun (Entrée = ${fallback}) : `);
131
+ const answer = await rl.question(messages.config.wizardSummaryPrompt(fallback));
132
132
  const value = answer.trim();
133
133
  if (isQuit(value))
134
134
  return undefined;
@@ -143,12 +143,12 @@ async function askSummaryAgent(rl, choices, defaultName, hasCurrentDefault, agen
143
143
  if (choices.some((choice) => choice.name === value)) {
144
144
  return value;
145
145
  }
146
- console.log("Choix invalide. Tape un numéro, un nom d'agent, 0, Entrée ou q.");
146
+ console.log(messages.config.wizardInvalidSummaryChoice);
147
147
  }
148
148
  }
149
- async function askChoice(rl, label, defaultValue, allowed) {
149
+ async function askChoice(rl, label, defaultValue, allowed, messages) {
150
150
  while (true) {
151
- const answer = await rl.question(`${label} (Entrée = ${defaultValue}) : `);
151
+ const answer = await rl.question(messages.config.wizardChoiceQuestion(label, defaultValue));
152
152
  const value = answer.trim();
153
153
  if (isQuit(value))
154
154
  return undefined;
@@ -156,17 +156,17 @@ async function askChoice(rl, label, defaultValue, allowed) {
156
156
  return defaultValue;
157
157
  if (allowed.includes(value))
158
158
  return value;
159
- console.log(`Choix invalide. Valeurs: ${allowed.join(", ")}, Entrée ou q.`);
159
+ console.log(messages.config.wizardInvalidChoice(allowed.join(", ")));
160
160
  }
161
161
  }
162
- async function askNumber(rl, label, defaultValue, hasCurrentDefault) {
163
- const fallbackLabel = hasCurrentDefault ? "Actuel" : "Suggestion";
162
+ async function askNumber(rl, label, defaultValue, hasCurrentDefault, messages) {
163
+ const fallbackLabel = hasCurrentDefault ? messages.config.wizardCurrent : messages.config.wizardSuggestion;
164
164
  console.log("");
165
165
  console.log(label);
166
166
  console.log(`${fallbackLabel} : ${defaultValue}`);
167
167
  console.log("");
168
168
  while (true) {
169
- const answer = await rl.question(`Tape le nombre total de réponses du débat (Entrée = ${defaultValue}) : `);
169
+ const answer = await rl.question(messages.config.wizardTurnsPrompt(defaultValue));
170
170
  const value = answer.trim();
171
171
  if (isQuit(value))
172
172
  return undefined;
@@ -175,21 +175,26 @@ async function askNumber(rl, label, defaultValue, hasCurrentDefault) {
175
175
  const parsed = Number(value);
176
176
  if (Number.isInteger(parsed)) {
177
177
  try {
178
- validateTurns(parsed, "Le nombre de réponses");
178
+ validateTurns(parsed, messages.config.wizardTurnsLabel, messages);
179
179
  return parsed;
180
180
  }
181
181
  catch {
182
182
  // Show the user-facing wizard hint below.
183
183
  }
184
184
  }
185
- console.log(`Entre un nombre entier entre 1 et ${MAX_TURNS}, Entrée ou q.`);
185
+ console.log(messages.config.wizardTurnsInvalid(MAX_TURNS));
186
186
  }
187
187
  }
188
188
  function formatAgentLine(choice) {
189
189
  return `${choice.name.padEnd(12)} ${choice.config.type} / ${choice.config.role}`;
190
190
  }
191
- function formatDefaults(defaults) {
192
- return `${defaults.agentA ?? "?"} <-> ${defaults.agentB ?? "?"}, réponses: ${turnsOrDefault(defaults.turns ?? DEFAULT_TURNS)}${defaults.summaryAgent ? `, synthèse: ${defaults.summaryAgent}` : ""}`;
191
+ function formatDefaults(defaults, messages) {
192
+ return messages.config.wizardDefaults({
193
+ agentA: defaults.agentA,
194
+ agentB: defaults.agentB,
195
+ turns: turnsOrDefault(defaults.turns ?? DEFAULT_TURNS),
196
+ summaryAgent: defaults.summaryAgent
197
+ });
193
198
  }
194
199
  function isQuit(value) {
195
200
  return ["q", "quit", "exit"].includes(value.toLowerCase());