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/dist/context.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { createTranslator } from "./i18n.js";
3
4
  const MAX_FILE_BYTES = 64 * 1024;
4
5
  const MAX_TOTAL_BYTES = 192 * 1024;
5
6
  const DEFAULT_EXCLUDED_NAMES = new Set([
@@ -32,8 +33,8 @@ const TEXT_EXTENSIONS = new Set([
32
33
  * Mode strict (`--files`) : charge uniquement les fichiers explicitement listés.
33
34
  * Lève une erreur si un chemin est un dossier, un binaire, ou dépasse 64 KiB / 192 KiB au total.
34
35
  */
35
- export async function loadProjectFiles(paths, cwd = process.cwd()) {
36
- const result = await loadProjectInputs(paths, [], cwd);
36
+ export async function loadProjectFiles(paths, cwd = process.cwd(), messages = createTranslator("fr")) {
37
+ const result = await loadProjectInputs(paths, [], cwd, messages);
37
38
  return result.files;
38
39
  }
39
40
  /**
@@ -41,13 +42,14 @@ export async function loadProjectFiles(paths, cwd = process.cwd()) {
41
42
  * Les fichiers explicites sont chargés en premier et comptent dans le budget total.
42
43
  * Les chemins de contexte acceptent fichiers et dossiers ; les fichiers ignorés génèrent des warnings, pas des erreurs.
43
44
  */
44
- export async function loadProjectInputs(filePaths, contextPaths, cwd = process.cwd()) {
45
+ export async function loadProjectInputs(filePaths, contextPaths, cwd = process.cwd(), messages = createTranslator("fr")) {
45
46
  const state = {
46
47
  files: [],
47
48
  warnings: [],
48
49
  seen: new Set(),
49
50
  totalBytes: 0,
50
- gitignoreRules: await loadGitignoreRules(cwd)
51
+ gitignoreRules: await loadGitignoreRules(cwd),
52
+ messages
51
53
  };
52
54
  await addExplicitFiles(filePaths, cwd, state);
53
55
  await addContextPaths(contextPaths, cwd, state);
@@ -62,14 +64,14 @@ async function addExplicitFiles(paths, cwd, state) {
62
64
  const absolutePath = path.resolve(cwd, inputPath);
63
65
  const fileStat = await stat(absolutePath);
64
66
  if (!fileStat.isFile()) {
65
- throw new Error(`Le contexte fichier doit pointer vers un fichier: ${inputPath}`);
67
+ throw new Error(state.messages.context.explicitMustBeFile(inputPath));
66
68
  }
67
69
  if (fileStat.size > MAX_FILE_BYTES) {
68
- throw new Error(`Fichier trop gros pour le contexte: ${inputPath} (${fileStat.size} bytes, max ${MAX_FILE_BYTES})`);
70
+ throw new Error(state.messages.context.explicitTooLarge(inputPath, fileStat.size, MAX_FILE_BYTES));
69
71
  }
70
72
  const content = await readFile(absolutePath, "utf8");
71
73
  if (content.includes("\u0000")) {
72
- throw new Error(`Fichier binaire ou non texte refuse: ${inputPath}`);
74
+ throw new Error(state.messages.context.explicitBinary(inputPath));
73
75
  }
74
76
  addFileToState(cwd, state, absolutePath, content, fileStat.size, "explicit");
75
77
  }
@@ -84,7 +86,7 @@ async function addContextPaths(paths, cwd, state) {
84
86
  continue;
85
87
  }
86
88
  if (!fileStat.isDirectory()) {
87
- state.warnings.push(`Contexte ignore (ni fichier ni dossier): ${inputPath}`);
89
+ state.warnings.push(state.messages.context.ignoredNotFileOrDirectory(inputPath));
88
90
  continue;
89
91
  }
90
92
  await walkContextDirectory(absolutePath, cwd, state);
@@ -113,21 +115,21 @@ async function addContextFile(absolutePath, cwd, state) {
113
115
  return;
114
116
  }
115
117
  if (!isLikelyTextFile(absolutePath)) {
116
- state.warnings.push(`Contexte ignore (extension non texte): ${relativePath}`);
118
+ state.warnings.push(state.messages.context.ignoredNonTextExtension(relativePath));
117
119
  return;
118
120
  }
119
121
  const fileStat = await stat(absolutePath);
120
122
  if (fileStat.size > MAX_FILE_BYTES) {
121
- state.warnings.push(`Contexte ignore (fichier trop gros): ${relativePath} (${fileStat.size} bytes)`);
123
+ state.warnings.push(state.messages.context.ignoredTooLarge(relativePath, fileStat.size));
122
124
  return;
123
125
  }
124
126
  if (state.totalBytes + fileStat.size > MAX_TOTAL_BYTES) {
125
- state.warnings.push(`Contexte ignore (limite totale atteinte): ${relativePath}`);
127
+ state.warnings.push(state.messages.context.ignoredTotalLimit(relativePath));
126
128
  return;
127
129
  }
128
130
  const content = await readFile(absolutePath, "utf8");
129
131
  if (content.includes("\u0000")) {
130
- state.warnings.push(`Contexte ignore (binaire detecte): ${relativePath}`);
132
+ state.warnings.push(state.messages.context.ignoredBinary(relativePath));
131
133
  return;
132
134
  }
133
135
  addFileToState(cwd, state, absolutePath, content, fileStat.size, "context");
@@ -138,9 +140,9 @@ function addFileToState(cwd, state, absolutePath, content, sizeBytes, source) {
138
140
  }
139
141
  if (state.totalBytes + sizeBytes > MAX_TOTAL_BYTES) {
140
142
  if (source === "explicit") {
141
- throw new Error(`Contexte fichiers trop gros (${state.totalBytes + sizeBytes} bytes, max ${MAX_TOTAL_BYTES})`);
143
+ throw new Error(state.messages.context.explicitTotalTooLarge(state.totalBytes + sizeBytes, MAX_TOTAL_BYTES));
142
144
  }
143
- state.warnings.push(`Contexte ignore (limite totale atteinte): ${normalizePath(path.relative(cwd, absolutePath))}`);
145
+ state.warnings.push(state.messages.context.ignoredTotalLimit(normalizePath(path.relative(cwd, absolutePath))));
144
146
  return;
145
147
  }
146
148
  state.seen.add(absolutePath);
package/dist/discovery.js CHANGED
@@ -5,10 +5,11 @@ import path from "node:path";
5
5
  * Sur Windows, tente `claude.exe` avant `claude`.
6
6
  */
7
7
  export async function discoverLocalTools() {
8
- const [codex, claude, gemini, opencode, ollamaCommand] = await Promise.all([
8
+ const [codex, claude, gemini, antigravity, opencode, ollamaCommand] = await Promise.all([
9
9
  detectCommand("codex"),
10
10
  detectFirstCommand(process.platform === "win32" ? ["claude.exe", "claude"] : ["claude"]),
11
11
  detectCommand("gemini"),
12
+ detectCommand("agy"),
12
13
  detectCommand("opencode"),
13
14
  detectCommand("ollama")
14
15
  ]);
@@ -17,6 +18,7 @@ export async function discoverLocalTools() {
17
18
  codex,
18
19
  claude,
19
20
  gemini,
21
+ antigravity,
20
22
  opencode,
21
23
  ollama: {
22
24
  ...ollamaServer,
package/dist/doctor.js CHANGED
@@ -1,218 +1,238 @@
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("Antigravity CLI", discovery.antigravity.available, discovery.antigravity.command, discovery.antigravity.path, t));
45
+ lines.push(formatCommand("OpenCode CLI", discovery.opencode.available, discovery.opencode.command, discovery.opencode.path, t));
34
46
  lines.push(discovery.ollama.available
35
- ? ok(`Ollama API joignable: ${discovery.ollama.baseUrl} (${discovery.ollama.models.length} modele(s))`)
47
+ ? ok(t.doctor.ollamaReachable(discovery.ollama.baseUrl, discovery.ollama.models.length))
36
48
  : 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);
49
+ ? t.doctor.ollamaInstalledNoApi(discovery.ollama.baseUrl, formatErrorSuffix(discovery.ollama.error))
50
+ : t.doctor.ollamaMissingNoApi(discovery.ollama.baseUrl, formatErrorSuffix(discovery.ollama.error))));
51
+ inspectDetectedMissingAgents(config, discovery, lines, t);
52
+ inspectAgents(config, discovery, lines, t);
53
+ return render(lines, plain, t);
42
54
  }
43
- async function loadConfigSafely(configPath, lines) {
55
+ async function loadConfigSafely(configPath) {
44
56
  try {
45
- const config = await loadConfig(configPath);
46
- lines.push(ok("Config JSON lisible."));
47
- return config;
57
+ return await loadConfig(configPath);
48
58
  }
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."));
59
+ catch {
53
60
  return undefined;
54
61
  }
55
62
  }
56
- async function inspectConfig(config, lines) {
63
+ async function getConfigLoadError(configPath) {
64
+ try {
65
+ await loadConfig(configPath);
66
+ return "";
67
+ }
68
+ catch (loadError) {
69
+ return loadError instanceof Error ? loadError.message : String(loadError);
70
+ }
71
+ }
72
+ async function inspectConfig(config, lines, t) {
57
73
  const agentNames = Object.keys(config.agents ?? {});
58
74
  if (agentNames.length === 0) {
59
- lines.push(error("Aucun agent configure."));
75
+ lines.push(error(t.doctor.noAgents));
60
76
  }
61
77
  else if (agentNames.length === 1) {
62
- lines.push(warn(`1 agent configure: ${agentNames[0]}. Palabre fonctionne mieux avec au moins deux agents.`));
78
+ lines.push(warn(t.doctor.oneAgent(agentNames[0])));
63
79
  }
64
80
  else {
65
- lines.push(ok(`${agentNames.length} agent(s) configure(s): ${agentNames.join(", ")}`));
81
+ lines.push(ok(t.doctor.agentCount(agentNames.length, agentNames.join(", "))));
66
82
  }
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);
83
+ inspectDefaultAgent("defaults.agentA", config.defaults?.agentA, config, lines, t);
84
+ inspectDefaultAgent("defaults.agentB", config.defaults?.agentB, config, lines, t);
85
+ inspectDefaultPair(config, lines, t);
86
+ inspectDefaultTurns(config.defaults?.turns, lines, t);
71
87
  if (config.defaults?.summaryAgent) {
72
- inspectDefaultAgent("defaults.summaryAgent", config.defaults.summaryAgent, config, lines);
88
+ inspectDefaultAgent("defaults.summaryAgent", config.defaults.summaryAgent, config, lines, t);
73
89
  }
74
90
  else {
75
- lines.push(warn("defaults.summaryAgent absent: la synthese utilisera agentB."));
91
+ lines.push(warn(t.doctor.summaryAgentMissing));
76
92
  }
77
- await inspectOutputDir(config.outputDir, lines);
93
+ await inspectOutputDir(config.outputDir, lines, t);
78
94
  }
79
- function inspectDefaultAgent(label, agentName, config, lines) {
95
+ function inspectDefaultAgent(label, agentName, config, lines, t) {
80
96
  if (!agentName) {
81
- lines.push(warn(`${label} absent.`));
97
+ lines.push(warn(t.doctor.defaultAgentMissing(label)));
82
98
  return;
83
99
  }
84
100
  if (!config.agents[agentName]) {
85
- lines.push(error(`${label} pointe vers un agent inconnu: ${agentName}`));
101
+ lines.push(error(t.doctor.defaultAgentUnknown(label, agentName)));
86
102
  return;
87
103
  }
88
- lines.push(ok(`${label}: ${agentName}`));
104
+ lines.push(ok(t.doctor.defaultAgentOk(label, agentName)));
89
105
  }
90
- function inspectDefaultPair(config, lines) {
106
+ function inspectDefaultPair(config, lines, t) {
91
107
  const { agentA, agentB } = config.defaults ?? {};
92
108
  if (!agentA || !agentB) {
93
- lines.push(warn("Paire par defaut incomplete. Action: `palabre config --set-defaults <agentA> <agentB>`."));
109
+ lines.push(warn(t.doctor.defaultPairIncomplete));
94
110
  return;
95
111
  }
96
112
  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.`));
113
+ lines.push(warn(t.doctor.sameDefaultAgent(agentA)));
98
114
  }
99
115
  }
100
- function inspectDefaultTurns(turns, lines) {
116
+ function inspectDefaultTurns(turns, lines, t) {
101
117
  const value = turns ?? DEFAULT_TURNS;
102
118
  if (turns === undefined) {
103
- lines.push(info(`defaults.turns absent: Palabre utilisera ${DEFAULT_TURNS} reponses.`));
119
+ lines.push(info(t.doctor.defaultTurnsMissing(DEFAULT_TURNS)));
104
120
  return;
105
121
  }
106
122
  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}.`));
123
+ lines.push(error(t.doctor.defaultTurnsInvalid(String(turns), MAX_TURNS)));
108
124
  return;
109
125
  }
110
- lines.push(ok(`defaults.turns: ${value}`));
126
+ lines.push(ok(t.doctor.defaultTurnsOk(value)));
111
127
  }
112
- async function inspectOutputDir(outputDir, lines) {
113
- const resolved = path.resolve(outputDir ?? ".");
128
+ async function inspectOutputDir(outputDir, lines, t) {
129
+ const effectiveOutputDir = resolveOutputDir(outputDir);
130
+ const resolved = path.resolve(effectiveOutputDir);
114
131
  if (!outputDir) {
115
- lines.push(info(`outputDir absent: les exports seront ecrits dans le dossier courant (${resolved}).`));
116
- return;
132
+ lines.push(info(t.doctor.outputDirMissing(resolved)));
133
+ }
134
+ else if (outputDir.trim() === ".") {
135
+ lines.push(info(t.doctor.outputDirLegacy(resolved)));
117
136
  }
118
137
  try {
119
138
  const stats = await stat(resolved);
120
139
  if (!stats.isDirectory()) {
121
- lines.push(error(`outputDir pointe vers un fichier, pas un dossier: ${resolved}`));
140
+ lines.push(error(t.doctor.outputDirIsFile(resolved)));
122
141
  return;
123
142
  }
124
- lines.push(ok(`outputDir configure: ${resolved}`));
143
+ lines.push(ok(t.doctor.outputDirConfigured(resolved)));
125
144
  }
126
145
  catch {
127
- lines.push(warn(`outputDir n'existe pas encore: ${resolved}. Palabre tentera de le creer au moment de l'export.`));
146
+ lines.push(warn(t.doctor.outputDirWillCreate(resolved)));
128
147
  }
129
148
  }
130
- function inspectDetectedMissingAgents(config, discovery, lines) {
149
+ function inspectDetectedMissingAgents(config, discovery, lines, t) {
131
150
  const missing = detectedAgentNames(discovery).filter((name) => !config.agents[name]);
132
151
  if (missing.length === 0) {
133
152
  return;
134
153
  }
135
- lines.push(warn(`Agent(s) detecte(s) mais absent(s) de la config: ${missing.join(", ")}. Action: lance ` + "`palabre config --sync-agents`."));
154
+ lines.push(warn(t.doctor.detectedMissing(missing.join(", "))));
136
155
  }
137
- function inspectAgents(config, discovery, lines) {
138
- lines.push(info("Agents configures:"));
156
+ function inspectAgents(config, discovery, lines, t) {
157
+ lines.push(info(t.doctor.configuredAgents, "agents"));
139
158
  for (const [name, agent] of Object.entries(config.agents)) {
140
- inspectAgentShape(name, agent, lines);
141
- if (agent.type === "cli") {
142
- inspectCliAgent(name, agent, discovery, lines);
159
+ inspectAgentShape(name, agent, lines, t);
160
+ if (agent.type === "cli" || agent.type === "cli-pty") {
161
+ inspectCliAgent(name, agent, discovery, lines, t);
143
162
  continue;
144
163
  }
145
- inspectOllamaAgent(name, agent, discovery, lines);
164
+ inspectOllamaAgent(name, agent, discovery, lines, t);
146
165
  }
147
166
  }
148
- function inspectAgentShape(name, agent, lines) {
167
+ function inspectAgentShape(name, agent, lines, t) {
149
168
  if (!agent.role) {
150
- lines.push(error(`${name}: role absent.`));
169
+ lines.push(error(t.doctor.roleMissing(name)));
151
170
  }
152
- if (agent.type === "cli") {
171
+ if (agent.type === "cli" || agent.type === "cli-pty") {
153
172
  if (!agent.command || !agent.command.trim()) {
154
- lines.push(error(`${name}: command CLI absent.`));
173
+ lines.push(error(t.doctor.cliCommandMissing(name)));
155
174
  }
156
175
  if (agent.promptMode && !["stdin", "argument"].includes(agent.promptMode)) {
157
- lines.push(error(`${name}: promptMode invalide (${agent.promptMode}). Valeurs attendues: stdin ou argument.`));
176
+ lines.push(error(t.doctor.promptModeInvalid(name, agent.promptMode)));
158
177
  }
159
178
  if (agent.timeoutMs !== undefined && (!Number.isFinite(agent.timeoutMs) || agent.timeoutMs <= 0)) {
160
- lines.push(error(`${name}: timeoutMs doit etre un nombre positif.`));
179
+ lines.push(error(t.doctor.positiveTimeout(name, "timeoutMs")));
161
180
  }
162
181
  if (agent.idleTimeoutMs !== undefined && (!Number.isFinite(agent.idleTimeoutMs) || agent.idleTimeoutMs <= 0)) {
163
- lines.push(error(`${name}: idleTimeoutMs doit etre un nombre positif.`));
182
+ lines.push(error(t.doctor.positiveTimeout(name, "idleTimeoutMs")));
164
183
  }
165
184
  return;
166
185
  }
167
186
  if (!agent.model || !agent.model.trim()) {
168
- lines.push(error(`${name}: modele Ollama absent.`));
187
+ lines.push(error(t.doctor.ollamaModelMissing(name)));
169
188
  }
170
189
  if (agent.baseUrl && !/^https?:\/\//.test(agent.baseUrl)) {
171
- lines.push(error(`${name}: baseUrl Ollama invalide (${agent.baseUrl}). Attendu: http://... ou https://...`));
190
+ lines.push(error(t.doctor.ollamaBaseUrlInvalid(name, agent.baseUrl)));
172
191
  }
173
192
  if (agent.timeoutMs !== undefined && (!Number.isFinite(agent.timeoutMs) || agent.timeoutMs <= 0)) {
174
- lines.push(error(`${name}: timeoutMs doit etre un nombre positif.`));
193
+ lines.push(error(t.doctor.positiveTimeout(name, "timeoutMs")));
175
194
  }
176
195
  }
177
- function inspectCliAgent(name, agent, discovery, lines) {
196
+ function inspectCliAgent(name, agent, discovery, lines, t) {
178
197
  const known = knownCliDetection(agent.command, discovery);
179
198
  const prefix = `${name} [cli:${agent.role}] command=${agent.command}`;
180
199
  if (!known) {
181
- lines.push(info(`${prefix} (commande custom non verifiee par doctor)`));
200
+ lines.push(info(t.doctor.customCommand(prefix)));
182
201
  return;
183
202
  }
184
203
  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.`));
204
+ ? ok(t.doctor.cliDetected(prefix, known.path ?? known.command))
205
+ : warn(t.doctor.cliMissing(prefix)));
187
206
  }
188
- function inspectOllamaAgent(name, agent, discovery, lines) {
207
+ function inspectOllamaAgent(name, agent, discovery, lines, t) {
189
208
  const prefix = `${name} [ollama:${agent.role}] model=${agent.model}`;
190
209
  if (!discovery.ollama.available) {
191
- lines.push(warn(`${prefix} non verifiable: API Ollama non joignable. Action: demarre Ollama ou corrige baseUrl.`));
210
+ lines.push(warn(t.doctor.ollamaNotVerifiable(prefix)));
192
211
  return;
193
212
  }
194
213
  if (agent.validateModel === false) {
195
- lines.push(info(`${prefix} non valide car validateModel=false.`));
214
+ lines.push(info(t.doctor.ollamaValidateFalse(prefix)));
196
215
  return;
197
216
  }
198
217
  const installed = discovery.ollama.models.includes(agent.model);
199
218
  lines.push(installed
200
- ? ok(`${prefix} installe.`)
201
- : warn(`${prefix} absent. Action: lance ` + "`ollama pull " + agent.model + "`" + " ou utilise `--pull-models`."));
219
+ ? ok(t.doctor.ollamaInstalled(prefix))
220
+ : warn(t.doctor.ollamaMissing(prefix, agent.model)));
202
221
  }
203
222
  function detectedAgentNames(discovery) {
204
223
  return [
205
224
  discovery.codex.available ? "codex" : undefined,
206
225
  discovery.claude.available ? "claude" : undefined,
207
226
  discovery.gemini.available ? "gemini" : undefined,
227
+ discovery.antigravity.available ? "antigravity" : undefined,
208
228
  discovery.opencode.available ? "opencode" : undefined,
209
229
  discovery.ollama.available ? "ollama-local" : undefined
210
230
  ].filter((name) => Boolean(name));
211
231
  }
212
- function formatCommand(label, available, command, resolvedPath) {
232
+ function formatCommand(label, available, command, resolvedPath, t) {
213
233
  return available
214
- ? ok(`${label}: detecte (${resolvedPath ?? command})`)
215
- : warn(`${label}: non detecte dans PATH.`);
234
+ ? ok(t.doctor.commandDetected(label, resolvedPath ?? command))
235
+ : warn(t.doctor.commandMissing(label));
216
236
  }
217
237
  function knownCliDetection(command, discovery) {
218
238
  const normalized = path.basename(command).toLowerCase().replace(/\.(exe|cmd|bat)$/i, "");
@@ -222,21 +242,25 @@ function knownCliDetection(command, discovery) {
222
242
  return discovery.claude;
223
243
  if (normalized === "gemini")
224
244
  return discovery.gemini;
245
+ if (normalized === "agy")
246
+ return discovery.antigravity;
247
+ if (normalized === "antigravity")
248
+ return discovery.antigravity;
225
249
  if (normalized === "opencode")
226
250
  return discovery.opencode;
227
251
  return undefined;
228
252
  }
229
- function render(lines, plain) {
253
+ function render(lines, plain, t) {
230
254
  const hasErrors = lines.some((line) => line.level === "error");
231
255
  return {
232
256
  ok: !hasErrors,
233
- output: plain ? renderPlain(lines) : renderPretty(lines)
257
+ output: plain ? renderPlain(lines, t) : renderPretty(lines, t)
234
258
  };
235
259
  }
236
- function renderPlain(lines) {
237
- return lines.map(formatLine).join("\n");
260
+ function renderPlain(lines, t) {
261
+ return lines.map((line) => formatLine(line, t)).join("\n");
238
262
  }
239
- function renderPretty(lines) {
263
+ function renderPretty(lines, t) {
240
264
  const configLines = [];
241
265
  const toolLines = [];
242
266
  const agentLines = [];
@@ -244,17 +268,17 @@ function renderPretty(lines) {
244
268
  let current = "config";
245
269
  let cwd = process.cwd();
246
270
  for (const line of lines) {
247
- if (line.text === "PALABRE doctor")
271
+ if (line.marker === "title")
248
272
  continue;
249
- if (line.text.startsWith("Dossier courant: ")) {
250
- cwd = line.text.replace("Dossier courant: ", "");
273
+ if (line.marker === "cwd") {
274
+ cwd = line.text.replace(/^.*?: /, "");
251
275
  continue;
252
276
  }
253
- if (line.text === "Outils locaux:") {
277
+ if (line.marker === "tools") {
254
278
  current = "tools";
255
279
  continue;
256
280
  }
257
- if (line.text === "Agents configures:") {
281
+ if (line.marker === "agents") {
258
282
  current = "agents";
259
283
  continue;
260
284
  }
@@ -273,68 +297,54 @@ function renderPretty(lines) {
273
297
  }
274
298
  const errorCount = lines.filter((line) => line.level === "error").length;
275
299
  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";
300
+ const status = t.doctor.status(errorCount, warnCount);
279
301
  return [
280
- ...renderDoctorHeader(status),
302
+ ...renderDoctorHeader(status, t),
281
303
  "",
282
- ...renderSection("Configuration", [info(`Dossier courant: ${cwd}`), ...configLines]),
304
+ ...renderSection(t.doctor.sections.configuration, [info(t.doctor.currentDirectory(cwd)), ...configLines], t),
283
305
  "",
284
- ...renderSection("Outils locaux", toolLines),
306
+ ...renderSection(t.doctor.sections.tools, toolLines, t),
285
307
  "",
286
- ...renderSection("Agents", agentLines),
287
- ...(actionLines.length > 0 ? ["", ...renderSection("A verifier", actionLines)] : []),
308
+ ...renderSection(t.doctor.sections.agents, agentLines, t),
309
+ ...(actionLines.length > 0 ? ["", ...renderSection(t.doctor.sections.check, actionLines, t)] : []),
288
310
  ""
289
311
  ].join("\n");
290
312
  }
291
- function renderDoctorHeader(status) {
292
- const title = "PALABRE doctor";
313
+ function renderDoctorHeader(status, t) {
314
+ const title = t.doctor.title;
293
315
  return [
294
316
  `┌─ ${title} ${"─".repeat(Math.max(1, 58 - title.length))}`,
295
- `│ Statut: ${status}`,
317
+ `│ ${t.doctor.statusLabel}: ${status}`,
296
318
  `└${"─".repeat(73)}`
297
319
  ];
298
320
  }
299
- function renderSection(title, lines) {
321
+ function renderSection(title, lines, t) {
300
322
  if (lines.length === 0) {
301
- return [title, " INFO Rien a afficher."];
323
+ return [title, ` ${t.doctor.prettyLevelLabels.info} ${t.doctor.nothingToDisplay}`];
302
324
  }
303
325
  return [
304
326
  title,
305
327
  "─".repeat(Math.max(16, title.length + 8)),
306
- ...lines.map((line) => ` ${formatPrettyLine(line)}`)
328
+ ...lines.map((line) => ` ${formatPrettyLine(line, t)}`)
307
329
  ];
308
330
  }
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}`;
331
+ function formatPrettyLine(line, t) {
332
+ return `${t.doctor.prettyLevelLabels[line.level]} ${line.text}`;
317
333
  }
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}`;
334
+ function formatLine(line, t) {
335
+ return `[${t.doctor.levelLabels[line.level]}] ${line.text}`;
326
336
  }
327
- function ok(text) {
328
- return { level: "ok", text };
337
+ function ok(text, marker) {
338
+ return { level: "ok", text, marker };
329
339
  }
330
- function warn(text) {
331
- return { level: "warn", text };
340
+ function warn(text, marker) {
341
+ return { level: "warn", text, marker };
332
342
  }
333
- function error(text) {
334
- return { level: "error", text };
343
+ function error(text, marker) {
344
+ return { level: "error", text, marker };
335
345
  }
336
- function info(text) {
337
- return { level: "info", text };
346
+ function info(text, marker) {
347
+ return { level: "info", text, marker };
338
348
  }
339
349
  function formatErrorSuffix(errorMessage) {
340
350
  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
  }