palabre 0.9.1 → 0.10.1

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.
Files changed (52) hide show
  1. package/README.md +6 -0
  2. package/dist/adapters/cli-pty.js +30 -10
  3. package/dist/adapters/cli-shared.js +73 -0
  4. package/dist/adapters/cli.js +40 -77
  5. package/dist/adapters/index.js +1 -0
  6. package/dist/adapters/ollama.js +32 -32
  7. package/dist/adapters/terminal.js +1 -0
  8. package/dist/args.js +1 -0
  9. package/dist/commands/agents.js +7 -1
  10. package/dist/commands/context.js +7 -1
  11. package/dist/commands/history.js +6 -1
  12. package/dist/commands/init.js +8 -3
  13. package/dist/commands/presets.js +6 -1
  14. package/dist/commands/shared.js +5 -1
  15. package/dist/commands/update.js +6 -1
  16. package/dist/config.js +17 -1
  17. package/dist/configWizard.js +5 -4
  18. package/dist/context.js +1 -0
  19. package/dist/contextScan.js +4 -3
  20. package/dist/discovery.js +1 -0
  21. package/dist/doctor.js +1 -0
  22. package/dist/errors.js +4 -0
  23. package/dist/exec.js +1 -0
  24. package/dist/history.js +10 -0
  25. package/dist/i18n.js +2 -0
  26. package/dist/index.js +170 -112
  27. package/dist/limits.js +4 -0
  28. package/dist/messages/adapter-errors.js +26 -2
  29. package/dist/messages/config.js +6 -0
  30. package/dist/messages/index.js +1 -0
  31. package/dist/messages/renderers.js +10 -2
  32. package/dist/messages/tui.js +8 -2
  33. package/dist/new.js +65 -11
  34. package/dist/ollamaUrl.js +20 -0
  35. package/dist/orchestrator.js +103 -150
  36. package/dist/output.js +1 -0
  37. package/dist/presets.js +1 -0
  38. package/dist/prompt.js +1 -0
  39. package/dist/renderers/console.js +1 -1
  40. package/dist/renderers/tui-prompts.js +343 -0
  41. package/dist/renderers/tui-renderer.js +228 -0
  42. package/dist/renderers/tui-screens.js +352 -0
  43. package/dist/renderers/tui-theme.js +356 -0
  44. package/dist/renderers/tui.js +7 -1086
  45. package/dist/runOptions.js +33 -2
  46. package/dist/session.js +1 -0
  47. package/dist/tuiController.js +61 -16
  48. package/dist/tuiState.js +4 -0
  49. package/dist/types.js +1 -0
  50. package/dist/update.js +1 -0
  51. package/dist/version.js +1 -0
  52. package/package.json +1 -1
package/dist/new.js CHANGED
@@ -1,9 +1,12 @@
1
+ /** @file Assistant interactif `palabre new` : compose les mêmes flags qu'un lancement direct, sans second chemin d'exécution. */
1
2
  import { createInterface } from "node:readline/promises";
2
3
  import { stdin as input, stdout as output } from "node:process";
3
4
  import { isAgentDetected } from "./agentRegistry.js";
4
5
  import { discoverLocalTools } from "./discovery.js";
5
6
  import { findPresetNameForPair } from "./presets.js";
6
7
  import { MAX_TURNS, turnsOrDefault, validateTurns } from "./limits.js";
8
+ import { accent, bold, brandHeader, card, clearScreen, dim, padBlock, supportsInteractiveOutput, surfaceWidth } from "./renderers/tui-theme.js";
9
+ const interruptedAnswer = "\u0000palabre-interrupted";
7
10
  /**
8
11
  * Lance le wizard interactif `palabre new`.
9
12
  * Détecte les outils locaux, liste les agents de la config et guide la composition du débat.
@@ -17,10 +20,7 @@ export async function runNewWizard(config, messages) {
17
20
  }
18
21
  const rl = await createQuestioner();
19
22
  try {
20
- console.log(messages.new.title);
21
- console.log(messages.new.quitHint);
22
- console.log(messages.new.defaultHint);
23
- console.log("");
23
+ renderWizardIntro(messages);
24
24
  const mode = await askMode(rl, config.defaults?.mode ?? "debate", messages);
25
25
  if (!mode)
26
26
  return undefined;
@@ -175,11 +175,21 @@ async function askMode(rl, defaultMode, messages) {
175
175
  { value: "ask", label: messages.new.modeAsk }
176
176
  ];
177
177
  const fallback = choices.find((choice) => choice.value === defaultMode)?.value ?? "debate";
178
- console.log(messages.new.mode);
179
- choices.forEach((choice, index) => {
180
- const marker = choice.value === fallback ? "(*)" : " ";
181
- console.log(` ${index + 1}) ${marker} ${choice.label}`);
182
- });
178
+ if (supportsInteractiveOutput) {
179
+ const lines = choices.map((choice, index) => {
180
+ const marker = choice.value === fallback ? accent("(*)") : " ";
181
+ return `${bold(`${index + 1})`)} ${marker} ${choice.label}`;
182
+ });
183
+ console.log(padBlock(card(lines, surfaceWidth(), messages.new.mode)).join("\n"));
184
+ console.log("");
185
+ }
186
+ else {
187
+ console.log(messages.new.mode);
188
+ choices.forEach((choice, index) => {
189
+ const marker = choice.value === fallback ? "(*)" : " ";
190
+ console.log(` ${index + 1}) ${marker} ${choice.label}`);
191
+ });
192
+ }
183
193
  while (true) {
184
194
  const answer = await rl.question(`${messages.new.mode} [${fallback}]: `);
185
195
  const value = answer.trim().toLowerCase();
@@ -200,7 +210,33 @@ async function askMode(rl, defaultMode, messages) {
200
210
  }
201
211
  async function createQuestioner() {
202
212
  if (input.isTTY) {
203
- return createInterface({ input, output });
213
+ const rl = createInterface({ input, output });
214
+ return {
215
+ question(prompt) {
216
+ return new Promise((resolve, reject) => {
217
+ let settled = false;
218
+ const cleanup = () => rl.off("SIGINT", onSigint);
219
+ const settle = (value) => {
220
+ if (settled)
221
+ return;
222
+ settled = true;
223
+ cleanup();
224
+ resolve(value);
225
+ };
226
+ const onSigint = () => settle(interruptedAnswer);
227
+ rl.once("SIGINT", onSigint);
228
+ rl.question(prompt).then(settle, (error) => {
229
+ if (settled)
230
+ return;
231
+ cleanup();
232
+ reject(error);
233
+ });
234
+ });
235
+ },
236
+ close() {
237
+ rl.close();
238
+ }
239
+ };
204
240
  }
205
241
  const lines = await readPipedLines();
206
242
  let index = 0;
@@ -375,7 +411,25 @@ function uniqueNames(names) {
375
411
  return names.filter((name, index) => names.indexOf(name) === index);
376
412
  }
377
413
  function isQuit(value) {
378
- return ["q", "quit", "exit"].includes(value.toLowerCase());
414
+ return value === interruptedAnswer || ["q", "quit", "exit"].includes(value.toLowerCase());
415
+ }
416
+ function renderWizardIntro(messages) {
417
+ if (!supportsInteractiveOutput) {
418
+ console.log(messages.new.title);
419
+ console.log(messages.new.quitHint);
420
+ console.log(messages.new.defaultHint);
421
+ console.log("");
422
+ return;
423
+ }
424
+ clearScreen();
425
+ console.log("");
426
+ console.log(padBlock([brandHeader(messages.new.title)]).join("\n"));
427
+ console.log("");
428
+ console.log(padBlock([
429
+ dim(messages.new.quitHint),
430
+ dim(messages.new.defaultHint)
431
+ ]).join("\n"));
432
+ console.log("");
379
433
  }
380
434
  function printCommandPreview(selection, messages) {
381
435
  const explicitCommand = buildExplicitCommand(selection);
package/dist/ollamaUrl.js CHANGED
@@ -1,4 +1,10 @@
1
+ /** Serveur Ollama utilisé quand aucune autre source (flag, env, config) n'est fournie. */
1
2
  export const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
3
+ /**
4
+ * Erreur levée par `normalizeOllamaBaseUrl` quand la valeur fournie n'est pas une URL HTTP(S) exploitable.
5
+ * Le message du constructeur est un texte technique de repli non localisé ; les appelants CLI/TUI
6
+ * doivent reformater `kind`/`value`/`protocol` via les messages traduits plutôt que d'afficher ce message brut.
7
+ */
2
8
  export class OllamaUrlError extends Error {
3
9
  kind;
4
10
  value;
@@ -15,6 +21,20 @@ export class OllamaUrlError extends Error {
15
21
  this.name = "OllamaUrlError";
16
22
  }
17
23
  }
24
+ /**
25
+ * Formate une `OllamaUrlError` en message localisé actionnable.
26
+ * Source unique du mapping `kind` → message, partagée entre le gestionnaire
27
+ * d'erreur du point d'entrée CLI et la conversion en `DebateFailure` de l'orchestrateur.
28
+ */
29
+ export function formatOllamaUrlError(error, messages) {
30
+ if (error.kind === "empty") {
31
+ return messages.common.ollamaUrlEmpty;
32
+ }
33
+ if (error.kind === "protocol") {
34
+ return messages.common.ollamaUrlProtocol(error.protocol ?? "");
35
+ }
36
+ return messages.common.ollamaUrlInvalid(error.value);
37
+ }
18
38
  /**
19
39
  * Résout l'adresse client Ollama selon la priorité produit :
20
40
  * flag CLI > OLLAMA_HOST > config agent > serveur local par défaut.
@@ -1,9 +1,12 @@
1
+ /** @file Boucle d'orchestration des modes `debate` et `ask` : tours, arrêt anticipé et synthèse finale. */
1
2
  import { createAgent } from "./adapters/index.js";
2
3
  import { AdapterError } from "./errors.js";
3
4
  import { createTranslator } from "./i18n.js";
4
5
  import { MAX_ASK_AGENTS } from "./limits.js";
5
- import { OllamaUrlError } from "./ollamaUrl.js";
6
+ import { formatOllamaUrlError, OllamaUrlError } from "./ollamaUrl.js";
6
7
  export { MAX_ASK_AGENTS } from "./limits.js";
8
+ /** Rôle imposé à l'agent de synthèse, indépendant de son rôle configuré. */
9
+ const SUMMARY_ROLE = "summarizer";
7
10
  /**
8
11
  * Point d'entrée de l'orchestration.
9
12
  * Lance le ping-pong entre `agentA` et `agentB` pendant `options.turns` tours,
@@ -52,86 +55,36 @@ export async function runDebate(config, options, renderer, messages = createTran
52
55
  const peer = agents[(index + 1) % agents.length];
53
56
  const turn = index + 1;
54
57
  renderer?.turnStart(turn, options.turns, current.name, current.role);
55
- renderer?.thinkingStart(current.name, current.role);
56
- let response;
57
- try {
58
- response = await current.generate({
59
- topic: options.topic,
60
- turn,
61
- totalTurns: options.turns,
62
- selfName: current.name,
63
- peerName: peer.name,
64
- selfRole: current.role,
65
- language: options.language,
66
- session: options.session,
67
- files: options.files,
68
- transcript,
69
- signal: options.signal
70
- });
71
- }
72
- catch (error) {
73
- const failure = toDebateFailure(error, {
74
- phase: "debate",
75
- agent: current.name,
76
- role: current.role,
77
- turn
78
- }, messages);
79
- renderer?.error(failure);
58
+ const outcome = await generateTurnMessage(current, {
59
+ topic: options.topic,
60
+ turn,
61
+ totalTurns: options.turns,
62
+ selfName: current.name,
63
+ peerName: peer.name,
64
+ selfRole: current.role,
65
+ language: options.language,
66
+ session: options.session,
67
+ files: options.files,
68
+ transcript,
69
+ signal: options.signal
70
+ }, { phase: "debate", agent: current.name, role: current.role, turn }, renderer, messages);
71
+ if (outcome.failure) {
80
72
  return {
81
73
  options,
82
74
  messages: transcript,
83
75
  stopReason,
84
- failure
76
+ failure: outcome.failure
85
77
  };
86
78
  }
87
- finally {
88
- renderer?.thinkingEnd();
89
- }
90
- const message = {
91
- agent: current.name,
92
- role: current.role,
93
- content: response.content,
94
- createdAt: new Date().toISOString()
95
- };
96
- transcript.push(message);
97
- renderer?.message(message.content);
79
+ transcript.push(outcome.message);
80
+ renderer?.message(outcome.message.content);
98
81
  if (shouldStopOnAgreement(options, transcript, messages)) {
99
82
  stopReason = messages.orchestrator.agreementStopReason;
100
83
  renderer?.notice(messages.orchestrator.earlyStop(stopReason));
101
84
  break;
102
85
  }
103
86
  }
104
- let summary;
105
- let failure;
106
- if (options.summaryEnabled) {
107
- try {
108
- const cancellation = cancellationFailureIfAborted(options, messages, {
109
- phase: "summary",
110
- agent: options.summaryAgent,
111
- role: summaryRole(),
112
- turn: transcript.length + 1
113
- });
114
- if (cancellation) {
115
- renderer?.error(cancellation);
116
- return {
117
- options,
118
- messages: transcript,
119
- stopReason,
120
- failure: cancellation
121
- };
122
- }
123
- summary = await generateSummary(config, options, transcript, renderer, messages);
124
- }
125
- catch (error) {
126
- failure = toDebateFailure(error, {
127
- phase: "summary",
128
- agent: options.summaryAgent,
129
- role: summaryRole(),
130
- turn: transcript.length + 1
131
- }, messages);
132
- renderer?.error(failure);
133
- }
134
- }
87
+ const { summary, failure } = await runSummaryPhase(config, options, transcript, renderer, messages);
135
88
  return {
136
89
  options,
137
90
  messages: transcript,
@@ -145,6 +98,8 @@ export async function runDebate(config, options, renderer, messages = createTran
145
98
  * puis un agent de synthèse résume fidèlement chaque réponse et les compare.
146
99
  */
147
100
  export async function runAsk(config, options, renderer, messages = createTranslator("fr")) {
101
+ // `runAsk` est appelable directement, hors du chemin CLI : on revalide la liste
102
+ // ask même si `resolveRunOptions` l'a déjà normalisée pour `palabre run`.
148
103
  const askAgentNames = resolveAskAgentNames(options);
149
104
  if (askAgentNames.length === 0) {
150
105
  throw new Error(messages.common.noAgentDefined("ask agent"));
@@ -159,7 +114,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
159
114
  }
160
115
  return [name, agentConfig];
161
116
  });
162
- warnIfOllamaHasNoContext(options, agentEntries.map(([name, agentConfig]) => [name, agentConfig]), renderer, messages);
117
+ warnIfOllamaHasNoContext(options, agentEntries, renderer, messages);
163
118
  renderer?.start(options, agentEntries.map(([name, agentConfig]) => ({
164
119
  name,
165
120
  role: agentConfig.role,
@@ -190,86 +145,36 @@ export async function runAsk(config, options, renderer, messages = createTransla
190
145
  else {
191
146
  renderer?.turnStart(response, agents.length, current.name, current.role);
192
147
  }
193
- renderer?.thinkingStart(current.name, current.role);
194
- let agentResponse;
195
- try {
196
- agentResponse = await current.generate({
197
- topic: options.topic,
198
- turn: response,
199
- totalTurns: agents.length,
200
- selfName: current.name,
201
- peerName: "independent-agents",
202
- selfRole: current.role,
203
- mode: "ask",
204
- language: options.language,
205
- session: options.session,
206
- files: options.files,
207
- transcript: [],
208
- signal: options.signal
209
- });
210
- }
211
- catch (error) {
212
- const failure = toDebateFailure(error, {
213
- phase: "ask",
214
- agent: current.name,
215
- role: current.role,
216
- turn: response
217
- }, messages);
218
- renderer?.error(failure);
148
+ const outcome = await generateTurnMessage(current, {
149
+ topic: options.topic,
150
+ turn: response,
151
+ totalTurns: agents.length,
152
+ selfName: current.name,
153
+ peerName: "independent-agents",
154
+ selfRole: current.role,
155
+ mode: "ask",
156
+ language: options.language,
157
+ session: options.session,
158
+ files: options.files,
159
+ transcript: [],
160
+ signal: options.signal
161
+ }, { phase: "ask", agent: current.name, role: current.role, turn: response }, renderer, messages);
162
+ if (outcome.failure) {
219
163
  return {
220
164
  options,
221
165
  messages: transcript,
222
- failure
166
+ failure: outcome.failure
223
167
  };
224
168
  }
225
- finally {
226
- renderer?.thinkingEnd();
227
- }
228
- const message = {
229
- agent: current.name,
230
- role: current.role,
231
- content: agentResponse.content,
232
- createdAt: new Date().toISOString()
233
- };
234
- transcript.push(message);
169
+ transcript.push(outcome.message);
235
170
  if (renderer?.askResponseMessage) {
236
- renderer.askResponseMessage(message.content);
171
+ renderer.askResponseMessage(outcome.message.content);
237
172
  }
238
173
  else {
239
- renderer?.message(message.content);
240
- }
241
- }
242
- let summary;
243
- let failure;
244
- if (options.summaryEnabled) {
245
- try {
246
- const summaryAgentName = options.summaryAgent;
247
- const cancellation = cancellationFailureIfAborted(options, messages, {
248
- phase: "summary",
249
- agent: summaryAgentName,
250
- role: summaryRole(),
251
- turn: transcript.length + 1
252
- });
253
- if (cancellation) {
254
- renderer?.error(cancellation);
255
- return {
256
- options,
257
- messages: transcript,
258
- failure: cancellation
259
- };
260
- }
261
- summary = await generateSummary(config, options, transcript, renderer, messages);
262
- }
263
- catch (error) {
264
- failure = toDebateFailure(error, {
265
- phase: "summary",
266
- agent: options.summaryAgent,
267
- role: summaryRole(),
268
- turn: transcript.length + 1
269
- }, messages);
270
- renderer?.error(failure);
174
+ renderer?.message(outcome.message.content);
271
175
  }
272
176
  }
177
+ const { summary, failure } = await runSummaryPhase(config, options, transcript, renderer, messages);
273
178
  return {
274
179
  options,
275
180
  messages: transcript,
@@ -277,6 +182,62 @@ export async function runAsk(config, options, renderer, messages = createTransla
277
182
  failure
278
183
  };
279
184
  }
185
+ /**
186
+ * Appelle `agent.generate` en encadrant l'état "thinking" du renderer, puis
187
+ * construit le `DebateMessage` horodaté ou convertit l'erreur en `DebateFailure`
188
+ * (notifiée au renderer). Mutualisé entre les tours `debate` et les réponses `ask`.
189
+ */
190
+ async function generateTurnMessage(agent, prompt, context, renderer, messages) {
191
+ renderer?.thinkingStart(agent.name, agent.role);
192
+ try {
193
+ const response = await agent.generate(prompt);
194
+ return {
195
+ message: {
196
+ agent: agent.name,
197
+ role: agent.role,
198
+ content: response.content,
199
+ createdAt: new Date().toISOString()
200
+ }
201
+ };
202
+ }
203
+ catch (error) {
204
+ const failure = toDebateFailure(error, context, messages);
205
+ renderer?.error(failure);
206
+ return { failure };
207
+ }
208
+ finally {
209
+ renderer?.thinkingEnd();
210
+ }
211
+ }
212
+ /**
213
+ * Phase de synthèse commune aux modes `debate` et `ask` : vérifie l'annulation,
214
+ * génère la synthèse et convertit tout échec en `DebateFailure` déjà notifiée au
215
+ * renderer. Retourne un objet vide si la synthèse est désactivée.
216
+ */
217
+ async function runSummaryPhase(config, options, transcript, renderer, messages) {
218
+ if (!options.summaryEnabled) {
219
+ return {};
220
+ }
221
+ const context = {
222
+ phase: "summary",
223
+ agent: options.summaryAgent,
224
+ role: SUMMARY_ROLE,
225
+ turn: transcript.length + 1
226
+ };
227
+ const cancellation = cancellationFailureIfAborted(options, messages, context);
228
+ if (cancellation) {
229
+ renderer?.error(cancellation);
230
+ return { failure: cancellation };
231
+ }
232
+ try {
233
+ return { summary: await generateSummary(config, options, transcript, renderer, messages) };
234
+ }
235
+ catch (error) {
236
+ const failure = toDebateFailure(error, context, messages);
237
+ renderer?.error(failure);
238
+ return { failure };
239
+ }
240
+ }
280
241
  /**
281
242
  * Heuristique d'arrêt sur accord explicite.
282
243
  * Ne s'active qu'après un tour complet (nombre pair de messages) pour éviter les faux positifs.
@@ -338,7 +299,7 @@ async function generateSummary(config, options, transcript, renderer, messages =
338
299
  throw new Error(messages.orchestrator.unknownSummaryAgent(summaryAgentName));
339
300
  }
340
301
  const summaryAgent = createAgent(summaryAgentName, summaryConfig, { ollamaUrl: options.ollamaUrl });
341
- const role = summaryRole();
302
+ const role = SUMMARY_ROLE;
342
303
  renderer?.summaryStart(summaryAgent.name, role);
343
304
  renderer?.thinkingStart(summaryAgent.name, role);
344
305
  const response = await summaryAgent.generate({
@@ -364,9 +325,6 @@ async function generateSummary(config, options, transcript, renderer, messages =
364
325
  renderer?.message(summary.content);
365
326
  return summary;
366
327
  }
367
- function summaryRole() {
368
- return "summarizer";
369
- }
370
328
  function cancellationFailureIfAborted(options, messages, context) {
371
329
  if (!options.signal?.aborted) {
372
330
  return undefined;
@@ -399,15 +357,10 @@ function toDebateFailure(error, context, messages) {
399
357
  };
400
358
  }
401
359
  if (error instanceof OllamaUrlError) {
402
- const message = error.kind === "empty"
403
- ? messages.common.ollamaUrlEmpty
404
- : error.kind === "protocol"
405
- ? messages.common.ollamaUrlProtocol(error.protocol ?? "")
406
- : messages.common.ollamaUrlInvalid(error.value);
407
360
  return {
408
361
  ...context,
409
362
  kind: "unknown",
410
- message
363
+ message: formatOllamaUrlError(error, messages)
411
364
  };
412
365
  }
413
366
  return {
package/dist/output.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** @file Export Markdown `.debate.md`/`.ask.md` : transcript, table de métadonnées et synthèse finale. */
1
2
  import { mkdir, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { createTranslator } from "./i18n.js";
package/dist/presets.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** @file Source de vérité des presets de paires d'agents et de leur disponibilité calculée. */
1
2
  import { detectionForCommand, isRetiredAgentName } from "./agentRegistry.js";
2
3
  const presets = {
3
4
  "codex-claude": {
package/dist/prompt.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** @file Rendu des prompts agent (débat, ask, synthèse) avec contexte de session, fichiers et transcript. */
1
2
  import { createTranslator } from "./i18n.js";
2
3
  /**
3
4
  * Formate le prompt complet transmis à l'adapter.
@@ -255,7 +255,7 @@ function formatContext(options, messages) {
255
255
  if (count === 0) {
256
256
  return messages.renderers.noInjectedFiles;
257
257
  }
258
- return messages.renderers.injectedFiles(count);
258
+ return messages.renderers.injectedFiles(count, options.files.map((file) => file.path));
259
259
  }
260
260
  function formatFailureLocation(failure, messages) {
261
261
  if (failure.phase === "summary") {