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 +2 -0
- package/dist/adapters/index.js +2 -2
- package/dist/adapters/ollama.js +8 -6
- package/dist/agentRegistry.js +6 -0
- package/dist/args.js +1 -0
- package/dist/commands/agents.js +85 -0
- package/dist/commands/context.js +25 -0
- package/dist/commands/history.js +28 -0
- package/dist/commands/init.js +59 -0
- package/dist/commands/presets.js +34 -0
- package/dist/commands/shared.js +4 -0
- package/dist/commands/update.js +21 -0
- package/dist/config.js +8 -0
- package/dist/discovery.js +36 -6
- package/dist/doctor.js +19 -4
- package/dist/index.js +66 -732
- package/dist/limits.js +1 -0
- package/dist/messages/common.js +6 -0
- package/dist/messages/doctor.js +2 -2
- package/dist/messages/help.js +6 -0
- package/dist/messages/tui.js +14 -0
- package/dist/ollamaUrl.js +76 -0
- package/dist/orchestrator.js +29 -24
- package/dist/output.js +1 -10
- package/dist/presets.js +25 -4
- package/dist/renderers/console.js +1 -7
- package/dist/renderers/ndjson.js +1 -10
- package/dist/renderers/tui.js +50 -17
- package/dist/runOptions.js +66 -0
- package/dist/tuiController.js +400 -0
- package/dist/tuiState.js +1 -0
- package/package.json +3 -2
package/dist/renderers/tui.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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) =>
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palabre",
|
|
3
|
-
"version": "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
|
},
|