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.
@@ -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 };
@@ -702,13 +747,7 @@ function formatSummary(options, messages) {
702
747
  if (!options.summaryEnabled) {
703
748
  return messages.renderers.disabled;
704
749
  }
705
- if (options.summaryAgent) {
706
- return options.summaryAgent;
707
- }
708
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
709
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
710
- }
711
- return options.agentB;
750
+ return options.summaryAgent;
712
751
  }
713
752
  function formatResponseCount(options) {
714
753
  return options.mode === "ask" ? options.askAgents?.length ?? 2 : options.turns;
@@ -829,9 +868,9 @@ function activeAgentNamesForMode(config, mode) {
829
868
  const agents = defaults.askAgents && defaults.askAgents.length > 0
830
869
  ? defaults.askAgents
831
870
  : [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent));
832
- return agents.filter((agent) => Boolean(config.agents[agent]));
871
+ return agents.filter((agent) => Boolean(config.agents[agent]) && !isRetiredAgentName(agent));
833
872
  }
834
- return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
873
+ return [defaults.agentA, defaults.agentB].filter((agent) => typeof agent === "string" && Boolean(config.agents[agent]) && !isRetiredAgentName(agent));
835
874
  }
836
875
  function agentInventoryLine(config, messages) {
837
876
  const agents = Object.entries(config.agents)
@@ -849,15 +888,12 @@ function agentInventoryRows(config, messages) {
849
888
  }
850
889
  return entries.map(([name, agent]) => row(name, `${agent.type} ${dim("·")} ${agent.role}`));
851
890
  }
852
- function isRetiredAgentName(name) {
853
- return name === "gemini";
854
- }
855
891
  function exampleAgentsForMode(config, mode) {
856
892
  const activeAgents = activeAgentNamesForMode(config, mode);
857
893
  if (activeAgents.length > 0) {
858
894
  return activeAgents;
859
895
  }
860
- const available = Object.keys(config.agents).sort();
896
+ const available = Object.keys(config.agents).filter((agent) => !isRetiredAgentName(agent)).sort();
861
897
  return mode === "ask" ? available.slice(0, 3) : available.slice(0, 2);
862
898
  }
863
899
  function documentationUrl(config) {
@@ -990,9 +1026,6 @@ function accent(value) {
990
1026
  function violet(value) {
991
1027
  return supportsColor ? `${codes.violet}${value}${codes.reset}` : value;
992
1028
  }
993
- function muted(value) {
994
- return supportsColor ? `${codes.gray}${value}${codes.reset}` : value;
995
- }
996
1029
  function agentLabel(agent) {
997
1030
  return supportsColor ? `${agentAnsi(agent)}${agent}${codes.reset}` : agent;
998
1031
  }
@@ -0,0 +1,66 @@
1
+ import { getStringListFlag } from "./args.js";
2
+ import { DEFAULT_TURNS, MAX_ASK_AGENTS, parseTurnsFlag } from "./limits.js";
3
+ import { normalizeOllamaBaseUrl } from "./ollamaUrl.js";
4
+ import { createSessionContext } from "./session.js";
5
+ import { askAgentSeedsForMode } from "./tuiState.js";
6
+ import { optionalString } from "./commands/shared.js";
7
+ /** Resolves flags and defaults into complete orchestrator options. */
8
+ export function resolveRunOptions(input, messages) {
9
+ const { flags, config, language, topic, files, preset, signal } = input;
10
+ const mode = parseModeFlag(optionalString(flags.mode) ?? config.defaults?.mode, messages);
11
+ const explicitAskAgents = getStringListFlag(flags.agents);
12
+ const askAgentSeeds = askAgentSeedsForMode(mode, explicitAskAgents, config.defaults?.askAgents);
13
+ const agentA = resolveAgentName("agent A", flags["agent-a"], preset?.agentA, askAgentSeeds[0] ?? config.defaults?.agentA, messages);
14
+ const agentB = resolveAgentName("agent B", flags["agent-b"], preset?.agentB, askAgentSeeds[1] ?? askAgentSeeds[0] ?? config.defaults?.agentB, messages);
15
+ const askAgents = mode === "ask" ? resolveAskAgents(explicitAskAgents, config.defaults?.askAgents, [agentA, agentB], messages) : undefined;
16
+ const ollamaUrl = optionalString(flags["ollama-url"]);
17
+ return {
18
+ mode,
19
+ language,
20
+ topic,
21
+ agentA,
22
+ agentB,
23
+ askAgents,
24
+ turns: parseTurnsFlag(flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
25
+ session: createSessionContext(),
26
+ files,
27
+ modelA: optionalString(flags["model-a"]),
28
+ modelB: optionalString(flags["model-b"]),
29
+ ollamaUrl: ollamaUrl ? normalizeOllamaBaseUrl(ollamaUrl) : undefined,
30
+ pullModels: Boolean(flags["pull-models"]),
31
+ summaryAgent: resolveSummaryAgent(flags["summary-agent"], config.defaults, mode, askAgents, agentB),
32
+ summaryModel: optionalString(flags["summary-model"]),
33
+ summaryEnabled: !flags["no-summary"],
34
+ earlyStopOnAgreement: !flags["no-early-stop"],
35
+ plainOutput: Boolean(flags.plain || flags.terminal),
36
+ signal
37
+ };
38
+ }
39
+ function resolveAgentName(label, explicitValue, presetValue, defaultValue, messages) {
40
+ const resolved = optionalString(explicitValue) ?? presetValue ?? defaultValue;
41
+ if (!resolved)
42
+ throw new Error(messages.common.noAgentDefined(label));
43
+ return resolved;
44
+ }
45
+ function resolveSummaryAgent(explicitValue, defaults, mode, askAgents, agentB) {
46
+ const explicit = optionalString(explicitValue);
47
+ if (explicit)
48
+ return explicit;
49
+ if (mode === "ask")
50
+ return defaults?.askSummaryAgent ?? defaults?.summaryAgent ?? askAgents?.at(-1) ?? agentB;
51
+ return defaults?.summaryAgent ?? agentB;
52
+ }
53
+ function parseModeFlag(value, messages) {
54
+ if (!value)
55
+ return "debate";
56
+ if (value === "debate" || value === "ask")
57
+ return value;
58
+ throw new Error(messages.common.unknownMode(value, "debate, ask"));
59
+ }
60
+ function resolveAskAgents(explicitAgents, defaultAgents, fallbackAgents, messages) {
61
+ const selected = explicitAgents.length > 0 ? explicitAgents : defaultAgents && defaultAgents.length > 0 ? defaultAgents : fallbackAgents;
62
+ const unique = selected.filter((agent, index) => agent.trim() && selected.indexOf(agent) === index);
63
+ if (unique.length > MAX_ASK_AGENTS)
64
+ throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
65
+ return unique;
66
+ }
@@ -0,0 +1,400 @@
1
+ import { setOllamaBaseUrl, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeExampleConfig } from "./config.js";
2
+ import { discoverLocalToolsForConfig } from "./discovery.js";
3
+ import { AdapterError, formatAdapterError } from "./errors.js";
4
+ import { createTranslator, DEFAULT_LANGUAGE, parseLanguage } from "./i18n.js";
5
+ import { MAX_ASK_AGENTS, validateTurns } from "./limits.js";
6
+ import { DEFAULT_OLLAMA_BASE_URL, normalizeOllamaBaseUrl, OllamaUrlError, resolveOllamaBaseUrl } from "./ollamaUrl.js";
7
+ import { promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiRolesWizard, renderTuiConfig } from "./renderers/tui.js";
8
+ export async function runTuiConfigLoop(configPath, config, messages, initialMode) {
9
+ let mode = initialMode;
10
+ let notice;
11
+ let currentMessages = messages;
12
+ let changedRunDefaults = false;
13
+ for (;;) {
14
+ renderTuiConfig(config, configPath, mode, currentMessages, { message: notice });
15
+ notice = undefined;
16
+ const input = await promptTuiConfigCommand(mode, currentMessages);
17
+ if (input.kind === "quit") {
18
+ return { mode, quit: true, changedRunDefaults };
19
+ }
20
+ if (input.kind === "back") {
21
+ return { mode, quit: false, changedRunDefaults };
22
+ }
23
+ if (input.kind === "unknown") {
24
+ notice = input.message;
25
+ continue;
26
+ }
27
+ if (input.kind === "mode") {
28
+ mode = mode === "ask" ? "debate" : "ask";
29
+ config.defaults = { ...(config.defaults ?? {}), mode };
30
+ await writeExampleConfig(configPath, config);
31
+ changedRunDefaults = true;
32
+ notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
33
+ continue;
34
+ }
35
+ if (input.kind === "default-mode") {
36
+ config.defaults = { ...(config.defaults ?? {}), mode };
37
+ await writeExampleConfig(configPath, config);
38
+ changedRunDefaults = true;
39
+ notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
40
+ continue;
41
+ }
42
+ if (input.kind === "interface") {
43
+ config.defaults = { ...(config.defaults ?? {}), interface: input.interfaceName };
44
+ await writeExampleConfig(configPath, config);
45
+ notice = currentMessages.tui.interfaceDefault(input.interfaceName);
46
+ continue;
47
+ }
48
+ if (input.kind === "language") {
49
+ config.language = parseLanguage(input.language, "--language");
50
+ await writeExampleConfig(configPath, config);
51
+ currentMessages = createTranslator(config.language ?? DEFAULT_LANGUAGE);
52
+ notice = currentMessages.tui.languageUpdated(input.language);
53
+ continue;
54
+ }
55
+ if (input.kind === "agents") {
56
+ try {
57
+ const agentsInput = input.agents.length > 0
58
+ ? { kind: "agents", agents: input.agents }
59
+ : await promptTuiAgentsWizard(config, mode, currentMessages);
60
+ if (agentsInput.kind === "quit") {
61
+ return { mode, quit: true, changedRunDefaults };
62
+ }
63
+ if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
64
+ notice = currentMessages.tui.agentsUnchanged;
65
+ continue;
66
+ }
67
+ if (mode === "ask") {
68
+ const agents = normalizeTuiAskAgents(config, agentsInput.agents, currentMessages);
69
+ config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
70
+ await writeExampleConfig(configPath, config);
71
+ changedRunDefaults = true;
72
+ notice = currentMessages.tui.askAgentsUpdated(agents.join(", "));
73
+ }
74
+ else {
75
+ const [agentA, agentB] = normalizeTuiDebateAgents(config, agentsInput.agents, currentMessages);
76
+ config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
77
+ await writeExampleConfig(configPath, config);
78
+ changedRunDefaults = true;
79
+ notice = currentMessages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
80
+ }
81
+ }
82
+ catch (error) {
83
+ notice = error instanceof Error ? error.message : String(error);
84
+ }
85
+ continue;
86
+ }
87
+ if (input.kind === "roles") {
88
+ try {
89
+ const rolesInput = input.roles.length > 0
90
+ ? { kind: "roles", roles: input.roles }
91
+ : await promptTuiRolesWizard(config, mode, currentMessages);
92
+ if (rolesInput.kind === "quit") {
93
+ return { mode, quit: true, changedRunDefaults };
94
+ }
95
+ if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
96
+ notice = currentMessages.tui.rolesUnchanged;
97
+ continue;
98
+ }
99
+ notice = applyTuiRoles(config, mode, rolesInput.roles, currentMessages);
100
+ await writeExampleConfig(configPath, config);
101
+ }
102
+ catch (error) {
103
+ notice = error instanceof Error ? error.message : String(error);
104
+ }
105
+ continue;
106
+ }
107
+ if (input.kind === "turns") {
108
+ if (mode === "ask") {
109
+ notice = currentMessages.tui.askTurnsNotice;
110
+ continue;
111
+ }
112
+ try {
113
+ validateTurns(input.turns, "--turns", currentMessages);
114
+ config.defaults = { ...(config.defaults ?? {}), turns: input.turns };
115
+ await writeExampleConfig(configPath, config);
116
+ changedRunDefaults = true;
117
+ notice = currentMessages.tui.turnsUpdated(input.turns);
118
+ }
119
+ catch (error) {
120
+ notice = error instanceof Error ? error.message : String(error);
121
+ }
122
+ continue;
123
+ }
124
+ if (input.kind === "summary") {
125
+ try {
126
+ const nextDefaults = { ...(config.defaults ?? {}) };
127
+ if (input.agent !== undefined) {
128
+ assertKnownAgent(config, input.agent, mode === "ask" ? "defaults.askSummaryAgent" : "defaults.summaryAgent", currentMessages);
129
+ }
130
+ if (mode === "ask") {
131
+ if (input.agent === undefined) {
132
+ delete nextDefaults.askSummaryAgent;
133
+ notice = currentMessages.tui.askSummaryFallback;
134
+ }
135
+ else {
136
+ nextDefaults.askSummaryAgent = input.agent;
137
+ notice = currentMessages.tui.askSummaryAgent(input.agent);
138
+ }
139
+ }
140
+ else if (input.agent === undefined) {
141
+ delete nextDefaults.summaryAgent;
142
+ notice = currentMessages.tui.debateSummaryFallback;
143
+ }
144
+ else {
145
+ nextDefaults.summaryAgent = input.agent;
146
+ notice = currentMessages.tui.debateSummaryAgent(input.agent);
147
+ }
148
+ config.defaults = nextDefaults;
149
+ await writeExampleConfig(configPath, config);
150
+ changedRunDefaults = true;
151
+ }
152
+ catch (error) {
153
+ notice = error instanceof Error ? error.message : String(error);
154
+ }
155
+ continue;
156
+ }
157
+ if (input.kind === "ollama-info") {
158
+ try {
159
+ notice = await formatTuiOllamaInfo(config, currentMessages);
160
+ }
161
+ catch (error) {
162
+ notice = error instanceof Error ? error.message : String(error);
163
+ }
164
+ continue;
165
+ }
166
+ if (input.kind === "ollama-url") {
167
+ try {
168
+ notice = await setTuiOllamaUrl(configPath, config, input.url, currentMessages);
169
+ changedRunDefaults = true;
170
+ }
171
+ catch (error) {
172
+ notice = formatTuiRuntimeError(error, currentMessages);
173
+ }
174
+ continue;
175
+ }
176
+ if (input.kind === "ollama-model") {
177
+ try {
178
+ notice = await setTuiOllamaModel(configPath, config, input.model, currentMessages);
179
+ changedRunDefaults = true;
180
+ }
181
+ catch (error) {
182
+ notice = error instanceof Error ? error.message : String(error);
183
+ }
184
+ continue;
185
+ }
186
+ if (input.kind === "ollama-sync") {
187
+ try {
188
+ notice = await syncTuiOllamaModel(configPath, config, currentMessages);
189
+ changedRunDefaults = true;
190
+ }
191
+ catch (error) {
192
+ notice = error instanceof Error ? error.message : String(error);
193
+ }
194
+ continue;
195
+ }
196
+ }
197
+ }
198
+ async function setTuiOllamaUrl(configPath, config, value, messages) {
199
+ if (!Object.values(config.agents).some((agent) => agent.type === "ollama")) {
200
+ throw new Error(messages.config.ollamaModelNoAgent);
201
+ }
202
+ const normalized = isDefaultOllamaUrl(value)
203
+ ? DEFAULT_OLLAMA_BASE_URL
204
+ : normalizeOllamaBaseUrl(value);
205
+ const effective = resolveOllamaBaseUrl({ configUrl: normalized });
206
+ setOllamaBaseUrl(config, normalized);
207
+ await writeExampleConfig(configPath, config);
208
+ return messages.tui.ollamaUrlUpdated(normalized, effective);
209
+ }
210
+ function isDefaultOllamaUrl(value) {
211
+ return ["default", "defaut", "défaut", "local", "localhost"].includes(value.trim().toLowerCase());
212
+ }
213
+ async function formatTuiOllamaInfo(config, messages) {
214
+ const discovery = await discoverLocalToolsForConfig(config);
215
+ const agent = config.agents["ollama-local"];
216
+ if (agent?.type !== "ollama") {
217
+ throw new Error(messages.config.ollamaModelNoAgent);
218
+ }
219
+ if (!discovery.ollama.available) {
220
+ return messages.tui.ollamaUnavailable(discovery.ollama.baseUrl);
221
+ }
222
+ const installed = discovery.ollama.models.length > 0
223
+ ? discovery.ollama.models.join(", ")
224
+ : messages.config.ollamaModelNoInstalledModels;
225
+ const api = `${discovery.ollama.baseUrl}`;
226
+ return messages.tui.ollamaInfo(agent.model, installed, api);
227
+ }
228
+ async function setTuiOllamaModel(configPath, config, model, messages) {
229
+ const trimmed = model.trim();
230
+ if (!trimmed) {
231
+ throw new Error(messages.tui.ollamaModelUsage);
232
+ }
233
+ const discovery = await discoverLocalToolsForConfig(config);
234
+ const agent = config.agents["ollama-local"];
235
+ if (agent?.type !== "ollama") {
236
+ throw new Error(messages.config.ollamaModelNoAgent);
237
+ }
238
+ if (!discovery.ollama.models.includes(trimmed)) {
239
+ throw new Error(messages.config.ollamaModelUnavailable(trimmed));
240
+ }
241
+ const result = setOllamaModel(config, trimmed);
242
+ await writeExampleConfig(configPath, config);
243
+ return result
244
+ ? messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel)
245
+ : messages.config.ollamaModelNoChange(configPath, agent.model);
246
+ }
247
+ async function syncTuiOllamaModel(configPath, config, messages) {
248
+ const discovery = await discoverLocalToolsForConfig(config);
249
+ const agent = config.agents["ollama-local"];
250
+ if (agent?.type !== "ollama") {
251
+ throw new Error(messages.config.ollamaModelNoAgent);
252
+ }
253
+ if (discovery.ollama.models.length === 0) {
254
+ throw new Error(messages.config.ollamaModelNoInstalledModels);
255
+ }
256
+ const result = syncOllamaModel(config, discovery);
257
+ if (!result) {
258
+ return messages.config.ollamaModelNoChange(configPath, agent.model);
259
+ }
260
+ await writeExampleConfig(configPath, config);
261
+ return messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel);
262
+ }
263
+ export async function syncInteractiveDetectedAgents(configPath, config) {
264
+ const discovery = await discoverLocalToolsForConfig(config);
265
+ const result = syncDetectedAgentsDetailed(config, discovery);
266
+ if (result.changed) {
267
+ await writeExampleConfig(configPath, config);
268
+ }
269
+ return {
270
+ addedAgents: result.addedAgents
271
+ };
272
+ }
273
+ function normalizeTuiDebateAgents(config, agents, messages) {
274
+ const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
275
+ if (unique.length !== 2) {
276
+ throw new Error(messages.tui.debateAgentsUsage);
277
+ }
278
+ assertKnownAgent(config, unique[0], "defaults.agentA", messages);
279
+ assertKnownAgent(config, unique[1], "defaults.agentB", messages);
280
+ return [unique[0], unique[1]];
281
+ }
282
+ function normalizeTuiAskAgents(config, agents, messages) {
283
+ const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
284
+ if (unique.length === 0) {
285
+ throw new Error(messages.tui.askAgentsUsage);
286
+ }
287
+ if (unique.length > MAX_ASK_AGENTS) {
288
+ throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
289
+ }
290
+ unique.forEach((agent) => assertKnownAgent(config, agent, "defaults.askAgents", messages));
291
+ return unique;
292
+ }
293
+ export async function runTuiAgentsWizard(configPath, config, messages, mode, inlineAgents = []) {
294
+ try {
295
+ const agentsInput = inlineAgents.length > 0
296
+ ? { kind: "agents", agents: inlineAgents }
297
+ : await promptTuiAgentsWizard(config, mode, messages);
298
+ if (agentsInput.kind === "quit") {
299
+ return { quit: true, changedRunDefaults: false };
300
+ }
301
+ if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
302
+ return { quit: false, changedRunDefaults: false };
303
+ }
304
+ const notice = applyTuiAgents(config, mode, agentsInput.agents, messages);
305
+ await writeExampleConfig(configPath, config);
306
+ return { notice, quit: false, changedRunDefaults: true };
307
+ }
308
+ catch (error) {
309
+ return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false, changedRunDefaults: false };
310
+ }
311
+ }
312
+ export async function runTuiRolesWizard(configPath, config, messages, mode, inlineRoles = []) {
313
+ try {
314
+ const rolesInput = inlineRoles.length > 0
315
+ ? { kind: "roles", roles: inlineRoles }
316
+ : await promptTuiRolesWizard(config, mode, messages);
317
+ if (rolesInput.kind === "quit") {
318
+ return { quit: true };
319
+ }
320
+ if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
321
+ return { quit: false };
322
+ }
323
+ const notice = applyTuiRoles(config, mode, rolesInput.roles, messages);
324
+ await writeExampleConfig(configPath, config);
325
+ return { notice, quit: false };
326
+ }
327
+ catch (error) {
328
+ return { notice: messages.tui.rolesError(error instanceof Error ? error.message : String(error)), quit: false };
329
+ }
330
+ }
331
+ function applyTuiAgents(config, mode, agentNames, messages) {
332
+ if (mode === "ask") {
333
+ const agents = normalizeTuiAskAgents(config, agentNames, messages);
334
+ config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
335
+ return messages.tui.askAgentsUpdated(agents.join(", "));
336
+ }
337
+ const [agentA, agentB] = normalizeTuiDebateAgents(config, agentNames, messages);
338
+ config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
339
+ return messages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
340
+ }
341
+ function applyTuiRoles(config, mode, roleNames, messages) {
342
+ const agents = activeAgentsForMode(config, mode);
343
+ if (agents.length === 0) {
344
+ throw new Error(mode === "ask" ? messages.tui.noAskAgentsConfigured : messages.tui.noDebateAgentsConfigured);
345
+ }
346
+ const roles = normalizeTuiRoles(roleNames, agents, mode, messages);
347
+ agents.forEach((agent, index) => {
348
+ config.agents[agent].role = roles[index];
349
+ });
350
+ return mode === "ask"
351
+ ? messages.tui.askRolesUpdated(roles.join(", "))
352
+ : messages.tui.debateRolesUpdated(roles.join(" <-> "));
353
+ }
354
+ function activeAgentsForMode(config, mode) {
355
+ const defaults = config.defaults ?? {};
356
+ if (mode === "ask") {
357
+ if (defaults.askAgents && defaults.askAgents.length > 0) {
358
+ return defaults.askAgents.filter((agent) => Boolean(config.agents[agent]));
359
+ }
360
+ return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
361
+ }
362
+ return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
363
+ }
364
+ function normalizeTuiRoles(roleNames, agents, mode, messages) {
365
+ const roles = roleNames.map((role) => role.trim().toLowerCase()).filter(Boolean);
366
+ const expectedCount = agents.length;
367
+ if (roles.length < expectedCount) {
368
+ const agentLabel = mode === "ask"
369
+ ? agents.join(", ")
370
+ : agents.join(" <-> ");
371
+ throw new Error(messages.tui.rolesCountError(roles.length, expectedCount, agentLabel));
372
+ }
373
+ return roles.slice(0, expectedCount).map((role) => {
374
+ if (isAgentRole(role)) {
375
+ return role;
376
+ }
377
+ throw new Error(messages.tui.unknownRole(role, VALID_AGENT_ROLES.join(", ")));
378
+ });
379
+ }
380
+ function isAgentRole(value) {
381
+ return VALID_AGENT_ROLES.includes(value);
382
+ }
383
+ const VALID_AGENT_ROLES = ["implementer", "reviewer", "architect", "scout", "critic", "summarizer"];
384
+ function assertKnownAgent(config, agentName, fieldName, messages) {
385
+ if (!config.agents[agentName]) {
386
+ throw new Error(messages.common.unknownAgentForField(fieldName, agentName, Object.keys(config.agents).join(", ")));
387
+ }
388
+ }
389
+ function formatTuiRuntimeError(error, messages) {
390
+ if (error instanceof AdapterError)
391
+ return formatAdapterError(error, messages);
392
+ if (error instanceof OllamaUrlError) {
393
+ if (error.kind === "empty")
394
+ return messages.common.ollamaUrlEmpty;
395
+ if (error.kind === "protocol")
396
+ return messages.common.ollamaUrlProtocol(error.protocol ?? "");
397
+ return messages.common.ollamaUrlInvalid(error.value);
398
+ }
399
+ return error instanceof Error ? error.message : String(error);
400
+ }
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.1",
4
4
  "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,8 @@
38
38
  "check": "tsc -p tsconfig.json --noEmit",
39
39
  "prepack": "pnpm build",
40
40
  "start": "node ./dist/index.js",
41
- "test": "pnpm build:test && node --test .tmp/test-dist/tests/*.test.js",
41
+ "test": "pnpm build:test && node --test .tmp/test-dist/tests/*.test.js && pnpm test:release-social",
42
+ "test:release-social": "node --test scripts/post_release_bluesky.test.mjs",
42
43
  "build:test": "node -e \"fs.rmSync('.tmp/test-dist',{recursive:true,force:true})\" && tsc -p tsconfig.test.json",
43
44
  "smoke:real-presets": "node -e \"fs.rmSync('.tmp/smoke-real',{recursive:true,force:true})\" && tsc --target ES2022 --module NodeNext --moduleResolution NodeNext --types node --outDir .tmp/smoke-real scripts/smoke_real_presets.ts && node .tmp/smoke-real/smoke_real_presets.js"
44
45
  },