palabre 0.9.1 → 0.10.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 (51) hide show
  1. package/dist/adapters/cli-pty.js +30 -10
  2. package/dist/adapters/cli-shared.js +73 -0
  3. package/dist/adapters/cli.js +40 -77
  4. package/dist/adapters/index.js +1 -0
  5. package/dist/adapters/ollama.js +32 -32
  6. package/dist/adapters/terminal.js +1 -0
  7. package/dist/args.js +1 -0
  8. package/dist/commands/agents.js +7 -1
  9. package/dist/commands/context.js +7 -1
  10. package/dist/commands/history.js +6 -1
  11. package/dist/commands/init.js +8 -3
  12. package/dist/commands/presets.js +6 -1
  13. package/dist/commands/shared.js +5 -1
  14. package/dist/commands/update.js +6 -1
  15. package/dist/config.js +17 -1
  16. package/dist/configWizard.js +5 -4
  17. package/dist/context.js +1 -0
  18. package/dist/contextScan.js +4 -3
  19. package/dist/discovery.js +1 -0
  20. package/dist/doctor.js +1 -0
  21. package/dist/errors.js +4 -0
  22. package/dist/exec.js +1 -0
  23. package/dist/history.js +10 -0
  24. package/dist/i18n.js +2 -0
  25. package/dist/index.js +151 -112
  26. package/dist/limits.js +4 -0
  27. package/dist/messages/adapter-errors.js +26 -2
  28. package/dist/messages/config.js +6 -0
  29. package/dist/messages/index.js +1 -0
  30. package/dist/messages/renderers.js +10 -2
  31. package/dist/messages/tui.js +8 -2
  32. package/dist/new.js +1 -0
  33. package/dist/ollamaUrl.js +20 -0
  34. package/dist/orchestrator.js +103 -150
  35. package/dist/output.js +1 -0
  36. package/dist/presets.js +1 -0
  37. package/dist/prompt.js +1 -0
  38. package/dist/renderers/console.js +1 -1
  39. package/dist/renderers/tui-prompts.js +339 -0
  40. package/dist/renderers/tui-renderer.js +224 -0
  41. package/dist/renderers/tui-screens.js +352 -0
  42. package/dist/renderers/tui-theme.js +356 -0
  43. package/dist/renderers/tui.js +7 -1086
  44. package/dist/runOptions.js +33 -2
  45. package/dist/session.js +1 -0
  46. package/dist/tuiController.js +61 -16
  47. package/dist/tuiState.js +4 -0
  48. package/dist/types.js +1 -0
  49. package/dist/update.js +1 -0
  50. package/dist/version.js +1 -0
  51. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
+ /** @file Adapter pseudo-terminal (`node-pty`) pour les CLIs qui exigent une vraie console (ex. Antigravity `agy`). */
1
2
  import { existsSync } from "node:fs";
2
3
  import path from "node:path";
3
- import { AdapterError } from "../errors.js";
4
+ import { AdapterError, cancelledError } from "../errors.js";
5
+ import { createTranslator } from "../i18n.js";
4
6
  import { executableExtensions } from "../exec.js";
5
7
  import { formatAgentPrompt } from "../prompt.js";
6
- import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS, withModelArgs } from "./cli-shared.js";
8
+ import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS, extractPtyUsageLimitMessage, withModelArgs } from "./cli-shared.js";
7
9
  import { cleanTerminalOutput } from "./terminal.js";
8
10
  /**
9
11
  * Adapter pour les CLIs qui exigent un vrai terminal.
@@ -42,6 +44,7 @@ export class CliPtyAdapter {
42
44
  throw cancelledError(this.name);
43
45
  }
44
46
  const renderedPrompt = formatAgentPrompt(prompt);
47
+ const errorMessages = createTranslator(prompt.language ?? "fr").adapterErrors;
45
48
  const promptMode = this.config.promptMode ?? "stdin";
46
49
  const baseArgs = withModelArgs(this.config.args ?? [], this.config.model, this.config.modelArg ?? "--model");
47
50
  const args = promptMode === "argument"
@@ -82,8 +85,18 @@ export class CliPtyAdapter {
82
85
  return;
83
86
  }
84
87
  const content = cleanTerminalOutput(output);
88
+ // Le PTY fusionne stdout/stderr : seuls les diagnostics autonomes ou machine
89
+ // sont acceptés pour éviter de rejeter une réponse normale parlant de rate-limit.
90
+ const usageLimitMessage = extractPtyUsageLimitMessage(content);
91
+ if (usageLimitMessage) {
92
+ reject(new AdapterError("usage-limit", this.name, errorMessages.usageLimit(this.name, usageLimitMessage), {
93
+ ...(exitCode === undefined || exitCode === 0 ? {} : { exitCode }),
94
+ raw: output
95
+ }));
96
+ return;
97
+ }
85
98
  if (exitCode && exitCode !== 0) {
86
- reject(createPtyExitError(this.name, exitCode, output));
99
+ reject(createPtyExitError(this.name, exitCode, output, errorMessages));
87
100
  return;
88
101
  }
89
102
  if (!content && !this.config.allowEmptyOutput) {
@@ -143,6 +156,10 @@ export class CliPtyAdapter {
143
156
  });
144
157
  }
145
158
  }
159
+ /**
160
+ * Résout le chemin absolu de l'exécutable dans le `PATH`, requis par `node-pty` qui ne fait pas
161
+ * lui-même cette résolution comme `child_process.spawn`. Retourne `command` tel quel si rien n'est trouvé.
162
+ */
146
163
  function resolveExecutable(command) {
147
164
  if (path.isAbsolute(command) || command.includes("\\") || command.includes("/")) {
148
165
  return command;
@@ -160,6 +177,11 @@ function resolveExecutable(command) {
160
177
  }
161
178
  return command;
162
179
  }
180
+ /**
181
+ * Force la libération des ressources internes ConPTY sur Windows après `term.kill()`, qui ne les
182
+ * relâche pas toujours. Accède à des champs privés non documentés de `node-pty` : purement
183
+ * best-effort, toute erreur est avalée sans remonter à l'appelant.
184
+ */
163
185
  function cleanupPty(term) {
164
186
  const maybeTerm = term;
165
187
  try {
@@ -170,16 +192,14 @@ function cleanupPty(term) {
170
192
  // Best-effort cleanup for Windows ConPTY internals.
171
193
  }
172
194
  }
173
- function createPtyExitError(adapterName, exitCode, raw) {
174
- return new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizePtyOutput(raw)}`, {
195
+ /** Construit une `AdapterError` `non-zero-exit` à partir de la sortie brute fusionnée du PTY. */
196
+ function createPtyExitError(adapterName, exitCode, raw, messages) {
197
+ return new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizePtyOutput(raw, messages)}`, {
175
198
  exitCode,
176
199
  raw
177
200
  });
178
201
  }
179
- function cancelledError(adapterName) {
180
- return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
181
- }
182
- function summarizePtyOutput(output) {
202
+ function summarizePtyOutput(output, messages) {
183
203
  const cleaned = cleanTerminalOutput(output);
184
- return cleaned ? cleaned.slice(-1_200) : "aucune sortie PTY capturee.";
204
+ return cleaned ? cleaned.slice(-1_200) : messages.noPtyOutputCaptured;
185
205
  }
@@ -1,7 +1,80 @@
1
+ /** @file Constantes et utilitaires partagés entre les adapters `cli` et `cli-pty`. */
1
2
  /** Limite de sortie par défaut des adapters CLI/PTY : 50 Mio avant `output-too-large`. */
2
3
  export const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
3
4
  /** Timeout dur par défaut d'un appel d'agent CLI/PTY (3 minutes). */
4
5
  export const DEFAULT_TIMEOUT_MS = 180_000;
6
+ /**
7
+ * Cherche dans un texte (stderr ou flux PTY) une ligne signalant un quota ou un
8
+ * rate-limit connu, pour classer l'erreur en `usage-limit`.
9
+ * @returns La ligne fautive nettoyée et tronquée, ou `undefined` si rien ne matche.
10
+ */
11
+ export function extractUsageLimitMessage(text) {
12
+ const match = uniqueNonEmptyLines(text).find((line) => isUsageLimitLine(line));
13
+ if (!match) {
14
+ return undefined;
15
+ }
16
+ return clipLine(stripLogPrefix(match), 500);
17
+ }
18
+ /**
19
+ * Détecte un diagnostic de quota dans un flux PTY fusionné sans interpréter une
20
+ * réponse normale qui mentionnerait simplement les notions de quota ou rate-limit.
21
+ * Les motifs acceptés sont des erreurs machine ou des messages de quota autonomes.
22
+ */
23
+ export function extractPtyUsageLimitMessage(text) {
24
+ const match = uniqueNonEmptyLines(text).find((line) => {
25
+ const cleaned = stripLogPrefix(line);
26
+ const normalized = cleaned.toLowerCase();
27
+ const hasErrorPrefix = /^(error|fatal|warning)\s*:/i.test(line.trim());
28
+ return /^individual quota reached(?:[.!:]|$)/i.test(cleaned)
29
+ || /\b(resource_exhausted|insufficient_quota)\b/i.test(cleaned)
30
+ || /\bhttp\s*429\b/i.test(cleaned)
31
+ || /^too many requests(?:[.!:]|$)/i.test(cleaned)
32
+ || (hasErrorPrefix && isUsageLimitLine(normalized));
33
+ });
34
+ return match ? clipLine(stripLogPrefix(match), 500) : undefined;
35
+ }
36
+ function isUsageLimitLine(line) {
37
+ const normalized = line.toLowerCase();
38
+ return [
39
+ "usage limit",
40
+ "rate limit",
41
+ "quota exceeded",
42
+ "quota reached",
43
+ "resource_exhausted",
44
+ "too many requests",
45
+ "insufficient_quota",
46
+ "exceeded your current quota",
47
+ "credit balance is too low",
48
+ "billing hard limit"
49
+ ].some((pattern) => normalized.includes(pattern));
50
+ }
51
+ /** Déduplique les lignes non vides d'un texte, en préservant l'ordre d'apparition. */
52
+ export function uniqueNonEmptyLines(value) {
53
+ const seen = new Set();
54
+ const lines = [];
55
+ for (const line of value.split(/\r?\n/)) {
56
+ const cleaned = line.trim();
57
+ if (!cleaned || seen.has(cleaned)) {
58
+ continue;
59
+ }
60
+ seen.add(cleaned);
61
+ lines.push(cleaned);
62
+ }
63
+ return lines;
64
+ }
65
+ /** Retire un éventuel préfixe de log (`2026-01-01T... ERROR module:` ou `ERROR:`) d'une ligne. */
66
+ export function stripLogPrefix(line) {
67
+ return line
68
+ .replace(/^\d{4}-\d{2}-\d{2}T\S+\s+(ERROR|WARN|INFO|DEBUG)\s+[^:]+:\s*/i, "")
69
+ .replace(/^ERROR:\s*/i, "")
70
+ .trim();
71
+ }
72
+ /** Tronque une ligne à `maxLength` caractères avec une ellipse. */
73
+ export function clipLine(value, maxLength) {
74
+ return value.length <= maxLength
75
+ ? value
76
+ : `${value.slice(0, maxLength - 1)}…`;
77
+ }
5
78
  /**
6
79
  * Insère `modelArg model` dans la liste d'arguments d'une commande CLI.
7
80
  * Si le dernier argument est `-` (marqueur stdin), insère avant lui pour
@@ -1,7 +1,9 @@
1
+ /** @file Adapter CLI batch minimal : spawn, injection du prompt, capture stdout, classement des erreurs connues. */
1
2
  import { spawn } from "node:child_process";
2
- import { AdapterError } from "../errors.js";
3
+ import { AdapterError, cancelledError } from "../errors.js";
4
+ import { createTranslator } from "../i18n.js";
3
5
  import { formatAgentPrompt } from "../prompt.js";
4
- import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS, withModelArgs } from "./cli-shared.js";
6
+ import { clipLine, DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS, extractUsageLimitMessage, stripLogPrefix, uniqueNonEmptyLines, withModelArgs } from "./cli-shared.js";
5
7
  import { cleanTerminalOutput } from "./terminal.js";
6
8
  /**
7
9
  * Adapter pour les CLIs batch (Codex, Claude, OpenCode, Vibe...).
@@ -41,6 +43,7 @@ export class CliAdapter {
41
43
  throw cancelledError(this.name);
42
44
  }
43
45
  const renderedPrompt = formatAgentPrompt(prompt);
46
+ const errorMessages = createTranslator(prompt.language ?? "fr").adapterErrors;
44
47
  const promptMode = this.config.promptMode ?? "stdin";
45
48
  const baseArgs = withModelArgs(this.config.args ?? [], this.config.model, this.config.modelArg ?? "--model");
46
49
  const args = promptMode === "argument"
@@ -76,7 +79,7 @@ export class CliAdapter {
76
79
  }
77
80
  const content = cleanCliOutput(stdout);
78
81
  if (!content && !this.config.allowEmptyOutput) {
79
- const knownError = createKnownCliError(this.name, undefined, stderr);
82
+ const knownError = createKnownCliError(this.name, undefined, stderr, errorMessages);
80
83
  if (knownError) {
81
84
  reject(knownError);
82
85
  return;
@@ -114,7 +117,8 @@ export class CliAdapter {
114
117
  }, this.config.idleTimeoutMs);
115
118
  };
116
119
  bumpIdleTimer();
117
- child.stdout.on("data", (chunk) => {
120
+ // stdout et stderr partagent le même budget `maxOutputBytes`.
121
+ const createDataHandler = (append) => (chunk) => {
118
122
  outputBytes += chunk.length;
119
123
  if (outputBytes > maxOutputBytes) {
120
124
  killChildProcess(child);
@@ -124,22 +128,15 @@ export class CliAdapter {
124
128
  }));
125
129
  return;
126
130
  }
127
- stdout += chunk.toString("utf8");
131
+ append(chunk.toString("utf8"));
128
132
  bumpIdleTimer();
129
- });
130
- child.stderr.on("data", (chunk) => {
131
- outputBytes += chunk.length;
132
- if (outputBytes > maxOutputBytes) {
133
- killChildProcess(child);
134
- finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
135
- maxOutputBytes,
136
- outputBytes
137
- }));
138
- return;
139
- }
140
- stderr += chunk.toString("utf8");
141
- bumpIdleTimer();
142
- });
133
+ };
134
+ child.stdout.on("data", createDataHandler((text) => {
135
+ stdout += text;
136
+ }));
137
+ child.stderr.on("data", createDataHandler((text) => {
138
+ stderr += text;
139
+ }));
143
140
  child.on("error", (error) => {
144
141
  const kind = error.code === "ENOENT" ? "command-not-found" : "spawn-failed";
145
142
  finish(new AdapterError(kind, this.name, `${this.name} failed to start command "${this.config.command}": ${error.message}`, {
@@ -149,7 +146,7 @@ export class CliAdapter {
149
146
  });
150
147
  const finishFromExitCode = (code) => {
151
148
  if (code && code !== 0) {
152
- finish(createCliExitError(this.name, code, stderr));
149
+ finish(createCliExitError(this.name, code, stderr, errorMessages));
153
150
  return;
154
151
  }
155
152
  finish();
@@ -166,6 +163,11 @@ export class CliAdapter {
166
163
  function cleanCliOutput(output) {
167
164
  return stripWindowsTaskkillNoise(cleanTerminalOutput(output));
168
165
  }
166
+ /**
167
+ * Retire les lignes de statut `taskkill` (FR/EN) que Windows peut mélanger à la sortie
168
+ * d'une CLI tuée après timeout ou annulation. Sans ce filtre, ce bruit contaminerait
169
+ * la réponse agent capturée sur stdout.
170
+ */
169
171
  function stripWindowsTaskkillNoise(output) {
170
172
  const lines = output.split("\n");
171
173
  const kept = [];
@@ -206,31 +208,32 @@ function normalizeForWindowsStatus(line) {
206
208
  * Construit une `AdapterError` typée depuis un exit code non nul.
207
209
  * Élève en `usage-limit` si le stderr contient un signal de quota/rate-limit connu.
208
210
  */
209
- function createCliExitError(adapterName, exitCode, stderr) {
210
- return createKnownCliError(adapterName, exitCode, stderr)
211
- ?? new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizeCliError(cleanCliOutput(stderr))}`, {
211
+ function createCliExitError(adapterName, exitCode, stderr, messages) {
212
+ return createKnownCliError(adapterName, exitCode, stderr, messages)
213
+ ?? new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizeCliError(cleanCliOutput(stderr), messages)}`, {
212
214
  exitCode,
213
215
  stderr: cleanCliOutput(stderr)
214
216
  });
215
217
  }
216
- function createKnownCliError(adapterName, exitCode, stderr) {
218
+ function createKnownCliError(adapterName, exitCode, stderr, messages) {
217
219
  const cleanedStderr = cleanCliOutput(stderr);
218
220
  const usageLimitMessage = extractUsageLimitMessage(cleanedStderr);
219
221
  const unsupportedModelMessage = extractUnsupportedModelMessage(cleanedStderr);
220
222
  if (usageLimitMessage) {
221
- return new AdapterError("usage-limit", adapterName, `${adapterName} a atteint une limite d'utilisation: ${usageLimitMessage}`, {
223
+ return new AdapterError("usage-limit", adapterName, messages.usageLimit(adapterName, usageLimitMessage), {
222
224
  ...(exitCode === undefined ? {} : { exitCode }),
223
225
  stderr: cleanedStderr
224
226
  });
225
227
  }
226
228
  if (unsupportedModelMessage) {
227
- return new AdapterError("unsupported-model", adapterName, `${adapterName} ne peut pas utiliser ce modèle: ${unsupportedModelMessage}`, {
229
+ return new AdapterError("unsupported-model", adapterName, messages.unsupportedModel(adapterName, unsupportedModelMessage), {
228
230
  ...(exitCode === undefined ? {} : { exitCode }),
229
231
  stderr: cleanedStderr
230
232
  });
231
233
  }
232
234
  return undefined;
233
235
  }
236
+ /** Cherche dans stderr une ligne signalant un modèle non supporté, pour classer l'erreur en `unsupported-model`. */
234
237
  function extractUnsupportedModelMessage(stderr) {
235
238
  const lines = uniqueNonEmptyLines(stderr);
236
239
  const match = lines.find((line) => isUnsupportedModelLine(line));
@@ -255,59 +258,17 @@ function extractJsonErrorMessage(line) {
255
258
  const match = line.match(/"message"\s*:\s*"([^"]+)"/);
256
259
  return match?.[1];
257
260
  }
258
- function extractUsageLimitMessage(stderr) {
259
- const lines = uniqueNonEmptyLines(stderr);
260
- const match = lines.find((line) => isUsageLimitLine(line));
261
- if (!match) {
262
- return undefined;
263
- }
264
- return clipLine(stripLogPrefix(match), 500);
265
- }
266
- function isUsageLimitLine(line) {
267
- const normalized = line.toLowerCase();
268
- return [
269
- "usage limit",
270
- "rate limit",
271
- "quota exceeded",
272
- "resource_exhausted",
273
- "too many requests",
274
- "insufficient_quota",
275
- "exceeded your current quota",
276
- "credit balance is too low",
277
- "billing hard limit"
278
- ].some((pattern) => normalized.includes(pattern));
279
- }
280
- function summarizeCliError(stderr) {
261
+ function summarizeCliError(stderr, messages) {
281
262
  const lines = uniqueNonEmptyLines(stderr).map(stripLogPrefix);
282
263
  if (lines.length === 0) {
283
- return "aucun stderr capture.";
264
+ return messages.noStderrCaptured;
284
265
  }
285
266
  return clipLine(lines.slice(-8).join("\n"), 1_200);
286
267
  }
287
- function uniqueNonEmptyLines(value) {
288
- const seen = new Set();
289
- const lines = [];
290
- for (const line of value.split(/\r?\n/)) {
291
- const cleaned = line.trim();
292
- if (!cleaned || seen.has(cleaned)) {
293
- continue;
294
- }
295
- seen.add(cleaned);
296
- lines.push(cleaned);
297
- }
298
- return lines;
299
- }
300
- function stripLogPrefix(line) {
301
- return line
302
- .replace(/^\d{4}-\d{2}-\d{2}T\S+\s+(ERROR|WARN|INFO|DEBUG)\s+[^:]+:\s*/i, "")
303
- .replace(/^ERROR:\s*/i, "")
304
- .trim();
305
- }
306
- function clipLine(value, maxLength) {
307
- return value.length <= maxLength
308
- ? value
309
- : `${value.slice(0, maxLength - 1)}…`;
310
- }
268
+ /**
269
+ * Recompose `command`/`args` en une seule ligne de commande quotée quand `shell: true` est requis
270
+ * (ex. wrappers npm shimmés sur Windows où `spawn` direct renvoie `EPERM`/`EINVAL`).
271
+ */
311
272
  function shellCommandForSpawn(command, args, shell) {
312
273
  if (!shell) {
313
274
  return { command, args };
@@ -337,9 +298,11 @@ function quotePosixShellArg(value) {
337
298
  }
338
299
  return `'${value.replace(/'/g, "'\\''")}'`;
339
300
  }
340
- function cancelledError(adapterName) {
341
- return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
342
- }
301
+ /**
302
+ * Termine le process et ses enfants (`/T`) sur Windows via `taskkill.exe`, car `child.kill()` seul
303
+ * ne tue pas les sous-processus d'un wrapper npm/shim. `taskkill` est lancé en fire-and-forget :
304
+ * seul son échec de spawn déclenche un `child.kill()` de repli, sans attendre sa fin.
305
+ */
343
306
  function killChildProcess(child) {
344
307
  if (process.platform === "win32" && child.pid) {
345
308
  const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
@@ -1,3 +1,4 @@
1
+ /** @file Factory d'adapters : mappe un `AgentConfig` vers l'implémentation `cli`/`cli-pty`/`ollama` correspondante. */
1
2
  import { CliAdapter } from "./cli.js";
2
3
  import { CliPtyAdapter } from "./cli-pty.js";
3
4
  import { OllamaAdapter } from "./ollama.js";
@@ -1,4 +1,5 @@
1
- import { AdapterError } from "../errors.js";
1
+ /** @file Adapter Ollama HTTP : validation/pull de modèle, déchargement des autres modèles chargés, appel `/api/chat`. */
2
+ import { AdapterError, cancelledError } from "../errors.js";
2
3
  import { createTranslator } from "../i18n.js";
3
4
  import { formatAgentPrompt } from "../prompt.js";
4
5
  import { resolveOllamaBaseUrl } from "../ollamaUrl.js";
@@ -41,15 +42,17 @@ export class OllamaAdapter {
41
42
  if (prompt.signal?.aborted) {
42
43
  throw cancelledError(this.name);
43
44
  }
45
+ const translator = createTranslator(prompt.language ?? "fr");
46
+ const errorMessages = translator.adapterErrors;
44
47
  const baseUrl = resolveOllamaBaseUrl({
45
48
  cliUrl: this.runtime.ollamaUrl,
46
49
  configUrl: this.config.baseUrl
47
50
  });
48
51
  if (this.config.validateModel !== false) {
49
- await this.ensureModelAvailable(baseUrl);
52
+ await this.ensureModelAvailable(baseUrl, errorMessages);
50
53
  }
51
54
  if (this.config.unloadOtherModels !== false) {
52
- await this.unloadOtherRunningModels(baseUrl);
55
+ await this.unloadOtherRunningModels(baseUrl, errorMessages);
53
56
  }
54
57
  const controller = new AbortController();
55
58
  const abortListener = () => controller.abort();
@@ -68,8 +71,7 @@ export class OllamaAdapter {
68
71
  messages: [
69
72
  {
70
73
  role: "system",
71
- content: this.config.systemPrompt ??
72
- createTranslator(prompt.language ?? "fr").prompt.ollamaSystemPrompt
74
+ content: this.config.systemPrompt ?? translator.prompt.ollamaSystemPrompt
73
75
  },
74
76
  {
75
77
  role: "user",
@@ -112,40 +114,39 @@ export class OllamaAdapter {
112
114
  * Si absent et `autoPullModel` est faux, lève `model-unavailable` avec la liste des modèles détectés.
113
115
  * Si absent et `autoPullModel` est vrai, déclenche le pull puis re-vérifie.
114
116
  */
115
- async ensureModelAvailable(baseUrl) {
116
- const available = await this.isModelAvailable(baseUrl);
117
+ async ensureModelAvailable(baseUrl, messages) {
118
+ const available = await this.isModelAvailable(baseUrl, messages);
117
119
  if (available) {
118
120
  return;
119
121
  }
120
122
  if (!this.config.autoPullModel) {
121
- const models = await this.listAvailableModels(baseUrl);
122
- throw new AdapterError("model-unavailable", this.name, `Modele Ollama indisponible: ${this.config.model}. Modeles detectes: ${models.join(", ") || "aucun"}. ` +
123
- "Utilise --pull-models ou autoPullModel: true pour autoriser le telechargement.", { model: this.config.model, availableModels: models });
123
+ const models = await this.listAvailableModels(baseUrl, messages);
124
+ throw new AdapterError("model-unavailable", this.name, messages.ollamaModelUnavailable(this.config.model, models), { model: this.config.model, availableModels: models });
124
125
  }
125
- process.stderr.write(`\n[ollama] Modele absent, telechargement: ${this.config.model}\n`);
126
- await this.pullModel(baseUrl);
127
- if (!(await this.isModelAvailable(baseUrl))) {
128
- throw new AdapterError("model-pull-failed", this.name, `Le modele Ollama ${this.config.model} reste indisponible apres telechargement.`);
126
+ process.stderr.write(`\n${messages.ollamaPullProgress(this.config.model)}\n`);
127
+ await this.pullModel(baseUrl, messages);
128
+ if (!(await this.isModelAvailable(baseUrl, messages))) {
129
+ throw new AdapterError("model-pull-failed", this.name, messages.ollamaModelStillUnavailable(this.config.model));
129
130
  }
130
131
  }
131
- async isModelAvailable(baseUrl) {
132
- const models = await this.listAvailableModels(baseUrl);
132
+ async isModelAvailable(baseUrl, messages) {
133
+ const models = await this.listAvailableModels(baseUrl, messages);
133
134
  return models.includes(this.config.model);
134
135
  }
135
- async listAvailableModels(baseUrl) {
136
+ async listAvailableModels(baseUrl, messages) {
136
137
  const controller = new AbortController();
137
138
  const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 120_000);
138
139
  try {
139
- return await this.fetchAvailableModels(baseUrl, controller.signal);
140
+ return await this.fetchAvailableModels(baseUrl, controller.signal, messages);
140
141
  }
141
142
  finally {
142
143
  clearTimeout(timeout);
143
144
  }
144
145
  }
145
- async fetchAvailableModels(baseUrl, signal) {
146
+ async fetchAvailableModels(baseUrl, signal, messages) {
146
147
  const response = await fetch(`${baseUrl}/api/tags`, { signal });
147
148
  if (!response.ok) {
148
- throw new AdapterError("http-error", this.name, `Ollama HTTP ${response.status} pendant la detection des modeles`, {
149
+ throw new AdapterError("http-error", this.name, messages.ollamaTagsHttpError(response.status), {
149
150
  status: response.status
150
151
  });
151
152
  }
@@ -154,7 +155,8 @@ export class OllamaAdapter {
154
155
  ?.map((model) => model.name ?? model.model)
155
156
  .filter((modelName) => Boolean(modelName)) ?? [];
156
157
  }
157
- async pullModel(baseUrl) {
158
+ /** Déclenche `POST /api/pull` et attend sa fin ; timeout dédié `pullTimeoutMs` (30 min par défaut). */
159
+ async pullModel(baseUrl, messages) {
158
160
  const controller = new AbortController();
159
161
  const timeout = setTimeout(() => controller.abort(), this.config.pullTimeoutMs ?? 1_800_000);
160
162
  try {
@@ -178,26 +180,27 @@ export class OllamaAdapter {
178
180
  }
179
181
  catch (error) {
180
182
  const message = error instanceof Error ? error.message : String(error);
181
- throw new AdapterError("model-pull-failed", this.name, `Echec du telechargement Ollama ${this.config.model}: ${message}`);
183
+ throw new AdapterError("model-pull-failed", this.name, messages.ollamaPullFailed(this.config.model, message));
182
184
  }
183
185
  finally {
184
186
  clearTimeout(timeout);
185
187
  }
186
188
  }
187
- async unloadOtherRunningModels(baseUrl) {
189
+ /** Décharge séquentiellement, via `GET /api/ps`, tout modèle chargé autre que celui de cet agent. */
190
+ async unloadOtherRunningModels(baseUrl, messages) {
188
191
  const controller = new AbortController();
189
192
  const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 120_000);
190
193
  try {
191
- await this.unloadOtherRunningModelsWithSignal(baseUrl, controller.signal);
194
+ await this.unloadOtherRunningModelsWithSignal(baseUrl, controller.signal, messages);
192
195
  }
193
196
  finally {
194
197
  clearTimeout(timeout);
195
198
  }
196
199
  }
197
- async unloadOtherRunningModelsWithSignal(baseUrl, signal) {
200
+ async unloadOtherRunningModelsWithSignal(baseUrl, signal, messages) {
198
201
  const response = await fetch(`${baseUrl}/api/ps`, { signal });
199
202
  if (!response.ok) {
200
- throw new AdapterError("http-error", this.name, `Ollama HTTP ${response.status} pendant la detection des modeles charges`, {
203
+ throw new AdapterError("http-error", this.name, messages.ollamaPsHttpError(response.status), {
201
204
  status: response.status
202
205
  });
203
206
  }
@@ -207,12 +210,12 @@ export class OllamaAdapter {
207
210
  .filter((modelName) => Boolean(modelName))
208
211
  .filter((modelName) => modelName !== this.config.model) ?? [];
209
212
  for (const model of runningModels) {
210
- await unloadModel(baseUrl, model, signal);
213
+ await unloadModel(baseUrl, model, signal, messages);
211
214
  }
212
215
  }
213
216
  }
214
217
  /** Décharge un modèle Ollama en mémoire GPU/CPU via `POST /api/generate` avec `keep_alive: 0`. */
215
- async function unloadModel(baseUrl, model, signal) {
218
+ async function unloadModel(baseUrl, model, signal, messages) {
216
219
  const response = await fetch(`${baseUrl}/api/generate`, {
217
220
  method: "POST",
218
221
  headers: {
@@ -225,12 +228,9 @@ async function unloadModel(baseUrl, model, signal) {
225
228
  signal
226
229
  });
227
230
  if (!response.ok) {
228
- throw new AdapterError("http-error", "ollama", `Impossible de decharger le modele Ollama ${model}: HTTP ${response.status}`, {
231
+ throw new AdapterError("http-error", "ollama", messages.ollamaUnloadFailed(model, response.status), {
229
232
  status: response.status,
230
233
  model
231
234
  });
232
235
  }
233
236
  }
234
- function cancelledError(adapterName) {
235
- return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
236
- }
@@ -1,3 +1,4 @@
1
+ /** @file Nettoyage des séquences ANSI/OSC partagé par les adapters CLI et PTY. */
1
2
  /** Retire les séquences de contrôle ANSI/OSC et normalise les retours ligne d'une sortie terminal. */
2
3
  export function cleanTerminalOutput(output) {
3
4
  return output
package/dist/args.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** @file Parseur d'arguments CLI : table d'arité des flags, syntaxe courte preset/sujet, alias `-s`/`--topic`. */
1
2
  import { listPresetNames } from "./presets.js";
2
3
  /**
3
4
  * Table centrale décrivant l'arité de chaque flag long canonique.
@@ -1,3 +1,4 @@
1
+ /** @file Commande palabre agents et contrat JSON v1 pour les integrations. */
1
2
  import { configExists, loadConfig, resolveDefaultConfigPath } from "../config.js";
2
3
  import { discoverLocalToolsForConfig } from "../discovery.js";
3
4
  import { createTranslator, resolveLanguage } from "../i18n.js";
@@ -5,7 +6,12 @@ import { turnsOrDefault } from "../limits.js";
5
6
  import { listAgentsWithAvailability } from "../presets.js";
6
7
  import { detectionForCommand, isRetiredAgentName } from "../agentRegistry.js";
7
8
  import { optionalString } from "./shared.js";
8
- /** Exécute `palabre agents` en sortie humaine ou JSON versionné. */
9
+ /**
10
+ * Liste les agents configurés avec leur disponibilité calculée par le CLI.
11
+ * @param flags - Flags de langue, config, URL Ollama et format JSON.
12
+ * @returns Une promesse résolue après écriture de la sortie.
13
+ * @throws {Error} Si aucune configuration n'est disponible.
14
+ */
9
15
  export async function runAgentsCommand(flags) {
10
16
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
11
17
  if (!(await configExists(configPath))) {
@@ -1,7 +1,13 @@
1
+ /** @file Commande palabre context scan et rendu de son contrat versionne. */
1
2
  import { buildContextScan } from "../contextScan.js";
2
3
  import { createTranslator, resolveLanguage } from "../i18n.js";
3
4
  import { optionalString } from "./shared.js";
4
- /** Exécute `palabre context scan` en sortie humaine ou JSON versionné. */
5
+ /**
6
+ * Scanne les chemins demandés avec les mêmes règles que `--context`.
7
+ * @param flags - Flags de langue et de format JSON.
8
+ * @param positionals - Sous-commande puis chemins à scanner.
9
+ * @returns Une promesse résolue après écriture du résultat et des warnings.
10
+ */
5
11
  export async function runContextCommand(flags, positionals) {
6
12
  const messages = createTranslator(resolveLanguage({ explicitLanguage: optionalString(flags.language) }));
7
13
  const subcommand = positionals[0] ?? "scan";
@@ -1,8 +1,13 @@
1
+ /** @file Commande de consultation des exports Markdown recents. */
1
2
  import { configExists, loadConfig, resolveDefaultConfigPath, resolveOutputDir } from "../config.js";
2
3
  import { listHistoryEntries } from "../history.js";
3
4
  import { createTranslator, resolveLanguage } from "../i18n.js";
4
5
  import { optionalString } from "./shared.js";
5
- /** Exécute `palabre history` en sortie humaine ou JSON versionné. */
6
+ /**
7
+ * Liste les exports du dossier de sortie configuré.
8
+ * @param flags - Flags de config, langue et format JSON.
9
+ * @returns Une promesse résolue après écriture de l'historique.
10
+ */
6
11
  export async function runHistoryCommand(flags) {
7
12
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
8
13
  const config = await configExists(configPath) ? await loadConfig(configPath) : undefined;
@@ -1,9 +1,14 @@
1
- import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, writeExampleConfig } from "../config.js";
1
+ /** @file Initialisation explicite de configuration globale ou locale. */
2
+ import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, writeConfig } from "../config.js";
2
3
  import { discoverLocalTools } from "../discovery.js";
3
4
  import { detectedAgentNames } from "../agentRegistry.js";
4
5
  import { createTranslator, DEFAULT_LANGUAGE, resolveLanguage } from "../i18n.js";
5
6
  import { optionalString } from "./shared.js";
6
- /** Exécute `palabre init` ou son alias `setup`. */
7
+ /**
8
+ * Crée une configuration à partir des outils détectés localement.
9
+ * @param flags - Flags de chemin, portée locale, langue et URL Ollama.
10
+ * @returns Une promesse résolue après création et affichage du récapitulatif.
11
+ */
7
12
  export async function runInitCommand(flags) {
8
13
  const configPath = optionalString(flags.config) ?? (flags.local ? DEFAULT_CONFIG_PATH : GLOBAL_CONFIG_PATH);
9
14
  const startupMessages = createTranslator(resolveLanguage({ explicitLanguage: optionalString(flags.language) }));
@@ -18,7 +23,7 @@ export async function runInitCommand(flags) {
18
23
  configLanguage: config.language
19
24
  });
20
25
  const messages = createTranslator(config.language);
21
- await writeExampleConfig(configPath, config);
26
+ await writeConfig(configPath, config);
22
27
  console.log(messages.init.configCreated(configPath));
23
28
  printInitDiscovery(discovery, config, messages);
24
29
  }
@@ -1,9 +1,14 @@
1
+ /** @file Commande de decouverte des presets et de leur disponibilite. */
1
2
  import { configExists, createConfigFromDiscovery, loadConfig, resolveDefaultConfigPath } from "../config.js";
2
3
  import { discoverLocalTools, discoverLocalToolsForConfig } from "../discovery.js";
3
4
  import { createTranslator, resolveLanguage } from "../i18n.js";
4
5
  import { listPresetsWithAvailability } from "../presets.js";
5
6
  import { optionalString } from "./shared.js";
6
- /** Exécute `palabre presets` en sortie humaine ou JSON versionné. */
7
+ /**
8
+ * Liste les presets enrichis avec la disponibilité issue de la config et de la discovery.
9
+ * @param flags - Flags de config, langue, URL Ollama et format JSON.
10
+ * @returns Une promesse résolue après écriture de la liste.
11
+ */
7
12
  export async function runPresetsCommand(flags) {
8
13
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
9
14
  const ollamaUrl = optionalString(flags["ollama-url"]);