palabre 0.8.1 → 0.9.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.
package/README.md CHANGED
@@ -70,6 +70,7 @@ PALABRE does not list models: they change often and depend on each CLI or user a
70
70
 
71
71
  PALABRE exposes versioned JSON outputs for external clients:
72
72
 
73
+ - `palabre agents --json` to read configured agents and CLI-owned availability;
73
74
  - `palabre presets --json` to read available agent pairs;
74
75
  - `palabre context scan --json` to preview the context `--context` would retain;
75
76
  - `--renderer ndjson` or `--json` to follow a debate event by event.
@@ -178,6 +179,7 @@ PALABRE ne liste pas les modèles : ils changent souvent et dépendent de chaque
178
179
 
179
180
  PALABRE expose des sorties JSON versionnées pour les clients externes :
180
181
 
182
+ - `palabre agents --json` pour lire les agents configurés et leur disponibilité calculée par le CLI ;
181
183
  - `palabre presets --json` pour lire les paires d'agents disponibles ;
182
184
  - `palabre context scan --json` pour prévisualiser le contexte que `--context` retiendrait ;
183
185
  - `--renderer ndjson` ou `--json` pour suivre un débat événement par événement.
@@ -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" },
@@ -0,0 +1,85 @@
1
+ import { configExists, loadConfig, resolveDefaultConfigPath } from "../config.js";
2
+ import { discoverLocalToolsForConfig } from "../discovery.js";
3
+ import { createTranslator, resolveLanguage } from "../i18n.js";
4
+ import { turnsOrDefault } from "../limits.js";
5
+ import { listAgentsWithAvailability } from "../presets.js";
6
+ import { detectionForCommand, isRetiredAgentName } from "../agentRegistry.js";
7
+ import { optionalString } from "./shared.js";
8
+ /** Exécute `palabre agents` en sortie humaine ou JSON versionné. */
9
+ export async function runAgentsCommand(flags) {
10
+ const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
11
+ if (!(await configExists(configPath))) {
12
+ const messages = createTranslator(resolveLanguage({ explicitLanguage: optionalString(flags.language) }));
13
+ throw new Error(messages.agents.noConfig);
14
+ }
15
+ const config = await loadConfig(configPath);
16
+ const messages = createTranslator(resolveLanguage({
17
+ explicitLanguage: optionalString(flags.language),
18
+ configLanguage: config.language
19
+ }));
20
+ const discovery = await discoverLocalToolsForConfig(config, optionalString(flags["ollama-url"]));
21
+ if (flags.json) {
22
+ const fallbackAskAgents = [config.defaults?.agentA, config.defaults?.agentB]
23
+ .filter((name) => typeof name === "string" && !isRetiredAgentName(name));
24
+ process.stdout.write(JSON.stringify({
25
+ v: 1,
26
+ agents: listAgentsWithAvailability(config, discovery, messages),
27
+ defaults: {
28
+ askAgents: config.defaults?.askAgents?.length
29
+ ? config.defaults.askAgents.filter((name) => !isRetiredAgentName(name))
30
+ : fallbackAskAgents
31
+ }
32
+ }) + "\n");
33
+ return;
34
+ }
35
+ printAgents(configPath, config, discovery, messages);
36
+ }
37
+ function printAgents(configPath, config, discovery, messages) {
38
+ const entries = Object.entries(config.agents)
39
+ .filter(([name]) => !isRetiredAgentName(name))
40
+ .sort(([left], [right]) => left.localeCompare(right));
41
+ console.log(messages.agents.config(configPath));
42
+ console.log("");
43
+ console.log(messages.agents.title);
44
+ for (const [name, agentConfig] of entries) {
45
+ const status = formatAgentDetection(name, agentConfig, discovery, messages);
46
+ const defaults = formatAgentDefaults(name, config, messages);
47
+ const details = formatAgentDetails(agentConfig, messages);
48
+ console.log(`- ${name.padEnd(13)} ${`${agentConfig.type}/${agentConfig.role}`.padEnd(18)} ${status}${defaults ? ` | ${defaults}` : ""}`);
49
+ if (details)
50
+ console.log(` ${details}`);
51
+ }
52
+ console.log("");
53
+ console.log(messages.agents.defaults(config.defaults?.agentA ?? messages.agents.none, config.defaults?.agentB ?? messages.agents.none, turnsOrDefault(config.defaults?.turns), config.defaults?.summaryAgent ?? messages.agents.summaryAgentB, config.defaults?.askSummaryAgent));
54
+ }
55
+ function formatAgentDefaults(name, config, messages) {
56
+ const labels = [];
57
+ if (config.defaults?.agentA === name)
58
+ labels.push(messages.agents.defaultAgentA);
59
+ if (config.defaults?.agentB === name)
60
+ labels.push(messages.agents.defaultAgentB);
61
+ if (config.defaults?.summaryAgent === name)
62
+ labels.push(messages.agents.defaultSummary);
63
+ if (config.defaults?.askSummaryAgent === name)
64
+ labels.push(messages.agents.defaultAskSummary);
65
+ return labels.join(", ");
66
+ }
67
+ function formatAgentDetails(agentConfig, messages) {
68
+ return agentConfig.type === "ollama"
69
+ ? messages.agents.model(agentConfig.model)
70
+ : messages.agents.command(agentConfig.command, agentConfig.model);
71
+ }
72
+ function formatAgentDetection(name, agentConfig, discovery, messages) {
73
+ if (agentConfig.type === "ollama") {
74
+ const ollama = discovery.ollama;
75
+ if (!ollama.available)
76
+ return ollama.commandAvailable ? messages.agents.ollamaUnreachable : messages.agents.ollamaNotDetected;
77
+ return ollama.models.includes(agentConfig.model) ? messages.agents.detected() : messages.agents.missingModel(agentConfig.model);
78
+ }
79
+ const detection = cliDetectionForAgent(name, agentConfig, discovery);
80
+ return detection.available ? messages.agents.detected(detection.command) : messages.agents.notDetected;
81
+ }
82
+ function cliDetectionForAgent(name, agentConfig, discovery) {
83
+ const command = agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name;
84
+ return detectionForCommand(command, discovery) ?? { available: true, command };
85
+ }
@@ -0,0 +1,25 @@
1
+ import { buildContextScan } from "../contextScan.js";
2
+ import { createTranslator, resolveLanguage } from "../i18n.js";
3
+ import { optionalString } from "./shared.js";
4
+ /** Exécute `palabre context scan` en sortie humaine ou JSON versionné. */
5
+ export async function runContextCommand(flags, positionals) {
6
+ const messages = createTranslator(resolveLanguage({ explicitLanguage: optionalString(flags.language) }));
7
+ const subcommand = positionals[0] ?? "scan";
8
+ if (subcommand !== "scan") {
9
+ throw new Error(messages.common.unknownCommand(`context ${subcommand}`, "context scan"));
10
+ }
11
+ const result = await buildContextScan(positionals.slice(1), process.cwd(), messages);
12
+ if (flags.json) {
13
+ console.log(JSON.stringify(result, null, 2));
14
+ return;
15
+ }
16
+ for (const folder of result.items.filter((item) => item.kind === "folder")) {
17
+ console.log(`[folder] ${folder.path}`);
18
+ }
19
+ for (const file of result.items.filter((item) => item.kind === "file")) {
20
+ console.log(`[file] ${file.path} (${file.sizeBytes} bytes)`);
21
+ }
22
+ for (const warning of result.warnings) {
23
+ console.error(`${messages.renderers.warningPrefix} ${warning}`);
24
+ }
25
+ }
@@ -0,0 +1,28 @@
1
+ import { configExists, loadConfig, resolveDefaultConfigPath, resolveOutputDir } from "../config.js";
2
+ import { listHistoryEntries } from "../history.js";
3
+ import { createTranslator, resolveLanguage } from "../i18n.js";
4
+ import { optionalString } from "./shared.js";
5
+ /** Exécute `palabre history` en sortie humaine ou JSON versionné. */
6
+ export async function runHistoryCommand(flags) {
7
+ const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
8
+ const config = await configExists(configPath) ? await loadConfig(configPath) : undefined;
9
+ const messages = createTranslator(resolveLanguage({
10
+ explicitLanguage: optionalString(flags.language),
11
+ configLanguage: config?.language
12
+ }));
13
+ const entries = await listHistoryEntries(resolveOutputDir(config?.outputDir));
14
+ if (flags.json) {
15
+ process.stdout.write(JSON.stringify({ v: 1, history: entries }) + "\n");
16
+ return;
17
+ }
18
+ console.log(messages.tui.historyTitle);
19
+ console.log("");
20
+ if (entries.length === 0) {
21
+ console.log(messages.tui.historyEmpty);
22
+ return;
23
+ }
24
+ for (const entry of entries) {
25
+ console.log(`- ${entry.date || entry.fileName} | ${entry.mode} | ${entry.topic}`);
26
+ console.log(` ${entry.path}`);
27
+ }
28
+ }
@@ -0,0 +1,59 @@
1
+ import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, writeExampleConfig } from "../config.js";
2
+ import { discoverLocalTools } from "../discovery.js";
3
+ import { detectedAgentNames } from "../agentRegistry.js";
4
+ import { createTranslator, DEFAULT_LANGUAGE, resolveLanguage } from "../i18n.js";
5
+ import { optionalString } from "./shared.js";
6
+ /** Exécute `palabre init` ou son alias `setup`. */
7
+ export async function runInitCommand(flags) {
8
+ const configPath = optionalString(flags.config) ?? (flags.local ? DEFAULT_CONFIG_PATH : GLOBAL_CONFIG_PATH);
9
+ const startupMessages = createTranslator(resolveLanguage({ explicitLanguage: optionalString(flags.language) }));
10
+ if (await configExists(configPath)) {
11
+ console.log(startupMessages.init.configExists(configPath));
12
+ return;
13
+ }
14
+ const discovery = await discoverLocalTools({ ollamaUrl: optionalString(flags["ollama-url"]) });
15
+ const config = createConfigFromDiscovery(discovery);
16
+ config.language = resolveLanguage({
17
+ explicitLanguage: optionalString(flags.language),
18
+ configLanguage: config.language
19
+ });
20
+ const messages = createTranslator(config.language);
21
+ await writeExampleConfig(configPath, config);
22
+ console.log(messages.init.configCreated(configPath));
23
+ printInitDiscovery(discovery, config, messages);
24
+ }
25
+ function printInitDiscovery(discovery, config, messages) {
26
+ console.log("");
27
+ console.log(messages.init.localDetectionTitle);
28
+ console.log(`- Codex CLI: ${formatCommandDetection(discovery.codex, messages)}`);
29
+ console.log(`- Claude CLI: ${formatCommandDetection(discovery.claude, messages)}`);
30
+ console.log(`- Antigravity CLI: ${formatCommandDetection(discovery.antigravity, messages)}`);
31
+ console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode, messages)}`);
32
+ console.log(`- Mistral Vibe CLI: ${formatCommandDetection(discovery.vibe, messages)}`);
33
+ console.log(`- Ollama API: ${formatOllamaDetection(discovery.ollama, messages)}`);
34
+ console.log("");
35
+ console.log(config.defaults?.agentA && config.defaults.agentB
36
+ ? messages.init.defaults(config.defaults.agentA, config.defaults.agentB)
37
+ : messages.init.noDefaultPair(formatDetectedAgentSummary(discovery, config.language ?? DEFAULT_LANGUAGE)));
38
+ console.log(messages.init.languageHint(config.language ?? DEFAULT_LANGUAGE));
39
+ }
40
+ function formatDetectedAgentSummary(discovery, language) {
41
+ const names = detectedAgentNames(discovery);
42
+ if (names.length === 0)
43
+ return language === "en" ? "no agent detected" : "aucun agent détecté";
44
+ if (names.length === 1) {
45
+ return language === "en" ? `only one agent detected (${names[0]})` : `un seul agent détecté (${names[0]})`;
46
+ }
47
+ return language === "en"
48
+ ? `no usable pair detected among ${names.join(", ")}`
49
+ : `aucune paire utilisable détectée parmi ${names.join(", ")}`;
50
+ }
51
+ function formatCommandDetection(detection, messages) {
52
+ return detection.available ? messages.init.commandDetected(detection.command) : messages.init.commandMissing;
53
+ }
54
+ function formatOllamaDetection(detection, messages) {
55
+ if (!detection.available) {
56
+ return detection.commandAvailable ? messages.init.ollamaServerUnreachable(detection.baseUrl) : messages.init.ollamaMissing;
57
+ }
58
+ return messages.init.ollamaDetected(detection.models.length);
59
+ }
@@ -0,0 +1,34 @@
1
+ import { configExists, createConfigFromDiscovery, loadConfig, resolveDefaultConfigPath } from "../config.js";
2
+ import { discoverLocalTools, discoverLocalToolsForConfig } from "../discovery.js";
3
+ import { createTranslator, resolveLanguage } from "../i18n.js";
4
+ import { listPresetsWithAvailability } from "../presets.js";
5
+ import { optionalString } from "./shared.js";
6
+ /** Exécute `palabre presets` en sortie humaine ou JSON versionné. */
7
+ export async function runPresetsCommand(flags) {
8
+ const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
9
+ const ollamaUrl = optionalString(flags["ollama-url"]);
10
+ const config = await configExists(configPath) ? await loadConfig(configPath) : undefined;
11
+ const discovery = config
12
+ ? await discoverLocalToolsForConfig(config, ollamaUrl)
13
+ : await discoverLocalTools({ ollamaUrl });
14
+ const resolvedConfig = config ?? createConfigFromDiscovery(discovery);
15
+ const messages = createTranslator(resolveLanguage({
16
+ explicitLanguage: optionalString(flags.language),
17
+ configLanguage: resolvedConfig.language
18
+ }));
19
+ const presets = listPresetsWithAvailability(resolvedConfig, discovery, messages);
20
+ if (flags.json) {
21
+ process.stdout.write(JSON.stringify({ v: 1, presets }) + "\n");
22
+ return;
23
+ }
24
+ console.log(messages.presets.title);
25
+ console.log("");
26
+ for (const preset of presets) {
27
+ const status = preset.available
28
+ ? messages.presets.available
29
+ : messages.presets.unavailable(preset.unavailableReasons.join("; "));
30
+ console.log(` ${preset.name.padEnd(20)} ${preset.agentA} <-> ${preset.agentB} ${status}`);
31
+ }
32
+ console.log("");
33
+ console.log(messages.presets.total(presets.length));
34
+ }
@@ -0,0 +1,4 @@
1
+ /** Extrait une chaîne non vide depuis une valeur de flag. */
2
+ export function optionalString(value) {
3
+ return typeof value === "string" && value.trim() ? value : undefined;
4
+ }
@@ -0,0 +1,21 @@
1
+ import { configExists, loadConfig, resolveDefaultConfigPath } from "../config.js";
2
+ import { createTranslator, resolveLanguage } from "../i18n.js";
3
+ import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "../update.js";
4
+ import { getPackageVersion } from "../version.js";
5
+ import { optionalString } from "./shared.js";
6
+ /** Exécute `palabre update`, avec application explicite pour un checkout source. */
7
+ export async function runUpdateCommand(flags) {
8
+ const info = await getUpdateInfo(await getPackageVersion());
9
+ const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
10
+ const config = await configExists(configPath) ? await loadConfig(configPath) : undefined;
11
+ const messages = createTranslator(resolveLanguage({
12
+ explicitLanguage: optionalString(flags.language),
13
+ configLanguage: config?.language
14
+ }));
15
+ if (flags.apply) {
16
+ await applySourceUpdate(info, messages);
17
+ console.log(messages.update.upToDate);
18
+ return;
19
+ }
20
+ console.log(formatUpdateInstructions(info, messages));
21
+ }
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 { configuredOllamaTargets, 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) {
@@ -111,3 +134,10 @@ async function isAccessible(filePath) {
111
134
  return false;
112
135
  }
113
136
  }
137
+ /** Détecte les outils en tenant compte de tous les serveurs Ollama configurés. */
138
+ export function discoverLocalToolsForConfig(config, ollamaUrl) {
139
+ return discoverLocalTools({
140
+ ollamaUrl,
141
+ ollamaTargets: configuredOllamaTargets(config)
142
+ });
143
+ }
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)));