@hachej/boring-workspace 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +36 -34
  2. package/dist/{FileTree-Dvaud3jU.js → FileTree-DHVB9rpk.js} +15 -15
  3. package/dist/{MarkdownEditor-sLkqTXDj.js → MarkdownEditor-L1KDH0bM.js} +1 -1
  4. package/dist/{WorkspaceLoadingState-zLzh1tGc.js → WorkspaceLoadingState-DYDxUYnx.js} +114 -110
  5. package/dist/WorkspaceProvider-CDPaAO5u.js +5971 -0
  6. package/dist/app-front.d.ts +94 -107
  7. package/dist/app-front.js +243 -233
  8. package/dist/app-server.d.ts +130 -15
  9. package/dist/app-server.js +1569 -304
  10. package/dist/{bootstrapServer-BreQ9QBc.d.ts → createInMemoryBridge-BDxDzihm.d.ts} +11 -26
  11. package/dist/manifest-CyNNdfYz.d.ts +58 -0
  12. package/dist/plugin.d.ts +199 -0
  13. package/dist/plugin.js +300 -0
  14. package/dist/server.d.ts +239 -4
  15. package/dist/server.js +901 -78
  16. package/dist/shared.d.ts +4 -112
  17. package/dist/surface-COYagY2m.d.ts +111 -0
  18. package/dist/testing.d.ts +19 -1
  19. package/dist/testing.js +2 -2
  20. package/dist/{agent-tool-DEtfQPVB.d.ts → ui-bridge-Gfh1MMgl.d.ts} +30 -30
  21. package/dist/workspace.css +36 -0
  22. package/dist/workspace.d.ts +165 -120
  23. package/dist/workspace.js +330 -377
  24. package/docs/INTERFACES.md +9 -9
  25. package/docs/PLUGIN_STRUCTURE.md +39 -145
  26. package/docs/PLUGIN_SYSTEM.md +355 -0
  27. package/docs/README.md +6 -1
  28. package/docs/plans/README.md +1 -0
  29. package/docs/plans/archive/HOT_RELOADABLE_AGENT_PLUGINS_PLAN.md +218 -0
  30. package/docs/plans/archive/RELOAD_PLUGGABILITY_PLAN.md +174 -0
  31. package/docs/plans/archive/UNIFIED_PLUGIN_SYSTEM_PLAN.md +769 -0
  32. package/package.json +11 -5
  33. package/dist/CommandPalette-CJHuTJlD.js +0 -5716
  34. package/docs/bridge.md +0 -135
  35. package/docs/panels.md +0 -102
  36. package/docs/plugins.md +0 -158
  37. /package/docs/plans/{MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md → archive/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md} +0 -0
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 ServerPluginError(`server plugin "${pluginId}": ${message}`);
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 Object.assign({}, plugin);
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 = collectPiPackages(finalPlugins);
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
- ServerPluginError,
1536
+ BoringPluginAssetManager,
1537
+ aggregatePluginPrompts,
723
1538
  bootstrapServer,
724
- composeServerPlugins,
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
  };