@kinqs/brainrouter-cli 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +55 -48
- package/bin/cli.cjs +71 -0
- package/dist/agent/agent.d.ts +212 -2
- package/dist/agent/agent.js +428 -38
- package/dist/cli/banner.d.ts +60 -0
- package/dist/cli/banner.js +199 -0
- package/dist/cli/cliPrompt.d.ts +69 -0
- package/dist/cli/cliPrompt.js +287 -0
- package/dist/cli/commands/_helpers.js +6 -6
- package/dist/cli/commands/guard.js +75 -10
- package/dist/cli/commands/mcp.d.ts +17 -0
- package/dist/cli/commands/mcp.js +121 -0
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/obs.js +22 -22
- package/dist/cli/commands/session.js +13 -5
- package/dist/cli/commands/ui.js +97 -45
- package/dist/cli/commands/workflow.d.ts +18 -0
- package/dist/cli/commands/workflow.js +314 -43
- package/dist/cli/repl.js +219 -132
- package/dist/cli/spinner.d.ts +34 -0
- package/dist/cli/spinner.js +36 -0
- package/dist/cli/statusline.d.ts +67 -0
- package/dist/cli/statusline.js +204 -0
- package/dist/cli/theme.d.ts +79 -0
- package/dist/cli/theme.js +106 -0
- package/dist/cli/whereView.d.ts +81 -0
- package/dist/cli/whereView.js +245 -0
- package/dist/config/config.d.ts +40 -0
- package/dist/config/config.js +45 -73
- package/dist/index.js +80 -13
- package/dist/memory/briefing.d.ts +10 -0
- package/dist/memory/briefing.js +69 -1
- package/dist/prompt/breadthHint.d.ts +5 -0
- package/dist/prompt/breadthHint.js +44 -0
- package/dist/prompt/systemPrompt.d.ts +34 -0
- package/dist/prompt/systemPrompt.js +124 -108
- package/dist/runtime/dangerousCommand.d.ts +53 -0
- package/dist/runtime/dangerousCommand.js +105 -0
- package/dist/runtime/mcpClient.d.ts +38 -1
- package/dist/runtime/mcpClient.js +90 -2
- package/dist/state/goalStore.d.ts +98 -17
- package/dist/state/goalStore.js +132 -42
- package/dist/state/preferencesStore.d.ts +67 -3
- package/dist/state/preferencesStore.js +84 -1
- package/dist/state/workflowArtifacts.d.ts +63 -2
- package/dist/state/workflowArtifacts.js +120 -8
- package/dist/tests/_helpers.d.ts +31 -0
- package/dist/tests/_helpers.js +91 -0
- package/package.json +5 -4
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* wrapper.
|
|
9
9
|
*/
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
-
import
|
|
11
|
+
import { spinner } from '../spinner.js';
|
|
12
12
|
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
13
13
|
import { clampPayload, extractMemories, renderMemoryCards } from '../../memory/formatters.js';
|
|
14
14
|
import { buildSkillPrompt, resolveSkill, SLASH_TO_SKILL } from '../../prompt/skillRunner.js';
|
|
@@ -19,9 +19,9 @@ import { buildSkillPrompt, resolveSkill, SLASH_TO_SKILL } from '../../prompt/ski
|
|
|
19
19
|
* raw output only when no records can be parsed.
|
|
20
20
|
*/
|
|
21
21
|
export async function printMemoryCards(mcpClient, toolName, args, heading) {
|
|
22
|
-
const
|
|
22
|
+
const s = spinner(chalk.gray(`${toolName}…`)).start();
|
|
23
23
|
const res = await callMcpTool(mcpClient, toolName, args);
|
|
24
|
-
|
|
24
|
+
s.stop();
|
|
25
25
|
console.log();
|
|
26
26
|
if (res.isError) {
|
|
27
27
|
console.log(chalk.red(`${heading}: tool error — ${res.text || '(no message)'}`));
|
|
@@ -44,9 +44,9 @@ export async function printMemoryCards(mcpClient, toolName, args, heading) {
|
|
|
44
44
|
* the tool's text output under a heading.
|
|
45
45
|
*/
|
|
46
46
|
export async function printMcpCall(mcpClient, toolName, args, heading) {
|
|
47
|
-
const
|
|
47
|
+
const s = spinner(chalk.gray(`${toolName}…`)).start();
|
|
48
48
|
const res = await callMcpTool(mcpClient, toolName, args);
|
|
49
|
-
|
|
49
|
+
s.stop();
|
|
50
50
|
console.log(chalk.bold(`\n${heading}`));
|
|
51
51
|
if (res.isError) {
|
|
52
52
|
console.log(chalk.red(` Tool error: ${res.text || '(no message)'}`));
|
|
@@ -113,7 +113,7 @@ export async function runSkillCommand(agent, mcpClient, slashCommand, userInput,
|
|
|
113
113
|
await runSkillByName(agent, mcpClient, skillName, userInput, orchestration, runTurn);
|
|
114
114
|
}
|
|
115
115
|
export async function runSkillByName(agent, mcpClient, skillName, userInput, orchestration, runTurn) {
|
|
116
|
-
const loader =
|
|
116
|
+
const loader = spinner(chalk.gray(`Loading skill: ${skillName}...`)).start();
|
|
117
117
|
let prompt;
|
|
118
118
|
try {
|
|
119
119
|
const skill = await resolveSkill(mcpClient, skillName, agent.workspaceRoot, 'full');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
-
import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
|
|
8
|
+
import { applyYoloOff, applyYoloOn, readPreferences, writePreferences } from '../../state/preferencesStore.js';
|
|
9
9
|
import { addHook, readHooks, removeHook, setHookEnabled } from '../../state/hooksStore.js';
|
|
10
10
|
import { createHookifyRule, deleteHookifyRule, listHookifyRules, toggleHookifyRule } from '../../state/hookifyStore.js';
|
|
11
11
|
export async function tryHandleGuardCommand(ctx) {
|
|
@@ -79,26 +79,91 @@ export async function tryHandleGuardCommand(ctx) {
|
|
|
79
79
|
console.log(chalk.red('\nUsage: /hooks [list | add <event> <cmd> | remove <id> | enable <id> | disable <id>]\n'));
|
|
80
80
|
return true;
|
|
81
81
|
}
|
|
82
|
-
case '/
|
|
82
|
+
case '/mode':
|
|
83
|
+
{
|
|
84
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
85
|
+
const arg = (args[0] ?? '').toLowerCase();
|
|
86
|
+
if (!arg) {
|
|
87
|
+
console.log(chalk.bold(`\nExecution mode: ${chalk.cyan(prefs.executionMode)}`));
|
|
88
|
+
console.log(chalk.gray(' planning — run_command asks before executing; agent leans toward clarify-before-act. (default)'));
|
|
89
|
+
console.log(chalk.gray(' fast — run_command auto-approves safe commands (dangerous ones still ask); agent jumps to implementation.'));
|
|
90
|
+
console.log(chalk.gray(' Toggle with: /mode planning | /mode fast\n'));
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (arg !== 'planning' && arg !== 'fast') {
|
|
94
|
+
console.log(chalk.red(`\nUnknown mode "${arg}". Choose: planning | fast\n`));
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
writePreferences(agent.workspaceRoot, { executionMode: arg });
|
|
98
|
+
agent.refreshSystemPrompt();
|
|
99
|
+
ctx.repl.refreshPromptForMode();
|
|
100
|
+
if (arg === 'fast') {
|
|
101
|
+
console.log(chalk.yellow(`\n✓ /mode fast — run_command auto-approves safe commands.`));
|
|
102
|
+
console.log(chalk.gray(' Dangerous commands (rm -rf, sudo, force-push, …) still prompt for confirmation.'));
|
|
103
|
+
console.log(chalk.gray(' Pair with /permissions write (no shell) or BRAINROUTER_SANDBOX=on for tighter guardrails.\n'));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log(chalk.green(`\n✓ /mode planning — run_command asks before each shell call.\n`));
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
case '/review-policy':
|
|
83
111
|
{
|
|
84
112
|
const prefs = readPreferences(agent.workspaceRoot);
|
|
85
113
|
const arg = (args[0] ?? '').toLowerCase();
|
|
86
114
|
if (!arg) {
|
|
87
|
-
console.log(chalk.bold(`\
|
|
88
|
-
console.log(chalk.gray('
|
|
89
|
-
console.log(chalk.gray('
|
|
115
|
+
console.log(chalk.bold(`\nReview policy: ${chalk.cyan(prefs.reviewPolicy)}`));
|
|
116
|
+
console.log(chalk.gray(' request — at workflow/multi-file gates, agent surfaces the plan and waits for /approve. (default)'));
|
|
117
|
+
console.log(chalk.gray(' proceed — agent applies the plan and reports after; use /approve manually for explicit gates.'));
|
|
118
|
+
console.log(chalk.gray(' Toggle with: /review-policy request | /review-policy proceed\n'));
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (arg !== 'request' && arg !== 'proceed') {
|
|
122
|
+
console.log(chalk.red(`\nUnknown policy "${arg}". Choose: request | proceed\n`));
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
writePreferences(agent.workspaceRoot, { reviewPolicy: arg });
|
|
126
|
+
agent.refreshSystemPrompt();
|
|
127
|
+
if (arg === 'proceed') {
|
|
128
|
+
console.log(chalk.yellow(`\n✓ /review-policy proceed — agent will apply plans without halting for prose approval.`));
|
|
129
|
+
console.log(chalk.gray(' /approve still works as an explicit gesture for workflows that need one.\n'));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.log(chalk.green(`\n✓ /review-policy request — agent will summarize and ask before applying multi-file changes.\n`));
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
case '/yolo':
|
|
137
|
+
{
|
|
138
|
+
// /yolo is a one-release alias for `/mode fast` + `/review-policy proceed`.
|
|
139
|
+
// We keep it because the muscle memory is established; new docs point to
|
|
140
|
+
// the two split commands for finer control.
|
|
141
|
+
const arg = (args[0] ?? '').toLowerCase();
|
|
142
|
+
if (!arg) {
|
|
143
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
144
|
+
const yoloOn = prefs.executionMode === 'fast' && prefs.reviewPolicy === 'proceed';
|
|
145
|
+
console.log(chalk.bold(`\nYolo (alias): ${yoloOn ? chalk.red('ON') : chalk.green('off')}`));
|
|
146
|
+
console.log(chalk.gray(' Shorthand for `/mode fast` + `/review-policy proceed` — flip both axes at once.'));
|
|
147
|
+
console.log(chalk.gray(` Current state: mode=${prefs.executionMode}, review-policy=${prefs.reviewPolicy}`));
|
|
148
|
+
console.log(chalk.gray(' Use /mode and /review-policy directly for finer control.'));
|
|
90
149
|
console.log(chalk.gray(' Toggle with: /yolo on | /yolo off\n'));
|
|
91
150
|
return true;
|
|
92
151
|
}
|
|
93
152
|
const next = arg === 'on' || arg === 'true' || arg === '1';
|
|
94
|
-
writePreferences(agent.workspaceRoot, { autoApproveShell: next });
|
|
95
153
|
if (next) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
154
|
+
applyYoloOn(agent.workspaceRoot);
|
|
155
|
+
agent.refreshSystemPrompt();
|
|
156
|
+
ctx.repl.refreshPromptForMode();
|
|
157
|
+
console.log(chalk.red('\n⚠ /yolo ON — shorthand for `/mode fast` + `/review-policy proceed`.'));
|
|
158
|
+
console.log(chalk.gray(' run_command will auto-approve safe commands; dangerous ones still prompt.'));
|
|
159
|
+
console.log(chalk.gray(' Agent will apply multi-file plans without the prose "ready?" pause.'));
|
|
160
|
+
console.log(chalk.gray(' Use /mode and /review-policy for finer control next time.\n'));
|
|
99
161
|
}
|
|
100
162
|
else {
|
|
101
|
-
|
|
163
|
+
applyYoloOff(agent.workspaceRoot);
|
|
164
|
+
agent.refreshSystemPrompt();
|
|
165
|
+
ctx.repl.refreshPromptForMode();
|
|
166
|
+
console.log(chalk.green('\n✓ /yolo off — restored /mode planning + /review-policy request.\n'));
|
|
102
167
|
}
|
|
103
168
|
return true;
|
|
104
169
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.3.6 item 11: `/mcp` slash-command surface. Scope-limited foundation —
|
|
3
|
+
* the full multi-MCP federation (parallel cross-MCP tool calls, MCP
|
|
4
|
+
* marketplace, capability tiers) is deferred to 0.4.0. What ships here:
|
|
5
|
+
*
|
|
6
|
+
* /mcp — show status of the active MCP (alias for /mcp list)
|
|
7
|
+
* /mcp list — list every configured profile with identity + status
|
|
8
|
+
* /mcp reconnect — reconnect the currently-active profile
|
|
9
|
+
* /mcp tools — list MCP tools grouped by namespace (pre-Item-11
|
|
10
|
+
* `/mcp` no-arg behaviour, moved here verbatim)
|
|
11
|
+
*
|
|
12
|
+
* The reconnect path leans on `mcpClient.close()` + `mcpClient.connect()`
|
|
13
|
+
* with the same config the CLI launched against — no plumbing required
|
|
14
|
+
* for the user beyond typing the command.
|
|
15
|
+
*/
|
|
16
|
+
import type { CommandContext } from './_context.js';
|
|
17
|
+
export declare function tryHandleMcpCommand(ctx: CommandContext): Promise<boolean>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.3.6 item 11: `/mcp` slash-command surface. Scope-limited foundation —
|
|
3
|
+
* the full multi-MCP federation (parallel cross-MCP tool calls, MCP
|
|
4
|
+
* marketplace, capability tiers) is deferred to 0.4.0. What ships here:
|
|
5
|
+
*
|
|
6
|
+
* /mcp — show status of the active MCP (alias for /mcp list)
|
|
7
|
+
* /mcp list — list every configured profile with identity + status
|
|
8
|
+
* /mcp reconnect — reconnect the currently-active profile
|
|
9
|
+
* /mcp tools — list MCP tools grouped by namespace (pre-Item-11
|
|
10
|
+
* `/mcp` no-arg behaviour, moved here verbatim)
|
|
11
|
+
*
|
|
12
|
+
* The reconnect path leans on `mcpClient.close()` + `mcpClient.connect()`
|
|
13
|
+
* with the same config the CLI launched against — no plumbing required
|
|
14
|
+
* for the user beyond typing the command.
|
|
15
|
+
*/
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import { spinner as makeSpinner } from '../spinner.js';
|
|
18
|
+
export async function tryHandleMcpCommand(ctx) {
|
|
19
|
+
const { command, args, mcpClient, config } = ctx;
|
|
20
|
+
if (command !== '/mcp')
|
|
21
|
+
return false;
|
|
22
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
23
|
+
if (sub === 'tools') {
|
|
24
|
+
// Pre-Item-11 `/mcp` no-arg behaviour: namespace-grouped tool listing.
|
|
25
|
+
// Kept verbatim under a subcommand so the muscle memory survives.
|
|
26
|
+
const profileName = config.activeServer;
|
|
27
|
+
const server = config.servers[profileName];
|
|
28
|
+
console.log(chalk.bold('\nMCP server'));
|
|
29
|
+
console.log(` Profile: ${chalk.green(profileName)} (${chalk.cyan(server?.type ?? 'unknown')})`);
|
|
30
|
+
if (server?.type === 'http') {
|
|
31
|
+
console.log(` URL: ${chalk.blue(server.url ?? '')}`);
|
|
32
|
+
}
|
|
33
|
+
else if (server?.type === 'stdio') {
|
|
34
|
+
console.log(` Cmd: ${chalk.blue(server.command ?? '')} ${server.args?.join(' ') || ''}`);
|
|
35
|
+
}
|
|
36
|
+
const spinner = makeSpinner(chalk.gray('Fetching MCP tool surface...')).start();
|
|
37
|
+
try {
|
|
38
|
+
const res = await mcpClient.listTools();
|
|
39
|
+
const tools = res.tools || [];
|
|
40
|
+
spinner.succeed(chalk.green(`${tools.length} MCP tools available`));
|
|
41
|
+
const namespaces = {};
|
|
42
|
+
for (const t of tools) {
|
|
43
|
+
const parts = (t.name || '').split('_');
|
|
44
|
+
const ns = parts.length > 1 ? parts[0] : 'misc';
|
|
45
|
+
(namespaces[ns] ||= []).push(t.name);
|
|
46
|
+
}
|
|
47
|
+
for (const ns of Object.keys(namespaces).sort()) {
|
|
48
|
+
console.log(`\n ${chalk.bold.cyan(ns)} (${namespaces[ns].length})`);
|
|
49
|
+
for (const name of namespaces[ns].sort()) {
|
|
50
|
+
console.log(` ${chalk.gray('•')} ${name}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (sub === 'list') {
|
|
61
|
+
const activeName = config.activeServer;
|
|
62
|
+
const profiles = Object.keys(config.servers ?? {});
|
|
63
|
+
if (profiles.length === 0) {
|
|
64
|
+
console.log(chalk.yellow('\nNo MCP profiles configured. Run `brainrouter login` or `brainrouter config` to set one up.\n'));
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk.bold('\nConfigured MCP profiles'));
|
|
68
|
+
for (const name of profiles) {
|
|
69
|
+
const profile = config.servers[name];
|
|
70
|
+
const isActive = name === activeName;
|
|
71
|
+
// Identity: explicit config field > live wrapper > 'unknown'.
|
|
72
|
+
let identity = profile.identity ?? 'unknown';
|
|
73
|
+
if (isActive && typeof mcpClient.getIdentity === 'function') {
|
|
74
|
+
identity = mcpClient.getIdentity();
|
|
75
|
+
}
|
|
76
|
+
const onlineLabel = isActive
|
|
77
|
+
? (mcpClient.isConnected() ? chalk.green('online') : chalk.red('offline'))
|
|
78
|
+
: chalk.gray('idle');
|
|
79
|
+
const idLabel = identity === 'brainrouter'
|
|
80
|
+
? chalk.cyan('brainrouter')
|
|
81
|
+
: identity === 'third-party'
|
|
82
|
+
? chalk.yellow('third-party')
|
|
83
|
+
: chalk.gray('unknown');
|
|
84
|
+
const marker = isActive ? chalk.bold('★ ') : ' ';
|
|
85
|
+
const transport = profile.type;
|
|
86
|
+
const target = profile.type === 'http' ? profile.url ?? '<no url>' : profile.command ?? '<no command>';
|
|
87
|
+
console.log(`${marker}${chalk.bold(name)} ${idLabel} ${transport} ${onlineLabel} ${chalk.gray(target)}`);
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.gray('\n★ = active profile. /mcp reconnect to refresh the active connection.\n'));
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (sub === 'reconnect') {
|
|
93
|
+
const activeName = config.activeServer;
|
|
94
|
+
const profile = config.servers?.[activeName];
|
|
95
|
+
if (!profile) {
|
|
96
|
+
console.log(chalk.red(`\nNo active MCP profile to reconnect (\`activeServer\` = ${JSON.stringify(activeName)}).\n`));
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
console.log(chalk.gray(`Reconnecting "${activeName}"…`));
|
|
100
|
+
try {
|
|
101
|
+
try {
|
|
102
|
+
await mcpClient.close();
|
|
103
|
+
}
|
|
104
|
+
catch { /* idempotent */ }
|
|
105
|
+
await mcpClient.connect(profile, config.llm, activeName);
|
|
106
|
+
// Re-probe tools so identity tagging and the prompt's tool list refresh.
|
|
107
|
+
try {
|
|
108
|
+
await mcpClient.listTools();
|
|
109
|
+
}
|
|
110
|
+
catch { /* tool-list failure is non-fatal */ }
|
|
111
|
+
console.log(chalk.green(`✓ Reconnected to "${activeName}" (${profile.type}).\n`));
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.log(chalk.red(`✗ Reconnect failed: ${err?.message ?? err}\n`));
|
|
115
|
+
console.log(chalk.gray('The CLI stays in offline mode. Check the MCP server, then try `/mcp reconnect` again.\n'));
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
console.log(chalk.red(`\nUnknown /mcp subcommand "${sub}". Usage: /mcp list | /mcp reconnect | /mcp tools\n`));
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import
|
|
14
|
+
import { spinner as makeSpinner } from '../spinner.js';
|
|
15
15
|
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
16
16
|
import { extractMemories, renderMemoryCards } from '../../memory/formatters.js';
|
|
17
17
|
import { consolidateMemories } from '../../memory/consolidation.js';
|
|
@@ -237,7 +237,7 @@ export async function tryHandleMemoryCommand(ctx) {
|
|
|
237
237
|
return true;
|
|
238
238
|
}
|
|
239
239
|
if (sub === 'consolidate') {
|
|
240
|
-
const spinner =
|
|
240
|
+
const spinner = makeSpinner(chalk.gray('Consolidating memories from MCP into filesystem artifacts...')).start();
|
|
241
241
|
try {
|
|
242
242
|
const result = await consolidateMemories(mcpClient, agent.workspaceRoot, { sessionKey: agent.sessionKey });
|
|
243
243
|
spinner.succeed(chalk.green(`Consolidated ${result.totalRecords} records.`));
|
package/dist/cli/commands/obs.js
CHANGED
|
@@ -98,21 +98,23 @@ export async function tryHandleObsCommand(ctx) {
|
|
|
98
98
|
{
|
|
99
99
|
const session = agent.sessionUsage;
|
|
100
100
|
const metrics = agent.memoryMetrics;
|
|
101
|
-
|
|
101
|
+
// Scope to the live parent: sessions.json is workspace-wide and
|
|
102
|
+
// persists across CLI restarts, so an unfiltered list mixes in every
|
|
103
|
+
// child spawned by every prior CLI process. Filtering by
|
|
104
|
+
// parentSessionKey limits the row to children spawned by THIS parent.
|
|
105
|
+
const children = listSessions(agent.workspaceRoot).filter((s) => s.usage && s.parentSessionKey === agent.sessionKey);
|
|
102
106
|
const childPrompt = children.reduce((acc, c) => acc + (c.usage?.promptTokens ?? 0), 0);
|
|
103
107
|
const childCompletion = children.reduce((acc, c) => acc + (c.usage?.completionTokens ?? 0), 0);
|
|
104
108
|
const childCalls = children.reduce((acc, c) => acc + (c.usage?.calls ?? 0), 0);
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
const offloadSavings = Math.round(metrics.offloadCharsAvoided / 4);
|
|
115
|
-
const totalSaved = recallSavings + offloadSavings;
|
|
109
|
+
// What we can actually measure:
|
|
110
|
+
// - offload: bytes of child output that did NOT land in the parent's
|
|
111
|
+
// context. These are real tokens not spent on the parent. We
|
|
112
|
+
// subtract the preview that DID land (OFFLOAD_PREVIEW_CHARS is
|
|
113
|
+
// already netted out in tools.ts before recordOffload fires).
|
|
114
|
+
// - briefing tokens: cost, not savings. They're already counted in
|
|
115
|
+
// session.promptTokens. We report them so the user can see how
|
|
116
|
+
// much of the prompt budget memory is consuming.
|
|
117
|
+
const offloadSavedTokens = Math.round(metrics.offloadCharsAvoided / 4);
|
|
116
118
|
const totalSpent = session.promptTokens + session.completionTokens + childPrompt + childCompletion;
|
|
117
119
|
console.log(chalk.bold('\nToken usage — this session'));
|
|
118
120
|
console.log(` Parent: ${chalk.cyan(session.promptTokens.toLocaleString())}↑ ${chalk.cyan(session.completionTokens.toLocaleString())}↓ ${chalk.gray(`(${session.turns} turn${session.turns === 1 ? '' : 's'}, ${session.calls} LLM call${session.calls === 1 ? '' : 's'})`)}`);
|
|
@@ -126,17 +128,15 @@ export async function tryHandleObsCommand(ctx) {
|
|
|
126
128
|
console.log(chalk.gray(` …and ${children.length - 5} more (see /agents)`));
|
|
127
129
|
}
|
|
128
130
|
console.log(` Total this session: ${chalk.bold.cyan(totalSpent.toLocaleString())} tokens`);
|
|
129
|
-
console.log(chalk.bold('\nMemory
|
|
130
|
-
console.log(` Briefing tokens injected:
|
|
131
|
-
console.log(`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const ratio = totalSaved / totalSpent;
|
|
137
|
-
console.log(chalk.gray(` Ratio: for every 1 token spent, memory saved ~${ratio.toFixed(2)} tokens of context.`));
|
|
131
|
+
console.log(chalk.bold('\nMemory'));
|
|
132
|
+
console.log(` Briefing tokens injected: ${chalk.gray(metrics.briefingTokensInjected.toLocaleString())} ${chalk.gray(`(${metrics.recallRecordsConsulted} records consulted — already included in parent ↑)`)}`);
|
|
133
|
+
console.log(` Child output offloaded: ${chalk.gray(metrics.offloadCharsAvoided.toLocaleString())} chars ${chalk.gray(`(≈${offloadSavedTokens.toLocaleString()} parent tokens not spent)`)}`);
|
|
134
|
+
if (offloadSavedTokens > 0 && totalSpent > 0) {
|
|
135
|
+
const ratio = offloadSavedTokens / totalSpent;
|
|
136
|
+
const display = ratio >= 0.01 ? ratio.toFixed(2) : '<0.01';
|
|
137
|
+
console.log(chalk.gray(` Offload ratio: ~${display} saved per token spent.`));
|
|
138
138
|
}
|
|
139
|
-
console.log(chalk.gray('\n (
|
|
139
|
+
console.log(chalk.gray('\n (Offload is measured; briefing tokens are an information-gain stat, not a savings number.)\n'));
|
|
140
140
|
return true;
|
|
141
141
|
}
|
|
142
142
|
case '/feedback':
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import
|
|
7
|
+
import { spinner as makeSpinner } from '../spinner.js';
|
|
8
8
|
import { marked } from 'marked';
|
|
9
9
|
import { listTranscripts, loadTranscript } from '../../state/sessionStore.js';
|
|
10
10
|
import { readGoal, resumeGoal } from '../../state/goalStore.js';
|
|
@@ -51,6 +51,11 @@ export async function tryHandleSessionCommand(ctx) {
|
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
53
|
agent.sessionKey = sessionKey;
|
|
54
|
+
// The persisted transcript doesn't record per-call token usage, so
|
|
55
|
+
// we can't reconstruct counters for the resumed session — start
|
|
56
|
+
// counting from this point forward instead of carrying over the
|
|
57
|
+
// pre-resume parent counts (which were for a different session).
|
|
58
|
+
agent.resetSessionCounters();
|
|
54
59
|
const loaded = agent.loadHistory(entries);
|
|
55
60
|
console.log(chalk.green(`\n✓ Resumed session ${chalk.cyan(sessionKey)} with ${loaded} prior messages.`));
|
|
56
61
|
// If the resumed session has a goal that was suspended (paused,
|
|
@@ -73,9 +78,12 @@ export async function tryHandleSessionCommand(ctx) {
|
|
|
73
78
|
// before/after. Just unpause and kick off the next iteration.
|
|
74
79
|
const reactivated = resumeGoal(agent.workspaceRoot, sessionKey);
|
|
75
80
|
if (reactivated) {
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
81
|
+
// 9d: pre-9d this branch had to drop a `goal-budget-steering`
|
|
82
|
+
// tagged system message left over from a budget-trigger pause.
|
|
83
|
+
// That message no longer exists — the wrap-up directive is
|
|
84
|
+
// folded into the goal-anchor and the anchor is re-rendered
|
|
85
|
+
// by the next runTurn. `refreshSystemPrompt` is still useful
|
|
86
|
+
// here to rebuild any overlays that depend on the active goal.
|
|
79
87
|
agent.refreshSystemPrompt();
|
|
80
88
|
console.log(chalk.green(`\n▶ Goal resumed (${reactivated.budget.iterationsUsed}/${reactivated.budget.maxIterations} used). Starting next iteration…\n`));
|
|
81
89
|
ctx.repl.runAgentTurn(buildGoalKickoffPrompt(reactivated, 'resume'));
|
|
@@ -123,7 +131,7 @@ export async function tryHandleSessionCommand(ctx) {
|
|
|
123
131
|
}
|
|
124
132
|
case '/compact':
|
|
125
133
|
{
|
|
126
|
-
const spinner =
|
|
134
|
+
const spinner = makeSpinner(chalk.gray('Summarizing conversation for compaction...')).start();
|
|
127
135
|
try {
|
|
128
136
|
const result = await agent.compactHistory();
|
|
129
137
|
if (!result) {
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -6,11 +6,11 @@ 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
|
|
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
15
|
import { getConfigPath } from '../../config/config.js';
|
|
16
16
|
import { copyToClipboard } from '../../runtime/clipboard.js';
|
|
@@ -41,7 +41,7 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
41
41
|
console.log(` LLM Endpoint: ${chalk.blue(llm.endpoint)}`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
const spinner =
|
|
44
|
+
const spinner = makeSpinner(chalk.gray('Querying diagnostics & testing latency...')).start();
|
|
45
45
|
try {
|
|
46
46
|
const start = Date.now();
|
|
47
47
|
const testRes = await mcpClient.callTool('list_skills', { scope: 'local' });
|
|
@@ -116,7 +116,7 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
116
116
|
else {
|
|
117
117
|
console.log(` Endpoint: ${chalk.blue(server.url)}`);
|
|
118
118
|
}
|
|
119
|
-
const spinner =
|
|
119
|
+
const spinner = makeSpinner(chalk.gray('Checking MCP tool surface...')).start();
|
|
120
120
|
try {
|
|
121
121
|
const startedAt = Date.now();
|
|
122
122
|
const res = await mcpClient.listTools();
|
|
@@ -204,42 +204,10 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
204
204
|
console.log(chalk.green(`\n✓ Model switched: ${chalk.gray(previous)} → ${chalk.cyan(newModel)}\n`));
|
|
205
205
|
return true;
|
|
206
206
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
239
|
-
}
|
|
240
|
-
console.log();
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
207
|
+
// /mcp moved to its own command file (commands/mcp.ts) as part of 0.3.6
|
|
208
|
+
// Item 11. The new dispatcher supports `/mcp list`, `/mcp reconnect`,
|
|
209
|
+
// and the original no-arg "show tools by namespace" behaviour is now
|
|
210
|
+
// covered by `/mcp tools` (handled in commands/mcp.ts).
|
|
243
211
|
case '/copy':
|
|
244
212
|
{
|
|
245
213
|
if (!agent.lastAnswer) {
|
|
@@ -267,18 +235,18 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
267
235
|
{
|
|
268
236
|
const prefs = readPreferences(agent.workspaceRoot);
|
|
269
237
|
const arg = args.join(' ').trim();
|
|
238
|
+
const { SEGMENT_NAMES, isKnownSegment } = await import('../statusline.js');
|
|
270
239
|
if (!arg) {
|
|
271
240
|
console.log(chalk.bold('\nStatusline'));
|
|
272
241
|
console.log(` Current: ${chalk.cyan(prefs.statusline)}`);
|
|
273
|
-
console.log(chalk.gray(
|
|
274
|
-
console.log(chalk.gray(' Example: /statusline mode,
|
|
242
|
+
console.log(chalk.gray(` Available segments: ${SEGMENT_NAMES.join(', ')}`));
|
|
243
|
+
console.log(chalk.gray(' Example: /statusline mode,workflow,goal,model,session,plan\n'));
|
|
275
244
|
return true;
|
|
276
245
|
}
|
|
277
|
-
const valid = new Set(['mode', 'branch', 'dirty', 'model', 'tokens', 'session']);
|
|
278
246
|
const requested = arg.split(',').map((s) => s.trim()).filter(Boolean);
|
|
279
|
-
const unknown = requested.filter((s) => !
|
|
247
|
+
const unknown = requested.filter((s) => !isKnownSegment(s));
|
|
280
248
|
if (unknown.length > 0) {
|
|
281
|
-
console.log(chalk.red(`\nUnknown segment(s): ${unknown.join(', ')}. Valid: ${
|
|
249
|
+
console.log(chalk.red(`\nUnknown segment(s): ${unknown.join(', ')}. Valid: ${SEGMENT_NAMES.join(', ')}\n`));
|
|
282
250
|
return true;
|
|
283
251
|
}
|
|
284
252
|
writePreferences(agent.workspaceRoot, { statusline: requested.join(',') });
|
|
@@ -373,6 +341,63 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
373
341
|
console.log(chalk.green(`\n✓ Raw scrollback ${next ? 'enabled' : 'disabled'}. Markdown rendering ${next ? 'OFF' : 'ON'} for next turn.\n`));
|
|
374
342
|
return true;
|
|
375
343
|
}
|
|
344
|
+
case '/effort':
|
|
345
|
+
{
|
|
346
|
+
const arg = (args[0] ?? '').toLowerCase();
|
|
347
|
+
const valid = ['low', 'medium', 'high'];
|
|
348
|
+
if (!arg) {
|
|
349
|
+
const resolved = resolveEffort(agent.workspaceRoot);
|
|
350
|
+
const sourceTag = resolved.source === 'env' ? chalk.gray(' (env: BRAINROUTER_EFFORT)') :
|
|
351
|
+
resolved.source === 'preference' ? chalk.gray(' (preference)') :
|
|
352
|
+
chalk.gray(' (default)');
|
|
353
|
+
console.log(chalk.bold(`\nReasoning depth: ${chalk.cyan(resolved.effort)}${sourceTag}`));
|
|
354
|
+
console.log(chalk.gray(' low — terse, one-paragraph answers; minimal ceremony.'));
|
|
355
|
+
console.log(chalk.gray(' medium — current default; no overlay, no provider reasoning slot. (default)'));
|
|
356
|
+
console.log(chalk.gray(' high — step-by-step reasoning; audits evidence before each tool call.'));
|
|
357
|
+
console.log(chalk.gray(' When the model supports it (gpt-5, o-series, gpt-oss, DeepSeek R1/V3+, Qwen3,'));
|
|
358
|
+
console.log(chalk.gray(' Magistral, *-reasoning, *-thinking — works on OpenAI, DeepSeek, OpenRouter,'));
|
|
359
|
+
console.log(chalk.gray(' LM Studio 0.3.29+, Ollama), the level is also forwarded as `reasoning_effort`.'));
|
|
360
|
+
console.log(chalk.gray(' Toggle with: /effort low | /effort medium | /effort high'));
|
|
361
|
+
console.log(chalk.gray(' Env override (one-shot): BRAINROUTER_EFFORT=high brainrouter\n'));
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
if (!valid.includes(arg)) {
|
|
365
|
+
console.log(chalk.red(`\nUnknown level "${arg}". Choose: ${valid.join(' | ')}\n`));
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
writePreferences(agent.workspaceRoot, { effort: arg });
|
|
369
|
+
agent.refreshSystemPrompt();
|
|
370
|
+
const after = resolveEffort(agent.workspaceRoot);
|
|
371
|
+
// Surface a friendly nudge when the env var would still shadow the new
|
|
372
|
+
// preference on the next process boot.
|
|
373
|
+
if (process.env.BRAINROUTER_EFFORT && after.source === 'env') {
|
|
374
|
+
console.log(chalk.yellow(`\n✓ Preference saved as ${arg}, but BRAINROUTER_EFFORT=${process.env.BRAINROUTER_EFFORT} is still active this process — env wins.\n`));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
console.log(chalk.green(`\n✓ Reasoning depth → ${arg}. Applies on the next turn.\n`));
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
case '/quiet':
|
|
382
|
+
{
|
|
383
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
384
|
+
const arg = (args[0] ?? '').toLowerCase();
|
|
385
|
+
const next = arg ? (arg === 'on' || arg === 'true' || arg === '1') : !prefs.quiet;
|
|
386
|
+
writePreferences(agent.workspaceRoot, { quiet: next });
|
|
387
|
+
// `--quiet` set a one-shot env override at startup; once the user
|
|
388
|
+
// explicitly toggles in-session their choice wins from now on.
|
|
389
|
+
if (next) {
|
|
390
|
+
process.env.BRAINROUTER_QUIET = '1';
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
delete process.env.BRAINROUTER_QUIET;
|
|
394
|
+
}
|
|
395
|
+
const detail = next
|
|
396
|
+
? 'recall tables, briefing dumps, and tool-completion previews are now hidden.'
|
|
397
|
+
: 'full chrome restored — recall tables, previews, and briefings will print again.';
|
|
398
|
+
console.log(chalk.green(`\n✓ Quiet mode ${next ? 'enabled' : 'disabled'}: ${detail}\n`));
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
376
401
|
case '/apps':
|
|
377
402
|
case '/plugins':
|
|
378
403
|
{
|
|
@@ -468,6 +493,33 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
468
493
|
console.log(chalk.gray(' Tip: configure IDE to launch brainrouter with -w <workspace> so paths match.\n'));
|
|
469
494
|
return true;
|
|
470
495
|
}
|
|
496
|
+
case '/where':
|
|
497
|
+
{
|
|
498
|
+
const { gatherWhereInputs, renderWhere } = await import('../whereView.js');
|
|
499
|
+
const { resolveTheme } = await import('../theme.js');
|
|
500
|
+
const theme = resolveTheme(agent.workspaceRoot);
|
|
501
|
+
const profileName = config.activeServer;
|
|
502
|
+
const server = config.servers[profileName];
|
|
503
|
+
const briefing = agent.getLastBriefing();
|
|
504
|
+
const inputs = gatherWhereInputs({
|
|
505
|
+
workspaceRoot: agent.workspaceRoot,
|
|
506
|
+
sessionKey: agent.sessionKey,
|
|
507
|
+
model: agent.getModel(),
|
|
508
|
+
mcpProfile: profileName,
|
|
509
|
+
mcpTransport: server?.type ?? 'unknown',
|
|
510
|
+
mcpOnline: mcpClient.isConnected(),
|
|
511
|
+
// 10c: identity flows from the live wrapper; falls back to the
|
|
512
|
+
// config field when present, otherwise 'unknown'.
|
|
513
|
+
mcpIdentity: typeof mcpClient.getIdentity === 'function'
|
|
514
|
+
? mcpClient.getIdentity()
|
|
515
|
+
: (server?.identity ?? 'unknown'),
|
|
516
|
+
accessMode: agent.getAccessMode(),
|
|
517
|
+
recalledRecords: agent.getRecalledRecords(),
|
|
518
|
+
briefingSources: briefing.sources,
|
|
519
|
+
});
|
|
520
|
+
console.log('\n' + renderWhere(inputs, theme) + '\n');
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
471
523
|
case '/help': {
|
|
472
524
|
renderHelp(args[0]?.toLowerCase());
|
|
473
525
|
return true;
|