palabre 0.9.0 → 0.10.0
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/cli-pty.js +30 -10
- package/dist/adapters/cli-shared.js +73 -0
- package/dist/adapters/cli.js +40 -77
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/ollama.js +32 -32
- package/dist/adapters/terminal.js +1 -0
- package/dist/args.js +1 -0
- package/dist/commands/agents.js +91 -0
- package/dist/commands/context.js +31 -0
- package/dist/commands/history.js +33 -0
- package/dist/commands/init.js +64 -0
- package/dist/commands/presets.js +39 -0
- package/dist/commands/shared.js +8 -0
- package/dist/commands/update.js +26 -0
- package/dist/config.js +17 -1
- package/dist/configWizard.js +5 -4
- package/dist/context.js +1 -0
- package/dist/contextScan.js +4 -3
- package/dist/discovery.js +9 -1
- package/dist/doctor.js +1 -0
- package/dist/errors.js +4 -0
- package/dist/exec.js +1 -0
- package/dist/history.js +10 -0
- package/dist/i18n.js +2 -0
- package/dist/index.js +174 -879
- package/dist/limits.js +5 -0
- package/dist/messages/adapter-errors.js +26 -2
- package/dist/messages/config.js +6 -0
- package/dist/messages/index.js +1 -0
- package/dist/messages/renderers.js +10 -2
- package/dist/messages/tui.js +8 -2
- package/dist/new.js +1 -0
- package/dist/ollamaUrl.js +20 -0
- package/dist/orchestrator.js +106 -161
- package/dist/output.js +2 -10
- package/dist/presets.js +1 -0
- package/dist/prompt.js +1 -0
- package/dist/renderers/console.js +2 -8
- package/dist/renderers/ndjson.js +1 -10
- package/dist/renderers/tui-prompts.js +339 -0
- package/dist/renderers/tui-renderer.js +224 -0
- package/dist/renderers/tui-screens.js +352 -0
- package/dist/renderers/tui-theme.js +356 -0
- package/dist/renderers/tui.js +7 -1095
- package/dist/runOptions.js +97 -0
- package/dist/session.js +1 -0
- package/dist/tuiController.js +445 -0
- package/dist/tuiState.js +4 -0
- package/dist/types.js +1 -0
- package/dist/update.js +1 -0
- package/dist/version.js +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** @file Résolution centralisée des flags et defaults en options runtime immuables. */
|
|
2
|
+
import { getStringListFlag } from "./args.js";
|
|
3
|
+
import { DEFAULT_TURNS, MAX_ASK_AGENTS, parseTurnsFlag } from "./limits.js";
|
|
4
|
+
import { normalizeOllamaBaseUrl } from "./ollamaUrl.js";
|
|
5
|
+
import { createSessionContext } from "./session.js";
|
|
6
|
+
import { askAgentSeedsForMode } from "./tuiState.js";
|
|
7
|
+
import { optionalString } from "./commands/shared.js";
|
|
8
|
+
/**
|
|
9
|
+
* Construit le contrat complet transmis à l'orchestrateur.
|
|
10
|
+
*
|
|
11
|
+
* La priorité est : flags explicites, preset, defaults de configuration, puis
|
|
12
|
+
* fallbacks propres au mode. La fonction ne modifie ni les flags ni la config.
|
|
13
|
+
*
|
|
14
|
+
* @param input - Configuration, flags, contexte et signal de la session.
|
|
15
|
+
* @param messages - Dictionnaire localisé utilisé pour les erreurs de validation.
|
|
16
|
+
* @returns Des options complètes, avec agent de synthèse déjà résolu.
|
|
17
|
+
* @throws {Error} Si le mode, les agents, le nombre de tours ou l'URL Ollama sont invalides.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveRunOptions(input, messages) {
|
|
20
|
+
const { flags, config, language, topic, files, preset, signal } = input;
|
|
21
|
+
const mode = parseModeFlag(optionalString(flags.mode) ?? config.defaults?.mode, messages);
|
|
22
|
+
const explicitAskAgents = getStringListFlag(flags.agents);
|
|
23
|
+
const askAgentSeeds = askAgentSeedsForMode(mode, explicitAskAgents, config.defaults?.askAgents);
|
|
24
|
+
const agentA = resolveAgentName("agent A", flags["agent-a"], preset?.agentA, askAgentSeeds[0] ?? config.defaults?.agentA, messages);
|
|
25
|
+
const agentB = resolveAgentName("agent B", flags["agent-b"], preset?.agentB, askAgentSeeds[1] ?? askAgentSeeds[0] ?? config.defaults?.agentB, messages);
|
|
26
|
+
const askAgents = mode === "ask" ? resolveAskAgents(explicitAskAgents, config.defaults?.askAgents, [agentA, agentB], messages) : undefined;
|
|
27
|
+
const ollamaUrl = optionalString(flags["ollama-url"]);
|
|
28
|
+
return {
|
|
29
|
+
mode,
|
|
30
|
+
language,
|
|
31
|
+
topic,
|
|
32
|
+
agentA,
|
|
33
|
+
agentB,
|
|
34
|
+
askAgents,
|
|
35
|
+
turns: parseTurnsFlag(flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
|
|
36
|
+
session: createSessionContext(),
|
|
37
|
+
files,
|
|
38
|
+
modelA: optionalString(flags["model-a"]),
|
|
39
|
+
modelB: optionalString(flags["model-b"]),
|
|
40
|
+
ollamaUrl: ollamaUrl ? normalizeOllamaBaseUrl(ollamaUrl) : undefined,
|
|
41
|
+
pullModels: Boolean(flags["pull-models"]),
|
|
42
|
+
summaryAgent: resolveSummaryAgent(flags["summary-agent"], config.defaults, mode, askAgents, agentB),
|
|
43
|
+
summaryModel: optionalString(flags["summary-model"]),
|
|
44
|
+
summaryEnabled: !flags["no-summary"],
|
|
45
|
+
earlyStopOnAgreement: !flags["no-early-stop"],
|
|
46
|
+
plainOutput: Boolean(flags.plain || flags.terminal),
|
|
47
|
+
signal
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Résout un agent selon la priorité flag > preset > default configuré. */
|
|
51
|
+
function resolveAgentName(label, explicitValue, presetValue, defaultValue, messages) {
|
|
52
|
+
const resolved = optionalString(explicitValue) ?? presetValue ?? defaultValue;
|
|
53
|
+
if (!resolved)
|
|
54
|
+
throw new Error(messages.common.noAgentDefined(label));
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
/** Résout une seule fois l'agent de synthèse avant l'entrée dans l'orchestrateur. */
|
|
58
|
+
function resolveSummaryAgent(explicitValue, defaults, mode, askAgents, agentB) {
|
|
59
|
+
const explicit = optionalString(explicitValue);
|
|
60
|
+
if (explicit)
|
|
61
|
+
return explicit;
|
|
62
|
+
if (mode === "ask")
|
|
63
|
+
return defaults?.askSummaryAgent ?? defaults?.summaryAgent ?? askAgents?.at(-1) ?? agentB;
|
|
64
|
+
return defaults?.summaryAgent ?? agentB;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Valide le mode demandé (`--mode`, `config --mode`) et applique `debate` quand
|
|
68
|
+
* aucune valeur n'est fournie. Partagé entre `run` et la commande `config`.
|
|
69
|
+
* @throws {Error} Si `value` n'est ni `debate` ni `ask`.
|
|
70
|
+
*/
|
|
71
|
+
export function parseModeFlag(value, messages) {
|
|
72
|
+
if (!value)
|
|
73
|
+
return "debate";
|
|
74
|
+
if (value === "debate" || value === "ask")
|
|
75
|
+
return value;
|
|
76
|
+
throw new Error(messages.common.unknownMode(value, "debate, ask"));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Valide l'interface demandée (`--interface`, `config --interface`) et applique `tui`
|
|
80
|
+
* quand aucune valeur n'est fournie.
|
|
81
|
+
* @throws {Error} Si `value` n'est ni `tui` ni `terminal`.
|
|
82
|
+
*/
|
|
83
|
+
export function parseInterfaceFlag(value, messages) {
|
|
84
|
+
if (!value)
|
|
85
|
+
return "tui";
|
|
86
|
+
if (value === "tui" || value === "terminal")
|
|
87
|
+
return value;
|
|
88
|
+
throw new Error(messages.common.unknownMode(value, "tui, terminal"));
|
|
89
|
+
}
|
|
90
|
+
/** Déduplique les agents Ask et applique la limite produit sans modifier les listes sources. */
|
|
91
|
+
function resolveAskAgents(explicitAgents, defaultAgents, fallbackAgents, messages) {
|
|
92
|
+
const selected = explicitAgents.length > 0 ? explicitAgents : defaultAgents && defaultAgents.length > 0 ? defaultAgents : fallbackAgents;
|
|
93
|
+
const unique = selected.filter((agent, index) => agent.trim() && selected.indexOf(agent) === index);
|
|
94
|
+
if (unique.length > MAX_ASK_AGENTS)
|
|
95
|
+
throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
|
|
96
|
+
return unique;
|
|
97
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/** @file Contrôleur des interactions TUI qui lisent et persistent la configuration Palabre. */
|
|
2
|
+
import { setOllamaBaseUrl, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeConfig } from "./config.js";
|
|
3
|
+
import { discoverLocalToolsForConfig } from "./discovery.js";
|
|
4
|
+
import { AdapterError, formatAdapterError } from "./errors.js";
|
|
5
|
+
import { createTranslator, DEFAULT_LANGUAGE, parseLanguage } from "./i18n.js";
|
|
6
|
+
import { MAX_ASK_AGENTS, validateTurns } from "./limits.js";
|
|
7
|
+
import { DEFAULT_OLLAMA_BASE_URL, normalizeOllamaBaseUrl, OllamaUrlError, resolveOllamaBaseUrl } from "./ollamaUrl.js";
|
|
8
|
+
import { promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiRolesWizard, renderTuiConfig } from "./renderers/tui.js";
|
|
9
|
+
/**
|
|
10
|
+
* Exécute la boucle `/config` jusqu'au retour à l'accueil ou à la fermeture.
|
|
11
|
+
*
|
|
12
|
+
* @param configPath - Fichier de configuration à persister après chaque changement.
|
|
13
|
+
* @param config - Configuration chargée et mutée en mémoire.
|
|
14
|
+
* @param messages - Dictionnaire actif, remplacé si la langue change.
|
|
15
|
+
* @param initialMode - Mode affiché à l'ouverture de la vue.
|
|
16
|
+
* @returns Le mode final et les indicateurs de sortie ou de defaults modifiés.
|
|
17
|
+
*/
|
|
18
|
+
export async function runTuiConfigLoop(configPath, config, messages, initialMode) {
|
|
19
|
+
let mode = initialMode;
|
|
20
|
+
let notice;
|
|
21
|
+
let currentMessages = messages;
|
|
22
|
+
let changedRunDefaults = false;
|
|
23
|
+
for (;;) {
|
|
24
|
+
renderTuiConfig(config, configPath, mode, currentMessages, { message: notice });
|
|
25
|
+
notice = undefined;
|
|
26
|
+
const input = await promptTuiConfigCommand(mode, currentMessages);
|
|
27
|
+
if (input.kind === "quit") {
|
|
28
|
+
return { mode, quit: true, changedRunDefaults };
|
|
29
|
+
}
|
|
30
|
+
if (input.kind === "back") {
|
|
31
|
+
return { mode, quit: false, changedRunDefaults };
|
|
32
|
+
}
|
|
33
|
+
if (input.kind === "unknown") {
|
|
34
|
+
notice = input.message;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (input.kind === "mode") {
|
|
38
|
+
mode = mode === "ask" ? "debate" : "ask";
|
|
39
|
+
config.defaults = { ...(config.defaults ?? {}), mode };
|
|
40
|
+
await writeConfig(configPath, config);
|
|
41
|
+
changedRunDefaults = true;
|
|
42
|
+
notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (input.kind === "default-mode") {
|
|
46
|
+
config.defaults = { ...(config.defaults ?? {}), mode };
|
|
47
|
+
await writeConfig(configPath, config);
|
|
48
|
+
changedRunDefaults = true;
|
|
49
|
+
notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (input.kind === "interface") {
|
|
53
|
+
config.defaults = { ...(config.defaults ?? {}), interface: input.interfaceName };
|
|
54
|
+
await writeConfig(configPath, config);
|
|
55
|
+
notice = currentMessages.tui.interfaceDefault(input.interfaceName);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (input.kind === "language") {
|
|
59
|
+
config.language = parseLanguage(input.language, "--language");
|
|
60
|
+
await writeConfig(configPath, config);
|
|
61
|
+
currentMessages = createTranslator(config.language ?? DEFAULT_LANGUAGE);
|
|
62
|
+
notice = currentMessages.tui.languageUpdated(input.language);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (input.kind === "agents") {
|
|
66
|
+
try {
|
|
67
|
+
const agentsInput = input.agents.length > 0
|
|
68
|
+
? { kind: "agents", agents: input.agents }
|
|
69
|
+
: await promptTuiAgentsWizard(config, mode, currentMessages);
|
|
70
|
+
if (agentsInput.kind === "quit") {
|
|
71
|
+
return { mode, quit: true, changedRunDefaults };
|
|
72
|
+
}
|
|
73
|
+
if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
|
|
74
|
+
notice = currentMessages.tui.agentsUnchanged;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (mode === "ask") {
|
|
78
|
+
const agents = normalizeTuiAskAgents(config, agentsInput.agents, currentMessages);
|
|
79
|
+
config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
|
|
80
|
+
await writeConfig(configPath, config);
|
|
81
|
+
changedRunDefaults = true;
|
|
82
|
+
notice = currentMessages.tui.askAgentsUpdated(agents.join(", "));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const [agentA, agentB] = normalizeTuiDebateAgents(config, agentsInput.agents, currentMessages);
|
|
86
|
+
config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
|
|
87
|
+
await writeConfig(configPath, config);
|
|
88
|
+
changedRunDefaults = true;
|
|
89
|
+
notice = currentMessages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (input.kind === "roles") {
|
|
98
|
+
try {
|
|
99
|
+
const rolesInput = input.roles.length > 0
|
|
100
|
+
? { kind: "roles", roles: input.roles }
|
|
101
|
+
: await promptTuiRolesWizard(config, mode, currentMessages);
|
|
102
|
+
if (rolesInput.kind === "quit") {
|
|
103
|
+
return { mode, quit: true, changedRunDefaults };
|
|
104
|
+
}
|
|
105
|
+
if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
|
|
106
|
+
notice = currentMessages.tui.rolesUnchanged;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
notice = applyTuiRoles(config, mode, rolesInput.roles, currentMessages);
|
|
110
|
+
await writeConfig(configPath, config);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (input.kind === "turns") {
|
|
118
|
+
if (mode === "ask") {
|
|
119
|
+
notice = currentMessages.tui.askTurnsNotice;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
validateTurns(input.turns, "--turns", currentMessages);
|
|
124
|
+
config.defaults = { ...(config.defaults ?? {}), turns: input.turns };
|
|
125
|
+
await writeConfig(configPath, config);
|
|
126
|
+
changedRunDefaults = true;
|
|
127
|
+
notice = currentMessages.tui.turnsUpdated(input.turns);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (input.kind === "summary") {
|
|
135
|
+
try {
|
|
136
|
+
const nextDefaults = { ...(config.defaults ?? {}) };
|
|
137
|
+
if (input.agent !== undefined) {
|
|
138
|
+
assertKnownAgent(config, input.agent, mode === "ask" ? "defaults.askSummaryAgent" : "defaults.summaryAgent", currentMessages);
|
|
139
|
+
}
|
|
140
|
+
if (mode === "ask") {
|
|
141
|
+
if (input.agent === undefined) {
|
|
142
|
+
delete nextDefaults.askSummaryAgent;
|
|
143
|
+
notice = currentMessages.tui.askSummaryFallback;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
nextDefaults.askSummaryAgent = input.agent;
|
|
147
|
+
notice = currentMessages.tui.askSummaryAgent(input.agent);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (input.agent === undefined) {
|
|
151
|
+
delete nextDefaults.summaryAgent;
|
|
152
|
+
notice = currentMessages.tui.debateSummaryFallback;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
nextDefaults.summaryAgent = input.agent;
|
|
156
|
+
notice = currentMessages.tui.debateSummaryAgent(input.agent);
|
|
157
|
+
}
|
|
158
|
+
config.defaults = nextDefaults;
|
|
159
|
+
await writeConfig(configPath, config);
|
|
160
|
+
changedRunDefaults = true;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (input.kind === "ollama-info") {
|
|
168
|
+
try {
|
|
169
|
+
notice = await formatTuiOllamaInfo(config, currentMessages);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (input.kind === "ollama-url") {
|
|
177
|
+
try {
|
|
178
|
+
notice = await setTuiOllamaUrl(configPath, config, input.url, currentMessages);
|
|
179
|
+
changedRunDefaults = true;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
notice = formatTuiRuntimeError(error, currentMessages);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (input.kind === "ollama-model") {
|
|
187
|
+
try {
|
|
188
|
+
notice = await setTuiOllamaModel(configPath, config, input.model, currentMessages);
|
|
189
|
+
changedRunDefaults = true;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (input.kind === "ollama-sync") {
|
|
197
|
+
try {
|
|
198
|
+
notice = await syncTuiOllamaModel(configPath, config, currentMessages);
|
|
199
|
+
changedRunDefaults = true;
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Normalise et persiste l'URL de tous les agents Ollama configurés. */
|
|
209
|
+
async function setTuiOllamaUrl(configPath, config, value, messages) {
|
|
210
|
+
if (!Object.values(config.agents).some((agent) => agent.type === "ollama")) {
|
|
211
|
+
throw new Error(messages.config.ollamaModelNoAgent);
|
|
212
|
+
}
|
|
213
|
+
const normalized = isDefaultOllamaUrl(value)
|
|
214
|
+
? DEFAULT_OLLAMA_BASE_URL
|
|
215
|
+
: normalizeOllamaBaseUrl(value);
|
|
216
|
+
const effective = resolveOllamaBaseUrl({ configUrl: normalized });
|
|
217
|
+
setOllamaBaseUrl(config, normalized);
|
|
218
|
+
await writeConfig(configPath, config);
|
|
219
|
+
return messages.tui.ollamaUrlUpdated(normalized, effective);
|
|
220
|
+
}
|
|
221
|
+
function isDefaultOllamaUrl(value) {
|
|
222
|
+
return ["default", "defaut", "défaut", "local", "localhost"].includes(value.trim().toLowerCase());
|
|
223
|
+
}
|
|
224
|
+
/** Interroge le serveur Ollama effectif et formate son état pour une notice TUI. */
|
|
225
|
+
async function formatTuiOllamaInfo(config, messages) {
|
|
226
|
+
const discovery = await discoverLocalToolsForConfig(config);
|
|
227
|
+
const agent = config.agents["ollama-local"];
|
|
228
|
+
if (agent?.type !== "ollama") {
|
|
229
|
+
throw new Error(messages.config.ollamaModelNoAgent);
|
|
230
|
+
}
|
|
231
|
+
if (!discovery.ollama.available) {
|
|
232
|
+
return messages.tui.ollamaUnavailable(discovery.ollama.baseUrl);
|
|
233
|
+
}
|
|
234
|
+
const installed = discovery.ollama.models.length > 0
|
|
235
|
+
? discovery.ollama.models.join(", ")
|
|
236
|
+
: messages.config.ollamaModelNoInstalledModels;
|
|
237
|
+
const api = `${discovery.ollama.baseUrl}`;
|
|
238
|
+
return messages.tui.ollamaInfo(agent.model, installed, api);
|
|
239
|
+
}
|
|
240
|
+
/** Valide un modèle Ollama installé avant de le persister. */
|
|
241
|
+
async function setTuiOllamaModel(configPath, config, model, messages) {
|
|
242
|
+
const trimmed = model.trim();
|
|
243
|
+
if (!trimmed) {
|
|
244
|
+
throw new Error(messages.tui.ollamaModelUsage);
|
|
245
|
+
}
|
|
246
|
+
const discovery = await discoverLocalToolsForConfig(config);
|
|
247
|
+
const agent = config.agents["ollama-local"];
|
|
248
|
+
if (agent?.type !== "ollama") {
|
|
249
|
+
throw new Error(messages.config.ollamaModelNoAgent);
|
|
250
|
+
}
|
|
251
|
+
if (!discovery.ollama.models.includes(trimmed)) {
|
|
252
|
+
throw new Error(messages.config.ollamaModelUnavailable(trimmed));
|
|
253
|
+
}
|
|
254
|
+
const result = setOllamaModel(config, trimmed);
|
|
255
|
+
await writeConfig(configPath, config);
|
|
256
|
+
return result
|
|
257
|
+
? messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel)
|
|
258
|
+
: messages.config.ollamaModelNoChange(configPath, agent.model);
|
|
259
|
+
}
|
|
260
|
+
/** Remplace le modèle Ollama absent par le fallback installé choisi par la config. */
|
|
261
|
+
async function syncTuiOllamaModel(configPath, config, messages) {
|
|
262
|
+
const discovery = await discoverLocalToolsForConfig(config);
|
|
263
|
+
const agent = config.agents["ollama-local"];
|
|
264
|
+
if (agent?.type !== "ollama") {
|
|
265
|
+
throw new Error(messages.config.ollamaModelNoAgent);
|
|
266
|
+
}
|
|
267
|
+
if (discovery.ollama.models.length === 0) {
|
|
268
|
+
throw new Error(messages.config.ollamaModelNoInstalledModels);
|
|
269
|
+
}
|
|
270
|
+
const result = syncOllamaModel(config, discovery);
|
|
271
|
+
if (!result) {
|
|
272
|
+
return messages.config.ollamaModelNoChange(configPath, agent.model);
|
|
273
|
+
}
|
|
274
|
+
await writeConfig(configPath, config);
|
|
275
|
+
return messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Synchronise les agents connus détectés avant l'affichage de l'accueil TUI.
|
|
279
|
+
*
|
|
280
|
+
* @param configPath - Fichier à réécrire uniquement si la synchronisation change la config.
|
|
281
|
+
* @param config - Configuration chargée et mise à jour en mémoire.
|
|
282
|
+
* @returns Les noms des agents nouvellement ajoutés, utilisés pour la notice d'accueil.
|
|
283
|
+
*/
|
|
284
|
+
export async function syncInteractiveDetectedAgents(configPath, config) {
|
|
285
|
+
const discovery = await discoverLocalToolsForConfig(config);
|
|
286
|
+
const result = syncDetectedAgentsDetailed(config, discovery);
|
|
287
|
+
if (result.changed) {
|
|
288
|
+
await writeConfig(configPath, config);
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
addedAgents: result.addedAgents
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function normalizeTuiDebateAgents(config, agents, messages) {
|
|
295
|
+
const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
|
|
296
|
+
if (unique.length !== 2) {
|
|
297
|
+
throw new Error(messages.tui.debateAgentsUsage);
|
|
298
|
+
}
|
|
299
|
+
assertKnownAgent(config, unique[0], "defaults.agentA", messages);
|
|
300
|
+
assertKnownAgent(config, unique[1], "defaults.agentB", messages);
|
|
301
|
+
return [unique[0], unique[1]];
|
|
302
|
+
}
|
|
303
|
+
function normalizeTuiAskAgents(config, agents, messages) {
|
|
304
|
+
const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
|
|
305
|
+
if (unique.length === 0) {
|
|
306
|
+
throw new Error(messages.tui.askAgentsUsage);
|
|
307
|
+
}
|
|
308
|
+
if (unique.length > MAX_ASK_AGENTS) {
|
|
309
|
+
throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
|
|
310
|
+
}
|
|
311
|
+
unique.forEach((agent) => assertKnownAgent(config, agent, "defaults.askAgents", messages));
|
|
312
|
+
return unique;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Exécute le wizard d'agents ou applique directement les noms fournis.
|
|
316
|
+
*
|
|
317
|
+
* @param configPath - Fichier de configuration à persister.
|
|
318
|
+
* @param config - Configuration chargée et modifiée en mémoire.
|
|
319
|
+
* @param messages - Dictionnaire localisé de la vue.
|
|
320
|
+
* @param mode - Mode dont les agents actifs doivent être modifiés.
|
|
321
|
+
* @param inlineAgents - Noms fournis directement après la commande `/agents`.
|
|
322
|
+
* @returns Une notice utilisateur et les indicateurs de sortie/changement.
|
|
323
|
+
*/
|
|
324
|
+
export async function runTuiAgentsWizard(configPath, config, messages, mode, inlineAgents = []) {
|
|
325
|
+
try {
|
|
326
|
+
const agentsInput = inlineAgents.length > 0
|
|
327
|
+
? { kind: "agents", agents: inlineAgents }
|
|
328
|
+
: await promptTuiAgentsWizard(config, mode, messages);
|
|
329
|
+
if (agentsInput.kind === "quit") {
|
|
330
|
+
return { quit: true, changedRunDefaults: false };
|
|
331
|
+
}
|
|
332
|
+
if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
|
|
333
|
+
return { quit: false, changedRunDefaults: false };
|
|
334
|
+
}
|
|
335
|
+
const notice = applyTuiAgents(config, mode, agentsInput.agents, messages);
|
|
336
|
+
await writeConfig(configPath, config);
|
|
337
|
+
return { notice, quit: false, changedRunDefaults: true };
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false, changedRunDefaults: false };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Exécute le wizard de rôles ou applique directement les rôles fournis.
|
|
345
|
+
*
|
|
346
|
+
* @param configPath - Fichier de configuration à persister.
|
|
347
|
+
* @param config - Configuration chargée et modifiée en mémoire.
|
|
348
|
+
* @param messages - Dictionnaire localisé de la vue.
|
|
349
|
+
* @param mode - Mode déterminant la liste d'agents concernés.
|
|
350
|
+
* @param inlineRoles - Rôles fournis directement après la commande `/roles`.
|
|
351
|
+
* @returns Une notice utilisateur et l'indicateur de fermeture de la TUI.
|
|
352
|
+
*/
|
|
353
|
+
export async function runTuiRolesWizard(configPath, config, messages, mode, inlineRoles = []) {
|
|
354
|
+
try {
|
|
355
|
+
const rolesInput = inlineRoles.length > 0
|
|
356
|
+
? { kind: "roles", roles: inlineRoles }
|
|
357
|
+
: await promptTuiRolesWizard(config, mode, messages);
|
|
358
|
+
if (rolesInput.kind === "quit") {
|
|
359
|
+
return { quit: true };
|
|
360
|
+
}
|
|
361
|
+
if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
|
|
362
|
+
return { quit: false };
|
|
363
|
+
}
|
|
364
|
+
const notice = applyTuiRoles(config, mode, rolesInput.roles, messages);
|
|
365
|
+
await writeConfig(configPath, config);
|
|
366
|
+
return { notice, quit: false };
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
return { notice: messages.tui.rolesError(error instanceof Error ? error.message : String(error)), quit: false };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** Valide puis applique les agents actifs du mode courant. */
|
|
373
|
+
function applyTuiAgents(config, mode, agentNames, messages) {
|
|
374
|
+
if (mode === "ask") {
|
|
375
|
+
const agents = normalizeTuiAskAgents(config, agentNames, messages);
|
|
376
|
+
config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
|
|
377
|
+
return messages.tui.askAgentsUpdated(agents.join(", "));
|
|
378
|
+
}
|
|
379
|
+
const [agentA, agentB] = normalizeTuiDebateAgents(config, agentNames, messages);
|
|
380
|
+
config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
|
|
381
|
+
return messages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
|
|
382
|
+
}
|
|
383
|
+
/** Associe les rôles saisis aux agents actifs du mode courant. */
|
|
384
|
+
function applyTuiRoles(config, mode, roleNames, messages) {
|
|
385
|
+
const agents = activeAgentsForMode(config, mode);
|
|
386
|
+
if (agents.length === 0) {
|
|
387
|
+
throw new Error(mode === "ask" ? messages.tui.noAskAgentsConfigured : messages.tui.noDebateAgentsConfigured);
|
|
388
|
+
}
|
|
389
|
+
const roles = normalizeTuiRoles(roleNames, agents, mode, messages);
|
|
390
|
+
agents.forEach((agent, index) => {
|
|
391
|
+
config.agents[agent].role = roles[index];
|
|
392
|
+
});
|
|
393
|
+
return mode === "ask"
|
|
394
|
+
? messages.tui.askRolesUpdated(roles.join(", "))
|
|
395
|
+
: messages.tui.debateRolesUpdated(roles.join(" <-> "));
|
|
396
|
+
}
|
|
397
|
+
function activeAgentsForMode(config, mode) {
|
|
398
|
+
const defaults = config.defaults ?? {};
|
|
399
|
+
if (mode === "ask") {
|
|
400
|
+
if (defaults.askAgents && defaults.askAgents.length > 0) {
|
|
401
|
+
return defaults.askAgents.filter((agent) => Boolean(config.agents[agent]));
|
|
402
|
+
}
|
|
403
|
+
return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
|
|
404
|
+
}
|
|
405
|
+
return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
|
|
406
|
+
}
|
|
407
|
+
/** Valide les rôles connus et conserve exactement le nombre attendu. */
|
|
408
|
+
function normalizeTuiRoles(roleNames, agents, mode, messages) {
|
|
409
|
+
const roles = roleNames.map((role) => role.trim().toLowerCase()).filter(Boolean);
|
|
410
|
+
const expectedCount = agents.length;
|
|
411
|
+
if (roles.length < expectedCount) {
|
|
412
|
+
const agentLabel = mode === "ask"
|
|
413
|
+
? agents.join(", ")
|
|
414
|
+
: agents.join(" <-> ");
|
|
415
|
+
throw new Error(messages.tui.rolesCountError(roles.length, expectedCount, agentLabel));
|
|
416
|
+
}
|
|
417
|
+
return roles.slice(0, expectedCount).map((role) => {
|
|
418
|
+
if (isAgentRole(role)) {
|
|
419
|
+
return role;
|
|
420
|
+
}
|
|
421
|
+
throw new Error(messages.tui.unknownRole(role, VALID_AGENT_ROLES.join(", ")));
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function isAgentRole(value) {
|
|
425
|
+
return VALID_AGENT_ROLES.includes(value);
|
|
426
|
+
}
|
|
427
|
+
const VALID_AGENT_ROLES = ["implementer", "reviewer", "architect", "scout", "critic", "summarizer"];
|
|
428
|
+
function assertKnownAgent(config, agentName, fieldName, messages) {
|
|
429
|
+
if (!config.agents[agentName]) {
|
|
430
|
+
throw new Error(messages.common.unknownAgentForField(fieldName, agentName, Object.keys(config.agents).join(", ")));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** Convertit les erreurs adapter et URL stables en message localisé pour la TUI. */
|
|
434
|
+
function formatTuiRuntimeError(error, messages) {
|
|
435
|
+
if (error instanceof AdapterError)
|
|
436
|
+
return formatAdapterError(error, messages);
|
|
437
|
+
if (error instanceof OllamaUrlError) {
|
|
438
|
+
if (error.kind === "empty")
|
|
439
|
+
return messages.common.ollamaUrlEmpty;
|
|
440
|
+
if (error.kind === "protocol")
|
|
441
|
+
return messages.common.ollamaUrlProtocol(error.protocol ?? "");
|
|
442
|
+
return messages.common.ollamaUrlInvalid(error.value);
|
|
443
|
+
}
|
|
444
|
+
return error instanceof Error ? error.message : String(error);
|
|
445
|
+
}
|
package/dist/tuiState.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @file État partagé entre le point d'entrée et la TUI pour réinitialiser les overrides de lancement entre deux sessions. */
|
|
2
|
+
/** Flags de session ponctuels (par sujet) qui ne doivent pas persister entre deux lancements TUI successifs. */
|
|
1
3
|
const TUI_RUN_OVERRIDE_FLAGS = [
|
|
2
4
|
"preset",
|
|
3
5
|
"agent-a",
|
|
@@ -19,11 +21,13 @@ const TUI_RUN_OVERRIDE_FLAGS = [
|
|
|
19
21
|
"terminal",
|
|
20
22
|
"json"
|
|
21
23
|
];
|
|
24
|
+
/** Supprime les overrides de lancement d'une session TUI terminée, pour repartir des defaults au prochain sujet. */
|
|
22
25
|
export function clearTuiRunOverrides(flags) {
|
|
23
26
|
for (const flag of TUI_RUN_OVERRIDE_FLAGS) {
|
|
24
27
|
delete flags[flag];
|
|
25
28
|
}
|
|
26
29
|
}
|
|
30
|
+
/** Pré-remplit les agents `ask` proposés par le wizard : explicites, sinon defaults de config, sinon aucun en mode `debate`. */
|
|
27
31
|
export function askAgentSeedsForMode(mode, explicitAskAgents, defaultAskAgents) {
|
|
28
32
|
if (mode !== "ask") {
|
|
29
33
|
return [];
|
package/dist/types.js
CHANGED
package/dist/update.js
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palabre",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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
|
},
|