@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,477 @@
|
|
|
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 { execSync } from 'node:child_process';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { LOCAL_TOOLS } from '../../agent/agent.js';
|
|
11
|
+
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
12
|
+
import { listSessions, reconcileStale } from '../../orchestration/orchestrator.js';
|
|
13
|
+
import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
|
|
14
|
+
import { readPlan } from '../../state/taskStore.js';
|
|
15
|
+
import { getConfigPath } from '../../config/config.js';
|
|
16
|
+
import { copyToClipboard } from '../../runtime/clipboard.js';
|
|
17
|
+
import { initAgentMd } from '../../prompt/initAgentMd.js';
|
|
18
|
+
import { completeWorkspacePath, renderHelp } from '../repl.js';
|
|
19
|
+
export async function tryHandleUiCommand(ctx) {
|
|
20
|
+
const { command, args, agent, mcpClient, config, rl, repl } = ctx;
|
|
21
|
+
// 'ctx' alias to keep references to the old ReplContext name working
|
|
22
|
+
const replCtx = repl;
|
|
23
|
+
switch (command) {
|
|
24
|
+
case '/status':
|
|
25
|
+
{
|
|
26
|
+
console.log(chalk.bold('\n🖥️ BrainRouter Status:'));
|
|
27
|
+
const activeServerName = config.activeServer;
|
|
28
|
+
const server = config.servers[activeServerName];
|
|
29
|
+
console.log(` Active Server: ${chalk.green(activeServerName)} (Type: ${chalk.cyan(server.type)})`);
|
|
30
|
+
if (server.type === 'http') {
|
|
31
|
+
console.log(` Endpoint URL: ${chalk.blue(server.url)}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(` Command: ${chalk.blue(server.command)} ${server.args?.join(' ') || ''}`);
|
|
35
|
+
}
|
|
36
|
+
const llm = config.llm;
|
|
37
|
+
if (llm) {
|
|
38
|
+
console.log(` LLM Provider: ${chalk.green(llm.provider)}`);
|
|
39
|
+
console.log(` LLM Model: ${chalk.cyan(llm.model)}`);
|
|
40
|
+
if (llm.endpoint) {
|
|
41
|
+
console.log(` LLM Endpoint: ${chalk.blue(llm.endpoint)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const spinner = ora(chalk.gray('Querying diagnostics & testing latency...')).start();
|
|
45
|
+
try {
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
const testRes = await mcpClient.callTool('list_skills', { scope: 'local' });
|
|
48
|
+
const latency = Date.now() - start;
|
|
49
|
+
spinner.succeed(chalk.green(`Latency check: ${latency}ms`));
|
|
50
|
+
// Diagnostics / memory stats
|
|
51
|
+
const diag = await callMcpTool(mcpClient, 'memory_diagnostics', {});
|
|
52
|
+
if (!diag.isError && diag.parsed) {
|
|
53
|
+
const stats = diag.parsed.databaseStats?.userStats;
|
|
54
|
+
if (stats) {
|
|
55
|
+
console.log(chalk.bold('\n📊 Cognitive Memory Database Stats:'));
|
|
56
|
+
console.log(` Total Memories: ${chalk.yellow(stats.totalCount ?? 0)}`);
|
|
57
|
+
console.log(` - Instructions: ${chalk.gray(stats.typeCounts?.instruction ?? 0)}`);
|
|
58
|
+
console.log(` - Codebase Facts: ${chalk.gray(stats.typeCounts?.codebase_fact ?? 0)}`);
|
|
59
|
+
console.log(` - Architectures: ${chalk.gray(stats.typeCounts?.architecture_decision ?? 0)}`);
|
|
60
|
+
console.log(` Total Focus Scenes: ${chalk.yellow(stats.totalScenes ?? 0)}`);
|
|
61
|
+
console.log(` Working Memory Items: ${chalk.yellow(stats.workingMemoryCount ?? 0)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
spinner.fail(chalk.red('Failed to fetch diagnostics.'));
|
|
67
|
+
console.warn(chalk.yellow(` Warning: ${err.message}`));
|
|
68
|
+
}
|
|
69
|
+
console.log();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
case '/workspace':
|
|
73
|
+
{
|
|
74
|
+
console.log(chalk.bold('\nWorkspace:'));
|
|
75
|
+
console.log(` Root: ${chalk.blue(agent.workspaceRoot)}`);
|
|
76
|
+
console.log(` Launch CWD: ${chalk.gray(agent.launchCwd)}`);
|
|
77
|
+
console.log(` Session: ${chalk.green(agent.sessionKey)}`);
|
|
78
|
+
console.log();
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
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
|
+
}
|
|
102
|
+
case '/doctor':
|
|
103
|
+
{
|
|
104
|
+
console.log(chalk.bold('\nBrainRouter Doctor:'));
|
|
105
|
+
console.log(` Config file: ${chalk.blue(getConfigPath())}`);
|
|
106
|
+
console.log(` Active profile: ${chalk.green(config.activeServer)}`);
|
|
107
|
+
const server = config.servers[config.activeServer];
|
|
108
|
+
if (!server) {
|
|
109
|
+
console.log(chalk.red(' Server profile: missing'));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
console.log(` Server profile: ${chalk.green(server.type)}`);
|
|
113
|
+
if (server.type === 'stdio') {
|
|
114
|
+
console.log(` Launch command: ${chalk.blue(server.command)} ${server.args?.join(' ') || ''}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.log(` Endpoint: ${chalk.blue(server.url)}`);
|
|
118
|
+
}
|
|
119
|
+
const spinner = ora(chalk.gray('Checking MCP tool surface...')).start();
|
|
120
|
+
try {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
const res = await mcpClient.listTools();
|
|
123
|
+
const latency = Date.now() - startedAt;
|
|
124
|
+
spinner.succeed(chalk.green(`MCP connection healthy (${latency}ms)`));
|
|
125
|
+
console.log(` MCP tools: ${chalk.yellow(res.tools?.length ?? 0)}`);
|
|
126
|
+
const toolNames = new Set((res.tools || []).map((tool) => tool.name));
|
|
127
|
+
const memoryTools = ['memory_recall', 'memory_capture_turn', 'memory_working_offload'];
|
|
128
|
+
for (const name of memoryTools) {
|
|
129
|
+
const hasTool = toolNames.has(name);
|
|
130
|
+
console.log(` ${name}: ${hasTool ? chalk.green('available') : chalk.yellow('not exposed')}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
spinner.fail(chalk.red('MCP connection check failed.'));
|
|
135
|
+
console.warn(chalk.yellow(` Warning: ${err.message}`));
|
|
136
|
+
}
|
|
137
|
+
// Memory health: are captures actually being extracted into searchable
|
|
138
|
+
// cognitive records, or are they piling up in sensory_stream? This is
|
|
139
|
+
// the silent failure mode that makes briefings return "0 records" — the
|
|
140
|
+
// CLI shows 💾 Captured after every turn but the LLM the extractor
|
|
141
|
+
// needs may not be configured in the MCP child env.
|
|
142
|
+
try {
|
|
143
|
+
const diagRes = await callMcpTool(mcpClient, 'memory_diagnostics', {});
|
|
144
|
+
const ext = diagRes.parsed?.databaseStats?.userStats?.extraction;
|
|
145
|
+
if (ext) {
|
|
146
|
+
const errs = ext.extractionErrors ?? 0;
|
|
147
|
+
const pending = ext.unextractedCount ?? 0;
|
|
148
|
+
const total = diagRes.parsed?.databaseStats?.userStats?.total ?? 0;
|
|
149
|
+
const headline = errs > 0
|
|
150
|
+
? chalk.red(` Memory extraction: DEGRADED — ${errs} consecutive failures`)
|
|
151
|
+
: pending > 5
|
|
152
|
+
? chalk.yellow(` Memory extraction: backlog of ${pending} sensory rows pending`)
|
|
153
|
+
: chalk.green(` Memory extraction: healthy (${total} cognitive records, ${pending} pending)`);
|
|
154
|
+
console.log(headline);
|
|
155
|
+
if (ext.lastErrorMessage) {
|
|
156
|
+
console.log(chalk.gray(` Last error: ${String(ext.lastErrorMessage).slice(0, 160)}`));
|
|
157
|
+
}
|
|
158
|
+
if (errs > 0 || !diagRes.parsed?.envKeys?.some?.((k) => /BRAINROUTER_LLM_API_KEY|OPENAI_API_KEY/.test(k))) {
|
|
159
|
+
console.log(chalk.gray(' Hint: set OPENAI_API_KEY (or BRAINROUTER_LLM_API_KEY) before launching brainrouter so the MCP child can run extraction.'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
console.log(chalk.yellow(` Memory extraction: unable to query (${err?.message ?? err})`));
|
|
165
|
+
}
|
|
166
|
+
const plan = readPlan(agent.workspaceRoot, agent.sessionKey);
|
|
167
|
+
console.log(` Plan items: ${chalk.yellow(plan.items.length)} (updated: ${chalk.gray(plan.updatedAt || 'never')})`);
|
|
168
|
+
const reconciled = reconcileStale(agent.workspaceRoot);
|
|
169
|
+
if (reconciled > 0)
|
|
170
|
+
console.log(` Reconciled ${chalk.yellow(reconciled)} stale child session(s).`);
|
|
171
|
+
const childSessions = listSessions(agent.workspaceRoot);
|
|
172
|
+
console.log(` Child sessions: ${chalk.yellow(childSessions.length)} total`);
|
|
173
|
+
const orchestrationTools = ['spawn_agent', 'list_agents', 'wait_agent', 'read_agent_transcript', 'close_agent', 'update_plan'];
|
|
174
|
+
for (const tn of orchestrationTools) {
|
|
175
|
+
const has = LOCAL_TOOLS.some((lt) => lt.name === tn);
|
|
176
|
+
console.log(` ${tn}: ${has ? chalk.green('available') : chalk.red('missing')}`);
|
|
177
|
+
}
|
|
178
|
+
console.log();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
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
|
+
}
|
|
194
|
+
case '/model':
|
|
195
|
+
{
|
|
196
|
+
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
|
+
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
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
239
|
+
}
|
|
240
|
+
console.log();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
case '/copy':
|
|
244
|
+
{
|
|
245
|
+
if (!agent.lastAnswer) {
|
|
246
|
+
console.log(chalk.yellow('\nNo response yet to copy.\n'));
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
const result = await copyToClipboard(agent.lastAnswer);
|
|
250
|
+
if (result.ok) {
|
|
251
|
+
console.log(chalk.green(`\n✓ Copied last response to clipboard via ${result.tool} (${agent.lastAnswer.length} chars).\n`));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
console.log(chalk.yellow(`\nClipboard tool unavailable (${result.error}). Selecting the text above with your terminal still works.\n`));
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
case '/vim':
|
|
259
|
+
{
|
|
260
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
261
|
+
const next = prefs.editorMode === 'vi' ? 'emacs' : 'vi';
|
|
262
|
+
writePreferences(agent.workspaceRoot, { editorMode: next });
|
|
263
|
+
console.log(chalk.green(`\n✓ Editor mode → ${next}. Restart the CLI to apply.\n`));
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
case '/statusline':
|
|
267
|
+
{
|
|
268
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
269
|
+
const arg = args.join(' ').trim();
|
|
270
|
+
if (!arg) {
|
|
271
|
+
console.log(chalk.bold('\nStatusline'));
|
|
272
|
+
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'));
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
const valid = new Set(['mode', 'branch', 'dirty', 'model', 'tokens', 'session']);
|
|
278
|
+
const requested = arg.split(',').map((s) => s.trim()).filter(Boolean);
|
|
279
|
+
const unknown = requested.filter((s) => !valid.has(s));
|
|
280
|
+
if (unknown.length > 0) {
|
|
281
|
+
console.log(chalk.red(`\nUnknown segment(s): ${unknown.join(', ')}. Valid: ${Array.from(valid).join(', ')}\n`));
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
writePreferences(agent.workspaceRoot, { statusline: requested.join(',') });
|
|
285
|
+
ctx.repl.refreshPromptForMode();
|
|
286
|
+
console.log(chalk.green(`\n✓ Statusline set to: ${requested.join(',')}\n`));
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
case '/theme':
|
|
290
|
+
{
|
|
291
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
292
|
+
const arg = (args[0] ?? '').toLowerCase();
|
|
293
|
+
const valid = new Set(['auto', 'light', 'dark', 'mono']);
|
|
294
|
+
if (!arg) {
|
|
295
|
+
console.log(chalk.bold('\nTheme'));
|
|
296
|
+
console.log(` Current: ${chalk.cyan(prefs.theme)}`);
|
|
297
|
+
console.log(chalk.gray(` Available: ${Array.from(valid).join(', ')}`));
|
|
298
|
+
console.log(chalk.gray(' Set with: /theme <name>\n'));
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
if (!valid.has(arg)) {
|
|
302
|
+
console.log(chalk.red(`\nUnknown theme "${arg}". Choose: ${Array.from(valid).join(', ')}\n`));
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
writePreferences(agent.workspaceRoot, { theme: arg });
|
|
306
|
+
console.log(chalk.green(`\n✓ Theme → ${arg}\n`));
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
case '/title':
|
|
310
|
+
{
|
|
311
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
312
|
+
const arg = args.join(' ').trim();
|
|
313
|
+
if (!arg) {
|
|
314
|
+
console.log(chalk.bold('\nTerminal title'));
|
|
315
|
+
console.log(` Current: ${chalk.cyan(prefs.terminalTitle)}`);
|
|
316
|
+
console.log(chalk.gray(' Segments: model, branch, session, mode (use "off" to disable)'));
|
|
317
|
+
console.log(chalk.gray(' Example: /title model,session\n'));
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
writePreferences(agent.workspaceRoot, { terminalTitle: arg });
|
|
321
|
+
try {
|
|
322
|
+
if (arg.toLowerCase() !== 'off') {
|
|
323
|
+
const segs = arg.split(',').map((s) => s.trim()).filter(Boolean);
|
|
324
|
+
const parts = [];
|
|
325
|
+
for (const seg of segs) {
|
|
326
|
+
if (seg === 'model')
|
|
327
|
+
parts.push(agent.getModel());
|
|
328
|
+
else if (seg === 'session')
|
|
329
|
+
parts.push(agent.sessionKey.slice(0, 24));
|
|
330
|
+
else if (seg === 'mode')
|
|
331
|
+
parts.push(agent.getAccessMode());
|
|
332
|
+
else if (seg === 'branch') {
|
|
333
|
+
try {
|
|
334
|
+
parts.push(execSync('git rev-parse --abbrev-ref HEAD', { cwd: agent.workspaceRoot, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim());
|
|
335
|
+
}
|
|
336
|
+
catch { /* not a git repo */ }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (parts.length > 0)
|
|
340
|
+
process.stdout.write(`\x1b]0;brainrouter · ${parts.join(' · ')}\x07`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch { /* terminal does not support OSC titles */ }
|
|
344
|
+
console.log(chalk.green(`\n✓ Terminal title → ${arg}\n`));
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
case '/personality':
|
|
348
|
+
{
|
|
349
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
350
|
+
const arg = (args[0] ?? '').toLowerCase();
|
|
351
|
+
const valid = new Set(['concise', 'standard', 'detailed', 'pair-programmer']);
|
|
352
|
+
if (!arg) {
|
|
353
|
+
console.log(chalk.bold('\nPersonality (communication style)'));
|
|
354
|
+
console.log(` Current: ${chalk.cyan(prefs.personality)}`);
|
|
355
|
+
console.log(chalk.gray(` Available: ${Array.from(valid).join(', ')}\n`));
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
if (!valid.has(arg)) {
|
|
359
|
+
console.log(chalk.red(`\nUnknown personality "${arg}". Choose: ${Array.from(valid).join(', ')}\n`));
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
writePreferences(agent.workspaceRoot, { personality: arg });
|
|
363
|
+
agent.refreshSystemPrompt();
|
|
364
|
+
console.log(chalk.green(`\n✓ Personality → ${arg}. New behavior applies on the next turn.\n`));
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
case '/raw':
|
|
368
|
+
{
|
|
369
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
370
|
+
const arg = (args[0] ?? '').toLowerCase();
|
|
371
|
+
const next = arg ? (arg === 'on' || arg === 'true' || arg === '1') : !prefs.rawScrollback;
|
|
372
|
+
writePreferences(agent.workspaceRoot, { rawScrollback: next });
|
|
373
|
+
console.log(chalk.green(`\n✓ Raw scrollback ${next ? 'enabled' : 'disabled'}. Markdown rendering ${next ? 'OFF' : 'ON'} for next turn.\n`));
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
case '/apps':
|
|
377
|
+
case '/plugins':
|
|
378
|
+
{
|
|
379
|
+
const skillsRoot = path.join(agent.workspaceRoot, 'skills');
|
|
380
|
+
const pluginsRoot = path.join(agent.workspaceRoot, 'plugins');
|
|
381
|
+
console.log(chalk.bold(`\n${command === '/apps' ? 'Apps' : 'Plugins'}`));
|
|
382
|
+
const roots = [skillsRoot, pluginsRoot].filter((p) => fs.existsSync(p));
|
|
383
|
+
if (roots.length === 0) {
|
|
384
|
+
console.log(chalk.yellow(' No skills/ or plugins/ directory in this workspace.'));
|
|
385
|
+
console.log(chalk.gray(' Drop a folder under skills/<category>/<name>/SKILL.md to register one.\n'));
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
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))}`));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
console.log();
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
case '/experimental':
|
|
400
|
+
{
|
|
401
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
402
|
+
const arg = (args[0] ?? '').toLowerCase();
|
|
403
|
+
const next = arg ? (arg === 'on' || arg === 'true' || arg === '1') : !prefs.experimental;
|
|
404
|
+
writePreferences(agent.workspaceRoot, { experimental: next });
|
|
405
|
+
console.log(chalk.green(`\n✓ Experimental features ${next ? 'enabled' : 'disabled'}.`));
|
|
406
|
+
if (next)
|
|
407
|
+
console.log(chalk.gray(' Streaming output, theme rendering, and other gated features are now active.\n'));
|
|
408
|
+
else
|
|
409
|
+
console.log();
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
case '/mention':
|
|
413
|
+
{
|
|
414
|
+
const partial = args.join(' ').trim();
|
|
415
|
+
console.log(chalk.bold('\nFile mention helper'));
|
|
416
|
+
console.log(chalk.gray(' Inline syntax: write `@path/to/file` in a prompt — the CLI expands it before sending.'));
|
|
417
|
+
const ws = agent.workspaceRoot;
|
|
418
|
+
const suggestions = completeWorkspacePath(ws, partial || '');
|
|
419
|
+
if (suggestions.length === 0) {
|
|
420
|
+
console.log(chalk.yellow(' No files matched.\n'));
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
console.log(chalk.gray(` Workspace matches${partial ? ` for "${partial}"` : ''}:`));
|
|
424
|
+
for (const s of suggestions.slice(0, 20))
|
|
425
|
+
console.log(` ${chalk.cyan('@' + s)}`);
|
|
426
|
+
if (suggestions.length > 20)
|
|
427
|
+
console.log(chalk.gray(` …and ${suggestions.length - 20} more`));
|
|
428
|
+
console.log();
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
case '/keymap':
|
|
432
|
+
{
|
|
433
|
+
const prefs = readPreferences(agent.workspaceRoot);
|
|
434
|
+
const arg = args.join(' ').trim();
|
|
435
|
+
if (!arg) {
|
|
436
|
+
console.log(chalk.bold('\nKeymap'));
|
|
437
|
+
console.log(chalk.gray(' Current overrides:'));
|
|
438
|
+
console.log(chalk.gray(` ${prefs.keymap || '(none — defaults)'}`));
|
|
439
|
+
console.log(chalk.bold('\n Built-in bindings'));
|
|
440
|
+
console.log(chalk.gray(' Shift+Tab cycle access mode (read → write → shell)'));
|
|
441
|
+
console.log(chalk.gray(' Tab autocomplete slash commands and @mentions'));
|
|
442
|
+
console.log(chalk.gray(' Ctrl+C interrupt current turn / exit'));
|
|
443
|
+
console.log(chalk.gray(' /vim toggle vi-mode for the composer'));
|
|
444
|
+
console.log(chalk.gray('\n Set custom overrides (JSON map): /keymap {"submit":"ctrl+s"}\n'));
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
JSON.parse(arg); // validate
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
console.log(chalk.red(`\nInvalid JSON: ${err.message}\n`));
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
writePreferences(agent.workspaceRoot, { keymap: arg });
|
|
455
|
+
console.log(chalk.green(`\n✓ Keymap overrides saved. Restart the CLI to apply.\n`));
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
case '/ide':
|
|
459
|
+
{
|
|
460
|
+
const env = process.env;
|
|
461
|
+
console.log(chalk.bold('\nIDE context'));
|
|
462
|
+
const cursor = env.CURSOR_TRACE_ID ? 'Cursor' : null;
|
|
463
|
+
const code = env.VSCODE_INJECTION || env.VSCODE_PID ? 'VS Code' : null;
|
|
464
|
+
const jet = env.JETBRAINS_IDE || env.IDEA_INITIAL_DIRECTORY ? 'JetBrains' : null;
|
|
465
|
+
const detected = [cursor, code, jet].filter(Boolean);
|
|
466
|
+
console.log(` Detected: ${detected.length > 0 ? chalk.cyan(detected.join(', ')) : chalk.gray('(none — running standalone)')}`);
|
|
467
|
+
console.log(chalk.gray(' Brainrouter reads files via the workspace root; if your IDE has an open selection, paste it with @ mentions or copy/paste.'));
|
|
468
|
+
console.log(chalk.gray(' Tip: configure IDE to launch brainrouter with -w <workspace> so paths match.\n'));
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
case '/help': {
|
|
472
|
+
renderHelp(args[0]?.toLowerCase());
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
@@ -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 tryHandleWorkflowCommand(ctx: CommandContext): Promise<boolean>;
|