palabre 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/doctor.js CHANGED
@@ -1,204 +1,222 @@
1
1
  import path from "node:path";
2
2
  import { stat } from "node:fs/promises";
3
- import { configExists, loadConfig, resolveDefaultConfigPath } from "./config.js";
3
+ import { configExists, loadConfig, resolveDefaultConfigPath, resolveOutputDir } from "./config.js";
4
4
  import { discoverLocalTools } from "./discovery.js";
5
+ import { createTranslator, resolveLanguage } from "./i18n.js";
5
6
  import { DEFAULT_TURNS, MAX_TURNS } from "./limits.js";
6
7
  /**
7
8
  * Exécute le diagnostic complet : config, outils locaux et agents.
8
9
  * Retourne toujours un résultat (pas de throw) ; les erreurs de config sont reportées comme lignes `error`.
9
10
  */
10
- export async function runDoctor(explicitConfigPath, plain = false) {
11
+ export async function runDoctor(explicitConfigPath, plain = false, explicitLanguage) {
11
12
  const lines = [];
12
13
  const configPath = explicitConfigPath ?? await resolveDefaultConfigPath();
13
14
  const hasConfig = await configExists(configPath);
14
- lines.push(info("PALABRE doctor"));
15
- lines.push(info(`Dossier courant: ${process.cwd()}`));
15
+ const config = hasConfig ? await loadConfigSafely(configPath) : undefined;
16
+ const language = resolveLanguage({
17
+ explicitLanguage,
18
+ configLanguage: config?.language
19
+ });
20
+ const t = createTranslator(language);
21
+ lines.push(info(t.doctor.title, "title"));
22
+ lines.push(info(t.doctor.currentDirectory(process.cwd()), "cwd"));
16
23
  lines.push(hasConfig
17
- ? ok(`Config trouvee: ${configPath}`)
18
- : error(`Config absente: ${configPath}`));
24
+ ? ok(t.doctor.configFound(configPath))
25
+ : error(t.doctor.configMissing(configPath)));
19
26
  if (!hasConfig) {
20
- lines.push(info("Action: lance `palabre init` pour creer la config globale, ou `palabre init --local` pour une config projet."));
21
- return render(lines, plain);
27
+ lines.push(info(t.doctor.noConfigAction));
28
+ return render(lines, plain, t);
22
29
  }
23
- const config = await loadConfigSafely(configPath, lines);
24
30
  if (!config) {
25
- return render(lines, plain);
31
+ const loadError = await getConfigLoadError(configPath);
32
+ lines.push(error(t.doctor.configUnreadable(loadError)));
33
+ lines.push(info(t.doctor.configUnreadableAction));
34
+ return render(lines, plain, t);
26
35
  }
27
- await inspectConfig(config, lines);
36
+ lines.push(ok(t.doctor.configReadable));
37
+ lines.push(ok(t.doctor.interfaceLanguage(language)));
38
+ await inspectConfig(config, lines, t);
28
39
  const discovery = await discoverLocalTools();
29
- lines.push(info("Outils locaux:"));
30
- lines.push(formatCommand("Codex CLI", discovery.codex.available, discovery.codex.command, discovery.codex.path));
31
- lines.push(formatCommand("Claude CLI", discovery.claude.available, discovery.claude.command, discovery.claude.path));
32
- lines.push(formatCommand("Gemini CLI", discovery.gemini.available, discovery.gemini.command, discovery.gemini.path));
33
- lines.push(formatCommand("OpenCode CLI", discovery.opencode.available, discovery.opencode.command, discovery.opencode.path));
40
+ lines.push(info(t.doctor.localTools, "tools"));
41
+ lines.push(formatCommand("Codex CLI", discovery.codex.available, discovery.codex.command, discovery.codex.path, t));
42
+ lines.push(formatCommand("Claude CLI", discovery.claude.available, discovery.claude.command, discovery.claude.path, t));
43
+ lines.push(formatCommand("Gemini CLI", discovery.gemini.available, discovery.gemini.command, discovery.gemini.path, t));
44
+ lines.push(formatCommand("OpenCode CLI", discovery.opencode.available, discovery.opencode.command, discovery.opencode.path, t));
34
45
  lines.push(discovery.ollama.available
35
- ? ok(`Ollama API joignable: ${discovery.ollama.baseUrl} (${discovery.ollama.models.length} modele(s))`)
46
+ ? ok(t.doctor.ollamaReachable(discovery.ollama.baseUrl, discovery.ollama.models.length))
36
47
  : warn(discovery.ollama.commandAvailable
37
- ? `Ollama installe mais API non joignable: ${discovery.ollama.baseUrl}${formatErrorSuffix(discovery.ollama.error)}`
38
- : `Ollama non detecte et API non joignable: ${discovery.ollama.baseUrl}${formatErrorSuffix(discovery.ollama.error)}`));
39
- inspectDetectedMissingAgents(config, discovery, lines);
40
- inspectAgents(config, discovery, lines);
41
- return render(lines, plain);
48
+ ? t.doctor.ollamaInstalledNoApi(discovery.ollama.baseUrl, formatErrorSuffix(discovery.ollama.error))
49
+ : t.doctor.ollamaMissingNoApi(discovery.ollama.baseUrl, formatErrorSuffix(discovery.ollama.error))));
50
+ inspectDetectedMissingAgents(config, discovery, lines, t);
51
+ inspectAgents(config, discovery, lines, t);
52
+ return render(lines, plain, t);
42
53
  }
43
- async function loadConfigSafely(configPath, lines) {
54
+ async function loadConfigSafely(configPath) {
44
55
  try {
45
- const config = await loadConfig(configPath);
46
- lines.push(ok("Config JSON lisible."));
47
- return config;
56
+ return await loadConfig(configPath);
48
57
  }
49
- catch (loadError) {
50
- const message = loadError instanceof Error ? loadError.message : String(loadError);
51
- lines.push(error(`Config illisible: ${message}`));
52
- lines.push(info("Action: corrige le JSON ou relance `palabre init --config <path>` vers un nouveau fichier."));
58
+ catch {
53
59
  return undefined;
54
60
  }
55
61
  }
56
- async function inspectConfig(config, lines) {
62
+ async function getConfigLoadError(configPath) {
63
+ try {
64
+ await loadConfig(configPath);
65
+ return "";
66
+ }
67
+ catch (loadError) {
68
+ return loadError instanceof Error ? loadError.message : String(loadError);
69
+ }
70
+ }
71
+ async function inspectConfig(config, lines, t) {
57
72
  const agentNames = Object.keys(config.agents ?? {});
58
73
  if (agentNames.length === 0) {
59
- lines.push(error("Aucun agent configure."));
74
+ lines.push(error(t.doctor.noAgents));
60
75
  }
61
76
  else if (agentNames.length === 1) {
62
- lines.push(warn(`1 agent configure: ${agentNames[0]}. Palabre fonctionne mieux avec au moins deux agents.`));
77
+ lines.push(warn(t.doctor.oneAgent(agentNames[0])));
63
78
  }
64
79
  else {
65
- lines.push(ok(`${agentNames.length} agent(s) configure(s): ${agentNames.join(", ")}`));
80
+ lines.push(ok(t.doctor.agentCount(agentNames.length, agentNames.join(", "))));
66
81
  }
67
- inspectDefaultAgent("defaults.agentA", config.defaults?.agentA, config, lines);
68
- inspectDefaultAgent("defaults.agentB", config.defaults?.agentB, config, lines);
69
- inspectDefaultPair(config, lines);
70
- inspectDefaultTurns(config.defaults?.turns, lines);
82
+ inspectDefaultAgent("defaults.agentA", config.defaults?.agentA, config, lines, t);
83
+ inspectDefaultAgent("defaults.agentB", config.defaults?.agentB, config, lines, t);
84
+ inspectDefaultPair(config, lines, t);
85
+ inspectDefaultTurns(config.defaults?.turns, lines, t);
71
86
  if (config.defaults?.summaryAgent) {
72
- inspectDefaultAgent("defaults.summaryAgent", config.defaults.summaryAgent, config, lines);
87
+ inspectDefaultAgent("defaults.summaryAgent", config.defaults.summaryAgent, config, lines, t);
73
88
  }
74
89
  else {
75
- lines.push(warn("defaults.summaryAgent absent: la synthese utilisera agentB."));
90
+ lines.push(warn(t.doctor.summaryAgentMissing));
76
91
  }
77
- await inspectOutputDir(config.outputDir, lines);
92
+ await inspectOutputDir(config.outputDir, lines, t);
78
93
  }
79
- function inspectDefaultAgent(label, agentName, config, lines) {
94
+ function inspectDefaultAgent(label, agentName, config, lines, t) {
80
95
  if (!agentName) {
81
- lines.push(warn(`${label} absent.`));
96
+ lines.push(warn(t.doctor.defaultAgentMissing(label)));
82
97
  return;
83
98
  }
84
99
  if (!config.agents[agentName]) {
85
- lines.push(error(`${label} pointe vers un agent inconnu: ${agentName}`));
100
+ lines.push(error(t.doctor.defaultAgentUnknown(label, agentName)));
86
101
  return;
87
102
  }
88
- lines.push(ok(`${label}: ${agentName}`));
103
+ lines.push(ok(t.doctor.defaultAgentOk(label, agentName)));
89
104
  }
90
- function inspectDefaultPair(config, lines) {
105
+ function inspectDefaultPair(config, lines, t) {
91
106
  const { agentA, agentB } = config.defaults ?? {};
92
107
  if (!agentA || !agentB) {
93
- lines.push(warn("Paire par defaut incomplete. Action: `palabre config --set-defaults <agentA> <agentB>`."));
108
+ lines.push(warn(t.doctor.defaultPairIncomplete));
94
109
  return;
95
110
  }
96
111
  if (agentA === agentB) {
97
- lines.push(warn(`defaults.agentA et defaults.agentB pointent vers le meme agent (${agentA}). C'est possible, mais souvent moins utile qu'une vraie paire.`));
112
+ lines.push(warn(t.doctor.sameDefaultAgent(agentA)));
98
113
  }
99
114
  }
100
- function inspectDefaultTurns(turns, lines) {
115
+ function inspectDefaultTurns(turns, lines, t) {
101
116
  const value = turns ?? DEFAULT_TURNS;
102
117
  if (turns === undefined) {
103
- lines.push(info(`defaults.turns absent: Palabre utilisera ${DEFAULT_TURNS} reponses.`));
118
+ lines.push(info(t.doctor.defaultTurnsMissing(DEFAULT_TURNS)));
104
119
  return;
105
120
  }
106
121
  if (!Number.isInteger(value) || value < 1 || value > MAX_TURNS) {
107
- lines.push(error(`defaults.turns invalide: ${String(turns)}. Action: choisis un entier entre 1 et ${MAX_TURNS}.`));
122
+ lines.push(error(t.doctor.defaultTurnsInvalid(String(turns), MAX_TURNS)));
108
123
  return;
109
124
  }
110
- lines.push(ok(`defaults.turns: ${value}`));
125
+ lines.push(ok(t.doctor.defaultTurnsOk(value)));
111
126
  }
112
- async function inspectOutputDir(outputDir, lines) {
113
- const resolved = path.resolve(outputDir ?? ".");
127
+ async function inspectOutputDir(outputDir, lines, t) {
128
+ const effectiveOutputDir = resolveOutputDir(outputDir);
129
+ const resolved = path.resolve(effectiveOutputDir);
114
130
  if (!outputDir) {
115
- lines.push(info(`outputDir absent: les exports seront ecrits dans le dossier courant (${resolved}).`));
116
- return;
131
+ lines.push(info(t.doctor.outputDirMissing(resolved)));
132
+ }
133
+ else if (outputDir.trim() === ".") {
134
+ lines.push(info(t.doctor.outputDirLegacy(resolved)));
117
135
  }
118
136
  try {
119
137
  const stats = await stat(resolved);
120
138
  if (!stats.isDirectory()) {
121
- lines.push(error(`outputDir pointe vers un fichier, pas un dossier: ${resolved}`));
139
+ lines.push(error(t.doctor.outputDirIsFile(resolved)));
122
140
  return;
123
141
  }
124
- lines.push(ok(`outputDir configure: ${resolved}`));
142
+ lines.push(ok(t.doctor.outputDirConfigured(resolved)));
125
143
  }
126
144
  catch {
127
- lines.push(warn(`outputDir n'existe pas encore: ${resolved}. Palabre tentera de le creer au moment de l'export.`));
145
+ lines.push(warn(t.doctor.outputDirWillCreate(resolved)));
128
146
  }
129
147
  }
130
- function inspectDetectedMissingAgents(config, discovery, lines) {
148
+ function inspectDetectedMissingAgents(config, discovery, lines, t) {
131
149
  const missing = detectedAgentNames(discovery).filter((name) => !config.agents[name]);
132
150
  if (missing.length === 0) {
133
151
  return;
134
152
  }
135
- lines.push(warn(`Agent(s) detecte(s) mais absent(s) de la config: ${missing.join(", ")}. Action: lance ` + "`palabre config --sync-agents`."));
153
+ lines.push(warn(t.doctor.detectedMissing(missing.join(", "))));
136
154
  }
137
- function inspectAgents(config, discovery, lines) {
138
- lines.push(info("Agents configures:"));
155
+ function inspectAgents(config, discovery, lines, t) {
156
+ lines.push(info(t.doctor.configuredAgents, "agents"));
139
157
  for (const [name, agent] of Object.entries(config.agents)) {
140
- inspectAgentShape(name, agent, lines);
158
+ inspectAgentShape(name, agent, lines, t);
141
159
  if (agent.type === "cli") {
142
- inspectCliAgent(name, agent, discovery, lines);
160
+ inspectCliAgent(name, agent, discovery, lines, t);
143
161
  continue;
144
162
  }
145
- inspectOllamaAgent(name, agent, discovery, lines);
163
+ inspectOllamaAgent(name, agent, discovery, lines, t);
146
164
  }
147
165
  }
148
- function inspectAgentShape(name, agent, lines) {
166
+ function inspectAgentShape(name, agent, lines, t) {
149
167
  if (!agent.role) {
150
- lines.push(error(`${name}: role absent.`));
168
+ lines.push(error(t.doctor.roleMissing(name)));
151
169
  }
152
170
  if (agent.type === "cli") {
153
171
  if (!agent.command || !agent.command.trim()) {
154
- lines.push(error(`${name}: command CLI absent.`));
172
+ lines.push(error(t.doctor.cliCommandMissing(name)));
155
173
  }
156
174
  if (agent.promptMode && !["stdin", "argument"].includes(agent.promptMode)) {
157
- lines.push(error(`${name}: promptMode invalide (${agent.promptMode}). Valeurs attendues: stdin ou argument.`));
175
+ lines.push(error(t.doctor.promptModeInvalid(name, agent.promptMode)));
158
176
  }
159
177
  if (agent.timeoutMs !== undefined && (!Number.isFinite(agent.timeoutMs) || agent.timeoutMs <= 0)) {
160
- lines.push(error(`${name}: timeoutMs doit etre un nombre positif.`));
178
+ lines.push(error(t.doctor.positiveTimeout(name, "timeoutMs")));
161
179
  }
162
180
  if (agent.idleTimeoutMs !== undefined && (!Number.isFinite(agent.idleTimeoutMs) || agent.idleTimeoutMs <= 0)) {
163
- lines.push(error(`${name}: idleTimeoutMs doit etre un nombre positif.`));
181
+ lines.push(error(t.doctor.positiveTimeout(name, "idleTimeoutMs")));
164
182
  }
165
183
  return;
166
184
  }
167
185
  if (!agent.model || !agent.model.trim()) {
168
- lines.push(error(`${name}: modele Ollama absent.`));
186
+ lines.push(error(t.doctor.ollamaModelMissing(name)));
169
187
  }
170
188
  if (agent.baseUrl && !/^https?:\/\//.test(agent.baseUrl)) {
171
- lines.push(error(`${name}: baseUrl Ollama invalide (${agent.baseUrl}). Attendu: http://... ou https://...`));
189
+ lines.push(error(t.doctor.ollamaBaseUrlInvalid(name, agent.baseUrl)));
172
190
  }
173
191
  if (agent.timeoutMs !== undefined && (!Number.isFinite(agent.timeoutMs) || agent.timeoutMs <= 0)) {
174
- lines.push(error(`${name}: timeoutMs doit etre un nombre positif.`));
192
+ lines.push(error(t.doctor.positiveTimeout(name, "timeoutMs")));
175
193
  }
176
194
  }
177
- function inspectCliAgent(name, agent, discovery, lines) {
195
+ function inspectCliAgent(name, agent, discovery, lines, t) {
178
196
  const known = knownCliDetection(agent.command, discovery);
179
197
  const prefix = `${name} [cli:${agent.role}] command=${agent.command}`;
180
198
  if (!known) {
181
- lines.push(info(`${prefix} (commande custom non verifiee par doctor)`));
199
+ lines.push(info(t.doctor.customCommand(prefix)));
182
200
  return;
183
201
  }
184
202
  lines.push(known.available
185
- ? ok(`${prefix} detectee (${known.path ?? known.command})`)
186
- : warn(`${prefix} non detectee dans PATH. Action: installe/authentifie la CLI ou corrige command dans la config.`));
203
+ ? ok(t.doctor.cliDetected(prefix, known.path ?? known.command))
204
+ : warn(t.doctor.cliMissing(prefix)));
187
205
  }
188
- function inspectOllamaAgent(name, agent, discovery, lines) {
206
+ function inspectOllamaAgent(name, agent, discovery, lines, t) {
189
207
  const prefix = `${name} [ollama:${agent.role}] model=${agent.model}`;
190
208
  if (!discovery.ollama.available) {
191
- lines.push(warn(`${prefix} non verifiable: API Ollama non joignable. Action: demarre Ollama ou corrige baseUrl.`));
209
+ lines.push(warn(t.doctor.ollamaNotVerifiable(prefix)));
192
210
  return;
193
211
  }
194
212
  if (agent.validateModel === false) {
195
- lines.push(info(`${prefix} non valide car validateModel=false.`));
213
+ lines.push(info(t.doctor.ollamaValidateFalse(prefix)));
196
214
  return;
197
215
  }
198
216
  const installed = discovery.ollama.models.includes(agent.model);
199
217
  lines.push(installed
200
- ? ok(`${prefix} installe.`)
201
- : warn(`${prefix} absent. Action: lance ` + "`ollama pull " + agent.model + "`" + " ou utilise `--pull-models`."));
218
+ ? ok(t.doctor.ollamaInstalled(prefix))
219
+ : warn(t.doctor.ollamaMissing(prefix, agent.model)));
202
220
  }
203
221
  function detectedAgentNames(discovery) {
204
222
  return [
@@ -209,10 +227,10 @@ function detectedAgentNames(discovery) {
209
227
  discovery.ollama.available ? "ollama-local" : undefined
210
228
  ].filter((name) => Boolean(name));
211
229
  }
212
- function formatCommand(label, available, command, resolvedPath) {
230
+ function formatCommand(label, available, command, resolvedPath, t) {
213
231
  return available
214
- ? ok(`${label}: detecte (${resolvedPath ?? command})`)
215
- : warn(`${label}: non detecte dans PATH.`);
232
+ ? ok(t.doctor.commandDetected(label, resolvedPath ?? command))
233
+ : warn(t.doctor.commandMissing(label));
216
234
  }
217
235
  function knownCliDetection(command, discovery) {
218
236
  const normalized = path.basename(command).toLowerCase().replace(/\.(exe|cmd|bat)$/i, "");
@@ -226,17 +244,17 @@ function knownCliDetection(command, discovery) {
226
244
  return discovery.opencode;
227
245
  return undefined;
228
246
  }
229
- function render(lines, plain) {
247
+ function render(lines, plain, t) {
230
248
  const hasErrors = lines.some((line) => line.level === "error");
231
249
  return {
232
250
  ok: !hasErrors,
233
- output: plain ? renderPlain(lines) : renderPretty(lines)
251
+ output: plain ? renderPlain(lines, t) : renderPretty(lines, t)
234
252
  };
235
253
  }
236
- function renderPlain(lines) {
237
- return lines.map(formatLine).join("\n");
254
+ function renderPlain(lines, t) {
255
+ return lines.map((line) => formatLine(line, t)).join("\n");
238
256
  }
239
- function renderPretty(lines) {
257
+ function renderPretty(lines, t) {
240
258
  const configLines = [];
241
259
  const toolLines = [];
242
260
  const agentLines = [];
@@ -244,17 +262,17 @@ function renderPretty(lines) {
244
262
  let current = "config";
245
263
  let cwd = process.cwd();
246
264
  for (const line of lines) {
247
- if (line.text === "PALABRE doctor")
265
+ if (line.marker === "title")
248
266
  continue;
249
- if (line.text.startsWith("Dossier courant: ")) {
250
- cwd = line.text.replace("Dossier courant: ", "");
267
+ if (line.marker === "cwd") {
268
+ cwd = line.text.replace(/^.*?: /, "");
251
269
  continue;
252
270
  }
253
- if (line.text === "Outils locaux:") {
271
+ if (line.marker === "tools") {
254
272
  current = "tools";
255
273
  continue;
256
274
  }
257
- if (line.text === "Agents configures:") {
275
+ if (line.marker === "agents") {
258
276
  current = "agents";
259
277
  continue;
260
278
  }
@@ -273,68 +291,54 @@ function renderPretty(lines) {
273
291
  }
274
292
  const errorCount = lines.filter((line) => line.level === "error").length;
275
293
  const warnCount = lines.filter((line) => line.level === "warn").length;
276
- const status = errorCount > 0
277
- ? `${errorCount} erreur(s), ${warnCount} avertissement(s)`
278
- : warnCount > 0 ? `${warnCount} avertissement(s)` : "OK";
294
+ const status = t.doctor.status(errorCount, warnCount);
279
295
  return [
280
- ...renderDoctorHeader(status),
296
+ ...renderDoctorHeader(status, t),
281
297
  "",
282
- ...renderSection("Configuration", [info(`Dossier courant: ${cwd}`), ...configLines]),
298
+ ...renderSection(t.doctor.sections.configuration, [info(t.doctor.currentDirectory(cwd)), ...configLines], t),
283
299
  "",
284
- ...renderSection("Outils locaux", toolLines),
300
+ ...renderSection(t.doctor.sections.tools, toolLines, t),
285
301
  "",
286
- ...renderSection("Agents", agentLines),
287
- ...(actionLines.length > 0 ? ["", ...renderSection("A verifier", actionLines)] : []),
302
+ ...renderSection(t.doctor.sections.agents, agentLines, t),
303
+ ...(actionLines.length > 0 ? ["", ...renderSection(t.doctor.sections.check, actionLines, t)] : []),
288
304
  ""
289
305
  ].join("\n");
290
306
  }
291
- function renderDoctorHeader(status) {
292
- const title = "PALABRE doctor";
307
+ function renderDoctorHeader(status, t) {
308
+ const title = t.doctor.title;
293
309
  return [
294
310
  `┌─ ${title} ${"─".repeat(Math.max(1, 58 - title.length))}`,
295
- `│ Statut: ${status}`,
311
+ `│ ${t.doctor.statusLabel}: ${status}`,
296
312
  `└${"─".repeat(73)}`
297
313
  ];
298
314
  }
299
- function renderSection(title, lines) {
315
+ function renderSection(title, lines, t) {
300
316
  if (lines.length === 0) {
301
- return [title, " INFO Rien a afficher."];
317
+ return [title, ` ${t.doctor.prettyLevelLabels.info} ${t.doctor.nothingToDisplay}`];
302
318
  }
303
319
  return [
304
320
  title,
305
321
  "─".repeat(Math.max(16, title.length + 8)),
306
- ...lines.map((line) => ` ${formatPrettyLine(line)}`)
322
+ ...lines.map((line) => ` ${formatPrettyLine(line, t)}`)
307
323
  ];
308
324
  }
309
- function formatPrettyLine(line) {
310
- const labels = {
311
- ok: "OK ",
312
- warn: "WARN ",
313
- error: "ERREUR",
314
- info: "INFO "
315
- };
316
- return `${labels[line.level]} ${line.text}`;
325
+ function formatPrettyLine(line, t) {
326
+ return `${t.doctor.prettyLevelLabels[line.level]} ${line.text}`;
317
327
  }
318
- function formatLine(line) {
319
- const labels = {
320
- ok: "OK",
321
- warn: "WARN",
322
- error: "ERREUR",
323
- info: "INFO"
324
- };
325
- return `[${labels[line.level]}] ${line.text}`;
328
+ function formatLine(line, t) {
329
+ return `[${t.doctor.levelLabels[line.level]}] ${line.text}`;
326
330
  }
327
- function ok(text) {
328
- return { level: "ok", text };
331
+ function ok(text, marker) {
332
+ return { level: "ok", text, marker };
329
333
  }
330
- function warn(text) {
331
- return { level: "warn", text };
334
+ function warn(text, marker) {
335
+ return { level: "warn", text, marker };
332
336
  }
333
- function error(text) {
334
- return { level: "error", text };
337
+ function error(text, marker) {
338
+ return { level: "error", text, marker };
335
339
  }
336
- function info(text) {
337
- return { level: "info", text };
340
+ function info(text, marker) {
341
+ return { level: "info", text, marker };
338
342
  }
339
343
  function formatErrorSuffix(errorMessage) {
340
344
  return errorMessage ? ` (${errorMessage})` : "";
package/dist/errors.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createTranslator } from "./i18n.js";
1
2
  /**
2
3
  * Erreur typée levée par les adapters.
3
4
  * `kind` est stable et utilisé par l'orchestrateur pour classifier l'échec sans inspecter le message.
@@ -15,35 +16,7 @@ export class AdapterError extends Error {
15
16
  }
16
17
  }
17
18
  /** Formate le message d'erreur avec une suggestion actionnable selon `error.kind`. */
18
- export function formatAdapterError(error) {
19
- const hint = hintForFailure(error.kind);
20
- return hint ? `${error.message}\nSuggestion: ${hint}` : error.message;
21
- }
22
- function hintForFailure(kind) {
23
- switch (kind) {
24
- case "command-not-found":
25
- return "Verifie que la CLI est installee, authentifiee et disponible dans le PATH.";
26
- case "spawn-failed":
27
- return "Sur Windows, essaye le wrapper .cmd ou active \"shell\": true dans la config agent.";
28
- case "timeout":
29
- return "Augmente timeoutMs ou teste la commande directement dans le terminal.";
30
- case "idle-timeout":
31
- return "Desactive idleTimeoutMs pour les CLIs IA qui restent silencieuses pendant la generation.";
32
- case "empty-output":
33
- return "Teste la commande en dehors de Palabre et verifie que le prompt est bien lu via stdin ou argument.";
34
- case "usage-limit":
35
- return "Attends la fenetre indiquee par la CLI, change de modele ou relance avec un autre agent/preset disponible.";
36
- case "non-zero-exit":
37
- return "Teste la commande directement, puis ajuste args, permissions, modele ou authentification de la CLI.";
38
- case "model-unavailable":
39
- return "Installe le modele Ollama ou relance avec --pull-models pour autoriser le telechargement.";
40
- case "unsupported-model":
41
- return "Verifie le nom du modele, ton abonnement, ou retire --model-a/--model-b/--summary-model pour laisser la CLI utiliser son modele par defaut.";
42
- case "model-pull-failed":
43
- return "Verifie le nom du modele, ta connexion et l'espace disque disponible.";
44
- case "http-error":
45
- return "Verifie que le service local est lance et que baseUrl est correct.";
46
- default:
47
- return undefined;
48
- }
19
+ export function formatAdapterError(error, messages = createTranslator("fr")) {
20
+ const hint = messages.adapterErrors.hint(error.kind);
21
+ return hint ? `${error.message}\n${messages.adapterErrors.suggestionPrefix}: ${hint}` : error.message;
49
22
  }
package/dist/i18n.js ADDED
@@ -0,0 +1,30 @@
1
+ import { createTranslator } from "./messages/index.js";
2
+ export const DEFAULT_LANGUAGE = "fr";
3
+ export const SUPPORTED_LANGUAGES = ["fr", "en"];
4
+ /**
5
+ * Valide une langue Palabre.
6
+ * Le contrat reste volontairement strict tant que l'interface ne supporte que
7
+ * le français et l'anglais.
8
+ */
9
+ export function parseLanguage(value, source = "language") {
10
+ const normalized = value?.trim().toLowerCase();
11
+ if (!normalized) {
12
+ return undefined;
13
+ }
14
+ if (SUPPORTED_LANGUAGES.includes(normalized)) {
15
+ return normalized;
16
+ }
17
+ throw new Error(createTranslator(DEFAULT_LANGUAGE).common.invalidLanguage(source, value ?? "", SUPPORTED_LANGUAGES));
18
+ }
19
+ /**
20
+ * Résout la langue de l'interface selon la précédence :
21
+ * flag CLI explicite -> `PALABRE_LANGUAGE` -> config -> français.
22
+ */
23
+ export function resolveLanguage(options = {}) {
24
+ const env = options.env ?? process.env;
25
+ return parseLanguage(options.explicitLanguage, "--language")
26
+ ?? parseLanguage(env.PALABRE_LANGUAGE, "PALABRE_LANGUAGE")
27
+ ?? parseLanguage(options.configLanguage, "config.language")
28
+ ?? DEFAULT_LANGUAGE;
29
+ }
30
+ export { createTranslator };