palabre 0.6.1 → 0.6.4
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 +6 -2
- package/dist/adapters/cli-pty.js +6 -29
- package/dist/adapters/cli-shared.js +24 -0
- package/dist/adapters/cli.js +56 -30
- package/dist/adapters/ollama.js +2 -1
- package/dist/agentRegistry.js +76 -0
- package/dist/args.js +265 -0
- package/dist/config.js +26 -20
- package/dist/discovery.js +3 -13
- package/dist/doctor.js +2 -27
- package/dist/exec.js +17 -0
- package/dist/index.js +9 -262
- package/dist/messages/adapter-errors.js +2 -2
- package/dist/messages/common.js +6 -0
- package/dist/messages/orchestrator.js +19 -0
- package/dist/messages/prompt.js +6 -2
- package/dist/new.js +1 -26
- package/dist/orchestrator.js +7 -14
- package/dist/presets.js +2 -21
- package/dist/prompt.js +4 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -93,7 +93,9 @@ palabre --version
|
|
|
93
93
|
|
|
94
94
|
Commandes utiles : `pnpm check`, `pnpm test`, `pnpm build`.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
Avant une publication, `pnpm smoke:real-presets -- --keep-going` lance des débats réels sur les presets prioritaires disponibles afin de vérifier le flux complet agent → NDJSON → export. Ce smoke test appelle de vraies CLIs IA et peut consommer des quotas ; il n'est donc pas lancé par `pnpm test`.
|
|
97
|
+
|
|
98
|
+
Roadmap publique : [docs/guide/fr/roadmap.md](./docs/guide/fr/roadmap.md). Changements : [CHANGELOG.md](./CHANGELOG.md). Guide agents/contributeurs : [AGENTS.md](./AGENTS.md).
|
|
97
99
|
|
|
98
100
|
### Licence
|
|
99
101
|
|
|
@@ -181,7 +183,9 @@ palabre --version
|
|
|
181
183
|
|
|
182
184
|
Useful commands: `pnpm check`, `pnpm test`, `pnpm build`.
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
Before publishing, `pnpm smoke:real-presets -- --keep-going` runs real debates for the available priority presets to validate the full agent → NDJSON → export flow. This smoke test calls real AI CLIs and may consume quota, so it is not part of `pnpm test`.
|
|
187
|
+
|
|
188
|
+
Public roadmap: [docs/guide/fr/roadmap.md](./docs/guide/fr/roadmap.md). Changes: [CHANGELOG.md](./CHANGELOG.md). Agent/contributor guide: [AGENTS.md](./AGENTS.md).
|
|
185
189
|
|
|
186
190
|
### License
|
|
187
191
|
|
package/dist/adapters/cli-pty.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { spawn as spawnPty } from "node-pty";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { AdapterError } from "../errors.js";
|
|
4
|
+
import { executableExtensions } from "../exec.js";
|
|
5
5
|
import { formatAgentPrompt } from "../prompt.js";
|
|
6
|
+
import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS, withModelArgs } from "./cli-shared.js";
|
|
6
7
|
import { cleanTerminalOutput } from "./terminal.js";
|
|
7
|
-
const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
|
|
8
8
|
/**
|
|
9
9
|
* Adapter pour les CLIs qui exigent un vrai terminal.
|
|
10
10
|
* Contrairement à `CliAdapter`, stdout/stderr sont fusionnés dans le flux PTY.
|
|
@@ -44,6 +44,7 @@ export class CliPtyAdapter {
|
|
|
44
44
|
const args = promptMode === "argument"
|
|
45
45
|
? [...baseArgs, renderedPrompt]
|
|
46
46
|
: baseArgs;
|
|
47
|
+
const { spawn: spawnPty } = await import("node-pty");
|
|
47
48
|
return new Promise((resolve, reject) => {
|
|
48
49
|
let output = "";
|
|
49
50
|
let outputBytes = 0;
|
|
@@ -107,10 +108,10 @@ export class CliPtyAdapter {
|
|
|
107
108
|
return;
|
|
108
109
|
}
|
|
109
110
|
hardTimer = setTimeout(() => {
|
|
110
|
-
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ??
|
|
111
|
-
timeoutMs: this.config.timeoutMs ??
|
|
111
|
+
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, {
|
|
112
|
+
timeoutMs: this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
112
113
|
}));
|
|
113
|
-
}, this.config.timeoutMs ??
|
|
114
|
+
}, this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
114
115
|
dataSubscription = term.onData((chunk) => {
|
|
115
116
|
outputBytes += Buffer.byteLength(chunk, "utf8");
|
|
116
117
|
if (outputBytes > maxOutputBytes) {
|
|
@@ -158,30 +159,6 @@ function cleanupPty(term) {
|
|
|
158
159
|
// Best-effort cleanup for Windows ConPTY internals.
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
|
-
function executableExtensions(command) {
|
|
162
|
-
if (path.extname(command) || process.platform !== "win32") {
|
|
163
|
-
return [""];
|
|
164
|
-
}
|
|
165
|
-
return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
166
|
-
.split(";")
|
|
167
|
-
.map((extension) => extension.toLowerCase())
|
|
168
|
-
.concat(".ps1", "");
|
|
169
|
-
}
|
|
170
|
-
function withModelArgs(args, model, modelArg) {
|
|
171
|
-
if (!model) {
|
|
172
|
-
return [...args];
|
|
173
|
-
}
|
|
174
|
-
const promptStdinIndex = args.lastIndexOf("-");
|
|
175
|
-
if (promptStdinIndex === args.length - 1) {
|
|
176
|
-
return [
|
|
177
|
-
...args.slice(0, promptStdinIndex),
|
|
178
|
-
modelArg,
|
|
179
|
-
model,
|
|
180
|
-
...args.slice(promptStdinIndex)
|
|
181
|
-
];
|
|
182
|
-
}
|
|
183
|
-
return [...args, modelArg, model];
|
|
184
|
-
}
|
|
185
162
|
function createPtyExitError(adapterName, exitCode, raw) {
|
|
186
163
|
return new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizePtyOutput(raw)}`, {
|
|
187
164
|
exitCode,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Limite de sortie par défaut des adapters CLI/PTY : 50 Mio avant `output-too-large`. */
|
|
2
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
|
|
3
|
+
/** Timeout dur par défaut d'un appel d'agent CLI/PTY (3 minutes). */
|
|
4
|
+
export const DEFAULT_TIMEOUT_MS = 180_000;
|
|
5
|
+
/**
|
|
6
|
+
* Insère `modelArg model` dans la liste d'arguments d'une commande CLI.
|
|
7
|
+
* Si le dernier argument est `-` (marqueur stdin), insère avant lui pour
|
|
8
|
+
* préserver l'ordre attendu par les CLIs qui lisent le prompt sur stdin.
|
|
9
|
+
*/
|
|
10
|
+
export function withModelArgs(args, model, modelArg) {
|
|
11
|
+
if (!model) {
|
|
12
|
+
return [...args];
|
|
13
|
+
}
|
|
14
|
+
const promptStdinIndex = args.lastIndexOf("-");
|
|
15
|
+
if (promptStdinIndex === args.length - 1) {
|
|
16
|
+
return [
|
|
17
|
+
...args.slice(0, promptStdinIndex),
|
|
18
|
+
modelArg,
|
|
19
|
+
model,
|
|
20
|
+
...args.slice(promptStdinIndex)
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
return [...args, modelArg, model];
|
|
24
|
+
}
|
package/dist/adapters/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { AdapterError } from "../errors.js";
|
|
3
3
|
import { formatAgentPrompt } from "../prompt.js";
|
|
4
|
+
import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS, withModelArgs } from "./cli-shared.js";
|
|
4
5
|
import { cleanTerminalOutput } from "./terminal.js";
|
|
5
|
-
const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
|
|
6
6
|
/**
|
|
7
7
|
* Adapter pour les CLIs batch (Codex, Claude, Gemini…).
|
|
8
8
|
* Lance un sous-processus, injecte le prompt via stdin ou argument, capture stdout.
|
|
@@ -68,6 +68,11 @@ export class CliAdapter {
|
|
|
68
68
|
}
|
|
69
69
|
const content = cleanCliOutput(stdout);
|
|
70
70
|
if (!content && !this.config.allowEmptyOutput) {
|
|
71
|
+
const knownError = createKnownCliError(this.name, undefined, stderr);
|
|
72
|
+
if (knownError) {
|
|
73
|
+
reject(knownError);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
71
76
|
const detail = stderr.trim() ? ` Stderr: ${stderr.trim()}` : "";
|
|
72
77
|
reject(new AdapterError("empty-output", this.name, `${this.name} produced empty output.${detail}`, {
|
|
73
78
|
stderr: stderr.trim()
|
|
@@ -81,10 +86,10 @@ export class CliAdapter {
|
|
|
81
86
|
};
|
|
82
87
|
hardTimer = setTimeout(() => {
|
|
83
88
|
child.kill();
|
|
84
|
-
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ??
|
|
85
|
-
timeoutMs: this.config.timeoutMs ??
|
|
89
|
+
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, {
|
|
90
|
+
timeoutMs: this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
86
91
|
}));
|
|
87
|
-
}, this.config.timeoutMs ??
|
|
92
|
+
}, this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
88
93
|
const bumpIdleTimer = () => {
|
|
89
94
|
if (!this.config.idleTimeoutMs)
|
|
90
95
|
return;
|
|
@@ -144,53 +149,74 @@ export class CliAdapter {
|
|
|
144
149
|
});
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Insère `modelArg model` dans la liste d'arguments.
|
|
149
|
-
* Si le dernier argument est `-` (stdin marker), insère avant lui pour préserver l'ordre attendu par les CLIs.
|
|
150
|
-
*/
|
|
151
|
-
function withModelArgs(args, model, modelArg) {
|
|
152
|
-
if (!model) {
|
|
153
|
-
return [...args];
|
|
154
|
-
}
|
|
155
|
-
const promptStdinIndex = args.lastIndexOf("-");
|
|
156
|
-
if (promptStdinIndex === args.length - 1) {
|
|
157
|
-
return [
|
|
158
|
-
...args.slice(0, promptStdinIndex),
|
|
159
|
-
modelArg,
|
|
160
|
-
model,
|
|
161
|
-
...args.slice(promptStdinIndex)
|
|
162
|
-
];
|
|
163
|
-
}
|
|
164
|
-
return [...args, modelArg, model];
|
|
165
|
-
}
|
|
166
152
|
/** Retire les séquences ANSI et les espaces en tête/fin. */
|
|
167
153
|
function cleanCliOutput(output) {
|
|
168
|
-
return cleanTerminalOutput(output);
|
|
154
|
+
return stripWindowsTaskkillNoise(cleanTerminalOutput(output));
|
|
155
|
+
}
|
|
156
|
+
function stripWindowsTaskkillNoise(output) {
|
|
157
|
+
const lines = output.split("\n");
|
|
158
|
+
const kept = [];
|
|
159
|
+
let skipNextFrenchContinuation = false;
|
|
160
|
+
for (const line of lines) {
|
|
161
|
+
const trimmed = line.trim();
|
|
162
|
+
const normalized = normalizeForWindowsStatus(trimmed);
|
|
163
|
+
if (skipNextFrenchContinuation && /^arr.*t.*\.$/i.test(normalized)) {
|
|
164
|
+
skipNextFrenchContinuation = false;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
skipNextFrenchContinuation = false;
|
|
168
|
+
if (isWindowsTaskkillStatusLine(trimmed)) {
|
|
169
|
+
skipNextFrenchContinuation = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
kept.push(line);
|
|
173
|
+
}
|
|
174
|
+
return kept.join("\n").trim();
|
|
175
|
+
}
|
|
176
|
+
function isWindowsTaskkillStatusLine(line) {
|
|
177
|
+
const normalized = normalizeForWindowsStatus(line);
|
|
178
|
+
const lower = line.toLowerCase();
|
|
179
|
+
return (/^SUCCESS:\s+The process with PID \d+ .* has been terminated\.$/i.test(line) ||
|
|
180
|
+
/^operation reussie.*processus de pid \d+ .* a ete$/.test(normalized) ||
|
|
181
|
+
(lower.startsWith("op") &&
|
|
182
|
+
lower.includes("processus de pid ") &&
|
|
183
|
+
lower.includes("processus enfant de pid") &&
|
|
184
|
+
lower.includes(" a ")));
|
|
185
|
+
}
|
|
186
|
+
function normalizeForWindowsStatus(line) {
|
|
187
|
+
return line
|
|
188
|
+
.normalize("NFD")
|
|
189
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
190
|
+
.toLowerCase();
|
|
169
191
|
}
|
|
170
192
|
/**
|
|
171
193
|
* Construit une `AdapterError` typée depuis un exit code non nul.
|
|
172
194
|
* Élève en `usage-limit` si le stderr contient un signal de quota/rate-limit connu.
|
|
173
195
|
*/
|
|
174
196
|
function createCliExitError(adapterName, exitCode, stderr) {
|
|
197
|
+
return createKnownCliError(adapterName, exitCode, stderr)
|
|
198
|
+
?? new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizeCliError(cleanCliOutput(stderr))}`, {
|
|
199
|
+
exitCode,
|
|
200
|
+
stderr: cleanCliOutput(stderr)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function createKnownCliError(adapterName, exitCode, stderr) {
|
|
175
204
|
const cleanedStderr = cleanCliOutput(stderr);
|
|
176
205
|
const usageLimitMessage = extractUsageLimitMessage(cleanedStderr);
|
|
177
206
|
const unsupportedModelMessage = extractUnsupportedModelMessage(cleanedStderr);
|
|
178
207
|
if (usageLimitMessage) {
|
|
179
208
|
return new AdapterError("usage-limit", adapterName, `${adapterName} a atteint une limite d'utilisation: ${usageLimitMessage}`, {
|
|
180
|
-
exitCode,
|
|
209
|
+
...(exitCode === undefined ? {} : { exitCode }),
|
|
181
210
|
stderr: cleanedStderr
|
|
182
211
|
});
|
|
183
212
|
}
|
|
184
213
|
if (unsupportedModelMessage) {
|
|
185
214
|
return new AdapterError("unsupported-model", adapterName, `${adapterName} ne peut pas utiliser ce modèle: ${unsupportedModelMessage}`, {
|
|
186
|
-
exitCode,
|
|
215
|
+
...(exitCode === undefined ? {} : { exitCode }),
|
|
187
216
|
stderr: cleanedStderr
|
|
188
217
|
});
|
|
189
218
|
}
|
|
190
|
-
return
|
|
191
|
-
exitCode,
|
|
192
|
-
stderr: cleanedStderr
|
|
193
|
-
});
|
|
219
|
+
return undefined;
|
|
194
220
|
}
|
|
195
221
|
function extractUnsupportedModelMessage(stderr) {
|
|
196
222
|
const lines = uniqueNonEmptyLines(stderr);
|
package/dist/adapters/ollama.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AdapterError } from "../errors.js";
|
|
2
|
+
import { createTranslator } from "../i18n.js";
|
|
2
3
|
import { formatAgentPrompt } from "../prompt.js";
|
|
3
4
|
/**
|
|
4
5
|
* Adapter pour Ollama via l'API HTTP locale (`POST /api/chat`).
|
|
@@ -57,7 +58,7 @@ export class OllamaAdapter {
|
|
|
57
58
|
{
|
|
58
59
|
role: "system",
|
|
59
60
|
content: this.config.systemPrompt ??
|
|
60
|
-
|
|
61
|
+
createTranslator(prompt.language ?? "fr").prompt.ollamaSystemPrompt
|
|
61
62
|
},
|
|
62
63
|
{
|
|
63
64
|
role: "user",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents CLI connus, dans l'ordre d'affichage canonique.
|
|
3
|
+
* `ollama-local` n'est pas listé ici : ce n'est pas une commande CLI, il est
|
|
4
|
+
* géré séparément via `discovery.ollama`.
|
|
5
|
+
*/
|
|
6
|
+
const KNOWN_CLI_AGENTS = [
|
|
7
|
+
{ configKey: "codex", commandAliases: ["codex"], discoveryKey: "codex" },
|
|
8
|
+
{ configKey: "claude", commandAliases: ["claude"], discoveryKey: "claude" },
|
|
9
|
+
{ configKey: "gemini", commandAliases: ["gemini"], discoveryKey: "gemini" },
|
|
10
|
+
{ configKey: "antigravity", commandAliases: ["agy", "antigravity"], discoveryKey: "antigravity" },
|
|
11
|
+
{ configKey: "opencode", commandAliases: ["opencode"], discoveryKey: "opencode" }
|
|
12
|
+
];
|
|
13
|
+
/** Clé de config de l'agent Ollama local par défaut. */
|
|
14
|
+
export const OLLAMA_AGENT_KEY = "ollama-local";
|
|
15
|
+
/**
|
|
16
|
+
* Extrait le nom de base d'une commande en supprimant le chemin et l'extension
|
|
17
|
+
* exécutable Windows éventuelle (ex. `C:\bin\claude.cmd` → `claude`).
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeCommandName(command) {
|
|
20
|
+
return command
|
|
21
|
+
.split(/[\\/]/)
|
|
22
|
+
.pop()
|
|
23
|
+
?.toLowerCase()
|
|
24
|
+
.replace(/\.(exe|cmd|bat|ps1)$/i, "") ?? command.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Résout l'entrée de découverte d'une commande d'agent CLI connue.
|
|
28
|
+
* Retourne `undefined` pour une commande custom non reconnue : Palabre ne peut
|
|
29
|
+
* pas connaître sa sémantique sans la lancer, donc l'appelant la considère
|
|
30
|
+
* généralement comme disponible.
|
|
31
|
+
*/
|
|
32
|
+
export function detectionForCommand(command, discovery) {
|
|
33
|
+
const normalized = normalizeCommandName(command);
|
|
34
|
+
const known = KNOWN_CLI_AGENTS.find((agent) => agent.commandAliases.includes(normalized));
|
|
35
|
+
return known ? discovery[known.discoveryKey] : undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Liste les clés d'agents connus effectivement détectés localement, dans
|
|
39
|
+
* l'ordre canonique (`codex`, `claude`, `gemini`, `antigravity`, `opencode`,
|
|
40
|
+
* puis `ollama-local`).
|
|
41
|
+
*/
|
|
42
|
+
export function detectedAgentNames(discovery) {
|
|
43
|
+
const names = KNOWN_CLI_AGENTS
|
|
44
|
+
.filter((agent) => discovery[agent.discoveryKey].available)
|
|
45
|
+
.map((agent) => agent.configKey);
|
|
46
|
+
if (discovery.ollama.available) {
|
|
47
|
+
names.push(OLLAMA_AGENT_KEY);
|
|
48
|
+
}
|
|
49
|
+
return names;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Applique les chemins de commande résolus localement aux agents CLI connus
|
|
53
|
+
* d'une config. Mute `config` : l'appelant est responsable de la cloner au besoin.
|
|
54
|
+
* Sans détection disponible, l'agent garde la commande déjà déclarée.
|
|
55
|
+
*/
|
|
56
|
+
export function applyDetectedCommands(config, discovery) {
|
|
57
|
+
for (const agent of KNOWN_CLI_AGENTS) {
|
|
58
|
+
const detection = discovery[agent.discoveryKey];
|
|
59
|
+
const cfg = config.agents[agent.configKey];
|
|
60
|
+
if (detection.available && cfg && (cfg.type === "cli" || cfg.type === "cli-pty")) {
|
|
61
|
+
cfg.command = detection.command;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Indique si un agent de config est détecté localement.
|
|
67
|
+
* Pour Ollama, reflète l'accessibilité du serveur ; pour les CLIs connues, l'état
|
|
68
|
+
* de découverte ; pour une CLI custom inconnue, retourne `true` (faute de pouvoir vérifier).
|
|
69
|
+
*/
|
|
70
|
+
export function isAgentDetected(name, config, discovery) {
|
|
71
|
+
if (config.type === "ollama") {
|
|
72
|
+
return discovery.ollama.available;
|
|
73
|
+
}
|
|
74
|
+
const detection = detectionForCommand(config.command || name, discovery);
|
|
75
|
+
return detection ? detection.available : true;
|
|
76
|
+
}
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { listPresetNames } from "./presets.js";
|
|
2
|
+
/**
|
|
3
|
+
* Table centrale décrivant l'arité de chaque flag long canonique.
|
|
4
|
+
*
|
|
5
|
+
* C'est la source de vérité du parser : un flag `boolean` ne consomme jamais le
|
|
6
|
+
* token suivant (évite que `--plain codex-claude "x"` avale le preset), un flag
|
|
7
|
+
* `single` exige une valeur, un flag `multi` collecte plusieurs valeurs.
|
|
8
|
+
*
|
|
9
|
+
* Les noms sont les noms canoniques après `normalizeFlagName` (ex. `subject`
|
|
10
|
+
* est normalisé en `topic` avant la recherche dans cette table).
|
|
11
|
+
*/
|
|
12
|
+
const FLAG_SPECS = {
|
|
13
|
+
// Booléens : présence = vrai, aucune valeur consommée.
|
|
14
|
+
help: { arity: "boolean" },
|
|
15
|
+
version: { arity: "boolean" },
|
|
16
|
+
plain: { arity: "boolean" },
|
|
17
|
+
json: { arity: "boolean" },
|
|
18
|
+
"no-summary": { arity: "boolean" },
|
|
19
|
+
"no-early-stop": { arity: "boolean" },
|
|
20
|
+
"show-prompt": { arity: "boolean" },
|
|
21
|
+
"pull-models": { arity: "boolean" },
|
|
22
|
+
local: { arity: "boolean" },
|
|
23
|
+
apply: { arity: "boolean" },
|
|
24
|
+
"clear-defaults": { arity: "boolean" },
|
|
25
|
+
"sync-agents": { arity: "boolean" },
|
|
26
|
+
// Valeur unique.
|
|
27
|
+
"agent-a": { arity: "single" },
|
|
28
|
+
"agent-b": { arity: "single" },
|
|
29
|
+
config: { arity: "single" },
|
|
30
|
+
language: { arity: "single" },
|
|
31
|
+
"model-a": { arity: "single" },
|
|
32
|
+
"model-b": { arity: "single" },
|
|
33
|
+
preset: { arity: "single" },
|
|
34
|
+
"summary-agent": { arity: "single" },
|
|
35
|
+
"summary-model": { arity: "single" },
|
|
36
|
+
topic: { arity: "single" },
|
|
37
|
+
turns: { arity: "single" },
|
|
38
|
+
renderer: { arity: "single" },
|
|
39
|
+
// Valeurs multiples.
|
|
40
|
+
"set-defaults": { arity: "multi", max: 2 },
|
|
41
|
+
files: { arity: "multi" },
|
|
42
|
+
context: { arity: "multi" }
|
|
43
|
+
};
|
|
44
|
+
/** Commandes acceptées comme premier argument positionnel. */
|
|
45
|
+
const COMMANDS = new Set([
|
|
46
|
+
"run",
|
|
47
|
+
"new",
|
|
48
|
+
"init",
|
|
49
|
+
"setup",
|
|
50
|
+
"help",
|
|
51
|
+
"version",
|
|
52
|
+
"update",
|
|
53
|
+
"doctor",
|
|
54
|
+
"config",
|
|
55
|
+
"agent",
|
|
56
|
+
"agents",
|
|
57
|
+
"preset",
|
|
58
|
+
"presets",
|
|
59
|
+
"context"
|
|
60
|
+
]);
|
|
61
|
+
/**
|
|
62
|
+
* Parse `process.argv` en une structure typée `ParsedArgs`.
|
|
63
|
+
* Gère les flags courts (-h, -v, -s, -t, -a), les flags longs pilotés par
|
|
64
|
+
* `FLAG_SPECS`, les flags multi-valeurs (--files, --context, --set-defaults) et
|
|
65
|
+
* les positionnels.
|
|
66
|
+
* @param args - Tableau d'arguments (généralement `process.argv.slice(2)`).
|
|
67
|
+
* @returns Commande détectée, indicateur d'explicitation et map de flags.
|
|
68
|
+
*/
|
|
69
|
+
export function parseArgs(args, messages) {
|
|
70
|
+
const flags = {};
|
|
71
|
+
let command = "run";
|
|
72
|
+
let commandExplicit = false;
|
|
73
|
+
const positionals = [];
|
|
74
|
+
const presets = new Set(listPresetNames());
|
|
75
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
76
|
+
const value = args[index];
|
|
77
|
+
if (!value.startsWith("-") && !commandExplicit && positionals.length === 0 && COMMANDS.has(value)) {
|
|
78
|
+
command = value;
|
|
79
|
+
commandExplicit = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!value.startsWith("-") && index === 0) {
|
|
83
|
+
if (COMMANDS.has(value)) {
|
|
84
|
+
command = value;
|
|
85
|
+
commandExplicit = true;
|
|
86
|
+
}
|
|
87
|
+
else if (isLikelyCommandTypo(value, COMMANDS)) {
|
|
88
|
+
throw new Error(messages.common.unknownCommand(value, Array.from(COMMANDS).join(", ")));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
positionals.push(value);
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!value.startsWith("-")) {
|
|
96
|
+
positionals.push(value);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (value === "-h") {
|
|
100
|
+
flags.help = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (value === "-v") {
|
|
104
|
+
flags.version = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (value === "-a") {
|
|
108
|
+
command = "agents";
|
|
109
|
+
commandExplicit = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (value === "-s") {
|
|
113
|
+
const next = args[index + 1];
|
|
114
|
+
if (!next || next.startsWith("-")) {
|
|
115
|
+
throw new Error(messages.common.optionRequiresValue("-s"));
|
|
116
|
+
}
|
|
117
|
+
flags.topic = next;
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (value === "-t") {
|
|
122
|
+
const next = args[index + 1];
|
|
123
|
+
if (!next || next.startsWith("-")) {
|
|
124
|
+
throw new Error(messages.common.optionRequiresValue("-t"));
|
|
125
|
+
}
|
|
126
|
+
flags.turns = next;
|
|
127
|
+
index += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (value.startsWith("--")) {
|
|
131
|
+
const rawKey = value.slice(2);
|
|
132
|
+
const key = normalizeFlagName(rawKey);
|
|
133
|
+
const spec = FLAG_SPECS[key];
|
|
134
|
+
if (spec?.arity === "multi") {
|
|
135
|
+
const values = [];
|
|
136
|
+
while (args[index + 1] && !args[index + 1].startsWith("-") && (spec.max === undefined || values.length < spec.max)) {
|
|
137
|
+
values.push(args[index + 1]);
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
140
|
+
if (key === "set-defaults" && values.length !== 2) {
|
|
141
|
+
throw new Error(messages.common.setDefaultsRequiresTwo);
|
|
142
|
+
}
|
|
143
|
+
flags[key] = key === "set-defaults"
|
|
144
|
+
? values
|
|
145
|
+
: [...getStringListFlag(flags[key]), ...values];
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (spec?.arity === "boolean") {
|
|
149
|
+
flags[key] = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const next = args[index + 1];
|
|
153
|
+
const wantsValue = spec?.arity === "single";
|
|
154
|
+
if (!next || next.startsWith("-")) {
|
|
155
|
+
if (wantsValue) {
|
|
156
|
+
throw new Error(messages.common.optionRequiresValue(`--${rawKey}`));
|
|
157
|
+
}
|
|
158
|
+
flags[key] = true;
|
|
159
|
+
}
|
|
160
|
+
else if (wantsValue) {
|
|
161
|
+
flags[key] = next;
|
|
162
|
+
index += 1;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Flag inconnu : traité comme booléen pour ne jamais avaler un positionnel.
|
|
166
|
+
flags[key] = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (command === "run") {
|
|
171
|
+
applyRunPositionals(positionals, flags, presets, commandExplicit, COMMANDS, messages);
|
|
172
|
+
}
|
|
173
|
+
return { command, commandExplicit, positionals, flags };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Détecte si une valeur ressemble à une faute de frappe d'une commande connue
|
|
177
|
+
* (même première lettre et distance de Levenshtein ≤ 2).
|
|
178
|
+
* @param value - Token saisi par l'utilisateur.
|
|
179
|
+
* @param commands - Ensemble des commandes valides.
|
|
180
|
+
*/
|
|
181
|
+
export function isLikelyCommandTypo(value, commands) {
|
|
182
|
+
const normalized = value.toLowerCase();
|
|
183
|
+
for (const command of commands) {
|
|
184
|
+
if (normalized[0] === command[0] && levenshteinDistance(normalized, command) <= 2) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Calcule la distance de Levenshtein entre deux chaînes (insertions, suppressions, substitutions).
|
|
192
|
+
* @param left - Première chaîne.
|
|
193
|
+
* @param right - Deuxième chaîne.
|
|
194
|
+
* @returns Distance entière ≥ 0.
|
|
195
|
+
*/
|
|
196
|
+
function levenshteinDistance(left, right) {
|
|
197
|
+
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
198
|
+
for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
|
|
199
|
+
let diagonal = previous[0];
|
|
200
|
+
previous[0] = leftIndex + 1;
|
|
201
|
+
for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
|
|
202
|
+
const insertCost = previous[rightIndex + 1] + 1;
|
|
203
|
+
const deleteCost = previous[rightIndex] + 1;
|
|
204
|
+
const replaceCost = diagonal + (left[leftIndex] === right[rightIndex] ? 0 : 1);
|
|
205
|
+
diagonal = previous[rightIndex + 1];
|
|
206
|
+
previous[rightIndex + 1] = Math.min(insertCost, deleteCost, replaceCost);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return previous[right.length] ?? 0;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Interprète les arguments positionnels pour la commande `run` :
|
|
213
|
+
* premier positionnel = preset si connu, sinon sujet complet concaténé.
|
|
214
|
+
* @param positionals - Arguments positionnels extraits du parseur.
|
|
215
|
+
* @param flags - Map de flags à muter si un preset ou un sujet est détecté.
|
|
216
|
+
* @param presets - Ensemble des noms de presets valides.
|
|
217
|
+
* @param commandExplicit - `true` si l'utilisateur a tapé `palabre run` explicitement.
|
|
218
|
+
*/
|
|
219
|
+
function applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages) {
|
|
220
|
+
if (positionals.length === 0) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const [first, ...rest] = positionals;
|
|
224
|
+
if (presets.has(first)) {
|
|
225
|
+
flags.preset ??= first;
|
|
226
|
+
if (rest.length > 0) {
|
|
227
|
+
flags.topic ??= rest.join(" ");
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!commandExplicit && positionals.length === 1 && !positionals[0]?.includes(" ")) {
|
|
232
|
+
if (isLikelyCommandTypo(positionals[0], commands)) {
|
|
233
|
+
throw new Error(messages.common.unknownCommand(positionals[0], Array.from(commands).join(", ")));
|
|
234
|
+
}
|
|
235
|
+
throw new Error(messages.common.ambiguousSubject(positionals[0]));
|
|
236
|
+
}
|
|
237
|
+
flags.topic ??= positionals.join(" ");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Normalise un nom de flag long en son alias canonique (ex. `subject` → `topic`).
|
|
241
|
+
* @param value - Nom brut extrait après `--`.
|
|
242
|
+
*/
|
|
243
|
+
export function normalizeFlagName(value) {
|
|
244
|
+
const aliases = {
|
|
245
|
+
lang: "language",
|
|
246
|
+
s: "topic",
|
|
247
|
+
subject: "topic",
|
|
248
|
+
t: "turns"
|
|
249
|
+
};
|
|
250
|
+
return aliases[value] ?? value;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Normalise une valeur de flag multi-valeur en tableau de chaînes.
|
|
254
|
+
* @param value - Valeur brute (tableau, chaîne unique ou absent).
|
|
255
|
+
* @returns Tableau de chaînes, vide si la valeur n'est pas applicable.
|
|
256
|
+
*/
|
|
257
|
+
export function getStringListFlag(value) {
|
|
258
|
+
if (Array.isArray(value)) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
if (typeof value === "string") {
|
|
262
|
+
return [value];
|
|
263
|
+
}
|
|
264
|
+
return [];
|
|
265
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { applyDetectedCommands } from "./agentRegistry.js";
|
|
4
5
|
export const DEFAULT_CONFIG_PATH = "palabre.config.json";
|
|
5
6
|
export const LEGACY_CONFIG_PATH = "chicane.config.json";
|
|
6
7
|
export const CONFIG_DIR_NAME = ".palabre";
|
|
@@ -118,6 +119,30 @@ export async function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
|
|
|
118
119
|
const raw = await readFile(resolved, "utf8");
|
|
119
120
|
return JSON.parse(raw);
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Valide qu'une config chargée est exploitable pour lancer un débat.
|
|
124
|
+
*
|
|
125
|
+
* `loadConfig` se contente de parser le JSON ; cette garde attrape les configs
|
|
126
|
+
* structurellement cassées (racine non-objet, bloc `agents` absent ou vide)
|
|
127
|
+
* avant qu'elles ne provoquent un `TypeError` opaque dans l'orchestrateur.
|
|
128
|
+
* Volontairement minimale : la validation sémantique fine (agents par défaut
|
|
129
|
+
* inconnus, timeouts invalides, etc.) reste du ressort de `palabre doctor`.
|
|
130
|
+
*
|
|
131
|
+
* @throws {Error} message actionnable si la config ne peut pas faire tourner un débat.
|
|
132
|
+
*/
|
|
133
|
+
export function assertRunnableConfig(config, messages, configPath = DEFAULT_CONFIG_PATH) {
|
|
134
|
+
const root = config;
|
|
135
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) {
|
|
136
|
+
throw new Error(messages.common.configInvalidShape(configPath));
|
|
137
|
+
}
|
|
138
|
+
const agents = root.agents;
|
|
139
|
+
if (!agents || typeof agents !== "object" || Array.isArray(agents)) {
|
|
140
|
+
throw new Error(messages.common.configMissingAgents(configPath));
|
|
141
|
+
}
|
|
142
|
+
if (Object.keys(agents).length === 0) {
|
|
143
|
+
throw new Error(messages.common.configEmptyAgents(configPath));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
121
146
|
/** Retourne `true` si le fichier de config est accessible en lecture. Silencieux sur toute erreur filesystem. */
|
|
122
147
|
export async function configExists(configPath = DEFAULT_CONFIG_PATH) {
|
|
123
148
|
try {
|
|
@@ -156,26 +181,7 @@ export async function resolveDefaultConfigPath() {
|
|
|
156
181
|
export function createConfigFromDiscovery(discovery) {
|
|
157
182
|
const config = cloneConfig(exampleConfig);
|
|
158
183
|
const pair = chooseDefaultPair(discovery);
|
|
159
|
-
config
|
|
160
|
-
...config.agents.codex,
|
|
161
|
-
...(discovery.codex.available ? { command: discovery.codex.command } : {})
|
|
162
|
-
};
|
|
163
|
-
config.agents.claude = {
|
|
164
|
-
...config.agents.claude,
|
|
165
|
-
...(discovery.claude.available ? { command: discovery.claude.command } : {})
|
|
166
|
-
};
|
|
167
|
-
config.agents.gemini = {
|
|
168
|
-
...config.agents.gemini,
|
|
169
|
-
...(discovery.gemini.available ? { command: discovery.gemini.command } : {})
|
|
170
|
-
};
|
|
171
|
-
config.agents.antigravity = {
|
|
172
|
-
...config.agents.antigravity,
|
|
173
|
-
...(discovery.antigravity.available ? { command: discovery.antigravity.command } : {})
|
|
174
|
-
};
|
|
175
|
-
config.agents.opencode = {
|
|
176
|
-
...config.agents.opencode,
|
|
177
|
-
...(discovery.opencode.available ? { command: discovery.opencode.command } : {})
|
|
178
|
-
};
|
|
184
|
+
applyDetectedCommands(config, discovery);
|
|
179
185
|
const ollamaAgent = config.agents["ollama-local"];
|
|
180
186
|
if (ollamaAgent?.type === "ollama") {
|
|
181
187
|
ollamaAgent.model = chooseDefaultOllamaModel(discovery);
|