palabre 0.1.5 → 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.
@@ -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,49 +1,50 @@
1
- {
2
- "name": "palabre",
3
- "version": "0.1.5",
4
- "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
- "license": "MIT",
6
- "type": "module",
7
- "packageManager": "pnpm@10.18.3",
8
- "homepage": "https://github.com/JuReyms/Palabre#readme",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/JuReyms/Palabre.git"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/JuReyms/Palabre/issues"
15
- },
16
- "keywords": [
17
- "ai",
18
- "cli",
19
- "agents",
20
- "ollama",
21
- "codex",
22
- "claude",
23
- "gemini",
24
- "opencode"
25
- ],
26
- "bin": {
27
- "palabre": "dist/index.js"
28
- },
29
- "files": [
30
- "dist/",
31
- "README.md",
32
- "palabre.config.example.json"
33
- ],
34
- "scripts": {
35
- "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
36
- "check": "tsc -p tsconfig.json --noEmit",
37
- "prepack": "pnpm build",
38
- "start": "node ./dist/index.js",
39
- "test": "pnpm build:test && node --test .tmp/test-dist/tests/*.test.js",
40
- "build:test": "node -e \"fs.rmSync('.tmp/test-dist',{recursive:true,force:true})\" && tsc -p tsconfig.test.json"
41
- },
42
- "engines": {
43
- "node": ">=20"
44
- },
45
- "devDependencies": {
46
- "@types/node": "^20.12.0",
47
- "typescript": "^5.4.0"
48
- }
49
- }
1
+ {
2
+ "name": "palabre",
3
+ "version": "0.2.0",
4
+ "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "packageManager": "pnpm@10.18.3",
8
+ "homepage": "https://github.com/JuReyms/Palabre#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/JuReyms/Palabre.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/JuReyms/Palabre/issues"
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "cli",
19
+ "agents",
20
+ "ollama",
21
+ "codex",
22
+ "claude",
23
+ "opencode",
24
+ "debate",
25
+ "orchestrator"
26
+ ],
27
+ "bin": {
28
+ "palabre": "dist/index.js"
29
+ },
30
+ "files": [
31
+ "dist/",
32
+ "README.md",
33
+ "palabre.config.example.json"
34
+ ],
35
+ "scripts": {
36
+ "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
37
+ "check": "tsc -p tsconfig.json --noEmit",
38
+ "prepack": "pnpm build",
39
+ "start": "node ./dist/index.js",
40
+ "test": "pnpm build:test && node --test .tmp/test-dist/tests/*.test.js",
41
+ "build:test": "node -e \"fs.rmSync('.tmp/test-dist',{recursive:true,force:true})\" && tsc -p tsconfig.test.json"
42
+ },
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.12.0",
48
+ "typescript": "^5.4.0"
49
+ }
50
+ }
@@ -66,7 +66,7 @@
66
66
  "shell": true,
67
67
  "role": "reviewer",
68
68
  "tier": "primary"
69
- },
69
+ },
70
70
  "ollama-local": {
71
71
  "type": "ollama",
72
72
  "baseUrl": "http://localhost:11434",
@@ -78,4 +78,4 @@
78
78
  "unloadOtherModels": true
79
79
  }
80
80
  }
81
- }
81
+ }