palabre 0.9.0 → 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.
- package/README.md +2 -0
- package/dist/adapters/cli-pty.js +30 -10
- package/dist/adapters/cli-shared.js +73 -0
- package/dist/adapters/cli.js +40 -77
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/ollama.js +32 -32
- package/dist/adapters/terminal.js +1 -0
- package/dist/args.js +1 -0
- package/dist/commands/agents.js +91 -0
- package/dist/commands/context.js +31 -0
- package/dist/commands/history.js +33 -0
- package/dist/commands/init.js +64 -0
- package/dist/commands/presets.js +39 -0
- package/dist/commands/shared.js +8 -0
- package/dist/commands/update.js +26 -0
- package/dist/config.js +17 -1
- package/dist/configWizard.js +5 -4
- package/dist/context.js +1 -0
- package/dist/contextScan.js +4 -3
- package/dist/discovery.js +9 -1
- package/dist/doctor.js +1 -0
- package/dist/errors.js +4 -0
- package/dist/exec.js +1 -0
- package/dist/history.js +10 -0
- package/dist/i18n.js +2 -0
- package/dist/index.js +174 -879
- package/dist/limits.js +5 -0
- package/dist/messages/adapter-errors.js +26 -2
- package/dist/messages/config.js +6 -0
- package/dist/messages/index.js +1 -0
- package/dist/messages/renderers.js +10 -2
- package/dist/messages/tui.js +8 -2
- package/dist/new.js +1 -0
- package/dist/ollamaUrl.js +20 -0
- package/dist/orchestrator.js +106 -161
- package/dist/output.js +2 -10
- package/dist/presets.js +1 -0
- package/dist/prompt.js +1 -0
- package/dist/renderers/console.js +2 -8
- package/dist/renderers/ndjson.js +1 -10
- package/dist/renderers/tui-prompts.js +339 -0
- package/dist/renderers/tui-renderer.js +224 -0
- package/dist/renderers/tui-screens.js +352 -0
- package/dist/renderers/tui-theme.js +356 -0
- package/dist/renderers/tui.js +7 -1095
- package/dist/runOptions.js +97 -0
- package/dist/session.js +1 -0
- package/dist/tuiController.js +445 -0
- package/dist/tuiState.js +4 -0
- package/dist/types.js +1 -0
- package/dist/update.js +1 -0
- package/dist/version.js +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ PALABRE does not list models: they change often and depend on each CLI or user a
|
|
|
70
70
|
|
|
71
71
|
PALABRE exposes versioned JSON outputs for external clients:
|
|
72
72
|
|
|
73
|
+
- `palabre agents --json` to read configured agents and CLI-owned availability;
|
|
73
74
|
- `palabre presets --json` to read available agent pairs;
|
|
74
75
|
- `palabre context scan --json` to preview the context `--context` would retain;
|
|
75
76
|
- `--renderer ndjson` or `--json` to follow a debate event by event.
|
|
@@ -178,6 +179,7 @@ PALABRE ne liste pas les modèles : ils changent souvent et dépendent de chaque
|
|
|
178
179
|
|
|
179
180
|
PALABRE expose des sorties JSON versionnées pour les clients externes :
|
|
180
181
|
|
|
182
|
+
- `palabre agents --json` pour lire les agents configurés et leur disponibilité calculée par le CLI ;
|
|
181
183
|
- `palabre presets --json` pour lire les paires d'agents disponibles ;
|
|
182
184
|
- `palabre context scan --json` pour prévisualiser le contexte que `--context` retiendrait ;
|
|
183
185
|
- `--renderer ndjson` ou `--json` pour suivre un débat événement par événement.
|
package/dist/adapters/cli-pty.js
CHANGED
|
@@ -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
|
-
|
|
174
|
-
|
|
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
|
|
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) :
|
|
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
|
package/dist/adapters/cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
131
|
+
append(chunk.toString("utf8"));
|
|
128
132
|
bumpIdleTimer();
|
|
129
|
-
}
|
|
130
|
-
child.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
264
|
+
return messages.noStderrCaptured;
|
|
284
265
|
}
|
|
285
266
|
return clipLine(lines.slice(-8).join("\n"), 1_200);
|
|
286
267
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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"], {
|
package/dist/adapters/index.js
CHANGED
package/dist/adapters/ollama.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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,
|
|
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
|
|
126
|
-
await this.pullModel(baseUrl);
|
|
127
|
-
if (!(await this.isModelAvailable(baseUrl))) {
|
|
128
|
-
throw new AdapterError("model-pull-failed", this.name,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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",
|
|
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
|
-
}
|
package/dist/args.js
CHANGED