palabre 0.9.0 → 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")) {
@@ -1,8 +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
+ import { MAX_ASK_AGENTS } from "./limits.js";
4
5
  import { OllamaUrlError } from "./ollamaUrl.js";
5
- export const MAX_ASK_AGENTS = 4;
6
+ export { MAX_ASK_AGENTS } from "./limits.js";
6
7
  /**
7
8
  * Point d'entrée de l'orchestration.
8
9
  * Lance le ping-pong entre `agentA` et `agentB` pendant `options.turns` tours,
@@ -106,7 +107,7 @@ export async function runDebate(config, options, renderer, messages = createTran
106
107
  try {
107
108
  const cancellation = cancellationFailureIfAborted(options, messages, {
108
109
  phase: "summary",
109
- agent: resolveSummaryAgentName(options),
110
+ agent: options.summaryAgent,
110
111
  role: summaryRole(),
111
112
  turn: transcript.length + 1
112
113
  });
@@ -124,7 +125,7 @@ export async function runDebate(config, options, renderer, messages = createTran
124
125
  catch (error) {
125
126
  failure = toDebateFailure(error, {
126
127
  phase: "summary",
127
- agent: resolveSummaryAgentName(options),
128
+ agent: options.summaryAgent,
128
129
  role: summaryRole(),
129
130
  turn: transcript.length + 1
130
131
  }, messages);
@@ -242,7 +243,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
242
243
  let failure;
243
244
  if (options.summaryEnabled) {
244
245
  try {
245
- const summaryAgentName = resolveSummaryAgentName(options);
246
+ const summaryAgentName = options.summaryAgent;
246
247
  const cancellation = cancellationFailureIfAborted(options, messages, {
247
248
  phase: "summary",
248
249
  agent: summaryAgentName,
@@ -262,7 +263,7 @@ export async function runAsk(config, options, renderer, messages = createTransla
262
263
  catch (error) {
263
264
  failure = toDebateFailure(error, {
264
265
  phase: "summary",
265
- agent: resolveSummaryAgentName(options),
266
+ agent: options.summaryAgent,
266
267
  role: summaryRole(),
267
268
  turn: transcript.length + 1
268
269
  }, messages);
@@ -330,7 +331,7 @@ function warnIfOllamaHasNoContext(options, agents, renderer, messages = createTr
330
331
  * @throws {Error} si l'agent de synthèse est absent de `config.agents`.
331
332
  */
332
333
  async function generateSummary(config, options, transcript, renderer, messages = createTranslator("fr")) {
333
- const summaryAgentName = resolveSummaryAgentName(options);
334
+ const summaryAgentName = options.summaryAgent;
334
335
  const summaryModel = options.summaryModel ?? modelForAgent(options, summaryAgentName);
335
336
  const summaryConfig = withRuntimeOverrides(config.agents[summaryAgentName], summaryModel, options.pullModels);
336
337
  if (!summaryConfig) {
@@ -385,15 +386,6 @@ function resolveAskAgentNames(options) {
385
386
  : [options.agentA, options.agentB];
386
387
  return agents.filter((agent, index) => Boolean(agent) && agents.indexOf(agent) === index);
387
388
  }
388
- function resolveSummaryAgentName(options) {
389
- if (options.summaryAgent) {
390
- return options.summaryAgent;
391
- }
392
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
393
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
394
- }
395
- return options.agentB;
396
- }
397
389
  function toDebateFailure(error, context, messages) {
398
390
  if (error instanceof AdapterError) {
399
391
  return {
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(", ");
@@ -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();
@@ -747,13 +747,7 @@ function formatSummary(options, messages) {
747
747
  if (!options.summaryEnabled) {
748
748
  return messages.renderers.disabled;
749
749
  }
750
- if (options.summaryAgent) {
751
- return options.summaryAgent;
752
- }
753
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
754
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
755
- }
756
- return options.agentB;
750
+ return options.summaryAgent;
757
751
  }
758
752
  function formatResponseCount(options) {
759
753
  return options.mode === "ask" ? options.askAgents?.length ?? 2 : options.turns;
@@ -1032,9 +1026,6 @@ function accent(value) {
1032
1026
  function violet(value) {
1033
1027
  return supportsColor ? `${codes.violet}${value}${codes.reset}` : value;
1034
1028
  }
1035
- function muted(value) {
1036
- return supportsColor ? `${codes.gray}${value}${codes.reset}` : value;
1037
- }
1038
1029
  function agentLabel(agent) {
1039
1030
  return supportsColor ? `${agentAnsi(agent)}${agent}${codes.reset}` : agent;
1040
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palabre",
3
- "version": "0.9.0",
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
  },