@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.
Files changed (87) hide show
  1. package/.env.example +109 -0
  2. package/README.md +185 -0
  3. package/dist/agent/agent.d.ts +765 -0
  4. package/dist/agent/agent.js +1977 -0
  5. package/dist/cli/cliPrompt.d.ts +15 -0
  6. package/dist/cli/cliPrompt.js +62 -0
  7. package/dist/cli/commands/_context.d.ts +53 -0
  8. package/dist/cli/commands/_context.js +14 -0
  9. package/dist/cli/commands/_helpers.d.ts +45 -0
  10. package/dist/cli/commands/_helpers.js +140 -0
  11. package/dist/cli/commands/guard.d.ts +6 -0
  12. package/dist/cli/commands/guard.js +292 -0
  13. package/dist/cli/commands/memory.d.ts +12 -0
  14. package/dist/cli/commands/memory.js +263 -0
  15. package/dist/cli/commands/obs.d.ts +6 -0
  16. package/dist/cli/commands/obs.js +208 -0
  17. package/dist/cli/commands/orchestration.d.ts +6 -0
  18. package/dist/cli/commands/orchestration.js +218 -0
  19. package/dist/cli/commands/session.d.ts +6 -0
  20. package/dist/cli/commands/session.js +191 -0
  21. package/dist/cli/commands/ui.d.ts +6 -0
  22. package/dist/cli/commands/ui.js +477 -0
  23. package/dist/cli/commands/workflow.d.ts +6 -0
  24. package/dist/cli/commands/workflow.js +691 -0
  25. package/dist/cli/repl.d.ts +12 -0
  26. package/dist/cli/repl.js +894 -0
  27. package/dist/config/config.d.ts +22 -0
  28. package/dist/config/config.js +105 -0
  29. package/dist/config/workspace.d.ts +7 -0
  30. package/dist/config/workspace.js +62 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +610 -0
  33. package/dist/memory/briefing.d.ts +46 -0
  34. package/dist/memory/briefing.js +152 -0
  35. package/dist/memory/consolidation.d.ts +60 -0
  36. package/dist/memory/consolidation.js +208 -0
  37. package/dist/memory/formatters.d.ts +38 -0
  38. package/dist/memory/formatters.js +102 -0
  39. package/dist/memory/mentions.d.ts +10 -0
  40. package/dist/memory/mentions.js +72 -0
  41. package/dist/orchestration/orchestrator.d.ts +36 -0
  42. package/dist/orchestration/orchestrator.js +71 -0
  43. package/dist/orchestration/roles.d.ts +11 -0
  44. package/dist/orchestration/roles.js +117 -0
  45. package/dist/orchestration/tools.d.ts +244 -0
  46. package/dist/orchestration/tools.js +528 -0
  47. package/dist/prompt/breadthHint.d.ts +48 -0
  48. package/dist/prompt/breadthHint.js +93 -0
  49. package/dist/prompt/compactor.d.ts +31 -0
  50. package/dist/prompt/compactor.js +112 -0
  51. package/dist/prompt/initAgentMd.d.ts +13 -0
  52. package/dist/prompt/initAgentMd.js +194 -0
  53. package/dist/prompt/skillRunner.d.ts +34 -0
  54. package/dist/prompt/skillRunner.js +146 -0
  55. package/dist/prompt/systemPrompt.d.ts +10 -0
  56. package/dist/prompt/systemPrompt.js +171 -0
  57. package/dist/runtime/clipboard.d.ts +17 -0
  58. package/dist/runtime/clipboard.js +52 -0
  59. package/dist/runtime/llmSemaphore.d.ts +30 -0
  60. package/dist/runtime/llmSemaphore.js +67 -0
  61. package/dist/runtime/loopRunner.d.ts +25 -0
  62. package/dist/runtime/loopRunner.js +79 -0
  63. package/dist/runtime/mcpClient.d.ts +156 -0
  64. package/dist/runtime/mcpClient.js +234 -0
  65. package/dist/runtime/mcpUtils.d.ts +36 -0
  66. package/dist/runtime/mcpUtils.js +64 -0
  67. package/dist/runtime/sandbox.d.ts +48 -0
  68. package/dist/runtime/sandbox.js +156 -0
  69. package/dist/runtime/tracing.d.ts +25 -0
  70. package/dist/runtime/tracing.js +91 -0
  71. package/dist/state/cliState.d.ts +59 -0
  72. package/dist/state/cliState.js +311 -0
  73. package/dist/state/goalStore.d.ts +174 -0
  74. package/dist/state/goalStore.js +410 -0
  75. package/dist/state/hookifyStore.d.ts +80 -0
  76. package/dist/state/hookifyStore.js +237 -0
  77. package/dist/state/hooksStore.d.ts +42 -0
  78. package/dist/state/hooksStore.js +71 -0
  79. package/dist/state/preferencesStore.d.ts +41 -0
  80. package/dist/state/preferencesStore.js +25 -0
  81. package/dist/state/sessionStore.d.ts +42 -0
  82. package/dist/state/sessionStore.js +193 -0
  83. package/dist/state/taskStore.d.ts +23 -0
  84. package/dist/state/taskStore.js +80 -0
  85. package/dist/state/workflowArtifacts.d.ts +33 -0
  86. package/dist/state/workflowArtifacts.js +139 -0
  87. package/package.json +71 -0
@@ -0,0 +1,15 @@
1
+ import readline from 'node:readline';
2
+ export declare function setActiveReadline(rl: readline.Interface | undefined): void;
3
+ export declare function getActiveReadline(): readline.Interface | undefined;
4
+ /**
5
+ * One-shot yes/no question. Returns true only when the user types y/yes
6
+ * (case-insensitive). Returns the supplied default when stdin isn't a TTY
7
+ * (e.g. piped non-interactive runs).
8
+ */
9
+ export declare function askYesNo(question: string, defaultValue?: boolean): Promise<boolean>;
10
+ /**
11
+ * Print a line of output while the prompt is showing, then redraw the prompt
12
+ * with whatever the user was mid-typing. Used by callbacks that fire while the
13
+ * REPL is idle (child agents that complete async after the parent turn ended).
14
+ */
15
+ export declare function safePrintAbovePrompt(msg: string): void;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shared bridge between the REPL's readline interface and modules outside
3
+ * repl.ts that need to (a) write above the prompt without scrambling input,
4
+ * or (b) ask the user a one-shot question while a turn is in progress
5
+ * (e.g. run_command approval).
6
+ *
7
+ * Previously this used `inquirer.prompt`. Inquirer creates its OWN readline
8
+ * interface attached to the same `process.stdin`, and on exit it leaves stray
9
+ * `line` events that the parent REPL then sees as "the user typed a new
10
+ * prompt while a turn is still running". Using the parent rl directly avoids
11
+ * that.
12
+ *
13
+ * Only one REPL exists per process, so a module-level pointer is fine.
14
+ */
15
+ let activeReadline;
16
+ export function setActiveReadline(rl) {
17
+ activeReadline = rl;
18
+ }
19
+ export function getActiveReadline() {
20
+ return activeReadline;
21
+ }
22
+ /**
23
+ * One-shot yes/no question. Returns true only when the user types y/yes
24
+ * (case-insensitive). Returns the supplied default when stdin isn't a TTY
25
+ * (e.g. piped non-interactive runs).
26
+ */
27
+ export function askYesNo(question, defaultValue = false) {
28
+ if (!activeReadline || !process.stdin.isTTY) {
29
+ return Promise.resolve(defaultValue);
30
+ }
31
+ return new Promise((resolve) => {
32
+ const rl = activeReadline;
33
+ // The parent rl was paused by runAgentTurn; resume so it actually reads
34
+ // keystrokes. We re-pause once we have the answer.
35
+ rl.resume();
36
+ rl.question(question, (answer) => {
37
+ const lower = (answer ?? '').trim().toLowerCase();
38
+ const yes = lower === 'y' || lower === 'yes';
39
+ rl.pause();
40
+ resolve(yes);
41
+ });
42
+ });
43
+ }
44
+ /**
45
+ * Print a line of output while the prompt is showing, then redraw the prompt
46
+ * with whatever the user was mid-typing. Used by callbacks that fire while the
47
+ * REPL is idle (child agents that complete async after the parent turn ended).
48
+ */
49
+ export function safePrintAbovePrompt(msg) {
50
+ if (!process.stdout.isTTY || !activeReadline) {
51
+ console.log(msg);
52
+ return;
53
+ }
54
+ process.stdout.write('\r\x1b[2K');
55
+ console.log(msg);
56
+ try {
57
+ activeReadline._refreshLine?.();
58
+ }
59
+ catch {
60
+ activeReadline.prompt(true);
61
+ }
62
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared types + helper context that every slash-command handler receives.
3
+ *
4
+ * Split out from repl.ts so individual command files can be small and
5
+ * topical. Each category file (session/memory/workflow/orchestration/…)
6
+ * exports a `tryHandle*(ctx)` function that returns true iff it matched
7
+ * `ctx.command`. The dispatch table in repl.ts walks them in order until
8
+ * one returns true; if none do, the user gets the "unknown command"
9
+ * message.
10
+ *
11
+ * Adding a new command means: pick the right category file, add a
12
+ * `case '/foo':` to its switch, done. No need to edit repl.ts at all.
13
+ */
14
+ import type readline from 'node:readline';
15
+ import type { Agent } from '../../agent/agent.js';
16
+ import type { McpClientWrapper } from '../../runtime/mcpClient.js';
17
+ import type { Config } from '../../config/config.js';
18
+ /**
19
+ * Lifecycle / REPL-scoped state that command handlers can read or mutate.
20
+ * Defined here (rather than inside the REPL closure) so commands stay in
21
+ * separate files without crossing closure boundaries. The REPL constructs
22
+ * one instance per session and threads it through every dispatch call.
23
+ */
24
+ export interface ReplContext {
25
+ /** Refresh the readline prompt (color reflects access mode + status segments). */
26
+ refreshPromptForMode: () => void;
27
+ /** True while the REPL is mid-turn; loop ticks should defer when set. */
28
+ isProcessing: () => boolean;
29
+ /** Programmatically run an agent turn (used by /continue and friends). */
30
+ runAgentTurn: (prompt: string) => void;
31
+ /**
32
+ * Awaitable variant — same semantics but the caller can attach a .finally
33
+ * to do post-turn cleanup. Used by /side and /btw to restore the parent
34
+ * sessionKey after the side conversation finishes.
35
+ */
36
+ runAgentTurnAsync: (prompt: string) => Promise<void>;
37
+ }
38
+ /**
39
+ * Everything a command handler needs. Constructed once per dispatch in
40
+ * the REPL line handler and passed by reference into every category's
41
+ * try-handler.
42
+ */
43
+ export interface CommandContext {
44
+ /** The raw slash command (e.g. `/spawn`), lowercased. */
45
+ command: string;
46
+ /** Arguments after the command, already split on whitespace. */
47
+ args: string[];
48
+ agent: Agent;
49
+ mcpClient: McpClientWrapper;
50
+ config: Config;
51
+ rl: readline.Interface;
52
+ repl: ReplContext;
53
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared types + helper context that every slash-command handler receives.
3
+ *
4
+ * Split out from repl.ts so individual command files can be small and
5
+ * topical. Each category file (session/memory/workflow/orchestration/…)
6
+ * exports a `tryHandle*(ctx)` function that returns true iff it matched
7
+ * `ctx.command`. The dispatch table in repl.ts walks them in order until
8
+ * one returns true; if none do, the user gets the "unknown command"
9
+ * message.
10
+ *
11
+ * Adding a new command means: pick the right category file, add a
12
+ * `case '/foo':` to its switch, done. No need to edit repl.ts at all.
13
+ */
14
+ export {};
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Helpers shared across multiple slash-command handler files.
3
+ *
4
+ * Stays small on purpose — anything used by only one category file should
5
+ * live in that file. The functions here are the cross-cutting bits that
6
+ * 3+ categories reach for: print-an-MCP-call helpers, the goal-kickoff
7
+ * prompt builder, the transcript content formatter, and the skill runner
8
+ * wrapper.
9
+ */
10
+ import type { Agent } from '../../agent/agent.js';
11
+ import type { McpClientWrapper } from '../../runtime/mcpClient.js';
12
+ /**
13
+ * Memory-aware variant of printMcpCall. Calls the tool, extracts the flat
14
+ * record list from whatever shape it returns, and renders compact cards
15
+ * (recordId, type, scene, content preview). Falls back to printMcpCall's
16
+ * raw output only when no records can be parsed.
17
+ */
18
+ export declare function printMemoryCards(mcpClient: McpClientWrapper, toolName: string, args: Record<string, unknown>, heading: string): Promise<void>;
19
+ /**
20
+ * Generic MCP call printer — used by /handover, /explain, /failed, /verify,
21
+ * /audit, /persona, /skill-hints, and anywhere else we just want to dump
22
+ * the tool's text output under a heading.
23
+ */
24
+ export declare function printMcpCall(mcpClient: McpClientWrapper, toolName: string, args: Record<string, unknown>, heading: string): Promise<void>;
25
+ /**
26
+ * Format a transcript entry's content for compact display in /transcript
27
+ * and /agent. Strips whitespace, JSON-stringifies non-strings, caps at 240
28
+ * chars so long tool payloads don't blow scrollback.
29
+ */
30
+ export declare function formatTranscriptContent(value: unknown): string;
31
+ /**
32
+ * Prompt the agent receives for the FIRST turn after /goal <text> or
33
+ * /goal resume. Once this turn finishes, runAgentTurn's continuation loop
34
+ * keeps firing iterations 2..N until the agent calls goal_complete or
35
+ * goal_blocked, the budget runs out, or the user interrupts.
36
+ */
37
+ export declare function buildGoalKickoffPrompt(goal: import('../../state/goalStore.js').Goal, mode: 'start' | 'resume'): string;
38
+ /**
39
+ * Resolve a slash-mapped skill (/spec, /feature-dev, /review, /implement-plan)
40
+ * to a SKILL.md body, refuse fallback placeholders, latch activeSkill on the
41
+ * agent, and hand the assembled prompt to the supplied runTurn callback.
42
+ * Centralized here so /skill itself and the workflow shortcuts share one path.
43
+ */
44
+ export declare function runSkillCommand(agent: Agent, mcpClient: McpClientWrapper, slashCommand: string, userInput: string, orchestration: string | undefined, runTurn: (prompt: string) => void): Promise<void>;
45
+ export declare function runSkillByName(agent: Agent, mcpClient: McpClientWrapper, skillName: string, userInput: string, orchestration: string | undefined, runTurn: (prompt: string) => void): Promise<void>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Helpers shared across multiple slash-command handler files.
3
+ *
4
+ * Stays small on purpose — anything used by only one category file should
5
+ * live in that file. The functions here are the cross-cutting bits that
6
+ * 3+ categories reach for: print-an-MCP-call helpers, the goal-kickoff
7
+ * prompt builder, the transcript content formatter, and the skill runner
8
+ * wrapper.
9
+ */
10
+ import chalk from 'chalk';
11
+ import ora from 'ora';
12
+ import { callMcpTool } from '../../runtime/mcpUtils.js';
13
+ import { clampPayload, extractMemories, renderMemoryCards } from '../../memory/formatters.js';
14
+ import { buildSkillPrompt, resolveSkill, SLASH_TO_SKILL } from '../../prompt/skillRunner.js';
15
+ /**
16
+ * Memory-aware variant of printMcpCall. Calls the tool, extracts the flat
17
+ * record list from whatever shape it returns, and renders compact cards
18
+ * (recordId, type, scene, content preview). Falls back to printMcpCall's
19
+ * raw output only when no records can be parsed.
20
+ */
21
+ export async function printMemoryCards(mcpClient, toolName, args, heading) {
22
+ const spinner = ora(chalk.gray(`${toolName}…`)).start();
23
+ const res = await callMcpTool(mcpClient, toolName, args);
24
+ spinner.stop();
25
+ console.log();
26
+ if (res.isError) {
27
+ console.log(chalk.red(`${heading}: tool error — ${res.text || '(no message)'}`));
28
+ return;
29
+ }
30
+ const cards = extractMemories(res.parsed);
31
+ if (cards.length > 0) {
32
+ console.log(renderMemoryCards(cards, heading));
33
+ }
34
+ else {
35
+ console.log(chalk.bold(heading));
36
+ const preview = clampPayload(res.text, 2000).trim();
37
+ console.log(preview ? chalk.gray(preview) : chalk.yellow(' (empty result)'));
38
+ console.log();
39
+ }
40
+ }
41
+ /**
42
+ * Generic MCP call printer — used by /handover, /explain, /failed, /verify,
43
+ * /audit, /persona, /skill-hints, and anywhere else we just want to dump
44
+ * the tool's text output under a heading.
45
+ */
46
+ export async function printMcpCall(mcpClient, toolName, args, heading) {
47
+ const spinner = ora(chalk.gray(`${toolName}…`)).start();
48
+ const res = await callMcpTool(mcpClient, toolName, args);
49
+ spinner.stop();
50
+ console.log(chalk.bold(`\n${heading}`));
51
+ if (res.isError) {
52
+ console.log(chalk.red(` Tool error: ${res.text || '(no message)'}`));
53
+ console.log();
54
+ return;
55
+ }
56
+ if (!res.text.trim()) {
57
+ console.log(chalk.yellow(' (empty result)'));
58
+ console.log();
59
+ return;
60
+ }
61
+ const preview = res.text.length > 4000
62
+ ? res.text.slice(0, 4000) + chalk.gray(`\n…(${res.text.length - 4000} chars truncated)`)
63
+ : res.text;
64
+ console.log(chalk.gray(preview));
65
+ console.log();
66
+ }
67
+ /**
68
+ * Format a transcript entry's content for compact display in /transcript
69
+ * and /agent. Strips whitespace, JSON-stringifies non-strings, caps at 240
70
+ * chars so long tool payloads don't blow scrollback.
71
+ */
72
+ export function formatTranscriptContent(value) {
73
+ const raw = typeof value === 'string' ? value : JSON.stringify(value);
74
+ return raw.replace(/\s+/g, ' ').trim().slice(0, 240);
75
+ }
76
+ /**
77
+ * Prompt the agent receives for the FIRST turn after /goal <text> or
78
+ * /goal resume. Once this turn finishes, runAgentTurn's continuation loop
79
+ * keeps firing iterations 2..N until the agent calls goal_complete or
80
+ * goal_blocked, the budget runs out, or the user interrupts.
81
+ */
82
+ export function buildGoalKickoffPrompt(goal, mode) {
83
+ const header = mode === 'start' ? '[GOAL KICKOFF — iteration 1]' : '[GOAL RESUME]';
84
+ return [
85
+ header,
86
+ '',
87
+ `Your active goal is: ${goal.text}`,
88
+ `Iteration budget: ${goal.budget.iterationsUsed}/${goal.budget.maxIterations} used.`,
89
+ '',
90
+ '## What to do right now',
91
+ mode === 'start'
92
+ ? '1. **Open with memory.** Run `memory_search` / `memory_recall` for prior work in this workspace. Cite the recordIds you find.'
93
+ : '1. **Reload context.** Check what was already done by reading the last few transcript entries, the current plan, and any open child agents (`list_agents`).',
94
+ '2. **Plan briefly.** If the work has 3+ vertical slices, call `update_plan` with statuses (pending / in_progress / completed; ≤ 1 in_progress).',
95
+ '3. **Take the first concrete tool action** toward the outcome. Read a file, write code, spawn an explorer child, run a verifier — whatever produces evidence the goal is satisfied.',
96
+ '4. The CLI will auto-continue you with another turn after this one finishes. Iterate until you can call `goal_complete(proof)` with concrete evidence (test pass / file written / benchmark hit) or `goal_blocked(reason)` if no path remains.',
97
+ '',
98
+ 'Do NOT respond with prose-only "I will get started" — the CLI suppresses the next auto-continuation after a turn with zero tool calls. Begin executing tools now.',
99
+ ].join('\n');
100
+ }
101
+ /**
102
+ * Resolve a slash-mapped skill (/spec, /feature-dev, /review, /implement-plan)
103
+ * to a SKILL.md body, refuse fallback placeholders, latch activeSkill on the
104
+ * agent, and hand the assembled prompt to the supplied runTurn callback.
105
+ * Centralized here so /skill itself and the workflow shortcuts share one path.
106
+ */
107
+ export async function runSkillCommand(agent, mcpClient, slashCommand, userInput, orchestration, runTurn) {
108
+ const skillName = SLASH_TO_SKILL[slashCommand];
109
+ if (!skillName) {
110
+ console.log(chalk.red(`\nNo skill mapped to ${slashCommand}.\n`));
111
+ return;
112
+ }
113
+ await runSkillByName(agent, mcpClient, skillName, userInput, orchestration, runTurn);
114
+ }
115
+ export async function runSkillByName(agent, mcpClient, skillName, userInput, orchestration, runTurn) {
116
+ const loader = ora(chalk.gray(`Loading skill: ${skillName}...`)).start();
117
+ let prompt;
118
+ try {
119
+ const skill = await resolveSkill(mcpClient, skillName, agent.workspaceRoot, 'full');
120
+ if (skill.source === 'fallback') {
121
+ // resolveSkill returns a placeholder body for unknown names; running it
122
+ // burns an LLM call on nothing. Refuse early and tell the user what's
123
+ // actually installed.
124
+ loader.fail(chalk.red(`Unknown skill "${skillName}".`));
125
+ console.log(chalk.gray(' Run `/skills` to list installed skills, or call `search_skills` for fuzzy matches.\n'));
126
+ return;
127
+ }
128
+ loader.succeed(chalk.green(`Skill loaded: ${skillName} (${skill.source})`));
129
+ prompt = buildSkillPrompt(skill, { input: userInput, orchestration });
130
+ }
131
+ catch (err) {
132
+ loader.fail(chalk.red(`Failed to resolve skill "${skillName}": ${err.message}`));
133
+ return;
134
+ }
135
+ // Mark the skill active so memory_recall / memory_capture_turn see it.
136
+ // The activeSkill stays latched while the turn runs; runAgentTurn's
137
+ // continuation loop will clear it via the post-turn hook.
138
+ agent.activeSkill = skillName;
139
+ runTurn(prompt);
140
+ }
@@ -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 tryHandleGuardCommand(ctx: CommandContext): Promise<boolean>;
@@ -0,0 +1,292 @@
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 chalk from 'chalk';
8
+ import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
9
+ import { addHook, readHooks, removeHook, setHookEnabled } from '../../state/hooksStore.js';
10
+ import { createHookifyRule, deleteHookifyRule, listHookifyRules, toggleHookifyRule } from '../../state/hookifyStore.js';
11
+ export async function tryHandleGuardCommand(ctx) {
12
+ const { command, args, agent, mcpClient, config, rl, repl } = ctx;
13
+ // 'ctx' alias to keep references to the old ReplContext name working
14
+ const replCtx = repl;
15
+ switch (command) {
16
+ case '/permissions':
17
+ {
18
+ const sub = args[0];
19
+ if (!sub) {
20
+ const mode = agent.getAccessMode();
21
+ console.log(chalk.bold(`\nCurrent access mode: ${chalk.cyan(mode)}`));
22
+ console.log(chalk.gray(' read — list/grep/read/web only. No file writes, no shell.'));
23
+ console.log(chalk.gray(' write — read + write_file / edit_file / apply_patch. No shell.'));
24
+ console.log(chalk.gray(' shell — write + run_command (still confirmed in the REPL).'));
25
+ console.log(chalk.gray('\nSwitch with: /permissions read | write | shell (or use Shift+Tab to cycle)\n'));
26
+ return true;
27
+ }
28
+ if (!['read', 'write', 'shell'].includes(sub)) {
29
+ console.log(chalk.red(`\nUnknown mode "${sub}". Choose: read, write, shell.\n`));
30
+ return true;
31
+ }
32
+ agent.setAccessMode(sub);
33
+ ctx.repl.refreshPromptForMode();
34
+ console.log(chalk.green(`\n✓ Access mode → ${chalk.cyan(sub)}\n`));
35
+ return true;
36
+ }
37
+ case '/hooks':
38
+ {
39
+ const sub = args[0];
40
+ if (!sub || sub === 'list') {
41
+ const hooks = readHooks(agent.workspaceRoot);
42
+ console.log(chalk.bold('\nLifecycle hooks'));
43
+ if (hooks.length === 0) {
44
+ console.log(chalk.yellow(' (none)'));
45
+ console.log(chalk.gray(' Add one with: /hooks add <event> <shell-command> (events: pre-turn, post-turn, pre-tool, post-tool, session-start, session-end)\n'));
46
+ }
47
+ else {
48
+ for (const h of hooks) {
49
+ const tag = h.enabled ? chalk.green('●') : chalk.gray('○');
50
+ console.log(` ${tag} ${chalk.cyan(h.id)} ${chalk.gray(h.event)}${h.match ? chalk.gray(` (match: ${h.match})`) : ''}`);
51
+ console.log(` ${chalk.gray(h.command)}`);
52
+ }
53
+ console.log();
54
+ }
55
+ return true;
56
+ }
57
+ if (sub === 'add') {
58
+ const event = args[1];
59
+ const command = args.slice(2).join(' ').trim();
60
+ const validEvents = ['pre-turn', 'post-turn', 'pre-tool', 'post-tool', 'session-start', 'session-end'];
61
+ if (!event || !validEvents.includes(event) || !command) {
62
+ console.log(chalk.red(`\nUsage: /hooks add <${validEvents.join('|')}> <shell-command>\n`));
63
+ return true;
64
+ }
65
+ const created = addHook(agent.workspaceRoot, { event, command });
66
+ console.log(chalk.green(`\n✓ Hook added: ${created.id}\n`));
67
+ return true;
68
+ }
69
+ if (sub === 'remove' && args[1]) {
70
+ const ok = removeHook(agent.workspaceRoot, args[1]);
71
+ console.log(ok ? chalk.green(`\n✓ Removed ${args[1]}\n`) : chalk.red(`\nNo hook with id ${args[1]}\n`));
72
+ return true;
73
+ }
74
+ if ((sub === 'enable' || sub === 'disable') && args[1]) {
75
+ const ok = setHookEnabled(agent.workspaceRoot, args[1], sub === 'enable');
76
+ console.log(ok ? chalk.green(`\n✓ ${sub === 'enable' ? 'Enabled' : 'Disabled'} ${args[1]}\n`) : chalk.red(`\nNo hook with id ${args[1]}\n`));
77
+ return true;
78
+ }
79
+ console.log(chalk.red('\nUsage: /hooks [list | add <event> <cmd> | remove <id> | enable <id> | disable <id>]\n'));
80
+ return true;
81
+ }
82
+ case '/yolo':
83
+ {
84
+ const prefs = readPreferences(agent.workspaceRoot);
85
+ const arg = (args[0] ?? '').toLowerCase();
86
+ if (!arg) {
87
+ console.log(chalk.bold(`\nAuto-approve shell: ${prefs.autoApproveShell ? chalk.red('ON') : chalk.green('off')}`));
88
+ console.log(chalk.gray(' When ON, run_command skips the per-call confirmation prompt and executes immediately.'));
89
+ console.log(chalk.gray(' Pair with BRAINROUTER_SANDBOX=on if you still want a safety net.'));
90
+ console.log(chalk.gray(' Toggle with: /yolo on | /yolo off\n'));
91
+ return true;
92
+ }
93
+ const next = arg === 'on' || arg === 'true' || arg === '1';
94
+ writePreferences(agent.workspaceRoot, { autoApproveShell: next });
95
+ if (next) {
96
+ console.log(chalk.red('\n⚠ /yolo ON — run_command will now execute without asking.'));
97
+ console.log(chalk.gray(' You are in access mode "shell" so the agent CAN call shell commands.'));
98
+ console.log(chalk.gray(' Lower the risk with /permissions write (no shell), or set BRAINROUTER_SANDBOX=on.\n'));
99
+ }
100
+ else {
101
+ console.log(chalk.green('\n✓ /yolo off — run_command will prompt for confirmation again.\n'));
102
+ }
103
+ return true;
104
+ }
105
+ case '/sandbox':
106
+ {
107
+ const sub = (args[0] ?? '').toLowerCase();
108
+ const rest = args.slice(1).join(' ').trim();
109
+ const prefs = readPreferences(agent.workspaceRoot);
110
+ const showState = () => {
111
+ const enabled = (process.env.BRAINROUTER_SANDBOX ?? '').toLowerCase() === 'on';
112
+ console.log(chalk.bold('\nSandbox'));
113
+ console.log(` Engine: ${enabled ? chalk.green('on') : chalk.gray('off')} ${chalk.gray('(BRAINROUTER_SANDBOX env)')}`);
114
+ console.log(` Platform: ${chalk.cyan(process.platform)} ${chalk.gray(process.platform === 'darwin' ? '(sandbox-exec)' : process.platform === 'linux' ? '(bwrap/firejail)' : '(unsupported — run_command runs unsandboxed)')}`);
115
+ console.log(` Workspace (always rw): ${chalk.blue(agent.workspaceRoot)}`);
116
+ console.log(chalk.bold(' Read-only grants:'));
117
+ if (prefs.sandboxReadPaths.length === 0)
118
+ console.log(chalk.gray(' (none)'));
119
+ else
120
+ for (const p of prefs.sandboxReadPaths)
121
+ console.log(` ${chalk.cyan(p)}`);
122
+ console.log(chalk.bold(' Write grants (beyond workspace):'));
123
+ if (prefs.sandboxWritePaths.length === 0)
124
+ console.log(chalk.gray(' (none)'));
125
+ else
126
+ for (const p of prefs.sandboxWritePaths)
127
+ console.log(` ${chalk.cyan(p)}`);
128
+ console.log(chalk.gray('\n Subcommands:'));
129
+ console.log(chalk.gray(' /sandbox add-read <path> grant read-only access'));
130
+ console.log(chalk.gray(' /sandbox add-write <path> grant read+write access'));
131
+ console.log(chalk.gray(' /sandbox remove <path> drop a grant (matches either list)'));
132
+ console.log(chalk.gray(' /sandbox clear drop all persisted grants'));
133
+ console.log(chalk.gray(' /sandbox status show this view\n'));
134
+ };
135
+ if (!sub || sub === 'status') {
136
+ showState();
137
+ break;
138
+ }
139
+ const resolveGrant = (p) => {
140
+ if (!p)
141
+ return null;
142
+ const abs = path.resolve(agent.workspaceRoot, p);
143
+ if (!fs.existsSync(abs)) {
144
+ console.log(chalk.yellow(`\n⚠ Path does not exist: ${abs}`));
145
+ console.log(chalk.gray(' Granting anyway — create it later or the sandbox will skip the bind.\n'));
146
+ }
147
+ return abs;
148
+ };
149
+ if (sub === 'add-read') {
150
+ const abs = resolveGrant(rest);
151
+ if (!abs) {
152
+ console.log(chalk.red('\nUsage: /sandbox add-read <path>\n'));
153
+ break;
154
+ }
155
+ const next = Array.from(new Set([...prefs.sandboxReadPaths, abs]));
156
+ writePreferences(agent.workspaceRoot, { sandboxReadPaths: next });
157
+ console.log(chalk.green(`\n✓ Added read grant: ${abs}\n`));
158
+ return true;
159
+ }
160
+ if (sub === 'add-write') {
161
+ const abs = resolveGrant(rest);
162
+ if (!abs) {
163
+ console.log(chalk.red('\nUsage: /sandbox add-write <path>\n'));
164
+ break;
165
+ }
166
+ const next = Array.from(new Set([...prefs.sandboxWritePaths, abs]));
167
+ writePreferences(agent.workspaceRoot, { sandboxWritePaths: next });
168
+ console.log(chalk.green(`\n✓ Added write grant: ${abs}\n`));
169
+ return true;
170
+ }
171
+ if (sub === 'remove') {
172
+ const abs = resolveGrant(rest);
173
+ if (!abs) {
174
+ console.log(chalk.red('\nUsage: /sandbox remove <path>\n'));
175
+ break;
176
+ }
177
+ writePreferences(agent.workspaceRoot, {
178
+ sandboxReadPaths: prefs.sandboxReadPaths.filter((p) => p !== abs),
179
+ sandboxWritePaths: prefs.sandboxWritePaths.filter((p) => p !== abs),
180
+ });
181
+ console.log(chalk.green(`\n✓ Removed grant: ${abs}\n`));
182
+ return true;
183
+ }
184
+ if (sub === 'clear') {
185
+ writePreferences(agent.workspaceRoot, { sandboxReadPaths: [], sandboxWritePaths: [] });
186
+ console.log(chalk.green('\n✓ Cleared all persisted sandbox grants.\n'));
187
+ return true;
188
+ }
189
+ console.log(chalk.red(`\nUnknown /sandbox subcommand "${sub}". Run /sandbox for help.\n`));
190
+ return true;
191
+ }
192
+ case '/logout':
193
+ {
194
+ // Remove the API key from the active server profile. The CLI keeps the
195
+ // profile so a future /login can re-attach credentials.
196
+ const profile = config.activeServer;
197
+ const server = config.servers[profile];
198
+ if (!server) {
199
+ console.log(chalk.red(`\nNo active profile to log out of.\n`));
200
+ return true;
201
+ }
202
+ const removed = [];
203
+ if (server.apiKey) {
204
+ delete server.apiKey;
205
+ removed.push('server.apiKey');
206
+ }
207
+ if (config.llm?.apiKey) {
208
+ config.llm.apiKey = '';
209
+ removed.push('llm.apiKey');
210
+ }
211
+ if (removed.length === 0) {
212
+ console.log(chalk.gray(`\nNo credentials were set on profile "${profile}".\n`));
213
+ return true;
214
+ }
215
+ const { saveConfig } = await import('../../config/config.js');
216
+ saveConfig(config);
217
+ console.log(chalk.green(`\n✓ Cleared ${removed.join(', ')} from profile "${profile}".`));
218
+ console.log(chalk.gray(' Re-attach with /login.\n'));
219
+ return true;
220
+ }
221
+ case '/hookify':
222
+ {
223
+ const sub = args[0];
224
+ if (!sub || sub === 'list') {
225
+ const rules = listHookifyRules(agent.workspaceRoot);
226
+ console.log(chalk.bold('\nHookify rules'));
227
+ if (rules.length === 0) {
228
+ console.log(chalk.yellow(' (none)'));
229
+ console.log(chalk.gray(' Add with: /hookify create <name>|<event>|<pattern>|<action>|<message>'));
230
+ console.log(chalk.gray(' event = bash | file | prompt | stop | all'));
231
+ console.log(chalk.gray(' action = warn | block'));
232
+ console.log(chalk.gray(' Rules live as markdown files in ~/.brainrouter/workspaces/<encoded>/hooks/'));
233
+ console.log(chalk.gray(' (legacy <workspace>/.brainrouter/hooks/ files are auto-migrated on first read).\n'));
234
+ }
235
+ else {
236
+ for (const r of rules) {
237
+ const tag = r.enabled ? chalk.green('●') : chalk.gray('○');
238
+ console.log(` ${tag} ${chalk.cyan(r.id)} ${chalk.gray(r.event)} → ${chalk.yellow(r.action)}${r.pattern ? chalk.gray(` (pattern: ${r.pattern})`) : ''}`);
239
+ console.log(chalk.gray(` ${r.message.split('\n')[0].slice(0, 120)}`));
240
+ }
241
+ console.log();
242
+ }
243
+ return true;
244
+ }
245
+ if (sub === 'create') {
246
+ const raw = args.slice(1).join(' ').trim();
247
+ const parts = raw.split('|').map((p) => p.trim());
248
+ if (parts.length < 5) {
249
+ console.log(chalk.red('\nUsage: /hookify create <name>|<event>|<pattern>|<action>|<message>\n'));
250
+ return true;
251
+ }
252
+ try {
253
+ const created = createHookifyRule(agent.workspaceRoot, {
254
+ name: parts[0],
255
+ event: parts[1],
256
+ pattern: parts[2],
257
+ action: parts[3],
258
+ message: parts.slice(4).join('|'),
259
+ });
260
+ console.log(chalk.green(`\n✓ Created hookify rule ${created.id} at ${path.relative(agent.workspaceRoot, created.sourcePath)}\n`));
261
+ }
262
+ catch (err) {
263
+ console.log(chalk.red(`\nFailed: ${err.message}\n`));
264
+ }
265
+ return true;
266
+ }
267
+ if (sub === 'enable' || sub === 'disable') {
268
+ const id = args[1];
269
+ if (!id) {
270
+ console.log(chalk.red(`\nUsage: /hookify ${sub} <id>\n`));
271
+ break;
272
+ }
273
+ const ok = toggleHookifyRule(agent.workspaceRoot, id, sub === 'enable');
274
+ console.log(ok ? chalk.green(`\n✓ ${sub === 'enable' ? 'Enabled' : 'Disabled'} ${id}\n`) : chalk.red(`\nNo rule ${id}\n`));
275
+ return true;
276
+ }
277
+ if (sub === 'remove') {
278
+ const id = args[1];
279
+ if (!id) {
280
+ console.log(chalk.red(`\nUsage: /hookify remove <id>\n`));
281
+ break;
282
+ }
283
+ const ok = deleteHookifyRule(agent.workspaceRoot, id);
284
+ console.log(ok ? chalk.green(`\n✓ Removed ${id}\n`) : chalk.red(`\nNo rule ${id}\n`));
285
+ return true;
286
+ }
287
+ console.log(chalk.red('\nUsage: /hookify [list | create <spec> | enable <id> | disable <id> | remove <id>]\n'));
288
+ return true;
289
+ }
290
+ }
291
+ return false;
292
+ }
@@ -0,0 +1,12 @@
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 type { CommandContext } from './_context.js';
12
+ export declare function tryHandleMemoryCommand(ctx: CommandContext): Promise<boolean>;