palabre 0.6.0 → 0.6.3
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 +33 -3
- package/dist/adapters/cli-pty.js +16 -28
- package/dist/adapters/cli-shared.js +24 -0
- package/dist/adapters/cli.js +61 -23
- 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/contextScan.js +48 -0
- package/dist/discovery.js +1 -12
- package/dist/doctor.js +2 -27
- package/dist/exec.js +17 -0
- package/dist/index.js +38 -258
- package/dist/messages/adapter-errors.js +2 -0
- package/dist/messages/common.js +6 -0
- package/dist/messages/help.js +22 -0
- package/dist/messages/orchestrator.js +19 -0
- package/dist/messages/output.js +14 -2
- package/dist/messages/prompt.js +6 -2
- package/dist/new.js +1 -26
- package/dist/orchestrator.js +79 -30
- package/dist/output.js +20 -3
- package/dist/presets.js +2 -21
- package/dist/prompt.js +4 -0
- package/dist/renderers/console.js +15 -0
- package/dist/renderers/ndjson.js +4 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ palabre -s "Compare ces deux approches" -t 2
|
|
|
50
50
|
palabre codex-claude "Relis cette architecture" --context src docs
|
|
51
51
|
palabre claude-ollama "Critique ce fichier" --files README.md
|
|
52
52
|
palabre codex-claude "Preview" --context src --show-prompt
|
|
53
|
+
palabre context scan src docs --json
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
### Agents supportés
|
|
@@ -63,10 +64,22 @@ palabre codex-claude "Preview" --context src --show-prompt
|
|
|
63
64
|
|
|
64
65
|
PALABRE ne liste pas les modèles : ils changent souvent et dépendent de chaque CLI ou compte utilisateur. `--model-a`, `--model-b` et `--summary-model` transmettent simplement la valeur brute à l'agent concerné.
|
|
65
66
|
|
|
67
|
+
### Intégrations
|
|
68
|
+
|
|
69
|
+
PALABRE expose des sorties JSON versionnées pour les clients externes :
|
|
70
|
+
|
|
71
|
+
- `palabre presets --json` pour lire les paires d'agents disponibles ;
|
|
72
|
+
- `palabre context scan --json` pour prévisualiser le contexte que `--context` retiendrait ;
|
|
73
|
+
- `--renderer ndjson` ou `--json` pour suivre un débat événement par événement.
|
|
74
|
+
|
|
75
|
+
Le flux NDJSON v1 est traité comme une API publique d'intégration. Les ajouts compatibles se font sans casser v1 ; les changements cassants doivent changer le champ `v`.
|
|
76
|
+
|
|
66
77
|
### Confidentialité
|
|
67
78
|
|
|
68
79
|
PALABRE tourne localement et n'envoie aucune donnée à un serveur appartenant à PALABRE. Les données envoyées aux agents dépendent des outils que vous utilisez : vérifiez les politiques de confidentialité de Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Ollama ou de tout autre agent configuré.
|
|
69
80
|
|
|
81
|
+
Si un agent échoue pendant le débat ou la synthèse, PALABRE conserve l'export Markdown partiel avec une section d'interruption quand c'est possible.
|
|
82
|
+
|
|
70
83
|
### Développement local
|
|
71
84
|
|
|
72
85
|
```bash
|
|
@@ -80,7 +93,9 @@ palabre --version
|
|
|
80
93
|
|
|
81
94
|
Commandes utiles : `pnpm check`, `pnpm test`, `pnpm build`.
|
|
82
95
|
|
|
83
|
-
|
|
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).
|
|
84
99
|
|
|
85
100
|
### Licence
|
|
86
101
|
|
|
@@ -97,7 +112,7 @@ It does not replace your tools: it drives them. You keep your subscriptions, def
|
|
|
97
112
|
- https://palab.re
|
|
98
113
|
- https://palabre.netlify.app
|
|
99
114
|
|
|
100
|
-
Useful pages: [Installation](https://palab.re/
|
|
115
|
+
Useful pages: [Installation](https://palab.re/en/get-started/installation), [Configuration](https://palab.re/en/get-started/configuration), [First debate](https://palab.re/en/get-started/first-debate), [CLI reference](https://palab.re/en/reference/cli), [Troubleshooting](https://palab.re/en/troubleshooting), [Roadmap](https://palab.re/en/roadmap).
|
|
101
116
|
|
|
102
117
|
### Installation
|
|
103
118
|
|
|
@@ -125,6 +140,7 @@ palabre -s "Compare these two approaches" -t 2
|
|
|
125
140
|
palabre codex-claude "Review this architecture" --context src docs
|
|
126
141
|
palabre claude-ollama "Review this file" --files README.md
|
|
127
142
|
palabre codex-claude "Preview" --context src --show-prompt
|
|
143
|
+
palabre context scan src docs --json
|
|
128
144
|
```
|
|
129
145
|
|
|
130
146
|
### Supported Agents
|
|
@@ -138,10 +154,22 @@ palabre codex-claude "Preview" --context src --show-prompt
|
|
|
138
154
|
|
|
139
155
|
PALABRE does not list models: they change often and depend on each CLI or user account. `--model-a`, `--model-b`, and `--summary-model` simply pass the raw value to the selected agent.
|
|
140
156
|
|
|
157
|
+
### Integrations
|
|
158
|
+
|
|
159
|
+
PALABRE exposes versioned JSON outputs for external clients:
|
|
160
|
+
|
|
161
|
+
- `palabre presets --json` to read available agent pairs;
|
|
162
|
+
- `palabre context scan --json` to preview the context `--context` would retain;
|
|
163
|
+
- `--renderer ndjson` or `--json` to follow a debate event by event.
|
|
164
|
+
|
|
165
|
+
The NDJSON v1 stream is treated as a public integration API. Compatible additions do not break v1; breaking changes must change the `v` field.
|
|
166
|
+
|
|
141
167
|
### Privacy
|
|
142
168
|
|
|
143
169
|
PALABRE runs locally and does not send data to a PALABRE-owned server. Data sent to agents depends on the tools you use: check the privacy policies of Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Ollama, or any custom agent you configure.
|
|
144
170
|
|
|
171
|
+
If an agent fails during the debate or final summary, PALABRE keeps the partial Markdown export with an interruption section whenever possible.
|
|
172
|
+
|
|
145
173
|
### Local Development
|
|
146
174
|
|
|
147
175
|
```bash
|
|
@@ -155,7 +183,9 @@ palabre --version
|
|
|
155
183
|
|
|
156
184
|
Useful commands: `pnpm check`, `pnpm test`, `pnpm build`.
|
|
157
185
|
|
|
158
|
-
|
|
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).
|
|
159
189
|
|
|
160
190
|
### License
|
|
161
191
|
|
package/dist/adapters/cli-pty.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
8
|
/**
|
|
8
9
|
* Adapter pour les CLIs qui exigent un vrai terminal.
|
|
@@ -43,13 +44,16 @@ export class CliPtyAdapter {
|
|
|
43
44
|
const args = promptMode === "argument"
|
|
44
45
|
? [...baseArgs, renderedPrompt]
|
|
45
46
|
: baseArgs;
|
|
47
|
+
const { spawn: spawnPty } = await import("node-pty");
|
|
46
48
|
return new Promise((resolve, reject) => {
|
|
47
49
|
let output = "";
|
|
50
|
+
let outputBytes = 0;
|
|
48
51
|
let settled = false;
|
|
49
52
|
let hardTimer;
|
|
50
53
|
let term;
|
|
51
54
|
let dataSubscription;
|
|
52
55
|
let exitSubscription;
|
|
56
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
53
57
|
const finish = (error, exitCode, kill = true) => {
|
|
54
58
|
if (settled)
|
|
55
59
|
return;
|
|
@@ -104,11 +108,19 @@ export class CliPtyAdapter {
|
|
|
104
108
|
return;
|
|
105
109
|
}
|
|
106
110
|
hardTimer = setTimeout(() => {
|
|
107
|
-
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ??
|
|
108
|
-
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
|
|
109
113
|
}));
|
|
110
|
-
}, this.config.timeoutMs ??
|
|
114
|
+
}, this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
111
115
|
dataSubscription = term.onData((chunk) => {
|
|
116
|
+
outputBytes += Buffer.byteLength(chunk, "utf8");
|
|
117
|
+
if (outputBytes > maxOutputBytes) {
|
|
118
|
+
finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of PTY output`, {
|
|
119
|
+
maxOutputBytes,
|
|
120
|
+
outputBytes
|
|
121
|
+
}));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
112
124
|
output += chunk;
|
|
113
125
|
});
|
|
114
126
|
exitSubscription = term.onExit(({ exitCode }) => {
|
|
@@ -147,30 +159,6 @@ function cleanupPty(term) {
|
|
|
147
159
|
// Best-effort cleanup for Windows ConPTY internals.
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
|
-
function executableExtensions(command) {
|
|
151
|
-
if (path.extname(command) || process.platform !== "win32") {
|
|
152
|
-
return [""];
|
|
153
|
-
}
|
|
154
|
-
return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
155
|
-
.split(";")
|
|
156
|
-
.map((extension) => extension.toLowerCase())
|
|
157
|
-
.concat(".ps1", "");
|
|
158
|
-
}
|
|
159
|
-
function withModelArgs(args, model, modelArg) {
|
|
160
|
-
if (!model) {
|
|
161
|
-
return [...args];
|
|
162
|
-
}
|
|
163
|
-
const promptStdinIndex = args.lastIndexOf("-");
|
|
164
|
-
if (promptStdinIndex === args.length - 1) {
|
|
165
|
-
return [
|
|
166
|
-
...args.slice(0, promptStdinIndex),
|
|
167
|
-
modelArg,
|
|
168
|
-
model,
|
|
169
|
-
...args.slice(promptStdinIndex)
|
|
170
|
-
];
|
|
171
|
-
}
|
|
172
|
-
return [...args, modelArg, model];
|
|
173
|
-
}
|
|
174
162
|
function createPtyExitError(adapterName, exitCode, raw) {
|
|
175
163
|
return new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizePtyOutput(raw)}`, {
|
|
176
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,6 +1,7 @@
|
|
|
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
6
|
/**
|
|
6
7
|
* Adapter pour les CLIs batch (Codex, Claude, Gemini…).
|
|
@@ -50,8 +51,10 @@ export class CliAdapter {
|
|
|
50
51
|
let stdout = "";
|
|
51
52
|
let stderr = "";
|
|
52
53
|
let settled = false;
|
|
54
|
+
let outputBytes = 0;
|
|
53
55
|
let hardTimer;
|
|
54
56
|
let idleTimer;
|
|
57
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
55
58
|
const finish = (error) => {
|
|
56
59
|
if (settled)
|
|
57
60
|
return;
|
|
@@ -78,10 +81,10 @@ export class CliAdapter {
|
|
|
78
81
|
};
|
|
79
82
|
hardTimer = setTimeout(() => {
|
|
80
83
|
child.kill();
|
|
81
|
-
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ??
|
|
82
|
-
timeoutMs: this.config.timeoutMs ??
|
|
84
|
+
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, {
|
|
85
|
+
timeoutMs: this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
83
86
|
}));
|
|
84
|
-
}, this.config.timeoutMs ??
|
|
87
|
+
}, this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
85
88
|
const bumpIdleTimer = () => {
|
|
86
89
|
if (!this.config.idleTimeoutMs)
|
|
87
90
|
return;
|
|
@@ -94,10 +97,28 @@ export class CliAdapter {
|
|
|
94
97
|
};
|
|
95
98
|
bumpIdleTimer();
|
|
96
99
|
child.stdout.on("data", (chunk) => {
|
|
100
|
+
outputBytes += chunk.length;
|
|
101
|
+
if (outputBytes > maxOutputBytes) {
|
|
102
|
+
child.kill();
|
|
103
|
+
finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
|
|
104
|
+
maxOutputBytes,
|
|
105
|
+
outputBytes
|
|
106
|
+
}));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
97
109
|
stdout += chunk.toString("utf8");
|
|
98
110
|
bumpIdleTimer();
|
|
99
111
|
});
|
|
100
112
|
child.stderr.on("data", (chunk) => {
|
|
113
|
+
outputBytes += chunk.length;
|
|
114
|
+
if (outputBytes > maxOutputBytes) {
|
|
115
|
+
child.kill();
|
|
116
|
+
finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
|
|
117
|
+
maxOutputBytes,
|
|
118
|
+
outputBytes
|
|
119
|
+
}));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
101
122
|
stderr += chunk.toString("utf8");
|
|
102
123
|
bumpIdleTimer();
|
|
103
124
|
});
|
|
@@ -123,28 +144,45 @@ export class CliAdapter {
|
|
|
123
144
|
});
|
|
124
145
|
}
|
|
125
146
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Insère `modelArg model` dans la liste d'arguments.
|
|
128
|
-
* Si le dernier argument est `-` (stdin marker), insère avant lui pour préserver l'ordre attendu par les CLIs.
|
|
129
|
-
*/
|
|
130
|
-
function withModelArgs(args, model, modelArg) {
|
|
131
|
-
if (!model) {
|
|
132
|
-
return [...args];
|
|
133
|
-
}
|
|
134
|
-
const promptStdinIndex = args.lastIndexOf("-");
|
|
135
|
-
if (promptStdinIndex === args.length - 1) {
|
|
136
|
-
return [
|
|
137
|
-
...args.slice(0, promptStdinIndex),
|
|
138
|
-
modelArg,
|
|
139
|
-
model,
|
|
140
|
-
...args.slice(promptStdinIndex)
|
|
141
|
-
];
|
|
142
|
-
}
|
|
143
|
-
return [...args, modelArg, model];
|
|
144
|
-
}
|
|
145
147
|
/** Retire les séquences ANSI et les espaces en tête/fin. */
|
|
146
148
|
function cleanCliOutput(output) {
|
|
147
|
-
return cleanTerminalOutput(output);
|
|
149
|
+
return stripWindowsTaskkillNoise(cleanTerminalOutput(output));
|
|
150
|
+
}
|
|
151
|
+
function stripWindowsTaskkillNoise(output) {
|
|
152
|
+
const lines = output.split("\n");
|
|
153
|
+
const kept = [];
|
|
154
|
+
let skipNextFrenchContinuation = false;
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
const trimmed = line.trim();
|
|
157
|
+
const normalized = normalizeForWindowsStatus(trimmed);
|
|
158
|
+
if (skipNextFrenchContinuation && /^arr.*t.*\.$/i.test(normalized)) {
|
|
159
|
+
skipNextFrenchContinuation = false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
skipNextFrenchContinuation = false;
|
|
163
|
+
if (isWindowsTaskkillStatusLine(trimmed)) {
|
|
164
|
+
skipNextFrenchContinuation = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
kept.push(line);
|
|
168
|
+
}
|
|
169
|
+
return kept.join("\n").trim();
|
|
170
|
+
}
|
|
171
|
+
function isWindowsTaskkillStatusLine(line) {
|
|
172
|
+
const normalized = normalizeForWindowsStatus(line);
|
|
173
|
+
const lower = line.toLowerCase();
|
|
174
|
+
return (/^SUCCESS:\s+The process with PID \d+ .* has been terminated\.$/i.test(line) ||
|
|
175
|
+
/^operation reussie.*processus de pid \d+ .* a ete$/.test(normalized) ||
|
|
176
|
+
(lower.startsWith("op") &&
|
|
177
|
+
lower.includes("processus de pid ") &&
|
|
178
|
+
lower.includes("processus enfant de pid") &&
|
|
179
|
+
lower.includes(" a ")));
|
|
180
|
+
}
|
|
181
|
+
function normalizeForWindowsStatus(line) {
|
|
182
|
+
return line
|
|
183
|
+
.normalize("NFD")
|
|
184
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
185
|
+
.toLowerCase();
|
|
148
186
|
}
|
|
149
187
|
/**
|
|
150
188
|
* Construit une `AdapterError` typée depuis un exit code non nul.
|
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
|
+
}
|