@kinqs/brainrouter-cli 0.3.5 → 0.3.7

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 (125) hide show
  1. package/README.md +29 -52
  2. package/agents/architect.json +18 -0
  3. package/agents/explorer.json +18 -0
  4. package/agents/reviewer.json +18 -0
  5. package/agents/verifier.json +18 -0
  6. package/agents/worker.json +18 -0
  7. package/bin/cli.cjs +71 -0
  8. package/dist/agent/agent.d.ts +224 -3
  9. package/dist/agent/agent.js +561 -55
  10. package/dist/cli/banner.d.ts +80 -0
  11. package/dist/cli/banner.js +232 -0
  12. package/dist/cli/cliPrompt.d.ts +106 -0
  13. package/dist/cli/cliPrompt.js +314 -0
  14. package/dist/cli/commands/_context.d.ts +3 -1
  15. package/dist/cli/commands/_helpers.d.ts +1 -1
  16. package/dist/cli/commands/_helpers.js +6 -6
  17. package/dist/cli/commands/config.d.ts +46 -0
  18. package/dist/cli/commands/config.js +1042 -0
  19. package/dist/cli/commands/guard.js +75 -10
  20. package/dist/cli/commands/init.d.ts +20 -0
  21. package/dist/cli/commands/init.js +64 -0
  22. package/dist/cli/commands/login.d.ts +13 -0
  23. package/dist/cli/commands/login.js +179 -0
  24. package/dist/cli/commands/mcp.d.ts +19 -0
  25. package/dist/cli/commands/mcp.js +286 -0
  26. package/dist/cli/commands/memory.js +2 -2
  27. package/dist/cli/commands/obs.js +22 -22
  28. package/dist/cli/commands/orchestration.js +18 -0
  29. package/dist/cli/commands/session.js +13 -5
  30. package/dist/cli/commands/ui.js +202 -91
  31. package/dist/cli/commands/workflow.d.ts +20 -0
  32. package/dist/cli/commands/workflow.js +368 -51
  33. package/dist/cli/ink/ChatApp.d.ts +206 -0
  34. package/dist/cli/ink/ChatApp.js +493 -0
  35. package/dist/cli/ink/Frame.d.ts +26 -0
  36. package/dist/cli/ink/Frame.js +5 -0
  37. package/dist/cli/ink/Picker.d.ts +65 -0
  38. package/dist/cli/ink/Picker.js +133 -0
  39. package/dist/cli/ink/SlashPalette.d.ts +51 -0
  40. package/dist/cli/ink/SlashPalette.js +136 -0
  41. package/dist/cli/ink/TextField.d.ts +34 -0
  42. package/dist/cli/ink/TextField.js +47 -0
  43. package/dist/cli/ink/WizardApp.d.ts +7 -0
  44. package/dist/cli/ink/WizardApp.js +422 -0
  45. package/dist/cli/ink/ambientChat.d.ts +34 -0
  46. package/dist/cli/ink/ambientChat.js +7 -0
  47. package/dist/cli/ink/consoleCapture.d.ts +11 -0
  48. package/dist/cli/ink/consoleCapture.js +33 -0
  49. package/dist/cli/ink/markdownRender.d.ts +41 -0
  50. package/dist/cli/ink/markdownRender.js +278 -0
  51. package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
  52. package/dist/cli/ink/renderWithResizeClear.js +33 -0
  53. package/dist/cli/ink/runChat.d.ts +34 -0
  54. package/dist/cli/ink/runChat.js +571 -0
  55. package/dist/cli/ink/runPicker.d.ts +31 -0
  56. package/dist/cli/ink/runPicker.js +139 -0
  57. package/dist/cli/ink/runSlashPalette.d.ts +23 -0
  58. package/dist/cli/ink/runSlashPalette.js +33 -0
  59. package/dist/cli/ink/runWizard.d.ts +22 -0
  60. package/dist/cli/ink/runWizard.js +133 -0
  61. package/dist/cli/ink/stdinHandoff.d.ts +51 -0
  62. package/dist/cli/ink/stdinHandoff.js +78 -0
  63. package/dist/cli/ink/toolFormat.d.ts +73 -0
  64. package/dist/cli/ink/toolFormat.js +180 -0
  65. package/dist/cli/ink/useTerminalSize.d.ts +35 -0
  66. package/dist/cli/ink/useTerminalSize.js +26 -0
  67. package/dist/cli/repl.d.ts +25 -3
  68. package/dist/cli/repl.js +64 -646
  69. package/dist/cli/slashSuggest.d.ts +32 -0
  70. package/dist/cli/slashSuggest.js +146 -0
  71. package/dist/cli/spinner.d.ts +34 -0
  72. package/dist/cli/spinner.js +36 -0
  73. package/dist/cli/statusline.d.ts +67 -0
  74. package/dist/cli/statusline.js +204 -0
  75. package/dist/cli/theme.d.ts +79 -0
  76. package/dist/cli/theme.js +106 -0
  77. package/dist/cli/whereView.d.ts +81 -0
  78. package/dist/cli/whereView.js +245 -0
  79. package/dist/cli/wizard/modelsApi.d.ts +72 -0
  80. package/dist/cli/wizard/modelsApi.js +166 -0
  81. package/dist/cli/wizard/picker.d.ts +202 -0
  82. package/dist/cli/wizard/picker.js +547 -0
  83. package/dist/cli/wizard/providers.d.ts +86 -0
  84. package/dist/cli/wizard/providers.js +190 -0
  85. package/dist/cli/wizard/runner.d.ts +13 -0
  86. package/dist/cli/wizard/runner.js +488 -0
  87. package/dist/cli/wizard/types.d.ts +122 -0
  88. package/dist/cli/wizard/types.js +109 -0
  89. package/dist/config/config.d.ts +52 -0
  90. package/dist/config/config.js +89 -75
  91. package/dist/index.js +215 -206
  92. package/dist/memory/briefing.d.ts +11 -1
  93. package/dist/memory/briefing.js +69 -1
  94. package/dist/memory/consolidation.d.ts +1 -1
  95. package/dist/orchestration/agentRegistry.d.ts +36 -0
  96. package/dist/orchestration/agentRegistry.js +64 -0
  97. package/dist/orchestration/orchestrator.d.ts +7 -0
  98. package/dist/orchestration/orchestrator.js +2 -0
  99. package/dist/orchestration/tools.d.ts +10 -1
  100. package/dist/orchestration/tools.js +48 -4
  101. package/dist/prompt/breadthHint.d.ts +5 -0
  102. package/dist/prompt/breadthHint.js +44 -0
  103. package/dist/prompt/skillCatalog.d.ts +11 -0
  104. package/dist/prompt/skillCatalog.js +134 -0
  105. package/dist/prompt/skillRunner.d.ts +2 -2
  106. package/dist/prompt/skillRunner.js +2 -31
  107. package/dist/prompt/systemPrompt.d.ts +34 -0
  108. package/dist/prompt/systemPrompt.js +128 -108
  109. package/dist/runtime/dangerousCommand.d.ts +53 -0
  110. package/dist/runtime/dangerousCommand.js +105 -0
  111. package/dist/runtime/mcpClient.d.ts +38 -1
  112. package/dist/runtime/mcpClient.js +104 -13
  113. package/dist/runtime/mcpPool.d.ts +162 -0
  114. package/dist/runtime/mcpPool.js +423 -0
  115. package/dist/runtime/mcpUtils.d.ts +3 -1
  116. package/dist/state/goalStore.d.ts +98 -17
  117. package/dist/state/goalStore.js +132 -42
  118. package/dist/state/preferencesStore.d.ts +67 -3
  119. package/dist/state/preferencesStore.js +84 -1
  120. package/dist/state/workflowArtifacts.d.ts +63 -2
  121. package/dist/state/workflowArtifacts.js +120 -8
  122. package/dist/tests/_helpers.d.ts +31 -0
  123. package/dist/tests/_helpers.js +91 -0
  124. package/package.json +12 -5
  125. package/.env.example +0 -109
@@ -6,16 +6,23 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { execSync } from 'node:child_process';
8
8
  import chalk from 'chalk';
9
- import ora from 'ora';
9
+ import { spinner as makeSpinner } from '../spinner.js';
10
10
  import { LOCAL_TOOLS } from '../../agent/agent.js';
11
11
  import { callMcpTool } from '../../runtime/mcpUtils.js';
12
12
  import { listSessions, reconcileStale } from '../../orchestration/orchestrator.js';
13
- import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
13
+ import { readPreferences, resolveEffort, writePreferences } from '../../state/preferencesStore.js';
14
14
  import { readPlan } from '../../state/taskStore.js';
15
- import { getConfigPath } from '../../config/config.js';
15
+ // initAgentMd usage moved to commands/init.ts (0.3.7 wizard). The
16
+ // legacy /config + /init switch cases here are gone — the dispatcher
17
+ // in repl.ts routes them to the new handlers first. getConfigPath
18
+ // stays in scope because /doctor still surfaces the path.
19
+ import { getConfigPath, saveConfig } from '../../config/config.js';
16
20
  import { copyToClipboard } from '../../runtime/clipboard.js';
17
- import { initAgentMd } from '../../prompt/initAgentMd.js';
18
21
  import { completeWorkspacePath, renderHelp } from '../repl.js';
22
+ import { PROVIDER_CATALOG, findProvider } from '../wizard/providers.js';
23
+ import { selectModel } from '../wizard/modelsApi.js';
24
+ import { buildTheme } from '../theme.js';
25
+ import { listFilesystemSkills } from '../../prompt/skillCatalog.js';
19
26
  export async function tryHandleUiCommand(ctx) {
20
27
  const { command, args, agent, mcpClient, config, rl, repl } = ctx;
21
28
  // 'ctx' alias to keep references to the old ReplContext name working
@@ -41,7 +48,7 @@ export async function tryHandleUiCommand(ctx) {
41
48
  console.log(` LLM Endpoint: ${chalk.blue(llm.endpoint)}`);
42
49
  }
43
50
  }
44
- const spinner = ora(chalk.gray('Querying diagnostics & testing latency...')).start();
51
+ const spinner = makeSpinner(chalk.gray('Querying diagnostics & testing latency...')).start();
45
52
  try {
46
53
  const start = Date.now();
47
54
  const testRes = await mcpClient.callTool('list_skills', { scope: 'local' });
@@ -78,27 +85,10 @@ export async function tryHandleUiCommand(ctx) {
78
85
  console.log();
79
86
  return true;
80
87
  }
81
- case '/config':
82
- {
83
- console.log(chalk.bold('\n⚙️ Active Configuration:'));
84
- console.log(` File Path: ${chalk.blue(getConfigPath())}\n`);
85
- // Print config without API keys
86
- const scrubbedConfig = JSON.parse(JSON.stringify(config));
87
- if (scrubbedConfig.llm?.apiKey) {
88
- scrubbedConfig.llm.apiKey = 'br_••••••••••••••••';
89
- }
90
- for (const s of Object.values(scrubbedConfig.servers)) {
91
- const srv = s;
92
- if (srv.apiKey)
93
- srv.apiKey = 'br_••••••••••••••••';
94
- if (srv.env?.BRAINROUTER_API_KEY) {
95
- srv.env.BRAINROUTER_API_KEY = 'br_••••••••••••••••';
96
- }
97
- }
98
- console.log(chalk.gray(JSON.stringify(scrubbedConfig, null, 2)));
99
- console.log();
100
- return true;
101
- }
88
+ // /config now lives in commands/config.ts (0.3.7 settings home panel
89
+ // + verb-overloaded get/set). The dispatcher in repl.ts routes it
90
+ // before this case, so leaving anything here is dead — removed.
91
+ // Use `/config raw` if you want the old scrubbed-JSON dump.
102
92
  case '/doctor':
103
93
  {
104
94
  console.log(chalk.bold('\nBrainRouter Doctor:'));
@@ -116,7 +106,7 @@ export async function tryHandleUiCommand(ctx) {
116
106
  else {
117
107
  console.log(` Endpoint: ${chalk.blue(server.url)}`);
118
108
  }
119
- const spinner = ora(chalk.gray('Checking MCP tool surface...')).start();
109
+ const spinner = makeSpinner(chalk.gray('Checking MCP tool surface...')).start();
120
110
  try {
121
111
  const startedAt = Date.now();
122
112
  const res = await mcpClient.listTools();
@@ -178,68 +168,75 @@ export async function tryHandleUiCommand(ctx) {
178
168
  console.log();
179
169
  return true;
180
170
  }
181
- case '/init':
182
- {
183
- const result = initAgentMd(agent.workspaceRoot);
184
- if (result.status === 'created') {
185
- console.log(chalk.green(`\n✓ Created ${result.path}`));
186
- console.log(chalk.gray('Edit it to describe your project, conventions, and boundaries — any AGENT.md-aware coding agent will read it.\n'));
187
- }
188
- else {
189
- console.log(chalk.yellow(`\nFile already exists: ${result.path}`));
190
- console.log(chalk.gray('Open it and edit by hand if you want to refresh it.\n'));
191
- }
192
- return true;
193
- }
171
+ // /init is now the onboarding-wizard entrypoint (commands/init.ts).
172
+ // The AGENT.md-only path lives behind `/init agentmd` for back-compat.
173
+ // Routed before this case in repl.ts; no fall-through handler needed.
194
174
  case '/model':
195
175
  {
196
176
  const newModel = args[0];
197
- if (!newModel) {
198
- console.log(chalk.bold(`\nCurrent model: ${chalk.cyan(agent.getModel())}`));
199
- console.log(chalk.gray('Switch with: /model <model-name> (e.g. /model gpt-4o-mini, /model gpt-5, /model qwen2.5-coder)\n'));
200
- return true;
201
- }
202
177
  const previous = agent.getModel();
203
- agent.setModel(newModel);
204
- console.log(chalk.green(`\n✓ Model switched: ${chalk.gray(previous)} ${chalk.cyan(newModel)}\n`));
205
- return true;
206
- }
207
- case '/mcp':
208
- {
209
- const profileName = config.activeServer;
210
- const server = config.servers[profileName];
211
- console.log(chalk.bold('\nMCP server'));
212
- console.log(` Profile: ${chalk.green(profileName)} (${chalk.cyan(server?.type ?? 'unknown')})`);
213
- if (server?.type === 'http') {
214
- console.log(` URL: ${chalk.blue(server.url)}`);
215
- }
216
- else if (server?.type === 'stdio') {
217
- console.log(` Cmd: ${chalk.blue(server.command)} ${server.args?.join(' ') || ''}`);
218
- }
219
- const spinner = ora(chalk.gray('Fetching MCP tool surface...')).start();
220
- try {
221
- const res = await mcpClient.listTools();
222
- const tools = res.tools || [];
223
- spinner.succeed(chalk.green(`${tools.length} MCP tools available`));
224
- const namespaces = {};
225
- for (const t of tools) {
226
- const parts = (t.name || '').split('_');
227
- const ns = parts.length > 1 ? parts[0] : 'misc';
228
- (namespaces[ns] ||= []).push(t.name);
229
- }
230
- for (const ns of Object.keys(namespaces).sort()) {
231
- console.log(`\n ${chalk.bold.cyan(ns)} (${namespaces[ns].length})`);
232
- for (const name of namespaces[ns].sort()) {
233
- console.log(` ${chalk.gray('•')} ${name}`);
234
- }
178
+ // Direct-switch form `/model <name>` stays for scripts and muscle
179
+ // memory. No-arg opens the picker (0.3.7).
180
+ if (newModel) {
181
+ agent.setModel(newModel);
182
+ if (config.llm) {
183
+ config.llm.model = newModel;
184
+ saveConfig(config);
235
185
  }
186
+ console.log(chalk.green(`\n✓ Model switched: ${chalk.gray(previous)} → ${chalk.cyan(newModel)}\n`));
187
+ return true;
236
188
  }
237
- catch (err) {
238
- spinner.fail(chalk.red(`Failed: ${err.message}`));
189
+ // No-arg → open the picker. Resolves provider by matching the
190
+ // saved endpoint against PROVIDER_CATALOG; falls back to the
191
+ // OpenAI entry when nothing matches (the agent loop also
192
+ // defaults to OpenAI-compatible shapes).
193
+ const themeMode = readPreferences(agent.workspaceRoot).theme;
194
+ const theme = buildTheme(themeMode === 'mono' ? 'mono' : themeMode === 'light' ? 'light' : 'dark');
195
+ const llm = config.llm;
196
+ const provider = (llm?.endpoint && PROVIDER_CATALOG.find((p) => p.endpoint.replace(/\/$/, '') === (llm.endpoint ?? '').replace(/\/$/, ''))) ||
197
+ findProvider('openai');
198
+ const result = await selectModel({
199
+ theme,
200
+ provider,
201
+ apiKey: llm?.apiKey ?? '',
202
+ endpointOverride: llm?.endpoint,
203
+ currentModel: previous,
204
+ title: '/model — quick-swap',
205
+ badge: provider.label,
206
+ });
207
+ if (!result) {
208
+ console.log(chalk.yellow('\n /model cancelled.\n'));
209
+ return true;
239
210
  }
240
- console.log();
211
+ if (result.model === previous) {
212
+ console.log(chalk.gray(`\n Model unchanged (${previous}).\n`));
213
+ return true;
214
+ }
215
+ // Cross-provider sanity check — if the picked model looks like
216
+ // a different vendor's namespace (anthropic/*, google/*, etc.)
217
+ // and the active provider isn't a multi-vendor gateway, warn so
218
+ // the user doesn't hit a confusing 404 on the next turn.
219
+ if (looksLikeForeignModel(result.model, provider)) {
220
+ console.log(chalk.yellow(`\n ⚠ "${result.model}" looks like a different provider's namespace. ` +
221
+ `Active endpoint: ${provider.label}.` +
222
+ `\n Run /config provider <id> to switch endpoints, or /model again to pick a native model.\n`));
223
+ }
224
+ agent.setModel(result.model);
225
+ if (config.llm) {
226
+ config.llm.model = result.model;
227
+ saveConfig(config);
228
+ }
229
+ const sourceTag = result.source === 'live' ? `live · ${result.liveCount} models` :
230
+ result.source === 'fallback' ? `offline · static catalog (${result.liveError ?? 'unknown'})` :
231
+ 'static catalog';
232
+ console.log(chalk.green(`\n✓ Model switched: ${chalk.gray(previous)} → ${chalk.cyan(result.model)}`));
233
+ console.log(chalk.gray(` Source: ${sourceTag}\n`));
241
234
  return true;
242
235
  }
236
+ // /mcp moved to its own command file (commands/mcp.ts) as part of 0.3.6
237
+ // Item 11. The new dispatcher supports `/mcp list`, `/mcp reconnect`,
238
+ // and the original no-arg "show tools by namespace" behaviour is now
239
+ // covered by `/mcp tools` (handled in commands/mcp.ts).
243
240
  case '/copy':
244
241
  {
245
242
  if (!agent.lastAnswer) {
@@ -267,18 +264,18 @@ export async function tryHandleUiCommand(ctx) {
267
264
  {
268
265
  const prefs = readPreferences(agent.workspaceRoot);
269
266
  const arg = args.join(' ').trim();
267
+ const { SEGMENT_NAMES, isKnownSegment } = await import('../statusline.js');
270
268
  if (!arg) {
271
269
  console.log(chalk.bold('\nStatusline'));
272
270
  console.log(` Current: ${chalk.cyan(prefs.statusline)}`);
273
- console.log(chalk.gray(' Available segments: mode, branch, dirty, model, tokens, session'));
274
- console.log(chalk.gray(' Example: /statusline mode,branch,dirty,tokens\n'));
271
+ console.log(chalk.gray(` Available segments: ${SEGMENT_NAMES.join(', ')}`));
272
+ console.log(chalk.gray(' Example: /statusline mode,workflow,goal,model,session,plan\n'));
275
273
  return true;
276
274
  }
277
- const valid = new Set(['mode', 'branch', 'dirty', 'model', 'tokens', 'session']);
278
275
  const requested = arg.split(',').map((s) => s.trim()).filter(Boolean);
279
- const unknown = requested.filter((s) => !valid.has(s));
276
+ const unknown = requested.filter((s) => !isKnownSegment(s));
280
277
  if (unknown.length > 0) {
281
- console.log(chalk.red(`\nUnknown segment(s): ${unknown.join(', ')}. Valid: ${Array.from(valid).join(', ')}\n`));
278
+ console.log(chalk.red(`\nUnknown segment(s): ${unknown.join(', ')}. Valid: ${SEGMENT_NAMES.join(', ')}\n`));
282
279
  return true;
283
280
  }
284
281
  writePreferences(agent.workspaceRoot, { statusline: requested.join(',') });
@@ -373,6 +370,63 @@ export async function tryHandleUiCommand(ctx) {
373
370
  console.log(chalk.green(`\n✓ Raw scrollback ${next ? 'enabled' : 'disabled'}. Markdown rendering ${next ? 'OFF' : 'ON'} for next turn.\n`));
374
371
  return true;
375
372
  }
373
+ case '/effort':
374
+ {
375
+ const arg = (args[0] ?? '').toLowerCase();
376
+ const valid = ['low', 'medium', 'high'];
377
+ if (!arg) {
378
+ const resolved = resolveEffort(agent.workspaceRoot);
379
+ const sourceTag = resolved.source === 'env' ? chalk.gray(' (env: BRAINROUTER_EFFORT)') :
380
+ resolved.source === 'preference' ? chalk.gray(' (preference)') :
381
+ chalk.gray(' (default)');
382
+ console.log(chalk.bold(`\nReasoning depth: ${chalk.cyan(resolved.effort)}${sourceTag}`));
383
+ console.log(chalk.gray(' low — terse, one-paragraph answers; minimal ceremony.'));
384
+ console.log(chalk.gray(' medium — current default; no overlay, no provider reasoning slot. (default)'));
385
+ console.log(chalk.gray(' high — step-by-step reasoning; audits evidence before each tool call.'));
386
+ console.log(chalk.gray(' When the model supports it (gpt-5, o-series, gpt-oss, DeepSeek R1/V3+, Qwen3,'));
387
+ console.log(chalk.gray(' Magistral, *-reasoning, *-thinking — works on OpenAI, DeepSeek, OpenRouter,'));
388
+ console.log(chalk.gray(' LM Studio 0.3.29+, Ollama), the level is also forwarded as `reasoning_effort`.'));
389
+ console.log(chalk.gray(' Toggle with: /effort low | /effort medium | /effort high'));
390
+ console.log(chalk.gray(' Env override (one-shot): BRAINROUTER_EFFORT=high brainrouter\n'));
391
+ return true;
392
+ }
393
+ if (!valid.includes(arg)) {
394
+ console.log(chalk.red(`\nUnknown level "${arg}". Choose: ${valid.join(' | ')}\n`));
395
+ return true;
396
+ }
397
+ writePreferences(agent.workspaceRoot, { effort: arg });
398
+ agent.refreshSystemPrompt();
399
+ const after = resolveEffort(agent.workspaceRoot);
400
+ // Surface a friendly nudge when the env var would still shadow the new
401
+ // preference on the next process boot.
402
+ if (process.env.BRAINROUTER_EFFORT && after.source === 'env') {
403
+ console.log(chalk.yellow(`\n✓ Preference saved as ${arg}, but BRAINROUTER_EFFORT=${process.env.BRAINROUTER_EFFORT} is still active this process — env wins.\n`));
404
+ }
405
+ else {
406
+ console.log(chalk.green(`\n✓ Reasoning depth → ${arg}. Applies on the next turn.\n`));
407
+ }
408
+ return true;
409
+ }
410
+ case '/quiet':
411
+ {
412
+ const prefs = readPreferences(agent.workspaceRoot);
413
+ const arg = (args[0] ?? '').toLowerCase();
414
+ const next = arg ? (arg === 'on' || arg === 'true' || arg === '1') : !prefs.quiet;
415
+ writePreferences(agent.workspaceRoot, { quiet: next });
416
+ // `--quiet` set a one-shot env override at startup; once the user
417
+ // explicitly toggles in-session their choice wins from now on.
418
+ if (next) {
419
+ process.env.BRAINROUTER_QUIET = '1';
420
+ }
421
+ else {
422
+ delete process.env.BRAINROUTER_QUIET;
423
+ }
424
+ const detail = next
425
+ ? 'recall tables, briefing dumps, and tool-completion previews are now hidden.'
426
+ : 'full chrome restored — recall tables, previews, and briefings will print again.';
427
+ console.log(chalk.green(`\n✓ Quiet mode ${next ? 'enabled' : 'disabled'}: ${detail}\n`));
428
+ return true;
429
+ }
376
430
  case '/apps':
377
431
  case '/plugins':
378
432
  {
@@ -385,12 +439,22 @@ export async function tryHandleUiCommand(ctx) {
385
439
  console.log(chalk.gray(' Drop a folder under skills/<category>/<name>/SKILL.md to register one.\n'));
386
440
  return true;
387
441
  }
388
- for (const root of roots) {
389
- const entries = fs.readdirSync(root, { withFileTypes: true });
390
- for (const entry of entries) {
391
- if (!entry.isDirectory())
392
- continue;
393
- console.log(chalk.cyan(` ${path.relative(agent.workspaceRoot, path.join(root, entry.name))}`));
442
+ const skills = listFilesystemSkills(agent.workspaceRoot);
443
+ if (skills.length > 0) {
444
+ console.log(chalk.gray(' Skills'));
445
+ for (const skill of skills) {
446
+ const category = skill.category ? `${skill.category}/` : '';
447
+ console.log(` • ${chalk.cyan(`${category}${skill.name}`)} (${chalk.gray(skill.scope ?? 'filesystem')})`);
448
+ }
449
+ }
450
+ if (fs.existsSync(pluginsRoot)) {
451
+ const entries = fs.readdirSync(pluginsRoot, { withFileTypes: true });
452
+ const pluginDirs = entries.filter((entry) => entry.isDirectory());
453
+ if (pluginDirs.length > 0) {
454
+ console.log(chalk.gray(' Plugin folders'));
455
+ for (const entry of pluginDirs) {
456
+ console.log(` • ${chalk.cyan(path.relative(agent.workspaceRoot, path.join(pluginsRoot, entry.name)))}`);
457
+ }
394
458
  }
395
459
  }
396
460
  console.log();
@@ -468,6 +532,29 @@ export async function tryHandleUiCommand(ctx) {
468
532
  console.log(chalk.gray(' Tip: configure IDE to launch brainrouter with -w <workspace> so paths match.\n'));
469
533
  return true;
470
534
  }
535
+ case '/where':
536
+ {
537
+ const { gatherWhereInputs, renderWhere } = await import('../whereView.js');
538
+ const { resolveDisplayedMcpState } = await import('../banner.js');
539
+ const { resolveTheme } = await import('../theme.js');
540
+ const theme = resolveTheme(agent.workspaceRoot);
541
+ const displayedMcp = resolveDisplayedMcpState(config, mcpClient);
542
+ const briefing = agent.getLastBriefing();
543
+ const inputs = gatherWhereInputs({
544
+ workspaceRoot: agent.workspaceRoot,
545
+ sessionKey: agent.sessionKey,
546
+ model: agent.getModel(),
547
+ mcpProfile: displayedMcp.profile,
548
+ mcpTransport: displayedMcp.transport,
549
+ mcpOnline: displayedMcp.online,
550
+ mcpIdentity: displayedMcp.identity,
551
+ accessMode: agent.getAccessMode(),
552
+ recalledRecords: agent.getRecalledRecords(),
553
+ briefingSources: briefing.sources,
554
+ });
555
+ console.log('\n' + renderWhere(inputs, theme) + '\n');
556
+ return true;
557
+ }
471
558
  case '/help': {
472
559
  renderHelp(args[0]?.toLowerCase());
473
560
  return true;
@@ -475,3 +562,27 @@ export async function tryHandleUiCommand(ctx) {
475
562
  }
476
563
  return false;
477
564
  }
565
+ /**
566
+ * Heuristic — does the picked model id look like it belongs to a
567
+ * different vendor than the active provider's endpoint? Catches the
568
+ * common foot-gun of picking `anthropic/claude-*` while pointed at
569
+ * OpenAI direct, where the request 404s at the endpoint and the user
570
+ * has no obvious "you needed to switch endpoints" signal.
571
+ *
572
+ * Returns false for gateway providers (OpenRouter, "anthropic-via-gateway")
573
+ * since multi-vendor namespaces are expected there.
574
+ */
575
+ function looksLikeForeignModel(model, provider) {
576
+ // Gateways are vendor-agnostic by design.
577
+ if (provider.id === 'openrouter' || provider.id === 'anthropic-via-gateway')
578
+ return false;
579
+ const FOREIGN_PREFIXES = {
580
+ openai: ['anthropic/', 'google/', 'meta/', 'mistralai/', 'qwen/', 'deepseek/'],
581
+ deepseek: ['anthropic/', 'google/', 'openai/', 'meta/', 'mistralai/'],
582
+ gemini: ['anthropic/', 'openai/', 'meta/', 'mistralai/', 'deepseek/'],
583
+ lmstudio: [],
584
+ ollama: [],
585
+ };
586
+ const list = FOREIGN_PREFIXES[provider.id] ?? [];
587
+ return list.some((prefix) => model.startsWith(prefix));
588
+ }
@@ -3,4 +3,24 @@
3
3
  * Hand-tune imports if the compiler complains.
4
4
  */
5
5
  import type { CommandContext } from './_context.js';
6
+ import { type SkillListItem } from '../../prompt/skillCatalog.js';
7
+ /**
8
+ * Decide whether `/grill-me` should refuse to fire because the current
9
+ * workflow already has a written `spec.md`. The clarifying pass is meant to
10
+ * happen BEFORE the spec is committed — once a spec exists, asking again
11
+ * usually means we're re-litigating answers the user already gave, which
12
+ * wastes a turn. `--force` is the explicit escape hatch when the user
13
+ * genuinely wants a second clarifying pass (e.g., scope has drifted).
14
+ *
15
+ * Exported helper for unit tests so the guard logic can be exercised
16
+ * without standing up the whole REPL context. NOT pure: reads workflow
17
+ * state from disk (`getCurrentWorkflow`, `readArtifact`) and the latter
18
+ * may mkdirSync the workflow folder as a side effect.
19
+ */
20
+ export declare function shouldSkipGrillMe(workspaceRoot: string, force: boolean, sessionKey?: string): {
21
+ skip: boolean;
22
+ slug?: string;
23
+ specPath?: string;
24
+ };
6
25
  export declare function tryHandleWorkflowCommand(ctx: CommandContext): Promise<boolean>;
26
+ export declare function normalizeSkillsList(payload: any): SkillListItem[] | undefined;