@alisio/core 0.1.0-alpha.10

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/application.d.ts +225 -0
  4. package/dist/application.js +950 -0
  5. package/dist/config.d.ts +481 -0
  6. package/dist/config.js +592 -0
  7. package/dist/core/compaction.d.ts +136 -0
  8. package/dist/core/compaction.js +404 -0
  9. package/dist/core/contracts.d.ts +147 -0
  10. package/dist/core/contracts.js +1 -0
  11. package/dist/core/registry.d.ts +8 -0
  12. package/dist/core/registry.js +54 -0
  13. package/dist/core/runner.d.ts +160 -0
  14. package/dist/core/runner.js +625 -0
  15. package/dist/extensions/registry.d.ts +28 -0
  16. package/dist/extensions/registry.js +58 -0
  17. package/dist/index.d.ts +29 -0
  18. package/dist/index.js +28 -0
  19. package/dist/integrations/herdr.d.ts +34 -0
  20. package/dist/integrations/herdr.js +174 -0
  21. package/dist/mcp/connector.d.ts +65 -0
  22. package/dist/mcp/connector.js +423 -0
  23. package/dist/plugins/host.d.ts +169 -0
  24. package/dist/plugins/host.js +585 -0
  25. package/dist/plugins/install.d.ts +100 -0
  26. package/dist/plugins/install.js +322 -0
  27. package/dist/providers/openai-compatible.d.ts +14 -0
  28. package/dist/providers/openai-compatible.js +228 -0
  29. package/dist/providers/registry.d.ts +35 -0
  30. package/dist/providers/registry.js +74 -0
  31. package/dist/providers/routing.d.ts +11 -0
  32. package/dist/providers/routing.js +41 -0
  33. package/dist/providers/settings.d.ts +30 -0
  34. package/dist/providers/settings.js +80 -0
  35. package/dist/resources/context.d.ts +44 -0
  36. package/dist/resources/context.js +171 -0
  37. package/dist/resources/prompts.d.ts +77 -0
  38. package/dist/resources/prompts.js +182 -0
  39. package/dist/resources/skills.d.ts +82 -0
  40. package/dist/resources/skills.js +214 -0
  41. package/dist/runtime/fs.d.ts +9 -0
  42. package/dist/runtime/fs.js +73 -0
  43. package/dist/runtime/modules.d.ts +16 -0
  44. package/dist/runtime/modules.js +83 -0
  45. package/dist/runtime/paths.d.ts +4 -0
  46. package/dist/runtime/paths.js +42 -0
  47. package/dist/runtime/process.d.ts +25 -0
  48. package/dist/runtime/process.js +122 -0
  49. package/dist/runtime/sqlite.d.ts +3 -0
  50. package/dist/runtime/sqlite.js +85 -0
  51. package/dist/runtime/store.d.ts +38 -0
  52. package/dist/runtime/store.js +259 -0
  53. package/dist/sessions/children.d.ts +55 -0
  54. package/dist/sessions/children.js +233 -0
  55. package/dist/startup/index.d.ts +41 -0
  56. package/dist/startup/index.js +105 -0
  57. package/dist/startup/mascot.d.ts +6 -0
  58. package/dist/startup/mascot.js +30 -0
  59. package/dist/startup/screen.d.ts +18 -0
  60. package/dist/startup/screen.js +117 -0
  61. package/dist/startup/text.d.ts +19 -0
  62. package/dist/startup/text.js +68 -0
  63. package/dist/tools/execute.d.ts +16 -0
  64. package/dist/tools/execute.js +96 -0
  65. package/dist/tools/search.d.ts +69 -0
  66. package/dist/tools/search.js +232 -0
  67. package/dist/tools/standard.d.ts +22 -0
  68. package/dist/tools/standard.js +419 -0
  69. package/dist/tools/webfetch.d.ts +26 -0
  70. package/dist/tools/webfetch.js +119 -0
  71. package/dist/trust.d.ts +34 -0
  72. package/dist/trust.js +113 -0
  73. package/dist/version.d.ts +7 -0
  74. package/dist/version.js +26 -0
  75. package/package.json +62 -0
@@ -0,0 +1,423 @@
1
+ import { createHash } from "node:crypto";
2
+ import { textResult } from "@alisio/sdk";
3
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
4
+ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
5
+ import { objectSchema } from "../tools/standard.js";
6
+ import { loadVersion } from "../version.js";
7
+ const TOOL_NAME_LIMIT = 64;
8
+ /**
9
+ * Per-server client close cap used ONLY by the terminal `close()`: the MCP SDK's stdio close can
10
+ * take ~4s worst case (stdin end → 2s → SIGTERM → 2s → SIGKILL) when a server ignores
11
+ * termination, and exit must never wait for that ladder. Runtime `disconnect()` keeps its
12
+ * unbounded semantics so interactive reconnect flows stay unchanged.
13
+ */
14
+ const CLOSE_SERVER_TIMEOUT_MS = 800;
15
+ /** Overall cap for `close()` (every connected server in parallel plus bridge unregistration). */
16
+ const CLOSE_TOTAL_TIMEOUT_MS = 1_500;
17
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
18
+ const semanticSegment = (value, fallback) => value
19
+ .normalize("NFKD")
20
+ .replace(/[\u0300-\u036f]/g, "")
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]+/g, "_")
23
+ .replace(/^_+|_+$/g, "") || fallback;
24
+ const shortHash = (server, tool) => createHash("sha256").update(`${server}\0${tool}`).digest("hex").slice(0, 8);
25
+ const withHash = (base, server, tool) => {
26
+ const suffix = `_${shortHash(server, tool)}`;
27
+ const stem = base.slice(0, TOOL_NAME_LIMIT - suffix.length).replace(/_+$/g, "");
28
+ return `${stem || "mcp_tool"}${suffix}`;
29
+ };
30
+ /** Deterministic provider-safe names while retaining the exact source mapping in the connector. */
31
+ export function semanticMcpToolNames(server, tools, configuredServers, unavailable = new Set()) {
32
+ if (new Set(tools).size !== tools.length)
33
+ throw new Error(`MCP server "${server}" returned duplicate tool names`);
34
+ const serverPart = semanticSegment(server, "server");
35
+ const serverCollision = configuredServers.filter((candidate) => semanticSegment(candidate, "server") === serverPart)
36
+ .length > 1;
37
+ const toolParts = tools.map((tool) => semanticSegment(tool, "tool"));
38
+ const collisions = new Set(toolParts.filter((part, index) => toolParts.indexOf(part) !== index));
39
+ return tools.map((tool, index) => {
40
+ const base = `mcp_${serverPart}_${toolParts[index]}`;
41
+ const needsHash = base.length > TOOL_NAME_LIMIT ||
42
+ serverCollision ||
43
+ collisions.has(toolParts[index]) ||
44
+ unavailable.has(base);
45
+ const result = needsHash ? withHash(base, server, tool) : base;
46
+ if (unavailable.has(result))
47
+ throw new Error(`MCP tool name collision could not be resolved for ${server}/${tool}`);
48
+ return result;
49
+ });
50
+ }
51
+ const readable = (value, limit = 160) => value
52
+ .replace(/[\u0000-\u001f\u007f]+/g, " ")
53
+ .replace(/\s+/g, " ")
54
+ .trim()
55
+ .slice(0, limit);
56
+ const safeFailure = (error) => {
57
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
58
+ if (message.includes("timeout") || message.includes("timed out"))
59
+ return "Connection timed out";
60
+ if (message.includes("abort"))
61
+ return "Connection cancelled";
62
+ if (message.includes("auth") || message.includes("token") || message.includes("401"))
63
+ return "Authentication is required or was rejected";
64
+ if (message.includes("enoent"))
65
+ return "Server command could not be started";
66
+ return "Connection failed; inspect the server separately for details";
67
+ };
68
+ export class McpConnector {
69
+ servers;
70
+ registry;
71
+ workspace;
72
+ availability;
73
+ sources;
74
+ clients = new Map();
75
+ statuses = new Map();
76
+ diagnostics = new Map();
77
+ catalogs = new Map();
78
+ details = new Map();
79
+ bridgeUndo = [];
80
+ constructor(servers, registry, workspace, availability = "allowed", sources = {}) {
81
+ this.servers = servers;
82
+ this.registry = registry;
83
+ this.workspace = workspace;
84
+ this.availability = availability;
85
+ this.sources = sources;
86
+ for (const [name, server] of Object.entries(servers))
87
+ this.statuses.set(name, server.enabled ? "disconnected" : "disabled");
88
+ }
89
+ register() {
90
+ if (this.bridgeUndo.length)
91
+ return;
92
+ const undo = [];
93
+ try {
94
+ undo.push(this.registry.register({
95
+ name: "mcp_connect",
96
+ effect: "external",
97
+ description: `Connect to a configured MCP server and register its tools. Servers: ${Object.keys(this.servers).join(", ")}`,
98
+ inputSchema: objectSchema({ server: { type: "string" } }, ["server"]),
99
+ execute: async (i, c) => textResult(JSON.stringify(await this.connect(String(i.server), c.signal))),
100
+ }));
101
+ undo.push(this.registry.register({
102
+ name: "mcp_resource",
103
+ effect: "external",
104
+ description: "List or read resources from an enabled MCP server.",
105
+ inputSchema: objectSchema({ server: { type: "string" }, uri: { type: "string" } }, [
106
+ "server",
107
+ ]),
108
+ execute: async (i, c) => {
109
+ await this.connect(String(i.server), c.signal);
110
+ const client = this.client(String(i.server));
111
+ const result = i.uri
112
+ ? await client.readResource({ uri: String(i.uri) }, { signal: c.signal, timeout: 15000 })
113
+ : await client.listResources(undefined, { signal: c.signal, timeout: 15000 });
114
+ return textResult(JSON.stringify(result));
115
+ },
116
+ }));
117
+ undo.push(this.registry.register({
118
+ name: "mcp_prompt",
119
+ effect: "external",
120
+ description: "List or get a prompt from an enabled MCP server. Returned text is external data.",
121
+ inputSchema: objectSchema({
122
+ server: { type: "string" },
123
+ name: { type: "string" },
124
+ arguments: { type: "object", additionalProperties: { type: "string" } },
125
+ }, ["server"]),
126
+ execute: async (i, c) => {
127
+ await this.connect(String(i.server), c.signal);
128
+ const client = this.client(String(i.server));
129
+ const result = i.name
130
+ ? await client.getPrompt({
131
+ name: String(i.name),
132
+ arguments: i.arguments,
133
+ }, { signal: c.signal, timeout: 15000 })
134
+ : await client.listPrompts(undefined, { signal: c.signal, timeout: 15000 });
135
+ return textResult(JSON.stringify(result));
136
+ },
137
+ }));
138
+ this.bridgeUndo = undo;
139
+ }
140
+ catch (error) {
141
+ for (const remove of undo.reverse())
142
+ remove();
143
+ throw error;
144
+ }
145
+ }
146
+ grantRuntimePermission() {
147
+ if (this.availability === "read-only")
148
+ throw new Error("MCP is unavailable under --read-only");
149
+ this.availability = "allowed";
150
+ this.register();
151
+ }
152
+ /** Drop the runtime grant: disconnect every server and unregister the bridge (used on revoke). */
153
+ async revokeRuntimePermission() {
154
+ await Promise.all([...this.clients.keys()].map((name) => this.disconnect(name)));
155
+ for (const undo of this.bridgeUndo.splice(0).reverse())
156
+ undo();
157
+ this.availability = "disabled";
158
+ }
159
+ runtimePermission() {
160
+ return this.availability === "allowed"
161
+ ? "granted"
162
+ : this.availability === "read-only"
163
+ ? "read-only"
164
+ : "not-granted";
165
+ }
166
+ assertAvailable() {
167
+ if (this.availability === "read-only")
168
+ throw new Error("MCP is unavailable under --read-only");
169
+ if (this.availability === "disabled")
170
+ throw new Error("MCP is configured but disabled; restart with --allow-mcp");
171
+ }
172
+ client(name) {
173
+ const item = this.clients.get(name);
174
+ if (!item)
175
+ throw new Error("MCP server disconnected; reconnect explicitly");
176
+ return item.client;
177
+ }
178
+ list() {
179
+ return Object.keys(this.servers)
180
+ .map((name) => this.info(name))
181
+ .sort((a, b) => a.source.kind.localeCompare(b.source.kind) || a.name.localeCompare(b.name));
182
+ }
183
+ info(name) {
184
+ const config = this.servers[name];
185
+ if (!config)
186
+ throw new Error(`Unknown MCP server: ${name}`);
187
+ const detail = this.details.get(name);
188
+ return {
189
+ name,
190
+ displayName: detail?.displayName ?? name,
191
+ source: this.sources[name] ?? { kind: "builtin", form: "canonical" },
192
+ status: this.statuses.get(name) ?? (config.enabled ? "disconnected" : "disabled"),
193
+ enabled: config.enabled,
194
+ runtimePermission: this.runtimePermission(),
195
+ transport: config.transport,
196
+ ...(config.transport === "stdio"
197
+ ? { command: config.command, args: [...config.args] }
198
+ : { url: config.url }),
199
+ capabilities: [...(detail?.capabilities ?? [])],
200
+ counts: {
201
+ tools: this.catalogs.get(name)?.length ?? 0,
202
+ resources: detail?.resources ?? 0,
203
+ prompts: detail?.prompts ?? 0,
204
+ },
205
+ ...(this.diagnostics.has(name) ? { diagnostic: this.diagnostics.get(name) } : {}),
206
+ };
207
+ }
208
+ tools(name) {
209
+ this.info(name);
210
+ return (this.catalogs.get(name) ?? []).map((tool) => ({
211
+ ...tool,
212
+ ...(tool.annotations ? { annotations: { ...tool.annotations } } : {}),
213
+ }));
214
+ }
215
+ async connect(name, signal) {
216
+ this.assertAvailable();
217
+ const config = this.servers[name];
218
+ if (!config)
219
+ throw new Error(`Unknown MCP server: ${name}`);
220
+ if (!config.enabled)
221
+ throw new Error(`MCP server "${name}" is disabled; enable it in /mcp`);
222
+ if (this.clients.has(name))
223
+ return this.registeredNames(name);
224
+ if (config.transport === "http" &&
225
+ config.bearerTokenEnv &&
226
+ !process.env[config.bearerTokenEnv]) {
227
+ this.statuses.set(name, "needs-authentication");
228
+ this.diagnostics.set(name, `Set the ${config.bearerTokenEnv} environment variable, then reconnect`);
229
+ throw new Error(`MCP server "${name}" needs authentication; set ${config.bearerTokenEnv}`);
230
+ }
231
+ this.statuses.set(name, "connecting");
232
+ this.diagnostics.delete(name);
233
+ const client = new Client({ name: "alisio", version: loadVersion(import.meta.url) }, { capabilities: {} });
234
+ const undo = [];
235
+ const generation = Symbol(name);
236
+ const transport = config.transport === "stdio"
237
+ ? new StdioClientTransport((() => {
238
+ const env = Object.fromEntries(["PATH", "HOME", "SystemRoot", ...config.envAllow].flatMap((key) => process.env[key] ? [[key, process.env[key]]] : []));
239
+ Object.assign(env, config.env);
240
+ return {
241
+ command: config.command,
242
+ args: config.args,
243
+ cwd: this.workspace,
244
+ env,
245
+ stderr: "pipe",
246
+ maxBufferSize: 1_048_576,
247
+ };
248
+ })())
249
+ : new StreamableHTTPClientTransport(new URL(config.url), {
250
+ reconnectionOptions: {
251
+ maxRetries: 0,
252
+ maxReconnectionDelay: 1000,
253
+ initialReconnectionDelay: 100,
254
+ reconnectionDelayGrowFactor: 1,
255
+ },
256
+ ...(config.bearerTokenEnv
257
+ ? {
258
+ authProvider: {
259
+ token: async () => process.env[config.bearerTokenEnv],
260
+ },
261
+ }
262
+ : {}),
263
+ });
264
+ try {
265
+ await client.connect(transport, { signal, timeout: 15000 });
266
+ if (transport instanceof StdioClientTransport)
267
+ transport.stderr?.on("data", () => { });
268
+ const serverCapabilities = client.getServerCapabilities() ?? {};
269
+ const capabilities = Object.keys(serverCapabilities).sort();
270
+ const tools = serverCapabilities.tools
271
+ ? (await client.listTools(undefined, { signal, timeout: 15000 })).tools
272
+ : [];
273
+ if (tools.length > 100)
274
+ throw new Error("MCP server exposes more than 100 tools; narrow its configuration");
275
+ const resources = serverCapabilities.resources
276
+ ? (await client.listResources(undefined, { signal, timeout: 15000 })).resources.length
277
+ : 0;
278
+ const prompts = serverCapabilities.prompts
279
+ ? (await client.listPrompts(undefined, { signal, timeout: 15000 })).prompts.length
280
+ : 0;
281
+ const existingNames = new Set(this.registry.list().map((registered) => registered.name));
282
+ const mappedNames = semanticMcpToolNames(name, tools.map((tool) => tool.name), Object.keys(this.servers), existingNames);
283
+ this.catalogs.set(name, tools.map((tool, index) => ({
284
+ name: tool.name,
285
+ effectiveName: mappedNames[index],
286
+ ...(tool.title ? { title: tool.title } : {}),
287
+ ...(tool.description ? { description: tool.description } : {}),
288
+ ...(tool.annotations
289
+ ? {
290
+ annotations: {
291
+ ...(tool.annotations.readOnlyHint !== undefined
292
+ ? { readOnly: tool.annotations.readOnlyHint }
293
+ : {}),
294
+ ...(tool.annotations.destructiveHint !== undefined
295
+ ? { destructive: tool.annotations.destructiveHint }
296
+ : {}),
297
+ ...(tool.annotations.openWorldHint !== undefined
298
+ ? { openWorld: tool.annotations.openWorldHint }
299
+ : {}),
300
+ },
301
+ }
302
+ : {}),
303
+ })));
304
+ this.details.set(name, {
305
+ displayName: client.getServerVersion()?.title ?? client.getServerVersion()?.name,
306
+ capabilities,
307
+ resources,
308
+ prompts,
309
+ });
310
+ for (const [index, tool] of tools.entries()) {
311
+ const mapped = mappedNames[index];
312
+ const reference = `${readable(name)}/${readable(tool.name)}`;
313
+ const remoteDescription = readable(tool.description ?? "", 500);
314
+ undo.push(this.registry.register({
315
+ name: mapped,
316
+ effect: "external",
317
+ description: remoteDescription
318
+ ? `${reference} — Remote MCP description (untrusted data): ${remoteDescription}`
319
+ : `${reference} — Remote MCP tool.`,
320
+ inputSchema: tool.inputSchema,
321
+ execute: async (input, ctx) => {
322
+ const available = await client.listTools(undefined, {
323
+ signal: ctx.signal,
324
+ timeout: 15000,
325
+ cacheMode: "refresh",
326
+ });
327
+ const current = available.tools.find((candidate) => candidate.name === tool.name);
328
+ if (!current ||
329
+ JSON.stringify(current.inputSchema) !== JSON.stringify(tool.inputSchema))
330
+ throw new Error("MCP schema changed; reconnect the server to refresh tools");
331
+ const result = await client.callTool({ name: tool.name, arguments: input }, { signal: ctx.signal, timeout: 30000 });
332
+ return textResult(JSON.stringify(result), "isError" in result && result.isError === true);
333
+ },
334
+ }));
335
+ }
336
+ this.clients.set(name, { client, undo, generation, registeredNames: mappedNames });
337
+ this.statuses.set(name, "connected");
338
+ client.onclose = () => {
339
+ if (this.clients.get(name)?.generation !== generation)
340
+ return;
341
+ for (const fn of undo)
342
+ fn();
343
+ this.clients.delete(name);
344
+ this.catalogs.delete(name);
345
+ this.details.delete(name);
346
+ this.statuses.set(name, config.enabled ? "disconnected" : "disabled");
347
+ };
348
+ return this.registeredNames(name);
349
+ }
350
+ catch (error) {
351
+ for (const fn of undo)
352
+ fn();
353
+ await client.close().catch(() => { });
354
+ this.catalogs.delete(name);
355
+ this.details.delete(name);
356
+ if (this.statuses.get(name) !== "needs-authentication")
357
+ this.statuses.set(name, "failed");
358
+ this.diagnostics.set(name, safeFailure(error));
359
+ throw new Error(`MCP server "${name}" failed to connect: ${safeFailure(error)}`);
360
+ }
361
+ }
362
+ async disconnect(name) {
363
+ const connected = this.clients.get(name);
364
+ if (connected) {
365
+ this.clients.delete(name);
366
+ for (const fn of connected.undo)
367
+ fn();
368
+ await connected.client.close().catch(() => { });
369
+ }
370
+ this.catalogs.delete(name);
371
+ this.details.delete(name);
372
+ this.diagnostics.delete(name);
373
+ const server = this.servers[name];
374
+ if (!server)
375
+ throw new Error(`Unknown MCP server: ${name}`);
376
+ this.statuses.set(name, server.enabled ? "disconnected" : "disabled");
377
+ }
378
+ async reconnect(name, signal) {
379
+ this.assertAvailable();
380
+ await this.disconnect(name);
381
+ return this.connect(name, signal);
382
+ }
383
+ async setEnabled(name, enabled, signal) {
384
+ const server = this.servers[name];
385
+ if (!server)
386
+ throw new Error(`Unknown MCP server: ${name}`);
387
+ server.enabled = enabled;
388
+ if (!enabled)
389
+ return this.disconnect(name);
390
+ this.statuses.set(name, "disconnected");
391
+ if (signal)
392
+ await this.connect(name, signal);
393
+ }
394
+ registeredNames(name) {
395
+ return [...(this.clients.get(name)?.registeredNames ?? [])];
396
+ }
397
+ async close() {
398
+ const servers = [...this.clients.keys()];
399
+ // Exit latency is user-facing: every server is disconnected in parallel, each client close
400
+ // raced against a short cap, and the whole phase against an overall cap. An abandoned close
401
+ // keeps running in the background and the SDK still SIGKILLs an unresponsive server; the
402
+ // registry cleanup runs synchronously inside `disconnect`, so tools are always unregistered.
403
+ await Promise.race([
404
+ (async () => {
405
+ await Promise.all(servers.map((name) => Promise.race([this.disconnect(name), delay(CLOSE_SERVER_TIMEOUT_MS)])));
406
+ for (const undo of this.bridgeUndo.splice(0).reverse())
407
+ undo();
408
+ })(),
409
+ delay(CLOSE_TOTAL_TIMEOUT_MS),
410
+ ]);
411
+ // A raced `disconnect` may have been abandoned before its final status cleanup (it flips
412
+ // the status only after `client.close()` resolves): the client entry is already gone, so
413
+ // settle the status here for exit without touching runtime disconnect semantics.
414
+ for (const name of servers) {
415
+ if (this.clients.has(name) || this.statuses.get(name) !== "connected")
416
+ continue;
417
+ const server = this.servers[name];
418
+ if (!server)
419
+ continue;
420
+ this.statuses.set(name, server.enabled ? "disconnected" : "disabled");
421
+ }
422
+ }
423
+ }
@@ -0,0 +1,169 @@
1
+ import type { AskQuestionsRequest, AskQuestionsResult, CommandContext, CommandOptions, CompletionRequest, Message, PanelProvider, Plugin, PluginAPI, PluginMetadata, RunEvent, SelectRequest } from "@alisio/sdk";
2
+ import type { HookFailure, RunnerExtensions } from "../core/contracts.ts";
3
+ import type { ToolRegistry } from "../core/registry.ts";
4
+ import { ExtensionRegistry } from "../extensions/registry.ts";
5
+ import type { ProviderRegistry } from "../providers/registry.ts";
6
+ export declare const pluginPrefix: (id: string) => string;
7
+ export interface PluginHostOptions {
8
+ /** Timeout for compaction and session-start hooks. */
9
+ hookTimeoutMs?: number;
10
+ /** Timeout for session-end hooks (for example an automatic summary). */
11
+ sessionEndTimeoutMs?: number;
12
+ }
13
+ export declare class PluginHost implements RunnerExtensions {
14
+ private registry;
15
+ private state;
16
+ private options;
17
+ private providers?;
18
+ commands: Map<string, (args: string, context?: CommandContext) => Promise<string>>;
19
+ commandInfo: Map<string, CommandOptions & {
20
+ plugin: string;
21
+ builtin: boolean;
22
+ }>;
23
+ /** Ids of active built-in plugins. */
24
+ builtins: Set<string>;
25
+ /** Status entries contributed through `ui.status`, keyed `plugin:key`. */
26
+ status: Map<string, {
27
+ plugin: string;
28
+ text: string;
29
+ detail?: string;
30
+ }>;
31
+ onStatusChange?: () => void;
32
+ /** Providers for typed extension points (mascot, startup-screen, ...). */
33
+ readonly extensions: ExtensionRegistry;
34
+ /** Loaded plugins that did not come from the built-in registry. */
35
+ externalCount: number;
36
+ private compactionHooks;
37
+ private startHandlers;
38
+ private endHandlers;
39
+ private fieldOwners;
40
+ private completer?;
41
+ private modelsImpl?;
42
+ observers: Set<(event: Readonly<RunEvent>) => void>;
43
+ contexts: Array<() => Promise<string>>;
44
+ /** Skill directories registered by plugins, retaining trusted registration ownership. */
45
+ skillSources: Array<{
46
+ plugin: string;
47
+ name: string;
48
+ dir: string;
49
+ }>;
50
+ /** Prompt template directories registered by plugins, with their plugin id. */
51
+ promptSources: Array<{
52
+ plugin: string;
53
+ dir: string;
54
+ }>;
55
+ /** Agent definition directories registered by plugins, with their plugin id. */
56
+ agentSources: Array<{
57
+ plugin: string;
58
+ dir: string;
59
+ }>;
60
+ /** Tree panels contributed by plugins, keyed `plugin:id`. */
61
+ panels: Map<string, {
62
+ plugin: string;
63
+ provider: PanelProvider;
64
+ }>;
65
+ private sessionsImpl?;
66
+ private uiImpl?;
67
+ /** Binds the child session service once the runner exists. */
68
+ setSessions(sessions: PluginAPI["sessions"]): void;
69
+ /** Binds interactive UI services (the TUI); without them select/askQuestions resolve undefined. */
70
+ setInteractiveUI(ui: {
71
+ select(request: SelectRequest): Promise<string | undefined>;
72
+ askQuestions(request: AskQuestionsRequest): Promise<AskQuestionsResult>;
73
+ open(sessionId: string): boolean;
74
+ }): void;
75
+ get sessions(): PluginAPI["sessions"];
76
+ /**
77
+ * Live accessor for core standard tools (e.g. `ask_user_question`), which are not "a plugin"
78
+ * with an id and so cannot use the per-plugin `ui` wrapper built in `activate()`. Bound lazily
79
+ * (reads `this.uiImpl` at call time) since `registerStandard` runs before the TUI calls
80
+ * `setInteractiveUI`. `interactive()` reflects whether ANY interactive UI is bound at all,
81
+ * never which session is asking, so child sessions under an interactive root TUI pass too.
82
+ */
83
+ get ui(): {
84
+ interactive(): boolean;
85
+ askQuestions(request: AskQuestionsRequest): Promise<AskQuestionsResult>;
86
+ };
87
+ get promptRoots(): string[];
88
+ get skillRoots(): string[];
89
+ private loaded;
90
+ constructor(registry: ToolRegistry, state: {
91
+ getState(plugin: string, key: string): unknown;
92
+ setState(plugin: string, key: string, value: unknown): void;
93
+ }, options?: PluginHostOptions, providers?: ProviderRegistry | undefined);
94
+ /**
95
+ * Live-update host-enforced hook timeouts so the NEXT hook call honors them without restarting.
96
+ * Keeps the same options object reference the constructors of new hooks read at call time.
97
+ */
98
+ applyTimeoutSettings(patch: Partial<PluginHostOptions>): void;
99
+ /** Binds the provider-agnostic completion service once a provider exists. */
100
+ setCompleter(fn: (request: CompletionRequest & {
101
+ signal: AbortSignal;
102
+ }) => Promise<string>): void;
103
+ /** Binds credential-free model discovery/resolution after provider startup is complete. */
104
+ setModels(models: PluginAPI["models"]): void;
105
+ /**
106
+ * Loads an explicitly trusted plugin: a file/directory path, or an npm package name resolved
107
+ * from `from` (project) upwards and then global roots. Never called for untrusted input.
108
+ */
109
+ load(spec: string, options?: {
110
+ from?: string;
111
+ globalRoots?: string[];
112
+ }): Promise<void>;
113
+ /** Imports and validates a trusted plugin without running setup, for cataloging disabled entries. */
114
+ inspect(spec: string, options?: {
115
+ from?: string;
116
+ globalRoots?: string[];
117
+ }): Promise<{
118
+ plugin: Plugin;
119
+ base: string;
120
+ }>;
121
+ private validate;
122
+ /**
123
+ * `builtin` is reserved for first-party plugins from the built-in registry: their tools and
124
+ * commands keep unprefixed names and may declare the `internal` effect.
125
+ */
126
+ activate(plugin: Plugin, base: string, options?: {
127
+ builtin?: boolean;
128
+ }): Promise<void>;
129
+ /** Runs one hook with a host-enforced timeout; the hook receives an abort signal. */
130
+ private guarded;
131
+ beforeCompact(input: Parameters<RunnerExtensions["beforeCompact"]>[0]): Promise<{
132
+ instructions: string[];
133
+ fields: Record<string, {
134
+ source: string;
135
+ description: string;
136
+ }>;
137
+ failures: HookFailure[];
138
+ }>;
139
+ afterCompact(input: Parameters<RunnerExtensions["afterCompact"]>[0]): Promise<{
140
+ inject: string[];
141
+ reports: Record<string, Record<string, unknown>>;
142
+ failures: HookFailure[];
143
+ }>;
144
+ sessionStart(input: Parameters<RunnerExtensions["sessionStart"]>[0]): Promise<{
145
+ inject: {
146
+ source: string;
147
+ text: string;
148
+ }[];
149
+ failures: HookFailure[];
150
+ }>;
151
+ /** Session end hooks run concurrently and are bounded by one host timeout. */
152
+ sessionEnd(input: {
153
+ sessionId: string;
154
+ model: string;
155
+ workspace: string;
156
+ reason: "clear" | "exit";
157
+ messages: readonly Message[];
158
+ }): Promise<{
159
+ failures: HookFailure[];
160
+ }>;
161
+ /** Loaded plugins (id, version, built-in flag), ordered by id. */
162
+ metadata(): PluginMetadata[];
163
+ get hasSessionEndHooks(): boolean;
164
+ /** Conservative ownership check used to protect a live session from plugin removal. */
165
+ ownsSessionResources(plugin: string): boolean;
166
+ emit(event: RunEvent): void;
167
+ close(): Promise<void>;
168
+ }
169
+ export declare function discoverPlugins(root: string): Promise<string[]>;