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 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/fr/get-started/installation), [Configuration](https://palab.re/fr/get-started/configuration), [First debate](https://palab.re/fr/get-started/first-debate), [CLI reference](https://palab.re/fr/reference/cli), [Troubleshooting](https://palab.re/fr/troubleshooting), [Roadmap](https://palab.re/fr/roadmap).
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
+ }
@@ -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
- child.on("close", (code) => {
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.
@@ -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.",
@@ -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
@@ -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
  };
@@ -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;
@@ -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
- const response = await current.generate({
40
- topic: options.topic,
41
- turn,
42
- totalTurns: options.turns,
43
- selfName: current.name,
44
- peerName: peer.name,
45
- selfRole: current.role,
46
- language: options.language,
47
- session: options.session,
48
- files: options.files,
49
- transcript
50
- }).finally(() => renderer?.thinkingEnd());
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
- const summary = options.summaryEnabled
66
- ? await generateSummary(config, options, transcript, renderer, messages)
67
- : undefined;
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, "&#58;**");
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"
@@ -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.5.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"