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/dist/index.js CHANGED
@@ -1,28 +1,35 @@
1
1
  #!/usr/bin/env node
2
- import { assertRunnableConfig, configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeExampleConfig } from "./config.js";
2
+ import { assertRunnableConfig, configExists, createConfigFromDiscovery, loadConfig, resolveDefaultConfigPath, resolveOutputDir, setOllamaModel, syncDetectedAgentsDetailed, syncOllamaModel, writeExampleConfig } from "./config.js";
3
3
  import { loadProjectInputs } from "./context.js";
4
- import { buildContextScan } from "./contextScan.js";
5
- import { discoverLocalTools } from "./discovery.js";
4
+ import { discoverLocalTools, discoverLocalToolsForConfig } from "./discovery.js";
6
5
  import { runDoctor } from "./doctor.js";
7
6
  import { AdapterError, formatAdapterError } from "./errors.js";
8
7
  import { runConfigWizard } from "./configWizard.js";
9
8
  import { createTranslator, DEFAULT_LANGUAGE, parseLanguage, resolveLanguage } from "./i18n.js";
10
- import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault, validateTurns } from "./limits.js";
9
+ import { DEFAULT_TURNS, parseTurnsFlag, turnsOrDefault } from "./limits.js";
11
10
  import { formatAgentPrompt } from "./prompt.js";
12
11
  import { runNewWizard } from "./new.js";
13
- import { listPresetNames, listPresetsWithAvailability, resolvePreset } from "./presets.js";
12
+ import { listPresetNames, resolvePreset } from "./presets.js";
14
13
  import { listHistoryEntries } from "./history.js";
15
14
  import { createConsoleRenderer } from "./renderers/console.js";
16
15
  import { createNdjsonRenderer } from "./renderers/ndjson.js";
17
- import { createTuiRenderer, promptTuiAgentsWizard, promptTuiConfigCommand, promptTuiHomeTopic, promptTuiRolesWizard, renderTuiConfig, renderTuiHelp, renderTuiHistory, renderTuiHome } from "./renderers/tui.js";
16
+ import { createTuiRenderer, promptTuiHomeTopic, renderTuiHelp, renderTuiHistory, renderTuiHome, renderTuiUpdate } from "./renderers/tui.js";
18
17
  import { MAX_ASK_AGENTS, runAsk, runDebate } from "./orchestrator.js";
19
18
  import { writeDebateMarkdown } from "./output.js";
20
- import { applySourceUpdate, formatUpdateInstructions, getUpdateInfo } from "./update.js";
21
- import { createSessionContext } from "./session.js";
19
+ import { formatUpdateInstructions, getUpdateInfo } from "./update.js";
22
20
  import { getStringListFlag, parseArgs } from "./args.js";
23
- import { askAgentSeedsForMode, clearTuiRunOverrides } from "./tuiState.js";
24
- import { detectedAgentNames, detectionForCommand } from "./agentRegistry.js";
25
- import { getPackageVersion } from "./version.js";
21
+ import { clearTuiRunOverrides } from "./tuiState.js";
22
+ import { OllamaUrlError } from "./ollamaUrl.js";
23
+ import { compareSemver, getLatestPackageVersion, getPackageVersion } from "./version.js";
24
+ import { runAgentsCommand } from "./commands/agents.js";
25
+ import { runContextCommand } from "./commands/context.js";
26
+ import { runHistoryCommand } from "./commands/history.js";
27
+ import { runInitCommand } from "./commands/init.js";
28
+ import { runPresetsCommand } from "./commands/presets.js";
29
+ import { runUpdateCommand } from "./commands/update.js";
30
+ import { optionalString } from "./commands/shared.js";
31
+ import { runTuiAgentsWizard, runTuiConfigLoop, runTuiRolesWizard, syncInteractiveDetectedAgents } from "./tuiController.js";
32
+ import { resolveRunOptions } from "./runOptions.js";
26
33
  /** Point d'entrée principal du CLI Palabre. Dispatche vers la commande appropriée selon les arguments. */
27
34
  async function main() {
28
35
  const rawArgs = process.argv.slice(2);
@@ -64,47 +71,20 @@ async function main() {
64
71
  return;
65
72
  }
66
73
  if (parsed.command === "update") {
67
- const info = await getUpdateInfo(await getPackageVersion());
68
- const updateConfigPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
69
- const updateConfig = await configExists(updateConfigPath)
70
- ? await loadConfig(updateConfigPath)
71
- : undefined;
72
- const updateLanguage = resolveLanguage({
73
- explicitLanguage: optionalString(parsed.flags.language),
74
- configLanguage: updateConfig?.language
75
- });
76
- const updateMessages = createTranslator(updateLanguage);
77
- if (parsed.flags.apply) {
78
- await applySourceUpdate(info, updateMessages);
79
- console.log(updateMessages.update.upToDate);
80
- return;
81
- }
82
- console.log(formatUpdateInstructions(info, updateMessages));
74
+ await runUpdateCommand(parsed.flags);
83
75
  return;
84
76
  }
85
77
  if (parsed.command === "init" || parsed.command === "setup") {
86
- const initConfigPath = optionalString(parsed.flags.config) ?? (parsed.flags.local ? DEFAULT_CONFIG_PATH : GLOBAL_CONFIG_PATH);
87
- if (await configExists(initConfigPath)) {
88
- console.log(startupMessages.init.configExists(initConfigPath));
89
- return;
90
- }
91
- const discovery = await discoverLocalTools();
92
- const config = createConfigFromDiscovery(discovery);
93
- config.language = resolveLanguage({
94
- explicitLanguage: optionalString(parsed.flags.language),
95
- configLanguage: config.language
96
- });
97
- const initMessages = createTranslator(config.language);
98
- await writeExampleConfig(initConfigPath, config);
99
- console.log(initMessages.init.configCreated(initConfigPath));
100
- printInitDiscovery(discovery, config, initMessages);
78
+ await runInitCommand(parsed.flags);
101
79
  return;
102
80
  }
103
81
  const configPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
104
82
  let config;
105
83
  let tuiNotice;
106
84
  if (!(await configExists(configPath))) {
107
- config = createConfigFromDiscovery(await discoverLocalTools());
85
+ config = createConfigFromDiscovery(await discoverLocalTools({
86
+ ollamaUrl: optionalString(parsed.flags["ollama-url"])
87
+ }));
108
88
  config.language = resolveLanguage({
109
89
  explicitLanguage: optionalString(parsed.flags.language),
110
90
  configLanguage: config.language
@@ -131,6 +111,7 @@ async function main() {
131
111
  let resetTuiRunOverridesOnNextTopic = false;
132
112
  let tuiMode = config.defaults?.mode ?? "debate";
133
113
  let tuiVersion = "";
114
+ let tuiLatestVersion;
134
115
  const handleTuiHomeInput = async (tuiInput) => {
135
116
  if (!tuiInput) {
136
117
  return "quit";
@@ -145,6 +126,12 @@ async function main() {
145
126
  const nextInput = await promptTuiHomeTopic(tuiMode, messages);
146
127
  return handleTuiHomeInput(nextInput);
147
128
  }
129
+ if (tuiInput.kind === "update") {
130
+ const info = await getUpdateInfo(tuiVersion);
131
+ renderTuiUpdate(formatUpdateInstructions(info, messages), messages);
132
+ const nextInput = await promptTuiHomeTopic(tuiMode, messages);
133
+ return handleTuiHomeInput(nextInput);
134
+ }
148
135
  if (tuiInput.kind === "home") {
149
136
  return "continue";
150
137
  }
@@ -200,14 +187,21 @@ async function main() {
200
187
  return "run";
201
188
  };
202
189
  if (shouldOpenTuiHome(parsed)) {
203
- const syncResult = await syncInteractiveDetectedAgents(configPath, config);
190
+ const [syncResult, currentVersion, latestVersion] = await Promise.all([
191
+ syncInteractiveDetectedAgents(configPath, config),
192
+ getPackageVersion(),
193
+ getLatestPackageVersion()
194
+ ]);
204
195
  if (!tuiNotice && syncResult.addedAgents.length > 0) {
205
196
  tuiNotice = messages.config.syncAdded(configPath, syncResult.addedAgents.join(", "));
206
197
  }
207
198
  stayInTuiAfterSession = true;
208
- tuiVersion = await getPackageVersion();
199
+ tuiVersion = currentVersion;
200
+ tuiLatestVersion = latestVersion && compareSemver(currentVersion, latestVersion) < 0
201
+ ? latestVersion
202
+ : undefined;
209
203
  for (;;) {
210
- renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion });
204
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion, latestVersion: tuiLatestVersion });
211
205
  const tuiInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
212
206
  tuiNotice = undefined;
213
207
  const action = await handleTuiHomeInput(tuiInput);
@@ -264,32 +258,15 @@ async function main() {
264
258
  if (!topic) {
265
259
  throw new Error(messages.common.topicRequired);
266
260
  }
267
- const mode = parseModeFlag(optionalString(parsed.flags.mode) ?? config.defaults?.mode, messages);
268
- const explicitAskAgents = getStringListFlag(parsed.flags.agents);
269
- const askAgentSeeds = askAgentSeedsForMode(mode, explicitAskAgents, config.defaults?.askAgents);
270
- const agentA = resolveAgentName("agent A", parsed.flags["agent-a"], preset?.agentA, askAgentSeeds[0] ?? config.defaults?.agentA, messages);
271
- const agentB = resolveAgentName("agent B", parsed.flags["agent-b"], preset?.agentB, askAgentSeeds[1] ?? askAgentSeeds[0] ?? config.defaults?.agentB, messages);
272
- const askAgents = mode === "ask" ? resolveAskAgents(explicitAskAgents, config.defaults?.askAgents, [agentA, agentB], messages) : undefined;
273
- const options = {
274
- mode,
261
+ const options = resolveRunOptions({
262
+ flags: parsed.flags,
263
+ config,
275
264
  language,
276
265
  topic,
277
- agentA,
278
- agentB,
279
- askAgents,
280
- turns: parseTurnsFlag(parsed.flags.turns, config.defaults?.turns ?? DEFAULT_TURNS, "--turns", messages),
281
- session: createSessionContext(),
282
266
  files: context.files,
283
- modelA: optionalString(parsed.flags["model-a"]),
284
- modelB: optionalString(parsed.flags["model-b"]),
285
- pullModels: Boolean(parsed.flags["pull-models"]),
286
- summaryAgent: resolveSummaryAgentOption(parsed.flags["summary-agent"], config.defaults, mode),
287
- summaryModel: optionalString(parsed.flags["summary-model"]),
288
- summaryEnabled: !parsed.flags["no-summary"],
289
- earlyStopOnAgreement: !parsed.flags["no-early-stop"],
290
- plainOutput: Boolean(parsed.flags.plain || parsed.flags.terminal),
267
+ preset,
291
268
  signal: debateAbortSignal()
292
- };
269
+ }, messages);
293
270
  if (parsed.flags["show-prompt"]) {
294
271
  printContextWarnings(context.warnings, messages);
295
272
  printPromptPreview(config, options, language, messages);
@@ -310,7 +287,7 @@ async function main() {
310
287
  if (!stayInTuiAfterSession) {
311
288
  return;
312
289
  }
313
- tuiMode = mode;
290
+ tuiMode = options.mode;
314
291
  for (;;) {
315
292
  const nextInput = await promptTuiHomeTopic(tuiMode, messages, { notice: tuiNotice });
316
293
  tuiNotice = undefined;
@@ -318,7 +295,7 @@ async function main() {
318
295
  if (action === "quit")
319
296
  return;
320
297
  if (action === "continue") {
321
- renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion });
298
+ renderTuiHome(config, configPath, messages, { mode: tuiMode, version: tuiVersion, latestVersion: tuiLatestVersion });
322
299
  continue;
323
300
  }
324
301
  if (action === "retry") {
@@ -342,25 +319,6 @@ function debateAbortSignal() {
342
319
  process.once("SIGTERM", abort);
343
320
  return controller.signal;
344
321
  }
345
- /**
346
- * Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
347
- * @param flags - Flags parsés depuis la ligne de commande.
348
- */
349
- async function runAgentsCommand(flags) {
350
- const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
351
- if (!(await configExists(configPath))) {
352
- const messages = createTranslator(resolveLanguage({ explicitLanguage: optionalString(flags.language) }));
353
- throw new Error(messages.agents.noConfig);
354
- }
355
- const config = await loadConfig(configPath);
356
- const language = resolveLanguage({
357
- explicitLanguage: optionalString(flags.language),
358
- configLanguage: config.language
359
- });
360
- const messages = createTranslator(language);
361
- const discovery = await discoverLocalTools();
362
- printAgents(configPath, config, discovery, messages);
363
- }
364
322
  /**
365
323
  * Exécute la commande `config` : wizard interactif ou mise à jour directe des paramètres par défaut.
366
324
  * @param flags - Flags parsés depuis la ligne de commande.
@@ -394,7 +352,7 @@ async function runConfigCommand(flags) {
394
352
  return;
395
353
  }
396
354
  if (flags["sync-agents"]) {
397
- const discovery = await discoverLocalTools();
355
+ const discovery = await discoverLocalToolsForConfig(config, optionalString(flags["ollama-url"]));
398
356
  const result = syncDetectedAgentsDetailed(config, discovery);
399
357
  if (!result.changed) {
400
358
  console.log(messages.config.syncNoMissing(configPath));
@@ -481,359 +439,8 @@ async function runConfigCommand(flags) {
481
439
  }
482
440
  await runConfigWizard(configPath, config, messages);
483
441
  }
484
- async function runTuiConfigLoop(configPath, config, messages, initialMode) {
485
- let mode = initialMode;
486
- let notice;
487
- let currentMessages = messages;
488
- let changedRunDefaults = false;
489
- for (;;) {
490
- renderTuiConfig(config, configPath, mode, currentMessages, { message: notice });
491
- notice = undefined;
492
- const input = await promptTuiConfigCommand(mode, currentMessages);
493
- if (input.kind === "quit") {
494
- return { mode, quit: true, changedRunDefaults };
495
- }
496
- if (input.kind === "back") {
497
- return { mode, quit: false, changedRunDefaults };
498
- }
499
- if (input.kind === "unknown") {
500
- notice = input.message;
501
- continue;
502
- }
503
- if (input.kind === "mode") {
504
- mode = mode === "ask" ? "debate" : "ask";
505
- config.defaults = { ...(config.defaults ?? {}), mode };
506
- await writeExampleConfig(configPath, config);
507
- changedRunDefaults = true;
508
- notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
509
- continue;
510
- }
511
- if (input.kind === "default-mode") {
512
- config.defaults = { ...(config.defaults ?? {}), mode };
513
- await writeExampleConfig(configPath, config);
514
- changedRunDefaults = true;
515
- notice = mode === "ask" ? currentMessages.tui.askDefaultMode : currentMessages.tui.debateDefaultMode;
516
- continue;
517
- }
518
- if (input.kind === "interface") {
519
- config.defaults = { ...(config.defaults ?? {}), interface: input.interfaceName };
520
- await writeExampleConfig(configPath, config);
521
- notice = currentMessages.tui.interfaceDefault(input.interfaceName);
522
- continue;
523
- }
524
- if (input.kind === "language") {
525
- config.language = parseLanguage(input.language, "--language");
526
- await writeExampleConfig(configPath, config);
527
- currentMessages = createTranslator(config.language ?? DEFAULT_LANGUAGE);
528
- notice = currentMessages.tui.languageUpdated(input.language);
529
- continue;
530
- }
531
- if (input.kind === "agents") {
532
- try {
533
- const agentsInput = input.agents.length > 0
534
- ? { kind: "agents", agents: input.agents }
535
- : await promptTuiAgentsWizard(config, mode, currentMessages);
536
- if (agentsInput.kind === "quit") {
537
- return { mode, quit: true, changedRunDefaults };
538
- }
539
- if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
540
- notice = currentMessages.tui.agentsUnchanged;
541
- continue;
542
- }
543
- if (mode === "ask") {
544
- const agents = normalizeTuiAskAgents(config, agentsInput.agents, currentMessages);
545
- config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
546
- await writeExampleConfig(configPath, config);
547
- changedRunDefaults = true;
548
- notice = currentMessages.tui.askAgentsUpdated(agents.join(", "));
549
- }
550
- else {
551
- const [agentA, agentB] = normalizeTuiDebateAgents(config, agentsInput.agents, currentMessages);
552
- config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
553
- await writeExampleConfig(configPath, config);
554
- changedRunDefaults = true;
555
- notice = currentMessages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
556
- }
557
- }
558
- catch (error) {
559
- notice = error instanceof Error ? error.message : String(error);
560
- }
561
- continue;
562
- }
563
- if (input.kind === "roles") {
564
- try {
565
- const rolesInput = input.roles.length > 0
566
- ? { kind: "roles", roles: input.roles }
567
- : await promptTuiRolesWizard(config, mode, currentMessages);
568
- if (rolesInput.kind === "quit") {
569
- return { mode, quit: true, changedRunDefaults };
570
- }
571
- if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
572
- notice = currentMessages.tui.rolesUnchanged;
573
- continue;
574
- }
575
- notice = applyTuiRoles(config, mode, rolesInput.roles, currentMessages);
576
- await writeExampleConfig(configPath, config);
577
- }
578
- catch (error) {
579
- notice = error instanceof Error ? error.message : String(error);
580
- }
581
- continue;
582
- }
583
- if (input.kind === "turns") {
584
- if (mode === "ask") {
585
- notice = currentMessages.tui.askTurnsNotice;
586
- continue;
587
- }
588
- try {
589
- validateTurns(input.turns, "--turns", currentMessages);
590
- config.defaults = { ...(config.defaults ?? {}), turns: input.turns };
591
- await writeExampleConfig(configPath, config);
592
- changedRunDefaults = true;
593
- notice = currentMessages.tui.turnsUpdated(input.turns);
594
- }
595
- catch (error) {
596
- notice = error instanceof Error ? error.message : String(error);
597
- }
598
- continue;
599
- }
600
- if (input.kind === "summary") {
601
- try {
602
- const nextDefaults = { ...(config.defaults ?? {}) };
603
- if (input.agent !== undefined) {
604
- assertKnownAgent(config, input.agent, mode === "ask" ? "defaults.askSummaryAgent" : "defaults.summaryAgent", currentMessages);
605
- }
606
- if (mode === "ask") {
607
- if (input.agent === undefined) {
608
- delete nextDefaults.askSummaryAgent;
609
- notice = currentMessages.tui.askSummaryFallback;
610
- }
611
- else {
612
- nextDefaults.askSummaryAgent = input.agent;
613
- notice = currentMessages.tui.askSummaryAgent(input.agent);
614
- }
615
- }
616
- else if (input.agent === undefined) {
617
- delete nextDefaults.summaryAgent;
618
- notice = currentMessages.tui.debateSummaryFallback;
619
- }
620
- else {
621
- nextDefaults.summaryAgent = input.agent;
622
- notice = currentMessages.tui.debateSummaryAgent(input.agent);
623
- }
624
- config.defaults = nextDefaults;
625
- await writeExampleConfig(configPath, config);
626
- changedRunDefaults = true;
627
- }
628
- catch (error) {
629
- notice = error instanceof Error ? error.message : String(error);
630
- }
631
- continue;
632
- }
633
- if (input.kind === "ollama-info") {
634
- try {
635
- notice = await formatTuiOllamaInfo(config, currentMessages);
636
- }
637
- catch (error) {
638
- notice = error instanceof Error ? error.message : String(error);
639
- }
640
- continue;
641
- }
642
- if (input.kind === "ollama-model") {
643
- try {
644
- notice = await setTuiOllamaModel(configPath, config, input.model, currentMessages);
645
- changedRunDefaults = true;
646
- }
647
- catch (error) {
648
- notice = error instanceof Error ? error.message : String(error);
649
- }
650
- continue;
651
- }
652
- if (input.kind === "ollama-sync") {
653
- try {
654
- notice = await syncTuiOllamaModel(configPath, config, currentMessages);
655
- changedRunDefaults = true;
656
- }
657
- catch (error) {
658
- notice = error instanceof Error ? error.message : String(error);
659
- }
660
- continue;
661
- }
662
- }
663
- }
664
- async function formatTuiOllamaInfo(config, messages) {
665
- const discovery = await discoverLocalTools();
666
- const agent = config.agents["ollama-local"];
667
- if (agent?.type !== "ollama") {
668
- throw new Error(messages.config.ollamaModelNoAgent);
669
- }
670
- if (!discovery.ollama.available) {
671
- return messages.tui.ollamaUnavailable(discovery.ollama.baseUrl);
672
- }
673
- const installed = discovery.ollama.models.length > 0
674
- ? discovery.ollama.models.join(", ")
675
- : messages.config.ollamaModelNoInstalledModels;
676
- const api = `${discovery.ollama.baseUrl}`;
677
- return messages.tui.ollamaInfo(agent.model, installed, api);
678
- }
679
- async function setTuiOllamaModel(configPath, config, model, messages) {
680
- const trimmed = model.trim();
681
- if (!trimmed) {
682
- throw new Error(messages.tui.ollamaModelUsage);
683
- }
684
- const discovery = await discoverLocalTools();
685
- const agent = config.agents["ollama-local"];
686
- if (agent?.type !== "ollama") {
687
- throw new Error(messages.config.ollamaModelNoAgent);
688
- }
689
- if (!discovery.ollama.models.includes(trimmed)) {
690
- throw new Error(messages.config.ollamaModelUnavailable(trimmed));
691
- }
692
- const result = setOllamaModel(config, trimmed);
693
- await writeExampleConfig(configPath, config);
694
- return result
695
- ? messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel)
696
- : messages.config.ollamaModelNoChange(configPath, agent.model);
697
- }
698
- async function syncTuiOllamaModel(configPath, config, messages) {
699
- const discovery = await discoverLocalTools();
700
- const agent = config.agents["ollama-local"];
701
- if (agent?.type !== "ollama") {
702
- throw new Error(messages.config.ollamaModelNoAgent);
703
- }
704
- if (discovery.ollama.models.length === 0) {
705
- throw new Error(messages.config.ollamaModelNoInstalledModels);
706
- }
707
- const result = syncOllamaModel(config, discovery);
708
- if (!result) {
709
- return messages.config.ollamaModelNoChange(configPath, agent.model);
710
- }
711
- await writeExampleConfig(configPath, config);
712
- return messages.config.ollamaModelUpdated(configPath, result.previousModel, result.nextModel);
713
- }
714
- async function syncInteractiveDetectedAgents(configPath, config) {
715
- const discovery = await discoverLocalTools();
716
- const result = syncDetectedAgentsDetailed(config, discovery);
717
- if (result.changed) {
718
- await writeExampleConfig(configPath, config);
719
- }
720
- return {
721
- addedAgents: result.addedAgents
722
- };
723
- }
724
- function normalizeTuiDebateAgents(config, agents, messages) {
725
- const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
726
- if (unique.length !== 2) {
727
- throw new Error(messages.tui.debateAgentsUsage);
728
- }
729
- assertKnownAgent(config, unique[0], "defaults.agentA", messages);
730
- assertKnownAgent(config, unique[1], "defaults.agentB", messages);
731
- return [unique[0], unique[1]];
732
- }
733
- function normalizeTuiAskAgents(config, agents, messages) {
734
- const unique = agents.map((agent) => agent.trim()).filter((agent, index, list) => agent && list.indexOf(agent) === index);
735
- if (unique.length === 0) {
736
- throw new Error(messages.tui.askAgentsUsage);
737
- }
738
- if (unique.length > MAX_ASK_AGENTS) {
739
- throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
740
- }
741
- unique.forEach((agent) => assertKnownAgent(config, agent, "defaults.askAgents", messages));
742
- return unique;
743
- }
744
- async function runTuiAgentsWizard(configPath, config, messages, mode, inlineAgents = []) {
745
- try {
746
- const agentsInput = inlineAgents.length > 0
747
- ? { kind: "agents", agents: inlineAgents }
748
- : await promptTuiAgentsWizard(config, mode, messages);
749
- if (agentsInput.kind === "quit") {
750
- return { quit: true, changedRunDefaults: false };
751
- }
752
- if (agentsInput.kind === "back" || agentsInput.agents.length === 0) {
753
- return { quit: false, changedRunDefaults: false };
754
- }
755
- const notice = applyTuiAgents(config, mode, agentsInput.agents, messages);
756
- await writeExampleConfig(configPath, config);
757
- return { notice, quit: false, changedRunDefaults: true };
758
- }
759
- catch (error) {
760
- return { notice: messages.tui.agentsError(error instanceof Error ? error.message : String(error)), quit: false, changedRunDefaults: false };
761
- }
762
- }
763
- async function runTuiRolesWizard(configPath, config, messages, mode, inlineRoles = []) {
764
- try {
765
- const rolesInput = inlineRoles.length > 0
766
- ? { kind: "roles", roles: inlineRoles }
767
- : await promptTuiRolesWizard(config, mode, messages);
768
- if (rolesInput.kind === "quit") {
769
- return { quit: true };
770
- }
771
- if (rolesInput.kind === "back" || rolesInput.roles.length === 0) {
772
- return { quit: false };
773
- }
774
- const notice = applyTuiRoles(config, mode, rolesInput.roles, messages);
775
- await writeExampleConfig(configPath, config);
776
- return { notice, quit: false };
777
- }
778
- catch (error) {
779
- return { notice: messages.tui.rolesError(error instanceof Error ? error.message : String(error)), quit: false };
780
- }
781
- }
782
- function applyTuiAgents(config, mode, agentNames, messages) {
783
- if (mode === "ask") {
784
- const agents = normalizeTuiAskAgents(config, agentNames, messages);
785
- config.defaults = { ...(config.defaults ?? {}), askAgents: agents };
786
- return messages.tui.askAgentsUpdated(agents.join(", "));
787
- }
788
- const [agentA, agentB] = normalizeTuiDebateAgents(config, agentNames, messages);
789
- config.defaults = { ...(config.defaults ?? {}), agentA, agentB };
790
- return messages.tui.debateAgentsUpdated(`${agentA} <-> ${agentB}`);
791
- }
792
- function applyTuiRoles(config, mode, roleNames, messages) {
793
- const agents = activeAgentsForMode(config, mode);
794
- if (agents.length === 0) {
795
- throw new Error(mode === "ask" ? messages.tui.noAskAgentsConfigured : messages.tui.noDebateAgentsConfigured);
796
- }
797
- const roles = normalizeTuiRoles(roleNames, agents, mode, messages);
798
- agents.forEach((agent, index) => {
799
- config.agents[agent].role = roles[index];
800
- });
801
- return mode === "ask"
802
- ? messages.tui.askRolesUpdated(roles.join(", "))
803
- : messages.tui.debateRolesUpdated(roles.join(" <-> "));
804
- }
805
- function activeAgentsForMode(config, mode) {
806
- const defaults = config.defaults ?? {};
807
- if (mode === "ask") {
808
- if (defaults.askAgents && defaults.askAgents.length > 0) {
809
- return defaults.askAgents.filter((agent) => Boolean(config.agents[agent]));
810
- }
811
- return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
812
- }
813
- return [defaults.agentA, defaults.agentB].filter((agent) => Boolean(agent && config.agents[agent]));
814
- }
815
- function normalizeTuiRoles(roleNames, agents, mode, messages) {
816
- const roles = roleNames.map((role) => role.trim().toLowerCase()).filter(Boolean);
817
- const expectedCount = agents.length;
818
- if (roles.length < expectedCount) {
819
- const agentLabel = mode === "ask"
820
- ? agents.join(", ")
821
- : agents.join(" <-> ");
822
- throw new Error(messages.tui.rolesCountError(roles.length, expectedCount, agentLabel));
823
- }
824
- return roles.slice(0, expectedCount).map((role) => {
825
- if (isAgentRole(role)) {
826
- return role;
827
- }
828
- throw new Error(messages.tui.unknownRole(role, VALID_AGENT_ROLES.join(", ")));
829
- });
830
- }
831
- function isAgentRole(value) {
832
- return VALID_AGENT_ROLES.includes(value);
833
- }
834
- const VALID_AGENT_ROLES = ["implementer", "reviewer", "architect", "scout", "critic", "summarizer"];
835
442
  async function runOllamaModelsCommand(config, json) {
836
- const discovery = await discoverLocalTools();
443
+ const discovery = await discoverLocalToolsForConfig(config);
837
444
  const agent = config.agents["ollama-local"];
838
445
  const currentModel = agent?.type === "ollama" ? agent.model : null;
839
446
  const payload = {
@@ -858,7 +465,7 @@ async function runSetOllamaModelCommand(configPath, config, model, messages) {
858
465
  if (!trimmed) {
859
466
  throw new Error(messages.common.optionRequiresValue("--set-ollama-model"));
860
467
  }
861
- const discovery = await discoverLocalTools();
468
+ const discovery = await discoverLocalToolsForConfig(config);
862
469
  const agent = config.agents["ollama-local"];
863
470
  if (agent?.type !== "ollama") {
864
471
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -873,7 +480,7 @@ async function runSetOllamaModelCommand(configPath, config, model, messages) {
873
480
  : messages.config.ollamaModelNoChange(configPath, agent.model));
874
481
  }
875
482
  async function runSyncOllamaModelCommand(configPath, config, messages) {
876
- const discovery = await discoverLocalTools();
483
+ const discovery = await discoverLocalToolsForConfig(config);
877
484
  const agent = config.agents["ollama-local"];
878
485
  if (agent?.type !== "ollama") {
879
486
  throw new Error(messages.config.ollamaModelNoAgent);
@@ -927,32 +534,6 @@ function assertKnownAgent(config, agentName, fieldName, messages) {
927
534
  throw new Error(messages.common.unknownAgentForField(fieldName, agentName, Object.keys(config.agents).join(", ")));
928
535
  }
929
536
  }
930
- /**
931
- * Résout le nom d'un agent selon la priorité : flag CLI > preset > défaut config.
932
- * Lève une erreur si aucune source ne fournit de valeur.
933
- * @param label - Libellé humain utilisé dans le message d'erreur (ex. "agent A").
934
- * @param explicitValue - Valeur passée via flag CLI.
935
- * @param presetValue - Valeur issue du preset sélectionné.
936
- * @param defaultValue - Valeur issue des défauts de la config.
937
- * @returns Nom de l'agent résolu.
938
- */
939
- function resolveAgentName(label, explicitValue, presetValue, defaultValue, messages) {
940
- const resolved = optionalString(explicitValue) ?? presetValue ?? defaultValue;
941
- if (!resolved) {
942
- throw new Error(messages.common.noAgentDefined(label));
943
- }
944
- return resolved;
945
- }
946
- function resolveSummaryAgentOption(explicitValue, defaults, mode) {
947
- const explicit = optionalString(explicitValue);
948
- if (explicit) {
949
- return explicit;
950
- }
951
- if (mode === "ask") {
952
- return defaults?.askSummaryAgent ?? defaults?.summaryAgent;
953
- }
954
- return defaults?.summaryAgent;
955
- }
956
537
  function parseModeFlag(value, messages) {
957
538
  if (!value) {
958
539
  return "debate";
@@ -971,16 +552,6 @@ function parseInterfaceFlag(value, messages) {
971
552
  }
972
553
  throw new Error(messages.common.unknownMode(value, "tui, terminal"));
973
554
  }
974
- function resolveAskAgents(explicitAgents, defaultAgents, fallbackAgents, messages) {
975
- const selected = explicitAgents.length > 0
976
- ? explicitAgents
977
- : defaultAgents && defaultAgents.length > 0 ? defaultAgents : fallbackAgents;
978
- const unique = selected.filter((agent, index) => agent.trim() && selected.indexOf(agent) === index);
979
- if (unique.length > MAX_ASK_AGENTS) {
980
- throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
981
- }
982
- return unique;
983
- }
984
555
  /**
985
556
  * Affiche un aperçu du prompt du premier tour sans appeler aucun agent (flag `--show-prompt`).
986
557
  * @param config - Config chargée.
@@ -1010,29 +581,13 @@ function printPromptPreview(config, options, language, messages) {
1010
581
  console.log(messages.preview.agent(previewAgent, agentConfig.role));
1011
582
  console.log(messages.preview.peer(peerName));
1012
583
  console.log(messages.preview.pullModels(options.pullModels));
1013
- console.log(messages.preview.summary(options.summaryEnabled ? previewSummaryAgent(options) : messages.preview.disabled));
584
+ console.log(messages.preview.summary(options.summaryEnabled ? options.summaryAgent : messages.preview.disabled));
1014
585
  console.log(messages.preview.interfaceLanguage(language));
1015
586
  console.log("");
1016
587
  console.log(prompt);
1017
588
  console.log("");
1018
589
  console.log(options.mode === "ask" ? messages.preview.askNote : messages.preview.note);
1019
590
  }
1020
- function previewSummaryAgent(options) {
1021
- if (options.summaryAgent) {
1022
- return options.summaryAgent;
1023
- }
1024
- if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
1025
- return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
1026
- }
1027
- return options.agentB;
1028
- }
1029
- /**
1030
- * Extrait une chaîne non vide depuis une valeur de flag, ou renvoie `undefined`.
1031
- * @param value - Valeur brute issue du parseur de flags.
1032
- */
1033
- function optionalString(value) {
1034
- return typeof value === "string" && value.trim() ? value : undefined;
1035
- }
1036
591
  /**
1037
592
  * Pré-lit seulement `--language`/`--lang` dans les arguments bruts pour localiser
1038
593
  * les erreurs qui peuvent survenir avant le parsing complet ou le chargement de config.
@@ -1110,94 +665,6 @@ function shouldOpenTuiHome(parsed) {
1110
665
  && parsed.flags.plain !== true
1111
666
  && parsed.flags.terminal !== true;
1112
667
  }
1113
- /**
1114
- * Exécute la commande `palabre presets`.
1115
- *
1116
- * Sortie humaine par défaut (liste alignée), ou JSON avec `--json` pour les
1117
- * intégrations (extension VS Code, scripts shell). Le schéma JSON est versionné
1118
- * via le champ `v` au cas où on enrichirait plus tard (ex : description par
1119
- * preset, tags premium/local).
1120
- *
1121
- * @param flags - Flags parsés depuis la ligne de commande.
1122
- */
1123
- async function runPresetsCommand(flags) {
1124
- const discovery = await discoverLocalTools();
1125
- const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
1126
- const config = await configExists(configPath)
1127
- ? await loadConfig(configPath)
1128
- : createConfigFromDiscovery(discovery);
1129
- const language = resolveLanguage({
1130
- explicitLanguage: optionalString(flags.language),
1131
- configLanguage: config.language
1132
- });
1133
- const messages = createTranslator(language);
1134
- const presets = listPresetsWithAvailability(config, discovery, messages);
1135
- if (flags.json) {
1136
- process.stdout.write(JSON.stringify({ v: 1, presets }) + "\n");
1137
- return;
1138
- }
1139
- console.log(messages.presets.title);
1140
- console.log("");
1141
- for (const preset of presets) {
1142
- const status = preset.available
1143
- ? messages.presets.available
1144
- : messages.presets.unavailable(preset.unavailableReasons.join("; "));
1145
- console.log(` ${preset.name.padEnd(20)} ${preset.agentA} <-> ${preset.agentB} ${status}`);
1146
- }
1147
- console.log("");
1148
- console.log(messages.presets.total(presets.length));
1149
- }
1150
- async function runHistoryCommand(flags) {
1151
- const configPath = optionalString(flags.config) ?? await resolveDefaultConfigPath();
1152
- const config = await configExists(configPath)
1153
- ? await loadConfig(configPath)
1154
- : undefined;
1155
- const language = resolveLanguage({
1156
- explicitLanguage: optionalString(flags.language),
1157
- configLanguage: config?.language
1158
- });
1159
- const messages = createTranslator(language);
1160
- const entries = await listHistoryEntries(resolveOutputDir(config?.outputDir));
1161
- if (flags.json) {
1162
- process.stdout.write(JSON.stringify({ v: 1, history: entries }) + "\n");
1163
- return;
1164
- }
1165
- console.log(messages.tui.historyTitle);
1166
- console.log("");
1167
- if (entries.length === 0) {
1168
- console.log(messages.tui.historyEmpty);
1169
- return;
1170
- }
1171
- for (const entry of entries) {
1172
- console.log(`- ${entry.date || entry.fileName} | ${entry.mode} | ${entry.topic}`);
1173
- console.log(` ${entry.path}`);
1174
- }
1175
- }
1176
- async function runContextCommand(flags, positionals) {
1177
- const language = resolveLanguage({ explicitLanguage: optionalString(flags.language) });
1178
- const messages = createTranslator(language);
1179
- const subcommand = positionals[0] ?? "scan";
1180
- if (subcommand !== "scan") {
1181
- throw new Error(messages.common.unknownCommand(`context ${subcommand}`, "context scan"));
1182
- }
1183
- const paths = positionals.slice(1);
1184
- const result = await buildContextScan(paths, process.cwd(), messages);
1185
- const folders = result.items.filter((item) => item.kind === "folder");
1186
- const files = result.items.filter((item) => item.kind === "file");
1187
- if (flags.json) {
1188
- console.log(JSON.stringify(result, null, 2));
1189
- return;
1190
- }
1191
- for (const folder of folders) {
1192
- console.log(`[folder] ${folder.path}`);
1193
- }
1194
- for (const file of files) {
1195
- console.log(`[file] ${file.path} (${file.sizeBytes} bytes)`);
1196
- }
1197
- for (const warning of result.warnings) {
1198
- console.error(`${messages.renderers.warningPrefix} ${warning}`);
1199
- }
1200
- }
1201
668
  /**
1202
669
  * Écrit les avertissements de contexte sur `stderr`.
1203
670
  * @param warnings - Messages d'avertissement issus du chargement des fichiers de contexte.
@@ -1207,150 +674,6 @@ function printContextWarnings(warnings, messages) {
1207
674
  process.stderr.write(`${messages.renderers.warningPrefix} ${warning}\n`);
1208
675
  }
1209
676
  }
1210
- /**
1211
- * Ajoute dans `config.agents` les agents détectés localement mais absents de la config.
1212
- * Mute `config` directement ; l'appelant est responsable de persister la config.
1213
- * @param config - Config Palabre à compléter.
1214
- * @param discovery - Résultat de la découverte locale des outils.
1215
- * @returns Noms des agents nouvellement ajoutés.
1216
- */
1217
- /**
1218
- * Affiche la liste des agents déclarés avec leur type, rôle, état de détection et défauts.
1219
- * @param configPath - Chemin du fichier de config (affiché en en-tête).
1220
- * @param config - Config Palabre chargée.
1221
- * @param discovery - Résultat de la découverte locale des outils.
1222
- */
1223
- function printAgents(configPath, config, discovery, messages) {
1224
- const entries = Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right));
1225
- console.log(messages.agents.config(configPath));
1226
- console.log("");
1227
- console.log(messages.agents.title);
1228
- for (const [name, agentConfig] of entries) {
1229
- const status = formatAgentDetection(name, agentConfig, discovery, messages);
1230
- const defaults = formatAgentDefaults(name, config, messages);
1231
- const details = formatAgentDetails(agentConfig, messages);
1232
- const suffix = defaults ? ` | ${defaults}` : "";
1233
- console.log(`- ${name.padEnd(13)} ${`${agentConfig.type}/${agentConfig.role}`.padEnd(18)} ${status}${suffix}`);
1234
- if (details) {
1235
- console.log(` ${details}`);
1236
- }
1237
- }
1238
- console.log("");
1239
- console.log(messages.agents.defaults(config.defaults?.agentA ?? messages.agents.none, config.defaults?.agentB ?? messages.agents.none, turnsOrDefault(config.defaults?.turns), config.defaults?.summaryAgent ?? messages.agents.summaryAgentB, config.defaults?.askSummaryAgent));
1240
- }
1241
- /**
1242
- * Renvoie un libellé indiquant si l'agent est agent A, agent B ou agent de synthèse par défaut.
1243
- * @param name - Nom de l'agent.
1244
- * @param config - Config Palabre contenant les défauts.
1245
- */
1246
- function formatAgentDefaults(name, config, messages) {
1247
- const labels = [];
1248
- if (config.defaults?.agentA === name)
1249
- labels.push(messages.agents.defaultAgentA);
1250
- if (config.defaults?.agentB === name)
1251
- labels.push(messages.agents.defaultAgentB);
1252
- if (config.defaults?.summaryAgent === name)
1253
- labels.push(messages.agents.defaultSummary);
1254
- if (config.defaults?.askSummaryAgent === name)
1255
- labels.push(messages.agents.defaultAskSummary);
1256
- return labels.join(", ");
1257
- }
1258
- /**
1259
- * Renvoie une ligne de détails pour un agent : commande CLI ou modèle Ollama.
1260
- * @param agentConfig - Configuration de l'agent.
1261
- */
1262
- function formatAgentDetails(agentConfig, messages) {
1263
- if (agentConfig.type === "ollama") {
1264
- return messages.agents.model(agentConfig.model);
1265
- }
1266
- return messages.agents.command(agentConfig.command, agentConfig.model);
1267
- }
1268
- /**
1269
- * Renvoie le statut de détection d'un agent sous forme de chaîne lisible.
1270
- * Pour Ollama, vérifie la disponibilité du serveur et la présence du modèle.
1271
- * @param name - Nom de l'agent dans la config.
1272
- * @param agentConfig - Configuration de l'agent.
1273
- * @param discovery - Résultat de la découverte locale des outils.
1274
- */
1275
- function formatAgentDetection(name, agentConfig, discovery, messages) {
1276
- if (agentConfig.type === "ollama") {
1277
- if (!discovery.ollama.available) {
1278
- return discovery.ollama.commandAvailable ? messages.agents.ollamaUnreachable : messages.agents.ollamaNotDetected;
1279
- }
1280
- return discovery.ollama.models.includes(agentConfig.model)
1281
- ? messages.agents.detected()
1282
- : messages.agents.missingModel(agentConfig.model);
1283
- }
1284
- const detection = cliDetectionForAgent(name, agentConfig, discovery);
1285
- return detection.available ? messages.agents.detected(detection.command) : messages.agents.notDetected;
1286
- }
1287
- /**
1288
- * Résout l'entrée de détection correspondant à un agent CLI.
1289
- * Renvoie un objet `{ available: true }` pour les agents CLI non reconnus (considérés disponibles).
1290
- * @param name - Nom de l'agent dans la config.
1291
- * @param agentConfig - Configuration de l'agent.
1292
- * @param discovery - Résultat de la découverte locale des outils.
1293
- */
1294
- function cliDetectionForAgent(name, agentConfig, discovery) {
1295
- const command = agentConfig.type === "cli" || agentConfig.type === "cli-pty" ? agentConfig.command : name;
1296
- return detectionForCommand(command, discovery) ?? { available: true, command };
1297
- }
1298
- /**
1299
- * Affiche le récapitulatif de détection locale après `palabre init`.
1300
- * @param discovery - Résultat de la découverte locale des outils.
1301
- * @param config - Config générée à partir de la découverte.
1302
- */
1303
- function printInitDiscovery(discovery, config, messages) {
1304
- console.log("");
1305
- console.log(messages.init.localDetectionTitle);
1306
- console.log(`- Codex CLI: ${formatCommandDetection(discovery.codex, messages)}`);
1307
- console.log(`- Claude CLI: ${formatCommandDetection(discovery.claude, messages)}`);
1308
- console.log(`- Antigravity CLI: ${formatCommandDetection(discovery.antigravity, messages)}`);
1309
- console.log(`- OpenCode CLI: ${formatCommandDetection(discovery.opencode, messages)}`);
1310
- console.log(`- Mistral Vibe CLI: ${formatCommandDetection(discovery.vibe, messages)}`);
1311
- console.log(`- Ollama API: ${formatOllamaDetection(discovery.ollama, messages)}`);
1312
- console.log("");
1313
- console.log(config.defaults?.agentA && config.defaults.agentB
1314
- ? messages.init.defaults(config.defaults.agentA, config.defaults.agentB)
1315
- : messages.init.noDefaultPair(formatDetectedAgentSummary(discovery, config.language ?? DEFAULT_LANGUAGE)));
1316
- console.log(messages.init.languageHint(config.language ?? DEFAULT_LANGUAGE));
1317
- }
1318
- function formatDetectedAgentSummary(discovery, language) {
1319
- const names = detectedAgentNames(discovery);
1320
- if (names.length === 0) {
1321
- return language === "en" ? "no agent detected" : "aucun agent détecté";
1322
- }
1323
- if (names.length === 1) {
1324
- return language === "en"
1325
- ? `only one agent detected (${names[0]})`
1326
- : `un seul agent détecté (${names[0]})`;
1327
- }
1328
- return language === "en"
1329
- ? `no usable pair detected among ${names.join(", ")}`
1330
- : `aucune paire utilisable détectée parmi ${names.join(", ")}`;
1331
- }
1332
- /**
1333
- * Formate le statut de détection d'un outil CLI (disponible ou non).
1334
- * @param detection - Résultat de détection d'un outil CLI.
1335
- */
1336
- function formatCommandDetection(detection, messages) {
1337
- return detection.available
1338
- ? messages.init.commandDetected(detection.command)
1339
- : messages.init.commandMissing;
1340
- }
1341
- /**
1342
- * Formate le statut de détection d'Ollama : commande absente, serveur injoignable ou modèles disponibles.
1343
- * @param detection - Résultat de détection d'Ollama.
1344
- */
1345
- function formatOllamaDetection(detection, messages) {
1346
- if (!detection.available) {
1347
- return detection.commandAvailable
1348
- ? messages.init.ollamaServerUnreachable(detection.baseUrl)
1349
- : messages.init.ollamaMissing;
1350
- }
1351
- const modelCount = detection.models.length;
1352
- return messages.init.ollamaDetected(modelCount);
1353
- }
1354
677
  /** Affiche le texte d'aide complet sur `stdout`. */
1355
678
  function printHelp(messages, command) {
1356
679
  const commandHelp = command ? messages.help.renderCommand(command) : undefined;
@@ -1383,12 +706,23 @@ async function resolveCommandMessages(flags) {
1383
706
  }
1384
707
  return createTranslator(resolveLanguage({ explicitLanguage, configLanguage }));
1385
708
  }
709
+ function formatRuntimeError(error, messages) {
710
+ if (error instanceof AdapterError) {
711
+ return formatAdapterError(error, messages);
712
+ }
713
+ if (error instanceof OllamaUrlError) {
714
+ if (error.kind === "empty")
715
+ return messages.common.ollamaUrlEmpty;
716
+ if (error.kind === "protocol")
717
+ return messages.common.ollamaUrlProtocol(error.protocol ?? "");
718
+ return messages.common.ollamaUrlInvalid(error.value);
719
+ }
720
+ return error instanceof Error ? error.message : String(error);
721
+ }
1386
722
  main().catch((error) => {
1387
723
  const language = safeStartupLanguage(process.argv.slice(2));
1388
724
  const messages = createTranslator(language);
1389
- const message = error instanceof AdapterError
1390
- ? formatAdapterError(error, messages)
1391
- : error instanceof Error ? error.message : String(error);
725
+ const message = formatRuntimeError(error, messages);
1392
726
  console.error(`${messages.common.errorPrefix}: ${message}`);
1393
727
  process.exitCode = 1;
1394
728
  });