palabre 0.2.0 → 0.5.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/dist/config.js +33 -7
- package/dist/configWizard.js +45 -40
- package/dist/context.js +16 -14
- package/dist/doctor.js +139 -135
- package/dist/errors.js +4 -31
- package/dist/i18n.js +30 -0
- package/dist/index.js +290 -245
- package/dist/limits.js +11 -10
- package/dist/messages/adapter-errors.js +36 -0
- package/dist/messages/agents.js +38 -0
- package/dist/messages/common.js +28 -0
- package/dist/messages/config.js +88 -0
- package/dist/messages/context.js +24 -0
- package/dist/messages/doctor.js +126 -0
- package/dist/messages/help.js +280 -0
- package/dist/messages/index.js +38 -0
- package/dist/messages/init.js +30 -0
- package/dist/messages/limits.js +12 -0
- package/dist/messages/new.js +66 -0
- package/dist/messages/orchestrator.js +14 -0
- package/dist/messages/output.js +64 -0
- package/dist/messages/presets.js +26 -0
- package/dist/messages/preview.js +22 -0
- package/dist/messages/prompt.js +102 -0
- package/dist/messages/renderers.js +38 -0
- package/dist/messages/update.js +40 -0
- package/dist/new.js +42 -42
- package/dist/orchestrator.js +23 -18
- package/dist/output.js +34 -33
- package/dist/presets.js +78 -2
- package/dist/prompt.js +43 -58
- package/dist/renderers/console.js +32 -26
- package/dist/update.js +10 -21
- package/package.json +1 -1
- package/palabre.config.example.json +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const rendererMessages = {
|
|
2
|
+
fr: {
|
|
3
|
+
subject: (topic) => `Sujet: ${topic}`,
|
|
4
|
+
agents: (pair) => `Agents: ${pair}`,
|
|
5
|
+
responsesSummaryContext: (turns, summary, context) => `Réponses: ${turns} | Synthèse: ${summary} | Contexte: ${context}`,
|
|
6
|
+
responsesSummary: (turns, summary) => `Réponses: ${turns} | Synthèse: ${summary}`,
|
|
7
|
+
context: (context) => `Contexte: ${context}`,
|
|
8
|
+
options: (earlyStop, pullModels) => `Options: arrêt anticipé ${earlyStop ? "activé" : "désactivé"}, auto-pull Ollama ${pullModels ? "activé" : "désactivé"}`,
|
|
9
|
+
enabled: "activé",
|
|
10
|
+
disabled: "désactivée",
|
|
11
|
+
warningPrefix: "Warning:",
|
|
12
|
+
infoPrefix: "Info:",
|
|
13
|
+
turn: (turn, totalTurns) => `tour ${turn}/${totalTurns}`,
|
|
14
|
+
thinking: (agent, role) => `${agent} (${role}) reflechit`,
|
|
15
|
+
summaryTitle: "Synthese",
|
|
16
|
+
exported: (path) => `Debat exporte: ${path}`,
|
|
17
|
+
noInjectedFiles: "aucun fichier injecté",
|
|
18
|
+
injectedFiles: (count) => `${count} fichier${count > 1 ? "s" : ""} injecté${count > 1 ? "s" : ""}`
|
|
19
|
+
},
|
|
20
|
+
en: {
|
|
21
|
+
subject: (topic) => `Subject: ${topic}`,
|
|
22
|
+
agents: (pair) => `Agents: ${pair}`,
|
|
23
|
+
responsesSummaryContext: (turns, summary, context) => `Responses: ${turns} | Summary: ${summary} | Context: ${context}`,
|
|
24
|
+
responsesSummary: (turns, summary) => `Responses: ${turns} | Summary: ${summary}`,
|
|
25
|
+
context: (context) => `Context: ${context}`,
|
|
26
|
+
options: (earlyStop, pullModels) => `Options: early stop ${earlyStop ? "enabled" : "disabled"}, Ollama auto-pull ${pullModels ? "enabled" : "disabled"}`,
|
|
27
|
+
enabled: "enabled",
|
|
28
|
+
disabled: "disabled",
|
|
29
|
+
warningPrefix: "Warning:",
|
|
30
|
+
infoPrefix: "Info:",
|
|
31
|
+
turn: (turn, totalTurns) => `turn ${turn}/${totalTurns}`,
|
|
32
|
+
thinking: (agent, role) => `${agent} (${role}) is thinking`,
|
|
33
|
+
summaryTitle: "Summary",
|
|
34
|
+
exported: (path) => `Debate exported: ${path}`,
|
|
35
|
+
noInjectedFiles: "no injected files",
|
|
36
|
+
injectedFiles: (count) => `${count} injected file${count > 1 ? "s" : ""}`
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const updateMessages = {
|
|
2
|
+
fr: {
|
|
3
|
+
upToDate: "PALABRE est a jour.",
|
|
4
|
+
automaticSourceOnly: "Mise a jour automatique disponible seulement depuis un checkout git. Utilise pnpm add --global palabre@latest.",
|
|
5
|
+
stepFailed: (command, args, exitCode) => `${command} ${args} a echoue avec le code ${exitCode}.`,
|
|
6
|
+
instructions: (options) => {
|
|
7
|
+
const lines = [
|
|
8
|
+
`PALABRE ${options.version}`,
|
|
9
|
+
"",
|
|
10
|
+
"Mise a jour recommandee:"
|
|
11
|
+
];
|
|
12
|
+
if (options.sourceCheckout) {
|
|
13
|
+
lines.push("", "Installation depuis le repo source detectee.", "", ` cd "${options.projectRoot}"`, " git pull --ff-only", " pnpm install", " pnpm build", " pnpm link --global", "", "Pour executer ces etapes automatiquement:", "", " palabre update --apply");
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
lines.push("", "Installation package detectee.", "", " pnpm add --global palabre@latest", "", "Si tu utilises npm:", "", " npm install --global palabre@latest");
|
|
17
|
+
}
|
|
18
|
+
return lines.join("\n");
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
en: {
|
|
22
|
+
upToDate: "PALABRE is up to date.",
|
|
23
|
+
automaticSourceOnly: "Automatic update is only available from a git checkout. Use pnpm add --global palabre@latest.",
|
|
24
|
+
stepFailed: (command, args, exitCode) => `${command} ${args} failed with exit code ${exitCode}.`,
|
|
25
|
+
instructions: (options) => {
|
|
26
|
+
const lines = [
|
|
27
|
+
`PALABRE ${options.version}`,
|
|
28
|
+
"",
|
|
29
|
+
"Recommended update:"
|
|
30
|
+
];
|
|
31
|
+
if (options.sourceCheckout) {
|
|
32
|
+
lines.push("", "Source repository installation detected.", "", ` cd "${options.projectRoot}"`, " git pull --ff-only", " pnpm install", " pnpm build", " pnpm link --global", "", "To run these steps automatically:", "", " palabre update --apply");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
lines.push("", "Package installation detected.", "", " pnpm add --global palabre@latest", "", "If you use npm:", "", " npm install --global palabre@latest");
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
package/dist/new.js
CHANGED
|
@@ -8,30 +8,30 @@ import { MAX_TURNS, turnsOrDefault, validateTurns } from "./limits.js";
|
|
|
8
8
|
* Détecte les outils locaux, liste les agents de la config et guide la composition du débat.
|
|
9
9
|
* Retourne `undefined` si l'utilisateur annule (q/quit/exit ou Ctrl+C).
|
|
10
10
|
*/
|
|
11
|
-
export async function runNewWizard(config) {
|
|
11
|
+
export async function runNewWizard(config, messages) {
|
|
12
12
|
const discovery = await discoverLocalTools();
|
|
13
|
-
const choices = buildAgentChoices(config, discovery);
|
|
13
|
+
const choices = buildAgentChoices(config, discovery, messages);
|
|
14
14
|
if (choices.length < 2) {
|
|
15
|
-
throw new Error(
|
|
15
|
+
throw new Error(messages.new.needsTwoAgents);
|
|
16
16
|
}
|
|
17
17
|
const rl = await createQuestioner();
|
|
18
18
|
try {
|
|
19
|
-
console.log(
|
|
20
|
-
console.log(
|
|
21
|
-
console.log(
|
|
19
|
+
console.log(messages.new.title);
|
|
20
|
+
console.log(messages.new.quitHint);
|
|
21
|
+
console.log(messages.new.defaultHint);
|
|
22
22
|
console.log("");
|
|
23
|
-
const agentA = await askAgent(rl, choices,
|
|
23
|
+
const agentA = await askAgent(rl, choices, messages.new.agentA, config.defaults?.agentA, messages);
|
|
24
24
|
if (!agentA)
|
|
25
25
|
return undefined;
|
|
26
|
-
const agentB = await askAgent(rl, choices.filter((choice) => choice.name !== agentA),
|
|
26
|
+
const agentB = await askAgent(rl, choices.filter((choice) => choice.name !== agentA), messages.new.agentB, config.defaults?.agentB === agentA ? undefined : config.defaults?.agentB, messages);
|
|
27
27
|
if (!agentB)
|
|
28
28
|
return undefined;
|
|
29
|
-
const topic = await askRequiredText(rl,
|
|
29
|
+
const topic = await askRequiredText(rl, messages.new.topic, messages);
|
|
30
30
|
if (!topic)
|
|
31
31
|
return undefined;
|
|
32
|
-
printCommandPreview({ agentA, agentB, topic, turns: turnsOrDefault(config.defaults?.turns) });
|
|
33
|
-
console.log(
|
|
34
|
-
const launchMinimal = await askYesNo(rl,
|
|
32
|
+
printCommandPreview({ agentA, agentB, topic, turns: turnsOrDefault(config.defaults?.turns) }, messages);
|
|
33
|
+
console.log(messages.new.advancedHint);
|
|
34
|
+
const launchMinimal = await askYesNo(rl, messages.new.launchMinimal, true, messages);
|
|
35
35
|
if (launchMinimal === undefined)
|
|
36
36
|
return undefined;
|
|
37
37
|
if (launchMinimal) {
|
|
@@ -45,34 +45,34 @@ export async function runNewWizard(config) {
|
|
|
45
45
|
plainOutput: false
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
const turns = await askNumber(rl,
|
|
48
|
+
const turns = await askNumber(rl, messages.new.turns, turnsOrDefault(config.defaults?.turns), messages);
|
|
49
49
|
if (turns === undefined)
|
|
50
50
|
return undefined;
|
|
51
|
-
const modelA = await askOptionalText(rl,
|
|
51
|
+
const modelA = await askOptionalText(rl, messages.new.modelFor(agentA));
|
|
52
52
|
if (modelA === undefined)
|
|
53
53
|
return undefined;
|
|
54
|
-
const modelB = await askOptionalText(rl,
|
|
54
|
+
const modelB = await askOptionalText(rl, messages.new.modelFor(agentB));
|
|
55
55
|
if (modelB === undefined)
|
|
56
56
|
return undefined;
|
|
57
|
-
const summaryEnabled = await askYesNo(rl,
|
|
57
|
+
const summaryEnabled = await askYesNo(rl, messages.new.summaryEnabled, true, messages);
|
|
58
58
|
if (summaryEnabled === undefined)
|
|
59
59
|
return undefined;
|
|
60
60
|
let summaryAgent;
|
|
61
61
|
let summaryModel;
|
|
62
62
|
if (summaryEnabled) {
|
|
63
|
-
summaryAgent = await askAgent(rl, choices,
|
|
63
|
+
summaryAgent = await askAgent(rl, choices, messages.new.summaryAgent, config.defaults?.summaryAgent ?? agentB, messages);
|
|
64
64
|
if (!summaryAgent)
|
|
65
65
|
return undefined;
|
|
66
|
-
summaryModel = await askOptionalText(rl,
|
|
66
|
+
summaryModel = await askOptionalText(rl, messages.new.summaryModelFor(summaryAgent));
|
|
67
67
|
if (summaryModel === undefined)
|
|
68
68
|
return undefined;
|
|
69
69
|
}
|
|
70
|
-
const context = splitPaths(await askOptionalText(rl,
|
|
71
|
-
const files = splitPaths(await askOptionalText(rl,
|
|
72
|
-
const showPrompt = await askYesNo(rl,
|
|
70
|
+
const context = splitPaths(await askOptionalText(rl, messages.new.contextPaths));
|
|
71
|
+
const files = splitPaths(await askOptionalText(rl, messages.new.filesPaths));
|
|
72
|
+
const showPrompt = await askYesNo(rl, messages.new.showPrompt, false, messages);
|
|
73
73
|
if (showPrompt === undefined)
|
|
74
74
|
return undefined;
|
|
75
|
-
const plainOutput = await askYesNo(rl,
|
|
75
|
+
const plainOutput = await askYesNo(rl, messages.new.plainOutput, false, messages);
|
|
76
76
|
if (plainOutput === undefined)
|
|
77
77
|
return undefined;
|
|
78
78
|
const selection = {
|
|
@@ -90,7 +90,7 @@ export async function runNewWizard(config) {
|
|
|
90
90
|
showPrompt,
|
|
91
91
|
plainOutput
|
|
92
92
|
};
|
|
93
|
-
printCommandPreview(selection);
|
|
93
|
+
printCommandPreview(selection, messages);
|
|
94
94
|
return selection;
|
|
95
95
|
}
|
|
96
96
|
finally {
|
|
@@ -123,7 +123,7 @@ async function readPipedLines() {
|
|
|
123
123
|
}
|
|
124
124
|
return raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
125
125
|
}
|
|
126
|
-
function buildAgentChoices(config, discovery) {
|
|
126
|
+
function buildAgentChoices(config, discovery, messages) {
|
|
127
127
|
return Object.entries(config.agents)
|
|
128
128
|
.map(([name, agentConfig]) => {
|
|
129
129
|
const detected = isAgentDetected(name, agentConfig, discovery);
|
|
@@ -131,7 +131,7 @@ function buildAgentChoices(config, discovery) {
|
|
|
131
131
|
name,
|
|
132
132
|
config: agentConfig,
|
|
133
133
|
detected,
|
|
134
|
-
status: agentStatus(name, agentConfig, discovery, detected)
|
|
134
|
+
status: agentStatus(name, agentConfig, discovery, detected, messages)
|
|
135
135
|
};
|
|
136
136
|
})
|
|
137
137
|
.sort((left, right) => Number(right.detected) - Number(left.detected) || left.name.localeCompare(right.name));
|
|
@@ -151,17 +151,17 @@ function isAgentDetected(name, config, discovery) {
|
|
|
151
151
|
return discovery.opencode.available;
|
|
152
152
|
return true;
|
|
153
153
|
}
|
|
154
|
-
function agentStatus(_name, config, discovery, detected) {
|
|
154
|
+
function agentStatus(_name, config, discovery, detected, messages) {
|
|
155
155
|
if (config.type === "ollama") {
|
|
156
156
|
return detected
|
|
157
|
-
?
|
|
158
|
-
:
|
|
157
|
+
? messages.new.detectedOllama(config.role, discovery.ollama.models.length)
|
|
158
|
+
: messages.new.ollamaUnreachable(config.role);
|
|
159
159
|
}
|
|
160
160
|
return detected
|
|
161
|
-
?
|
|
162
|
-
:
|
|
161
|
+
? messages.new.detectedCli(config.role)
|
|
162
|
+
: messages.new.missingCli(config.role);
|
|
163
163
|
}
|
|
164
|
-
async function askAgent(rl, choices, label, defaultName) {
|
|
164
|
+
async function askAgent(rl, choices, label, defaultName, messages) {
|
|
165
165
|
const fallback = choices.find((choice) => choice.name === defaultName)?.name ?? choices[0]?.name;
|
|
166
166
|
console.log(label);
|
|
167
167
|
choices.forEach((choice, index) => {
|
|
@@ -182,10 +182,10 @@ async function askAgent(rl, choices, label, defaultName) {
|
|
|
182
182
|
if (choices.some((choice) => choice.name === value)) {
|
|
183
183
|
return value;
|
|
184
184
|
}
|
|
185
|
-
console.log(
|
|
185
|
+
console.log(messages.new.invalidAgentChoice);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
-
async function askRequiredText(rl, label) {
|
|
188
|
+
async function askRequiredText(rl, label, messages) {
|
|
189
189
|
while (true) {
|
|
190
190
|
const answer = await rl.question(`${label}: `);
|
|
191
191
|
const value = answer.trim();
|
|
@@ -193,7 +193,7 @@ async function askRequiredText(rl, label) {
|
|
|
193
193
|
return undefined;
|
|
194
194
|
if (value)
|
|
195
195
|
return value;
|
|
196
|
-
console.log(
|
|
196
|
+
console.log(messages.new.requiredField);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
async function askOptionalText(rl, label) {
|
|
@@ -201,7 +201,7 @@ async function askOptionalText(rl, label) {
|
|
|
201
201
|
const value = answer.trim();
|
|
202
202
|
return isQuit(value) ? undefined : value;
|
|
203
203
|
}
|
|
204
|
-
async function askNumber(rl, label, defaultValue) {
|
|
204
|
+
async function askNumber(rl, label, defaultValue, messages) {
|
|
205
205
|
while (true) {
|
|
206
206
|
const answer = await rl.question(`${label} [${defaultValue}]: `);
|
|
207
207
|
const value = answer.trim();
|
|
@@ -212,18 +212,18 @@ async function askNumber(rl, label, defaultValue) {
|
|
|
212
212
|
const parsed = Number(value);
|
|
213
213
|
if (Number.isInteger(parsed)) {
|
|
214
214
|
try {
|
|
215
|
-
validateTurns(parsed,
|
|
215
|
+
validateTurns(parsed, messages.new.turnsValidationLabel, messages);
|
|
216
216
|
return parsed;
|
|
217
217
|
}
|
|
218
218
|
catch {
|
|
219
219
|
// Show the user-facing wizard hint below.
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
-
console.log(
|
|
222
|
+
console.log(messages.new.invalidTurns(MAX_TURNS));
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
|
-
async function askYesNo(rl, label, defaultValue) {
|
|
226
|
-
const suffix = defaultValue
|
|
225
|
+
async function askYesNo(rl, label, defaultValue, messages) {
|
|
226
|
+
const suffix = messages.new.yesNoSuffix(defaultValue);
|
|
227
227
|
while (true) {
|
|
228
228
|
const answer = await rl.question(`${label} [${suffix}]: `);
|
|
229
229
|
const value = answer.trim().toLowerCase();
|
|
@@ -235,7 +235,7 @@ async function askYesNo(rl, label, defaultValue) {
|
|
|
235
235
|
return true;
|
|
236
236
|
if (["n", "no", "non"].includes(value))
|
|
237
237
|
return false;
|
|
238
|
-
console.log(
|
|
238
|
+
console.log(messages.new.invalidYesNo);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
function splitPaths(value) {
|
|
@@ -254,11 +254,11 @@ function normalizeCommandName(command) {
|
|
|
254
254
|
function isQuit(value) {
|
|
255
255
|
return ["q", "quit", "exit"].includes(value.toLowerCase());
|
|
256
256
|
}
|
|
257
|
-
function printCommandPreview(selection) {
|
|
257
|
+
function printCommandPreview(selection, messages) {
|
|
258
258
|
const explicitCommand = buildExplicitCommand(selection);
|
|
259
259
|
const shortCommand = buildShortCommand(selection);
|
|
260
260
|
console.log("");
|
|
261
|
-
console.log(
|
|
261
|
+
console.log(messages.new.equivalentCommands);
|
|
262
262
|
console.log(` ${explicitCommand}`);
|
|
263
263
|
if (shortCommand) {
|
|
264
264
|
console.log(` ${shortCommand}`);
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createAgent } from "./adapters/index.js";
|
|
2
|
+
import { createTranslator } from "./i18n.js";
|
|
2
3
|
/**
|
|
3
4
|
* Point d'entrée de l'orchestration.
|
|
4
5
|
* Lance le ping-pong entre `agentA` et `agentB` pendant `options.turns` tours,
|
|
@@ -6,19 +7,19 @@ import { createAgent } from "./adapters/index.js";
|
|
|
6
7
|
*
|
|
7
8
|
* @throws {Error} si un agent référencé dans `options` est absent de `config.agents`.
|
|
8
9
|
*/
|
|
9
|
-
export async function runDebate(config, options, renderer) {
|
|
10
|
+
export async function runDebate(config, options, renderer, messages = createTranslator("fr")) {
|
|
10
11
|
const agentAConfig = withRuntimeOverrides(config.agents[options.agentA], options.modelA, options.pullModels);
|
|
11
12
|
const agentBConfig = withRuntimeOverrides(config.agents[options.agentB], options.modelB, options.pullModels);
|
|
12
13
|
if (!agentAConfig) {
|
|
13
|
-
throw new Error(
|
|
14
|
+
throw new Error(messages.common.unknownAgent(options.agentA));
|
|
14
15
|
}
|
|
15
16
|
if (!agentBConfig) {
|
|
16
|
-
throw new Error(
|
|
17
|
+
throw new Error(messages.common.unknownAgent(options.agentB));
|
|
17
18
|
}
|
|
18
19
|
warnIfOllamaHasNoContext(options, [
|
|
19
20
|
[options.agentA, agentAConfig],
|
|
20
21
|
[options.agentB, agentBConfig]
|
|
21
|
-
], renderer);
|
|
22
|
+
], renderer, messages);
|
|
22
23
|
renderer?.start(options, [
|
|
23
24
|
{ name: options.agentA, role: agentAConfig.role, type: agentAConfig.type },
|
|
24
25
|
{ name: options.agentB, role: agentBConfig.role, type: agentBConfig.type }
|
|
@@ -27,7 +28,7 @@ export async function runDebate(config, options, renderer) {
|
|
|
27
28
|
createAgent(options.agentA, agentAConfig),
|
|
28
29
|
createAgent(options.agentB, agentBConfig)
|
|
29
30
|
];
|
|
30
|
-
const
|
|
31
|
+
const transcript = [];
|
|
31
32
|
let stopReason;
|
|
32
33
|
for (let index = 0; index < options.turns; index += 1) {
|
|
33
34
|
const current = agents[index % agents.length];
|
|
@@ -38,12 +39,14 @@ export async function runDebate(config, options, renderer) {
|
|
|
38
39
|
const response = await current.generate({
|
|
39
40
|
topic: options.topic,
|
|
40
41
|
turn,
|
|
42
|
+
totalTurns: options.turns,
|
|
41
43
|
selfName: current.name,
|
|
42
44
|
peerName: peer.name,
|
|
43
45
|
selfRole: current.role,
|
|
46
|
+
language: options.language,
|
|
44
47
|
session: options.session,
|
|
45
48
|
files: options.files,
|
|
46
|
-
transcript
|
|
49
|
+
transcript
|
|
47
50
|
}).finally(() => renderer?.thinkingEnd());
|
|
48
51
|
const message = {
|
|
49
52
|
agent: current.name,
|
|
@@ -51,20 +54,20 @@ export async function runDebate(config, options, renderer) {
|
|
|
51
54
|
content: response.content,
|
|
52
55
|
createdAt: new Date().toISOString()
|
|
53
56
|
};
|
|
54
|
-
|
|
57
|
+
transcript.push(message);
|
|
55
58
|
renderer?.message(message.content);
|
|
56
|
-
if (shouldStopOnAgreement(options,
|
|
57
|
-
stopReason =
|
|
58
|
-
renderer?.notice(
|
|
59
|
+
if (shouldStopOnAgreement(options, transcript)) {
|
|
60
|
+
stopReason = messages.orchestrator.agreementStopReason;
|
|
61
|
+
renderer?.notice(messages.orchestrator.earlyStop(stopReason));
|
|
59
62
|
break;
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
const summary = options.summaryEnabled
|
|
63
|
-
? await generateSummary(config, options,
|
|
66
|
+
? await generateSummary(config, options, transcript, renderer, messages)
|
|
64
67
|
: undefined;
|
|
65
68
|
return {
|
|
66
69
|
options,
|
|
67
|
-
messages,
|
|
70
|
+
messages: transcript,
|
|
68
71
|
summary,
|
|
69
72
|
stopReason
|
|
70
73
|
};
|
|
@@ -111,7 +114,7 @@ function normalizeForAgreement(value) {
|
|
|
111
114
|
* Émet un avertissement si un agent Ollama participe sans contexte fichier.
|
|
112
115
|
* L'adapter Ollama ne lit pas le filesystem : sans `--files` ou `--context`, il ne voit pas le projet.
|
|
113
116
|
*/
|
|
114
|
-
function warnIfOllamaHasNoContext(options, agents, renderer) {
|
|
117
|
+
function warnIfOllamaHasNoContext(options, agents, renderer, messages = createTranslator("fr")) {
|
|
115
118
|
if (options.files.length > 0) {
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
@@ -122,33 +125,35 @@ function warnIfOllamaHasNoContext(options, agents, renderer) {
|
|
|
122
125
|
if (ollamaAgents.length === 0) {
|
|
123
126
|
return;
|
|
124
127
|
}
|
|
125
|
-
renderer?.warning(
|
|
128
|
+
renderer?.warning(messages.orchestrator.ollamaNoContext(ollamaAgents.join(", ")));
|
|
126
129
|
}
|
|
127
130
|
/**
|
|
128
131
|
* Phase de synthèse post-débat. Utilise `options.summaryAgent` quand il est défini, sinon `agentB`.
|
|
129
132
|
*
|
|
130
133
|
* @throws {Error} si l'agent de synthèse est absent de `config.agents`.
|
|
131
134
|
*/
|
|
132
|
-
async function generateSummary(config, options,
|
|
135
|
+
async function generateSummary(config, options, transcript, renderer, messages = createTranslator("fr")) {
|
|
133
136
|
const summaryAgentName = options.summaryAgent ?? options.agentB;
|
|
134
137
|
const summaryModel = options.summaryModel ?? modelForAgent(options, summaryAgentName);
|
|
135
138
|
const summaryConfig = withRuntimeOverrides(config.agents[summaryAgentName], summaryModel, options.pullModels);
|
|
136
139
|
if (!summaryConfig) {
|
|
137
|
-
throw new Error(
|
|
140
|
+
throw new Error(messages.orchestrator.unknownSummaryAgent(summaryAgentName));
|
|
138
141
|
}
|
|
139
142
|
const summaryAgent = createAgent(summaryAgentName, summaryConfig);
|
|
140
143
|
renderer?.summaryStart(summaryAgent.name, summaryAgent.role);
|
|
141
144
|
renderer?.thinkingStart(summaryAgent.name, summaryAgent.role);
|
|
142
145
|
const response = await summaryAgent.generate({
|
|
143
146
|
topic: options.topic,
|
|
144
|
-
turn:
|
|
147
|
+
turn: transcript.length + 1,
|
|
148
|
+
totalTurns: options.turns,
|
|
145
149
|
selfName: summaryAgent.name,
|
|
146
150
|
peerName: "transcript",
|
|
147
151
|
selfRole: summaryAgent.role,
|
|
148
152
|
mode: "summary",
|
|
153
|
+
language: options.language,
|
|
149
154
|
session: options.session,
|
|
150
155
|
files: options.files,
|
|
151
|
-
transcript
|
|
156
|
+
transcript
|
|
152
157
|
}).finally(() => renderer?.thinkingEnd());
|
|
153
158
|
const summary = {
|
|
154
159
|
agent: summaryAgent.name,
|
package/dist/output.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { createTranslator } from "./i18n.js";
|
|
3
4
|
/**
|
|
4
5
|
* Écrit le débat au format Markdown dans `outputDir`.
|
|
5
6
|
* Crée le répertoire si absent. Retourne le chemin absolu du fichier créé.
|
|
6
7
|
*/
|
|
7
|
-
export async function writeDebateMarkdown(outputDir, options,
|
|
8
|
+
export async function writeDebateMarkdown(outputDir, options, debateMessages, summary, stopReason, messages = createTranslator("fr")) {
|
|
8
9
|
const safeDate = new Date().toISOString().replace(/[:.]/g, "-");
|
|
9
10
|
const fileName = `palabre-${slugifyTopic(options.topic)}-${safeDate}.debate.md`;
|
|
10
11
|
const filePath = path.resolve(outputDir, fileName);
|
|
11
12
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
12
|
-
await writeFile(filePath, renderDebateMarkdown(options,
|
|
13
|
+
await writeFile(filePath, renderDebateMarkdown(options, debateMessages, summary, stopReason, messages), "utf8");
|
|
13
14
|
return filePath;
|
|
14
15
|
}
|
|
15
16
|
function slugifyTopic(topic) {
|
|
@@ -27,33 +28,33 @@ function slugifyTopic(topic) {
|
|
|
27
28
|
* Produit la représentation Markdown complète du débat.
|
|
28
29
|
* Fonction pure : aucun effet de bord sur le filesystem.
|
|
29
30
|
*/
|
|
30
|
-
export function renderDebateMarkdown(options,
|
|
31
|
+
export function renderDebateMarkdown(options, debateMessages, summary, stopReason, messages = createTranslator("fr")) {
|
|
31
32
|
const lines = [
|
|
32
|
-
|
|
33
|
+
messages.output.title,
|
|
33
34
|
"",
|
|
34
|
-
...renderSessionHeader(options,
|
|
35
|
+
...renderSessionHeader(options, debateMessages, stopReason, messages),
|
|
35
36
|
"",
|
|
36
|
-
|
|
37
|
+
messages.output.contextTitle,
|
|
37
38
|
"",
|
|
38
|
-
...renderFileList(options.files),
|
|
39
|
+
...renderFileList(options.files, messages),
|
|
39
40
|
"",
|
|
40
|
-
|
|
41
|
+
messages.output.exchangesTitle,
|
|
41
42
|
""
|
|
42
43
|
];
|
|
43
|
-
for (const message of
|
|
44
|
+
for (const message of debateMessages) {
|
|
44
45
|
lines.push(`### ${message.agent} (${message.role})`, "", normalizeMarkdownForWindowsPreview(message.content.trim()), "");
|
|
45
46
|
}
|
|
46
|
-
lines.push("---", "",
|
|
47
|
+
lines.push("---", "", messages.output.finalSummaryTitle, "", ...renderSummaryBlock(options, summary, messages));
|
|
47
48
|
return `${lines.join("\n")}\n`;
|
|
48
49
|
}
|
|
49
|
-
function renderSummaryBlock(options, summary) {
|
|
50
|
+
function renderSummaryBlock(options, summary, messages) {
|
|
50
51
|
if (summary) {
|
|
51
52
|
return [
|
|
52
|
-
|
|
53
|
+
`| ${messages.output.tableField} | ${messages.output.tableValue} |`,
|
|
53
54
|
"| --- | --- |",
|
|
54
|
-
`|
|
|
55
|
-
`|
|
|
56
|
-
`|
|
|
55
|
+
`| ${messages.output.fields.agent} | ${escapeTableCell(summary.agent)} |`,
|
|
56
|
+
`| ${messages.output.fields.role} | ${escapeTableCell(summary.role)} |`,
|
|
57
|
+
`| ${messages.output.fields.date} | ${escapeTableCell(summary.createdAt)} |`,
|
|
57
58
|
"",
|
|
58
59
|
normalizeMarkdownForWindowsPreview(summary.content.trim()),
|
|
59
60
|
""
|
|
@@ -61,30 +62,30 @@ function renderSummaryBlock(options, summary) {
|
|
|
61
62
|
}
|
|
62
63
|
return [
|
|
63
64
|
options.summaryEnabled
|
|
64
|
-
?
|
|
65
|
-
:
|
|
65
|
+
? messages.output.summaryMissing
|
|
66
|
+
: messages.output.summaryDisabled,
|
|
66
67
|
""
|
|
67
68
|
];
|
|
68
69
|
}
|
|
69
70
|
function normalizeMarkdownForWindowsPreview(content) {
|
|
70
71
|
return content.replace(/:\*\*/g, ":**");
|
|
71
72
|
}
|
|
72
|
-
function renderSessionHeader(options,
|
|
73
|
+
function renderSessionHeader(options, debateMessages, stopReason, messages) {
|
|
73
74
|
const rows = [
|
|
74
|
-
[
|
|
75
|
-
[
|
|
76
|
-
[
|
|
77
|
-
[
|
|
78
|
-
[
|
|
79
|
-
[
|
|
80
|
-
[
|
|
81
|
-
[
|
|
82
|
-
[
|
|
83
|
-
[
|
|
84
|
-
[
|
|
75
|
+
[messages.output.fields.subject, options.topic],
|
|
76
|
+
[messages.output.fields.agents, `${options.agentA} <-> ${options.agentB}`],
|
|
77
|
+
[messages.output.fields.autoPullOllama, options.pullModels ? messages.output.yes : messages.output.no],
|
|
78
|
+
[messages.output.fields.summary, options.summaryEnabled ? options.summaryAgent ?? options.agentB : messages.output.disabled],
|
|
79
|
+
[messages.output.fields.requestedTurns, String(options.turns)],
|
|
80
|
+
[messages.output.fields.playedTurns, String(debateMessages.length)],
|
|
81
|
+
[messages.output.fields.earlyStop, stopReason ?? messages.output.no],
|
|
82
|
+
[messages.output.fields.localDate, options.session.localDate],
|
|
83
|
+
[messages.output.fields.timeZone, options.session.timeZone],
|
|
84
|
+
[messages.output.fields.cwd, options.session.cwd],
|
|
85
|
+
[messages.output.fields.sessionStartedAt, options.session.startedAt]
|
|
85
86
|
];
|
|
86
87
|
return [
|
|
87
|
-
|
|
88
|
+
`| ${messages.output.tableField} | ${messages.output.tableValue} |`,
|
|
88
89
|
"| --- | --- |",
|
|
89
90
|
...rows.map(([label, value]) => `| ${escapeTableCell(label)} | ${escapeTableCell(value)} |`)
|
|
90
91
|
];
|
|
@@ -92,9 +93,9 @@ function renderSessionHeader(options, messages, stopReason) {
|
|
|
92
93
|
function escapeTableCell(value) {
|
|
93
94
|
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
94
95
|
}
|
|
95
|
-
function renderFileList(files) {
|
|
96
|
+
function renderFileList(files, messages) {
|
|
96
97
|
if (files.length === 0) {
|
|
97
|
-
return [
|
|
98
|
+
return [messages.output.noFileContext];
|
|
98
99
|
}
|
|
99
|
-
return files.map((file) => `- \`${file.path}\` (${file.sizeBytes}
|
|
100
|
+
return files.map((file) => `- \`${file.path}\` (${file.sizeBytes} ${messages.output.fileSizeUnit})`);
|
|
100
101
|
}
|
package/dist/presets.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
const presets = {
|
|
2
3
|
"codex-claude": {
|
|
3
4
|
agentA: "codex",
|
|
@@ -81,10 +82,11 @@ const presets = {
|
|
|
81
82
|
}
|
|
82
83
|
};
|
|
83
84
|
/** Retourne la paire d'agents pour `name`. Lève une erreur avec la liste des presets disponibles si inconnu. */
|
|
84
|
-
export function resolvePreset(name) {
|
|
85
|
+
export function resolvePreset(name, messages) {
|
|
85
86
|
const preset = presets[name];
|
|
86
87
|
if (!preset) {
|
|
87
|
-
|
|
88
|
+
const available = Object.keys(presets).join(", ");
|
|
89
|
+
throw new Error(messages?.presets.unknown(name, available) ?? `Preset inconnu: ${name}. Presets disponibles: ${available}`);
|
|
88
90
|
}
|
|
89
91
|
return preset;
|
|
90
92
|
}
|
|
@@ -92,7 +94,81 @@ export function resolvePreset(name) {
|
|
|
92
94
|
export function listPresetNames() {
|
|
93
95
|
return Object.keys(presets);
|
|
94
96
|
}
|
|
97
|
+
/** Retourne les presets complets, dans l'ordre de déclaration. */
|
|
98
|
+
export function listPresets() {
|
|
99
|
+
return Object.entries(presets).map(([name, preset]) => ({
|
|
100
|
+
name,
|
|
101
|
+
agentA: preset.agentA,
|
|
102
|
+
agentB: preset.agentB
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Retourne les presets enrichis par la disponibilité réelle des agents déclarés.
|
|
107
|
+
* Les intégrations peuvent filtrer `available === true` sans réimplémenter la découverte locale.
|
|
108
|
+
*/
|
|
109
|
+
export function listPresetsWithAvailability(config, discovery, messages) {
|
|
110
|
+
return listPresets().map((preset) => {
|
|
111
|
+
const checks = [
|
|
112
|
+
checkAgentAvailability(preset.agentA, config, discovery, messages),
|
|
113
|
+
checkAgentAvailability(preset.agentB, config, discovery, messages)
|
|
114
|
+
];
|
|
115
|
+
const unavailable = checks.filter((check) => !check.available);
|
|
116
|
+
return {
|
|
117
|
+
...preset,
|
|
118
|
+
available: unavailable.length === 0,
|
|
119
|
+
missingAgents: unavailable.map((check) => check.agent),
|
|
120
|
+
unavailableReasons: unavailable.map((check) => check.reason)
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
95
124
|
/** Recherche inverse : retourne le nom du preset correspondant à une paire `(agentA, agentB)`, ou `undefined`. */
|
|
96
125
|
export function findPresetNameForPair(agentA, agentB) {
|
|
97
126
|
return Object.entries(presets).find(([, preset]) => preset.agentA === agentA && preset.agentB === agentB)?.[0];
|
|
98
127
|
}
|
|
128
|
+
function checkAgentAvailability(agentName, config, discovery, messages) {
|
|
129
|
+
const agent = config.agents[agentName];
|
|
130
|
+
if (!agent) {
|
|
131
|
+
return unavailable(agentName, messages?.presets.missingAgent(agentName) ?? `agent absent de la config: ${agentName}`);
|
|
132
|
+
}
|
|
133
|
+
if (agent.type === "ollama") {
|
|
134
|
+
if (!discovery.ollama.available) {
|
|
135
|
+
return unavailable(agentName, discovery.ollama.commandAvailable
|
|
136
|
+
? messages?.presets.ollamaUnreachable(agentName) ?? `Ollama non joignable pour ${agentName}`
|
|
137
|
+
: messages?.presets.ollamaNotDetected(agentName) ?? `Ollama non détecté pour ${agentName}`);
|
|
138
|
+
}
|
|
139
|
+
if (!discovery.ollama.models.includes(agent.model)) {
|
|
140
|
+
return unavailable(agentName, messages?.presets.missingOllamaModel(agentName, agent.model) ?? `modèle Ollama absent pour ${agentName}: ${agent.model}`);
|
|
141
|
+
}
|
|
142
|
+
return available(agentName);
|
|
143
|
+
}
|
|
144
|
+
const detection = knownCliDetection(agent, discovery);
|
|
145
|
+
if (!detection) {
|
|
146
|
+
// Les CLIs custom déclarées par l'utilisateur restent considérées utilisables :
|
|
147
|
+
// Palabre ne peut pas connaître leur sémantique sans les lancer.
|
|
148
|
+
return available(agentName);
|
|
149
|
+
}
|
|
150
|
+
return detection.available
|
|
151
|
+
? available(agentName)
|
|
152
|
+
: unavailable(agentName, messages?.presets.missingCommand(agentName, detection.command) ?? `commande non détectée pour ${agentName}: ${detection.command}`);
|
|
153
|
+
}
|
|
154
|
+
function knownCliDetection(agent, discovery) {
|
|
155
|
+
const command = normalizeCommandName(agent.command);
|
|
156
|
+
if (command === "codex")
|
|
157
|
+
return discovery.codex;
|
|
158
|
+
if (command === "claude")
|
|
159
|
+
return discovery.claude;
|
|
160
|
+
if (command === "gemini")
|
|
161
|
+
return discovery.gemini;
|
|
162
|
+
if (command === "opencode")
|
|
163
|
+
return discovery.opencode;
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
function normalizeCommandName(command) {
|
|
167
|
+
return path.basename(command).toLowerCase().replace(/\.(exe|cmd|ps1|bat)$/i, "");
|
|
168
|
+
}
|
|
169
|
+
function available(agent) {
|
|
170
|
+
return { agent, available: true, reason: "" };
|
|
171
|
+
}
|
|
172
|
+
function unavailable(agent, reason) {
|
|
173
|
+
return { agent, available: false, reason };
|
|
174
|
+
}
|