palabre 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/adapters/cli-pty.js +183 -0
- package/dist/adapters/cli.js +6 -6
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/terminal.js +13 -0
- package/dist/config.js +55 -8
- package/dist/configWizard.js +45 -40
- package/dist/context.js +16 -14
- package/dist/discovery.js +3 -1
- package/dist/doctor.js +147 -137
- package/dist/errors.js +4 -31
- package/dist/i18n.js +30 -0
- package/dist/index.js +275 -258
- package/dist/limits.js +11 -10
- package/dist/messages/adapter-errors.js +36 -0
- package/dist/messages/agents.js +38 -0
- package/dist/messages/common.js +28 -0
- package/dist/messages/config.js +88 -0
- package/dist/messages/context.js +24 -0
- package/dist/messages/doctor.js +126 -0
- package/dist/messages/help.js +280 -0
- package/dist/messages/index.js +38 -0
- package/dist/messages/init.js +30 -0
- package/dist/messages/limits.js +12 -0
- package/dist/messages/new.js +66 -0
- package/dist/messages/orchestrator.js +14 -0
- package/dist/messages/output.js +64 -0
- package/dist/messages/presets.js +26 -0
- package/dist/messages/preview.js +22 -0
- package/dist/messages/prompt.js +102 -0
- package/dist/messages/renderers.js +38 -0
- package/dist/messages/update.js +40 -0
- package/dist/new.js +46 -42
- package/dist/orchestrator.js +23 -18
- package/dist/output.js +34 -33
- package/dist/presets.js +122 -2
- package/dist/prompt.js +43 -58
- package/dist/renderers/console.js +33 -27
- package/dist/update.js +10 -21
- package/package.json +4 -1
- package/palabre.config.example.json +1 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## Français
|
|
15
15
|
|
|
16
|
-
PALABRE est un orchestrateur CLI qui fait dialoguer plusieurs agents IA installés sur votre machine : Claude Code, Codex CLI, Gemini CLI, OpenCode et Ollama.
|
|
16
|
+
PALABRE est un orchestrateur CLI qui fait dialoguer plusieurs agents IA installés sur votre machine : Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode et Ollama.
|
|
17
17
|
|
|
18
18
|
Il ne remplace pas vos outils : il les pilote. Vous gardez vos abonnements, vos modèles par défaut, vos habitudes de terminal et vos fichiers en local. PALABRE exporte ensuite le débat en Markdown.
|
|
19
19
|
|
|
@@ -57,6 +57,7 @@ palabre codex-claude "Preview" --context src --show-prompt
|
|
|
57
57
|
- Claude Code via `claude --print`
|
|
58
58
|
- Codex CLI via `codex exec`
|
|
59
59
|
- Gemini CLI via `gemini --prompt -`
|
|
60
|
+
- Antigravity CLI via `agy --print` en pseudo-terminal
|
|
60
61
|
- OpenCode via `opencode run`
|
|
61
62
|
- Ollama via l'API locale HTTP
|
|
62
63
|
|
|
@@ -64,7 +65,7 @@ PALABRE ne liste pas les modèles : ils changent souvent et dépendent de chaque
|
|
|
64
65
|
|
|
65
66
|
### Confidentialité
|
|
66
67
|
|
|
67
|
-
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, OpenCode, Ollama ou de tout autre agent configuré.
|
|
68
|
+
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é.
|
|
68
69
|
|
|
69
70
|
### Développement local
|
|
70
71
|
|
|
@@ -87,7 +88,7 @@ MIT. Voir [LICENSE](./LICENSE).
|
|
|
87
88
|
|
|
88
89
|
## English
|
|
89
90
|
|
|
90
|
-
PALABRE is a CLI orchestrator that lets multiple AI agents installed on your machine talk to each other: Claude Code, Codex CLI, Gemini CLI, OpenCode, and Ollama.
|
|
91
|
+
PALABRE is a CLI orchestrator that lets multiple AI agents installed on your machine talk to each other: Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, and Ollama.
|
|
91
92
|
|
|
92
93
|
It does not replace your tools: it drives them. You keep your subscriptions, default models, terminal habits, and local files. PALABRE then exports the debate as Markdown.
|
|
93
94
|
|
|
@@ -131,6 +132,7 @@ palabre codex-claude "Preview" --context src --show-prompt
|
|
|
131
132
|
- Claude Code via `claude --print`
|
|
132
133
|
- Codex CLI via `codex exec`
|
|
133
134
|
- Gemini CLI via `gemini --prompt -`
|
|
135
|
+
- Antigravity CLI via `agy --print` in a pseudo-terminal
|
|
134
136
|
- OpenCode via `opencode run`
|
|
135
137
|
- Ollama via the local HTTP API
|
|
136
138
|
|
|
@@ -138,7 +140,7 @@ PALABRE does not list models: they change often and depend on each CLI or user a
|
|
|
138
140
|
|
|
139
141
|
### Privacy
|
|
140
142
|
|
|
141
|
-
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, OpenCode, Ollama, or any custom agent you configure.
|
|
143
|
+
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.
|
|
142
144
|
|
|
143
145
|
### Local Development
|
|
144
146
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawn as spawnPty } from "node-pty";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AdapterError } from "../errors.js";
|
|
5
|
+
import { formatAgentPrompt } from "../prompt.js";
|
|
6
|
+
import { cleanTerminalOutput } from "./terminal.js";
|
|
7
|
+
/**
|
|
8
|
+
* Adapter pour les CLIs qui exigent un vrai terminal.
|
|
9
|
+
* Contrairement à `CliAdapter`, stdout/stderr sont fusionnés dans le flux PTY.
|
|
10
|
+
*/
|
|
11
|
+
export class CliPtyAdapter {
|
|
12
|
+
name;
|
|
13
|
+
config;
|
|
14
|
+
role;
|
|
15
|
+
contract;
|
|
16
|
+
constructor(name, config) {
|
|
17
|
+
this.name = name;
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.role = config.role;
|
|
20
|
+
this.contract = {
|
|
21
|
+
name,
|
|
22
|
+
kind: "cli-pty",
|
|
23
|
+
capabilities: {
|
|
24
|
+
mode: "pty",
|
|
25
|
+
supportsModelOverride: true,
|
|
26
|
+
supportsFilesystemAccess: true,
|
|
27
|
+
supportsStreaming: false,
|
|
28
|
+
supportsProcessExitCode: true,
|
|
29
|
+
supportsStderr: false
|
|
30
|
+
},
|
|
31
|
+
guarantees: {
|
|
32
|
+
rejectsEmptyOutput: !config.allowEmptyOutput,
|
|
33
|
+
rejectsNonZeroExit: true,
|
|
34
|
+
rejectsTimeout: true,
|
|
35
|
+
returnsRawOutput: true
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async generate(prompt) {
|
|
40
|
+
const renderedPrompt = formatAgentPrompt(prompt);
|
|
41
|
+
const promptMode = this.config.promptMode ?? "stdin";
|
|
42
|
+
const baseArgs = withModelArgs(this.config.args ?? [], this.config.model, this.config.modelArg ?? "--model");
|
|
43
|
+
const args = promptMode === "argument"
|
|
44
|
+
? [...baseArgs, renderedPrompt]
|
|
45
|
+
: baseArgs;
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
let output = "";
|
|
48
|
+
let settled = false;
|
|
49
|
+
let hardTimer;
|
|
50
|
+
let term;
|
|
51
|
+
let dataSubscription;
|
|
52
|
+
let exitSubscription;
|
|
53
|
+
const finish = (error, exitCode, kill = true) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(hardTimer);
|
|
58
|
+
dataSubscription?.dispose();
|
|
59
|
+
exitSubscription?.dispose();
|
|
60
|
+
if (kill) {
|
|
61
|
+
try {
|
|
62
|
+
term.kill();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// The PTY may already be closed.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
cleanupPty(term);
|
|
69
|
+
if (error) {
|
|
70
|
+
reject(error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const content = cleanTerminalOutput(output);
|
|
74
|
+
if (exitCode && exitCode !== 0 && !content) {
|
|
75
|
+
reject(createPtyExitError(this.name, exitCode, output));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!content && !this.config.allowEmptyOutput) {
|
|
79
|
+
reject(new AdapterError("empty-output", this.name, `${this.name} produced empty PTY output.`, {
|
|
80
|
+
raw: output
|
|
81
|
+
}));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
resolve({
|
|
85
|
+
content,
|
|
86
|
+
raw: output
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
term = spawnPty(resolveExecutable(this.config.command), args, {
|
|
91
|
+
name: "xterm-256color",
|
|
92
|
+
cols: this.config.cols ?? 120,
|
|
93
|
+
rows: this.config.rows ?? 40,
|
|
94
|
+
cwd: process.cwd(),
|
|
95
|
+
env: process.env,
|
|
96
|
+
...(process.platform !== "win32" ? { encoding: "utf8" } : {}),
|
|
97
|
+
...(process.platform === "win32" ? { useConpty: true } : {})
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
reject(new AdapterError("spawn-failed", this.name, `${this.name} failed to start PTY command "${this.config.command}": ${error instanceof Error ? error.message : String(error)}`, {
|
|
102
|
+
command: this.config.command
|
|
103
|
+
}));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
hardTimer = setTimeout(() => {
|
|
107
|
+
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ?? 180_000}ms`, {
|
|
108
|
+
timeoutMs: this.config.timeoutMs ?? 180_000
|
|
109
|
+
}));
|
|
110
|
+
}, this.config.timeoutMs ?? 180_000);
|
|
111
|
+
dataSubscription = term.onData((chunk) => {
|
|
112
|
+
output += chunk;
|
|
113
|
+
});
|
|
114
|
+
exitSubscription = term.onExit(({ exitCode }) => {
|
|
115
|
+
finish(undefined, exitCode, false);
|
|
116
|
+
});
|
|
117
|
+
if (promptMode === "stdin") {
|
|
118
|
+
term.write(`${renderedPrompt}\r`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function resolveExecutable(command) {
|
|
124
|
+
if (path.isAbsolute(command) || command.includes("\\") || command.includes("/")) {
|
|
125
|
+
return command;
|
|
126
|
+
}
|
|
127
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
128
|
+
const trimmed = directory.trim();
|
|
129
|
+
if (!trimmed)
|
|
130
|
+
continue;
|
|
131
|
+
for (const extension of executableExtensions(command)) {
|
|
132
|
+
const candidate = path.join(trimmed, `${command}${extension}`);
|
|
133
|
+
if (existsSync(candidate)) {
|
|
134
|
+
return candidate;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return command;
|
|
139
|
+
}
|
|
140
|
+
function cleanupPty(term) {
|
|
141
|
+
const maybeTerm = term;
|
|
142
|
+
try {
|
|
143
|
+
maybeTerm._agent?._cleanUpProcess?.();
|
|
144
|
+
maybeTerm._agent?._conoutSocketWorker?._worker?.terminate?.();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Best-effort cleanup for Windows ConPTY internals.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
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
|
+
function createPtyExitError(adapterName, exitCode, raw) {
|
|
175
|
+
return new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizePtyOutput(raw)}`, {
|
|
176
|
+
exitCode,
|
|
177
|
+
raw
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function summarizePtyOutput(output) {
|
|
181
|
+
const cleaned = cleanTerminalOutput(output);
|
|
182
|
+
return cleaned ? cleaned.slice(-1_200) : "aucune sortie PTY capturee.";
|
|
183
|
+
}
|
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 { cleanTerminalOutput } from "./terminal.js";
|
|
4
5
|
/**
|
|
5
6
|
* Adapter pour les CLIs batch (Codex, Claude, Gemini…).
|
|
6
7
|
* Lance un sous-processus, injecte le prompt via stdin ou argument, capture stdout.
|
|
@@ -107,17 +108,18 @@ export class CliAdapter {
|
|
|
107
108
|
command: this.config.command
|
|
108
109
|
}));
|
|
109
110
|
});
|
|
110
|
-
|
|
111
|
+
const finishFromExitCode = (code) => {
|
|
111
112
|
if (code && code !== 0 && !stdout.trim()) {
|
|
112
113
|
finish(createCliExitError(this.name, code, stderr));
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
finish();
|
|
116
|
-
}
|
|
117
|
+
};
|
|
118
|
+
child.on("close", finishFromExitCode);
|
|
117
119
|
if (promptMode === "stdin") {
|
|
118
120
|
child.stdin.write(renderedPrompt);
|
|
119
|
-
child.stdin.end();
|
|
120
121
|
}
|
|
122
|
+
child.stdin.end();
|
|
121
123
|
});
|
|
122
124
|
}
|
|
123
125
|
}
|
|
@@ -142,9 +144,7 @@ function withModelArgs(args, model, modelArg) {
|
|
|
142
144
|
}
|
|
143
145
|
/** Retire les séquences ANSI et les espaces en tête/fin. */
|
|
144
146
|
function cleanCliOutput(output) {
|
|
145
|
-
return output
|
|
146
|
-
.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
147
|
-
.trim();
|
|
147
|
+
return cleanTerminalOutput(output);
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
150
|
* Construit une `AdapterError` typée depuis un exit code non nul.
|
package/dist/adapters/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { CliAdapter } from "./cli.js";
|
|
2
|
+
import { CliPtyAdapter } from "./cli-pty.js";
|
|
2
3
|
import { OllamaAdapter } from "./ollama.js";
|
|
3
4
|
/** Factory qui instancie l'adapter approprié selon `config.type`. Exhaustive : tout `AgentConfig` valide produit un adapter. */
|
|
4
5
|
export function createAgent(name, config) {
|
|
5
6
|
switch (config.type) {
|
|
6
7
|
case "cli":
|
|
7
8
|
return new CliAdapter(name, config);
|
|
9
|
+
case "cli-pty":
|
|
10
|
+
return new CliPtyAdapter(name, config);
|
|
8
11
|
case "ollama":
|
|
9
12
|
return new OllamaAdapter(name, config);
|
|
10
13
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Retire les séquences de contrôle ANSI/OSC et normalise les retours ligne d'une sortie terminal. */
|
|
2
|
+
export function cleanTerminalOutput(output) {
|
|
3
|
+
return output
|
|
4
|
+
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
|
|
5
|
+
.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
6
|
+
.replace(/\u001b[()][A-Za-z0-9]/g, "")
|
|
7
|
+
.replace(/\u001b[=>]/g, "")
|
|
8
|
+
.replace(/\r\n/g, "\n")
|
|
9
|
+
.replace(/\r/g, "\n")
|
|
10
|
+
.replace(/\u0007/g, "")
|
|
11
|
+
.replace(/\u0000/g, "")
|
|
12
|
+
.trim();
|
|
13
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -6,8 +6,11 @@ export const LEGACY_CONFIG_PATH = "chicane.config.json";
|
|
|
6
6
|
export const CONFIG_DIR_NAME = ".palabre";
|
|
7
7
|
export const GLOBAL_CONFIG_PATH = path.join(os.homedir(), CONFIG_DIR_NAME, DEFAULT_CONFIG_PATH);
|
|
8
8
|
export const GLOBAL_LEGACY_CONFIG_PATH = path.join(os.homedir(), CONFIG_DIR_NAME, LEGACY_CONFIG_PATH);
|
|
9
|
+
export const DEFAULT_OLLAMA_MODEL = "nemotron-3-nano:4b";
|
|
10
|
+
export const DEFAULT_OUTPUT_DIR = ".palabre";
|
|
9
11
|
export const exampleConfig = {
|
|
10
|
-
|
|
12
|
+
language: "fr",
|
|
13
|
+
outputDir: DEFAULT_OUTPUT_DIR,
|
|
11
14
|
defaults: {
|
|
12
15
|
agentA: "codex",
|
|
13
16
|
agentB: "claude",
|
|
@@ -63,6 +66,19 @@ export const exampleConfig = {
|
|
|
63
66
|
role: "reviewer",
|
|
64
67
|
tier: "primary"
|
|
65
68
|
},
|
|
69
|
+
antigravity: {
|
|
70
|
+
type: "cli-pty",
|
|
71
|
+
command: "agy",
|
|
72
|
+
args: [
|
|
73
|
+
"--print-timeout",
|
|
74
|
+
"5m0s",
|
|
75
|
+
"--print"
|
|
76
|
+
],
|
|
77
|
+
promptMode: "argument",
|
|
78
|
+
role: "reviewer",
|
|
79
|
+
tier: "primary",
|
|
80
|
+
timeoutMs: 300_000
|
|
81
|
+
},
|
|
66
82
|
opencode: {
|
|
67
83
|
type: "cli",
|
|
68
84
|
command: "opencode",
|
|
@@ -78,7 +94,7 @@ export const exampleConfig = {
|
|
|
78
94
|
"ollama-local": {
|
|
79
95
|
type: "ollama",
|
|
80
96
|
baseUrl: "http://localhost:11434",
|
|
81
|
-
model:
|
|
97
|
+
model: DEFAULT_OLLAMA_MODEL,
|
|
82
98
|
role: "critic",
|
|
83
99
|
tier: "local",
|
|
84
100
|
temperature: 0.2,
|
|
@@ -87,6 +103,15 @@ export const exampleConfig = {
|
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* Résout le dossier d'export effectif.
|
|
108
|
+
* `.` est traité comme l'ancien défaut historique afin de regrouper les exports
|
|
109
|
+
* dans un dossier dédié sans demander de migration manuelle aux utilisateurs.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveOutputDir(outputDir) {
|
|
112
|
+
const normalized = outputDir?.trim();
|
|
113
|
+
return !normalized || normalized === "." ? DEFAULT_OUTPUT_DIR : normalized;
|
|
114
|
+
}
|
|
90
115
|
/** Charge et parse la config depuis `configPath`. Lance une erreur si le fichier est absent ou invalide. */
|
|
91
116
|
export async function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
|
|
92
117
|
const resolved = path.resolve(configPath);
|
|
@@ -126,7 +151,7 @@ export async function resolveDefaultConfigPath() {
|
|
|
126
151
|
/**
|
|
127
152
|
* Construit une `PalabreConfig` complète à partir des outils détectés localement.
|
|
128
153
|
* Ajuste `defaults.agentA/agentB/summaryAgent` en fonction de la paire disponible.
|
|
129
|
-
* Si aucune paire n'est détectée,
|
|
154
|
+
* Si aucune paire n'est détectée, seuls les defaults sans agent sont conservés.
|
|
130
155
|
*/
|
|
131
156
|
export function createConfigFromDiscovery(discovery) {
|
|
132
157
|
const config = cloneConfig(exampleConfig);
|
|
@@ -143,14 +168,26 @@ export function createConfigFromDiscovery(discovery) {
|
|
|
143
168
|
...config.agents.gemini,
|
|
144
169
|
...(discovery.gemini.available ? { command: discovery.gemini.command } : {})
|
|
145
170
|
};
|
|
171
|
+
config.agents.antigravity = {
|
|
172
|
+
...config.agents.antigravity,
|
|
173
|
+
...(discovery.antigravity.available ? { command: discovery.antigravity.command } : {})
|
|
174
|
+
};
|
|
146
175
|
config.agents.opencode = {
|
|
147
176
|
...config.agents.opencode,
|
|
148
177
|
...(discovery.opencode.available ? { command: discovery.opencode.command } : {})
|
|
149
178
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
179
|
+
const ollamaAgent = config.agents["ollama-local"];
|
|
180
|
+
if (ollamaAgent?.type === "ollama") {
|
|
181
|
+
ollamaAgent.model = chooseDefaultOllamaModel(discovery);
|
|
182
|
+
}
|
|
183
|
+
config.defaults = pair
|
|
184
|
+
? {
|
|
185
|
+
...config.defaults,
|
|
186
|
+
agentA: pair[0],
|
|
187
|
+
agentB: pair[1],
|
|
188
|
+
summaryAgent: chooseDefaultSummaryAgent(pair)
|
|
189
|
+
}
|
|
190
|
+
: { turns: config.defaults?.turns };
|
|
154
191
|
return config;
|
|
155
192
|
}
|
|
156
193
|
/** Écrit `config` sérialisé en JSON dans `configPath`. Crée le répertoire parent si nécessaire. */
|
|
@@ -159,8 +196,14 @@ export async function writeExampleConfig(configPath = DEFAULT_CONFIG_PATH, confi
|
|
|
159
196
|
await mkdir(path.dirname(resolved), { recursive: true });
|
|
160
197
|
await writeFile(resolved, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
161
198
|
}
|
|
199
|
+
function chooseDefaultOllamaModel(discovery) {
|
|
200
|
+
if (discovery.ollama.models.includes(DEFAULT_OLLAMA_MODEL)) {
|
|
201
|
+
return DEFAULT_OLLAMA_MODEL;
|
|
202
|
+
}
|
|
203
|
+
return discovery.ollama.models[0] ?? DEFAULT_OLLAMA_MODEL;
|
|
204
|
+
}
|
|
162
205
|
function chooseDefaultSummaryAgent(pair) {
|
|
163
|
-
for (const preferred of ["claude", "codex", "gemini"]) {
|
|
206
|
+
for (const preferred of ["claude", "codex", "antigravity", "gemini"]) {
|
|
164
207
|
if (pair.includes(preferred)) {
|
|
165
208
|
return preferred;
|
|
166
209
|
}
|
|
@@ -180,12 +223,16 @@ function chooseDefaultPair(discovery) {
|
|
|
180
223
|
if (discovery.opencode.available && discovery.ollama.available) {
|
|
181
224
|
return ["opencode", "ollama-local"];
|
|
182
225
|
}
|
|
226
|
+
if (discovery.antigravity.available && discovery.ollama.available) {
|
|
227
|
+
return ["antigravity", "ollama-local"];
|
|
228
|
+
}
|
|
183
229
|
if (discovery.gemini.available && discovery.ollama.available) {
|
|
184
230
|
return ["gemini", "ollama-local"];
|
|
185
231
|
}
|
|
186
232
|
const cliAgents = [
|
|
187
233
|
discovery.codex.available ? "codex" : undefined,
|
|
188
234
|
discovery.claude.available ? "claude" : undefined,
|
|
235
|
+
discovery.antigravity.available ? "antigravity" : undefined,
|
|
189
236
|
discovery.opencode.available ? "opencode" : undefined,
|
|
190
237
|
discovery.gemini.available ? "gemini" : undefined
|
|
191
238
|
].filter((agent) => Boolean(agent));
|
package/dist/configWizard.js
CHANGED
|
@@ -7,47 +7,47 @@ import { DEFAULT_TURNS, MAX_TURNS, turnsOrDefault, validateTurns } from "./limit
|
|
|
7
7
|
* Fonctionne en mode TTY (readline) et en mode piped (stdin lu en avance).
|
|
8
8
|
* Écrit la config sur disque si l'utilisateur confirme ; sort sans modifier si l'utilisateur quitte.
|
|
9
9
|
*/
|
|
10
|
-
export async function runConfigWizard(configPath, config) {
|
|
10
|
+
export async function runConfigWizard(configPath, config, messages) {
|
|
11
11
|
const choices = Object.entries(config.agents).map(([name, agentConfig]) => ({ name, config: agentConfig }));
|
|
12
12
|
if (choices.length < 2) {
|
|
13
|
-
throw new Error(
|
|
13
|
+
throw new Error(messages.config.wizardNeedsTwoAgents);
|
|
14
14
|
}
|
|
15
15
|
const rl = await createQuestioner();
|
|
16
16
|
try {
|
|
17
|
-
console.log(
|
|
18
|
-
console.log(
|
|
17
|
+
console.log(messages.config.wizardTitle);
|
|
18
|
+
console.log(messages.config.wizardQuitHint);
|
|
19
19
|
console.log("");
|
|
20
|
-
console.log(
|
|
20
|
+
console.log(messages.config.wizardConfigFile);
|
|
21
21
|
console.log(` ${configPath}`);
|
|
22
22
|
console.log("");
|
|
23
|
-
console.log(
|
|
24
|
-
console.log(` ${config.defaults ? formatDefaults(config.defaults) :
|
|
23
|
+
console.log(messages.config.wizardCurrentDefaults);
|
|
24
|
+
console.log(` ${config.defaults ? formatDefaults(config.defaults, messages) : messages.config.wizardNoDefaults}`);
|
|
25
25
|
console.log("");
|
|
26
|
-
console.log(
|
|
27
|
-
console.log(
|
|
28
|
-
console.log(
|
|
29
|
-
console.log(
|
|
30
|
-
const action = await askChoice(rl,
|
|
26
|
+
console.log(messages.config.wizardActionQuestion);
|
|
27
|
+
console.log(` 1) ${messages.config.wizardActionSetDefaults}`);
|
|
28
|
+
console.log(` 2) ${messages.config.wizardActionClearDefaults}`);
|
|
29
|
+
console.log(` 3) ${messages.config.wizardActionExit}`);
|
|
30
|
+
const action = await askChoice(rl, messages.config.wizardChoicePrompt, "1", ["1", "2", "3"], messages);
|
|
31
31
|
if (!action || action === "3") {
|
|
32
|
-
console.log(
|
|
32
|
+
console.log(messages.config.wizardUnchanged);
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
if (action === "2") {
|
|
36
36
|
delete config.defaults;
|
|
37
37
|
await writeExampleConfig(configPath, config);
|
|
38
|
-
console.log(
|
|
38
|
+
console.log(messages.config.wizardCleared(configPath));
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
const agentA = await askAgent(rl, choices,
|
|
41
|
+
const agentA = await askAgent(rl, choices, messages.config.wizardAgentADescription, config.defaults?.agentA, messages);
|
|
42
42
|
if (!agentA)
|
|
43
43
|
return;
|
|
44
|
-
const agentB = await askAgent(rl, choices.filter((choice) => choice.name !== agentA),
|
|
44
|
+
const agentB = await askAgent(rl, choices.filter((choice) => choice.name !== agentA), messages.config.wizardAgentBDescription, config.defaults?.agentB === agentA ? undefined : config.defaults?.agentB, messages);
|
|
45
45
|
if (!agentB)
|
|
46
46
|
return;
|
|
47
|
-
const turns = await askNumber(rl,
|
|
47
|
+
const turns = await askNumber(rl, messages.config.wizardTurnsLabel, turnsOrDefault(config.defaults?.turns), Boolean(config.defaults?.turns), messages);
|
|
48
48
|
if (turns === undefined)
|
|
49
49
|
return;
|
|
50
|
-
const summaryAgent = await askSummaryAgent(rl, choices, config.defaults?.summaryAgent ?? agentB, Boolean(config.defaults?.summaryAgent), agentB);
|
|
50
|
+
const summaryAgent = await askSummaryAgent(rl, choices, config.defaults?.summaryAgent ?? agentB, Boolean(config.defaults?.summaryAgent), agentB, messages);
|
|
51
51
|
if (summaryAgent === undefined)
|
|
52
52
|
return;
|
|
53
53
|
config.defaults = {
|
|
@@ -57,7 +57,7 @@ export async function runConfigWizard(configPath, config) {
|
|
|
57
57
|
turns
|
|
58
58
|
};
|
|
59
59
|
await writeExampleConfig(configPath, config);
|
|
60
|
-
console.log(
|
|
60
|
+
console.log(messages.config.wizardDefaultsSet(configPath, formatDefaults(config.defaults, messages)));
|
|
61
61
|
}
|
|
62
62
|
finally {
|
|
63
63
|
rl.close();
|
|
@@ -89,9 +89,9 @@ async function readPipedLines() {
|
|
|
89
89
|
}
|
|
90
90
|
return raw ? raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n") : [];
|
|
91
91
|
}
|
|
92
|
-
async function askAgent(rl, choices,
|
|
92
|
+
async function askAgent(rl, choices, description, defaultName, messages) {
|
|
93
93
|
const fallback = choices.find((choice) => choice.name === defaultName)?.name ?? choices[0]?.name;
|
|
94
|
-
const fallbackLabel = defaultName ?
|
|
94
|
+
const fallbackLabel = defaultName ? messages.config.wizardCurrent : messages.config.wizardSuggestion;
|
|
95
95
|
console.log("");
|
|
96
96
|
console.log(description);
|
|
97
97
|
console.log(`${fallbackLabel} : ${fallback}`);
|
|
@@ -100,7 +100,7 @@ async function askAgent(rl, choices, _label, description, defaultName) {
|
|
|
100
100
|
console.log(` ${index + 1}) ${formatAgentLine(choice)}`);
|
|
101
101
|
});
|
|
102
102
|
while (true) {
|
|
103
|
-
const answer = await rl.question(
|
|
103
|
+
const answer = await rl.question(messages.config.wizardAgentPrompt(fallback));
|
|
104
104
|
const value = answer.trim();
|
|
105
105
|
if (isQuit(value))
|
|
106
106
|
return undefined;
|
|
@@ -113,22 +113,22 @@ async function askAgent(rl, choices, _label, description, defaultName) {
|
|
|
113
113
|
if (choices.some((choice) => choice.name === value)) {
|
|
114
114
|
return value;
|
|
115
115
|
}
|
|
116
|
-
console.log(
|
|
116
|
+
console.log(messages.config.wizardInvalidAgentChoice);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
async function askSummaryAgent(rl, choices, defaultName, hasCurrentDefault, agentB) {
|
|
119
|
+
async function askSummaryAgent(rl, choices, defaultName, hasCurrentDefault, agentB, messages) {
|
|
120
120
|
const fallback = choices.some((choice) => choice.name === defaultName) ? defaultName : choices[0]?.name;
|
|
121
|
-
const fallbackLabel = hasCurrentDefault ?
|
|
121
|
+
const fallbackLabel = hasCurrentDefault ? messages.config.wizardCurrent : messages.config.wizardSuggestion;
|
|
122
122
|
console.log("");
|
|
123
|
-
console.log(
|
|
124
|
-
console.log(`${fallbackLabel} : ${fallback}${!hasCurrentDefault && fallback === agentB ?
|
|
123
|
+
console.log(messages.config.wizardSummaryTitle);
|
|
124
|
+
console.log(`${fallbackLabel} : ${fallback}${!hasCurrentDefault && fallback === agentB ? ` (${messages.config.wizardAgentBHint})` : ""}`);
|
|
125
125
|
console.log("");
|
|
126
|
-
console.log(
|
|
126
|
+
console.log(` 0) ${messages.config.wizardNoSummary}`);
|
|
127
127
|
choices.forEach((choice, index) => {
|
|
128
128
|
console.log(` ${index + 1}) ${formatAgentLine(choice)}`);
|
|
129
129
|
});
|
|
130
130
|
while (true) {
|
|
131
|
-
const answer = await rl.question(
|
|
131
|
+
const answer = await rl.question(messages.config.wizardSummaryPrompt(fallback));
|
|
132
132
|
const value = answer.trim();
|
|
133
133
|
if (isQuit(value))
|
|
134
134
|
return undefined;
|
|
@@ -143,12 +143,12 @@ async function askSummaryAgent(rl, choices, defaultName, hasCurrentDefault, agen
|
|
|
143
143
|
if (choices.some((choice) => choice.name === value)) {
|
|
144
144
|
return value;
|
|
145
145
|
}
|
|
146
|
-
console.log(
|
|
146
|
+
console.log(messages.config.wizardInvalidSummaryChoice);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
async function askChoice(rl, label, defaultValue, allowed) {
|
|
149
|
+
async function askChoice(rl, label, defaultValue, allowed, messages) {
|
|
150
150
|
while (true) {
|
|
151
|
-
const answer = await rl.question(
|
|
151
|
+
const answer = await rl.question(messages.config.wizardChoiceQuestion(label, defaultValue));
|
|
152
152
|
const value = answer.trim();
|
|
153
153
|
if (isQuit(value))
|
|
154
154
|
return undefined;
|
|
@@ -156,17 +156,17 @@ async function askChoice(rl, label, defaultValue, allowed) {
|
|
|
156
156
|
return defaultValue;
|
|
157
157
|
if (allowed.includes(value))
|
|
158
158
|
return value;
|
|
159
|
-
console.log(
|
|
159
|
+
console.log(messages.config.wizardInvalidChoice(allowed.join(", ")));
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
async function askNumber(rl, label, defaultValue, hasCurrentDefault) {
|
|
163
|
-
const fallbackLabel = hasCurrentDefault ?
|
|
162
|
+
async function askNumber(rl, label, defaultValue, hasCurrentDefault, messages) {
|
|
163
|
+
const fallbackLabel = hasCurrentDefault ? messages.config.wizardCurrent : messages.config.wizardSuggestion;
|
|
164
164
|
console.log("");
|
|
165
165
|
console.log(label);
|
|
166
166
|
console.log(`${fallbackLabel} : ${defaultValue}`);
|
|
167
167
|
console.log("");
|
|
168
168
|
while (true) {
|
|
169
|
-
const answer = await rl.question(
|
|
169
|
+
const answer = await rl.question(messages.config.wizardTurnsPrompt(defaultValue));
|
|
170
170
|
const value = answer.trim();
|
|
171
171
|
if (isQuit(value))
|
|
172
172
|
return undefined;
|
|
@@ -175,21 +175,26 @@ async function askNumber(rl, label, defaultValue, hasCurrentDefault) {
|
|
|
175
175
|
const parsed = Number(value);
|
|
176
176
|
if (Number.isInteger(parsed)) {
|
|
177
177
|
try {
|
|
178
|
-
validateTurns(parsed,
|
|
178
|
+
validateTurns(parsed, messages.config.wizardTurnsLabel, messages);
|
|
179
179
|
return parsed;
|
|
180
180
|
}
|
|
181
181
|
catch {
|
|
182
182
|
// Show the user-facing wizard hint below.
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
console.log(
|
|
185
|
+
console.log(messages.config.wizardTurnsInvalid(MAX_TURNS));
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
function formatAgentLine(choice) {
|
|
189
189
|
return `${choice.name.padEnd(12)} ${choice.config.type} / ${choice.config.role}`;
|
|
190
190
|
}
|
|
191
|
-
function formatDefaults(defaults) {
|
|
192
|
-
return
|
|
191
|
+
function formatDefaults(defaults, messages) {
|
|
192
|
+
return messages.config.wizardDefaults({
|
|
193
|
+
agentA: defaults.agentA,
|
|
194
|
+
agentB: defaults.agentB,
|
|
195
|
+
turns: turnsOrDefault(defaults.turns ?? DEFAULT_TURNS),
|
|
196
|
+
summaryAgent: defaults.summaryAgent
|
|
197
|
+
});
|
|
193
198
|
}
|
|
194
199
|
function isQuit(value) {
|
|
195
200
|
return ["q", "quit", "exit"].includes(value.toLowerCase());
|