@kinqs/brainrouter-cli 0.3.4
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 +109 -0
- package/README.md +185 -0
- package/dist/agent/agent.d.ts +765 -0
- package/dist/agent/agent.js +1977 -0
- package/dist/cli/cliPrompt.d.ts +15 -0
- package/dist/cli/cliPrompt.js +62 -0
- package/dist/cli/commands/_context.d.ts +53 -0
- package/dist/cli/commands/_context.js +14 -0
- package/dist/cli/commands/_helpers.d.ts +45 -0
- package/dist/cli/commands/_helpers.js +140 -0
- package/dist/cli/commands/guard.d.ts +6 -0
- package/dist/cli/commands/guard.js +292 -0
- package/dist/cli/commands/memory.d.ts +12 -0
- package/dist/cli/commands/memory.js +263 -0
- package/dist/cli/commands/obs.d.ts +6 -0
- package/dist/cli/commands/obs.js +208 -0
- package/dist/cli/commands/orchestration.d.ts +6 -0
- package/dist/cli/commands/orchestration.js +218 -0
- package/dist/cli/commands/session.d.ts +6 -0
- package/dist/cli/commands/session.js +191 -0
- package/dist/cli/commands/ui.d.ts +6 -0
- package/dist/cli/commands/ui.js +477 -0
- package/dist/cli/commands/workflow.d.ts +6 -0
- package/dist/cli/commands/workflow.js +691 -0
- package/dist/cli/repl.d.ts +12 -0
- package/dist/cli/repl.js +894 -0
- package/dist/config/config.d.ts +22 -0
- package/dist/config/config.js +105 -0
- package/dist/config/workspace.d.ts +7 -0
- package/dist/config/workspace.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +610 -0
- package/dist/memory/briefing.d.ts +46 -0
- package/dist/memory/briefing.js +152 -0
- package/dist/memory/consolidation.d.ts +60 -0
- package/dist/memory/consolidation.js +208 -0
- package/dist/memory/formatters.d.ts +38 -0
- package/dist/memory/formatters.js +102 -0
- package/dist/memory/mentions.d.ts +10 -0
- package/dist/memory/mentions.js +72 -0
- package/dist/orchestration/orchestrator.d.ts +36 -0
- package/dist/orchestration/orchestrator.js +71 -0
- package/dist/orchestration/roles.d.ts +11 -0
- package/dist/orchestration/roles.js +117 -0
- package/dist/orchestration/tools.d.ts +244 -0
- package/dist/orchestration/tools.js +528 -0
- package/dist/prompt/breadthHint.d.ts +48 -0
- package/dist/prompt/breadthHint.js +93 -0
- package/dist/prompt/compactor.d.ts +31 -0
- package/dist/prompt/compactor.js +112 -0
- package/dist/prompt/initAgentMd.d.ts +13 -0
- package/dist/prompt/initAgentMd.js +194 -0
- package/dist/prompt/skillRunner.d.ts +34 -0
- package/dist/prompt/skillRunner.js +146 -0
- package/dist/prompt/systemPrompt.d.ts +10 -0
- package/dist/prompt/systemPrompt.js +171 -0
- package/dist/runtime/clipboard.d.ts +17 -0
- package/dist/runtime/clipboard.js +52 -0
- package/dist/runtime/llmSemaphore.d.ts +30 -0
- package/dist/runtime/llmSemaphore.js +67 -0
- package/dist/runtime/loopRunner.d.ts +25 -0
- package/dist/runtime/loopRunner.js +79 -0
- package/dist/runtime/mcpClient.d.ts +156 -0
- package/dist/runtime/mcpClient.js +234 -0
- package/dist/runtime/mcpUtils.d.ts +36 -0
- package/dist/runtime/mcpUtils.js +64 -0
- package/dist/runtime/sandbox.d.ts +48 -0
- package/dist/runtime/sandbox.js +156 -0
- package/dist/runtime/tracing.d.ts +25 -0
- package/dist/runtime/tracing.js +91 -0
- package/dist/state/cliState.d.ts +59 -0
- package/dist/state/cliState.js +311 -0
- package/dist/state/goalStore.d.ts +174 -0
- package/dist/state/goalStore.js +410 -0
- package/dist/state/hookifyStore.d.ts +80 -0
- package/dist/state/hookifyStore.js +237 -0
- package/dist/state/hooksStore.d.ts +42 -0
- package/dist/state/hooksStore.js +71 -0
- package/dist/state/preferencesStore.d.ts +41 -0
- package/dist/state/preferencesStore.js +25 -0
- package/dist/state/sessionStore.d.ts +42 -0
- package/dist/state/sessionStore.js +193 -0
- package/dist/state/taskStore.d.ts +23 -0
- package/dist/state/taskStore.js +80 -0
- package/dist/state/workflowArtifacts.d.ts +33 -0
- package/dist/state/workflowArtifacts.js +139 -0
- package/package.json +71 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-related slash commands. All cases here are leaf operations against
|
|
3
|
+
* the MCP memory tools (search, recall, briefing inspection, scenes,
|
|
4
|
+
* forget, handover, explain, trace, failed, verify, audit, export, import,
|
|
5
|
+
* persona, skill-hints, diagnostics, working canvas) plus the pipeline
|
|
6
|
+
* toggle / consolidation operation.
|
|
7
|
+
*
|
|
8
|
+
* They mostly delegate to printMcpCall / printMemoryCards and write nothing
|
|
9
|
+
* back to the workspace except /export (which writes the JSON envelope).
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import ora from 'ora';
|
|
15
|
+
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
16
|
+
import { extractMemories, renderMemoryCards } from '../../memory/formatters.js';
|
|
17
|
+
import { consolidateMemories } from '../../memory/consolidation.js';
|
|
18
|
+
import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
|
|
19
|
+
import { printMcpCall, printMemoryCards } from './_helpers.js';
|
|
20
|
+
export async function tryHandleMemoryCommand(ctx) {
|
|
21
|
+
const { command, args, agent, mcpClient } = ctx;
|
|
22
|
+
switch (command) {
|
|
23
|
+
case '/memory': {
|
|
24
|
+
const query = args.join(' ').trim();
|
|
25
|
+
if (!query) {
|
|
26
|
+
console.log(chalk.red('\nUsage: /memory <query>\n'));
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
await printMemoryCards(mcpClient, 'memory_search', { query, sessionKey: agent.sessionKey }, `Memory search · "${query}"`);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
case '/recall': {
|
|
33
|
+
const query = args.join(' ').trim();
|
|
34
|
+
if (!query) {
|
|
35
|
+
console.log(chalk.red('\nUsage: /recall <query>\n'));
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
await printMemoryCards(mcpClient, 'memory_recall', { sessionKey: agent.sessionKey, query }, `Cognitive recall · "${query}"`);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
case '/briefing': {
|
|
42
|
+
const b = agent.getLastBriefing();
|
|
43
|
+
console.log(chalk.bold('\nLast Memory Briefing'));
|
|
44
|
+
if (b.sources.length === 0) {
|
|
45
|
+
console.log(chalk.yellow(' No briefing has been built yet. Start a turn or use /recall.'));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(` Sources queried: ${chalk.cyan(b.sources.join(', '))}`);
|
|
49
|
+
console.log(` Recalled record IDs (${b.recordIds.length}): ${chalk.gray(b.recordIds.slice(0, 10).join(', '))}${b.recordIds.length > 10 ? '…' : ''}`);
|
|
50
|
+
}
|
|
51
|
+
console.log();
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
case '/scenes': {
|
|
55
|
+
const res = await callMcpTool(mcpClient, 'memory_recall', { sessionKey: agent.sessionKey, query: 'list focus scenes' });
|
|
56
|
+
if (res.isError) {
|
|
57
|
+
console.log(chalk.red(`\nmemory_recall failed: ${res.text || '(no message)'}\n`));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const persona = res.parsed?.appendSystemContext ?? '';
|
|
61
|
+
const sceneRe = /Recent focus scenes:\s*\n([\s\S]*?)(\n\n|<\/scene-navigation>)/;
|
|
62
|
+
const m = sceneRe.exec(persona);
|
|
63
|
+
console.log(chalk.bold('\nActive focus scenes'));
|
|
64
|
+
if (m) {
|
|
65
|
+
for (const line of m[1].split('\n')) {
|
|
66
|
+
const trimmed = line.replace(/^\s+/, '').replace(/^-\s*/, '').trim();
|
|
67
|
+
if (trimmed)
|
|
68
|
+
console.log(` • ${chalk.cyan(trimmed)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(chalk.yellow(' (no scenes returned — recall may be empty)'));
|
|
73
|
+
}
|
|
74
|
+
const cards = extractMemories(res.parsed);
|
|
75
|
+
if (cards.length > 0) {
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(renderMemoryCards(cards, 'Related memories', 5));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
case '/forget': {
|
|
83
|
+
const id = args[0];
|
|
84
|
+
if (!id) {
|
|
85
|
+
console.log(chalk.red('\nUsage: /forget <recordId>\n'));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
await printMcpCall(mcpClient, 'memory_update', { recordId: id, status: 'archived' }, `Archive memory ${id}`);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
case '/handover': {
|
|
92
|
+
// Generate a compact continuation note from current task memories so
|
|
93
|
+
// the next session can pick up. Uses memory_handover.
|
|
94
|
+
await printMcpCall(mcpClient, 'memory_handover', { sessionKey: agent.sessionKey }, 'Session handover note');
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
case '/explain': {
|
|
98
|
+
const query = args.join(' ').trim();
|
|
99
|
+
if (!query) {
|
|
100
|
+
console.log(chalk.red('\nUsage: /explain <query>\n'));
|
|
101
|
+
console.log(chalk.gray(' Re-runs recall in explain mode: shows FTS hits, vector hits, RRF scores, type/skill boosts, reranker, graph expansion.\n'));
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
await printMcpCall(mcpClient, 'memory_explain_recall', { sessionKey: agent.sessionKey, query }, `Recall explanation · "${query}"`);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
case '/trace': {
|
|
108
|
+
const sub = args[0];
|
|
109
|
+
if (sub === 'save') {
|
|
110
|
+
const rest = args.slice(1).join(' ').trim();
|
|
111
|
+
if (!rest) {
|
|
112
|
+
console.log(chalk.red('\nUsage: /trace save <description>\n'));
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
await printMcpCall(mcpClient, 'memory_debug_trace_save', { content: rest }, 'Saved debug trace');
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (sub === 'search' || !sub) {
|
|
119
|
+
const query = args.slice(1).join(' ').trim();
|
|
120
|
+
if (sub !== 'search' && !query) {
|
|
121
|
+
console.log(chalk.red('\nUsage: /trace save <description> | /trace search <query>\n'));
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
await printMcpCall(mcpClient, 'memory_debug_trace_search', { query: query || '*' }, 'Prior debug traces');
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
console.log(chalk.red('\nUsage: /trace save <description> | /trace search <query>\n'));
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
case '/failed': {
|
|
131
|
+
const area = args.join(' ').trim();
|
|
132
|
+
await printMcpCall(mcpClient, 'memory_failed_attempts', area ? { area } : {}, `Past failed attempts${area ? ` · "${area}"` : ''}`);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
case '/verify': {
|
|
136
|
+
const id = args[0];
|
|
137
|
+
if (!id) {
|
|
138
|
+
console.log(chalk.red('\nUsage: /verify <recordId> [status] [confidence]\n'));
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
const status = args[1] || 'verified';
|
|
142
|
+
const confidence = args[2] ? Number(args[2]) : 0.9;
|
|
143
|
+
await printMcpCall(mcpClient, 'memory_verify', { recordId: id, verificationStatus: status, confidence }, `Verify ${id}`);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
case '/audit': {
|
|
147
|
+
await printMcpCall(mcpClient, 'memory_audit', { limit: 30 }, 'Recent memory audit log');
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
case '/export': {
|
|
151
|
+
const out = args[0] || `.brainrouter/cli/memory-export-${Date.now()}.json`;
|
|
152
|
+
const res = await callMcpTool(mcpClient, 'memory_export', {});
|
|
153
|
+
if (res.isError) {
|
|
154
|
+
console.log(chalk.red(`\nmemory_export failed: ${res.text}\n`));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
try {
|
|
158
|
+
fs.writeFileSync(path.resolve(agent.workspaceRoot, out), res.text, 'utf8');
|
|
159
|
+
console.log(chalk.green(`\n✓ Exported memory to ${out} (${res.text.length} chars)\n`));
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.log(chalk.red(`\nWrite failed: ${err.message}\n`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
case '/import': {
|
|
168
|
+
const src = args[0];
|
|
169
|
+
if (!src) {
|
|
170
|
+
console.log(chalk.red('\nUsage: /import <path-to-export.json>\n'));
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
let envelope;
|
|
174
|
+
try {
|
|
175
|
+
envelope = fs.readFileSync(path.resolve(agent.workspaceRoot, src), 'utf8');
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.log(chalk.red(`\nRead failed: ${err.message}\n`));
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
await printMcpCall(mcpClient, 'memory_import', { envelope }, `Import from ${src}`);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
case '/persona': {
|
|
185
|
+
const name = args.join(' ').trim();
|
|
186
|
+
if (!name) {
|
|
187
|
+
console.log(chalk.red('\nUsage: /persona <persona-name>\n'));
|
|
188
|
+
console.log(chalk.gray(' Example: /persona code-reviewer (see /skills for available personas)\n'));
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
await printMcpCall(mcpClient, 'get_persona', { name }, `Persona · ${name}`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
case '/skill-hints': {
|
|
195
|
+
const skill = args[0];
|
|
196
|
+
const hints = args.slice(1).join(' ').trim();
|
|
197
|
+
if (!skill || !hints) {
|
|
198
|
+
console.log(chalk.red('\nUsage: /skill-hints <skill-name> <hints>\n'));
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
await printMcpCall(mcpClient, 'memory_register_skill_hints', { skill, hints }, `Registered hints for ${skill}`);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
case '/diagnostics': {
|
|
205
|
+
await printMcpCall(mcpClient, 'memory_diagnostics', {}, 'Memory diagnostics');
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
case '/working': {
|
|
209
|
+
const sub = args[0];
|
|
210
|
+
if (sub === 'reset') {
|
|
211
|
+
const confirm = args[1];
|
|
212
|
+
if (confirm !== 'confirm') {
|
|
213
|
+
console.log(chalk.yellow('\n⚠ /working reset clears the working-memory canvas. Confirm with: /working reset confirm\n'));
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
await printMcpCall(mcpClient, 'memory_working_reset', { sessionKey: agent.sessionKey, workspacePath: agent.workspaceRoot }, 'Working memory reset');
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
await printMcpCall(mcpClient, 'memory_working_context', { sessionKey: agent.sessionKey, workspacePath: agent.workspaceRoot }, 'Working memory canvas');
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
case '/memories': {
|
|
223
|
+
const sub = args[0];
|
|
224
|
+
if (!sub || sub === 'status') {
|
|
225
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
226
|
+
console.log(chalk.bold('\nMemories pipeline'));
|
|
227
|
+
console.log(` Enabled: ${prefs.memoriesEnabled ? chalk.green('on') : chalk.gray('off')}`);
|
|
228
|
+
console.log(chalk.gray(' Subcommands:'));
|
|
229
|
+
console.log(chalk.gray(' /memories on | off — toggle the pipeline'));
|
|
230
|
+
console.log(chalk.gray(' /memories consolidate — write user/feedback/project/reference files'));
|
|
231
|
+
console.log(chalk.gray(' /memories status — show this view\n'));
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (sub === 'on' || sub === 'off') {
|
|
235
|
+
writePreferences(agent.workspaceRoot, { memoriesEnabled: sub === 'on' });
|
|
236
|
+
console.log(chalk.green(`\n✓ Memories pipeline ${sub === 'on' ? 'enabled' : 'disabled'}.\n`));
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (sub === 'consolidate') {
|
|
240
|
+
const spinner = ora(chalk.gray('Consolidating memories from MCP into filesystem artifacts...')).start();
|
|
241
|
+
try {
|
|
242
|
+
const result = await consolidateMemories(mcpClient, agent.workspaceRoot, { sessionKey: agent.sessionKey });
|
|
243
|
+
spinner.succeed(chalk.green(`Consolidated ${result.totalRecords} records.`));
|
|
244
|
+
console.log(chalk.bold('\nPer-type counts:'));
|
|
245
|
+
for (const [t, n] of Object.entries(result.perType)) {
|
|
246
|
+
console.log(` ${chalk.cyan(t.padEnd(10))} ${n}`);
|
|
247
|
+
}
|
|
248
|
+
console.log(chalk.bold('\nFiles written:'));
|
|
249
|
+
for (const f of result.files)
|
|
250
|
+
console.log(` ${chalk.gray(f)}`);
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
spinner.fail(chalk.red(`Consolidation failed: ${err.message}\n`));
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
console.log(chalk.red(`\nUnknown /memories subcommand "${sub}". Try: status, on, off, consolidate.\n`));
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-EXTRACTED from cli/repl.ts as part of the slash-command split.
|
|
3
|
+
* Hand-tune imports if the compiler complains.
|
|
4
|
+
*/
|
|
5
|
+
import type { CommandContext } from './_context.js';
|
|
6
|
+
export declare function tryHandleObsCommand(ctx: CommandContext): Promise<boolean>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-EXTRACTED from cli/repl.ts as part of the slash-command split.
|
|
3
|
+
* Hand-tune imports if the compiler complains.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { exec } from 'node:child_process';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { listSessions } from '../../orchestration/orchestrator.js';
|
|
10
|
+
import { readPreferences } from '../../state/preferencesStore.js';
|
|
11
|
+
import { readTranscriptEntries } from '../../state/sessionStore.js';
|
|
12
|
+
import { getCliStateFile } from '../../state/cliState.js';
|
|
13
|
+
import { formatTranscriptContent } from './_helpers.js';
|
|
14
|
+
export async function tryHandleObsCommand(ctx) {
|
|
15
|
+
const { command, args, agent, mcpClient, config, rl, repl } = ctx;
|
|
16
|
+
// 'ctx' alias to keep references to the old ReplContext name working
|
|
17
|
+
const replCtx = repl;
|
|
18
|
+
switch (command) {
|
|
19
|
+
case '/transcript':
|
|
20
|
+
{
|
|
21
|
+
const requestedSession = args.join(' ').trim();
|
|
22
|
+
const sessionKey = !requestedSession || requestedSession === 'main'
|
|
23
|
+
? agent.sessionKey
|
|
24
|
+
: requestedSession;
|
|
25
|
+
const entries = readTranscriptEntries(agent.workspaceRoot, sessionKey, 20);
|
|
26
|
+
console.log(chalk.bold(`\nTranscript: ${sessionKey}`));
|
|
27
|
+
if (entries.length === 0) {
|
|
28
|
+
console.log(chalk.yellow(' No transcript entries found.'));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const label = entry.name ? `${entry.role}:${entry.name}` : entry.role;
|
|
33
|
+
const text = formatTranscriptContent(entry.content ?? entry.tool_calls ?? '');
|
|
34
|
+
console.log(`${chalk.gray(entry.timestamp)} ${chalk.cyan(label)} ${chalk.gray(text)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
console.log();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
case '/watch':
|
|
41
|
+
{
|
|
42
|
+
const tracePath = process.env.BRAINROUTER_TRACE_LOG?.trim();
|
|
43
|
+
if (!tracePath) {
|
|
44
|
+
console.log(chalk.yellow('\nLive tracing is off. Enable with:'));
|
|
45
|
+
console.log(chalk.gray(' export BRAINROUTER_TRACE_LOG=' + path.join(agent.workspaceRoot, '.brainrouter/cli/trace.jsonl')));
|
|
46
|
+
console.log(chalk.gray(' (restart the CLI so the change takes effect)\n'));
|
|
47
|
+
console.log(chalk.gray('Without it, you can still see per-tool activity inline in this REPL,'));
|
|
48
|
+
console.log(chalk.gray('and child-agent tool calls now surface as "role:id → tool" lines.'));
|
|
49
|
+
console.log(chalk.gray('Use /agents and /agent <id> --full for the persisted child transcripts.\n'));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (!fs.existsSync(tracePath)) {
|
|
53
|
+
console.log(chalk.yellow(`\nTrace file does not exist yet: ${tracePath}\nIt will appear after the first turn.\n`));
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.bold(`\n📡 Tailing ${tracePath} — Ctrl+C to stop.\n`));
|
|
57
|
+
// Stream the last 30 lines + new appends as JSONL until the user
|
|
58
|
+
// interrupts with Ctrl+C. We use child_process tail because that's
|
|
59
|
+
// dramatically simpler than re-implementing inotify in Node.
|
|
60
|
+
const tail = exec(`tail -n 30 -f "${tracePath}"`);
|
|
61
|
+
const lineHandler = (chunk) => {
|
|
62
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
63
|
+
for (const raw of text.split('\n')) {
|
|
64
|
+
if (!raw.trim())
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
const e = JSON.parse(raw);
|
|
68
|
+
const attrs = e.attributes ?? {};
|
|
69
|
+
const dur = typeof e.duration_ms === 'number' ? chalk.gray(` ${e.duration_ms}ms`) : '';
|
|
70
|
+
const detail = Object.entries(attrs)
|
|
71
|
+
.slice(0, 4)
|
|
72
|
+
.map(([k, v]) => `${k}=${String(v).slice(0, 40)}`)
|
|
73
|
+
.join(' ');
|
|
74
|
+
console.log(`${chalk.gray(e.ts?.slice(11, 19) ?? '')} ${chalk.cyan(e.name)}${dur} ${chalk.gray(detail)}`);
|
|
75
|
+
}
|
|
76
|
+
catch { /* not JSON — print raw */
|
|
77
|
+
console.log(chalk.gray(raw));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
tail.stdout?.on('data', lineHandler);
|
|
82
|
+
tail.stderr?.on('data', (c) => process.stderr.write(c));
|
|
83
|
+
const onInterrupt = () => {
|
|
84
|
+
try {
|
|
85
|
+
tail.kill('SIGTERM');
|
|
86
|
+
}
|
|
87
|
+
catch { /* noop */ }
|
|
88
|
+
console.log(chalk.gray('\nwatch ended.\n'));
|
|
89
|
+
rl.off('SIGINT', onInterrupt);
|
|
90
|
+
rl.prompt();
|
|
91
|
+
};
|
|
92
|
+
rl.once('SIGINT', onInterrupt);
|
|
93
|
+
// Resume the prompt only after the user interrupts; otherwise the
|
|
94
|
+
// tail stays attached.
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
case '/tokens':
|
|
98
|
+
{
|
|
99
|
+
const session = agent.sessionUsage;
|
|
100
|
+
const metrics = agent.memoryMetrics;
|
|
101
|
+
const children = listSessions(agent.workspaceRoot).filter((s) => s.usage);
|
|
102
|
+
const childPrompt = children.reduce((acc, c) => acc + (c.usage?.promptTokens ?? 0), 0);
|
|
103
|
+
const childCompletion = children.reduce((acc, c) => acc + (c.usage?.completionTokens ?? 0), 0);
|
|
104
|
+
const childCalls = children.reduce((acc, c) => acc + (c.usage?.calls ?? 0), 0);
|
|
105
|
+
// Memory savings estimate:
|
|
106
|
+
// - Each recalled record (avg ~200 chars ≈ 50 tokens) supplies cross-
|
|
107
|
+
// session context that would otherwise require either a manual
|
|
108
|
+
// user explanation, a re-read of files, or skill re-discovery.
|
|
109
|
+
// Conservative multiplier of 5× to account for the "without memory
|
|
110
|
+
// you would have read 3-5 files" replacement cost.
|
|
111
|
+
// - Offloaded child output bytes are subtracted from what the parent
|
|
112
|
+
// would otherwise have had to carry in context.
|
|
113
|
+
const recallSavings = metrics.briefingTokensInjected * 5;
|
|
114
|
+
const offloadSavings = Math.round(metrics.offloadCharsAvoided / 4);
|
|
115
|
+
const totalSaved = recallSavings + offloadSavings;
|
|
116
|
+
const totalSpent = session.promptTokens + session.completionTokens + childPrompt + childCompletion;
|
|
117
|
+
console.log(chalk.bold('\nToken usage — this session'));
|
|
118
|
+
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'})`)}`);
|
|
119
|
+
if (children.length > 0) {
|
|
120
|
+
console.log(` Children (${children.length}): ${chalk.cyan(childPrompt.toLocaleString())}↑ ${chalk.cyan(childCompletion.toLocaleString())}↓ ${chalk.gray(`(${childCalls} LLM call${childCalls === 1 ? '' : 's'})`)}`);
|
|
121
|
+
for (const c of children.slice(0, 5)) {
|
|
122
|
+
const u = c.usage;
|
|
123
|
+
console.log(chalk.gray(` · ${c.id} (${c.role}): ${u.promptTokens.toLocaleString()}↑ ${u.completionTokens.toLocaleString()}↓`));
|
|
124
|
+
}
|
|
125
|
+
if (children.length > 5)
|
|
126
|
+
console.log(chalk.gray(` …and ${children.length - 5} more (see /agents)`));
|
|
127
|
+
}
|
|
128
|
+
console.log(` Total this session: ${chalk.bold.cyan(totalSpent.toLocaleString())} tokens`);
|
|
129
|
+
console.log(chalk.bold('\nMemory savings (estimated)'));
|
|
130
|
+
console.log(` Briefing tokens injected: ${chalk.gray(metrics.briefingTokensInjected.toLocaleString())} (${metrics.recallRecordsConsulted} records consulted)`);
|
|
131
|
+
console.log(` Cross-session recall value: ~${chalk.green(recallSavings.toLocaleString())} tokens you'd otherwise spend re-reading files / re-explaining context`);
|
|
132
|
+
console.log(` Offload bytes avoided: ${chalk.gray(metrics.offloadCharsAvoided.toLocaleString())} chars (large child outputs that stayed out of parent context)`);
|
|
133
|
+
console.log(` → Offload value: ~${chalk.green(offloadSavings.toLocaleString())} tokens`);
|
|
134
|
+
console.log(` ${chalk.bold('Total estimated savings:')} ${chalk.bold.green('~' + totalSaved.toLocaleString())} tokens`);
|
|
135
|
+
if (totalSpent > 0) {
|
|
136
|
+
const ratio = totalSaved / totalSpent;
|
|
137
|
+
console.log(chalk.gray(` Ratio: for every 1 token spent, memory saved ~${ratio.toFixed(2)} tokens of context.`));
|
|
138
|
+
}
|
|
139
|
+
console.log(chalk.gray('\n (Estimates use a 5× multiplier on briefing tokens — a heuristic for "you would have needed to re-derive this from files/prompts otherwise". Treat as directional, not exact.)\n'));
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
case '/feedback':
|
|
143
|
+
{
|
|
144
|
+
// Personal CLI state lives under the user-global brainrouter home (per
|
|
145
|
+
// README's storage contract), NOT inside the workspace — writing
|
|
146
|
+
// feedback.jsonl into the project tree risks accidental commits and
|
|
147
|
+
// breaks the "workflows are the only thing written inside the project"
|
|
148
|
+
// guarantee. Route through getCliStateFile() so the path becomes
|
|
149
|
+
// ~/.brainrouter/workspaces/<encoded>/cli/feedback.jsonl.
|
|
150
|
+
const msg = args.join(' ').trim();
|
|
151
|
+
const file = getCliStateFile(agent.workspaceRoot, 'feedback.jsonl');
|
|
152
|
+
const entry = {
|
|
153
|
+
ts: new Date().toISOString(),
|
|
154
|
+
sessionKey: agent.sessionKey,
|
|
155
|
+
model: agent.getModel(),
|
|
156
|
+
accessMode: agent.getAccessMode(),
|
|
157
|
+
message: msg || '(no message provided)',
|
|
158
|
+
};
|
|
159
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8');
|
|
160
|
+
console.log(chalk.green(`\n✓ Feedback recorded at ${file}`));
|
|
161
|
+
console.log(chalk.gray(' This stays in your user-global brainrouter home — share by copying the file into a GitHub issue.\n'));
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
case '/rollout':
|
|
165
|
+
{
|
|
166
|
+
const { getSessionStateDir } = await import('../../state/cliState.js');
|
|
167
|
+
const sessionDir = getSessionStateDir(agent.workspaceRoot, agent.sessionKey);
|
|
168
|
+
console.log(chalk.bold('\nSession bucket'));
|
|
169
|
+
console.log(` Session: ${chalk.cyan(agent.sessionKey)}`);
|
|
170
|
+
console.log(` Directory: ${chalk.blue(sessionDir)}`);
|
|
171
|
+
const interestingFiles = ['transcript.jsonl', 'goal.json', 'tasks.json'];
|
|
172
|
+
console.log(chalk.bold('\nFiles in bucket:'));
|
|
173
|
+
let printedAny = false;
|
|
174
|
+
for (const name of interestingFiles) {
|
|
175
|
+
const full = path.join(sessionDir, name);
|
|
176
|
+
if (fs.existsSync(full)) {
|
|
177
|
+
const stat = fs.statSync(full);
|
|
178
|
+
console.log(` ${chalk.cyan(name.padEnd(18))} ${chalk.gray(`${stat.size} bytes · modified ${stat.mtime.toISOString()}`)}`);
|
|
179
|
+
printedAny = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!printedAny)
|
|
183
|
+
console.log(chalk.gray(' (empty — files appear after you set a goal, update a plan, or run a turn)'));
|
|
184
|
+
console.log();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
case '/debug-config':
|
|
188
|
+
{
|
|
189
|
+
console.log(chalk.bold('\nConfig layers (in order of precedence)'));
|
|
190
|
+
console.log(` Workspace: ${chalk.cyan(agent.workspaceRoot)}`);
|
|
191
|
+
console.log(` CLI state: ${chalk.cyan(path.join(agent.workspaceRoot, '.brainrouter/cli'))}`);
|
|
192
|
+
console.log(` Profile: ${chalk.cyan(config.activeServer)}`);
|
|
193
|
+
console.log(` Server: ${chalk.cyan(JSON.stringify(config.servers[config.activeServer], null, 2).split('\n').map((l) => ' ' + l).join('\n').trim())}`);
|
|
194
|
+
console.log(chalk.bold('\nEnvironment'));
|
|
195
|
+
const flags = ['BRAINROUTER_SANDBOX', 'BRAINROUTER_SANDBOX_READ_PATHS', 'BRAINROUTER_SANDBOX_WRITE_PATHS', 'BRAINROUTER_SANDBOX_NETWORK', 'BRAINROUTER_TRACE_LOG', 'BRAINROUTER_MAX_TOOL_LOOPS', 'BRAINROUTER_LLM_TIMEOUT_MS', 'BRAINROUTER_WORKSPACE'];
|
|
196
|
+
for (const f of flags) {
|
|
197
|
+
const v = process.env[f];
|
|
198
|
+
if (v)
|
|
199
|
+
console.log(` ${chalk.cyan(f)} = ${v}`);
|
|
200
|
+
}
|
|
201
|
+
console.log(chalk.bold('\nPreferences'));
|
|
202
|
+
console.log(chalk.gray(JSON.stringify(readPreferences(agent.workspaceRoot), null, 2)));
|
|
203
|
+
console.log();
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-EXTRACTED from cli/repl.ts as part of the slash-command split.
|
|
3
|
+
* Hand-tune imports if the compiler complains.
|
|
4
|
+
*/
|
|
5
|
+
import type { CommandContext } from './_context.js';
|
|
6
|
+
export declare function tryHandleOrchestrationCommand(ctx: CommandContext): Promise<boolean>;
|