palabre 0.6.0 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { applyDetectedCommands } from "./agentRegistry.js";
4
5
  export const DEFAULT_CONFIG_PATH = "palabre.config.json";
5
6
  export const LEGACY_CONFIG_PATH = "chicane.config.json";
6
7
  export const CONFIG_DIR_NAME = ".palabre";
@@ -118,6 +119,30 @@ export async function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
118
119
  const raw = await readFile(resolved, "utf8");
119
120
  return JSON.parse(raw);
120
121
  }
122
+ /**
123
+ * Valide qu'une config chargée est exploitable pour lancer un débat.
124
+ *
125
+ * `loadConfig` se contente de parser le JSON ; cette garde attrape les configs
126
+ * structurellement cassées (racine non-objet, bloc `agents` absent ou vide)
127
+ * avant qu'elles ne provoquent un `TypeError` opaque dans l'orchestrateur.
128
+ * Volontairement minimale : la validation sémantique fine (agents par défaut
129
+ * inconnus, timeouts invalides, etc.) reste du ressort de `palabre doctor`.
130
+ *
131
+ * @throws {Error} message actionnable si la config ne peut pas faire tourner un débat.
132
+ */
133
+ export function assertRunnableConfig(config, messages, configPath = DEFAULT_CONFIG_PATH) {
134
+ const root = config;
135
+ if (!root || typeof root !== "object" || Array.isArray(root)) {
136
+ throw new Error(messages.common.configInvalidShape(configPath));
137
+ }
138
+ const agents = root.agents;
139
+ if (!agents || typeof agents !== "object" || Array.isArray(agents)) {
140
+ throw new Error(messages.common.configMissingAgents(configPath));
141
+ }
142
+ if (Object.keys(agents).length === 0) {
143
+ throw new Error(messages.common.configEmptyAgents(configPath));
144
+ }
145
+ }
121
146
  /** Retourne `true` si le fichier de config est accessible en lecture. Silencieux sur toute erreur filesystem. */
122
147
  export async function configExists(configPath = DEFAULT_CONFIG_PATH) {
123
148
  try {
@@ -156,26 +181,7 @@ export async function resolveDefaultConfigPath() {
156
181
  export function createConfigFromDiscovery(discovery) {
157
182
  const config = cloneConfig(exampleConfig);
158
183
  const pair = chooseDefaultPair(discovery);
159
- config.agents.codex = {
160
- ...config.agents.codex,
161
- ...(discovery.codex.available ? { command: discovery.codex.command } : {})
162
- };
163
- config.agents.claude = {
164
- ...config.agents.claude,
165
- ...(discovery.claude.available ? { command: discovery.claude.command } : {})
166
- };
167
- config.agents.gemini = {
168
- ...config.agents.gemini,
169
- ...(discovery.gemini.available ? { command: discovery.gemini.command } : {})
170
- };
171
- config.agents.antigravity = {
172
- ...config.agents.antigravity,
173
- ...(discovery.antigravity.available ? { command: discovery.antigravity.command } : {})
174
- };
175
- config.agents.opencode = {
176
- ...config.agents.opencode,
177
- ...(discovery.opencode.available ? { command: discovery.opencode.command } : {})
178
- };
184
+ applyDetectedCommands(config, discovery);
179
185
  const ollamaAgent = config.agents["ollama-local"];
180
186
  if (ollamaAgent?.type === "ollama") {
181
187
  ollamaAgent.model = chooseDefaultOllamaModel(discovery);
@@ -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
@@ -1,5 +1,6 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { executableExtensions } from "./exec.js";
3
4
  /**
4
5
  * Détecte en parallèle toutes les CLIs supportées et le serveur Ollama local.
5
6
  * Sur Windows, tente `claude.exe` avant `claude`.
@@ -100,18 +101,6 @@ async function findExecutable(command) {
100
101
  }
101
102
  return undefined;
102
103
  }
103
- function executableExtensions(command) {
104
- if (path.extname(command)) {
105
- return [""];
106
- }
107
- if (process.platform !== "win32") {
108
- return [""];
109
- }
110
- return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
111
- .split(";")
112
- .map((extension) => extension.toLowerCase())
113
- .concat(".ps1", "");
114
- }
115
104
  async function isAccessible(filePath) {
116
105
  try {
117
106
  await access(filePath);
package/dist/doctor.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { stat } from "node:fs/promises";
3
3
  import { configExists, loadConfig, resolveDefaultConfigPath, resolveOutputDir } from "./config.js";
4
+ import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
4
5
  import { discoverLocalTools } from "./discovery.js";
5
6
  import { createTranslator, resolveLanguage } from "./i18n.js";
6
7
  import { DEFAULT_TURNS, MAX_TURNS } from "./limits.js";
@@ -194,7 +195,7 @@ function inspectAgentShape(name, agent, lines, t) {
194
195
  }
195
196
  }
196
197
  function inspectCliAgent(name, agent, discovery, lines, t) {
197
- const known = knownCliDetection(agent.command, discovery);
198
+ const known = detectionForCommand(agent.command, discovery);
198
199
  const prefix = `${name} [cli:${agent.role}] command=${agent.command}`;
199
200
  if (!known) {
200
201
  lines.push(info(t.doctor.customCommand(prefix)));
@@ -219,37 +220,11 @@ function inspectOllamaAgent(name, agent, discovery, lines, t) {
219
220
  ? ok(t.doctor.ollamaInstalled(prefix))
220
221
  : warn(t.doctor.ollamaMissing(prefix, agent.model)));
221
222
  }
222
- function detectedAgentNames(discovery) {
223
- return [
224
- discovery.codex.available ? "codex" : undefined,
225
- discovery.claude.available ? "claude" : undefined,
226
- discovery.gemini.available ? "gemini" : undefined,
227
- discovery.antigravity.available ? "antigravity" : undefined,
228
- discovery.opencode.available ? "opencode" : undefined,
229
- discovery.ollama.available ? "ollama-local" : undefined
230
- ].filter((name) => Boolean(name));
231
- }
232
223
  function formatCommand(label, available, command, resolvedPath, t) {
233
224
  return available
234
225
  ? ok(t.doctor.commandDetected(label, resolvedPath ?? command))
235
226
  : warn(t.doctor.commandMissing(label));
236
227
  }
237
- function knownCliDetection(command, discovery) {
238
- const normalized = path.basename(command).toLowerCase().replace(/\.(exe|cmd|bat)$/i, "");
239
- if (normalized === "codex")
240
- return discovery.codex;
241
- if (normalized === "claude")
242
- return discovery.claude;
243
- if (normalized === "gemini")
244
- return discovery.gemini;
245
- if (normalized === "agy")
246
- return discovery.antigravity;
247
- if (normalized === "antigravity")
248
- return discovery.antigravity;
249
- if (normalized === "opencode")
250
- return discovery.opencode;
251
- return undefined;
252
- }
253
228
  function render(lines, plain, t) {
254
229
  const hasErrors = lines.some((line) => line.level === "error");
255
230
  return {
package/dist/exec.js ADDED
@@ -0,0 +1,17 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Extensions exécutables candidates pour résoudre une commande dans le PATH.
4
+ *
5
+ * Retourne `[""]` quand la commande porte déjà une extension ou hors Windows.
6
+ * Sur Windows sans extension, dérive la liste de `PATHEXT` et ajoute `.ps1`
7
+ * ainsi que la candidate vide (binaire sans extension).
8
+ */
9
+ export function executableExtensions(command) {
10
+ if (path.extname(command) || process.platform !== "win32") {
11
+ return [""];
12
+ }
13
+ return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
14
+ .split(";")
15
+ .map((extension) => extension.toLowerCase())
16
+ .concat(".ps1", "");
17
+ }
package/dist/index.js CHANGED
@@ -2,8 +2,9 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
5
+ import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
6
6
  import { loadProjectInputs } from "./context.js";
7
+ 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";
@@ -19,6 +20,8 @@ import { runDebate } from "./orchestrator.js";
19
20
  import { writeDebateMarkdown } from "./output.js";
20
21
  import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./update.js";
21
22
  import { createSessionContext } from "./session.js";
23
+ import { getStringListFlag, parseArgs } from "./args.js";
24
+ import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
22
25
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
23
26
  async function main() {
24
27
  const rawArgs = process.argv.slice(2);
@@ -51,6 +54,10 @@ async function main() {
51
54
  await runPresetsCommand(parsed.flags);
52
55
  return;
53
56
  }
57
+ if (parsed.command === "context") {
58
+ await runContextCommand(parsed.flags, parsed.positionals);
59
+ return;
60
+ }
54
61
  if (parsed.command === "update") {
55
62
  const info = await getUpdateInfo(await getPackageVersion());
56
63
  const updateConfigPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
@@ -106,6 +113,7 @@ async function main() {
106
113
  configLanguage: config.language
107
114
  });
108
115
  const messages = createTranslator(language);
116
+ assertRunnableConfig(config, messages, configPath);
109
117
  if (parsed.command === "new") {
110
118
  const selection = await runNewWizard(config, messages);
111
119
  if (!selection) {
@@ -168,8 +176,11 @@ async function main() {
168
176
  const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, messages);
169
177
  context.warnings.forEach((warning) => renderer.warning(warning));
170
178
  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);
179
+ const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages, result.failure);
172
180
  renderer.done(outputPath);
181
+ if (result.failure) {
182
+ process.exitCode = 1;
183
+ }
173
184
  }
174
185
  /**
175
186
  * Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
@@ -436,213 +447,30 @@ async function runPresetsCommand(flags) {
436
447
  console.log("");
437
448
  console.log(messages.presets.total(presets.length));
438
449
  }
439
- /**
440
- * Parse `process.argv` en une structure typée `ParsedArgs`.
441
- * Gère les flags courts (-h, -v, -s, -t, -a), les flags longs (--topic, --agent-a…),
442
- * les flags multi-valeurs (--files, --context, --set-defaults) et les positionnels.
443
- * @param args - Tableau d'arguments (généralement `process.argv.slice(2)`).
444
- * @returns Commande détectée, indicateur d'explicitation et map de flags.
445
- */
446
- function parseArgs(args, messages) {
447
- const flags = {};
448
- let command = "run";
449
- let commandExplicit = false;
450
- const positionals = [];
451
- const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents", "preset", "presets"]);
452
- const presets = new Set(listPresetNames());
453
- for (let index = 0; index < args.length; index += 1) {
454
- const value = args[index];
455
- if (!value.startsWith("-") && !commandExplicit && positionals.length === 0 && commands.has(value)) {
456
- command = value;
457
- commandExplicit = true;
458
- continue;
459
- }
460
- if (!value.startsWith("-") && index === 0) {
461
- if (commands.has(value)) {
462
- command = value;
463
- commandExplicit = true;
464
- }
465
- else if (isLikelyCommandTypo(value, commands)) {
466
- throw new Error(messages.common.unknownCommand(value, Array.from(commands).join(", ")));
467
- }
468
- else {
469
- positionals.push(value);
470
- }
471
- continue;
472
- }
473
- if (!value.startsWith("-")) {
474
- positionals.push(value);
475
- continue;
476
- }
477
- if (value === "-h") {
478
- flags.help = true;
479
- continue;
480
- }
481
- if (value === "-v") {
482
- flags.version = true;
483
- continue;
484
- }
485
- if (value === "-a") {
486
- command = "agents";
487
- commandExplicit = true;
488
- continue;
489
- }
490
- if (value === "-s") {
491
- const next = args[index + 1];
492
- if (!next || next.startsWith("-")) {
493
- throw new Error(messages.common.optionRequiresValue("-s"));
494
- }
495
- flags.topic = next;
496
- index += 1;
497
- continue;
498
- }
499
- if (value === "-t") {
500
- const next = args[index + 1];
501
- if (!next || next.startsWith("-")) {
502
- throw new Error(messages.common.optionRequiresValue("-t"));
503
- }
504
- flags.turns = next;
505
- index += 1;
506
- continue;
507
- }
508
- if (value.startsWith("--")) {
509
- const rawKey = value.slice(2);
510
- const key = normalizeFlagName(rawKey);
511
- if (key === "set-defaults") {
512
- const values = [];
513
- while (args[index + 1] && !args[index + 1].startsWith("-") && values.length < 2) {
514
- values.push(args[index + 1]);
515
- index += 1;
516
- }
517
- if (values.length !== 2) {
518
- throw new Error(messages.common.setDefaultsRequiresTwo);
519
- }
520
- flags[key] = values;
521
- continue;
522
- }
523
- if (key === "files" || key === "context") {
524
- const values = [];
525
- while (args[index + 1] && !args[index + 1].startsWith("-")) {
526
- values.push(args[index + 1]);
527
- index += 1;
528
- }
529
- flags[key] = [...getStringListFlag(flags[key]), ...values];
530
- continue;
531
- }
532
- const next = args[index + 1];
533
- if (!next || next.startsWith("-")) {
534
- if (requiresFlagValue(key)) {
535
- throw new Error(messages.common.optionRequiresValue(`--${rawKey}`));
536
- }
537
- flags[key] = true;
538
- }
539
- else {
540
- flags[key] = next;
541
- index += 1;
542
- }
543
- }
544
- }
545
- if (command === "run") {
546
- applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages);
547
- }
548
- return { command, commandExplicit, flags };
549
- }
550
- /**
551
- * Détecte si une valeur ressemble à une faute de frappe d'une commande connue
552
- * (même première lettre et distance de Levenshtein ≤ 2).
553
- * @param value - Token saisi par l'utilisateur.
554
- * @param commands - Ensemble des commandes valides.
555
- */
556
- function isLikelyCommandTypo(value, commands) {
557
- const normalized = value.toLowerCase();
558
- for (const command of commands) {
559
- if (normalized[0] === command[0] && levenshteinDistance(normalized, command) <= 2) {
560
- return true;
561
- }
562
- }
563
- return false;
564
- }
565
- /**
566
- * Calcule la distance de Levenshtein entre deux chaînes (insertions, suppressions, substitutions).
567
- * @param left - Première chaîne.
568
- * @param right - Deuxième chaîne.
569
- * @returns Distance entière ≥ 0.
570
- */
571
- function levenshteinDistance(left, right) {
572
- const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
573
- for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
574
- let diagonal = previous[0];
575
- previous[0] = leftIndex + 1;
576
- for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
577
- const insertCost = previous[rightIndex + 1] + 1;
578
- const deleteCost = previous[rightIndex] + 1;
579
- const replaceCost = diagonal + (left[leftIndex] === right[rightIndex] ? 0 : 1);
580
- diagonal = previous[rightIndex + 1];
581
- previous[rightIndex + 1] = Math.min(insertCost, deleteCost, replaceCost);
582
- }
583
- }
584
- return previous[right.length] ?? 0;
585
- }
586
- /**
587
- * Interprète les arguments positionnels pour la commande `run` :
588
- * premier positionnel = preset si connu, sinon sujet complet concaténé.
589
- * @param positionals - Arguments positionnels extraits du parseur.
590
- * @param flags - Map de flags à muter si un preset ou un sujet est détecté.
591
- * @param presets - Ensemble des noms de presets valides.
592
- * @param commandExplicit - `true` si l'utilisateur a tapé `palabre run` explicitement.
593
- */
594
- function applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages) {
595
- if (positionals.length === 0) {
450
+ async function runContextCommand(flags, positionals) {
451
+ const language = resolveLanguage({ explicitLanguage: optionalString(flags.language) });
452
+ const messages = createTranslator(language);
453
+ const subcommand = positionals[0] ?? "scan";
454
+ if (subcommand !== "scan") {
455
+ throw new Error(messages.common.unknownCommand(`context ${subcommand}`, "context scan"));
456
+ }
457
+ const paths = positionals.slice(1);
458
+ const result = await buildContextScan(paths, process.cwd(), messages);
459
+ const folders = result.items.filter((item) => item.kind === "folder");
460
+ const files = result.items.filter((item) => item.kind === "file");
461
+ if (flags.json) {
462
+ console.log(JSON.stringify(result, null, 2));
596
463
  return;
597
464
  }
598
- const [first, ...rest] = positionals;
599
- if (presets.has(first)) {
600
- flags.preset ??= first;
601
- if (rest.length > 0) {
602
- flags.topic ??= rest.join(" ");
603
- }
604
- return;
465
+ for (const folder of folders) {
466
+ console.log(`[folder] ${folder.path}`);
605
467
  }
606
- if (!commandExplicit && positionals.length === 1 && !positionals[0]?.includes(" ")) {
607
- if (isLikelyCommandTypo(positionals[0], commands)) {
608
- throw new Error(messages.common.unknownCommand(positionals[0], Array.from(commands).join(", ")));
609
- }
610
- throw new Error(messages.common.ambiguousSubject(positionals[0]));
468
+ for (const file of files) {
469
+ console.log(`[file] ${file.path} (${file.sizeBytes} bytes)`);
470
+ }
471
+ for (const warning of result.warnings) {
472
+ console.error(`${messages.renderers.warningPrefix} ${warning}`);
611
473
  }
612
- flags.topic ??= positionals.join(" ");
613
- }
614
- /**
615
- * Normalise un nom de flag long en son alias canonique (ex. `subject` → `topic`).
616
- * @param value - Nom brut extrait après `--`.
617
- */
618
- function normalizeFlagName(value) {
619
- const aliases = {
620
- lang: "language",
621
- s: "topic",
622
- subject: "topic",
623
- t: "turns"
624
- };
625
- return aliases[value] ?? value;
626
- }
627
- /**
628
- * Indique si un flag long nécessite une valeur suivante (lève une erreur si absente).
629
- * @param value - Nom canonique du flag (sans `--`).
630
- */
631
- function requiresFlagValue(value) {
632
- return new Set([
633
- "agent-a",
634
- "agent-b",
635
- "config",
636
- "language",
637
- "model-a",
638
- "model-b",
639
- "preset",
640
- "summary-agent",
641
- "summary-model",
642
- "set-defaults",
643
- "topic",
644
- "turns"
645
- ]).has(value);
646
474
  }
647
475
  /** Lit la version depuis `package.json` adjacent au bundle compilé. */
648
476
  async function getPackageVersion() {
@@ -651,20 +479,6 @@ async function getPackageVersion() {
651
479
  const packageJson = JSON.parse(raw);
652
480
  return packageJson.version ?? "0.0.0";
653
481
  }
654
- /**
655
- * Normalise une valeur de flag multi-valeur en tableau de chaînes.
656
- * @param value - Valeur brute (tableau, chaîne unique ou absent).
657
- * @returns Tableau de chaînes, vide si la valeur n'est pas applicable.
658
- */
659
- function getStringListFlag(value) {
660
- if (Array.isArray(value)) {
661
- return value;
662
- }
663
- if (typeof value === "string") {
664
- return [value];
665
- }
666
- return [];
667
- }
668
482
  /**
669
483
  * Écrit les avertissements de contexte sur `stderr`.
670
484
  * @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
@@ -695,15 +509,7 @@ function syncDetectedAgents(config, discovery) {
695
509
  * @param discovery - Résultat de la découverte locale des outils.
696
510
  */
697
511
  function findDetectedMissingAgents(config, discovery) {
698
- const detectedAgents = [
699
- discovery.codex.available ? "codex" : undefined,
700
- discovery.claude.available ? "claude" : undefined,
701
- discovery.gemini.available ? "gemini" : undefined,
702
- discovery.antigravity.available ? "antigravity" : undefined,
703
- discovery.opencode.available ? "opencode" : undefined,
704
- discovery.ollama.available ? "ollama-local" : undefined
705
- ].filter((agent) => Boolean(agent));
706
- return detectedAgents.filter((agentName) => !config.agents[agentName]);
512
+ return detectedAgentNames(discovery).filter((agentName) => !config.agents[agentName]);
707
513
  }
708
514
  /**
709
515
  * Affiche la liste des agents déclarés avec leur type, rôle, état de détection et défauts.
@@ -774,34 +580,15 @@ function formatAgentDetection(name, agentConfig, discovery, messages) {
774
580
  return detection.available ? messages.agents.detected(detection.command) : messages.agents.notDetected;
775
581
  }
776
582
  /**
777
- * Résout l'entrée de détection correspondant à un agent CLI dans le résultat de découverte.
583
+ * Résout l'entrée de détection correspondant à un agent CLI.
778
584
  * Renvoie un objet `{ available: true }` pour les agents CLI non reconnus (considérés disponibles).
779
585
  * @param name - Nom de l'agent dans la config.
780
586
  * @param agentConfig - Configuration de l'agent.
781
587
  * @param discovery - Résultat de la découverte locale des outils.
782
588
  */
783
589
  function cliDetectionForAgent(name, agentConfig, discovery) {
784
- const command = normalizeCommandName(agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name);
785
- if (command === "codex")
786
- return discovery.codex;
787
- if (command === "claude")
788
- return discovery.claude;
789
- if (command === "gemini")
790
- return discovery.gemini;
791
- if (command === "agy")
792
- return discovery.antigravity;
793
- if (command === "antigravity")
794
- return discovery.antigravity;
795
- if (command === "opencode")
796
- return discovery.opencode;
797
- return { available: true, command: agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name };
798
- }
799
- /**
800
- * Extrait le nom de base d'une commande en supprimant le chemin et l'extension Windows éventuelle.
801
- * @param command - Chemin ou nom de commande brut (ex. `C:\bin\claude.cmd`).
802
- */
803
- function normalizeCommandName(command) {
804
- return path.basename(command).replace(/\.(cmd|exe|ps1|bat)$/i, "").toLowerCase();
590
+ const command = agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name;
591
+ return detectionForCommand(command, discovery) ?? { available: true, command };
805
592
  }
806
593
  /**
807
594
  * Affiche le récapitulatif de détection locale après `palabre init`.
@@ -824,14 +611,7 @@ function printInitDiscovery(discovery, config, messages) {
824
611
  console.log(messages.init.languageHint(config.language ?? DEFAULT_LANGUAGE));
825
612
  }
826
613
  function formatDetectedAgentSummary(discovery, language) {
827
- const names = [
828
- discovery.codex.available ? "codex" : undefined,
829
- discovery.claude.available ? "claude" : undefined,
830
- discovery.gemini.available ? "gemini" : undefined,
831
- discovery.antigravity.available ? "antigravity" : undefined,
832
- discovery.opencode.available ? "opencode" : undefined,
833
- discovery.ollama.available ? "ollama-local" : undefined
834
- ].filter((name) => Boolean(name));
614
+ const names = detectedAgentNames(discovery);
835
615
  if (names.length === 0) {
836
616
  return language === "en" ? "no agent detected" : "aucun agent détecté";
837
617
  }
@@ -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.",
@@ -10,6 +10,9 @@ export const commonMessages = {
10
10
  unknownAgentForField: (field, agent, available) => `Agent inconnu pour ${field}: ${agent}. Agents disponibles: ${available}.`,
11
11
  unknownAgent: (agent) => `Agent inconnu: ${agent}`,
12
12
  unknownRenderer: (value, supported) => `Renderer inconnu: ${value}. Valeurs supportées: ${supported}.`,
13
+ configInvalidShape: (configPath) => `Config invalide: ${configPath} ne contient pas un objet JSON. Relance palabre init ou corrige le fichier.`,
14
+ configMissingAgents: (configPath) => `Config invalide: ${configPath} ne déclare pas de bloc "agents". Relance palabre init ou ajoute au moins un agent.`,
15
+ configEmptyAgents: (configPath) => `Config invalide: ${configPath} ne déclare aucun agent. Ajoute au moins un agent ou relance palabre init.`,
13
16
  errorPrefix: "Erreur"
14
17
  },
15
18
  en: {
@@ -23,6 +26,9 @@ export const commonMessages = {
23
26
  unknownAgentForField: (field, agent, available) => `Unknown agent for ${field}: ${agent}. Available agents: ${available}.`,
24
27
  unknownAgent: (agent) => `Unknown agent: ${agent}`,
25
28
  unknownRenderer: (value, supported) => `Unknown renderer: ${value}. Supported values: ${supported}.`,
29
+ configInvalidShape: (configPath) => `Invalid config: ${configPath} does not contain a JSON object. Run palabre init or fix the file.`,
30
+ configMissingAgents: (configPath) => `Invalid config: ${configPath} has no "agents" block. Run palabre init or add at least one agent.`,
31
+ configEmptyAgents: (configPath) => `Invalid config: ${configPath} declares no agent. Add at least one agent or run palabre init.`,
26
32
  errorPrefix: "Error"
27
33
  }
28
34
  };