bernard-agent 0.8.1 → 0.9.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.
Files changed (174) hide show
  1. package/README.md +80 -44
  2. package/dist/agent.d.ts +14 -3
  3. package/dist/agent.js +228 -38
  4. package/dist/agent.js.map +1 -1
  5. package/dist/builtin-specialists/correction-agent.json +32 -0
  6. package/dist/builtin-specialists/file-wrapper.json +43 -0
  7. package/dist/builtin-specialists/shell-wrapper.json +50 -0
  8. package/dist/builtin-specialists/specialist-creator.json +32 -0
  9. package/dist/builtin-specialists/web-wrapper.json +38 -0
  10. package/dist/candidate-bootstrap.d.ts +18 -0
  11. package/dist/candidate-bootstrap.js +61 -0
  12. package/dist/candidate-bootstrap.js.map +1 -0
  13. package/dist/config.d.ts +126 -10
  14. package/dist/config.js +222 -45
  15. package/dist/config.js.map +1 -1
  16. package/dist/context.js +23 -6
  17. package/dist/context.js.map +1 -1
  18. package/dist/correction-candidates.d.ts +54 -0
  19. package/dist/correction-candidates.js +138 -0
  20. package/dist/correction-candidates.js.map +1 -0
  21. package/dist/correction.d.ts +67 -0
  22. package/dist/correction.js +138 -0
  23. package/dist/correction.js.map +1 -0
  24. package/dist/critic.js +2 -1
  25. package/dist/critic.js.map +1 -1
  26. package/dist/cron/notes-store.d.ts +41 -0
  27. package/dist/cron/notes-store.js +134 -0
  28. package/dist/cron/notes-store.js.map +1 -0
  29. package/dist/cron/runner.js +25 -3
  30. package/dist/cron/runner.js.map +1 -1
  31. package/dist/cron/scoped-notes-tools.d.ts +24 -0
  32. package/dist/cron/scoped-notes-tools.js +50 -0
  33. package/dist/cron/scoped-notes-tools.js.map +1 -0
  34. package/dist/custom-providers.d.ts +80 -0
  35. package/dist/custom-providers.js +238 -0
  36. package/dist/custom-providers.js.map +1 -0
  37. package/dist/fs-utils.d.ts +2 -0
  38. package/dist/fs-utils.js +44 -0
  39. package/dist/fs-utils.js.map +1 -0
  40. package/dist/history.js +3 -1
  41. package/dist/history.js.map +1 -1
  42. package/dist/image.d.ts +59 -0
  43. package/dist/image.js +228 -0
  44. package/dist/image.js.map +1 -0
  45. package/dist/index.js +72 -4
  46. package/dist/index.js.map +1 -1
  47. package/dist/mcp.js +1 -1
  48. package/dist/mcp.js.map +1 -1
  49. package/dist/memory.d.ts +13 -0
  50. package/dist/memory.js +45 -4
  51. package/dist/memory.js.map +1 -1
  52. package/dist/menu.d.ts +97 -0
  53. package/dist/menu.js +338 -0
  54. package/dist/menu.js.map +1 -0
  55. package/dist/os-info.d.ts +22 -0
  56. package/dist/os-info.js +111 -0
  57. package/dist/os-info.js.map +1 -0
  58. package/dist/output.d.ts +35 -1
  59. package/dist/output.js +256 -45
  60. package/dist/output.js.map +1 -1
  61. package/dist/pac.d.ts +14 -2
  62. package/dist/pac.js +5 -5
  63. package/dist/pac.js.map +1 -1
  64. package/dist/paths.d.ts +5 -0
  65. package/dist/paths.js +6 -1
  66. package/dist/paths.js.map +1 -1
  67. package/dist/plan-store.d.ts +47 -0
  68. package/dist/plan-store.js +94 -0
  69. package/dist/plan-store.js.map +1 -0
  70. package/dist/prompt-rewriter.d.ts +29 -0
  71. package/dist/prompt-rewriter.js +155 -0
  72. package/dist/prompt-rewriter.js.map +1 -0
  73. package/dist/providers/index.d.ts +56 -4
  74. package/dist/providers/index.js +86 -5
  75. package/dist/providers/index.js.map +1 -1
  76. package/dist/providers/profiles.d.ts +37 -0
  77. package/dist/providers/profiles.js +110 -0
  78. package/dist/providers/profiles.js.map +1 -0
  79. package/dist/providers/types.d.ts +11 -2
  80. package/dist/providers/types.js +3 -0
  81. package/dist/providers/types.js.map +1 -1
  82. package/dist/rag-query.js +15 -1
  83. package/dist/rag-query.js.map +1 -1
  84. package/dist/react.d.ts +38 -0
  85. package/dist/react.js +116 -0
  86. package/dist/react.js.map +1 -0
  87. package/dist/reasoning-log.d.ts +30 -0
  88. package/dist/reasoning-log.js +102 -0
  89. package/dist/reasoning-log.js.map +1 -0
  90. package/dist/reference-resolver.d.ts +47 -0
  91. package/dist/reference-resolver.js +316 -0
  92. package/dist/reference-resolver.js.map +1 -0
  93. package/dist/reference-tool-lookup.d.ts +37 -0
  94. package/dist/reference-tool-lookup.js +318 -0
  95. package/dist/reference-tool-lookup.js.map +1 -0
  96. package/dist/repl.js +1038 -371
  97. package/dist/repl.js.map +1 -1
  98. package/dist/setup.js +2 -1
  99. package/dist/setup.js.map +1 -1
  100. package/dist/specialist-detector.js +2 -1
  101. package/dist/specialist-detector.js.map +1 -1
  102. package/dist/specialists.d.ts +74 -3
  103. package/dist/specialists.js +152 -20
  104. package/dist/specialists.js.map +1 -1
  105. package/dist/structured-output.d.ts +58 -0
  106. package/dist/structured-output.js +138 -0
  107. package/dist/structured-output.js.map +1 -0
  108. package/dist/theme.d.ts +2 -0
  109. package/dist/theme.js +18 -12
  110. package/dist/theme.js.map +1 -1
  111. package/dist/tool-call-repair.d.ts +29 -0
  112. package/dist/tool-call-repair.js +99 -0
  113. package/dist/tool-call-repair.js.map +1 -0
  114. package/dist/tool-profiles.d.ts +70 -0
  115. package/dist/tool-profiles.js +385 -0
  116. package/dist/tool-profiles.js.map +1 -0
  117. package/dist/tools/activity-summary.d.ts +15 -0
  118. package/dist/tools/activity-summary.js +44 -0
  119. package/dist/tools/activity-summary.js.map +1 -0
  120. package/dist/tools/ask-user.d.ts +49 -0
  121. package/dist/tools/ask-user.js +52 -0
  122. package/dist/tools/ask-user.js.map +1 -0
  123. package/dist/tools/augment.d.ts +17 -0
  124. package/dist/tools/augment.js +102 -0
  125. package/dist/tools/augment.js.map +1 -0
  126. package/dist/tools/cron-logs.js +7 -0
  127. package/dist/tools/cron-logs.js.map +1 -1
  128. package/dist/tools/cron-notes.d.ts +52 -0
  129. package/dist/tools/cron-notes.js +105 -0
  130. package/dist/tools/cron-notes.js.map +1 -0
  131. package/dist/tools/datetime.d.ts +7 -0
  132. package/dist/tools/datetime.js +29 -3
  133. package/dist/tools/datetime.js.map +1 -1
  134. package/dist/tools/evaluate.d.ts +20 -0
  135. package/dist/tools/evaluate.js +29 -0
  136. package/dist/tools/evaluate.js.map +1 -0
  137. package/dist/tools/index.js +4 -0
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/mcp.d.ts +3 -3
  140. package/dist/tools/plan.d.ts +81 -0
  141. package/dist/tools/plan.js +108 -0
  142. package/dist/tools/plan.js.map +1 -0
  143. package/dist/tools/result-cap.d.ts +24 -0
  144. package/dist/tools/result-cap.js +44 -0
  145. package/dist/tools/result-cap.js.map +1 -0
  146. package/dist/tools/routine.d.ts +3 -3
  147. package/dist/tools/shell.d.ts +14 -1
  148. package/dist/tools/shell.js +86 -4
  149. package/dist/tools/shell.js.map +1 -1
  150. package/dist/tools/specialist-run.d.ts +5 -3
  151. package/dist/tools/specialist-run.js +115 -24
  152. package/dist/tools/specialist-run.js.map +1 -1
  153. package/dist/tools/specialist.d.ts +83 -3
  154. package/dist/tools/specialist.js +83 -3
  155. package/dist/tools/specialist.js.map +1 -1
  156. package/dist/tools/subagent.js +32 -14
  157. package/dist/tools/subagent.js.map +1 -1
  158. package/dist/tools/task.d.ts +5 -5
  159. package/dist/tools/task.js +9 -42
  160. package/dist/tools/task.js.map +1 -1
  161. package/dist/tools/think.d.ts +18 -0
  162. package/dist/tools/think.js +25 -0
  163. package/dist/tools/think.js.map +1 -0
  164. package/dist/tools/tool-wrapper-run.d.ts +121 -0
  165. package/dist/tools/tool-wrapper-run.js +382 -0
  166. package/dist/tools/tool-wrapper-run.js.map +1 -0
  167. package/dist/tools/types.d.ts +28 -2
  168. package/dist/tools/web-search.d.ts +31 -0
  169. package/dist/tools/web-search.js +172 -0
  170. package/dist/tools/web-search.js.map +1 -0
  171. package/dist/tools/wrap-with-specialist.d.ts +55 -0
  172. package/dist/tools/wrap-with-specialist.js +137 -0
  173. package/dist/tools/wrap-with-specialist.js.map +1 -0
  174. package/package.json +2 -2
package/dist/repl.js CHANGED
@@ -43,10 +43,15 @@ const os = __importStar(require("node:os"));
43
43
  const paths_js_1 = require("./paths.js");
44
44
  const agent_js_1 = require("./agent.js");
45
45
  const memory_js_1 = require("./memory.js");
46
+ const reference_resolver_js_1 = require("./reference-resolver.js");
47
+ const reference_tool_lookup_js_1 = require("./reference-tool-lookup.js");
48
+ const web_js_1 = require("./tools/web.js");
49
+ const web_search_js_1 = require("./tools/web-search.js");
46
50
  const rag_js_1 = require("./rag.js");
47
51
  const mcp_js_1 = require("./mcp.js");
48
52
  const output_js_1 = require("./output.js");
49
53
  const config_js_1 = require("./config.js");
54
+ const custom_providers_js_1 = require("./custom-providers.js");
50
55
  const theme_js_1 = require("./theme.js");
51
56
  const update_js_1 = require("./update.js");
52
57
  const store_js_1 = require("./cron/store.js");
@@ -54,49 +59,22 @@ const client_js_1 = require("./cron/client.js");
54
59
  const history_js_1 = require("./history.js");
55
60
  const ai_1 = require("ai");
56
61
  const index_js_1 = require("./providers/index.js");
62
+ const prompt_rewriter_js_1 = require("./prompt-rewriter.js");
57
63
  const context_js_1 = require("./context.js");
58
64
  const domains_js_1 = require("./domains.js");
59
65
  const routines_js_1 = require("./routines.js");
60
66
  const specialists_js_1 = require("./specialists.js");
67
+ const correction_js_1 = require("./correction.js");
61
68
  const specialist_candidates_js_1 = require("./specialist-candidates.js");
69
+ const candidate_bootstrap_js_1 = require("./candidate-bootstrap.js");
62
70
  const specialist_detector_js_1 = require("./specialist-detector.js");
63
71
  const task_js_1 = require("./tools/task.js");
64
72
  const index_js_2 = require("./tools/index.js");
65
73
  const output_js_2 = require("./output.js");
66
74
  const memory_context_js_1 = require("./memory-context.js");
67
75
  const logger_js_1 = require("./logger.js");
68
- /** Promote a pending candidate to a full specialist, updating status and logging. */
69
- function promoteCandidate(candidate, specialistStore, candidateStore, threshold) {
70
- specialistStore.create(candidate.draftId, candidate.name, candidate.description, candidate.systemPrompt, candidate.guidelines);
71
- candidateStore.updateStatus(candidate.id, 'accepted');
72
- (0, logger_js_1.debugLog)('repl:auto-create', {
73
- candidate: candidate.name,
74
- confidence: candidate.confidence,
75
- threshold,
76
- });
77
- (0, output_js_1.printInfo)(`Specialist auto-created: "${candidate.name}" (confidence: ${Math.round(candidate.confidence * 100)}%). Use /specialists to view.`);
78
- }
79
- /** Re-evaluate all pending candidates and auto-create those meeting the threshold. */
80
- function promotePendingCandidates(candidateStore, specialistStore, threshold) {
81
- const pending = candidateStore.listPending();
82
- for (const c of pending) {
83
- if (c.confidence >= threshold) {
84
- try {
85
- promoteCandidate(c, specialistStore, candidateStore, threshold);
86
- }
87
- catch (error) {
88
- const errorMessage = error instanceof Error ? error.message : String(error);
89
- (0, logger_js_1.debugLog)('repl:auto-create', {
90
- action: 're-evaluate-failed',
91
- candidate: c.name,
92
- confidence: c.confidence,
93
- error: errorMessage,
94
- });
95
- (0, output_js_2.printWarning)(`Failed to auto-create specialist "${c.name}": ${errorMessage}`);
96
- }
97
- }
98
- }
99
- }
76
+ const image_js_1 = require("./image.js");
77
+ const menu_js_1 = require("./menu.js");
100
78
  /**
101
79
  * Launch the interactive REPL, wiring up readline, MCP servers, memory stores, and the agent loop.
102
80
  * @param config - Resolved runtime configuration (provider, model, tokens, etc.).
@@ -104,6 +82,7 @@ function promotePendingCandidates(candidateStore, specialistStore, threshold) {
104
82
  * @param resume - When true, reload the previous conversation from disk and continue it.
105
83
  */
106
84
  async function startRepl(config, alertContext, resume) {
85
+ (0, output_js_2.setToolDetailsVisible)(config.toolDetails);
107
86
  const SLASH_COMMANDS = [
108
87
  { command: '/help', description: 'Show this help' },
109
88
  { command: '/clear', description: 'Clear conversation (--save/-s to summarize first)' },
@@ -129,9 +108,11 @@ async function startRepl(config, alertContext, resume) {
129
108
  { command: '/specialists', description: 'List specialist agents' },
130
109
  { command: '/create-specialist', description: 'Create a specialist with guided AI assistance' },
131
110
  { command: '/candidates', description: 'Review specialist suggestions' },
132
- { command: '/critic', description: 'Toggle critic mode for response verification' },
133
- { command: '/agent-options', description: 'Configure auto-creation for specialist agents' },
134
- { command: '/debug', description: 'Print diagnostic report for troubleshooting' },
111
+ {
112
+ command: '/agent-options',
113
+ description: 'Configure agent behavior (toggles, thresholds, saved assets)',
114
+ },
115
+ { command: '/image', description: 'Attach an image: /image <path> [prompt]' },
135
116
  { command: '/exit', description: 'Quit Bernard' },
136
117
  ];
137
118
  const routineStore = new routines_js_1.RoutineStore();
@@ -171,7 +152,8 @@ async function startRepl(config, alertContext, resume) {
171
152
  function getPromptStr() {
172
153
  const { ansi } = (0, theme_js_1.getTheme)();
173
154
  const criticLabel = config.criticMode ? `${ansi.warning}\u25C6${ansi.reset} ` : '';
174
- return `${criticLabel}${ansi.prompt}bernard>${ansi.reset} `;
155
+ const reactLabel = config.reactMode ? `${ansi.prompt}\u25B7${ansi.reset} ` : '';
156
+ return `${criticLabel}${reactLabel}${ansi.prompt}bernard>${ansi.reset} `;
175
157
  }
176
158
  if (process.stdin.isTTY) {
177
159
  process.stdout.write('\x1b[?2004h'); // enable bracket paste mode
@@ -208,9 +190,382 @@ async function startRepl(config, alertContext, resume) {
208
190
  let processing = false;
209
191
  let interrupted = false;
210
192
  let taskAbortController = null;
193
+ let menuAbortController = null;
194
+ function createMenuSignal() {
195
+ menuAbortController = new AbortController();
196
+ return menuAbortController.signal;
197
+ }
198
+ function clearMenuSignal() {
199
+ menuAbortController = null;
200
+ }
201
+ async function toggleBooleanPref(key, label, onMsg, offMsg, onToggle) {
202
+ const entries = [
203
+ { label: 'On', active: config[key] === true },
204
+ { label: 'Off', active: config[key] === false },
205
+ ];
206
+ const signal = createMenuSignal();
207
+ try {
208
+ const result = await (0, menu_js_1.selectFromMenu)(rl, entries, { title: `${label}: ${config[key] ? 'ON' : 'OFF'}` }, signal);
209
+ if (!result.cancelled) {
210
+ config[key] = result.index === 0;
211
+ (0, config_js_1.savePreferences)({
212
+ ...(0, config_js_1.loadPreferences)(),
213
+ provider: config.provider,
214
+ model: config.model,
215
+ [key]: config[key],
216
+ });
217
+ onToggle?.(config[key]);
218
+ (0, output_js_1.printInfo)(config[key] ? onMsg : offMsg);
219
+ }
220
+ }
221
+ finally {
222
+ clearMenuSignal();
223
+ }
224
+ console.log();
225
+ }
226
+ function printSpecialistsList() {
227
+ const specialists = specialistStore.list();
228
+ if (specialists.length === 0) {
229
+ (0, output_js_1.printInfo)('No specialist agents defined yet. Ask me to create one or use /create-specialist.');
230
+ return;
231
+ }
232
+ const builtinIds = (0, specialists_js_1.getBuiltinSpecialistIds)();
233
+ const bundled = specialists.filter((s) => builtinIds.has(s.id));
234
+ const user = specialists.filter((s) => !builtinIds.has(s.id));
235
+ const t = (0, theme_js_1.getTheme)();
236
+ (0, output_js_1.printInfo)(`\n Specialists (${specialists.length}):`);
237
+ if (bundled.length > 0) {
238
+ console.log(t.muted('\n Bundled:'));
239
+ for (const s of bundled) {
240
+ (0, output_js_1.printInfo)(` ${s.id} — ${s.name}: ${s.description}`);
241
+ }
242
+ }
243
+ if (user.length > 0) {
244
+ console.log(t.muted('\n Yours:'));
245
+ for (const s of user) {
246
+ (0, output_js_1.printInfo)(` ${s.id} — ${s.name}: ${s.description}`);
247
+ }
248
+ }
249
+ console.log();
250
+ }
251
+ function printRoutinesList(kind) {
252
+ const all = routineStore.list();
253
+ const match = all.filter((r) => kind === 'tasks' ? r.id.startsWith('task-') : !r.id.startsWith('task-'));
254
+ if (match.length === 0) {
255
+ if (kind === 'tasks') {
256
+ (0, output_js_1.printInfo)('No tasks saved. Use /create-task to define one.');
257
+ }
258
+ else {
259
+ (0, output_js_1.printInfo)('No routines saved. Teach me a workflow and I can save it as a routine.');
260
+ }
261
+ return;
262
+ }
263
+ const heading = kind === 'tasks'
264
+ ? `\n Tasks (${match.length}) — single-step, structured output:`
265
+ : `\n Routines (${match.length}) — multi-step workflows:`;
266
+ (0, output_js_1.printInfo)(heading);
267
+ const t = (0, theme_js_1.getTheme)();
268
+ for (const r of match) {
269
+ console.log(` ${t.accent(`/${r.id}`)} ${t.muted(`— ${r.name}: ${r.description}`)}`);
270
+ }
271
+ console.log();
272
+ }
273
+ function printDebugReport() {
274
+ const t = (0, theme_js_1.getTheme)();
275
+ console.log(t.accent('\n Bernard Diagnostic Report'));
276
+ console.log(t.accent(' ' + '─'.repeat(40)));
277
+ console.log(t.text('\n Runtime:'));
278
+ console.log(t.muted(` Bernard version: ${(0, update_js_1.getLocalVersion)()}`));
279
+ console.log(t.muted(` Node.js version: ${process.version}`));
280
+ console.log(t.muted(` OS: ${process.platform} ${process.arch} (${os.release()})`));
281
+ console.log(t.text('\n LLM:'));
282
+ console.log(t.muted(` Provider: ${config.provider}`));
283
+ console.log(t.muted(` Model: ${config.model}`));
284
+ console.log(t.muted(` maxTokens: ${config.maxTokens}`));
285
+ console.log(t.muted(` shellTimeout: ${config.shellTimeout}ms`));
286
+ console.log(t.muted(` tokenWindow: ${config.tokenWindow || 'auto-detect'}`));
287
+ console.log(t.text('\n API Keys:'));
288
+ for (const { provider, hasKey } of (0, config_js_1.getProviderKeyStatus)()) {
289
+ console.log(t.muted(` ${provider}: ${hasKey ? 'configured' : 'not set'}`));
290
+ }
291
+ const debugStatuses = mcpManager.getServerStatuses();
292
+ console.log(t.text('\n MCP Servers:'));
293
+ if (debugStatuses.length === 0) {
294
+ console.log(t.muted(' (none configured)'));
295
+ }
296
+ else {
297
+ for (const s of debugStatuses) {
298
+ if (s.connected) {
299
+ console.log(t.muted(` ${s.name}: connected (${s.toolCount} tools)`));
300
+ }
301
+ else {
302
+ console.log(t.muted(` ${s.name}: failed — ${s.error}`));
303
+ }
304
+ }
305
+ }
306
+ console.log(t.text('\n RAG:'));
307
+ console.log(t.muted(` Enabled: ${config.ragEnabled}`));
308
+ if (ragStore) {
309
+ console.log(t.muted(` Facts: ${ragStore.count()}`));
310
+ }
311
+ console.log(t.text('\n Memory:'));
312
+ console.log(t.muted(` Persistent memories: ${memoryStore.listMemory().length}`));
313
+ console.log(t.text('\n Cron:'));
314
+ console.log(t.muted(` Daemon: ${(0, client_js_1.isDaemonRunning)() ? 'running' : 'stopped'}`));
315
+ let debugJobCount = 0;
316
+ try {
317
+ const raw = fs.readFileSync(paths_js_1.CRON_JOBS_FILE, 'utf-8');
318
+ debugJobCount = JSON.parse(raw).length;
319
+ }
320
+ catch {
321
+ // jobs.json doesn't exist yet — that's fine
322
+ }
323
+ console.log(t.muted(` Jobs: ${debugJobCount}`));
324
+ console.log(t.text('\n Conversation:'));
325
+ console.log(t.muted(` Messages: ${agent.getHistory().length}`));
326
+ console.log(t.text('\n Settings:'));
327
+ console.log(t.muted(` Theme: ${(0, theme_js_1.getActiveThemeKey)()}`));
328
+ console.log(t.muted(` Critic mode: ${config.criticMode ? 'on' : 'off'}`));
329
+ console.log(t.muted(` Coordinator mode: ${config.reactMode ? 'on' : 'off'}`));
330
+ console.log(t.muted(` Tool details: ${config.toolDetails ? 'on' : 'off'}`));
331
+ console.log(t.muted(` Prompt rewriter: ${config.promptRewriter ? 'on' : 'off'}`));
332
+ const debugEnabled = process.env.BERNARD_DEBUG === 'true' || process.env.BERNARD_DEBUG === '1';
333
+ console.log(t.muted(` Debug mode: ${debugEnabled ? 'on' : 'off'}`));
334
+ console.log(t.text('\n Paths:'));
335
+ if (process.env.BERNARD_HOME) {
336
+ console.log(t.muted(` BERNARD_HOME: ${process.env.BERNARD_HOME}`));
337
+ }
338
+ console.log(t.muted(` Config: ${paths_js_1.CONFIG_DIR}`));
339
+ console.log(t.muted(` Data: ${paths_js_1.DATA_DIR}`));
340
+ console.log(t.muted(` Cache: ${paths_js_1.CACHE_DIR}`));
341
+ console.log(t.muted(` State: ${paths_js_1.STATE_DIR}`));
342
+ console.log();
343
+ }
344
+ async function promptDisambiguation(reference, candidates) {
345
+ const entries = [];
346
+ for (const c of candidates) {
347
+ entries.push({ label: c.label, description: c.preview });
348
+ }
349
+ entries.push({ label: 'Pass as-is (do not resolve)' });
350
+ for (const c of candidates) {
351
+ entries.push({ label: `Remember: "${reference}" → ${c.label}` });
352
+ }
353
+ const signal = createMenuSignal();
354
+ try {
355
+ const result = await (0, menu_js_1.selectFromMenu)(rl, entries, { title: `Ambiguous reference: "${reference}"`, promptLabel: 'Resolve to' }, signal);
356
+ if (result.cancelled)
357
+ return { cancelled: true };
358
+ const idx = result.index;
359
+ if (idx < candidates.length) {
360
+ const chosen = candidates[idx];
361
+ return {
362
+ cancelled: false,
363
+ passAsIs: false,
364
+ entry: {
365
+ phrase: reference,
366
+ resolvedTo: chosen.label,
367
+ sourceKey: chosen.sourceKey,
368
+ },
369
+ remember: false,
370
+ };
371
+ }
372
+ if (idx === candidates.length) {
373
+ return { cancelled: false, passAsIs: true };
374
+ }
375
+ const rememberIdx = idx - candidates.length - 1;
376
+ const chosen = candidates[rememberIdx];
377
+ return {
378
+ cancelled: false,
379
+ passAsIs: false,
380
+ entry: {
381
+ phrase: reference,
382
+ resolvedTo: chosen.label,
383
+ sourceKey: chosen.sourceKey,
384
+ },
385
+ remember: true,
386
+ };
387
+ }
388
+ finally {
389
+ clearMenuSignal();
390
+ }
391
+ }
392
+ function persistReference(reference, value, store) {
393
+ const baseKey = (0, reference_resolver_js_1.deriveKeyFromReference)(reference) || 'entity';
394
+ const existing = new Set(store.listMemory());
395
+ let key = baseKey;
396
+ let suffix = 2;
397
+ while (existing.has(key)) {
398
+ key = `${baseKey}-${suffix++}`;
399
+ }
400
+ store.writeMemory(key, value);
401
+ (0, output_js_1.printInfo)(` Saved as memory: ${key}`);
402
+ return { phrase: reference, resolvedTo: value, sourceKey: key };
403
+ }
404
+ async function promptUnknownReference(reference, store) {
405
+ (0, output_js_1.printInfo)(`\n I don't have memory for "${reference}". Tell me about them and I'll remember.\n (Enter or Esc skips — the agent will run without this resolved.)`);
406
+ console.log();
407
+ const signal = createMenuSignal();
408
+ try {
409
+ const result = await (0, menu_js_1.promptValue)(rl, { label: `"${reference}" is` }, signal);
410
+ if (result.cancelled)
411
+ return { entry: null };
412
+ if (!result.raw.trim())
413
+ return { entry: null };
414
+ return { entry: persistReference(reference, result.raw, store) };
415
+ }
416
+ finally {
417
+ clearMenuSignal();
418
+ }
419
+ }
420
+ // User confirmation is mandatory — the resolver lookup is a hint, not a fact,
421
+ // and may have matched the wrong person.
422
+ async function promptToolLookupConfirmation(reference, resolvedTo, toolName, store) {
423
+ (0, output_js_1.printInfo)(`\n Lookup via ${toolName} found a possible match for "${reference}":\n → ${resolvedTo}`);
424
+ console.log();
425
+ const SAVE = 0;
426
+ const EDIT = 1;
427
+ const SKIP = 2;
428
+ const entries = [
429
+ { label: 'Save as memory', description: resolvedTo },
430
+ { label: 'Edit before saving' },
431
+ { label: 'Skip (do not resolve)' },
432
+ ];
433
+ const signal = createMenuSignal();
434
+ try {
435
+ const result = await (0, menu_js_1.selectFromMenu)(rl, entries, { title: `Save lookup result for "${reference}"?`, promptLabel: 'Choose' }, signal);
436
+ if (result.cancelled || result.index === SKIP)
437
+ return { entry: null };
438
+ let value = resolvedTo;
439
+ if (result.index === EDIT) {
440
+ const edited = await (0, menu_js_1.promptValue)(rl, { label: `"${reference}" is` }, signal);
441
+ if (edited.cancelled || !edited.raw.trim())
442
+ return { entry: null };
443
+ value = edited.raw;
444
+ }
445
+ else if (result.index !== SAVE) {
446
+ return { entry: null };
447
+ }
448
+ return { entry: persistReference(reference, value, store) };
449
+ }
450
+ finally {
451
+ clearMenuSignal();
452
+ }
453
+ }
454
+ async function runReferenceResolver(trimmed) {
455
+ // Strip image-attachment paths and tool-resolvable tokens (URLs, PR/issue refs, file
456
+ // paths, commit hashes) so the resolver's LLM doesn't mistake them for unresolved
457
+ // entities. The main agent fetches those directly via shell/gh/web_read.
458
+ const resolverInput = (0, reference_resolver_js_1.stripToolResolvableTokens)((0, image_js_1.stripImagePaths)(trimmed));
459
+ if ((0, reference_resolver_js_1.shouldSkipResolver)(resolverInput) || resolverInput.length === 0)
460
+ return [];
461
+ try {
462
+ const hints = (0, memory_js_1.loadRewriterHints)(memoryStore);
463
+ const resolveSignal = createMenuSignal();
464
+ let resolveResult;
465
+ (0, output_js_1.startSpinner)();
466
+ try {
467
+ resolveResult = await (0, reference_resolver_js_1.resolveReferences)(resolverInput, memoryStore, config, hints, resolveSignal, ragStore, agent.getHistory());
468
+ }
469
+ finally {
470
+ (0, output_js_1.stopSpinner)();
471
+ clearMenuSignal();
472
+ }
473
+ let entries = [];
474
+ if (resolveResult.status === 'resolved') {
475
+ entries = resolveResult.entries;
476
+ }
477
+ else if (resolveResult.status === 'ambiguous') {
478
+ const outcome = await promptDisambiguation(resolveResult.reference, resolveResult.candidates);
479
+ // Esc/Enter is treated as "pass as-is" — the agent still runs with the original
480
+ // prompt, consistent with the unknown-reference skip behavior.
481
+ if (!outcome.cancelled && !outcome.passAsIs) {
482
+ entries = [outcome.entry];
483
+ if (outcome.remember) {
484
+ (0, memory_js_1.saveRewriterHint)(memoryStore, outcome.entry.phrase, outcome.entry.sourceKey);
485
+ (0, output_js_1.printInfo)(` Remembered: "${outcome.entry.phrase}" → ${outcome.entry.sourceKey}`);
486
+ }
487
+ }
488
+ }
489
+ else if (resolveResult.status === 'unknown') {
490
+ // Try a read-only tool lookup before falling back to asking the user.
491
+ // Fail-open at every layer: any error or `none` drops through to the
492
+ // existing free-form prompt.
493
+ let lookupHandled = false;
494
+ if (config.referenceLookup) {
495
+ const lookupSignal = createMenuSignal();
496
+ (0, output_js_1.startSpinner)();
497
+ let lookup;
498
+ try {
499
+ lookup = await (0, reference_tool_lookup_js_1.runReferenceLookup)(resolveResult.reference, lookupTools, config, lookupSignal);
500
+ }
501
+ finally {
502
+ (0, output_js_1.stopSpinner)();
503
+ clearMenuSignal();
504
+ }
505
+ if (lookup.status === 'found') {
506
+ const outcome = await promptToolLookupConfirmation(resolveResult.reference, lookup.resolvedTo, lookup.toolName, memoryStore);
507
+ if (outcome.entry)
508
+ entries = [outcome.entry];
509
+ lookupHandled = true;
510
+ }
511
+ }
512
+ if (!lookupHandled) {
513
+ const outcome = await promptUnknownReference(resolveResult.reference, memoryStore);
514
+ if (outcome.entry)
515
+ entries = [outcome.entry];
516
+ }
517
+ }
518
+ if (entries.length > 0) {
519
+ (0, logger_js_1.debugLog)('repl:resolved-references', {
520
+ prompt: trimmed,
521
+ entries,
522
+ injectedBlock: (0, reference_resolver_js_1.renderResolvedBlock)(entries),
523
+ });
524
+ }
525
+ return entries;
526
+ }
527
+ catch (err) {
528
+ (0, logger_js_1.debugLog)('repl:resolve-references', err instanceof Error ? err.message : String(err));
529
+ return [];
530
+ }
531
+ }
532
+ async function runPromptRewriter(trimmed, resolvedEntries) {
533
+ if (!config.promptRewriter)
534
+ return null;
535
+ try {
536
+ const profile = (0, index_js_1.getModelProfile)(config.provider, config.model, config.customProviders?.[config.provider]?.sdk);
537
+ const rewriteSignal = createMenuSignal();
538
+ let result;
539
+ (0, output_js_1.startSpinner)();
540
+ try {
541
+ result = await (0, prompt_rewriter_js_1.rewritePrompt)(trimmed, profile, resolvedEntries, config, rewriteSignal);
542
+ }
543
+ finally {
544
+ (0, output_js_1.stopSpinner)();
545
+ clearMenuSignal();
546
+ }
547
+ if (result.status === 'rewritten') {
548
+ (0, logger_js_1.debugLog)('repl:prompt-rewritten', {
549
+ original: trimmed,
550
+ rewritten: result.text,
551
+ family: profile.family,
552
+ });
553
+ return result.text;
554
+ }
555
+ return null;
556
+ }
557
+ catch (err) {
558
+ (0, logger_js_1.debugLog)('repl:prompt-rewriter', err instanceof Error ? err.message : String(err));
559
+ return null;
560
+ }
561
+ }
211
562
  process.stdin.on('keypress', (_str, key) => {
212
563
  if (!key)
213
564
  return;
565
+ if (key.name === 'escape' && menuAbortController) {
566
+ menuAbortController.abort();
567
+ return;
568
+ }
214
569
  if (key.name === 'escape' && processing) {
215
570
  if (taskAbortController) {
216
571
  taskAbortController.abort();
@@ -299,17 +654,80 @@ async function startRepl(config, alertContext, resume) {
299
654
  }
300
655
  const mcpTools = mcpManager.getTools();
301
656
  const mcpServerNames = mcpManager.getConnectedServerNames();
302
- const confirmFn = (command) => {
303
- return new Promise((resolve) => {
304
- const { ansi } = (0, theme_js_1.getTheme)();
305
- rl.question(`${ansi.warning} ⚠ Dangerous command: ${command}\n Allow? (y/N): ${ansi.reset}`, (answer) => {
306
- resolve(answer.trim().toLowerCase() === 'y');
307
- });
308
- });
657
+ // Tool registry exposed to the resolver's pre-fallback lookup pass. Limited
658
+ // to read-only network tools + MCP tools so a slow or write-side tool can't
659
+ // be selected. The lookup module further filters via `isAllowedLookupTool`.
660
+ const lookupTools = {
661
+ web_search: (0, web_search_js_1.createWebSearchTool)(),
662
+ web_read: (0, web_js_1.createWebReadTool)(),
663
+ ...mcpTools,
664
+ };
665
+ // The thinking-spinner repaints stdout every 80 ms and would blank any
666
+ // interactive prompt, so callers that need to talk to the user must run
667
+ // through this helper to stop the spinner first and restart it after.
668
+ const withPausedSpinner = async (signal, fn) => {
669
+ (0, output_js_1.stopSpinner)();
670
+ try {
671
+ return await fn();
672
+ }
673
+ finally {
674
+ // Skip restart when the turn is over or aborted — nothing left to think about.
675
+ if (processing && !signal?.aborted) {
676
+ resumeSpinner();
677
+ }
678
+ }
679
+ };
680
+ const confirmFn = (command, signal) => withPausedSpinner(signal, async () => {
681
+ const result = await (0, menu_js_1.selectFromMenu)(rl, [{ label: 'Allow once' }, { label: 'Cancel' }], { title: `⚠ Dangerous command: ${command}` }, signal);
682
+ return !result.cancelled && result.index === 0;
683
+ });
684
+ const renderTabStrip = (currentIndex, total) => {
685
+ const theme = (0, theme_js_1.getTheme)();
686
+ const tabs = [];
687
+ for (let i = 0; i < total; i++) {
688
+ const num = String(i + 1);
689
+ if (i < currentIndex)
690
+ tabs.push(theme.success(`${num} ✓`));
691
+ else if (i === currentIndex)
692
+ tabs.push(theme.accentBold(`▸${num}◂`));
693
+ else
694
+ tabs.push(theme.dim(num));
695
+ }
696
+ return ' ' + tabs.join(' ');
309
697
  };
698
+ const askSingleQuestion = async (q, headerLines, signal) => {
699
+ if (!q.choices || q.choices.length === 0) {
700
+ const r = await (0, menu_js_1.promptValue)(rl, { label: q.question, headerLines }, signal);
701
+ return r.cancelled ? null : r.raw;
702
+ }
703
+ const entries = q.choices.map((label) => ({ label }));
704
+ if (q.allowOther)
705
+ entries.push({ label: q.otherLabel ?? 'Other (type a custom answer)' });
706
+ const r = await (0, menu_js_1.selectFromMenu)(rl, entries, { title: q.question, headerLines }, signal);
707
+ if (r.cancelled)
708
+ return null;
709
+ if (q.allowOther && r.index === q.choices.length) {
710
+ const free = await (0, menu_js_1.promptValue)(rl, { label: q.question, headerLines }, signal);
711
+ return free.cancelled ? null : free.raw;
712
+ }
713
+ return q.choices[r.index];
714
+ };
715
+ const askUserFn = (questions, signal) => withPausedSpinner(signal, async () => {
716
+ const answered = [];
717
+ const showTabs = questions.length > 1;
718
+ for (let i = 0; i < questions.length; i++) {
719
+ const headerLines = showTabs ? [renderTabStrip(i, questions.length)] : undefined;
720
+ const answer = await askSingleQuestion(questions[i], headerLines, signal);
721
+ if (answer === null)
722
+ return { cancelled: true, answered };
723
+ answered.push(answer);
724
+ }
725
+ return { answers: answered };
726
+ });
310
727
  const toolOptions = {
311
728
  shellTimeout: config.shellTimeout,
312
729
  confirmDangerous: confirmFn,
730
+ askUser: askUserFn,
313
731
  };
314
732
  const historyStore = new history_js_1.HistoryStore();
315
733
  let initialHistory;
@@ -336,14 +754,10 @@ async function startRepl(config, alertContext, resume) {
336
754
  (0, output_js_1.printInfo)('No previous conversation found — starting fresh.');
337
755
  }
338
756
  }
339
- // Surface pending specialist candidates at session start
340
- candidateStore.pruneOld();
341
- candidateStore.reconcileSaved(specialistStore.list());
342
- const pendingCandidates = candidateStore.listPending();
343
- if (pendingCandidates.length > 0) {
757
+ const { pending: pendingCandidates, contextBlock } = (0, candidate_bootstrap_js_1.bootstrapPendingCandidates)(candidateStore, specialistStore, config);
758
+ if (contextBlock) {
344
759
  (0, output_js_1.printInfo)(` ${pendingCandidates.length} specialist suggestion(s) pending. Use /candidates to review.`);
345
- const candidateContext = `## Specialist Suggestions\n\nBernard detected patterns in previous sessions that might benefit from saved specialists. Mention these when relevant.\n\n${pendingCandidates.map((c) => `- "${c.name}" (${c.draftId}): ${c.description}`).join('\n')}`;
346
- alertContext = alertContext ? alertContext + '\n\n' + candidateContext : candidateContext;
760
+ alertContext = alertContext ? alertContext + '\n\n' + contextBlock : contextBlock;
347
761
  }
348
762
  const agent = new agent_js_1.Agent(config, toolOptions, memoryStore, mcpTools, mcpServerNames, alertContext, initialHistory, ragStore, routineStore, specialistStore, candidateStore);
349
763
  let cleanedUp = false;
@@ -380,22 +794,66 @@ async function startRepl(config, alertContext, resume) {
380
794
  catch {
381
795
  // Silent failure — don't block exit
382
796
  }
797
+ // Run the correction agent over any tool-wrapper failures queued this
798
+ // session. Best-effort; never block shutdown on errors.
799
+ if (config.correctionEnabled) {
800
+ try {
801
+ const correctionStore = agent.getCorrectionStore();
802
+ const pending = correctionStore.listPending();
803
+ if (pending.length > 0) {
804
+ (0, output_js_1.printInfo)(`Reviewing ${pending.length} tool-wrapper failure(s) for learning...`);
805
+ const result = await (0, correction_js_1.runCorrectionAgent)({
806
+ config,
807
+ toolOptions,
808
+ memoryStore,
809
+ specialistStore: agent.getSpecialistStore(),
810
+ correctionStore,
811
+ ragStore,
812
+ routineStore,
813
+ candidateStore,
814
+ mcpTools,
815
+ }, pending);
816
+ if (result.applied > 0) {
817
+ (0, output_js_1.printInfo)(` Learned from ${result.applied}/${result.processed} failure(s); examples updated.`);
818
+ }
819
+ }
820
+ }
821
+ catch (err) {
822
+ (0, logger_js_1.debugLog)('correction:error', err instanceof Error ? err.message : String(err));
823
+ }
824
+ }
383
825
  await mcpManager.close();
384
826
  };
827
+ // Tracks the SpinnerStats for the in-flight turn so resumeSpinner() can
828
+ // re-attach the spinner after a pause (e.g. an ask_user prompt) without
829
+ // resetting startTime or the running token totals.
830
+ let activeSpinnerStats = null;
831
+ function initSpinner() {
832
+ const spinnerStats = {
833
+ startTime: Date.now(),
834
+ totalPromptTokens: 0,
835
+ totalCompletionTokens: 0,
836
+ latestPromptTokens: 0,
837
+ model: config.model,
838
+ contextWindowOverride: config.tokenWindow || undefined,
839
+ };
840
+ activeSpinnerStats = spinnerStats;
841
+ agent.setSpinnerStats(spinnerStats);
842
+ (0, output_js_1.startSpinner)(() => (0, output_js_1.buildSpinnerMessage)(spinnerStats));
843
+ }
844
+ function resumeSpinner() {
845
+ const stats = activeSpinnerStats;
846
+ if (!stats) {
847
+ initSpinner();
848
+ return;
849
+ }
850
+ (0, output_js_1.startSpinner)(() => (0, output_js_1.buildSpinnerMessage)(stats));
851
+ }
385
852
  async function runGuidedCreation(message) {
386
853
  processing = true;
387
854
  interrupted = false;
388
855
  try {
389
- const spinnerStats = {
390
- startTime: Date.now(),
391
- totalPromptTokens: 0,
392
- totalCompletionTokens: 0,
393
- latestPromptTokens: 0,
394
- model: config.model,
395
- contextWindowOverride: config.tokenWindow || undefined,
396
- };
397
- agent.setSpinnerStats(spinnerStats);
398
- (0, output_js_1.startSpinner)(() => (0, output_js_1.buildSpinnerMessage)(spinnerStats));
856
+ initSpinner();
399
857
  await agent.processInput(message);
400
858
  historyStore.save(agent.getHistory());
401
859
  }
@@ -451,7 +909,8 @@ async function startRepl(config, alertContext, resume) {
451
909
  }
452
910
  const taskMaxSteps = (0, task_js_1.getTaskMaxSteps)(config);
453
911
  const result = await (0, ai_1.generateText)({
454
- model: (0, index_js_1.getModel)(config.provider, config.model),
912
+ model: (0, index_js_1.getModelForConfig)(config, config.provider, config.model),
913
+ providerOptions: (0, index_js_1.getProviderOptionsForConfig)(config, config.provider),
455
914
  tools: baseTools,
456
915
  maxSteps: taskMaxSteps,
457
916
  maxTokens: config.maxTokens,
@@ -559,7 +1018,8 @@ async function startRepl(config, alertContext, resume) {
559
1018
  const serialized = (0, context_js_1.serializeMessages)(history);
560
1019
  const [summaryResult, domainFacts, candidateResult] = await Promise.all([
561
1020
  (0, ai_1.generateText)({
562
- model: (0, index_js_1.getModel)(config.provider, config.model),
1021
+ model: (0, index_js_1.getModelForConfig)(config, config.provider, config.model),
1022
+ providerOptions: (0, index_js_1.getProviderOptionsForConfig)(config, config.provider),
563
1023
  maxTokens: 2048,
564
1024
  system: context_js_1.SUMMARIZATION_PROMPT,
565
1025
  messages: [
@@ -599,7 +1059,7 @@ async function startRepl(config, alertContext, resume) {
599
1059
  const created = candidateStore.create(candidateResult.candidate, 'clear-save');
600
1060
  if (config.autoCreateSpecialists &&
601
1061
  candidateResult.candidate.confidence >= config.autoCreateThreshold) {
602
- promoteCandidate({ ...candidateResult.candidate, id: created.id }, specialistStore, candidateStore, config.autoCreateThreshold);
1062
+ (0, candidate_bootstrap_js_1.promoteCandidate)({ ...candidateResult.candidate, id: created.id }, specialistStore, candidateStore, config.autoCreateThreshold);
603
1063
  }
604
1064
  else {
605
1065
  (0, logger_js_1.debugLog)('repl:auto-create', {
@@ -634,7 +1094,7 @@ async function startRepl(config, alertContext, resume) {
634
1094
  agent.clearHistory();
635
1095
  historyStore.clear();
636
1096
  console.clear();
637
- (0, output_js_1.printWelcome)(config.provider, config.model, (0, update_js_1.getLocalVersion)());
1097
+ (0, output_js_1.printWelcome)(config.provider, config.model, (0, update_js_1.getLocalVersion)(), config.customProviders?.[config.provider]?.baseURL);
638
1098
  (0, output_js_1.printInfo)('Conversation history and scratch notes cleared.');
639
1099
  void prompt();
640
1100
  return;
@@ -820,73 +1280,131 @@ async function startRepl(config, alertContext, resume) {
820
1280
  }
821
1281
  if (trimmed === '/provider') {
822
1282
  const available = (0, config_js_1.getAvailableProviders)(config);
823
- if (available.length === 0) {
824
- (0, output_js_1.printError)('No providers have API keys configured.');
825
- void prompt();
826
- return;
827
- }
828
- (0, output_js_1.printInfo)(`\n Current: ${config.provider} (${config.model})\n`);
829
- (0, output_js_1.printInfo)(' Available providers:');
830
- for (let i = 0; i < available.length; i++) {
831
- (0, output_js_1.printInfo)(` ${i + 1}. ${available[i]}`);
832
- }
833
- console.log();
834
- rl.question(` Select [1-${available.length}]: `, (answer) => {
835
- const num = parseInt(answer.trim(), 10);
836
- if (num >= 1 && num <= available.length) {
837
- config.provider = available[num - 1];
838
- config.model = (0, config_js_1.getDefaultModel)(config.provider);
839
- (0, config_js_1.savePreferences)({
840
- provider: config.provider,
841
- model: config.model,
842
- maxTokens: config.maxTokens,
843
- shellTimeout: config.shellTimeout,
844
- tokenWindow: config.tokenWindow,
845
- theme: config.theme,
1283
+ const customProviders = config.customProviders ?? {};
1284
+ const builtinAvailable = available.filter((p) => !customProviders[p]);
1285
+ const customAvailable = available.filter((p) => customProviders[p]);
1286
+ const entries = [];
1287
+ for (const p of builtinAvailable)
1288
+ entries.push({ label: p, value: p });
1289
+ if (customAvailable.length > 0) {
1290
+ entries.push({ type: 'section', title: 'Custom:' });
1291
+ for (const p of customAvailable) {
1292
+ const entry = customProviders[p];
1293
+ entries.push({
1294
+ label: p,
1295
+ annotation: `(${entry.sdk} ${entry.baseURL})`,
1296
+ value: p,
846
1297
  });
847
- (0, output_js_1.printInfo)(` Switched to ${config.provider} (${config.model})`);
848
1298
  }
849
- else {
850
- (0, output_js_1.printInfo)(' Cancelled.');
1299
+ }
1300
+ if (builtinAvailable.length === 0 && customAvailable.length === 0) {
1301
+ (0, output_js_1.printInfo)('No providers have API keys configured yet — add one below.');
1302
+ }
1303
+ entries.push({ type: 'section', title: '' });
1304
+ entries.push({ label: '+ Add custom provider…', value: '__add__' });
1305
+ const signal = createMenuSignal();
1306
+ try {
1307
+ const result = await (0, menu_js_1.selectFromMenu)(rl, entries, { title: `Providers — current: ${config.provider} (${config.model})` }, signal);
1308
+ if (!result.cancelled) {
1309
+ const value = result.item.value;
1310
+ if (value === '__add__') {
1311
+ const added = await runAddProviderWizard(rl, createMenuSignal, clearMenuSignal);
1312
+ if (added) {
1313
+ config.customProviders = {
1314
+ ...customProviders,
1315
+ [added.entry.name]: added.entry,
1316
+ };
1317
+ config.apiKeys = { ...(config.apiKeys ?? {}), [added.entry.name]: added.apiKey };
1318
+ config.provider = added.entry.name;
1319
+ config.model = added.entry.defaultModel;
1320
+ (0, config_js_1.savePreferences)({
1321
+ provider: config.provider,
1322
+ model: config.model,
1323
+ maxTokens: config.maxTokens,
1324
+ shellTimeout: config.shellTimeout,
1325
+ tokenWindow: config.tokenWindow,
1326
+ theme: config.theme,
1327
+ });
1328
+ (0, output_js_1.printInfo)(` Added and switched to ${added.entry.name} (${added.entry.defaultModel})`);
1329
+ }
1330
+ }
1331
+ else {
1332
+ config.provider = value;
1333
+ config.model = (0, config_js_1.getDefaultModel)(config.provider, customProviders);
1334
+ (0, config_js_1.savePreferences)({
1335
+ provider: config.provider,
1336
+ model: config.model,
1337
+ maxTokens: config.maxTokens,
1338
+ shellTimeout: config.shellTimeout,
1339
+ tokenWindow: config.tokenWindow,
1340
+ theme: config.theme,
1341
+ });
1342
+ (0, output_js_1.printInfo)(` Switched to ${config.provider} (${config.model})`);
1343
+ }
851
1344
  }
852
- console.log();
853
- void prompt();
854
- });
1345
+ }
1346
+ finally {
1347
+ clearMenuSignal();
1348
+ }
1349
+ console.log();
1350
+ void prompt();
855
1351
  return;
856
1352
  }
857
1353
  if (trimmed === '/model') {
858
- const models = config_js_1.PROVIDER_MODELS[config.provider];
1354
+ const customProviders = config.customProviders ?? {};
1355
+ const customEntry = customProviders[config.provider];
1356
+ const models = customEntry ? customEntry.models : config_js_1.PROVIDER_MODELS[config.provider];
859
1357
  if (!models || models.length === 0) {
860
1358
  (0, output_js_1.printError)(`No models listed for provider "${config.provider}".`);
861
1359
  void prompt();
862
1360
  return;
863
1361
  }
864
- (0, output_js_1.printInfo)(`\n Current: ${config.provider} / ${config.model}\n`);
865
- (0, output_js_1.printInfo)(' Available models:');
866
- for (let i = 0; i < models.length; i++) {
867
- (0, output_js_1.printInfo)(` ${i + 1}. ${models[i]}`);
1362
+ const entries = models.map((m) => ({ label: m, value: m }));
1363
+ if (customEntry) {
1364
+ entries.push({ type: 'section', title: '' });
1365
+ entries.push({ label: '+ Type a new model name…', value: '__free__' });
868
1366
  }
869
- console.log();
870
- rl.question(` Select [1-${models.length}]: `, (answer) => {
871
- const num = parseInt(answer.trim(), 10);
872
- if (num >= 1 && num <= models.length) {
873
- config.model = models[num - 1];
874
- (0, config_js_1.savePreferences)({
875
- provider: config.provider,
876
- model: config.model,
877
- maxTokens: config.maxTokens,
878
- shellTimeout: config.shellTimeout,
879
- tokenWindow: config.tokenWindow,
880
- theme: config.theme,
881
- });
882
- (0, output_js_1.printInfo)(` Switched to ${config.model}`);
883
- }
884
- else {
885
- (0, output_js_1.printInfo)(' Cancelled.');
1367
+ const signal = createMenuSignal();
1368
+ try {
1369
+ const result = await (0, menu_js_1.selectFromMenu)(rl, entries, { title: `Models — current: ${config.provider} / ${config.model}` }, signal);
1370
+ if (!result.cancelled) {
1371
+ const value = result.item.value;
1372
+ let chosenModel = null;
1373
+ if (value === '__free__' && customEntry) {
1374
+ const valueResult = await (0, menu_js_1.promptValue)(rl, { label: 'Model name' }, createMenuSignal());
1375
+ clearMenuSignal();
1376
+ if (!valueResult.cancelled) {
1377
+ const newModel = valueResult.raw.trim();
1378
+ if (newModel) {
1379
+ (0, custom_providers_js_1.rememberCustomModel)(config.provider, newModel);
1380
+ // Refresh local copy
1381
+ config.customProviders = (0, custom_providers_js_1.loadCustomProviders)();
1382
+ chosenModel = newModel;
1383
+ }
1384
+ }
1385
+ }
1386
+ else {
1387
+ chosenModel = value;
1388
+ }
1389
+ if (chosenModel) {
1390
+ config.model = chosenModel;
1391
+ (0, config_js_1.savePreferences)({
1392
+ provider: config.provider,
1393
+ model: config.model,
1394
+ maxTokens: config.maxTokens,
1395
+ shellTimeout: config.shellTimeout,
1396
+ tokenWindow: config.tokenWindow,
1397
+ theme: config.theme,
1398
+ });
1399
+ (0, output_js_1.printInfo)(` Switched to ${config.model}`);
1400
+ }
886
1401
  }
887
- console.log();
888
- void prompt();
889
- });
1402
+ }
1403
+ finally {
1404
+ clearMenuSignal();
1405
+ }
1406
+ console.log();
1407
+ void prompt();
890
1408
  return;
891
1409
  }
892
1410
  if (trimmed === '/theme') {
@@ -894,26 +1412,24 @@ async function startRepl(config, alertContext, resume) {
894
1412
  const currentKey = (0, theme_js_1.getActiveThemeKey)();
895
1413
  const regularKeys = allKeys.filter((k) => k !== 'high-contrast' && k !== 'colorblind');
896
1414
  const a11yKeys = allKeys.filter((k) => k === 'high-contrast' || k === 'colorblind');
897
- (0, output_js_1.printInfo)(`\n Current theme: ${theme_js_1.THEMES[currentKey].name}\n`);
898
- (0, output_js_1.printInfo)(' Themes:');
899
- let idx = 1;
900
- for (const k of regularKeys) {
901
- const marker = k === currentKey ? ' (active)' : '';
902
- (0, output_js_1.printInfo)(` ${idx}. ${theme_js_1.THEMES[k].name}${marker}`);
903
- idx++;
904
- }
905
- (0, output_js_1.printInfo)('\n Accessibility:');
906
- for (const k of a11yKeys) {
907
- const marker = k === currentKey ? ' (active)' : '';
908
- (0, output_js_1.printInfo)(` ${idx}. ${theme_js_1.THEMES[k].name}${marker}`);
909
- idx++;
910
- }
911
- console.log();
912
- const ordered = [...regularKeys, ...a11yKeys];
913
- rl.question(` Select [1-${ordered.length}]: `, (answer) => {
914
- const num = parseInt(answer.trim(), 10);
915
- if (num >= 1 && num <= ordered.length) {
916
- const chosen = ordered[num - 1];
1415
+ const entries = [
1416
+ ...regularKeys.map((k) => ({
1417
+ label: theme_js_1.THEMES[k].name,
1418
+ active: k === currentKey,
1419
+ value: k,
1420
+ })),
1421
+ { type: 'section', title: 'Accessibility:' },
1422
+ ...a11yKeys.map((k) => ({
1423
+ label: theme_js_1.THEMES[k].name,
1424
+ active: k === currentKey,
1425
+ value: k,
1426
+ })),
1427
+ ];
1428
+ const signal = createMenuSignal();
1429
+ try {
1430
+ const result = await (0, menu_js_1.selectFromMenu)(rl, entries, { title: `Themes — current: ${theme_js_1.THEMES[currentKey].name}` }, signal);
1431
+ if (!result.cancelled) {
1432
+ const chosen = result.item.value;
917
1433
  (0, theme_js_1.setTheme)(chosen);
918
1434
  config.theme = chosen;
919
1435
  (0, config_js_1.savePreferences)({
@@ -926,60 +1442,74 @@ async function startRepl(config, alertContext, resume) {
926
1442
  });
927
1443
  (0, output_js_1.printInfo)(` Switched to ${theme_js_1.THEMES[chosen].name} theme.`);
928
1444
  }
929
- else {
930
- (0, output_js_1.printInfo)(' Cancelled.');
931
- }
932
- console.log();
933
- void prompt();
934
- });
1445
+ }
1446
+ finally {
1447
+ clearMenuSignal();
1448
+ }
1449
+ console.log();
1450
+ void prompt();
935
1451
  return;
936
1452
  }
937
1453
  if (trimmed === '/options') {
938
- const entries = Object.entries(config_js_1.OPTIONS_REGISTRY);
939
- (0, output_js_1.printInfo)('\n Options:');
940
- for (let i = 0; i < entries.length; i++) {
941
- const [name, opt] = entries[i];
942
- const current = config[opt.configKey];
943
- const isDefault = current === opt.default;
944
- const label = isDefault ? '(default)' : '(custom)';
945
- (0, output_js_1.printInfo)(` ${i + 1}. ${name} = ${current} ${label}`);
946
- (0, output_js_1.printInfo)(` ${opt.description}`);
1454
+ const optEntries = Object.entries(config_js_1.OPTIONS_REGISTRY);
1455
+ const menuEntries = [
1456
+ ...optEntries.map(([name, opt]) => {
1457
+ const current = config[opt.configKey];
1458
+ const tag = current === opt.default ? '(default)' : '(custom)';
1459
+ return {
1460
+ label: name,
1461
+ annotation: `= ${current} ${tag}`,
1462
+ description: opt.description,
1463
+ };
1464
+ }),
1465
+ { type: 'section', title: 'Info' },
1466
+ { label: 'Debug report', description: 'Print a diagnostic report for troubleshooting' },
1467
+ ];
1468
+ const signal1 = createMenuSignal();
1469
+ let optResult;
1470
+ try {
1471
+ optResult = await (0, menu_js_1.selectFromMenu)(rl, menuEntries, { title: 'Options', promptLabel: 'Select option' }, signal1);
947
1472
  }
948
- console.log();
949
- rl.question(` Select option [1-${entries.length}] (Enter to cancel): `, (answer) => {
950
- const num = parseInt(answer.trim(), 10);
951
- if (num >= 1 && num <= entries.length) {
952
- const [name, opt] = entries[num - 1];
953
- rl.question(` New value for ${name} (Enter to cancel): `, (valAnswer) => {
954
- const val = parseInt(valAnswer.trim(), 10);
955
- const minVal = opt.default === 0 ? 0 : 1;
956
- if (!isNaN(val) && val >= minVal) {
957
- (0, config_js_1.saveOption)(name, val);
958
- config[opt.configKey] = val;
959
- (0, output_js_1.printInfo)(` ${name} set to ${val}`);
960
- if (name === 'token-window') {
961
- const modelWindow = (0, context_js_1.getContextWindow)(config.model);
962
- if (val > modelWindow) {
963
- (0, output_js_1.printInfo)(` Warning: ${val} exceeds ${config.model}'s context window (${modelWindow})`);
964
- }
1473
+ finally {
1474
+ clearMenuSignal();
1475
+ }
1476
+ if (!optResult.cancelled) {
1477
+ if (optResult.index >= optEntries.length) {
1478
+ // Debug report is the only non-editable entry beyond the option rows.
1479
+ printDebugReport();
1480
+ void prompt();
1481
+ return;
1482
+ }
1483
+ const [name, opt] = optEntries[optResult.index];
1484
+ const signal2 = createMenuSignal();
1485
+ let valResult;
1486
+ try {
1487
+ valResult = await (0, menu_js_1.promptValue)(rl, { label: `New value for ${name}` }, signal2);
1488
+ }
1489
+ finally {
1490
+ clearMenuSignal();
1491
+ }
1492
+ if (!valResult.cancelled) {
1493
+ const val = parseInt(valResult.raw, 10);
1494
+ const minVal = opt.default === 0 ? 0 : 1;
1495
+ if (!isNaN(val) && val >= minVal) {
1496
+ (0, config_js_1.saveOption)(name, val);
1497
+ config[opt.configKey] = val;
1498
+ (0, output_js_1.printInfo)(` ${name} set to ${val}`);
1499
+ if (name === 'token-window') {
1500
+ const modelWindow = (0, context_js_1.getContextWindow)(config.model);
1501
+ if (val > modelWindow) {
1502
+ (0, output_js_1.printInfo)(` Warning: ${val} exceeds ${config.model}'s context window (${modelWindow})`);
965
1503
  }
966
1504
  }
967
- else if (valAnswer.trim() === '') {
968
- (0, output_js_1.printInfo)(' Cancelled.');
969
- }
970
- else {
971
- (0, output_js_1.printError)(` Invalid value. Must be ${minVal === 0 ? 'a non-negative integer' : 'a positive integer'}.`);
972
- }
973
- console.log();
974
- void prompt();
975
- });
976
- }
977
- else {
978
- (0, output_js_1.printInfo)(' Cancelled.');
979
- console.log();
980
- void prompt();
1505
+ }
1506
+ else {
1507
+ (0, output_js_1.printError)(` Invalid value. Must be ${minVal === 0 ? 'a non-negative integer' : 'a positive integer'}.`);
1508
+ }
981
1509
  }
982
- });
1510
+ }
1511
+ console.log();
1512
+ void prompt();
983
1513
  return;
984
1514
  }
985
1515
  if (trimmed === '/update') {
@@ -993,21 +1523,12 @@ async function startRepl(config, alertContext, resume) {
993
1523
  (0, output_js_1.printInfo)('No routines saved. Teach me a workflow and I can save it as a routine.');
994
1524
  }
995
1525
  else {
996
- const tasks = allRoutines.filter((r) => r.id.startsWith('task-'));
997
- const routines = allRoutines.filter((r) => !r.id.startsWith('task-'));
998
- if (tasks.length > 0) {
999
- (0, output_js_1.printInfo)(`\n Tasks (${tasks.length}) — single-step, structured output:`);
1000
- for (const r of tasks) {
1001
- (0, output_js_1.printInfo)(` /${r.id} — ${r.name}: ${r.description}`);
1002
- }
1003
- }
1004
- if (routines.length > 0) {
1005
- (0, output_js_1.printInfo)(`\n Routines (${routines.length}) — multi-step workflows:`);
1006
- for (const r of routines) {
1007
- (0, output_js_1.printInfo)(` /${r.id} — ${r.name}: ${r.description}`);
1008
- }
1009
- }
1010
- console.log();
1526
+ const hasTasks = allRoutines.some((r) => r.id.startsWith('task-'));
1527
+ const hasRoutines = allRoutines.some((r) => !r.id.startsWith('task-'));
1528
+ if (hasTasks)
1529
+ printRoutinesList('tasks');
1530
+ if (hasRoutines)
1531
+ printRoutinesList('routines');
1011
1532
  }
1012
1533
  void prompt();
1013
1534
  return;
@@ -1054,17 +1575,7 @@ Remember: task content should describe a single atomic operation with clear succ
1054
1575
  return;
1055
1576
  }
1056
1577
  if (trimmed === '/specialists') {
1057
- const specialists = specialistStore.list();
1058
- if (specialists.length === 0) {
1059
- (0, output_js_1.printInfo)('No specialist agents defined yet. Ask me to create one or use /create-specialist.');
1060
- }
1061
- else {
1062
- (0, output_js_1.printInfo)(`\n Specialists (${specialists.length}):`);
1063
- for (const s of specialists) {
1064
- (0, output_js_1.printInfo)(` ${s.id} — ${s.name}: ${s.description}`);
1065
- }
1066
- console.log();
1067
- }
1578
+ printSpecialistsList();
1068
1579
  void prompt();
1069
1580
  return;
1070
1581
  }
@@ -1109,81 +1620,81 @@ Remember: the systemPrompt should read like a persona definition — who this sp
1109
1620
  (0, output_js_1.printInfo)(' To accept or reject, tell Bernard conversationally (e.g., "accept the code-review candidate").');
1110
1621
  (0, output_js_1.printInfo)(' The agent can create the specialist via the specialist tool, then update candidate status.\n');
1111
1622
  // Inject candidate context so the agent knows about them for the rest of the session
1112
- const candidateContext = `## Specialist Suggestions\n\nBernard detected patterns in previous sessions that might benefit from saved specialists. Mention these when relevant.\n\n${pending.map((c) => `- "${c.name}" (${c.draftId}): ${c.description}`).join('\n')}`;
1113
- agent.setAlertContext(candidateContext);
1623
+ agent.setAlertContext((0, candidate_bootstrap_js_1.buildCandidateContextBlock)(pending));
1114
1624
  }
1115
1625
  void prompt();
1116
1626
  return;
1117
1627
  }
1118
- if (trimmed === '/critic' || trimmed.startsWith('/critic ')) {
1119
- const arg = trimmed.slice('/critic'.length).trim().toLowerCase();
1120
- if (arg === 'on') {
1121
- config.criticMode = true;
1122
- (0, config_js_1.savePreferences)({
1123
- provider: config.provider,
1124
- model: config.model,
1125
- maxTokens: config.maxTokens,
1126
- shellTimeout: config.shellTimeout,
1127
- tokenWindow: config.tokenWindow,
1128
- theme: config.theme,
1129
- criticMode: true,
1130
- });
1131
- (0, output_js_1.printInfo)('[CRITIC:ON] Responses will be planned and verified.');
1132
- }
1133
- else if (arg === 'off') {
1134
- config.criticMode = false;
1135
- (0, config_js_1.savePreferences)({
1136
- provider: config.provider,
1137
- model: config.model,
1138
- maxTokens: config.maxTokens,
1139
- shellTimeout: config.shellTimeout,
1140
- tokenWindow: config.tokenWindow,
1141
- theme: config.theme,
1142
- criticMode: false,
1143
- });
1144
- (0, output_js_1.printInfo)('[CRITIC:OFF] Critic mode disabled.');
1145
- }
1146
- else {
1147
- (0, output_js_1.printInfo)(`Critic mode: ${config.criticMode ? 'ON' : 'OFF'}. Usage: /critic on|off`);
1148
- }
1628
+ // Backwards-compat shims: the standalone toggles (/critic, /react, /tool-details, /debug)
1629
+ // were consolidated into /agent-options and /options. Print a short pointer so users typing
1630
+ // the old command aren't silently dropped into the prompt.
1631
+ const legacyToggle = {
1632
+ '/critic': 'Critic mode → /agent-options',
1633
+ '/react': 'Coordinator (ReAct) mode → /agent-options',
1634
+ '/tool-details': 'Tool-call details → /agent-options',
1635
+ '/debug': 'Debug logging → /options',
1636
+ }[trimmed];
1637
+ if (legacyToggle) {
1638
+ (0, output_js_1.printInfo)(` This command moved. ${legacyToggle}`);
1149
1639
  void prompt();
1150
1640
  return;
1151
1641
  }
1152
- if (trimmed === '/agent-options' || trimmed.startsWith('/agent-options ')) {
1153
- const args = trimmed.slice('/agent-options'.length).trim();
1154
- if (!args) {
1155
- // Display current settings
1156
- (0, output_js_1.printInfo)(`Auto-create specialists: ${config.autoCreateSpecialists ? 'on' : 'off'}`);
1157
- (0, output_js_1.printInfo)(`Auto-create threshold: ${config.autoCreateThreshold} (${Math.round(config.autoCreateThreshold * 100)}%)`);
1158
- }
1159
- else if (args === 'auto-create on') {
1160
- config.autoCreateSpecialists = true;
1161
- (0, config_js_1.savePreferences)({
1162
- ...(0, config_js_1.loadPreferences)(),
1163
- autoCreateSpecialists: true,
1164
- provider: config.provider,
1165
- model: config.model,
1166
- });
1167
- (0, output_js_1.printInfo)('Auto-create specialists: on');
1168
- promotePendingCandidates(candidateStore, specialistStore, config.autoCreateThreshold);
1169
- }
1170
- else if (args === 'auto-create off') {
1171
- config.autoCreateSpecialists = false;
1172
- (0, config_js_1.savePreferences)({
1173
- ...(0, config_js_1.loadPreferences)(),
1174
- autoCreateSpecialists: false,
1175
- provider: config.provider,
1176
- model: config.model,
1177
- });
1178
- (0, output_js_1.printInfo)('Auto-create specialists: off');
1179
- }
1180
- else if (args.startsWith('threshold ')) {
1181
- const val = parseFloat(args.slice('threshold '.length));
1182
- if (isNaN(val) || val < 0 || val > 100) {
1183
- (0, output_js_1.printError)('Threshold must be a number between 0 and 100 (e.g. 0.8 or 80)');
1184
- }
1185
- else {
1186
- const normalized = (0, config_js_1.normalizeThreshold)(val);
1642
+ if (trimmed === '/agent-options') {
1643
+ const systemBools = [
1644
+ {
1645
+ key: 'autoCreateSpecialists',
1646
+ label: 'Auto-create specialists',
1647
+ description: 'Auto-promote pending specialist candidates whose score exceeds the threshold.',
1648
+ onMsg: ' Auto-create specialists: on',
1649
+ offMsg: ' Auto-create specialists: off',
1650
+ onToggle: (value) => {
1651
+ if (value) {
1652
+ (0, candidate_bootstrap_js_1.promotePendingCandidates)(candidateStore, specialistStore, config.autoCreateThreshold);
1653
+ }
1654
+ },
1655
+ },
1656
+ {
1657
+ key: 'criticMode',
1658
+ label: 'Critic mode',
1659
+ description: 'Plan the response, verify it with a critic pass, and retry on failure before replying.',
1660
+ onMsg: ' [CRITIC:ON] Responses will be planned and verified.',
1661
+ offMsg: ' [CRITIC:OFF] Critic mode disabled.',
1662
+ },
1663
+ {
1664
+ key: 'reactMode',
1665
+ label: 'Coordinator (ReAct) mode',
1666
+ description: 'Iterate think → act → evaluate; delegate subtasks to subagents for complex work.',
1667
+ onMsg: ' [REACT:ON] Operating as coordinator with iterative reasoning and delegation.',
1668
+ offMsg: ' [REACT:OFF] Coordinator mode disabled.',
1669
+ },
1670
+ {
1671
+ key: 'promptRewriter',
1672
+ label: 'Prompt rewriter',
1673
+ description: 'Restructure your prompt for the active model family before each turn.',
1674
+ onMsg: ' [REWRITER:ON] User prompts will be restructured for the active model before execution.',
1675
+ offMsg: ' [REWRITER:OFF] Prompts will be sent to the model verbatim.',
1676
+ },
1677
+ {
1678
+ key: 'toolDetails',
1679
+ label: 'Tool details',
1680
+ description: 'Show full tool call args and results in the transcript.',
1681
+ onMsg: ' [TOOL-DETAILS:ON] Full tool call args and results will be shown.',
1682
+ offMsg: ' [TOOL-DETAILS:OFF] Only tool names shown; args and results hidden.',
1683
+ onToggle: output_js_2.setToolDetailsVisible,
1684
+ },
1685
+ ];
1686
+ async function runThresholdPrompt() {
1687
+ const signal = createMenuSignal();
1688
+ try {
1689
+ const val = await (0, menu_js_1.promptValue)(rl, { label: 'New threshold (0-100)' }, signal);
1690
+ if (val.cancelled)
1691
+ return;
1692
+ const parsed = parseFloat(val.raw);
1693
+ if (isNaN(parsed) || parsed < 0 || parsed > 100) {
1694
+ (0, output_js_1.printError)('Threshold must be a number between 0 and 100 (e.g. 0.8 or 80)');
1695
+ return;
1696
+ }
1697
+ const normalized = (0, config_js_1.normalizeThreshold)(parsed);
1187
1698
  config.autoCreateThreshold = normalized;
1188
1699
  (0, config_js_1.savePreferences)({
1189
1700
  ...(0, config_js_1.loadPreferences)(),
@@ -1191,84 +1702,72 @@ Remember: the systemPrompt should read like a persona definition — who this sp
1191
1702
  provider: config.provider,
1192
1703
  model: config.model,
1193
1704
  });
1194
- (0, output_js_1.printInfo)(`Auto-create threshold: ${normalized} (${Math.round(normalized * 100)}%)`);
1705
+ (0, output_js_1.printInfo)(` Auto-create threshold: ${normalized} (${Math.round(normalized * 100)}%)`);
1195
1706
  if (config.autoCreateSpecialists) {
1196
- promotePendingCandidates(candidateStore, specialistStore, config.autoCreateThreshold);
1707
+ (0, candidate_bootstrap_js_1.promotePendingCandidates)(candidateStore, specialistStore, config.autoCreateThreshold);
1197
1708
  }
1198
1709
  }
1710
+ finally {
1711
+ clearMenuSignal();
1712
+ }
1199
1713
  }
1200
- else {
1201
- (0, output_js_1.printError)('Usage: /agent-options auto-create on|off OR /agent-options threshold <0-100>');
1202
- }
1203
- void prompt();
1204
- return;
1205
- }
1206
- if (trimmed === '/debug') {
1207
- const t = (0, theme_js_1.getTheme)();
1208
- console.log(t.accent('\n Bernard Diagnostic Report'));
1209
- console.log(t.accent(' ' + '─'.repeat(40)));
1210
- console.log(t.text('\n Runtime:'));
1211
- console.log(t.muted(` Bernard version: ${(0, update_js_1.getLocalVersion)()}`));
1212
- console.log(t.muted(` Node.js version: ${process.version}`));
1213
- console.log(t.muted(` OS: ${process.platform} ${process.arch} (${os.release()})`));
1214
- console.log(t.text('\n LLM:'));
1215
- console.log(t.muted(` Provider: ${config.provider}`));
1216
- console.log(t.muted(` Model: ${config.model}`));
1217
- console.log(t.muted(` maxTokens: ${config.maxTokens}`));
1218
- console.log(t.muted(` shellTimeout: ${config.shellTimeout}ms`));
1219
- console.log(t.muted(` tokenWindow: ${config.tokenWindow || 'auto-detect'}`));
1220
- console.log(t.text('\n API Keys:'));
1221
- for (const { provider, hasKey } of (0, config_js_1.getProviderKeyStatus)()) {
1222
- console.log(t.muted(` ${provider}: ${hasKey ? 'configured' : 'not set'}`));
1223
- }
1224
- const debugStatuses = mcpManager.getServerStatuses();
1225
- console.log(t.text('\n MCP Servers:'));
1226
- if (debugStatuses.length === 0) {
1227
- console.log(t.muted(' (none configured)'));
1714
+ const toggleRow = (opt) => ({
1715
+ kind: 'item',
1716
+ item: {
1717
+ label: opt.label,
1718
+ annotation: `= ${config[opt.key] ? 'on' : 'off'}`,
1719
+ description: opt.description,
1720
+ },
1721
+ action: () => toggleBooleanPref(opt.key, opt.label, opt.onMsg, opt.offMsg, opt.onToggle),
1722
+ });
1723
+ const rows = [
1724
+ { kind: 'section', title: 'System' },
1725
+ toggleRow(systemBools[0]),
1726
+ {
1727
+ kind: 'item',
1728
+ item: {
1729
+ label: 'Auto-create threshold',
1730
+ annotation: `= ${config.autoCreateThreshold} (${Math.round(config.autoCreateThreshold * 100)}%)`,
1731
+ description: 'Minimum score (0-1) a pending specialist needs before auto-promotion.',
1732
+ },
1733
+ action: runThresholdPrompt,
1734
+ },
1735
+ ...systemBools.slice(1).map(toggleRow),
1736
+ { kind: 'section', title: 'User-created' },
1737
+ {
1738
+ kind: 'item',
1739
+ item: {
1740
+ label: 'Specialists',
1741
+ description: 'List bundled and user-created specialists.',
1742
+ },
1743
+ action: () => printSpecialistsList(),
1744
+ },
1745
+ {
1746
+ kind: 'item',
1747
+ item: { label: 'Tasks', description: 'List saved single-step tasks.' },
1748
+ action: () => printRoutinesList('tasks'),
1749
+ },
1750
+ {
1751
+ kind: 'item',
1752
+ item: { label: 'Routines', description: 'List saved multi-step routines.' },
1753
+ action: () => printRoutinesList('routines'),
1754
+ },
1755
+ ];
1756
+ const topEntries = rows.map((r) => r.kind === 'section' ? { type: 'section', title: r.title } : r.item);
1757
+ const itemActions = rows.flatMap((r) => (r.kind === 'item' ? [r.action] : []));
1758
+ const signal1 = createMenuSignal();
1759
+ let topResult;
1760
+ try {
1761
+ topResult = await (0, menu_js_1.selectFromMenu)(rl, topEntries, { title: 'Agent Options' }, signal1);
1228
1762
  }
1229
- else {
1230
- for (const s of debugStatuses) {
1231
- if (s.connected) {
1232
- console.log(t.muted(` ${s.name}: connected (${s.toolCount} tools)`));
1233
- }
1234
- else {
1235
- console.log(t.muted(` ${s.name}: failed — ${s.error}`));
1236
- }
1237
- }
1763
+ finally {
1764
+ clearMenuSignal();
1238
1765
  }
1239
- console.log(t.text('\n RAG:'));
1240
- console.log(t.muted(` Enabled: ${config.ragEnabled}`));
1241
- if (ragStore) {
1242
- console.log(t.muted(` Facts: ${ragStore.count()}`));
1766
+ if (!topResult.cancelled) {
1767
+ const action = itemActions[topResult.index];
1768
+ if (action)
1769
+ await action();
1243
1770
  }
1244
- console.log(t.text('\n Memory:'));
1245
- console.log(t.muted(` Persistent memories: ${memoryStore.listMemory().length}`));
1246
- console.log(t.text('\n Cron:'));
1247
- console.log(t.muted(` Daemon: ${(0, client_js_1.isDaemonRunning)() ? 'running' : 'stopped'}`));
1248
- let debugJobCount = 0;
1249
- try {
1250
- const raw = fs.readFileSync(paths_js_1.CRON_JOBS_FILE, 'utf-8');
1251
- debugJobCount = JSON.parse(raw).length;
1252
- }
1253
- catch {
1254
- // jobs.json doesn't exist yet — that's fine
1255
- }
1256
- console.log(t.muted(` Jobs: ${debugJobCount}`));
1257
- console.log(t.text('\n Conversation:'));
1258
- console.log(t.muted(` Messages: ${agent.getHistory().length}`));
1259
- console.log(t.text('\n Settings:'));
1260
- console.log(t.muted(` Theme: ${(0, theme_js_1.getActiveThemeKey)()}`));
1261
- console.log(t.muted(` Critic mode: ${config.criticMode ? 'on' : 'off'}`));
1262
- const debugEnabled = process.env.BERNARD_DEBUG === 'true' || process.env.BERNARD_DEBUG === '1';
1263
- console.log(t.muted(` Debug mode: ${debugEnabled ? 'on' : 'off'}`));
1264
- console.log(t.text('\n Paths:'));
1265
- if (process.env.BERNARD_HOME) {
1266
- console.log(t.muted(` BERNARD_HOME: ${process.env.BERNARD_HOME}`));
1267
- }
1268
- console.log(t.muted(` Config: ${paths_js_1.CONFIG_DIR}`));
1269
- console.log(t.muted(` Data: ${paths_js_1.DATA_DIR}`));
1270
- console.log(t.muted(` Cache: ${paths_js_1.CACHE_DIR}`));
1271
- console.log(t.muted(` State: ${paths_js_1.STATE_DIR}`));
1272
1771
  console.log();
1273
1772
  void prompt();
1274
1773
  return;
@@ -1286,6 +1785,70 @@ Remember: the systemPrompt should read like a persona definition — who this sp
1286
1785
  void prompt();
1287
1786
  return;
1288
1787
  }
1788
+ if (trimmed === '/image' || trimmed.startsWith('/image ')) {
1789
+ const args = trimmed.slice('/image'.length).trim();
1790
+ if (!args) {
1791
+ (0, output_js_1.printError)('Usage: /image <path> [prompt]');
1792
+ (0, output_js_1.printInfo)(' Example: /image ~/screenshot.png What is on the screen?');
1793
+ (0, output_js_1.printInfo)(' Tip: you can also paste image paths inline, e.g. "describe ~/screenshot.png"');
1794
+ void prompt();
1795
+ return;
1796
+ }
1797
+ let imagePath;
1798
+ let userText;
1799
+ const quoteMatch = args.match(/^(["'])(.+?)\1(?:\s+(.*))?$/);
1800
+ if (quoteMatch) {
1801
+ imagePath = quoteMatch[2];
1802
+ userText = quoteMatch[3]?.trim() || 'Describe this image.';
1803
+ }
1804
+ else {
1805
+ const spaceIdx = args.indexOf(' ');
1806
+ imagePath = spaceIdx === -1 ? args : args.slice(0, spaceIdx);
1807
+ userText =
1808
+ spaceIdx === -1
1809
+ ? 'Describe this image.'
1810
+ : args.slice(spaceIdx + 1).trim() || 'Describe this image.';
1811
+ }
1812
+ if (!(0, image_js_1.isVisionCapableModel)(config.provider, config.model)) {
1813
+ (0, output_js_1.printError)(`Model "${config.model}" does not support image input. Switch to a vision-capable model with /model.`);
1814
+ void prompt();
1815
+ return;
1816
+ }
1817
+ let attachment;
1818
+ try {
1819
+ attachment = (0, image_js_1.loadImage)(imagePath);
1820
+ }
1821
+ catch (err) {
1822
+ (0, output_js_1.printError)(err instanceof Error ? err.message : String(err));
1823
+ void prompt();
1824
+ return;
1825
+ }
1826
+ (0, output_js_1.printInfo)(` Attaching image: ${attachment.path}`);
1827
+ (0, output_js_1.printInfo)(` Image will be sent to ${config.provider}/${config.model}`);
1828
+ processing = true;
1829
+ interrupted = false;
1830
+ try {
1831
+ initSpinner();
1832
+ await agent.processInput(userText, [attachment]);
1833
+ historyStore.save(agent.getHistory());
1834
+ }
1835
+ catch (err) {
1836
+ if (!interrupted) {
1837
+ (0, output_js_1.printError)(err instanceof Error ? err.message : String(err));
1838
+ }
1839
+ }
1840
+ finally {
1841
+ processing = false;
1842
+ (0, output_js_1.stopSpinner)();
1843
+ }
1844
+ if (interrupted) {
1845
+ (0, output_js_1.printInfo)('Interrupted.');
1846
+ interrupted = false;
1847
+ }
1848
+ console.log();
1849
+ void prompt();
1850
+ return;
1851
+ }
1289
1852
  // Dynamic routine invocation: /{routine-id} [args...]
1290
1853
  {
1291
1854
  const parts = trimmed.slice(1).split(/\s+/);
@@ -1310,16 +1873,7 @@ Remember: the systemPrompt should read like a persona definition — who this sp
1310
1873
  processing = true;
1311
1874
  interrupted = false;
1312
1875
  try {
1313
- const spinnerStats = {
1314
- startTime: Date.now(),
1315
- totalPromptTokens: 0,
1316
- totalCompletionTokens: 0,
1317
- latestPromptTokens: 0,
1318
- model: config.model,
1319
- contextWindowOverride: config.tokenWindow || undefined,
1320
- };
1321
- agent.setSpinnerStats(spinnerStats);
1322
- (0, output_js_1.startSpinner)(() => (0, output_js_1.buildSpinnerMessage)(spinnerStats));
1876
+ initSpinner();
1323
1877
  await agent.processInput(message);
1324
1878
  historyStore.save(agent.getHistory());
1325
1879
  }
@@ -1343,20 +1897,35 @@ Remember: the systemPrompt should read like a persona definition — who this sp
1343
1897
  }
1344
1898
  }
1345
1899
  } // end slash command handling
1900
+ let inlineImages;
1901
+ const candidatePaths = (0, image_js_1.extractImagePaths)(trimmed);
1902
+ if (candidatePaths.length > 0) {
1903
+ if ((0, image_js_1.isVisionCapableModel)(config.provider, config.model)) {
1904
+ const loaded = [];
1905
+ for (const p of candidatePaths) {
1906
+ const img = (0, image_js_1.tryLoadImage)(p);
1907
+ if (img)
1908
+ loaded.push(img);
1909
+ }
1910
+ if (loaded.length > 0) {
1911
+ for (const img of loaded) {
1912
+ (0, output_js_1.printInfo)(` Attaching image: ${img.path}`);
1913
+ }
1914
+ inlineImages = loaded;
1915
+ }
1916
+ }
1917
+ else {
1918
+ (0, output_js_2.printWarning)(`Image(s) detected but model "${config.model}" does not support vision. Sending as text only.`);
1919
+ }
1920
+ }
1921
+ const resolvedEntries = await runReferenceResolver(trimmed);
1922
+ const rewritten = await runPromptRewriter(trimmed, resolvedEntries);
1923
+ const agentInput = rewritten ?? trimmed;
1346
1924
  processing = true;
1347
1925
  interrupted = false;
1348
1926
  try {
1349
- const spinnerStats = {
1350
- startTime: Date.now(),
1351
- totalPromptTokens: 0,
1352
- totalCompletionTokens: 0,
1353
- latestPromptTokens: 0,
1354
- model: config.model,
1355
- contextWindowOverride: config.tokenWindow || undefined,
1356
- };
1357
- agent.setSpinnerStats(spinnerStats);
1358
- (0, output_js_1.startSpinner)(() => (0, output_js_1.buildSpinnerMessage)(spinnerStats));
1359
- await agent.processInput(trimmed);
1927
+ initSpinner();
1928
+ await agent.processInput(agentInput, inlineImages, resolvedEntries);
1360
1929
  historyStore.save(agent.getHistory());
1361
1930
  }
1362
1931
  catch (err) {
@@ -1400,4 +1969,102 @@ Remember: the systemPrompt should read like a persona definition — who this sp
1400
1969
  });
1401
1970
  void prompt();
1402
1971
  }
1972
+ /**
1973
+ * Interactive wizard for adding a custom provider from `/provider`.
1974
+ * Returns the saved entry plus the freshly entered API key on success
1975
+ * (so the caller can update the live `config.apiKeys` map without
1976
+ * re-reading from disk), or `null` on cancel/error.
1977
+ */
1978
+ async function runAddProviderWizard(rl, createMenuSignal, clearMenuSignal) {
1979
+ (0, output_js_1.printInfo)('\nAdd custom provider — follows the same SDK contract as built-ins.');
1980
+ // 1. SDK choice
1981
+ const sdkEntries = custom_providers_js_1.SUPPORTED_SDKS.map((s) => ({ label: s, value: s }));
1982
+ let sdkResult;
1983
+ let sig = createMenuSignal();
1984
+ try {
1985
+ sdkResult = await (0, menu_js_1.selectFromMenu)(rl, sdkEntries, { title: 'Which SDK to use?' }, sig);
1986
+ }
1987
+ finally {
1988
+ clearMenuSignal();
1989
+ }
1990
+ if (sdkResult.cancelled)
1991
+ return null;
1992
+ const sdk = sdkResult.item.value;
1993
+ // 2. Name
1994
+ sig = createMenuSignal();
1995
+ let nameResult;
1996
+ try {
1997
+ nameResult = await (0, menu_js_1.promptValue)(rl, { label: 'Provider name (lowercase, e.g. "ollama")' }, sig);
1998
+ }
1999
+ finally {
2000
+ clearMenuSignal();
2001
+ }
2002
+ if (nameResult.cancelled)
2003
+ return null;
2004
+ const name = nameResult.raw.trim();
2005
+ const nameErr = (0, custom_providers_js_1.validateProviderName)(name);
2006
+ if (nameErr) {
2007
+ (0, output_js_1.printError)(nameErr);
2008
+ return null;
2009
+ }
2010
+ // 3. Base URL
2011
+ sig = createMenuSignal();
2012
+ let urlResult;
2013
+ try {
2014
+ urlResult = await (0, menu_js_1.promptValue)(rl, { label: 'Base URL (e.g. http://localhost:11434/v1)' }, sig);
2015
+ }
2016
+ finally {
2017
+ clearMenuSignal();
2018
+ }
2019
+ if (urlResult.cancelled)
2020
+ return null;
2021
+ const baseURL = urlResult.raw.trim();
2022
+ const urlErr = (0, custom_providers_js_1.validateBaseURL)(baseURL);
2023
+ if (urlErr) {
2024
+ (0, output_js_1.printError)(urlErr);
2025
+ return null;
2026
+ }
2027
+ // 4. Default model
2028
+ sig = createMenuSignal();
2029
+ let modelResult;
2030
+ try {
2031
+ modelResult = await (0, menu_js_1.promptValue)(rl, { label: 'Default model name' }, sig);
2032
+ }
2033
+ finally {
2034
+ clearMenuSignal();
2035
+ }
2036
+ if (modelResult.cancelled)
2037
+ return null;
2038
+ const defaultModel = modelResult.raw.trim();
2039
+ if (!defaultModel) {
2040
+ (0, output_js_1.printError)('Default model cannot be empty.');
2041
+ return null;
2042
+ }
2043
+ // 5. API key
2044
+ sig = createMenuSignal();
2045
+ let keyResult;
2046
+ try {
2047
+ keyResult = await (0, menu_js_1.promptValue)(rl, { label: 'API key (any non-empty token; some local servers ignore the value)' }, sig);
2048
+ }
2049
+ finally {
2050
+ clearMenuSignal();
2051
+ }
2052
+ if (keyResult.cancelled)
2053
+ return null;
2054
+ const apiKey = keyResult.raw.trim();
2055
+ if (!apiKey) {
2056
+ (0, output_js_1.printError)('API key cannot be empty.');
2057
+ return null;
2058
+ }
2059
+ try {
2060
+ const entry = (0, custom_providers_js_1.saveCustomProvider)({ name, sdk, baseURL, defaultModel });
2061
+ (0, config_js_1.saveProviderKey)(name, apiKey);
2062
+ return { entry, apiKey };
2063
+ }
2064
+ catch (err) {
2065
+ const message = err instanceof Error ? err.message : String(err);
2066
+ (0, output_js_1.printError)(message);
2067
+ return null;
2068
+ }
2069
+ }
1403
2070
  //# sourceMappingURL=repl.js.map