ai-runtime-engine 1.2.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 +602 -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 +56 -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 +2 -0
- package/dist/cli/commands/run.js +36 -4
- 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 +105 -16
- package/dist/cli/interactive/session.d.ts +12 -1
- package/dist/cli/interactive/session.js +83 -5
- package/dist/cli/render.d.ts +13 -0
- package/dist/cli/render.js +18 -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 +8 -0
- package/dist/core/fallback/fallback.js +3 -1
- package/dist/core/router/executor.d.ts +6 -1
- package/dist/core/router/executor.js +9 -2
- package/dist/core/router/normalize.d.ts +2 -0
- package/dist/core/router/request.js +2 -0
- package/dist/core/router/router.d.ts +3 -0
- package/dist/core/router/router.js +7 -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 +44 -5
- 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/providers/httpClient.d.ts +25 -1
- package/dist/providers/httpClient.js +93 -0
- package/dist/providers/httpProvider.d.ts +1 -0
- package/dist/providers/httpProvider.js +67 -1
- package/dist/providers/mock/mockProvider.d.ts +3 -0
- package/dist/providers/mock/mockProvider.js +54 -0
- package/dist/providers/mock/scenarios.d.ts +7 -0
- package/dist/providers/provider.d.ts +6 -0
- package/dist/providers/wire/anthropicWire.js +34 -0
- package/dist/providers/wire/openaiWire.js +30 -0
- package/dist/providers/wire/types.d.ts +16 -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 +723 -50
- package/dist/runtime/types.d.ts +94 -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 +11 -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,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ACTION-capability registry (Phase 3.1) — capability → provider(s), with a fully deterministic
|
|
3
|
+
* resolution algorithm and structured gaps for what cannot be satisfied.
|
|
4
|
+
*
|
|
5
|
+
* Population is driven by the tool/skill registries (their optional change listeners), so builtins,
|
|
6
|
+
* autoloaded manifest skills, and npm skill packs are all covered by one funnel. Availability is
|
|
7
|
+
* DERIVED at query time from injected read-only accessors, so it is never a stale snapshot.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY: resolution NEVER grants permission. It drops policy-denied providers (so it doesn't propose
|
|
10
|
+
* something the run cannot do) and returns bindings that still execute through `runTool`/`runSkill`,
|
|
11
|
+
* where the real permission check lives.
|
|
12
|
+
*/
|
|
13
|
+
import { rankOf } from '../core/capabilities/evidence.js';
|
|
14
|
+
import { curated, isCurated } from './vocabulary.js';
|
|
15
|
+
import { effectiveDeclId, effectsOf, entityFor, namespacedId, } from './capability.js';
|
|
16
|
+
const SOURCE_ORDER = ['native', 'skill', 'mcp', 'openapi', 'plugin'];
|
|
17
|
+
export class ActionCapabilityRegistry {
|
|
18
|
+
/** providerId → its entries (so a re-registration can drop exactly that provider's contributions). */
|
|
19
|
+
byProvider = new Map();
|
|
20
|
+
accessors = {};
|
|
21
|
+
config = {};
|
|
22
|
+
/** Warnings recorded during ingestion (sanitized ids, unknown alias/pin targets). */
|
|
23
|
+
warnings = [];
|
|
24
|
+
/** Wire the live registries (call once, before any registration). */
|
|
25
|
+
attachAccessors(a) {
|
|
26
|
+
this.accessors = { ...this.accessors, ...a };
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
/** Apply `runtime.capabilities` config (aliases claim; pins select). */
|
|
30
|
+
configure(config) {
|
|
31
|
+
this.config = config ?? {};
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
configWarnings() {
|
|
35
|
+
return [...this.warnings];
|
|
36
|
+
}
|
|
37
|
+
// ── ingestion ──────────────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Ingest a tool's declarations (or infer one). `trusted` marks in-tree builtins, the only code allowed
|
|
40
|
+
* to claim a bare curated id. Idempotent: prior entries for this provider are replaced.
|
|
41
|
+
*/
|
|
42
|
+
ingestTool(tool, trusted = false) {
|
|
43
|
+
const providerId = namespacedId('tool', tool.id);
|
|
44
|
+
if (!this.claimProviderId(providerId, tool.id))
|
|
45
|
+
return;
|
|
46
|
+
this.replaceProvider(providerId, this.declsToEntries(tool.provides, providerId, 'native', { kind: 'tool', toolId: tool.id }, 'tool', tool.id, trusted, tool.description));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Ingest an MCP-backed Tool. Its OWN namespace is derived from the Runtime-generated tool id
|
|
50
|
+
* (`<serverId>.<toolName>`) — never from the server's own declaration — so a server can claim a
|
|
51
|
+
* capability inside its own `mcp:` namespace and nowhere else. MCP is never a trusted claimant, so a
|
|
52
|
+
* curated id in its declaration is namespaced away like any other untrusted source.
|
|
53
|
+
*/
|
|
54
|
+
ingestMcpTool(tool) {
|
|
55
|
+
const providerId = namespacedId('mcp', tool.id);
|
|
56
|
+
if (!this.claimProviderId(providerId, tool.id))
|
|
57
|
+
return;
|
|
58
|
+
this.replaceProvider(providerId, this.declsToEntries(tool.provides, providerId, 'mcp', { kind: 'tool', toolId: tool.id }, 'mcp', tool.id, false, tool.description));
|
|
59
|
+
}
|
|
60
|
+
/** Ingest a skill's declarations (or infer one). Skills are never trusted to claim curated ids. */
|
|
61
|
+
ingestSkill(skill) {
|
|
62
|
+
const providerId = namespacedId('skill', skill.id);
|
|
63
|
+
if (!this.claimProviderId(providerId, skill.id))
|
|
64
|
+
return;
|
|
65
|
+
this.replaceProvider(providerId, this.declsToEntries(skill.provides, providerId, 'skill', { kind: 'skill', skillId: skill.id }, 'skill', skill.id, false, skill.description));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ownership guard: `providerId` is a SANITIZED view of a source id, and sanitization is not injective
|
|
69
|
+
* (`Filesystem`, `FILESYSTEM`, `file@system` all fold to `filesystem`). Re-registering the SAME raw id
|
|
70
|
+
* replaces its entries (idempotent, matching the tool/skill registries); a DIFFERENT raw id that folds
|
|
71
|
+
* onto an already-owned providerId is refused with a warning — it can neither erase the incumbent's
|
|
72
|
+
* capabilities nor inherit an operator alias/pin that names that providerId.
|
|
73
|
+
*/
|
|
74
|
+
claimProviderId(providerId, rawId) {
|
|
75
|
+
const owner = this.byProvider.get(providerId)?.[0]?.rawId;
|
|
76
|
+
if (owner !== undefined && owner !== rawId) {
|
|
77
|
+
this.warnings.push(`'${rawId}' maps to provider id '${providerId}', already held by '${owner}' — its action capabilities were not registered (rename the source to a distinct id)`);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** Remove everything a provider contributed (tool/skill unregistration, MCP server removal). */
|
|
83
|
+
removeProvider(providerId) {
|
|
84
|
+
this.byProvider.delete(providerId);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Remove a source's entries by its EXACT id (what the tool/skill registries emit on unregister).
|
|
88
|
+
* Sanitization is applied here, and only the owning raw id may remove — so a colliding id can never
|
|
89
|
+
* unregister the incumbent's capabilities.
|
|
90
|
+
*/
|
|
91
|
+
removeSource(kind, rawId) {
|
|
92
|
+
const providerId = namespacedId(kind, rawId);
|
|
93
|
+
if (this.byProvider.get(providerId)?.[0]?.rawId === rawId)
|
|
94
|
+
this.byProvider.delete(providerId);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Drop every capability contributed by one MCP server's tools. Scoped by the `mcp:<server>.` prefix
|
|
98
|
+
* INCLUDING the dot — without it, removing `demo` would also strip `demo2`.
|
|
99
|
+
*/
|
|
100
|
+
removeMcpServer(serverId) {
|
|
101
|
+
this.removeBySourcePrefix(`${namespacedId('mcp', serverId)}.`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Remove every provider whose providerId starts with the given LITERAL prefix. A low-level primitive:
|
|
105
|
+
* prefer `removeSource`/`removeMcpServer`, which scope correctly — a bare `mcp:demo` prefix also
|
|
106
|
+
* matches `mcp:demo2.x`.
|
|
107
|
+
*/
|
|
108
|
+
removeBySourcePrefix(prefix) {
|
|
109
|
+
for (const id of [...this.byProvider.keys()])
|
|
110
|
+
if (id.startsWith(prefix))
|
|
111
|
+
this.byProvider.delete(id);
|
|
112
|
+
}
|
|
113
|
+
replaceProvider(providerId, entries) {
|
|
114
|
+
this.byProvider.set(providerId, entries);
|
|
115
|
+
}
|
|
116
|
+
declsToEntries(provides, providerId, source, binding, kind, segment, trusted, description) {
|
|
117
|
+
// Undeclared ⇒ one inferred, namespaced capability so nothing is ever invisible.
|
|
118
|
+
if (!Array.isArray(provides) || provides.length === 0) {
|
|
119
|
+
return [{ capabilityId: namespacedId(kind, segment), providerId, rawId: segment, source, binding, effects: ['write'], verification: 'inferred', ...(description ? { description } : {}) }];
|
|
120
|
+
}
|
|
121
|
+
// One entry per RESOLVED capability id: several declarations collapsing onto the same id (e.g. two
|
|
122
|
+
// bare non-curated ids both namespaced to the source) must not produce duplicate providers.
|
|
123
|
+
const byId = new Map();
|
|
124
|
+
for (const decl of provides) {
|
|
125
|
+
const raw = String(decl?.id ?? '').trim();
|
|
126
|
+
const id = effectiveDeclId(decl, kind, segment, trusted);
|
|
127
|
+
if (raw && id !== raw) {
|
|
128
|
+
this.warnings.push(isCurated(raw)
|
|
129
|
+
? `${providerId} declared curated capability '${raw}' — registered as '${id}' (only builtins and config aliases may claim curated ids)`
|
|
130
|
+
: `${providerId} declared capability '${raw}' — registered as '${id}' (a declaration may only use its own namespaced id)`);
|
|
131
|
+
}
|
|
132
|
+
byId.set(id, {
|
|
133
|
+
capabilityId: id,
|
|
134
|
+
providerId,
|
|
135
|
+
rawId: segment,
|
|
136
|
+
source,
|
|
137
|
+
binding,
|
|
138
|
+
effects: effectsOf(decl, id),
|
|
139
|
+
verification: 'documented',
|
|
140
|
+
...(decl?.description ? { description: decl.description } : description ? { description } : {}),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return [...byId.values()];
|
|
144
|
+
}
|
|
145
|
+
// ── derived views ──────────────────────────────────────────────────────────
|
|
146
|
+
/** Every provider entry, including alias-claimed ones, with availability derived. */
|
|
147
|
+
allProviders() {
|
|
148
|
+
// Keyed by (capabilityId, providerId) so one provider can never appear twice for one capability —
|
|
149
|
+
// an alias pointing at a provider that already declares the id collapses onto a single entry.
|
|
150
|
+
const out = new Map();
|
|
151
|
+
const add = (p) => {
|
|
152
|
+
const key = `${p.capabilityId}\u0000${p.providerId}`;
|
|
153
|
+
if (!out.has(key))
|
|
154
|
+
out.set(key, p);
|
|
155
|
+
};
|
|
156
|
+
for (const entries of this.byProvider.values()) {
|
|
157
|
+
for (const e of entries)
|
|
158
|
+
add(this.toProvider(e, e.capabilityId));
|
|
159
|
+
}
|
|
160
|
+
// Config aliases: claim a curated id for an existing provider (its effects merge fail-closed).
|
|
161
|
+
for (const [curatedId, providerId] of Object.entries(this.config.aliases ?? {})) {
|
|
162
|
+
const src = this.findEntry(providerId);
|
|
163
|
+
if (!src)
|
|
164
|
+
continue; // unknown target — reported by configWarnings/gaps, never a silent grant
|
|
165
|
+
add(this.toProvider({ ...src, capabilityId: curatedId, verification: 'documented' }, curatedId));
|
|
166
|
+
}
|
|
167
|
+
return [...out.values()];
|
|
168
|
+
}
|
|
169
|
+
findEntry(providerId) {
|
|
170
|
+
return this.byProvider.get(providerId)?.[0];
|
|
171
|
+
}
|
|
172
|
+
toProvider(e, capabilityId) {
|
|
173
|
+
return {
|
|
174
|
+
capabilityId,
|
|
175
|
+
providerId: e.providerId,
|
|
176
|
+
source: e.source,
|
|
177
|
+
binding: e.binding,
|
|
178
|
+
effects: effectsOf({ id: capabilityId, effects: e.effects }, capabilityId),
|
|
179
|
+
verification: e.verification,
|
|
180
|
+
availability: this.availabilityOf(e),
|
|
181
|
+
...(e.note ? { note: e.note } : {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** Availability is DERIVED: a skill whose tools are not all registered is unavailable. */
|
|
185
|
+
availabilityOf(e) {
|
|
186
|
+
// An MCP provider's availability is the SERVER's, not merely "is the tool object registered" — a
|
|
187
|
+
// cache-seeded tool is registered and plan-referenceable while its server is only `unknown`.
|
|
188
|
+
if (e.source === 'mcp') {
|
|
189
|
+
const server = e.providerId.slice('mcp:'.length).split('.')[0] ?? '';
|
|
190
|
+
const a = this.accessors.mcpAvailability?.(server);
|
|
191
|
+
if (a)
|
|
192
|
+
return a;
|
|
193
|
+
}
|
|
194
|
+
if (e.binding.kind === 'tool') {
|
|
195
|
+
const ids = this.accessors.toolIds?.();
|
|
196
|
+
if (!ids)
|
|
197
|
+
return 'unknown';
|
|
198
|
+
return ids.includes(e.binding.toolId) ? 'available' : 'unavailable';
|
|
199
|
+
}
|
|
200
|
+
const skills = this.accessors.skills?.();
|
|
201
|
+
const toolIds = this.accessors.toolIds?.();
|
|
202
|
+
if (!skills || !toolIds)
|
|
203
|
+
return 'unknown';
|
|
204
|
+
const skill = skills.find((s) => s.id === e.binding.skillId);
|
|
205
|
+
if (!skill)
|
|
206
|
+
return 'unavailable';
|
|
207
|
+
return (skill.tools ?? []).every((t) => toolIds.includes(t)) ? 'available' : 'unavailable';
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The capability ids ONE provider contributes: its own stored entries plus any config alias that
|
|
211
|
+
* CLAIMS a curated id for it. A pure lookup over `byProvider` — no inference — so the registry stays
|
|
212
|
+
* offline and property-testable. Phase 3.3's post-plan check uses it to ask "what does this step's
|
|
213
|
+
* provider actually do?" instead of guessing from the step input.
|
|
214
|
+
*/
|
|
215
|
+
capabilitiesOf(providerId, rawId) {
|
|
216
|
+
const entries = this.byProvider.get(providerId) ?? [];
|
|
217
|
+
// Ownership check: `providerId` is a SANITIZED view of a source id, so `Filesystem` and `filesystem`
|
|
218
|
+
// fold together. `claimProviderId` refuses the second registration, which means a lookup by the
|
|
219
|
+
// loser's id would otherwise be answered with the INCUMBENT's capabilities — charging a step with
|
|
220
|
+
// permissions and effects belonging to a different provider. Given the caller's exact id, refuse.
|
|
221
|
+
if (rawId !== undefined && entries.length > 0 && entries[0].rawId !== rawId)
|
|
222
|
+
return [];
|
|
223
|
+
const own = entries.map((e) => e.capabilityId);
|
|
224
|
+
const aliased = Object.entries(this.config.aliases ?? {})
|
|
225
|
+
.filter(([, target]) => target === providerId && this.findEntry(target) !== undefined)
|
|
226
|
+
.map(([curatedId]) => curatedId);
|
|
227
|
+
return [...new Set([...own, ...aliased])].sort();
|
|
228
|
+
}
|
|
229
|
+
/** All capability entities (derived views), sorted by id for stable output. */
|
|
230
|
+
list() {
|
|
231
|
+
const ids = new Map();
|
|
232
|
+
for (const p of this.allProviders()) {
|
|
233
|
+
const arr = ids.get(p.capabilityId) ?? [];
|
|
234
|
+
arr.push(p);
|
|
235
|
+
ids.set(p.capabilityId, arr);
|
|
236
|
+
}
|
|
237
|
+
return [...ids.entries()]
|
|
238
|
+
.map(([id, providers]) => {
|
|
239
|
+
// The winning provider by the same total order resolve() uses — never Map insertion order.
|
|
240
|
+
const top = [...providers].sort(compareProviders)[0];
|
|
241
|
+
return entityFor(id, { description: this.byProvider.get(top.providerId)?.find((e) => e.capabilityId === id)?.description, effects: top.effects });
|
|
242
|
+
})
|
|
243
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
244
|
+
}
|
|
245
|
+
get(id) {
|
|
246
|
+
return this.list().find((c) => c.id === id);
|
|
247
|
+
}
|
|
248
|
+
providersOf(capabilityId) {
|
|
249
|
+
return this.allProviders().filter((p) => p.capabilityId === capabilityId);
|
|
250
|
+
}
|
|
251
|
+
query(q = {}) {
|
|
252
|
+
const text = q.text?.toLowerCase();
|
|
253
|
+
return this.list().filter((c) => {
|
|
254
|
+
if (text && !`${c.id} ${c.name} ${c.description}`.toLowerCase().includes(text))
|
|
255
|
+
return false;
|
|
256
|
+
if (q.effects && !c.effects.includes(q.effects))
|
|
257
|
+
return false;
|
|
258
|
+
const providers = this.providersOf(c.id);
|
|
259
|
+
if (q.source && !providers.some((p) => p.source === q.source))
|
|
260
|
+
return false;
|
|
261
|
+
if (q.availableOnly && !providers.some((p) => p.availability === 'available'))
|
|
262
|
+
return false;
|
|
263
|
+
return true;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// ── resolution (D9: the deterministic 8-step algorithm) ────────────────────
|
|
267
|
+
resolve(required, opts = {}) {
|
|
268
|
+
const satisfied = [];
|
|
269
|
+
const gaps = [];
|
|
270
|
+
for (const capabilityId of required) {
|
|
271
|
+
// (1) collect
|
|
272
|
+
const all = this.providersOf(capabilityId);
|
|
273
|
+
if (all.length === 0) {
|
|
274
|
+
gaps.push({ capabilityId, reason: 'unknown', providers: [], suggestions: [{ kind: 'register', hint: `no tool or skill provides '${capabilityId}' — register one, install a skill pack, or check the id` }] });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// (2) disabled
|
|
278
|
+
let pool = all.filter((p) => p.availability !== 'disabled');
|
|
279
|
+
if (pool.length === 0) {
|
|
280
|
+
gaps.push({ capabilityId, reason: 'disabled', providers: all, suggestions: all.filter((p) => p.source === 'mcp').map((p) => ({ kind: 'enable-server', serverId: p.providerId.replace(/^mcp:/, '').split('.')[0] ?? p.providerId })) });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// (3) policy-denied. Two predicates, and only these two: the curated vocabulary's `permissionHint`
|
|
284
|
+
// for a curated id, and — for an `mcp:` provider — the run's per-server grant. An MCP provider whose
|
|
285
|
+
// server is not granted is NOT eligible: reporting it as available would have the planner build a
|
|
286
|
+
// plan around a tool the executor then refuses.
|
|
287
|
+
const hint = curated(capabilityId)?.permissionHint;
|
|
288
|
+
const perms = opts.permissions;
|
|
289
|
+
if (hint !== undefined && perms !== undefined && !permitted(hint, perms)) {
|
|
290
|
+
gaps.push({ capabilityId, reason: 'permission', providers: pool, suggestions: [{ kind: 'grant-permission', policyKey: hintToPolicyKey(hint) }] });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (perms !== undefined) {
|
|
294
|
+
const granted = pool.filter((p) => p.source !== 'mcp' || mcpGranted(p, perms));
|
|
295
|
+
if (granted.length === 0) {
|
|
296
|
+
const server = mcpServerOf(pool[0]);
|
|
297
|
+
gaps.push({ capabilityId, reason: 'permission', providers: pool, suggestions: [{ kind: 'grant-permission', policyKey: `permissions.mcp.servers.${server}` }] });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
pool = granted;
|
|
301
|
+
}
|
|
302
|
+
// (4) unavailable
|
|
303
|
+
const usable = pool.filter((p) => p.availability === 'available' || p.availability === 'unknown');
|
|
304
|
+
if (usable.length === 0) {
|
|
305
|
+
gaps.push({ capabilityId, reason: 'unavailable', providers: pool, suggestions: [{ kind: 'register', hint: `'${capabilityId}' has provider(s) that are not usable right now (a required tool is not registered, or a server is unreachable)` }] });
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
pool = usable;
|
|
309
|
+
// (5) user pin → (6) evidence rank → (7) source order → (8) lexicographic providerId
|
|
310
|
+
const pinned = this.config.pins?.[capabilityId];
|
|
311
|
+
const pin = pinned ? pool.find((p) => p.providerId === pinned) : undefined;
|
|
312
|
+
const ordered = [...pool].sort(compareProviders);
|
|
313
|
+
const chosen = pin ?? ordered[0];
|
|
314
|
+
satisfied.push({ capabilityId, chosen, alternates: ordered.filter((p) => p !== chosen) });
|
|
315
|
+
}
|
|
316
|
+
return { ok: gaps.length === 0, satisfied, gaps };
|
|
317
|
+
}
|
|
318
|
+
/** Build the user-facing report from a resolution (one shape; gaps carry the actionable suggestions). */
|
|
319
|
+
report(required, opts = {}) {
|
|
320
|
+
return capabilityReportFrom(required, this.resolve(required, opts).gaps, (id) => this.providersOf(id));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
324
|
+
/** The MCP server a provider belongs to: `mcp:<server>.<tool>` → `<server>`. */
|
|
325
|
+
function mcpServerOf(p) {
|
|
326
|
+
return p.providerId.replace(/^mcp:/, '').split('.')[0] ?? p.providerId;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Whether the run's permissions admit this MCP provider at all. A write-like tool needs `full`, a
|
|
330
|
+
* read-only one needs `read` — the same split the tool adapter enforces at execution, so resolution and
|
|
331
|
+
* execution agree. Own-property checked, so an unconfigured server cannot inherit a grant.
|
|
332
|
+
*/
|
|
333
|
+
function mcpGranted(p, perms) {
|
|
334
|
+
const servers = perms.mcp.servers;
|
|
335
|
+
const server = mcpServerOf(p);
|
|
336
|
+
const grant = Object.prototype.hasOwnProperty.call(servers, server) ? servers[server] : 'off';
|
|
337
|
+
if (grant === 'full')
|
|
338
|
+
return true;
|
|
339
|
+
if (grant !== 'read')
|
|
340
|
+
return false;
|
|
341
|
+
return !p.effects.includes('write');
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Build the ONE `MissingCapabilityReport` shape from an already-resolved gap set. Factored out of
|
|
345
|
+
* `report()` (Phase 3.3) so the run path and the public reporter cannot diverge: before this, the
|
|
346
|
+
* Runtime hand-built the same shape without `dedupeSuggestions` and listed only gapped ids in
|
|
347
|
+
* `required`. Every construction of this type now goes through here.
|
|
348
|
+
*/
|
|
349
|
+
export function capabilityReportFrom(required, gaps, providersOf) {
|
|
350
|
+
const gapById = new Map(gaps.map((g) => [g.capabilityId, g]));
|
|
351
|
+
return {
|
|
352
|
+
required: required.map((id) => {
|
|
353
|
+
const gap = gapById.get(id);
|
|
354
|
+
return {
|
|
355
|
+
capabilityId: id,
|
|
356
|
+
status: gap ? gap.reason : 'available',
|
|
357
|
+
providers: (gap ? gap.providers : providersOf(id)).map((p) => p.providerId),
|
|
358
|
+
};
|
|
359
|
+
}),
|
|
360
|
+
gaps,
|
|
361
|
+
suggestions: dedupeSuggestions(gaps.flatMap((g) => g.suggestions)),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function permitted(hint, p) {
|
|
365
|
+
switch (hint) {
|
|
366
|
+
case 'filesystem.read':
|
|
367
|
+
return p.filesystem.read;
|
|
368
|
+
case 'filesystem.write':
|
|
369
|
+
return p.filesystem.write;
|
|
370
|
+
case 'shell.enabled':
|
|
371
|
+
return p.shell.enabled;
|
|
372
|
+
case 'git.commit':
|
|
373
|
+
return p.git.commit;
|
|
374
|
+
case 'git.push':
|
|
375
|
+
return p.git.push;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function hintToPolicyKey(hint) {
|
|
379
|
+
switch (hint) {
|
|
380
|
+
case 'filesystem.read':
|
|
381
|
+
return 'permissions.fsRead';
|
|
382
|
+
case 'filesystem.write':
|
|
383
|
+
return 'permissions.fsWrite';
|
|
384
|
+
case 'shell.enabled':
|
|
385
|
+
return 'permissions.shell';
|
|
386
|
+
case 'git.commit':
|
|
387
|
+
return 'permissions.gitCommit';
|
|
388
|
+
case 'git.push':
|
|
389
|
+
return 'permissions.gitPush';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Total order: evidence rank desc → source order → providerId lexicographic (fully deterministic). */
|
|
393
|
+
function compareProviders(a, b) {
|
|
394
|
+
const ev = rankOf(b.verification) - rankOf(a.verification);
|
|
395
|
+
if (ev !== 0)
|
|
396
|
+
return ev;
|
|
397
|
+
const src = SOURCE_ORDER.indexOf(a.source) - SOURCE_ORDER.indexOf(b.source);
|
|
398
|
+
if (src !== 0)
|
|
399
|
+
return src;
|
|
400
|
+
return a.providerId < b.providerId ? -1 : a.providerId > b.providerId ? 1 : 0;
|
|
401
|
+
}
|
|
402
|
+
function dedupeSuggestions(list) {
|
|
403
|
+
const seen = new Set();
|
|
404
|
+
const out = [];
|
|
405
|
+
for (const s of list) {
|
|
406
|
+
const key = JSON.stringify(s);
|
|
407
|
+
if (seen.has(key))
|
|
408
|
+
continue;
|
|
409
|
+
seen.add(key);
|
|
410
|
+
out.push(s);
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The curated ACTION-capability vocabulary (Phase 3.1). These are the canonical, cross-provider ids a
|
|
3
|
+
* goal can require — `read_file`, `run_tests`, `read_repository` — independent of which tool, skill, or
|
|
4
|
+
* MCP server provides them.
|
|
5
|
+
*
|
|
6
|
+
* Curated metadata is CANONICAL: a declaration may add itself as a provider of a curated id, but can
|
|
7
|
+
* never override the id's name/description/effects. Bare curated ids are claimable only by (a) the
|
|
8
|
+
* in-tree builtin declarations registered at Runtime construction and (b) operator config aliases —
|
|
9
|
+
* every other declared bare id is auto-namespaced to its source, so third-party code can never squat a
|
|
10
|
+
* curated name (see `namespacedId` in ./capability.ts).
|
|
11
|
+
*
|
|
12
|
+
* `permissionHint` names the ResolvedPermissions predicate a provider of this capability will need. It
|
|
13
|
+
* is used ONLY to drop policy-denied providers during resolution and to render an actionable "grant
|
|
14
|
+
* this" suggestion — it is never authorization: the selected tool's own permission check is what
|
|
15
|
+
* actually gates execution.
|
|
16
|
+
*/
|
|
17
|
+
import type { ActionCapabilityEffect } from './capability.js';
|
|
18
|
+
/** The permission predicates a curated capability can depend on (keys into ResolvedPermissions). */
|
|
19
|
+
export type PermissionHint = 'filesystem.read' | 'filesystem.write' | 'shell.enabled' | 'git.commit' | 'git.push';
|
|
20
|
+
export interface CuratedCapability {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
effects: ActionCapabilityEffect[];
|
|
25
|
+
permissionHint?: PermissionHint;
|
|
26
|
+
}
|
|
27
|
+
/** The shipped vocabulary. Adding an entry is an additive, documented decision (see DECISIONS.md). */
|
|
28
|
+
export declare const CURATED_VOCABULARY: readonly CuratedCapability[];
|
|
29
|
+
/** The curated entry for an id, or undefined when the id is not curated. */
|
|
30
|
+
export declare function curated(id: string): CuratedCapability | undefined;
|
|
31
|
+
/** Whether an id belongs to the curated vocabulary (the membership oracle — "curated" is never a guess). */
|
|
32
|
+
export declare function isCurated(id: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The curated ACTION-capability vocabulary (Phase 3.1). These are the canonical, cross-provider ids a
|
|
3
|
+
* goal can require — `read_file`, `run_tests`, `read_repository` — independent of which tool, skill, or
|
|
4
|
+
* MCP server provides them.
|
|
5
|
+
*
|
|
6
|
+
* Curated metadata is CANONICAL: a declaration may add itself as a provider of a curated id, but can
|
|
7
|
+
* never override the id's name/description/effects. Bare curated ids are claimable only by (a) the
|
|
8
|
+
* in-tree builtin declarations registered at Runtime construction and (b) operator config aliases —
|
|
9
|
+
* every other declared bare id is auto-namespaced to its source, so third-party code can never squat a
|
|
10
|
+
* curated name (see `namespacedId` in ./capability.ts).
|
|
11
|
+
*
|
|
12
|
+
* `permissionHint` names the ResolvedPermissions predicate a provider of this capability will need. It
|
|
13
|
+
* is used ONLY to drop policy-denied providers during resolution and to render an actionable "grant
|
|
14
|
+
* this" suggestion — it is never authorization: the selected tool's own permission check is what
|
|
15
|
+
* actually gates execution.
|
|
16
|
+
*/
|
|
17
|
+
/** The shipped vocabulary. Adding an entry is an additive, documented decision (see DECISIONS.md). */
|
|
18
|
+
export const CURATED_VOCABULARY = [
|
|
19
|
+
{ id: 'read_file', name: 'Read a file', description: 'Read the contents of a workspace file.', effects: ['read'], permissionHint: 'filesystem.read' },
|
|
20
|
+
{ id: 'write_file', name: 'Write a file', description: 'Create or modify a workspace file.', effects: ['write'], permissionHint: 'filesystem.write' },
|
|
21
|
+
{ id: 'read_repository', name: 'Read the repository', description: 'Inspect repository structure, history, or metadata.', effects: ['read'], permissionHint: 'filesystem.read' },
|
|
22
|
+
{ id: 'run_command', name: 'Run a command', description: 'Execute a shell command in the workspace.', effects: ['write'], permissionHint: 'shell.enabled' },
|
|
23
|
+
{ id: 'run_tests', name: 'Run tests', description: 'Execute the project test suite or a subset of it.', effects: ['write'], permissionHint: 'shell.enabled' },
|
|
24
|
+
{ id: 'commit_changes', name: 'Commit changes', description: 'Record changes to the repository history.', effects: ['write'], permissionHint: 'git.commit' },
|
|
25
|
+
];
|
|
26
|
+
const BY_ID = new Map(CURATED_VOCABULARY.map((c) => [c.id, c]));
|
|
27
|
+
/** The curated entry for an id, or undefined when the id is not curated. */
|
|
28
|
+
export function curated(id) {
|
|
29
|
+
return BY_ID.get(id);
|
|
30
|
+
}
|
|
31
|
+
/** Whether an id belongs to the curated vocabulary (the membership oracle — "curated" is never a guess). */
|
|
32
|
+
export function isCurated(id) {
|
|
33
|
+
return BY_ID.has(id);
|
|
34
|
+
}
|
package/dist/cli/cli.js
CHANGED
|
@@ -17,10 +17,11 @@ import { setupCommand } from './commands/setup.js';
|
|
|
17
17
|
import { infoCommand } from './commands/info.js';
|
|
18
18
|
import { cleanupCommand } from './commands/cleanup.js';
|
|
19
19
|
import { skillsCommand } from './commands/skills.js';
|
|
20
|
+
import { mcpCommand, mcpAddCommand, mcpRemoveCommand, mcpEnableCommand, mcpTestCommand } from './commands/mcp.js';
|
|
20
21
|
import { startRepl } from './interactive/repl.js';
|
|
21
22
|
import { printError } from './render.js';
|
|
22
23
|
const program = new Command();
|
|
23
|
-
program.name('ai-runtime').description('Universal, provider-agnostic AI Runtime & Orchestration Platform').version('
|
|
24
|
+
program.name('ai-runtime').description('Universal, provider-agnostic AI Runtime & Orchestration Platform').version('2.7.0');
|
|
24
25
|
const configOpt = ['-c, --config <path>', 'path to an ai-runtime config file'];
|
|
25
26
|
// Bare `ai-runtime` (no subcommand) opens the interactive terminal. `allowExcessArguments(false)` keeps
|
|
26
27
|
// a mistyped subcommand (e.g. `ai-runtime porviders`) failing fast instead of silently opening the REPL.
|
|
@@ -35,6 +36,8 @@ program
|
|
|
35
36
|
.option(...configOpt)
|
|
36
37
|
.option('--json', 'print the full RuntimeResult as JSON')
|
|
37
38
|
.option('--dry-run', 'plan and report what would happen without making any changes')
|
|
39
|
+
.option('--stream', 'stream the answer token-by-token (text output only)')
|
|
40
|
+
.option('--partial', 'if over the call budget, run the phases that fit and pause (vs. notify-and-wait)')
|
|
38
41
|
.action((input, o) => runCommand(input, o));
|
|
39
42
|
program
|
|
40
43
|
.command('executions')
|
|
@@ -59,11 +62,57 @@ program
|
|
|
59
62
|
.action((o) => setupCommand(o));
|
|
60
63
|
program
|
|
61
64
|
.command('skills')
|
|
62
|
-
.description('List available skills; --discover scans the workspace for more
|
|
65
|
+
.description('List available skills; --discover scans the workspace for more; --scaffold drafts a new one')
|
|
63
66
|
.option(...configOpt)
|
|
64
67
|
.option('--discover', 'scan the workspace for skill files elsewhere and report them')
|
|
68
|
+
.option('--scaffold <goal>', 'draft a *.skill.yaml manifest for a goal (asks before saving)')
|
|
69
|
+
.option('-y, --yes', 'with --scaffold, save the drafted skill without prompting')
|
|
65
70
|
.option('--json', 'print as JSON')
|
|
66
71
|
.action((o) => skillsCommand(o));
|
|
72
|
+
// `mcp` — MCP servers. A subcommand-bearing group whose bare form lists, and `mcp <id>` details one.
|
|
73
|
+
// Every path releases stdio children on completion (Runtime.close), so a one-shot command never orphans.
|
|
74
|
+
const mcp = program.command('mcp').description('Inspect and manage MCP servers (tools from external servers)');
|
|
75
|
+
mcp
|
|
76
|
+
.argument('[id]', 'a server id to show in detail')
|
|
77
|
+
.option(...configOpt)
|
|
78
|
+
.option('--json', 'print as JSON')
|
|
79
|
+
.action((id, o) => mcpCommand(id, o));
|
|
80
|
+
mcp
|
|
81
|
+
.command('add')
|
|
82
|
+
.argument('<id>', 'a short id for the server (lowercase, kebab/snake)')
|
|
83
|
+
.description('Add a server (stored locally — your config file is never modified)')
|
|
84
|
+
.option('--command <argv>', 'stdio server command line, e.g. "npx -y some-mcp-server"')
|
|
85
|
+
.option('--url <url>', 'streamable-HTTP endpoint')
|
|
86
|
+
.option('--token-env <NAME>', 'env-var NAME holding the credential (never the value itself)')
|
|
87
|
+
.option('--timeout <ms>', 'per-call timeout override')
|
|
88
|
+
.option(...configOpt)
|
|
89
|
+
.option('--json', 'print as JSON')
|
|
90
|
+
.action((id, o) => mcpAddCommand(id, o));
|
|
91
|
+
mcp
|
|
92
|
+
.command('remove')
|
|
93
|
+
.argument('<id>', 'server id')
|
|
94
|
+
.description('Remove a server that was added with `mcp add`')
|
|
95
|
+
.option(...configOpt)
|
|
96
|
+
.action((id, o) => mcpRemoveCommand(id, o));
|
|
97
|
+
mcp
|
|
98
|
+
.command('enable')
|
|
99
|
+
.argument('<id>', 'server id')
|
|
100
|
+
.description('Enable a disabled server')
|
|
101
|
+
.option(...configOpt)
|
|
102
|
+
.action((id, o) => mcpEnableCommand(id, true, o));
|
|
103
|
+
mcp
|
|
104
|
+
.command('disable')
|
|
105
|
+
.argument('<id>', 'server id')
|
|
106
|
+
.description('Disable a server without removing it')
|
|
107
|
+
.option(...configOpt)
|
|
108
|
+
.action((id, o) => mcpEnableCommand(id, false, o));
|
|
109
|
+
mcp
|
|
110
|
+
.command('test')
|
|
111
|
+
.argument('<id>', 'server id')
|
|
112
|
+
.description('Connect, handshake, list tools, and ping — reporting exactly what happened')
|
|
113
|
+
.option(...configOpt)
|
|
114
|
+
.option('--json', 'print as JSON')
|
|
115
|
+
.action((id, o) => mcpTestCommand(id, o));
|
|
67
116
|
program
|
|
68
117
|
.command('info')
|
|
69
118
|
.description('Summarize the runtime: workspace, providers, skills, tools, memory, learning, storage')
|
|
@@ -89,13 +138,16 @@ program
|
|
|
89
138
|
.description('List known models across providers')
|
|
90
139
|
.option(...configOpt)
|
|
91
140
|
.option('--json', 'print as JSON')
|
|
141
|
+
.option('--profile', 'show the models.md per-mode/per-task routing profile instead')
|
|
92
142
|
.action((o) => modelsCommand(o));
|
|
93
143
|
program
|
|
94
144
|
.command('capabilities')
|
|
95
|
-
.
|
|
145
|
+
.argument('[goal]', 'with --actions: which action capabilities this goal needs (offline — no model call)')
|
|
146
|
+
.description('Show the model capability matrix; --actions shows action capabilities (read_file, run_tests, …)')
|
|
96
147
|
.option(...configOpt)
|
|
148
|
+
.option('--actions', 'show ACTION capabilities (what the runtime can do) instead of the model matrix')
|
|
97
149
|
.option('--json', 'print as JSON')
|
|
98
|
-
.action((o) => capabilitiesCommand(o));
|
|
150
|
+
.action((goal, o) => capabilitiesCommand(goal, o));
|
|
99
151
|
program
|
|
100
152
|
.command('doctor')
|
|
101
153
|
.description('Diagnose the runtime: config, .env, providers, models, store, skills, tools, permissions, git')
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Nothing is deleted without confirmation unless `--yes`; `--dry-run` only reports the plan. Integrity
|
|
5
5
|
* issues are reported, never auto-"repaired" destructively.
|
|
6
6
|
*/
|
|
7
|
-
import { Runtime } from '../../runtime/runtime.js';
|
|
8
7
|
import { TERMINAL } from '../../executions/execution.js';
|
|
9
8
|
import { confirm } from '../prompt.js';
|
|
9
|
+
import { withRuntime } from '../runtimeSession.js';
|
|
10
10
|
import { print } from '../render.js';
|
|
11
11
|
const DAY_MS = 86_400_000;
|
|
12
12
|
function numEnv(v) {
|
|
@@ -67,30 +67,32 @@ function renderPlan(plan) {
|
|
|
67
67
|
return lines;
|
|
68
68
|
}
|
|
69
69
|
export async function cleanupCommand(opts) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
70
|
+
// Every Runtime-based command releases its Runtime (MCP children included) on every path.
|
|
71
|
+
return withRuntime(opts, async (rt) => {
|
|
72
|
+
if (!rt.store.enabled) {
|
|
73
|
+
print('storage is disabled (stateless mode) — nothing to clean.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const plan = planCleanup(rt, Date.now());
|
|
77
|
+
if (opts.json)
|
|
78
|
+
return print(JSON.stringify(plan, null, 2));
|
|
79
|
+
for (const line of renderPlan(plan))
|
|
80
|
+
print(line);
|
|
81
|
+
const removable = toRemove(plan);
|
|
82
|
+
if (removable === 0) {
|
|
83
|
+
print('\nNothing to remove.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (opts.dryRun) {
|
|
87
|
+
print('\n(dry run — nothing was removed)');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const ok = opts.yes || (await confirm(`\nRemove ${removable} item(s)?`));
|
|
91
|
+
if (!ok) {
|
|
92
|
+
print('cancelled — nothing was removed.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const done = applyCleanup(rt, plan);
|
|
96
|
+
print(`\nremoved: ${done.conversations} conversation(s), ${done.executions} execution(s), ${done.artifacts} artifact(s), ${done.cache} cache entr${done.cache === 1 ? 'y' : 'ies'}.`);
|
|
97
|
+
});
|
|
96
98
|
}
|