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/dist/limits.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createTranslator } from "./i18n.js";
2
2
  export const DEFAULT_TURNS = 4;
3
+ export const MAX_ASK_AGENTS = 4;
3
4
  export const MAX_TURNS = 20;
4
5
  /** Convertit `value` en nombre et valide la plage [1, `MAX_TURNS`]. Lève une erreur si invalide. */
5
6
  export function parseTurns(value, label = "--turns", messages = createTranslator("fr")) {
@@ -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,7 +1,9 @@
1
1
  import { createAgent } from "./adapters/index.js";
2
2
  import { AdapterError } from "./errors.js";
3
3
  import { createTranslator } from "./i18n.js";
4
- export const MAX_ASK_AGENTS = 4;
4
+ import { MAX_ASK_AGENTS } from "./limits.js";
5
+ import { OllamaUrlError } from "./ollamaUrl.js";
6
+ export { MAX_ASK_AGENTS } from "./limits.js";
5
7
  /**
6
8
  * Point d'entrée de l'orchestration.
7
9
  * Lance le ping-pong entre `agentA` et `agentB` pendant `options.turns` tours,
@@ -27,8 +29,8 @@ export async function runDebate(config, options, renderer, messages = createTran
27
29
  { name: options.agentB, role: agentBConfig.role, type: agentBConfig.type }
28
30
  ]);
29
31
  const agents = [
30
- createAgent(options.agentA, agentAConfig),
31
- createAgent(options.agentB, agentBConfig)
32
+ createAgent(options.agentA, agentAConfig, { ollamaUrl: options.ollamaUrl }),
33
+ createAgent(options.agentB, agentBConfig, { ollamaUrl: options.ollamaUrl })
32
34
  ];
33
35
  const transcript = [];
34
36
  let stopReason;
@@ -73,7 +75,7 @@ export async function runDebate(config, options, renderer, messages = createTran
73
75
  agent: current.name,
74
76
  role: current.role,
75
77
  turn
76
- });
78
+ }, messages);
77
79
  renderer?.error(failure);
78
80
  return {
79
81
  options,
@@ -105,7 +107,7 @@ export async function runDebate(config, options, renderer, messages = createTran
105
107
  try {
106
108
  const cancellation = cancellationFailureIfAborted(options, messages, {
107
109
  phase: "summary",
108
- agent: resolveSummaryAgentName(options),
110
+ agent: options.summaryAgent,
109
111
  role: summaryRole(),
110
112
  turn: transcript.length + 1
111
113
  });
@@ -123,10 +125,10 @@ export async function runDebate(config, options, renderer, messages = createTran
123
125
  catch (error) {
124
126
  failure = toDebateFailure(error, {
125
127
  phase: "summary",
126
- agent: resolveSummaryAgentName(options),
128
+ agent: options.summaryAgent,
127
129
  role: summaryRole(),
128
130
  turn: transcript.length + 1
129
- });
131
+ }, messages);
130
132
  renderer?.error(failure);
131
133
  }
132
134
  }
@@ -163,7 +165,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
163
165
  role: agentConfig.role,
164
166
  type: agentConfig.type
165
167
  })));
166
- const agents = agentEntries.map(([name, agentConfig]) => createAgent(name, agentConfig));
168
+ const agents = agentEntries.map(([name, agentConfig]) => createAgent(name, agentConfig, { ollamaUrl: options.ollamaUrl }));
167
169
  const transcript = [];
168
170
  for (let index = 0; index < agents.length; index += 1) {
169
171
  const current = agents[index];
@@ -212,7 +214,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
212
214
  agent: current.name,
213
215
  role: current.role,
214
216
  turn: response
215
- });
217
+ }, messages);
216
218
  renderer?.error(failure);
217
219
  return {
218
220
  options,
@@ -241,7 +243,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
241
243
  let failure;
242
244
  if (options.summaryEnabled) {
243
245
  try {
244
- const summaryAgentName = resolveSummaryAgentName(options);
246
+ const summaryAgentName = options.summaryAgent;
245
247
  const cancellation = cancellationFailureIfAborted(options, messages, {
246
248
  phase: "summary",
247
249
  agent: summaryAgentName,
@@ -261,10 +263,10 @@ export async function runAsk(config, options, renderer, messages = createTransla
261
263
  catch (error) {
262
264
  failure = toDebateFailure(error, {
263
265
  phase: "summary",
264
- agent: resolveSummaryAgentName(options),
266
+ agent: options.summaryAgent,
265
267
  role: summaryRole(),
266
268
  turn: transcript.length + 1
267
- });
269
+ }, messages);
268
270
  renderer?.error(failure);
269
271
  }
270
272
  }
@@ -329,13 +331,13 @@ function warnIfOllamaHasNoContext(options, agents, renderer, messages = createTr
329
331
  * @throws {Error} si l'agent de synthèse est absent de `config.agents`.
330
332
  */
331
333
  async function generateSummary(config, options, transcript, renderer, messages = createTranslator("fr")) {
332
- const summaryAgentName = resolveSummaryAgentName(options);
334
+ const summaryAgentName = options.summaryAgent;
333
335
  const summaryModel = options.summaryModel ?? modelForAgent(options, summaryAgentName);
334
336
  const summaryConfig = withRuntimeOverrides(config.agents[summaryAgentName], summaryModel, options.pullModels);
335
337
  if (!summaryConfig) {
336
338
  throw new Error(messages.orchestrator.unknownSummaryAgent(summaryAgentName));
337
339
  }
338
- const summaryAgent = createAgent(summaryAgentName, summaryConfig);
340
+ const summaryAgent = createAgent(summaryAgentName, summaryConfig, { ollamaUrl: options.ollamaUrl });
339
341
  const role = summaryRole();
340
342
  renderer?.summaryStart(summaryAgent.name, role);
341
343
  renderer?.thinkingStart(summaryAgent.name, role);
@@ -384,16 +386,7 @@ function resolveAskAgentNames(options) {
384
386
  : [options.agentA, options.agentB];
385
387
  return agents.filter((agent, index) => Boolean(agent) && agents.indexOf(agent) === index);
386
388
  }
387
- function resolveSummaryAgentName(options) {
388
- if (options.summaryAgent) {
389
- return options.summaryAgent;
390
- }
391
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
392
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
393
- }
394
- return options.agentB;
395
- }
396
- function toDebateFailure(error, context) {
389
+ function toDebateFailure(error, context, messages) {
397
390
  if (error instanceof AdapterError) {
398
391
  return {
399
392
  phase: context.phase,
@@ -405,6 +398,18 @@ function toDebateFailure(error, context) {
405
398
  details: error.details
406
399
  };
407
400
  }
401
+ if (error instanceof OllamaUrlError) {
402
+ const message = error.kind === "empty"
403
+ ? messages.common.ollamaUrlEmpty
404
+ : error.kind === "protocol"
405
+ ? messages.common.ollamaUrlProtocol(error.protocol ?? "")
406
+ : messages.common.ollamaUrlInvalid(error.value);
407
+ return {
408
+ ...context,
409
+ kind: "unknown",
410
+ message
411
+ };
412
+ }
408
413
  return {
409
414
  phase: context.phase,
410
415
  agent: context.agent,
package/dist/output.js CHANGED
@@ -94,7 +94,7 @@ function renderSessionHeader(options, debateMessages, stopReason, messages) {
94
94
  [messages.output.fields.mode, options.mode],
95
95
  [messages.output.fields.agents, formatAgentsForHeader(options)],
96
96
  [messages.output.fields.autoPullOllama, options.pullModels ? messages.output.yes : messages.output.no],
97
- [messages.output.fields.summary, options.summaryEnabled ? formatSummaryAgent(options) : messages.output.disabled],
97
+ [messages.output.fields.summary, options.summaryEnabled ? options.summaryAgent : messages.output.disabled],
98
98
  [
99
99
  options.mode === "ask" ? messages.output.fields.requestedResponses : messages.output.fields.requestedTurns,
100
100
  String(options.mode === "ask" ? options.askAgents?.length ?? debateMessages.length : options.turns)
@@ -124,15 +124,6 @@ function renderFileList(files, messages) {
124
124
  }
125
125
  return files.map((file) => `- \`${file.path}\` (${file.sizeBytes} ${messages.output.fileSizeUnit})`);
126
126
  }
127
- function formatSummaryAgent(options) {
128
- if (options.summaryAgent) {
129
- return options.summaryAgent;
130
- }
131
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
132
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
133
- }
134
- return options.agentB;
135
- }
136
127
  function formatAgentsForHeader(options) {
137
128
  if (options.mode === "ask") {
138
129
  return (options.askAgents && options.askAgents.length > 0 ? options.askAgents : [options.agentA, options.agentB]).join(", ");
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);
@@ -241,13 +241,7 @@ function formatSummary(options, messages) {
241
241
  if (!options.summaryEnabled) {
242
242
  return messages.renderers.disabled;
243
243
  }
244
- if (options.summaryAgent) {
245
- return options.summaryAgent;
246
- }
247
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
248
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
249
- }
250
- return options.agentB;
244
+ return options.summaryAgent;
251
245
  }
252
246
  function formatResponseCount(options) {
253
247
  return options.mode === "ask" ? options.askAgents?.length ?? 2 : options.turns;
@@ -31,7 +31,7 @@ export class NdjsonRenderer {
31
31
  agents: agents.map((a) => ({ name: a.name, role: a.role, type: a.type })),
32
32
  summaryEnabled: options.summaryEnabled,
33
33
  summaryAgent: options.summaryEnabled
34
- ? resolveSummaryAgent(options)
34
+ ? options.summaryAgent
35
35
  : null,
36
36
  earlyStop: options.earlyStopOnAgreement,
37
37
  filesCount: options.files.length,
@@ -134,15 +134,6 @@ export class NdjsonRenderer {
134
134
  process.stdout.write(JSON.stringify({ v: this.schemaVersion, ...event }) + "\n");
135
135
  }
136
136
  }
137
- function resolveSummaryAgent(options) {
138
- if (options.summaryAgent) {
139
- return options.summaryAgent;
140
- }
141
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
142
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
143
- }
144
- return options.agentB;
145
- }
146
137
  /** Factory pratique pour conserver la symétrie avec `createConsoleRenderer`. */
147
138
  export function createNdjsonRenderer() {
148
139
  return new NdjsonRenderer();