palabre 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -5
- package/dist/adapters/cli-pty.js +194 -0
- package/dist/adapters/cli.js +27 -6
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/terminal.js +13 -0
- package/dist/config.js +22 -1
- package/dist/contextScan.js +48 -0
- package/dist/discovery.js +3 -1
- package/dist/doctor.js +8 -2
- package/dist/index.js +45 -5
- package/dist/messages/adapter-errors.js +2 -0
- package/dist/messages/help.js +22 -0
- package/dist/messages/init.js +2 -2
- package/dist/messages/output.js +14 -2
- package/dist/new.js +4 -0
- package/dist/orchestrator.js +72 -16
- package/dist/output.js +20 -3
- package/dist/presets.js +44 -0
- package/dist/renderers/console.js +16 -1
- package/dist/renderers/ndjson.js +4 -0
- package/package.json +4 -1
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
|
|
|
@@ -50,6 +50,7 @@ palabre -s "Compare ces deux approches" -t 2
|
|
|
50
50
|
palabre codex-claude "Relis cette architecture" --context src docs
|
|
51
51
|
palabre claude-ollama "Critique ce fichier" --files README.md
|
|
52
52
|
palabre codex-claude "Preview" --context src --show-prompt
|
|
53
|
+
palabre context scan src docs --json
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
### Agents supportés
|
|
@@ -57,14 +58,27 @@ palabre codex-claude "Preview" --context src --show-prompt
|
|
|
57
58
|
- Claude Code via `claude --print`
|
|
58
59
|
- Codex CLI via `codex exec`
|
|
59
60
|
- Gemini CLI via `gemini --prompt -`
|
|
61
|
+
- Antigravity CLI via `agy --print` en pseudo-terminal
|
|
60
62
|
- OpenCode via `opencode run`
|
|
61
63
|
- Ollama via l'API locale HTTP
|
|
62
64
|
|
|
63
65
|
PALABRE ne liste pas les modèles : ils changent souvent et dépendent de chaque CLI ou compte utilisateur. `--model-a`, `--model-b` et `--summary-model` transmettent simplement la valeur brute à l'agent concerné.
|
|
64
66
|
|
|
67
|
+
### Intégrations
|
|
68
|
+
|
|
69
|
+
PALABRE expose des sorties JSON versionnées pour les clients externes :
|
|
70
|
+
|
|
71
|
+
- `palabre presets --json` pour lire les paires d'agents disponibles ;
|
|
72
|
+
- `palabre context scan --json` pour prévisualiser le contexte que `--context` retiendrait ;
|
|
73
|
+
- `--renderer ndjson` ou `--json` pour suivre un débat événement par événement.
|
|
74
|
+
|
|
75
|
+
Le flux NDJSON v1 est traité comme une API publique d'intégration. Les ajouts compatibles se font sans casser v1 ; les changements cassants doivent changer le champ `v`.
|
|
76
|
+
|
|
65
77
|
### Confidentialité
|
|
66
78
|
|
|
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é.
|
|
79
|
+
PALABRE tourne localement et n'envoie aucune donnée à un serveur appartenant à PALABRE. Les données envoyées aux agents dépendent des outils que vous utilisez : vérifiez les politiques de confidentialité de Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Ollama ou de tout autre agent configuré.
|
|
80
|
+
|
|
81
|
+
Si un agent échoue pendant le débat ou la synthèse, PALABRE conserve l'export Markdown partiel avec une section d'interruption quand c'est possible.
|
|
68
82
|
|
|
69
83
|
### Développement local
|
|
70
84
|
|
|
@@ -87,7 +101,7 @@ MIT. Voir [LICENSE](./LICENSE).
|
|
|
87
101
|
|
|
88
102
|
## English
|
|
89
103
|
|
|
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.
|
|
104
|
+
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
105
|
|
|
92
106
|
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
107
|
|
|
@@ -96,7 +110,7 @@ It does not replace your tools: it drives them. You keep your subscriptions, def
|
|
|
96
110
|
- https://palab.re
|
|
97
111
|
- https://palabre.netlify.app
|
|
98
112
|
|
|
99
|
-
Useful pages: [Installation](https://palab.re/
|
|
113
|
+
Useful pages: [Installation](https://palab.re/en/get-started/installation), [Configuration](https://palab.re/en/get-started/configuration), [First debate](https://palab.re/en/get-started/first-debate), [CLI reference](https://palab.re/en/reference/cli), [Troubleshooting](https://palab.re/en/troubleshooting), [Roadmap](https://palab.re/en/roadmap).
|
|
100
114
|
|
|
101
115
|
### Installation
|
|
102
116
|
|
|
@@ -124,6 +138,7 @@ palabre -s "Compare these two approaches" -t 2
|
|
|
124
138
|
palabre codex-claude "Review this architecture" --context src docs
|
|
125
139
|
palabre claude-ollama "Review this file" --files README.md
|
|
126
140
|
palabre codex-claude "Preview" --context src --show-prompt
|
|
141
|
+
palabre context scan src docs --json
|
|
127
142
|
```
|
|
128
143
|
|
|
129
144
|
### Supported Agents
|
|
@@ -131,14 +146,27 @@ palabre codex-claude "Preview" --context src --show-prompt
|
|
|
131
146
|
- Claude Code via `claude --print`
|
|
132
147
|
- Codex CLI via `codex exec`
|
|
133
148
|
- Gemini CLI via `gemini --prompt -`
|
|
149
|
+
- Antigravity CLI via `agy --print` in a pseudo-terminal
|
|
134
150
|
- OpenCode via `opencode run`
|
|
135
151
|
- Ollama via the local HTTP API
|
|
136
152
|
|
|
137
153
|
PALABRE does not list models: they change often and depend on each CLI or user account. `--model-a`, `--model-b`, and `--summary-model` simply pass the raw value to the selected agent.
|
|
138
154
|
|
|
155
|
+
### Integrations
|
|
156
|
+
|
|
157
|
+
PALABRE exposes versioned JSON outputs for external clients:
|
|
158
|
+
|
|
159
|
+
- `palabre presets --json` to read available agent pairs;
|
|
160
|
+
- `palabre context scan --json` to preview the context `--context` would retain;
|
|
161
|
+
- `--renderer ndjson` or `--json` to follow a debate event by event.
|
|
162
|
+
|
|
163
|
+
The NDJSON v1 stream is treated as a public integration API. Compatible additions do not break v1; breaking changes must change the `v` field.
|
|
164
|
+
|
|
139
165
|
### Privacy
|
|
140
166
|
|
|
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.
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
If an agent fails during the debate or final summary, PALABRE keeps the partial Markdown export with an interruption section whenever possible.
|
|
142
170
|
|
|
143
171
|
### Local Development
|
|
144
172
|
|
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
|
|
8
|
+
/**
|
|
9
|
+
* Adapter pour les CLIs qui exigent un vrai terminal.
|
|
10
|
+
* Contrairement à `CliAdapter`, stdout/stderr sont fusionnés dans le flux PTY.
|
|
11
|
+
*/
|
|
12
|
+
export class CliPtyAdapter {
|
|
13
|
+
name;
|
|
14
|
+
config;
|
|
15
|
+
role;
|
|
16
|
+
contract;
|
|
17
|
+
constructor(name, config) {
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.role = config.role;
|
|
21
|
+
this.contract = {
|
|
22
|
+
name,
|
|
23
|
+
kind: "cli-pty",
|
|
24
|
+
capabilities: {
|
|
25
|
+
mode: "pty",
|
|
26
|
+
supportsModelOverride: true,
|
|
27
|
+
supportsFilesystemAccess: true,
|
|
28
|
+
supportsStreaming: false,
|
|
29
|
+
supportsProcessExitCode: true,
|
|
30
|
+
supportsStderr: false
|
|
31
|
+
},
|
|
32
|
+
guarantees: {
|
|
33
|
+
rejectsEmptyOutput: !config.allowEmptyOutput,
|
|
34
|
+
rejectsNonZeroExit: true,
|
|
35
|
+
rejectsTimeout: true,
|
|
36
|
+
returnsRawOutput: true
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async generate(prompt) {
|
|
41
|
+
const renderedPrompt = formatAgentPrompt(prompt);
|
|
42
|
+
const promptMode = this.config.promptMode ?? "stdin";
|
|
43
|
+
const baseArgs = withModelArgs(this.config.args ?? [], this.config.model, this.config.modelArg ?? "--model");
|
|
44
|
+
const args = promptMode === "argument"
|
|
45
|
+
? [...baseArgs, renderedPrompt]
|
|
46
|
+
: baseArgs;
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
let output = "";
|
|
49
|
+
let outputBytes = 0;
|
|
50
|
+
let settled = false;
|
|
51
|
+
let hardTimer;
|
|
52
|
+
let term;
|
|
53
|
+
let dataSubscription;
|
|
54
|
+
let exitSubscription;
|
|
55
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
56
|
+
const finish = (error, exitCode, kill = true) => {
|
|
57
|
+
if (settled)
|
|
58
|
+
return;
|
|
59
|
+
settled = true;
|
|
60
|
+
clearTimeout(hardTimer);
|
|
61
|
+
dataSubscription?.dispose();
|
|
62
|
+
exitSubscription?.dispose();
|
|
63
|
+
if (kill) {
|
|
64
|
+
try {
|
|
65
|
+
term.kill();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// The PTY may already be closed.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
cleanupPty(term);
|
|
72
|
+
if (error) {
|
|
73
|
+
reject(error);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const content = cleanTerminalOutput(output);
|
|
77
|
+
if (exitCode && exitCode !== 0 && !content) {
|
|
78
|
+
reject(createPtyExitError(this.name, exitCode, output));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!content && !this.config.allowEmptyOutput) {
|
|
82
|
+
reject(new AdapterError("empty-output", this.name, `${this.name} produced empty PTY output.`, {
|
|
83
|
+
raw: output
|
|
84
|
+
}));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
resolve({
|
|
88
|
+
content,
|
|
89
|
+
raw: output
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
try {
|
|
93
|
+
term = spawnPty(resolveExecutable(this.config.command), args, {
|
|
94
|
+
name: "xterm-256color",
|
|
95
|
+
cols: this.config.cols ?? 120,
|
|
96
|
+
rows: this.config.rows ?? 40,
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
env: process.env,
|
|
99
|
+
...(process.platform !== "win32" ? { encoding: "utf8" } : {}),
|
|
100
|
+
...(process.platform === "win32" ? { useConpty: true } : {})
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
reject(new AdapterError("spawn-failed", this.name, `${this.name} failed to start PTY command "${this.config.command}": ${error instanceof Error ? error.message : String(error)}`, {
|
|
105
|
+
command: this.config.command
|
|
106
|
+
}));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
hardTimer = setTimeout(() => {
|
|
110
|
+
finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ?? 180_000}ms`, {
|
|
111
|
+
timeoutMs: this.config.timeoutMs ?? 180_000
|
|
112
|
+
}));
|
|
113
|
+
}, this.config.timeoutMs ?? 180_000);
|
|
114
|
+
dataSubscription = term.onData((chunk) => {
|
|
115
|
+
outputBytes += Buffer.byteLength(chunk, "utf8");
|
|
116
|
+
if (outputBytes > maxOutputBytes) {
|
|
117
|
+
finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of PTY output`, {
|
|
118
|
+
maxOutputBytes,
|
|
119
|
+
outputBytes
|
|
120
|
+
}));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
output += chunk;
|
|
124
|
+
});
|
|
125
|
+
exitSubscription = term.onExit(({ exitCode }) => {
|
|
126
|
+
finish(undefined, exitCode, false);
|
|
127
|
+
});
|
|
128
|
+
if (promptMode === "stdin") {
|
|
129
|
+
term.write(`${renderedPrompt}\r`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function resolveExecutable(command) {
|
|
135
|
+
if (path.isAbsolute(command) || command.includes("\\") || command.includes("/")) {
|
|
136
|
+
return command;
|
|
137
|
+
}
|
|
138
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
139
|
+
const trimmed = directory.trim();
|
|
140
|
+
if (!trimmed)
|
|
141
|
+
continue;
|
|
142
|
+
for (const extension of executableExtensions(command)) {
|
|
143
|
+
const candidate = path.join(trimmed, `${command}${extension}`);
|
|
144
|
+
if (existsSync(candidate)) {
|
|
145
|
+
return candidate;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return command;
|
|
150
|
+
}
|
|
151
|
+
function cleanupPty(term) {
|
|
152
|
+
const maybeTerm = term;
|
|
153
|
+
try {
|
|
154
|
+
maybeTerm._agent?._cleanUpProcess?.();
|
|
155
|
+
maybeTerm._agent?._conoutSocketWorker?._worker?.terminate?.();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Best-effort cleanup for Windows ConPTY internals.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
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
|
+
function createPtyExitError(adapterName, exitCode, raw) {
|
|
186
|
+
return new AdapterError("non-zero-exit", adapterName, `${adapterName} exited with code ${exitCode}: ${summarizePtyOutput(raw)}`, {
|
|
187
|
+
exitCode,
|
|
188
|
+
raw
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function summarizePtyOutput(output) {
|
|
192
|
+
const cleaned = cleanTerminalOutput(output);
|
|
193
|
+
return cleaned ? cleaned.slice(-1_200) : "aucune sortie PTY capturee.";
|
|
194
|
+
}
|
package/dist/adapters/cli.js
CHANGED
|
@@ -1,6 +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 { cleanTerminalOutput } from "./terminal.js";
|
|
5
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
|
|
4
6
|
/**
|
|
5
7
|
* Adapter pour les CLIs batch (Codex, Claude, Gemini…).
|
|
6
8
|
* Lance un sous-processus, injecte le prompt via stdin ou argument, capture stdout.
|
|
@@ -49,8 +51,10 @@ export class CliAdapter {
|
|
|
49
51
|
let stdout = "";
|
|
50
52
|
let stderr = "";
|
|
51
53
|
let settled = false;
|
|
54
|
+
let outputBytes = 0;
|
|
52
55
|
let hardTimer;
|
|
53
56
|
let idleTimer;
|
|
57
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
54
58
|
const finish = (error) => {
|
|
55
59
|
if (settled)
|
|
56
60
|
return;
|
|
@@ -93,10 +97,28 @@ export class CliAdapter {
|
|
|
93
97
|
};
|
|
94
98
|
bumpIdleTimer();
|
|
95
99
|
child.stdout.on("data", (chunk) => {
|
|
100
|
+
outputBytes += chunk.length;
|
|
101
|
+
if (outputBytes > maxOutputBytes) {
|
|
102
|
+
child.kill();
|
|
103
|
+
finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
|
|
104
|
+
maxOutputBytes,
|
|
105
|
+
outputBytes
|
|
106
|
+
}));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
96
109
|
stdout += chunk.toString("utf8");
|
|
97
110
|
bumpIdleTimer();
|
|
98
111
|
});
|
|
99
112
|
child.stderr.on("data", (chunk) => {
|
|
113
|
+
outputBytes += chunk.length;
|
|
114
|
+
if (outputBytes > maxOutputBytes) {
|
|
115
|
+
child.kill();
|
|
116
|
+
finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
|
|
117
|
+
maxOutputBytes,
|
|
118
|
+
outputBytes
|
|
119
|
+
}));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
100
122
|
stderr += chunk.toString("utf8");
|
|
101
123
|
bumpIdleTimer();
|
|
102
124
|
});
|
|
@@ -107,17 +129,18 @@ export class CliAdapter {
|
|
|
107
129
|
command: this.config.command
|
|
108
130
|
}));
|
|
109
131
|
});
|
|
110
|
-
|
|
132
|
+
const finishFromExitCode = (code) => {
|
|
111
133
|
if (code && code !== 0 && !stdout.trim()) {
|
|
112
134
|
finish(createCliExitError(this.name, code, stderr));
|
|
113
135
|
return;
|
|
114
136
|
}
|
|
115
137
|
finish();
|
|
116
|
-
}
|
|
138
|
+
};
|
|
139
|
+
child.on("close", finishFromExitCode);
|
|
117
140
|
if (promptMode === "stdin") {
|
|
118
141
|
child.stdin.write(renderedPrompt);
|
|
119
|
-
child.stdin.end();
|
|
120
142
|
}
|
|
143
|
+
child.stdin.end();
|
|
121
144
|
});
|
|
122
145
|
}
|
|
123
146
|
}
|
|
@@ -142,9 +165,7 @@ function withModelArgs(args, model, modelArg) {
|
|
|
142
165
|
}
|
|
143
166
|
/** Retire les séquences ANSI et les espaces en tête/fin. */
|
|
144
167
|
function cleanCliOutput(output) {
|
|
145
|
-
return output
|
|
146
|
-
.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
147
|
-
.trim();
|
|
168
|
+
return cleanTerminalOutput(output);
|
|
148
169
|
}
|
|
149
170
|
/**
|
|
150
171
|
* 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
|
@@ -66,6 +66,19 @@ export const exampleConfig = {
|
|
|
66
66
|
role: "reviewer",
|
|
67
67
|
tier: "primary"
|
|
68
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
|
+
},
|
|
69
82
|
opencode: {
|
|
70
83
|
type: "cli",
|
|
71
84
|
command: "opencode",
|
|
@@ -155,6 +168,10 @@ export function createConfigFromDiscovery(discovery) {
|
|
|
155
168
|
...config.agents.gemini,
|
|
156
169
|
...(discovery.gemini.available ? { command: discovery.gemini.command } : {})
|
|
157
170
|
};
|
|
171
|
+
config.agents.antigravity = {
|
|
172
|
+
...config.agents.antigravity,
|
|
173
|
+
...(discovery.antigravity.available ? { command: discovery.antigravity.command } : {})
|
|
174
|
+
};
|
|
158
175
|
config.agents.opencode = {
|
|
159
176
|
...config.agents.opencode,
|
|
160
177
|
...(discovery.opencode.available ? { command: discovery.opencode.command } : {})
|
|
@@ -186,7 +203,7 @@ function chooseDefaultOllamaModel(discovery) {
|
|
|
186
203
|
return discovery.ollama.models[0] ?? DEFAULT_OLLAMA_MODEL;
|
|
187
204
|
}
|
|
188
205
|
function chooseDefaultSummaryAgent(pair) {
|
|
189
|
-
for (const preferred of ["claude", "codex", "gemini"]) {
|
|
206
|
+
for (const preferred of ["claude", "codex", "antigravity", "gemini"]) {
|
|
190
207
|
if (pair.includes(preferred)) {
|
|
191
208
|
return preferred;
|
|
192
209
|
}
|
|
@@ -206,12 +223,16 @@ function chooseDefaultPair(discovery) {
|
|
|
206
223
|
if (discovery.opencode.available && discovery.ollama.available) {
|
|
207
224
|
return ["opencode", "ollama-local"];
|
|
208
225
|
}
|
|
226
|
+
if (discovery.antigravity.available && discovery.ollama.available) {
|
|
227
|
+
return ["antigravity", "ollama-local"];
|
|
228
|
+
}
|
|
209
229
|
if (discovery.gemini.available && discovery.ollama.available) {
|
|
210
230
|
return ["gemini", "ollama-local"];
|
|
211
231
|
}
|
|
212
232
|
const cliAgents = [
|
|
213
233
|
discovery.codex.available ? "codex" : undefined,
|
|
214
234
|
discovery.claude.available ? "claude" : undefined,
|
|
235
|
+
discovery.antigravity.available ? "antigravity" : undefined,
|
|
215
236
|
discovery.opencode.available ? "opencode" : undefined,
|
|
216
237
|
discovery.gemini.available ? "gemini" : undefined
|
|
217
238
|
].filter((agent) => Boolean(agent));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadProjectInputs } from "./context.js";
|
|
3
|
+
import { createTranslator } from "./i18n.js";
|
|
4
|
+
/**
|
|
5
|
+
* Builds the machine-readable context preview used by integrations.
|
|
6
|
+
*
|
|
7
|
+
* The scan intentionally reuses the same tolerant loader as `--context`, so
|
|
8
|
+
* the returned files are the files Palabre would actually inject into a debate.
|
|
9
|
+
*/
|
|
10
|
+
export async function buildContextScan(scanPaths, cwd = process.cwd(), messages = createTranslator("fr")) {
|
|
11
|
+
const effectiveScanPaths = scanPaths.length > 0 ? scanPaths : ["."];
|
|
12
|
+
const result = await loadProjectInputs([], effectiveScanPaths, cwd, messages);
|
|
13
|
+
const files = result.files.map((file) => ({
|
|
14
|
+
kind: "file",
|
|
15
|
+
path: file.path,
|
|
16
|
+
absolutePath: file.absolutePath,
|
|
17
|
+
sizeBytes: file.sizeBytes
|
|
18
|
+
}));
|
|
19
|
+
const folders = collectContextFolders(files.map((file) => file.path), cwd);
|
|
20
|
+
return {
|
|
21
|
+
v: 1,
|
|
22
|
+
root: cwd,
|
|
23
|
+
scanned: effectiveScanPaths,
|
|
24
|
+
items: [...folders, ...files],
|
|
25
|
+
warnings: result.warnings
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function collectContextFolders(filePaths, cwd) {
|
|
29
|
+
const counts = new Map();
|
|
30
|
+
if (filePaths.length > 0) {
|
|
31
|
+
counts.set(".", filePaths.length);
|
|
32
|
+
}
|
|
33
|
+
for (const filePath of filePaths) {
|
|
34
|
+
const parts = filePath.split("/").filter(Boolean);
|
|
35
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
36
|
+
const folder = parts.slice(0, index).join("/");
|
|
37
|
+
counts.set(folder, (counts.get(folder) ?? 0) + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [...counts.entries()]
|
|
41
|
+
.sort(([left], [right]) => left === "." ? -1 : right === "." ? 1 : left.localeCompare(right))
|
|
42
|
+
.map(([folder, filesCount]) => ({
|
|
43
|
+
kind: "folder",
|
|
44
|
+
path: folder,
|
|
45
|
+
absolutePath: path.resolve(cwd, folder),
|
|
46
|
+
filesCount
|
|
47
|
+
}));
|
|
48
|
+
}
|
package/dist/discovery.js
CHANGED
|
@@ -5,10 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
* Sur Windows, tente `claude.exe` avant `claude`.
|
|
6
6
|
*/
|
|
7
7
|
export async function discoverLocalTools() {
|
|
8
|
-
const [codex, claude, gemini, opencode, ollamaCommand] = await Promise.all([
|
|
8
|
+
const [codex, claude, gemini, antigravity, opencode, ollamaCommand] = await Promise.all([
|
|
9
9
|
detectCommand("codex"),
|
|
10
10
|
detectFirstCommand(process.platform === "win32" ? ["claude.exe", "claude"] : ["claude"]),
|
|
11
11
|
detectCommand("gemini"),
|
|
12
|
+
detectCommand("agy"),
|
|
12
13
|
detectCommand("opencode"),
|
|
13
14
|
detectCommand("ollama")
|
|
14
15
|
]);
|
|
@@ -17,6 +18,7 @@ export async function discoverLocalTools() {
|
|
|
17
18
|
codex,
|
|
18
19
|
claude,
|
|
19
20
|
gemini,
|
|
21
|
+
antigravity,
|
|
20
22
|
opencode,
|
|
21
23
|
ollama: {
|
|
22
24
|
...ollamaServer,
|
package/dist/doctor.js
CHANGED
|
@@ -41,6 +41,7 @@ export async function runDoctor(explicitConfigPath, plain = false, explicitLangu
|
|
|
41
41
|
lines.push(formatCommand("Codex CLI", discovery.codex.available, discovery.codex.command, discovery.codex.path, t));
|
|
42
42
|
lines.push(formatCommand("Claude CLI", discovery.claude.available, discovery.claude.command, discovery.claude.path, t));
|
|
43
43
|
lines.push(formatCommand("Gemini CLI", discovery.gemini.available, discovery.gemini.command, discovery.gemini.path, t));
|
|
44
|
+
lines.push(formatCommand("Antigravity CLI", discovery.antigravity.available, discovery.antigravity.command, discovery.antigravity.path, t));
|
|
44
45
|
lines.push(formatCommand("OpenCode CLI", discovery.opencode.available, discovery.opencode.command, discovery.opencode.path, t));
|
|
45
46
|
lines.push(discovery.ollama.available
|
|
46
47
|
? ok(t.doctor.ollamaReachable(discovery.ollama.baseUrl, discovery.ollama.models.length))
|
|
@@ -156,7 +157,7 @@ function inspectAgents(config, discovery, lines, t) {
|
|
|
156
157
|
lines.push(info(t.doctor.configuredAgents, "agents"));
|
|
157
158
|
for (const [name, agent] of Object.entries(config.agents)) {
|
|
158
159
|
inspectAgentShape(name, agent, lines, t);
|
|
159
|
-
if (agent.type === "cli") {
|
|
160
|
+
if (agent.type === "cli" || agent.type === "cli-pty") {
|
|
160
161
|
inspectCliAgent(name, agent, discovery, lines, t);
|
|
161
162
|
continue;
|
|
162
163
|
}
|
|
@@ -167,7 +168,7 @@ function inspectAgentShape(name, agent, lines, t) {
|
|
|
167
168
|
if (!agent.role) {
|
|
168
169
|
lines.push(error(t.doctor.roleMissing(name)));
|
|
169
170
|
}
|
|
170
|
-
if (agent.type === "cli") {
|
|
171
|
+
if (agent.type === "cli" || agent.type === "cli-pty") {
|
|
171
172
|
if (!agent.command || !agent.command.trim()) {
|
|
172
173
|
lines.push(error(t.doctor.cliCommandMissing(name)));
|
|
173
174
|
}
|
|
@@ -223,6 +224,7 @@ function detectedAgentNames(discovery) {
|
|
|
223
224
|
discovery.codex.available ? "codex" : undefined,
|
|
224
225
|
discovery.claude.available ? "claude" : undefined,
|
|
225
226
|
discovery.gemini.available ? "gemini" : undefined,
|
|
227
|
+
discovery.antigravity.available ? "antigravity" : undefined,
|
|
226
228
|
discovery.opencode.available ? "opencode" : undefined,
|
|
227
229
|
discovery.ollama.available ? "ollama-local" : undefined
|
|
228
230
|
].filter((name) => Boolean(name));
|
|
@@ -240,6 +242,10 @@ function knownCliDetection(command, discovery) {
|
|
|
240
242
|
return discovery.claude;
|
|
241
243
|
if (normalized === "gemini")
|
|
242
244
|
return discovery.gemini;
|
|
245
|
+
if (normalized === "agy")
|
|
246
|
+
return discovery.antigravity;
|
|
247
|
+
if (normalized === "antigravity")
|
|
248
|
+
return discovery.antigravity;
|
|
243
249
|
if (normalized === "opencode")
|
|
244
250
|
return discovery.opencode;
|
|
245
251
|
return undefined;
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
|
|
6
6
|
import { loadProjectInputs } from "./context.js";
|
|
7
|
+
import { buildContextScan } from "./contextScan.js";
|
|
7
8
|
import { discoverLocalTools } from "./discovery.js";
|
|
8
9
|
import { runDoctor } from "./doctor.js";
|
|
9
10
|
import { AdapterError, formatAdapterError } from "./errors.js";
|
|
@@ -51,6 +52,10 @@ async function main() {
|
|
|
51
52
|
await runPresetsCommand(parsed.flags);
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
55
|
+
if (parsed.command === "context") {
|
|
56
|
+
await runContextCommand(parsed.flags, parsed.positionals);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
54
59
|
if (parsed.command === "update") {
|
|
55
60
|
const info = await getUpdateInfo(await getPackageVersion());
|
|
56
61
|
const updateConfigPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
|
|
@@ -168,8 +173,11 @@ async function main() {
|
|
|
168
173
|
const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, messages);
|
|
169
174
|
context.warnings.forEach((warning) => renderer.warning(warning));
|
|
170
175
|
const result = await runDebate(config, options, renderer, messages);
|
|
171
|
-
const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages);
|
|
176
|
+
const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages, result.failure);
|
|
172
177
|
renderer.done(outputPath);
|
|
178
|
+
if (result.failure) {
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
}
|
|
173
181
|
}
|
|
174
182
|
/**
|
|
175
183
|
* Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
|
|
@@ -436,6 +444,31 @@ async function runPresetsCommand(flags) {
|
|
|
436
444
|
console.log("");
|
|
437
445
|
console.log(messages.presets.total(presets.length));
|
|
438
446
|
}
|
|
447
|
+
async function runContextCommand(flags, positionals) {
|
|
448
|
+
const language = resolveLanguage({ explicitLanguage: optionalString(flags.language) });
|
|
449
|
+
const messages = createTranslator(language);
|
|
450
|
+
const subcommand = positionals[0] ?? "scan";
|
|
451
|
+
if (subcommand !== "scan") {
|
|
452
|
+
throw new Error(messages.common.unknownCommand(`context ${subcommand}`, "context scan"));
|
|
453
|
+
}
|
|
454
|
+
const paths = positionals.slice(1);
|
|
455
|
+
const result = await buildContextScan(paths, process.cwd(), messages);
|
|
456
|
+
const folders = result.items.filter((item) => item.kind === "folder");
|
|
457
|
+
const files = result.items.filter((item) => item.kind === "file");
|
|
458
|
+
if (flags.json) {
|
|
459
|
+
console.log(JSON.stringify(result, null, 2));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
for (const folder of folders) {
|
|
463
|
+
console.log(`[folder] ${folder.path}`);
|
|
464
|
+
}
|
|
465
|
+
for (const file of files) {
|
|
466
|
+
console.log(`[file] ${file.path} (${file.sizeBytes} bytes)`);
|
|
467
|
+
}
|
|
468
|
+
for (const warning of result.warnings) {
|
|
469
|
+
console.error(`${messages.renderers.warningPrefix} ${warning}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
439
472
|
/**
|
|
440
473
|
* Parse `process.argv` en une structure typée `ParsedArgs`.
|
|
441
474
|
* Gère les flags courts (-h, -v, -s, -t, -a), les flags longs (--topic, --agent-a…),
|
|
@@ -448,7 +481,7 @@ function parseArgs(args, messages) {
|
|
|
448
481
|
let command = "run";
|
|
449
482
|
let commandExplicit = false;
|
|
450
483
|
const positionals = [];
|
|
451
|
-
const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents", "preset", "presets"]);
|
|
484
|
+
const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents", "preset", "presets", "context"]);
|
|
452
485
|
const presets = new Set(listPresetNames());
|
|
453
486
|
for (let index = 0; index < args.length; index += 1) {
|
|
454
487
|
const value = args[index];
|
|
@@ -545,7 +578,7 @@ function parseArgs(args, messages) {
|
|
|
545
578
|
if (command === "run") {
|
|
546
579
|
applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages);
|
|
547
580
|
}
|
|
548
|
-
return { command, commandExplicit, flags };
|
|
581
|
+
return { command, commandExplicit, positionals, flags };
|
|
549
582
|
}
|
|
550
583
|
/**
|
|
551
584
|
* Détecte si une valeur ressemble à une faute de frappe d'une commande connue
|
|
@@ -699,6 +732,7 @@ function findDetectedMissingAgents(config, discovery) {
|
|
|
699
732
|
discovery.codex.available ? "codex" : undefined,
|
|
700
733
|
discovery.claude.available ? "claude" : undefined,
|
|
701
734
|
discovery.gemini.available ? "gemini" : undefined,
|
|
735
|
+
discovery.antigravity.available ? "antigravity" : undefined,
|
|
702
736
|
discovery.opencode.available ? "opencode" : undefined,
|
|
703
737
|
discovery.ollama.available ? "ollama-local" : undefined
|
|
704
738
|
].filter((agent) => Boolean(agent));
|
|
@@ -780,16 +814,20 @@ function formatAgentDetection(name, agentConfig, discovery, messages) {
|
|
|
780
814
|
* @param discovery - Résultat de la découverte locale des outils.
|
|
781
815
|
*/
|
|
782
816
|
function cliDetectionForAgent(name, agentConfig, discovery) {
|
|
783
|
-
const command = normalizeCommandName(agentConfig.type === "cli" ? agentConfig.command : name);
|
|
817
|
+
const command = normalizeCommandName(agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name);
|
|
784
818
|
if (command === "codex")
|
|
785
819
|
return discovery.codex;
|
|
786
820
|
if (command === "claude")
|
|
787
821
|
return discovery.claude;
|
|
788
822
|
if (command === "gemini")
|
|
789
823
|
return discovery.gemini;
|
|
824
|
+
if (command === "agy")
|
|
825
|
+
return discovery.antigravity;
|
|
826
|
+
if (command === "antigravity")
|
|
827
|
+
return discovery.antigravity;
|
|
790
828
|
if (command === "opencode")
|
|
791
829
|
return discovery.opencode;
|
|
792
|
-
return { available: true, command: agentConfig.type === "cli" ? agentConfig.command : name };
|
|
830
|
+
return { available: true, command: agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name };
|
|
793
831
|
}
|
|
794
832
|
/**
|
|
795
833
|
* Extrait le nom de base d'une commande en supprimant le chemin et l'extension Windows éventuelle.
|
|
@@ -809,6 +847,7 @@ function printInitDiscovery(discovery, config, messages) {
|
|
|
809
847
|
console.log(`- Codex CLI: ${formatCommandDetection(discovery.codex, messages)}`);
|
|
810
848
|
console.log(`- Claude CLI: ${formatCommandDetection(discovery.claude, messages)}`);
|
|
811
849
|
console.log(`- Gemini CLI: ${formatCommandDetection(discovery.gemini, messages)}`);
|
|
850
|
+
console.log(`- Antigravity CLI: ${formatCommandDetection(discovery.antigravity, messages)}`);
|
|
812
851
|
console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode, messages)}`);
|
|
813
852
|
console.log(`- Ollama API: ${formatOllamaDetection(discovery.ollama, messages)}`);
|
|
814
853
|
console.log("");
|
|
@@ -822,6 +861,7 @@ function formatDetectedAgentSummary(discovery, language) {
|
|
|
822
861
|
discovery.codex.available ? "codex" : undefined,
|
|
823
862
|
discovery.claude.available ? "claude" : undefined,
|
|
824
863
|
discovery.gemini.available ? "gemini" : undefined,
|
|
864
|
+
discovery.antigravity.available ? "antigravity" : undefined,
|
|
825
865
|
discovery.opencode.available ? "opencode" : undefined,
|
|
826
866
|
discovery.ollama.available ? "ollama-local" : undefined
|
|
827
867
|
].filter((name) => Boolean(name));
|
|
@@ -3,6 +3,7 @@ const frHints = {
|
|
|
3
3
|
"spawn-failed": "Sur Windows, essaye le wrapper .cmd ou active \"shell\": true dans la config agent.",
|
|
4
4
|
timeout: "Augmente timeoutMs ou teste la commande directement dans le terminal.",
|
|
5
5
|
"idle-timeout": "Desactive idleTimeoutMs pour les CLIs IA qui restent silencieuses pendant la generation.",
|
|
6
|
+
"output-too-large": "Reduis le contexte, le nombre de tours ou configure maxOutputBytes pour cet agent si ce volume est attendu.",
|
|
6
7
|
"empty-output": "Teste la commande en dehors de Palabre et verifie que le prompt est bien lu via stdin ou argument.",
|
|
7
8
|
"usage-limit": "Attends la fenetre indiquee par la CLI, change de modele ou relance avec un autre agent/preset disponible.",
|
|
8
9
|
"non-zero-exit": "Teste la commande directement, puis ajuste args, permissions, modele ou authentification de la CLI.",
|
|
@@ -16,6 +17,7 @@ const enHints = {
|
|
|
16
17
|
"spawn-failed": "On Windows, try the .cmd wrapper or enable \"shell\": true in the agent config.",
|
|
17
18
|
timeout: "Increase timeoutMs or test the command directly in the terminal.",
|
|
18
19
|
"idle-timeout": "Disable idleTimeoutMs for AI CLIs that stay silent while generating.",
|
|
20
|
+
"output-too-large": "Reduce context, turn count, or configure maxOutputBytes for this agent if this volume is expected.",
|
|
19
21
|
"empty-output": "Test the command outside Palabre and check that the prompt is read through stdin or an argument.",
|
|
20
22
|
"usage-limit": "Wait for the window indicated by the CLI, change model, or run again with another available agent/preset.",
|
|
21
23
|
"non-zero-exit": "Test the command directly, then adjust args, permissions, model, or CLI authentication.",
|
package/dist/messages/help.js
CHANGED
|
@@ -29,6 +29,16 @@ Usage:
|
|
|
29
29
|
Flags:
|
|
30
30
|
--json sortie structuree pour integrations
|
|
31
31
|
--config <path> chemin de config explicite
|
|
32
|
+
`,
|
|
33
|
+
context: `
|
|
34
|
+
Scanne le contexte projet avec les memes regles que --context.
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
palabre context scan [paths...] [flags]
|
|
38
|
+
|
|
39
|
+
Flags:
|
|
40
|
+
--json sortie structuree pour integrations
|
|
41
|
+
--language <fr|en> force la langue des avertissements
|
|
32
42
|
`,
|
|
33
43
|
config: `
|
|
34
44
|
Configure les agents par defaut, la synthese, le nombre de reponses et la langue.
|
|
@@ -124,6 +134,16 @@ Usage:
|
|
|
124
134
|
Flags:
|
|
125
135
|
--json structured output for integrations
|
|
126
136
|
--config <path> explicit config path
|
|
137
|
+
`,
|
|
138
|
+
context: `
|
|
139
|
+
Scans project context with the same rules as --context.
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
palabre context scan [paths...] [flags]
|
|
143
|
+
|
|
144
|
+
Flags:
|
|
145
|
+
--json structured output for integrations
|
|
146
|
+
--language <fr|en> forces warning language
|
|
127
147
|
`,
|
|
128
148
|
config: `
|
|
129
149
|
Configures default agents, summary, response count, and language.
|
|
@@ -212,6 +232,7 @@ Commandes:
|
|
|
212
232
|
new Assistant interactif de debat
|
|
213
233
|
agents Lister les agents configures
|
|
214
234
|
presets Lister les presets disponibles
|
|
235
|
+
context Scanner le contexte projet
|
|
215
236
|
config Modifier les parametres par defaut
|
|
216
237
|
doctor Verifier la config et les outils locaux
|
|
217
238
|
update Afficher ou appliquer les etapes de mise a jour
|
|
@@ -256,6 +277,7 @@ Commands:
|
|
|
256
277
|
new Interactive debate assistant
|
|
257
278
|
agents List configured agents
|
|
258
279
|
presets List available presets
|
|
280
|
+
context Scan project context
|
|
259
281
|
config Edit default settings
|
|
260
282
|
doctor Check config and local tools
|
|
261
283
|
update Show or apply update steps
|
package/dist/messages/init.js
CHANGED
|
@@ -10,7 +10,7 @@ export const initMessages = {
|
|
|
10
10
|
ollamaMissing: "non détecté",
|
|
11
11
|
ollamaDetected: (modelCount) => `détectée (${modelCount} modèle${modelCount > 1 ? "s" : ""})`,
|
|
12
12
|
defaults: (agentA, agentB) => `Défauts: ${agentA} <-> ${agentB}`,
|
|
13
|
-
noDefaultPair: (detectedAgents) => `Défauts: ${detectedAgents}. Palabre a besoin d'au moins deux agents.\nAgents compatibles: Codex CLI, Claude CLI, Gemini CLI, OpenCode CLI, Ollama local.\nGuide: https://palab.re/fr/agents/overview`,
|
|
13
|
+
noDefaultPair: (detectedAgents) => `Défauts: ${detectedAgents}. Palabre a besoin d'au moins deux agents.\nAgents compatibles: Codex CLI, Claude CLI, Gemini CLI, Antigravity CLI, OpenCode CLI, Ollama local.\nGuide: https://palab.re/fr/agents/overview`,
|
|
14
14
|
languageHint: (language) => `Langue: ${language}\nEnglish > palabre config --language en`
|
|
15
15
|
},
|
|
16
16
|
en: {
|
|
@@ -24,7 +24,7 @@ export const initMessages = {
|
|
|
24
24
|
ollamaMissing: "not detected",
|
|
25
25
|
ollamaDetected: (modelCount) => `detected (${modelCount} model${modelCount > 1 ? "s" : ""})`,
|
|
26
26
|
defaults: (agentA, agentB) => `Defaults: ${agentA} <-> ${agentB}`,
|
|
27
|
-
noDefaultPair: (detectedAgents) => `Defaults: ${detectedAgents}. Palabre needs at least two agents.\nCompatible agents: Codex CLI, Claude CLI, Gemini CLI, OpenCode CLI, local Ollama.\nGuide: https://palab.re/en/agents/overview`,
|
|
27
|
+
noDefaultPair: (detectedAgents) => `Defaults: ${detectedAgents}. Palabre needs at least two agents.\nCompatible agents: Codex CLI, Claude CLI, Gemini CLI, Antigravity CLI, OpenCode CLI, local Ollama.\nGuide: https://palab.re/en/agents/overview`,
|
|
28
28
|
languageHint: (language) => `Language: ${language}\nFrançais > palabre config --language fr`
|
|
29
29
|
}
|
|
30
30
|
};
|
package/dist/messages/output.js
CHANGED
|
@@ -3,6 +3,7 @@ export const outputMessages = {
|
|
|
3
3
|
title: "# PALABRE Debate",
|
|
4
4
|
contextTitle: "## Contexte",
|
|
5
5
|
exchangesTitle: "## Echanges",
|
|
6
|
+
failureTitle: "## Interruption",
|
|
6
7
|
finalSummaryTitle: "## Synthese finale",
|
|
7
8
|
tableField: "Champ",
|
|
8
9
|
tableValue: "Valeur",
|
|
@@ -27,13 +28,19 @@ export const outputMessages = {
|
|
|
27
28
|
sessionStartedAt: "Session demarree a",
|
|
28
29
|
agent: "Agent",
|
|
29
30
|
role: "Role",
|
|
30
|
-
date: "Date"
|
|
31
|
+
date: "Date",
|
|
32
|
+
failurePhase: "Phase",
|
|
33
|
+
failureAgent: "Agent",
|
|
34
|
+
failureTurn: "Tour",
|
|
35
|
+
failureKind: "Type d'erreur",
|
|
36
|
+
failureMessage: "Message"
|
|
31
37
|
}
|
|
32
38
|
},
|
|
33
39
|
en: {
|
|
34
40
|
title: "# PALABRE Debate",
|
|
35
41
|
contextTitle: "## Context",
|
|
36
42
|
exchangesTitle: "## Exchanges",
|
|
43
|
+
failureTitle: "## Interruption",
|
|
37
44
|
finalSummaryTitle: "## Final summary",
|
|
38
45
|
tableField: "Field",
|
|
39
46
|
tableValue: "Value",
|
|
@@ -58,7 +65,12 @@ export const outputMessages = {
|
|
|
58
65
|
sessionStartedAt: "Session started at",
|
|
59
66
|
agent: "Agent",
|
|
60
67
|
role: "Role",
|
|
61
|
-
date: "Date"
|
|
68
|
+
date: "Date",
|
|
69
|
+
failurePhase: "Phase",
|
|
70
|
+
failureAgent: "Agent",
|
|
71
|
+
failureTurn: "Turn",
|
|
72
|
+
failureKind: "Error kind",
|
|
73
|
+
failureMessage: "Message"
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
};
|
package/dist/new.js
CHANGED
|
@@ -147,6 +147,10 @@ function isAgentDetected(name, config, discovery) {
|
|
|
147
147
|
return discovery.claude.available;
|
|
148
148
|
if (normalized === "gemini")
|
|
149
149
|
return discovery.gemini.available;
|
|
150
|
+
if (normalized === "agy")
|
|
151
|
+
return discovery.antigravity.available;
|
|
152
|
+
if (normalized === "antigravity")
|
|
153
|
+
return discovery.antigravity.available;
|
|
150
154
|
if (normalized === "opencode")
|
|
151
155
|
return discovery.opencode.available;
|
|
152
156
|
return true;
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createAgent } from "./adapters/index.js";
|
|
2
|
+
import { AdapterError } from "./errors.js";
|
|
2
3
|
import { createTranslator } from "./i18n.js";
|
|
3
4
|
/**
|
|
4
5
|
* Point d'entrée de l'orchestration.
|
|
@@ -36,18 +37,39 @@ export async function runDebate(config, options, renderer, messages = createTran
|
|
|
36
37
|
const turn = index + 1;
|
|
37
38
|
renderer?.turnStart(turn, options.turns, current.name, current.role);
|
|
38
39
|
renderer?.thinkingStart(current.name, current.role);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
let response;
|
|
41
|
+
try {
|
|
42
|
+
response = await current.generate({
|
|
43
|
+
topic: options.topic,
|
|
44
|
+
turn,
|
|
45
|
+
totalTurns: options.turns,
|
|
46
|
+
selfName: current.name,
|
|
47
|
+
peerName: peer.name,
|
|
48
|
+
selfRole: current.role,
|
|
49
|
+
language: options.language,
|
|
50
|
+
session: options.session,
|
|
51
|
+
files: options.files,
|
|
52
|
+
transcript
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const failure = toDebateFailure(error, {
|
|
57
|
+
phase: "debate",
|
|
58
|
+
agent: current.name,
|
|
59
|
+
role: current.role,
|
|
60
|
+
turn
|
|
61
|
+
});
|
|
62
|
+
renderer?.error(failure);
|
|
63
|
+
return {
|
|
64
|
+
options,
|
|
65
|
+
messages: transcript,
|
|
66
|
+
stopReason,
|
|
67
|
+
failure
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
renderer?.thinkingEnd();
|
|
72
|
+
}
|
|
51
73
|
const message = {
|
|
52
74
|
agent: current.name,
|
|
53
75
|
role: current.role,
|
|
@@ -62,14 +84,27 @@ export async function runDebate(config, options, renderer, messages = createTran
|
|
|
62
84
|
break;
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
let summary;
|
|
88
|
+
let failure;
|
|
89
|
+
if (options.summaryEnabled) {
|
|
90
|
+
try {
|
|
91
|
+
summary = await generateSummary(config, options, transcript, renderer, messages);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
failure = toDebateFailure(error, {
|
|
95
|
+
phase: "summary",
|
|
96
|
+
agent: options.summaryAgent ?? options.agentB,
|
|
97
|
+
turn: transcript.length + 1
|
|
98
|
+
});
|
|
99
|
+
renderer?.error(failure);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
68
102
|
return {
|
|
69
103
|
options,
|
|
70
104
|
messages: transcript,
|
|
71
105
|
summary,
|
|
72
|
-
stopReason
|
|
106
|
+
stopReason,
|
|
107
|
+
failure
|
|
73
108
|
};
|
|
74
109
|
}
|
|
75
110
|
/**
|
|
@@ -164,6 +199,27 @@ async function generateSummary(config, options, transcript, renderer, messages =
|
|
|
164
199
|
renderer?.message(summary.content);
|
|
165
200
|
return summary;
|
|
166
201
|
}
|
|
202
|
+
function toDebateFailure(error, context) {
|
|
203
|
+
if (error instanceof AdapterError) {
|
|
204
|
+
return {
|
|
205
|
+
phase: context.phase,
|
|
206
|
+
agent: context.agent ?? error.adapterName,
|
|
207
|
+
role: context.role,
|
|
208
|
+
turn: context.turn,
|
|
209
|
+
kind: error.kind,
|
|
210
|
+
message: error.message,
|
|
211
|
+
details: error.details
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
phase: context.phase,
|
|
216
|
+
agent: context.agent,
|
|
217
|
+
role: context.role,
|
|
218
|
+
turn: context.turn,
|
|
219
|
+
kind: "unknown",
|
|
220
|
+
message: error instanceof Error ? error.message : String(error)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
167
223
|
/** Résout le model override pour un agent donné. Retourne `undefined` si l'agent n'est ni A ni B. */
|
|
168
224
|
function modelForAgent(options, agent) {
|
|
169
225
|
if (agent === options.agentA) {
|
package/dist/output.js
CHANGED
|
@@ -5,12 +5,12 @@ import { createTranslator } from "./i18n.js";
|
|
|
5
5
|
* Écrit le débat au format Markdown dans `outputDir`.
|
|
6
6
|
* Crée le répertoire si absent. Retourne le chemin absolu du fichier créé.
|
|
7
7
|
*/
|
|
8
|
-
export async function writeDebateMarkdown(outputDir, options, debateMessages, summary, stopReason, messages = createTranslator("fr")) {
|
|
8
|
+
export async function writeDebateMarkdown(outputDir, options, debateMessages, summary, stopReason, messages = createTranslator("fr"), failure) {
|
|
9
9
|
const safeDate = new Date().toISOString().replace(/[:.]/g, "-");
|
|
10
10
|
const fileName = `palabre-${slugifyTopic(options.topic)}-${safeDate}.debate.md`;
|
|
11
11
|
const filePath = path.resolve(outputDir, fileName);
|
|
12
12
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
13
|
-
await writeFile(filePath, renderDebateMarkdown(options, debateMessages, summary, stopReason, messages), "utf8");
|
|
13
|
+
await writeFile(filePath, renderDebateMarkdown(options, debateMessages, summary, stopReason, messages, failure), "utf8");
|
|
14
14
|
return filePath;
|
|
15
15
|
}
|
|
16
16
|
function slugifyTopic(topic) {
|
|
@@ -28,7 +28,7 @@ function slugifyTopic(topic) {
|
|
|
28
28
|
* Produit la représentation Markdown complète du débat.
|
|
29
29
|
* Fonction pure : aucun effet de bord sur le filesystem.
|
|
30
30
|
*/
|
|
31
|
-
export function renderDebateMarkdown(options, debateMessages, summary, stopReason, messages = createTranslator("fr")) {
|
|
31
|
+
export function renderDebateMarkdown(options, debateMessages, summary, stopReason, messages = createTranslator("fr"), failure) {
|
|
32
32
|
const lines = [
|
|
33
33
|
messages.output.title,
|
|
34
34
|
"",
|
|
@@ -44,6 +44,9 @@ export function renderDebateMarkdown(options, debateMessages, summary, stopReaso
|
|
|
44
44
|
for (const message of debateMessages) {
|
|
45
45
|
lines.push(`### ${message.agent} (${message.role})`, "", normalizeMarkdownForWindowsPreview(message.content.trim()), "");
|
|
46
46
|
}
|
|
47
|
+
if (failure) {
|
|
48
|
+
lines.push("---", "", messages.output.failureTitle, "", ...renderFailureBlock(failure, messages), "");
|
|
49
|
+
}
|
|
47
50
|
lines.push("---", "", messages.output.finalSummaryTitle, "", ...renderSummaryBlock(options, summary, messages));
|
|
48
51
|
return `${lines.join("\n")}\n`;
|
|
49
52
|
}
|
|
@@ -67,6 +70,20 @@ function renderSummaryBlock(options, summary, messages) {
|
|
|
67
70
|
""
|
|
68
71
|
];
|
|
69
72
|
}
|
|
73
|
+
function renderFailureBlock(failure, messages) {
|
|
74
|
+
const rows = [
|
|
75
|
+
[messages.output.fields.failurePhase, failure.phase],
|
|
76
|
+
[messages.output.fields.failureAgent, failure.agent ?? messages.output.no],
|
|
77
|
+
[messages.output.fields.failureTurn, failure.turn === undefined ? messages.output.no : String(failure.turn)],
|
|
78
|
+
[messages.output.fields.failureKind, failure.kind],
|
|
79
|
+
[messages.output.fields.failureMessage, failure.message]
|
|
80
|
+
];
|
|
81
|
+
return [
|
|
82
|
+
`| ${messages.output.tableField} | ${messages.output.tableValue} |`,
|
|
83
|
+
"| --- | --- |",
|
|
84
|
+
...rows.map(([label, value]) => `| ${escapeTableCell(label)} | ${escapeTableCell(value)} |`)
|
|
85
|
+
];
|
|
86
|
+
}
|
|
70
87
|
function normalizeMarkdownForWindowsPreview(content) {
|
|
71
88
|
return content.replace(/:\*\*/g, ":**");
|
|
72
89
|
}
|
package/dist/presets.js
CHANGED
|
@@ -16,6 +16,14 @@ const presets = {
|
|
|
16
16
|
agentA: "opencode",
|
|
17
17
|
agentB: "codex"
|
|
18
18
|
},
|
|
19
|
+
"codex-antigravity": {
|
|
20
|
+
agentA: "codex",
|
|
21
|
+
agentB: "antigravity"
|
|
22
|
+
},
|
|
23
|
+
"antigravity-codex": {
|
|
24
|
+
agentA: "antigravity",
|
|
25
|
+
agentB: "codex"
|
|
26
|
+
},
|
|
19
27
|
"claude-opencode": {
|
|
20
28
|
agentA: "claude",
|
|
21
29
|
agentB: "opencode"
|
|
@@ -24,6 +32,14 @@ const presets = {
|
|
|
24
32
|
agentA: "opencode",
|
|
25
33
|
agentB: "claude"
|
|
26
34
|
},
|
|
35
|
+
"claude-antigravity": {
|
|
36
|
+
agentA: "claude",
|
|
37
|
+
agentB: "antigravity"
|
|
38
|
+
},
|
|
39
|
+
"antigravity-claude": {
|
|
40
|
+
agentA: "antigravity",
|
|
41
|
+
agentB: "claude"
|
|
42
|
+
},
|
|
27
43
|
"gemini-opencode": {
|
|
28
44
|
agentA: "gemini",
|
|
29
45
|
agentB: "opencode"
|
|
@@ -32,6 +48,22 @@ const presets = {
|
|
|
32
48
|
agentA: "opencode",
|
|
33
49
|
agentB: "gemini"
|
|
34
50
|
},
|
|
51
|
+
"gemini-antigravity": {
|
|
52
|
+
agentA: "gemini",
|
|
53
|
+
agentB: "antigravity"
|
|
54
|
+
},
|
|
55
|
+
"antigravity-gemini": {
|
|
56
|
+
agentA: "antigravity",
|
|
57
|
+
agentB: "gemini"
|
|
58
|
+
},
|
|
59
|
+
"opencode-antigravity": {
|
|
60
|
+
agentA: "opencode",
|
|
61
|
+
agentB: "antigravity"
|
|
62
|
+
},
|
|
63
|
+
"antigravity-opencode": {
|
|
64
|
+
agentA: "antigravity",
|
|
65
|
+
agentB: "opencode"
|
|
66
|
+
},
|
|
35
67
|
"opencode-ollama": {
|
|
36
68
|
agentA: "opencode",
|
|
37
69
|
agentB: "ollama-local"
|
|
@@ -64,6 +96,14 @@ const presets = {
|
|
|
64
96
|
agentA: "ollama-local",
|
|
65
97
|
agentB: "gemini"
|
|
66
98
|
},
|
|
99
|
+
"antigravity-ollama": {
|
|
100
|
+
agentA: "antigravity",
|
|
101
|
+
agentB: "ollama-local"
|
|
102
|
+
},
|
|
103
|
+
"ollama-antigravity": {
|
|
104
|
+
agentA: "ollama-local",
|
|
105
|
+
agentB: "antigravity"
|
|
106
|
+
},
|
|
67
107
|
"codex-gemini": {
|
|
68
108
|
agentA: "codex",
|
|
69
109
|
agentB: "gemini"
|
|
@@ -159,6 +199,10 @@ function knownCliDetection(agent, discovery) {
|
|
|
159
199
|
return discovery.claude;
|
|
160
200
|
if (command === "gemini")
|
|
161
201
|
return discovery.gemini;
|
|
202
|
+
if (command === "agy")
|
|
203
|
+
return discovery.antigravity;
|
|
204
|
+
if (command === "antigravity")
|
|
205
|
+
return discovery.antigravity;
|
|
162
206
|
if (command === "opencode")
|
|
163
207
|
return discovery.opencode;
|
|
164
208
|
return undefined;
|
|
@@ -27,7 +27,7 @@ class PrettyConsoleRenderer {
|
|
|
27
27
|
}
|
|
28
28
|
/** Affiche l'en-tête du débat (sujet, agents, options). */
|
|
29
29
|
start(options, agents = []) {
|
|
30
|
-
const title = "PALABRE";
|
|
30
|
+
const title = "PALABRE CLI";
|
|
31
31
|
process.stdout.write([
|
|
32
32
|
"",
|
|
33
33
|
this.c("cyan", `┌─ ${title} ${"─".repeat(Math.max(1, 54 - title.length))}`),
|
|
@@ -99,6 +99,10 @@ class PrettyConsoleRenderer {
|
|
|
99
99
|
""
|
|
100
100
|
].join("\n"));
|
|
101
101
|
}
|
|
102
|
+
error(failure) {
|
|
103
|
+
this.thinkingEnd();
|
|
104
|
+
process.stderr.write(`\n${this.c("red", this.messages.common.errorPrefix)} ${formatFailureLocation(failure, this.messages)}: ${failure.message}\n`);
|
|
105
|
+
}
|
|
102
106
|
/** Affiche le chemin du fichier de sortie en vert à la fin du débat. */
|
|
103
107
|
done(outputPath) {
|
|
104
108
|
process.stdout.write(`\n\n${this.c("green", this.messages.renderers.exported(outputPath))}\n\n`);
|
|
@@ -175,6 +179,9 @@ class PlainConsoleRenderer {
|
|
|
175
179
|
summaryStart(agent, role) {
|
|
176
180
|
process.stdout.write(`\n[${this.messages.renderers.summaryTitle}] ${agent} (${role})...\n`);
|
|
177
181
|
}
|
|
182
|
+
error(failure) {
|
|
183
|
+
process.stderr.write(`\n${this.messages.common.errorPrefix}: ${formatFailureLocation(failure, this.messages)}: ${failure.message}\n`);
|
|
184
|
+
}
|
|
178
185
|
/** Affiche le chemin du fichier de sortie à la fin du débat. */
|
|
179
186
|
done(outputPath) {
|
|
180
187
|
process.stdout.write(`\n${this.messages.renderers.exported(outputPath)}\n`);
|
|
@@ -220,6 +227,13 @@ function formatContext(options, messages) {
|
|
|
220
227
|
}
|
|
221
228
|
return messages.renderers.injectedFiles(count);
|
|
222
229
|
}
|
|
230
|
+
function formatFailureLocation(failure, messages) {
|
|
231
|
+
if (failure.phase === "summary") {
|
|
232
|
+
return messages.renderers.summaryTitle;
|
|
233
|
+
}
|
|
234
|
+
const turn = failure.turn === undefined ? "" : `, turn ${failure.turn}`;
|
|
235
|
+
return `${failure.agent ?? "?"} (${failure.role ?? "?"}${turn})`;
|
|
236
|
+
}
|
|
223
237
|
/** Codes d'échappement ANSI utilisés par `PrettyConsoleRenderer`. */
|
|
224
238
|
const codes = {
|
|
225
239
|
reset: "\u001b[0m",
|
|
@@ -228,6 +242,7 @@ const codes = {
|
|
|
228
242
|
cyan: "\u001b[36m",
|
|
229
243
|
green: "\u001b[32m",
|
|
230
244
|
magenta: "\u001b[35m",
|
|
245
|
+
red: "\u001b[31m",
|
|
231
246
|
yellow: "\u001b[33m",
|
|
232
247
|
orange: "\u001b[38;5;208m",
|
|
233
248
|
pink: "\u001b[38;5;205m"
|
package/dist/renderers/ndjson.js
CHANGED
|
@@ -101,6 +101,10 @@ export class NdjsonRenderer {
|
|
|
101
101
|
this.currentRole = role;
|
|
102
102
|
this.emit({ type: "summary-start", agent, role });
|
|
103
103
|
}
|
|
104
|
+
/** Émet une erreur runtime structurée. */
|
|
105
|
+
error(failure) {
|
|
106
|
+
this.emit({ type: "error", ...failure });
|
|
107
|
+
}
|
|
104
108
|
/** Émet `done` avec le chemin du `.debate.md` écrit. */
|
|
105
109
|
done(outputPath) {
|
|
106
110
|
this.emit({ type: "done", outputPath });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palabre",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=20"
|
|
45
45
|
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"node-pty": "^1.1.0"
|
|
48
|
+
},
|
|
46
49
|
"devDependencies": {
|
|
47
50
|
"@types/node": "^20.12.0",
|
|
48
51
|
"typescript": "^5.4.0"
|