palabre 0.1.7 → 0.2.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) {
@@ -129,12 +131,16 @@ async function main() {
129
131
  printPromptPreview(config, options);
130
132
  return;
131
133
  }
132
- const renderer = createConsoleRenderer(options.plainOutput);
134
+ const renderer = createRendererFromFlags(parsed.flags, options.plainOutput);
133
135
  context.warnings.forEach((warning) => renderer.warning(warning));
134
136
  const result = await runDebate(config, options, renderer);
135
137
  const outputPath = await writeDebateMarkdown(config.outputDir ?? ".", result.options, result.messages, result.summary, result.stopReason);
136
138
  renderer.done(outputPath);
137
139
  }
140
+ /**
141
+ * Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
142
+ * @param flags - Flags parsés depuis la ligne de commande.
143
+ */
138
144
  async function runAgentsCommand(flags) {
139
145
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
140
146
  if (!(await configExists(configPath))) {
@@ -144,6 +150,10 @@ async function runAgentsCommand(flags) {
144
150
  const discovery = await discoverLocalTools();
145
151
  printAgents(configPath, config, discovery);
146
152
  }
153
+ /**
154
+ * Exécute la commande `config` : wizard interactif ou mise à jour directe des paramètres par défaut.
155
+ * @param flags - Flags parsés depuis la ligne de commande.
156
+ */
147
157
  async function runConfigCommand(flags) {
148
158
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
149
159
  if (!(await configExists(configPath))) {
@@ -203,9 +213,18 @@ async function runConfigCommand(flags) {
203
213
  }
204
214
  await runConfigWizard(configPath, config);
205
215
  }
216
+ /**
217
+ * Renvoie `true` si la valeur représente une désactivation explicite (ex. "none", "0", "disabled").
218
+ * @param value - Chaîne saisie par l'utilisateur.
219
+ */
206
220
  function isNoneValue(value) {
207
221
  return ["0", "none", "aucun", "disabled", "désactivé", "desactive"].includes(value.trim().toLowerCase());
208
222
  }
223
+ /**
224
+ * Formate les paramètres par défaut en une ligne lisible pour les messages console.
225
+ * @param defaults - Objet `defaults` de la config Palabre.
226
+ * @returns Chaîne résumant la paire d'agents, le nombre de réponses et l'agent de synthèse.
227
+ */
209
228
  function formatDefaultsForMessage(defaults) {
210
229
  const pair = defaults.agentA && defaults.agentB
211
230
  ? `agents: ${defaults.agentA} <-> ${defaults.agentB}`
@@ -213,11 +232,26 @@ function formatDefaultsForMessage(defaults) {
213
232
  const summary = defaults.summaryAgent ? `synthèse: ${defaults.summaryAgent}` : "synthèse: agent B";
214
233
  return `${pair}, réponses: ${turnsOrDefault(defaults.turns)}, ${summary}`;
215
234
  }
235
+ /**
236
+ * Lève une erreur si `agentName` n'est pas déclaré dans la config.
237
+ * @param config - Config chargée.
238
+ * @param agentName - Nom de l'agent à vérifier.
239
+ * @param fieldName - Nom du champ (utilisé dans le message d'erreur).
240
+ */
216
241
  function assertKnownAgent(config, agentName, fieldName) {
217
242
  if (!config.agents[agentName]) {
218
243
  throw new Error(`Agent inconnu pour ${fieldName}: ${agentName}. Agents disponibles: ${Object.keys(config.agents).join(", ")}.`);
219
244
  }
220
245
  }
246
+ /**
247
+ * Résout le nom d'un agent selon la priorité : flag CLI > preset > défaut config.
248
+ * Lève une erreur si aucune source ne fournit de valeur.
249
+ * @param label - Libellé humain utilisé dans le message d'erreur (ex. "agent A").
250
+ * @param explicitValue - Valeur passée via flag CLI.
251
+ * @param presetValue - Valeur issue du preset sélectionné.
252
+ * @param defaultValue - Valeur issue des défauts de la config.
253
+ * @returns Nom de l'agent résolu.
254
+ */
221
255
  function resolveAgentName(label, explicitValue, presetValue, defaultValue) {
222
256
  const resolved = optionalString(explicitValue) ?? presetValue ?? defaultValue;
223
257
  if (!resolved) {
@@ -225,6 +259,11 @@ function resolveAgentName(label, explicitValue, presetValue, defaultValue) {
225
259
  }
226
260
  return resolved;
227
261
  }
262
+ /**
263
+ * Affiche un aperçu du prompt du premier tour sans appeler aucun agent (flag `--show-prompt`).
264
+ * @param config - Config chargée.
265
+ * @param options - Options du débat résolues.
266
+ */
228
267
  function printPromptPreview(config, options) {
229
268
  const agentConfig = config.agents[options.agentA];
230
269
  if (!agentConfig) {
@@ -250,9 +289,56 @@ function printPromptPreview(config, options) {
250
289
  console.log("");
251
290
  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
291
  }
292
+ /**
293
+ * Extrait une chaîne non vide depuis une valeur de flag, ou renvoie `undefined`.
294
+ * @param value - Valeur brute issue du parseur de flags.
295
+ */
253
296
  function optionalString(value) {
254
297
  return typeof value === "string" && value.trim() ? value : undefined;
255
298
  }
299
+ /** Liste des kinds de renderer acceptés par `--renderer`. */
300
+ const SUPPORTED_RENDERERS = ["auto", "pretty", "plain", "ndjson"];
301
+ /**
302
+ * Instancie le renderer en fonction des flags CLI.
303
+ *
304
+ * Précédence :
305
+ * 1. `--renderer <kind>` (canonique).
306
+ * 2. `--json` (alias pour `--renderer ndjson`).
307
+ * 3. `--plain` (rétro-compatible, équivalent `--renderer plain`).
308
+ * 4. par défaut : `auto` (pretty si TTY, plain sinon, hérité de `createConsoleRenderer`).
309
+ *
310
+ * Lève si la valeur de `--renderer` n'est pas dans `SUPPORTED_RENDERERS`.
311
+ */
312
+ function createRendererFromFlags(flags, plainOutputFallback) {
313
+ const explicit = optionalString(flags.renderer);
314
+ if (explicit) {
315
+ if (!SUPPORTED_RENDERERS.includes(explicit)) {
316
+ throw new Error(`Renderer inconnu: ${explicit}. Valeurs supportées: ${SUPPORTED_RENDERERS.join(", ")}.`);
317
+ }
318
+ const kind = explicit;
319
+ switch (kind) {
320
+ case "ndjson":
321
+ return createNdjsonRenderer();
322
+ case "plain":
323
+ return createConsoleRenderer(true);
324
+ case "pretty":
325
+ return createConsoleRenderer(false);
326
+ case "auto":
327
+ return createConsoleRenderer(plainOutputFallback);
328
+ }
329
+ }
330
+ if (flags.json) {
331
+ return createNdjsonRenderer();
332
+ }
333
+ return createConsoleRenderer(plainOutputFallback);
334
+ }
335
+ /**
336
+ * Parse `process.argv` en une structure typée `ParsedArgs`.
337
+ * Gère les flags courts (-h, -v, -s, -t, -a), les flags longs (--topic, --agent-a…),
338
+ * les flags multi-valeurs (--files, --context, --set-defaults) et les positionnels.
339
+ * @param args - Tableau d'arguments (généralement `process.argv.slice(2)`).
340
+ * @returns Commande détectée, indicateur d'explicitation et map de flags.
341
+ */
256
342
  function parseArgs(args) {
257
343
  const flags = {};
258
344
  let command = "run";
@@ -352,6 +438,12 @@ function parseArgs(args) {
352
438
  }
353
439
  return { command, commandExplicit, flags };
354
440
  }
441
+ /**
442
+ * Détecte si une valeur ressemble à une faute de frappe d'une commande connue
443
+ * (même première lettre et distance de Levenshtein ≤ 2).
444
+ * @param value - Token saisi par l'utilisateur.
445
+ * @param commands - Ensemble des commandes valides.
446
+ */
355
447
  function isLikelyCommandTypo(value, commands) {
356
448
  const normalized = value.toLowerCase();
357
449
  for (const command of commands) {
@@ -361,6 +453,12 @@ function isLikelyCommandTypo(value, commands) {
361
453
  }
362
454
  return false;
363
455
  }
456
+ /**
457
+ * Calcule la distance de Levenshtein entre deux chaînes (insertions, suppressions, substitutions).
458
+ * @param left - Première chaîne.
459
+ * @param right - Deuxième chaîne.
460
+ * @returns Distance entière ≥ 0.
461
+ */
364
462
  function levenshteinDistance(left, right) {
365
463
  const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
366
464
  for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
@@ -376,6 +474,14 @@ function levenshteinDistance(left, right) {
376
474
  }
377
475
  return previous[right.length] ?? 0;
378
476
  }
477
+ /**
478
+ * Interprète les arguments positionnels pour la commande `run` :
479
+ * premier positionnel = preset si connu, sinon sujet complet concaténé.
480
+ * @param positionals - Arguments positionnels extraits du parseur.
481
+ * @param flags - Map de flags à muter si un preset ou un sujet est détecté.
482
+ * @param presets - Ensemble des noms de presets valides.
483
+ * @param commandExplicit - `true` si l'utilisateur a tapé `palabre run` explicitement.
484
+ */
379
485
  function applyRunPositionals(positionals, flags, presets, commandExplicit) {
380
486
  if (positionals.length === 0) {
381
487
  return;
@@ -393,6 +499,10 @@ function applyRunPositionals(positionals, flags, presets, commandExplicit) {
393
499
  }
394
500
  flags.topic ??= positionals.join(" ");
395
501
  }
502
+ /**
503
+ * Normalise un nom de flag long en son alias canonique (ex. `subject` → `topic`).
504
+ * @param value - Nom brut extrait après `--`.
505
+ */
396
506
  function normalizeFlagName(value) {
397
507
  const aliases = {
398
508
  s: "topic",
@@ -401,6 +511,10 @@ function normalizeFlagName(value) {
401
511
  };
402
512
  return aliases[value] ?? value;
403
513
  }
514
+ /**
515
+ * Indique si un flag long nécessite une valeur suivante (lève une erreur si absente).
516
+ * @param value - Nom canonique du flag (sans `--`).
517
+ */
404
518
  function requiresFlagValue(value) {
405
519
  return new Set([
406
520
  "agent-a",
@@ -416,12 +530,18 @@ function requiresFlagValue(value) {
416
530
  "turns"
417
531
  ]).has(value);
418
532
  }
533
+ /** Lit la version depuis `package.json` adjacent au bundle compilé. */
419
534
  async function getPackageVersion() {
420
535
  const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
421
536
  const raw = await readFile(packageJsonPath, "utf8");
422
537
  const packageJson = JSON.parse(raw);
423
538
  return packageJson.version ?? "0.0.0";
424
539
  }
540
+ /**
541
+ * Normalise une valeur de flag multi-valeur en tableau de chaînes.
542
+ * @param value - Valeur brute (tableau, chaîne unique ou absent).
543
+ * @returns Tableau de chaînes, vide si la valeur n'est pas applicable.
544
+ */
425
545
  function getStringListFlag(value) {
426
546
  if (Array.isArray(value)) {
427
547
  return value;
@@ -431,11 +551,22 @@ function getStringListFlag(value) {
431
551
  }
432
552
  return [];
433
553
  }
554
+ /**
555
+ * Écrit les avertissements de contexte sur `stderr`.
556
+ * @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
557
+ */
434
558
  function printContextWarnings(warnings) {
435
559
  for (const warning of warnings) {
436
560
  process.stderr.write(`Warning: ${warning}\n`);
437
561
  }
438
562
  }
563
+ /**
564
+ * Ajoute dans `config.agents` les agents détectés localement mais absents de la config.
565
+ * Mute `config` directement ; l'appelant est responsable de persister la config.
566
+ * @param config - Config Palabre à compléter.
567
+ * @param discovery - Résultat de la découverte locale des outils.
568
+ * @returns Noms des agents nouvellement ajoutés.
569
+ */
439
570
  function syncDetectedAgents(config, discovery) {
440
571
  const discoveredConfig = createConfigFromDiscovery(discovery);
441
572
  const missingAgents = findDetectedMissingAgents(config, discovery);
@@ -444,6 +575,11 @@ function syncDetectedAgents(config, discovery) {
444
575
  }
445
576
  return missingAgents;
446
577
  }
578
+ /**
579
+ * Renvoie les noms des agents détectés localement qui ne sont pas encore dans `config.agents`.
580
+ * @param config - Config Palabre existante.
581
+ * @param discovery - Résultat de la découverte locale des outils.
582
+ */
447
583
  function findDetectedMissingAgents(config, discovery) {
448
584
  const detectedAgents = [
449
585
  discovery.codex.available ? "codex" : undefined,
@@ -454,6 +590,12 @@ function findDetectedMissingAgents(config, discovery) {
454
590
  ].filter((agent) => Boolean(agent));
455
591
  return detectedAgents.filter((agentName) => !config.agents[agentName]);
456
592
  }
593
+ /**
594
+ * Affiche la liste des agents déclarés avec leur type, rôle, état de détection et défauts.
595
+ * @param configPath - Chemin du fichier de config (affiché en en-tête).
596
+ * @param config - Config Palabre chargée.
597
+ * @param discovery - Résultat de la découverte locale des outils.
598
+ */
457
599
  function printAgents(configPath, config, discovery) {
458
600
  const entries = Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right));
459
601
  console.log(`Config: ${configPath}`);
@@ -472,6 +614,11 @@ function printAgents(configPath, config, discovery) {
472
614
  console.log("");
473
615
  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
616
  }
617
+ /**
618
+ * Renvoie un libellé indiquant si l'agent est agent A, agent B ou agent de synthèse par défaut.
619
+ * @param name - Nom de l'agent.
620
+ * @param config - Config Palabre contenant les défauts.
621
+ */
475
622
  function formatAgentDefaults(name, config) {
476
623
  const labels = [];
477
624
  if (config.defaults?.agentA === name)
@@ -482,12 +629,23 @@ function formatAgentDefaults(name, config) {
482
629
  labels.push("synthèse par défaut");
483
630
  return labels.join(", ");
484
631
  }
632
+ /**
633
+ * Renvoie une ligne de détails pour un agent : commande CLI ou modèle Ollama.
634
+ * @param agentConfig - Configuration de l'agent.
635
+ */
485
636
  function formatAgentDetails(agentConfig) {
486
637
  if (agentConfig.type === "ollama") {
487
638
  return `modèle: ${agentConfig.model}`;
488
639
  }
489
640
  return `commande: ${agentConfig.command}${agentConfig.model ? ` | modèle: ${agentConfig.model}` : ""}`;
490
641
  }
642
+ /**
643
+ * Renvoie le statut de détection d'un agent sous forme de chaîne lisible.
644
+ * Pour Ollama, vérifie la disponibilité du serveur et la présence du modèle.
645
+ * @param name - Nom de l'agent dans la config.
646
+ * @param agentConfig - Configuration de l'agent.
647
+ * @param discovery - Résultat de la découverte locale des outils.
648
+ */
491
649
  function formatAgentDetection(name, agentConfig, discovery) {
492
650
  if (agentConfig.type === "ollama") {
493
651
  if (!discovery.ollama.available) {
@@ -500,6 +658,13 @@ function formatAgentDetection(name, agentConfig, discovery) {
500
658
  const detection = cliDetectionForAgent(name, agentConfig, discovery);
501
659
  return detection.available ? `détecté (${detection.command})` : "non détecté";
502
660
  }
661
+ /**
662
+ * Résout l'entrée de détection correspondant à un agent CLI dans le résultat de découverte.
663
+ * Renvoie un objet `{ available: true }` pour les agents CLI non reconnus (considérés disponibles).
664
+ * @param name - Nom de l'agent dans la config.
665
+ * @param agentConfig - Configuration de l'agent.
666
+ * @param discovery - Résultat de la découverte locale des outils.
667
+ */
503
668
  function cliDetectionForAgent(name, agentConfig, discovery) {
504
669
  const command = normalizeCommandName(agentConfig.type === "cli" ? agentConfig.command : name);
505
670
  if (command === "codex")
@@ -512,9 +677,18 @@ function cliDetectionForAgent(name, agentConfig, discovery) {
512
677
  return discovery.opencode;
513
678
  return { available: true, command: agentConfig.type === "cli" ? agentConfig.command : name };
514
679
  }
680
+ /**
681
+ * Extrait le nom de base d'une commande en supprimant le chemin et l'extension Windows éventuelle.
682
+ * @param command - Chemin ou nom de commande brut (ex. `C:\bin\claude.cmd`).
683
+ */
515
684
  function normalizeCommandName(command) {
516
685
  return path.basename(command).replace(/\.(cmd|exe|ps1|bat)$/i, "").toLowerCase();
517
686
  }
687
+ /**
688
+ * Affiche le récapitulatif de détection locale après `palabre init`.
689
+ * @param discovery - Résultat de la découverte locale des outils.
690
+ * @param config - Config générée à partir de la découverte.
691
+ */
518
692
  function printInitDiscovery(discovery, config) {
519
693
  console.log("");
520
694
  console.log("Détection locale:");
@@ -526,11 +700,19 @@ function printInitDiscovery(discovery, config) {
526
700
  console.log("");
527
701
  console.log(`Défauts: ${config.defaults?.agentA ?? "codex"} <-> ${config.defaults?.agentB ?? "ollama-local"}`);
528
702
  }
703
+ /**
704
+ * Formate le statut de détection d'un outil CLI (disponible ou non).
705
+ * @param detection - Résultat de détection d'un outil CLI.
706
+ */
529
707
  function formatCommandDetection(detection) {
530
708
  return detection.available
531
709
  ? `détecté (${detection.command})`
532
710
  : "non détecté";
533
711
  }
712
+ /**
713
+ * Formate le statut de détection d'Ollama : commande absente, serveur injoignable ou modèles disponibles.
714
+ * @param detection - Résultat de détection d'Ollama.
715
+ */
534
716
  function formatOllamaDetection(detection) {
535
717
  if (!detection.available) {
536
718
  return detection.commandAvailable
@@ -540,54 +722,90 @@ function formatOllamaDetection(detection) {
540
722
  const modelCount = detection.models.length;
541
723
  return `détectée (${modelCount} modèle${modelCount > 1 ? "s" : ""})`;
542
724
  }
725
+ /** Affiche le texte d'aide complet sur `stdout`. */
543
726
  function printHelp() {
544
727
  console.log(`
545
728
  PALABRE
729
+ _____________________________________________
546
730
 
547
731
  Usage rapide:
732
+
733
+ palabre init
734
+ Crée une config globale et détecte les agents AI disponibles sur la machine.
735
+
736
+ palabre agents
737
+ Affiche les agents déclarés dans la config.
738
+
739
+ palabre config
740
+ Assistant pour définir ou supprimer les paramètres par défaut.
741
+
548
742
  palabre new
549
743
  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.
744
+
552
745
  palabre claude-gemini "Sujet" -t 4
553
746
  Lance avec un preset et un sujet positionnel.
554
747
 
748
+ palabre "Sujet"
749
+ Lance le débat avec paramètres par défaut de la config.
750
+
751
+ _____________________________________________
752
+
753
+
555
754
  Commandes:
755
+
556
756
  palabre init [--local]
557
- Crée une config et détecte Codex, Claude, Gemini, OpenCode et Ollama.
757
+ Crée une config locale et détecte Codex, Claude, Gemini, OpenCode et Ollama.
758
+
558
759
  palabre agents [--config <path>]
559
760
  Liste les agents déclarés dans la config et leur détection locale.
761
+
560
762
  palabre config
561
763
  Assistant pour définir ou supprimer les paramètres par défaut.
764
+
562
765
  palabre config --set-defaults <agentA> <agentB> [-t <n>] [--summary-agent <name>]
563
766
  Définit les agents par défaut, et optionnellement les réponses et la synthèse.
767
+
564
768
  palabre config -t <n>
565
769
  Définit seulement le nombre de réponses par défaut.
770
+
566
771
  palabre config --summary-agent <name|none>
567
772
  Définit ou retire seulement l'agent de synthèse par défaut.
773
+
568
774
  palabre config --clear-defaults
569
775
  Supprime les paramètres par défaut.
776
+
570
777
  palabre doctor [--config <path>]
571
778
  Vérifie la config et les outils locaux.
779
+
572
780
  palabre update [--apply]
573
781
  Affiche ou exécute les étapes de mise à jour d'un checkout git.
782
+
574
783
  palabre help
575
784
  Affiche cette aide. Identique à -h ou --help.
785
+
576
786
  palabre version
577
787
  Affiche la version. Identique à -v ou --version.
578
788
 
789
+ _____________________________________________
790
+
791
+
579
792
  Notation:
793
+
580
794
  [option] signifie facultatif. Ne tape pas les crochets.
581
795
  <valeur> signifie qu'il faut remplacer ce texte par ta valeur.
582
796
 
583
797
  Options générales:
798
+
584
799
  -h, --help Affiche cette aide
585
800
  -v, --version Affiche la version
586
- -a Liste les agents. Identique à palabre agents
801
+ -a, --agents Liste les agents. Identique à palabre agents
587
802
  --config <path> Chemin vers un fichier de config explicite
588
803
  --plain Utilise le rendu console simple sans habillage TUI
804
+ --json Émet un événement NDJSON par ligne sur stdout (alias de --renderer ndjson)
805
+ --renderer <kind> Force le renderer : auto | pretty | plain | ndjson
589
806
 
590
807
  Sujet et lancement:
808
+
591
809
  -s, --subject <text> Sujet du débat, option recommandée
592
810
  --topic <text> Alias compatible de --subject
593
811
  --agent-a <name> Premier agent
@@ -597,21 +815,25 @@ Sujet et lancement:
597
815
  --no-early-stop Désactive l'arrêt anticipé si les agents sont clairement d'accord
598
816
 
599
817
  Modèles:
818
+
600
819
  --model-a <model> Modèle brut transmis à l'agent A
601
820
  --model-b <model> Modèle brut transmis à l'agent B
602
821
  --pull-models Autorise Ollama à télécharger un modèle manquant
603
822
 
604
823
  Synthèse:
824
+
605
825
  --summary-agent <name> Agent utilisé pour produire la synthèse finale
606
826
  --summary-model <model> Modèle brut transmis à l'agent de synthèse
607
827
  --no-summary Désactive la synthèse finale
608
828
 
609
829
  Contexte:
830
+
610
831
  --files <paths...> Fichiers texte à injecter explicitement dans le contexte
611
832
  --context <paths...> Scanne fichiers/dossiers texte en respectant les limites de contexte
612
833
  --show-prompt Affiche le prompt du premier tour sans appeler d'agent
613
834
 
614
835
  Configuration:
836
+
615
837
  --local Avec init/setup, crée ./palabre.config.json
616
838
  --set-defaults <a b> Avec config, définit les agents par défaut
617
839
  --summary-agent <name> Avec config, définit l'agent de synthèse par défaut
@@ -620,10 +842,18 @@ Configuration:
620
842
  --sync-agents Avec config, ajoute les agents détectés manquants
621
843
 
622
844
  Mise à jour:
845
+
623
846
  --apply Avec update, exécute les étapes de mise à jour
624
847
 
848
+ _____________________________________________
849
+
850
+
625
851
  Presets disponibles:
852
+
626
853
  ${listPresetNames().join(", ")}
854
+
855
+ _____________________________________________
856
+
627
857
  `);
628
858
  }
629
859
  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.2.0",
4
4
  "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
5
  "license": "MIT",
6
6
  "type": "module",