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
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
* All go through the key-free facade views and redacted output.
|
|
4
4
|
*/
|
|
5
5
|
import { loadAI } from '../context.js';
|
|
6
|
-
import {
|
|
6
|
+
import { withRuntime } from '../runtimeSession.js';
|
|
7
|
+
import { print, displaySafe } from '../render.js';
|
|
8
|
+
import { loadModelProfile } from '../../runtime/models/modelProfile.js';
|
|
9
|
+
import { candidatesFrom, deriveCapabilitiesOffline } from '../../runtime/planning/deriveCapabilities.js';
|
|
10
|
+
/** Render one models.md directive compactly. */
|
|
11
|
+
function describeDirective(d) {
|
|
12
|
+
if (d.kind === 'strategy')
|
|
13
|
+
return `strategy: ${d.strategy}`;
|
|
14
|
+
if (d.kind === 'pin')
|
|
15
|
+
return `pin: ${d.provider ?? ''}${d.model ? ':' + d.model : ''}`;
|
|
16
|
+
return `prefer: ${[...new Set([...d.models, ...d.providers])].join(', ')}`;
|
|
17
|
+
}
|
|
7
18
|
export async function providersCommand(opts) {
|
|
8
19
|
const { ai, hasProviders } = loadAI(opts.config);
|
|
9
20
|
if (!hasProviders)
|
|
@@ -20,6 +31,30 @@ export async function providersCommand(opts) {
|
|
|
20
31
|
}
|
|
21
32
|
}
|
|
22
33
|
export async function modelsCommand(opts) {
|
|
34
|
+
// `--profile` prints the loaded models.md routing profile (per-mode / per-task directives) instead.
|
|
35
|
+
if (opts.profile) {
|
|
36
|
+
const profile = loadModelProfile(process.cwd());
|
|
37
|
+
if (!profile)
|
|
38
|
+
return print('No models.md found (workspace root or .ai-runtime/models.md).');
|
|
39
|
+
if (opts.json)
|
|
40
|
+
return print(JSON.stringify(profile, null, 2));
|
|
41
|
+
print('Model routing (models.md):');
|
|
42
|
+
const byMode = Object.entries(profile.byMode);
|
|
43
|
+
const byTask = Object.entries(profile.byTask);
|
|
44
|
+
if (byMode.length) {
|
|
45
|
+
print(' By mode:');
|
|
46
|
+
for (const [k, d] of byMode)
|
|
47
|
+
print(` ${k.padEnd(12)} ${describeDirective(d)}`);
|
|
48
|
+
}
|
|
49
|
+
if (byTask.length) {
|
|
50
|
+
print(' By task:');
|
|
51
|
+
for (const [k, d] of byTask)
|
|
52
|
+
print(` ${k.padEnd(28)} ${describeDirective(d)}`);
|
|
53
|
+
}
|
|
54
|
+
if (!byMode.length && !byTask.length)
|
|
55
|
+
print(' (empty)');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
23
58
|
const { ai, hasProviders } = loadAI(opts.config);
|
|
24
59
|
if (!hasProviders)
|
|
25
60
|
return print('No providers configured.');
|
|
@@ -36,7 +71,126 @@ export async function modelsCommand(opts) {
|
|
|
36
71
|
}
|
|
37
72
|
}
|
|
38
73
|
const GROUPS = ['input', 'output', 'intelligence', 'agent'];
|
|
39
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Gather the ACTION-capability table (Phase 3.1). Uses a full Runtime (like `doctor`) so autoloaded
|
|
76
|
+
* skills appear — a listing that omitted them would misreport availability.
|
|
77
|
+
*/
|
|
78
|
+
export function actionCapabilityRows(rt) {
|
|
79
|
+
const reg = rt.capabilities();
|
|
80
|
+
const rows = [];
|
|
81
|
+
for (const cap of reg.list()) {
|
|
82
|
+
for (const p of reg.providersOf(cap.id)) {
|
|
83
|
+
rows.push({ id: cap.id, provider: p.providerId, source: p.source, effects: p.effects.join('/'), availability: p.availability, evidence: p.verification, description: cap.description });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return rows;
|
|
87
|
+
}
|
|
88
|
+
export async function gatherActionCapabilities(config) {
|
|
89
|
+
return withRuntime({ ...(config ? { config } : {}) }, async (rt) => {
|
|
90
|
+
// This command's whole purpose is an accurate picture of what the runtime can do, so it DOES connect
|
|
91
|
+
// (the lazy rule is per command class): reporting a cache-seeded MCP tool's fail-closed placeholders
|
|
92
|
+
// as if they were the server's real annotations would be the wrong kind of honest.
|
|
93
|
+
if (rt.hasMcpServers())
|
|
94
|
+
await rt.connectMcp();
|
|
95
|
+
return actionCapabilityRows(rt);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Render the action-capability table as aligned text (shared by the CLI and the REPL). */
|
|
99
|
+
export function renderActionCapabilities(rows) {
|
|
100
|
+
if (rows.length === 0)
|
|
101
|
+
return ['No action capabilities registered.'];
|
|
102
|
+
const lines = ['Action capabilities (what this runtime can DO, and who provides it):'];
|
|
103
|
+
for (const r of rows)
|
|
104
|
+
lines.push(` ${r.id.padEnd(22)} ${r.provider.padEnd(26)} ${r.source.padEnd(8)} ${r.effects.padEnd(10)} ${r.availability.padEnd(12)} ${r.evidence}`);
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
/** One-line gloss per gap reason (exhaustive over CapabilityGapReason). */
|
|
108
|
+
export const GAP_REASON_GLOSS = {
|
|
109
|
+
unknown: 'nothing registered provides it',
|
|
110
|
+
unavailable: 'its provider is not usable right now',
|
|
111
|
+
disabled: 'its provider is turned off',
|
|
112
|
+
permission: 'the runtime is not allowed to do this',
|
|
113
|
+
};
|
|
114
|
+
/** One actionable line per GapSuggestion (exhaustive over all four arms). */
|
|
115
|
+
export function renderGapSuggestion(s) {
|
|
116
|
+
switch (s.kind) {
|
|
117
|
+
case 'grant-permission':
|
|
118
|
+
return `grant ${displaySafe(s.policyKey, 60)} in .ai-runtime/config.yaml`;
|
|
119
|
+
case 'enable-server':
|
|
120
|
+
return `enable the MCP server '${displaySafe(s.serverId, 40)}' (ai-runtime mcp enable ${displaySafe(s.serverId, 40)}) and grant permissions.mcp.servers.${displaySafe(s.serverId, 40)}`;
|
|
121
|
+
case 'register':
|
|
122
|
+
return displaySafe(s.hint, 160);
|
|
123
|
+
case 'install-skill':
|
|
124
|
+
return displaySafe(s.hint, 160);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Render a MissingCapabilityReport. THE one gap renderer: `run`, the REPL result tail,
|
|
129
|
+
* `capabilities --actions <goal>` and `/capabilities <goal>` all call this, so they cannot drift.
|
|
130
|
+
* Every id, hint, and policyKey passes `displaySafe` — a gap's capabilityId originates in the model's
|
|
131
|
+
* plan JSON, and `sanitizeSegment` restricts its charset but sets no length bound.
|
|
132
|
+
*/
|
|
133
|
+
export function renderCapabilityGaps(report) {
|
|
134
|
+
if (report.required.length === 0)
|
|
135
|
+
return [];
|
|
136
|
+
// "appears to need": derivation is a lexical inference over capability descriptions, and a plan step's
|
|
137
|
+
// provider can do more than the step will. The rows are advice, and the wording says so.
|
|
138
|
+
const lines = ['', 'Capabilities this goal appears to need:'];
|
|
139
|
+
for (const r of report.required) {
|
|
140
|
+
const gloss = r.status === 'available' ? displaySafe(r.providers.join(', '), 60) : GAP_REASON_GLOSS[r.status];
|
|
141
|
+
lines.push(` ${r.status === 'available' ? '✓' : '✗'} ${displaySafe(r.capabilityId, 30).padEnd(30)} ${r.status.padEnd(12)} ${gloss}`);
|
|
142
|
+
}
|
|
143
|
+
if (!report.gaps.length)
|
|
144
|
+
return lines;
|
|
145
|
+
const fixes = report.suggestions.map(renderGapSuggestion);
|
|
146
|
+
// A gap with an EMPTY suggestions array is reachable (every provider disabled, none MCP-sourced), so a
|
|
147
|
+
// gap is never printed without a remedy line.
|
|
148
|
+
for (const g of report.gaps) {
|
|
149
|
+
if (!g.suggestions.length)
|
|
150
|
+
fixes.push(`no automatic remedy for '${displaySafe(g.capabilityId, 30)}' — see \`ai-runtime capabilities --actions\``);
|
|
151
|
+
}
|
|
152
|
+
lines.push('To fix:', ...fixes.map((f) => ` • ${f}`));
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `capabilities --actions <goal>` — a free, OFFLINE "can you do this?" pre-flight. No model call and no
|
|
157
|
+
* provider needed: derivation is pure BM25 over the registry and resolution is pure, so this answers
|
|
158
|
+
* correctly with `providers: []` and no network.
|
|
159
|
+
*/
|
|
160
|
+
async function actionsForGoal(goal, opts) {
|
|
161
|
+
return withRuntime({ ...(opts.config ? { config: opts.config } : {}) }, async (rt) => {
|
|
162
|
+
if (rt.hasMcpServers())
|
|
163
|
+
await rt.connectMcp();
|
|
164
|
+
const required = deriveCapabilitiesOffline(goal, candidatesFrom(rt.capabilities().list()));
|
|
165
|
+
if (!required.length) {
|
|
166
|
+
// `--json` must stay machine-readable on EVERY path — a consumer that asked for JSON and got prose
|
|
167
|
+
// has to special-case it.
|
|
168
|
+
if (opts.json)
|
|
169
|
+
return print(JSON.stringify({ goal: displaySafe(goal, 200), required: [], gaps: [], suggestions: [] }, null, 2));
|
|
170
|
+
return print(`No action capability matched ${JSON.stringify(displaySafe(goal, 80))} — run \`ai-runtime capabilities --actions\` to see everything this runtime can do.`);
|
|
171
|
+
}
|
|
172
|
+
const report = rt.capabilityReport(required);
|
|
173
|
+
if (opts.json)
|
|
174
|
+
return print(JSON.stringify(report, null, 2));
|
|
175
|
+
for (const line of renderCapabilityGaps(report))
|
|
176
|
+
print(line);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export async function capabilitiesCommand(goal, opts) {
|
|
180
|
+
// `--actions` shows ACTION capabilities (Phase 3.1); the default stays the MODEL capability matrix.
|
|
181
|
+
// With a goal, `--actions` answers "what does THIS need, and what is missing?" — offline (Phase 3.3).
|
|
182
|
+
// A goal only means anything for action capabilities (the model matrix takes no goal), so a goal
|
|
183
|
+
// IMPLIES --actions rather than being silently discarded.
|
|
184
|
+
if (goal)
|
|
185
|
+
return actionsForGoal(goal, opts);
|
|
186
|
+
if (opts.actions) {
|
|
187
|
+
const rows = await gatherActionCapabilities(opts.config);
|
|
188
|
+
if (opts.json)
|
|
189
|
+
return print(JSON.stringify(rows, null, 2));
|
|
190
|
+
for (const line of renderActionCapabilities(rows))
|
|
191
|
+
print(line);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
40
194
|
const { ai, hasProviders } = loadAI(opts.config);
|
|
41
195
|
if (!hasProviders)
|
|
42
196
|
return print('No providers configured.');
|
|
@@ -57,4 +211,5 @@ export async function capabilitiesCommand(opts) {
|
|
|
57
211
|
print(` context ${ctx.value} (${ctx.evidence})`);
|
|
58
212
|
}
|
|
59
213
|
}
|
|
214
|
+
print('\n(model capabilities — use `--actions` for action capabilities: read_file, run_tests, …)');
|
|
60
215
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime mcp` — inspect and manage MCP servers (Phase 3.2).
|
|
3
|
+
*
|
|
4
|
+
* mcp list every configured server with its state, tool count, and grant
|
|
5
|
+
* mcp <id> one server in detail, including redacted diagnostics
|
|
6
|
+
* mcp add <id> … add a server — persisted to the STORE, never to config.yaml
|
|
7
|
+
* mcp remove <id> remove a store-added server (a config-declared one is the operator's file)
|
|
8
|
+
* mcp enable|disable <id> flip a server without editing anything
|
|
9
|
+
* mcp test <id> connect, handshake, list tools, ping — and report honestly
|
|
10
|
+
*
|
|
11
|
+
* Credentials are named, never valued: `--token-env NAME` records an env-var NAME, and nothing here ever
|
|
12
|
+
* prints a token value. Every server-supplied string reaching this output has already passed the
|
|
13
|
+
* discovery-time normalization gate, so a server cannot forge a row.
|
|
14
|
+
*/
|
|
15
|
+
import type { McpServerStatus } from '../../mcp/manager.js';
|
|
16
|
+
/**
|
|
17
|
+
* The per-server grant map (`permissions.mcp.servers`), passed to the renderers instead of a Runtime so
|
|
18
|
+
* they stay Runtime-free — `mcp`, the REPL `/mcp`, and `doctor` therefore share ONE formatter and can
|
|
19
|
+
* never disagree. The map is null-prototype (src/tools/permissions.ts), so `?? 'off'` is correct for
|
|
20
|
+
* every key, configured or not.
|
|
21
|
+
*/
|
|
22
|
+
export type McpGrants = Record<string, string>;
|
|
23
|
+
export declare function mcpRow(grants: McpGrants, s: McpServerStatus): string;
|
|
24
|
+
export declare function renderMcpList(grants: McpGrants, list: McpServerStatus[]): string[];
|
|
25
|
+
export declare function renderMcpDetail(grants: McpGrants, s: McpServerStatus, diagnostics: string[], tools: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
readOnly: boolean;
|
|
29
|
+
destructive: boolean;
|
|
30
|
+
}>): string[];
|
|
31
|
+
export interface McpCommandOptions {
|
|
32
|
+
config?: string;
|
|
33
|
+
json?: boolean;
|
|
34
|
+
/** add: */
|
|
35
|
+
command?: string;
|
|
36
|
+
args?: string;
|
|
37
|
+
url?: string;
|
|
38
|
+
tokenEnv?: string;
|
|
39
|
+
timeout?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function mcpCommand(id: string | undefined, opts: McpCommandOptions): Promise<void>;
|
|
42
|
+
export declare function mcpAddCommand(id: string, opts: McpCommandOptions): Promise<void>;
|
|
43
|
+
export declare function mcpRemoveCommand(id: string, opts: McpCommandOptions): Promise<void>;
|
|
44
|
+
export declare function mcpEnableCommand(id: string, enabled: boolean, opts: McpCommandOptions): Promise<void>;
|
|
45
|
+
export declare function mcpTestCommand(id: string, opts: McpCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime mcp` — inspect and manage MCP servers (Phase 3.2).
|
|
3
|
+
*
|
|
4
|
+
* mcp list every configured server with its state, tool count, and grant
|
|
5
|
+
* mcp <id> one server in detail, including redacted diagnostics
|
|
6
|
+
* mcp add <id> … add a server — persisted to the STORE, never to config.yaml
|
|
7
|
+
* mcp remove <id> remove a store-added server (a config-declared one is the operator's file)
|
|
8
|
+
* mcp enable|disable <id> flip a server without editing anything
|
|
9
|
+
* mcp test <id> connect, handshake, list tools, ping — and report honestly
|
|
10
|
+
*
|
|
11
|
+
* Credentials are named, never valued: `--token-env NAME` records an env-var NAME, and nothing here ever
|
|
12
|
+
* prints a token value. Every server-supplied string reaching this output has already passed the
|
|
13
|
+
* discovery-time normalization gate, so a server cannot forge a row.
|
|
14
|
+
*/
|
|
15
|
+
import { withRuntime } from '../runtimeSession.js';
|
|
16
|
+
import { print, printError } from '../render.js';
|
|
17
|
+
import { KEY_LIKE } from '../../config/schema.js';
|
|
18
|
+
function grantOf(grants, id) {
|
|
19
|
+
// Own-property checked, not `grants[id] ?? 'off'`. The resolved map is null-prototype, but a caller may
|
|
20
|
+
// hand us a plain object (doctor JSON-serializes it with a spread), and a server id like `constructor`
|
|
21
|
+
// is legal under the id regex — a prototype lookup would then answer with a function, which is both
|
|
22
|
+
// wrong on screen and a hole in deny-by-default.
|
|
23
|
+
return Object.prototype.hasOwnProperty.call(grants, id) ? String(grants[id]) : 'off';
|
|
24
|
+
}
|
|
25
|
+
export function mcpRow(grants, s) {
|
|
26
|
+
const tools = s.toolCount + (s.truncated ? '+' : '');
|
|
27
|
+
const notes = [s.rejectedTools ? `${s.rejectedTools} rejected` : '', s.tokenSet ? `token ${s.tokenEnv}` : ''].filter(Boolean).join(', ');
|
|
28
|
+
return ` ${s.id.padEnd(16)} ${s.state.padEnd(12)} ${s.transport.padEnd(6)} tools ${String(tools).padEnd(5)} grant ${grantOf(grants, s.id).padEnd(5)} ${s.origin === 'dynamic' ? '(added)' : ''}${notes ? ` ${notes}` : ''}`;
|
|
29
|
+
}
|
|
30
|
+
export function renderMcpList(grants, list) {
|
|
31
|
+
if (!list.length) {
|
|
32
|
+
return ['No MCP servers configured.', '', 'Add one: ai-runtime mcp add <id> --command "npx -y some-mcp-server"', ' ai-runtime mcp add <id> --url https://example.com/mcp --token-env MY_TOKEN'];
|
|
33
|
+
}
|
|
34
|
+
const lines = ['MCP servers:', ...list.map((s) => mcpRow(grants, s))];
|
|
35
|
+
const ungranted = list.filter((s) => grantOf(grants, s.id) === 'off');
|
|
36
|
+
if (ungranted.length) {
|
|
37
|
+
lines.push('', `Not permitted to execute: ${ungranted.map((s) => s.id).join(', ')}`, ' Grant in .ai-runtime/config.yaml: permissions.mcp.servers.<id>: read (or full)');
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
export function renderMcpDetail(grants, s, diagnostics, tools) {
|
|
42
|
+
const lines = [
|
|
43
|
+
`MCP server '${s.id}'`,
|
|
44
|
+
` state ${s.state}${s.lastError ? ` — ${s.lastError}` : ''}`,
|
|
45
|
+
` transport ${s.transport}${s.endpoint ? ` ${s.endpoint}` : ''}`,
|
|
46
|
+
` origin ${s.origin === 'dynamic' ? 'added (store)' : 'config'}`,
|
|
47
|
+
` enabled ${s.enabled}`,
|
|
48
|
+
` credential ${s.tokenSet ? `set via ${s.tokenEnv}` : s.tokenEnv ? `${s.tokenEnv} (NOT set)` : 'none'}`,
|
|
49
|
+
` grant ${grantOf(grants, s.id)}`,
|
|
50
|
+
` protocol ${s.protocolVersion ?? '(not negotiated)'}${s.serverName ? ` server: ${s.serverName}` : ''}`,
|
|
51
|
+
` tools ${s.toolCount}${s.truncated ? ' (truncated — the server offers more than we accept)' : ''}${s.rejectedTools ? `, ${s.rejectedTools} rejected as unsafe` : ''}`,
|
|
52
|
+
];
|
|
53
|
+
if (tools.length) {
|
|
54
|
+
lines.push('', ' Tools:');
|
|
55
|
+
for (const t of tools)
|
|
56
|
+
lines.push(` ${t.name.padEnd(28)} ${t.readOnly ? 'read ' : 'write'}${t.destructive ? ' destructive' : ''} ${t.description}`);
|
|
57
|
+
}
|
|
58
|
+
if (diagnostics.length) {
|
|
59
|
+
lines.push('', ' Recent diagnostics (redacted):');
|
|
60
|
+
for (const d of diagnostics)
|
|
61
|
+
lines.push(` ${d}`);
|
|
62
|
+
}
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
export async function mcpCommand(id, opts) {
|
|
66
|
+
await withRuntime(opts, async (rt) => {
|
|
67
|
+
if (!rt.hasMcpServers()) {
|
|
68
|
+
if (opts.json)
|
|
69
|
+
return print(JSON.stringify({ servers: [] }, null, 2));
|
|
70
|
+
return print(renderMcpList(rt.permissions().mcp.servers, []).join('\n'));
|
|
71
|
+
}
|
|
72
|
+
await rt.connectMcp();
|
|
73
|
+
if (!id) {
|
|
74
|
+
const list = rt.mcp().list();
|
|
75
|
+
if (opts.json)
|
|
76
|
+
return print(JSON.stringify({ servers: list, warnings: rt.mcpWarningsList() }, null, 2));
|
|
77
|
+
const lines = renderMcpList(rt.permissions().mcp.servers, list);
|
|
78
|
+
const warnings = rt.mcpWarningsList();
|
|
79
|
+
if (warnings.length)
|
|
80
|
+
lines.push('', 'Warnings:', ...warnings.map((w) => ` ! ${w}`));
|
|
81
|
+
return print(lines.join('\n'));
|
|
82
|
+
}
|
|
83
|
+
const status = rt.mcp().status(id);
|
|
84
|
+
if (!status)
|
|
85
|
+
return printError(`no MCP server '${id}' — run 'ai-runtime mcp' to list them`);
|
|
86
|
+
const tools = rt.mcp().tools(id);
|
|
87
|
+
if (opts.json)
|
|
88
|
+
return print(JSON.stringify({ server: status, tools, diagnostics: rt.mcp().diagnostics(id) }, null, 2));
|
|
89
|
+
print(renderMcpDetail(rt.permissions().mcp.servers, status, rt.mcp().diagnostics(id), tools).join('\n'));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export async function mcpAddCommand(id, opts) {
|
|
93
|
+
await withRuntime(opts, async (rt) => {
|
|
94
|
+
if (!opts.command && !opts.url)
|
|
95
|
+
return printError('specify either --command "<argv>" (stdio) or --url <endpoint> (http)');
|
|
96
|
+
if (opts.command && opts.url)
|
|
97
|
+
return printError('a server is either stdio (--command) or http (--url), not both');
|
|
98
|
+
// --token-env takes a NAME. Refuse a value here, before it would be written to the store in plain
|
|
99
|
+
// text and echoed back to the terminal (the manager refuses too; this is the friendly message).
|
|
100
|
+
if (opts.tokenEnv && KEY_LIKE.test(opts.tokenEnv)) {
|
|
101
|
+
return printError('--token-env takes the NAME of an environment variable, not the token itself (e.g. --token-env GITHUB_MCP_TOKEN)');
|
|
102
|
+
}
|
|
103
|
+
const argv = (opts.command ?? '').trim().split(/\s+/).filter(Boolean);
|
|
104
|
+
const cfg = opts.url
|
|
105
|
+
? { transport: 'http', url: opts.url, ...(opts.tokenEnv ? { tokenEnv: opts.tokenEnv } : {}), ...(opts.timeout ? { timeoutMs: Number(opts.timeout) } : {}) }
|
|
106
|
+
: { transport: 'stdio', command: argv[0], ...(argv.length > 1 ? { args: argv.slice(1) } : {}), ...(opts.tokenEnv ? { tokenEnv: opts.tokenEnv } : {}), ...(opts.timeout ? { timeoutMs: Number(opts.timeout) } : {}) };
|
|
107
|
+
try {
|
|
108
|
+
const status = await rt.mcp().addServer(id, cfg);
|
|
109
|
+
if (opts.json)
|
|
110
|
+
return print(JSON.stringify({ ok: true, server: status }, null, 2));
|
|
111
|
+
print(renderMcpDetail(rt.permissions().mcp.servers, status, rt.mcp().diagnostics(id), rt.mcp().tools(id)).join('\n'));
|
|
112
|
+
print(`\nAdded '${id}' (stored locally — your config file was not modified).`);
|
|
113
|
+
print(`It can execute nothing yet. Grant it in .ai-runtime/config.yaml:\n permissions:\n mcp:\n servers:\n ${id}: read # or full`);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
printError(e instanceof Error ? e.message : String(e));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export async function mcpRemoveCommand(id, opts) {
|
|
121
|
+
await withRuntime(opts, async (rt) => {
|
|
122
|
+
const res = rt.mcp().removeServer(id);
|
|
123
|
+
if (!res.ok)
|
|
124
|
+
return printError(res.reason ?? `could not remove '${id}'`);
|
|
125
|
+
print(`Removed MCP server '${id}'.`);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export async function mcpEnableCommand(id, enabled, opts) {
|
|
129
|
+
await withRuntime(opts, async (rt) => {
|
|
130
|
+
const res = rt.mcp().setEnabled(id, enabled);
|
|
131
|
+
if (!res.ok)
|
|
132
|
+
return printError(res.reason ?? `no MCP server '${id}'`);
|
|
133
|
+
print(`MCP server '${id}' is now ${enabled ? 'enabled' : 'disabled'}.`);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export async function mcpTestCommand(id, opts) {
|
|
137
|
+
await withRuntime(opts, async (rt) => {
|
|
138
|
+
if (!rt.mcp().status(id))
|
|
139
|
+
return printError(`no MCP server '${id}' — run 'ai-runtime mcp' to list them`);
|
|
140
|
+
const res = await rt.mcp().test(id);
|
|
141
|
+
if (opts.json)
|
|
142
|
+
return print(JSON.stringify(res, null, 2));
|
|
143
|
+
const lines = renderMcpDetail(rt.permissions().mcp.servers, res, rt.mcp().diagnostics(id), rt.mcp().tools(id));
|
|
144
|
+
if (res.pingMs !== undefined)
|
|
145
|
+
lines.push(` ping ${res.pingMs}ms`);
|
|
146
|
+
print(lines.join('\n'));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { buildDemoAI } from '../../providers/mock/demo.js';
|
|
7
7
|
import { loadAI } from '../context.js';
|
|
8
8
|
import { print } from '../render.js';
|
|
9
|
+
import { loadModelProfile, resolveModelDirective, directiveToOverrides } from '../../runtime/models/modelProfile.js';
|
|
9
10
|
export async function routeCommand(task, options) {
|
|
10
11
|
const loaded = loadAI(options.config);
|
|
11
12
|
const ai = loaded.hasProviders ? loaded.ai : buildDemoAI();
|
|
@@ -18,6 +19,26 @@ export async function routeCommand(task, options) {
|
|
|
18
19
|
request.provider = options.provider;
|
|
19
20
|
if (options.model)
|
|
20
21
|
request.model = options.model;
|
|
22
|
+
// models.md (Phase 23): a `## By task` entry for this router task steers routing — a strategy, a soft
|
|
23
|
+
// prefer (merged into request.routing), or a hard pin — but only where the caller passed no explicit
|
|
24
|
+
// flag (explicit -s/-p/-m win). This is where per-TASK routing applies (the Runtime handles per-MODE).
|
|
25
|
+
const profile = loadModelProfile(process.cwd());
|
|
26
|
+
const directive = profile ? resolveModelDirective(profile, { task }) : undefined;
|
|
27
|
+
if (directive) {
|
|
28
|
+
const o = directiveToOverrides(directive);
|
|
29
|
+
if (o.strategy && request.strategy === undefined)
|
|
30
|
+
request.strategy = o.strategy;
|
|
31
|
+
if (o.routing)
|
|
32
|
+
request.routing = { ...o.routing, ...request.routing };
|
|
33
|
+
// A pin is atomic (provider+model together) — apply it only when the caller pinned NEITHER, so an
|
|
34
|
+
// explicit -p/-m is never combined with the profile's pin into an impossible provider/model pair.
|
|
35
|
+
if ((o.provider || o.model) && request.provider === undefined && request.model === undefined) {
|
|
36
|
+
if (o.provider)
|
|
37
|
+
request.provider = o.provider;
|
|
38
|
+
if (o.model)
|
|
39
|
+
request.model = o.model;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
21
42
|
const result = await ai.run(request);
|
|
22
43
|
if (options.json) {
|
|
23
44
|
print(JSON.stringify(result, null, 2));
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
* `ai-runtime run "<input>"` — one-shot Runtime invocation for CI/scripts. Shares the exact Runtime
|
|
3
3
|
* the interactive REPL uses. Prints the response (or a structured result with --json).
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { withRuntime } from '../runtimeSession.js';
|
|
6
6
|
import { print, printChunk } from '../render.js';
|
|
7
|
+
import { renderCapabilityGaps } from './inspect.js';
|
|
7
8
|
import { RUNTIME_MODES } from '../../runtime/types.js';
|
|
8
9
|
export async function runCommand(input, options) {
|
|
9
|
-
|
|
10
|
+
// withRuntime releases MCP stdio children on every path — a one-shot command never orphans a process.
|
|
11
|
+
return withRuntime(options, (rt) => runWith(rt, input, options));
|
|
12
|
+
}
|
|
13
|
+
async function runWith(rt, input, options) {
|
|
14
|
+
// Lazy MCP connect, per command class: `run` may plan and execute tools, so its catalog must include
|
|
15
|
+
// MCP tools. A command that cannot execute tools pays nothing (it never calls this).
|
|
16
|
+
if (rt.hasMcpServers()) {
|
|
17
|
+
await rt.connectMcp();
|
|
18
|
+
for (const w of rt.mcpWarningsList())
|
|
19
|
+
print(`! ${w}`);
|
|
20
|
+
}
|
|
10
21
|
const req = { input };
|
|
11
22
|
if (options.dryRun)
|
|
12
23
|
req.dryRun = true;
|
|
24
|
+
if (options.partial)
|
|
25
|
+
req.partial = true;
|
|
13
26
|
// --stream renders tokens live (incompatible with --json, which needs the whole object).
|
|
14
27
|
let streamedAny = false;
|
|
15
28
|
if (options.stream && !options.json) {
|
|
@@ -45,6 +58,12 @@ export async function runCommand(input, options) {
|
|
|
45
58
|
if (result.error)
|
|
46
59
|
print(`[${result.error.category}] ${result.error.message}`);
|
|
47
60
|
// Any un-ok result (a router error OR a failed plan/execute/orchestrate) is a non-zero exit for CI.
|
|
61
|
+
// Phase 3.3: the ONE gap renderer, shared with the REPL and both capabilities surfaces. `--json`
|
|
62
|
+
// already dumps the whole result, so this is text-mode only. A gap is metadata on an outcome, so
|
|
63
|
+
// the exit code below is unchanged — it is never a new failure class.
|
|
64
|
+
if (result.capabilityGaps)
|
|
65
|
+
for (const l of renderCapabilityGaps(result.capabilityGaps))
|
|
66
|
+
print(l);
|
|
48
67
|
if (!result.ok)
|
|
49
68
|
process.exitCode = 1;
|
|
50
69
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* them without loading — the user enables one by moving it into `.ai-runtime/skills/` or adding its path to
|
|
5
5
|
* `skills.paths` in config (loading a module runs its code, so it is never implicit).
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { withRuntime } from '../runtimeSession.js';
|
|
8
8
|
import { print } from '../render.js';
|
|
9
|
+
import { confirm } from '../prompt.js';
|
|
9
10
|
export async function gatherSkills(rt, discover) {
|
|
10
11
|
const sources = rt.skillSources();
|
|
11
12
|
const sourceById = new Map();
|
|
@@ -39,10 +40,31 @@ function renderSkills(r, discover) {
|
|
|
39
40
|
return lines;
|
|
40
41
|
}
|
|
41
42
|
export async function skillsCommand(opts) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
// Every Runtime-based command releases its Runtime (MCP children included) on every path.
|
|
44
|
+
return withRuntime(opts, async (rt) => {
|
|
45
|
+
// `--scaffold "<goal>"` drafts a *.skill.yaml manifest via the model and offers to save it (never writes
|
|
46
|
+
// without confirmation; the model emits validated CONFIG, never code).
|
|
47
|
+
if (opts.scaffold) {
|
|
48
|
+
const draft = await rt.scaffoldSkill(opts.scaffold);
|
|
49
|
+
if (!draft.ok || !draft.manifest || !draft.yaml) {
|
|
50
|
+
if (opts.json)
|
|
51
|
+
return print(JSON.stringify({ ok: false, error: draft.error }, null, 2));
|
|
52
|
+
return print(`Could not scaffold a skill: ${draft.error}`);
|
|
53
|
+
}
|
|
54
|
+
if (opts.json)
|
|
55
|
+
return print(JSON.stringify({ ok: true, manifest: draft.manifest, yaml: draft.yaml }, null, 2));
|
|
56
|
+
print(`Drafted skill "${draft.manifest.id}":\n`);
|
|
57
|
+
print(draft.yaml);
|
|
58
|
+
const proceed = opts.yes || (await confirm(`Save to .ai-runtime/skills/${draft.manifest.id}.skill.yaml?`));
|
|
59
|
+
if (!proceed)
|
|
60
|
+
return print('Not saved. (Re-run with --yes to save without prompting.)');
|
|
61
|
+
const path = rt.saveScaffoldedSkill(draft.manifest);
|
|
62
|
+
return print(`Saved ${path}`);
|
|
63
|
+
}
|
|
64
|
+
const report = await gatherSkills(rt, !!opts.discover);
|
|
65
|
+
if (opts.json)
|
|
66
|
+
return print(JSON.stringify(report, null, 2));
|
|
67
|
+
for (const line of renderSkills(report, !!opts.discover))
|
|
68
|
+
print(line);
|
|
69
|
+
});
|
|
48
70
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency ANSI helpers for the REPL (Phase 21b). Color/bold are applied ONLY when the output is an
|
|
3
|
+
* interactive TTY and the environment permits it (`NO_COLOR` unset, `TERM` not "dumb") — so piped/CI output
|
|
4
|
+
* stays plain and the line-based session tests are unaffected. Every helper is pure and returns a string;
|
|
5
|
+
* the caller decides where to write it (always through `redactString` at the render layer).
|
|
6
|
+
*/
|
|
7
|
+
export interface ColorOptions {
|
|
8
|
+
isTTY?: boolean;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}
|
|
11
|
+
/** Whether ANSI color should be emitted: an interactive TTY, `NO_COLOR` unset, and a non-dumb terminal. */
|
|
12
|
+
export declare function colorEnabled(opts?: ColorOptions): boolean;
|
|
13
|
+
declare const CODES: {
|
|
14
|
+
readonly reset: 0;
|
|
15
|
+
readonly bold: 1;
|
|
16
|
+
readonly dim: 2;
|
|
17
|
+
readonly red: 31;
|
|
18
|
+
readonly green: 32;
|
|
19
|
+
readonly yellow: 33;
|
|
20
|
+
readonly blue: 34;
|
|
21
|
+
readonly magenta: 35;
|
|
22
|
+
readonly cyan: 36;
|
|
23
|
+
readonly gray: 90;
|
|
24
|
+
};
|
|
25
|
+
type CodeName = keyof typeof CODES;
|
|
26
|
+
/** Wrap `text` in the named SGR code when `enabled`; otherwise return it unchanged. */
|
|
27
|
+
export declare function style(text: string, code: CodeName, enabled: boolean): string;
|
|
28
|
+
export declare const bold: (t: string, on: boolean) => string;
|
|
29
|
+
export declare const dim: (t: string, on: boolean) => string;
|
|
30
|
+
export declare const green: (t: string, on: boolean) => string;
|
|
31
|
+
export declare const red: (t: string, on: boolean) => string;
|
|
32
|
+
export declare const yellow: (t: string, on: boolean) => string;
|
|
33
|
+
export declare const cyan: (t: string, on: boolean) => string;
|
|
34
|
+
export declare const gray: (t: string, on: boolean) => string;
|
|
35
|
+
/** Frames for an in-place spinner. */
|
|
36
|
+
export declare const SPINNER_FRAMES: string[];
|
|
37
|
+
/** A carriage-return status line (`\r` + clear-to-EOL + text). Empty string when color/TTY is off. */
|
|
38
|
+
export declare function statusLine(text: string, enabled: boolean): string;
|
|
39
|
+
/** Clear the current line in place (`\r` + clear-to-EOL). Empty string when color/TTY is off. */
|
|
40
|
+
export declare function clearLine(enabled: boolean): string;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency ANSI helpers for the REPL (Phase 21b). Color/bold are applied ONLY when the output is an
|
|
3
|
+
* interactive TTY and the environment permits it (`NO_COLOR` unset, `TERM` not "dumb") — so piped/CI output
|
|
4
|
+
* stays plain and the line-based session tests are unaffected. Every helper is pure and returns a string;
|
|
5
|
+
* the caller decides where to write it (always through `redactString` at the render layer).
|
|
6
|
+
*/
|
|
7
|
+
/** Whether ANSI color should be emitted: an interactive TTY, `NO_COLOR` unset, and a non-dumb terminal. */
|
|
8
|
+
export function colorEnabled(opts = {}) {
|
|
9
|
+
const env = opts.env ?? process.env;
|
|
10
|
+
if (!opts.isTTY)
|
|
11
|
+
return false;
|
|
12
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
|
|
13
|
+
return false;
|
|
14
|
+
if (env.TERM === 'dumb')
|
|
15
|
+
return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const CODES = { reset: 0, bold: 1, dim: 2, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, gray: 90 };
|
|
19
|
+
/** Wrap `text` in the named SGR code when `enabled`; otherwise return it unchanged. */
|
|
20
|
+
export function style(text, code, enabled) {
|
|
21
|
+
if (!enabled)
|
|
22
|
+
return text;
|
|
23
|
+
return `[${CODES[code]}m${text}[${CODES.reset}m`;
|
|
24
|
+
}
|
|
25
|
+
export const bold = (t, on) => style(t, 'bold', on);
|
|
26
|
+
export const dim = (t, on) => style(t, 'dim', on);
|
|
27
|
+
export const green = (t, on) => style(t, 'green', on);
|
|
28
|
+
export const red = (t, on) => style(t, 'red', on);
|
|
29
|
+
export const yellow = (t, on) => style(t, 'yellow', on);
|
|
30
|
+
export const cyan = (t, on) => style(t, 'cyan', on);
|
|
31
|
+
export const gray = (t, on) => style(t, 'gray', on);
|
|
32
|
+
/** Frames for an in-place spinner. */
|
|
33
|
+
export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
34
|
+
/** A carriage-return status line (`\r` + clear-to-EOL + text). Empty string when color/TTY is off. */
|
|
35
|
+
export function statusLine(text, enabled) {
|
|
36
|
+
if (!enabled)
|
|
37
|
+
return '';
|
|
38
|
+
return `\r[2K${text}`;
|
|
39
|
+
}
|
|
40
|
+
/** Clear the current line in place (`\r` + clear-to-EOL). Empty string when color/TTY is off. */
|
|
41
|
+
export function clearLine(enabled) {
|
|
42
|
+
return enabled ? '\r[2K' : '';
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-command tab-completion for the REPL (Phase 21b). A pure `node:readline`-shaped completer: given the
|
|
3
|
+
* current line, return `[matches, lineToComplete]`. It completes ONLY the leading command token of a `/…`
|
|
4
|
+
* line — never natural-language input and never command arguments — so it can't interfere with typing.
|
|
5
|
+
*/
|
|
6
|
+
/** readline completer contract: `[completions, substringBeingCompleted]`. */
|
|
7
|
+
export type CompleterResult = [string[], string];
|
|
8
|
+
export declare function completeSlash(line: string, commands: readonly string[]): CompleterResult;
|
|
9
|
+
/** Build a `node:readline`-compatible completer bound to a command list. */
|
|
10
|
+
export declare function makeCompleter(commands: readonly string[]): (line: string) => CompleterResult;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-command tab-completion for the REPL (Phase 21b). A pure `node:readline`-shaped completer: given the
|
|
3
|
+
* current line, return `[matches, lineToComplete]`. It completes ONLY the leading command token of a `/…`
|
|
4
|
+
* line — never natural-language input and never command arguments — so it can't interfere with typing.
|
|
5
|
+
*/
|
|
6
|
+
export function completeSlash(line, commands) {
|
|
7
|
+
const trimmed = line.trimStart();
|
|
8
|
+
// Only a bare `/command` prefix (no space yet) is completed; args and plain text are left alone.
|
|
9
|
+
if (!trimmed.startsWith('/') || /\s/.test(trimmed))
|
|
10
|
+
return [[], line];
|
|
11
|
+
const prefix = trimmed.slice(1).toLowerCase();
|
|
12
|
+
const hits = commands.filter((c) => c.startsWith(prefix)).map((c) => `/${c}`);
|
|
13
|
+
// When there is exactly one full match already typed, offer nothing (avoids a redundant redraw).
|
|
14
|
+
return [hits, line];
|
|
15
|
+
}
|
|
16
|
+
/** Build a `node:readline`-compatible completer bound to a command list. */
|
|
17
|
+
export function makeCompleter(commands) {
|
|
18
|
+
return (line) => completeSlash(line, commands);
|
|
19
|
+
}
|