@hachej/boring-workspace 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +36 -34
  2. package/dist/{FileTree-Dvaud3jU.js → FileTree-DHVB9rpk.js} +15 -15
  3. package/dist/{MarkdownEditor-sLkqTXDj.js → MarkdownEditor-L1KDH0bM.js} +1 -1
  4. package/dist/{WorkspaceLoadingState-zLzh1tGc.js → WorkspaceLoadingState-DYDxUYnx.js} +114 -110
  5. package/dist/WorkspaceProvider-CDPaAO5u.js +5971 -0
  6. package/dist/app-front.d.ts +94 -107
  7. package/dist/app-front.js +243 -233
  8. package/dist/app-server.d.ts +130 -15
  9. package/dist/app-server.js +1569 -304
  10. package/dist/{bootstrapServer-BreQ9QBc.d.ts → createInMemoryBridge-BDxDzihm.d.ts} +11 -26
  11. package/dist/manifest-CyNNdfYz.d.ts +58 -0
  12. package/dist/plugin.d.ts +199 -0
  13. package/dist/plugin.js +300 -0
  14. package/dist/server.d.ts +239 -4
  15. package/dist/server.js +901 -78
  16. package/dist/shared.d.ts +4 -112
  17. package/dist/surface-COYagY2m.d.ts +111 -0
  18. package/dist/testing.d.ts +19 -1
  19. package/dist/testing.js +2 -2
  20. package/dist/{agent-tool-DEtfQPVB.d.ts → ui-bridge-Gfh1MMgl.d.ts} +30 -30
  21. package/dist/workspace.css +36 -0
  22. package/dist/workspace.d.ts +165 -120
  23. package/dist/workspace.js +330 -377
  24. package/docs/INTERFACES.md +9 -9
  25. package/docs/PLUGIN_STRUCTURE.md +39 -145
  26. package/docs/PLUGIN_SYSTEM.md +355 -0
  27. package/docs/README.md +6 -1
  28. package/docs/plans/README.md +1 -0
  29. package/docs/plans/archive/HOT_RELOADABLE_AGENT_PLUGINS_PLAN.md +218 -0
  30. package/docs/plans/archive/RELOAD_PLUGGABILITY_PLAN.md +174 -0
  31. package/docs/plans/archive/UNIFIED_PLUGIN_SYSTEM_PLAN.md +769 -0
  32. package/package.json +11 -5
  33. package/dist/CommandPalette-CJHuTJlD.js +0 -5716
  34. package/docs/bridge.md +0 -135
  35. package/docs/panels.md +0 -102
  36. package/docs/plugins.md +0 -158
  37. /package/docs/plans/{MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md → archive/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md} +0 -0
@@ -5,53 +5,1320 @@ import {
5
5
  provisionRuntimeWorkspace,
6
6
  resolveMode
7
7
  } from "@hachej/boring-agent/server";
8
- import { join as join2 } from "path";
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 { existsSync, readFileSync } from "fs";
14
+ import { createRequire } from "module";
12
15
  import { dirname, join } from "path";
13
- import { fileURLToPath } from "url";
14
- var __dirname = dirname(fileURLToPath(import.meta.url));
15
- function resolveDocsPath() {
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
- const candidates = [
19
- join(__dirname, "../docs"),
20
- // dist/server.js → packages/workspace/docs/
21
- join(__dirname, "../../docs")
22
- // src/server/*.ts → packages/workspace/docs/
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
- return candidates.find(existsSync) ?? null;
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 readDocOrFallback(docsPath, name) {
27
- const file = join(docsPath, name);
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
- return existsSync(file) ? readFileSync(file, "utf-8").trim() : "";
1267
+ pkg = JSON.parse(readFileSync5(appPackageJsonPath, "utf8"));
30
1268
  } catch {
31
- return "";
32
- }
33
- }
34
- function buildBoringSystemPrompt() {
35
- const docsPath = resolveDocsPath();
36
- const intro = `You are an expert agent operating inside boring-ui, an open-source workspace for building agent-powered products. You help users by reading files, executing commands, editing code, and opening workspace panels.`;
37
- if (!docsPath) {
38
- return intro;
39
- }
40
- const plugins = readDocOrFallback(docsPath, "plugins.md");
41
- const panels = readDocOrFallback(docsPath, "panels.md");
42
- const bridge = readDocOrFallback(docsPath, "bridge.md");
43
- const sections = [
44
- plugins && `<boring-ui-docs topic="plugin-system">
45
- ${plugins}
46
- </boring-ui-docs>`,
47
- panels && `<boring-ui-docs topic="panel-components">
48
- ${panels}
49
- </boring-ui-docs>`,
50
- bridge && `<boring-ui-docs topic="ui-bridge">
51
- ${bridge}
52
- </boring-ui-docs>`
53
- ].filter(Boolean);
54
- return [intro, ...sections].join("\n\n");
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 isAbsolute(filePath) || win32.isAbsolute(filePath);
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 = resolve(workspaceRoot, relPath);
142
- const rel = relative(workspaceRoot, resolved);
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/plugins/piPackages.ts
517
- import {
518
- compactPiPackages,
519
- mergePiPackageSources,
520
- piPackageSourceKey,
521
- PI_PACKAGE_RESOURCE_FILTERS
522
- } from "@hachej/boring-agent/server";
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 validateServerPlugin(plugin) {
638
- if (!plugin.id || typeof plugin.id !== "string") {
639
- fail(plugin.id ?? "<unknown>", "id must be a non-empty string");
640
- }
641
- if (plugin.label !== void 0 && typeof plugin.label !== "string") {
642
- fail(plugin.id, "label must be a string when provided");
643
- }
644
- if (plugin.systemPrompt !== void 0 && typeof plugin.systemPrompt !== "string") {
645
- fail(plugin.id, "systemPrompt must be a string when provided");
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
- if (plugin.provisioning !== void 0) {
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
- // src/server/plugins/composeServerPlugins.ts
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
- ...templateDirs.length > 0 ? { templateDirs } : {},
687
- ...python.length > 0 ? { python } : {}
1808
+ id: contributionId,
1809
+ provisioning: {
1810
+ nodePackages: [{ id: nodePackageId, packageName, packageRoot }]
1811
+ }
688
1812
  };
689
1813
  }
690
- function composeRoutes(routes) {
691
- const routePlugins = routes.filter(
692
- (route) => Boolean(route)
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 composeServerPlugins(options) {
702
- const piPackages = compactPiPackages([
703
- ...options.plugins.flatMap((plugin) => plugin.piPackages ?? []),
704
- ...options.piPackages ?? []
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 systemPrompt = compactPrompts([
711
- ...options.plugins.map((plugin) => plugin.systemPrompt),
712
- options.systemPrompt
713
- ]);
714
- const provisioning = mergeProvisioning([
715
- ...options.plugins.map((plugin) => plugin.provisioning),
716
- options.provisioning
717
- ]);
718
- const routes = composeRoutes([
719
- ...options.plugins.map((plugin) => plugin.routes),
720
- options.routes
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
- // src/server/plugins/bootstrapServer.ts
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 bootstrapServer(options) {
743
- const excludedDefaults = new Set(options.excludeDefaults ?? []);
744
- const finalPlugins = [
745
- ...(options.defaults ?? []).filter((p) => !excludedDefaults.has(p.id)),
746
- ...options.plugins ?? []
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 seenIds = /* @__PURE__ */ new Set();
749
- for (const plugin of finalPlugins) {
750
- validateServerPlugin(plugin);
751
- if (seenIds.has(plugin.id)) {
752
- throw new Error(`plugin "${plugin.id}" registered twice`);
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
- const agentTools = [];
757
- for (const plugin of finalPlugins) {
758
- for (const tool of plugin.agentTools ?? []) {
759
- agentTools.push(tool);
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
- // src/app/server/createWorkspaceAgentServer.ts
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 = join2(workspaceRoot, ".agents", "skills");
795
- const callerAdditional = opts.resourceLoaderOptions?.additionalSkillPaths ?? [];
796
- const callerPiPackages = opts.resourceLoaderOptions?.piPackages ?? [];
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: result.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
- resourceLoaderOptions: {
805
- ...opts.resourceLoaderOptions,
1915
+ pi: {
1916
+ ...opts.pi,
806
1917
  additionalSkillPaths: [workspaceSkillsDir, ...callerAdditional],
807
- piPackages: compactPiPackages([...result.piPackages, ...callerPiPackages])
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 factoryPlugins = opts.pluginFactories?.map((factory) => factory({ workspaceRoot, bridge })) ?? [];
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: [...opts.plugins ?? [], ...factoryPlugins]
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
- buildBoringSystemPrompt(),
853
- pluginCollection.agentOptions.systemPromptAppend
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
- resourceLoaderOptions: pluginCollection.agentOptions.resourceLoaderOptions
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
- defineServerPlugin,
869
- provisionWorkspaceAgentServer
2130
+ hasDirServerPlugin,
2131
+ provisionWorkspaceAgentServer,
2132
+ readWorkspacePluginPackagePiSnapshot,
2133
+ resolveDefaultWorkspacePluginPackagePaths,
2134
+ resolveOnePluginEntry
870
2135
  };