palabre 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -15
- package/dist/adapters/cli-pty.js +1 -1
- package/dist/adapters/cli.js +33 -3
- package/dist/adapters/index.js +2 -2
- package/dist/adapters/ollama.js +9 -7
- package/dist/agentRegistry.js +7 -2
- package/dist/args.js +5 -2
- package/dist/config.js +33 -25
- package/dist/context.js +5 -1
- package/dist/discovery.js +30 -9
- package/dist/doctor.js +19 -5
- package/dist/history.js +85 -0
- package/dist/index.js +450 -205
- package/dist/messages/common.js +6 -0
- package/dist/messages/config.js +2 -0
- package/dist/messages/doctor.js +2 -2
- package/dist/messages/help.js +64 -8
- package/dist/messages/init.js +2 -2
- package/dist/messages/tui.js +78 -28
- package/dist/ollamaUrl.js +76 -0
- package/dist/orchestrator.js +34 -13
- package/dist/presets.js +25 -52
- package/dist/renderers/tui.js +242 -77
- package/dist/tuiState.js +32 -0
- package/package.json +1 -1
- package/palabre.config.example.json +0 -17
package/README.md
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
<a href="https://palab.re"><img src="https://img.shields.io/badge/docs-palab.re-18181B?logo=netlify&logoColor=7C3AED" alt="Documentation"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
-

|
|
11
11
|
|
|
12
12
|
[English](#english) | [Français](#français)
|
|
13
13
|
|
|
14
14
|
## English
|
|
15
15
|
|
|
16
|
-
PALABRE is a CLI/TUI orchestrator that lets multiple AI agents installed on your machine work together: Claude Code, Codex CLI,
|
|
16
|
+
PALABRE is a CLI/TUI orchestrator that lets multiple AI agents installed on your machine work together: Claude Code, Codex CLI, Antigravity CLI, OpenCode, Mistral Vibe, and Ollama.
|
|
17
17
|
|
|
18
18
|
It does not replace your tools: it drives them. You keep your subscriptions, default models, terminal habits, and local files. PALABRE can run a debate between two agents or an Ask request where several agents answer independently before a comparative summary. It then exports the session as Markdown.
|
|
19
19
|
|
|
@@ -37,7 +37,6 @@ palabre --help
|
|
|
37
37
|
### Quick Start
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
palabre init
|
|
41
40
|
palabre doctor
|
|
42
41
|
palabre
|
|
43
42
|
```
|
|
@@ -54,16 +53,15 @@ palabre codex-claude "Preview" --context src --show-prompt
|
|
|
54
53
|
palabre context scan src docs --json
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
In an interactive terminal, Palabre uses the TUI by default. `palabre` opens the home screen, `/ask` switches from debate to independent answers, `/agents` and `/roles` help you choose the active setup, and `--terminal` forces the older raw rendering suitable for logs.
|
|
56
|
+
In an interactive terminal, Palabre uses the TUI by default. `palabre` opens the home screen, creates the global config on first launch when needed, and refreshes detected known agents before showing the UI. `/ask` switches from debate to independent answers, `/agents` and `/roles` help you choose the active setup, `/history` shows recent exports, and `/home` returns to the home screen. `--terminal` forces the older raw rendering suitable for logs. `palabre init` remains available for explicit setup, especially with `--local`.
|
|
58
57
|
|
|
59
58
|
### Supported Agents
|
|
60
59
|
|
|
61
60
|
- Claude Code via `claude --print`
|
|
62
61
|
- Codex CLI via `codex exec`
|
|
63
|
-
- Gemini CLI via `gemini --prompt -`
|
|
64
62
|
- Antigravity CLI via `agy --print` in a pseudo-terminal
|
|
65
63
|
- OpenCode via `opencode run`
|
|
66
|
-
- Mistral Vibe via `vibe --output text --
|
|
64
|
+
- Mistral Vibe via `vibe --output text --trust --prompt`
|
|
67
65
|
- Ollama via the local HTTP API
|
|
68
66
|
|
|
69
67
|
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.
|
|
@@ -80,7 +78,7 @@ The NDJSON v1 stream is treated as a public integration API. Compatible addition
|
|
|
80
78
|
|
|
81
79
|
### Skill for AI agents
|
|
82
80
|
|
|
83
|
-
PALABRE ships a ready-to-use skill that teaches an AI agent when and how to run Palabre sessions. It follows the open [agentskills.io](https://agentskills.io) standard, so it is portable across Hermes Agent, Claude, Codex,
|
|
81
|
+
PALABRE ships a ready-to-use skill that teaches an AI agent when and how to run Palabre sessions. It follows the open [agentskills.io](https://agentskills.io) standard, so it is portable across Hermes Agent, Claude, Codex, and any skills-compatible agent.
|
|
84
82
|
|
|
85
83
|
Install it in **Hermes Agent**:
|
|
86
84
|
|
|
@@ -94,7 +92,7 @@ The skill is versioned under [skills/palabre](./skills/palabre).
|
|
|
94
92
|
|
|
95
93
|
### Privacy
|
|
96
94
|
|
|
97
|
-
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,
|
|
95
|
+
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, Antigravity CLI, OpenCode, Mistral Vibe, Ollama, or any custom agent you configure.
|
|
98
96
|
|
|
99
97
|
If an agent fails during the debate or final summary, PALABRE keeps the partial Markdown export with an interruption section whenever possible.
|
|
100
98
|
|
|
@@ -119,9 +117,11 @@ Public roadmap: [docs/guide/fr/roadmap.md](./docs/guide/fr/roadmap.md). Changes:
|
|
|
119
117
|
|
|
120
118
|
MIT. See [LICENSE](./LICENSE).
|
|
121
119
|
|
|
120
|
+

|
|
121
|
+
|
|
122
122
|
## Français
|
|
123
123
|
|
|
124
|
-
PALABRE est un orchestrateur CLI/TUI qui fait travailler plusieurs agents IA installés sur votre machine : Claude Code, Codex CLI,
|
|
124
|
+
PALABRE est un orchestrateur CLI/TUI qui fait travailler plusieurs agents IA installés sur votre machine : Claude Code, Codex CLI, Antigravity CLI, OpenCode, Mistral Vibe et Ollama.
|
|
125
125
|
|
|
126
126
|
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 peut lancer un débat entre deux agents ou une demande Ask où plusieurs agents répondent indépendamment avant une synthèse comparative. Il exporte ensuite la session en Markdown.
|
|
127
127
|
|
|
@@ -145,7 +145,6 @@ palabre --help
|
|
|
145
145
|
### Démarrage rapide
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
|
-
palabre init
|
|
149
148
|
palabre doctor
|
|
150
149
|
palabre
|
|
151
150
|
```
|
|
@@ -162,16 +161,15 @@ palabre codex-claude "Preview" --context src --show-prompt
|
|
|
162
161
|
palabre context scan src docs --json
|
|
163
162
|
```
|
|
164
163
|
|
|
165
|
-
Dans un terminal interactif, Palabre utilise l'interface TUI par défaut. `palabre` ouvre l'accueil, `/ask` passe du débat aux réponses indépendantes, `/agents` et `/roles` aident à choisir la configuration courante, et `--terminal` force l'ancien rendu brut adapté aux logs.
|
|
164
|
+
Dans un terminal interactif, Palabre utilise l'interface TUI par défaut. `palabre` ouvre l'accueil, crée la config globale au premier lancement si nécessaire, et rafraîchit les agents connus détectés avant d'afficher l'interface. `/ask` passe du débat aux réponses indépendantes, `/agents` et `/roles` aident à choisir la configuration courante, `/history` affiche les derniers exports, et `/home` revient à l'accueil. `--terminal` force l'ancien rendu brut adapté aux logs. `palabre init` reste disponible pour un setup explicite, notamment avec `--local`.
|
|
166
165
|
|
|
167
166
|
### Agents supportés
|
|
168
167
|
|
|
169
168
|
- Claude Code via `claude --print`
|
|
170
169
|
- Codex CLI via `codex exec`
|
|
171
|
-
- Gemini CLI via `gemini --prompt -`
|
|
172
170
|
- Antigravity CLI via `agy --print` en pseudo-terminal
|
|
173
171
|
- OpenCode via `opencode run`
|
|
174
|
-
- Mistral Vibe via `vibe --output text --
|
|
172
|
+
- Mistral Vibe via `vibe --output text --trust --prompt`
|
|
175
173
|
- Ollama via l'API locale HTTP
|
|
176
174
|
|
|
177
175
|
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é.
|
|
@@ -188,7 +186,7 @@ Le flux NDJSON v1 est traité comme une API publique d'intégration. Les ajouts
|
|
|
188
186
|
|
|
189
187
|
### Skill pour agents IA
|
|
190
188
|
|
|
191
|
-
PALABRE fournit un skill prêt à l'emploi qui apprend à un agent IA quand et comment lancer des sessions Palabre. Il suit le standard ouvert [agentskills.io](https://agentskills.io) : il est donc portable entre Hermes Agent, Claude, Codex
|
|
189
|
+
PALABRE fournit un skill prêt à l'emploi qui apprend à un agent IA quand et comment lancer des sessions Palabre. Il suit le standard ouvert [agentskills.io](https://agentskills.io) : il est donc portable entre Hermes Agent, Claude, Codex et tout agent compatible skills.
|
|
192
190
|
|
|
193
191
|
Installation dans **Hermes Agent** :
|
|
194
192
|
|
|
@@ -202,7 +200,7 @@ Le skill est versionné dans [skills/palabre](./skills/palabre).
|
|
|
202
200
|
|
|
203
201
|
### Confidentialité
|
|
204
202
|
|
|
205
|
-
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,
|
|
203
|
+
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, Antigravity CLI, OpenCode, Mistral Vibe, Ollama ou de tout autre agent configuré.
|
|
206
204
|
|
|
207
205
|
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.
|
|
208
206
|
|
package/dist/adapters/cli-pty.js
CHANGED
package/dist/adapters/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { formatAgentPrompt } from "../prompt.js";
|
|
|
4
4
|
import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS, withModelArgs } from "./cli-shared.js";
|
|
5
5
|
import { cleanTerminalOutput } from "./terminal.js";
|
|
6
6
|
/**
|
|
7
|
-
* Adapter pour les CLIs batch (Codex, Claude,
|
|
7
|
+
* Adapter pour les CLIs batch (Codex, Claude, OpenCode, Vibe...).
|
|
8
8
|
* Lance un sous-processus, injecte le prompt via stdin ou argument, capture stdout.
|
|
9
9
|
* Garantit : rejection des sorties vides (sauf `allowEmptyOutput`), des timeouts et des exit codes non nuls sans stdout.
|
|
10
10
|
*/
|
|
@@ -47,7 +47,8 @@ export class CliAdapter {
|
|
|
47
47
|
? [...baseArgs, renderedPrompt]
|
|
48
48
|
: baseArgs;
|
|
49
49
|
return new Promise((resolve, reject) => {
|
|
50
|
-
const
|
|
50
|
+
const spawnCommand = shellCommandForSpawn(this.config.command, args, this.config.shell ?? false);
|
|
51
|
+
const child = spawn(spawnCommand.command, spawnCommand.args, {
|
|
51
52
|
stdio: ["pipe", "pipe", "pipe"],
|
|
52
53
|
shell: this.config.shell ?? false
|
|
53
54
|
});
|
|
@@ -147,7 +148,7 @@ export class CliAdapter {
|
|
|
147
148
|
}));
|
|
148
149
|
});
|
|
149
150
|
const finishFromExitCode = (code) => {
|
|
150
|
-
if (code && code !== 0
|
|
151
|
+
if (code && code !== 0) {
|
|
151
152
|
finish(createCliExitError(this.name, code, stderr));
|
|
152
153
|
return;
|
|
153
154
|
}
|
|
@@ -307,6 +308,35 @@ function clipLine(value, maxLength) {
|
|
|
307
308
|
? value
|
|
308
309
|
: `${value.slice(0, maxLength - 1)}…`;
|
|
309
310
|
}
|
|
311
|
+
function shellCommandForSpawn(command, args, shell) {
|
|
312
|
+
if (!shell) {
|
|
313
|
+
return { command, args };
|
|
314
|
+
}
|
|
315
|
+
if (process.platform !== "win32") {
|
|
316
|
+
return {
|
|
317
|
+
command: [command, ...args].map(quotePosixShellArg).join(" "),
|
|
318
|
+
args: []
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
command: [command, ...args].map(quoteWindowsShellArg).join(" "),
|
|
323
|
+
args: []
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function quoteWindowsShellArg(value) {
|
|
327
|
+
if (value.length === 0) {
|
|
328
|
+
return "\"\"";
|
|
329
|
+
}
|
|
330
|
+
return `"${value
|
|
331
|
+
.replace(/(\\*)"/g, "$1$1\\\"")
|
|
332
|
+
.replace(/(\\+)$/g, "$1$1")}"`;
|
|
333
|
+
}
|
|
334
|
+
function quotePosixShellArg(value) {
|
|
335
|
+
if (value.length === 0) {
|
|
336
|
+
return "''";
|
|
337
|
+
}
|
|
338
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
339
|
+
}
|
|
310
340
|
function cancelledError(adapterName) {
|
|
311
341
|
return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
|
|
312
342
|
}
|
package/dist/adapters/index.js
CHANGED
|
@@ -2,13 +2,13 @@ import { CliAdapter } from "./cli.js";
|
|
|
2
2
|
import { CliPtyAdapter } from "./cli-pty.js";
|
|
3
3
|
import { OllamaAdapter } from "./ollama.js";
|
|
4
4
|
/** Factory qui instancie l'adapter approprié selon `config.type`. Exhaustive : tout `AgentConfig` valide produit un adapter. */
|
|
5
|
-
export function createAgent(name, config) {
|
|
5
|
+
export function createAgent(name, config, runtime = {}) {
|
|
6
6
|
switch (config.type) {
|
|
7
7
|
case "cli":
|
|
8
8
|
return new CliAdapter(name, config);
|
|
9
9
|
case "cli-pty":
|
|
10
10
|
return new CliPtyAdapter(name, config);
|
|
11
11
|
case "ollama":
|
|
12
|
-
return new OllamaAdapter(name, config);
|
|
12
|
+
return new OllamaAdapter(name, config, runtime);
|
|
13
13
|
}
|
|
14
14
|
}
|
package/dist/adapters/ollama.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AdapterError } from "../errors.js";
|
|
2
2
|
import { createTranslator } from "../i18n.js";
|
|
3
3
|
import { formatAgentPrompt } from "../prompt.js";
|
|
4
|
+
import { resolveOllamaBaseUrl } from "../ollamaUrl.js";
|
|
4
5
|
/**
|
|
5
6
|
* Adapter pour Ollama via l'API HTTP locale (`POST /api/chat`).
|
|
6
7
|
* N'accède jamais au filesystem : ne voit que le prompt et le transcript fournis par l'orchestrateur.
|
|
@@ -9,11 +10,13 @@ import { formatAgentPrompt } from "../prompt.js";
|
|
|
9
10
|
export class OllamaAdapter {
|
|
10
11
|
name;
|
|
11
12
|
config;
|
|
13
|
+
runtime;
|
|
12
14
|
role;
|
|
13
15
|
contract;
|
|
14
|
-
constructor(name, config) {
|
|
16
|
+
constructor(name, config, runtime = {}) {
|
|
15
17
|
this.name = name;
|
|
16
18
|
this.config = config;
|
|
19
|
+
this.runtime = runtime;
|
|
17
20
|
this.role = config.role;
|
|
18
21
|
this.contract = {
|
|
19
22
|
name,
|
|
@@ -38,7 +41,10 @@ export class OllamaAdapter {
|
|
|
38
41
|
if (prompt.signal?.aborted) {
|
|
39
42
|
throw cancelledError(this.name);
|
|
40
43
|
}
|
|
41
|
-
const baseUrl =
|
|
44
|
+
const baseUrl = resolveOllamaBaseUrl({
|
|
45
|
+
cliUrl: this.runtime.ollamaUrl,
|
|
46
|
+
configUrl: this.config.baseUrl
|
|
47
|
+
});
|
|
42
48
|
if (this.config.validateModel !== false) {
|
|
43
49
|
await this.ensureModelAvailable(baseUrl);
|
|
44
50
|
}
|
|
@@ -116,7 +122,7 @@ export class OllamaAdapter {
|
|
|
116
122
|
throw new AdapterError("model-unavailable", this.name, `Modele Ollama indisponible: ${this.config.model}. Modeles detectes: ${models.join(", ") || "aucun"}. ` +
|
|
117
123
|
"Utilise --pull-models ou autoPullModel: true pour autoriser le telechargement.", { model: this.config.model, availableModels: models });
|
|
118
124
|
}
|
|
119
|
-
process.
|
|
125
|
+
process.stderr.write(`\n[ollama] Modele absent, telechargement: ${this.config.model}\n`);
|
|
120
126
|
await this.pullModel(baseUrl);
|
|
121
127
|
if (!(await this.isModelAvailable(baseUrl))) {
|
|
122
128
|
throw new AdapterError("model-pull-failed", this.name, `Le modele Ollama ${this.config.model} reste indisponible apres telechargement.`);
|
|
@@ -225,10 +231,6 @@ async function unloadModel(baseUrl, model, signal) {
|
|
|
225
231
|
});
|
|
226
232
|
}
|
|
227
233
|
}
|
|
228
|
-
/** Supprime le slash final de `baseUrl` pour éviter les doubles slashs dans les URLs construites. */
|
|
229
|
-
function normalizeBaseUrl(baseUrl) {
|
|
230
|
-
return baseUrl.replace(/\/$/, "");
|
|
231
|
-
}
|
|
232
234
|
function cancelledError(adapterName) {
|
|
233
235
|
return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
|
|
234
236
|
}
|
package/dist/agentRegistry.js
CHANGED
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
const KNOWN_CLI_AGENTS = [
|
|
7
7
|
{ configKey: "codex", commandAliases: ["codex"], discoveryKey: "codex" },
|
|
8
8
|
{ configKey: "claude", commandAliases: ["claude"], discoveryKey: "claude" },
|
|
9
|
-
{ configKey: "gemini", commandAliases: ["gemini"], discoveryKey: "gemini" },
|
|
10
9
|
{ configKey: "antigravity", commandAliases: ["agy", "antigravity"], discoveryKey: "antigravity" },
|
|
11
10
|
{ configKey: "opencode", commandAliases: ["opencode"], discoveryKey: "opencode" },
|
|
12
11
|
{ configKey: "vibe", commandAliases: ["vibe"], discoveryKey: "vibe" }
|
|
13
12
|
];
|
|
13
|
+
/** Agents retirés conservés uniquement pour lire les anciennes configurations. */
|
|
14
|
+
const RETIRED_AGENT_NAMES = new Set(["gemini"]);
|
|
15
|
+
/** Indique qu'un nom d'agent ne doit plus être proposé ni exposé aux intégrations. */
|
|
16
|
+
export function isRetiredAgentName(name) {
|
|
17
|
+
return RETIRED_AGENT_NAMES.has(name.toLowerCase());
|
|
18
|
+
}
|
|
14
19
|
/** Clé de config de l'agent Ollama local par défaut. */
|
|
15
20
|
export const OLLAMA_AGENT_KEY = "ollama-local";
|
|
16
21
|
/**
|
|
@@ -37,7 +42,7 @@ export function detectionForCommand(command, discovery) {
|
|
|
37
42
|
}
|
|
38
43
|
/**
|
|
39
44
|
* Liste les clés d'agents connus effectivement détectés localement, dans
|
|
40
|
-
* l'ordre canonique (`codex`, `claude`, `
|
|
45
|
+
* l'ordre canonique (`codex`, `claude`, `antigravity`, `opencode`, `vibe`,
|
|
41
46
|
* puis `ollama-local`).
|
|
42
47
|
*/
|
|
43
48
|
export function detectedAgentNames(discovery) {
|
package/dist/args.js
CHANGED
|
@@ -35,6 +35,7 @@ const FLAG_SPECS = {
|
|
|
35
35
|
language: { arity: "single" },
|
|
36
36
|
"model-a": { arity: "single" },
|
|
37
37
|
"model-b": { arity: "single" },
|
|
38
|
+
"ollama-url": { arity: "single" },
|
|
38
39
|
mode: { arity: "single" },
|
|
39
40
|
"set-ollama-model": { arity: "single" },
|
|
40
41
|
preset: { arity: "single" },
|
|
@@ -45,8 +46,8 @@ const FLAG_SPECS = {
|
|
|
45
46
|
turns: { arity: "single" },
|
|
46
47
|
renderer: { arity: "single" },
|
|
47
48
|
// Valeurs multiples.
|
|
48
|
-
agents: { arity: "multi"
|
|
49
|
-
"ask-agents": { arity: "multi"
|
|
49
|
+
agents: { arity: "multi" },
|
|
50
|
+
"ask-agents": { arity: "multi" },
|
|
50
51
|
"set-defaults": { arity: "multi", max: 2 },
|
|
51
52
|
files: { arity: "multi" },
|
|
52
53
|
context: { arity: "multi" }
|
|
@@ -67,6 +68,8 @@ const COMMANDS = new Set([
|
|
|
67
68
|
"agents",
|
|
68
69
|
"preset",
|
|
69
70
|
"presets",
|
|
71
|
+
"history",
|
|
72
|
+
"historique",
|
|
70
73
|
"context"
|
|
71
74
|
]);
|
|
72
75
|
/**
|
package/dist/config.js
CHANGED
|
@@ -54,23 +54,6 @@ export const exampleConfig = {
|
|
|
54
54
|
role: "reviewer",
|
|
55
55
|
tier: "primary"
|
|
56
56
|
},
|
|
57
|
-
gemini: {
|
|
58
|
-
type: "cli",
|
|
59
|
-
command: "gemini",
|
|
60
|
-
args: [
|
|
61
|
-
"--output-format",
|
|
62
|
-
"text",
|
|
63
|
-
"--approval-mode",
|
|
64
|
-
"plan",
|
|
65
|
-
"--skip-trust",
|
|
66
|
-
"--prompt",
|
|
67
|
-
"-"
|
|
68
|
-
],
|
|
69
|
-
promptMode: "stdin",
|
|
70
|
-
shell: process.platform === "win32",
|
|
71
|
-
role: "reviewer",
|
|
72
|
-
tier: "primary"
|
|
73
|
-
},
|
|
74
57
|
antigravity: {
|
|
75
58
|
type: "cli-pty",
|
|
76
59
|
command: "agy",
|
|
@@ -102,8 +85,6 @@ export const exampleConfig = {
|
|
|
102
85
|
args: [
|
|
103
86
|
"--output",
|
|
104
87
|
"text",
|
|
105
|
-
"--agent",
|
|
106
|
-
"plan",
|
|
107
88
|
"--trust",
|
|
108
89
|
"--prompt"
|
|
109
90
|
],
|
|
@@ -234,11 +215,34 @@ export function syncDetectedAgents(config, discovery) {
|
|
|
234
215
|
const discoveredConfig = createConfigFromDiscovery(discovery);
|
|
235
216
|
const missingAgents = detectedAgentNames(discovery).filter((agentName) => !config.agents[agentName]);
|
|
236
217
|
applyDetectedCommands(config, discovery);
|
|
218
|
+
migrateKnownAgentDefaults(config);
|
|
237
219
|
for (const agentName of missingAgents) {
|
|
238
220
|
config.agents[agentName] = discoveredConfig.agents[agentName];
|
|
239
221
|
}
|
|
240
222
|
return missingAgents;
|
|
241
223
|
}
|
|
224
|
+
function migrateKnownAgentDefaults(config) {
|
|
225
|
+
migrateVibePlanAgent(config);
|
|
226
|
+
}
|
|
227
|
+
function migrateVibePlanAgent(config) {
|
|
228
|
+
const agent = config.agents.vibe;
|
|
229
|
+
if (agent?.type !== "cli" || !isLegacyVibePlanArgs(agent.args)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
agent.args = ["--output", "text", "--trust", "--prompt"];
|
|
233
|
+
}
|
|
234
|
+
function isLegacyVibePlanArgs(args) {
|
|
235
|
+
return JSON.stringify(args) === JSON.stringify(["--output", "text", "--agent", "plan", "--trust", "--prompt"]);
|
|
236
|
+
}
|
|
237
|
+
export function syncDetectedAgentsDetailed(config, discovery) {
|
|
238
|
+
const before = JSON.stringify(config.agents);
|
|
239
|
+
const addedAgents = syncDetectedAgents(config, discovery);
|
|
240
|
+
const changed = JSON.stringify(config.agents) !== before;
|
|
241
|
+
return {
|
|
242
|
+
addedAgents,
|
|
243
|
+
changed
|
|
244
|
+
};
|
|
245
|
+
}
|
|
242
246
|
export function syncOllamaModel(config, discovery) {
|
|
243
247
|
const agent = config.agents["ollama-local"];
|
|
244
248
|
if (agent?.type !== "ollama" || discovery.ollama.models.length === 0) {
|
|
@@ -266,6 +270,14 @@ export function setOllamaModel(config, model) {
|
|
|
266
270
|
nextModel: agent.model
|
|
267
271
|
};
|
|
268
272
|
}
|
|
273
|
+
/** Met à jour l'adresse persistante de tous les agents Ollama configurés. */
|
|
274
|
+
export function setOllamaBaseUrl(config, baseUrl) {
|
|
275
|
+
const agents = Object.values(config.agents).filter((agent) => agent.type === "ollama");
|
|
276
|
+
for (const agent of agents) {
|
|
277
|
+
agent.baseUrl = baseUrl;
|
|
278
|
+
}
|
|
279
|
+
return agents.length;
|
|
280
|
+
}
|
|
269
281
|
/** Écrit `config` sérialisé en JSON dans `configPath`. Crée le répertoire parent si nécessaire. */
|
|
270
282
|
export async function writeExampleConfig(configPath = DEFAULT_CONFIG_PATH, config = exampleConfig) {
|
|
271
283
|
const resolved = path.resolve(configPath);
|
|
@@ -279,7 +291,7 @@ function chooseDefaultOllamaModel(discovery) {
|
|
|
279
291
|
return discovery.ollama.models[0] ?? DEFAULT_OLLAMA_MODEL;
|
|
280
292
|
}
|
|
281
293
|
function chooseDefaultSummaryAgent(pair) {
|
|
282
|
-
for (const preferred of ["claude", "codex", "antigravity", "vibe"
|
|
294
|
+
for (const preferred of ["claude", "codex", "antigravity", "vibe"]) {
|
|
283
295
|
if (pair.includes(preferred)) {
|
|
284
296
|
return preferred;
|
|
285
297
|
}
|
|
@@ -305,16 +317,12 @@ function chooseDefaultPair(discovery) {
|
|
|
305
317
|
if (discovery.antigravity.available && discovery.ollama.available) {
|
|
306
318
|
return ["antigravity", "ollama-local"];
|
|
307
319
|
}
|
|
308
|
-
if (discovery.gemini.available && discovery.ollama.available) {
|
|
309
|
-
return ["gemini", "ollama-local"];
|
|
310
|
-
}
|
|
311
320
|
const cliAgents = [
|
|
312
321
|
discovery.codex.available ? "codex" : undefined,
|
|
313
322
|
discovery.claude.available ? "claude" : undefined,
|
|
314
323
|
discovery.antigravity.available ? "antigravity" : undefined,
|
|
315
324
|
discovery.opencode.available ? "opencode" : undefined,
|
|
316
|
-
discovery.vibe.available ? "vibe" : undefined
|
|
317
|
-
discovery.gemini.available ? "gemini" : undefined
|
|
325
|
+
discovery.vibe.available ? "vibe" : undefined
|
|
318
326
|
].filter((agent) => Boolean(agent));
|
|
319
327
|
if (cliAgents.length >= 2) {
|
|
320
328
|
return [cliAgents[0], cliAgents[1]];
|
package/dist/context.js
CHANGED
|
@@ -80,7 +80,11 @@ async function addContextPaths(paths, cwd, state) {
|
|
|
80
80
|
const uniquePaths = [...new Set(paths.map((item) => item.trim()).filter(Boolean))];
|
|
81
81
|
for (const inputPath of uniquePaths) {
|
|
82
82
|
const absolutePath = path.resolve(cwd, inputPath);
|
|
83
|
-
const fileStat = await stat(absolutePath);
|
|
83
|
+
const fileStat = await stat(absolutePath).catch(() => undefined);
|
|
84
|
+
if (!fileStat) {
|
|
85
|
+
state.warnings.push(state.messages.context.ignoredNotFileOrDirectory(inputPath));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
84
88
|
if (fileStat.isFile()) {
|
|
85
89
|
await addContextFile(absolutePath, cwd, state);
|
|
86
90
|
continue;
|
package/dist/discovery.js
CHANGED
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { executableExtensions } from "./exec.js";
|
|
4
|
+
import { resolveOllamaBaseUrl } from "./ollamaUrl.js";
|
|
4
5
|
/**
|
|
5
6
|
* Détecte en parallèle toutes les CLIs supportées et le serveur Ollama local.
|
|
6
7
|
* Sur Windows, tente `claude.exe` avant `claude`.
|
|
7
8
|
* Antigravity est exposé selon les installations sous `agy` ou `antigravity`.
|
|
8
9
|
*/
|
|
9
|
-
export async function discoverLocalTools() {
|
|
10
|
-
const [codex, claude,
|
|
10
|
+
export async function discoverLocalTools(options = {}) {
|
|
11
|
+
const [codex, claude, antigravity, opencode, vibe, ollamaCommand] = await Promise.all([
|
|
11
12
|
detectCommand("codex"),
|
|
12
13
|
detectFirstCommand(process.platform === "win32" ? ["claude.exe", "claude"] : ["claude"]),
|
|
13
|
-
detectCommand("gemini"),
|
|
14
14
|
detectFirstCommand(["agy", "antigravity"]),
|
|
15
15
|
detectCommand("opencode"),
|
|
16
16
|
detectCommand("vibe"),
|
|
17
17
|
detectCommand("ollama")
|
|
18
18
|
]);
|
|
19
|
-
const
|
|
19
|
+
const configuredTargets = Object.entries(options.ollamaTargets ?? {});
|
|
20
|
+
const targets = configuredTargets.length > 0
|
|
21
|
+
? configuredTargets
|
|
22
|
+
: [["ollama-local", options.ollamaConfigUrl]];
|
|
23
|
+
const resolvedTargets = targets.map(([name, configUrl]) => ({
|
|
24
|
+
name,
|
|
25
|
+
baseUrl: resolveOllamaBaseUrl({
|
|
26
|
+
cliUrl: options.ollamaUrl,
|
|
27
|
+
configUrl
|
|
28
|
+
})
|
|
29
|
+
}));
|
|
30
|
+
const uniqueUrls = resolvedTargets
|
|
31
|
+
.map((target) => target.baseUrl)
|
|
32
|
+
.filter((baseUrl, index, urls) => urls.indexOf(baseUrl) === index);
|
|
33
|
+
const servers = await Promise.all(uniqueUrls.map(async (baseUrl) => [
|
|
34
|
+
baseUrl,
|
|
35
|
+
await detectOllamaServer(baseUrl)
|
|
36
|
+
]));
|
|
37
|
+
const serversByUrl = new Map(servers);
|
|
38
|
+
const ollamaAgents = Object.fromEntries(resolvedTargets.map(({ name, baseUrl }) => [name, {
|
|
39
|
+
...serversByUrl.get(baseUrl),
|
|
40
|
+
commandAvailable: ollamaCommand.available
|
|
41
|
+
}]));
|
|
42
|
+
const primaryName = ollamaAgents["ollama-local"] ? "ollama-local" : resolvedTargets[0].name;
|
|
43
|
+
const ollamaServer = ollamaAgents[primaryName];
|
|
20
44
|
return {
|
|
21
45
|
codex,
|
|
22
46
|
claude,
|
|
23
|
-
gemini,
|
|
24
47
|
antigravity,
|
|
25
48
|
opencode,
|
|
26
49
|
vibe,
|
|
27
|
-
ollama:
|
|
28
|
-
|
|
29
|
-
commandAvailable: ollamaCommand.available
|
|
30
|
-
}
|
|
50
|
+
ollama: ollamaServer,
|
|
51
|
+
ollamaAgents
|
|
31
52
|
};
|
|
32
53
|
}
|
|
33
54
|
async function detectFirstCommand(commands) {
|
package/dist/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
|
|
|
5
5
|
import { discoverLocalTools } from "./discovery.js";
|
|
6
6
|
import { createTranslator, resolveLanguage } from "./i18n.js";
|
|
7
7
|
import { DEFAULT_TURNS, MAX_TURNS } from "./limits.js";
|
|
8
|
+
import { configuredOllamaTargets, normalizeOllamaBaseUrl } from "./ollamaUrl.js";
|
|
8
9
|
import { compareSemver, getLatestPackageVersion, getPackageVersion } from "./version.js";
|
|
9
10
|
/**
|
|
10
11
|
* Exécute le diagnostic complet : config, outils locaux et agents.
|
|
@@ -39,11 +40,14 @@ export async function runDoctor(explicitConfigPath, plain = false, explicitLangu
|
|
|
39
40
|
lines.push(ok(t.doctor.configReadable));
|
|
40
41
|
lines.push(ok(t.doctor.interfaceLanguage(language)));
|
|
41
42
|
await inspectConfig(config, lines, t);
|
|
42
|
-
const
|
|
43
|
+
const ollamaTargets = Object.fromEntries(Object.entries(configuredOllamaTargets(config))
|
|
44
|
+
.map(([name, value]) => [name, value && isValidOllamaBaseUrl(value) ? value : undefined]));
|
|
45
|
+
const discovery = await discoverLocalTools({
|
|
46
|
+
ollamaTargets
|
|
47
|
+
});
|
|
43
48
|
lines.push(info(t.doctor.localTools, "tools"));
|
|
44
49
|
lines.push(formatCommand("Codex CLI", discovery.codex.available, discovery.codex.command, discovery.codex.path, t));
|
|
45
50
|
lines.push(formatCommand("Claude CLI", discovery.claude.available, discovery.claude.command, discovery.claude.path, t));
|
|
46
|
-
lines.push(formatCommand("Gemini CLI", discovery.gemini.available, discovery.gemini.command, discovery.gemini.path, t));
|
|
47
51
|
lines.push(formatCommand("Antigravity CLI", discovery.antigravity.available, discovery.antigravity.command, discovery.antigravity.path, t));
|
|
48
52
|
lines.push(formatCommand("OpenCode CLI", discovery.opencode.available, discovery.opencode.command, discovery.opencode.path, t));
|
|
49
53
|
lines.push(formatCommand("Mistral Vibe CLI", discovery.vibe.available, discovery.vibe.command, discovery.vibe.path, t));
|
|
@@ -205,13 +209,22 @@ function inspectAgentShape(name, agent, lines, t) {
|
|
|
205
209
|
if (!agent.model || !agent.model.trim()) {
|
|
206
210
|
lines.push(error(t.doctor.ollamaModelMissing(name)));
|
|
207
211
|
}
|
|
208
|
-
if (agent.baseUrl &&
|
|
212
|
+
if (agent.baseUrl && !isValidOllamaBaseUrl(agent.baseUrl)) {
|
|
209
213
|
lines.push(error(t.doctor.ollamaBaseUrlInvalid(name, agent.baseUrl)));
|
|
210
214
|
}
|
|
211
215
|
if (agent.timeoutMs !== undefined && (!Number.isFinite(agent.timeoutMs) || agent.timeoutMs <= 0)) {
|
|
212
216
|
lines.push(error(t.doctor.positiveTimeout(name, "timeoutMs")));
|
|
213
217
|
}
|
|
214
218
|
}
|
|
219
|
+
function isValidOllamaBaseUrl(value) {
|
|
220
|
+
try {
|
|
221
|
+
normalizeOllamaBaseUrl(value);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
215
228
|
function inspectCliAgent(name, agent, discovery, lines, t) {
|
|
216
229
|
const known = detectionForCommand(agent.command, discovery);
|
|
217
230
|
const prefix = `${name} [cli:${agent.role}] command=${agent.command}`;
|
|
@@ -225,7 +238,8 @@ function inspectCliAgent(name, agent, discovery, lines, t) {
|
|
|
225
238
|
}
|
|
226
239
|
function inspectOllamaAgent(name, agent, discovery, lines, t) {
|
|
227
240
|
const prefix = `${name} [ollama:${agent.role}] model=${agent.model}`;
|
|
228
|
-
|
|
241
|
+
const ollama = discovery.ollamaAgents?.[name] ?? discovery.ollama;
|
|
242
|
+
if (!ollama.available) {
|
|
229
243
|
lines.push(warn(t.doctor.ollamaNotVerifiable(prefix)));
|
|
230
244
|
return;
|
|
231
245
|
}
|
|
@@ -233,7 +247,7 @@ function inspectOllamaAgent(name, agent, discovery, lines, t) {
|
|
|
233
247
|
lines.push(info(t.doctor.ollamaValidateFalse(prefix)));
|
|
234
248
|
return;
|
|
235
249
|
}
|
|
236
|
-
const installed =
|
|
250
|
+
const installed = ollama.models.includes(agent.model);
|
|
237
251
|
lines.push(installed
|
|
238
252
|
? ok(t.doctor.ollamaInstalled(prefix))
|
|
239
253
|
: warn(t.doctor.ollamaMissing(prefix, agent.model)));
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const maxHeaderBytes = 12_000;
|
|
4
|
+
export async function listHistoryEntries(outputDir, limit = 10) {
|
|
5
|
+
const resolved = path.resolve(outputDir);
|
|
6
|
+
let entries;
|
|
7
|
+
try {
|
|
8
|
+
entries = await readdir(resolved, { withFileTypes: true });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const markdownFiles = entries
|
|
14
|
+
.filter((entry) => entry.isFile() && /\.(debate|ask)\.md$/i.test(entry.name))
|
|
15
|
+
.map((entry) => path.join(resolved, entry.name));
|
|
16
|
+
const history = await Promise.all(markdownFiles.map(readHistoryFile));
|
|
17
|
+
return history
|
|
18
|
+
.filter((entry) => Boolean(entry))
|
|
19
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
20
|
+
.slice(0, limit);
|
|
21
|
+
}
|
|
22
|
+
async function readHistoryFile(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
const [metadata, raw] = await Promise.all([
|
|
25
|
+
stat(filePath),
|
|
26
|
+
readFile(filePath, "utf8")
|
|
27
|
+
]);
|
|
28
|
+
const header = raw.slice(0, maxHeaderBytes);
|
|
29
|
+
const table = parseMetadataTable(header);
|
|
30
|
+
const fileName = path.basename(filePath);
|
|
31
|
+
const mode = fileName.endsWith(".ask.md") ? "ask" : "debate";
|
|
32
|
+
return {
|
|
33
|
+
fileName,
|
|
34
|
+
path: filePath,
|
|
35
|
+
mode,
|
|
36
|
+
topic: table.Sujet ?? table.Subject ?? topicFromFileName(fileName),
|
|
37
|
+
agents: table.Agents ?? "",
|
|
38
|
+
date: table["Date locale"] ?? table["Local date"] ?? table["Session demarree a"] ?? table["Session started at"] ?? "",
|
|
39
|
+
count: countFromTable(mode, table),
|
|
40
|
+
mtimeMs: metadata.mtimeMs
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function countFromTable(mode, table) {
|
|
48
|
+
if (mode === "ask") {
|
|
49
|
+
const received = table["Reponses recues"] ?? table["Received responses"];
|
|
50
|
+
const requested = table["Reponses attendues"] ?? table["Expected responses"];
|
|
51
|
+
return received && requested ? `${received}/${requested}` : received ?? requested ?? "";
|
|
52
|
+
}
|
|
53
|
+
const played = table["Tours joues"] ?? table["Played turns"];
|
|
54
|
+
const requested = table["Tours demandes"] ?? table["Requested turns"];
|
|
55
|
+
return played && requested ? `${played}/${requested}` : played ?? requested ?? "";
|
|
56
|
+
}
|
|
57
|
+
function parseMetadataTable(markdown) {
|
|
58
|
+
const fields = {};
|
|
59
|
+
const lines = markdown.split(/\r?\n/);
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const match = line.match(/^\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|$/);
|
|
62
|
+
if (!match)
|
|
63
|
+
continue;
|
|
64
|
+
const key = stripMarkdown(match[1] ?? "");
|
|
65
|
+
const value = stripMarkdown(match[2] ?? "");
|
|
66
|
+
if (!key || key === "Champ" || key === "Field" || /^-+$/.test(key))
|
|
67
|
+
continue;
|
|
68
|
+
fields[key] = value;
|
|
69
|
+
}
|
|
70
|
+
return fields;
|
|
71
|
+
}
|
|
72
|
+
function stripMarkdown(value) {
|
|
73
|
+
return value
|
|
74
|
+
.replace(/\*\*/g, "")
|
|
75
|
+
.replace(/`/g, "")
|
|
76
|
+
.trim();
|
|
77
|
+
}
|
|
78
|
+
function topicFromFileName(fileName) {
|
|
79
|
+
return fileName
|
|
80
|
+
.replace(/^palabre-/, "")
|
|
81
|
+
.replace(/\.(debate|ask)\.md$/i, "")
|
|
82
|
+
.replace(/-\d{4}-\d{2}-\d{2}t.*$/i, "")
|
|
83
|
+
.replace(/-/g, " ")
|
|
84
|
+
.trim() || fileName;
|
|
85
|
+
}
|