palabre 0.8.1 → 0.9.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.
@@ -2,13 +2,13 @@ import { CliAdapter } from "./cli.js";
2
2
  import { CliPtyAdapter } from "./cli-pty.js";
3
3
  import { OllamaAdapter } from "./ollama.js";
4
4
  /** Factory qui instancie l'adapter approprié selon `config.type`. Exhaustive : tout `AgentConfig` valide produit un adapter. */
5
- export function createAgent(name, config) {
5
+ export function createAgent(name, config, runtime = {}) {
6
6
  switch (config.type) {
7
7
  case "cli":
8
8
  return new CliAdapter(name, config);
9
9
  case "cli-pty":
10
10
  return new CliPtyAdapter(name, config);
11
11
  case "ollama":
12
- return new OllamaAdapter(name, config);
12
+ return new OllamaAdapter(name, config, runtime);
13
13
  }
14
14
  }
@@ -1,6 +1,7 @@
1
1
  import { AdapterError } from "../errors.js";
2
2
  import { createTranslator } from "../i18n.js";
3
3
  import { formatAgentPrompt } from "../prompt.js";
4
+ import { resolveOllamaBaseUrl } from "../ollamaUrl.js";
4
5
  /**
5
6
  * Adapter pour Ollama via l'API HTTP locale (`POST /api/chat`).
6
7
  * N'accède jamais au filesystem : ne voit que le prompt et le transcript fournis par l'orchestrateur.
@@ -9,11 +10,13 @@ import { formatAgentPrompt } from "../prompt.js";
9
10
  export class OllamaAdapter {
10
11
  name;
11
12
  config;
13
+ runtime;
12
14
  role;
13
15
  contract;
14
- constructor(name, config) {
16
+ constructor(name, config, runtime = {}) {
15
17
  this.name = name;
16
18
  this.config = config;
19
+ this.runtime = runtime;
17
20
  this.role = config.role;
18
21
  this.contract = {
19
22
  name,
@@ -38,7 +41,10 @@ export class OllamaAdapter {
38
41
  if (prompt.signal?.aborted) {
39
42
  throw cancelledError(this.name);
40
43
  }
41
- const baseUrl = normalizeBaseUrl(this.config.baseUrl ?? "http://localhost:11434");
44
+ const baseUrl = resolveOllamaBaseUrl({
45
+ cliUrl: this.runtime.ollamaUrl,
46
+ configUrl: this.config.baseUrl
47
+ });
42
48
  if (this.config.validateModel !== false) {
43
49
  await this.ensureModelAvailable(baseUrl);
44
50
  }
@@ -225,10 +231,6 @@ async function unloadModel(baseUrl, model, signal) {
225
231
  });
226
232
  }
227
233
  }
228
- /** Supprime le slash final de `baseUrl` pour éviter les doubles slashs dans les URLs construites. */
229
- function normalizeBaseUrl(baseUrl) {
230
- return baseUrl.replace(/\/$/, "");
231
- }
232
234
  function cancelledError(adapterName) {
233
235
  return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
234
236
  }
@@ -10,6 +10,12 @@ const KNOWN_CLI_AGENTS = [
10
10
  { configKey: "opencode", commandAliases: ["opencode"], discoveryKey: "opencode" },
11
11
  { configKey: "vibe", commandAliases: ["vibe"], discoveryKey: "vibe" }
12
12
  ];
13
+ /** Agents retirés conservés uniquement pour lire les anciennes configurations. */
14
+ const RETIRED_AGENT_NAMES = new Set(["gemini"]);
15
+ /** Indique qu'un nom d'agent ne doit plus être proposé ni exposé aux intégrations. */
16
+ export function isRetiredAgentName(name) {
17
+ return RETIRED_AGENT_NAMES.has(name.toLowerCase());
18
+ }
13
19
  /** Clé de config de l'agent Ollama local par défaut. */
14
20
  export const OLLAMA_AGENT_KEY = "ollama-local";
15
21
  /**
package/dist/args.js CHANGED
@@ -35,6 +35,7 @@ const FLAG_SPECS = {
35
35
  language: { arity: "single" },
36
36
  "model-a": { arity: "single" },
37
37
  "model-b": { arity: "single" },
38
+ "ollama-url": { arity: "single" },
38
39
  mode: { arity: "single" },
39
40
  "set-ollama-model": { arity: "single" },
40
41
  preset: { arity: "single" },
package/dist/config.js CHANGED
@@ -270,6 +270,14 @@ export function setOllamaModel(config, model) {
270
270
  nextModel: agent.model
271
271
  };
272
272
  }
273
+ /** Met à jour l'adresse persistante de tous les agents Ollama configurés. */
274
+ export function setOllamaBaseUrl(config, baseUrl) {
275
+ const agents = Object.values(config.agents).filter((agent) => agent.type === "ollama");
276
+ for (const agent of agents) {
277
+ agent.baseUrl = baseUrl;
278
+ }
279
+ return agents.length;
280
+ }
273
281
  /** Écrit `config` sérialisé en JSON dans `configPath`. Crée le répertoire parent si nécessaire. */
274
282
  export async function writeExampleConfig(configPath = DEFAULT_CONFIG_PATH, config = exampleConfig) {
275
283
  const resolved = path.resolve(configPath);
package/dist/discovery.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { executableExtensions } from "./exec.js";
4
+ import { resolveOllamaBaseUrl } from "./ollamaUrl.js";
4
5
  /**
5
6
  * Détecte en parallèle toutes les CLIs supportées et le serveur Ollama local.
6
7
  * Sur Windows, tente `claude.exe` avant `claude`.
7
8
  * Antigravity est exposé selon les installations sous `agy` ou `antigravity`.
8
9
  */
9
- export async function discoverLocalTools() {
10
+ export async function discoverLocalTools(options = {}) {
10
11
  const [codex, claude, antigravity, opencode, vibe, ollamaCommand] = await Promise.all([
11
12
  detectCommand("codex"),
12
13
  detectFirstCommand(process.platform === "win32" ? ["claude.exe", "claude"] : ["claude"]),
@@ -15,17 +16,39 @@ export async function discoverLocalTools() {
15
16
  detectCommand("vibe"),
16
17
  detectCommand("ollama")
17
18
  ]);
18
- const ollamaServer = await detectOllamaServer();
19
+ const configuredTargets = Object.entries(options.ollamaTargets ?? {});
20
+ const targets = configuredTargets.length > 0
21
+ ? configuredTargets
22
+ : [["ollama-local", options.ollamaConfigUrl]];
23
+ const resolvedTargets = targets.map(([name, configUrl]) => ({
24
+ name,
25
+ baseUrl: resolveOllamaBaseUrl({
26
+ cliUrl: options.ollamaUrl,
27
+ configUrl
28
+ })
29
+ }));
30
+ const uniqueUrls = resolvedTargets
31
+ .map((target) => target.baseUrl)
32
+ .filter((baseUrl, index, urls) => urls.indexOf(baseUrl) === index);
33
+ const servers = await Promise.all(uniqueUrls.map(async (baseUrl) => [
34
+ baseUrl,
35
+ await detectOllamaServer(baseUrl)
36
+ ]));
37
+ const serversByUrl = new Map(servers);
38
+ const ollamaAgents = Object.fromEntries(resolvedTargets.map(({ name, baseUrl }) => [name, {
39
+ ...serversByUrl.get(baseUrl),
40
+ commandAvailable: ollamaCommand.available
41
+ }]));
42
+ const primaryName = ollamaAgents["ollama-local"] ? "ollama-local" : resolvedTargets[0].name;
43
+ const ollamaServer = ollamaAgents[primaryName];
19
44
  return {
20
45
  codex,
21
46
  claude,
22
47
  antigravity,
23
48
  opencode,
24
49
  vibe,
25
- ollama: {
26
- ...ollamaServer,
27
- commandAvailable: ollamaCommand.available
28
- }
50
+ ollama: ollamaServer,
51
+ ollamaAgents
29
52
  };
30
53
  }
31
54
  async function detectFirstCommand(commands) {
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 { configuredOllamaTargets, normalizeOllamaBaseUrl } from "./ollamaUrl.js";
8
9
  import { compareSemver, getLatestPackageVersion, getPackageVersion } from "./version.js";
9
10
  /**
10
11
  * Exécute le diagnostic complet : config, outils locaux et agents.
@@ -39,7 +40,11 @@ export async function runDoctor(explicitConfigPath, plain = false, explicitLangu
39
40
  lines.push(ok(t.doctor.configReadable));
40
41
  lines.push(ok(t.doctor.interfaceLanguage(language)));
41
42
  await inspectConfig(config, lines, t);
42
- const discovery = await discoverLocalTools();
43
+ const ollamaTargets = Object.fromEntries(Object.entries(configuredOllamaTargets(config))
44
+ .map(([name, value]) => [name, value && isValidOllamaBaseUrl(value) ? value : undefined]));
45
+ const discovery = await discoverLocalTools({
46
+ ollamaTargets
47
+ });
43
48
  lines.push(info(t.doctor.localTools, "tools"));
44
49
  lines.push(formatCommand("Codex CLI", discovery.codex.available, discovery.codex.command, discovery.codex.path, t));
45
50
  lines.push(formatCommand("Claude CLI", discovery.claude.available, discovery.claude.command, discovery.claude.path, t));
@@ -204,13 +209,22 @@ function inspectAgentShape(name, agent, lines, t) {
204
209
  if (!agent.model || !agent.model.trim()) {
205
210
  lines.push(error(t.doctor.ollamaModelMissing(name)));
206
211
  }
207
- if (agent.baseUrl && !/^https?:\/\//.test(agent.baseUrl)) {
212
+ if (agent.baseUrl && !isValidOllamaBaseUrl(agent.baseUrl)) {
208
213
  lines.push(error(t.doctor.ollamaBaseUrlInvalid(name, agent.baseUrl)));
209
214
  }
210
215
  if (agent.timeoutMs !== undefined && (!Number.isFinite(agent.timeoutMs) || agent.timeoutMs <= 0)) {
211
216
  lines.push(error(t.doctor.positiveTimeout(name, "timeoutMs")));
212
217
  }
213
218
  }
219
+ function isValidOllamaBaseUrl(value) {
220
+ try {
221
+ normalizeOllamaBaseUrl(value);
222
+ return true;
223
+ }
224
+ catch {
225
+ return false;
226
+ }
227
+ }
214
228
  function inspectCliAgent(name, agent, discovery, lines, t) {
215
229
  const known = detectionForCommand(agent.command, discovery);
216
230
  const prefix = `${name} [cli:${agent.role}] command=${agent.command}`;
@@ -224,7 +238,8 @@ function inspectCliAgent(name, agent, discovery, lines, t) {
224
238
  }
225
239
  function inspectOllamaAgent(name, agent, discovery, lines, t) {
226
240
  const prefix = `${name} [ollama:${agent.role}] model=${agent.model}`;
227
- if (!discovery.ollama.available) {
241
+ const ollama = discovery.ollamaAgents?.[name] ?? discovery.ollama;
242
+ if (!ollama.available) {
228
243
  lines.push(warn(t.doctor.ollamaNotVerifiable(prefix)));
229
244
  return;
230
245
  }
@@ -232,7 +247,7 @@ function inspectOllamaAgent(name, agent, discovery, lines, t) {
232
247
  lines.push(info(t.doctor.ollamaValidateFalse(prefix)));
233
248
  return;
234
249
  }
235
- const installed = discovery.ollama.models.includes(agent.model);
250
+ const installed = ollama.models.includes(agent.model);
236
251
  lines.push(installed
237
252
  ? ok(t.doctor.ollamaInstalled(prefix))
238
253
  : warn(t.doctor.ollamaMissing(prefix, agent.model)));
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeExampleConfig } from "./config.js";
2
+ import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaBaseUrl, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeExampleConfig } from "./config.js";
3
3
  import { loadProjectInputs } from "./context.js";
4
4
  import { buildContextScan } from "./contextScan.js";
5
5
  import { discoverLocalTools } from "./discovery.js";
@@ -10,19 +10,20 @@ import { createTranslator, DEFAULT_LANGUAGE, parseLanguage, resolveLanguage } fr
10
10
  import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault, validateTurns } from "./limits.js";
11
11
  import { formatAgentPrompt } from "./prompt.js";
12
12
  import { runNewWizard } from "./new.js";
13
- import { listPresetNames, listPresetsWithAvailability, resolvePreset } from "./presets.js";
13
+ import { listAgentsWithAvailability, listPresetNames, listPresetsWithAvailability, resolvePreset } from "./presets.js";
14
14
  import { listHistoryEntries } from "./history.js";
15
15
  import { createConsoleRenderer } from "./renderers/console.js";
16
16
  import { createNdjsonRenderer } from "./renderers/ndjson.js";
17
- import { createTuiRenderer, promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiHomeTopic, promptTuiRolesWizard, renderTuiConfig, renderTuiHelp, renderTuiHistory, renderTuiHome } from "./renderers/tui.js";
17
+ import { createTuiRenderer, promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiHomeTopic, promptTuiRolesWizard, renderTuiConfig, renderTuiHelp, renderTuiHistory, renderTuiHome, renderTuiUpdate } from "./renderers/tui.js";
18
18
  import { MAX_ASK_AGENTS, runAsk, runDebate } from "./orchestrator.js";
19
19
  import { writeDebateMarkdown } from "./output.js";
20
20
  import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./update.js";
21
21
  import { createSessionContext } from "./session.js";
22
22
  import { getStringListFlag, parseArgs } from "./args.js";
23
23
  import { askAgentSeedsForMode, clearTuiRunOverrides } from "./tuiState.js";
24
- import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
25
- import { getPackageVersion } from "./version.js";
24
+ import { detectedAgentNames, detectionForCommand, isRetiredAgentName } from "./agentRegistry.js";
25
+ import { configuredOllamaTargets, DEFAULT_OLLAMA_BASE_URL, normalizeOllamaBaseUrl, OllamaUrlError, resolveOllamaBaseUrl } from "./ollamaUrl.js";
26
+ import { compareSemver, getLatestPackageVersion, getPackageVersion } from "./version.js";
26
27
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
27
28
  async function main() {
28
29
  const rawArgs = process.argv.slice(2);
@@ -88,7 +89,9 @@ async function main() {
88
89
  console.log(startupMessages.init.configExists(initConfigPath));
89
90
  return;
90
91
  }
91
- const discovery = await discoverLocalTools();
92
+ const discovery = await discoverLocalTools({
93
+ ollamaUrl: optionalString(parsed.flags["ollama-url"])
94
+ });
92
95
  const config = createConfigFromDiscovery(discovery);
93
96
  config.language = resolveLanguage({
94
97
  explicitLanguage: optionalString(parsed.flags.language),
@@ -104,7 +107,9 @@ async function main() {
104
107
  let config;
105
108
  let tuiNotice;
106
109
  if (!(await configExists(configPath))) {
107
- config = createConfigFromDiscovery(await discoverLocalTools());
110
+ config = createConfigFromDiscovery(await discoverLocalTools({
111
+ ollamaUrl: optionalString(parsed.flags["ollama-url"])
112
+ }));
108
113
  config.language = resolveLanguage({
109
114
  explicitLanguage: optionalString(parsed.flags.language),
110
115
  configLanguage: config.language
@@ -131,6 +136,7 @@ async function main() {
131
136
  let resetTuiRunOverridesOnNextTopic = false;
132
137
  let tuiMode = config.defaults?.mode ?? "debate";
133
138
  let tuiVersion = "";
139
+ let tuiLatestVersion;
134
140
  const handleTuiHomeInput = async (tuiInput) => {
135
141
  if (!tuiInput) {
136
142
  return "quit";
@@ -145,6 +151,12 @@ async function main() {
145
151
  const nextInput = await promptTuiHomeTopic(tuiMode, messages);
146
152
  return handleTuiHomeInput(nextInput);
147
153
  }
154
+ if (tuiInput.kind === "update") {
155
+ const info = await getUpdateInfo(tuiVersion);
156
+ renderTuiUpdate(formatUpdateInstructions(info, messages), messages);
157
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages);
158
+ return handleTuiHomeInput(nextInput);
159
+ }
148
160
  if (tuiInput.kind === "home") {
149
161
  return "continue";
150
162
  }
@@ -200,14 +212,21 @@ async function main() {
200
212
  return "run";
201
213
  };
202
214
  if (shouldOpenTuiHome(parsed)) {
203
- const syncResult = await syncInteractiveDetectedAgents(configPath, config);
215
+ const [syncResult, currentVersion, latestVersion] = await Promise.all([
216
+ syncInteractiveDetectedAgents(configPath, config),
217
+ getPackageVersion(),
218
+ getLatestPackageVersion()
219
+ ]);
204
220
  if (!tuiNotice && syncResult.addedAgents.length > 0) {
205
221
  tuiNotice = messages.config.syncAdded(configPath, syncResult.addedAgents.join(", "));
206
222
  }
207
223
  stayInTuiAfterSession = true;
208
- tuiVersion = await getPackageVersion();
224
+ tuiVersion = currentVersion;
225
+ tuiLatestVersion = latestVersion && compareSemver(currentVersion, latestVersion) < 0
226
+ ? latestVersion
227
+ : undefined;
209
228
  for (;;) {
210
- renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion });
229
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion, latestVersion: tuiLatestVersion });
211
230
  const tuiInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
212
231
  tuiNotice = undefined;
213
232
  const action = await handleTuiHomeInput(tuiInput);
@@ -282,6 +301,9 @@ async function main() {
282
301
  files: context.files,
283
302
  modelA: optionalString(parsed.flags["model-a"]),
284
303
  modelB: optionalString(parsed.flags["model-b"]),
304
+ ollamaUrl: optionalString(parsed.flags["ollama-url"])
305
+ ? normalizeOllamaBaseUrl(optionalString(parsed.flags["ollama-url"]))
306
+ : undefined,
285
307
  pullModels: Boolean(parsed.flags["pull-models"]),
286
308
  summaryAgent: resolveSummaryAgentOption(parsed.flags["summary-agent"], config.defaults, mode),
287
309
  summaryModel: optionalString(parsed.flags["summary-model"]),
@@ -318,7 +340,7 @@ async function main() {
318
340
  if (action === "quit")
319
341
  return;
320
342
  if (action === "continue") {
321
- renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion });
343
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion, latestVersion: tuiLatestVersion });
322
344
  continue;
323
345
  }
324
346
  if (action === "retry") {
@@ -358,7 +380,21 @@ async function runAgentsCommand(flags) {
358
380
  configLanguage: config.language
359
381
  });
360
382
  const messages = createTranslator(language);
361
- const discovery = await discoverLocalTools();
383
+ const discovery = await discoverLocalToolsForConfig(config, optionalString(flags["ollama-url"]));
384
+ if (flags.json) {
385
+ const fallbackAskAgents = [config.defaults?.agentA, config.defaults?.agentB]
386
+ .filter((name) => typeof name === "string" && !isRetiredAgentName(name));
387
+ process.stdout.write(JSON.stringify({
388
+ v: 1,
389
+ agents: listAgentsWithAvailability(config, discovery, messages),
390
+ defaults: {
391
+ askAgents: config.defaults?.askAgents?.length
392
+ ? config.defaults.askAgents.filter((name) => !isRetiredAgentName(name))
393
+ : fallbackAskAgents
394
+ }
395
+ }) + "\n");
396
+ return;
397
+ }
362
398
  printAgents(configPath, config, discovery, messages);
363
399
  }
364
400
  /**
@@ -394,7 +430,7 @@ async function runConfigCommand(flags) {
394
430
  return;
395
431
  }
396
432
  if (flags["sync-agents"]) {
397
- const discovery = await discoverLocalTools();
433
+ const discovery = await discoverLocalToolsForConfig(config, optionalString(flags["ollama-url"]));
398
434
  const result = syncDetectedAgentsDetailed(config, discovery);
399
435
  if (!result.changed) {
400
436
  console.log(messages.config.syncNoMissing(configPath));
@@ -639,6 +675,16 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
639
675
  }
640
676
  continue;
641
677
  }
678
+ if (input.kind === "ollama-url") {
679
+ try {
680
+ notice = await setTuiOllamaUrl(configPath, config, input.url, currentMessages);
681
+ changedRunDefaults = true;
682
+ }
683
+ catch (error) {
684
+ notice = formatRuntimeError(error, currentMessages);
685
+ }
686
+ continue;
687
+ }
642
688
  if (input.kind === "ollama-model") {
643
689
  try {
644
690
  notice = await setTuiOllamaModel(configPath, config, input.model, currentMessages);
@@ -661,8 +707,23 @@ async function runTuiConfigLoop(configPath, config, messages, initialMode) {
661
707
  }
662
708
  }
663
709
  }
710
+ async function setTuiOllamaUrl(configPath, config, value, messages) {
711
+ if (!Object.values(config.agents).some((agent) => agent.type === "ollama")) {
712
+ throw new Error(messages.config.ollamaModelNoAgent);
713
+ }
714
+ const normalized = isDefaultOllamaUrl(value)
715
+ ? DEFAULT_OLLAMA_BASE_URL
716
+ : normalizeOllamaBaseUrl(value);
717
+ const effective = resolveOllamaBaseUrl({ configUrl: normalized });
718
+ setOllamaBaseUrl(config, normalized);
719
+ await writeExampleConfig(configPath, config);
720
+ return messages.tui.ollamaUrlUpdated(normalized, effective);
721
+ }
722
+ function isDefaultOllamaUrl(value) {
723
+ return ["default", "defaut", "défaut", "local", "localhost"].includes(value.trim().toLowerCase());
724
+ }
664
725
  async function formatTuiOllamaInfo(config, messages) {
665
- const discovery = await discoverLocalTools();
726
+ const discovery = await discoverLocalToolsForConfig(config);
666
727
  const agent = config.agents["ollama-local"];
667
728
  if (agent?.type !== "ollama") {
668
729
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -681,7 +742,7 @@ async function setTuiOllamaModel(configPath, config, model, messages) {
681
742
  if (!trimmed) {
682
743
  throw new Error(messages.tui.ollamaModelUsage);
683
744
  }
684
- const discovery = await discoverLocalTools();
745
+ const discovery = await discoverLocalToolsForConfig(config);
685
746
  const agent = config.agents["ollama-local"];
686
747
  if (agent?.type !== "ollama") {
687
748
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -696,7 +757,7 @@ async function setTuiOllamaModel(configPath, config, model, messages) {
696
757
  : messages.config.ollamaModelNoChange(configPath, agent.model);
697
758
  }
698
759
  async function syncTuiOllamaModel(configPath, config, messages) {
699
- const discovery = await discoverLocalTools();
760
+ const discovery = await discoverLocalToolsForConfig(config);
700
761
  const agent = config.agents["ollama-local"];
701
762
  if (agent?.type !== "ollama") {
702
763
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -712,7 +773,7 @@ async function syncTuiOllamaModel(configPath, config, messages) {
712
773
  return messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel);
713
774
  }
714
775
  async function syncInteractiveDetectedAgents(configPath, config) {
715
- const discovery = await discoverLocalTools();
776
+ const discovery = await discoverLocalToolsForConfig(config);
716
777
  const result = syncDetectedAgentsDetailed(config, discovery);
717
778
  if (result.changed) {
718
779
  await writeExampleConfig(configPath, config);
@@ -833,7 +894,7 @@ function isAgentRole(value) {
833
894
  }
834
895
  const VALID_AGENT_ROLES = ["implementer", "reviewer", "architect", "scout", "critic", "summarizer"];
835
896
  async function runOllamaModelsCommand(config, json) {
836
- const discovery = await discoverLocalTools();
897
+ const discovery = await discoverLocalToolsForConfig(config);
837
898
  const agent = config.agents["ollama-local"];
838
899
  const currentModel = agent?.type === "ollama" ? agent.model : null;
839
900
  const payload = {
@@ -858,7 +919,7 @@ async function runSetOllamaModelCommand(configPath, config, model, messages) {
858
919
  if (!trimmed) {
859
920
  throw new Error(messages.common.optionRequiresValue("--set-ollama-model"));
860
921
  }
861
- const discovery = await discoverLocalTools();
922
+ const discovery = await discoverLocalToolsForConfig(config);
862
923
  const agent = config.agents["ollama-local"];
863
924
  if (agent?.type !== "ollama") {
864
925
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -873,7 +934,7 @@ async function runSetOllamaModelCommand(configPath, config, model, messages) {
873
934
  : messages.config.ollamaModelNoChange(configPath, agent.model));
874
935
  }
875
936
  async function runSyncOllamaModelCommand(configPath, config, messages) {
876
- const discovery = await discoverLocalTools();
937
+ const discovery = await discoverLocalToolsForConfig(config);
877
938
  const agent = config.agents["ollama-local"];
878
939
  if (agent?.type !== "ollama") {
879
940
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -1110,28 +1171,32 @@ function shouldOpenTuiHome(parsed) {
1110
1171
  && parsed.flags.plain !== true
1111
1172
  && parsed.flags.terminal !== true;
1112
1173
  }
1174
+ /** Lance la discovery avec la même adresse Ollama effective que la config et les overrides globaux. */
1175
+ async function discoverLocalToolsForConfig(config, ollamaUrl) {
1176
+ return discoverLocalTools({
1177
+ ollamaUrl,
1178
+ ollamaTargets: configuredOllamaTargets(config)
1179
+ });
1180
+ }
1113
1181
  /**
1114
- * Exécute la commande `palabre presets`.
1115
- *
1116
- * Sortie humaine par défaut (liste alignée), ou JSON avec `--json` pour les
1117
- * intégrations (extension VS Code, scripts shell). Le schéma JSON est versionné
1118
- * via le champ `v` au cas où on enrichirait plus tard (ex : description par
1119
- * preset, tags premium/local).
1120
- *
1121
- * @param flags - Flags parsés depuis la ligne de commande.
1182
+ * Exécute la commande `palabre presets` en sortie humaine ou JSON versionné.
1122
1183
  */
1123
1184
  async function runPresetsCommand(flags) {
1124
- const discovery = await discoverLocalTools();
1125
1185
  const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
1186
+ const ollamaUrl = optionalString(flags["ollama-url"]);
1126
1187
  const config = await configExists(configPath)
1127
1188
  ? await loadConfig(configPath)
1128
- : createConfigFromDiscovery(discovery);
1189
+ : undefined;
1190
+ const discovery = config
1191
+ ? await discoverLocalToolsForConfig(config, ollamaUrl)
1192
+ : await discoverLocalTools({ ollamaUrl });
1193
+ const resolvedConfig = config ?? createConfigFromDiscovery(discovery);
1129
1194
  const language = resolveLanguage({
1130
1195
  explicitLanguage: optionalString(flags.language),
1131
- configLanguage: config.language
1196
+ configLanguage: resolvedConfig.language
1132
1197
  });
1133
1198
  const messages = createTranslator(language);
1134
- const presets = listPresetsWithAvailability(config, discovery, messages);
1199
+ const presets = listPresetsWithAvailability(resolvedConfig, discovery, messages);
1135
1200
  if (flags.json) {
1136
1201
  process.stdout.write(JSON.stringify({ v: 1, presets }) + "\n");
1137
1202
  return;
@@ -1221,7 +1286,9 @@ function printContextWarnings(warnings, messages) {
1221
1286
  * @param discovery - Résultat de la découverte locale des outils.
1222
1287
  */
1223
1288
  function printAgents(configPath, config, discovery, messages) {
1224
- const entries = Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right));
1289
+ const entries = Object.entries(config.agents)
1290
+ .filter(([name]) => !isRetiredAgentName(name))
1291
+ .sort(([left], [right]) => left.localeCompare(right));
1225
1292
  console.log(messages.agents.config(configPath));
1226
1293
  console.log("");
1227
1294
  console.log(messages.agents.title);
@@ -1383,12 +1450,23 @@ async function resolveCommandMessages(flags) {
1383
1450
  }
1384
1451
  return createTranslator(resolveLanguage({ explicitLanguage, configLanguage }));
1385
1452
  }
1453
+ function formatRuntimeError(error, messages) {
1454
+ if (error instanceof AdapterError) {
1455
+ return formatAdapterError(error, messages);
1456
+ }
1457
+ if (error instanceof OllamaUrlError) {
1458
+ if (error.kind === "empty")
1459
+ return messages.common.ollamaUrlEmpty;
1460
+ if (error.kind === "protocol")
1461
+ return messages.common.ollamaUrlProtocol(error.protocol ?? "");
1462
+ return messages.common.ollamaUrlInvalid(error.value);
1463
+ }
1464
+ return error instanceof Error ? error.message : String(error);
1465
+ }
1386
1466
  main().catch((error) => {
1387
1467
  const language = safeStartupLanguage(process.argv.slice(2));
1388
1468
  const messages = createTranslator(language);
1389
- const message = error instanceof AdapterError
1390
- ? formatAdapterError(error, messages)
1391
- : error instanceof Error ? error.message : String(error);
1469
+ const message = formatRuntimeError(error, messages);
1392
1470
  console.error(`${messages.common.errorPrefix}: ${message}`);
1393
1471
  process.exitCode = 1;
1394
1472
  });
@@ -15,6 +15,9 @@ export const commonMessages = {
15
15
  configInvalidShape: (configPath) => `Config invalide: ${configPath} ne contient pas un objet JSON. Relance palabre init ou corrige le fichier.`,
16
16
  configMissingAgents: (configPath) => `Config invalide: ${configPath} ne déclare pas de bloc "agents". Relance palabre init ou ajoute au moins un agent.`,
17
17
  configEmptyAgents: (configPath) => `Config invalide: ${configPath} ne déclare aucun agent. Ajoute au moins un agent ou relance palabre init.`,
18
+ ollamaUrlEmpty: "L'adresse Ollama ne peut pas être vide.",
19
+ ollamaUrlInvalid: (value) => `Adresse Ollama invalide: ${value}.`,
20
+ ollamaUrlProtocol: (protocol) => `Protocole Ollama invalide: ${protocol}. Utilise http: ou https:.`,
18
21
  errorPrefix: "Erreur"
19
22
  },
20
23
  en: {
@@ -33,6 +36,9 @@ export const commonMessages = {
33
36
  configInvalidShape: (configPath) => `Invalid config: ${configPath} does not contain a JSON object. Run palabre init or fix the file.`,
34
37
  configMissingAgents: (configPath) => `Invalid config: ${configPath} has no "agents" block. Run palabre init or add at least one agent.`,
35
38
  configEmptyAgents: (configPath) => `Invalid config: ${configPath} declares no agent. Add at least one agent or run palabre init.`,
39
+ ollamaUrlEmpty: "The Ollama address cannot be empty.",
40
+ ollamaUrlInvalid: (value) => `Invalid Ollama address: ${value}.`,
41
+ ollamaUrlProtocol: (protocol) => `Invalid Ollama protocol: ${protocol}. Use http: or https:.`,
36
42
  errorPrefix: "Error"
37
43
  }
38
44
  };
@@ -41,7 +41,7 @@ export const doctorMessages = {
41
41
  promptModeInvalid: (name, value) => `${name}: promptMode invalide (${value}). Valeurs attendues: stdin ou argument.`,
42
42
  positiveTimeout: (name, field) => `${name}: ${field} doit être un nombre positif.`,
43
43
  ollamaModelMissing: (name) => `${name}: modèle Ollama absent.`,
44
- ollamaBaseUrlInvalid: (name, value) => `${name}: baseUrl Ollama invalide (${value}). Attendu: http://... ou https://...`,
44
+ ollamaBaseUrlInvalid: (name, value) => `${name}: baseUrl Ollama invalide (${value}). Attendu: une adresse HTTP(S), avec ou sans schema.`,
45
45
  customCommand: (prefix) => `${prefix} (commande custom non vérifiée par doctor)`,
46
46
  cliDetected: (prefix, path) => `${prefix} détectée (${path})`,
47
47
  cliMissing: (prefix) => `${prefix} non détectée dans PATH. Action: installe/authentifie la CLI ou corrige command dans la config.`,
@@ -107,7 +107,7 @@ export const doctorMessages = {
107
107
  promptModeInvalid: (name, value) => `${name}: invalid promptMode (${value}). Expected values: stdin or argument.`,
108
108
  positiveTimeout: (name, field) => `${name}: ${field} must be a positive number.`,
109
109
  ollamaModelMissing: (name) => `${name}: missing Ollama model.`,
110
- ollamaBaseUrlInvalid: (name, value) => `${name}: invalid Ollama baseUrl (${value}). Expected: http://... or https://...`,
110
+ ollamaBaseUrlInvalid: (name, value) => `${name}: invalid Ollama baseUrl (${value}). Expected: an HTTP(S) address, with or without a scheme.`,
111
111
  customCommand: (prefix) => `${prefix} (custom command not checked by doctor)`,
112
112
  cliDetected: (prefix, path) => `${prefix} detected (${path})`,
113
113
  cliMissing: (prefix) => `${prefix} not detected in PATH. Action: install/authenticate the CLI or fix command in the config.`,
@@ -22,6 +22,7 @@ Usage:
22
22
  Flags:
23
23
  --config <path> chemin de config explicite
24
24
  --language <fr|en> force la langue
25
+ --json sortie JSON pour les integrations
25
26
  `,
26
27
  presets: `
27
28
  Liste les presets de paires d'agents.
@@ -129,6 +130,7 @@ Flags:
129
130
  --preset <name> preset d'agents
130
131
  --agent-a <name> premier agent
131
132
  --agent-b <name> second agent
133
+ --ollama-url <url> surcharge l'adresse Ollama pour cette session
132
134
  --tui force l'interface TUI
133
135
  --terminal force le rendu terminal brut
134
136
  --renderer <kind> auto, pretty, plain, tui ou ndjson
@@ -144,6 +146,7 @@ Usage:
144
146
  Flags:
145
147
  --agents <names...> agents qui repondent, 4 maximum
146
148
  --summary-agent <n> agent de synthese pour ce lancement
149
+ --ollama-url <url> surcharge l'adresse Ollama pour cette session
147
150
  --tui force l'interface TUI
148
151
  --terminal force le rendu terminal brut
149
152
  --renderer <kind> auto, pretty, plain, tui ou ndjson
@@ -174,6 +177,7 @@ Usage:
174
177
  Flags:
175
178
  --config <path> explicit config path
176
179
  --language <fr|en> forces the language
180
+ --json JSON output for integrations
177
181
  `,
178
182
  presets: `
179
183
  Lists agent-pair presets.
@@ -281,6 +285,7 @@ Flags:
281
285
  --preset <name> agent preset
282
286
  --agent-a <name> first agent
283
287
  --agent-b <name> second agent
288
+ --ollama-url <url> overrides the Ollama address for this session
284
289
  --tui forces the TUI interface
285
290
  --terminal forces raw terminal rendering
286
291
  --renderer <kind> auto, pretty, plain, tui, or ndjson
@@ -296,6 +301,7 @@ Usage:
296
301
  Flags:
297
302
  --agents <names...> responding agents, 4 maximum
298
303
  --summary-agent <n> summary agent for this run
304
+ --ollama-url <url> overrides the Ollama address for this session
299
305
  --tui forces the TUI interface
300
306
  --terminal forces raw terminal rendering
301
307
  --renderer <kind> auto, pretty, plain, tui, or ndjson
@@ -1,6 +1,7 @@
1
1
  export const tuiMessages = {
2
2
  fr: {
3
3
  tagline: "Orchestrez des conversations entre agents IA",
4
+ updateAvailable: (current, latest) => `Mise a jour disponible: ${current} -> ${latest}. Utilise /update.`,
4
5
  modeLabel: (mode) => mode === "ask" ? "Ask" : "Debat",
5
6
  modeValue: (mode) => mode === "ask" ? "Ask" : "Debat",
6
7
  noValue: "non definis",
@@ -8,6 +9,8 @@ export const tuiMessages = {
8
9
  roles: "Roles",
9
10
  summary: "Synthese",
10
11
  ollamaModel: "Modele Ollama",
12
+ ollamaUrl: "Adresse Ollama configuree",
13
+ ollamaUrlEffective: "Adresse Ollama effective",
11
14
  responses: "Tours",
12
15
  folder: "Dossier",
13
16
  docs: "Docs",
@@ -24,6 +27,7 @@ export const tuiMessages = {
24
27
  helpNew: "assistant guide",
25
28
  helpRetry: "relancer la derniere session",
26
29
  helpHistory: "voir les derniers exports",
30
+ helpUpdate: "voir les informations de mise a jour",
27
31
  helpHelp: "aide",
28
32
  helpQuit: "quitter",
29
33
  helpFallback: "Tape un sujet ou une commande.",
@@ -78,10 +82,12 @@ export const tuiMessages = {
78
82
  turnsUsage: "Usage: /turns <tours>",
79
83
  summaryUsage: "Usage: /summary <agent|none>",
80
84
  ollamaModelUsage: "Usage: /ollama-model <modele>",
85
+ ollamaUrlUsage: "Usage: /ollama-url <url|default>",
81
86
  interfaceUsage: "Usage: /interface <tui|terminal>",
82
87
  languageUsage: "Usage: /language <fr|en>",
83
88
  rolesUsage: "Usage: /roles <role...>",
84
89
  ollamaInfoCommand: "afficher modeles installes",
90
+ ollamaUrlCommand: "modifier l'adresse (<url|default>)",
85
91
  ollamaSyncCommand: "choisir un modele installe disponible",
86
92
  interfaceDefault: (value) => `Interface par defaut: ${value}.`,
87
93
  languageUpdated: (value) => `Langue mise a jour: ${value}.`,
@@ -98,6 +104,7 @@ export const tuiMessages = {
98
104
  askSummaryAgent: (value) => `Synthese Ask: ${value}.`,
99
105
  debateSummaryAgent: (value) => `Synthese Debat: ${value}.`,
100
106
  ollamaInfo: (current, installed, api) => `Ollama ${api}. Modele actuel: ${current}. Modeles installes: ${installed}.`,
107
+ ollamaUrlUpdated: (configured, effective) => configured === effective ? `Adresse Ollama mise a jour: ${configured}.` : `Adresse Ollama configuree: ${configured}. Adresse effective via OLLAMA_HOST: ${effective}.`,
101
108
  ollamaUnavailable: (baseUrl) => `API Ollama indisponible (${baseUrl}). Lance ollama serve puis reessaie /ollama.`,
102
109
  askAgentsUpdated: (value) => `Agents Ask mis a jour: ${value}.`,
103
110
  debateAgentsUpdated: (value) => `Agents Debat mis a jour: ${value}.`,
@@ -114,6 +121,7 @@ export const tuiMessages = {
114
121
  },
115
122
  en: {
116
123
  tagline: "Orchestrate conversations between AI agents",
124
+ updateAvailable: (current, latest) => `Update available: ${current} -> ${latest}. Use /update.`,
117
125
  modeLabel: (mode) => mode === "ask" ? "Ask" : "Debate",
118
126
  modeValue: (mode) => mode === "ask" ? "Ask" : "Debate",
119
127
  noValue: "not set",
@@ -121,6 +129,8 @@ export const tuiMessages = {
121
129
  roles: "Roles",
122
130
  summary: "Summary",
123
131
  ollamaModel: "Ollama model",
132
+ ollamaUrl: "Configured Ollama address",
133
+ ollamaUrlEffective: "Effective Ollama address",
124
134
  responses: "Turns",
125
135
  folder: "Folder",
126
136
  docs: "Docs",
@@ -137,6 +147,7 @@ export const tuiMessages = {
137
147
  helpNew: "guided assistant",
138
148
  helpRetry: "rerun the last session",
139
149
  helpHistory: "show recent exports",
150
+ helpUpdate: "show update information",
140
151
  helpHelp: "help",
141
152
  helpQuit: "quit",
142
153
  helpFallback: "Type a topic or a command.",
@@ -191,10 +202,12 @@ export const tuiMessages = {
191
202
  turnsUsage: "Usage: /turns <turns>",
192
203
  summaryUsage: "Usage: /summary <agent|none>",
193
204
  ollamaModelUsage: "Usage: /ollama-model <model>",
205
+ ollamaUrlUsage: "Usage: /ollama-url <url|default>",
194
206
  interfaceUsage: "Usage: /interface <tui|terminal>",
195
207
  languageUsage: "Usage: /language <fr|en>",
196
208
  rolesUsage: "Usage: /roles <role...>",
197
209
  ollamaInfoCommand: "show installed models",
210
+ ollamaUrlCommand: "change the address (<url|default>)",
198
211
  ollamaSyncCommand: "choose an available installed model",
199
212
  interfaceDefault: (value) => `Default interface: ${value}.`,
200
213
  languageUpdated: (value) => `Language updated: ${value}.`,
@@ -211,6 +224,7 @@ export const tuiMessages = {
211
224
  askSummaryAgent: (value) => `Ask summary: ${value}.`,
212
225
  debateSummaryAgent: (value) => `Debate summary: ${value}.`,
213
226
  ollamaInfo: (current, installed, api) => `Ollama ${api}. Current model: ${current}. Installed models: ${installed}.`,
227
+ ollamaUrlUpdated: (configured, effective) => configured === effective ? `Ollama address updated: ${configured}.` : `Ollama address configured: ${configured}. Effective address from OLLAMA_HOST: ${effective}.`,
214
228
  ollamaUnavailable: (baseUrl) => `Ollama API unavailable (${baseUrl}). Run ollama serve, then try /ollama again.`,
215
229
  askAgentsUpdated: (value) => `Ask agents updated: ${value}.`,
216
230
  debateAgentsUpdated: (value) => `Debate agents updated: ${value}.`,
@@ -0,0 +1,76 @@
1
+ export const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
2
+ export class OllamaUrlError extends Error {
3
+ kind;
4
+ value;
5
+ protocol;
6
+ constructor(kind, value, protocol) {
7
+ super(kind === "empty"
8
+ ? "Invalid Ollama URL: the value is empty."
9
+ : kind === "protocol"
10
+ ? `Invalid Ollama URL protocol: ${protocol ?? ""}`
11
+ : `Invalid Ollama URL: ${value}`);
12
+ this.kind = kind;
13
+ this.value = value;
14
+ this.protocol = protocol;
15
+ this.name = "OllamaUrlError";
16
+ }
17
+ }
18
+ /**
19
+ * Résout l'adresse client Ollama selon la priorité produit :
20
+ * flag CLI > OLLAMA_HOST > config agent > serveur local par défaut.
21
+ */
22
+ export function resolveOllamaBaseUrl(sources = {}) {
23
+ const value = firstNonEmpty(sources.cliUrl, sources.envUrl ?? process.env.OLLAMA_HOST, sources.configUrl, DEFAULT_OLLAMA_BASE_URL);
24
+ return normalizeOllamaBaseUrl(value);
25
+ }
26
+ /** Normalise les formats acceptés par Ollama en URL HTTP(S) utilisable par fetch. */
27
+ export function normalizeOllamaBaseUrl(value) {
28
+ const trimmed = value.trim();
29
+ if (!trimmed) {
30
+ throw new OllamaUrlError("empty", value);
31
+ }
32
+ const withHost = trimmed.startsWith(":") ? `127.0.0.1${trimmed}` : trimmed;
33
+ const hasExplicitScheme = withHost.includes("://");
34
+ const withScheme = hasExplicitScheme ? withHost : `http://${withHost}`;
35
+ let url;
36
+ try {
37
+ url = new URL(withScheme);
38
+ }
39
+ catch {
40
+ throw new OllamaUrlError("invalid", value);
41
+ }
42
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
43
+ throw new OllamaUrlError("protocol", value, url.protocol);
44
+ }
45
+ if (!url.hostname || url.username || url.password || url.search || url.hash) {
46
+ throw new OllamaUrlError("invalid", value);
47
+ }
48
+ if (!hasExplicitScheme && !url.port) {
49
+ url.port = "11434";
50
+ }
51
+ if (url.hostname === "0.0.0.0") {
52
+ url.hostname = "127.0.0.1";
53
+ }
54
+ else if (url.hostname === "[::]") {
55
+ url.hostname = "[::1]";
56
+ }
57
+ return url.toString().replace(/\/+$/, "");
58
+ }
59
+ /** Retourne l'URL configurée pour l'agent Ollama principal, puis le premier agent Ollama. */
60
+ export function configuredOllamaBaseUrl(config) {
61
+ const primary = config.agents["ollama-local"];
62
+ if (primary?.type === "ollama" && primary.baseUrl) {
63
+ return primary.baseUrl;
64
+ }
65
+ const configured = Object.values(config.agents).find((agent) => agent.type === "ollama" && agent.baseUrl);
66
+ return configured?.type === "ollama" ? configured.baseUrl : undefined;
67
+ }
68
+ /** Retourne les URL configurées par nom d'agent Ollama. */
69
+ export function configuredOllamaTargets(config) {
70
+ return Object.fromEntries(Object.entries(config.agents)
71
+ .filter(([, agent]) => agent.type === "ollama")
72
+ .map(([name, agent]) => [name, agent.type === "ollama" ? agent.baseUrl : undefined]));
73
+ }
74
+ function firstNonEmpty(...values) {
75
+ return values.find((value) => Boolean(value?.trim())) ?? DEFAULT_OLLAMA_BASE_URL;
76
+ }
@@ -1,6 +1,7 @@
1
1
  import { createAgent } from "./adapters/index.js";
2
2
  import { AdapterError } from "./errors.js";
3
3
  import { createTranslator } from "./i18n.js";
4
+ import { OllamaUrlError } from "./ollamaUrl.js";
4
5
  export const MAX_ASK_AGENTS = 4;
5
6
  /**
6
7
  * Point d'entrée de l'orchestration.
@@ -27,8 +28,8 @@ export async function runDebate(config, options, renderer, messages = createTran
27
28
  { name: options.agentB, role: agentBConfig.role, type: agentBConfig.type }
28
29
  ]);
29
30
  const agents = [
30
- createAgent(options.agentA, agentAConfig),
31
- createAgent(options.agentB, agentBConfig)
31
+ createAgent(options.agentA, agentAConfig, { ollamaUrl: options.ollamaUrl }),
32
+ createAgent(options.agentB, agentBConfig, { ollamaUrl: options.ollamaUrl })
32
33
  ];
33
34
  const transcript = [];
34
35
  let stopReason;
@@ -73,7 +74,7 @@ export async function runDebate(config, options, renderer, messages = createTran
73
74
  agent: current.name,
74
75
  role: current.role,
75
76
  turn
76
- });
77
+ }, messages);
77
78
  renderer?.error(failure);
78
79
  return {
79
80
  options,
@@ -126,7 +127,7 @@ export async function runDebate(config, options, renderer, messages = createTran
126
127
  agent: resolveSummaryAgentName(options),
127
128
  role: summaryRole(),
128
129
  turn: transcript.length + 1
129
- });
130
+ }, messages);
130
131
  renderer?.error(failure);
131
132
  }
132
133
  }
@@ -163,7 +164,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
163
164
  role: agentConfig.role,
164
165
  type: agentConfig.type
165
166
  })));
166
- const agents = agentEntries.map(([name, agentConfig]) => createAgent(name, agentConfig));
167
+ const agents = agentEntries.map(([name, agentConfig]) => createAgent(name, agentConfig, { ollamaUrl: options.ollamaUrl }));
167
168
  const transcript = [];
168
169
  for (let index = 0; index < agents.length; index += 1) {
169
170
  const current = agents[index];
@@ -212,7 +213,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
212
213
  agent: current.name,
213
214
  role: current.role,
214
215
  turn: response
215
- });
216
+ }, messages);
216
217
  renderer?.error(failure);
217
218
  return {
218
219
  options,
@@ -264,7 +265,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
264
265
  agent: resolveSummaryAgentName(options),
265
266
  role: summaryRole(),
266
267
  turn: transcript.length + 1
267
- });
268
+ }, messages);
268
269
  renderer?.error(failure);
269
270
  }
270
271
  }
@@ -335,7 +336,7 @@ async function generateSummary(config, options, transcript, renderer, messages =
335
336
  if (!summaryConfig) {
336
337
  throw new Error(messages.orchestrator.unknownSummaryAgent(summaryAgentName));
337
338
  }
338
- const summaryAgent = createAgent(summaryAgentName, summaryConfig);
339
+ const summaryAgent = createAgent(summaryAgentName, summaryConfig, { ollamaUrl: options.ollamaUrl });
339
340
  const role = summaryRole();
340
341
  renderer?.summaryStart(summaryAgent.name, role);
341
342
  renderer?.thinkingStart(summaryAgent.name, role);
@@ -393,7 +394,7 @@ function resolveSummaryAgentName(options) {
393
394
  }
394
395
  return options.agentB;
395
396
  }
396
- function toDebateFailure(error, context) {
397
+ function toDebateFailure(error, context, messages) {
397
398
  if (error instanceof AdapterError) {
398
399
  return {
399
400
  phase: context.phase,
@@ -405,6 +406,18 @@ function toDebateFailure(error, context) {
405
406
  details: error.details
406
407
  };
407
408
  }
409
+ if (error instanceof OllamaUrlError) {
410
+ const message = error.kind === "empty"
411
+ ? messages.common.ollamaUrlEmpty
412
+ : error.kind === "protocol"
413
+ ? messages.common.ollamaUrlProtocol(error.protocol ?? "")
414
+ : messages.common.ollamaUrlInvalid(error.value);
415
+ return {
416
+ ...context,
417
+ kind: "unknown",
418
+ message
419
+ };
420
+ }
408
421
  return {
409
422
  phase: context.phase,
410
423
  agent: context.agent,
package/dist/presets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { detectionForCommand } from "./agentRegistry.js";
1
+ import { detectionForCommand, isRetiredAgentName } from "./agentRegistry.js";
2
2
  const presets = {
3
3
  "codex-claude": {
4
4
  agentA: "codex",
@@ -161,6 +161,26 @@ export function listPresetsWithAvailability(config, discovery, messages) {
161
161
  };
162
162
  });
163
163
  }
164
+ /**
165
+ * Retourne tous les agents de la config avec la même disponibilité que celle
166
+ * utilisée pour les presets. Les intégrations ne doivent pas réimplémenter la
167
+ * découverte des commandes ou des modèles Ollama.
168
+ */
169
+ export function listAgentsWithAvailability(config, discovery, messages) {
170
+ return Object.entries(config.agents)
171
+ .filter(([name]) => !isRetiredAgentName(name))
172
+ .sort(([left], [right]) => left.localeCompare(right))
173
+ .map(([name, agentConfig]) => {
174
+ const check = checkAgentAvailability(name, config, discovery, messages);
175
+ return {
176
+ name,
177
+ type: agentConfig.type,
178
+ role: agentConfig.role,
179
+ available: check.available,
180
+ ...(check.available ? {} : { unavailableReason: check.reason })
181
+ };
182
+ });
183
+ }
164
184
  /** Recherche inverse : retourne le nom du preset correspondant à une paire `(agentA, agentB)`, ou `undefined`. */
165
185
  export function findPresetNameForPair(agentA, agentB) {
166
186
  return Object.entries(presets).find(([, preset]) => preset.agentA === agentA && preset.agentB === agentB)?.[0];
@@ -171,12 +191,13 @@ function checkAgentAvailability(agentName, config, discovery, messages) {
171
191
  return unavailable(agentName, messages?.presets.missingAgent(agentName) ?? `agent absent de la config: ${agentName}`);
172
192
  }
173
193
  if (agent.type === "ollama") {
174
- if (!discovery.ollama.available) {
175
- return unavailable(agentName, discovery.ollama.commandAvailable
194
+ const ollama = discovery.ollamaAgents?.[agentName] ?? discovery.ollama;
195
+ if (!ollama.available) {
196
+ return unavailable(agentName, ollama.commandAvailable
176
197
  ? messages?.presets.ollamaUnreachable(agentName) ?? `Ollama non joignable pour ${agentName}`
177
198
  : messages?.presets.ollamaNotDetected(agentName) ?? `Ollama non détecté pour ${agentName}`);
178
199
  }
179
- if (!discovery.ollama.models.includes(agent.model)) {
200
+ if (!ollama.models.includes(agent.model)) {
180
201
  return unavailable(agentName, messages?.presets.missingOllamaModel(agentName, agent.model) ?? `modèle Ollama absent pour ${agentName}: ${agent.model}`);
181
202
  }
182
203
  return available(agentName);
@@ -2,6 +2,8 @@ import { createInterface } from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import { isRetiredAgentName } from "../agentRegistry.js";
6
+ import { DEFAULT_OLLAMA_BASE_URL, resolveOllamaBaseUrl } from "../ollamaUrl.js";
5
7
  const supportsColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
6
8
  const supportsInteractiveOutput = Boolean(process.stdout.isTTY);
7
9
  /** Cree le premier renderer TUI leger, sans dependance UI externe. */
@@ -35,10 +37,14 @@ export function renderTuiHome(config, _configPath, messages, state = {}) {
35
37
  const summary = mode === "ask"
36
38
  ? defaults.askSummaryAgent ?? defaults.summaryAgent ?? messages.tui.lastAskAgent
37
39
  : defaults.summaryAgent ?? defaults.agentB ?? "agent B";
40
+ const version = state.version ?? "0.0.0";
41
+ const versionLines = state.latestVersion
42
+ ? [dim(`v${version}`), accent(messages.tui.updateAvailable(version, state.latestVersion))]
43
+ : [dim(`v${version}`)];
38
44
  const lines = [
39
45
  "",
40
46
  ...centerLogo(viewport, messages),
41
- ...centerBlock([dim(`v${state.version ?? "0.0.0"}`)], viewport),
47
+ ...centerBlock(versionLines, viewport),
42
48
  "",
43
49
  ...centerBlock(composerCard([
44
50
  `${accent(messages.tui.modeValue(mode))} ${dim("·")} ${mode === "ask" ? askAgents : debateAgents}`,
@@ -56,6 +62,21 @@ export function renderTuiHome(config, _configPath, messages, state = {}) {
56
62
  ];
57
63
  process.stdout.write(lines.join("\n") + "\n");
58
64
  }
65
+ /** Affiche les instructions de mise a jour sans quitter le TUI. */
66
+ export function renderTuiUpdate(instructions, messages) {
67
+ if (supportsInteractiveOutput) {
68
+ clearScreen();
69
+ }
70
+ const viewport = viewportWidth();
71
+ const width = surfaceWidth();
72
+ process.stdout.write([
73
+ "",
74
+ ...centerLogo(viewport, messages),
75
+ "",
76
+ ...centerBlock(card(instructions.split(/\r?\n/), width), viewport),
77
+ ""
78
+ ].join("\n"));
79
+ }
59
80
  /** Affiche l'aide interne du composer TUI. */
60
81
  export function renderTuiHelp(messages) {
61
82
  if (supportsInteractiveOutput) {
@@ -80,6 +101,7 @@ export function renderTuiHelp(messages) {
80
101
  row("/new", messages.tui.helpNew),
81
102
  row("/retry", messages.tui.helpRetry),
82
103
  row("/history", messages.tui.helpHistory),
104
+ row("/update", messages.tui.helpUpdate),
83
105
  row("/home", messages.tui.backCommand),
84
106
  row("/help", messages.tui.helpHelp),
85
107
  row("/quit", messages.tui.helpQuit),
@@ -277,6 +299,8 @@ export function renderTuiConfig(config, configPath, mode, messages, state = {})
277
299
  : defaults.summaryAgent ?? defaults.agentB ?? messages.tui.noValue;
278
300
  const ollamaAgent = config.agents["ollama-local"];
279
301
  const ollamaModel = ollamaAgent?.type === "ollama" ? ollamaAgent.model : undefined;
302
+ const ollamaUrl = ollamaAgent?.type === "ollama" ? ollamaAgent.baseUrl ?? DEFAULT_OLLAMA_BASE_URL : undefined;
303
+ const ollamaEffectiveUrl = ollamaUrl ? safeEffectiveOllamaUrl(ollamaUrl) : undefined;
280
304
  const currentLines = mode === "ask"
281
305
  ? [
282
306
  row(messages.tui.activeAgents, askAgents),
@@ -310,6 +334,8 @@ export function renderTuiConfig(config, configPath, mode, messages, state = {})
310
334
  bold(messages.tui.configTitle),
311
335
  "",
312
336
  row(messages.tui.activeMode, messages.tui.modeValue(mode)),
337
+ ...(ollamaUrl ? [row(messages.tui.ollamaUrl, ollamaUrl)] : []),
338
+ ...(ollamaEffectiveUrl && ollamaEffectiveUrl !== ollamaUrl ? [row(messages.tui.ollamaUrlEffective, ollamaEffectiveUrl)] : []),
313
339
  row(messages.tui.configFile, configPath),
314
340
  row(messages.tui.interface, defaults.interface ?? "tui"),
315
341
  row(messages.tui.language, config.language ?? "fr"),
@@ -323,6 +349,7 @@ export function renderTuiConfig(config, configPath, mode, messages, state = {})
323
349
  ...(ollamaModel ? [
324
350
  row("/ollama", messages.tui.ollamaInfoCommand),
325
351
  row("/ollama-model", messages.tui.ollamaModelUsage),
352
+ row("/ollama-url", messages.tui.ollamaUrlCommand),
326
353
  row("/ollama-sync", messages.tui.ollamaSyncCommand),
327
354
  ""
328
355
  ] : []),
@@ -336,6 +363,18 @@ export function renderTuiConfig(config, configPath, mode, messages, state = {})
336
363
  ];
337
364
  process.stdout.write(lines.join("\n") + "\n");
338
365
  }
366
+ export function parseTuiOllamaUrlCommand(parts, messages) {
367
+ const value = parts[1];
368
+ return value ? { kind: "ollama-url", url: value } : { kind: "unknown", message: messages.tui.ollamaUrlUsage };
369
+ }
370
+ function safeEffectiveOllamaUrl(configUrl) {
371
+ try {
372
+ return resolveOllamaBaseUrl({ configUrl });
373
+ }
374
+ catch {
375
+ return process.env.OLLAMA_HOST?.trim() || configUrl;
376
+ }
377
+ }
339
378
  let lastTuiInterruptAt = 0;
340
379
  const doubleInterruptMs = 1200;
341
380
  function nextInterruptKind() {
@@ -394,6 +433,9 @@ export async function promptTuiHomeTopic(mode = "debate", messages, options = {}
394
433
  if (command === "/retry") {
395
434
  return { kind: "retry" };
396
435
  }
436
+ if (command === "/update") {
437
+ return { kind: "update" };
438
+ }
397
439
  if (command === "/historique" || command === "/history") {
398
440
  return { kind: "history" };
399
441
  }
@@ -495,6 +537,9 @@ export async function promptTuiConfigCommand(mode, messages) {
495
537
  const value = parts[1];
496
538
  return value ? { kind: "ollama-model", model: value } : { kind: "ollama-info" };
497
539
  }
540
+ if (command === "/ollama-url" || command === "/ollama-host") {
541
+ return parseTuiOllamaUrlCommand(parts, messages);
542
+ }
498
543
  if (command === "/ollama-model") {
499
544
  const value = parts[1];
500
545
  return value ? { kind: "ollama-model", model: value } : { kind: "unknown", message: messages.tui.ollamaModelUsage };
@@ -829,9 +874,9 @@ function activeAgentNamesForMode(config, mode) {
829
874
  const agents = defaults.askAgents && defaults.askAgents.length > 0
830
875
  ? defaults.askAgents
831
876
  : [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent));
832
- return agents.filter((agent) => Boolean(config.agents[agent]));
877
+ return agents.filter((agent) => Boolean(config.agents[agent]) && !isRetiredAgentName(agent));
833
878
  }
834
- return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
879
+ return [defaults.agentA, defaults.agentB].filter((agent) => typeof agent === "string" && Boolean(config.agents[agent]) && !isRetiredAgentName(agent));
835
880
  }
836
881
  function agentInventoryLine(config, messages) {
837
882
  const agents = Object.entries(config.agents)
@@ -849,15 +894,12 @@ function agentInventoryRows(config, messages) {
849
894
  }
850
895
  return entries.map(([name, agent]) => row(name, `${agent.type} ${dim("·")} ${agent.role}`));
851
896
  }
852
- function isRetiredAgentName(name) {
853
- return name === "gemini";
854
- }
855
897
  function exampleAgentsForMode(config, mode) {
856
898
  const activeAgents = activeAgentNamesForMode(config, mode);
857
899
  if (activeAgents.length > 0) {
858
900
  return activeAgents;
859
901
  }
860
- const available = Object.keys(config.agents).sort();
902
+ const available = Object.keys(config.agents).filter((agent) => !isRetiredAgentName(agent)).sort();
861
903
  return mode === "ask" ? available.slice(0, 3) : available.slice(0, 2);
862
904
  }
863
905
  function documentationUrl(config) {
package/dist/tuiState.js CHANGED
@@ -4,6 +4,7 @@ const TUI_RUN_OVERRIDE_FLAGS = [
4
4
  "agent-b",
5
5
  "agents",
6
6
  "model-a",
7
+ "ollama-url",
7
8
  "model-b",
8
9
  "summary-agent",
9
10
  "summary-model",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palabre",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
5
  "license": "MIT",
6
6
  "type": "module",