palabre 0.1.7 → 0.3.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 CHANGED
@@ -7,6 +7,8 @@
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
+ ![PALABRE](docs/assets/palabre-logo-text-og.png)
11
+
10
12
  [Français](#français) | [English](#english)
11
13
 
12
14
  ## Français
@@ -20,7 +22,7 @@ Il ne remplace pas vos outils : il les pilote. Vous gardez vos abonnements, vos
20
22
  - https://palab.re
21
23
  - https://palabre.netlify.app
22
24
 
23
- Pages utiles : [Installation](https://palab.re/get-started/installation), [Configuration](https://palab.re/get-started/configuration), [Premier débat](https://palab.re/get-started/first-debate), [Référence CLI](https://palab.re/reference/cli), [Dépannage](https://palab.re/troubleshooting), [Roadmap](https://palab.re/roadmap).
25
+ Pages utiles : [Installation](https://palab.re/fr/get-started/installation), [Configuration](https://palab.re/fr/get-started/configuration), [Premier débat](https://palab.re/fr/get-started/first-debate), [Référence CLI](https://palab.re/fr/reference/cli), [Dépannage](https://palab.re/fr/troubleshooting), [Roadmap](https://palab.re/fr/roadmap).
24
26
 
25
27
  ### Installation
26
28
 
@@ -77,7 +79,7 @@ palabre --version
77
79
 
78
80
  Commandes utiles : `pnpm check`, `pnpm test`, `pnpm build`.
79
81
 
80
- Roadmap projet : [docs/roadmap.md](./docs/roadmap.md). Guide agents/contributeurs : [AGENTS.md](./AGENTS.md).
82
+ Roadmap publique : [docs/guide/fr/roadmap.md](./docs/guide/fr/roadmap.md). Guide agents/contributeurs : [AGENTS.md](./AGENTS.md).
81
83
 
82
84
  ### Licence
83
85
 
@@ -94,7 +96,7 @@ It does not replace your tools: it drives them. You keep your subscriptions, def
94
96
  - https://palab.re
95
97
  - https://palabre.netlify.app
96
98
 
97
- Useful pages: [Installation](https://palab.re/get-started/installation), [Configuration](https://palab.re/get-started/configuration), [First debate](https://palab.re/get-started/first-debate), [CLI reference](https://palab.re/reference/cli), [Troubleshooting](https://palab.re/troubleshooting), [Roadmap](https://palab.re/roadmap).
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).
98
100
 
99
101
  ### Installation
100
102
 
@@ -151,7 +153,7 @@ palabre --version
151
153
 
152
154
  Useful commands: `pnpm check`, `pnpm test`, `pnpm build`.
153
155
 
154
- Project roadmap: [docs/roadmap.md](./docs/roadmap.md). Agent/contributor guide: [AGENTS.md](./AGENTS.md).
156
+ Public roadmap: [docs/guide/fr/roadmap.md](./docs/guide/fr/roadmap.md). Agent/contributor guide: [AGENTS.md](./AGENTS.md).
155
157
 
156
158
  ### License
157
159
 
package/dist/index.js CHANGED
@@ -13,10 +13,12 @@ import { formatAgentPrompt } from "./prompt.js";
13
13
  import { runNewWizard } from "./new.js";
14
14
  import { listPresetNames, resolvePreset } from "./presets.js";
15
15
  import { createConsoleRenderer } from "./renderers/console.js";
16
+ import { createNdjsonRenderer } from "./renderers/ndjson.js";
16
17
  import { runDebate } from "./orchestrator.js";
17
18
  import { writeDebateMarkdown } from "./output.js";
18
19
  import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./update.js";
19
20
  import { createSessionContext } from "./session.js";
21
+ /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
20
22
  async function main() {
21
23
  const parsed = parseArgs(process.argv.slice(2));
22
24
  if (parsed.command === "version" || parsed.flags.version) {
@@ -41,6 +43,10 @@ async function main() {
41
43
  await runAgentsCommand(parsed.flags);
42
44
  return;
43
45
  }
46
+ if (parsed.command === "presets" || parsed.command === "preset") {
47
+ runPresetsCommand(parsed.flags);
48
+ return;
49
+ }
44
50
  if (parsed.command === "update") {
45
51
  const info = await getUpdateInfo(await getPackageVersion());
46
52
  if (parsed.flags.apply) {
@@ -129,12 +135,16 @@ async function main() {
129
135
  printPromptPreview(config, options);
130
136
  return;
131
137
  }
132
- const renderer = createConsoleRenderer(options.plainOutput);
138
+ const renderer = createRendererFromFlags(parsed.flags, options.plainOutput);
133
139
  context.warnings.forEach((warning) => renderer.warning(warning));
134
140
  const result = await runDebate(config, options, renderer);
135
141
  const outputPath = await writeDebateMarkdown(config.outputDir ?? ".", result.options, result.messages, result.summary, result.stopReason);
136
142
  renderer.done(outputPath);
137
143
  }
144
+ /**
145
+ * Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
146
+ * @param flags - Flags parsés depuis la ligne de commande.
147
+ */
138
148
  async function runAgentsCommand(flags) {
139
149
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
140
150
  if (!(await configExists(configPath))) {
@@ -144,6 +154,10 @@ async function runAgentsCommand(flags) {
144
154
  const discovery = await discoverLocalTools();
145
155
  printAgents(configPath, config, discovery);
146
156
  }
157
+ /**
158
+ * Exécute la commande `config` : wizard interactif ou mise à jour directe des paramètres par défaut.
159
+ * @param flags - Flags parsés depuis la ligne de commande.
160
+ */
147
161
  async function runConfigCommand(flags) {
148
162
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
149
163
  if (!(await configExists(configPath))) {
@@ -203,9 +217,18 @@ async function runConfigCommand(flags) {
203
217
  }
204
218
  await runConfigWizard(configPath, config);
205
219
  }
220
+ /**
221
+ * Renvoie `true` si la valeur représente une désactivation explicite (ex. "none", "0", "disabled").
222
+ * @param value - Chaîne saisie par l'utilisateur.
223
+ */
206
224
  function isNoneValue(value) {
207
225
  return ["0", "none", "aucun", "disabled", "désactivé", "desactive"].includes(value.trim().toLowerCase());
208
226
  }
227
+ /**
228
+ * Formate les paramètres par défaut en une ligne lisible pour les messages console.
229
+ * @param defaults - Objet `defaults` de la config Palabre.
230
+ * @returns Chaîne résumant la paire d'agents, le nombre de réponses et l'agent de synthèse.
231
+ */
209
232
  function formatDefaultsForMessage(defaults) {
210
233
  const pair = defaults.agentA && defaults.agentB
211
234
  ? `agents: ${defaults.agentA} <-> ${defaults.agentB}`
@@ -213,11 +236,26 @@ function formatDefaultsForMessage(defaults) {
213
236
  const summary = defaults.summaryAgent ? `synthèse: ${defaults.summaryAgent}` : "synthèse: agent B";
214
237
  return `${pair}, réponses: ${turnsOrDefault(defaults.turns)}, ${summary}`;
215
238
  }
239
+ /**
240
+ * Lève une erreur si `agentName` n'est pas déclaré dans la config.
241
+ * @param config - Config chargée.
242
+ * @param agentName - Nom de l'agent à vérifier.
243
+ * @param fieldName - Nom du champ (utilisé dans le message d'erreur).
244
+ */
216
245
  function assertKnownAgent(config, agentName, fieldName) {
217
246
  if (!config.agents[agentName]) {
218
247
  throw new Error(`Agent inconnu pour ${fieldName}: ${agentName}. Agents disponibles: ${Object.keys(config.agents).join(", ")}.`);
219
248
  }
220
249
  }
250
+ /**
251
+ * Résout le nom d'un agent selon la priorité : flag CLI > preset > défaut config.
252
+ * Lève une erreur si aucune source ne fournit de valeur.
253
+ * @param label - Libellé humain utilisé dans le message d'erreur (ex. "agent A").
254
+ * @param explicitValue - Valeur passée via flag CLI.
255
+ * @param presetValue - Valeur issue du preset sélectionné.
256
+ * @param defaultValue - Valeur issue des défauts de la config.
257
+ * @returns Nom de l'agent résolu.
258
+ */
221
259
  function resolveAgentName(label, explicitValue, presetValue, defaultValue) {
222
260
  const resolved = optionalString(explicitValue) ?? presetValue ?? defaultValue;
223
261
  if (!resolved) {
@@ -225,6 +263,11 @@ function resolveAgentName(label, explicitValue, presetValue, defaultValue) {
225
263
  }
226
264
  return resolved;
227
265
  }
266
+ /**
267
+ * Affiche un aperçu du prompt du premier tour sans appeler aucun agent (flag `--show-prompt`).
268
+ * @param config - Config chargée.
269
+ * @param options - Options du débat résolues.
270
+ */
228
271
  function printPromptPreview(config, options) {
229
272
  const agentConfig = config.agents[options.agentA];
230
273
  if (!agentConfig) {
@@ -250,15 +293,89 @@ function printPromptPreview(config, options) {
250
293
  console.log("");
251
294
  console.log("Note: seuls les prompts du premier tour sont exacts sans exécuter les agents. Les tours suivants incluent le transcript réel.");
252
295
  }
296
+ /**
297
+ * Extrait une chaîne non vide depuis une valeur de flag, ou renvoie `undefined`.
298
+ * @param value - Valeur brute issue du parseur de flags.
299
+ */
253
300
  function optionalString(value) {
254
301
  return typeof value === "string" && value.trim() ? value : undefined;
255
302
  }
303
+ /** Liste des kinds de renderer acceptés par `--renderer`. */
304
+ const SUPPORTED_RENDERERS = ["auto", "pretty", "plain", "ndjson"];
305
+ /**
306
+ * Instancie le renderer en fonction des flags CLI.
307
+ *
308
+ * Précédence :
309
+ * 1. `--renderer <kind>` (canonique).
310
+ * 2. `--json` (alias pour `--renderer ndjson`).
311
+ * 3. `--plain` (rétro-compatible, équivalent `--renderer plain`).
312
+ * 4. par défaut : `auto` (pretty si TTY, plain sinon, hérité de `createConsoleRenderer`).
313
+ *
314
+ * Lève si la valeur de `--renderer` n'est pas dans `SUPPORTED_RENDERERS`.
315
+ */
316
+ function createRendererFromFlags(flags, plainOutputFallback) {
317
+ const explicit = optionalString(flags.renderer);
318
+ if (explicit) {
319
+ if (!SUPPORTED_RENDERERS.includes(explicit)) {
320
+ throw new Error(`Renderer inconnu: ${explicit}. Valeurs supportées: ${SUPPORTED_RENDERERS.join(", ")}.`);
321
+ }
322
+ const kind = explicit;
323
+ switch (kind) {
324
+ case "ndjson":
325
+ return createNdjsonRenderer();
326
+ case "plain":
327
+ return createConsoleRenderer(true);
328
+ case "pretty":
329
+ return createConsoleRenderer(false);
330
+ case "auto":
331
+ return createConsoleRenderer(plainOutputFallback);
332
+ }
333
+ }
334
+ if (flags.json) {
335
+ return createNdjsonRenderer();
336
+ }
337
+ return createConsoleRenderer(plainOutputFallback);
338
+ }
339
+ /**
340
+ * Exécute la commande `palabre presets`.
341
+ *
342
+ * Sortie humaine par défaut (liste alignée), ou JSON avec `--json` pour les
343
+ * intégrations (extension VS Code, scripts shell). Le schéma JSON est versionné
344
+ * via le champ `v` au cas où on enrichirait plus tard (ex : description par
345
+ * preset, tags premium/local).
346
+ *
347
+ * @param flags - Flags parsés depuis la ligne de commande.
348
+ */
349
+ function runPresetsCommand(flags) {
350
+ const presets = listPresetNames().map((name) => {
351
+ const pair = resolvePreset(name);
352
+ return { name, agentA: pair.agentA, agentB: pair.agentB };
353
+ });
354
+ if (flags.json) {
355
+ process.stdout.write(JSON.stringify({ v: 1, presets }) + "\n");
356
+ return;
357
+ }
358
+ console.log("Presets disponibles:");
359
+ console.log("");
360
+ for (const preset of presets) {
361
+ console.log(` ${preset.name.padEnd(20)} ${preset.agentA} <-> ${preset.agentB}`);
362
+ }
363
+ console.log("");
364
+ console.log(`Total : ${presets.length} preset(s). Utilise --json pour une sortie machine-readable.`);
365
+ }
366
+ /**
367
+ * Parse `process.argv` en une structure typée `ParsedArgs`.
368
+ * Gère les flags courts (-h, -v, -s, -t, -a), les flags longs (--topic, --agent-a…),
369
+ * les flags multi-valeurs (--files, --context, --set-defaults) et les positionnels.
370
+ * @param args - Tableau d'arguments (généralement `process.argv.slice(2)`).
371
+ * @returns Commande détectée, indicateur d'explicitation et map de flags.
372
+ */
256
373
  function parseArgs(args) {
257
374
  const flags = {};
258
375
  let command = "run";
259
376
  let commandExplicit = false;
260
377
  const positionals = [];
261
- const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents"]);
378
+ const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents", "preset", "presets"]);
262
379
  const presets = new Set(listPresetNames());
263
380
  for (let index = 0; index < args.length; index += 1) {
264
381
  const value = args[index];
@@ -352,6 +469,12 @@ function parseArgs(args) {
352
469
  }
353
470
  return { command, commandExplicit, flags };
354
471
  }
472
+ /**
473
+ * Détecte si une valeur ressemble à une faute de frappe d'une commande connue
474
+ * (même première lettre et distance de Levenshtein ≤ 2).
475
+ * @param value - Token saisi par l'utilisateur.
476
+ * @param commands - Ensemble des commandes valides.
477
+ */
355
478
  function isLikelyCommandTypo(value, commands) {
356
479
  const normalized = value.toLowerCase();
357
480
  for (const command of commands) {
@@ -361,6 +484,12 @@ function isLikelyCommandTypo(value, commands) {
361
484
  }
362
485
  return false;
363
486
  }
487
+ /**
488
+ * Calcule la distance de Levenshtein entre deux chaînes (insertions, suppressions, substitutions).
489
+ * @param left - Première chaîne.
490
+ * @param right - Deuxième chaîne.
491
+ * @returns Distance entière ≥ 0.
492
+ */
364
493
  function levenshteinDistance(left, right) {
365
494
  const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
366
495
  for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
@@ -376,6 +505,14 @@ function levenshteinDistance(left, right) {
376
505
  }
377
506
  return previous[right.length] ?? 0;
378
507
  }
508
+ /**
509
+ * Interprète les arguments positionnels pour la commande `run` :
510
+ * premier positionnel = preset si connu, sinon sujet complet concaténé.
511
+ * @param positionals - Arguments positionnels extraits du parseur.
512
+ * @param flags - Map de flags à muter si un preset ou un sujet est détecté.
513
+ * @param presets - Ensemble des noms de presets valides.
514
+ * @param commandExplicit - `true` si l'utilisateur a tapé `palabre run` explicitement.
515
+ */
379
516
  function applyRunPositionals(positionals, flags, presets, commandExplicit) {
380
517
  if (positionals.length === 0) {
381
518
  return;
@@ -393,6 +530,10 @@ function applyRunPositionals(positionals, flags, presets, commandExplicit) {
393
530
  }
394
531
  flags.topic ??= positionals.join(" ");
395
532
  }
533
+ /**
534
+ * Normalise un nom de flag long en son alias canonique (ex. `subject` → `topic`).
535
+ * @param value - Nom brut extrait après `--`.
536
+ */
396
537
  function normalizeFlagName(value) {
397
538
  const aliases = {
398
539
  s: "topic",
@@ -401,6 +542,10 @@ function normalizeFlagName(value) {
401
542
  };
402
543
  return aliases[value] ?? value;
403
544
  }
545
+ /**
546
+ * Indique si un flag long nécessite une valeur suivante (lève une erreur si absente).
547
+ * @param value - Nom canonique du flag (sans `--`).
548
+ */
404
549
  function requiresFlagValue(value) {
405
550
  return new Set([
406
551
  "agent-a",
@@ -416,12 +561,18 @@ function requiresFlagValue(value) {
416
561
  "turns"
417
562
  ]).has(value);
418
563
  }
564
+ /** Lit la version depuis `package.json` adjacent au bundle compilé. */
419
565
  async function getPackageVersion() {
420
566
  const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
421
567
  const raw = await readFile(packageJsonPath, "utf8");
422
568
  const packageJson = JSON.parse(raw);
423
569
  return packageJson.version ?? "0.0.0";
424
570
  }
571
+ /**
572
+ * Normalise une valeur de flag multi-valeur en tableau de chaînes.
573
+ * @param value - Valeur brute (tableau, chaîne unique ou absent).
574
+ * @returns Tableau de chaînes, vide si la valeur n'est pas applicable.
575
+ */
425
576
  function getStringListFlag(value) {
426
577
  if (Array.isArray(value)) {
427
578
  return value;
@@ -431,11 +582,22 @@ function getStringListFlag(value) {
431
582
  }
432
583
  return [];
433
584
  }
585
+ /**
586
+ * Écrit les avertissements de contexte sur `stderr`.
587
+ * @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
588
+ */
434
589
  function printContextWarnings(warnings) {
435
590
  for (const warning of warnings) {
436
591
  process.stderr.write(`Warning: ${warning}\n`);
437
592
  }
438
593
  }
594
+ /**
595
+ * Ajoute dans `config.agents` les agents détectés localement mais absents de la config.
596
+ * Mute `config` directement ; l'appelant est responsable de persister la config.
597
+ * @param config - Config Palabre à compléter.
598
+ * @param discovery - Résultat de la découverte locale des outils.
599
+ * @returns Noms des agents nouvellement ajoutés.
600
+ */
439
601
  function syncDetectedAgents(config, discovery) {
440
602
  const discoveredConfig = createConfigFromDiscovery(discovery);
441
603
  const missingAgents = findDetectedMissingAgents(config, discovery);
@@ -444,6 +606,11 @@ function syncDetectedAgents(config, discovery) {
444
606
  }
445
607
  return missingAgents;
446
608
  }
609
+ /**
610
+ * Renvoie les noms des agents détectés localement qui ne sont pas encore dans `config.agents`.
611
+ * @param config - Config Palabre existante.
612
+ * @param discovery - Résultat de la découverte locale des outils.
613
+ */
447
614
  function findDetectedMissingAgents(config, discovery) {
448
615
  const detectedAgents = [
449
616
  discovery.codex.available ? "codex" : undefined,
@@ -454,6 +621,12 @@ function findDetectedMissingAgents(config, discovery) {
454
621
  ].filter((agent) => Boolean(agent));
455
622
  return detectedAgents.filter((agentName) => !config.agents[agentName]);
456
623
  }
624
+ /**
625
+ * Affiche la liste des agents déclarés avec leur type, rôle, état de détection et défauts.
626
+ * @param configPath - Chemin du fichier de config (affiché en en-tête).
627
+ * @param config - Config Palabre chargée.
628
+ * @param discovery - Résultat de la découverte locale des outils.
629
+ */
457
630
  function printAgents(configPath, config, discovery) {
458
631
  const entries = Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right));
459
632
  console.log(`Config: ${configPath}`);
@@ -472,6 +645,11 @@ function printAgents(configPath, config, discovery) {
472
645
  console.log("");
473
646
  console.log(`Défauts: ${config.defaults?.agentA ?? "aucun"} <-> ${config.defaults?.agentB ?? "aucun"}, réponses: ${turnsOrDefault(config.defaults?.turns)}, synthèse: ${config.defaults?.summaryAgent ?? "agent B"}`);
474
647
  }
648
+ /**
649
+ * Renvoie un libellé indiquant si l'agent est agent A, agent B ou agent de synthèse par défaut.
650
+ * @param name - Nom de l'agent.
651
+ * @param config - Config Palabre contenant les défauts.
652
+ */
475
653
  function formatAgentDefaults(name, config) {
476
654
  const labels = [];
477
655
  if (config.defaults?.agentA === name)
@@ -482,12 +660,23 @@ function formatAgentDefaults(name, config) {
482
660
  labels.push("synthèse par défaut");
483
661
  return labels.join(", ");
484
662
  }
663
+ /**
664
+ * Renvoie une ligne de détails pour un agent : commande CLI ou modèle Ollama.
665
+ * @param agentConfig - Configuration de l'agent.
666
+ */
485
667
  function formatAgentDetails(agentConfig) {
486
668
  if (agentConfig.type === "ollama") {
487
669
  return `modèle: ${agentConfig.model}`;
488
670
  }
489
671
  return `commande: ${agentConfig.command}${agentConfig.model ? ` | modèle: ${agentConfig.model}` : ""}`;
490
672
  }
673
+ /**
674
+ * Renvoie le statut de détection d'un agent sous forme de chaîne lisible.
675
+ * Pour Ollama, vérifie la disponibilité du serveur et la présence du modèle.
676
+ * @param name - Nom de l'agent dans la config.
677
+ * @param agentConfig - Configuration de l'agent.
678
+ * @param discovery - Résultat de la découverte locale des outils.
679
+ */
491
680
  function formatAgentDetection(name, agentConfig, discovery) {
492
681
  if (agentConfig.type === "ollama") {
493
682
  if (!discovery.ollama.available) {
@@ -500,6 +689,13 @@ function formatAgentDetection(name, agentConfig, discovery) {
500
689
  const detection = cliDetectionForAgent(name, agentConfig, discovery);
501
690
  return detection.available ? `détecté (${detection.command})` : "non détecté";
502
691
  }
692
+ /**
693
+ * Résout l'entrée de détection correspondant à un agent CLI dans le résultat de découverte.
694
+ * Renvoie un objet `{ available: true }` pour les agents CLI non reconnus (considérés disponibles).
695
+ * @param name - Nom de l'agent dans la config.
696
+ * @param agentConfig - Configuration de l'agent.
697
+ * @param discovery - Résultat de la découverte locale des outils.
698
+ */
503
699
  function cliDetectionForAgent(name, agentConfig, discovery) {
504
700
  const command = normalizeCommandName(agentConfig.type === "cli" ? agentConfig.command : name);
505
701
  if (command === "codex")
@@ -512,9 +708,18 @@ function cliDetectionForAgent(name, agentConfig, discovery) {
512
708
  return discovery.opencode;
513
709
  return { available: true, command: agentConfig.type === "cli" ? agentConfig.command : name };
514
710
  }
711
+ /**
712
+ * Extrait le nom de base d'une commande en supprimant le chemin et l'extension Windows éventuelle.
713
+ * @param command - Chemin ou nom de commande brut (ex. `C:\bin\claude.cmd`).
714
+ */
515
715
  function normalizeCommandName(command) {
516
716
  return path.basename(command).replace(/\.(cmd|exe|ps1|bat)$/i, "").toLowerCase();
517
717
  }
718
+ /**
719
+ * Affiche le récapitulatif de détection locale après `palabre init`.
720
+ * @param discovery - Résultat de la découverte locale des outils.
721
+ * @param config - Config générée à partir de la découverte.
722
+ */
518
723
  function printInitDiscovery(discovery, config) {
519
724
  console.log("");
520
725
  console.log("Détection locale:");
@@ -526,11 +731,19 @@ function printInitDiscovery(discovery, config) {
526
731
  console.log("");
527
732
  console.log(`Défauts: ${config.defaults?.agentA ?? "codex"} <-> ${config.defaults?.agentB ?? "ollama-local"}`);
528
733
  }
734
+ /**
735
+ * Formate le statut de détection d'un outil CLI (disponible ou non).
736
+ * @param detection - Résultat de détection d'un outil CLI.
737
+ */
529
738
  function formatCommandDetection(detection) {
530
739
  return detection.available
531
740
  ? `détecté (${detection.command})`
532
741
  : "non détecté";
533
742
  }
743
+ /**
744
+ * Formate le statut de détection d'Ollama : commande absente, serveur injoignable ou modèles disponibles.
745
+ * @param detection - Résultat de détection d'Ollama.
746
+ */
534
747
  function formatOllamaDetection(detection) {
535
748
  if (!detection.available) {
536
749
  return detection.commandAvailable
@@ -540,54 +753,94 @@ function formatOllamaDetection(detection) {
540
753
  const modelCount = detection.models.length;
541
754
  return `détectée (${modelCount} modèle${modelCount > 1 ? "s" : ""})`;
542
755
  }
756
+ /** Affiche le texte d'aide complet sur `stdout`. */
543
757
  function printHelp() {
544
758
  console.log(`
545
759
  PALABRE
760
+ _____________________________________________
546
761
 
547
762
  Usage rapide:
763
+
764
+ palabre init
765
+ Crée une config globale et détecte les agents AI disponibles sur la machine.
766
+
767
+ palabre agents
768
+ Affiche les agents déclarés dans la config.
769
+
770
+ palabre config
771
+ Assistant pour définir ou supprimer les paramètres par défaut.
772
+
548
773
  palabre new
549
774
  Assistant interactif pour choisir les agents, le sujet et les options.
550
- palabre run -s "Sujet"
551
- Lance avec les agents par défaut de la config.
775
+
552
776
  palabre claude-gemini "Sujet" -t 4
553
777
  Lance avec un preset et un sujet positionnel.
554
778
 
779
+ palabre "Sujet"
780
+ Lance le débat avec paramètres par défaut de la config.
781
+
782
+ _____________________________________________
783
+
784
+
555
785
  Commandes:
786
+
556
787
  palabre init [--local]
557
- Crée une config et détecte Codex, Claude, Gemini, OpenCode et Ollama.
788
+ Crée une config locale et détecte Codex, Claude, Gemini, OpenCode et Ollama.
789
+
558
790
  palabre agents [--config <path>]
559
791
  Liste les agents déclarés dans la config et leur détection locale.
792
+
793
+ palabre presets [--json]
794
+ Liste les presets de paires d'agents. \`--json\` émet la liste structurée
795
+ pour les intégrations (extension VS Code, scripts).
796
+
560
797
  palabre config
561
798
  Assistant pour définir ou supprimer les paramètres par défaut.
799
+
562
800
  palabre config --set-defaults <agentA> <agentB> [-t <n>] [--summary-agent <name>]
563
801
  Définit les agents par défaut, et optionnellement les réponses et la synthèse.
802
+
564
803
  palabre config -t <n>
565
804
  Définit seulement le nombre de réponses par défaut.
805
+
566
806
  palabre config --summary-agent <name|none>
567
807
  Définit ou retire seulement l'agent de synthèse par défaut.
808
+
568
809
  palabre config --clear-defaults
569
810
  Supprime les paramètres par défaut.
811
+
570
812
  palabre doctor [--config <path>]
571
813
  Vérifie la config et les outils locaux.
814
+
572
815
  palabre update [--apply]
573
816
  Affiche ou exécute les étapes de mise à jour d'un checkout git.
817
+
574
818
  palabre help
575
819
  Affiche cette aide. Identique à -h ou --help.
820
+
576
821
  palabre version
577
822
  Affiche la version. Identique à -v ou --version.
578
823
 
824
+ _____________________________________________
825
+
826
+
579
827
  Notation:
828
+
580
829
  [option] signifie facultatif. Ne tape pas les crochets.
581
830
  <valeur> signifie qu'il faut remplacer ce texte par ta valeur.
582
831
 
583
832
  Options générales:
833
+
584
834
  -h, --help Affiche cette aide
585
835
  -v, --version Affiche la version
586
- -a Liste les agents. Identique à palabre agents
836
+ -a, --agents Liste les agents. Identique à palabre agents
587
837
  --config <path> Chemin vers un fichier de config explicite
588
838
  --plain Utilise le rendu console simple sans habillage TUI
839
+ --json Émet un événement NDJSON par ligne sur stdout (alias de --renderer ndjson)
840
+ --renderer <kind> Force le renderer : auto | pretty | plain | ndjson
589
841
 
590
842
  Sujet et lancement:
843
+
591
844
  -s, --subject <text> Sujet du débat, option recommandée
592
845
  --topic <text> Alias compatible de --subject
593
846
  --agent-a <name> Premier agent
@@ -597,21 +850,25 @@ Sujet et lancement:
597
850
  --no-early-stop Désactive l'arrêt anticipé si les agents sont clairement d'accord
598
851
 
599
852
  Modèles:
853
+
600
854
  --model-a <model> Modèle brut transmis à l'agent A
601
855
  --model-b <model> Modèle brut transmis à l'agent B
602
856
  --pull-models Autorise Ollama à télécharger un modèle manquant
603
857
 
604
858
  Synthèse:
859
+
605
860
  --summary-agent <name> Agent utilisé pour produire la synthèse finale
606
861
  --summary-model <model> Modèle brut transmis à l'agent de synthèse
607
862
  --no-summary Désactive la synthèse finale
608
863
 
609
864
  Contexte:
865
+
610
866
  --files <paths...> Fichiers texte à injecter explicitement dans le contexte
611
867
  --context <paths...> Scanne fichiers/dossiers texte en respectant les limites de contexte
612
868
  --show-prompt Affiche le prompt du premier tour sans appeler d'agent
613
869
 
614
870
  Configuration:
871
+
615
872
  --local Avec init/setup, crée ./palabre.config.json
616
873
  --set-defaults <a b> Avec config, définit les agents par défaut
617
874
  --summary-agent <name> Avec config, définit l'agent de synthèse par défaut
@@ -620,10 +877,18 @@ Configuration:
620
877
  --sync-agents Avec config, ajoute les agents détectés manquants
621
878
 
622
879
  Mise à jour:
880
+
623
881
  --apply Avec update, exécute les étapes de mise à jour
624
882
 
883
+ _____________________________________________
884
+
885
+
625
886
  Presets disponibles:
887
+
626
888
  ${listPresetNames().join(", ")}
889
+
890
+ _____________________________________________
891
+
627
892
  `);
628
893
  }
629
894
  main().catch((error) => {
@@ -4,6 +4,10 @@ const supportsInteractiveOutput = Boolean(process.stdout.isTTY);
4
4
  export function createConsoleRenderer(plain) {
5
5
  return plain ? new PlainConsoleRenderer() : new PrettyConsoleRenderer(supportsColor, supportsInteractiveOutput);
6
6
  }
7
+ /**
8
+ * Renderer interactif avec spinner, couleurs ANSI et encadrés de section.
9
+ * Utilisé quand `stdout` est un TTY et que `--plain` n'est pas passé.
10
+ */
7
11
  class PrettyConsoleRenderer {
8
12
  color;
9
13
  interactive;
@@ -11,10 +15,15 @@ class PrettyConsoleRenderer {
11
15
  spinnerFrame = 0;
12
16
  renderingSummary = false;
13
17
  frames = ["-", "\\", "|", "/"];
18
+ /**
19
+ * @param color - Active les codes couleur ANSI.
20
+ * @param interactive - Active le spinner en place (mode TTY interactif).
21
+ */
14
22
  constructor(color, interactive) {
15
23
  this.color = color;
16
24
  this.interactive = interactive;
17
25
  }
26
+ /** Affiche l'en-tête du débat (sujet, agents, options). */
18
27
  start(options, agents = []) {
19
28
  const title = "PALABRE";
20
29
  process.stdout.write([
@@ -29,21 +38,25 @@ class PrettyConsoleRenderer {
29
38
  ""
30
39
  ].join("\n"));
31
40
  }
41
+ /** Écrit un avertissement sur `stderr` en jaune. */
32
42
  warning(message) {
33
43
  process.stderr.write(`${this.c("yellow", "Warning:")} ${message}\n`);
34
44
  }
45
+ /** Écrit une notice informative sur `stdout` en vert. */
35
46
  notice(message) {
36
47
  process.stdout.write(`${this.c("green", "Info:")} ${message}\n`);
37
48
  }
49
+ /** Affiche l'en-tête d'un nouveau tour (agent, rôle, progression). */
38
50
  turnStart(turn, totalTurns, agent, role) {
39
51
  this.renderingSummary = false;
40
52
  process.stdout.write([
41
53
  "",
42
- this.c("blue", `◆ ${agent}`) + this.dim(` · ${role} · tour ${turn}/${totalTurns}`),
54
+ this.c("orange", `◆ ${agent}`) + this.dim(` · ${role} · tour ${turn}/${totalTurns}`),
43
55
  this.dim("─".repeat(60)),
44
56
  ""
45
57
  ].join("\n"));
46
58
  }
59
+ /** Démarre le spinner de réflexion (ou affiche une ligne fixe si non interactif). */
47
60
  thinkingStart(agent, role) {
48
61
  this.thinkingEnd();
49
62
  const text = `${agent} (${role}) reflechit`;
@@ -59,6 +72,7 @@ class PrettyConsoleRenderer {
59
72
  render();
60
73
  this.spinner = setInterval(render, 120);
61
74
  }
75
+ /** Arrête le spinner et efface la ligne de réflexion en mode interactif. */
62
76
  thinkingEnd() {
63
77
  if (this.spinner) {
64
78
  clearInterval(this.spinner);
@@ -68,22 +82,29 @@ class PrettyConsoleRenderer {
68
82
  process.stdout.write("\r\u001b[2K");
69
83
  }
70
84
  }
85
+ /** Écrit le contenu d'un message agent, avec formatage de synthèse si applicable. */
71
86
  message(content) {
72
87
  const trimmed = content.trim();
73
88
  process.stdout.write(`${this.renderingSummary ? this.formatSummaryMessage(trimmed) : trimmed}\n`);
74
89
  }
90
+ /** Affiche l'en-tête de section synthèse et active le mode formatage de résumé. */
75
91
  summaryStart(agent, role) {
76
92
  this.renderingSummary = true;
77
93
  process.stdout.write([
78
94
  "",
79
- this.c("magenta", `◆ Synthese`) + this.dim(` · ${agent} · ${role}`),
95
+ this.c("pink", `◆ Synthese`) + this.dim(` · ${agent} · ${role}`),
80
96
  this.dim("─".repeat(60)),
81
97
  ""
82
98
  ].join("\n"));
83
99
  }
100
+ /** Affiche le chemin du fichier de sortie en vert à la fin du débat. */
84
101
  done(outputPath) {
85
- process.stdout.write(`\n${this.c("green", "Debat exporte:")} ${outputPath}\n`);
102
+ process.stdout.write(`\n\n${this.c("green", "Debat exporte:")} ${outputPath}\n\n`);
86
103
  }
104
+ /**
105
+ * Convertit les titres Markdown `### Heading` en titres colorés avec séparateur.
106
+ * @param content - Contenu texte de la synthèse.
107
+ */
87
108
  formatSummaryMessage(content) {
88
109
  return content
89
110
  .split(/\r?\n/)
@@ -93,66 +114,99 @@ class PrettyConsoleRenderer {
93
114
  return line;
94
115
  return [
95
116
  "",
96
- this.c("magenta", heading[1] ?? line),
117
+ this.c("pink", heading[1] ?? line),
97
118
  this.dim("─".repeat(40))
98
119
  ].join("\n");
99
120
  })
100
121
  .join("\n")
101
122
  .trimStart();
102
123
  }
124
+ /** Entoure `value` avec le code couleur ANSI si les couleurs sont activées. */
103
125
  c(color, value) {
104
126
  if (!this.color)
105
127
  return value;
106
128
  return `${codes[color]}${value}${codes.reset}`;
107
129
  }
130
+ /** Applique le style dim (atténué) si les couleurs sont activées. */
108
131
  dim(value) {
109
132
  if (!this.color)
110
133
  return value;
111
134
  return `${codes.dim}${value}${codes.reset}`;
112
135
  }
113
136
  }
137
+ /**
138
+ * Renderer minimaliste sans couleurs ni spinner.
139
+ * Utilisé avec `--plain` ou quand `stdout` n'est pas un TTY.
140
+ */
114
141
  class PlainConsoleRenderer {
142
+ /** Affiche les informations de démarrage du débat en texte brut. */
115
143
  start(options, agents = []) {
116
144
  process.stdout.write(`Sujet: ${options.topic}` + "\n");
117
145
  process.stdout.write(`Agents: ${formatAgentPair(options, agents)}` + "\n");
118
146
  process.stdout.write(`Réponses: ${options.turns} | Synthèse: ${formatSummary(options)} | Contexte: ${formatContext(options)}` + "\n");
119
147
  }
148
+ /** Écrit un avertissement sur `stderr`. */
120
149
  warning(message) {
121
150
  process.stderr.write(`Warning: ${message}\n`);
122
151
  }
152
+ /** Écrit une notice informative sur `stdout`. */
123
153
  notice(message) {
124
154
  process.stdout.write(`Info: ${message}\n`);
125
155
  }
156
+ /** Affiche la progression du tour en texte brut. */
126
157
  turnStart(turn, totalTurns, agent, role) {
127
158
  process.stdout.write(`\n[${turn}/${totalTurns}] ${agent} (${role})...\n`);
128
159
  }
160
+ /** No-op : pas de spinner en mode plain. */
129
161
  thinkingStart(_agent, _role) { }
162
+ /** No-op : pas de spinner à arrêter en mode plain. */
130
163
  thinkingEnd() { }
164
+ /** Écrit le contenu du message agent trimé. */
131
165
  message(content) {
132
166
  process.stdout.write(`${content.trim()}\n`);
133
167
  }
168
+ /** Affiche l'en-tête de la section synthèse en texte brut. */
134
169
  summaryStart(agent, role) {
135
170
  process.stdout.write(`\n[Synthese] ${agent} (${role})...\n`);
136
171
  }
172
+ /** Affiche le chemin du fichier de sortie à la fin du débat. */
137
173
  done(outputPath) {
138
174
  process.stdout.write(`\nDebat exporte: ${outputPath}\n`);
139
175
  }
140
176
  }
177
+ /**
178
+ * Formate la paire d'agents pour l'en-tête : utilise les infos enrichies si disponibles,
179
+ * sinon les noms bruts des options.
180
+ * @param options - Options du débat.
181
+ * @param agents - Infos de démarrage des agents (type, rôle, nom).
182
+ */
141
183
  function formatAgentPair(options, agents) {
142
184
  if (agents.length >= 2) {
143
185
  return `${formatAgentLabel(agents[0])} <-> ${formatAgentLabel(agents[1])}`;
144
186
  }
145
187
  return `${options.agentA} <-> ${options.agentB}`;
146
188
  }
189
+ /**
190
+ * Formate un agent en `nom (rôle, type)` ou `"?"` si absent.
191
+ * @param agent - Info de démarrage de l'agent, ou `undefined`.
192
+ */
147
193
  function formatAgentLabel(agent) {
148
194
  if (!agent) {
149
195
  return "?";
150
196
  }
151
197
  return `${agent.name} (${agent.role}, ${agent.type})`;
152
198
  }
199
+ /**
200
+ * Renvoie le nom de l'agent de synthèse ou `"désactivée"` si la synthèse est désactivée.
201
+ * @param options - Options du débat.
202
+ */
153
203
  function formatSummary(options) {
154
204
  return options.summaryEnabled ? options.summaryAgent ?? options.agentB : "désactivée";
155
205
  }
206
+ /**
207
+ * Renvoie un résumé du contexte injecté (nombre de fichiers ou mention d'absence).
208
+ * @param options - Options du débat.
209
+ */
156
210
  function formatContext(options) {
157
211
  const count = options.files.length;
158
212
  if (count === 0) {
@@ -160,6 +214,7 @@ function formatContext(options) {
160
214
  }
161
215
  return `${count} fichier${count > 1 ? "s" : ""} injecté${count > 1 ? "s" : ""}`;
162
216
  }
217
+ /** Codes d'échappement ANSI utilisés par `PrettyConsoleRenderer`. */
163
218
  const codes = {
164
219
  reset: "\u001b[0m",
165
220
  dim: "\u001b[2m",
@@ -167,5 +222,7 @@ const codes = {
167
222
  cyan: "\u001b[36m",
168
223
  green: "\u001b[32m",
169
224
  magenta: "\u001b[35m",
170
- yellow: "\u001b[33m"
225
+ yellow: "\u001b[33m",
226
+ orange: "\u001b[38;5;208m",
227
+ pink: "\u001b[38;5;205m"
171
228
  };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Renderer NDJSON : émet une ligne JSON par événement DebateRenderer.
3
+ *
4
+ * Cible : intégrations qui pilotent Palabre en out-of-process (extension
5
+ * VS Code, plugin Obsidian, scripts shell). Le rendu humain reste
6
+ * `PrettyConsoleRenderer` ou `PlainConsoleRenderer`.
7
+ *
8
+ * Contrat :
9
+ * - une ligne = un événement JSON valide, terminé par `\n` ;
10
+ * - tous les événements portent un champ `v` (entier) pour le versioning ;
11
+ * - toute évolution cassante du schéma doit incrémenter `v` et documenter
12
+ * la migration dans AGENTS.md (section "Renderer NDJSON").
13
+ *
14
+ * Sortie : stdout uniquement. stderr reste libre pour les messages bas
15
+ * niveau (Node, shell) que les consommateurs peuvent agréger comme ils
16
+ * veulent.
17
+ */
18
+ export class NdjsonRenderer {
19
+ schemaVersion = 1;
20
+ currentSection = "debate";
21
+ currentTurn = 0;
22
+ currentAgent = null;
23
+ currentRole = null;
24
+ /** Émet `start` avec le sujet, les agents, les options principales et le contexte de session. */
25
+ start(options, agents = []) {
26
+ this.emit({
27
+ type: "start",
28
+ topic: options.topic,
29
+ turns: options.turns,
30
+ agents: agents.map((a) => ({ name: a.name, role: a.role, type: a.type })),
31
+ summaryEnabled: options.summaryEnabled,
32
+ summaryAgent: options.summaryEnabled
33
+ ? options.summaryAgent ?? options.agentB
34
+ : null,
35
+ earlyStop: options.earlyStopOnAgreement,
36
+ filesCount: options.files.length,
37
+ session: {
38
+ startedAt: options.session.startedAt,
39
+ localDate: options.session.localDate,
40
+ timeZone: options.session.timeZone,
41
+ cwd: options.session.cwd,
42
+ },
43
+ });
44
+ }
45
+ /** Émet un événement informatif. */
46
+ notice(message) {
47
+ this.emit({ type: "notice", message });
48
+ }
49
+ /** Émet un avertissement. Reste sur stdout pour conserver l'ordre des événements. */
50
+ warning(message) {
51
+ this.emit({ type: "warning", message });
52
+ }
53
+ /** Émet `turn-start` et bascule la section courante en débat. */
54
+ turnStart(turn, totalTurns, agent, role) {
55
+ this.currentSection = "debate";
56
+ this.currentTurn = turn;
57
+ this.currentAgent = agent;
58
+ this.currentRole = role;
59
+ this.emit({ type: "turn-start", turn, totalTurns, agent, role });
60
+ }
61
+ /**
62
+ * Émet `thinking-start`. Les consommateurs UI peuvent l'utiliser pour un
63
+ * indicateur "agent en cours" ; les consommateurs purement data peuvent
64
+ * l'ignorer sans perte d'information sémantique.
65
+ */
66
+ thinkingStart(agent, role) {
67
+ this.emit({ type: "thinking-start", agent, role });
68
+ }
69
+ /** Émet `thinking-end`. */
70
+ thinkingEnd() {
71
+ this.emit({ type: "thinking-end" });
72
+ }
73
+ /**
74
+ * Émet `message` (section débat) ou `summary-message` (section synthèse)
75
+ * selon l'état courant. La discrimination par type permet aux
76
+ * consommateurs de router sans maintenir d'état eux-mêmes.
77
+ */
78
+ message(content) {
79
+ if (this.currentSection === "summary") {
80
+ this.emit({
81
+ type: "summary-message",
82
+ agent: this.currentAgent,
83
+ role: this.currentRole,
84
+ content,
85
+ });
86
+ }
87
+ else {
88
+ this.emit({
89
+ type: "message",
90
+ turn: this.currentTurn,
91
+ agent: this.currentAgent,
92
+ role: this.currentRole,
93
+ content,
94
+ });
95
+ }
96
+ }
97
+ /** Émet `summary-start` et bascule la section courante en synthèse. */
98
+ summaryStart(agent, role) {
99
+ this.currentSection = "summary";
100
+ this.currentAgent = agent;
101
+ this.currentRole = role;
102
+ this.emit({ type: "summary-start", agent, role });
103
+ }
104
+ /** Émet `done` avec le chemin du `.debate.md` écrit. */
105
+ done(outputPath) {
106
+ this.emit({ type: "done", outputPath });
107
+ }
108
+ /** Sérialise un événement et l'écrit sur stdout, terminé par `\n`. */
109
+ emit(event) {
110
+ process.stdout.write(JSON.stringify({ v: this.schemaVersion, ...event }) + "\n");
111
+ }
112
+ }
113
+ /** Factory pratique pour conserver la symétrie avec `createConsoleRenderer`. */
114
+ export function createNdjsonRenderer() {
115
+ return new NdjsonRenderer();
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palabre",
3
- "version": "0.1.7",
3
+ "version": "0.3.0",
4
4
  "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
5
  "license": "MIT",
6
6
  "type": "module",