palabre 0.6.1 → 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 +6 -2
- package/dist/adapters/cli-pty.js +6 -29
- package/dist/adapters/cli-shared.js +24 -0
- package/dist/adapters/cli.js +41 -24
- 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 +1 -12
- package/dist/doctor.js +2 -27
- package/dist/exec.js +17 -0
- package/dist/index.js +9 -262
- 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.
|
|
@@ -81,10 +81,10 @@ export class CliAdapter {
|
|
|
81
81
|
};
|
|
82
82
|
hardTimer = setTimeout(() => {
|
|
83
83
|
child.kill();
|
|
84
|
-
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ??
|
|
85
|
-
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
|
|
86
86
|
}));
|
|
87
|
-
}, this.config.timeoutMs ??
|
|
87
|
+
}, this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
88
88
|
const bumpIdleTimer = () => {
|
|
89
89
|
if (!this.config.idleTimeoutMs)
|
|
90
90
|
return;
|
|
@@ -144,28 +144,45 @@ export class CliAdapter {
|
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
146
|
}
|
|
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
147
|
/** Retire les séquences ANSI et les espaces en tête/fin. */
|
|
167
148
|
function cleanCliOutput(output) {
|
|
168
|
-
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();
|
|
169
186
|
}
|
|
170
187
|
/**
|
|
171
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
|
+
}
|
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);
|
package/dist/discovery.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { executableExtensions } from "./exec.js";
|
|
3
4
|
/**
|
|
4
5
|
* Détecte en parallèle toutes les CLIs supportées et le serveur Ollama local.
|
|
5
6
|
* Sur Windows, tente `claude.exe` avant `claude`.
|
|
@@ -100,18 +101,6 @@ async function findExecutable(command) {
|
|
|
100
101
|
}
|
|
101
102
|
return undefined;
|
|
102
103
|
}
|
|
103
|
-
function executableExtensions(command) {
|
|
104
|
-
if (path.extname(command)) {
|
|
105
|
-
return [""];
|
|
106
|
-
}
|
|
107
|
-
if (process.platform !== "win32") {
|
|
108
|
-
return [""];
|
|
109
|
-
}
|
|
110
|
-
return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
111
|
-
.split(";")
|
|
112
|
-
.map((extension) => extension.toLowerCase())
|
|
113
|
-
.concat(".ps1", "");
|
|
114
|
-
}
|
|
115
104
|
async function isAccessible(filePath) {
|
|
116
105
|
try {
|
|
117
106
|
await access(filePath);
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
3
|
import { configExists, loadConfig, resolveDefaultConfigPath, resolveOutputDir } from "./config.js";
|
|
4
|
+
import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
|
|
4
5
|
import { discoverLocalTools } from "./discovery.js";
|
|
5
6
|
import { createTranslator, resolveLanguage } from "./i18n.js";
|
|
6
7
|
import { DEFAULT_TURNS, MAX_TURNS } from "./limits.js";
|
|
@@ -194,7 +195,7 @@ function inspectAgentShape(name, agent, lines, t) {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
function inspectCliAgent(name, agent, discovery, lines, t) {
|
|
197
|
-
const known =
|
|
198
|
+
const known = detectionForCommand(agent.command, discovery);
|
|
198
199
|
const prefix = `${name} [cli:${agent.role}] command=${agent.command}`;
|
|
199
200
|
if (!known) {
|
|
200
201
|
lines.push(info(t.doctor.customCommand(prefix)));
|
|
@@ -219,37 +220,11 @@ function inspectOllamaAgent(name, agent, discovery, lines, t) {
|
|
|
219
220
|
? ok(t.doctor.ollamaInstalled(prefix))
|
|
220
221
|
: warn(t.doctor.ollamaMissing(prefix, agent.model)));
|
|
221
222
|
}
|
|
222
|
-
function detectedAgentNames(discovery) {
|
|
223
|
-
return [
|
|
224
|
-
discovery.codex.available ? "codex" : undefined,
|
|
225
|
-
discovery.claude.available ? "claude" : undefined,
|
|
226
|
-
discovery.gemini.available ? "gemini" : undefined,
|
|
227
|
-
discovery.antigravity.available ? "antigravity" : undefined,
|
|
228
|
-
discovery.opencode.available ? "opencode" : undefined,
|
|
229
|
-
discovery.ollama.available ? "ollama-local" : undefined
|
|
230
|
-
].filter((name) => Boolean(name));
|
|
231
|
-
}
|
|
232
223
|
function formatCommand(label, available, command, resolvedPath, t) {
|
|
233
224
|
return available
|
|
234
225
|
? ok(t.doctor.commandDetected(label, resolvedPath ?? command))
|
|
235
226
|
: warn(t.doctor.commandMissing(label));
|
|
236
227
|
}
|
|
237
|
-
function knownCliDetection(command, discovery) {
|
|
238
|
-
const normalized = path.basename(command).toLowerCase().replace(/\.(exe|cmd|bat)$/i, "");
|
|
239
|
-
if (normalized === "codex")
|
|
240
|
-
return discovery.codex;
|
|
241
|
-
if (normalized === "claude")
|
|
242
|
-
return discovery.claude;
|
|
243
|
-
if (normalized === "gemini")
|
|
244
|
-
return discovery.gemini;
|
|
245
|
-
if (normalized === "agy")
|
|
246
|
-
return discovery.antigravity;
|
|
247
|
-
if (normalized === "antigravity")
|
|
248
|
-
return discovery.antigravity;
|
|
249
|
-
if (normalized === "opencode")
|
|
250
|
-
return discovery.opencode;
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
228
|
function render(lines, plain, t) {
|
|
254
229
|
const hasErrors = lines.some((line) => line.level === "error");
|
|
255
230
|
return {
|
package/dist/exec.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Extensions exécutables candidates pour résoudre une commande dans le PATH.
|
|
4
|
+
*
|
|
5
|
+
* Retourne `[""]` quand la commande porte déjà une extension ou hors Windows.
|
|
6
|
+
* Sur Windows sans extension, dérive la liste de `PATHEXT` et ajoute `.ps1`
|
|
7
|
+
* ainsi que la candidate vide (binaire sans extension).
|
|
8
|
+
*/
|
|
9
|
+
export function executableExtensions(command) {
|
|
10
|
+
if (path.extname(command) || process.platform !== "win32") {
|
|
11
|
+
return [""];
|
|
12
|
+
}
|
|
13
|
+
return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
14
|
+
.split(";")
|
|
15
|
+
.map((extension) => extension.toLowerCase())
|
|
16
|
+
.concat(".ps1", "");
|
|
17
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
|
|
5
|
+
import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
|
|
6
6
|
import { loadProjectInputs } from "./context.js";
|
|
7
7
|
import { buildContextScan } from "./contextScan.js";
|
|
8
8
|
import { discoverLocalTools } from "./discovery.js";
|
|
@@ -20,6 +20,8 @@ import { runDebate } from "./orchestrator.js";
|
|
|
20
20
|
import { writeDebateMarkdown } from "./output.js";
|
|
21
21
|
import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./update.js";
|
|
22
22
|
import { createSessionContext } from "./session.js";
|
|
23
|
+
import { getStringListFlag, parseArgs } from "./args.js";
|
|
24
|
+
import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
|
|
23
25
|
/** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
|
|
24
26
|
async function main() {
|
|
25
27
|
const rawArgs = process.argv.slice(2);
|
|
@@ -111,6 +113,7 @@ async function main() {
|
|
|
111
113
|
configLanguage: config.language
|
|
112
114
|
});
|
|
113
115
|
const messages = createTranslator(language);
|
|
116
|
+
assertRunnableConfig(config, messages, configPath);
|
|
114
117
|
if (parsed.command === "new") {
|
|
115
118
|
const selection = await runNewWizard(config, messages);
|
|
116
119
|
if (!selection) {
|
|
@@ -469,214 +472,6 @@ async function runContextCommand(flags, positionals) {
|
|
|
469
472
|
console.error(`${messages.renderers.warningPrefix} ${warning}`);
|
|
470
473
|
}
|
|
471
474
|
}
|
|
472
|
-
/**
|
|
473
|
-
* Parse `process.argv` en une structure typée `ParsedArgs`.
|
|
474
|
-
* Gère les flags courts (-h, -v, -s, -t, -a), les flags longs (--topic, --agent-a…),
|
|
475
|
-
* les flags multi-valeurs (--files, --context, --set-defaults) et les positionnels.
|
|
476
|
-
* @param args - Tableau d'arguments (généralement `process.argv.slice(2)`).
|
|
477
|
-
* @returns Commande détectée, indicateur d'explicitation et map de flags.
|
|
478
|
-
*/
|
|
479
|
-
function parseArgs(args, messages) {
|
|
480
|
-
const flags = {};
|
|
481
|
-
let command = "run";
|
|
482
|
-
let commandExplicit = false;
|
|
483
|
-
const positionals = [];
|
|
484
|
-
const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents", "preset", "presets", "context"]);
|
|
485
|
-
const presets = new Set(listPresetNames());
|
|
486
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
487
|
-
const value = args[index];
|
|
488
|
-
if (!value.startsWith("-") && !commandExplicit && positionals.length === 0 && commands.has(value)) {
|
|
489
|
-
command = value;
|
|
490
|
-
commandExplicit = true;
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
if (!value.startsWith("-") && index === 0) {
|
|
494
|
-
if (commands.has(value)) {
|
|
495
|
-
command = value;
|
|
496
|
-
commandExplicit = true;
|
|
497
|
-
}
|
|
498
|
-
else if (isLikelyCommandTypo(value, commands)) {
|
|
499
|
-
throw new Error(messages.common.unknownCommand(value, Array.from(commands).join(", ")));
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
positionals.push(value);
|
|
503
|
-
}
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
if (!value.startsWith("-")) {
|
|
507
|
-
positionals.push(value);
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
if (value === "-h") {
|
|
511
|
-
flags.help = true;
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (value === "-v") {
|
|
515
|
-
flags.version = true;
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
if (value === "-a") {
|
|
519
|
-
command = "agents";
|
|
520
|
-
commandExplicit = true;
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
if (value === "-s") {
|
|
524
|
-
const next = args[index + 1];
|
|
525
|
-
if (!next || next.startsWith("-")) {
|
|
526
|
-
throw new Error(messages.common.optionRequiresValue("-s"));
|
|
527
|
-
}
|
|
528
|
-
flags.topic = next;
|
|
529
|
-
index += 1;
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
if (value === "-t") {
|
|
533
|
-
const next = args[index + 1];
|
|
534
|
-
if (!next || next.startsWith("-")) {
|
|
535
|
-
throw new Error(messages.common.optionRequiresValue("-t"));
|
|
536
|
-
}
|
|
537
|
-
flags.turns = next;
|
|
538
|
-
index += 1;
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
if (value.startsWith("--")) {
|
|
542
|
-
const rawKey = value.slice(2);
|
|
543
|
-
const key = normalizeFlagName(rawKey);
|
|
544
|
-
if (key === "set-defaults") {
|
|
545
|
-
const values = [];
|
|
546
|
-
while (args[index + 1] && !args[index + 1].startsWith("-") && values.length < 2) {
|
|
547
|
-
values.push(args[index + 1]);
|
|
548
|
-
index += 1;
|
|
549
|
-
}
|
|
550
|
-
if (values.length !== 2) {
|
|
551
|
-
throw new Error(messages.common.setDefaultsRequiresTwo);
|
|
552
|
-
}
|
|
553
|
-
flags[key] = values;
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
if (key === "files" || key === "context") {
|
|
557
|
-
const values = [];
|
|
558
|
-
while (args[index + 1] && !args[index + 1].startsWith("-")) {
|
|
559
|
-
values.push(args[index + 1]);
|
|
560
|
-
index += 1;
|
|
561
|
-
}
|
|
562
|
-
flags[key] = [...getStringListFlag(flags[key]), ...values];
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
const next = args[index + 1];
|
|
566
|
-
if (!next || next.startsWith("-")) {
|
|
567
|
-
if (requiresFlagValue(key)) {
|
|
568
|
-
throw new Error(messages.common.optionRequiresValue(`--${rawKey}`));
|
|
569
|
-
}
|
|
570
|
-
flags[key] = true;
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
flags[key] = next;
|
|
574
|
-
index += 1;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
if (command === "run") {
|
|
579
|
-
applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages);
|
|
580
|
-
}
|
|
581
|
-
return { command, commandExplicit, positionals, flags };
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Détecte si une valeur ressemble à une faute de frappe d'une commande connue
|
|
585
|
-
* (même première lettre et distance de Levenshtein ≤ 2).
|
|
586
|
-
* @param value - Token saisi par l'utilisateur.
|
|
587
|
-
* @param commands - Ensemble des commandes valides.
|
|
588
|
-
*/
|
|
589
|
-
function isLikelyCommandTypo(value, commands) {
|
|
590
|
-
const normalized = value.toLowerCase();
|
|
591
|
-
for (const command of commands) {
|
|
592
|
-
if (normalized[0] === command[0] && levenshteinDistance(normalized, command) <= 2) {
|
|
593
|
-
return true;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
return false;
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Calcule la distance de Levenshtein entre deux chaînes (insertions, suppressions, substitutions).
|
|
600
|
-
* @param left - Première chaîne.
|
|
601
|
-
* @param right - Deuxième chaîne.
|
|
602
|
-
* @returns Distance entière ≥ 0.
|
|
603
|
-
*/
|
|
604
|
-
function levenshteinDistance(left, right) {
|
|
605
|
-
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
606
|
-
for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
|
|
607
|
-
let diagonal = previous[0];
|
|
608
|
-
previous[0] = leftIndex + 1;
|
|
609
|
-
for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
|
|
610
|
-
const insertCost = previous[rightIndex + 1] + 1;
|
|
611
|
-
const deleteCost = previous[rightIndex] + 1;
|
|
612
|
-
const replaceCost = diagonal + (left[leftIndex] === right[rightIndex] ? 0 : 1);
|
|
613
|
-
diagonal = previous[rightIndex + 1];
|
|
614
|
-
previous[rightIndex + 1] = Math.min(insertCost, deleteCost, replaceCost);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
return previous[right.length] ?? 0;
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Interprète les arguments positionnels pour la commande `run` :
|
|
621
|
-
* premier positionnel = preset si connu, sinon sujet complet concaténé.
|
|
622
|
-
* @param positionals - Arguments positionnels extraits du parseur.
|
|
623
|
-
* @param flags - Map de flags à muter si un preset ou un sujet est détecté.
|
|
624
|
-
* @param presets - Ensemble des noms de presets valides.
|
|
625
|
-
* @param commandExplicit - `true` si l'utilisateur a tapé `palabre run` explicitement.
|
|
626
|
-
*/
|
|
627
|
-
function applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages) {
|
|
628
|
-
if (positionals.length === 0) {
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
const [first, ...rest] = positionals;
|
|
632
|
-
if (presets.has(first)) {
|
|
633
|
-
flags.preset ??= first;
|
|
634
|
-
if (rest.length > 0) {
|
|
635
|
-
flags.topic ??= rest.join(" ");
|
|
636
|
-
}
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
if (!commandExplicit && positionals.length === 1 && !positionals[0]?.includes(" ")) {
|
|
640
|
-
if (isLikelyCommandTypo(positionals[0], commands)) {
|
|
641
|
-
throw new Error(messages.common.unknownCommand(positionals[0], Array.from(commands).join(", ")));
|
|
642
|
-
}
|
|
643
|
-
throw new Error(messages.common.ambiguousSubject(positionals[0]));
|
|
644
|
-
}
|
|
645
|
-
flags.topic ??= positionals.join(" ");
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Normalise un nom de flag long en son alias canonique (ex. `subject` → `topic`).
|
|
649
|
-
* @param value - Nom brut extrait après `--`.
|
|
650
|
-
*/
|
|
651
|
-
function normalizeFlagName(value) {
|
|
652
|
-
const aliases = {
|
|
653
|
-
lang: "language",
|
|
654
|
-
s: "topic",
|
|
655
|
-
subject: "topic",
|
|
656
|
-
t: "turns"
|
|
657
|
-
};
|
|
658
|
-
return aliases[value] ?? value;
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Indique si un flag long nécessite une valeur suivante (lève une erreur si absente).
|
|
662
|
-
* @param value - Nom canonique du flag (sans `--`).
|
|
663
|
-
*/
|
|
664
|
-
function requiresFlagValue(value) {
|
|
665
|
-
return new Set([
|
|
666
|
-
"agent-a",
|
|
667
|
-
"agent-b",
|
|
668
|
-
"config",
|
|
669
|
-
"language",
|
|
670
|
-
"model-a",
|
|
671
|
-
"model-b",
|
|
672
|
-
"preset",
|
|
673
|
-
"summary-agent",
|
|
674
|
-
"summary-model",
|
|
675
|
-
"set-defaults",
|
|
676
|
-
"topic",
|
|
677
|
-
"turns"
|
|
678
|
-
]).has(value);
|
|
679
|
-
}
|
|
680
475
|
/** Lit la version depuis `package.json` adjacent au bundle compilé. */
|
|
681
476
|
async function getPackageVersion() {
|
|
682
477
|
const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
@@ -684,20 +479,6 @@ async function getPackageVersion() {
|
|
|
684
479
|
const packageJson = JSON.parse(raw);
|
|
685
480
|
return packageJson.version ?? "0.0.0";
|
|
686
481
|
}
|
|
687
|
-
/**
|
|
688
|
-
* Normalise une valeur de flag multi-valeur en tableau de chaînes.
|
|
689
|
-
* @param value - Valeur brute (tableau, chaîne unique ou absent).
|
|
690
|
-
* @returns Tableau de chaînes, vide si la valeur n'est pas applicable.
|
|
691
|
-
*/
|
|
692
|
-
function getStringListFlag(value) {
|
|
693
|
-
if (Array.isArray(value)) {
|
|
694
|
-
return value;
|
|
695
|
-
}
|
|
696
|
-
if (typeof value === "string") {
|
|
697
|
-
return [value];
|
|
698
|
-
}
|
|
699
|
-
return [];
|
|
700
|
-
}
|
|
701
482
|
/**
|
|
702
483
|
* Écrit les avertissements de contexte sur `stderr`.
|
|
703
484
|
* @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
|
|
@@ -728,15 +509,7 @@ function syncDetectedAgents(config, discovery) {
|
|
|
728
509
|
* @param discovery - Résultat de la découverte locale des outils.
|
|
729
510
|
*/
|
|
730
511
|
function findDetectedMissingAgents(config, discovery) {
|
|
731
|
-
|
|
732
|
-
discovery.codex.available ? "codex" : undefined,
|
|
733
|
-
discovery.claude.available ? "claude" : undefined,
|
|
734
|
-
discovery.gemini.available ? "gemini" : undefined,
|
|
735
|
-
discovery.antigravity.available ? "antigravity" : undefined,
|
|
736
|
-
discovery.opencode.available ? "opencode" : undefined,
|
|
737
|
-
discovery.ollama.available ? "ollama-local" : undefined
|
|
738
|
-
].filter((agent) => Boolean(agent));
|
|
739
|
-
return detectedAgents.filter((agentName) => !config.agents[agentName]);
|
|
512
|
+
return detectedAgentNames(discovery).filter((agentName) => !config.agents[agentName]);
|
|
740
513
|
}
|
|
741
514
|
/**
|
|
742
515
|
* Affiche la liste des agents déclarés avec leur type, rôle, état de détection et défauts.
|
|
@@ -807,34 +580,15 @@ function formatAgentDetection(name, agentConfig, discovery, messages) {
|
|
|
807
580
|
return detection.available ? messages.agents.detected(detection.command) : messages.agents.notDetected;
|
|
808
581
|
}
|
|
809
582
|
/**
|
|
810
|
-
* Résout l'entrée de détection correspondant à un agent CLI
|
|
583
|
+
* Résout l'entrée de détection correspondant à un agent CLI.
|
|
811
584
|
* Renvoie un objet `{ available: true }` pour les agents CLI non reconnus (considérés disponibles).
|
|
812
585
|
* @param name - Nom de l'agent dans la config.
|
|
813
586
|
* @param agentConfig - Configuration de l'agent.
|
|
814
587
|
* @param discovery - Résultat de la découverte locale des outils.
|
|
815
588
|
*/
|
|
816
589
|
function cliDetectionForAgent(name, agentConfig, discovery) {
|
|
817
|
-
const command =
|
|
818
|
-
|
|
819
|
-
return discovery.codex;
|
|
820
|
-
if (command === "claude")
|
|
821
|
-
return discovery.claude;
|
|
822
|
-
if (command === "gemini")
|
|
823
|
-
return discovery.gemini;
|
|
824
|
-
if (command === "agy")
|
|
825
|
-
return discovery.antigravity;
|
|
826
|
-
if (command === "antigravity")
|
|
827
|
-
return discovery.antigravity;
|
|
828
|
-
if (command === "opencode")
|
|
829
|
-
return discovery.opencode;
|
|
830
|
-
return { available: true, command: agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name };
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Extrait le nom de base d'une commande en supprimant le chemin et l'extension Windows éventuelle.
|
|
834
|
-
* @param command - Chemin ou nom de commande brut (ex. `C:\bin\claude.cmd`).
|
|
835
|
-
*/
|
|
836
|
-
function normalizeCommandName(command) {
|
|
837
|
-
return path.basename(command).replace(/\.(cmd|exe|ps1|bat)$/i, "").toLowerCase();
|
|
590
|
+
const command = agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name;
|
|
591
|
+
return detectionForCommand(command, discovery) ?? { available: true, command };
|
|
838
592
|
}
|
|
839
593
|
/**
|
|
840
594
|
* Affiche le récapitulatif de détection locale après `palabre init`.
|
|
@@ -857,14 +611,7 @@ function printInitDiscovery(discovery, config, messages) {
|
|
|
857
611
|
console.log(messages.init.languageHint(config.language ?? DEFAULT_LANGUAGE));
|
|
858
612
|
}
|
|
859
613
|
function formatDetectedAgentSummary(discovery, language) {
|
|
860
|
-
const names =
|
|
861
|
-
discovery.codex.available ? "codex" : undefined,
|
|
862
|
-
discovery.claude.available ? "claude" : undefined,
|
|
863
|
-
discovery.gemini.available ? "gemini" : undefined,
|
|
864
|
-
discovery.antigravity.available ? "antigravity" : undefined,
|
|
865
|
-
discovery.opencode.available ? "opencode" : undefined,
|
|
866
|
-
discovery.ollama.available ? "ollama-local" : undefined
|
|
867
|
-
].filter((name) => Boolean(name));
|
|
614
|
+
const names = detectedAgentNames(discovery);
|
|
868
615
|
if (names.length === 0) {
|
|
869
616
|
return language === "en" ? "no agent detected" : "aucun agent détecté";
|
|
870
617
|
}
|
package/dist/messages/common.js
CHANGED
|
@@ -10,6 +10,9 @@ export const commonMessages = {
|
|
|
10
10
|
unknownAgentForField: (field, agent, available) => `Agent inconnu pour ${field}: ${agent}. Agents disponibles: ${available}.`,
|
|
11
11
|
unknownAgent: (agent) => `Agent inconnu: ${agent}`,
|
|
12
12
|
unknownRenderer: (value, supported) => `Renderer inconnu: ${value}. Valeurs supportées: ${supported}.`,
|
|
13
|
+
configInvalidShape: (configPath) => `Config invalide: ${configPath} ne contient pas un objet JSON. Relance palabre init ou corrige le fichier.`,
|
|
14
|
+
configMissingAgents: (configPath) => `Config invalide: ${configPath} ne déclare pas de bloc "agents". Relance palabre init ou ajoute au moins un agent.`,
|
|
15
|
+
configEmptyAgents: (configPath) => `Config invalide: ${configPath} ne déclare aucun agent. Ajoute au moins un agent ou relance palabre init.`,
|
|
13
16
|
errorPrefix: "Erreur"
|
|
14
17
|
},
|
|
15
18
|
en: {
|
|
@@ -23,6 +26,9 @@ export const commonMessages = {
|
|
|
23
26
|
unknownAgentForField: (field, agent, available) => `Unknown agent for ${field}: ${agent}. Available agents: ${available}.`,
|
|
24
27
|
unknownAgent: (agent) => `Unknown agent: ${agent}`,
|
|
25
28
|
unknownRenderer: (value, supported) => `Unknown renderer: ${value}. Supported values: ${supported}.`,
|
|
29
|
+
configInvalidShape: (configPath) => `Invalid config: ${configPath} does not contain a JSON object. Run palabre init or fix the file.`,
|
|
30
|
+
configMissingAgents: (configPath) => `Invalid config: ${configPath} has no "agents" block. Run palabre init or add at least one agent.`,
|
|
31
|
+
configEmptyAgents: (configPath) => `Invalid config: ${configPath} declares no agent. Add at least one agent or run palabre init.`,
|
|
26
32
|
errorPrefix: "Error"
|
|
27
33
|
}
|
|
28
34
|
};
|
|
@@ -2,12 +2,31 @@ export const orchestratorMessages = {
|
|
|
2
2
|
fr: {
|
|
3
3
|
agreementStopReason: "Accord clair detecte apres un tour complet.",
|
|
4
4
|
earlyStop: (reason) => `Arret anticipe: ${reason}`,
|
|
5
|
+
agreementPatterns: [
|
|
6
|
+
"accord complet",
|
|
7
|
+
"accord total",
|
|
8
|
+
"aucun desaccord",
|
|
9
|
+
"aucune incertitude",
|
|
10
|
+
"rien a trancher",
|
|
11
|
+
"rien a ajouter",
|
|
12
|
+
"question factuelle resolue"
|
|
13
|
+
],
|
|
5
14
|
ollamaNoContext: (agentNames) => `${agentNames} ne lit pas le filesystem. Ajoute --files ou --context pour fournir un contexte projet.`,
|
|
6
15
|
unknownSummaryAgent: (agentName) => `Agent de synthese inconnu: ${agentName}`
|
|
7
16
|
},
|
|
8
17
|
en: {
|
|
9
18
|
agreementStopReason: "Clear agreement detected after a complete round.",
|
|
10
19
|
earlyStop: (reason) => `Early stop: ${reason}`,
|
|
20
|
+
agreementPatterns: [
|
|
21
|
+
"full agreement",
|
|
22
|
+
"complete agreement",
|
|
23
|
+
"total agreement",
|
|
24
|
+
"no disagreement",
|
|
25
|
+
"no remaining uncertainty",
|
|
26
|
+
"nothing to settle",
|
|
27
|
+
"nothing to add",
|
|
28
|
+
"factual question resolved"
|
|
29
|
+
],
|
|
11
30
|
ollamaNoContext: (agentNames) => `${agentNames} cannot read the filesystem. Add --files or --context to provide project context.`,
|
|
12
31
|
unknownSummaryAgent: (agentName) => `Unknown summary agent: ${agentName}`
|
|
13
32
|
}
|
package/dist/messages/prompt.js
CHANGED
|
@@ -29,6 +29,7 @@ export const promptMessages = {
|
|
|
29
29
|
cwd: (value) => `- Dossier courant: ${value}`,
|
|
30
30
|
sessionStartedAt: (value) => `- Session demarree a: ${value}`,
|
|
31
31
|
turnProgress: (turn, totalTurns) => `- Tour courant: ${turn}/${totalTurns}`,
|
|
32
|
+
responseLanguageInstruction: "Langue de reponse obligatoire: francais. Reponds uniquement en francais, meme si le sujet ou le transcript contient une autre langue.",
|
|
32
33
|
objectiveTitle: "Objectif:",
|
|
33
34
|
debateObjectives: [
|
|
34
35
|
"- Apporte une reponse utile, concrete et courte.",
|
|
@@ -55,7 +56,8 @@ export const promptMessages = {
|
|
|
55
56
|
actionsHeading: "### Actions proposees",
|
|
56
57
|
conclusionHeading: "### Conclusion",
|
|
57
58
|
finalProseInstruction: "Un court paragraphe de synthese en prose, sans liste, qui resume le sens general du debat et la decision ou direction la plus raisonnable.",
|
|
58
|
-
summaryAnswerTitle: "Synthese:"
|
|
59
|
+
summaryAnswerTitle: "Synthese:",
|
|
60
|
+
ollamaSystemPrompt: "Tu participes a un debat technique orchestre. Reste precis, utile et honnete sur tes limites."
|
|
59
61
|
},
|
|
60
62
|
en: {
|
|
61
63
|
subject: (topic) => `Subject: ${topic}`,
|
|
@@ -71,6 +73,7 @@ export const promptMessages = {
|
|
|
71
73
|
cwd: (value) => `- Current directory: ${value}`,
|
|
72
74
|
sessionStartedAt: (value) => `- Session started at: ${value}`,
|
|
73
75
|
turnProgress: (turn, totalTurns) => `- Current turn: ${turn}/${totalTurns}`,
|
|
76
|
+
responseLanguageInstruction: "Required response language: English. Answer only in English, even if the subject or transcript contains another language.",
|
|
74
77
|
objectiveTitle: "Objective:",
|
|
75
78
|
debateObjectives: [
|
|
76
79
|
"- Provide a useful, concrete, and concise answer.",
|
|
@@ -97,6 +100,7 @@ export const promptMessages = {
|
|
|
97
100
|
actionsHeading: "### Proposed actions",
|
|
98
101
|
conclusionHeading: "### Conclusion",
|
|
99
102
|
finalProseInstruction: "A short prose summary paragraph, without a list, that captures the general meaning of the debate and the most reasonable decision or direction.",
|
|
100
|
-
summaryAnswerTitle: "Summary:"
|
|
103
|
+
summaryAnswerTitle: "Summary:",
|
|
104
|
+
ollamaSystemPrompt: "You are taking part in an orchestrated technical debate. Stay precise, useful, and honest about your limits."
|
|
101
105
|
}
|
|
102
106
|
};
|
package/dist/new.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { isAgentDetected } from "./agentRegistry.js";
|
|
3
4
|
import { discoverLocalTools } from "./discovery.js";
|
|
4
5
|
import { findPresetNameForPair } from "./presets.js";
|
|
5
6
|
import { MAX_TURNS, turnsOrDefault, validateTurns } from "./limits.js";
|
|
@@ -136,25 +137,6 @@ function buildAgentChoices(config, discovery, messages) {
|
|
|
136
137
|
})
|
|
137
138
|
.sort((left, right) => Number(right.detected) - Number(left.detected) || left.name.localeCompare(right.name));
|
|
138
139
|
}
|
|
139
|
-
function isAgentDetected(name, config, discovery) {
|
|
140
|
-
if (config.type === "ollama") {
|
|
141
|
-
return discovery.ollama.available;
|
|
142
|
-
}
|
|
143
|
-
const normalized = normalizeCommandName(config.command || name);
|
|
144
|
-
if (normalized === "codex")
|
|
145
|
-
return discovery.codex.available;
|
|
146
|
-
if (normalized === "claude")
|
|
147
|
-
return discovery.claude.available;
|
|
148
|
-
if (normalized === "gemini")
|
|
149
|
-
return discovery.gemini.available;
|
|
150
|
-
if (normalized === "agy")
|
|
151
|
-
return discovery.antigravity.available;
|
|
152
|
-
if (normalized === "antigravity")
|
|
153
|
-
return discovery.antigravity.available;
|
|
154
|
-
if (normalized === "opencode")
|
|
155
|
-
return discovery.opencode.available;
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
140
|
function agentStatus(_name, config, discovery, detected, messages) {
|
|
159
141
|
if (config.type === "ollama") {
|
|
160
142
|
return detected
|
|
@@ -248,13 +230,6 @@ function splitPaths(value) {
|
|
|
248
230
|
.map((entry) => entry.trim())
|
|
249
231
|
.filter(Boolean) ?? [];
|
|
250
232
|
}
|
|
251
|
-
function normalizeCommandName(command) {
|
|
252
|
-
return command
|
|
253
|
-
.split(/[\\/]/)
|
|
254
|
-
.pop()
|
|
255
|
-
?.toLowerCase()
|
|
256
|
-
.replace(/\.(exe|cmd|bat|ps1)$/i, "") ?? command.toLowerCase();
|
|
257
|
-
}
|
|
258
233
|
function isQuit(value) {
|
|
259
234
|
return ["q", "quit", "exit"].includes(value.toLowerCase());
|
|
260
235
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -78,7 +78,7 @@ export async function runDebate(config, options, renderer, messages = createTran
|
|
|
78
78
|
};
|
|
79
79
|
transcript.push(message);
|
|
80
80
|
renderer?.message(message.content);
|
|
81
|
-
if (shouldStopOnAgreement(options, transcript)) {
|
|
81
|
+
if (shouldStopOnAgreement(options, transcript, messages)) {
|
|
82
82
|
stopReason = messages.orchestrator.agreementStopReason;
|
|
83
83
|
renderer?.notice(messages.orchestrator.earlyStop(stopReason));
|
|
84
84
|
break;
|
|
@@ -110,28 +110,21 @@ export async function runDebate(config, options, renderer, messages = createTran
|
|
|
110
110
|
/**
|
|
111
111
|
* Heuristique d'arrêt sur accord explicite.
|
|
112
112
|
* Ne s'active qu'après un tour complet (nombre pair de messages) pour éviter les faux positifs.
|
|
113
|
+
* Les phrases d'accord proviennent du dictionnaire i18n pour suivre la langue d'interface.
|
|
113
114
|
* Intentionnellement prudente : ne remplace pas une évaluation sémantique réelle.
|
|
114
115
|
*/
|
|
115
|
-
function shouldStopOnAgreement(options, messages) {
|
|
116
|
-
if (!options.earlyStopOnAgreement ||
|
|
116
|
+
function shouldStopOnAgreement(options, transcript, messages) {
|
|
117
|
+
if (!options.earlyStopOnAgreement || transcript.length < 2 || transcript.length % 2 !== 0) {
|
|
117
118
|
return false;
|
|
118
119
|
}
|
|
119
|
-
const latest = normalizeForAgreement(
|
|
120
|
+
const latest = normalizeForAgreement(transcript[transcript.length - 1]?.content ?? "");
|
|
120
121
|
if (!latest) {
|
|
121
122
|
return false;
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
"accord complet",
|
|
125
|
-
"accord total",
|
|
126
|
-
"aucun desaccord",
|
|
127
|
-
"aucune incertitude",
|
|
128
|
-
"rien a trancher",
|
|
129
|
-
"rien a ajouter",
|
|
130
|
-
"question factuelle resolue"
|
|
131
|
-
];
|
|
132
|
-
if (positivePatterns.some((pattern) => latest.includes(pattern))) {
|
|
124
|
+
if (messages.orchestrator.agreementPatterns.some((pattern) => latest.includes(pattern))) {
|
|
133
125
|
return true;
|
|
134
126
|
}
|
|
127
|
+
// Combinaison française historique : confirmation explicite + absence de point ouvert.
|
|
135
128
|
return (latest.includes("confirme") || latest.includes("acte")) &&
|
|
136
129
|
(latest.includes("aucun") || latest.includes("rien a trancher") || latest.includes("rien a ajouter"));
|
|
137
130
|
}
|
package/dist/presets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { detectionForCommand } from "./agentRegistry.js";
|
|
2
2
|
const presets = {
|
|
3
3
|
"codex-claude": {
|
|
4
4
|
agentA: "codex",
|
|
@@ -181,7 +181,7 @@ function checkAgentAvailability(agentName, config, discovery, messages) {
|
|
|
181
181
|
}
|
|
182
182
|
return available(agentName);
|
|
183
183
|
}
|
|
184
|
-
const detection =
|
|
184
|
+
const detection = detectionForCommand(agent.command, discovery);
|
|
185
185
|
if (!detection) {
|
|
186
186
|
// Les CLIs custom déclarées par l'utilisateur restent considérées utilisables :
|
|
187
187
|
// Palabre ne peut pas connaître leur sémantique sans les lancer.
|
|
@@ -191,25 +191,6 @@ function checkAgentAvailability(agentName, config, discovery, messages) {
|
|
|
191
191
|
? available(agentName)
|
|
192
192
|
: unavailable(agentName, messages?.presets.missingCommand(agentName, detection.command) ?? `commande non détectée pour ${agentName}: ${detection.command}`);
|
|
193
193
|
}
|
|
194
|
-
function knownCliDetection(agent, discovery) {
|
|
195
|
-
const command = normalizeCommandName(agent.command);
|
|
196
|
-
if (command === "codex")
|
|
197
|
-
return discovery.codex;
|
|
198
|
-
if (command === "claude")
|
|
199
|
-
return discovery.claude;
|
|
200
|
-
if (command === "gemini")
|
|
201
|
-
return discovery.gemini;
|
|
202
|
-
if (command === "agy")
|
|
203
|
-
return discovery.antigravity;
|
|
204
|
-
if (command === "antigravity")
|
|
205
|
-
return discovery.antigravity;
|
|
206
|
-
if (command === "opencode")
|
|
207
|
-
return discovery.opencode;
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
function normalizeCommandName(command) {
|
|
211
|
-
return path.basename(command).toLowerCase().replace(/\.(exe|cmd|ps1|bat)$/i, "");
|
|
212
|
-
}
|
|
213
194
|
function available(agent) {
|
|
214
195
|
return { agent, available: true, reason: "" };
|
|
215
196
|
}
|
package/dist/prompt.js
CHANGED
|
@@ -25,6 +25,8 @@ export function formatAgentPrompt(input) {
|
|
|
25
25
|
messages.sessionStartedAt(input.session.startedAt),
|
|
26
26
|
messages.turnProgress(input.turn, input.totalTurns),
|
|
27
27
|
"",
|
|
28
|
+
messages.responseLanguageInstruction,
|
|
29
|
+
"",
|
|
28
30
|
messages.objectiveTitle,
|
|
29
31
|
...messages.debateObjectives,
|
|
30
32
|
"",
|
|
@@ -56,6 +58,8 @@ function formatSummaryPrompt(input, messages) {
|
|
|
56
58
|
messages.cwd(input.session.cwd),
|
|
57
59
|
messages.sessionStartedAt(input.session.startedAt),
|
|
58
60
|
"",
|
|
61
|
+
messages.responseLanguageInstruction,
|
|
62
|
+
"",
|
|
59
63
|
messages.objectiveTitle,
|
|
60
64
|
...messages.summaryObjectives,
|
|
61
65
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palabre",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"prepack": "pnpm build",
|
|
39
39
|
"start": "node ./dist/index.js",
|
|
40
40
|
"test": "pnpm build:test && node --test .tmp/test-dist/tests/*.test.js",
|
|
41
|
-
"build:test": "node -e \"fs.rmSync('.tmp/test-dist',{recursive:true,force:true})\" && tsc -p tsconfig.test.json"
|
|
41
|
+
"build:test": "node -e \"fs.rmSync('.tmp/test-dist',{recursive:true,force:true})\" && tsc -p tsconfig.test.json",
|
|
42
|
+
"smoke:real-presets": "node -e \"fs.rmSync('.tmp/smoke-real',{recursive:true,force:true})\" && tsc --target ES2022 --module NodeNext --moduleResolution NodeNext --types node --outDir .tmp/smoke-real scripts/smoke_real_presets.ts && node .tmp/smoke-real/smoke_real_presets.js"
|
|
42
43
|
},
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=20"
|