@kinqs/brainrouter-cli 0.3.5 → 0.3.7
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/README.md +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/bin/cli.cjs +71 -0
- package/dist/agent/agent.d.ts +224 -3
- package/dist/agent/agent.js +561 -55
- package/dist/cli/banner.d.ts +80 -0
- package/dist/cli/banner.js +232 -0
- package/dist/cli/cliPrompt.d.ts +106 -0
- package/dist/cli/cliPrompt.js +314 -0
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/_helpers.js +6 -6
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/guard.js +75 -10
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +19 -0
- package/dist/cli/commands/mcp.js +286 -0
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/obs.js +22 -22
- package/dist/cli/commands/orchestration.js +18 -0
- package/dist/cli/commands/session.js +13 -5
- package/dist/cli/commands/ui.js +202 -91
- package/dist/cli/commands/workflow.d.ts +20 -0
- package/dist/cli/commands/workflow.js +368 -51
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +65 -0
- package/dist/cli/ink/Picker.js +133 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +571 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +73 -0
- package/dist/cli/ink/toolFormat.js +180 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +64 -646
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/spinner.d.ts +34 -0
- package/dist/cli/spinner.js +36 -0
- package/dist/cli/statusline.d.ts +67 -0
- package/dist/cli/statusline.js +204 -0
- package/dist/cli/theme.d.ts +79 -0
- package/dist/cli/theme.js +106 -0
- package/dist/cli/whereView.d.ts +81 -0
- package/dist/cli/whereView.js +245 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +52 -0
- package/dist/config/config.js +89 -75
- package/dist/index.js +215 -206
- package/dist/memory/briefing.d.ts +11 -1
- package/dist/memory/briefing.js +69 -1
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +10 -1
- package/dist/orchestration/tools.js +48 -4
- package/dist/prompt/breadthHint.d.ts +5 -0
- package/dist/prompt/breadthHint.js +44 -0
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.d.ts +34 -0
- package/dist/prompt/systemPrompt.js +128 -108
- package/dist/runtime/dangerousCommand.d.ts +53 -0
- package/dist/runtime/dangerousCommand.js +105 -0
- package/dist/runtime/mcpClient.d.ts +38 -1
- package/dist/runtime/mcpClient.js +104 -13
- package/dist/runtime/mcpPool.d.ts +162 -0
- package/dist/runtime/mcpPool.js +423 -0
- package/dist/runtime/mcpUtils.d.ts +3 -1
- package/dist/state/goalStore.d.ts +98 -17
- package/dist/state/goalStore.js +132 -42
- package/dist/state/preferencesStore.d.ts +67 -3
- package/dist/state/preferencesStore.js +84 -1
- package/dist/state/workflowArtifacts.d.ts +63 -2
- package/dist/state/workflowArtifacts.js +120 -8
- package/dist/tests/_helpers.d.ts +31 -0
- package/dist/tests/_helpers.js +91 -0
- package/package.json +12 -5
- package/.env.example +0 -109
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Config } from '../config/config.js';
|
|
2
|
+
import type { Goal } from '../state/goalStore.js';
|
|
3
|
+
import { type Theme } from './theme.js';
|
|
4
|
+
export interface BannerInputs {
|
|
5
|
+
workspaceRoot: string;
|
|
6
|
+
/** "local-http", "stdio", "custom" — config.activeServer. */
|
|
7
|
+
mcpProfile: string;
|
|
8
|
+
/** "stdio" | "http". */
|
|
9
|
+
mcpTransport: string;
|
|
10
|
+
/** True when the MCP handshake succeeded. */
|
|
11
|
+
mcpOnline: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* 10c: which MCP this profile actually IS — drives the distinct "brain"
|
|
14
|
+
* row when the active MCP is BrainRouter (or unknown, which we treat as
|
|
15
|
+
* "likely brain"). When the active MCP is explicitly third-party, the
|
|
16
|
+
* brain row is omitted entirely so the box stays compact.
|
|
17
|
+
*/
|
|
18
|
+
mcpIdentity?: 'brainrouter' | 'third-party' | 'unknown';
|
|
19
|
+
/** Resolved sessionKey for this CLI process. */
|
|
20
|
+
sessionKey: string;
|
|
21
|
+
/** Chat-LLM model name (e.g. gpt-4o-mini). */
|
|
22
|
+
model: string;
|
|
23
|
+
/** Slug + status of the currently-bound workflow, if any. */
|
|
24
|
+
workflow?: {
|
|
25
|
+
slug: string;
|
|
26
|
+
status: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Slug of the last workflow that was active in this workspace, surfaced
|
|
30
|
+
* only when the current session has NO workflow bound. Rendered as a
|
|
31
|
+
* one-line hint so the user can `/workflow switch <slug>` to resume.
|
|
32
|
+
* Doesn't auto-bind anything — workflows are pure storage now (goals
|
|
33
|
+
* are session-scoped runtime state). Empty / matching the active
|
|
34
|
+
* workflow → no hint row.
|
|
35
|
+
*/
|
|
36
|
+
lastUsedWorkflow?: string;
|
|
37
|
+
/** Goal-store snapshot, if any. */
|
|
38
|
+
goal?: Goal;
|
|
39
|
+
/** Version override (test fixture). */
|
|
40
|
+
version?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface DisplayedMcpState {
|
|
43
|
+
profile: string;
|
|
44
|
+
transport: string;
|
|
45
|
+
online: boolean;
|
|
46
|
+
identity: 'brainrouter' | 'third-party' | 'unknown';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Pure renderer — returns the box as a single newline-joined string with
|
|
50
|
+
* ANSI sequences from `theme`. Caller appends the trailing newline.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderBanner(inputs: BannerInputs, theme: Theme): string;
|
|
53
|
+
/**
|
|
54
|
+
* Convenience: assemble the inputs from live agent + config + workspace
|
|
55
|
+
* state. Pure read; no side effects. Anything that throws while reading the
|
|
56
|
+
* goal or current-workflow files is treated as "not set" so a half-set-up
|
|
57
|
+
* workspace doesn't crash the banner.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildBannerInputs(config: Config, agent: {
|
|
60
|
+
sessionKey: string;
|
|
61
|
+
workspaceRoot: string;
|
|
62
|
+
getModel: () => string;
|
|
63
|
+
}, mcpClient: {
|
|
64
|
+
isConnected: () => boolean;
|
|
65
|
+
getIdentity?: () => 'brainrouter' | 'third-party' | 'unknown';
|
|
66
|
+
getStatus?: (serverId: string) => {
|
|
67
|
+
status: string;
|
|
68
|
+
identity: 'brainrouter' | 'third-party' | 'unknown';
|
|
69
|
+
} | undefined;
|
|
70
|
+
getActiveBrainrouterServerId?: () => string | undefined;
|
|
71
|
+
}): BannerInputs;
|
|
72
|
+
export declare function resolveDisplayedMcpState(config: Config, mcpClient: {
|
|
73
|
+
isConnected: () => boolean;
|
|
74
|
+
getIdentity?: () => 'brainrouter' | 'third-party' | 'unknown';
|
|
75
|
+
getStatus?: (serverId: string) => {
|
|
76
|
+
status: string;
|
|
77
|
+
identity: 'brainrouter' | 'third-party' | 'unknown';
|
|
78
|
+
} | undefined;
|
|
79
|
+
getActiveBrainrouterServerId?: () => string | undefined;
|
|
80
|
+
}): DisplayedMcpState;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { formatBudget } from '../state/goalStore.js';
|
|
4
|
+
import { getCurrentWorkflow, getLastUsedWorkflow } from '../state/workflowArtifacts.js';
|
|
5
|
+
import { readGoal } from '../state/goalStore.js';
|
|
6
|
+
import { BOX } from './theme.js';
|
|
7
|
+
/**
|
|
8
|
+
* Compose the boxed startup banner. Replaces the prior three-line text dump
|
|
9
|
+
* (chalk title + workspace line + connecting-to line) with a single visually
|
|
10
|
+
* scannable block:
|
|
11
|
+
*
|
|
12
|
+
* ╭─ 🧠 BrainRouter CLI 0.3.7 ──────────────────────────────╮
|
|
13
|
+
* │ workspace BrainRouter · c5b8c12d │
|
|
14
|
+
* │ mcp local-http · http · online │
|
|
15
|
+
* │ workflow cli-shell-redesign (in-progress) │
|
|
16
|
+
* │ goal active 3 of unlimited iterations │
|
|
17
|
+
* │ session 7f3a1e0c │
|
|
18
|
+
* │ model gpt-4o-mini │
|
|
19
|
+
* ╰─────────────────────────────────────────────────────────╯
|
|
20
|
+
*
|
|
21
|
+
* Designed so a glance tells the user where they are: which repo, which MCP
|
|
22
|
+
* profile, what's the in-flight goal, which model is running. Anything not
|
|
23
|
+
* applicable (e.g. no goal set, no current workflow) is silently omitted —
|
|
24
|
+
* the box shrinks to fit instead of showing "—" placeholders.
|
|
25
|
+
*
|
|
26
|
+
* The function returns a single string with embedded ANSI; the caller prints
|
|
27
|
+
* it once. Pure-function so tests can assert against the rendered output.
|
|
28
|
+
*/
|
|
29
|
+
const VERSION = '0.3.7';
|
|
30
|
+
const TITLE = '🧠 BrainRouter CLI';
|
|
31
|
+
// Width floor for the BOXED banner. Below this we fall through to the
|
|
32
|
+
// `renderPlainBanner` plaintext format. Was 56 — that caused the box to
|
|
33
|
+
// overflow on terminals narrower than 58 cols (each row wrapped to
|
|
34
|
+
// multiple terminal rows with broken border alignment). 38 fits a
|
|
35
|
+
// 40-col terminal (the smallest realistic phone / split-pane width).
|
|
36
|
+
const MIN_BOX_WIDTH = 38;
|
|
37
|
+
const MAX_WIDTH = 100;
|
|
38
|
+
// Below this width we skip the box entirely and render the rows as
|
|
39
|
+
// "label: value" lines. The boxed format with horizontal borders +
|
|
40
|
+
// title is meaningless when each border row wraps.
|
|
41
|
+
const PLAIN_TEXT_THRESHOLD = 38;
|
|
42
|
+
function shortHash(absPath) {
|
|
43
|
+
return crypto.createHash('sha1').update(absPath).digest('hex').slice(0, 8);
|
|
44
|
+
}
|
|
45
|
+
function formatGoalSummary(goal) {
|
|
46
|
+
const cap = formatBudget(goal.budget.maxIterations);
|
|
47
|
+
const used = goal.budget.iterationsUsed;
|
|
48
|
+
// Status verbs read naturally inline. "usage_limited" → "limited".
|
|
49
|
+
const statusWord = goal.status === 'usage_limited' ? 'limited' :
|
|
50
|
+
goal.status === 'active' ? 'active' :
|
|
51
|
+
goal.status;
|
|
52
|
+
if (goal.status === 'complete')
|
|
53
|
+
return 'complete';
|
|
54
|
+
if (goal.status === 'blocked')
|
|
55
|
+
return `blocked — ${goal.blockedReason?.slice(0, 40) ?? 'see /goal'}`;
|
|
56
|
+
return `${statusWord} ${used} of ${cap} iterations`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Workspace label — basename of the workspace root, with a short hash so
|
|
60
|
+
* two repos with the same basename (e.g. two clones of "playground") don't
|
|
61
|
+
* look identical.
|
|
62
|
+
*/
|
|
63
|
+
function formatWorkspace(workspaceRoot) {
|
|
64
|
+
const base = path.basename(workspaceRoot) || workspaceRoot;
|
|
65
|
+
return `${base} · ${shortHash(workspaceRoot)}`;
|
|
66
|
+
}
|
|
67
|
+
function formatMcp(profile, transport, online) {
|
|
68
|
+
const dot = online ? 'online' : 'offline';
|
|
69
|
+
return `${profile} · ${transport} · ${dot}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 10c: brain row — distinct from the generic MCP row so the user can tell
|
|
73
|
+
* "the BrainRouter cloud brain is down" from "a third-party MCP is down".
|
|
74
|
+
* Renders only when the active MCP is BrainRouter (or unknown). Returns
|
|
75
|
+
* `undefined` when there's nothing meaningful to say (e.g. user only has a
|
|
76
|
+
* third-party MCP connected).
|
|
77
|
+
*/
|
|
78
|
+
function formatBrain(identity, online) {
|
|
79
|
+
if (identity === 'third-party')
|
|
80
|
+
return undefined;
|
|
81
|
+
if (identity === 'unknown')
|
|
82
|
+
return undefined; // wait for tool-signature detection
|
|
83
|
+
if (online)
|
|
84
|
+
return '🟢 online';
|
|
85
|
+
return '🔴 offline · cloud unreachable';
|
|
86
|
+
}
|
|
87
|
+
function formatWorkflow(workflow) {
|
|
88
|
+
if (!workflow)
|
|
89
|
+
return undefined;
|
|
90
|
+
return `${workflow.slug} (${workflow.status})`;
|
|
91
|
+
}
|
|
92
|
+
function clipValue(value, width) {
|
|
93
|
+
if (value.length <= width)
|
|
94
|
+
return value;
|
|
95
|
+
if (width <= 1)
|
|
96
|
+
return value.slice(0, width);
|
|
97
|
+
return value.slice(0, width - 1) + '…';
|
|
98
|
+
}
|
|
99
|
+
function padRight(s, width) {
|
|
100
|
+
return s.length >= width ? s : s + ' '.repeat(width - s.length);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Pure renderer — returns the box as a single newline-joined string with
|
|
104
|
+
* ANSI sequences from `theme`. Caller appends the trailing newline.
|
|
105
|
+
*/
|
|
106
|
+
export function renderBanner(inputs, theme) {
|
|
107
|
+
const rows = [];
|
|
108
|
+
rows.push({ label: 'workspace', value: formatWorkspace(inputs.workspaceRoot) });
|
|
109
|
+
rows.push({ label: 'mcp', value: formatMcp(inputs.mcpProfile, inputs.mcpTransport, inputs.mcpOnline) });
|
|
110
|
+
// 10c: brain status sits below the mcp row — same level of visibility,
|
|
111
|
+
// but distinct so multi-MCP setups (Item 11) won't be ambiguous.
|
|
112
|
+
const brain = formatBrain(inputs.mcpIdentity, inputs.mcpOnline);
|
|
113
|
+
if (brain)
|
|
114
|
+
rows.push({ label: 'brain', value: brain });
|
|
115
|
+
const wf = formatWorkflow(inputs.workflow);
|
|
116
|
+
if (wf) {
|
|
117
|
+
rows.push({ label: 'workflow', value: wf });
|
|
118
|
+
}
|
|
119
|
+
else if (inputs.lastUsedWorkflow) {
|
|
120
|
+
// Fresh session with no current workflow but a known last-used
|
|
121
|
+
// workflow in this workspace — offer the resume incantation without
|
|
122
|
+
// auto-binding. Quiet so the user notices but isn't pushed into it.
|
|
123
|
+
rows.push({ label: 'last on', value: `${inputs.lastUsedWorkflow} /workflow switch ${inputs.lastUsedWorkflow}` });
|
|
124
|
+
}
|
|
125
|
+
if (inputs.goal)
|
|
126
|
+
rows.push({ label: 'goal', value: formatGoalSummary(inputs.goal) });
|
|
127
|
+
rows.push({ label: 'session', value: inputs.sessionKey.slice(0, 8) });
|
|
128
|
+
rows.push({ label: 'model', value: inputs.model });
|
|
129
|
+
const version = inputs.version ?? VERSION;
|
|
130
|
+
const titleText = `${TITLE} ${version}`;
|
|
131
|
+
const labelWidth = rows.reduce((w, r) => Math.max(w, r.label.length), 0);
|
|
132
|
+
// Inner width is the widest "label + 2 spaces + value", clamped.
|
|
133
|
+
const naturalInner = rows.reduce((w, r) => Math.max(w, labelWidth + 2 + r.value.length), titleText.length + 4);
|
|
134
|
+
const targetCols = process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : MAX_WIDTH;
|
|
135
|
+
// Below the plaintext threshold the boxed layout is hostile (each
|
|
136
|
+
// border row wraps and looks chaotic). Fall back to a label:value
|
|
137
|
+
// text dump that the terminal can wrap naturally.
|
|
138
|
+
if (targetCols < PLAIN_TEXT_THRESHOLD) {
|
|
139
|
+
return renderPlainBanner(titleText, rows, theme);
|
|
140
|
+
}
|
|
141
|
+
// Reserve 2 columns for the side borders.
|
|
142
|
+
const innerWidth = Math.max(MIN_BOX_WIDTH, Math.min(MAX_WIDTH, Math.min(naturalInner, targetCols - 2)));
|
|
143
|
+
const top = (() => {
|
|
144
|
+
// ╭─ <title> ──╮ — title sits inline at the top border.
|
|
145
|
+
const titlePiece = ` ${titleText} `;
|
|
146
|
+
const horizontalFill = Math.max(0, innerWidth - 1 - titlePiece.length);
|
|
147
|
+
return theme.primary(BOX.topLeft + BOX.horizontal + titlePiece + BOX.horizontal.repeat(horizontalFill) + BOX.topRight);
|
|
148
|
+
})();
|
|
149
|
+
const bodyLines = rows.map((row) => {
|
|
150
|
+
const valueWidth = innerWidth - labelWidth - 3; // 1 left pad + 2 gap
|
|
151
|
+
const clipped = clipValue(row.value, valueWidth);
|
|
152
|
+
const inside = ' ' + theme.muted(padRight(row.label, labelWidth)) + ' ' + theme.plain(padRight(clipped, valueWidth));
|
|
153
|
+
return theme.primary(BOX.vertical) + inside + theme.primary(BOX.vertical);
|
|
154
|
+
});
|
|
155
|
+
const bottom = theme.primary(BOX.bottomLeft + BOX.horizontal.repeat(innerWidth) + BOX.bottomRight);
|
|
156
|
+
return [top, ...bodyLines, bottom].join('\n');
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Compact label:value text banner — used on terminals narrower than
|
|
160
|
+
* PLAIN_TEXT_THRESHOLD cols where the boxed layout's border rows
|
|
161
|
+
* would wrap and look broken. Same information, no chrome.
|
|
162
|
+
*/
|
|
163
|
+
function renderPlainBanner(titleText, rows, theme) {
|
|
164
|
+
const labelWidth = rows.reduce((w, r) => Math.max(w, r.label.length), 0);
|
|
165
|
+
const headerLine = theme.primary(titleText);
|
|
166
|
+
const bodyLines = rows.map((row) => theme.muted(padRight(row.label, labelWidth) + ' ') + theme.plain(row.value));
|
|
167
|
+
return [headerLine, ...bodyLines].join('\n');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Convenience: assemble the inputs from live agent + config + workspace
|
|
171
|
+
* state. Pure read; no side effects. Anything that throws while reading the
|
|
172
|
+
* goal or current-workflow files is treated as "not set" so a half-set-up
|
|
173
|
+
* workspace doesn't crash the banner.
|
|
174
|
+
*/
|
|
175
|
+
export function buildBannerInputs(config, agent, mcpClient) {
|
|
176
|
+
const displayedMcp = resolveDisplayedMcpState(config, mcpClient);
|
|
177
|
+
let workflow;
|
|
178
|
+
let lastUsedWorkflow;
|
|
179
|
+
try {
|
|
180
|
+
// 9d-bugfix: read the session-scoped binding so a fresh CLI session
|
|
181
|
+
// shows no workflow row even when another CLI in the same workspace
|
|
182
|
+
// has one bound.
|
|
183
|
+
const slug = getCurrentWorkflow(agent.workspaceRoot, agent.sessionKey);
|
|
184
|
+
if (slug) {
|
|
185
|
+
// We don't crack open workflowArtifacts.listWorkflows here — just the
|
|
186
|
+
// pointer file. Status would require parsing meta.json, which has its
|
|
187
|
+
// own cost on a slow disk; "bound" is enough to communicate state.
|
|
188
|
+
workflow = { slug, status: 'bound' };
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Fresh session in a workspace where a previous CLI was on
|
|
192
|
+
// workflow X — surface that as a hint so the user can rebind via
|
|
193
|
+
// `/workflow switch X` if they want continuity. Doesn't auto-bind
|
|
194
|
+
// (per the decoupling design — workflows are storage, goals are
|
|
195
|
+
// runtime, the two have orthogonal lifecycles).
|
|
196
|
+
try {
|
|
197
|
+
lastUsedWorkflow = getLastUsedWorkflow(agent.workspaceRoot);
|
|
198
|
+
}
|
|
199
|
+
catch { /* ignore */ }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch { /* ignore — no workflow bound */ }
|
|
203
|
+
let goal;
|
|
204
|
+
try {
|
|
205
|
+
goal = readGoal(agent.workspaceRoot, agent.sessionKey) ?? undefined;
|
|
206
|
+
}
|
|
207
|
+
catch { /* ignore — no goal yet */ }
|
|
208
|
+
return {
|
|
209
|
+
workspaceRoot: agent.workspaceRoot,
|
|
210
|
+
mcpProfile: displayedMcp.profile,
|
|
211
|
+
mcpTransport: displayedMcp.transport,
|
|
212
|
+
mcpOnline: displayedMcp.online,
|
|
213
|
+
mcpIdentity: displayedMcp.identity,
|
|
214
|
+
sessionKey: agent.sessionKey,
|
|
215
|
+
model: agent.getModel(),
|
|
216
|
+
workflow,
|
|
217
|
+
lastUsedWorkflow,
|
|
218
|
+
goal,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function resolveDisplayedMcpState(config, mcpClient) {
|
|
222
|
+
const liveBrain = mcpClient.getActiveBrainrouterServerId?.();
|
|
223
|
+
const profile = liveBrain || config.activeServer;
|
|
224
|
+
const server = config.servers[profile];
|
|
225
|
+
const status = profile ? mcpClient.getStatus?.(profile) : undefined;
|
|
226
|
+
return {
|
|
227
|
+
profile,
|
|
228
|
+
transport: server?.type ?? 'unknown',
|
|
229
|
+
online: status ? status.status === 'connected' : mcpClient.isConnected(),
|
|
230
|
+
identity: status?.identity ?? server?.identity ?? mcpClient.getIdentity?.() ?? 'unknown',
|
|
231
|
+
};
|
|
232
|
+
}
|
package/dist/cli/cliPrompt.d.ts
CHANGED
|
@@ -1,12 +1,118 @@
|
|
|
1
1
|
import readline from 'node:readline';
|
|
2
2
|
export declare function setActiveReadline(rl: readline.Interface | undefined): void;
|
|
3
3
|
export declare function getActiveReadline(): readline.Interface | undefined;
|
|
4
|
+
export declare function isPickerActive(): boolean;
|
|
4
5
|
/**
|
|
5
6
|
* One-shot yes/no question. Returns true only when the user types y/yes
|
|
6
7
|
* (case-insensitive). Returns the supplied default when stdin isn't a TTY
|
|
7
8
|
* (e.g. piped non-interactive runs).
|
|
8
9
|
*/
|
|
9
10
|
export declare function askYesNo(question: string, defaultValue?: boolean): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Surfaced when `askChoice` is called outside an interactive TTY. The tool
|
|
13
|
+
* wrapper turns this into a tool-call error so the LLM falls back to deciding
|
|
14
|
+
* itself — silently picking option 1 for the agent in CI / piped runs would
|
|
15
|
+
* make a load-bearing decision the user never saw.
|
|
16
|
+
*/
|
|
17
|
+
export declare class NoTTYError extends Error {
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Surfaced when the user pressed Esc / q / Ctrl+C inside the picker.
|
|
22
|
+
* The tool wrapper converts this into a tool-call error so the LLM knows
|
|
23
|
+
* the user declined to commit and can re-plan instead of guessing.
|
|
24
|
+
*/
|
|
25
|
+
export declare class CancelledChoiceError extends Error {
|
|
26
|
+
constructor(message?: string);
|
|
27
|
+
}
|
|
28
|
+
export interface ChoiceOption {
|
|
29
|
+
label: string;
|
|
30
|
+
description: string;
|
|
31
|
+
}
|
|
32
|
+
export interface PickerState {
|
|
33
|
+
/** Includes the synthetic Other entry at the last index. */
|
|
34
|
+
options: ChoiceOption[];
|
|
35
|
+
cursor: number;
|
|
36
|
+
multiSelect: boolean;
|
|
37
|
+
/** Indices of options the user has toggled on (multi-select only). */
|
|
38
|
+
selected: Set<number>;
|
|
39
|
+
/** True once the user confirmed Other and we're collecting free text. */
|
|
40
|
+
awaitingOther: boolean;
|
|
41
|
+
/** Accumulated free-text for the Other prompt. */
|
|
42
|
+
otherText: string;
|
|
43
|
+
done: boolean;
|
|
44
|
+
cancelled: boolean;
|
|
45
|
+
/** Final resolved value when `done && !cancelled`. */
|
|
46
|
+
result: string | string[] | null;
|
|
47
|
+
}
|
|
48
|
+
/** Normalized keystroke shape the reducer consumes. Decoupled from Node's
|
|
49
|
+
* raw `keypress` event so the reducer can be driven by test inputs that
|
|
50
|
+
* don't go through `emitKeypressEvents`. */
|
|
51
|
+
export interface PickerKey {
|
|
52
|
+
name?: string;
|
|
53
|
+
ctrl?: boolean;
|
|
54
|
+
sequence?: string;
|
|
55
|
+
/** A single printable character for free-text capture (Other phase). */
|
|
56
|
+
char?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `prefilledOther` drops the picker straight into the free-text "Other"
|
|
60
|
+
* phase with the supplied string already in the buffer. Used by the
|
|
61
|
+
* 0.3.7 wizard / `/config` panel when a value can be derived from an
|
|
62
|
+
* env var — the user sees the env value and presses ENTER to accept or
|
|
63
|
+
* edits to override. Pass an empty string to keep today's behaviour.
|
|
64
|
+
*
|
|
65
|
+
* `initialCursor` lets a picker open with a non-zero highlight so the
|
|
66
|
+
* settings home panel can re-open on the row the user just edited
|
|
67
|
+
* without re-scrolling them to the top.
|
|
68
|
+
*/
|
|
69
|
+
export interface InitPickerStateOptions {
|
|
70
|
+
prefilledOther?: string;
|
|
71
|
+
initialCursor?: number;
|
|
72
|
+
}
|
|
73
|
+
export declare function initPickerState(options: ChoiceOption[], multiSelect: boolean, init?: InitPickerStateOptions): PickerState;
|
|
74
|
+
export declare function reducePicker(state: PickerState, key: PickerKey): PickerState;
|
|
75
|
+
export declare function renderPicker(state: PickerState, question: string, header?: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Mid-turn multi-choice prompt with arrow-key navigation, a checkbox UI
|
|
78
|
+
* for multi-select, and an always-on "Other" option that drops to free-text
|
|
79
|
+
* input. Pause/resume the parent REPL the same way `askYesNo` does, so it
|
|
80
|
+
* composes cleanly with the existing readline bridge.
|
|
81
|
+
*
|
|
82
|
+
* Non-TTY behavior is strict: throws `NoTTYError` instead of defaulting to
|
|
83
|
+
* option 1. The agent calling this is asking the human for judgment; making
|
|
84
|
+
* the call for them in CI / piped / `brainrouter run` would silently commit
|
|
85
|
+
* to a path the user never saw.
|
|
86
|
+
*
|
|
87
|
+
* User cancellation (Esc, q, Ctrl+C) throws `CancelledChoiceError` so the
|
|
88
|
+
* tool wrapper can surface "user declined to commit" as a tool-call error.
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* 0.3.7 picker opts.
|
|
92
|
+
*
|
|
93
|
+
* `onCursorChange(index)` fires after every arrow-key move that actually
|
|
94
|
+
* moves the cursor (no-op keys, ENTER, SPACE don't fire it). The 0.3.7
|
|
95
|
+
* theme picker uses this to live-preview the selected theme by redrawing
|
|
96
|
+
* the banner accent before the user confirms — pattern lifted from
|
|
97
|
+
* `openSrc/codex/codex-rs/tui/src/bottom_pane/list_selection_view.rs` and
|
|
98
|
+
* `openSrc/codex/codex-rs/tui/src/theme_picker.rs`.
|
|
99
|
+
*
|
|
100
|
+
* `prefilledOther` opens the picker with the synthetic "Other" row
|
|
101
|
+
* already selected AND the free-text input pre-filled. Used when a value
|
|
102
|
+
* is derived from an env var so the user can press ENTER to accept or
|
|
103
|
+
* edit in-place. Pre-fill flips `awaitingOther` true on init.
|
|
104
|
+
*
|
|
105
|
+
* `initialCursor` lets the settings home panel re-open on the row the
|
|
106
|
+
* user just left, avoiding a snap-to-row-0 after every sub-picker.
|
|
107
|
+
*/
|
|
108
|
+
export interface AskChoiceOptions {
|
|
109
|
+
multiSelect?: boolean;
|
|
110
|
+
header?: string;
|
|
111
|
+
onCursorChange?: (cursor: number) => void;
|
|
112
|
+
prefilledOther?: string;
|
|
113
|
+
initialCursor?: number;
|
|
114
|
+
}
|
|
115
|
+
export declare function askChoice(question: string, options: ChoiceOption[], opts?: AskChoiceOptions): Promise<string | string[]>;
|
|
10
116
|
/**
|
|
11
117
|
* Print a line of output while the prompt is showing, then redraw the prompt
|
|
12
118
|
* with whatever the user was mid-typing. Used by callbacks that fire while the
|