@hachej/boring-workspace 0.1.23 → 0.1.26

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