@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,218 @@
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 chalk from 'chalk';
6
+ import { childSessionKey } from '../../runtime/mcpUtils.js';
7
+ import { listRoles } from '../../orchestration/roles.js';
8
+ import { formatSessionSummary, getSession, listSessions, reconcileStale } from '../../orchestration/orchestrator.js';
9
+ import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
10
+ import { readTranscriptEntries } from '../../state/sessionStore.js';
11
+ import { getLoopState, stopLoop } from '../../runtime/loopRunner.js';
12
+ import { formatTranscriptContent } from './_helpers.js';
13
+ export async function tryHandleOrchestrationCommand(ctx) {
14
+ const { command, args, agent, mcpClient, config, rl, repl } = ctx;
15
+ // 'ctx' alias to keep references to the old ReplContext name working
16
+ const replCtx = repl;
17
+ switch (command) {
18
+ case '/roles':
19
+ {
20
+ console.log(chalk.bold('\nAvailable Agent Roles:'));
21
+ for (const r of listRoles()) {
22
+ console.log(` ${chalk.cyan(r.name)} (${chalk.gray(r.defaultAccess)}) - ${r.description}`);
23
+ }
24
+ console.log();
25
+ return true;
26
+ }
27
+ case '/agents':
28
+ {
29
+ reconcileStale(agent.workspaceRoot);
30
+ const sessions = listSessions(agent.workspaceRoot);
31
+ // `--json` for scripting. Emits a single JSON line on stdout so
32
+ // tmux-resurrect, status bars, agent pickers, and pipelines can
33
+ // parse the live session list reliably.
34
+ if (args.includes('--json')) {
35
+ const payload = sessions.map((s) => ({
36
+ id: s.id,
37
+ role: s.role,
38
+ status: s.status,
39
+ label: s.label,
40
+ startedAt: s.startedAt,
41
+ updatedAt: s.updatedAt,
42
+ completedAt: s.completedAt,
43
+ prompt: s.prompt,
44
+ usage: s.usage,
45
+ parentSessionKey: s.parentSessionKey,
46
+ finalOutputPreview: s.finalOutput ? String(s.finalOutput).slice(0, 280) : undefined,
47
+ }));
48
+ // process.stdout.write with no chalk so jq / scripts get clean JSON.
49
+ process.stdout.write(JSON.stringify({ sessions: payload }) + '\n');
50
+ return true;
51
+ }
52
+ console.log(chalk.bold('\nChild Agent Sessions:'));
53
+ if (sessions.length === 0) {
54
+ console.log(chalk.yellow(' No child agents yet. Use /spawn <role> <prompt> to start one.'));
55
+ }
56
+ else {
57
+ for (const s of sessions) {
58
+ const colorFn = s.status === 'completed' ? chalk.green :
59
+ s.status === 'failed' ? chalk.red :
60
+ s.status === 'stale' ? chalk.yellow :
61
+ s.status === 'closed' ? chalk.gray : chalk.cyan;
62
+ console.log(` ${colorFn(formatSessionSummary(s))}`);
63
+ if (s.usage) {
64
+ console.log(chalk.gray(` tokens: ${s.usage.promptTokens.toLocaleString()}↑ ${s.usage.completionTokens.toLocaleString()}↓ across ${s.usage.calls} call${s.usage.calls === 1 ? '' : 's'} (${s.usage.turns} turn${s.usage.turns === 1 ? '' : 's'})`));
65
+ }
66
+ if (s.prompt) {
67
+ console.log(chalk.gray(` prompt: ${s.prompt.replace(/\s+/g, ' ').slice(0, 100)}${s.prompt.length > 100 ? '…' : ''}`));
68
+ }
69
+ }
70
+ console.log(chalk.gray('\n (pipe-friendly output: /agents --json)'));
71
+ }
72
+ console.log();
73
+ return true;
74
+ }
75
+ case '/agent':
76
+ {
77
+ const id = args[0];
78
+ if (!id) {
79
+ console.log(chalk.red('\nUsage: /agent <id> [--full]\n'));
80
+ break;
81
+ }
82
+ const full = args.includes('--full');
83
+ const s = getSession(agent.workspaceRoot, id);
84
+ if (!s) {
85
+ console.log(chalk.red(`\nNo session ${id}\n`));
86
+ break;
87
+ }
88
+ console.log(chalk.bold(`\nAgent ${s.id}`));
89
+ console.log(` Role: ${chalk.cyan(s.role)} (${s.access})`);
90
+ console.log(` Status: ${chalk.yellow(s.status)}`);
91
+ console.log(` Started: ${chalk.gray(s.startedAt)}`);
92
+ if (s.completedAt)
93
+ console.log(` Ended: ${chalk.gray(s.completedAt)}`);
94
+ if (s.label)
95
+ console.log(` Label: ${s.label}`);
96
+ console.log(` Prompt: ${chalk.gray(s.prompt.slice(0, 240))}`);
97
+ if (s.usage) {
98
+ console.log(` Tokens: ${chalk.cyan(s.usage.promptTokens.toLocaleString())}↑ ${chalk.cyan(s.usage.completionTokens.toLocaleString())}↓ ${chalk.gray(`(${s.usage.calls} LLM call${s.usage.calls === 1 ? '' : 's'}, ${s.usage.turns} turn${s.usage.turns === 1 ? '' : 's'})`)}`);
99
+ }
100
+ if (s.finalOutput)
101
+ console.log(`\n${chalk.bold('Final output:')}\n${s.finalOutput}`);
102
+ if (s.error)
103
+ console.log(`\n${chalk.red('Error:')} ${s.error}`);
104
+ const entries = readTranscriptEntries(agent.workspaceRoot, childSessionKey(s.parentSessionKey, s.id), full ? 1000 : 10);
105
+ if (entries.length > 0) {
106
+ console.log(chalk.bold(`\n${full ? 'Full' : 'Recent'} transcript (${entries.length} entries):`));
107
+ for (const e of entries) {
108
+ const text = formatTranscriptContent(e.content ?? e.tool_calls ?? '');
109
+ const roleColor = e.role === 'user' ? chalk.yellow : e.role === 'assistant' ? chalk.green : e.role === 'tool' ? chalk.magenta : chalk.cyan;
110
+ console.log(` ${chalk.gray(e.timestamp)} ${roleColor(e.role)} ${chalk.gray(text)}`);
111
+ }
112
+ if (!full && entries.length === 10) {
113
+ console.log(chalk.gray(`\n (use /agent ${id} --full to see all entries)`));
114
+ }
115
+ }
116
+ console.log();
117
+ return true;
118
+ }
119
+ case '/spawn':
120
+ {
121
+ const role = args[0];
122
+ const prompt = args.slice(1).join(' ').trim();
123
+ if (!role || !prompt) {
124
+ console.log(chalk.red('\nUsage: /spawn <role> <prompt>\n'));
125
+ return true;
126
+ }
127
+ // Validate the role upfront — saves an LLM round-trip that would just
128
+ // error out server-side anyway.
129
+ const validRoles = listRoles().map((r) => r.name);
130
+ if (!validRoles.includes(role)) {
131
+ console.log(chalk.red(`\nUnknown role "${role}". Available: ${validRoles.join(', ')}.\n`));
132
+ return true;
133
+ }
134
+ ctx.repl.runAgentTurn(`Use the spawn_agent tool to start a ${role} child agent with this prompt:\n\n${prompt}\n\nReturn the child agent id when done.`);
135
+ return true;
136
+ }
137
+ case '/wait':
138
+ {
139
+ const id = args[0];
140
+ const ms = args[1] ? Number(args[1]) : 120000;
141
+ if (!id) {
142
+ console.log(chalk.red('\nUsage: /wait <id> [timeoutMs]\n'));
143
+ break;
144
+ }
145
+ ctx.repl.runAgentTurn(`Use the wait_agent tool with id="${id}" and timeoutMs=${ms}. Then summarize the child output for me.`);
146
+ return true;
147
+ }
148
+ case '/auto-review':
149
+ {
150
+ const prefs = readPreferences(agent.workspaceRoot);
151
+ const arg = args[0];
152
+ if (!arg) {
153
+ console.log(chalk.bold(`\nAuto-review: ${prefs.autoReview ? chalk.green('on') : chalk.gray('off')}`));
154
+ console.log(chalk.gray(' When on, every worker child agent is auto-followed by a reviewer agent on its diff.'));
155
+ console.log(chalk.gray(' Toggle with: /auto-review on | off\n'));
156
+ return true;
157
+ }
158
+ const next = arg === 'on' || arg === 'true';
159
+ writePreferences(agent.workspaceRoot, { autoReview: next });
160
+ console.log(chalk.green(`\n✓ Auto-review ${next ? 'enabled' : 'disabled'}.\n`));
161
+ return true;
162
+ }
163
+ case '/kill':
164
+ {
165
+ const id = args[0];
166
+ if (!id) {
167
+ console.log(chalk.red('\nUsage: /kill <agent-id>\n'));
168
+ break;
169
+ }
170
+ const session = getSession(agent.workspaceRoot, id);
171
+ if (!session) {
172
+ console.log(chalk.red(`\nNo agent session with id "${id}".\n`));
173
+ break;
174
+ }
175
+ if (session.status !== 'pending' && session.status !== 'running') {
176
+ console.log(chalk.gray(`\nAgent ${id} is already ${session.status}.\n`));
177
+ return true;
178
+ }
179
+ ctx.repl.runAgentTurn(`Use the close_agent tool with id="${id}" and reason="user-requested kill". Then confirm the close result.`);
180
+ return true;
181
+ }
182
+ case '/ps':
183
+ {
184
+ const loopState = getLoopState();
185
+ console.log(chalk.bold('\nBackground tasks'));
186
+ if (!loopState) {
187
+ console.log(chalk.gray(' No /loop running.'));
188
+ }
189
+ else {
190
+ console.log(` Loop: ${chalk.cyan(loopState.prompt)} (${loopState.iterations} ticks, every ${loopState.intervalMs}ms)`);
191
+ }
192
+ reconcileStale(agent.workspaceRoot);
193
+ const sessions = listSessions(agent.workspaceRoot).filter((s) => s.status === 'pending' || s.status === 'running');
194
+ if (sessions.length === 0) {
195
+ console.log(chalk.gray(' No running child agents.'));
196
+ }
197
+ else {
198
+ for (const s of sessions) {
199
+ console.log(` Agent: ${chalk.cyan(s.id)} ${chalk.gray(s.role)} (${s.status})`);
200
+ }
201
+ }
202
+ console.log();
203
+ return true;
204
+ }
205
+ case '/stop':
206
+ {
207
+ // Stop the loop AND mark any running children stale.
208
+ const stopped = stopLoop();
209
+ console.log(stopped ? chalk.green('\n✓ Stopped /loop.') : chalk.gray('\nNo loop was running.'));
210
+ const reconciled = reconcileStale(agent.workspaceRoot);
211
+ if (reconciled > 0)
212
+ console.log(chalk.yellow(`Marked ${reconciled} child session(s) stale.`));
213
+ console.log();
214
+ return true;
215
+ }
216
+ }
217
+ return false;
218
+ }
@@ -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 tryHandleSessionCommand(ctx: CommandContext): Promise<boolean>;
@@ -0,0 +1,191 @@
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 { randomUUID } from 'node:crypto';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import { marked } from 'marked';
9
+ import { listTranscripts, loadTranscript } from '../../state/sessionStore.js';
10
+ import { readGoal, resumeGoal } from '../../state/goalStore.js';
11
+ import { askYesNo } from '../cliPrompt.js';
12
+ import { buildGoalKickoffPrompt } from './_helpers.js';
13
+ export async function tryHandleSessionCommand(ctx) {
14
+ const { command, args, agent, mcpClient, config, rl, repl } = ctx;
15
+ // 'ctx' alias to keep references to the old ReplContext name working
16
+ const replCtx = repl;
17
+ switch (command) {
18
+ case '/sessions':
19
+ {
20
+ const transcripts = listTranscripts(agent.workspaceRoot);
21
+ console.log(chalk.bold('\nPersisted sessions:'));
22
+ if (transcripts.length === 0) {
23
+ console.log(chalk.yellow(' (none — start chatting and your transcript will appear here)'));
24
+ }
25
+ else {
26
+ for (const t of transcripts.slice(0, 30)) {
27
+ const when = t.modifiedAt.replace('T', ' ').slice(0, 19);
28
+ const isCurrent = t.sessionKey === agent.sessionKey;
29
+ const tag = isCurrent ? chalk.green(' (current)') : '';
30
+ console.log(` ${chalk.cyan(t.sessionKey)}${tag}`);
31
+ console.log(` ${chalk.gray(`${t.turnCount} entries · ${when}`)}`);
32
+ if (t.firstUserMessage)
33
+ console.log(` ${chalk.gray(`"${t.firstUserMessage}"`)}`);
34
+ }
35
+ console.log(chalk.gray('\nResume one with: /resume <sessionKey>'));
36
+ }
37
+ console.log();
38
+ return true;
39
+ }
40
+ case '/resume':
41
+ {
42
+ const sessionKey = args.join(' ').trim();
43
+ if (!sessionKey) {
44
+ console.log(chalk.red('\nUsage: /resume <sessionKey>\n'));
45
+ console.log(chalk.gray('Tip: copy a sessionKey from /sessions.\n'));
46
+ return true;
47
+ }
48
+ const entries = loadTranscript(agent.workspaceRoot, sessionKey);
49
+ if (entries.length === 0) {
50
+ console.log(chalk.red(`\nNo transcript found for "${sessionKey}".\n`));
51
+ return true;
52
+ }
53
+ agent.sessionKey = sessionKey;
54
+ const loaded = agent.loadHistory(entries);
55
+ console.log(chalk.green(`\n✓ Resumed session ${chalk.cyan(sessionKey)} with ${loaded} prior messages.`));
56
+ // If the resumed session has a goal that was suspended (paused,
57
+ // blocked, or hit usage limit), prompt the user whether to resume it
58
+ // now. Without this prompt the loop silently stays paused and the
59
+ // user has to remember to `/goal resume` — easy to miss.
60
+ const resumedGoal = readGoal(agent.workspaceRoot, sessionKey);
61
+ if (resumedGoal &&
62
+ (resumedGoal.status === 'paused' ||
63
+ resumedGoal.status === 'blocked' ||
64
+ resumedGoal.status === 'usage_limited')) {
65
+ const label = resumedGoal.status.replace('_', ' ');
66
+ console.log(chalk.yellow(`\n⏸ This session has a ${label} goal:`));
67
+ console.log(` ${chalk.cyan(resumedGoal.text)}`);
68
+ console.log(` ${chalk.gray(`${resumedGoal.budget.iterationsUsed}/${resumedGoal.budget.maxIterations} iterations used`)}${resumedGoal.blockedReason ? chalk.gray(` · ${resumedGoal.blockedReason}`) : ''}`);
69
+ const resume = await askYesNo('Resume the goal and continue auto-iteration? (y/N) ', false);
70
+ if (resume) {
71
+ // If the goal hit a budget cap, the user probably also wants to
72
+ // raise it — but don't force the question; they can /goal budget
73
+ // before/after. Just unpause and kick off the next iteration.
74
+ const reactivated = resumeGoal(agent.workspaceRoot, sessionKey);
75
+ if (reactivated) {
76
+ // Same rationale as /goal resume — drop any stale wrap-up
77
+ // steering left over from the budget-trigger that paused us.
78
+ agent.removeTaggedSystemMessage('goal-budget-steering');
79
+ agent.refreshSystemPrompt();
80
+ console.log(chalk.green(`\n▶ Goal resumed (${reactivated.budget.iterationsUsed}/${reactivated.budget.maxIterations} used). Starting next iteration…\n`));
81
+ ctx.repl.runAgentTurn(buildGoalKickoffPrompt(reactivated, 'resume'));
82
+ return true; // runAgentTurn owns its prompt cycle
83
+ }
84
+ }
85
+ else {
86
+ console.log(chalk.gray(`\nGoal stays ${label}. Run /goal resume later to continue.\n`));
87
+ }
88
+ }
89
+ else {
90
+ console.log(chalk.gray('Your next message will continue the conversation.\n'));
91
+ }
92
+ return true;
93
+ }
94
+ case '/fork':
95
+ {
96
+ const label = args.join(' ').trim() || `fork-${new Date().toISOString().slice(11, 19)}`;
97
+ const newKey = `${agent.sessionKey}:fork:${randomUUID().slice(0, 8)}:${label.replace(/[^A-Za-z0-9._-]+/g, '-')}`;
98
+ const previous = agent.sessionKey;
99
+ agent.fork(newKey);
100
+ console.log(chalk.green(`\n✓ Forked session.`));
101
+ console.log(chalk.gray(` Parent : ${previous}`));
102
+ console.log(chalk.gray(` New : ${newKey}`));
103
+ console.log(chalk.gray(' Your next message starts a new transcript while keeping prior context.\n'));
104
+ return true;
105
+ }
106
+ case '/rename':
107
+ {
108
+ const newName = args.join(' ').trim();
109
+ if (!newName) {
110
+ console.log(chalk.red('\nUsage: /rename <new session label>\n'));
111
+ return true;
112
+ }
113
+ const safe = newName.replace(/[^A-Za-z0-9._-]+/g, '-');
114
+ const previous = agent.sessionKey;
115
+ const newKey = `${previous.split(':')[0]}:${safe}`;
116
+ agent.sessionKey = newKey;
117
+ agent.refreshSystemPrompt();
118
+ console.log(chalk.green(`\n✓ Session renamed`));
119
+ console.log(chalk.gray(` Old: ${previous}`));
120
+ console.log(chalk.gray(` New: ${newKey}`));
121
+ console.log(chalk.gray(' (Future transcript entries land under the new key; existing entries stay under the old.)\n'));
122
+ return true;
123
+ }
124
+ case '/compact':
125
+ {
126
+ const spinner = ora(chalk.gray('Summarizing conversation for compaction...')).start();
127
+ try {
128
+ const result = await agent.compactHistory();
129
+ if (!result) {
130
+ spinner.warn(chalk.yellow('Nothing to compact — chat history is already short.'));
131
+ return true;
132
+ }
133
+ spinner.succeed(chalk.green(`Compacted ${result.replacedMessages} messages → ~${result.estimatedTokens} tokens (${result.durationMs}ms).`));
134
+ console.log(chalk.bold('\nCompaction summary:'));
135
+ console.log(marked.parse(result.summary));
136
+ console.log(chalk.gray('The summary is now part of system context. Continue normally.\n'));
137
+ }
138
+ catch (err) {
139
+ spinner.fail(chalk.red(`Compaction failed: ${err.message}`));
140
+ console.log(chalk.gray('Fallback: nothing was changed. Use /clear if you want to drop history without summarizing.\n'));
141
+ }
142
+ return true;
143
+ }
144
+ case '/new':
145
+ {
146
+ const label = args.join(' ').trim() || `new-${new Date().toISOString().slice(11, 19)}`;
147
+ const newKey = `${agent.sessionKey.split(':')[0]}:${label.replace(/[^A-Za-z0-9._-]+/g, '-')}`;
148
+ const previous = agent.sessionKey;
149
+ agent.sessionKey = newKey;
150
+ agent.clearHistory();
151
+ console.log(chalk.green(`\n✓ Started a new chat.`));
152
+ console.log(chalk.gray(` Old: ${previous}`));
153
+ console.log(chalk.gray(` New: ${newKey}\n`));
154
+ return true;
155
+ }
156
+ case '/side':
157
+ case '/btw':
158
+ {
159
+ const prompt = args.join(' ').trim();
160
+ if (!prompt) {
161
+ console.log(chalk.red(`\nUsage: ${command} <ephemeral side question>\n`));
162
+ console.log(chalk.gray(' Side conversations run in a forked chat history and discard the result on exit.\n'));
163
+ return true;
164
+ }
165
+ const original = agent.sessionKey;
166
+ const sideKey = `${original}:side:${randomUUID().slice(0, 6)}`;
167
+ agent.sessionKey = sideKey;
168
+ console.log(chalk.gray(`(side conversation in ${sideKey} — answer is ephemeral)\n`));
169
+ // Fire-and-forget BUT restore the sessionKey when the turn finishes,
170
+ // not after a fixed 100ms. The old setTimeout race restored the key
171
+ // long before the turn finished its async work — capture, transcript
172
+ // writes, contradiction checks — so side-conversation tool messages
173
+ // and the assistant reply ended up appended to the MAIN session.
174
+ void ctx.repl.runAgentTurnAsync(prompt).finally(() => {
175
+ agent.sessionKey = original;
176
+ });
177
+ return true;
178
+ }
179
+ case '/clear': {
180
+ agent.clearHistory();
181
+ console.log(chalk.yellow('\nConversation history cleared.\n'));
182
+ return true;
183
+ }
184
+ case '/quit':
185
+ case '/exit': {
186
+ rl.close();
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ }
@@ -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 tryHandleUiCommand(ctx: CommandContext): Promise<boolean>;