@hachej/boring-workspace 0.1.23 → 0.1.26
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/dist/{FileTree-D8Rmj8Bo.js → FileTree-BZGu5Ap6.js} +114 -104
- package/dist/{MarkdownEditor-DKC4gNT4.js → MarkdownEditor-DshmttZM.js} +9 -9
- package/dist/{WorkspaceLoadingState-hKrnYCL3.js → WorkspaceLoadingState-DVCLcOQu.js} +167 -146
- package/dist/WorkspaceProvider-DQ-325Qs.js +6367 -0
- package/dist/app-front.d.ts +114 -2
- package/dist/app-front.js +787 -333
- package/dist/app-server.d.ts +10 -3
- package/dist/app-server.js +744 -579
- package/dist/createInMemoryBridge--ZFPAgXy.d.ts +161 -0
- package/dist/events.d.ts +3 -0
- package/dist/{manifest-CyNNdfYz.d.ts → manifest-C2vVgH_e.d.ts} +2 -0
- package/dist/plugin.d.ts +8 -3
- package/dist/plugin.js +3 -2
- package/dist/server.d.ts +50 -70
- package/dist/server.js +192 -44
- package/dist/shared.d.ts +2 -2
- package/dist/{surface-COYagY2m.d.ts → surface-CEEkd81D.d.ts} +1 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +409 -404
- package/dist/{ui-bridge-CT18yqwN.d.ts → ui-bridge-Bdgl2hR8.d.ts} +2 -0
- package/dist/workspace.css +73 -0
- package/dist/workspace.d.ts +228 -6
- package/dist/workspace.js +188 -179
- package/docs/INTERFACES.md +6 -0
- package/docs/plans/FULL_PAGE_PANEL_ROUTE_SPEC.md +633 -0
- package/package.json +6 -6
- package/dist/WorkspaceProvider-Cn0sPgaB.js +0 -5976
- package/dist/createInMemoryBridge-CYNW1h_o.d.ts +0 -61
package/dist/app-server.js
CHANGED
|
@@ -54,7 +54,7 @@ function buildBoringSystemPrompt(opts) {
|
|
|
54
54
|
if (opts.scaffoldCommand) {
|
|
55
55
|
n += 1;
|
|
56
56
|
steps.push(
|
|
57
|
-
`**${n}.
|
|
57
|
+
`**${n}. Check plugin-root support, then scaffold.** Bash \`boring-ui plugin-status --json\`; continue only if \`workspaceLocalPluginRoots\` is \`true\`. Then bash \`${opts.scaffoldCommand} <kebab-name> "$BORING_AGENT_WORKSPACE_ROOT"\`. Read generated \`package.json\` + \`front/index.tsx\`; do NOT write from memory.`
|
|
58
58
|
);
|
|
59
59
|
} else {
|
|
60
60
|
n += 1;
|
|
@@ -80,7 +80,7 @@ function buildBoringSystemPrompt(opts) {
|
|
|
80
80
|
steps.push(`**${n}. Ask the user to run \`/reload\`** to publish the change.`);
|
|
81
81
|
const docsBlock = boringPiRoot ? [
|
|
82
82
|
"## boring-ui plugin authoring documentation",
|
|
83
|
-
"Read these only when the user asks to build, modify, or debug a workspace plugin. Use your `read` tool with
|
|
83
|
+
"Read these only when the user asks to build, modify, or debug a workspace plugin. Use your `read` tool with these workspace-relative paths; the agent runtime guarantees they exist inside `$BORING_AGENT_WORKSPACE_ROOT`:",
|
|
84
84
|
...buildDocsRefs(boringPiRoot).map((r) => `- ${r.topic}: ${r.path}`),
|
|
85
85
|
"Follow .md cross-references when present (e.g. SKILL.md may link to a reference doc \u2014 read both)."
|
|
86
86
|
].join("\n") : [
|
|
@@ -88,7 +88,7 @@ function buildBoringSystemPrompt(opts) {
|
|
|
88
88
|
"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."
|
|
89
89
|
].join("\n");
|
|
90
90
|
return [
|
|
91
|
-
"You are operating inside boring-ui.
|
|
91
|
+
"You are operating inside boring-ui. Before `.pi/extensions/<name>/`, run `boring-ui plugin-status --json`; continue only when `workspaceLocalPluginRoots` is `true`. Default to `.pi/extensions/<name>/`. Global `~/.pi/agent/extensions/` only for explicit requests.",
|
|
92
92
|
[
|
|
93
93
|
"## Plugin authoring \u2014 required workflow",
|
|
94
94
|
"",
|
|
@@ -98,7 +98,7 @@ function buildBoringSystemPrompt(opts) {
|
|
|
98
98
|
"- API factories: `createPlugin`, `defineFrontPlugin`, `defineComponent` \u2014 use `definePlugin({id, panels, commands, ...})` from `@hachej/boring-workspace/plugin`.",
|
|
99
99
|
"- 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).",
|
|
100
100
|
"- 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.",
|
|
101
|
-
'- File visualizers:
|
|
101
|
+
'- File visualizers: import `WORKSPACE_OPEN_PATH_SURFACE_KIND`/`PaneProps` from `@hachej/boring-workspace/plugin`; import `useApiBaseUrl`/`useWorkspaceRequestId` from `@hachej/boring-workspace`; read `request.target`; fetch `${apiBaseUrl}/api/v1/files/raw?...` with `credentials: "include"` and `x-boring-workspace-id` when present. Never use `/workspace/read` or string kind `"WORKSPACE_OPEN_PATH_SURFACE_KIND"`.',
|
|
102
102
|
"- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, execute }) }`.",
|
|
103
103
|
'- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
|
|
104
104
|
"- 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.",
|
|
@@ -112,7 +112,7 @@ function buildBoringSystemPrompt(opts) {
|
|
|
112
112
|
// src/server/agentPlugins/manager.ts
|
|
113
113
|
import { createHash } from "crypto";
|
|
114
114
|
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";
|
|
115
|
-
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as
|
|
115
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve4 } from "path";
|
|
116
116
|
|
|
117
117
|
// src/shared/plugins/manifest.ts
|
|
118
118
|
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
@@ -162,8 +162,8 @@ function validateBoringField(issues, boring) {
|
|
|
162
162
|
));
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
-
if (boring.id !== void 0) {
|
|
166
|
-
issues.push(issue("
|
|
165
|
+
if (boring.id !== void 0 && (typeof boring.id !== "string" || !isValidBoringPluginId(boring.id))) {
|
|
166
|
+
issues.push(issue("INVALID_ID", "boring.id", "boring.id must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash"));
|
|
167
167
|
}
|
|
168
168
|
const front = boring.front;
|
|
169
169
|
if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
|
|
@@ -177,6 +177,7 @@ function validateBoringField(issues, boring) {
|
|
|
177
177
|
issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
|
|
178
178
|
}
|
|
179
179
|
return {
|
|
180
|
+
...typeof boring.id === "string" ? { id: boring.id } : {},
|
|
180
181
|
...typeof boring.front === "string" ? { front: boring.front } : {},
|
|
181
182
|
...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
|
|
182
183
|
...typeof boring.label === "string" ? { label: boring.label } : {}
|
|
@@ -343,6 +344,8 @@ function resolveSafePluginEntryPath({
|
|
|
343
344
|
|
|
344
345
|
// src/server/agentPlugins/scan.ts
|
|
345
346
|
function pluginIdFromPackageJson(pkg, rootDir) {
|
|
347
|
+
const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
|
|
348
|
+
if (explicitId) return explicitId;
|
|
346
349
|
const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : void 0;
|
|
347
350
|
return (name ?? rootDir.split(/[\\/]/).at(-1) ?? "plugin").replace(/^@/, "").replaceAll("/", "-");
|
|
348
351
|
}
|
|
@@ -526,8 +529,8 @@ import { dirname as dirname4, join as join3 } from "path";
|
|
|
526
529
|
var PLUGIN_SIGNATURE_CACHE_FILE = ".boring-signature.json";
|
|
527
530
|
function pluginFileSignature(path) {
|
|
528
531
|
if (!path || !existsSync3(path)) return "missing";
|
|
529
|
-
const
|
|
530
|
-
return `${
|
|
532
|
+
const stat2 = statSync2(path);
|
|
533
|
+
return `${stat2.mtimeMs}:${stat2.size}`;
|
|
531
534
|
}
|
|
532
535
|
function cachePath(pluginRootDir) {
|
|
533
536
|
return join3(pluginRootDir, PLUGIN_SIGNATURE_CACHE_FILE);
|
|
@@ -548,632 +551,721 @@ function clearPluginSignatureCache(pluginRootDir) {
|
|
|
548
551
|
if (existsSync3(path)) rmSync(path, { force: true });
|
|
549
552
|
}
|
|
550
553
|
|
|
551
|
-
// src/server/agentPlugins/
|
|
552
|
-
|
|
553
|
-
|
|
554
|
+
// src/server/agentPlugins/piPackages.ts
|
|
555
|
+
import { resolve as resolve3 } from "path";
|
|
556
|
+
var REMOTE_PI_PACKAGE_PREFIXES2 = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
|
|
557
|
+
function isRemotePiPackageSource2(source) {
|
|
558
|
+
return REMOTE_PI_PACKAGE_PREFIXES2.some((prefix) => source.startsWith(prefix));
|
|
554
559
|
}
|
|
555
|
-
function
|
|
556
|
-
if (
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
560
|
+
function packageLocalPathFromSource(source) {
|
|
561
|
+
if (isRemotePiPackageSource2(source)) return null;
|
|
562
|
+
return source.startsWith("file:") ? source.slice("file:".length) : source;
|
|
563
|
+
}
|
|
564
|
+
function normalizeLocalPiPackageSource(pluginRoot, source) {
|
|
565
|
+
const localPath = packageLocalPathFromSource(source);
|
|
566
|
+
if (localPath == null) return source;
|
|
567
|
+
if (localPath === "." || localPath === "./") return resolve3(pluginRoot);
|
|
568
|
+
const normalized = localPath.startsWith("./") ? localPath.slice(2) : localPath;
|
|
569
|
+
if (!isSafePluginRelativePath(normalized)) {
|
|
570
|
+
throw new Error(`unsafe Pi package source: ${source}`);
|
|
564
571
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
if (stat.isSymbolicLink()) {
|
|
576
|
-
let target;
|
|
577
|
-
try {
|
|
578
|
-
target = realpathSync2(path);
|
|
579
|
-
} catch {
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
if (visited.has(target)) {
|
|
583
|
-
hash.update(rel);
|
|
584
|
-
hash.update("symlink-cycle");
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
visited.add(target);
|
|
588
|
-
const targetStat = statSync3(target);
|
|
589
|
-
hash.update(rel);
|
|
590
|
-
hash.update("symlink:");
|
|
591
|
-
hash.update(target);
|
|
592
|
-
if (targetStat.isDirectory()) visit(target, depth + 1);
|
|
593
|
-
else if (targetStat.isFile()) {
|
|
594
|
-
hash.update(String(targetStat.mtimeMs));
|
|
595
|
-
hash.update(String(targetStat.size));
|
|
596
|
-
}
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
hash.update(rel);
|
|
600
|
-
hash.update(String(stat.mtimeMs));
|
|
601
|
-
hash.update(String(stat.size));
|
|
602
|
-
if (stat.isDirectory()) {
|
|
603
|
-
visit(path, depth + 1);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
572
|
+
return resolve3(pluginRoot, normalized);
|
|
573
|
+
}
|
|
574
|
+
function normalizeBoringPluginPiPackageSource(pluginRoot, source) {
|
|
575
|
+
if (typeof source === "string") return normalizeLocalPiPackageSource(pluginRoot, source);
|
|
576
|
+
return {
|
|
577
|
+
source: normalizeLocalPiPackageSource(pluginRoot, source.source),
|
|
578
|
+
...source.extensions ? { extensions: source.extensions } : {},
|
|
579
|
+
...source.skills ? { skills: source.skills } : {},
|
|
580
|
+
...source.prompts ? { prompts: source.prompts } : {},
|
|
581
|
+
...source.themes ? { themes: source.themes } : {}
|
|
606
582
|
};
|
|
607
|
-
visit(root, 0);
|
|
608
|
-
return hash.digest("hex");
|
|
609
583
|
}
|
|
610
|
-
function
|
|
611
|
-
return
|
|
584
|
+
function normalizeBoringPluginPiPackages(plugins) {
|
|
585
|
+
return plugins.flatMap(
|
|
586
|
+
(plugin) => (plugin.pi?.packages ?? []).map(
|
|
587
|
+
(source) => normalizeBoringPluginPiPackageSource(plugin.rootDir, source)
|
|
588
|
+
)
|
|
589
|
+
);
|
|
612
590
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
591
|
+
|
|
592
|
+
// src/server/plugins/piPackages.ts
|
|
593
|
+
import {
|
|
594
|
+
compactPiPackages,
|
|
595
|
+
PI_PACKAGE_RESOURCE_FILTERS
|
|
596
|
+
} from "@hachej/boring-agent/server";
|
|
597
|
+
|
|
598
|
+
// src/server/plugins/defineServerPlugin.ts
|
|
599
|
+
function fail(pluginId, message) {
|
|
600
|
+
throw new Error(`server plugin "${pluginId}": ${message}`);
|
|
622
601
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
this.pluginDirs = options.pluginDirs;
|
|
633
|
-
this.errorRoot = options.errorRoot ?? join4(process.cwd(), ".pi", "extensions");
|
|
602
|
+
function isUrl(value) {
|
|
603
|
+
return value instanceof URL;
|
|
604
|
+
}
|
|
605
|
+
function isPathLike(value) {
|
|
606
|
+
return typeof value === "string" && value.length > 0 || isUrl(value);
|
|
607
|
+
}
|
|
608
|
+
function validateAgentTool(pluginId, tool, index) {
|
|
609
|
+
if (!tool || typeof tool !== "object") {
|
|
610
|
+
fail(pluginId, `agentTools[${index}] must be an object`);
|
|
634
611
|
}
|
|
635
|
-
|
|
636
|
-
|
|
612
|
+
const candidate = tool;
|
|
613
|
+
if (!candidate.name || typeof candidate.name !== "string") {
|
|
614
|
+
fail(pluginId, `agentTools[${index}].name must be a non-empty string`);
|
|
637
615
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
id: plugin.id,
|
|
641
|
-
boring: plugin.boring,
|
|
642
|
-
...plugin.pi ? { pi: plugin.pi } : {},
|
|
643
|
-
version: plugin.version,
|
|
644
|
-
revision: plugin.revision,
|
|
645
|
-
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
|
|
646
|
-
}));
|
|
616
|
+
if (typeof candidate.description !== "string") {
|
|
617
|
+
fail(pluginId, `agentTools[${index}].description must be a string`);
|
|
647
618
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (!path || !existsSync4(path)) return null;
|
|
651
|
-
return readFileSync3(path, "utf8");
|
|
619
|
+
if (!candidate.parameters || typeof candidate.parameters !== "object") {
|
|
620
|
+
fail(pluginId, `agentTools[${index}].parameters must be an object`);
|
|
652
621
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return () => this.listeners.delete(listener);
|
|
622
|
+
if (typeof candidate.execute !== "function") {
|
|
623
|
+
fail(pluginId, `agentTools[${index}].execute must be a function`);
|
|
656
624
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
625
|
+
}
|
|
626
|
+
function validatePiPackages2(pluginId, piPackages) {
|
|
627
|
+
for (let i = 0; i < piPackages.length; i++) {
|
|
628
|
+
const source = piPackages[i];
|
|
629
|
+
if (typeof source === "string") {
|
|
630
|
+
if (source.length === 0) {
|
|
631
|
+
fail(pluginId, `piPackages[${i}] must be a non-empty string`);
|
|
632
|
+
}
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
636
|
+
fail(pluginId, `piPackages[${i}] must be a string or package source object`);
|
|
637
|
+
}
|
|
638
|
+
const candidate = source;
|
|
639
|
+
if (typeof candidate.source !== "string" || candidate.source.length === 0) {
|
|
640
|
+
fail(pluginId, `piPackages[${i}].source must be a non-empty string`);
|
|
641
|
+
}
|
|
642
|
+
for (const key of PI_PACKAGE_RESOURCE_FILTERS) {
|
|
643
|
+
const value = candidate[key];
|
|
644
|
+
if (value === void 0) continue;
|
|
645
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
|
|
646
|
+
fail(pluginId, `piPackages[${i}].${key} must be a string array when provided`);
|
|
647
|
+
}
|
|
661
648
|
}
|
|
662
|
-
this.loading = this.drainLoads().finally(() => {
|
|
663
|
-
this.loading = null;
|
|
664
|
-
});
|
|
665
|
-
return this.loading;
|
|
666
649
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
650
|
+
}
|
|
651
|
+
function validateSkills(pluginId, skills) {
|
|
652
|
+
for (let i = 0; i < skills.length; i++) {
|
|
653
|
+
const skill = skills[i];
|
|
654
|
+
if (!skill || typeof skill !== "object") {
|
|
655
|
+
fail(pluginId, `skills[${i}] must be an object`);
|
|
656
|
+
}
|
|
657
|
+
if (!skill.name || typeof skill.name !== "string") {
|
|
658
|
+
fail(pluginId, `skills[${i}].name must be a non-empty string`);
|
|
659
|
+
}
|
|
660
|
+
if (!isPathLike(skill.source)) {
|
|
661
|
+
fail(pluginId, `skills[${i}].source must be a string or URL`);
|
|
662
|
+
}
|
|
674
663
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
if (
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
664
|
+
}
|
|
665
|
+
function validateProvisioning(pluginId, provisioning) {
|
|
666
|
+
if (!provisioning || typeof provisioning !== "object") {
|
|
667
|
+
fail(pluginId, "provisioning must be an object");
|
|
668
|
+
}
|
|
669
|
+
if (provisioning.templateDirs !== void 0) {
|
|
670
|
+
if (!Array.isArray(provisioning.templateDirs)) {
|
|
671
|
+
fail(pluginId, "provisioning.templateDirs must be an array when provided");
|
|
672
|
+
}
|
|
673
|
+
for (let i = 0; i < provisioning.templateDirs.length; i++) {
|
|
674
|
+
const contribution = provisioning.templateDirs[i];
|
|
675
|
+
if (!contribution || typeof contribution !== "object") {
|
|
676
|
+
fail(pluginId, `provisioning.templateDirs[${i}] must be an object`);
|
|
677
|
+
}
|
|
678
|
+
if (!contribution.id || typeof contribution.id !== "string") {
|
|
679
|
+
fail(pluginId, `provisioning.templateDirs[${i}].id must be a non-empty string`);
|
|
680
|
+
}
|
|
681
|
+
if (!isPathLike(contribution.path)) {
|
|
682
|
+
fail(pluginId, `provisioning.templateDirs[${i}].path must be a string or URL`);
|
|
683
|
+
}
|
|
684
|
+
if (contribution.target !== void 0 && typeof contribution.target !== "string") {
|
|
685
|
+
fail(pluginId, `provisioning.templateDirs[${i}].target must be a string when provided`);
|
|
694
686
|
}
|
|
695
|
-
const event = { type: "boring.plugin.unload", id, revision };
|
|
696
|
-
events.push(event);
|
|
697
|
-
this.emit(event);
|
|
698
687
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
688
|
+
}
|
|
689
|
+
if (provisioning.nodePackages !== void 0) {
|
|
690
|
+
if (!Array.isArray(provisioning.nodePackages)) {
|
|
691
|
+
fail(pluginId, "provisioning.nodePackages must be an array when provided");
|
|
692
|
+
}
|
|
693
|
+
for (let i = 0; i < provisioning.nodePackages.length; i++) {
|
|
694
|
+
const spec = provisioning.nodePackages[i];
|
|
695
|
+
if (!spec || typeof spec !== "object") {
|
|
696
|
+
fail(pluginId, `provisioning.nodePackages[${i}] must be an object`);
|
|
697
|
+
}
|
|
698
|
+
if (!spec.id || typeof spec.id !== "string") {
|
|
699
|
+
fail(pluginId, `provisioning.nodePackages[${i}].id must be a non-empty string`);
|
|
700
|
+
}
|
|
701
|
+
if (!spec.packageName || typeof spec.packageName !== "string") {
|
|
702
|
+
fail(pluginId, `provisioning.nodePackages[${i}].packageName must be a non-empty string`);
|
|
703
|
+
}
|
|
704
|
+
if (spec.packageRoot !== void 0 && !isPathLike(spec.packageRoot)) {
|
|
705
|
+
fail(pluginId, `provisioning.nodePackages[${i}].packageRoot must be a string or URL`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (provisioning.python !== void 0) {
|
|
710
|
+
if (!Array.isArray(provisioning.python)) {
|
|
711
|
+
fail(pluginId, "provisioning.python must be an array when provided");
|
|
712
|
+
}
|
|
713
|
+
for (let i = 0; i < provisioning.python.length; i++) {
|
|
714
|
+
const spec = provisioning.python[i];
|
|
715
|
+
if (!spec || typeof spec !== "object") {
|
|
716
|
+
fail(pluginId, `provisioning.python[${i}] must be an object`);
|
|
717
|
+
}
|
|
718
|
+
if (!spec.id || typeof spec.id !== "string") {
|
|
719
|
+
fail(pluginId, `provisioning.python[${i}].id must be a non-empty string`);
|
|
720
|
+
}
|
|
721
|
+
if (!isPathLike(spec.projectFile)) {
|
|
722
|
+
fail(pluginId, `provisioning.python[${i}].projectFile must be a string or URL`);
|
|
723
|
+
}
|
|
724
|
+
if (spec.extraLibs !== void 0 && (!Array.isArray(spec.extraLibs) || spec.extraLibs.some((item) => typeof item !== "string"))) {
|
|
725
|
+
fail(pluginId, `provisioning.python[${i}].extraLibs must be a string array when provided`);
|
|
726
|
+
}
|
|
727
|
+
if (spec.env !== void 0) {
|
|
728
|
+
if (!spec.env || typeof spec.env !== "object" || Array.isArray(spec.env)) {
|
|
729
|
+
fail(pluginId, `provisioning.python[${i}].env must be an object when provided`);
|
|
730
|
+
}
|
|
731
|
+
for (const [key, value] of Object.entries(spec.env)) {
|
|
732
|
+
if (!key || !isPathLike(value)) {
|
|
733
|
+
fail(pluginId, `provisioning.python[${i}].env values must be strings or URLs`);
|
|
734
|
+
}
|
|
712
735
|
}
|
|
713
|
-
const requiresRestart = computeRequiresRestart(previous, plugin);
|
|
714
|
-
const event = {
|
|
715
|
-
type: "boring.plugin.load",
|
|
716
|
-
id: plugin.id,
|
|
717
|
-
boring: plugin.boring,
|
|
718
|
-
version: plugin.version,
|
|
719
|
-
revision,
|
|
720
|
-
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {},
|
|
721
|
-
...requiresRestart.length > 0 ? { requiresRestart } : {}
|
|
722
|
-
};
|
|
723
|
-
events.push(event);
|
|
724
|
-
this.emit(event);
|
|
725
|
-
} catch (error) {
|
|
726
|
-
const revision = this.bumpRevision(plugin.id);
|
|
727
|
-
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
728
|
-
this.writeError(plugin.id, message);
|
|
729
|
-
const event = { type: "boring.plugin.error", id: plugin.id, revision, message };
|
|
730
|
-
errors.push({ id: plugin.id, revision, message });
|
|
731
|
-
events.push(event);
|
|
732
|
-
this.emit(event);
|
|
733
736
|
}
|
|
734
737
|
}
|
|
735
|
-
return { loaded: this.list(), events, errors };
|
|
736
738
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
739
|
+
}
|
|
740
|
+
function validateServerPlugin(plugin) {
|
|
741
|
+
if (!plugin.id || typeof plugin.id !== "string") {
|
|
742
|
+
fail(plugin.id ?? "<unknown>", "id must be a non-empty string");
|
|
743
|
+
}
|
|
744
|
+
if (plugin.label !== void 0 && typeof plugin.label !== "string") {
|
|
745
|
+
fail(plugin.id, "label must be a string when provided");
|
|
746
|
+
}
|
|
747
|
+
if (plugin.systemPrompt !== void 0 && typeof plugin.systemPrompt !== "string") {
|
|
748
|
+
fail(plugin.id, "systemPrompt must be a string when provided");
|
|
749
|
+
}
|
|
750
|
+
if (plugin.piPackages !== void 0) {
|
|
751
|
+
if (!Array.isArray(plugin.piPackages)) {
|
|
752
|
+
fail(plugin.id, "piPackages must be an array when provided");
|
|
753
|
+
}
|
|
754
|
+
validatePiPackages2(plugin.id, plugin.piPackages);
|
|
755
|
+
}
|
|
756
|
+
if (plugin.extensionPaths !== void 0) {
|
|
757
|
+
if (!Array.isArray(plugin.extensionPaths)) {
|
|
758
|
+
fail(plugin.id, "extensionPaths must be an array when provided");
|
|
750
759
|
}
|
|
760
|
+
plugin.extensionPaths.forEach((path, index) => {
|
|
761
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
762
|
+
fail(plugin.id, `extensionPaths[${index}] must be a non-empty string`);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
751
765
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
766
|
+
if (plugin.skills !== void 0) {
|
|
767
|
+
if (!Array.isArray(plugin.skills)) {
|
|
768
|
+
fail(plugin.id, "skills must be an array when provided");
|
|
769
|
+
}
|
|
770
|
+
validateSkills(plugin.id, plugin.skills);
|
|
756
771
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
listener(event);
|
|
761
|
-
} catch (error) {
|
|
762
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
763
|
-
console.error(`[BoringPluginAssetManager] listener threw on ${event.type} for ${event.id}: ${message}`);
|
|
764
|
-
}
|
|
772
|
+
if (plugin.agentTools !== void 0) {
|
|
773
|
+
if (!Array.isArray(plugin.agentTools)) {
|
|
774
|
+
fail(plugin.id, "agentTools must be an array when provided");
|
|
765
775
|
}
|
|
776
|
+
plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
|
|
766
777
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const root = resolve3(this.errorRoot);
|
|
770
|
-
const path = resolve3(root, pluginId, ".error");
|
|
771
|
-
const rel = relative2(root, path);
|
|
772
|
-
if (rel.startsWith("..") || isAbsolute2(rel)) return null;
|
|
773
|
-
return path;
|
|
778
|
+
if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
|
|
779
|
+
fail(plugin.id, "routes must be a Fastify plugin function when provided");
|
|
774
780
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
writeFileSync2(path, message, "utf8");
|
|
781
|
+
if (plugin.preservedUiStateKeys !== void 0) {
|
|
782
|
+
if (!Array.isArray(plugin.preservedUiStateKeys) || plugin.preservedUiStateKeys.some((key) => typeof key !== "string" || key.length === 0)) {
|
|
783
|
+
fail(plugin.id, "preservedUiStateKeys must be a non-empty string array when provided");
|
|
784
|
+
}
|
|
780
785
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (path && existsSync4(path)) rmSync2(path, { force: true });
|
|
786
|
+
if (plugin.provisioning !== void 0) {
|
|
787
|
+
validateProvisioning(plugin.id, plugin.provisioning);
|
|
784
788
|
}
|
|
785
|
-
}
|
|
789
|
+
}
|
|
786
790
|
|
|
787
|
-
// src/server/
|
|
788
|
-
function
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
791
|
+
// src/server/plugins/bootstrapServer.ts
|
|
792
|
+
function bootstrapServer(options) {
|
|
793
|
+
const excludedDefaults = new Set(options.excludeDefaults ?? []);
|
|
794
|
+
const finalPlugins = [
|
|
795
|
+
...(options.defaults ?? []).filter((p) => !excludedDefaults.has(p.id)),
|
|
796
|
+
...options.plugins ?? []
|
|
797
|
+
];
|
|
798
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
799
|
+
for (const plugin of finalPlugins) {
|
|
800
|
+
validateServerPlugin(plugin);
|
|
801
|
+
if (seenIds.has(plugin.id)) {
|
|
802
|
+
throw new Error(`plugin "${plugin.id}" registered twice`);
|
|
803
|
+
}
|
|
804
|
+
seenIds.add(plugin.id);
|
|
799
805
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
app.post("/api/boring.reload", async (_request, reply) => {
|
|
806
|
-
const scan = await manager.load();
|
|
807
|
-
const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
|
|
808
|
-
const restart_warnings = collectRestartWarnings(scan.events);
|
|
809
|
-
const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
|
|
810
|
-
if (hasFailures) {
|
|
811
|
-
return reply.status(422).send({
|
|
812
|
-
ok: false,
|
|
813
|
-
errors: scan.errors,
|
|
814
|
-
diagnostics: rebuild.diagnostics,
|
|
815
|
-
plugins: scan.loaded,
|
|
816
|
-
// Even on failure, emit warnings for plugins that DID reload
|
|
817
|
-
// — partial-failure tolerance means some loaded successfully.
|
|
818
|
-
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
return reply.send({
|
|
822
|
-
ok: true,
|
|
823
|
-
plugins: scan.loaded,
|
|
824
|
-
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
825
|
-
});
|
|
826
|
-
});
|
|
806
|
+
const agentTools = [];
|
|
807
|
+
for (const plugin of finalPlugins) {
|
|
808
|
+
for (const tool of plugin.agentTools ?? []) {
|
|
809
|
+
agentTools.push(tool);
|
|
810
|
+
}
|
|
827
811
|
}
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
812
|
+
const systemPromptAppend = finalPlugins.filter((p) => p.systemPrompt && p.systemPrompt.trim()).map((p) => p.systemPrompt.trim()).join("\n\n");
|
|
813
|
+
const piPackages = compactPiPackages(finalPlugins.flatMap((plugin) => plugin.piPackages ?? []));
|
|
814
|
+
const extensionPaths = finalPlugins.flatMap((p) => p.extensionPaths ?? []);
|
|
815
|
+
const provisioningContributions = finalPlugins.filter((p) => p.provisioning).map((p) => ({ id: p.id, provisioning: p.provisioning }));
|
|
816
|
+
const runtimePlugins = finalPlugins.map((plugin) => ({
|
|
817
|
+
id: plugin.id,
|
|
818
|
+
...plugin.skills ? { skills: plugin.skills } : {},
|
|
819
|
+
...plugin.provisioning ? { provisioning: plugin.provisioning } : {}
|
|
820
|
+
}));
|
|
821
|
+
const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
|
|
822
|
+
const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
|
|
823
|
+
return {
|
|
824
|
+
registered: finalPlugins.map((p) => p.id),
|
|
825
|
+
systemPromptAppend,
|
|
826
|
+
piPackages,
|
|
827
|
+
extensionPaths,
|
|
828
|
+
agentTools,
|
|
829
|
+
runtimePlugins,
|
|
830
|
+
provisioningContributions,
|
|
831
|
+
routeContributions,
|
|
832
|
+
preservedUiStateKeys
|
|
834
833
|
};
|
|
835
|
-
app.get("/api/v1/agent-plugins/:id/error", getPluginError);
|
|
836
|
-
app.get("/api/v1/agent-plugins/events", async (request, reply) => {
|
|
837
|
-
reply.hijack();
|
|
838
|
-
const res = reply.raw;
|
|
839
|
-
res.statusCode = 200;
|
|
840
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
841
|
-
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
842
|
-
res.setHeader("Connection", "keep-alive");
|
|
843
|
-
res.setHeader("X-Accel-Buffering", "no");
|
|
844
|
-
res.flushHeaders?.();
|
|
845
|
-
const write = (event) => {
|
|
846
|
-
try {
|
|
847
|
-
res.write(`event: ${event.type}
|
|
848
|
-
`);
|
|
849
|
-
res.write(`data: ${JSON.stringify(event)}
|
|
850
|
-
|
|
851
|
-
`);
|
|
852
|
-
} catch {
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
for (const plugin of manager.list()) {
|
|
856
|
-
write({
|
|
857
|
-
type: "boring.plugin.load",
|
|
858
|
-
id: plugin.id,
|
|
859
|
-
boring: plugin.boring,
|
|
860
|
-
version: plugin.version,
|
|
861
|
-
revision: plugin.revision,
|
|
862
|
-
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
const unsubscribe = manager.subscribe(write);
|
|
866
|
-
const heartbeat = setInterval(() => {
|
|
867
|
-
try {
|
|
868
|
-
res.write(": heartbeat\n\n");
|
|
869
|
-
} catch {
|
|
870
|
-
}
|
|
871
|
-
}, 25e3);
|
|
872
|
-
request.raw.on("close", () => {
|
|
873
|
-
clearInterval(heartbeat);
|
|
874
|
-
unsubscribe();
|
|
875
|
-
});
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// src/server/agentPlugins/aggregatePluginPrompts.ts
|
|
880
|
-
function aggregatePluginPrompts(manager) {
|
|
881
|
-
const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
882
|
-
if (prompts.length === 0) return void 0;
|
|
883
|
-
return `# Loaded boring-ui plugin context
|
|
884
|
-
|
|
885
|
-
${prompts.join("\n\n")}`;
|
|
886
834
|
}
|
|
887
835
|
|
|
888
|
-
// src/server/agentPlugins/
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
function isRemotePiPackageSource2(source) {
|
|
892
|
-
return REMOTE_PI_PACKAGE_PREFIXES2.some((prefix) => source.startsWith(prefix));
|
|
836
|
+
// src/server/agentPlugins/manager.ts
|
|
837
|
+
function skillPathForPiLoader(path) {
|
|
838
|
+
return existsSync4(join4(path, "SKILL.md")) ? dirname5(path) : path;
|
|
893
839
|
}
|
|
894
|
-
function
|
|
895
|
-
|
|
896
|
-
return source.startsWith("file:") ? source.slice("file:".length) : source;
|
|
840
|
+
function preflightErrorId(pluginDir) {
|
|
841
|
+
return `preflight-${createHash("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
|
|
897
842
|
}
|
|
898
|
-
function
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
843
|
+
function directorySignature(root) {
|
|
844
|
+
if (!root || !existsSync4(root)) return "missing";
|
|
845
|
+
const hash = createHash("sha256");
|
|
846
|
+
const visited = /* @__PURE__ */ new Set();
|
|
847
|
+
let rootReal;
|
|
848
|
+
try {
|
|
849
|
+
rootReal = realpathSync2(root);
|
|
850
|
+
} catch {
|
|
851
|
+
return "missing";
|
|
905
852
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
853
|
+
visited.add(rootReal);
|
|
854
|
+
let count = 0;
|
|
855
|
+
const visit = (dir, depth) => {
|
|
856
|
+
if (depth > 8 || count > 5e4) return;
|
|
857
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").sort((a, b) => a.name.localeCompare(b.name));
|
|
858
|
+
for (const entry of entries) {
|
|
859
|
+
count++;
|
|
860
|
+
const path = join4(dir, entry.name);
|
|
861
|
+
const rel = relative2(root, path);
|
|
862
|
+
const stat2 = lstatSync(path);
|
|
863
|
+
if (stat2.isSymbolicLink()) {
|
|
864
|
+
let target;
|
|
865
|
+
try {
|
|
866
|
+
target = realpathSync2(path);
|
|
867
|
+
} catch {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (visited.has(target)) {
|
|
871
|
+
hash.update(rel);
|
|
872
|
+
hash.update("symlink-cycle");
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
visited.add(target);
|
|
876
|
+
const targetStat = statSync3(target);
|
|
877
|
+
hash.update(rel);
|
|
878
|
+
hash.update("symlink:");
|
|
879
|
+
hash.update(target);
|
|
880
|
+
if (targetStat.isDirectory()) visit(target, depth + 1);
|
|
881
|
+
else if (targetStat.isFile()) {
|
|
882
|
+
hash.update(String(targetStat.mtimeMs));
|
|
883
|
+
hash.update(String(targetStat.size));
|
|
884
|
+
}
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
hash.update(rel);
|
|
888
|
+
hash.update(String(stat2.mtimeMs));
|
|
889
|
+
hash.update(String(stat2.size));
|
|
890
|
+
if (stat2.isDirectory()) {
|
|
891
|
+
visit(path, depth + 1);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
916
894
|
};
|
|
895
|
+
visit(root, 0);
|
|
896
|
+
return hash.digest("hex");
|
|
917
897
|
}
|
|
918
|
-
function
|
|
919
|
-
return
|
|
920
|
-
(plugin) => (plugin.pi?.packages ?? []).map(
|
|
921
|
-
(source) => normalizeBoringPluginPiPackageSource(plugin.rootDir, source)
|
|
922
|
-
)
|
|
923
|
-
);
|
|
898
|
+
function normalizePluginSubpath(rootDir, path) {
|
|
899
|
+
return relative2(rootDir, path).replaceAll("\\", "/");
|
|
924
900
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
import { pathToFileURL } from "url";
|
|
931
|
-
|
|
932
|
-
// src/server/plugins/piPackages.ts
|
|
933
|
-
import {
|
|
934
|
-
compactPiPackages,
|
|
935
|
-
PI_PACKAGE_RESOURCE_FILTERS
|
|
936
|
-
} from "@hachej/boring-agent/server";
|
|
937
|
-
|
|
938
|
-
// src/server/plugins/defineServerPlugin.ts
|
|
939
|
-
function fail(pluginId, message) {
|
|
940
|
-
throw new Error(`server plugin "${pluginId}": ${message}`);
|
|
901
|
+
function frontSignatureRoot(plugin) {
|
|
902
|
+
if (!plugin.frontPath) return void 0;
|
|
903
|
+
const frontRoot = join4(plugin.rootDir, "front");
|
|
904
|
+
const rel = relative2(frontRoot, plugin.frontPath);
|
|
905
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel) ? frontRoot : dirname5(plugin.frontPath);
|
|
941
906
|
}
|
|
942
|
-
function
|
|
943
|
-
return
|
|
907
|
+
function pluginSignature(plugin) {
|
|
908
|
+
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(frontSignatureRoot(plugin))).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");
|
|
944
909
|
}
|
|
945
|
-
function
|
|
946
|
-
|
|
910
|
+
function computeRequiresRestart(previous, next) {
|
|
911
|
+
if (!previous) return [];
|
|
912
|
+
const prevHasServer = !!previous.serverPath;
|
|
913
|
+
const nextHasServer = !!next.serverPath;
|
|
914
|
+
if (!prevHasServer && !nextHasServer) return [];
|
|
915
|
+
if (prevHasServer !== nextHasServer) return ["routes", "agentTools"];
|
|
916
|
+
const nextSig = pluginFileSignature(next.serverPath);
|
|
917
|
+
if (previous.serverSignature === nextSig) return [];
|
|
918
|
+
return ["routes", "agentTools"];
|
|
947
919
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
920
|
+
var BoringPluginAssetManager = class {
|
|
921
|
+
pluginDirs;
|
|
922
|
+
errorRoot;
|
|
923
|
+
frontTargetResolver;
|
|
924
|
+
includeLegacyFrontUrl;
|
|
925
|
+
loaded = /* @__PURE__ */ new Map();
|
|
926
|
+
revisions = /* @__PURE__ */ new Map();
|
|
927
|
+
listeners = /* @__PURE__ */ new Set();
|
|
928
|
+
lastErrors = /* @__PURE__ */ new Map();
|
|
929
|
+
loading = null;
|
|
930
|
+
reloadQueued = false;
|
|
931
|
+
constructor(options) {
|
|
932
|
+
this.pluginDirs = options.pluginDirs;
|
|
933
|
+
this.errorRoot = options.errorRoot ?? join4(process.cwd(), ".pi", "extensions");
|
|
934
|
+
this.frontTargetResolver = options.frontTargetResolver;
|
|
935
|
+
this.includeLegacyFrontUrl = options.includeLegacyFrontUrl ?? true;
|
|
951
936
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
fail(pluginId, `agentTools[${index}].name must be a non-empty string`);
|
|
937
|
+
preflight() {
|
|
938
|
+
return preflightBoringPlugins(this.pluginDirs);
|
|
955
939
|
}
|
|
956
|
-
|
|
957
|
-
|
|
940
|
+
list() {
|
|
941
|
+
return [...this.loaded.values()].map((plugin) => this.toListEntry(plugin));
|
|
958
942
|
}
|
|
959
|
-
|
|
960
|
-
|
|
943
|
+
getError(pluginId) {
|
|
944
|
+
const path = this.errorPath(pluginId);
|
|
945
|
+
if (!path || !existsSync4(path)) return null;
|
|
946
|
+
return readFileSync3(path, "utf8");
|
|
961
947
|
}
|
|
962
|
-
|
|
963
|
-
|
|
948
|
+
getErrors() {
|
|
949
|
+
return [...this.lastErrors.values()];
|
|
964
950
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
}
|
|
975
|
-
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
976
|
-
fail(pluginId, `piPackages[${i}] must be a string or package source object`);
|
|
977
|
-
}
|
|
978
|
-
const candidate = source;
|
|
979
|
-
if (typeof candidate.source !== "string" || candidate.source.length === 0) {
|
|
980
|
-
fail(pluginId, `piPackages[${i}].source must be a non-empty string`);
|
|
981
|
-
}
|
|
982
|
-
for (const key of PI_PACKAGE_RESOURCE_FILTERS) {
|
|
983
|
-
const value = candidate[key];
|
|
984
|
-
if (value === void 0) continue;
|
|
985
|
-
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
|
|
986
|
-
fail(pluginId, `piPackages[${i}].${key} must be a string array when provided`);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
951
|
+
inspectLoaded() {
|
|
952
|
+
return [...this.loaded.values()].map((plugin) => ({
|
|
953
|
+
id: plugin.id,
|
|
954
|
+
version: plugin.version,
|
|
955
|
+
revision: plugin.revision,
|
|
956
|
+
rootDir: plugin.rootDir,
|
|
957
|
+
...plugin.frontPath ? { frontPath: plugin.frontPath } : {},
|
|
958
|
+
...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
|
|
959
|
+
}));
|
|
989
960
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
fail(pluginId, `skills[${i}].source must be a string or URL`);
|
|
1002
|
-
}
|
|
961
|
+
inspectLoadedPiSnapshot() {
|
|
962
|
+
const plugins = [...this.loaded.values()];
|
|
963
|
+
const prompts = plugins.map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
964
|
+
return {
|
|
965
|
+
additionalSkillPaths: [...new Set(plugins.flatMap((plugin) => plugin.skillPaths ?? []).map(skillPathForPiLoader))],
|
|
966
|
+
packages: compactPiPackages(normalizeBoringPluginPiPackages(plugins)),
|
|
967
|
+
extensionPaths: plugins.flatMap((plugin) => plugin.extensionPaths ?? []),
|
|
968
|
+
...prompts.length > 0 ? { systemPromptAppend: `# Loaded boring-ui plugin context
|
|
969
|
+
|
|
970
|
+
${prompts.join("\n\n")}` } : {}
|
|
971
|
+
};
|
|
1003
972
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
fail(pluginId, "provisioning must be an object");
|
|
973
|
+
subscribe(listener) {
|
|
974
|
+
this.listeners.add(listener);
|
|
975
|
+
return () => this.listeners.delete(listener);
|
|
1008
976
|
}
|
|
1009
|
-
|
|
1010
|
-
if (
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
for (let i = 0; i < provisioning.templateDirs.length; i++) {
|
|
1014
|
-
const contribution = provisioning.templateDirs[i];
|
|
1015
|
-
if (!contribution || typeof contribution !== "object") {
|
|
1016
|
-
fail(pluginId, `provisioning.templateDirs[${i}] must be an object`);
|
|
1017
|
-
}
|
|
1018
|
-
if (!contribution.id || typeof contribution.id !== "string") {
|
|
1019
|
-
fail(pluginId, `provisioning.templateDirs[${i}].id must be a non-empty string`);
|
|
1020
|
-
}
|
|
1021
|
-
if (!isPathLike(contribution.path)) {
|
|
1022
|
-
fail(pluginId, `provisioning.templateDirs[${i}].path must be a string or URL`);
|
|
1023
|
-
}
|
|
1024
|
-
if (contribution.target !== void 0 && typeof contribution.target !== "string") {
|
|
1025
|
-
fail(pluginId, `provisioning.templateDirs[${i}].target must be a string when provided`);
|
|
1026
|
-
}
|
|
977
|
+
async load() {
|
|
978
|
+
if (this.loading) {
|
|
979
|
+
this.reloadQueued = true;
|
|
980
|
+
return this.loading;
|
|
1027
981
|
}
|
|
982
|
+
this.loading = this.drainLoads().finally(() => {
|
|
983
|
+
this.loading = null;
|
|
984
|
+
});
|
|
985
|
+
return this.loading;
|
|
1028
986
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
fail(pluginId, `provisioning.nodePackages[${i}] must be an object`);
|
|
1037
|
-
}
|
|
1038
|
-
if (!spec.id || typeof spec.id !== "string") {
|
|
1039
|
-
fail(pluginId, `provisioning.nodePackages[${i}].id must be a non-empty string`);
|
|
1040
|
-
}
|
|
1041
|
-
if (!spec.packageName || typeof spec.packageName !== "string") {
|
|
1042
|
-
fail(pluginId, `provisioning.nodePackages[${i}].packageName must be a non-empty string`);
|
|
1043
|
-
}
|
|
1044
|
-
if (spec.packageRoot !== void 0 && !isPathLike(spec.packageRoot)) {
|
|
1045
|
-
fail(pluginId, `provisioning.nodePackages[${i}].packageRoot must be a string or URL`);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
987
|
+
async drainLoads() {
|
|
988
|
+
let result;
|
|
989
|
+
do {
|
|
990
|
+
this.reloadQueued = false;
|
|
991
|
+
result = await this.doLoadOnce();
|
|
992
|
+
} while (this.reloadQueued);
|
|
993
|
+
return result;
|
|
1048
994
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
fail(pluginId, `provisioning.python[${i}].env must be an object when provided`);
|
|
995
|
+
async doLoadOnce() {
|
|
996
|
+
this.lastErrors.clear();
|
|
997
|
+
const scan = scanBoringPlugins(this.pluginDirs);
|
|
998
|
+
const nextPlugins = scan.plugins;
|
|
999
|
+
const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
|
|
1000
|
+
const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve4(error.pluginDir)));
|
|
1001
|
+
const events = [];
|
|
1002
|
+
const errors = [];
|
|
1003
|
+
this.collectPreflightErrors(scan.preflight, events, errors);
|
|
1004
|
+
for (const id of [...this.loaded.keys()]) {
|
|
1005
|
+
if (nextIds.has(id)) continue;
|
|
1006
|
+
const previous = this.loaded.get(id);
|
|
1007
|
+
if (previous && invalidPluginDirs.has(resolve4(previous.rootDir))) continue;
|
|
1008
|
+
const revision = this.bumpRevision(id);
|
|
1009
|
+
this.loaded.delete(id);
|
|
1010
|
+
this.lastErrors.delete(id);
|
|
1011
|
+
if (previous) {
|
|
1012
|
+
try {
|
|
1013
|
+
clearPluginSignatureCache(previous.rootDir);
|
|
1014
|
+
} catch {
|
|
1070
1015
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1016
|
+
}
|
|
1017
|
+
const event = { type: "boring.plugin.unload", id, revision };
|
|
1018
|
+
events.push(event);
|
|
1019
|
+
this.emit(event);
|
|
1020
|
+
}
|
|
1021
|
+
for (const plugin of nextPlugins) {
|
|
1022
|
+
try {
|
|
1023
|
+
const signature = pluginSignature(plugin);
|
|
1024
|
+
const previous = this.loaded.get(plugin.id);
|
|
1025
|
+
if (previous?.signature === signature) continue;
|
|
1026
|
+
const revision = this.bumpRevision(plugin.id);
|
|
1027
|
+
const frontTarget = this.resolveFrontTarget(plugin, revision);
|
|
1028
|
+
const serverSignature = plugin.serverPath ? pluginFileSignature(plugin.serverPath) : null;
|
|
1029
|
+
const record = {
|
|
1030
|
+
...plugin,
|
|
1031
|
+
revision,
|
|
1032
|
+
signature,
|
|
1033
|
+
...frontTarget ? { frontTarget } : {},
|
|
1034
|
+
serverSignature
|
|
1035
|
+
};
|
|
1036
|
+
this.loaded.set(plugin.id, record);
|
|
1037
|
+
this.lastErrors.delete(plugin.id);
|
|
1038
|
+
this.clearError(plugin.id);
|
|
1039
|
+
try {
|
|
1040
|
+
writePluginSignatureCache(plugin.rootDir, { serverSignature });
|
|
1041
|
+
} catch {
|
|
1075
1042
|
}
|
|
1043
|
+
const requiresRestart = computeRequiresRestart(previous, plugin);
|
|
1044
|
+
const event = {
|
|
1045
|
+
type: "boring.plugin.load",
|
|
1046
|
+
id: plugin.id,
|
|
1047
|
+
boring: plugin.boring,
|
|
1048
|
+
version: plugin.version,
|
|
1049
|
+
revision,
|
|
1050
|
+
...this.frontUrlPayload(plugin.frontUrl),
|
|
1051
|
+
...frontTarget ? { frontTarget } : {},
|
|
1052
|
+
...requiresRestart.length > 0 ? { requiresRestart } : {}
|
|
1053
|
+
};
|
|
1054
|
+
events.push(event);
|
|
1055
|
+
this.emit(event);
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
const revision = this.bumpRevision(plugin.id);
|
|
1058
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1059
|
+
this.writeError(plugin.id, message);
|
|
1060
|
+
const event = { type: "boring.plugin.error", id: plugin.id, revision, message };
|
|
1061
|
+
const loadError = { id: plugin.id, revision, message };
|
|
1062
|
+
this.lastErrors.set(plugin.id, loadError);
|
|
1063
|
+
errors.push(loadError);
|
|
1064
|
+
events.push(event);
|
|
1065
|
+
this.emit(event);
|
|
1076
1066
|
}
|
|
1077
1067
|
}
|
|
1068
|
+
return { loaded: this.list(), events, errors };
|
|
1078
1069
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1070
|
+
collectPreflightErrors(preflight, events, errors) {
|
|
1071
|
+
for (const error of preflight.errors) {
|
|
1072
|
+
const id = error.pluginId ?? preflightErrorId(error.pluginDir);
|
|
1073
|
+
const revision = this.bumpRevision(id);
|
|
1074
|
+
const message = `${error.code}: ${error.message}
|
|
1075
|
+
|
|
1076
|
+
Plugin dir: ${error.pluginDir}`;
|
|
1077
|
+
const loadError = { id, revision, message };
|
|
1078
|
+
this.lastErrors.set(id, loadError);
|
|
1079
|
+
errors.push(loadError);
|
|
1080
|
+
this.writeError(id, message);
|
|
1081
|
+
const event = { type: "boring.plugin.error", id, revision, message };
|
|
1082
|
+
events.push(event);
|
|
1083
|
+
this.emit(event);
|
|
1084
|
+
}
|
|
1083
1085
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
+
bumpRevision(id) {
|
|
1087
|
+
const next = (this.revisions.get(id) ?? 0) + 1;
|
|
1088
|
+
this.revisions.set(id, next);
|
|
1089
|
+
return next;
|
|
1086
1090
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1091
|
+
toListEntry(plugin) {
|
|
1092
|
+
return {
|
|
1093
|
+
id: plugin.id,
|
|
1094
|
+
boring: plugin.boring,
|
|
1095
|
+
...plugin.pi ? { pi: plugin.pi } : {},
|
|
1096
|
+
version: plugin.version,
|
|
1097
|
+
revision: plugin.revision,
|
|
1098
|
+
...this.frontUrlPayload(plugin.frontUrl),
|
|
1099
|
+
...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
|
|
1100
|
+
};
|
|
1089
1101
|
}
|
|
1090
|
-
|
|
1091
|
-
if (!
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
validatePiPackages2(plugin.id, plugin.piPackages);
|
|
1102
|
+
frontUrlPayload(frontUrl) {
|
|
1103
|
+
if (!this.includeLegacyFrontUrl || !frontUrl) return {};
|
|
1104
|
+
return { frontUrl };
|
|
1095
1105
|
}
|
|
1096
|
-
|
|
1097
|
-
if (!
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
fail(plugin.id, `extensionPaths[${index}] must be a non-empty string`);
|
|
1103
|
-
}
|
|
1106
|
+
resolveFrontTarget(plugin, revision) {
|
|
1107
|
+
if (!plugin.frontPath || !this.frontTargetResolver) return void 0;
|
|
1108
|
+
const frontEntrySubpath = typeof plugin.boring.front === "string" ? plugin.boring.front.replace(/^\.\//, "") : normalizePluginSubpath(plugin.rootDir, plugin.frontPath);
|
|
1109
|
+
const frontTarget = this.frontTargetResolver(plugin, {
|
|
1110
|
+
revision,
|
|
1111
|
+
frontEntrySubpath
|
|
1104
1112
|
});
|
|
1113
|
+
if (!frontTarget) return void 0;
|
|
1114
|
+
return { ...frontTarget, revision };
|
|
1105
1115
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
fail(plugin.id, "agentTools must be an array when provided");
|
|
1116
|
+
emit(event) {
|
|
1117
|
+
for (const listener of [...this.listeners]) {
|
|
1118
|
+
try {
|
|
1119
|
+
listener(event);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1122
|
+
console.error(`[BoringPluginAssetManager] listener threw on ${event.type} for ${event.id}: ${message}`);
|
|
1123
|
+
}
|
|
1115
1124
|
}
|
|
1116
|
-
plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
|
|
1117
1125
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1126
|
+
errorPath(pluginId) {
|
|
1127
|
+
if (!isValidBoringPluginId(pluginId)) return null;
|
|
1128
|
+
const root = resolve4(this.errorRoot);
|
|
1129
|
+
const path = resolve4(root, pluginId, ".error");
|
|
1130
|
+
const rel = relative2(root, path);
|
|
1131
|
+
if (rel.startsWith("..") || isAbsolute2(rel)) return null;
|
|
1132
|
+
return path;
|
|
1120
1133
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1134
|
+
writeError(pluginId, message) {
|
|
1135
|
+
const path = this.errorPath(pluginId);
|
|
1136
|
+
if (!path) return;
|
|
1137
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
1138
|
+
writeFileSync2(path, message, "utf8");
|
|
1125
1139
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1140
|
+
clearError(pluginId) {
|
|
1141
|
+
const path = this.errorPath(pluginId);
|
|
1142
|
+
if (path && existsSync4(path)) rmSync2(path, { force: true });
|
|
1128
1143
|
}
|
|
1129
|
-
}
|
|
1144
|
+
};
|
|
1130
1145
|
|
|
1131
|
-
// src/server/
|
|
1132
|
-
function
|
|
1133
|
-
const
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
}
|
|
1144
|
-
seenIds.add(plugin.id);
|
|
1146
|
+
// src/server/agentPlugins/routes.ts
|
|
1147
|
+
function collectRestartWarnings(events) {
|
|
1148
|
+
const warnings = [];
|
|
1149
|
+
for (const event of events) {
|
|
1150
|
+
if (event.type !== "boring.plugin.load") continue;
|
|
1151
|
+
const surfaces = event.requiresRestart;
|
|
1152
|
+
if (!surfaces || surfaces.length === 0) continue;
|
|
1153
|
+
warnings.push({
|
|
1154
|
+
id: event.id,
|
|
1155
|
+
surfaces: [...surfaces],
|
|
1156
|
+
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.`
|
|
1157
|
+
});
|
|
1145
1158
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1159
|
+
return warnings;
|
|
1160
|
+
}
|
|
1161
|
+
async function boringPluginRoutes(app, opts) {
|
|
1162
|
+
const { manager, rebuildPlugins, enableReloadRoute = true } = opts;
|
|
1163
|
+
if (enableReloadRoute) {
|
|
1164
|
+
app.post("/api/boring.reload", async (_request, reply) => {
|
|
1165
|
+
const scan = await manager.load();
|
|
1166
|
+
const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
|
|
1167
|
+
const restart_warnings = collectRestartWarnings(scan.events);
|
|
1168
|
+
const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
|
|
1169
|
+
if (hasFailures) {
|
|
1170
|
+
return reply.status(422).send({
|
|
1171
|
+
ok: false,
|
|
1172
|
+
errors: scan.errors,
|
|
1173
|
+
diagnostics: rebuild.diagnostics,
|
|
1174
|
+
plugins: scan.loaded,
|
|
1175
|
+
// Even on failure, emit warnings for plugins that DID reload
|
|
1176
|
+
// — partial-failure tolerance means some loaded successfully.
|
|
1177
|
+
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
return reply.send({
|
|
1181
|
+
ok: true,
|
|
1182
|
+
plugins: scan.loaded,
|
|
1183
|
+
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1151
1186
|
}
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
...plugin.skills ? { skills: plugin.skills } : {},
|
|
1159
|
-
...plugin.provisioning ? { provisioning: plugin.provisioning } : {}
|
|
1160
|
-
}));
|
|
1161
|
-
const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
|
|
1162
|
-
const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
|
|
1163
|
-
return {
|
|
1164
|
-
registered: finalPlugins.map((p) => p.id),
|
|
1165
|
-
systemPromptAppend,
|
|
1166
|
-
piPackages,
|
|
1167
|
-
extensionPaths,
|
|
1168
|
-
agentTools,
|
|
1169
|
-
runtimePlugins,
|
|
1170
|
-
provisioningContributions,
|
|
1171
|
-
routeContributions,
|
|
1172
|
-
preservedUiStateKeys
|
|
1187
|
+
const listPlugins = async () => manager.list();
|
|
1188
|
+
app.get("/api/v1/agent-plugins", listPlugins);
|
|
1189
|
+
const getPluginError = async (request, reply) => {
|
|
1190
|
+
const error = manager.getError(request.params.id);
|
|
1191
|
+
if (error == null) return reply.status(404).send({ error: "not_found" });
|
|
1192
|
+
return reply.type("text/plain").send(error);
|
|
1173
1193
|
};
|
|
1194
|
+
app.get("/api/v1/agent-plugins/:id/error", getPluginError);
|
|
1195
|
+
app.get("/api/v1/agent-plugins/events", async (request, reply) => {
|
|
1196
|
+
reply.hijack();
|
|
1197
|
+
const res = reply.raw;
|
|
1198
|
+
res.statusCode = 200;
|
|
1199
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1200
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1201
|
+
res.setHeader("Connection", "keep-alive");
|
|
1202
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
1203
|
+
res.flushHeaders?.();
|
|
1204
|
+
const write = (eventName, payload) => {
|
|
1205
|
+
try {
|
|
1206
|
+
res.write(`event: ${eventName}
|
|
1207
|
+
`);
|
|
1208
|
+
res.write(`data: ${JSON.stringify(payload)}
|
|
1209
|
+
|
|
1210
|
+
`);
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
const liveQueue = [];
|
|
1215
|
+
let replaying = true;
|
|
1216
|
+
const unsubscribe = manager.subscribe((event) => {
|
|
1217
|
+
const payload = { ...event, replay: false };
|
|
1218
|
+
if (replaying) {
|
|
1219
|
+
liveQueue.push({ eventName: event.type, payload });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
write(event.type, payload);
|
|
1223
|
+
});
|
|
1224
|
+
for (const plugin of manager.list()) {
|
|
1225
|
+
write("boring.plugin.load", {
|
|
1226
|
+
type: "boring.plugin.load",
|
|
1227
|
+
id: plugin.id,
|
|
1228
|
+
boring: plugin.boring,
|
|
1229
|
+
version: plugin.version,
|
|
1230
|
+
revision: plugin.revision,
|
|
1231
|
+
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {},
|
|
1232
|
+
...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {},
|
|
1233
|
+
replay: true
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
write("boring.plugin.replay-complete", {
|
|
1237
|
+
type: "boring.plugin.replay-complete",
|
|
1238
|
+
replay: true
|
|
1239
|
+
});
|
|
1240
|
+
replaying = false;
|
|
1241
|
+
for (const event of liveQueue) write(event.eventName, event.payload);
|
|
1242
|
+
const heartbeat = setInterval(() => {
|
|
1243
|
+
try {
|
|
1244
|
+
res.write(": heartbeat\n\n");
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
}, 25e3);
|
|
1248
|
+
request.raw.on("close", () => {
|
|
1249
|
+
clearInterval(heartbeat);
|
|
1250
|
+
unsubscribe();
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/server/agentPlugins/aggregatePluginPrompts.ts
|
|
1256
|
+
function aggregatePluginPrompts(manager) {
|
|
1257
|
+
const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
1258
|
+
if (prompts.length === 0) return void 0;
|
|
1259
|
+
return `# Loaded boring-ui plugin context
|
|
1260
|
+
|
|
1261
|
+
${prompts.join("\n\n")}`;
|
|
1174
1262
|
}
|
|
1175
1263
|
|
|
1176
1264
|
// src/app/server/pluginEntryResolver.ts
|
|
1265
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1266
|
+
import { join as join5, resolve as resolve5 } from "path";
|
|
1267
|
+
import { createRequire as createRequire2 } from "module";
|
|
1268
|
+
import { pathToFileURL } from "url";
|
|
1177
1269
|
function readPluginPackageJson(dir) {
|
|
1178
1270
|
const pkgPath = resolve5(dir, "package.json");
|
|
1179
1271
|
if (!existsSync5(pkgPath)) return null;
|
|
@@ -1393,7 +1485,7 @@ function createInMemoryBridge() {
|
|
|
1393
1485
|
}
|
|
1394
1486
|
|
|
1395
1487
|
// src/server/ui-control/tools/uiTools.ts
|
|
1396
|
-
import {
|
|
1488
|
+
import { stat } from "fs/promises";
|
|
1397
1489
|
import { resolve as resolve6, isAbsolute as isAbsolute4, relative as relative3, win32 } from "path";
|
|
1398
1490
|
function makeError(message) {
|
|
1399
1491
|
return {
|
|
@@ -1431,7 +1523,7 @@ function validatePathSyntax(relPath, workspaceRoot) {
|
|
|
1431
1523
|
}
|
|
1432
1524
|
return { ok: true };
|
|
1433
1525
|
}
|
|
1434
|
-
async function
|
|
1526
|
+
async function validateExistingPath(workspaceRoot, relPath) {
|
|
1435
1527
|
const syntax = validatePathSyntax(relPath, workspaceRoot);
|
|
1436
1528
|
if (!syntax.ok) return syntax;
|
|
1437
1529
|
const resolved = resolve6(workspaceRoot, relPath);
|
|
@@ -1443,8 +1535,8 @@ async function validatePath(workspaceRoot, relPath) {
|
|
|
1443
1535
|
};
|
|
1444
1536
|
}
|
|
1445
1537
|
try {
|
|
1446
|
-
await
|
|
1447
|
-
return { ok: true };
|
|
1538
|
+
const fileStat = await stat(resolved);
|
|
1539
|
+
return { ok: true, kind: fileStat.isDirectory() ? "dir" : "file" };
|
|
1448
1540
|
} catch {
|
|
1449
1541
|
return {
|
|
1450
1542
|
ok: false,
|
|
@@ -1455,6 +1547,7 @@ async function validatePath(workspaceRoot, relPath) {
|
|
|
1455
1547
|
function createGetUiStateTool(uiBridge) {
|
|
1456
1548
|
return {
|
|
1457
1549
|
name: "get_ui_state",
|
|
1550
|
+
readinessRequirements: ["ui-bridge"],
|
|
1458
1551
|
description: [
|
|
1459
1552
|
"Read the current workspace UI state. Returns a JSON object with:",
|
|
1460
1553
|
"- workbenchOpen (boolean): is the right-side workbench pane visible?",
|
|
@@ -1508,12 +1601,13 @@ function isVerified(kind, params, state) {
|
|
|
1508
1601
|
return true;
|
|
1509
1602
|
}
|
|
1510
1603
|
function createExecUiTool(uiBridge, opts = {}) {
|
|
1511
|
-
const { workspaceRoot } = opts;
|
|
1604
|
+
const { workspaceRoot, resolvePathKind } = opts;
|
|
1512
1605
|
const verifyDelayMs = opts.verifyDelayMs ?? 200;
|
|
1513
1606
|
const verifyRetries = opts.verifyRetries ?? 2;
|
|
1514
1607
|
const verifyIntervalMs = opts.verifyIntervalMs ?? 200;
|
|
1515
1608
|
return {
|
|
1516
1609
|
name: "exec_ui",
|
|
1610
|
+
readinessRequirements: ["ui-bridge"],
|
|
1517
1611
|
description: [
|
|
1518
1612
|
"Execute a UI command in the workspace. Use this to open files, panels,",
|
|
1519
1613
|
"navigate to lines, or show notifications.",
|
|
@@ -1547,6 +1641,8 @@ function createExecUiTool(uiBridge, opts = {}) {
|
|
|
1547
1641
|
" returned \u2014 don't give up and don't switch to the read",
|
|
1548
1642
|
" tool. Repeat until openFile succeeds or no candidate",
|
|
1549
1643
|
" is found.",
|
|
1644
|
+
" If the path is a folder, openFile reveals/selects it in",
|
|
1645
|
+
" the file tree instead of opening an editor tab.",
|
|
1550
1646
|
" Example: {kind:'openFile', params:{path:'README.md'}}",
|
|
1551
1647
|
"",
|
|
1552
1648
|
" openPanel params: { id: string, component: string,",
|
|
@@ -1630,6 +1726,7 @@ function createExecUiTool(uiBridge, opts = {}) {
|
|
|
1630
1726
|
return makeError("openSurface: meta must be an object when provided");
|
|
1631
1727
|
}
|
|
1632
1728
|
}
|
|
1729
|
+
let effectiveKind = kind;
|
|
1633
1730
|
if (PATH_BEARING_KINDS.has(kind)) {
|
|
1634
1731
|
const relPath = getPathParam(kind, cmdParams);
|
|
1635
1732
|
if (!relPath) {
|
|
@@ -1640,14 +1737,25 @@ function createExecUiTool(uiBridge, opts = {}) {
|
|
|
1640
1737
|
const syntax = validatePathSyntax(relPath, workspaceRoot);
|
|
1641
1738
|
if (!syntax.ok) return makeError(syntax.reason);
|
|
1642
1739
|
if (workspaceRoot) {
|
|
1643
|
-
const check = await
|
|
1740
|
+
const check = await validateExistingPath(workspaceRoot, relPath);
|
|
1644
1741
|
if (!check.ok) {
|
|
1645
1742
|
return makeError(check.reason);
|
|
1646
1743
|
}
|
|
1744
|
+
if (kind === "openFile" && check.kind === "dir") {
|
|
1745
|
+
effectiveKind = "expandToFile";
|
|
1746
|
+
}
|
|
1747
|
+
} else if (resolvePathKind) {
|
|
1748
|
+
const pathKind = await resolvePathKind(relPath);
|
|
1749
|
+
if (!pathKind) {
|
|
1750
|
+
return makeError(`file not found at "${relPath}". Try find or grep to locate the file before retrying openFile.`);
|
|
1751
|
+
}
|
|
1752
|
+
if (kind === "openFile" && pathKind === "dir") {
|
|
1753
|
+
effectiveKind = "expandToFile";
|
|
1754
|
+
}
|
|
1647
1755
|
}
|
|
1648
1756
|
}
|
|
1649
1757
|
try {
|
|
1650
|
-
const command = { kind, params: cmdParams };
|
|
1758
|
+
const command = { kind: effectiveKind, params: cmdParams };
|
|
1651
1759
|
const result = await uiBridge.postCommand(command);
|
|
1652
1760
|
if (result.status === "error") {
|
|
1653
1761
|
return {
|
|
@@ -1656,11 +1764,11 @@ function createExecUiTool(uiBridge, opts = {}) {
|
|
|
1656
1764
|
details: result
|
|
1657
1765
|
};
|
|
1658
1766
|
}
|
|
1659
|
-
if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(
|
|
1767
|
+
if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(effectiveKind)) {
|
|
1660
1768
|
await new Promise((r) => setTimeout(r, verifyDelayMs));
|
|
1661
1769
|
let uiState = await uiBridge.getState();
|
|
1662
1770
|
for (let i = 0; i < verifyRetries; i++) {
|
|
1663
|
-
if (isVerified(
|
|
1771
|
+
if (isVerified(effectiveKind, cmdParams, uiState)) break;
|
|
1664
1772
|
await new Promise((r) => setTimeout(r, verifyIntervalMs));
|
|
1665
1773
|
uiState = await uiBridge.getState();
|
|
1666
1774
|
}
|
|
@@ -1813,9 +1921,10 @@ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
|
|
|
1813
1921
|
var __dirname = dirname7(fileURLToPath(import.meta.url));
|
|
1814
1922
|
var require4 = createRequire4(import.meta.url);
|
|
1815
1923
|
function boringPiRootVisibleToAgentTools(workspaceRoot, resolvedMode, provisioned) {
|
|
1924
|
+
void workspaceRoot;
|
|
1925
|
+
void resolvedMode;
|
|
1816
1926
|
if (!provisioned) return void 0;
|
|
1817
|
-
|
|
1818
|
-
return join7(workspaceRoot, "node_modules", "@hachej", "boring-pi");
|
|
1927
|
+
return "/workspace/.boring-agent/node/node_modules/@hachej/boring-pi";
|
|
1819
1928
|
}
|
|
1820
1929
|
function resolveWorkspacePackageRoot() {
|
|
1821
1930
|
const candidates = [
|
|
@@ -1831,6 +1940,15 @@ function resolveWorkspacePackageRoot() {
|
|
|
1831
1940
|
}
|
|
1832
1941
|
return join7(__dirname, "../../..");
|
|
1833
1942
|
}
|
|
1943
|
+
function readPackageVersion(packageRoot) {
|
|
1944
|
+
if (!packageRoot) return void 0;
|
|
1945
|
+
try {
|
|
1946
|
+
const pkg = JSON.parse(readFileSync6(join7(packageRoot, "package.json"), "utf8"));
|
|
1947
|
+
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : void 0;
|
|
1948
|
+
} catch {
|
|
1949
|
+
return void 0;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1834
1952
|
function nodePackageContribution(contributionId, nodePackageId, packageName, packageRoot) {
|
|
1835
1953
|
if (!packageRoot || !existsSync7(join7(packageRoot, "package.json"))) return null;
|
|
1836
1954
|
return {
|
|
@@ -1840,6 +1958,24 @@ function nodePackageContribution(contributionId, nodePackageId, packageName, pac
|
|
|
1840
1958
|
}
|
|
1841
1959
|
};
|
|
1842
1960
|
}
|
|
1961
|
+
function publishedNodePackageContribution(contributionId, nodePackageId, packageName, version, expectedBins) {
|
|
1962
|
+
return {
|
|
1963
|
+
id: contributionId,
|
|
1964
|
+
provisioning: {
|
|
1965
|
+
nodePackages: [
|
|
1966
|
+
{
|
|
1967
|
+
id: nodePackageId,
|
|
1968
|
+
packageName,
|
|
1969
|
+
...version ? { version } : {},
|
|
1970
|
+
...expectedBins ? { expectedBins } : {}
|
|
1971
|
+
}
|
|
1972
|
+
]
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
function useLocalPackageProvisioning() {
|
|
1977
|
+
return process.env.BORING_USE_LOCAL_PACKAGES === "1";
|
|
1978
|
+
}
|
|
1843
1979
|
function createWorkspacePackageProvisioningContribution() {
|
|
1844
1980
|
return nodePackageContribution(
|
|
1845
1981
|
"boring-workspace-package",
|
|
@@ -1890,11 +2026,21 @@ function resolveBoringUiCliPackageRoot() {
|
|
|
1890
2026
|
}
|
|
1891
2027
|
}
|
|
1892
2028
|
function createBoringUiCliPackageProvisioningContribution() {
|
|
1893
|
-
|
|
2029
|
+
const packageRoot = resolveBoringUiCliPackageRoot();
|
|
2030
|
+
if (useLocalPackageProvisioning()) {
|
|
2031
|
+
return nodePackageContribution(
|
|
2032
|
+
"boring-ui-cli-package",
|
|
2033
|
+
"boring-ui-cli",
|
|
2034
|
+
"@hachej/boring-ui-cli",
|
|
2035
|
+
packageRoot
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
return publishedNodePackageContribution(
|
|
1894
2039
|
"boring-ui-cli-package",
|
|
1895
2040
|
"boring-ui-cli",
|
|
1896
2041
|
"@hachej/boring-ui-cli",
|
|
1897
|
-
|
|
2042
|
+
packageRoot === join7(resolveWorkspacePackageRoot(), "..", "cli") ? void 0 : readPackageVersion(packageRoot) ?? readPackageVersion(resolveWorkspacePackageRoot()),
|
|
2043
|
+
["boring-ui"]
|
|
1898
2044
|
);
|
|
1899
2045
|
}
|
|
1900
2046
|
function createBoringPiPackageSource(workspaceRoot) {
|
|
@@ -1970,7 +2116,7 @@ async function provisionWorkspaceAgentServer(opts) {
|
|
|
1970
2116
|
force: opts.force
|
|
1971
2117
|
});
|
|
1972
2118
|
}
|
|
1973
|
-
function collectBoringPluginDirs(workspaceRoot, pluginCollection) {
|
|
2119
|
+
function collectBoringPluginDirs(workspaceRoot, pluginCollection, additionalPluginDirs = []) {
|
|
1974
2120
|
const extensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
|
|
1975
2121
|
const pluginRoots = extensionPaths.flatMap((path) => {
|
|
1976
2122
|
try {
|
|
@@ -1979,10 +2125,11 @@ function collectBoringPluginDirs(workspaceRoot, pluginCollection) {
|
|
|
1979
2125
|
return [];
|
|
1980
2126
|
}
|
|
1981
2127
|
});
|
|
1982
|
-
return [
|
|
2128
|
+
return [.../* @__PURE__ */ new Set([
|
|
1983
2129
|
join7(workspaceRoot, ".pi", "extensions"),
|
|
1984
|
-
...pluginRoots
|
|
1985
|
-
|
|
2130
|
+
...pluginRoots,
|
|
2131
|
+
...additionalPluginDirs
|
|
2132
|
+
])];
|
|
1986
2133
|
}
|
|
1987
2134
|
function mergeRuntimeProvisioningInputs(plugins) {
|
|
1988
2135
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -2008,6 +2155,12 @@ function skillNameFromResolvedPath(path) {
|
|
|
2008
2155
|
if (leaf.toLowerCase() !== "skill.md") return leaf;
|
|
2009
2156
|
return path.split(/[\\/]/).filter(Boolean).at(-2) ?? "skill";
|
|
2010
2157
|
}
|
|
2158
|
+
function skillPathForPiLoader2(path) {
|
|
2159
|
+
return existsSync7(join7(path, "SKILL.md")) ? dirname7(path) : path;
|
|
2160
|
+
}
|
|
2161
|
+
function uniqueStrings(values) {
|
|
2162
|
+
return [...new Set(values)];
|
|
2163
|
+
}
|
|
2011
2164
|
function readWorkspacePluginPackageRuntimePlugins(pluginDirs) {
|
|
2012
2165
|
const scan = scanBoringPlugins(pluginDirs);
|
|
2013
2166
|
return scan.plugins.map((plugin) => ({
|
|
@@ -2032,7 +2185,9 @@ function readWorkspacePluginPackagePiSnapshot(pluginDirs) {
|
|
|
2032
2185
|
const scan = scanBoringPlugins(pluginDirs);
|
|
2033
2186
|
const systemPromptAppend = aggregatePluginSystemPromptsFromScan(scan);
|
|
2034
2187
|
return {
|
|
2035
|
-
additionalSkillPaths:
|
|
2188
|
+
additionalSkillPaths: uniqueStrings(
|
|
2189
|
+
scan.plugins.flatMap((plugin) => plugin.skillPaths ?? []).map(skillPathForPiLoader2)
|
|
2190
|
+
),
|
|
2036
2191
|
packages: compactPiPackages(normalizeBoringPluginPiPackages(scan.plugins)),
|
|
2037
2192
|
extensionPaths: scan.plugins.flatMap((plugin) => plugin.extensionPaths ?? []),
|
|
2038
2193
|
...systemPromptAppend ? { systemPromptAppend } : {}
|
|
@@ -2086,7 +2241,7 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2086
2241
|
];
|
|
2087
2242
|
const baseStaticPiExtensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
|
|
2088
2243
|
const boringPluginDirs = [
|
|
2089
|
-
...collectBoringPluginDirs(workspaceRoot, pluginCollection),
|
|
2244
|
+
...collectBoringPluginDirs(workspaceRoot, pluginCollection, opts.additionalBoringPluginDirs),
|
|
2090
2245
|
...defaultPluginPackagePaths
|
|
2091
2246
|
];
|
|
2092
2247
|
const staticPluginPackagePiSnapshot = pluginHotReload ? emptyPackageJsonPiSnapshot() : readWorkspacePluginPackagePiSnapshot(boringPluginDirs);
|
|
@@ -2105,7 +2260,9 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2105
2260
|
const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(boringPluginDirs) : void 0;
|
|
2106
2261
|
const boringAssetManager = new BoringPluginAssetManager({
|
|
2107
2262
|
pluginDirs: boringPluginDirs,
|
|
2108
|
-
errorRoot: join7(workspaceRoot, ".pi", "extensions")
|
|
2263
|
+
errorRoot: join7(workspaceRoot, ".pi", "extensions"),
|
|
2264
|
+
frontTargetResolver: opts.boringPluginFrontTargetResolver,
|
|
2265
|
+
includeLegacyFrontUrl: opts.boringPluginIncludeLegacyFrontUrl
|
|
2109
2266
|
});
|
|
2110
2267
|
const buildRuntimeProvisioningInputs = () => mergeRuntimeProvisioningInputs([
|
|
2111
2268
|
...pluginCollection.runtimePlugins,
|
|
@@ -2121,11 +2278,18 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2121
2278
|
sessionId: opts.sessionId ?? "default"
|
|
2122
2279
|
});
|
|
2123
2280
|
if (!adapter) return currentRuntimeProvisioning;
|
|
2124
|
-
|
|
2281
|
+
const provisioned = await provisionWorkspaceRuntime({
|
|
2125
2282
|
plugins: buildRuntimeProvisioningInputs(),
|
|
2126
2283
|
adapter,
|
|
2127
2284
|
runtimeLayout
|
|
2128
2285
|
});
|
|
2286
|
+
currentRuntimeProvisioning = provisioned ? {
|
|
2287
|
+
...provisioned,
|
|
2288
|
+
env: {
|
|
2289
|
+
...provisioned.env,
|
|
2290
|
+
BORING_AGENT_WORKSPACE_LOCAL_PLUGIN_ROOTS: workspaceFsCapability === "strong" ? "1" : "0"
|
|
2291
|
+
}
|
|
2292
|
+
} : currentRuntimeProvisioning;
|
|
2129
2293
|
return currentRuntimeProvisioning;
|
|
2130
2294
|
};
|
|
2131
2295
|
await runRuntimeProvisioning();
|
|
@@ -2210,6 +2374,7 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2210
2374
|
}
|
|
2211
2375
|
;
|
|
2212
2376
|
app.__boringRebuildPlugins = rebuildPlugins;
|
|
2377
|
+
app.__boringAssetManager = boringAssetManager;
|
|
2213
2378
|
return app;
|
|
2214
2379
|
}
|
|
2215
2380
|
export {
|