ai-runtime-engine 1.3.0 → 2.8.0
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/CHANGELOG.md +638 -0
- package/dist/agents/admit.d.ts +69 -0
- package/dist/agents/admit.js +129 -0
- package/dist/agents/definition.d.ts +36 -0
- package/dist/agents/definition.js +9 -0
- package/dist/agents/envelope.d.ts +53 -0
- package/dist/agents/envelope.js +68 -0
- package/dist/agents/finding.d.ts +79 -0
- package/dist/agents/finding.js +80 -0
- package/dist/agents/roles.d.ts +36 -0
- package/dist/agents/roles.js +44 -0
- package/dist/agents/synthesize.d.ts +44 -0
- package/dist/agents/synthesize.js +60 -0
- package/dist/agents/task.d.ts +112 -0
- package/dist/agents/task.js +48 -0
- package/dist/agents/worker.d.ts +91 -0
- package/dist/agents/worker.js +377 -0
- package/dist/capabilities/capability.d.ts +117 -0
- package/dist/capabilities/capability.js +66 -0
- package/dist/capabilities/registry.d.ts +139 -0
- package/dist/capabilities/registry.js +413 -0
- package/dist/capabilities/vocabulary.d.ts +32 -0
- package/dist/capabilities/vocabulary.js +34 -0
- package/dist/cli/cli.js +55 -4
- package/dist/cli/commands/cleanup.js +29 -27
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +38 -8
- package/dist/cli/commands/executions.js +34 -25
- package/dist/cli/commands/info.d.ts +1 -0
- package/dist/cli/commands/info.js +11 -9
- package/dist/cli/commands/init.js +19 -0
- package/dist/cli/commands/inspect.d.ts +40 -1
- package/dist/cli/commands/inspect.js +157 -2
- package/dist/cli/commands/mcp.d.ts +45 -0
- package/dist/cli/commands/mcp.js +148 -0
- package/dist/cli/commands/route.js +21 -0
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.js +21 -2
- package/dist/cli/commands/skills.d.ts +2 -0
- package/dist/cli/commands/skills.js +29 -7
- package/dist/cli/interactive/ansi.d.ts +41 -0
- package/dist/cli/interactive/ansi.js +43 -0
- package/dist/cli/interactive/complete.d.ts +10 -0
- package/dist/cli/interactive/complete.js +19 -0
- package/dist/cli/interactive/lanes.d.ts +69 -0
- package/dist/cli/interactive/lanes.js +181 -0
- package/dist/cli/interactive/repl.d.ts +3 -0
- package/dist/cli/interactive/repl.js +91 -13
- package/dist/cli/interactive/session.d.ts +8 -0
- package/dist/cli/interactive/session.js +73 -2
- package/dist/cli/render.d.ts +7 -0
- package/dist/cli/render.js +10 -0
- package/dist/cli/runtimeSession.d.ts +11 -0
- package/dist/cli/runtimeSession.js +17 -0
- package/dist/config/defaults.d.ts +3 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +2 -2
- package/dist/context/lossVerifier.d.ts +24 -0
- package/dist/context/lossVerifier.js +45 -0
- package/dist/context/summarize.d.ts +19 -0
- package/dist/context/summarize.js +53 -0
- package/dist/core/fallback/fallback.d.ts +5 -0
- package/dist/core/fallback/fallback.js +3 -1
- package/dist/core/router/router.d.ts +3 -0
- package/dist/core/router/router.js +1 -0
- package/dist/executions/agentTasks.d.ts +627 -0
- package/dist/executions/agentTasks.js +149 -0
- package/dist/executions/checkpoint.d.ts +5 -1
- package/dist/executions/checkpoint.js +13 -1
- package/dist/executions/execution.d.ts +36 -2
- package/dist/executions/store.d.ts +37 -0
- package/dist/executions/store.js +33 -0
- package/dist/generation/generateAdapter.d.ts +14 -0
- package/dist/generation/generateAdapter.js +38 -0
- package/dist/generation/generateSkill.d.ts +26 -0
- package/dist/generation/generateSkill.js +51 -0
- package/dist/index.d.ts +47 -4
- package/dist/index.js +33 -2
- package/dist/mcp/client.d.ts +70 -0
- package/dist/mcp/client.js +221 -0
- package/dist/mcp/manager.d.ts +151 -0
- package/dist/mcp/manager.js +493 -0
- package/dist/mcp/protocol.d.ts +216 -0
- package/dist/mcp/protocol.js +149 -0
- package/dist/mcp/toolAdapter.d.ts +44 -0
- package/dist/mcp/toolAdapter.js +94 -0
- package/dist/mcp/transport.d.ts +109 -0
- package/dist/mcp/transport.js +383 -0
- package/dist/memory/embedders/hash.d.ts +12 -0
- package/dist/memory/embedders/hash.js +31 -0
- package/dist/memory/embedders/http.d.ts +25 -0
- package/dist/memory/embedders/http.js +48 -0
- package/dist/memory/memory.d.ts +19 -2
- package/dist/memory/memory.js +75 -11
- package/dist/memory/semantic.d.ts +17 -0
- package/dist/memory/semantic.js +29 -0
- package/dist/orchestration/budget.d.ts +30 -0
- package/dist/orchestration/budget.js +40 -0
- package/dist/orchestration/executor.d.ts +64 -1
- package/dist/orchestration/executor.js +104 -7
- package/dist/orchestration/orchestrator.d.ts +35 -1
- package/dist/orchestration/orchestrator.js +106 -8
- package/dist/orchestration/plan.d.ts +15 -1
- package/dist/orchestration/plan.js +23 -4
- package/dist/orchestration/planner.d.ts +19 -1
- package/dist/orchestration/planner.js +25 -5
- package/dist/plugin/ai.d.ts +4 -0
- package/dist/plugin/ai.js +9 -0
- package/dist/runtime/config.js +50 -6
- package/dist/runtime/intent/aiClassifier.d.ts +19 -0
- package/dist/runtime/intent/aiClassifier.js +74 -0
- package/dist/runtime/models/modelProfile.d.ts +61 -0
- package/dist/runtime/models/modelProfile.js +139 -0
- package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
- package/dist/runtime/planning/deriveCapabilities.js +146 -0
- package/dist/runtime/policy.d.ts +10 -0
- package/dist/runtime/policy.js +9 -2
- package/dist/runtime/runtime.d.ts +233 -0
- package/dist/runtime/runtime.js +1042 -60
- package/dist/runtime/types.d.ts +88 -2
- package/dist/security/redact.js +22 -10
- package/dist/skills/manifest.d.ts +3 -0
- package/dist/skills/manifest.js +24 -0
- package/dist/skills/registry.d.ts +16 -1
- package/dist/skills/registry.js +21 -1
- package/dist/skills/skill.d.ts +6 -1
- package/dist/store/area.d.ts +15 -1
- package/dist/store/area.js +19 -8
- package/dist/store/crypto.d.ts +21 -0
- package/dist/store/crypto.js +49 -0
- package/dist/store/paths.d.ts +5 -1
- package/dist/store/paths.js +6 -0
- package/dist/store/store.d.ts +15 -3
- package/dist/store/store.js +28 -7
- package/dist/telemetry/sinks/otlp.d.ts +31 -0
- package/dist/telemetry/sinks/otlp.js +76 -0
- package/dist/tools/builtins/filesystem.js +1 -0
- package/dist/tools/builtins/git.js +1 -0
- package/dist/tools/builtins/shell.js +1 -0
- package/dist/tools/permissions.d.ts +28 -0
- package/dist/tools/permissions.js +72 -0
- package/dist/tools/registry.d.ts +18 -2
- package/dist/tools/registry.js +22 -2
- package/dist/tools/tool.d.ts +4 -0
- package/dist/types.d.ts +5 -1
- package/dist/util/flatten.d.ts +11 -0
- package/dist/util/flatten.js +18 -0
- package/dist/util/hash.d.ts +19 -0
- package/dist/util/hash.js +39 -0
- package/dist/util/semaphore.d.ts +19 -0
- package/dist/util/semaphore.js +60 -0
- package/package.json +24 -9
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-lane agent display (Phase 3.6) — concurrent agent steps as live rows in the REPL.
|
|
3
|
+
*
|
|
4
|
+
* PURE BY CONSTRUCTION. Nothing here reads `process.stdout`, a clock, or an environment variable: the
|
|
5
|
+
* terminal width, whether colour is on, and every value rendered are parameters. That is what makes a
|
|
6
|
+
* terminal feature testable offline and deterministically, and it is the rule to keep — the moment this
|
|
7
|
+
* module reads the real terminal, its tests start describing the machine they ran on.
|
|
8
|
+
*
|
|
9
|
+
* WRAPPING IS MADE IMPOSSIBLE RATHER THAN HANDLED. In-place rewriting works by moving the cursor up N
|
|
10
|
+
* rows, which is only correct while one logical line occupies exactly one physical row. So every line is
|
|
11
|
+
* clamped to `cols - 1`: no line ever reaches the final column, nothing wraps, and the arithmetic is
|
|
12
|
+
* exact. Lines are built as plain text, clamped, and styled last, so the clamp never has to count
|
|
13
|
+
* invisible escape bytes.
|
|
14
|
+
*/
|
|
15
|
+
import type { RuntimeEvent } from '../../runtime/events.js';
|
|
16
|
+
export interface AgentLane {
|
|
17
|
+
agentTaskId: string;
|
|
18
|
+
agentId: string;
|
|
19
|
+
stepId: string;
|
|
20
|
+
/** `running` covers started + progress; the terminal states come from the completion event. */
|
|
21
|
+
state: 'running' | 'completed' | 'failed' | 'cancelled' | 'waiting';
|
|
22
|
+
succeeded: number;
|
|
23
|
+
total: number;
|
|
24
|
+
callsUsed: number;
|
|
25
|
+
findings: number;
|
|
26
|
+
}
|
|
27
|
+
/** Display width in terminal columns, not code units. */
|
|
28
|
+
export declare function displayWidth(text: string): number;
|
|
29
|
+
/** Truncate to `cols` display columns, never splitting a surrogate pair or half a wide glyph. */
|
|
30
|
+
export declare function sliceColumns(text: string, cols: number): string;
|
|
31
|
+
/**
|
|
32
|
+
* Accumulates agent lanes from the event stream. Insertion-ordered, so lanes do not jump around between
|
|
33
|
+
* frames — a display that reorders itself is unreadable even when every value in it is right.
|
|
34
|
+
*/
|
|
35
|
+
export declare class LaneSet {
|
|
36
|
+
private readonly lanes;
|
|
37
|
+
/**
|
|
38
|
+
* Fold one event in. Returns the lane it touched, or `undefined` for an event that is not an agent
|
|
39
|
+
* event — a lane rather than a boolean, so the caller neither re-narrows the event nor looks the lane
|
|
40
|
+
* up a second time.
|
|
41
|
+
*/
|
|
42
|
+
observe(event: RuntimeEvent): AgentLane | undefined;
|
|
43
|
+
list(): AgentLane[];
|
|
44
|
+
/** True once at least one lane exists and none is still in flight. */
|
|
45
|
+
settled(): boolean;
|
|
46
|
+
clear(): void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Render the lanes as one line each. `tick` animates the running marker; it is a parameter rather than a
|
|
50
|
+
* timer so a test can render frame 3 without waiting for it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function laneLines(lanes: AgentLane[], opts: {
|
|
53
|
+
cols: number;
|
|
54
|
+
colors: boolean;
|
|
55
|
+
tick: number;
|
|
56
|
+
}): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Rewrite an owned region of the terminal in place, returning the bytes to write.
|
|
59
|
+
*
|
|
60
|
+
* `prevRows === 0` opens the region at the cursor's current line. Otherwise the cursor is assumed to be
|
|
61
|
+
* on the region's LAST row (which is where this function always leaves it) and walks up from there.
|
|
62
|
+
*
|
|
63
|
+
* The cursor never ends on a fresh line below the frame: a trailing newline on the terminal's last row
|
|
64
|
+
* scrolls the viewport, and the next frame's `cursorUp` would then land one row high and duplicate the
|
|
65
|
+
* region on every tick, forever.
|
|
66
|
+
*/
|
|
67
|
+
export declare function frameDiff(prevRows: number, lines: string[], enabled: boolean): string;
|
|
68
|
+
/** How many rows `frameDiff` will have left behind — the `prevRows` for the next call. */
|
|
69
|
+
export declare function frameRows(lines: string[]): number;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-lane agent display (Phase 3.6) — concurrent agent steps as live rows in the REPL.
|
|
3
|
+
*
|
|
4
|
+
* PURE BY CONSTRUCTION. Nothing here reads `process.stdout`, a clock, or an environment variable: the
|
|
5
|
+
* terminal width, whether colour is on, and every value rendered are parameters. That is what makes a
|
|
6
|
+
* terminal feature testable offline and deterministically, and it is the rule to keep — the moment this
|
|
7
|
+
* module reads the real terminal, its tests start describing the machine they ran on.
|
|
8
|
+
*
|
|
9
|
+
* WRAPPING IS MADE IMPOSSIBLE RATHER THAN HANDLED. In-place rewriting works by moving the cursor up N
|
|
10
|
+
* rows, which is only correct while one logical line occupies exactly one physical row. So every line is
|
|
11
|
+
* clamped to `cols - 1`: no line ever reaches the final column, nothing wraps, and the arithmetic is
|
|
12
|
+
* exact. Lines are built as plain text, clamped, and styled last, so the clamp never has to count
|
|
13
|
+
* invisible escape bytes.
|
|
14
|
+
*/
|
|
15
|
+
import { style, SPINNER_FRAMES } from './ansi.js';
|
|
16
|
+
import { flattenClamp } from '../../util/flatten.js';
|
|
17
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
18
|
+
/**
|
|
19
|
+
* East Asian Wide and Fullwidth code points, which a terminal draws in TWO columns. The no-wrap
|
|
20
|
+
* invariant is stated in COLUMNS, so counting UTF-16 code units would let a CJK step id — and step ids
|
|
21
|
+
* are model-authored — produce a line that measures 79 and draws 95, wrapping and breaking every
|
|
22
|
+
* cursor-up that follows it.
|
|
23
|
+
*/
|
|
24
|
+
function isWide(cp) {
|
|
25
|
+
return ((cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
26
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals, Kangxi, punctuation
|
|
27
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana .. CJK compatibility
|
|
28
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
29
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK unified ideographs
|
|
30
|
+
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
|
31
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
|
|
32
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
33
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
34
|
+
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth forms
|
|
35
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
36
|
+
(cp >= 0x1f300 && cp <= 0x1f64f) || // emoji
|
|
37
|
+
(cp >= 0x1f900 && cp <= 0x1f9ff) ||
|
|
38
|
+
(cp >= 0x20000 && cp <= 0x3fffd));
|
|
39
|
+
}
|
|
40
|
+
/** Display width in terminal columns, not code units. */
|
|
41
|
+
export function displayWidth(text) {
|
|
42
|
+
let n = 0;
|
|
43
|
+
for (const ch of text)
|
|
44
|
+
n += isWide(ch.codePointAt(0)) ? 2 : 1;
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
47
|
+
/** Truncate to `cols` display columns, never splitting a surrogate pair or half a wide glyph. */
|
|
48
|
+
export function sliceColumns(text, cols) {
|
|
49
|
+
let out = '';
|
|
50
|
+
let used = 0;
|
|
51
|
+
for (const ch of text) {
|
|
52
|
+
const w = isWide(ch.codePointAt(0)) ? 2 : 1;
|
|
53
|
+
if (used + w > cols)
|
|
54
|
+
break;
|
|
55
|
+
out += ch;
|
|
56
|
+
used += w;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
const cursorUp = (n) => (n > 0 ? `\x1b[${n}A` : '');
|
|
61
|
+
/** Which lane state an agent-task state projects onto. Anything unrecognised reads as still running. */
|
|
62
|
+
function laneState(state) {
|
|
63
|
+
if (state === 'completed' || state === 'failed' || state === 'cancelled')
|
|
64
|
+
return state;
|
|
65
|
+
if (state.startsWith('waiting'))
|
|
66
|
+
return 'waiting';
|
|
67
|
+
return 'running';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Accumulates agent lanes from the event stream. Insertion-ordered, so lanes do not jump around between
|
|
71
|
+
* frames — a display that reorders itself is unreadable even when every value in it is right.
|
|
72
|
+
*/
|
|
73
|
+
export class LaneSet {
|
|
74
|
+
lanes = new Map();
|
|
75
|
+
/**
|
|
76
|
+
* Fold one event in. Returns the lane it touched, or `undefined` for an event that is not an agent
|
|
77
|
+
* event — a lane rather than a boolean, so the caller neither re-narrows the event nor looks the lane
|
|
78
|
+
* up a second time.
|
|
79
|
+
*/
|
|
80
|
+
observe(event) {
|
|
81
|
+
if (!event.type.startsWith('agent.task.'))
|
|
82
|
+
return undefined;
|
|
83
|
+
const e = event;
|
|
84
|
+
const existing = this.lanes.get(e.agentTaskId);
|
|
85
|
+
const lane = existing ?? {
|
|
86
|
+
agentTaskId: e.agentTaskId,
|
|
87
|
+
agentId: e.agentId,
|
|
88
|
+
stepId: e.stepId,
|
|
89
|
+
state: 'running',
|
|
90
|
+
succeeded: 0,
|
|
91
|
+
total: 0,
|
|
92
|
+
callsUsed: 0,
|
|
93
|
+
findings: 0,
|
|
94
|
+
};
|
|
95
|
+
if ('innerSteps' in e) {
|
|
96
|
+
lane.total = e.innerSteps.total;
|
|
97
|
+
lane.succeeded = e.innerSteps.succeeded;
|
|
98
|
+
}
|
|
99
|
+
if ('callsUsed' in e)
|
|
100
|
+
lane.callsUsed = e.callsUsed;
|
|
101
|
+
if ('findings' in e)
|
|
102
|
+
lane.findings = e.findings;
|
|
103
|
+
if ('state' in e)
|
|
104
|
+
lane.state = laneState(e.state);
|
|
105
|
+
this.lanes.set(e.agentTaskId, lane);
|
|
106
|
+
return lane;
|
|
107
|
+
}
|
|
108
|
+
list() {
|
|
109
|
+
return [...this.lanes.values()];
|
|
110
|
+
}
|
|
111
|
+
/** True once at least one lane exists and none is still in flight. */
|
|
112
|
+
settled() {
|
|
113
|
+
const all = this.list();
|
|
114
|
+
return all.length > 0 && all.every((l) => l.state !== 'running');
|
|
115
|
+
}
|
|
116
|
+
clear() {
|
|
117
|
+
this.lanes.clear();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const MARK = { running: '', completed: '✓', failed: '✗', cancelled: '−', waiting: '?' };
|
|
121
|
+
const TINT = {
|
|
122
|
+
running: 'cyan',
|
|
123
|
+
completed: 'green',
|
|
124
|
+
failed: 'red',
|
|
125
|
+
cancelled: 'gray',
|
|
126
|
+
waiting: 'yellow',
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Render the lanes as one line each. `tick` animates the running marker; it is a parameter rather than a
|
|
130
|
+
* timer so a test can render frame 3 without waiting for it.
|
|
131
|
+
*/
|
|
132
|
+
export function laneLines(lanes, opts) {
|
|
133
|
+
// `|| 80` not `?? 80`: a pty can report ZERO columns, and zero is not nullish. And the floor is 1,
|
|
134
|
+
// not 20 — raising the width for a narrow terminal MAKES it wrap, which is the failure this clamp
|
|
135
|
+
// exists to prevent.
|
|
136
|
+
const width = Math.max(1, (opts.cols || 80) - 1);
|
|
137
|
+
return lanes.map((l) => {
|
|
138
|
+
const mark = l.state === 'running' ? SPINNER_FRAMES[opts.tick % SPINNER_FRAMES.length] : MARK[l.state];
|
|
139
|
+
const progress = l.total > 0 ? `${l.succeeded}/${l.total}` : '…';
|
|
140
|
+
const found = l.findings > 0 ? `, ${l.findings} finding(s)` : '';
|
|
141
|
+
// Built plain, clamped, THEN styled — so the clamp never counts escape bytes.
|
|
142
|
+
const plain = ` ${mark} ${flattenClamp(l.agentId, 24)} @ ${flattenClamp(l.stepId, 16)} ${progress} step(s), ${l.callsUsed} call(s)${found}`;
|
|
143
|
+
return style(sliceColumns(plain, width), TINT[l.state], opts.colors);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Rewrite an owned region of the terminal in place, returning the bytes to write.
|
|
148
|
+
*
|
|
149
|
+
* `prevRows === 0` opens the region at the cursor's current line. Otherwise the cursor is assumed to be
|
|
150
|
+
* on the region's LAST row (which is where this function always leaves it) and walks up from there.
|
|
151
|
+
*
|
|
152
|
+
* The cursor never ends on a fresh line below the frame: a trailing newline on the terminal's last row
|
|
153
|
+
* scrolls the viewport, and the next frame's `cursorUp` would then land one row high and duplicate the
|
|
154
|
+
* region on every tick, forever.
|
|
155
|
+
*/
|
|
156
|
+
export function frameDiff(prevRows, lines, enabled) {
|
|
157
|
+
if (!enabled)
|
|
158
|
+
return '';
|
|
159
|
+
const rows = Math.max(prevRows, lines.length);
|
|
160
|
+
if (rows === 0)
|
|
161
|
+
return '';
|
|
162
|
+
let out = '\r';
|
|
163
|
+
if (prevRows > 0)
|
|
164
|
+
out += cursorUp(prevRows - 1);
|
|
165
|
+
for (let i = 0; i < rows; i += 1) {
|
|
166
|
+
out += CLEAR_LINE + (lines[i] ?? '');
|
|
167
|
+
if (i < rows - 1)
|
|
168
|
+
out += '\n';
|
|
169
|
+
}
|
|
170
|
+
// Drew fewer rows than last time: the cursor sits on a cleared row below the frame, so walk back up
|
|
171
|
+
// to the real last line, keeping the "cursor is on the last row" invariant true for the next call.
|
|
172
|
+
// CLOSING the region (`lines` empty) has no last line, so the target is the region's TOP row — which
|
|
173
|
+
// is one higher than the shrink case, and the reason this is `max(1, …)` rather than `lines.length`.
|
|
174
|
+
if (rows > lines.length)
|
|
175
|
+
out += `\r${cursorUp(rows - Math.max(1, lines.length))}`;
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/** How many rows `frameDiff` will have left behind — the `prevRows` for the next call. */
|
|
179
|
+
export function frameRows(lines) {
|
|
180
|
+
return lines.length;
|
|
181
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The interactive REPL — a thin node:readline wrapper around ReplSession. All output goes through the
|
|
3
3
|
* redacted `print`. Progress is shown by subscribing to the runtime's lifecycle events. Zero new deps.
|
|
4
|
+
* Phase 21b adds (gated on an interactive TTY): a colored banner/prompt, slash-command tab-completion, a
|
|
5
|
+
* best-effort persistent history file, and an in-place spinner while a run is in flight. Non-TTY/CI output
|
|
6
|
+
* stays plain, and `ReplSession.handle` (the tested surface) is untouched.
|
|
4
7
|
*/
|
|
5
8
|
/** Start the interactive session. Resolves when the user exits (or stdin closes). */
|
|
6
9
|
export declare function startRepl(configPath?: string): Promise<void>;
|
|
@@ -1,66 +1,142 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The interactive REPL — a thin node:readline wrapper around ReplSession. All output goes through the
|
|
3
3
|
* redacted `print`. Progress is shown by subscribing to the runtime's lifecycle events. Zero new deps.
|
|
4
|
+
* Phase 21b adds (gated on an interactive TTY): a colored banner/prompt, slash-command tab-completion, a
|
|
5
|
+
* best-effort persistent history file, and an in-place spinner while a run is in flight. Non-TTY/CI output
|
|
6
|
+
* stays plain, and `ReplSession.handle` (the tested surface) is untouched.
|
|
4
7
|
*/
|
|
5
8
|
import { createInterface } from 'node:readline';
|
|
9
|
+
import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { join, dirname } from 'node:path';
|
|
6
11
|
import { Runtime } from '../../runtime/runtime.js';
|
|
7
12
|
import { print, printChunk, printError } from '../render.js';
|
|
8
13
|
import { summarizeWorkspace } from '../../runtime/workspace/workspace.js';
|
|
9
|
-
import { ReplSession } from './session.js';
|
|
10
|
-
|
|
14
|
+
import { ReplSession, SLASH_COMMANDS } from './session.js';
|
|
15
|
+
import { makeCompleter } from './complete.js';
|
|
16
|
+
import { colorEnabled, bold, cyan, dim, gray, SPINNER_FRAMES, statusLine, clearLine } from './ansi.js';
|
|
17
|
+
function banner(rt, colors) {
|
|
11
18
|
const ws = rt.workspaceInfo();
|
|
12
|
-
print('ai-runtime');
|
|
19
|
+
print(bold('ai-runtime', colors));
|
|
13
20
|
if (ws)
|
|
14
|
-
print(summarizeWorkspace(ws));
|
|
15
|
-
print(`mode: auto · ${rt.ai.providers().length} providers · type /help or a request, /exit to quit
|
|
21
|
+
print(dim(summarizeWorkspace(ws), colors));
|
|
22
|
+
print(gray(`mode: auto · ${rt.ai.providers().length} providers · type /help or a request, /exit to quit`, colors));
|
|
16
23
|
print('');
|
|
17
24
|
}
|
|
18
25
|
/** A concise, redacted progress line for a lifecycle event — the REPL's live feedback during a run. */
|
|
19
|
-
function progressLine(e) {
|
|
26
|
+
function progressLine(e, colors) {
|
|
20
27
|
switch (e.type) {
|
|
21
28
|
case 'mode.selected':
|
|
22
|
-
return e.source === 'auto' ? ` · mode: ${e.selected} (auto, ${Math.round(e.confidence * 100)}%)` : ` · mode: ${e.selected}
|
|
29
|
+
return dim(e.source === 'auto' ? ` · mode: ${e.selected} (auto, ${Math.round(e.confidence * 100)}%)` : ` · mode: ${e.selected}`, colors);
|
|
23
30
|
case 'provider.selected':
|
|
24
|
-
return ` · routed → ${e.providerId}/${e.model}
|
|
31
|
+
return dim(` · routed → ${e.providerId}/${e.model}`, colors);
|
|
25
32
|
case 'provider.failed':
|
|
26
|
-
return ` · ${e.providerId}/${e.model} failed${e.category ? ` (${e.category})` : ''} — falling back
|
|
33
|
+
return dim(` · ${e.providerId}/${e.model} failed${e.category ? ` (${e.category})` : ''} — falling back`, colors);
|
|
27
34
|
default:
|
|
28
35
|
return undefined; // runtime.started / clarification.requested / run.completed are covered by the result block
|
|
29
36
|
}
|
|
30
37
|
}
|
|
38
|
+
/** An in-place spinner (TTY only). `stop()` clears the line and is idempotent. */
|
|
39
|
+
function startSpinner(colors) {
|
|
40
|
+
if (!colors)
|
|
41
|
+
return { stop: () => { } };
|
|
42
|
+
let i = 0;
|
|
43
|
+
const tick = () => {
|
|
44
|
+
try {
|
|
45
|
+
process.stdout.write(statusLine(`${cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length], colors)} working…`, colors));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* never let a draw break the REPL */
|
|
49
|
+
}
|
|
50
|
+
i += 1;
|
|
51
|
+
};
|
|
52
|
+
tick();
|
|
53
|
+
const handle = setInterval(tick, 90);
|
|
54
|
+
if (typeof handle.unref === 'function')
|
|
55
|
+
handle.unref();
|
|
56
|
+
let stopped = false;
|
|
57
|
+
return {
|
|
58
|
+
stop: () => {
|
|
59
|
+
if (stopped)
|
|
60
|
+
return;
|
|
61
|
+
stopped = true;
|
|
62
|
+
clearInterval(handle);
|
|
63
|
+
try {
|
|
64
|
+
process.stdout.write(clearLine(colors));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* ignore */
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
31
72
|
/** Start the interactive session. Resolves when the user exits (or stdin closes). */
|
|
32
73
|
export async function startRepl(configPath) {
|
|
33
74
|
const rt = await Runtime.load({ ...(configPath ? { config: configPath } : {}) });
|
|
75
|
+
const colors = colorEnabled({ isTTY: !!process.stdout.isTTY, env: process.env });
|
|
34
76
|
const session = new ReplSession(rt, { streaming: true });
|
|
35
|
-
//
|
|
77
|
+
// Best-effort persistent history under the project store's cache dir.
|
|
78
|
+
const historyPath = join(rt.store.paths().project, 'cache', 'repl_history');
|
|
79
|
+
let history = [];
|
|
80
|
+
try {
|
|
81
|
+
history = readFileSync(historyPath, 'utf8').split('\n').filter(Boolean).slice(-500).reverse(); // readline wants newest-first
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* no history yet */
|
|
85
|
+
}
|
|
86
|
+
const remember = (line) => {
|
|
87
|
+
if (!line.trim())
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(dirname(historyPath), { recursive: true });
|
|
91
|
+
appendFileSync(historyPath, `${line}\n`);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* history is a convenience; a write failure must never break the REPL */
|
|
95
|
+
}
|
|
96
|
+
};
|
|
36
97
|
let streamedThisRun = false;
|
|
98
|
+
let spinner;
|
|
37
99
|
// Live progress + token streaming. Events are already redacted; a throwing observer can't break a run.
|
|
38
100
|
rt.on((e) => {
|
|
101
|
+
spinner?.stop(); // any event means work has started producing output — drop the spinner first
|
|
39
102
|
if (e.type === 'response.delta') {
|
|
40
103
|
printChunk(e.text);
|
|
41
104
|
streamedThisRun = true;
|
|
42
105
|
return;
|
|
43
106
|
}
|
|
44
|
-
const line = progressLine(e);
|
|
107
|
+
const line = progressLine(e, colors);
|
|
45
108
|
if (line)
|
|
46
109
|
print(line);
|
|
47
110
|
});
|
|
48
|
-
banner(rt);
|
|
49
|
-
|
|
111
|
+
banner(rt, colors);
|
|
112
|
+
// The REPL is long-lived and interactive, so MCP connects EAGERLY here (unlike one-shot commands):
|
|
113
|
+
// the user should see their servers' tools in the very first request, and pay the cost once.
|
|
114
|
+
if (rt.hasMcpServers()) {
|
|
115
|
+
const servers = await rt.connectMcp();
|
|
116
|
+
const up = servers.filter((sv) => sv.state === 'connected' || sv.state === 'degraded').length;
|
|
117
|
+
print(gray(`mcp: ${up}/${servers.length} server(s) connected`, colors));
|
|
118
|
+
for (const w of rt.mcpWarningsList())
|
|
119
|
+
printError(`! ${w}`);
|
|
120
|
+
}
|
|
121
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: cyan('> ', colors), completer: makeCompleter(SLASH_COMMANDS), history, historySize: 500 });
|
|
50
122
|
rl.prompt();
|
|
51
123
|
for await (const line of rl) {
|
|
124
|
+
remember(line);
|
|
52
125
|
let result;
|
|
53
126
|
streamedThisRun = false;
|
|
127
|
+
spinner = startSpinner(colors);
|
|
54
128
|
try {
|
|
55
129
|
result = await session.handle(line);
|
|
56
130
|
}
|
|
57
131
|
catch (err) {
|
|
132
|
+
spinner.stop();
|
|
58
133
|
if (streamedThisRun)
|
|
59
134
|
process.stdout.write('\n');
|
|
60
135
|
printError(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
61
136
|
rl.prompt();
|
|
62
137
|
continue;
|
|
63
138
|
}
|
|
139
|
+
spinner.stop();
|
|
64
140
|
if (streamedThisRun)
|
|
65
141
|
process.stdout.write('\n'); // close the streamed line before printing result lines
|
|
66
142
|
if (result.clear)
|
|
@@ -69,8 +145,10 @@ export async function startRepl(configPath) {
|
|
|
69
145
|
print(l);
|
|
70
146
|
if (result.exit) {
|
|
71
147
|
rl.close();
|
|
148
|
+
await rt.close(); // release MCP stdio children on a clean exit
|
|
72
149
|
return;
|
|
73
150
|
}
|
|
74
151
|
rl.prompt();
|
|
75
152
|
}
|
|
153
|
+
await rt.close(); // stdin closed (piped input) — same release path
|
|
76
154
|
}
|
|
@@ -10,6 +10,8 @@ export interface HandleResult {
|
|
|
10
10
|
exit?: boolean;
|
|
11
11
|
clear?: boolean;
|
|
12
12
|
}
|
|
13
|
+
/** Top-level slash commands, for REPL tab-completion (Phase 21b). Kept in sync with the `handle` dispatch. */
|
|
14
|
+
export declare const SLASH_COMMANDS: readonly ["help", "status", "info", "doctor", "cleanup", "mode", "compare", "models", "config", "providers", "tools", "capabilities", "mcp", "skills", "memory", "conversations", "executions", "resume", "resume-execution", "pause", "cancel", "approve", "deny", "learning", "feedback", "permissions", "budget", "stream", "dry-run", "clear", "exit", "quit"];
|
|
13
15
|
export declare class ReplSession {
|
|
14
16
|
private readonly runtime;
|
|
15
17
|
private mode;
|
|
@@ -22,6 +24,12 @@ export declare class ReplSession {
|
|
|
22
24
|
});
|
|
23
25
|
currentMode(): RuntimeMode;
|
|
24
26
|
private views;
|
|
27
|
+
/**
|
|
28
|
+
* MCP servers in the REPL. The session's Runtime is already connected (the REPL connects eagerly at
|
|
29
|
+
* start), so this NEVER opens a second Runtime — that would spawn duplicate stdio children and give
|
|
30
|
+
* `close()` the wrong one to tear down. It reuses the exact formatters the `mcp` command prints.
|
|
31
|
+
*/
|
|
32
|
+
private mcpView;
|
|
25
33
|
handle(raw: string): Promise<HandleResult>;
|
|
26
34
|
private runInput;
|
|
27
35
|
private memory;
|
|
@@ -9,6 +9,16 @@ import { SETUP_CANDIDATES } from '../commands/setup.js';
|
|
|
9
9
|
import { gatherInfo, renderInfo } from '../commands/info.js';
|
|
10
10
|
import { gatherDoctor, renderDoctor } from '../commands/doctor.js';
|
|
11
11
|
import { planCleanup } from '../commands/cleanup.js';
|
|
12
|
+
import { actionCapabilityRows, renderActionCapabilities, renderCapabilityGaps } from '../commands/inspect.js';
|
|
13
|
+
import { renderMcpList, renderMcpDetail } from '../commands/mcp.js';
|
|
14
|
+
import { candidatesFrom, deriveCapabilitiesOffline } from '../../runtime/planning/deriveCapabilities.js';
|
|
15
|
+
import { displaySafe } from '../render.js';
|
|
16
|
+
/** Top-level slash commands, for REPL tab-completion (Phase 21b). Kept in sync with the `handle` dispatch. */
|
|
17
|
+
export const SLASH_COMMANDS = [
|
|
18
|
+
'help', 'status', 'info', 'doctor', 'cleanup', 'mode', 'compare', 'models', 'config', 'providers', 'tools', 'capabilities', 'mcp',
|
|
19
|
+
'skills', 'memory', 'conversations', 'executions', 'resume', 'resume-execution', 'pause', 'cancel', 'approve',
|
|
20
|
+
'deny', 'learning', 'feedback', 'permissions', 'budget', 'stream', 'dry-run', 'clear', 'exit', 'quit',
|
|
21
|
+
];
|
|
12
22
|
const HELP = [
|
|
13
23
|
'Commands:',
|
|
14
24
|
' /help show this help',
|
|
@@ -24,6 +34,10 @@ const HELP = [
|
|
|
24
34
|
' /providers/<id> [models|capabilities|health] inspect one provider',
|
|
25
35
|
' /models list all known models',
|
|
26
36
|
' /tools list registered tools',
|
|
37
|
+
' /capabilities list action capabilities (what the runtime can DO) + providers',
|
|
38
|
+
' /capabilities <goal> which capabilities a goal needs, and what is missing (offline)',
|
|
39
|
+
' /mcp list MCP servers (state, tools, grant)',
|
|
40
|
+
' /mcp/<id> inspect one MCP server (protocol, tools, diagnostics)',
|
|
27
41
|
' /skills [discover] list available skills; discover scans the workspace',
|
|
28
42
|
' /permissions show the current tool permissions',
|
|
29
43
|
' /feedback <text> record feedback on the last result (e.g. "that worked")',
|
|
@@ -43,6 +57,7 @@ const HELP = [
|
|
|
43
57
|
' /config show the resolved configuration',
|
|
44
58
|
' /dry-run toggle dry-run (plan only, no changes)',
|
|
45
59
|
' /stream toggle token-by-token streaming of answers',
|
|
60
|
+
' /budget show the call/cost budget (AI_MAX_CALLS / AI_MAX_COST_USD)',
|
|
46
61
|
' /clear clear the screen',
|
|
47
62
|
' /exit leave the session',
|
|
48
63
|
'',
|
|
@@ -67,6 +82,27 @@ export class ReplSession {
|
|
|
67
82
|
this.viewCache = new ProviderViewCache(this.runtime.ai);
|
|
68
83
|
return this.viewCache;
|
|
69
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* MCP servers in the REPL. The session's Runtime is already connected (the REPL connects eagerly at
|
|
87
|
+
* start), so this NEVER opens a second Runtime — that would spawn duplicate stdio children and give
|
|
88
|
+
* `close()` the wrong one to tear down. It reuses the exact formatters the `mcp` command prints.
|
|
89
|
+
*/
|
|
90
|
+
mcpView(target) {
|
|
91
|
+
if (!this.runtime.hasMcpServers())
|
|
92
|
+
return { lines: renderMcpList({}, []) };
|
|
93
|
+
const grants = this.runtime.permissions().mcp.servers;
|
|
94
|
+
if (!target) {
|
|
95
|
+
const lines = renderMcpList(grants, this.runtime.mcp().list());
|
|
96
|
+
const warnings = this.runtime.mcpWarningsList();
|
|
97
|
+
if (warnings.length)
|
|
98
|
+
lines.push('', 'Warnings:', ...warnings.map((w) => ` ! ${w}`));
|
|
99
|
+
return { lines };
|
|
100
|
+
}
|
|
101
|
+
const status = this.runtime.mcp().status(target);
|
|
102
|
+
if (!status)
|
|
103
|
+
return { lines: [`no MCP server '${displaySafe(target, 40)}' — /mcp lists them`] };
|
|
104
|
+
return { lines: renderMcpDetail(grants, status, this.runtime.mcp().diagnostics(target), this.runtime.mcp().tools(target)) };
|
|
105
|
+
}
|
|
70
106
|
async handle(raw) {
|
|
71
107
|
const line = raw.trim();
|
|
72
108
|
if (!line)
|
|
@@ -109,6 +145,24 @@ export class ReplSession {
|
|
|
109
145
|
return this.config();
|
|
110
146
|
case 'providers':
|
|
111
147
|
return this.providers(parts[1], args[0]);
|
|
148
|
+
case 'capabilities': {
|
|
149
|
+
// `capabilities --actions` connects (inspect.ts); without this the same question reported
|
|
150
|
+
// different MCP availability on the two surfaces. connectMcp is idempotent.
|
|
151
|
+
if (this.runtime.hasMcpServers())
|
|
152
|
+
await this.runtime.connectMcp();
|
|
153
|
+
// Both forms, and never a truncated goal: `/capabilities run the tests` and
|
|
154
|
+
// `/capabilities/run the tests` must mean the same thing.
|
|
155
|
+
const goal = [...parts.slice(1), ...args].join(' ').trim();
|
|
156
|
+
if (!goal)
|
|
157
|
+
return { lines: renderActionCapabilities(actionCapabilityRows(this.runtime)) };
|
|
158
|
+
const required = deriveCapabilitiesOffline(goal, candidatesFrom(this.runtime.capabilities().list()));
|
|
159
|
+
if (!required.length)
|
|
160
|
+
return { lines: [`no action capability matched "${displaySafe(goal, 80)}" — /capabilities lists everything this runtime can do.`] };
|
|
161
|
+
return { lines: renderCapabilityGaps(this.runtime.capabilityReport(required)) };
|
|
162
|
+
}
|
|
163
|
+
case 'mcp':
|
|
164
|
+
// `/mcp/<id>` and `/mcp <id>` both address one server.
|
|
165
|
+
return this.mcpView(parts[1] ?? args[0]);
|
|
112
166
|
case 'tools':
|
|
113
167
|
return { lines: ['Tools:', ...this.runtime.tools().map((t) => ` ${t.id.padEnd(14)} ${t.description}`)] };
|
|
114
168
|
case 'skills': {
|
|
@@ -139,6 +193,14 @@ export class ReplSession {
|
|
|
139
193
|
case 'stream':
|
|
140
194
|
this.streaming = !this.streaming;
|
|
141
195
|
return { lines: [`streaming ${this.streaming ? 'ON — answers render token-by-token' : 'OFF'}`] };
|
|
196
|
+
case 'budget':
|
|
197
|
+
return {
|
|
198
|
+
lines: [
|
|
199
|
+
`call budget (AI_MAX_CALLS): ${process.env.AI_MAX_CALLS ?? '(unset — no limit)'}`,
|
|
200
|
+
`cost budget (AI_MAX_COST_USD): ${process.env.AI_MAX_COST_USD ?? '(unset — no limit)'}`,
|
|
201
|
+
'over budget: notify-and-wait by default; add --partial (one-shot) to run the phases that fit and pause.',
|
|
202
|
+
],
|
|
203
|
+
};
|
|
142
204
|
case 'memory':
|
|
143
205
|
return this.memory(args);
|
|
144
206
|
case 'conversations':
|
|
@@ -182,17 +244,25 @@ export class ReplSession {
|
|
|
182
244
|
const execId = result.execution?.id;
|
|
183
245
|
lines.push(execId ? `(approval required — /approve ${execId} to proceed, or /deny ${execId})` : '(approval required — approve the execution via /executions then /approve <id>)');
|
|
184
246
|
}
|
|
247
|
+
if (result.status === 'waiting_for_budget') {
|
|
248
|
+
const execId = result.execution?.id;
|
|
249
|
+
lines.push(execId ? `(over budget — raise AI_MAX_CALLS and /resume-execution ${execId}, or re-run with more budget)` : '(over budget — raise the call budget and re-run, or add --partial)');
|
|
250
|
+
}
|
|
185
251
|
if (result.status === 'failed')
|
|
186
252
|
lines.push('(did not complete)');
|
|
187
253
|
if (result.clarification)
|
|
188
254
|
lines.push(`? ${result.clarification.question}`);
|
|
189
255
|
if (result.memory?.captured)
|
|
190
256
|
lines.push('(remembered)');
|
|
257
|
+
// Phase 3.3: the ONE gap renderer, shared with `ai-runtime run` and both capabilities surfaces.
|
|
258
|
+
// Appended AFTER the existing advisory tail so every positional assertion above is untouched.
|
|
259
|
+
if (result.capabilityGaps)
|
|
260
|
+
lines.push(...renderCapabilityGaps(result.capabilityGaps));
|
|
191
261
|
if (this.conversationId && result.response?.text)
|
|
192
262
|
this.runtime.conversations.append(this.conversationId, 'assistant', result.response.text, result.runId);
|
|
193
263
|
return { lines };
|
|
194
264
|
}
|
|
195
|
-
memory(args) {
|
|
265
|
+
async memory(args) {
|
|
196
266
|
if (!this.runtime.memory.enabled)
|
|
197
267
|
return { lines: ['memory is disabled (stateless mode).'] };
|
|
198
268
|
const sub = args[0];
|
|
@@ -200,7 +270,8 @@ export class ReplSession {
|
|
|
200
270
|
const q = args.slice(1).join(' ');
|
|
201
271
|
if (!q)
|
|
202
272
|
return { lines: ['usage: /memory search <query>'] };
|
|
203
|
-
|
|
273
|
+
// Semantic ranking when an embedder is configured; degrades to BM25 internally.
|
|
274
|
+
const hits = await this.runtime.memory.searchSemantic(q, { limit: 10 });
|
|
204
275
|
return { lines: hits.length ? hits.map((r) => ` ${r.id} [${r.scope}] ${r.text}`) : ['no matches.'] };
|
|
205
276
|
}
|
|
206
277
|
if (sub === 'delete') {
|
package/dist/cli/render.d.ts
CHANGED
|
@@ -11,3 +11,10 @@ export declare function print(line: string): void;
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function printChunk(chunk: string): void;
|
|
13
13
|
export declare function printError(line: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Clamp a source- or MODEL-controlled string before it reaches the terminal — the terminal twin of
|
|
16
|
+
* `promptSafe`. LOAD-BEARING: `print` redacts secrets but strips no control characters and bounds no
|
|
17
|
+
* length, and a gap's `capabilityId` originates in the model's plan JSON — `sanitizeSegment` restricts
|
|
18
|
+
* its charset but sets no maximum, and the `register` gap hint interpolates it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function displaySafe(raw: string, max?: number): string;
|
package/dist/cli/render.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* directly from a command.
|
|
5
5
|
*/
|
|
6
6
|
import { redactString } from '../security/redact.js';
|
|
7
|
+
import { flattenClamp } from '../util/flatten.js';
|
|
7
8
|
export function print(line) {
|
|
8
9
|
// eslint-disable-next-line no-console
|
|
9
10
|
console.log(redactString(line));
|
|
@@ -20,3 +21,12 @@ export function printError(line) {
|
|
|
20
21
|
// eslint-disable-next-line no-console
|
|
21
22
|
console.error(redactString(line));
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Clamp a source- or MODEL-controlled string before it reaches the terminal — the terminal twin of
|
|
26
|
+
* `promptSafe`. LOAD-BEARING: `print` redacts secrets but strips no control characters and bounds no
|
|
27
|
+
* length, and a gap's `capabilityId` originates in the model's plan JSON — `sanitizeSegment` restricts
|
|
28
|
+
* its charset but sets no maximum, and the `register` gap hint interpolates it.
|
|
29
|
+
*/
|
|
30
|
+
export function displaySafe(raw, max = 80) {
|
|
31
|
+
return flattenClamp(raw, max);
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place where a CLI command borrows a Runtime and gives it back.
|
|
3
|
+
*
|
|
4
|
+
* A `Runtime` can own OS resources — today MCP stdio child processes. A one-shot command that returns
|
|
5
|
+
* without releasing them does not merely leak: a live child keeps the event loop alive, so the CLI never
|
|
6
|
+
* exits. `withRuntime` guarantees the release on every path, including a thrown error.
|
|
7
|
+
*/
|
|
8
|
+
import { Runtime } from '../runtime/runtime.js';
|
|
9
|
+
export declare function withRuntime<T>(opts: {
|
|
10
|
+
config?: string;
|
|
11
|
+
}, fn: (rt: Runtime) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place where a CLI command borrows a Runtime and gives it back.
|
|
3
|
+
*
|
|
4
|
+
* A `Runtime` can own OS resources — today MCP stdio child processes. A one-shot command that returns
|
|
5
|
+
* without releasing them does not merely leak: a live child keeps the event loop alive, so the CLI never
|
|
6
|
+
* exits. `withRuntime` guarantees the release on every path, including a thrown error.
|
|
7
|
+
*/
|
|
8
|
+
import { Runtime } from '../runtime/runtime.js';
|
|
9
|
+
export async function withRuntime(opts, fn) {
|
|
10
|
+
const rt = await Runtime.load({ ...(opts.config ? { config: opts.config } : {}) });
|
|
11
|
+
try {
|
|
12
|
+
return await fn(rt);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await rt.close();
|
|
16
|
+
}
|
|
17
|
+
}
|