@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.
- package/README.md +36 -34
- package/dist/{FileTree-Dvaud3jU.js → FileTree-DHVB9rpk.js} +15 -15
- package/dist/{MarkdownEditor-sLkqTXDj.js → MarkdownEditor-L1KDH0bM.js} +1 -1
- package/dist/{WorkspaceLoadingState-zLzh1tGc.js → WorkspaceLoadingState-DYDxUYnx.js} +114 -110
- package/dist/WorkspaceProvider-CDPaAO5u.js +5971 -0
- package/dist/app-front.d.ts +94 -107
- package/dist/app-front.js +243 -233
- package/dist/app-server.d.ts +130 -15
- package/dist/app-server.js +1569 -304
- package/dist/{bootstrapServer-BreQ9QBc.d.ts → createInMemoryBridge-BDxDzihm.d.ts} +11 -26
- package/dist/manifest-CyNNdfYz.d.ts +58 -0
- package/dist/plugin.d.ts +199 -0
- package/dist/plugin.js +300 -0
- package/dist/server.d.ts +239 -4
- package/dist/server.js +901 -78
- package/dist/shared.d.ts +4 -112
- package/dist/surface-COYagY2m.d.ts +111 -0
- package/dist/testing.d.ts +19 -1
- package/dist/testing.js +2 -2
- package/dist/{agent-tool-DEtfQPVB.d.ts → ui-bridge-Gfh1MMgl.d.ts} +30 -30
- package/dist/workspace.css +36 -0
- package/dist/workspace.d.ts +165 -120
- package/dist/workspace.js +330 -377
- package/docs/INTERFACES.md +9 -9
- package/docs/PLUGIN_STRUCTURE.md +39 -145
- package/docs/PLUGIN_SYSTEM.md +355 -0
- package/docs/README.md +6 -1
- package/docs/plans/README.md +1 -0
- package/docs/plans/archive/HOT_RELOADABLE_AGENT_PLUGINS_PLAN.md +218 -0
- package/docs/plans/archive/RELOAD_PLUGGABILITY_PLAN.md +174 -0
- package/docs/plans/archive/UNIFIED_PLUGIN_SYSTEM_PLAN.md +769 -0
- package/package.json +11 -5
- package/dist/CommandPalette-CJHuTJlD.js +0 -5716
- package/docs/bridge.md +0 -135
- package/docs/panels.md +0 -102
- package/docs/plugins.md +0 -158
- /package/docs/plans/{MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md → archive/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md} +0 -0
package/dist/server.js
CHANGED
|
@@ -460,19 +460,12 @@ function createWorkspaceUiTools(uiBridge, opts = {}) {
|
|
|
460
460
|
// src/server/plugins/piPackages.ts
|
|
461
461
|
import {
|
|
462
462
|
compactPiPackages,
|
|
463
|
-
mergePiPackageSources,
|
|
464
|
-
piPackageSourceKey,
|
|
465
463
|
PI_PACKAGE_RESOURCE_FILTERS
|
|
466
464
|
} from "@hachej/boring-agent/server";
|
|
467
465
|
|
|
468
466
|
// src/server/plugins/defineServerPlugin.ts
|
|
469
|
-
var ServerPluginError = class extends Error {
|
|
470
|
-
constructor(message) {
|
|
471
|
-
super(message);
|
|
472
|
-
}
|
|
473
|
-
};
|
|
474
467
|
function fail(pluginId, message) {
|
|
475
|
-
throw new
|
|
468
|
+
throw new Error(`server plugin "${pluginId}": ${message}`);
|
|
476
469
|
}
|
|
477
470
|
function isUrl(value) {
|
|
478
471
|
return value instanceof URL;
|
|
@@ -547,6 +540,26 @@ function validateProvisioning(pluginId, provisioning) {
|
|
|
547
540
|
}
|
|
548
541
|
}
|
|
549
542
|
}
|
|
543
|
+
if (provisioning.nodePackages !== void 0) {
|
|
544
|
+
if (!Array.isArray(provisioning.nodePackages)) {
|
|
545
|
+
fail(pluginId, "provisioning.nodePackages must be an array when provided");
|
|
546
|
+
}
|
|
547
|
+
for (let i = 0; i < provisioning.nodePackages.length; i++) {
|
|
548
|
+
const spec = provisioning.nodePackages[i];
|
|
549
|
+
if (!spec || typeof spec !== "object") {
|
|
550
|
+
fail(pluginId, `provisioning.nodePackages[${i}] must be an object`);
|
|
551
|
+
}
|
|
552
|
+
if (!spec.id || typeof spec.id !== "string") {
|
|
553
|
+
fail(pluginId, `provisioning.nodePackages[${i}].id must be a non-empty string`);
|
|
554
|
+
}
|
|
555
|
+
if (!spec.packageName || typeof spec.packageName !== "string") {
|
|
556
|
+
fail(pluginId, `provisioning.nodePackages[${i}].packageName must be a non-empty string`);
|
|
557
|
+
}
|
|
558
|
+
if (!isPathLike(spec.packageRoot)) {
|
|
559
|
+
fail(pluginId, `provisioning.nodePackages[${i}].packageRoot must be a string or URL`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
550
563
|
if (provisioning.python !== void 0) {
|
|
551
564
|
if (!Array.isArray(provisioning.python)) {
|
|
552
565
|
fail(pluginId, "provisioning.python must be an array when provided");
|
|
@@ -594,6 +607,16 @@ function validateServerPlugin(plugin) {
|
|
|
594
607
|
}
|
|
595
608
|
validatePiPackages(plugin.id, plugin.piPackages);
|
|
596
609
|
}
|
|
610
|
+
if (plugin.extensionPaths !== void 0) {
|
|
611
|
+
if (!Array.isArray(plugin.extensionPaths)) {
|
|
612
|
+
fail(plugin.id, "extensionPaths must be an array when provided");
|
|
613
|
+
}
|
|
614
|
+
plugin.extensionPaths.forEach((path, index) => {
|
|
615
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
616
|
+
fail(plugin.id, `extensionPaths[${index}] must be a non-empty string`);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
597
620
|
if (plugin.agentTools !== void 0) {
|
|
598
621
|
if (!Array.isArray(plugin.agentTools)) {
|
|
599
622
|
fail(plugin.id, "agentTools must be an array when provided");
|
|
@@ -614,75 +637,10 @@ function validateServerPlugin(plugin) {
|
|
|
614
637
|
}
|
|
615
638
|
function defineServerPlugin(plugin) {
|
|
616
639
|
validateServerPlugin(plugin);
|
|
617
|
-
return
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// src/server/plugins/composeServerPlugins.ts
|
|
621
|
-
function compactPrompts(prompts) {
|
|
622
|
-
const text = prompts.map((prompt) => prompt?.trim()).filter((prompt) => Boolean(prompt)).join("\n\n");
|
|
623
|
-
return text || void 0;
|
|
624
|
-
}
|
|
625
|
-
function mergeProvisioning(contributions) {
|
|
626
|
-
const templateDirs = contributions.flatMap((entry) => entry?.templateDirs ?? []);
|
|
627
|
-
const python = contributions.flatMap((entry) => entry?.python ?? []);
|
|
628
|
-
if (templateDirs.length === 0 && python.length === 0) return void 0;
|
|
629
|
-
return {
|
|
630
|
-
...templateDirs.length > 0 ? { templateDirs } : {},
|
|
631
|
-
...python.length > 0 ? { python } : {}
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
function composeRoutes(routes) {
|
|
635
|
-
const routePlugins = routes.filter(
|
|
636
|
-
(route) => Boolean(route)
|
|
637
|
-
);
|
|
638
|
-
if (routePlugins.length === 0) return void 0;
|
|
639
|
-
return async (app) => {
|
|
640
|
-
for (const routes2 of routePlugins) {
|
|
641
|
-
await app.register(routes2);
|
|
642
|
-
}
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
function composeServerPlugins(options) {
|
|
646
|
-
const piPackages = compactPiPackages([
|
|
647
|
-
...options.plugins.flatMap((plugin) => plugin.piPackages ?? []),
|
|
648
|
-
...options.piPackages ?? []
|
|
649
|
-
]);
|
|
650
|
-
const agentTools = [
|
|
651
|
-
...options.plugins.flatMap((plugin) => plugin.agentTools ?? []),
|
|
652
|
-
...options.agentTools ?? []
|
|
653
|
-
];
|
|
654
|
-
const systemPrompt = compactPrompts([
|
|
655
|
-
...options.plugins.map((plugin) => plugin.systemPrompt),
|
|
656
|
-
options.systemPrompt
|
|
657
|
-
]);
|
|
658
|
-
const provisioning = mergeProvisioning([
|
|
659
|
-
...options.plugins.map((plugin) => plugin.provisioning),
|
|
660
|
-
options.provisioning
|
|
661
|
-
]);
|
|
662
|
-
const routes = composeRoutes([
|
|
663
|
-
...options.plugins.map((plugin) => plugin.routes),
|
|
664
|
-
options.routes
|
|
665
|
-
]);
|
|
666
|
-
const preservedUiStateKeys = [.../* @__PURE__ */ new Set([
|
|
667
|
-
...options.plugins.flatMap((plugin) => plugin.preservedUiStateKeys ?? []),
|
|
668
|
-
...options.preservedUiStateKeys ?? []
|
|
669
|
-
])];
|
|
670
|
-
return defineServerPlugin({
|
|
671
|
-
id: options.id,
|
|
672
|
-
...options.label !== void 0 ? { label: options.label } : {},
|
|
673
|
-
...piPackages.length > 0 ? { piPackages } : {},
|
|
674
|
-
...systemPrompt ? { systemPrompt } : {},
|
|
675
|
-
...agentTools.length > 0 ? { agentTools } : {},
|
|
676
|
-
...provisioning ? { provisioning } : {},
|
|
677
|
-
...routes ? { routes } : {},
|
|
678
|
-
...preservedUiStateKeys.length > 0 ? { preservedUiStateKeys } : {}
|
|
679
|
-
});
|
|
640
|
+
return { ...plugin };
|
|
680
641
|
}
|
|
681
642
|
|
|
682
643
|
// src/server/plugins/bootstrapServer.ts
|
|
683
|
-
function collectPiPackages(plugins) {
|
|
684
|
-
return compactPiPackages(plugins.flatMap((plugin) => plugin.piPackages ?? []));
|
|
685
|
-
}
|
|
686
644
|
function bootstrapServer(options) {
|
|
687
645
|
const excludedDefaults = new Set(options.excludeDefaults ?? []);
|
|
688
646
|
const finalPlugins = [
|
|
@@ -704,7 +662,8 @@ function bootstrapServer(options) {
|
|
|
704
662
|
}
|
|
705
663
|
}
|
|
706
664
|
const systemPromptAppend = finalPlugins.filter((p) => p.systemPrompt && p.systemPrompt.trim()).map((p) => p.systemPrompt.trim()).join("\n\n");
|
|
707
|
-
const piPackages =
|
|
665
|
+
const piPackages = compactPiPackages(finalPlugins.flatMap((plugin) => plugin.piPackages ?? []));
|
|
666
|
+
const extensionPaths = finalPlugins.flatMap((p) => p.extensionPaths ?? []);
|
|
708
667
|
const provisioningContributions = finalPlugins.filter((p) => p.provisioning).map((p) => ({ id: p.id, provisioning: p.provisioning }));
|
|
709
668
|
const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
|
|
710
669
|
const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
|
|
@@ -712,21 +671,885 @@ function bootstrapServer(options) {
|
|
|
712
671
|
registered: finalPlugins.map((p) => p.id),
|
|
713
672
|
systemPromptAppend,
|
|
714
673
|
piPackages,
|
|
674
|
+
extensionPaths,
|
|
715
675
|
agentTools,
|
|
716
676
|
provisioningContributions,
|
|
717
677
|
routeContributions,
|
|
718
678
|
preservedUiStateKeys
|
|
719
679
|
};
|
|
720
680
|
}
|
|
681
|
+
|
|
682
|
+
// src/server/boringSystemPrompt.ts
|
|
683
|
+
import { createRequire } from "module";
|
|
684
|
+
import { dirname, join } from "path";
|
|
685
|
+
var require2 = createRequire(import.meta.url);
|
|
686
|
+
function resolveBoringPiRoot(override) {
|
|
687
|
+
if (override === null) return null;
|
|
688
|
+
if (override) return override;
|
|
689
|
+
try {
|
|
690
|
+
return dirname(require2.resolve("@hachej/boring-pi/package.json"));
|
|
691
|
+
} catch {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function buildDocsRefs(boringPiRoot) {
|
|
696
|
+
return [
|
|
697
|
+
{
|
|
698
|
+
topic: "Workflow + how-to + full plugin authoring reference",
|
|
699
|
+
path: join(boringPiRoot, "skills/boring-plugin-authoring/SKILL.md")
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
topic: "Panels (registration, dockview, layout)",
|
|
703
|
+
path: join(boringPiRoot, "references/workspace/panels.md")
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
topic: "Bridge / UI control (get_ui_state, exec_ui)",
|
|
707
|
+
path: join(boringPiRoot, "references/workspace/bridge.md")
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
topic: "Server plugins (defineServerPlugin, routes, agent tools)",
|
|
711
|
+
path: join(boringPiRoot, "references/workspace/plugins.md")
|
|
712
|
+
}
|
|
713
|
+
];
|
|
714
|
+
}
|
|
715
|
+
function buildBoringSystemPrompt(opts) {
|
|
716
|
+
const verify = opts.verifyCommand;
|
|
717
|
+
const boringPiRoot = resolveBoringPiRoot(opts.boringPiRootOverride);
|
|
718
|
+
const steps = [];
|
|
719
|
+
let n = 0;
|
|
720
|
+
if (opts.scaffoldCommand) {
|
|
721
|
+
n += 1;
|
|
722
|
+
steps.push(
|
|
723
|
+
`**${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/\`.`
|
|
724
|
+
);
|
|
725
|
+
} else {
|
|
726
|
+
n += 1;
|
|
727
|
+
steps.push(
|
|
728
|
+
`**${n}. Read the \`boring-plugin-authoring\` skill** from the \`<location>\` listed under \`<available_skills>\` for the canonical \`package.json\` + \`front/index.tsx\` shape.`
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
n += 1;
|
|
732
|
+
steps.push(
|
|
733
|
+
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.`
|
|
734
|
+
);
|
|
735
|
+
n += 1;
|
|
736
|
+
if (verify) {
|
|
737
|
+
steps.push(
|
|
738
|
+
`**${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.`
|
|
739
|
+
);
|
|
740
|
+
} else {
|
|
741
|
+
steps.push(
|
|
742
|
+
`**${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.`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
n += 1;
|
|
746
|
+
steps.push(`**${n}. Ask the user to run \`/reload\`** to publish the change.`);
|
|
747
|
+
const docsBlock = boringPiRoot ? [
|
|
748
|
+
"## boring-ui plugin authoring documentation",
|
|
749
|
+
"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:",
|
|
750
|
+
...buildDocsRefs(boringPiRoot).map((r) => `- ${r.topic}: ${r.path}`),
|
|
751
|
+
"Follow .md cross-references when present (e.g. SKILL.md may link to a reference doc \u2014 read both)."
|
|
752
|
+
].join("\n") : [
|
|
753
|
+
"## boring-ui plugin authoring documentation",
|
|
754
|
+
"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."
|
|
755
|
+
].join("\n");
|
|
756
|
+
return [
|
|
757
|
+
"You are operating inside boring-ui. Workspace root: `$BORING_AGENT_WORKSPACE_ROOT`; plugin files go under `$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/<name>/`.",
|
|
758
|
+
[
|
|
759
|
+
"## Plugin authoring \u2014 required workflow",
|
|
760
|
+
"",
|
|
761
|
+
...steps,
|
|
762
|
+
"",
|
|
763
|
+
"**Common hallucinations** \u2014 these names DO NOT EXIST in boring-ui and will silently fail; do not write them:",
|
|
764
|
+
"- API factories: `createPlugin`, `defineFrontPlugin`, `defineComponent` \u2014 use `definePlugin({id, panels, commands, ...})` from `@hachej/boring-workspace/plugin`.",
|
|
765
|
+
"- 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).",
|
|
766
|
+
"- 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.",
|
|
767
|
+
'- 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"`.',
|
|
768
|
+
"- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, execute }) }`.",
|
|
769
|
+
'- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
|
|
770
|
+
"- 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.",
|
|
771
|
+
"- 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`.",
|
|
772
|
+
"- 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."
|
|
773
|
+
].join("\n"),
|
|
774
|
+
docsBlock
|
|
775
|
+
].join("\n\n");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/server/agentPlugins/manager.ts
|
|
779
|
+
import { createHash } from "crypto";
|
|
780
|
+
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";
|
|
781
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join4, relative as relative3, resolve as resolve4 } from "path";
|
|
782
|
+
|
|
783
|
+
// src/shared/plugins/manifest.ts
|
|
784
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
785
|
+
var PLUGIN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
786
|
+
function isValidBoringPluginId(id) {
|
|
787
|
+
return typeof id === "string" && id.length > 0 && PLUGIN_ID_RE.test(id);
|
|
788
|
+
}
|
|
789
|
+
function isSafePluginRelativePath(value) {
|
|
790
|
+
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("..");
|
|
791
|
+
}
|
|
792
|
+
function issue(code, field, message) {
|
|
793
|
+
return { code, field, message };
|
|
794
|
+
}
|
|
795
|
+
function isRecord(value) {
|
|
796
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
797
|
+
}
|
|
798
|
+
function validateStringArray(issues, value, field, pathLike) {
|
|
799
|
+
if (value === void 0) return;
|
|
800
|
+
if (!Array.isArray(value)) {
|
|
801
|
+
issues.push(issue("INVALID_FIELD", field, `${field} must be an array`));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
value.forEach((entry, index) => {
|
|
805
|
+
const itemField = `${field}[${index}]`;
|
|
806
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
807
|
+
issues.push(issue("INVALID_FIELD", itemField, `${itemField} must be a non-empty string`));
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (pathLike && !isSafePluginRelativePath(entry)) {
|
|
811
|
+
issues.push(issue("INVALID_PATH", itemField, `${itemField} must be a safe relative path`));
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
var REMOVED_BORING_UI_FIELDS = ["outputs", "panels", "commands", "leftTabs", "surfaceResolvers", "providers", "bindings", "catalogs"];
|
|
816
|
+
function validateBoringField(issues, boring) {
|
|
817
|
+
if (boring === void 0) return void 0;
|
|
818
|
+
if (!isRecord(boring)) {
|
|
819
|
+
issues.push(issue("INVALID_FIELD", "boring", "boring must be an object when provided"));
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
for (const field of REMOVED_BORING_UI_FIELDS) {
|
|
823
|
+
if (boring[field] !== void 0) {
|
|
824
|
+
issues.push(issue(
|
|
825
|
+
"INVALID_FIELD",
|
|
826
|
+
`boring.${field}`,
|
|
827
|
+
`boring.${field} is not supported; declare front contributions in boring.front via definePlugin({ ... })`
|
|
828
|
+
));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (boring.id !== void 0) {
|
|
832
|
+
issues.push(issue("INVALID_FIELD", "boring.id", "boring.id is not supported; package discovery identity comes from package.json#name"));
|
|
833
|
+
}
|
|
834
|
+
const front = boring.front;
|
|
835
|
+
if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
|
|
836
|
+
issues.push(issue("INVALID_PATH", "boring.front", "boring.front must be a safe relative path"));
|
|
837
|
+
}
|
|
838
|
+
const server = boring.server;
|
|
839
|
+
if (server !== void 0 && server !== false && (typeof server !== "string" || !isSafePluginRelativePath(server))) {
|
|
840
|
+
issues.push(issue("INVALID_PATH", "boring.server", "boring.server must be a safe relative path or false"));
|
|
841
|
+
}
|
|
842
|
+
if (boring.label !== void 0 && typeof boring.label !== "string") {
|
|
843
|
+
issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
...typeof boring.front === "string" ? { front: boring.front } : {},
|
|
847
|
+
...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
|
|
848
|
+
...typeof boring.label === "string" ? { label: boring.label } : {}
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
var REMOTE_PI_PACKAGE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
|
|
852
|
+
function isRemotePiPackageSource(value) {
|
|
853
|
+
return REMOTE_PI_PACKAGE_PREFIXES.some((prefix) => value.startsWith(prefix));
|
|
854
|
+
}
|
|
855
|
+
function isSafePiPackageSource(value) {
|
|
856
|
+
if (value.length === 0) return false;
|
|
857
|
+
if (isRemotePiPackageSource(value)) return true;
|
|
858
|
+
const path = value.startsWith("file:") ? value.slice("file:".length) : value;
|
|
859
|
+
if (path === "." || path === "./") return true;
|
|
860
|
+
const normalized = path.startsWith("./") ? path.slice(2) : path;
|
|
861
|
+
return isSafePluginRelativePath(normalized);
|
|
862
|
+
}
|
|
863
|
+
function validatePiPackages2(issues, value) {
|
|
864
|
+
if (value === void 0) return;
|
|
865
|
+
if (!Array.isArray(value)) {
|
|
866
|
+
issues.push(issue("INVALID_FIELD", "pi.packages", "pi.packages must be an array when provided"));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
value.forEach((entry, index) => {
|
|
870
|
+
const field = `pi.packages[${index}]`;
|
|
871
|
+
if (typeof entry === "string") {
|
|
872
|
+
if (!isSafePiPackageSource(entry)) {
|
|
873
|
+
issues.push(issue("INVALID_PATH", field, `${field} must be a safe package source`));
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (!isRecord(entry)) {
|
|
878
|
+
issues.push(issue("INVALID_FIELD", field, `${field} must be a string or package source object`));
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (typeof entry.source !== "string" || entry.source.length === 0) {
|
|
882
|
+
issues.push(issue("INVALID_FIELD", `${field}.source`, `${field}.source must be a non-empty string`));
|
|
883
|
+
} else if (!isSafePiPackageSource(entry.source)) {
|
|
884
|
+
issues.push(issue("INVALID_PATH", `${field}.source`, `${field}.source must be a safe package source`));
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function validatePiField(issues, pi) {
|
|
889
|
+
if (pi === void 0) return void 0;
|
|
890
|
+
if (!isRecord(pi)) {
|
|
891
|
+
issues.push(issue("INVALID_FIELD", "pi", "pi must be an object when provided"));
|
|
892
|
+
return void 0;
|
|
893
|
+
}
|
|
894
|
+
validateStringArray(issues, pi.extensions, "pi.extensions", true);
|
|
895
|
+
validateStringArray(issues, pi.skills, "pi.skills", true);
|
|
896
|
+
validatePiPackages2(issues, pi.packages);
|
|
897
|
+
if (pi.systemPrompt !== void 0 && typeof pi.systemPrompt !== "string") {
|
|
898
|
+
issues.push(issue("INVALID_FIELD", "pi.systemPrompt", "pi.systemPrompt must be a string when provided"));
|
|
899
|
+
}
|
|
900
|
+
return pi;
|
|
901
|
+
}
|
|
902
|
+
function validateBoringPluginManifest(raw) {
|
|
903
|
+
const issues = [];
|
|
904
|
+
if (!isRecord(raw)) {
|
|
905
|
+
return {
|
|
906
|
+
valid: false,
|
|
907
|
+
issues: [issue("INVALID_FIELD", "<root>", "package.json manifest must be an object")]
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
if (raw.name !== void 0 && typeof raw.name !== "string") {
|
|
911
|
+
issues.push(issue("INVALID_FIELD", "name", "name must be a string when provided"));
|
|
912
|
+
}
|
|
913
|
+
if (raw.version !== void 0 && typeof raw.version !== "string") {
|
|
914
|
+
issues.push(issue("INVALID_VERSION", "version", "version must be a string when provided"));
|
|
915
|
+
} else if (typeof raw.version === "string" && raw.version.length > 0 && !SEMVER_RE.test(raw.version)) {
|
|
916
|
+
issues.push(issue("INVALID_VERSION", "version", "version must be a valid semver string"));
|
|
917
|
+
}
|
|
918
|
+
const boring = validateBoringField(issues, raw.boring);
|
|
919
|
+
const pi = validatePiField(issues, raw.pi);
|
|
920
|
+
if (!boring && !pi) {
|
|
921
|
+
issues.push(issue("MISSING_REQUIRED_FIELD", "boring|pi", "package.json must include boring and/or pi plugin metadata"));
|
|
922
|
+
}
|
|
923
|
+
if (issues.length > 0) return { valid: false, issues };
|
|
924
|
+
return {
|
|
925
|
+
valid: true,
|
|
926
|
+
packageJson: {
|
|
927
|
+
...typeof raw.name === "string" ? { name: raw.name } : {},
|
|
928
|
+
...typeof raw.version === "string" ? { version: raw.version } : {},
|
|
929
|
+
...boring ? { boring } : {},
|
|
930
|
+
...pi ? { pi } : {}
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/server/agentPlugins/scan.ts
|
|
936
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
|
|
937
|
+
import { basename, dirname as dirname3, join as join2, resolve as resolve3 } from "path";
|
|
938
|
+
|
|
939
|
+
// src/server/agentPlugins/pluginPaths.ts
|
|
940
|
+
import { existsSync, realpathSync } from "fs";
|
|
941
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "path";
|
|
942
|
+
function isInsideRoot(rootReal, targetReal) {
|
|
943
|
+
const rel = relative2(rootReal, targetReal);
|
|
944
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
945
|
+
}
|
|
946
|
+
function nearestExistingAncestor(path, rootDir) {
|
|
947
|
+
let current = path;
|
|
948
|
+
const root = resolve2(rootDir);
|
|
949
|
+
while (!existsSync(current)) {
|
|
950
|
+
const parent = dirname2(current);
|
|
951
|
+
if (parent === current) return void 0;
|
|
952
|
+
if (!isInsideRoot(root, parent) && parent !== root) return void 0;
|
|
953
|
+
current = parent;
|
|
954
|
+
}
|
|
955
|
+
return current;
|
|
956
|
+
}
|
|
957
|
+
function resolveContainedPluginPath(rootDir, value, options = {}) {
|
|
958
|
+
if (!value || !isSafePluginRelativePath(value)) return void 0;
|
|
959
|
+
if (!existsSync(rootDir)) return void 0;
|
|
960
|
+
const root = resolve2(rootDir);
|
|
961
|
+
const resolved = resolve2(root, value);
|
|
962
|
+
const rootReal = realpathSync(root);
|
|
963
|
+
const existing = nearestExistingAncestor(resolved, root);
|
|
964
|
+
if (!existing) return void 0;
|
|
965
|
+
const existingReal = realpathSync(existing);
|
|
966
|
+
if (!isInsideRoot(rootReal, existingReal)) return void 0;
|
|
967
|
+
if (!existsSync(resolved)) return options.mustExist ? void 0 : resolved;
|
|
968
|
+
const resolvedReal = realpathSync(resolved);
|
|
969
|
+
if (!isInsideRoot(rootReal, resolvedReal)) return void 0;
|
|
970
|
+
return resolvedReal;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/server/agentPlugins/scan.ts
|
|
974
|
+
function pluginIdFromPackageJson(pkg, rootDir) {
|
|
975
|
+
const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : void 0;
|
|
976
|
+
return (name ?? rootDir.split(/[\\/]/).at(-1) ?? "plugin").replace(/^@/, "").replaceAll("/", "-");
|
|
977
|
+
}
|
|
978
|
+
function safePluginIdFromPackageJson(pkg, rootDir) {
|
|
979
|
+
const id = pluginIdFromPackageJson(pkg, rootDir);
|
|
980
|
+
return isValidBoringPluginId(id) ? id : void 0;
|
|
981
|
+
}
|
|
982
|
+
function parsePackageJson(rootDir) {
|
|
983
|
+
return JSON.parse(readFileSync(join2(rootDir, "package.json"), "utf8"));
|
|
984
|
+
}
|
|
985
|
+
function hasPluginMetadata(pkg) {
|
|
986
|
+
return pkg.boring !== void 0 || pkg.pi !== void 0;
|
|
987
|
+
}
|
|
988
|
+
function resolvePluginPath(rootDir, value, options = {}) {
|
|
989
|
+
return resolveContainedPluginPath(rootDir, value, options);
|
|
990
|
+
}
|
|
991
|
+
function resolvePluginPaths(rootDir, values) {
|
|
992
|
+
return (values ?? []).map((value) => resolvePluginPath(rootDir, value)).filter((value) => Boolean(value));
|
|
993
|
+
}
|
|
994
|
+
function pathPreflightIssue(rootDir, value, field, options = {}) {
|
|
995
|
+
if (!value || !isSafePluginRelativePath(value)) return void 0;
|
|
996
|
+
const containedPath = resolveContainedPluginPath(rootDir, value);
|
|
997
|
+
if (!containedPath) {
|
|
998
|
+
return {
|
|
999
|
+
pluginDir: rootDir,
|
|
1000
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
1001
|
+
message: `${field}: resolved path escapes plugin root`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
if (options.mustExist && !existsSync2(containedPath)) {
|
|
1005
|
+
return {
|
|
1006
|
+
pluginDir: rootDir,
|
|
1007
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
1008
|
+
message: `${field}: declared path does not exist: ${value}`
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
return void 0;
|
|
1012
|
+
}
|
|
1013
|
+
function packagePathContainmentIssues(rootDir, pkg) {
|
|
1014
|
+
const issues = [];
|
|
1015
|
+
const boring = pkg.boring;
|
|
1016
|
+
const pi = pkg.pi;
|
|
1017
|
+
const pluginId = safePluginIdFromPackageJson(pkg, rootDir);
|
|
1018
|
+
const push = (issue2) => {
|
|
1019
|
+
if (issue2) issues.push({ ...issue2, ...pluginId ? { pluginId } : {} });
|
|
1020
|
+
};
|
|
1021
|
+
push(pathPreflightIssue(rootDir, boring?.front, "boring.front", { mustExist: true }));
|
|
1022
|
+
if (boring?.server !== false && boring?.server !== void 0) {
|
|
1023
|
+
push(pathPreflightIssue(rootDir, boring.server, "boring.server"));
|
|
1024
|
+
}
|
|
1025
|
+
pi?.extensions?.forEach((value, index) => push(pathPreflightIssue(rootDir, value, `pi.extensions[${index}]`)));
|
|
1026
|
+
pi?.skills?.forEach((value, index) => push(pathPreflightIssue(rootDir, value, `pi.skills[${index}]`)));
|
|
1027
|
+
return issues;
|
|
1028
|
+
}
|
|
1029
|
+
function discoverBoringPluginDirs(pluginDirs) {
|
|
1030
|
+
const out = /* @__PURE__ */ new Set();
|
|
1031
|
+
const missingPackageJson = [];
|
|
1032
|
+
for (const raw of pluginDirs) {
|
|
1033
|
+
const dir = resolve3(raw);
|
|
1034
|
+
if (!existsSync2(dir)) continue;
|
|
1035
|
+
const info = statSync(dir);
|
|
1036
|
+
if (!info.isDirectory()) continue;
|
|
1037
|
+
const hasPackageJson = existsSync2(join2(dir, "package.json"));
|
|
1038
|
+
const childPackageDirs = [];
|
|
1039
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1040
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1041
|
+
const child = join2(dir, entry.name);
|
|
1042
|
+
if (existsSync2(join2(child, "package.json"))) childPackageDirs.push(child);
|
|
1043
|
+
}
|
|
1044
|
+
if (hasPackageJson) out.add(dir);
|
|
1045
|
+
for (const child of childPackageDirs) out.add(child);
|
|
1046
|
+
if (!hasPackageJson && childPackageDirs.length === 0 && basename(dir) !== "extensions") {
|
|
1047
|
+
missingPackageJson.push(dir);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return { dirs: [...out].sort(), missingPackageJson: [...new Set(missingPackageJson)].sort() };
|
|
1051
|
+
}
|
|
1052
|
+
function scanBoringPlugins(pluginDirs) {
|
|
1053
|
+
const errors = [];
|
|
1054
|
+
const plugins = [];
|
|
1055
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
1056
|
+
const discovered = discoverBoringPluginDirs(pluginDirs);
|
|
1057
|
+
for (const pluginDir of discovered.missingPackageJson) {
|
|
1058
|
+
errors.push({ pluginDir, code: "MISSING_PACKAGE_JSON", message: "package.json is missing" });
|
|
1059
|
+
}
|
|
1060
|
+
for (const rootDir of discovered.dirs) {
|
|
1061
|
+
let raw;
|
|
1062
|
+
try {
|
|
1063
|
+
raw = parsePackageJson(rootDir);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
errors.push({
|
|
1066
|
+
pluginDir: rootDir,
|
|
1067
|
+
code: "INVALID_PACKAGE_JSON",
|
|
1068
|
+
message: error instanceof Error ? error.message : "invalid package.json"
|
|
1069
|
+
});
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (!hasPluginMetadata(raw)) continue;
|
|
1073
|
+
const result = validateBoringPluginManifest(raw);
|
|
1074
|
+
if (!result.valid) {
|
|
1075
|
+
const pluginId = safePluginIdFromPackageJson(raw, rootDir);
|
|
1076
|
+
for (const issue2 of result.issues) {
|
|
1077
|
+
errors.push({
|
|
1078
|
+
pluginDir: rootDir,
|
|
1079
|
+
...pluginId ? { pluginId } : {},
|
|
1080
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
1081
|
+
message: `${issue2.field}: ${issue2.message}`
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
const id = pluginIdFromPackageJson(result.packageJson, rootDir);
|
|
1087
|
+
let canAddPlugin = true;
|
|
1088
|
+
if (!isValidBoringPluginId(id)) {
|
|
1089
|
+
errors.push({
|
|
1090
|
+
pluginDir: rootDir,
|
|
1091
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
1092
|
+
message: `effective plugin id "${id}" must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash`
|
|
1093
|
+
});
|
|
1094
|
+
canAddPlugin = false;
|
|
1095
|
+
} else {
|
|
1096
|
+
const previous = seenIds.get(id);
|
|
1097
|
+
if (previous) {
|
|
1098
|
+
errors.push({
|
|
1099
|
+
pluginDir: rootDir,
|
|
1100
|
+
pluginId: id,
|
|
1101
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
1102
|
+
message: `duplicate plugin id "${id}" also declared by ${previous}`
|
|
1103
|
+
});
|
|
1104
|
+
const previousPluginIndex = plugins.findIndex((plugin) => plugin.id === id);
|
|
1105
|
+
if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
|
|
1106
|
+
canAddPlugin = false;
|
|
1107
|
+
} else {
|
|
1108
|
+
seenIds.set(id, rootDir);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const containmentIssues = packagePathContainmentIssues(rootDir, result.packageJson);
|
|
1112
|
+
if (containmentIssues.length > 0) {
|
|
1113
|
+
errors.push(...containmentIssues);
|
|
1114
|
+
canAddPlugin = false;
|
|
1115
|
+
}
|
|
1116
|
+
if (!canAddPlugin) continue;
|
|
1117
|
+
const pkg = result.packageJson;
|
|
1118
|
+
const boring = pkg.boring ?? {};
|
|
1119
|
+
const pi = pkg.pi;
|
|
1120
|
+
const frontPath = resolvePluginPath(rootDir, boring.front, { mustExist: true });
|
|
1121
|
+
const serverPath = typeof boring.server === "string" ? resolvePluginPath(rootDir, boring.server) : void 0;
|
|
1122
|
+
const version = pkg.version ?? "0.0.0";
|
|
1123
|
+
const extensionPaths = resolvePluginPaths(rootDir, pi?.extensions);
|
|
1124
|
+
const skillPaths = resolvePluginPaths(rootDir, pi?.skills);
|
|
1125
|
+
plugins.push({
|
|
1126
|
+
id,
|
|
1127
|
+
rootDir,
|
|
1128
|
+
version,
|
|
1129
|
+
boring,
|
|
1130
|
+
...pi ? { pi } : {},
|
|
1131
|
+
...frontPath ? { frontPath, frontUrl: `/@fs/${frontPath}` } : {},
|
|
1132
|
+
...serverPath ? { serverPath } : {},
|
|
1133
|
+
...extensionPaths.length > 0 ? { extensionPaths } : {},
|
|
1134
|
+
...skillPaths.length > 0 ? { skillPaths } : {}
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
const preflight = { ok: errors.length === 0, errors };
|
|
1138
|
+
return { preflight, plugins };
|
|
1139
|
+
}
|
|
1140
|
+
function preflightBoringPlugins(pluginDirs) {
|
|
1141
|
+
return scanBoringPlugins(pluginDirs).preflight;
|
|
1142
|
+
}
|
|
1143
|
+
function readBoringPlugins(pluginDirs) {
|
|
1144
|
+
const scan = scanBoringPlugins(pluginDirs);
|
|
1145
|
+
return scan.preflight.ok ? scan.plugins : [];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/server/agentPlugins/signatureCache.ts
|
|
1149
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync } from "fs";
|
|
1150
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
1151
|
+
var PLUGIN_SIGNATURE_CACHE_FILE = ".boring-signature.json";
|
|
1152
|
+
function pluginFileSignature(path) {
|
|
1153
|
+
if (!path || !existsSync3(path)) return "missing";
|
|
1154
|
+
const stat = statSync2(path);
|
|
1155
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
1156
|
+
}
|
|
1157
|
+
function cachePath(pluginRootDir) {
|
|
1158
|
+
return join3(pluginRootDir, PLUGIN_SIGNATURE_CACHE_FILE);
|
|
1159
|
+
}
|
|
1160
|
+
function writePluginSignatureCache(pluginRootDir, payload) {
|
|
1161
|
+
const full = {
|
|
1162
|
+
version: 1,
|
|
1163
|
+
serverSignature: payload.serverSignature,
|
|
1164
|
+
loadedAt: payload.loadedAt ?? Date.now()
|
|
1165
|
+
};
|
|
1166
|
+
const path = cachePath(pluginRootDir);
|
|
1167
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
1168
|
+
writeFileSync(path, `${JSON.stringify(full, null, 2)}
|
|
1169
|
+
`, "utf8");
|
|
1170
|
+
}
|
|
1171
|
+
function readPluginSignatureCache(pluginRootDir) {
|
|
1172
|
+
const path = cachePath(pluginRootDir);
|
|
1173
|
+
if (!existsSync3(path)) return null;
|
|
1174
|
+
let raw;
|
|
1175
|
+
try {
|
|
1176
|
+
raw = readFileSync2(path, "utf8");
|
|
1177
|
+
} catch {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
let parsed;
|
|
1181
|
+
try {
|
|
1182
|
+
parsed = JSON.parse(raw);
|
|
1183
|
+
} catch {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1187
|
+
const obj = parsed;
|
|
1188
|
+
if (obj.version !== 1) return null;
|
|
1189
|
+
const sig = obj.serverSignature;
|
|
1190
|
+
if (sig !== null && typeof sig !== "string") return null;
|
|
1191
|
+
const loadedAt = typeof obj.loadedAt === "number" ? obj.loadedAt : 0;
|
|
1192
|
+
return { version: 1, serverSignature: sig, loadedAt };
|
|
1193
|
+
}
|
|
1194
|
+
function clearPluginSignatureCache(pluginRootDir) {
|
|
1195
|
+
const path = cachePath(pluginRootDir);
|
|
1196
|
+
if (existsSync3(path)) rmSync(path, { force: true });
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/server/agentPlugins/manager.ts
|
|
1200
|
+
function preflightErrorId(pluginDir) {
|
|
1201
|
+
return `preflight-${createHash("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
|
|
1202
|
+
}
|
|
1203
|
+
function directorySignature(root) {
|
|
1204
|
+
if (!root || !existsSync4(root)) return "missing";
|
|
1205
|
+
const hash = createHash("sha256");
|
|
1206
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1207
|
+
let rootReal;
|
|
1208
|
+
try {
|
|
1209
|
+
rootReal = realpathSync2(root);
|
|
1210
|
+
} catch {
|
|
1211
|
+
return "missing";
|
|
1212
|
+
}
|
|
1213
|
+
visited.add(rootReal);
|
|
1214
|
+
let count = 0;
|
|
1215
|
+
const visit = (dir, depth) => {
|
|
1216
|
+
if (depth > 8 || count > 5e4) return;
|
|
1217
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").sort((a, b) => a.name.localeCompare(b.name));
|
|
1218
|
+
for (const entry of entries) {
|
|
1219
|
+
count++;
|
|
1220
|
+
const path = join4(dir, entry.name);
|
|
1221
|
+
const rel = relative3(root, path);
|
|
1222
|
+
const stat = lstatSync(path);
|
|
1223
|
+
if (stat.isSymbolicLink()) {
|
|
1224
|
+
let target;
|
|
1225
|
+
try {
|
|
1226
|
+
target = realpathSync2(path);
|
|
1227
|
+
} catch {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (visited.has(target)) {
|
|
1231
|
+
hash.update(rel);
|
|
1232
|
+
hash.update("symlink-cycle");
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
visited.add(target);
|
|
1236
|
+
const targetStat = statSync3(target);
|
|
1237
|
+
hash.update(rel);
|
|
1238
|
+
hash.update("symlink:");
|
|
1239
|
+
hash.update(target);
|
|
1240
|
+
if (targetStat.isDirectory()) visit(target, depth + 1);
|
|
1241
|
+
else if (targetStat.isFile()) {
|
|
1242
|
+
hash.update(String(targetStat.mtimeMs));
|
|
1243
|
+
hash.update(String(targetStat.size));
|
|
1244
|
+
}
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
hash.update(rel);
|
|
1248
|
+
hash.update(String(stat.mtimeMs));
|
|
1249
|
+
hash.update(String(stat.size));
|
|
1250
|
+
if (stat.isDirectory()) {
|
|
1251
|
+
visit(path, depth + 1);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
visit(root, 0);
|
|
1256
|
+
return hash.digest("hex");
|
|
1257
|
+
}
|
|
1258
|
+
function pluginSignature(plugin) {
|
|
1259
|
+
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");
|
|
1260
|
+
}
|
|
1261
|
+
function computeRequiresRestart(previous, next) {
|
|
1262
|
+
if (!previous) return [];
|
|
1263
|
+
const prevHasServer = !!previous.serverPath;
|
|
1264
|
+
const nextHasServer = !!next.serverPath;
|
|
1265
|
+
if (!prevHasServer && !nextHasServer) return [];
|
|
1266
|
+
if (prevHasServer !== nextHasServer) return ["routes", "agentTools"];
|
|
1267
|
+
const nextSig = pluginFileSignature(next.serverPath);
|
|
1268
|
+
if (previous.serverSignature === nextSig) return [];
|
|
1269
|
+
return ["routes", "agentTools"];
|
|
1270
|
+
}
|
|
1271
|
+
var BoringPluginAssetManager = class {
|
|
1272
|
+
pluginDirs;
|
|
1273
|
+
errorRoot;
|
|
1274
|
+
loaded = /* @__PURE__ */ new Map();
|
|
1275
|
+
revisions = /* @__PURE__ */ new Map();
|
|
1276
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1277
|
+
loading = null;
|
|
1278
|
+
reloadQueued = false;
|
|
1279
|
+
constructor(options) {
|
|
1280
|
+
this.pluginDirs = options.pluginDirs;
|
|
1281
|
+
this.errorRoot = options.errorRoot ?? join4(process.cwd(), ".pi", "extensions");
|
|
1282
|
+
}
|
|
1283
|
+
preflight() {
|
|
1284
|
+
return preflightBoringPlugins(this.pluginDirs);
|
|
1285
|
+
}
|
|
1286
|
+
list() {
|
|
1287
|
+
return [...this.loaded.values()].map((plugin) => ({
|
|
1288
|
+
id: plugin.id,
|
|
1289
|
+
boring: plugin.boring,
|
|
1290
|
+
...plugin.pi ? { pi: plugin.pi } : {},
|
|
1291
|
+
version: plugin.version,
|
|
1292
|
+
revision: plugin.revision,
|
|
1293
|
+
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
|
|
1294
|
+
}));
|
|
1295
|
+
}
|
|
1296
|
+
getError(pluginId) {
|
|
1297
|
+
const path = this.errorPath(pluginId);
|
|
1298
|
+
if (!path || !existsSync4(path)) return null;
|
|
1299
|
+
return readFileSync3(path, "utf8");
|
|
1300
|
+
}
|
|
1301
|
+
subscribe(listener) {
|
|
1302
|
+
this.listeners.add(listener);
|
|
1303
|
+
return () => this.listeners.delete(listener);
|
|
1304
|
+
}
|
|
1305
|
+
async load() {
|
|
1306
|
+
if (this.loading) {
|
|
1307
|
+
this.reloadQueued = true;
|
|
1308
|
+
return this.loading;
|
|
1309
|
+
}
|
|
1310
|
+
this.loading = this.drainLoads().finally(() => {
|
|
1311
|
+
this.loading = null;
|
|
1312
|
+
});
|
|
1313
|
+
return this.loading;
|
|
1314
|
+
}
|
|
1315
|
+
async drainLoads() {
|
|
1316
|
+
let result;
|
|
1317
|
+
do {
|
|
1318
|
+
this.reloadQueued = false;
|
|
1319
|
+
result = await this.doLoadOnce();
|
|
1320
|
+
} while (this.reloadQueued);
|
|
1321
|
+
return result;
|
|
1322
|
+
}
|
|
1323
|
+
async doLoadOnce() {
|
|
1324
|
+
const scan = scanBoringPlugins(this.pluginDirs);
|
|
1325
|
+
const nextPlugins = scan.plugins;
|
|
1326
|
+
const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
|
|
1327
|
+
const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve4(error.pluginDir)));
|
|
1328
|
+
const events = [];
|
|
1329
|
+
const errors = [];
|
|
1330
|
+
this.collectPreflightErrors(scan.preflight, events, errors);
|
|
1331
|
+
for (const id of [...this.loaded.keys()]) {
|
|
1332
|
+
if (nextIds.has(id)) continue;
|
|
1333
|
+
const previous = this.loaded.get(id);
|
|
1334
|
+
if (previous && invalidPluginDirs.has(resolve4(previous.rootDir))) continue;
|
|
1335
|
+
const revision = this.bumpRevision(id);
|
|
1336
|
+
this.loaded.delete(id);
|
|
1337
|
+
if (previous) {
|
|
1338
|
+
try {
|
|
1339
|
+
clearPluginSignatureCache(previous.rootDir);
|
|
1340
|
+
} catch {
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const event = { type: "boring.plugin.unload", id, revision };
|
|
1344
|
+
events.push(event);
|
|
1345
|
+
this.emit(event);
|
|
1346
|
+
}
|
|
1347
|
+
for (const plugin of nextPlugins) {
|
|
1348
|
+
try {
|
|
1349
|
+
const signature = pluginSignature(plugin);
|
|
1350
|
+
const previous = this.loaded.get(plugin.id);
|
|
1351
|
+
if (previous?.signature === signature) continue;
|
|
1352
|
+
const revision = this.bumpRevision(plugin.id);
|
|
1353
|
+
const serverSignature = plugin.serverPath ? pluginFileSignature(plugin.serverPath) : null;
|
|
1354
|
+
const record = { ...plugin, revision, signature, serverSignature };
|
|
1355
|
+
this.loaded.set(plugin.id, record);
|
|
1356
|
+
this.clearError(plugin.id);
|
|
1357
|
+
try {
|
|
1358
|
+
writePluginSignatureCache(plugin.rootDir, { serverSignature });
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
const requiresRestart = computeRequiresRestart(previous, plugin);
|
|
1362
|
+
const event = {
|
|
1363
|
+
type: "boring.plugin.load",
|
|
1364
|
+
id: plugin.id,
|
|
1365
|
+
boring: plugin.boring,
|
|
1366
|
+
version: plugin.version,
|
|
1367
|
+
revision,
|
|
1368
|
+
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {},
|
|
1369
|
+
...requiresRestart.length > 0 ? { requiresRestart } : {}
|
|
1370
|
+
};
|
|
1371
|
+
events.push(event);
|
|
1372
|
+
this.emit(event);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
const revision = this.bumpRevision(plugin.id);
|
|
1375
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1376
|
+
this.writeError(plugin.id, message);
|
|
1377
|
+
const event = { type: "boring.plugin.error", id: plugin.id, revision, message };
|
|
1378
|
+
errors.push({ id: plugin.id, revision, message });
|
|
1379
|
+
events.push(event);
|
|
1380
|
+
this.emit(event);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return { loaded: this.list(), events, errors };
|
|
1384
|
+
}
|
|
1385
|
+
collectPreflightErrors(preflight, events, errors) {
|
|
1386
|
+
for (const error of preflight.errors) {
|
|
1387
|
+
const id = error.pluginId ?? preflightErrorId(error.pluginDir);
|
|
1388
|
+
const revision = this.bumpRevision(id);
|
|
1389
|
+
const message = `${error.code}: ${error.message}
|
|
1390
|
+
|
|
1391
|
+
Plugin dir: ${error.pluginDir}`;
|
|
1392
|
+
const loadError = { id, revision, message };
|
|
1393
|
+
errors.push(loadError);
|
|
1394
|
+
this.writeError(id, message);
|
|
1395
|
+
const event = { type: "boring.plugin.error", id, revision, message };
|
|
1396
|
+
events.push(event);
|
|
1397
|
+
this.emit(event);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
bumpRevision(id) {
|
|
1401
|
+
const next = (this.revisions.get(id) ?? 0) + 1;
|
|
1402
|
+
this.revisions.set(id, next);
|
|
1403
|
+
return next;
|
|
1404
|
+
}
|
|
1405
|
+
emit(event) {
|
|
1406
|
+
for (const listener of [...this.listeners]) {
|
|
1407
|
+
try {
|
|
1408
|
+
listener(event);
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1411
|
+
console.error(`[BoringPluginAssetManager] listener threw on ${event.type} for ${event.id}: ${message}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
errorPath(pluginId) {
|
|
1416
|
+
if (!isValidBoringPluginId(pluginId)) return null;
|
|
1417
|
+
const root = resolve4(this.errorRoot);
|
|
1418
|
+
const path = resolve4(root, pluginId, ".error");
|
|
1419
|
+
const rel = relative3(root, path);
|
|
1420
|
+
if (rel.startsWith("..") || isAbsolute3(rel)) return null;
|
|
1421
|
+
return path;
|
|
1422
|
+
}
|
|
1423
|
+
writeError(pluginId, message) {
|
|
1424
|
+
const path = this.errorPath(pluginId);
|
|
1425
|
+
if (!path) return;
|
|
1426
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
1427
|
+
writeFileSync2(path, message, "utf8");
|
|
1428
|
+
}
|
|
1429
|
+
clearError(pluginId) {
|
|
1430
|
+
const path = this.errorPath(pluginId);
|
|
1431
|
+
if (path && existsSync4(path)) rmSync2(path, { force: true });
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
// src/server/agentPlugins/routes.ts
|
|
1436
|
+
function collectRestartWarnings(events) {
|
|
1437
|
+
const warnings = [];
|
|
1438
|
+
for (const event of events) {
|
|
1439
|
+
if (event.type !== "boring.plugin.load") continue;
|
|
1440
|
+
const surfaces = event.requiresRestart;
|
|
1441
|
+
if (!surfaces || surfaces.length === 0) continue;
|
|
1442
|
+
warnings.push({
|
|
1443
|
+
id: event.id,
|
|
1444
|
+
surfaces: [...surfaces],
|
|
1445
|
+
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.`
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
return warnings;
|
|
1449
|
+
}
|
|
1450
|
+
async function boringPluginRoutes(app, opts) {
|
|
1451
|
+
const { manager, rebuildPlugins, enableReloadRoute = true } = opts;
|
|
1452
|
+
if (enableReloadRoute) {
|
|
1453
|
+
app.post("/api/boring.reload", async (_request, reply) => {
|
|
1454
|
+
const scan = await manager.load();
|
|
1455
|
+
const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
|
|
1456
|
+
const restart_warnings = collectRestartWarnings(scan.events);
|
|
1457
|
+
const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
|
|
1458
|
+
if (hasFailures) {
|
|
1459
|
+
return reply.status(422).send({
|
|
1460
|
+
ok: false,
|
|
1461
|
+
errors: scan.errors,
|
|
1462
|
+
diagnostics: rebuild.diagnostics,
|
|
1463
|
+
plugins: scan.loaded,
|
|
1464
|
+
// Even on failure, emit warnings for plugins that DID reload
|
|
1465
|
+
// — partial-failure tolerance means some loaded successfully.
|
|
1466
|
+
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return reply.send({
|
|
1470
|
+
ok: true,
|
|
1471
|
+
plugins: scan.loaded,
|
|
1472
|
+
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
const listPlugins = async () => manager.list();
|
|
1477
|
+
app.get("/api/v1/agent-plugins", listPlugins);
|
|
1478
|
+
const getPluginError = async (request, reply) => {
|
|
1479
|
+
const error = manager.getError(request.params.id);
|
|
1480
|
+
if (error == null) return reply.status(404).send({ error: "not_found" });
|
|
1481
|
+
return reply.type("text/plain").send(error);
|
|
1482
|
+
};
|
|
1483
|
+
app.get("/api/v1/agent-plugins/:id/error", getPluginError);
|
|
1484
|
+
app.get("/api/v1/agent-plugins/events", async (request, reply) => {
|
|
1485
|
+
reply.hijack();
|
|
1486
|
+
const res = reply.raw;
|
|
1487
|
+
res.statusCode = 200;
|
|
1488
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1489
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1490
|
+
res.setHeader("Connection", "keep-alive");
|
|
1491
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
1492
|
+
res.flushHeaders?.();
|
|
1493
|
+
const write = (event) => {
|
|
1494
|
+
try {
|
|
1495
|
+
res.write(`event: ${event.type}
|
|
1496
|
+
`);
|
|
1497
|
+
res.write(`data: ${JSON.stringify(event)}
|
|
1498
|
+
|
|
1499
|
+
`);
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
for (const plugin of manager.list()) {
|
|
1504
|
+
write({
|
|
1505
|
+
type: "boring.plugin.load",
|
|
1506
|
+
id: plugin.id,
|
|
1507
|
+
boring: plugin.boring,
|
|
1508
|
+
version: plugin.version,
|
|
1509
|
+
revision: plugin.revision,
|
|
1510
|
+
...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
const unsubscribe = manager.subscribe(write);
|
|
1514
|
+
const heartbeat = setInterval(() => {
|
|
1515
|
+
try {
|
|
1516
|
+
res.write(": heartbeat\n\n");
|
|
1517
|
+
} catch {
|
|
1518
|
+
}
|
|
1519
|
+
}, 25e3);
|
|
1520
|
+
request.raw.on("close", () => {
|
|
1521
|
+
clearInterval(heartbeat);
|
|
1522
|
+
unsubscribe();
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/server/agentPlugins/aggregatePluginPrompts.ts
|
|
1528
|
+
function aggregatePluginPrompts(manager) {
|
|
1529
|
+
const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
1530
|
+
if (prompts.length === 0) return void 0;
|
|
1531
|
+
return `# Loaded boring-ui plugin context
|
|
1532
|
+
|
|
1533
|
+
${prompts.join("\n\n")}`;
|
|
1534
|
+
}
|
|
721
1535
|
export {
|
|
722
|
-
|
|
1536
|
+
BoringPluginAssetManager,
|
|
1537
|
+
aggregatePluginPrompts,
|
|
723
1538
|
bootstrapServer,
|
|
724
|
-
|
|
1539
|
+
boringPluginRoutes,
|
|
1540
|
+
buildBoringSystemPrompt,
|
|
1541
|
+
collectRestartWarnings,
|
|
725
1542
|
createExecUiTool,
|
|
726
1543
|
createGetUiStateTool,
|
|
727
1544
|
createInMemoryBridge,
|
|
728
1545
|
createWorkspaceUiTools,
|
|
729
1546
|
defineServerPlugin,
|
|
1547
|
+
pluginFileSignature,
|
|
1548
|
+
preflightBoringPlugins,
|
|
1549
|
+
readBoringPlugins,
|
|
1550
|
+
readPluginSignatureCache,
|
|
1551
|
+
scanBoringPlugins,
|
|
730
1552
|
uiRoutes,
|
|
731
|
-
validateServerPlugin
|
|
1553
|
+
validateServerPlugin,
|
|
1554
|
+
writePluginSignatureCache
|
|
732
1555
|
};
|