ai-runtime-engine 1.3.0 → 2.7.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 +578 -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/task.d.ts +60 -0
- package/dist/agents/task.js +32 -0
- package/dist/agents/worker.d.ts +68 -0
- package/dist/agents/worker.js +256 -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/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/execution.d.ts +13 -2
- 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 +43 -4
- package/dist/index.js +26 -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 +39 -1
- package/dist/orchestration/executor.js +64 -4
- package/dist/orchestration/orchestrator.d.ts +29 -1
- package/dist/orchestration/orchestrator.js +89 -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 +173 -0
- package/dist/runtime/runtime.js +705 -50
- package/dist/runtime/types.d.ts +88 -2
- 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/semaphore.d.ts +19 -0
- package/dist/util/semaphore.js +60 -0
- package/package.json +24 -9
|
@@ -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
|
+
}
|
|
@@ -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;
|