@xingjisoft/neutrino 0.6.2 → 0.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/README.md +6 -1
- package/assets/fast-planner.txt +57 -0
- package/assets/fast-replanner.txt +28 -0
- package/dist/agents.d.ts +25 -0
- package/dist/app.d.ts +206 -0
- package/dist/app.js +15 -0
- package/dist/background.d.ts +96 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +40 -3
- package/dist/browser.d.ts +18 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/colors.d.ts +29 -0
- package/dist/compact.d.ts +52 -0
- package/dist/config.d.ts +221 -0
- package/dist/config.js +19 -7
- package/dist/cwd.d.ts +19 -0
- package/dist/fast/artifacts.d.ts +26 -0
- package/dist/fast/artifacts.js +109 -0
- package/dist/fast/engine.d.ts +78 -0
- package/dist/fast/engine.js +657 -0
- package/dist/fast/evidence.d.ts +21 -0
- package/dist/fast/evidence.js +68 -0
- package/dist/fast/planner.d.ts +10 -0
- package/dist/fast/planner.js +51 -0
- package/dist/fast/pool.d.ts +17 -0
- package/dist/fast/pool.js +78 -0
- package/dist/fast/scheduler.d.ts +54 -0
- package/dist/fast/scheduler.js +383 -0
- package/dist/fast/types.d.ts +100 -0
- package/dist/fast/types.js +21 -0
- package/dist/fast/worker.d.ts +23 -0
- package/dist/fast/worker.js +185 -0
- package/dist/header.d.ts +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/llm/approval.d.ts +204 -0
- package/dist/llm/approval.js +14 -3
- package/dist/llm/client.d.ts +118 -0
- package/dist/llm/client.js +57 -8
- package/dist/llm/codeExecutor.d.ts +14 -0
- package/dist/llm/codemode.d.ts +15 -0
- package/dist/llm/context.d.ts +29 -0
- package/dist/llm/diagnostics.d.ts +91 -0
- package/dist/llm/diagnostics.js +242 -0
- package/dist/llm/plan.d.ts +148 -0
- package/dist/llm/retry.d.ts +80 -0
- package/dist/llm/risk.d.ts +21 -0
- package/dist/llm/risk.js +7 -0
- package/dist/llm/runtime.d.ts +189 -0
- package/dist/llm/runtime.js +77 -4
- package/dist/llm/search.d.ts +2 -0
- package/dist/llm/searchNetwork.d.ts +8 -0
- package/dist/llm/subagent.d.ts +112 -0
- package/dist/llm/subagent.js +4 -2
- package/dist/llm/tools.d.ts +20 -0
- package/dist/llm/tools.js +4 -1
- package/dist/llm/workspace.d.ts +6 -0
- package/dist/llm/workspace.js +271 -0
- package/dist/markdown.d.ts +58 -0
- package/dist/migration.d.ts +8 -0
- package/dist/persistence.d.ts +14 -0
- package/dist/plugins/catalog.d.ts +20 -0
- package/dist/plugins/catalog.js +45 -0
- package/dist/plugins/cli.d.ts +1 -0
- package/dist/plugins/cli.js +106 -0
- package/dist/plugins/host.d.ts +121 -0
- package/dist/plugins/host.js +450 -0
- package/dist/plugins/manager.d.ts +24 -0
- package/dist/plugins/manager.js +288 -0
- package/dist/plugins/package.d.ts +5 -0
- package/dist/plugins/package.js +53 -0
- package/dist/plugins/runner.d.ts +1 -0
- package/dist/plugins/runner.js +227 -0
- package/dist/plugins/session.d.ts +12 -0
- package/dist/plugins/session.js +97 -0
- package/dist/plugins/terminal.d.ts +22 -0
- package/dist/plugins/terminal.js +33 -0
- package/dist/plugins/types.d.ts +178 -0
- package/dist/plugins/types.js +6 -0
- package/dist/plugins/validation.d.ts +14 -0
- package/dist/plugins/validation.js +174 -0
- package/dist/plugins/web-sdk.d.ts +58 -0
- package/dist/plugins/web-sdk.js +1 -0
- package/dist/pro/agents.d.ts +34 -0
- package/dist/pro/config.d.ts +75 -0
- package/dist/pro/config.js +3 -2
- package/dist/pro/preflight.d.ts +21 -0
- package/dist/sanitize.d.ts +34 -0
- package/dist/screens/agentConversation.d.ts +28 -0
- package/dist/screens/agentHistory.d.ts +25 -0
- package/dist/screens/agentSelect.d.ts +16 -0
- package/dist/screens/chat.d.ts +292 -0
- package/dist/screens/chat.js +139 -21
- package/dist/screens/history.d.ts +23 -0
- package/dist/screens/modelSelect.d.ts +29 -0
- package/dist/screens/plugins.d.ts +29 -0
- package/dist/screens/plugins.js +196 -0
- package/dist/screens/settings.d.ts +42 -0
- package/dist/screens/start.d.ts +43 -0
- package/dist/screens/start.js +25 -17
- package/dist/screens/updatePrompt.d.ts +41 -0
- package/dist/selection.d.ts +75 -0
- package/dist/state.d.ts +26 -0
- package/dist/terminal/buffer.d.ts +47 -0
- package/dist/terminal/input.d.ts +70 -0
- package/dist/terminal/renderer.d.ts +43 -0
- package/dist/terminal/restore.d.ts +41 -0
- package/dist/terminal/screen.d.ts +57 -0
- package/dist/tray.d.ts +14 -0
- package/dist/ui.d.ts +123 -0
- package/dist/update.d.ts +71 -0
- package/dist/updateWatch.d.ts +43 -0
- package/dist/web/approvalQueue.d.ts +8 -0
- package/dist/web/approvalQueue.js +59 -0
- package/dist/web/auth.d.ts +14 -0
- package/dist/web/clientState.d.ts +16 -0
- package/dist/web/folderPicker.d.ts +34 -0
- package/dist/web/folderPicker.js +544 -0
- package/dist/web/hub.d.ts +213 -0
- package/dist/web/hub.js +14 -2
- package/dist/web/localApi.d.ts +11 -0
- package/dist/web/localApi.js +260 -0
- package/dist/web/plugins.d.ts +8 -0
- package/dist/web/plugins.js +64 -0
- package/dist/web/server.d.ts +81 -0
- package/dist/web/server.js +127 -19
- package/dist/web/service.d.ts +7 -0
- package/dist/web/service.js +1 -0
- package/dist/web/session.d.ts +176 -0
- package/dist/web/session.js +153 -19
- package/dist/web/settings.d.ts +65 -0
- package/dist/web/terminalBridge.d.ts +15 -0
- package/dist/web/terminalBridge.js +8 -2
- package/dist/width.d.ts +6 -0
- package/docs/plugins/README.md +126 -0
- package/docs/plugins/agent-sdk.md +92 -0
- package/docs/plugins/architecture.md +64 -0
- package/docs/plugins/compatibility.md +34 -0
- package/docs/plugins/development.md +55 -0
- package/docs/plugins/manifest.md +59 -0
- package/docs/plugins/manifest.schema.json +286 -0
- package/docs/plugins/security.md +53 -0
- package/docs/plugins/skills-prompts.md +69 -0
- package/docs/plugins/ui-sdk.md +109 -0
- package/examples/local-api.py +105 -0
- package/examples/plugins/cards/card.html +6 -0
- package/examples/plugins/cards/neutrino.plugin.json +42 -0
- package/examples/plugins/composer/editor.html +10 -0
- package/examples/plugins/composer/neutrino.plugin.json +10 -0
- package/examples/plugins/function-tools/agent.js +32 -0
- package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/panel.html +6 -0
- package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
- package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
- package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
- package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
- package/examples/plugins/theme/neutrino.plugin.json +12 -0
- package/package.json +66 -42
- package/web/LOCAL-API.md +65 -0
- package/web/README.md +27 -6
- package/web/app.js +201 -32
- package/web/i18n.js +15 -0
- package/web/index.html +3 -1
- package/web/plugins.css +10 -0
- package/web/plugins.js +230 -0
- package/web/style.css +11 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { configurePlugin, installPlugin, listPlugins, removePlugin, rollbackPlugin, selectProvider } from "./manager.js";
|
|
5
|
+
import { PluginHost } from "./host.js";
|
|
6
|
+
import { packPlugin } from "./package.js";
|
|
7
|
+
export async function runPluginCLI(args) {
|
|
8
|
+
let profile = "standard", cwd, trustNative = false;
|
|
9
|
+
const values = [];
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
if (args[i] === "--profile") {
|
|
12
|
+
const p = args[++i];
|
|
13
|
+
if (p !== "standard" && p !== "pro")
|
|
14
|
+
throw new Error("Profile must be standard or pro");
|
|
15
|
+
profile = p;
|
|
16
|
+
}
|
|
17
|
+
else if (args[i] === "--workspace") {
|
|
18
|
+
const value = args[++i];
|
|
19
|
+
if (!value || value.startsWith("--"))
|
|
20
|
+
throw new Error("--workspace requires a directory");
|
|
21
|
+
cwd = path.resolve(value);
|
|
22
|
+
}
|
|
23
|
+
else if (args[i] === "--trust-native")
|
|
24
|
+
trustNative = true;
|
|
25
|
+
else
|
|
26
|
+
values.push(args[i]);
|
|
27
|
+
}
|
|
28
|
+
const [action, id, extra] = values;
|
|
29
|
+
const print = (v) => process.stdout.write((typeof v === "string" ? v : JSON.stringify(v, null, 2)) + "\n");
|
|
30
|
+
if (!action || action === "help" || action === "--help") {
|
|
31
|
+
print(`Plugin API v1\n\nneutrino plugin list [--profile standard|pro] [--workspace <path>]\nneutrino plugin inspect <id>\nneutrino plugin install <local-directory-or-bundle>\nneutrino plugin enable <id> [--trust-native]\nneutrino plugin disable <id>\nneutrino plugin config <id> '<JSON object>'\nneutrino plugin update <id>\nneutrino plugin rollback <id>\nneutrino plugin remove <id>\nneutrino plugin provider <slot> <plugin-id/contribution-id|default>\nneutrino plugin doctor\nneutrino plugin create <directory>\nneutrino plugin pack <directory> <output.neutrino-plugin>\nneutrino --safe-mode --no-web\n\nInstall is separate from enable. Native plugins execute unrestricted local code.\n`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (action === "list") {
|
|
35
|
+
print(listPlugins(profile, cwd));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (action === "pack") {
|
|
39
|
+
if (!id || !extra)
|
|
40
|
+
throw new Error("Usage: neutrino plugin pack <directory> <output.neutrino-plugin>");
|
|
41
|
+
print(packPlugin(path.resolve(id), path.resolve(extra)));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (action === "install") {
|
|
45
|
+
if (!id)
|
|
46
|
+
throw new Error("Missing source directory");
|
|
47
|
+
print(installPlugin(path.resolve(id)));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (action === "enable" || action === "disable") {
|
|
51
|
+
configurePlugin(id, { enabled: action === "enable", trustNative }, profile, cwd);
|
|
52
|
+
print(`${id}: ${action}d`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (action === "config") {
|
|
56
|
+
configurePlugin(id, { config: JSON.parse(extra ?? "{}") }, profile, cwd);
|
|
57
|
+
print("Configuration saved");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (action === "remove") {
|
|
61
|
+
removePlugin(id);
|
|
62
|
+
print("Plugin removed; immutable files retained for running tasks");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (action === "rollback") {
|
|
66
|
+
rollbackPlugin(id);
|
|
67
|
+
print("Previous package revision restored");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (action === "provider") {
|
|
71
|
+
selectProvider(id, extra === "default" ? "" : extra, profile);
|
|
72
|
+
print("Provider selected");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (action === "update" || action === "inspect") {
|
|
76
|
+
const p = listPlugins(profile, cwd).plugins.find(p => p.manifest.id === id);
|
|
77
|
+
if (!p)
|
|
78
|
+
throw new Error("Unknown plugin");
|
|
79
|
+
print(action === "update" ? installPlugin(p.source) : p);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (action === "doctor") {
|
|
83
|
+
const host = await PluginHost.create({ scope: { profile, cwd: cwd ?? process.cwd(), role: "main" } });
|
|
84
|
+
try {
|
|
85
|
+
print({ plugins: host.pins, tools: host.definitions(), commands: [...host.commands.keys()], errors: host.diagnostics });
|
|
86
|
+
if (host.diagnostics.length)
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await host.dispose();
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (action === "create") {
|
|
95
|
+
if (!id)
|
|
96
|
+
throw new Error("Missing target directory");
|
|
97
|
+
const dir = path.resolve(id);
|
|
98
|
+
if (fs.existsSync(dir))
|
|
99
|
+
throw new Error("Target directory already exists");
|
|
100
|
+
const source = fileURLToPath(new URL("../../examples/plugins/function-tools/", import.meta.url));
|
|
101
|
+
fs.cpSync(source, dir, { recursive: true });
|
|
102
|
+
print(`Created ${dir}. Edit neutrino.plugin.json and agent.js, then install the directory.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Unknown plugin command ${action}`);
|
|
106
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../llm/tools.js";
|
|
2
|
+
import type { Effect, EventName, InvocationContext, JsonObject, PluginPin, PluginRecord, PluginScope, Profile, PromptContribution, UIContribution } from "./types.js";
|
|
3
|
+
interface Registered {
|
|
4
|
+
kind: string;
|
|
5
|
+
handler: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface Owned {
|
|
9
|
+
record: PluginRecord;
|
|
10
|
+
worker?: PluginWorker;
|
|
11
|
+
}
|
|
12
|
+
interface Tool extends Owned {
|
|
13
|
+
definition: ToolDefinition;
|
|
14
|
+
handler: string;
|
|
15
|
+
effects: Effect[];
|
|
16
|
+
roles?: string[];
|
|
17
|
+
}
|
|
18
|
+
interface Command extends Owned {
|
|
19
|
+
handler: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
parameters: JsonObject;
|
|
23
|
+
effects: Effect[];
|
|
24
|
+
}
|
|
25
|
+
export interface OwnedUI extends UIContribution {
|
|
26
|
+
pluginId: string;
|
|
27
|
+
key: string;
|
|
28
|
+
entry?: string;
|
|
29
|
+
permissions: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface Skill {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
text: string;
|
|
36
|
+
pluginId: string;
|
|
37
|
+
}
|
|
38
|
+
interface HostOptions {
|
|
39
|
+
scope: PluginScope;
|
|
40
|
+
context?(): InvocationContext;
|
|
41
|
+
broker?(name: string, args: JsonObject, signal?: AbortSignal): Promise<unknown>;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
allowNative?(): boolean;
|
|
45
|
+
}
|
|
46
|
+
declare class PluginWorker {
|
|
47
|
+
private record;
|
|
48
|
+
private timeout;
|
|
49
|
+
private log;
|
|
50
|
+
private child;
|
|
51
|
+
private readyResolve;
|
|
52
|
+
private readyReject;
|
|
53
|
+
readonly ready: Promise<Registered[]>;
|
|
54
|
+
private current?;
|
|
55
|
+
private sequence;
|
|
56
|
+
private queue;
|
|
57
|
+
private operationQueue;
|
|
58
|
+
private closed;
|
|
59
|
+
private initTimer;
|
|
60
|
+
constructor(record: PluginRecord, scope: PluginScope, timeout: number, log: (message: string) => void);
|
|
61
|
+
private resetTimer;
|
|
62
|
+
private receive;
|
|
63
|
+
private reply;
|
|
64
|
+
private fail;
|
|
65
|
+
invoke(handler: string, value: unknown, context: InvocationContext, operation: (op: string, args: JsonObject, signal: AbortSignal) => Promise<unknown>): Promise<unknown>;
|
|
66
|
+
dispose(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/** One frozen plugin composition per turn, shared by both frontends and all child scopes. */
|
|
69
|
+
export declare class PluginHost {
|
|
70
|
+
private options;
|
|
71
|
+
readonly providers: Record<string, string>;
|
|
72
|
+
readonly tools: Map<string, Tool>;
|
|
73
|
+
readonly commands: Map<string, Command>;
|
|
74
|
+
readonly ui: OwnedUI[];
|
|
75
|
+
readonly skills: Skill[];
|
|
76
|
+
readonly pins: PluginPin[];
|
|
77
|
+
readonly diagnostics: string[];
|
|
78
|
+
readonly prompts: Array<PromptContribution & {
|
|
79
|
+
pluginId: string;
|
|
80
|
+
}>;
|
|
81
|
+
readonly theme: Record<string, string>;
|
|
82
|
+
private workers;
|
|
83
|
+
private hooks;
|
|
84
|
+
private events;
|
|
85
|
+
private disposed;
|
|
86
|
+
get profile(): Profile;
|
|
87
|
+
private constructor();
|
|
88
|
+
static create(options: HostOptions): Promise<PluginHost>;
|
|
89
|
+
private report;
|
|
90
|
+
private permission;
|
|
91
|
+
private declarations;
|
|
92
|
+
private addSkill;
|
|
93
|
+
private resource;
|
|
94
|
+
private addPrompt;
|
|
95
|
+
private addUI;
|
|
96
|
+
private register;
|
|
97
|
+
private remove;
|
|
98
|
+
definitions(role?: string): ToolDefinition[];
|
|
99
|
+
toolPolicy(name: string): {
|
|
100
|
+
effects: Effect[];
|
|
101
|
+
native: boolean;
|
|
102
|
+
roles?: string[];
|
|
103
|
+
} | undefined;
|
|
104
|
+
private context;
|
|
105
|
+
private invoke;
|
|
106
|
+
execute(name: string, args: JsonObject): Promise<string>;
|
|
107
|
+
runCommand(name: string, args?: JsonObject, stack?: string[]): Promise<unknown>;
|
|
108
|
+
guardTool(name: string, args: JsonObject): Promise<void>;
|
|
109
|
+
requestSuffix(value: JsonObject): Promise<string>;
|
|
110
|
+
emit(name: EventName, value: JsonObject): Promise<void>;
|
|
111
|
+
composePrompt(base: string, role: string): {
|
|
112
|
+
text: string;
|
|
113
|
+
sections: Array<{
|
|
114
|
+
id: string;
|
|
115
|
+
text: string;
|
|
116
|
+
}>;
|
|
117
|
+
};
|
|
118
|
+
effectiveUI(platform: "web" | "tui"): OwnedUI[];
|
|
119
|
+
dispose(): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
export {};
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { fork } from "node:child_process";
|
|
3
|
+
import { AsyncResource } from "node:async_hooks";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { defaults, assert, NAME, object, safeFile, validatePrompt, validateSchema, validateUI, validateValue } from "./validation.js";
|
|
7
|
+
import { pluginRecords, pluginStorage, providerSelections } from "./manager.js";
|
|
8
|
+
const EFFECTS = ["filesystem.read", "filesystem.write", "network", "process"];
|
|
9
|
+
const EXCLUSIVE = new Set(["chat.composer", "chat.message.renderer", "chat.card.plan", "chat.card.tool"]);
|
|
10
|
+
class PluginWorker {
|
|
11
|
+
record;
|
|
12
|
+
timeout;
|
|
13
|
+
log;
|
|
14
|
+
child;
|
|
15
|
+
readyResolve;
|
|
16
|
+
readyReject;
|
|
17
|
+
ready;
|
|
18
|
+
current;
|
|
19
|
+
sequence = 0;
|
|
20
|
+
queue = Promise.resolve();
|
|
21
|
+
operationQueue = Promise.resolve();
|
|
22
|
+
closed = false;
|
|
23
|
+
initTimer;
|
|
24
|
+
constructor(record, scope, timeout, log) {
|
|
25
|
+
this.record = record;
|
|
26
|
+
this.timeout = timeout;
|
|
27
|
+
this.log = log;
|
|
28
|
+
this.ready = new Promise((resolve, reject) => { this.readyResolve = resolve; this.readyReject = reject; });
|
|
29
|
+
this.child = fork(fileURLToPath(new URL("./runner.js", import.meta.url)), [], { cwd: record.directory,
|
|
30
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"], execArgv: ["--max-old-space-size=128"], ...{ windowsHide: true },
|
|
31
|
+
env: record.manifest.runtime === "native" ? process.env : { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot },
|
|
32
|
+
});
|
|
33
|
+
this.initTimer = setTimeout(() => this.fail(new Error("Plugin activation timed out")), timeout);
|
|
34
|
+
this.child.stderr?.on("data", data => this.log(String(data).slice(0, 1000)));
|
|
35
|
+
this.child.on("error", error => this.fail(error));
|
|
36
|
+
this.child.on("exit", () => this.fail(new Error("Plugin process exited")));
|
|
37
|
+
this.child.on("message", message => { void this.receive(message).catch(error => this.fail(error instanceof Error ? error : new Error(String(error)))); });
|
|
38
|
+
this.child.send({ type: "init", initial: { id: record.manifest.id, entry: safeFile(record.directory, record.manifest.entries.agent), runtime: record.manifest.runtime ?? "sandbox", config: { ...defaults(record.manifest.configuration), ...record.activation.config }, scope } });
|
|
39
|
+
}
|
|
40
|
+
resetTimer() {
|
|
41
|
+
if (!this.current)
|
|
42
|
+
return;
|
|
43
|
+
clearTimeout(this.current.timer);
|
|
44
|
+
this.current.timer = setTimeout(() => this.fail(new Error("Plugin callback timed out")), this.timeout);
|
|
45
|
+
}
|
|
46
|
+
async receive(m) {
|
|
47
|
+
assert(object(m), "Invalid plugin protocol message");
|
|
48
|
+
if (m.type === "ready") {
|
|
49
|
+
clearTimeout(this.initTimer);
|
|
50
|
+
this.readyResolve(m.registrations);
|
|
51
|
+
}
|
|
52
|
+
else if (m.type === "fatal")
|
|
53
|
+
this.fail(new Error(String(m.error)));
|
|
54
|
+
else if (m.type === "result" && this.current?.id === m.id) {
|
|
55
|
+
const c = this.current;
|
|
56
|
+
this.current = undefined;
|
|
57
|
+
clearTimeout(c.timer);
|
|
58
|
+
c.signal?.removeEventListener("abort", c.abort);
|
|
59
|
+
if (c.operations) {
|
|
60
|
+
c.stop.abort();
|
|
61
|
+
c.reject(new Error("Plugin returned with unawaited host operations"));
|
|
62
|
+
}
|
|
63
|
+
else if (m.error)
|
|
64
|
+
c.reject(new Error(String(m.error)));
|
|
65
|
+
else
|
|
66
|
+
c.resolve(m.value);
|
|
67
|
+
}
|
|
68
|
+
else if (m.type === "operation") {
|
|
69
|
+
const c = this.current;
|
|
70
|
+
if (m.op === "log") {
|
|
71
|
+
this.log(String(m.value?.message ?? "").slice(0, 4000));
|
|
72
|
+
this.reply(m.id, null);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!c) {
|
|
76
|
+
if (["storage.get", "storage.set"].includes(String(m.op)) && this.record.manifest.permissions?.includes("storage")) {
|
|
77
|
+
try {
|
|
78
|
+
const args = m.value;
|
|
79
|
+
this.reply(m.id, pluginStorage(this.record.manifest.id, String(args.key), args.value, m.op === "storage.set"));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
this.reply(m.id, undefined, e.message);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.reply(m.id, undefined, "Host operations are available only inside an awaited callback");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (++c.calls > 256 || c.operations >= 64) {
|
|
90
|
+
this.fail(new Error("Plugin exceeded host operation limits"));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
c.operations++;
|
|
94
|
+
clearTimeout(c.timer);
|
|
95
|
+
const operation = this.operationQueue.then(async () => { assert(this.current === c && !c.stop.signal.aborted, "Plugin callback already ended"); return c.operation(String(m.op), (m.value ?? {})); });
|
|
96
|
+
this.operationQueue = operation.then(() => { }, () => { });
|
|
97
|
+
try {
|
|
98
|
+
this.reply(m.id, await operation);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
this.reply(m.id, undefined, e.message);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
c.operations--;
|
|
105
|
+
if (this.current === c && !c.operations)
|
|
106
|
+
this.resetTimer();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
reply(id, value, error) { if (this.child.connected)
|
|
111
|
+
this.child.send({ type: "operation-result", id, value, error }); }
|
|
112
|
+
fail(error) {
|
|
113
|
+
if (this.closed)
|
|
114
|
+
return;
|
|
115
|
+
this.closed = true;
|
|
116
|
+
clearTimeout(this.initTimer);
|
|
117
|
+
this.readyReject(error);
|
|
118
|
+
if (this.current) {
|
|
119
|
+
const c = this.current;
|
|
120
|
+
this.current = undefined;
|
|
121
|
+
clearTimeout(c.timer);
|
|
122
|
+
c.stop.abort(error);
|
|
123
|
+
c.signal?.removeEventListener("abort", c.abort);
|
|
124
|
+
c.reject(error);
|
|
125
|
+
}
|
|
126
|
+
this.child.kill("SIGKILL");
|
|
127
|
+
}
|
|
128
|
+
invoke(handler, value, context, operation) {
|
|
129
|
+
const stop = new AbortController();
|
|
130
|
+
const boundOperation = AsyncResource.bind((op, args) => operation(op, args, stop.signal));
|
|
131
|
+
const { signal, ...wire } = context;
|
|
132
|
+
const boundInvoke = AsyncResource.bind(() => new Promise((resolve, reject) => {
|
|
133
|
+
if (this.closed) {
|
|
134
|
+
reject(new Error("Plugin is unavailable"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const abort = () => this.fail(new Error("Plugin callback cancelled"));
|
|
138
|
+
this.current = { id: ++this.sequence, resolve, reject, operation: boundOperation, abort, signal, operations: 0, calls: 0, stop };
|
|
139
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
140
|
+
if (signal?.aborted) {
|
|
141
|
+
abort();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.resetTimer();
|
|
145
|
+
this.child.send({ type: "invoke", id: this.sequence, handler, value, context: wire });
|
|
146
|
+
}));
|
|
147
|
+
const result = this.queue.then(boundInvoke);
|
|
148
|
+
this.queue = result.then(() => { }, () => { });
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
async dispose() {
|
|
152
|
+
if (this.closed)
|
|
153
|
+
return;
|
|
154
|
+
await this.queue;
|
|
155
|
+
await new Promise(resolve => {
|
|
156
|
+
const timer = setTimeout(() => { this.fail(new Error("Plugin cleanup timed out")); resolve(); }, 1000);
|
|
157
|
+
this.child.once("exit", () => { clearTimeout(timer); resolve(); });
|
|
158
|
+
if (this.child.connected)
|
|
159
|
+
this.child.send({ type: "dispose" });
|
|
160
|
+
else {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
resolve();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
this.closed = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** One frozen plugin composition per turn, shared by both frontends and all child scopes. */
|
|
169
|
+
export class PluginHost {
|
|
170
|
+
options;
|
|
171
|
+
providers;
|
|
172
|
+
tools = new Map();
|
|
173
|
+
commands = new Map();
|
|
174
|
+
ui = [];
|
|
175
|
+
skills = [];
|
|
176
|
+
pins = [];
|
|
177
|
+
diagnostics = [];
|
|
178
|
+
prompts = [];
|
|
179
|
+
theme = {};
|
|
180
|
+
workers = [];
|
|
181
|
+
hooks = [];
|
|
182
|
+
events = [];
|
|
183
|
+
disposed = false;
|
|
184
|
+
get profile() { return this.options.scope.profile; }
|
|
185
|
+
constructor(options, providers) {
|
|
186
|
+
this.options = options;
|
|
187
|
+
this.providers = providers;
|
|
188
|
+
}
|
|
189
|
+
static async create(options) {
|
|
190
|
+
let providers = {};
|
|
191
|
+
try {
|
|
192
|
+
providers = providerSelections(options.scope.profile);
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
const host = new PluginHost(options, {});
|
|
196
|
+
host.report("host", e.message);
|
|
197
|
+
return host;
|
|
198
|
+
}
|
|
199
|
+
const host = new PluginHost(options, providers);
|
|
200
|
+
let records;
|
|
201
|
+
try {
|
|
202
|
+
records = pluginRecords(options.scope.profile, options.scope.cwd);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
host.report("host", e.message);
|
|
206
|
+
return host;
|
|
207
|
+
}
|
|
208
|
+
const failed = new Set();
|
|
209
|
+
for (const record of records) {
|
|
210
|
+
let worker;
|
|
211
|
+
const previousTheme = { ...host.theme };
|
|
212
|
+
try {
|
|
213
|
+
assert(!options.signal?.aborted, "Task cancelled during plugin activation");
|
|
214
|
+
assert(!record.error, record.error ?? "Invalid plugin");
|
|
215
|
+
for (const dep of Object.keys(record.manifest.dependencies ?? {}))
|
|
216
|
+
assert(!failed.has(dep), `Dependency ${dep} failed to activate`);
|
|
217
|
+
if (record.manifest.entries?.agent) {
|
|
218
|
+
worker = new PluginWorker(record, options.scope, options.timeoutMs ?? 15000, message => host.report(record.manifest.id, message));
|
|
219
|
+
host.workers.push(worker);
|
|
220
|
+
const registrations = await worker.ready;
|
|
221
|
+
assert(Array.isArray(registrations) && registrations.length <= 300, "Too many plugin registrations");
|
|
222
|
+
for (const item of registrations)
|
|
223
|
+
host.register(record, worker, item);
|
|
224
|
+
}
|
|
225
|
+
host.declarations(record);
|
|
226
|
+
host.pins.push({ id: record.manifest.id, version: record.manifest.version, integrity: record.integrity, config: { ...defaults(record.manifest.configuration), ...record.activation.config } });
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
for (const key of Object.keys(host.theme))
|
|
230
|
+
delete host.theme[key];
|
|
231
|
+
Object.assign(host.theme, previousTheme);
|
|
232
|
+
failed.add(record.manifest.id);
|
|
233
|
+
host.remove(record.manifest.id);
|
|
234
|
+
await worker?.dispose();
|
|
235
|
+
host.report(record.manifest.id, e.message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return host;
|
|
239
|
+
}
|
|
240
|
+
report(id, message) { if (this.diagnostics.length < 100)
|
|
241
|
+
this.diagnostics.push(`${id}: ${message.slice(0, 4000)}`); }
|
|
242
|
+
permission(record, permission) { assert(record.manifest.permissions?.includes(permission), `${record.manifest.id} requires ${permission}`); }
|
|
243
|
+
declarations(record) {
|
|
244
|
+
const c = record.manifest.contributes;
|
|
245
|
+
for (const prompt of c?.prompts ?? [])
|
|
246
|
+
this.addPrompt(record, prompt);
|
|
247
|
+
for (const ui of c?.ui ?? [])
|
|
248
|
+
this.addUI(record, ui);
|
|
249
|
+
if (c?.theme) {
|
|
250
|
+
this.permission(record, "ui.contribute");
|
|
251
|
+
Object.assign(this.theme, c.theme);
|
|
252
|
+
}
|
|
253
|
+
for (const file of c?.skills ?? []) {
|
|
254
|
+
this.addSkill(record, file);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
addSkill(record, file) {
|
|
258
|
+
this.permission(record, "prompts.contribute");
|
|
259
|
+
const text = this.resource(record, file);
|
|
260
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/.exec(text);
|
|
261
|
+
const name = /^name:\s*["']?([^\r\n"']+)/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? file.split("/").slice(-2, -1)[0] ?? "skill";
|
|
262
|
+
const description = /^description:\s*["']?([^\r\n"']+)/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? name;
|
|
263
|
+
const id = `${record.manifest.id}/${name}`;
|
|
264
|
+
assert(!this.skills.some(s => s.id === id), `Duplicate skill ${id}`);
|
|
265
|
+
this.skills.push({ id, name, description, text: text.slice(front?.[0].length ?? 0), pluginId: record.manifest.id });
|
|
266
|
+
}
|
|
267
|
+
resource(record, file) { const resolved = safeFile(record.directory, file); assert(fs.statSync(resolved).size <= 64_000, "Plugin text resource exceeds 64 KB"); return fs.readFileSync(resolved, "utf8"); }
|
|
268
|
+
addPrompt(record, prompt) {
|
|
269
|
+
this.permission(record, "prompts.contribute");
|
|
270
|
+
validatePrompt(prompt);
|
|
271
|
+
assert(!this.prompts.some(p => p.pluginId === record.manifest.id && p.id === prompt.id), "Duplicate prompt id");
|
|
272
|
+
this.prompts.push({ ...prompt, text: prompt.file ? this.resource(record, prompt.file) : prompt.text, file: undefined, pluginId: record.manifest.id });
|
|
273
|
+
}
|
|
274
|
+
addUI(record, ui) {
|
|
275
|
+
this.permission(record, "ui.contribute");
|
|
276
|
+
validateUI(ui);
|
|
277
|
+
const key = `${record.manifest.id}/${ui.id}`;
|
|
278
|
+
assert(!this.ui.some(u => u.key === key), `Duplicate UI contribution ${key}`);
|
|
279
|
+
if (ui.view) {
|
|
280
|
+
safeFile(record.directory, ui.view);
|
|
281
|
+
assert((record.manifest.contributes?.ui ?? []).some(u => u.view === ui.view) || record.manifest.webAssets?.includes(ui.view), "Dynamic views must be declared in webAssets");
|
|
282
|
+
}
|
|
283
|
+
this.ui.push({ ...ui, key, pluginId: record.manifest.id, entry: record.manifest.entries?.web, permissions: record.manifest.permissions ?? [] });
|
|
284
|
+
}
|
|
285
|
+
register(record, worker, item) {
|
|
286
|
+
assert(object(item) && typeof item.handler === "string", "Invalid registration");
|
|
287
|
+
if (item.kind === "prompt") {
|
|
288
|
+
this.addPrompt(record, item);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (item.kind === "skill") {
|
|
292
|
+
assert(typeof item.file === "string", "Invalid skill file");
|
|
293
|
+
this.addSkill(record, item.file);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (item.kind === "ui") {
|
|
297
|
+
this.addUI(record, item);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (item.kind === "tool" || item.kind === "command") {
|
|
301
|
+
this.permission(record, item.kind === "tool" ? "tools.register" : "commands.register");
|
|
302
|
+
assert(NAME.test(String(item.name)) && typeof item.description === "string" && item.description.length <= 8000, "Invalid tool/command name or description");
|
|
303
|
+
const parameters = (item.parameters ?? { type: "object", additionalProperties: false });
|
|
304
|
+
validateSchema(parameters);
|
|
305
|
+
assert(parameters.type === "object", "Tool arguments must be an object");
|
|
306
|
+
assert(Array.isArray(item.effects) && item.effects.every(e => EFFECTS.includes(String(e))), "Tools and commands must declare effects[] (empty for pure computation)");
|
|
307
|
+
for (const effect of item.effects)
|
|
308
|
+
this.permission(record, String(effect));
|
|
309
|
+
if (item.kind === "tool" && (record.manifest.runtime === "native" || item.effects.some(e => e !== "filesystem.read"))) {
|
|
310
|
+
const properties = (parameters.properties ?? {});
|
|
311
|
+
assert(!properties.reason || properties.reason.type === "string", "Risky tool reason must be a string");
|
|
312
|
+
parameters.properties = { ...properties, reason: properties.reason ?? { type: "string", minLength: 4, maxLength: 600, description: "Required: explain why this call is needed for the user's request." } };
|
|
313
|
+
parameters.required = [...new Set([...(parameters.required ?? []), "reason"])];
|
|
314
|
+
}
|
|
315
|
+
const common = { record, worker, handler: item.handler, effects: item.effects };
|
|
316
|
+
const publicName = `${record.manifest.id}/${item.name}`;
|
|
317
|
+
if (item.kind === "tool") {
|
|
318
|
+
const alias = `p_${createHash("sha256").update(record.manifest.id).digest("hex").slice(0, 12)}_${item.name}`;
|
|
319
|
+
assert(!this.tools.has(alias), `Duplicate tool ${publicName}`);
|
|
320
|
+
assert(item.roles === undefined || (Array.isArray(item.roles) && item.roles.every(r => typeof r === "string")), "Invalid tool roles");
|
|
321
|
+
this.tools.set(alias, { ...common, roles: item.roles,
|
|
322
|
+
definition: { type: "function", function: { name: alias, description: `[${publicName}] ${item.description}`, parameters } } });
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
assert(!this.commands.has(publicName), `Duplicate command ${publicName}`);
|
|
326
|
+
this.commands.set(publicName, { ...common, name: publicName, description: item.description, parameters });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else if (item.kind === "hook" || item.kind === "event") {
|
|
330
|
+
this.permission(record, "hooks.register");
|
|
331
|
+
const names = item.kind === "hook" ? ["request.before", "tool.before"] : ["turn.started", "turn.finished", "tool.finished"];
|
|
332
|
+
assert(names.includes(String(item.name)), "Unsupported plugin hook/event");
|
|
333
|
+
(item.kind === "hook" ? this.hooks : this.events).push({ record, worker, handler: item.handler, name: String(item.name) });
|
|
334
|
+
}
|
|
335
|
+
else
|
|
336
|
+
throw new Error(`Unsupported registration ${item.kind}`);
|
|
337
|
+
}
|
|
338
|
+
remove(id) {
|
|
339
|
+
for (const [key, tool] of this.tools)
|
|
340
|
+
if (tool.record.manifest.id === id)
|
|
341
|
+
this.tools.delete(key);
|
|
342
|
+
for (const [key, cmd] of this.commands)
|
|
343
|
+
if (cmd.record.manifest.id === id)
|
|
344
|
+
this.commands.delete(key);
|
|
345
|
+
for (const list of [this.ui, this.prompts, this.skills])
|
|
346
|
+
for (let i = list.length - 1; i >= 0; i--)
|
|
347
|
+
if (list[i].pluginId === id)
|
|
348
|
+
list.splice(i, 1);
|
|
349
|
+
this.hooks = this.hooks.filter(h => h.record.manifest.id !== id);
|
|
350
|
+
this.events = this.events.filter(h => h.record.manifest.id !== id);
|
|
351
|
+
}
|
|
352
|
+
definitions(role = this.context().role) { return [...this.tools.values()].filter(t => !t.roles || t.roles.includes(role)).map(t => structuredClone(t.definition)); }
|
|
353
|
+
toolPolicy(name) {
|
|
354
|
+
const tool = this.tools.get(name);
|
|
355
|
+
return tool ? { effects: tool.effects, native: tool.record.manifest.runtime === "native", roles: tool.roles } : undefined;
|
|
356
|
+
}
|
|
357
|
+
context() { return this.options.context?.() ?? { ...this.options.scope, signal: this.options.signal }; }
|
|
358
|
+
async invoke(owned, value, effects = [], stack = []) {
|
|
359
|
+
assert(!this.disposed, "Plugin host was disposed");
|
|
360
|
+
const id = owned.record.manifest.id;
|
|
361
|
+
assert(owned.record.manifest.runtime !== "native" || this.options.allowNative?.() !== false, "Native plugins cannot run inside a restricted child scope");
|
|
362
|
+
assert(!stack.includes(id) && stack.length < 8, "Plugin service dependency cycle");
|
|
363
|
+
const context = this.context();
|
|
364
|
+
return owned.worker.invoke(owned.handler, structuredClone(value), context, async (op, args, signal) => {
|
|
365
|
+
if (op === "storage.get" || op === "storage.set") {
|
|
366
|
+
this.permission(owned.record, "storage");
|
|
367
|
+
return pluginStorage(id, String(args.key), args.value, op === "storage.set");
|
|
368
|
+
}
|
|
369
|
+
if (op === "service.call") {
|
|
370
|
+
const target = String(args.pluginId);
|
|
371
|
+
assert(Object.hasOwn(owned.record.manifest.dependencies ?? {}, target) || Object.hasOwn(owned.record.manifest.optionalDependencies ?? {}, target), "Declare a dependency before calling another plugin");
|
|
372
|
+
const command = this.commands.get(`${target}/${args.command}`);
|
|
373
|
+
assert(command, "Unknown plugin service command");
|
|
374
|
+
assert(command.effects.every(e => effects.includes(e)), "Service effects exceed the caller's declared effects");
|
|
375
|
+
return this.runCommand(command.name, (args.args ?? {}), [...stack, id]);
|
|
376
|
+
}
|
|
377
|
+
const effect = { read_file: "filesystem.read", list_files: "filesystem.read", write_file: "filesystem.write", run_command: "process", fetch: "network" }[op];
|
|
378
|
+
assert(effect && effects.includes(effect), `Operation ${op} exceeds callback effects`);
|
|
379
|
+
this.permission(owned.record, effect);
|
|
380
|
+
assert(this.options.broker, "Host operations require an active conversation");
|
|
381
|
+
assert(typeof args.reason === "string" && args.reason.trim().length >= 4, "Host operations need an honest reason");
|
|
382
|
+
return this.options.broker(op, args, signal);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async execute(name, args) {
|
|
386
|
+
const tool = this.tools.get(name);
|
|
387
|
+
assert(tool, "Unknown plugin tool");
|
|
388
|
+
validateValue(args, tool.definition.function.parameters, "arguments");
|
|
389
|
+
const result = await this.invoke(tool, args, tool.effects);
|
|
390
|
+
return (typeof result === "string" ? result : JSON.stringify(result ?? null)).slice(0, 200_000);
|
|
391
|
+
}
|
|
392
|
+
async runCommand(name, args = {}, stack = []) {
|
|
393
|
+
const command = this.commands.get(name);
|
|
394
|
+
assert(command, "Unknown plugin command");
|
|
395
|
+
validateValue(args, command.parameters, "arguments");
|
|
396
|
+
return this.invoke(command, args, command.effects, stack);
|
|
397
|
+
}
|
|
398
|
+
async guardTool(name, args) {
|
|
399
|
+
const tool = this.tools.get(name);
|
|
400
|
+
if (tool)
|
|
401
|
+
validateValue(args, tool.definition.function.parameters, "arguments");
|
|
402
|
+
for (const hook of this.hooks.filter(h => h.name === "tool.before")) {
|
|
403
|
+
const result = await this.invoke(hook, { name, arguments: args });
|
|
404
|
+
assert(!object(result) || !result.deny, `${hook.record.manifest.id}: ${object(result) ? result.reason ?? "Tool denied by plugin" : "Tool denied"}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async requestSuffix(value) {
|
|
408
|
+
let text = "";
|
|
409
|
+
for (const hook of this.hooks.filter(h => h.name === "request.before")) {
|
|
410
|
+
const result = await this.invoke(hook, value);
|
|
411
|
+
if (object(result)) {
|
|
412
|
+
assert(!result.deny, String(result.reason ?? "Request denied by plugin"));
|
|
413
|
+
if (typeof result.systemSuffix === "string")
|
|
414
|
+
text += `\n[Plugin ${hook.record.manifest.id}]\n${result.systemSuffix.slice(0, 32_000)}`;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return text;
|
|
418
|
+
}
|
|
419
|
+
async emit(name, value) {
|
|
420
|
+
for (const listener of this.events.filter(h => h.name === name))
|
|
421
|
+
try {
|
|
422
|
+
await this.invoke(listener, value);
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
this.report(listener.record.manifest.id, e.message);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
composePrompt(base, role) {
|
|
429
|
+
if (["confirm_model", "reviewer"].includes(role))
|
|
430
|
+
return { text: base, sections: [] };
|
|
431
|
+
const active = this.prompts.filter(p => !p.roles || p.roles.includes(role)).sort((a, b) => ({ persona: 0, rules: 1, context: 2 }[a.slot ?? "rules"] - { persona: 0, rules: 1, context: 2 }[b.slot ?? "rules"]) || (a.order ?? 0) - (b.order ?? 0) || `${a.pluginId}/${a.id}`.localeCompare(`${b.pluginId}/${b.id}`));
|
|
432
|
+
const replacement = active.find(p => p.replace && this.providers.persona === `${p.pluginId}/${p.id}`);
|
|
433
|
+
if (base.includes("[Plugin section: "))
|
|
434
|
+
return { text: base, sections: [] };
|
|
435
|
+
const sections = active.filter(p => !p.replace).map(p => ({ id: `${p.pluginId}/${p.id}`, text: p.text ?? "" }));
|
|
436
|
+
if (this.skills.length) {
|
|
437
|
+
sections.push({ id: "skills/catalog", text: "Available skills (load by writing /skill <id> in the request):\n" + this.skills.map(s => `${s.id}: ${s.description}`).join("\n") });
|
|
438
|
+
for (const match of (this.options.scope.request ?? "").matchAll(/\/skill\s+([a-zA-Z0-9_.\/-]+)/g)) {
|
|
439
|
+
const skill = this.skills.find(s => s.id === match[1]);
|
|
440
|
+
if (skill)
|
|
441
|
+
sections.push({ id: skill.id, text: skill.text });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return { text: (replacement?.text ?? base) + sections.map(s => `\n\n[Plugin section: ${s.id}]\n${s.text}`).join(""), sections };
|
|
445
|
+
}
|
|
446
|
+
effectiveUI(platform) { return this.ui.filter(u => (!u.platforms || u.platforms.includes(platform)) && (!EXCLUSIVE.has(u.slot) || this.providers[u.slot] === u.key)).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.key.localeCompare(b.key)); }
|
|
447
|
+
async dispose() { if (this.disposed)
|
|
448
|
+
return; this.disposed = true; for (const worker of this.workers.reverse())
|
|
449
|
+
await worker.dispose(); this.tools.clear(); this.commands.clear(); }
|
|
450
|
+
}
|