@hachej/boring-workspace 0.1.17 → 0.1.20
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 +36 -34
- package/dist/{FileTree-Dvaud3jU.js → FileTree-DHVB9rpk.js} +15 -15
- package/dist/{MarkdownEditor-sLkqTXDj.js → MarkdownEditor-L1KDH0bM.js} +1 -1
- package/dist/{WorkspaceLoadingState-zLzh1tGc.js → WorkspaceLoadingState-DYDxUYnx.js} +114 -110
- package/dist/WorkspaceProvider-CDPaAO5u.js +5971 -0
- package/dist/app-front.d.ts +94 -107
- package/dist/app-front.js +243 -233
- package/dist/app-server.d.ts +130 -15
- package/dist/app-server.js +1569 -304
- package/dist/{bootstrapServer-BreQ9QBc.d.ts → createInMemoryBridge-BDxDzihm.d.ts} +11 -26
- package/dist/manifest-CyNNdfYz.d.ts +58 -0
- package/dist/plugin.d.ts +199 -0
- package/dist/plugin.js +300 -0
- package/dist/server.d.ts +239 -4
- package/dist/server.js +901 -78
- package/dist/shared.d.ts +4 -112
- package/dist/surface-COYagY2m.d.ts +111 -0
- package/dist/testing.d.ts +19 -1
- package/dist/testing.js +2 -2
- package/dist/{agent-tool-DEtfQPVB.d.ts → ui-bridge-Gfh1MMgl.d.ts} +30 -30
- package/dist/workspace.css +36 -0
- package/dist/workspace.d.ts +165 -120
- package/dist/workspace.js +330 -377
- package/docs/INTERFACES.md +9 -9
- package/docs/PLUGIN_STRUCTURE.md +39 -145
- package/docs/PLUGIN_SYSTEM.md +355 -0
- package/docs/README.md +6 -1
- package/docs/plans/README.md +1 -0
- package/docs/plans/archive/HOT_RELOADABLE_AGENT_PLUGINS_PLAN.md +218 -0
- package/docs/plans/archive/RELOAD_PLUGGABILITY_PLAN.md +174 -0
- package/docs/plans/archive/UNIFIED_PLUGIN_SYSTEM_PLAN.md +769 -0
- package/package.json +11 -5
- package/dist/CommandPalette-CJHuTJlD.js +0 -5716
- package/docs/bridge.md +0 -135
- package/docs/panels.md +0 -102
- package/docs/plugins.md +0 -158
- /package/docs/plans/{MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md → archive/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md} +0 -0
package/dist/app-server.js
CHANGED
|
@@ -5,53 +5,1320 @@ import {
|
|
|
5
5
|
provisionRuntimeWorkspace,
|
|
6
6
|
resolveMode
|
|
7
7
|
} from "@hachej/boring-agent/server";
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
9
|
+
import { dirname as dirname7, join as join7 } from "path";
|
|
10
|
+
import { createRequire as createRequire4 } from "module";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
9
12
|
|
|
10
13
|
// src/server/boringSystemPrompt.ts
|
|
11
|
-
import {
|
|
14
|
+
import { createRequire } from "module";
|
|
12
15
|
import { dirname, join } from "path";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const override = process.env.BORING_DOCS_PATH;
|
|
16
|
+
var require2 = createRequire(import.meta.url);
|
|
17
|
+
function resolveBoringPiRoot(override) {
|
|
18
|
+
if (override === null) return null;
|
|
17
19
|
if (override) return override;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
try {
|
|
21
|
+
return dirname(require2.resolve("@hachej/boring-pi/package.json"));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function buildDocsRefs(boringPiRoot) {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
topic: "Workflow + how-to + full plugin authoring reference",
|
|
30
|
+
path: join(boringPiRoot, "skills/boring-plugin-authoring/SKILL.md")
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
topic: "Panels (registration, dockview, layout)",
|
|
34
|
+
path: join(boringPiRoot, "references/workspace/panels.md")
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
topic: "Bridge / UI control (get_ui_state, exec_ui)",
|
|
38
|
+
path: join(boringPiRoot, "references/workspace/bridge.md")
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
topic: "Server plugins (defineServerPlugin, routes, agent tools)",
|
|
42
|
+
path: join(boringPiRoot, "references/workspace/plugins.md")
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function buildBoringSystemPrompt(opts) {
|
|
47
|
+
const verify = opts.verifyCommand;
|
|
48
|
+
const boringPiRoot = resolveBoringPiRoot(opts.boringPiRootOverride);
|
|
49
|
+
const steps = [];
|
|
50
|
+
let n = 0;
|
|
51
|
+
if (opts.scaffoldCommand) {
|
|
52
|
+
n += 1;
|
|
53
|
+
steps.push(
|
|
54
|
+
`**${n}. Scaffold.** Bash \`${opts.scaffoldCommand} <kebab-name> "$BORING_AGENT_WORKSPACE_ROOT"\` \u2014 writes canonical files under \`$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/<kebab-name>/\`. Read the generated \`package.json\` + \`front/index.tsx\`. Do NOT skip this or write from memory. Never \`cd\` to a parent repo or write plugins outside \`$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/\`.`
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
n += 1;
|
|
58
|
+
steps.push(
|
|
59
|
+
`**${n}. Read the \`boring-plugin-authoring\` skill** from the \`<location>\` listed under \`<available_skills>\` for the canonical \`package.json\` + \`front/index.tsx\` shape.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
n += 1;
|
|
63
|
+
steps.push(
|
|
64
|
+
opts.scaffoldCommand ? `**${n}. Edit the generated files to implement the request.** Keep the scaffold imports, \`definePlugin\` shape, and manifest layout; replace only placeholder content/ids/labels with the real implementation.` : `**${n}. Create or edit the plugin files to implement what the user asked for.** Use the boring-plugin-authoring skill as the canonical source for imports, the \`definePlugin\` call shape, and the manifest layout.`
|
|
65
|
+
);
|
|
66
|
+
n += 1;
|
|
67
|
+
if (verify) {
|
|
68
|
+
steps.push(
|
|
69
|
+
`**${n}. Verify.** Bash \`${verify} <kebab-name> "$BORING_AGENT_WORKSPACE_ROOT"\`. If it warns about empty/missing dirs, your files went to the wrong cwd. Fix issues and re-run until \`OK\`. Use after EVERY edit.`
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
steps.push(
|
|
73
|
+
`**${n}. Verify.** The boring-ui CLI is not available in this host, so do not invent CLI commands. Validate by re-reading the manifest/front files against the boring-plugin-authoring skill, then ask the user to run \`/reload\` and inspect reload diagnostics.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
n += 1;
|
|
77
|
+
steps.push(`**${n}. Ask the user to run \`/reload\`** to publish the change.`);
|
|
78
|
+
const docsBlock = boringPiRoot ? [
|
|
79
|
+
"## boring-ui plugin authoring documentation",
|
|
80
|
+
"Read these only when the user asks to build, modify, or debug a workspace plugin. Use your `read` tool with the absolute path; the agent runtime guarantees these files exist on the host:",
|
|
81
|
+
...buildDocsRefs(boringPiRoot).map((r) => `- ${r.topic}: ${r.path}`),
|
|
82
|
+
"Follow .md cross-references when present (e.g. SKILL.md may link to a reference doc \u2014 read both)."
|
|
83
|
+
].join("\n") : [
|
|
84
|
+
"## boring-ui plugin authoring documentation",
|
|
85
|
+
"The `boring-plugin-authoring` skill listed under `<available_skills>` is the authoritative reference (read its `<location>`). Additional reference docs (`panels.md`, `bridge.md`, `plugins.md`) are unavailable on this host \u2014 `@hachej/boring-pi` is not installed."
|
|
86
|
+
].join("\n");
|
|
87
|
+
return [
|
|
88
|
+
"You are operating inside boring-ui. Workspace root: `$BORING_AGENT_WORKSPACE_ROOT`; plugin files go under `$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/<name>/`.",
|
|
89
|
+
[
|
|
90
|
+
"## Plugin authoring \u2014 required workflow",
|
|
91
|
+
"",
|
|
92
|
+
...steps,
|
|
93
|
+
"",
|
|
94
|
+
"**Common hallucinations** \u2014 these names DO NOT EXIST in boring-ui and will silently fail; do not write them:",
|
|
95
|
+
"- API factories: `createPlugin`, `defineFrontPlugin`, `defineComponent` \u2014 use `definePlugin({id, panels, commands, ...})` from `@hachej/boring-workspace/plugin`.",
|
|
96
|
+
"- Imperative method names: `registerComponent`, `addPanel`, `registerCommand` (no `Panel`), `registerTab` \u2014 the actual names are `registerPanel`, `registerPanelCommand`, `registerLeftTab`, `registerSurfaceResolver` (and you usually express these declaratively, not as method calls).",
|
|
97
|
+
"- Import paths: `@hachej/boring-pi` (it's a skills package, not for code), `@boring-ui/*`, `@hachej/pi-sdk` \u2014 use `@hachej/boring-workspace/plugin` for front and `@hachej/boring-workspace/server` for server.",
|
|
98
|
+
'- File visualizers: for `.csv`/file-tree opens, import `WORKSPACE_OPEN_PATH_SURFACE_KIND` (and `PaneProps`) from `@hachej/boring-workspace/plugin`, read `request.target`, and fetch `/api/v1/files/raw?path=${encodeURIComponent(request.target)}`. Never import these from the root package, use `/workspace/read`, or string kind `"WORKSPACE_OPEN_PATH_SURFACE_KIND"`.',
|
|
99
|
+
"- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, execute }) }`.",
|
|
100
|
+
'- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
|
|
101
|
+
"- Manifest values: `boring.server: true` \u2014 use `false`/omit for hot-reload user plugins, or a relative path string only for advanced boot-time/static server integration.",
|
|
102
|
+
"- File layout: files at the package root, or `src/` / `dist/` / `lib/` subdirectories \u2014 the scaffold's hot-reload layout (`front/index.tsx`, optional `agent/index.ts` declared in `pi.extensions`) is the one the workspace refreshes on `/reload`.",
|
|
103
|
+
"- Hot-reload agent tools: do NOT put them in `.pi/extensions/<name>/server/index.ts`; use `pi.extensions` instead. `boring.server` requires static composition plus process restart."
|
|
104
|
+
].join("\n"),
|
|
105
|
+
docsBlock
|
|
106
|
+
].join("\n\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/server/agentPlugins/manager.ts
|
|
110
|
+
import { createHash } from "crypto";
|
|
111
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
112
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve3 } from "path";
|
|
113
|
+
|
|
114
|
+
// src/shared/plugins/manifest.ts
|
|
115
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
116
|
+
var PLUGIN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
117
|
+
function isValidBoringPluginId(id) {
|
|
118
|
+
return typeof id === "string" && id.length > 0 && PLUGIN_ID_RE.test(id);
|
|
119
|
+
}
|
|
120
|
+
function isSafePluginRelativePath(value) {
|
|
121
|
+
return typeof value === "string" && value.length > 0 && value !== "." && !value.includes("\0") && !value.includes("\\") && !value.startsWith("/") && !value.startsWith("//") && !/^[A-Za-z]:[\\/]/.test(value) && !value.split("/").includes("..");
|
|
122
|
+
}
|
|
123
|
+
function issue(code, field, message) {
|
|
124
|
+
return { code, field, message };
|
|
125
|
+
}
|
|
126
|
+
function isRecord(value) {
|
|
127
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
function validateStringArray(issues, value, field, pathLike) {
|
|
130
|
+
if (value === void 0) return;
|
|
131
|
+
if (!Array.isArray(value)) {
|
|
132
|
+
issues.push(issue("INVALID_FIELD", field, `${field} must be an array`));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
value.forEach((entry, index) => {
|
|
136
|
+
const itemField = `${field}[${index}]`;
|
|
137
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
138
|
+
issues.push(issue("INVALID_FIELD", itemField, `${itemField} must be a non-empty string`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (pathLike && !isSafePluginRelativePath(entry)) {
|
|
142
|
+
issues.push(issue("INVALID_PATH", itemField, `${itemField} must be a safe relative path`));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
var REMOVED_BORING_UI_FIELDS = ["outputs", "panels", "commands", "leftTabs", "surfaceResolvers", "providers", "bindings", "catalogs"];
|
|
147
|
+
function validateBoringField(issues, boring) {
|
|
148
|
+
if (boring === void 0) return void 0;
|
|
149
|
+
if (!isRecord(boring)) {
|
|
150
|
+
issues.push(issue("INVALID_FIELD", "boring", "boring must be an object when provided"));
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
for (const field of REMOVED_BORING_UI_FIELDS) {
|
|
154
|
+
if (boring[field] !== void 0) {
|
|
155
|
+
issues.push(issue(
|
|
156
|
+
"INVALID_FIELD",
|
|
157
|
+
`boring.${field}`,
|
|
158
|
+
`boring.${field} is not supported; declare front contributions in boring.front via definePlugin({ ... })`
|
|
159
|
+
));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (boring.id !== void 0) {
|
|
163
|
+
issues.push(issue("INVALID_FIELD", "boring.id", "boring.id is not supported; package discovery identity comes from package.json#name"));
|
|
164
|
+
}
|
|
165
|
+
const front = boring.front;
|
|
166
|
+
if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
|
|
167
|
+
issues.push(issue("INVALID_PATH", "boring.front", "boring.front must be a safe relative path"));
|
|
168
|
+
}
|
|
169
|
+
const server = boring.server;
|
|
170
|
+
if (server !== void 0 && server !== false && (typeof server !== "string" || !isSafePluginRelativePath(server))) {
|
|
171
|
+
issues.push(issue("INVALID_PATH", "boring.server", "boring.server must be a safe relative path or false"));
|
|
172
|
+
}
|
|
173
|
+
if (boring.label !== void 0 && typeof boring.label !== "string") {
|
|
174
|
+
issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
...typeof boring.front === "string" ? { front: boring.front } : {},
|
|
178
|
+
...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
|
|
179
|
+
...typeof boring.label === "string" ? { label: boring.label } : {}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
var REMOTE_PI_PACKAGE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
|
|
183
|
+
function isRemotePiPackageSource(value) {
|
|
184
|
+
return REMOTE_PI_PACKAGE_PREFIXES.some((prefix) => value.startsWith(prefix));
|
|
185
|
+
}
|
|
186
|
+
function isSafePiPackageSource(value) {
|
|
187
|
+
if (value.length === 0) return false;
|
|
188
|
+
if (isRemotePiPackageSource(value)) return true;
|
|
189
|
+
const path = value.startsWith("file:") ? value.slice("file:".length) : value;
|
|
190
|
+
if (path === "." || path === "./") return true;
|
|
191
|
+
const normalized = path.startsWith("./") ? path.slice(2) : path;
|
|
192
|
+
return isSafePluginRelativePath(normalized);
|
|
193
|
+
}
|
|
194
|
+
function validatePiPackages(issues, value) {
|
|
195
|
+
if (value === void 0) return;
|
|
196
|
+
if (!Array.isArray(value)) {
|
|
197
|
+
issues.push(issue("INVALID_FIELD", "pi.packages", "pi.packages must be an array when provided"));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
value.forEach((entry, index) => {
|
|
201
|
+
const field = `pi.packages[${index}]`;
|
|
202
|
+
if (typeof entry === "string") {
|
|
203
|
+
if (!isSafePiPackageSource(entry)) {
|
|
204
|
+
issues.push(issue("INVALID_PATH", field, `${field} must be a safe package source`));
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!isRecord(entry)) {
|
|
209
|
+
issues.push(issue("INVALID_FIELD", field, `${field} must be a string or package source object`));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (typeof entry.source !== "string" || entry.source.length === 0) {
|
|
213
|
+
issues.push(issue("INVALID_FIELD", `${field}.source`, `${field}.source must be a non-empty string`));
|
|
214
|
+
} else if (!isSafePiPackageSource(entry.source)) {
|
|
215
|
+
issues.push(issue("INVALID_PATH", `${field}.source`, `${field}.source must be a safe package source`));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function validatePiField(issues, pi) {
|
|
220
|
+
if (pi === void 0) return void 0;
|
|
221
|
+
if (!isRecord(pi)) {
|
|
222
|
+
issues.push(issue("INVALID_FIELD", "pi", "pi must be an object when provided"));
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
validateStringArray(issues, pi.extensions, "pi.extensions", true);
|
|
226
|
+
validateStringArray(issues, pi.skills, "pi.skills", true);
|
|
227
|
+
validatePiPackages(issues, pi.packages);
|
|
228
|
+
if (pi.systemPrompt !== void 0 && typeof pi.systemPrompt !== "string") {
|
|
229
|
+
issues.push(issue("INVALID_FIELD", "pi.systemPrompt", "pi.systemPrompt must be a string when provided"));
|
|
230
|
+
}
|
|
231
|
+
return pi;
|
|
232
|
+
}
|
|
233
|
+
function validateBoringPluginManifest(raw) {
|
|
234
|
+
const issues = [];
|
|
235
|
+
if (!isRecord(raw)) {
|
|
236
|
+
return {
|
|
237
|
+
valid: false,
|
|
238
|
+
issues: [issue("INVALID_FIELD", "<root>", "package.json manifest must be an object")]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (raw.name !== void 0 && typeof raw.name !== "string") {
|
|
242
|
+
issues.push(issue("INVALID_FIELD", "name", "name must be a string when provided"));
|
|
243
|
+
}
|
|
244
|
+
if (raw.version !== void 0 && typeof raw.version !== "string") {
|
|
245
|
+
issues.push(issue("INVALID_VERSION", "version", "version must be a string when provided"));
|
|
246
|
+
} else if (typeof raw.version === "string" && raw.version.length > 0 && !SEMVER_RE.test(raw.version)) {
|
|
247
|
+
issues.push(issue("INVALID_VERSION", "version", "version must be a valid semver string"));
|
|
248
|
+
}
|
|
249
|
+
const boring = validateBoringField(issues, raw.boring);
|
|
250
|
+
const pi = validatePiField(issues, raw.pi);
|
|
251
|
+
if (!boring && !pi) {
|
|
252
|
+
issues.push(issue("MISSING_REQUIRED_FIELD", "boring|pi", "package.json must include boring and/or pi plugin metadata"));
|
|
253
|
+
}
|
|
254
|
+
if (issues.length > 0) return { valid: false, issues };
|
|
255
|
+
return {
|
|
256
|
+
valid: true,
|
|
257
|
+
packageJson: {
|
|
258
|
+
...typeof raw.name === "string" ? { name: raw.name } : {},
|
|
259
|
+
...typeof raw.version === "string" ? { version: raw.version } : {},
|
|
260
|
+
...boring ? { boring } : {},
|
|
261
|
+
...pi ? { pi } : {}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/server/agentPlugins/scan.ts
|
|
267
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
|
|
268
|
+
import { basename, dirname as dirname3, join as join2, resolve as resolve2 } from "path";
|
|
269
|
+
|
|
270
|
+
// src/server/agentPlugins/pluginPaths.ts
|
|
271
|
+
import { existsSync, realpathSync } from "fs";
|
|
272
|
+
import { dirname as dirname2, isAbsolute, relative, resolve } from "path";
|
|
273
|
+
function isInsideRoot(rootReal, targetReal) {
|
|
274
|
+
const rel = relative(rootReal, targetReal);
|
|
275
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
276
|
+
}
|
|
277
|
+
function nearestExistingAncestor(path, rootDir) {
|
|
278
|
+
let current = path;
|
|
279
|
+
const root = resolve(rootDir);
|
|
280
|
+
while (!existsSync(current)) {
|
|
281
|
+
const parent = dirname2(current);
|
|
282
|
+
if (parent === current) return void 0;
|
|
283
|
+
if (!isInsideRoot(root, parent) && parent !== root) return void 0;
|
|
284
|
+
current = parent;
|
|
285
|
+
}
|
|
286
|
+
return current;
|
|
287
|
+
}
|
|
288
|
+
function resolveContainedPluginPath(rootDir, value, options = {}) {
|
|
289
|
+
if (!value || !isSafePluginRelativePath(value)) return void 0;
|
|
290
|
+
if (!existsSync(rootDir)) return void 0;
|
|
291
|
+
const root = resolve(rootDir);
|
|
292
|
+
const resolved = resolve(root, value);
|
|
293
|
+
const rootReal = realpathSync(root);
|
|
294
|
+
const existing = nearestExistingAncestor(resolved, root);
|
|
295
|
+
if (!existing) return void 0;
|
|
296
|
+
const existingReal = realpathSync(existing);
|
|
297
|
+
if (!isInsideRoot(rootReal, existingReal)) return void 0;
|
|
298
|
+
if (!existsSync(resolved)) return options.mustExist ? void 0 : resolved;
|
|
299
|
+
const resolvedReal = realpathSync(resolved);
|
|
300
|
+
if (!isInsideRoot(rootReal, resolvedReal)) return void 0;
|
|
301
|
+
return resolvedReal;
|
|
302
|
+
}
|
|
303
|
+
function resolveSafePluginEntryPath({
|
|
304
|
+
rootDir,
|
|
305
|
+
explicit,
|
|
306
|
+
conventions,
|
|
307
|
+
field,
|
|
308
|
+
manifestPath
|
|
309
|
+
}) {
|
|
310
|
+
if (explicit === false) return null;
|
|
311
|
+
if (explicit !== void 0) {
|
|
312
|
+
if (typeof explicit !== "string" || !isSafePluginRelativePath(explicit)) {
|
|
313
|
+
throw new Error(`${field}: ${JSON.stringify(explicit)} must be a safe relative path inside the plugin root`);
|
|
314
|
+
}
|
|
315
|
+
const path = resolveContainedPluginPath(rootDir, explicit, { mustExist: true });
|
|
316
|
+
if (!path) {
|
|
317
|
+
const resolved = resolve(rootDir, explicit);
|
|
318
|
+
if (!existsSync(resolved)) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`boring plugin entry declared but not found: ${resolved}
|
|
321
|
+
declared in: ${manifestPath}#boring`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`${field}: resolved path escapes plugin root: ${explicit}`);
|
|
325
|
+
}
|
|
326
|
+
return path;
|
|
327
|
+
}
|
|
328
|
+
for (const candidate of conventions) {
|
|
329
|
+
if (!isSafePluginRelativePath(candidate)) {
|
|
330
|
+
throw new Error(`conventional ${field} path ${JSON.stringify(candidate)} is not a safe relative path`);
|
|
331
|
+
}
|
|
332
|
+
const path = resolveContainedPluginPath(rootDir, candidate, { mustExist: true });
|
|
333
|
+
if (path) return path;
|
|
334
|
+
if (existsSync(resolve(rootDir, candidate))) {
|
|
335
|
+
throw new Error(`conventional ${field} path escapes plugin root: ${candidate}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/server/agentPlugins/scan.ts
|
|
342
|
+
function pluginIdFromPackageJson(pkg, rootDir) {
|
|
343
|
+
const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : void 0;
|
|
344
|
+
return (name ?? rootDir.split(/[\\/]/).at(-1) ?? "plugin").replace(/^@/, "").replaceAll("/", "-");
|
|
345
|
+
}
|
|
346
|
+
function safePluginIdFromPackageJson(pkg, rootDir) {
|
|
347
|
+
const id = pluginIdFromPackageJson(pkg, rootDir);
|
|
348
|
+
return isValidBoringPluginId(id) ? id : void 0;
|
|
349
|
+
}
|
|
350
|
+
function parsePackageJson(rootDir) {
|
|
351
|
+
return JSON.parse(readFileSync(join2(rootDir, "package.json"), "utf8"));
|
|
352
|
+
}
|
|
353
|
+
function hasPluginMetadata(pkg) {
|
|
354
|
+
return pkg.boring !== void 0 || pkg.pi !== void 0;
|
|
355
|
+
}
|
|
356
|
+
function resolvePluginPath(rootDir, value, options = {}) {
|
|
357
|
+
return resolveContainedPluginPath(rootDir, value, options);
|
|
358
|
+
}
|
|
359
|
+
function resolvePluginPaths(rootDir, values) {
|
|
360
|
+
return (values ?? []).map((value) => resolvePluginPath(rootDir, value)).filter((value) => Boolean(value));
|
|
361
|
+
}
|
|
362
|
+
function pathPreflightIssue(rootDir, value, field, options = {}) {
|
|
363
|
+
if (!value || !isSafePluginRelativePath(value)) return void 0;
|
|
364
|
+
const containedPath = resolveContainedPluginPath(rootDir, value);
|
|
365
|
+
if (!containedPath) {
|
|
366
|
+
return {
|
|
367
|
+
pluginDir: rootDir,
|
|
368
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
369
|
+
message: `${field}: resolved path escapes plugin root`
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (options.mustExist && !existsSync2(containedPath)) {
|
|
373
|
+
return {
|
|
374
|
+
pluginDir: rootDir,
|
|
375
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
376
|
+
message: `${field}: declared path does not exist: ${value}`
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
function packagePathContainmentIssues(rootDir, pkg) {
|
|
382
|
+
const issues = [];
|
|
383
|
+
const boring = pkg.boring;
|
|
384
|
+
const pi = pkg.pi;
|
|
385
|
+
const pluginId = safePluginIdFromPackageJson(pkg, rootDir);
|
|
386
|
+
const push = (issue2) => {
|
|
387
|
+
if (issue2) issues.push({ ...issue2, ...pluginId ? { pluginId } : {} });
|
|
388
|
+
};
|
|
389
|
+
push(pathPreflightIssue(rootDir, boring?.front, "boring.front", { mustExist: true }));
|
|
390
|
+
if (boring?.server !== false && boring?.server !== void 0) {
|
|
391
|
+
push(pathPreflightIssue(rootDir, boring.server, "boring.server"));
|
|
392
|
+
}
|
|
393
|
+
pi?.extensions?.forEach((value, index) => push(pathPreflightIssue(rootDir, value, `pi.extensions[${index}]`)));
|
|
394
|
+
pi?.skills?.forEach((value, index) => push(pathPreflightIssue(rootDir, value, `pi.skills[${index}]`)));
|
|
395
|
+
return issues;
|
|
396
|
+
}
|
|
397
|
+
function discoverBoringPluginDirs(pluginDirs) {
|
|
398
|
+
const out = /* @__PURE__ */ new Set();
|
|
399
|
+
const missingPackageJson = [];
|
|
400
|
+
for (const raw of pluginDirs) {
|
|
401
|
+
const dir = resolve2(raw);
|
|
402
|
+
if (!existsSync2(dir)) continue;
|
|
403
|
+
const info = statSync(dir);
|
|
404
|
+
if (!info.isDirectory()) continue;
|
|
405
|
+
const hasPackageJson = existsSync2(join2(dir, "package.json"));
|
|
406
|
+
const childPackageDirs = [];
|
|
407
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
408
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
409
|
+
const child = join2(dir, entry.name);
|
|
410
|
+
if (existsSync2(join2(child, "package.json"))) childPackageDirs.push(child);
|
|
411
|
+
}
|
|
412
|
+
if (hasPackageJson) out.add(dir);
|
|
413
|
+
for (const child of childPackageDirs) out.add(child);
|
|
414
|
+
if (!hasPackageJson && childPackageDirs.length === 0 && basename(dir) !== "extensions") {
|
|
415
|
+
missingPackageJson.push(dir);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { dirs: [...out].sort(), missingPackageJson: [...new Set(missingPackageJson)].sort() };
|
|
419
|
+
}
|
|
420
|
+
function scanBoringPlugins(pluginDirs) {
|
|
421
|
+
const errors = [];
|
|
422
|
+
const plugins = [];
|
|
423
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
424
|
+
const discovered = discoverBoringPluginDirs(pluginDirs);
|
|
425
|
+
for (const pluginDir of discovered.missingPackageJson) {
|
|
426
|
+
errors.push({ pluginDir, code: "MISSING_PACKAGE_JSON", message: "package.json is missing" });
|
|
427
|
+
}
|
|
428
|
+
for (const rootDir of discovered.dirs) {
|
|
429
|
+
let raw;
|
|
430
|
+
try {
|
|
431
|
+
raw = parsePackageJson(rootDir);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
errors.push({
|
|
434
|
+
pluginDir: rootDir,
|
|
435
|
+
code: "INVALID_PACKAGE_JSON",
|
|
436
|
+
message: error instanceof Error ? error.message : "invalid package.json"
|
|
437
|
+
});
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (!hasPluginMetadata(raw)) continue;
|
|
441
|
+
const result = validateBoringPluginManifest(raw);
|
|
442
|
+
if (!result.valid) {
|
|
443
|
+
const pluginId = safePluginIdFromPackageJson(raw, rootDir);
|
|
444
|
+
for (const issue2 of result.issues) {
|
|
445
|
+
errors.push({
|
|
446
|
+
pluginDir: rootDir,
|
|
447
|
+
...pluginId ? { pluginId } : {},
|
|
448
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
449
|
+
message: `${issue2.field}: ${issue2.message}`
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const id = pluginIdFromPackageJson(result.packageJson, rootDir);
|
|
455
|
+
let canAddPlugin = true;
|
|
456
|
+
if (!isValidBoringPluginId(id)) {
|
|
457
|
+
errors.push({
|
|
458
|
+
pluginDir: rootDir,
|
|
459
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
460
|
+
message: `effective plugin id "${id}" must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash`
|
|
461
|
+
});
|
|
462
|
+
canAddPlugin = false;
|
|
463
|
+
} else {
|
|
464
|
+
const previous = seenIds.get(id);
|
|
465
|
+
if (previous) {
|
|
466
|
+
errors.push({
|
|
467
|
+
pluginDir: rootDir,
|
|
468
|
+
pluginId: id,
|
|
469
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
470
|
+
message: `duplicate plugin id "${id}" also declared by ${previous}`
|
|
471
|
+
});
|
|
472
|
+
const previousPluginIndex = plugins.findIndex((plugin) => plugin.id === id);
|
|
473
|
+
if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
|
|
474
|
+
canAddPlugin = false;
|
|
475
|
+
} else {
|
|
476
|
+
seenIds.set(id, rootDir);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const containmentIssues = packagePathContainmentIssues(rootDir, result.packageJson);
|
|
480
|
+
if (containmentIssues.length > 0) {
|
|
481
|
+
errors.push(...containmentIssues);
|
|
482
|
+
canAddPlugin = false;
|
|
483
|
+
}
|
|
484
|
+
if (!canAddPlugin) continue;
|
|
485
|
+
const pkg = result.packageJson;
|
|
486
|
+
const boring = pkg.boring ?? {};
|
|
487
|
+
const pi = pkg.pi;
|
|
488
|
+
const frontPath = resolvePluginPath(rootDir, boring.front, { mustExist: true });
|
|
489
|
+
const serverPath = typeof boring.server === "string" ? resolvePluginPath(rootDir, boring.server) : void 0;
|
|
490
|
+
const version = pkg.version ?? "0.0.0";
|
|
491
|
+
const extensionPaths = resolvePluginPaths(rootDir, pi?.extensions);
|
|
492
|
+
const skillPaths = resolvePluginPaths(rootDir, pi?.skills);
|
|
493
|
+
plugins.push({
|
|
494
|
+
id,
|
|
495
|
+
rootDir,
|
|
496
|
+
version,
|
|
497
|
+
boring,
|
|
498
|
+
...pi ? { pi } : {},
|
|
499
|
+
...frontPath ? { frontPath, frontUrl: `/@fs/${frontPath}` } : {},
|
|
500
|
+
...serverPath ? { serverPath } : {},
|
|
501
|
+
...extensionPaths.length > 0 ? { extensionPaths } : {},
|
|
502
|
+
...skillPaths.length > 0 ? { skillPaths } : {}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const preflight = { ok: errors.length === 0, errors };
|
|
506
|
+
return { preflight, plugins };
|
|
507
|
+
}
|
|
508
|
+
function preflightBoringPlugins(pluginDirs) {
|
|
509
|
+
return scanBoringPlugins(pluginDirs).preflight;
|
|
510
|
+
}
|
|
511
|
+
function pluginRootFromExtensionPath(extensionPath) {
|
|
512
|
+
const resolved = resolve2(extensionPath);
|
|
513
|
+
const agentDir = dirname3(resolved);
|
|
514
|
+
if (basename(agentDir) !== "agent") {
|
|
515
|
+
throw new Error(`boring plugin extension path must follow <pluginRoot>/agent/<entry>: ${extensionPath}`);
|
|
516
|
+
}
|
|
517
|
+
return dirname3(agentDir);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/server/agentPlugins/signatureCache.ts
|
|
521
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync } from "fs";
|
|
522
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
523
|
+
var PLUGIN_SIGNATURE_CACHE_FILE = ".boring-signature.json";
|
|
524
|
+
function pluginFileSignature(path) {
|
|
525
|
+
if (!path || !existsSync3(path)) return "missing";
|
|
526
|
+
const stat = statSync2(path);
|
|
527
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
528
|
+
}
|
|
529
|
+
function cachePath(pluginRootDir) {
|
|
530
|
+
return join3(pluginRootDir, PLUGIN_SIGNATURE_CACHE_FILE);
|
|
531
|
+
}
|
|
532
|
+
function writePluginSignatureCache(pluginRootDir, payload) {
|
|
533
|
+
const full = {
|
|
534
|
+
version: 1,
|
|
535
|
+
serverSignature: payload.serverSignature,
|
|
536
|
+
loadedAt: payload.loadedAt ?? Date.now()
|
|
537
|
+
};
|
|
538
|
+
const path = cachePath(pluginRootDir);
|
|
539
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
540
|
+
writeFileSync(path, `${JSON.stringify(full, null, 2)}
|
|
541
|
+
`, "utf8");
|
|
542
|
+
}
|
|
543
|
+
function clearPluginSignatureCache(pluginRootDir) {
|
|
544
|
+
const path = cachePath(pluginRootDir);
|
|
545
|
+
if (existsSync3(path)) rmSync(path, { force: true });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/server/agentPlugins/manager.ts
|
|
549
|
+
function preflightErrorId(pluginDir) {
|
|
550
|
+
return `preflight-${createHash("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
|
|
551
|
+
}
|
|
552
|
+
function directorySignature(root) {
|
|
553
|
+
if (!root || !existsSync4(root)) return "missing";
|
|
554
|
+
const hash = createHash("sha256");
|
|
555
|
+
const visited = /* @__PURE__ */ new Set();
|
|
556
|
+
let rootReal;
|
|
557
|
+
try {
|
|
558
|
+
rootReal = realpathSync2(root);
|
|
559
|
+
} catch {
|
|
560
|
+
return "missing";
|
|
561
|
+
}
|
|
562
|
+
visited.add(rootReal);
|
|
563
|
+
let count = 0;
|
|
564
|
+
const visit = (dir, depth) => {
|
|
565
|
+
if (depth > 8 || count > 5e4) return;
|
|
566
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").sort((a, b) => a.name.localeCompare(b.name));
|
|
567
|
+
for (const entry of entries) {
|
|
568
|
+
count++;
|
|
569
|
+
const path = join4(dir, entry.name);
|
|
570
|
+
const rel = relative2(root, path);
|
|
571
|
+
const stat = lstatSync(path);
|
|
572
|
+
if (stat.isSymbolicLink()) {
|
|
573
|
+
let target;
|
|
574
|
+
try {
|
|
575
|
+
target = realpathSync2(path);
|
|
576
|
+
} catch {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (visited.has(target)) {
|
|
580
|
+
hash.update(rel);
|
|
581
|
+
hash.update("symlink-cycle");
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
visited.add(target);
|
|
585
|
+
const targetStat = statSync3(target);
|
|
586
|
+
hash.update(rel);
|
|
587
|
+
hash.update("symlink:");
|
|
588
|
+
hash.update(target);
|
|
589
|
+
if (targetStat.isDirectory()) visit(target, depth + 1);
|
|
590
|
+
else if (targetStat.isFile()) {
|
|
591
|
+
hash.update(String(targetStat.mtimeMs));
|
|
592
|
+
hash.update(String(targetStat.size));
|
|
593
|
+
}
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
hash.update(rel);
|
|
597
|
+
hash.update(String(stat.mtimeMs));
|
|
598
|
+
hash.update(String(stat.size));
|
|
599
|
+
if (stat.isDirectory()) {
|
|
600
|
+
visit(path, depth + 1);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
visit(root, 0);
|
|
605
|
+
return hash.digest("hex");
|
|
606
|
+
}
|
|
607
|
+
function pluginSignature(plugin) {
|
|
608
|
+
return createHash("sha256").update(JSON.stringify(plugin.boring)).update(JSON.stringify(plugin.pi ?? {})).update(plugin.version).update(plugin.frontPath ?? "").update(pluginFileSignature(plugin.frontPath)).update(directorySignature(plugin.frontPath ? dirname5(plugin.frontPath) : void 0)).update(directorySignature(join4(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname5(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
|
|
609
|
+
}
|
|
610
|
+
function computeRequiresRestart(previous, next) {
|
|
611
|
+
if (!previous) return [];
|
|
612
|
+
const prevHasServer = !!previous.serverPath;
|
|
613
|
+
const nextHasServer = !!next.serverPath;
|
|
614
|
+
if (!prevHasServer && !nextHasServer) return [];
|
|
615
|
+
if (prevHasServer !== nextHasServer) return ["routes", "agentTools"];
|
|
616
|
+
const nextSig = pluginFileSignature(next.serverPath);
|
|
617
|
+
if (previous.serverSignature === nextSig) return [];
|
|
618
|
+
return ["routes", "agentTools"];
|
|
619
|
+
}
|
|
620
|
+
var BoringPluginAssetManager = class {
|
|
621
|
+
pluginDirs;
|
|
622
|
+
errorRoot;
|
|
623
|
+
loaded = /* @__PURE__ */ new Map();
|
|
624
|
+
revisions = /* @__PURE__ */ new Map();
|
|
625
|
+
listeners = /* @__PURE__ */ new Set();
|
|
626
|
+
loading = null;
|
|
627
|
+
reloadQueued = false;
|
|
628
|
+
constructor(options) {
|
|
629
|
+
this.pluginDirs = options.pluginDirs;
|
|
630
|
+
this.errorRoot = options.errorRoot ?? join4(process.cwd(), ".pi", "extensions");
|
|
631
|
+
}
|
|
632
|
+
preflight() {
|
|
633
|
+
return preflightBoringPlugins(this.pluginDirs);
|
|
634
|
+
}
|
|
635
|
+
list() {
|
|
636
|
+
return [...this.loaded.values()].map((plugin) => ({
|
|
637
|
+
id: plugin.id,
|
|
638
|
+
boring: plugin.boring,
|
|
639
|
+
...plugin.pi ? { pi: plugin.pi } : {},
|
|
640
|
+
version: plugin.version,
|
|
641
|
+
revision: plugin.revision,
|
|
642
|
+
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
getError(pluginId) {
|
|
646
|
+
const path = this.errorPath(pluginId);
|
|
647
|
+
if (!path || !existsSync4(path)) return null;
|
|
648
|
+
return readFileSync3(path, "utf8");
|
|
649
|
+
}
|
|
650
|
+
subscribe(listener) {
|
|
651
|
+
this.listeners.add(listener);
|
|
652
|
+
return () => this.listeners.delete(listener);
|
|
653
|
+
}
|
|
654
|
+
async load() {
|
|
655
|
+
if (this.loading) {
|
|
656
|
+
this.reloadQueued = true;
|
|
657
|
+
return this.loading;
|
|
658
|
+
}
|
|
659
|
+
this.loading = this.drainLoads().finally(() => {
|
|
660
|
+
this.loading = null;
|
|
661
|
+
});
|
|
662
|
+
return this.loading;
|
|
663
|
+
}
|
|
664
|
+
async drainLoads() {
|
|
665
|
+
let result;
|
|
666
|
+
do {
|
|
667
|
+
this.reloadQueued = false;
|
|
668
|
+
result = await this.doLoadOnce();
|
|
669
|
+
} while (this.reloadQueued);
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
async doLoadOnce() {
|
|
673
|
+
const scan = scanBoringPlugins(this.pluginDirs);
|
|
674
|
+
const nextPlugins = scan.plugins;
|
|
675
|
+
const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
|
|
676
|
+
const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve3(error.pluginDir)));
|
|
677
|
+
const events = [];
|
|
678
|
+
const errors = [];
|
|
679
|
+
this.collectPreflightErrors(scan.preflight, events, errors);
|
|
680
|
+
for (const id of [...this.loaded.keys()]) {
|
|
681
|
+
if (nextIds.has(id)) continue;
|
|
682
|
+
const previous = this.loaded.get(id);
|
|
683
|
+
if (previous && invalidPluginDirs.has(resolve3(previous.rootDir))) continue;
|
|
684
|
+
const revision = this.bumpRevision(id);
|
|
685
|
+
this.loaded.delete(id);
|
|
686
|
+
if (previous) {
|
|
687
|
+
try {
|
|
688
|
+
clearPluginSignatureCache(previous.rootDir);
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const event = { type: "boring.plugin.unload", id, revision };
|
|
693
|
+
events.push(event);
|
|
694
|
+
this.emit(event);
|
|
695
|
+
}
|
|
696
|
+
for (const plugin of nextPlugins) {
|
|
697
|
+
try {
|
|
698
|
+
const signature = pluginSignature(plugin);
|
|
699
|
+
const previous = this.loaded.get(plugin.id);
|
|
700
|
+
if (previous?.signature === signature) continue;
|
|
701
|
+
const revision = this.bumpRevision(plugin.id);
|
|
702
|
+
const serverSignature = plugin.serverPath ? pluginFileSignature(plugin.serverPath) : null;
|
|
703
|
+
const record = { ...plugin, revision, signature, serverSignature };
|
|
704
|
+
this.loaded.set(plugin.id, record);
|
|
705
|
+
this.clearError(plugin.id);
|
|
706
|
+
try {
|
|
707
|
+
writePluginSignatureCache(plugin.rootDir, { serverSignature });
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
const requiresRestart = computeRequiresRestart(previous, plugin);
|
|
711
|
+
const event = {
|
|
712
|
+
type: "boring.plugin.load",
|
|
713
|
+
id: plugin.id,
|
|
714
|
+
boring: plugin.boring,
|
|
715
|
+
version: plugin.version,
|
|
716
|
+
revision,
|
|
717
|
+
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {},
|
|
718
|
+
...requiresRestart.length > 0 ? { requiresRestart } : {}
|
|
719
|
+
};
|
|
720
|
+
events.push(event);
|
|
721
|
+
this.emit(event);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
const revision = this.bumpRevision(plugin.id);
|
|
724
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
725
|
+
this.writeError(plugin.id, message);
|
|
726
|
+
const event = { type: "boring.plugin.error", id: plugin.id, revision, message };
|
|
727
|
+
errors.push({ id: plugin.id, revision, message });
|
|
728
|
+
events.push(event);
|
|
729
|
+
this.emit(event);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return { loaded: this.list(), events, errors };
|
|
733
|
+
}
|
|
734
|
+
collectPreflightErrors(preflight, events, errors) {
|
|
735
|
+
for (const error of preflight.errors) {
|
|
736
|
+
const id = error.pluginId ?? preflightErrorId(error.pluginDir);
|
|
737
|
+
const revision = this.bumpRevision(id);
|
|
738
|
+
const message = `${error.code}: ${error.message}
|
|
739
|
+
|
|
740
|
+
Plugin dir: ${error.pluginDir}`;
|
|
741
|
+
const loadError = { id, revision, message };
|
|
742
|
+
errors.push(loadError);
|
|
743
|
+
this.writeError(id, message);
|
|
744
|
+
const event = { type: "boring.plugin.error", id, revision, message };
|
|
745
|
+
events.push(event);
|
|
746
|
+
this.emit(event);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
bumpRevision(id) {
|
|
750
|
+
const next = (this.revisions.get(id) ?? 0) + 1;
|
|
751
|
+
this.revisions.set(id, next);
|
|
752
|
+
return next;
|
|
753
|
+
}
|
|
754
|
+
emit(event) {
|
|
755
|
+
for (const listener of [...this.listeners]) {
|
|
756
|
+
try {
|
|
757
|
+
listener(event);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
760
|
+
console.error(`[BoringPluginAssetManager] listener threw on ${event.type} for ${event.id}: ${message}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
errorPath(pluginId) {
|
|
765
|
+
if (!isValidBoringPluginId(pluginId)) return null;
|
|
766
|
+
const root = resolve3(this.errorRoot);
|
|
767
|
+
const path = resolve3(root, pluginId, ".error");
|
|
768
|
+
const rel = relative2(root, path);
|
|
769
|
+
if (rel.startsWith("..") || isAbsolute2(rel)) return null;
|
|
770
|
+
return path;
|
|
771
|
+
}
|
|
772
|
+
writeError(pluginId, message) {
|
|
773
|
+
const path = this.errorPath(pluginId);
|
|
774
|
+
if (!path) return;
|
|
775
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
776
|
+
writeFileSync2(path, message, "utf8");
|
|
777
|
+
}
|
|
778
|
+
clearError(pluginId) {
|
|
779
|
+
const path = this.errorPath(pluginId);
|
|
780
|
+
if (path && existsSync4(path)) rmSync2(path, { force: true });
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/server/agentPlugins/routes.ts
|
|
785
|
+
function collectRestartWarnings(events) {
|
|
786
|
+
const warnings = [];
|
|
787
|
+
for (const event of events) {
|
|
788
|
+
if (event.type !== "boring.plugin.load") continue;
|
|
789
|
+
const surfaces = event.requiresRestart;
|
|
790
|
+
if (!surfaces || surfaces.length === 0) continue;
|
|
791
|
+
warnings.push({
|
|
792
|
+
id: event.id,
|
|
793
|
+
surfaces: [...surfaces],
|
|
794
|
+
message: `${event.id} reloaded \u2014 front bundle is live, but server-side ${surfaces.join(" + ")} were wired at boot and still run the old code. Stop and restart the workspace process (Ctrl-C, then re-run your dev command) to pick up changes.`
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return warnings;
|
|
798
|
+
}
|
|
799
|
+
async function boringPluginRoutes(app, opts) {
|
|
800
|
+
const { manager, rebuildPlugins, enableReloadRoute = true } = opts;
|
|
801
|
+
if (enableReloadRoute) {
|
|
802
|
+
app.post("/api/boring.reload", async (_request, reply) => {
|
|
803
|
+
const scan = await manager.load();
|
|
804
|
+
const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
|
|
805
|
+
const restart_warnings = collectRestartWarnings(scan.events);
|
|
806
|
+
const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
|
|
807
|
+
if (hasFailures) {
|
|
808
|
+
return reply.status(422).send({
|
|
809
|
+
ok: false,
|
|
810
|
+
errors: scan.errors,
|
|
811
|
+
diagnostics: rebuild.diagnostics,
|
|
812
|
+
plugins: scan.loaded,
|
|
813
|
+
// Even on failure, emit warnings for plugins that DID reload
|
|
814
|
+
// — partial-failure tolerance means some loaded successfully.
|
|
815
|
+
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
return reply.send({
|
|
819
|
+
ok: true,
|
|
820
|
+
plugins: scan.loaded,
|
|
821
|
+
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
const listPlugins = async () => manager.list();
|
|
826
|
+
app.get("/api/v1/agent-plugins", listPlugins);
|
|
827
|
+
const getPluginError = async (request, reply) => {
|
|
828
|
+
const error = manager.getError(request.params.id);
|
|
829
|
+
if (error == null) return reply.status(404).send({ error: "not_found" });
|
|
830
|
+
return reply.type("text/plain").send(error);
|
|
831
|
+
};
|
|
832
|
+
app.get("/api/v1/agent-plugins/:id/error", getPluginError);
|
|
833
|
+
app.get("/api/v1/agent-plugins/events", async (request, reply) => {
|
|
834
|
+
reply.hijack();
|
|
835
|
+
const res = reply.raw;
|
|
836
|
+
res.statusCode = 200;
|
|
837
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
838
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
839
|
+
res.setHeader("Connection", "keep-alive");
|
|
840
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
841
|
+
res.flushHeaders?.();
|
|
842
|
+
const write = (event) => {
|
|
843
|
+
try {
|
|
844
|
+
res.write(`event: ${event.type}
|
|
845
|
+
`);
|
|
846
|
+
res.write(`data: ${JSON.stringify(event)}
|
|
847
|
+
|
|
848
|
+
`);
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
for (const plugin of manager.list()) {
|
|
853
|
+
write({
|
|
854
|
+
type: "boring.plugin.load",
|
|
855
|
+
id: plugin.id,
|
|
856
|
+
boring: plugin.boring,
|
|
857
|
+
version: plugin.version,
|
|
858
|
+
revision: plugin.revision,
|
|
859
|
+
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
const unsubscribe = manager.subscribe(write);
|
|
863
|
+
const heartbeat = setInterval(() => {
|
|
864
|
+
try {
|
|
865
|
+
res.write(": heartbeat\n\n");
|
|
866
|
+
} catch {
|
|
867
|
+
}
|
|
868
|
+
}, 25e3);
|
|
869
|
+
request.raw.on("close", () => {
|
|
870
|
+
clearInterval(heartbeat);
|
|
871
|
+
unsubscribe();
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/server/agentPlugins/aggregatePluginPrompts.ts
|
|
877
|
+
function aggregatePluginPrompts(manager) {
|
|
878
|
+
const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
879
|
+
if (prompts.length === 0) return void 0;
|
|
880
|
+
return `# Loaded boring-ui plugin context
|
|
881
|
+
|
|
882
|
+
${prompts.join("\n\n")}`;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/server/agentPlugins/piPackages.ts
|
|
886
|
+
import { resolve as resolve4 } from "path";
|
|
887
|
+
var REMOTE_PI_PACKAGE_PREFIXES2 = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
|
|
888
|
+
function isRemotePiPackageSource2(source) {
|
|
889
|
+
return REMOTE_PI_PACKAGE_PREFIXES2.some((prefix) => source.startsWith(prefix));
|
|
890
|
+
}
|
|
891
|
+
function packageLocalPathFromSource(source) {
|
|
892
|
+
if (isRemotePiPackageSource2(source)) return null;
|
|
893
|
+
return source.startsWith("file:") ? source.slice("file:".length) : source;
|
|
894
|
+
}
|
|
895
|
+
function normalizeLocalPiPackageSource(pluginRoot, source) {
|
|
896
|
+
const localPath = packageLocalPathFromSource(source);
|
|
897
|
+
if (localPath == null) return source;
|
|
898
|
+
if (localPath === "." || localPath === "./") return resolve4(pluginRoot);
|
|
899
|
+
const normalized = localPath.startsWith("./") ? localPath.slice(2) : localPath;
|
|
900
|
+
if (!isSafePluginRelativePath(normalized)) {
|
|
901
|
+
throw new Error(`unsafe Pi package source: ${source}`);
|
|
902
|
+
}
|
|
903
|
+
return resolve4(pluginRoot, normalized);
|
|
904
|
+
}
|
|
905
|
+
function normalizeBoringPluginPiPackageSource(pluginRoot, source) {
|
|
906
|
+
if (typeof source === "string") return normalizeLocalPiPackageSource(pluginRoot, source);
|
|
907
|
+
return {
|
|
908
|
+
source: normalizeLocalPiPackageSource(pluginRoot, source.source),
|
|
909
|
+
...source.extensions ? { extensions: source.extensions } : {},
|
|
910
|
+
...source.skills ? { skills: source.skills } : {},
|
|
911
|
+
...source.prompts ? { prompts: source.prompts } : {},
|
|
912
|
+
...source.themes ? { themes: source.themes } : {}
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function normalizeBoringPluginPiPackages(plugins) {
|
|
916
|
+
return plugins.flatMap(
|
|
917
|
+
(plugin) => (plugin.pi?.packages ?? []).map(
|
|
918
|
+
(source) => normalizeBoringPluginPiPackageSource(plugin.rootDir, source)
|
|
919
|
+
)
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/app/server/pluginEntryResolver.ts
|
|
924
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
925
|
+
import { join as join5, resolve as resolve5 } from "path";
|
|
926
|
+
import { createRequire as createRequire2 } from "module";
|
|
927
|
+
import { pathToFileURL } from "url";
|
|
928
|
+
|
|
929
|
+
// src/server/plugins/piPackages.ts
|
|
930
|
+
import {
|
|
931
|
+
compactPiPackages,
|
|
932
|
+
PI_PACKAGE_RESOURCE_FILTERS
|
|
933
|
+
} from "@hachej/boring-agent/server";
|
|
934
|
+
|
|
935
|
+
// src/server/plugins/defineServerPlugin.ts
|
|
936
|
+
function fail(pluginId, message) {
|
|
937
|
+
throw new Error(`server plugin "${pluginId}": ${message}`);
|
|
938
|
+
}
|
|
939
|
+
function isUrl(value) {
|
|
940
|
+
return value instanceof URL;
|
|
941
|
+
}
|
|
942
|
+
function isPathLike(value) {
|
|
943
|
+
return typeof value === "string" && value.length > 0 || isUrl(value);
|
|
944
|
+
}
|
|
945
|
+
function validateAgentTool(pluginId, tool, index) {
|
|
946
|
+
if (!tool || typeof tool !== "object") {
|
|
947
|
+
fail(pluginId, `agentTools[${index}] must be an object`);
|
|
948
|
+
}
|
|
949
|
+
const candidate = tool;
|
|
950
|
+
if (!candidate.name || typeof candidate.name !== "string") {
|
|
951
|
+
fail(pluginId, `agentTools[${index}].name must be a non-empty string`);
|
|
952
|
+
}
|
|
953
|
+
if (typeof candidate.description !== "string") {
|
|
954
|
+
fail(pluginId, `agentTools[${index}].description must be a string`);
|
|
955
|
+
}
|
|
956
|
+
if (!candidate.parameters || typeof candidate.parameters !== "object") {
|
|
957
|
+
fail(pluginId, `agentTools[${index}].parameters must be an object`);
|
|
958
|
+
}
|
|
959
|
+
if (typeof candidate.execute !== "function") {
|
|
960
|
+
fail(pluginId, `agentTools[${index}].execute must be a function`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function validatePiPackages2(pluginId, piPackages) {
|
|
964
|
+
for (let i = 0; i < piPackages.length; i++) {
|
|
965
|
+
const source = piPackages[i];
|
|
966
|
+
if (typeof source === "string") {
|
|
967
|
+
if (source.length === 0) {
|
|
968
|
+
fail(pluginId, `piPackages[${i}] must be a non-empty string`);
|
|
969
|
+
}
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
973
|
+
fail(pluginId, `piPackages[${i}] must be a string or package source object`);
|
|
974
|
+
}
|
|
975
|
+
const candidate = source;
|
|
976
|
+
if (typeof candidate.source !== "string" || candidate.source.length === 0) {
|
|
977
|
+
fail(pluginId, `piPackages[${i}].source must be a non-empty string`);
|
|
978
|
+
}
|
|
979
|
+
for (const key of PI_PACKAGE_RESOURCE_FILTERS) {
|
|
980
|
+
const value = candidate[key];
|
|
981
|
+
if (value === void 0) continue;
|
|
982
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
|
|
983
|
+
fail(pluginId, `piPackages[${i}].${key} must be a string array when provided`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function validateProvisioning(pluginId, provisioning) {
|
|
989
|
+
if (!provisioning || typeof provisioning !== "object") {
|
|
990
|
+
fail(pluginId, "provisioning must be an object");
|
|
991
|
+
}
|
|
992
|
+
if (provisioning.templateDirs !== void 0) {
|
|
993
|
+
if (!Array.isArray(provisioning.templateDirs)) {
|
|
994
|
+
fail(pluginId, "provisioning.templateDirs must be an array when provided");
|
|
995
|
+
}
|
|
996
|
+
for (let i = 0; i < provisioning.templateDirs.length; i++) {
|
|
997
|
+
const contribution = provisioning.templateDirs[i];
|
|
998
|
+
if (!contribution || typeof contribution !== "object") {
|
|
999
|
+
fail(pluginId, `provisioning.templateDirs[${i}] must be an object`);
|
|
1000
|
+
}
|
|
1001
|
+
if (!contribution.id || typeof contribution.id !== "string") {
|
|
1002
|
+
fail(pluginId, `provisioning.templateDirs[${i}].id must be a non-empty string`);
|
|
1003
|
+
}
|
|
1004
|
+
if (!isPathLike(contribution.path)) {
|
|
1005
|
+
fail(pluginId, `provisioning.templateDirs[${i}].path must be a string or URL`);
|
|
1006
|
+
}
|
|
1007
|
+
if (contribution.target !== void 0 && typeof contribution.target !== "string") {
|
|
1008
|
+
fail(pluginId, `provisioning.templateDirs[${i}].target must be a string when provided`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (provisioning.nodePackages !== void 0) {
|
|
1013
|
+
if (!Array.isArray(provisioning.nodePackages)) {
|
|
1014
|
+
fail(pluginId, "provisioning.nodePackages must be an array when provided");
|
|
1015
|
+
}
|
|
1016
|
+
for (let i = 0; i < provisioning.nodePackages.length; i++) {
|
|
1017
|
+
const spec = provisioning.nodePackages[i];
|
|
1018
|
+
if (!spec || typeof spec !== "object") {
|
|
1019
|
+
fail(pluginId, `provisioning.nodePackages[${i}] must be an object`);
|
|
1020
|
+
}
|
|
1021
|
+
if (!spec.id || typeof spec.id !== "string") {
|
|
1022
|
+
fail(pluginId, `provisioning.nodePackages[${i}].id must be a non-empty string`);
|
|
1023
|
+
}
|
|
1024
|
+
if (!spec.packageName || typeof spec.packageName !== "string") {
|
|
1025
|
+
fail(pluginId, `provisioning.nodePackages[${i}].packageName must be a non-empty string`);
|
|
1026
|
+
}
|
|
1027
|
+
if (!isPathLike(spec.packageRoot)) {
|
|
1028
|
+
fail(pluginId, `provisioning.nodePackages[${i}].packageRoot must be a string or URL`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (provisioning.python !== void 0) {
|
|
1033
|
+
if (!Array.isArray(provisioning.python)) {
|
|
1034
|
+
fail(pluginId, "provisioning.python must be an array when provided");
|
|
1035
|
+
}
|
|
1036
|
+
for (let i = 0; i < provisioning.python.length; i++) {
|
|
1037
|
+
const spec = provisioning.python[i];
|
|
1038
|
+
if (!spec || typeof spec !== "object") {
|
|
1039
|
+
fail(pluginId, `provisioning.python[${i}] must be an object`);
|
|
1040
|
+
}
|
|
1041
|
+
if (!spec.id || typeof spec.id !== "string") {
|
|
1042
|
+
fail(pluginId, `provisioning.python[${i}].id must be a non-empty string`);
|
|
1043
|
+
}
|
|
1044
|
+
if (!isPathLike(spec.projectFile)) {
|
|
1045
|
+
fail(pluginId, `provisioning.python[${i}].projectFile must be a string or URL`);
|
|
1046
|
+
}
|
|
1047
|
+
if (spec.extraLibs !== void 0 && (!Array.isArray(spec.extraLibs) || spec.extraLibs.some((item) => typeof item !== "string"))) {
|
|
1048
|
+
fail(pluginId, `provisioning.python[${i}].extraLibs must be a string array when provided`);
|
|
1049
|
+
}
|
|
1050
|
+
if (spec.env !== void 0) {
|
|
1051
|
+
if (!spec.env || typeof spec.env !== "object" || Array.isArray(spec.env)) {
|
|
1052
|
+
fail(pluginId, `provisioning.python[${i}].env must be an object when provided`);
|
|
1053
|
+
}
|
|
1054
|
+
for (const [key, value] of Object.entries(spec.env)) {
|
|
1055
|
+
if (!key || !isPathLike(value)) {
|
|
1056
|
+
fail(pluginId, `provisioning.python[${i}].env values must be strings or URLs`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function validateServerPlugin(plugin) {
|
|
1064
|
+
if (!plugin.id || typeof plugin.id !== "string") {
|
|
1065
|
+
fail(plugin.id ?? "<unknown>", "id must be a non-empty string");
|
|
1066
|
+
}
|
|
1067
|
+
if (plugin.label !== void 0 && typeof plugin.label !== "string") {
|
|
1068
|
+
fail(plugin.id, "label must be a string when provided");
|
|
1069
|
+
}
|
|
1070
|
+
if (plugin.systemPrompt !== void 0 && typeof plugin.systemPrompt !== "string") {
|
|
1071
|
+
fail(plugin.id, "systemPrompt must be a string when provided");
|
|
1072
|
+
}
|
|
1073
|
+
if (plugin.piPackages !== void 0) {
|
|
1074
|
+
if (!Array.isArray(plugin.piPackages)) {
|
|
1075
|
+
fail(plugin.id, "piPackages must be an array when provided");
|
|
1076
|
+
}
|
|
1077
|
+
validatePiPackages2(plugin.id, plugin.piPackages);
|
|
1078
|
+
}
|
|
1079
|
+
if (plugin.extensionPaths !== void 0) {
|
|
1080
|
+
if (!Array.isArray(plugin.extensionPaths)) {
|
|
1081
|
+
fail(plugin.id, "extensionPaths must be an array when provided");
|
|
1082
|
+
}
|
|
1083
|
+
plugin.extensionPaths.forEach((path, index) => {
|
|
1084
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1085
|
+
fail(plugin.id, `extensionPaths[${index}] must be a non-empty string`);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
if (plugin.agentTools !== void 0) {
|
|
1090
|
+
if (!Array.isArray(plugin.agentTools)) {
|
|
1091
|
+
fail(plugin.id, "agentTools must be an array when provided");
|
|
1092
|
+
}
|
|
1093
|
+
plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
|
|
1094
|
+
}
|
|
1095
|
+
if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
|
|
1096
|
+
fail(plugin.id, "routes must be a Fastify plugin function when provided");
|
|
1097
|
+
}
|
|
1098
|
+
if (plugin.preservedUiStateKeys !== void 0) {
|
|
1099
|
+
if (!Array.isArray(plugin.preservedUiStateKeys) || plugin.preservedUiStateKeys.some((key) => typeof key !== "string" || key.length === 0)) {
|
|
1100
|
+
fail(plugin.id, "preservedUiStateKeys must be a non-empty string array when provided");
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (plugin.provisioning !== void 0) {
|
|
1104
|
+
validateProvisioning(plugin.id, plugin.provisioning);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/server/plugins/bootstrapServer.ts
|
|
1109
|
+
function bootstrapServer(options) {
|
|
1110
|
+
const excludedDefaults = new Set(options.excludeDefaults ?? []);
|
|
1111
|
+
const finalPlugins = [
|
|
1112
|
+
...(options.defaults ?? []).filter((p) => !excludedDefaults.has(p.id)),
|
|
1113
|
+
...options.plugins ?? []
|
|
23
1114
|
];
|
|
24
|
-
|
|
1115
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1116
|
+
for (const plugin of finalPlugins) {
|
|
1117
|
+
validateServerPlugin(plugin);
|
|
1118
|
+
if (seenIds.has(plugin.id)) {
|
|
1119
|
+
throw new Error(`plugin "${plugin.id}" registered twice`);
|
|
1120
|
+
}
|
|
1121
|
+
seenIds.add(plugin.id);
|
|
1122
|
+
}
|
|
1123
|
+
const agentTools = [];
|
|
1124
|
+
for (const plugin of finalPlugins) {
|
|
1125
|
+
for (const tool of plugin.agentTools ?? []) {
|
|
1126
|
+
agentTools.push(tool);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const systemPromptAppend = finalPlugins.filter((p) => p.systemPrompt && p.systemPrompt.trim()).map((p) => p.systemPrompt.trim()).join("\n\n");
|
|
1130
|
+
const piPackages = compactPiPackages(finalPlugins.flatMap((plugin) => plugin.piPackages ?? []));
|
|
1131
|
+
const extensionPaths = finalPlugins.flatMap((p) => p.extensionPaths ?? []);
|
|
1132
|
+
const provisioningContributions = finalPlugins.filter((p) => p.provisioning).map((p) => ({ id: p.id, provisioning: p.provisioning }));
|
|
1133
|
+
const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
|
|
1134
|
+
const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
|
|
1135
|
+
return {
|
|
1136
|
+
registered: finalPlugins.map((p) => p.id),
|
|
1137
|
+
systemPromptAppend,
|
|
1138
|
+
piPackages,
|
|
1139
|
+
extensionPaths,
|
|
1140
|
+
agentTools,
|
|
1141
|
+
provisioningContributions,
|
|
1142
|
+
routeContributions,
|
|
1143
|
+
preservedUiStateKeys
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// src/app/server/pluginEntryResolver.ts
|
|
1148
|
+
function readPluginPackageJson(dir) {
|
|
1149
|
+
const pkgPath = resolve5(dir, "package.json");
|
|
1150
|
+
if (!existsSync5(pkgPath)) return null;
|
|
1151
|
+
try {
|
|
1152
|
+
return JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
1153
|
+
} catch {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
var require3 = createRequire2(import.meta.url);
|
|
1158
|
+
var warnedJitiMissing = false;
|
|
1159
|
+
function warnJitiUnavailable(serverPath, reason) {
|
|
1160
|
+
if (warnedJitiMissing) return;
|
|
1161
|
+
warnedJitiMissing = true;
|
|
1162
|
+
console.warn(
|
|
1163
|
+
`[boring-workspace] hotReload requested but jiti is unavailable (${reason}). Falling back to native import() for ${serverPath}; subsequent reloads will NOT pick up source changes because Node's module cache will return the same module. Install jiti or set hotReload: false.`
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
function jitiImport(serverPath) {
|
|
1167
|
+
try {
|
|
1168
|
+
const jitiModule = require3("jiti");
|
|
1169
|
+
const create = jitiModule.createJiti;
|
|
1170
|
+
if (!create) {
|
|
1171
|
+
warnJitiUnavailable(serverPath, "createJiti not exported");
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
return create(import.meta.url, { moduleCache: false }).import(serverPath);
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
warnJitiUnavailable(serverPath, err instanceof Error ? err.message : String(err));
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async function importServerModule(serverPath, hotReload) {
|
|
1181
|
+
if (hotReload) {
|
|
1182
|
+
const jiti = jitiImport(serverPath);
|
|
1183
|
+
if (jiti) return await jiti;
|
|
1184
|
+
}
|
|
1185
|
+
const href = pathToFileURL(serverPath).href;
|
|
1186
|
+
return await import(
|
|
1187
|
+
/* @vite-ignore */
|
|
1188
|
+
href
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
function resolveDirServerEntryPath(dir) {
|
|
1192
|
+
const rootDir = resolve5(dir);
|
|
1193
|
+
const pkg = readPluginPackageJson(rootDir);
|
|
1194
|
+
if (!pkg) throw new Error(`boring plugin: no package.json found in ${rootDir}`);
|
|
1195
|
+
return resolveSafePluginEntryPath({
|
|
1196
|
+
rootDir,
|
|
1197
|
+
explicit: pkg.boring?.server,
|
|
1198
|
+
conventions: [],
|
|
1199
|
+
field: "boring.server",
|
|
1200
|
+
manifestPath: join5(rootDir, "package.json")
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
function hasDirServerPlugin(entry) {
|
|
1204
|
+
const rootDir = resolve5(entry.dir);
|
|
1205
|
+
const pkg = readPluginPackageJson(rootDir);
|
|
1206
|
+
if (!pkg) throw new Error(`boring plugin: no package.json found in ${rootDir}`);
|
|
1207
|
+
if (pkg.boring?.server === void 0 || pkg.boring.server === false) return false;
|
|
1208
|
+
return resolveDirServerEntryPath(rootDir) !== null;
|
|
1209
|
+
}
|
|
1210
|
+
async function resolveDirServerPlugin(entry, ctx) {
|
|
1211
|
+
const dir = resolve5(entry.dir);
|
|
1212
|
+
const serverPath = resolveDirServerEntryPath(dir);
|
|
1213
|
+
if (!serverPath) {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`boring plugin: no server entry resolved for ${dir}
|
|
1216
|
+
set "boring.server" in package.json to a safe relative server entry`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
const mod = await importServerModule(serverPath, entry.hotReload === true);
|
|
1220
|
+
const value = typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
|
|
1221
|
+
if (typeof value === "function") {
|
|
1222
|
+
const plugin = await value(entry.options, ctx);
|
|
1223
|
+
validateServerPlugin(plugin);
|
|
1224
|
+
return plugin;
|
|
1225
|
+
}
|
|
1226
|
+
if (value && typeof value === "object") {
|
|
1227
|
+
const plugin = value;
|
|
1228
|
+
validateServerPlugin(plugin);
|
|
1229
|
+
return plugin;
|
|
1230
|
+
}
|
|
1231
|
+
throw new Error(`boring plugin: ${serverPath} default export is neither a function nor a plugin object`);
|
|
25
1232
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
1233
|
+
function isDirEntry(entry) {
|
|
1234
|
+
return typeof entry === "object" && entry !== null && "dir" in entry;
|
|
1235
|
+
}
|
|
1236
|
+
async function resolveOnePluginEntry(entry, ctx) {
|
|
1237
|
+
if (isDirEntry(entry)) return await resolveDirServerPlugin(entry, ctx);
|
|
1238
|
+
return entry;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/app/server/rebuildServerPlugins.ts
|
|
1242
|
+
async function rebuildServerPlugins(opts) {
|
|
1243
|
+
const { entries, ctx } = opts;
|
|
1244
|
+
const diagnostics = [];
|
|
1245
|
+
for (const entry of entries) {
|
|
1246
|
+
try {
|
|
1247
|
+
await resolveOnePluginEntry(entry, ctx);
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
const source = isDirEntry(entry) ? `directory (${entry.dir})` : "entry";
|
|
1250
|
+
diagnostics.push({
|
|
1251
|
+
source,
|
|
1252
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/app/server/defaultPluginPackages.ts
|
|
1260
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1261
|
+
import { createRequire as createRequire3 } from "module";
|
|
1262
|
+
import { dirname as dirname6, isAbsolute as isAbsolute3, join as join6 } from "path";
|
|
1263
|
+
function readAppManifestDefaultPlugins(appPackageJsonPath) {
|
|
1264
|
+
if (!appPackageJsonPath || !existsSync6(appPackageJsonPath)) return [];
|
|
1265
|
+
let pkg;
|
|
28
1266
|
try {
|
|
29
|
-
|
|
1267
|
+
pkg = JSON.parse(readFileSync5(appPackageJsonPath, "utf8"));
|
|
30
1268
|
} catch {
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1269
|
+
return [];
|
|
1270
|
+
}
|
|
1271
|
+
const entries = pkg.boring?.defaultPluginPackages;
|
|
1272
|
+
if (!Array.isArray(entries)) return [];
|
|
1273
|
+
const pkgDir = dirname6(appPackageJsonPath);
|
|
1274
|
+
return entries.filter((e) => typeof e === "string").map((entry) => {
|
|
1275
|
+
if (entry.startsWith("./") || entry.startsWith("../")) {
|
|
1276
|
+
return join6(pkgDir, entry);
|
|
1277
|
+
}
|
|
1278
|
+
return entry;
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
function resolveDefaultPluginPackagePaths(workspaceRoot, defaultPluginPackages) {
|
|
1282
|
+
if (defaultPluginPackages.length === 0) return [];
|
|
1283
|
+
const require5 = createRequire3(join6(workspaceRoot, "package.json"));
|
|
1284
|
+
const requireFromHere = createRequire3(import.meta.url);
|
|
1285
|
+
const resolved = [];
|
|
1286
|
+
for (const entry of defaultPluginPackages) {
|
|
1287
|
+
if (isAbsolute3(entry)) {
|
|
1288
|
+
if (!existsSync6(join6(entry, "package.json"))) {
|
|
1289
|
+
throw new Error(
|
|
1290
|
+
`defaultPluginPackages: "${entry}" has no package.json \u2014 provide a path to a directory containing package.json with a "boring" field.`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
resolved.push(entry);
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
let resolvedPath = null;
|
|
1297
|
+
try {
|
|
1298
|
+
resolvedPath = dirname6(require5.resolve(`${entry}/package.json`));
|
|
1299
|
+
} catch {
|
|
1300
|
+
try {
|
|
1301
|
+
resolvedPath = dirname6(requireFromHere.resolve(`${entry}/package.json`));
|
|
1302
|
+
} catch {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`defaultPluginPackages: cannot resolve "${entry}" \u2014 install it as a dep of the app (or workspace root) so require.resolve can find its package.json. Pass an absolute path instead if the package lives outside node_modules.`
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
resolved.push(resolvedPath);
|
|
1309
|
+
}
|
|
1310
|
+
return resolved;
|
|
1311
|
+
}
|
|
1312
|
+
function resolveDefaultWorkspacePluginPackagePaths({
|
|
1313
|
+
workspaceRoot = process.cwd(),
|
|
1314
|
+
defaultPluginPackages = [],
|
|
1315
|
+
appPackageJsonPath
|
|
1316
|
+
} = {}) {
|
|
1317
|
+
const manifestPluginPackages = readAppManifestDefaultPlugins(appPackageJsonPath);
|
|
1318
|
+
return resolveDefaultPluginPackagePaths(workspaceRoot, [
|
|
1319
|
+
...manifestPluginPackages,
|
|
1320
|
+
...defaultPluginPackages
|
|
1321
|
+
]);
|
|
55
1322
|
}
|
|
56
1323
|
|
|
57
1324
|
// src/server/bridge/createInMemoryBridge.ts
|
|
@@ -98,7 +1365,7 @@ function createInMemoryBridge() {
|
|
|
98
1365
|
|
|
99
1366
|
// src/server/ui-control/tools/uiTools.ts
|
|
100
1367
|
import { access } from "fs/promises";
|
|
101
|
-
import { resolve, isAbsolute, relative, win32 } from "path";
|
|
1368
|
+
import { resolve as resolve6, isAbsolute as isAbsolute4, relative as relative3, win32 } from "path";
|
|
102
1369
|
function makeError(message) {
|
|
103
1370
|
return {
|
|
104
1371
|
content: [{ type: "text", text: message }],
|
|
@@ -111,7 +1378,7 @@ function getPathParam(kind, params) {
|
|
|
111
1378
|
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
112
1379
|
}
|
|
113
1380
|
function isPathAbsolute(filePath) {
|
|
114
|
-
return
|
|
1381
|
+
return isAbsolute4(filePath) || win32.isAbsolute(filePath);
|
|
115
1382
|
}
|
|
116
1383
|
function isOutsideWorkspaceRel(rel) {
|
|
117
1384
|
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isPathAbsolute(rel);
|
|
@@ -138,8 +1405,8 @@ function validatePathSyntax(relPath, workspaceRoot) {
|
|
|
138
1405
|
async function validatePath(workspaceRoot, relPath) {
|
|
139
1406
|
const syntax = validatePathSyntax(relPath, workspaceRoot);
|
|
140
1407
|
if (!syntax.ok) return syntax;
|
|
141
|
-
const resolved =
|
|
142
|
-
const rel =
|
|
1408
|
+
const resolved = resolve6(workspaceRoot, relPath);
|
|
1409
|
+
const rel = relative3(workspaceRoot, resolved);
|
|
143
1410
|
if (isOutsideWorkspaceRel(rel)) {
|
|
144
1411
|
return {
|
|
145
1412
|
ok: false,
|
|
@@ -513,269 +1780,107 @@ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
|
|
|
513
1780
|
done();
|
|
514
1781
|
}
|
|
515
1782
|
|
|
516
|
-
// src/server/
|
|
517
|
-
import
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
// src/server/plugins/defineServerPlugin.ts
|
|
525
|
-
var ServerPluginError = class extends Error {
|
|
526
|
-
constructor(message) {
|
|
527
|
-
super(message);
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
function fail(pluginId, message) {
|
|
531
|
-
throw new ServerPluginError(`server plugin "${pluginId}": ${message}`);
|
|
532
|
-
}
|
|
533
|
-
function isUrl(value) {
|
|
534
|
-
return value instanceof URL;
|
|
535
|
-
}
|
|
536
|
-
function isPathLike(value) {
|
|
537
|
-
return typeof value === "string" && value.length > 0 || isUrl(value);
|
|
538
|
-
}
|
|
539
|
-
function validateAgentTool(pluginId, tool, index) {
|
|
540
|
-
if (!tool || typeof tool !== "object") {
|
|
541
|
-
fail(pluginId, `agentTools[${index}] must be an object`);
|
|
542
|
-
}
|
|
543
|
-
const candidate = tool;
|
|
544
|
-
if (!candidate.name || typeof candidate.name !== "string") {
|
|
545
|
-
fail(pluginId, `agentTools[${index}].name must be a non-empty string`);
|
|
546
|
-
}
|
|
547
|
-
if (typeof candidate.description !== "string") {
|
|
548
|
-
fail(pluginId, `agentTools[${index}].description must be a string`);
|
|
549
|
-
}
|
|
550
|
-
if (!candidate.parameters || typeof candidate.parameters !== "object") {
|
|
551
|
-
fail(pluginId, `agentTools[${index}].parameters must be an object`);
|
|
552
|
-
}
|
|
553
|
-
if (typeof candidate.execute !== "function") {
|
|
554
|
-
fail(pluginId, `agentTools[${index}].execute must be a function`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
function validatePiPackages(pluginId, piPackages) {
|
|
558
|
-
for (let i = 0; i < piPackages.length; i++) {
|
|
559
|
-
const source = piPackages[i];
|
|
560
|
-
if (typeof source === "string") {
|
|
561
|
-
if (source.length === 0) {
|
|
562
|
-
fail(pluginId, `piPackages[${i}] must be a non-empty string`);
|
|
563
|
-
}
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
567
|
-
fail(pluginId, `piPackages[${i}] must be a string or package source object`);
|
|
568
|
-
}
|
|
569
|
-
const candidate = source;
|
|
570
|
-
if (typeof candidate.source !== "string" || candidate.source.length === 0) {
|
|
571
|
-
fail(pluginId, `piPackages[${i}].source must be a non-empty string`);
|
|
572
|
-
}
|
|
573
|
-
for (const key of PI_PACKAGE_RESOURCE_FILTERS) {
|
|
574
|
-
const value = candidate[key];
|
|
575
|
-
if (value === void 0) continue;
|
|
576
|
-
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
|
|
577
|
-
fail(pluginId, `piPackages[${i}].${key} must be a string array when provided`);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
function validateProvisioning(pluginId, provisioning) {
|
|
583
|
-
if (!provisioning || typeof provisioning !== "object") {
|
|
584
|
-
fail(pluginId, "provisioning must be an object");
|
|
585
|
-
}
|
|
586
|
-
if (provisioning.templateDirs !== void 0) {
|
|
587
|
-
if (!Array.isArray(provisioning.templateDirs)) {
|
|
588
|
-
fail(pluginId, "provisioning.templateDirs must be an array when provided");
|
|
589
|
-
}
|
|
590
|
-
for (let i = 0; i < provisioning.templateDirs.length; i++) {
|
|
591
|
-
const contribution = provisioning.templateDirs[i];
|
|
592
|
-
if (!contribution || typeof contribution !== "object") {
|
|
593
|
-
fail(pluginId, `provisioning.templateDirs[${i}] must be an object`);
|
|
594
|
-
}
|
|
595
|
-
if (!contribution.id || typeof contribution.id !== "string") {
|
|
596
|
-
fail(pluginId, `provisioning.templateDirs[${i}].id must be a non-empty string`);
|
|
597
|
-
}
|
|
598
|
-
if (!isPathLike(contribution.path)) {
|
|
599
|
-
fail(pluginId, `provisioning.templateDirs[${i}].path must be a string or URL`);
|
|
600
|
-
}
|
|
601
|
-
if (contribution.target !== void 0 && typeof contribution.target !== "string") {
|
|
602
|
-
fail(pluginId, `provisioning.templateDirs[${i}].target must be a string when provided`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (provisioning.python !== void 0) {
|
|
607
|
-
if (!Array.isArray(provisioning.python)) {
|
|
608
|
-
fail(pluginId, "provisioning.python must be an array when provided");
|
|
609
|
-
}
|
|
610
|
-
for (let i = 0; i < provisioning.python.length; i++) {
|
|
611
|
-
const spec = provisioning.python[i];
|
|
612
|
-
if (!spec || typeof spec !== "object") {
|
|
613
|
-
fail(pluginId, `provisioning.python[${i}] must be an object`);
|
|
614
|
-
}
|
|
615
|
-
if (!spec.id || typeof spec.id !== "string") {
|
|
616
|
-
fail(pluginId, `provisioning.python[${i}].id must be a non-empty string`);
|
|
617
|
-
}
|
|
618
|
-
if (!isPathLike(spec.projectFile)) {
|
|
619
|
-
fail(pluginId, `provisioning.python[${i}].projectFile must be a string or URL`);
|
|
620
|
-
}
|
|
621
|
-
if (spec.extraLibs !== void 0 && (!Array.isArray(spec.extraLibs) || spec.extraLibs.some((item) => typeof item !== "string"))) {
|
|
622
|
-
fail(pluginId, `provisioning.python[${i}].extraLibs must be a string array when provided`);
|
|
623
|
-
}
|
|
624
|
-
if (spec.env !== void 0) {
|
|
625
|
-
if (!spec.env || typeof spec.env !== "object" || Array.isArray(spec.env)) {
|
|
626
|
-
fail(pluginId, `provisioning.python[${i}].env must be an object when provided`);
|
|
627
|
-
}
|
|
628
|
-
for (const [key, value] of Object.entries(spec.env)) {
|
|
629
|
-
if (!key || !isPathLike(value)) {
|
|
630
|
-
fail(pluginId, `provisioning.python[${i}].env values must be strings or URLs`);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
1783
|
+
// src/app/server/createWorkspaceAgentServer.ts
|
|
1784
|
+
var __dirname = dirname7(fileURLToPath(import.meta.url));
|
|
1785
|
+
var require4 = createRequire4(import.meta.url);
|
|
1786
|
+
function boringPiRootVisibleToAgentTools(workspaceRoot, resolvedMode, provisioned) {
|
|
1787
|
+
if (!provisioned) return void 0;
|
|
1788
|
+
if (resolvedMode === "local") return "/workspace/node_modules/@hachej/boring-pi";
|
|
1789
|
+
return join7(workspaceRoot, "node_modules", "@hachej", "boring-pi");
|
|
636
1790
|
}
|
|
637
|
-
function
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (plugin.piPackages !== void 0) {
|
|
648
|
-
if (!Array.isArray(plugin.piPackages)) {
|
|
649
|
-
fail(plugin.id, "piPackages must be an array when provided");
|
|
650
|
-
}
|
|
651
|
-
validatePiPackages(plugin.id, plugin.piPackages);
|
|
652
|
-
}
|
|
653
|
-
if (plugin.agentTools !== void 0) {
|
|
654
|
-
if (!Array.isArray(plugin.agentTools)) {
|
|
655
|
-
fail(plugin.id, "agentTools must be an array when provided");
|
|
656
|
-
}
|
|
657
|
-
plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
|
|
658
|
-
}
|
|
659
|
-
if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
|
|
660
|
-
fail(plugin.id, "routes must be a Fastify plugin function when provided");
|
|
661
|
-
}
|
|
662
|
-
if (plugin.preservedUiStateKeys !== void 0) {
|
|
663
|
-
if (!Array.isArray(plugin.preservedUiStateKeys) || plugin.preservedUiStateKeys.some((key) => typeof key !== "string" || key.length === 0)) {
|
|
664
|
-
fail(plugin.id, "preservedUiStateKeys must be a non-empty string array when provided");
|
|
1791
|
+
function resolveWorkspacePackageRoot() {
|
|
1792
|
+
const candidates = [
|
|
1793
|
+
join7(__dirname, ".."),
|
|
1794
|
+
join7(__dirname, "../../..")
|
|
1795
|
+
];
|
|
1796
|
+
for (const candidate of candidates) {
|
|
1797
|
+
try {
|
|
1798
|
+
const pkg = JSON.parse(readFileSync6(join7(candidate, "package.json"), "utf8"));
|
|
1799
|
+
if (pkg.name === "@hachej/boring-workspace") return candidate;
|
|
1800
|
+
} catch {
|
|
665
1801
|
}
|
|
666
1802
|
}
|
|
667
|
-
|
|
668
|
-
validateProvisioning(plugin.id, plugin.provisioning);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
function defineServerPlugin(plugin) {
|
|
672
|
-
validateServerPlugin(plugin);
|
|
673
|
-
return Object.assign({}, plugin);
|
|
1803
|
+
return join7(__dirname, "../../..");
|
|
674
1804
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
function compactPrompts(prompts) {
|
|
678
|
-
const text = prompts.map((prompt) => prompt?.trim()).filter((prompt) => Boolean(prompt)).join("\n\n");
|
|
679
|
-
return text || void 0;
|
|
680
|
-
}
|
|
681
|
-
function mergeProvisioning(contributions) {
|
|
682
|
-
const templateDirs = contributions.flatMap((entry) => entry?.templateDirs ?? []);
|
|
683
|
-
const python = contributions.flatMap((entry) => entry?.python ?? []);
|
|
684
|
-
if (templateDirs.length === 0 && python.length === 0) return void 0;
|
|
1805
|
+
function nodePackageContribution(contributionId, nodePackageId, packageName, packageRoot) {
|
|
1806
|
+
if (!packageRoot || !existsSync7(join7(packageRoot, "package.json"))) return null;
|
|
685
1807
|
return {
|
|
686
|
-
|
|
687
|
-
|
|
1808
|
+
id: contributionId,
|
|
1809
|
+
provisioning: {
|
|
1810
|
+
nodePackages: [{ id: nodePackageId, packageName, packageRoot }]
|
|
1811
|
+
}
|
|
688
1812
|
};
|
|
689
1813
|
}
|
|
690
|
-
function
|
|
691
|
-
|
|
692
|
-
|
|
1814
|
+
function createWorkspacePackageProvisioningContribution() {
|
|
1815
|
+
return nodePackageContribution(
|
|
1816
|
+
"boring-workspace-package",
|
|
1817
|
+
"boring-workspace",
|
|
1818
|
+
"@hachej/boring-workspace",
|
|
1819
|
+
resolveWorkspacePackageRoot()
|
|
693
1820
|
);
|
|
694
|
-
if (routePlugins.length === 0) return void 0;
|
|
695
|
-
return async (app) => {
|
|
696
|
-
for (const routes2 of routePlugins) {
|
|
697
|
-
await app.register(routes2);
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
1821
|
}
|
|
701
|
-
function
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const agentTools = [
|
|
707
|
-
...options.plugins.flatMap((plugin) => plugin.agentTools ?? []),
|
|
708
|
-
...options.agentTools ?? []
|
|
1822
|
+
function resolveBoringPiPackageRoot() {
|
|
1823
|
+
const workspacePackageRoot = resolveWorkspacePackageRoot();
|
|
1824
|
+
const candidates = [
|
|
1825
|
+
join7(workspacePackageRoot, "..", "pi"),
|
|
1826
|
+
join7(workspacePackageRoot, "node_modules", "@hachej", "boring-pi")
|
|
709
1827
|
];
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const preservedUiStateKeys = [.../* @__PURE__ */ new Set([
|
|
723
|
-
...options.plugins.flatMap((plugin) => plugin.preservedUiStateKeys ?? []),
|
|
724
|
-
...options.preservedUiStateKeys ?? []
|
|
725
|
-
])];
|
|
726
|
-
return defineServerPlugin({
|
|
727
|
-
id: options.id,
|
|
728
|
-
...options.label !== void 0 ? { label: options.label } : {},
|
|
729
|
-
...piPackages.length > 0 ? { piPackages } : {},
|
|
730
|
-
...systemPrompt ? { systemPrompt } : {},
|
|
731
|
-
...agentTools.length > 0 ? { agentTools } : {},
|
|
732
|
-
...provisioning ? { provisioning } : {},
|
|
733
|
-
...routes ? { routes } : {},
|
|
734
|
-
...preservedUiStateKeys.length > 0 ? { preservedUiStateKeys } : {}
|
|
735
|
-
});
|
|
1828
|
+
for (const candidate of candidates) {
|
|
1829
|
+
try {
|
|
1830
|
+
const pkg = JSON.parse(readFileSync6(join7(candidate, "package.json"), "utf8"));
|
|
1831
|
+
if (pkg.name === "@hachej/boring-pi") return candidate;
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
try {
|
|
1836
|
+
return dirname7(require4.resolve("@hachej/boring-pi/package.json"));
|
|
1837
|
+
} catch {
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
736
1840
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
function collectPiPackages(plugins) {
|
|
740
|
-
return compactPiPackages(plugins.flatMap((plugin) => plugin.piPackages ?? []));
|
|
1841
|
+
function createBoringPiPackageProvisioningContribution() {
|
|
1842
|
+
return nodePackageContribution("boring-pi-package", "boring-pi", "@hachej/boring-pi", resolveBoringPiPackageRoot());
|
|
741
1843
|
}
|
|
742
|
-
function
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
1844
|
+
function resolveBoringUiCliPackageRoot() {
|
|
1845
|
+
const workspacePackageRoot = resolveWorkspacePackageRoot();
|
|
1846
|
+
const candidates = [
|
|
1847
|
+
join7(workspacePackageRoot, "..", "cli"),
|
|
1848
|
+
join7(workspacePackageRoot, "node_modules", "@hachej", "boring-ui-cli")
|
|
747
1849
|
];
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1850
|
+
for (const candidate of candidates) {
|
|
1851
|
+
try {
|
|
1852
|
+
const pkg = JSON.parse(readFileSync6(join7(candidate, "package.json"), "utf8"));
|
|
1853
|
+
if (pkg.name === "@hachej/boring-ui-cli") return candidate;
|
|
1854
|
+
} catch {
|
|
753
1855
|
}
|
|
754
|
-
seenIds.add(plugin.id);
|
|
755
1856
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
1857
|
+
try {
|
|
1858
|
+
return dirname7(require4.resolve("@hachej/boring-ui-cli/package.json"));
|
|
1859
|
+
} catch {
|
|
1860
|
+
return null;
|
|
761
1861
|
}
|
|
762
|
-
const systemPromptAppend = finalPlugins.filter((p) => p.systemPrompt && p.systemPrompt.trim()).map((p) => p.systemPrompt.trim()).join("\n\n");
|
|
763
|
-
const piPackages = collectPiPackages(finalPlugins);
|
|
764
|
-
const provisioningContributions = finalPlugins.filter((p) => p.provisioning).map((p) => ({ id: p.id, provisioning: p.provisioning }));
|
|
765
|
-
const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
|
|
766
|
-
const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
|
|
767
|
-
return {
|
|
768
|
-
registered: finalPlugins.map((p) => p.id),
|
|
769
|
-
systemPromptAppend,
|
|
770
|
-
piPackages,
|
|
771
|
-
agentTools,
|
|
772
|
-
provisioningContributions,
|
|
773
|
-
routeContributions,
|
|
774
|
-
preservedUiStateKeys
|
|
775
|
-
};
|
|
776
1862
|
}
|
|
777
|
-
|
|
778
|
-
|
|
1863
|
+
function createBoringUiCliPackageProvisioningContribution() {
|
|
1864
|
+
return nodePackageContribution(
|
|
1865
|
+
"boring-ui-cli-package",
|
|
1866
|
+
"boring-ui-cli",
|
|
1867
|
+
"@hachej/boring-ui-cli",
|
|
1868
|
+
resolveBoringUiCliPackageRoot()
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
function createBoringPiPackageSource(workspaceRoot) {
|
|
1872
|
+
const workspacePackageRoot = join7(workspaceRoot, "node_modules", "@hachej", "boring-pi");
|
|
1873
|
+
const source = existsSync7(join7(workspacePackageRoot, "package.json")) ? workspacePackageRoot : resolveBoringPiPackageRoot();
|
|
1874
|
+
if (!source || !existsSync7(join7(source, "package.json"))) return void 0;
|
|
1875
|
+
return { source, skills: ["skills/boring-plugin-authoring"] };
|
|
1876
|
+
}
|
|
1877
|
+
function resolveBoringPiSkillPaths(workspaceRoot) {
|
|
1878
|
+
const pkg = createBoringPiPackageSource(workspaceRoot);
|
|
1879
|
+
const root = typeof pkg === "string" ? pkg : pkg?.source;
|
|
1880
|
+
if (!root) return [];
|
|
1881
|
+
const skillFile = join7(root, "skills", "boring-plugin-authoring", "SKILL.md");
|
|
1882
|
+
return existsSync7(skillFile) ? [skillFile] : [];
|
|
1883
|
+
}
|
|
779
1884
|
function buildWorkspaceContextPrompt() {
|
|
780
1885
|
return [
|
|
781
1886
|
"## Workspace",
|
|
@@ -791,20 +1896,31 @@ function collectWorkspaceAgentServerPlugins(opts = {}) {
|
|
|
791
1896
|
plugins: opts.plugins,
|
|
792
1897
|
excludeDefaults: opts.excludeDefaults
|
|
793
1898
|
});
|
|
794
|
-
const workspaceSkillsDir =
|
|
795
|
-
const callerAdditional = opts.
|
|
796
|
-
const callerPiPackages = opts.
|
|
1899
|
+
const workspaceSkillsDir = join7(workspaceRoot, ".agents", "skills");
|
|
1900
|
+
const callerAdditional = opts.pi?.additionalSkillPaths ?? [];
|
|
1901
|
+
const callerPiPackages = opts.pi?.packages ?? [];
|
|
1902
|
+
const callerExtensionPaths = opts.pi?.extensionPaths ?? [];
|
|
797
1903
|
return {
|
|
798
|
-
provisioningContributions:
|
|
1904
|
+
provisioningContributions: [
|
|
1905
|
+
createWorkspacePackageProvisioningContribution(),
|
|
1906
|
+
createBoringPiPackageProvisioningContribution(),
|
|
1907
|
+
createBoringUiCliPackageProvisioningContribution(),
|
|
1908
|
+
...result.provisioningContributions
|
|
1909
|
+
].filter((entry) => Boolean(entry)),
|
|
799
1910
|
routeContributions: result.routeContributions,
|
|
800
1911
|
preservedUiStateKeys: result.preservedUiStateKeys,
|
|
801
1912
|
agentOptions: {
|
|
802
1913
|
extraTools: result.agentTools,
|
|
803
1914
|
systemPromptAppend: [opts.systemPromptAppend, result.systemPromptAppend].filter(Boolean).join("\n\n") || void 0,
|
|
804
|
-
|
|
805
|
-
...opts.
|
|
1915
|
+
pi: {
|
|
1916
|
+
...opts.pi,
|
|
806
1917
|
additionalSkillPaths: [workspaceSkillsDir, ...callerAdditional],
|
|
807
|
-
|
|
1918
|
+
packages: compactPiPackages([...result.piPackages, ...callerPiPackages]),
|
|
1919
|
+
extensionPaths: [...result.extensionPaths, ...callerExtensionPaths]
|
|
1920
|
+
// Host-level extensionFactories (opts.pi.extensionFactories) flow
|
|
1921
|
+
// straight through via the ...opts.pi spread above. Plugins no
|
|
1922
|
+
// longer contribute extensionFactories — tools live on agentTools,
|
|
1923
|
+
// file-based extensions on extensionPaths.
|
|
808
1924
|
}
|
|
809
1925
|
}
|
|
810
1926
|
};
|
|
@@ -817,6 +1933,49 @@ async function provisionWorkspaceAgentServer(opts) {
|
|
|
817
1933
|
force: opts.force
|
|
818
1934
|
});
|
|
819
1935
|
}
|
|
1936
|
+
function collectBoringPluginDirs(workspaceRoot, pluginCollection) {
|
|
1937
|
+
const extensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
|
|
1938
|
+
const pluginRoots = extensionPaths.flatMap((path) => {
|
|
1939
|
+
try {
|
|
1940
|
+
return [pluginRootFromExtensionPath(path)];
|
|
1941
|
+
} catch {
|
|
1942
|
+
return [];
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
return [
|
|
1946
|
+
join7(workspaceRoot, ".pi", "extensions"),
|
|
1947
|
+
...pluginRoots
|
|
1948
|
+
];
|
|
1949
|
+
}
|
|
1950
|
+
function emptyPackageJsonPiSnapshot() {
|
|
1951
|
+
return { additionalSkillPaths: [], packages: [], extensionPaths: [] };
|
|
1952
|
+
}
|
|
1953
|
+
function aggregatePluginSystemPromptsFromScan(scan) {
|
|
1954
|
+
const prompts = scan.plugins.map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
1955
|
+
if (prompts.length === 0) return void 0;
|
|
1956
|
+
return `# Loaded boring-ui plugin context
|
|
1957
|
+
|
|
1958
|
+
${prompts.join("\n\n")}`;
|
|
1959
|
+
}
|
|
1960
|
+
function readWorkspacePluginPackagePiSnapshot(pluginDirs) {
|
|
1961
|
+
try {
|
|
1962
|
+
const scan = scanBoringPlugins(pluginDirs);
|
|
1963
|
+
const systemPromptAppend = aggregatePluginSystemPromptsFromScan(scan);
|
|
1964
|
+
return {
|
|
1965
|
+
additionalSkillPaths: scan.plugins.flatMap((plugin) => plugin.skillPaths ?? []),
|
|
1966
|
+
packages: compactPiPackages(normalizeBoringPluginPiPackages(scan.plugins)),
|
|
1967
|
+
extensionPaths: scan.plugins.flatMap((plugin) => plugin.extensionPaths ?? []),
|
|
1968
|
+
...systemPromptAppend ? { systemPromptAppend } : {}
|
|
1969
|
+
};
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1972
|
+
console.warn(
|
|
1973
|
+
"[boring-workspace] readWorkspacePluginPackagePiSnapshot failed \u2014 falling back to empty Pi snapshot:",
|
|
1974
|
+
message
|
|
1975
|
+
);
|
|
1976
|
+
return emptyPackageJsonPiSnapshot();
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
820
1979
|
async function createWorkspaceAgentServer(opts = {}) {
|
|
821
1980
|
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
822
1981
|
const bridge = createInMemoryBridge();
|
|
@@ -826,10 +1985,24 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
826
1985
|
const uiTools = createWorkspaceUiTools(bridge, {
|
|
827
1986
|
workspaceRoot: validateUiPaths ? workspaceRoot : void 0
|
|
828
1987
|
});
|
|
829
|
-
const
|
|
1988
|
+
const ctx = { workspaceRoot, bridge };
|
|
1989
|
+
const defaultPluginPackagePaths = resolveDefaultWorkspacePluginPackagePaths({
|
|
1990
|
+
workspaceRoot,
|
|
1991
|
+
appPackageJsonPath: opts.appPackageJsonPath,
|
|
1992
|
+
defaultPluginPackages: opts.defaultPluginPackages
|
|
1993
|
+
});
|
|
1994
|
+
const pluginHotReload = opts.pluginHotReload ?? true;
|
|
1995
|
+
const defaultPluginDirEntries = defaultPluginPackagePaths.map((dir) => ({ dir, hotReload: pluginHotReload })).filter((entry) => hasDirServerPlugin(entry));
|
|
1996
|
+
const allPluginEntries = [
|
|
1997
|
+
...defaultPluginDirEntries,
|
|
1998
|
+
...opts.plugins ?? []
|
|
1999
|
+
];
|
|
2000
|
+
const resolvedPlugins = await Promise.all(
|
|
2001
|
+
allPluginEntries.map((entry) => resolveOnePluginEntry(entry, ctx))
|
|
2002
|
+
);
|
|
830
2003
|
const pluginCollection = collectWorkspaceAgentServerPlugins({
|
|
831
2004
|
...opts,
|
|
832
|
-
plugins:
|
|
2005
|
+
plugins: resolvedPlugins
|
|
833
2006
|
});
|
|
834
2007
|
if (opts.provisionWorkspace !== false) {
|
|
835
2008
|
await provisionWorkspaceAgentServer({
|
|
@@ -838,6 +2011,41 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
838
2011
|
force: opts.workspaceProvisioning?.force
|
|
839
2012
|
});
|
|
840
2013
|
}
|
|
2014
|
+
const workspacePackagePiPackage = createBoringPiPackageSource(workspaceRoot);
|
|
2015
|
+
const baseStaticPiSkillPaths = [
|
|
2016
|
+
...resolveBoringPiSkillPaths(workspaceRoot),
|
|
2017
|
+
...pluginCollection.agentOptions.pi?.additionalSkillPaths ?? []
|
|
2018
|
+
];
|
|
2019
|
+
const baseStaticPiPackages = [
|
|
2020
|
+
workspacePackagePiPackage,
|
|
2021
|
+
...pluginCollection.agentOptions.pi?.packages ?? []
|
|
2022
|
+
];
|
|
2023
|
+
const baseStaticPiExtensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
|
|
2024
|
+
const boringPluginDirs = [
|
|
2025
|
+
...collectBoringPluginDirs(workspaceRoot, pluginCollection),
|
|
2026
|
+
...defaultPluginPackagePaths
|
|
2027
|
+
];
|
|
2028
|
+
const staticPluginPackagePiSnapshot = pluginHotReload ? emptyPackageJsonPiSnapshot() : readWorkspacePluginPackagePiSnapshot(boringPluginDirs);
|
|
2029
|
+
const staticPiSkillPaths = [
|
|
2030
|
+
...baseStaticPiSkillPaths,
|
|
2031
|
+
...staticPluginPackagePiSnapshot.additionalSkillPaths
|
|
2032
|
+
];
|
|
2033
|
+
const staticPiPackages = compactPiPackages([
|
|
2034
|
+
...baseStaticPiPackages,
|
|
2035
|
+
...staticPluginPackagePiSnapshot.packages
|
|
2036
|
+
]);
|
|
2037
|
+
const staticPiExtensionPaths = [
|
|
2038
|
+
...baseStaticPiExtensionPaths,
|
|
2039
|
+
...staticPluginPackagePiSnapshot.extensionPaths
|
|
2040
|
+
];
|
|
2041
|
+
const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(boringPluginDirs) : void 0;
|
|
2042
|
+
const boringAssetManager = new BoringPluginAssetManager({
|
|
2043
|
+
pluginDirs: boringPluginDirs,
|
|
2044
|
+
errorRoot: join7(workspaceRoot, ".pi", "extensions")
|
|
2045
|
+
});
|
|
2046
|
+
const rebuildPlugins = async () => {
|
|
2047
|
+
return rebuildServerPlugins({ entries: allPluginEntries, ctx });
|
|
2048
|
+
};
|
|
841
2049
|
const app = await createAgentApp({
|
|
842
2050
|
...opts,
|
|
843
2051
|
mode: resolvedMode,
|
|
@@ -849,22 +2057,79 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
849
2057
|
],
|
|
850
2058
|
systemPromptAppend: [
|
|
851
2059
|
workspaceFsCapability === "strong" ? buildWorkspaceContextPrompt() : void 0,
|
|
852
|
-
|
|
853
|
-
|
|
2060
|
+
// `boring-ui` resolves via PATH (pnpm's workspace bin or the
|
|
2061
|
+
// user's npx/global install). We deliberately do NOT prefix with
|
|
2062
|
+
// `npx @hachej/boring-ui-cli` here — that would pull the
|
|
2063
|
+
// published version, which lags the locally-installed CLI when
|
|
2064
|
+
// the agent is iterating in a monorepo. Keep the bin name short.
|
|
2065
|
+
buildBoringSystemPrompt({
|
|
2066
|
+
scaffoldCommand: "boring-ui scaffold-plugin",
|
|
2067
|
+
verifyCommand: "boring-ui verify-plugin",
|
|
2068
|
+
boringPiRootOverride: boringPiRootVisibleToAgentTools(
|
|
2069
|
+
workspaceRoot,
|
|
2070
|
+
resolvedMode,
|
|
2071
|
+
opts.provisionWorkspace !== false
|
|
2072
|
+
)
|
|
2073
|
+
}),
|
|
2074
|
+
pluginCollection.agentOptions.systemPromptAppend,
|
|
2075
|
+
staticPluginPackagePiSnapshot.systemPromptAppend
|
|
854
2076
|
].filter(Boolean).join("\n\n") || void 0,
|
|
855
|
-
|
|
2077
|
+
beforeReload: async () => {
|
|
2078
|
+
let restart_warnings = [];
|
|
2079
|
+
let diagnostics = [];
|
|
2080
|
+
if (pluginHotReload) {
|
|
2081
|
+
const scan = await boringAssetManager.load();
|
|
2082
|
+
restart_warnings = collectRestartWarnings(scan.events);
|
|
2083
|
+
const scanDiagnostics = scan.errors.map((error) => ({
|
|
2084
|
+
source: `boring plugin asset scan (${error.id})`,
|
|
2085
|
+
message: error.message,
|
|
2086
|
+
pluginId: error.id
|
|
2087
|
+
}));
|
|
2088
|
+
const rebuild = await rebuildPlugins();
|
|
2089
|
+
diagnostics = [...scanDiagnostics, ...rebuild.diagnostics];
|
|
2090
|
+
}
|
|
2091
|
+
const callerResult = await opts.beforeReload?.();
|
|
2092
|
+
const callerRestartWarnings = callerResult && typeof callerResult === "object" ? callerResult.restart_warnings ?? [] : [];
|
|
2093
|
+
const callerDiagnostics = callerResult && typeof callerResult === "object" ? callerResult.diagnostics ?? [] : [];
|
|
2094
|
+
const mergedRestartWarnings = [...restart_warnings, ...callerRestartWarnings];
|
|
2095
|
+
const mergedDiagnostics = [...diagnostics, ...callerDiagnostics];
|
|
2096
|
+
if (mergedRestartWarnings.length === 0 && mergedDiagnostics.length === 0) return void 0;
|
|
2097
|
+
return {
|
|
2098
|
+
...mergedRestartWarnings.length > 0 ? { restart_warnings: mergedRestartWarnings } : {},
|
|
2099
|
+
...mergedDiagnostics.length > 0 ? { diagnostics: mergedDiagnostics } : {}
|
|
2100
|
+
};
|
|
2101
|
+
},
|
|
2102
|
+
pi: {
|
|
2103
|
+
...pluginCollection.agentOptions.pi,
|
|
2104
|
+
additionalSkillPaths: staticPiSkillPaths,
|
|
2105
|
+
packages: staticPiPackages,
|
|
2106
|
+
extensionPaths: staticPiExtensionPaths,
|
|
2107
|
+
extensionFactories: pluginCollection.agentOptions.pi?.extensionFactories,
|
|
2108
|
+
getHotReloadableResources: getHotReloadablePiResources
|
|
2109
|
+
},
|
|
2110
|
+
systemPromptDynamic: pluginHotReload ? () => aggregatePluginPrompts(boringAssetManager) : void 0
|
|
856
2111
|
});
|
|
2112
|
+
await boringAssetManager.load();
|
|
857
2113
|
await app.register(uiRoutes, { bridge, preserveStateKeys: pluginCollection.preservedUiStateKeys });
|
|
2114
|
+
await app.register(boringPluginRoutes, {
|
|
2115
|
+
manager: boringAssetManager,
|
|
2116
|
+
rebuildPlugins,
|
|
2117
|
+
enableReloadRoute: pluginHotReload
|
|
2118
|
+
});
|
|
858
2119
|
for (const { routes } of pluginCollection.routeContributions) {
|
|
859
2120
|
await app.register(routes);
|
|
860
2121
|
}
|
|
2122
|
+
;
|
|
2123
|
+
app.__boringRebuildPlugins = rebuildPlugins;
|
|
861
2124
|
return app;
|
|
862
2125
|
}
|
|
863
2126
|
export {
|
|
864
2127
|
buildWorkspaceContextPrompt,
|
|
865
2128
|
collectWorkspaceAgentServerPlugins,
|
|
866
|
-
composeServerPlugins,
|
|
867
2129
|
createWorkspaceAgentServer,
|
|
868
|
-
|
|
869
|
-
provisionWorkspaceAgentServer
|
|
2130
|
+
hasDirServerPlugin,
|
|
2131
|
+
provisionWorkspaceAgentServer,
|
|
2132
|
+
readWorkspacePluginPackagePiSnapshot,
|
|
2133
|
+
resolveDefaultWorkspacePluginPackagePaths,
|
|
2134
|
+
resolveOnePluginEntry
|
|
870
2135
|
};
|