palabre 0.6.4 → 0.7.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.
@@ -38,6 +38,9 @@ export class CliPtyAdapter {
38
38
  };
39
39
  }
40
40
  async generate(prompt) {
41
+ if (prompt.signal?.aborted) {
42
+ throw cancelledError(this.name);
43
+ }
41
44
  const renderedPrompt = formatAgentPrompt(prompt);
42
45
  const promptMode = this.config.promptMode ?? "stdin";
43
46
  const baseArgs = withModelArgs(this.config.args ?? [], this.config.model, this.config.modelArg ?? "--model");
@@ -53,6 +56,7 @@ export class CliPtyAdapter {
53
56
  let term;
54
57
  let dataSubscription;
55
58
  let exitSubscription;
59
+ let abortListener;
56
60
  const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
57
61
  const finish = (error, exitCode, kill = true) => {
58
62
  if (settled)
@@ -61,6 +65,9 @@ export class CliPtyAdapter {
61
65
  clearTimeout(hardTimer);
62
66
  dataSubscription?.dispose();
63
67
  exitSubscription?.dispose();
68
+ if (abortListener) {
69
+ prompt.signal?.removeEventListener("abort", abortListener);
70
+ }
64
71
  if (kill) {
65
72
  try {
66
73
  term.kill();
@@ -107,6 +114,10 @@ export class CliPtyAdapter {
107
114
  }));
108
115
  return;
109
116
  }
117
+ abortListener = () => {
118
+ finish(cancelledError(this.name));
119
+ };
120
+ prompt.signal?.addEventListener("abort", abortListener, { once: true });
110
121
  hardTimer = setTimeout(() => {
111
122
  finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, {
112
123
  timeoutMs: this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS
@@ -165,6 +176,9 @@ function createPtyExitError(adapterName, exitCode, raw) {
165
176
  raw
166
177
  });
167
178
  }
179
+ function cancelledError(adapterName) {
180
+ return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
181
+ }
168
182
  function summarizePtyOutput(output) {
169
183
  const cleaned = cleanTerminalOutput(output);
170
184
  return cleaned ? cleaned.slice(-1_200) : "aucune sortie PTY capturee.";
@@ -37,6 +37,9 @@ export class CliAdapter {
37
37
  };
38
38
  }
39
39
  async generate(prompt) {
40
+ if (prompt.signal?.aborted) {
41
+ throw cancelledError(this.name);
42
+ }
40
43
  const renderedPrompt = formatAgentPrompt(prompt);
41
44
  const promptMode = this.config.promptMode ?? "stdin";
42
45
  const baseArgs = withModelArgs(this.config.args ?? [], this.config.model, this.config.modelArg ?? "--model");
@@ -54,6 +57,7 @@ export class CliAdapter {
54
57
  let outputBytes = 0;
55
58
  let hardTimer;
56
59
  let idleTimer;
60
+ let abortListener;
57
61
  const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
58
62
  const finish = (error) => {
59
63
  if (settled)
@@ -62,6 +66,9 @@ export class CliAdapter {
62
66
  clearTimeout(hardTimer);
63
67
  if (idleTimer)
64
68
  clearTimeout(idleTimer);
69
+ if (abortListener) {
70
+ prompt.signal?.removeEventListener("abort", abortListener);
71
+ }
65
72
  if (error) {
66
73
  reject(error);
67
74
  return;
@@ -84,8 +91,13 @@ export class CliAdapter {
84
91
  raw: stdout
85
92
  });
86
93
  };
94
+ abortListener = () => {
95
+ killChildProcess(child);
96
+ finish(cancelledError(this.name));
97
+ };
98
+ prompt.signal?.addEventListener("abort", abortListener, { once: true });
87
99
  hardTimer = setTimeout(() => {
88
- child.kill();
100
+ killChildProcess(child);
89
101
  finish(new AdapterError("timeout", this.name, `${this.name} timed out after ${this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, {
90
102
  timeoutMs: this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS
91
103
  }));
@@ -96,7 +108,7 @@ export class CliAdapter {
96
108
  if (idleTimer)
97
109
  clearTimeout(idleTimer);
98
110
  idleTimer = setTimeout(() => {
99
- child.kill();
111
+ killChildProcess(child);
100
112
  finish(new AdapterError("idle-timeout", this.name, `${this.name} stopped producing output for ${this.config.idleTimeoutMs}ms`, { idleTimeoutMs: this.config.idleTimeoutMs }));
101
113
  }, this.config.idleTimeoutMs);
102
114
  };
@@ -104,7 +116,7 @@ export class CliAdapter {
104
116
  child.stdout.on("data", (chunk) => {
105
117
  outputBytes += chunk.length;
106
118
  if (outputBytes > maxOutputBytes) {
107
- child.kill();
119
+ killChildProcess(child);
108
120
  finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
109
121
  maxOutputBytes,
110
122
  outputBytes
@@ -117,7 +129,7 @@ export class CliAdapter {
117
129
  child.stderr.on("data", (chunk) => {
118
130
  outputBytes += chunk.length;
119
131
  if (outputBytes > maxOutputBytes) {
120
- child.kill();
132
+ killChildProcess(child);
121
133
  finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
122
134
  maxOutputBytes,
123
135
  outputBytes
@@ -295,3 +307,19 @@ function clipLine(value, maxLength) {
295
307
  ? value
296
308
  : `${value.slice(0, maxLength - 1)}…`;
297
309
  }
310
+ function cancelledError(adapterName) {
311
+ return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
312
+ }
313
+ function killChildProcess(child) {
314
+ if (process.platform === "win32" && child.pid) {
315
+ const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
316
+ windowsHide: true,
317
+ stdio: "ignore"
318
+ });
319
+ killer.on("error", () => {
320
+ child.kill();
321
+ });
322
+ return;
323
+ }
324
+ child.kill();
325
+ }
@@ -35,6 +35,9 @@ export class OllamaAdapter {
35
35
  };
36
36
  }
37
37
  async generate(prompt) {
38
+ if (prompt.signal?.aborted) {
39
+ throw cancelledError(this.name);
40
+ }
38
41
  const baseUrl = normalizeBaseUrl(this.config.baseUrl ?? "http://localhost:11434");
39
42
  if (this.config.validateModel !== false) {
40
43
  await this.ensureModelAvailable(baseUrl);
@@ -43,6 +46,8 @@ export class OllamaAdapter {
43
46
  await this.unloadOtherRunningModels(baseUrl);
44
47
  }
45
48
  const controller = new AbortController();
49
+ const abortListener = () => controller.abort();
50
+ prompt.signal?.addEventListener("abort", abortListener, { once: true });
46
51
  const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 120_000);
47
52
  try {
48
53
  const response = await fetch(`${baseUrl}/api/chat`, {
@@ -85,8 +90,15 @@ export class OllamaAdapter {
85
90
  content
86
91
  };
87
92
  }
93
+ catch (error) {
94
+ if (prompt.signal?.aborted) {
95
+ throw cancelledError(this.name);
96
+ }
97
+ throw error;
98
+ }
88
99
  finally {
89
100
  clearTimeout(timeout);
101
+ prompt.signal?.removeEventListener("abort", abortListener);
90
102
  }
91
103
  }
92
104
  /**
@@ -217,3 +229,6 @@ async function unloadModel(baseUrl, model, signal) {
217
229
  function normalizeBaseUrl(baseUrl) {
218
230
  return baseUrl.replace(/\/$/, "");
219
231
  }
232
+ function cancelledError(adapterName) {
233
+ return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
234
+ }
package/dist/args.js CHANGED
@@ -23,6 +23,8 @@ const FLAG_SPECS = {
23
23
  apply: { arity: "boolean" },
24
24
  "clear-defaults": { arity: "boolean" },
25
25
  "sync-agents": { arity: "boolean" },
26
+ "sync-ollama-model": { arity: "boolean" },
27
+ "ollama-models": { arity: "boolean" },
26
28
  // Valeur unique.
27
29
  "agent-a": { arity: "single" },
28
30
  "agent-b": { arity: "single" },
@@ -30,6 +32,7 @@ const FLAG_SPECS = {
30
32
  language: { arity: "single" },
31
33
  "model-a": { arity: "single" },
32
34
  "model-b": { arity: "single" },
35
+ "set-ollama-model": { arity: "single" },
33
36
  preset: { arity: "single" },
34
37
  "summary-agent": { arity: "single" },
35
38
  "summary-model": { arity: "single" },
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { applyDetectedCommands } from "./agentRegistry.js";
4
+ import { applyDetectedCommands, detectedAgentNames } from "./agentRegistry.js";
5
5
  export const DEFAULT_CONFIG_PATH = "palabre.config.json";
6
6
  export const LEGACY_CONFIG_PATH = "chicane.config.json";
7
7
  export const CONFIG_DIR_NAME = ".palabre";
@@ -196,6 +196,46 @@ export function createConfigFromDiscovery(discovery) {
196
196
  : { turns: config.defaults?.turns };
197
197
  return config;
198
198
  }
199
+ /**
200
+ * Ajoute dans `config.agents` les agents détectés localement mais absents de la config.
201
+ * Mute `config` directement ; l'appelant est responsable de persister la config.
202
+ */
203
+ export function syncDetectedAgents(config, discovery) {
204
+ const discoveredConfig = createConfigFromDiscovery(discovery);
205
+ const missingAgents = detectedAgentNames(discovery).filter((agentName) => !config.agents[agentName]);
206
+ applyDetectedCommands(config, discovery);
207
+ for (const agentName of missingAgents) {
208
+ config.agents[agentName] = discoveredConfig.agents[agentName];
209
+ }
210
+ return missingAgents;
211
+ }
212
+ export function syncOllamaModel(config, discovery) {
213
+ const agent = config.agents["ollama-local"];
214
+ if (agent?.type !== "ollama" || discovery.ollama.models.length === 0) {
215
+ return undefined;
216
+ }
217
+ if (discovery.ollama.models.includes(agent.model)) {
218
+ return undefined;
219
+ }
220
+ const previousModel = agent.model;
221
+ agent.model = chooseDefaultOllamaModel(discovery);
222
+ return {
223
+ previousModel,
224
+ nextModel: agent.model
225
+ };
226
+ }
227
+ export function setOllamaModel(config, model) {
228
+ const agent = config.agents["ollama-local"];
229
+ if (agent?.type !== "ollama") {
230
+ return undefined;
231
+ }
232
+ const previousModel = agent.model;
233
+ agent.model = model;
234
+ return {
235
+ previousModel,
236
+ nextModel: agent.model
237
+ };
238
+ }
199
239
  /** Écrit `config` sérialisé en JSON dans `configPath`. Crée le répertoire parent si nécessaire. */
200
240
  export async function writeExampleConfig(configPath = DEFAULT_CONFIG_PATH, config = exampleConfig) {
201
241
  const resolved = path.resolve(configPath);
@@ -1,6 +1,7 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
- import { writeExampleConfig } from "./config.js";
3
+ import { syncDetectedAgents, writeExampleConfig } from "./config.js";
4
+ import { discoverLocalTools } from "./discovery.js";
4
5
  import { DEFAULT_TURNS, MAX_TURNS, turnsOrDefault, validateTurns } from "./limits.js";
5
6
  /**
6
7
  * Lance le wizard interactif de configuration des defaults.
@@ -26,9 +27,10 @@ export async function runConfigWizard(configPath, config, messages) {
26
27
  console.log(messages.config.wizardActionQuestion);
27
28
  console.log(` 1) ${messages.config.wizardActionSetDefaults}`);
28
29
  console.log(` 2) ${messages.config.wizardActionClearDefaults}`);
29
- console.log(` 3) ${messages.config.wizardActionExit}`);
30
- const action = await askChoice(rl, messages.config.wizardChoicePrompt, "1", ["1", "2", "3"], messages);
31
- if (!action || action === "3") {
30
+ console.log(` 3) ${messages.config.wizardActionSyncAgents}`);
31
+ console.log(` 4) ${messages.config.wizardActionExit}`);
32
+ const action = await askChoice(rl, messages.config.wizardChoicePrompt, "1", ["1", "2", "3", "4"], messages);
33
+ if (!action || action === "4") {
32
34
  console.log(messages.config.wizardUnchanged);
33
35
  return;
34
36
  }
@@ -38,6 +40,17 @@ export async function runConfigWizard(configPath, config, messages) {
38
40
  console.log(messages.config.wizardCleared(configPath));
39
41
  return;
40
42
  }
43
+ if (action === "3") {
44
+ const discovery = await discoverLocalTools();
45
+ const addedAgents = syncDetectedAgents(config, discovery);
46
+ if (addedAgents.length === 0) {
47
+ console.log(messages.config.syncNoMissing(configPath));
48
+ return;
49
+ }
50
+ await writeExampleConfig(configPath, config);
51
+ console.log(messages.config.syncAdded(configPath, addedAgents.join(", ")));
52
+ return;
53
+ }
41
54
  const agentA = await askAgent(rl, choices, messages.config.wizardAgentADescription, config.defaults?.agentA, messages);
42
55
  if (!agentA)
43
56
  return;
package/dist/doctor.js CHANGED
@@ -5,6 +5,7 @@ import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
5
5
  import { discoverLocalTools } from "./discovery.js";
6
6
  import { createTranslator, resolveLanguage } from "./i18n.js";
7
7
  import { DEFAULT_TURNS, MAX_TURNS } from "./limits.js";
8
+ import { compareSemver, getLatestPackageVersion, getPackageVersion } from "./version.js";
8
9
  /**
9
10
  * Exécute le diagnostic complet : config, outils locaux et agents.
10
11
  * Retourne toujours un résultat (pas de throw) ; les erreurs de config sont reportées comme lignes `error`.
@@ -20,6 +21,7 @@ export async function runDoctor(explicitConfigPath, plain = false, explicitLangu
20
21
  });
21
22
  const t = createTranslator(language);
22
23
  lines.push(info(t.doctor.title, "title"));
24
+ await inspectCliVersion(lines, t);
23
25
  lines.push(info(t.doctor.currentDirectory(process.cwd()), "cwd"));
24
26
  lines.push(hasConfig
25
27
  ? ok(t.doctor.configFound(configPath))
@@ -53,6 +55,18 @@ export async function runDoctor(explicitConfigPath, plain = false, explicitLangu
53
55
  inspectAgents(config, discovery, lines, t);
54
56
  return render(lines, plain, t);
55
57
  }
58
+ async function inspectCliVersion(lines, t) {
59
+ const currentVersion = await getPackageVersion();
60
+ lines.push(info(t.doctor.cliVersion(currentVersion)));
61
+ const latestVersion = await getLatestPackageVersion();
62
+ if (!latestVersion) {
63
+ lines.push(info(t.doctor.updateUnknown));
64
+ return;
65
+ }
66
+ lines.push(compareSemver(currentVersion, latestVersion) < 0
67
+ ? warn(t.doctor.updateAvailable(currentVersion, latestVersion))
68
+ : ok(t.doctor.updateCurrent(latestVersion)));
69
+ }
56
70
  async function loadConfigSafely(configPath) {
57
71
  try {
58
72
  return await loadConfig(configPath);
package/dist/index.js CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
2
+ import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaModel, syncDetectedAgents, syncOllamaModel, writeExampleConfig } from "./config.js";
6
3
  import { loadProjectInputs } from "./context.js";
7
4
  import { buildContextScan } from "./contextScan.js";
8
5
  import { discoverLocalTools } from "./discovery.js";
@@ -22,6 +19,7 @@ import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./up
22
19
  import { createSessionContext } from "./session.js";
23
20
  import { getStringListFlag, parseArgs } from "./args.js";
24
21
  import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
22
+ import { getPackageVersion } from "./version.js";
25
23
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
26
24
  async function main() {
27
25
  const rawArgs = process.argv.slice(2);
@@ -166,7 +164,8 @@ async function main() {
166
164
  summaryModel: optionalString(parsed.flags["summary-model"]),
167
165
  summaryEnabled: !parsed.flags["no-summary"],
168
166
  earlyStopOnAgreement: !parsed.flags["no-early-stop"],
169
- plainOutput: Boolean(parsed.flags.plain)
167
+ plainOutput: Boolean(parsed.flags.plain),
168
+ signal: debateAbortSignal()
170
169
  };
171
170
  if (parsed.flags["show-prompt"]) {
172
171
  printContextWarnings(context.warnings, messages);
@@ -179,9 +178,20 @@ async function main() {
179
178
  const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages, result.failure);
180
179
  renderer.done(outputPath);
181
180
  if (result.failure) {
182
- process.exitCode = 1;
181
+ process.exitCode = result.failure.kind === "cancelled" ? 130 : 1;
183
182
  }
184
183
  }
184
+ function debateAbortSignal() {
185
+ const controller = new AbortController();
186
+ const abort = () => {
187
+ if (!controller.signal.aborted) {
188
+ controller.abort();
189
+ }
190
+ };
191
+ process.once("SIGINT", abort);
192
+ process.once("SIGTERM", abort);
193
+ return controller.signal;
194
+ }
185
195
  /**
186
196
  * Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
187
197
  * @param flags - Flags parsés depuis la ligne de commande.
@@ -220,6 +230,19 @@ async function runConfigCommand(flags) {
220
230
  configLanguage: config.language
221
231
  });
222
232
  const messages = createTranslator(language);
233
+ if (flags["ollama-models"]) {
234
+ await runOllamaModelsCommand(config, Boolean(flags.json));
235
+ return;
236
+ }
237
+ const setOllamaModelValue = optionalString(flags["set-ollama-model"]);
238
+ if (setOllamaModelValue !== undefined) {
239
+ await runSetOllamaModelCommand(configPath, config, setOllamaModelValue, messages);
240
+ return;
241
+ }
242
+ if (flags["sync-ollama-model"]) {
243
+ await runSyncOllamaModelCommand(configPath, config, messages);
244
+ return;
245
+ }
223
246
  if (flags["sync-agents"]) {
224
247
  const discovery = await discoverLocalTools();
225
248
  const addedAgents = syncDetectedAgents(config, discovery);
@@ -278,6 +301,63 @@ async function runConfigCommand(flags) {
278
301
  }
279
302
  await runConfigWizard(configPath, config, messages);
280
303
  }
304
+ async function runOllamaModelsCommand(config, json) {
305
+ const discovery = await discoverLocalTools();
306
+ const agent = config.agents["ollama-local"];
307
+ const currentModel = agent?.type === "ollama" ? agent.model : null;
308
+ const payload = {
309
+ v: 1,
310
+ agent: "ollama-local",
311
+ available: discovery.ollama.available,
312
+ baseUrl: discovery.ollama.baseUrl,
313
+ currentModel,
314
+ currentModelInstalled: currentModel ? discovery.ollama.models.includes(currentModel) : false,
315
+ installedModels: discovery.ollama.models
316
+ };
317
+ if (json) {
318
+ console.log(JSON.stringify(payload, null, 2));
319
+ return;
320
+ }
321
+ console.log(`ollama-local: ${currentModel ?? "(non configuré)"}`);
322
+ console.log(`Ollama API: ${discovery.ollama.available ? "joignable" : "indisponible"} (${discovery.ollama.baseUrl})`);
323
+ console.log(`Modèles installés: ${discovery.ollama.models.length > 0 ? discovery.ollama.models.join(", ") : "(aucun)"}`);
324
+ }
325
+ async function runSetOllamaModelCommand(configPath, config, model, messages) {
326
+ const trimmed = model.trim();
327
+ if (!trimmed) {
328
+ throw new Error(messages.common.optionRequiresValue("--set-ollama-model"));
329
+ }
330
+ const discovery = await discoverLocalTools();
331
+ const agent = config.agents["ollama-local"];
332
+ if (agent?.type !== "ollama") {
333
+ throw new Error(messages.config.ollamaModelNoAgent);
334
+ }
335
+ if (!discovery.ollama.models.includes(trimmed)) {
336
+ throw new Error(messages.config.ollamaModelUnavailable(trimmed));
337
+ }
338
+ const result = setOllamaModel(config, trimmed);
339
+ await writeExampleConfig(configPath, config);
340
+ console.log(result
341
+ ? messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel)
342
+ : messages.config.ollamaModelNoChange(configPath, agent.model));
343
+ }
344
+ async function runSyncOllamaModelCommand(configPath, config, messages) {
345
+ const discovery = await discoverLocalTools();
346
+ const agent = config.agents["ollama-local"];
347
+ if (agent?.type !== "ollama") {
348
+ throw new Error(messages.config.ollamaModelNoAgent);
349
+ }
350
+ if (discovery.ollama.models.length === 0) {
351
+ throw new Error(messages.config.ollamaModelNoInstalledModels);
352
+ }
353
+ const result = syncOllamaModel(config, discovery);
354
+ if (!result) {
355
+ console.log(messages.config.ollamaModelNoChange(configPath, agent.model));
356
+ return;
357
+ }
358
+ await writeExampleConfig(configPath, config);
359
+ console.log(messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel));
360
+ }
281
361
  /**
282
362
  * Renvoie `true` si la valeur représente une désactivation explicite (ex. "none", "0", "disabled").
283
363
  * @param value - Chaîne saisie par l'utilisateur.
@@ -472,13 +552,6 @@ async function runContextCommand(flags, positionals) {
472
552
  console.error(`${messages.renderers.warningPrefix} ${warning}`);
473
553
  }
474
554
  }
475
- /** Lit la version depuis `package.json` adjacent au bundle compilé. */
476
- async function getPackageVersion() {
477
- const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
478
- const raw = await readFile(packageJsonPath, "utf8");
479
- const packageJson = JSON.parse(raw);
480
- return packageJson.version ?? "0.0.0";
481
- }
482
555
  /**
483
556
  * Écrit les avertissements de contexte sur `stderr`.
484
557
  * @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
@@ -495,22 +568,6 @@ function printContextWarnings(warnings, messages) {
495
568
  * @param discovery - Résultat de la découverte locale des outils.
496
569
  * @returns Noms des agents nouvellement ajoutés.
497
570
  */
498
- function syncDetectedAgents(config, discovery) {
499
- const discoveredConfig = createConfigFromDiscovery(discovery);
500
- const missingAgents = findDetectedMissingAgents(config, discovery);
501
- for (const agentName of missingAgents) {
502
- config.agents[agentName] = discoveredConfig.agents[agentName];
503
- }
504
- return missingAgents;
505
- }
506
- /**
507
- * Renvoie les noms des agents détectés localement qui ne sont pas encore dans `config.agents`.
508
- * @param config - Config Palabre existante.
509
- * @param discovery - Résultat de la découverte locale des outils.
510
- */
511
- function findDetectedMissingAgents(config, discovery) {
512
- return detectedAgentNames(discovery).filter((agentName) => !config.agents[agentName]);
513
- }
514
571
  /**
515
572
  * Affiche la liste des agents déclarés avec leur type, rôle, état de détection et défauts.
516
573
  * @param configPath - Chemin du fichier de config (affiché en en-tête).
@@ -3,6 +3,11 @@ export const configMessages = {
3
3
  createdForConfig: (path) => `${path} créé. Édite la config puis relance palabre config.`,
4
4
  syncNoMissing: (path) => `Aucun agent détecté manquant dans ${path}.`,
5
5
  syncAdded: (path, agents) => `Agents ajoutés dans ${path}: ${agents}.`,
6
+ ollamaModelNoChange: (path, model) => `Modèle Ollama inchangé dans ${path}: ${model ?? "aucun"}.`,
7
+ ollamaModelUpdated: (path, previousModel, nextModel) => `Modèle Ollama mis à jour dans ${path}: ${previousModel} -> ${nextModel}.`,
8
+ ollamaModelUnavailable: (model) => `Modèle Ollama non installé: ${model}. Action: choisis un modèle installé ou lance \`ollama pull ${model}\`.`,
9
+ ollamaModelNoAgent: "Agent ollama-local absent ou invalide dans la config.",
10
+ ollamaModelNoInstalledModels: "Aucun modèle Ollama installé détecté. Action: lance `ollama pull <modèle>`.",
6
11
  updated: (path, defaults, language) => `Configuration mise à jour dans ${path}: ${defaults}, langue: ${language}.`,
7
12
  cleared: (path) => `Paramètres par défaut supprimés dans ${path}. Utilise maintenant un preset ou --agent-a/--agent-b pour lancer un débat.`,
8
13
  defaultsSummary: (agentA, agentB, turns, summaryAgent) => {
@@ -19,6 +24,7 @@ export const configMessages = {
19
24
  wizardActionQuestion: "Que veux-tu faire ?",
20
25
  wizardActionSetDefaults: "Définir des paramètres par défaut",
21
26
  wizardActionClearDefaults: "Supprimer les paramètres par défaut",
27
+ wizardActionSyncAgents: "Synchroniser les agents détectés",
22
28
  wizardActionExit: "Quitter sans modifier",
23
29
  wizardChoicePrompt: "Tape le numéro de ton choix",
24
30
  wizardChoiceQuestion: (label, defaultValue) => `${label} (Entrée = ${defaultValue}) : `,
@@ -46,6 +52,11 @@ export const configMessages = {
46
52
  createdForConfig: (path) => `${path} created. Edit the config, then run palabre config again.`,
47
53
  syncNoMissing: (path) => `No missing detected agent in ${path}.`,
48
54
  syncAdded: (path, agents) => `Agents added to ${path}: ${agents}.`,
55
+ ollamaModelNoChange: (path, model) => `Ollama model unchanged in ${path}: ${model ?? "none"}.`,
56
+ ollamaModelUpdated: (path, previousModel, nextModel) => `Ollama model updated in ${path}: ${previousModel} -> ${nextModel}.`,
57
+ ollamaModelUnavailable: (model) => `Ollama model is not installed: ${model}. Action: choose an installed model or run \`ollama pull ${model}\`.`,
58
+ ollamaModelNoAgent: "ollama-local agent is missing or invalid in the config.",
59
+ ollamaModelNoInstalledModels: "No installed Ollama model detected. Action: run `ollama pull <model>`.",
49
60
  updated: (path, defaults, language) => `Configuration updated in ${path}: ${defaults}, language: ${language}.`,
50
61
  cleared: (path) => `Default settings cleared in ${path}. Use a preset or --agent-a/--agent-b to start a debate now.`,
51
62
  defaultsSummary: (agentA, agentB, turns, summaryAgent) => {
@@ -62,6 +73,7 @@ export const configMessages = {
62
73
  wizardActionQuestion: "What do you want to do?",
63
74
  wizardActionSetDefaults: "Set default settings",
64
75
  wizardActionClearDefaults: "Clear default settings",
76
+ wizardActionSyncAgents: "Sync detected agents",
65
77
  wizardActionExit: "Exit without changes",
66
78
  wizardChoicePrompt: "Type the number of your choice",
67
79
  wizardChoiceQuestion: (label, defaultValue) => `${label} (Enter = ${defaultValue}): `,
@@ -1,6 +1,10 @@
1
1
  export const doctorMessages = {
2
2
  fr: {
3
3
  title: "PALABRE doctor",
4
+ cliVersion: (version) => `Version CLI: ${version}`,
5
+ updateCurrent: (version) => `Mise à jour: CLI à jour (${version}).`,
6
+ updateAvailable: (current, latest) => `Mise à jour disponible: ${current} -> ${latest}. Action: lance ` + "`palabre update`.",
7
+ updateUnknown: "Mise à jour: vérification npm indisponible. Action: lance `palabre update` pour les instructions.",
4
8
  currentDirectory: (cwd) => `Dossier courant: ${cwd}`,
5
9
  configFound: (path) => `Config trouvée: ${path}`,
6
10
  configMissing: (path) => `Config absente: ${path}`,
@@ -63,6 +67,10 @@ export const doctorMessages = {
63
67
  },
64
68
  en: {
65
69
  title: "PALABRE doctor",
70
+ cliVersion: (version) => `CLI version: ${version}`,
71
+ updateCurrent: (version) => `Update: CLI is up to date (${version}).`,
72
+ updateAvailable: (current, latest) => `Update available: ${current} -> ${latest}. Action: run ` + "`palabre update`.",
73
+ updateUnknown: "Update: npm check unavailable. Action: run `palabre update` for instructions.",
66
74
  currentDirectory: (cwd) => `Current directory: ${cwd}`,
67
75
  configFound: (path) => `Config found: ${path}`,
68
76
  configMissing: (path) => `Config missing: ${path}`,
@@ -11,6 +11,7 @@ export const orchestratorMessages = {
11
11
  "rien a ajouter",
12
12
  "question factuelle resolue"
13
13
  ],
14
+ cancelled: "Débat annulé par l'utilisateur.",
14
15
  ollamaNoContext: (agentNames) => `${agentNames} ne lit pas le filesystem. Ajoute --files ou --context pour fournir un contexte projet.`,
15
16
  unknownSummaryAgent: (agentName) => `Agent de synthese inconnu: ${agentName}`
16
17
  },
@@ -27,6 +28,7 @@ export const orchestratorMessages = {
27
28
  "nothing to add",
28
29
  "factual question resolved"
29
30
  ],
31
+ cancelled: "Debate cancelled by the user.",
30
32
  ollamaNoContext: (agentNames) => `${agentNames} cannot read the filesystem. Add --files or --context to provide project context.`,
31
33
  unknownSummaryAgent: (agentName) => `Unknown summary agent: ${agentName}`
32
34
  }
@@ -32,6 +32,19 @@ export async function runDebate(config, options, renderer, messages = createTran
32
32
  const transcript = [];
33
33
  let stopReason;
34
34
  for (let index = 0; index < options.turns; index += 1) {
35
+ const cancellation = cancellationFailureIfAborted(options, messages, {
36
+ phase: "debate",
37
+ turn: index + 1
38
+ });
39
+ if (cancellation) {
40
+ renderer?.error(cancellation);
41
+ return {
42
+ options,
43
+ messages: transcript,
44
+ stopReason,
45
+ failure: cancellation
46
+ };
47
+ }
35
48
  const current = agents[index % agents.length];
36
49
  const peer = agents[(index + 1) % agents.length];
37
50
  const turn = index + 1;
@@ -49,7 +62,8 @@ export async function runDebate(config, options, renderer, messages = createTran
49
62
  language: options.language,
50
63
  session: options.session,
51
64
  files: options.files,
52
- transcript
65
+ transcript,
66
+ signal: options.signal
53
67
  });
54
68
  }
55
69
  catch (error) {
@@ -88,6 +102,20 @@ export async function runDebate(config, options, renderer, messages = createTran
88
102
  let failure;
89
103
  if (options.summaryEnabled) {
90
104
  try {
105
+ const cancellation = cancellationFailureIfAborted(options, messages, {
106
+ phase: "summary",
107
+ agent: options.summaryAgent ?? options.agentB,
108
+ turn: transcript.length + 1
109
+ });
110
+ if (cancellation) {
111
+ renderer?.error(cancellation);
112
+ return {
113
+ options,
114
+ messages: transcript,
115
+ stopReason,
116
+ failure: cancellation
117
+ };
118
+ }
91
119
  summary = await generateSummary(config, options, transcript, renderer, messages);
92
120
  }
93
121
  catch (error) {
@@ -181,7 +209,8 @@ async function generateSummary(config, options, transcript, renderer, messages =
181
209
  language: options.language,
182
210
  session: options.session,
183
211
  files: options.files,
184
- transcript
212
+ transcript,
213
+ signal: options.signal
185
214
  }).finally(() => renderer?.thinkingEnd());
186
215
  const summary = {
187
216
  agent: summaryAgent.name,
@@ -192,6 +221,19 @@ async function generateSummary(config, options, transcript, renderer, messages =
192
221
  renderer?.message(summary.content);
193
222
  return summary;
194
223
  }
224
+ function cancellationFailureIfAborted(options, messages, context) {
225
+ if (!options.signal?.aborted) {
226
+ return undefined;
227
+ }
228
+ return {
229
+ phase: context.phase,
230
+ agent: context.agent,
231
+ role: context.role,
232
+ turn: context.turn,
233
+ kind: "cancelled",
234
+ message: messages.orchestrator.cancelled
235
+ };
236
+ }
195
237
  function toDebateFailure(error, context) {
196
238
  if (error instanceof AdapterError) {
197
239
  return {
@@ -0,0 +1,54 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** Lit la version depuis `package.json` adjacent au bundle compilé. */
5
+ export async function getPackageVersion() {
6
+ const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
7
+ const raw = await readFile(packageJsonPath, "utf8");
8
+ const packageJson = JSON.parse(raw);
9
+ return packageJson.version ?? "0.0.0";
10
+ }
11
+ /** Compare deux versions semver simples `major.minor.patch`. */
12
+ export function compareSemver(left, right) {
13
+ const a = parseSemverParts(left);
14
+ const b = parseSemverParts(right);
15
+ for (let index = 0; index < 3; index += 1) {
16
+ const diff = a[index] - b[index];
17
+ if (diff !== 0) {
18
+ return diff;
19
+ }
20
+ }
21
+ return 0;
22
+ }
23
+ /** Lit la dernière version publiée sur npm. Retourne `undefined` hors ligne ou si le registre ne répond pas. */
24
+ export async function getLatestPackageVersion(timeoutMs = 1_500) {
25
+ const controller = new AbortController();
26
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
27
+ try {
28
+ const response = await fetch("https://registry.npmjs.org/palabre/latest", {
29
+ signal: controller.signal,
30
+ headers: {
31
+ accept: "application/json"
32
+ }
33
+ });
34
+ if (!response.ok) {
35
+ return undefined;
36
+ }
37
+ const data = await response.json();
38
+ return typeof data.version === "string" ? data.version : undefined;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ finally {
44
+ clearTimeout(timeout);
45
+ }
46
+ }
47
+ function parseSemverParts(value) {
48
+ const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
49
+ return [
50
+ Number(match?.[1] ?? 0),
51
+ Number(match?.[2] ?? 0),
52
+ Number(match?.[3] ?? 0)
53
+ ];
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palabre",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
5
  "license": "MIT",
6
6
  "type": "module",