@hachej/boring-workspace 0.1.33 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{FileTree-SmsE0Raq.js → FileTree-CkCxHBDu.js} +1 -1
- package/dist/MarkdownEditor-CtPphEVP.js +549 -0
- package/dist/{WorkspaceLoadingState-BlvZXmFg.js → WorkspaceLoadingState-Bl-92Dly.js} +287 -252
- package/dist/WorkspaceProvider-BhRPFy5R.js +7508 -0
- package/dist/app-front.d.ts +50 -3
- package/dist/app-front.js +893 -756
- package/dist/app-server.d.ts +11 -8
- package/dist/app-server.js +609 -79
- package/dist/boring-workspace.css +1 -1
- package/dist/{createInMemoryBridge-HJopAIbo.d.ts → createInMemoryBridge-zb8MpO60.d.ts} +11 -1
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.js +8 -1
- package/dist/runtime-server.d.ts +47 -0
- package/dist/runtime-server.js +32 -0
- package/dist/server.d.ts +63 -25
- package/dist/server.js +555 -46
- package/dist/testing.js +1 -1
- package/dist/workspace.css +328 -30
- package/dist/workspace.d.ts +68 -2
- package/dist/workspace.js +31 -30
- package/docs/PLUGIN_SYSTEM.md +5 -6
- package/package.json +10 -4
- package/dist/MarkdownEditor-zbp8ezds.js +0 -540
- package/dist/WorkspaceProvider-CuIZx1ua.js +0 -6553
package/dist/app-server.js
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
VERCEL_SANDBOX_WORKSPACE_ROOT
|
|
10
10
|
} from "@hachej/boring-agent/server";
|
|
11
11
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
12
|
-
import { dirname as dirname7, join as join7 } from "path";
|
|
12
|
+
import { dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
13
14
|
import { createRequire as createRequire4 } from "module";
|
|
14
15
|
import { fileURLToPath } from "url";
|
|
15
16
|
|
|
@@ -103,13 +104,19 @@ function buildBoringSystemPrompt(opts) {
|
|
|
103
104
|
"- 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).",
|
|
104
105
|
"- 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.",
|
|
105
106
|
'- 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"`.',
|
|
106
|
-
|
|
107
|
+
'- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, parameters: { type: "object", properties: {} }, execute }) }`. `parameters` is mandatory even for no-arg tools; omitting it breaks tool execution.',
|
|
107
108
|
'- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
|
|
108
109
|
"- 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.",
|
|
109
110
|
"- 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`.",
|
|
110
111
|
"- Dependency installs: do NOT install plugin UI dependencies at the workspace root. Install them inside `.pi/extensions/<name>/` and keep React/workspace/boring-ui-kit imports as host singletons, not plugin dependencies.",
|
|
111
112
|
"- 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."
|
|
112
113
|
].join("\n"),
|
|
114
|
+
[
|
|
115
|
+
"## Installing an existing or published plugin",
|
|
116
|
+
"To ADD an existing or published plugin (not author a new one), use `boring-ui-plugin install <source>` via bash \u2014 `<source>` is `npm:<package>`, `git:<repo>`, `github:<owner>/<repo>`, an `http(s)` git URL, or a local path; add `--global` for all workspaces (default is this workspace).",
|
|
117
|
+
"A bare `npm install <package>` does NOT register it as a plugin (no `.pi/settings.json` package source), so it will NOT load \u2014 always use `boring-ui-plugin install`, then ask the user to `/reload` (a `boring.server` backend also needs a process restart).",
|
|
118
|
+
"Inspect with `boring-ui-plugin list [--json]`; remove with `boring-ui-plugin remove <id-or-source>`."
|
|
119
|
+
].join("\n"),
|
|
113
120
|
docsBlock
|
|
114
121
|
].join("\n\n");
|
|
115
122
|
}
|
|
@@ -348,6 +355,14 @@ function resolveSafePluginEntryPath({
|
|
|
348
355
|
}
|
|
349
356
|
|
|
350
357
|
// src/server/agentPlugins/scan.ts
|
|
358
|
+
function normalizeBoringPluginSource(input) {
|
|
359
|
+
if (typeof input === "string") return { rootDir: resolve2(input), kind: "internal" };
|
|
360
|
+
return {
|
|
361
|
+
rootDir: resolve2(input.rootDir),
|
|
362
|
+
kind: input.kind,
|
|
363
|
+
...input.workspaceId ? { workspaceId: input.workspaceId } : {}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
351
366
|
function pluginIdFromPackageJson(pkg, rootDir) {
|
|
352
367
|
const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
|
|
353
368
|
if (explicitId) return explicitId;
|
|
@@ -406,10 +421,11 @@ function packagePathContainmentIssues(rootDir, pkg) {
|
|
|
406
421
|
return issues;
|
|
407
422
|
}
|
|
408
423
|
function discoverBoringPluginDirs(pluginDirs) {
|
|
409
|
-
const out = /* @__PURE__ */ new
|
|
424
|
+
const out = /* @__PURE__ */ new Map();
|
|
410
425
|
const missingPackageJson = [];
|
|
411
426
|
for (const raw of pluginDirs) {
|
|
412
|
-
const
|
|
427
|
+
const source = normalizeBoringPluginSource(raw);
|
|
428
|
+
const dir = source.rootDir;
|
|
413
429
|
if (!existsSync2(dir)) continue;
|
|
414
430
|
const info = statSync(dir);
|
|
415
431
|
if (!info.isDirectory()) continue;
|
|
@@ -420,13 +436,19 @@ function discoverBoringPluginDirs(pluginDirs) {
|
|
|
420
436
|
const child = join2(dir, entry.name);
|
|
421
437
|
if (existsSync2(join2(child, "package.json"))) childPackageDirs.push(child);
|
|
422
438
|
}
|
|
423
|
-
if (hasPackageJson) out.
|
|
424
|
-
for (const child of childPackageDirs)
|
|
425
|
-
|
|
439
|
+
if (hasPackageJson && !out.has(dir)) out.set(dir, source);
|
|
440
|
+
for (const child of childPackageDirs) {
|
|
441
|
+
if (!out.has(child)) out.set(child, { ...source, rootDir: child });
|
|
442
|
+
}
|
|
443
|
+
const collectionDirNames = /* @__PURE__ */ new Set(["extensions", "npm", "git"]);
|
|
444
|
+
if (!hasPackageJson && childPackageDirs.length === 0 && !collectionDirNames.has(basename(dir))) {
|
|
426
445
|
missingPackageJson.push(dir);
|
|
427
446
|
}
|
|
428
447
|
}
|
|
429
|
-
return {
|
|
448
|
+
return {
|
|
449
|
+
sources: [...out.values()].sort((a, b) => a.rootDir.localeCompare(b.rootDir)),
|
|
450
|
+
missingPackageJson: [...new Set(missingPackageJson)].sort()
|
|
451
|
+
};
|
|
430
452
|
}
|
|
431
453
|
function scanBoringPlugins(pluginDirs) {
|
|
432
454
|
const errors = [];
|
|
@@ -436,7 +458,8 @@ function scanBoringPlugins(pluginDirs) {
|
|
|
436
458
|
for (const pluginDir of discovered.missingPackageJson) {
|
|
437
459
|
errors.push({ pluginDir, code: "MISSING_PACKAGE_JSON", message: "package.json is missing" });
|
|
438
460
|
}
|
|
439
|
-
for (const
|
|
461
|
+
for (const source of discovered.sources) {
|
|
462
|
+
const rootDir = source.rootDir;
|
|
440
463
|
let raw;
|
|
441
464
|
try {
|
|
442
465
|
raw = parsePackageJson(rootDir);
|
|
@@ -474,15 +497,26 @@ function scanBoringPlugins(pluginDirs) {
|
|
|
474
497
|
} else {
|
|
475
498
|
const previous = seenIds.get(id);
|
|
476
499
|
if (previous) {
|
|
477
|
-
errors.push({
|
|
478
|
-
pluginDir: rootDir,
|
|
479
|
-
pluginId: id,
|
|
480
|
-
code: "INVALID_PLUGIN_METADATA",
|
|
481
|
-
message: `duplicate plugin id "${id}" also declared by ${previous}`
|
|
482
|
-
});
|
|
483
500
|
const previousPluginIndex = plugins.findIndex((plugin) => plugin.id === id);
|
|
484
|
-
|
|
485
|
-
|
|
501
|
+
const previousPlugin = previousPluginIndex >= 0 ? plugins[previousPluginIndex] : void 0;
|
|
502
|
+
const currentIsWorkspaceLocal = Boolean(source.workspaceId);
|
|
503
|
+
const previousIsWorkspaceLocal = Boolean(previousPlugin?.source.workspaceId);
|
|
504
|
+
const currentMayShadowPrevious = source.kind === "external" && currentIsWorkspaceLocal && previousPlugin?.source.kind === "external" && !previousIsWorkspaceLocal;
|
|
505
|
+
if (currentMayShadowPrevious) {
|
|
506
|
+
if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
|
|
507
|
+
seenIds.set(id, rootDir);
|
|
508
|
+
} else if (!currentIsWorkspaceLocal && previousIsWorkspaceLocal) {
|
|
509
|
+
canAddPlugin = false;
|
|
510
|
+
} else {
|
|
511
|
+
errors.push({
|
|
512
|
+
pluginDir: rootDir,
|
|
513
|
+
pluginId: id,
|
|
514
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
515
|
+
message: `duplicate plugin id "${id}" also declared by ${previous}`
|
|
516
|
+
});
|
|
517
|
+
if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
|
|
518
|
+
canAddPlugin = false;
|
|
519
|
+
}
|
|
486
520
|
} else {
|
|
487
521
|
seenIds.set(id, rootDir);
|
|
488
522
|
}
|
|
@@ -494,6 +528,7 @@ function scanBoringPlugins(pluginDirs) {
|
|
|
494
528
|
}
|
|
495
529
|
if (!canAddPlugin) continue;
|
|
496
530
|
const pkg = result.packageJson;
|
|
531
|
+
const hasBoring = pkg.boring !== void 0;
|
|
497
532
|
const boring = pkg.boring ?? {};
|
|
498
533
|
const pi = pkg.pi;
|
|
499
534
|
const frontPath = resolvePluginPath(rootDir, boring.front, { mustExist: true });
|
|
@@ -506,11 +541,13 @@ function scanBoringPlugins(pluginDirs) {
|
|
|
506
541
|
rootDir,
|
|
507
542
|
version,
|
|
508
543
|
boring,
|
|
544
|
+
hasBoring,
|
|
509
545
|
...pi ? { pi } : {},
|
|
510
546
|
...frontPath ? { frontPath, frontUrl: `/@fs/${frontPath}` } : {},
|
|
511
547
|
...serverPath ? { serverPath } : {},
|
|
512
548
|
...extensionPaths.length > 0 ? { extensionPaths } : {},
|
|
513
|
-
...skillPaths.length > 0 ? { skillPaths } : {}
|
|
549
|
+
...skillPaths.length > 0 ? { skillPaths } : {},
|
|
550
|
+
source
|
|
514
551
|
});
|
|
515
552
|
}
|
|
516
553
|
const preflight = { ok: errors.length === 0, errors };
|
|
@@ -933,10 +970,11 @@ function frontSignatureRoot(plugin) {
|
|
|
933
970
|
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel) ? frontRoot : dirname5(plugin.frontPath);
|
|
934
971
|
}
|
|
935
972
|
function pluginSignature(plugin) {
|
|
936
|
-
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");
|
|
973
|
+
return createHash("sha256").update(JSON.stringify(plugin.boring)).update(JSON.stringify(plugin.pi ?? {})).update(plugin.version).update(JSON.stringify(plugin.source)).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");
|
|
937
974
|
}
|
|
938
975
|
function computeRequiresRestart(previous, next) {
|
|
939
976
|
if (!previous) return [];
|
|
977
|
+
if (previous.source.kind === "external" && next.source.kind === "external") return [];
|
|
940
978
|
const prevHasServer = !!previous.serverPath;
|
|
941
979
|
const nextHasServer = !!next.serverPath;
|
|
942
980
|
if (!prevHasServer && !nextHasServer) return [];
|
|
@@ -982,8 +1020,10 @@ var BoringPluginAssetManager = class {
|
|
|
982
1020
|
version: plugin.version,
|
|
983
1021
|
revision: plugin.revision,
|
|
984
1022
|
rootDir: plugin.rootDir,
|
|
1023
|
+
source: plugin.source,
|
|
985
1024
|
...plugin.frontPath ? { frontPath: plugin.frontPath } : {},
|
|
986
|
-
...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
|
|
1025
|
+
...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {},
|
|
1026
|
+
...plugin.serverPath ? { serverPath: plugin.serverPath } : {}
|
|
987
1027
|
}));
|
|
988
1028
|
}
|
|
989
1029
|
inspectLoadedPiSnapshot() {
|
|
@@ -1023,7 +1063,7 @@ ${prompts.join("\n\n")}` } : {}
|
|
|
1023
1063
|
async doLoadOnce() {
|
|
1024
1064
|
this.lastErrors.clear();
|
|
1025
1065
|
const scan = scanBoringPlugins(this.pluginDirs);
|
|
1026
|
-
const nextPlugins = scan.plugins;
|
|
1066
|
+
const nextPlugins = scan.plugins.filter((plugin) => plugin.hasBoring);
|
|
1027
1067
|
const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
|
|
1028
1068
|
const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve4(error.pluginDir)));
|
|
1029
1069
|
const events = [];
|
|
@@ -1187,31 +1227,7 @@ function collectRestartWarnings(events) {
|
|
|
1187
1227
|
return warnings;
|
|
1188
1228
|
}
|
|
1189
1229
|
async function boringPluginRoutes(app, opts) {
|
|
1190
|
-
const { manager
|
|
1191
|
-
if (enableReloadRoute) {
|
|
1192
|
-
app.post("/api/boring.reload", async (_request, reply) => {
|
|
1193
|
-
const scan = await manager.load();
|
|
1194
|
-
const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
|
|
1195
|
-
const restart_warnings = collectRestartWarnings(scan.events);
|
|
1196
|
-
const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
|
|
1197
|
-
if (hasFailures) {
|
|
1198
|
-
return reply.status(422).send({
|
|
1199
|
-
ok: false,
|
|
1200
|
-
errors: scan.errors,
|
|
1201
|
-
diagnostics: rebuild.diagnostics,
|
|
1202
|
-
plugins: scan.loaded,
|
|
1203
|
-
// Even on failure, emit warnings for plugins that DID reload
|
|
1204
|
-
// — partial-failure tolerance means some loaded successfully.
|
|
1205
|
-
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
return reply.send({
|
|
1209
|
-
ok: true,
|
|
1210
|
-
plugins: scan.loaded,
|
|
1211
|
-
...restart_warnings.length > 0 ? { restart_warnings } : {}
|
|
1212
|
-
});
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1230
|
+
const { manager } = opts;
|
|
1215
1231
|
const listPlugins = async () => manager.list();
|
|
1216
1232
|
app.get("/api/v1/agent-plugins", listPlugins);
|
|
1217
1233
|
const getPluginError = async (request, reply) => {
|
|
@@ -1280,29 +1296,106 @@ async function boringPluginRoutes(app, opts) {
|
|
|
1280
1296
|
});
|
|
1281
1297
|
}
|
|
1282
1298
|
|
|
1283
|
-
// src/server/
|
|
1284
|
-
function
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
return
|
|
1299
|
+
// src/server/runtimeBackend/defineRuntimeServerPlugin.ts
|
|
1300
|
+
function isPlainObject(value) {
|
|
1301
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1302
|
+
const proto = Object.getPrototypeOf(value);
|
|
1303
|
+
return proto === Object.prototype || proto === null;
|
|
1304
|
+
}
|
|
1305
|
+
function validateRuntimeServerPlugin(value) {
|
|
1306
|
+
if (!isPlainObject(value)) {
|
|
1307
|
+
throw new Error("runtime server plugin default export must be a plain object");
|
|
1308
|
+
}
|
|
1309
|
+
if ("id" in value) {
|
|
1310
|
+
throw new Error("runtime server plugin must not declare id; the host supplies plugin id from package metadata");
|
|
1311
|
+
}
|
|
1312
|
+
if (typeof value.routes !== "function") {
|
|
1313
|
+
throw new Error("runtime server plugin default export must define routes(router)");
|
|
1314
|
+
}
|
|
1315
|
+
if (value.dispose !== void 0 && typeof value.dispose !== "function") {
|
|
1316
|
+
throw new Error("runtime server plugin dispose must be a function when provided");
|
|
1317
|
+
}
|
|
1318
|
+
return value;
|
|
1319
|
+
}
|
|
1320
|
+
function isRuntimePluginResponse(value) {
|
|
1321
|
+
return isPlainObject(value) && value.kind === "response";
|
|
1322
|
+
}
|
|
1288
1323
|
|
|
1289
|
-
|
|
1324
|
+
// src/server/runtimeBackend/runtimePathSegments.ts
|
|
1325
|
+
function findUnsafeRuntimePathSegment(path) {
|
|
1326
|
+
if (path.includes("\\")) return "\\";
|
|
1327
|
+
for (const segment of path.split("/")) {
|
|
1328
|
+
if (segment === "." || segment === "..") return segment;
|
|
1329
|
+
}
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
function describeUnsafeRuntimePathSegment(segment) {
|
|
1333
|
+
if (segment === "\\") return "backslashes";
|
|
1334
|
+
return `${segment} segments`;
|
|
1290
1335
|
}
|
|
1291
1336
|
|
|
1292
|
-
// src/
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
function
|
|
1298
|
-
|
|
1299
|
-
|
|
1337
|
+
// src/server/runtimeBackend/routerCapture.ts
|
|
1338
|
+
var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "ALL"];
|
|
1339
|
+
function routeKey(method, path) {
|
|
1340
|
+
return `${method} ${path}`;
|
|
1341
|
+
}
|
|
1342
|
+
function runtimeRouteKey(method, path) {
|
|
1343
|
+
return routeKey(method.toUpperCase(), path);
|
|
1344
|
+
}
|
|
1345
|
+
function validateRuntimeRoutePath(path) {
|
|
1346
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1347
|
+
throw new Error("runtime route path must be a non-empty string");
|
|
1348
|
+
}
|
|
1349
|
+
if (!path.startsWith("/")) {
|
|
1350
|
+
throw new Error(`runtime route path must start with /: ${path}`);
|
|
1351
|
+
}
|
|
1352
|
+
if (path.includes("?") || path.includes("#")) {
|
|
1353
|
+
throw new Error(`runtime route path must not include query strings or fragments: ${path}`);
|
|
1354
|
+
}
|
|
1355
|
+
let decodedPath;
|
|
1300
1356
|
try {
|
|
1301
|
-
|
|
1357
|
+
decodedPath = decodeURIComponent(path);
|
|
1302
1358
|
} catch {
|
|
1303
|
-
|
|
1359
|
+
throw new Error(`runtime route path must be valid percent-encoding: ${path}`);
|
|
1360
|
+
}
|
|
1361
|
+
const unsafeSegment = findUnsafeRuntimePathSegment(decodedPath);
|
|
1362
|
+
if (unsafeSegment) {
|
|
1363
|
+
throw new Error(`runtime route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}: ${path}`);
|
|
1364
|
+
}
|
|
1365
|
+
if (path.includes(":")) {
|
|
1366
|
+
throw new Error(`runtime route path must be exact and must not contain params: ${path}`);
|
|
1304
1367
|
}
|
|
1368
|
+
if (path.includes("*")) {
|
|
1369
|
+
throw new Error(`runtime route path must be exact and must not contain wildcards: ${path}`);
|
|
1370
|
+
}
|
|
1371
|
+
return path;
|
|
1372
|
+
}
|
|
1373
|
+
async function captureRuntimeRoutes(register) {
|
|
1374
|
+
const routes = [];
|
|
1375
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1376
|
+
const add = (method, path, handler) => {
|
|
1377
|
+
if (typeof handler !== "function") {
|
|
1378
|
+
throw new Error(`runtime route ${method} ${path} handler must be a function`);
|
|
1379
|
+
}
|
|
1380
|
+
const normalizedPath = validateRuntimeRoutePath(path);
|
|
1381
|
+
const key = routeKey(method, normalizedPath);
|
|
1382
|
+
if (seen.has(key)) throw new Error(`duplicate runtime route: ${key}`);
|
|
1383
|
+
seen.add(key);
|
|
1384
|
+
routes.push({ method, path: normalizedPath, handler });
|
|
1385
|
+
};
|
|
1386
|
+
const router = Object.fromEntries(
|
|
1387
|
+
METHODS.map((method) => [method.toLowerCase(), (path, handler) => add(method, path, handler)])
|
|
1388
|
+
);
|
|
1389
|
+
await register(router);
|
|
1390
|
+
return routes;
|
|
1305
1391
|
}
|
|
1392
|
+
|
|
1393
|
+
// src/server/runtimeBackend/runtimeBackendRegistry.ts
|
|
1394
|
+
import { ErrorCode } from "@hachej/boring-agent/shared";
|
|
1395
|
+
|
|
1396
|
+
// src/server/pluginImports/importServerModule.ts
|
|
1397
|
+
import { createRequire as createRequire2 } from "module";
|
|
1398
|
+
import { pathToFileURL } from "url";
|
|
1306
1399
|
var require3 = createRequire2(import.meta.url);
|
|
1307
1400
|
var warnedJitiMissing = false;
|
|
1308
1401
|
function warnJitiUnavailable(serverPath, reason) {
|
|
@@ -1337,6 +1430,379 @@ async function importServerModule(serverPath, hotReload) {
|
|
|
1337
1430
|
href
|
|
1338
1431
|
);
|
|
1339
1432
|
}
|
|
1433
|
+
|
|
1434
|
+
// src/server/runtimeBackend/runtimeBackendRegistry.ts
|
|
1435
|
+
var RuntimeBackendError = class extends Error {
|
|
1436
|
+
constructor(code, statusCode, message, details) {
|
|
1437
|
+
super(message);
|
|
1438
|
+
this.code = code;
|
|
1439
|
+
this.statusCode = statusCode;
|
|
1440
|
+
this.details = details;
|
|
1441
|
+
this.name = "RuntimeBackendError";
|
|
1442
|
+
}
|
|
1443
|
+
code;
|
|
1444
|
+
statusCode;
|
|
1445
|
+
details;
|
|
1446
|
+
};
|
|
1447
|
+
function errorMessage(error) {
|
|
1448
|
+
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1449
|
+
}
|
|
1450
|
+
function moduleValue(mod) {
|
|
1451
|
+
return typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
|
|
1452
|
+
}
|
|
1453
|
+
function toRouteMap(routes) {
|
|
1454
|
+
const map = /* @__PURE__ */ new Map();
|
|
1455
|
+
for (const route of routes) map.set(runtimeRouteKey(route.method, route.path), route);
|
|
1456
|
+
return map;
|
|
1457
|
+
}
|
|
1458
|
+
function assertJsonSerializable(value) {
|
|
1459
|
+
if (value === void 0 || value === null) return;
|
|
1460
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
|
1461
|
+
throw new RuntimeBackendError(
|
|
1462
|
+
ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
|
|
1463
|
+
500,
|
|
1464
|
+
"runtime plugin response is not JSON-serializable"
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
JSON.stringify(value);
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
throw new RuntimeBackendError(
|
|
1471
|
+
ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
|
|
1472
|
+
500,
|
|
1473
|
+
`runtime plugin response is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function normalizeResponse(value) {
|
|
1478
|
+
if (value === void 0 || value === null) return { status: 204, headers: {} };
|
|
1479
|
+
if (isRuntimePluginResponse(value)) return normalizeExplicitResponse(value);
|
|
1480
|
+
assertJsonSerializable(value);
|
|
1481
|
+
return { status: 200, headers: { "content-type": "application/json; charset=utf-8" }, body: value };
|
|
1482
|
+
}
|
|
1483
|
+
function normalizeExplicitResponse(value) {
|
|
1484
|
+
const status = value.status ?? (value.body === void 0 || value.body === null ? 204 : 200);
|
|
1485
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
1486
|
+
throw new RuntimeBackendError(
|
|
1487
|
+
ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
|
|
1488
|
+
500,
|
|
1489
|
+
"runtime plugin response status must be an integer HTTP status code"
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
const headers = {};
|
|
1493
|
+
if (value.headers !== void 0) {
|
|
1494
|
+
for (const [name, headerValue] of Object.entries(value.headers)) {
|
|
1495
|
+
if (typeof headerValue !== "string") {
|
|
1496
|
+
throw new RuntimeBackendError(
|
|
1497
|
+
ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
|
|
1498
|
+
500,
|
|
1499
|
+
"runtime plugin response headers must be strings"
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
headers[name] = headerValue;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (value.body === void 0 || value.body === null) return { status, headers };
|
|
1506
|
+
assertJsonSerializable(value.body);
|
|
1507
|
+
if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
|
|
1508
|
+
headers["content-type"] = "application/json; charset=utf-8";
|
|
1509
|
+
}
|
|
1510
|
+
return { status, headers, body: value.body };
|
|
1511
|
+
}
|
|
1512
|
+
async function disposeSnapshot(snapshot) {
|
|
1513
|
+
if (!snapshot.module.dispose) return [];
|
|
1514
|
+
try {
|
|
1515
|
+
await snapshot.module.dispose();
|
|
1516
|
+
return [];
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
return [{
|
|
1519
|
+
pluginId: snapshot.pluginId,
|
|
1520
|
+
source: `runtime backend dispose (${snapshot.pluginId})`,
|
|
1521
|
+
code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
|
|
1522
|
+
message: errorMessage(error)
|
|
1523
|
+
}];
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
var RuntimeBackendRegistry = class {
|
|
1527
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1528
|
+
lastDiagnostics = [];
|
|
1529
|
+
reloadQueue = Promise.resolve({ ok: true, diagnostics: [] });
|
|
1530
|
+
getDiagnostics() {
|
|
1531
|
+
return [...this.lastDiagnostics];
|
|
1532
|
+
}
|
|
1533
|
+
listPluginIds() {
|
|
1534
|
+
return [...this.snapshots.keys()].sort();
|
|
1535
|
+
}
|
|
1536
|
+
async reloadFromLoadedPlugins(plugins) {
|
|
1537
|
+
const run = this.reloadQueue.then(() => this.reloadOnce(plugins), () => this.reloadOnce(plugins));
|
|
1538
|
+
this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
|
|
1539
|
+
return run;
|
|
1540
|
+
}
|
|
1541
|
+
async close() {
|
|
1542
|
+
const run = this.reloadQueue.then(() => this.closeOnce(), () => this.closeOnce());
|
|
1543
|
+
this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
|
|
1544
|
+
return run;
|
|
1545
|
+
}
|
|
1546
|
+
async dispatch(request) {
|
|
1547
|
+
const snapshot = this.snapshots.get(request.pluginId);
|
|
1548
|
+
if (!snapshot) {
|
|
1549
|
+
throw new RuntimeBackendError(
|
|
1550
|
+
ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
|
|
1551
|
+
404,
|
|
1552
|
+
`runtime backend plugin not found: ${request.pluginId}`
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
if (snapshot.source.workspaceId && snapshot.source.workspaceId !== request.workspaceId) {
|
|
1556
|
+
throw new RuntimeBackendError(
|
|
1557
|
+
ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
|
|
1558
|
+
404,
|
|
1559
|
+
`runtime backend plugin not found in workspace: ${request.pluginId}`
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
const route = snapshot.routes.get(runtimeRouteKey(request.method, request.path)) ?? snapshot.routes.get(runtimeRouteKey("ALL", request.path));
|
|
1563
|
+
if (!route) {
|
|
1564
|
+
throw new RuntimeBackendError(
|
|
1565
|
+
ErrorCode.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
|
|
1566
|
+
404,
|
|
1567
|
+
`runtime backend route not found: ${request.method.toUpperCase()} ${request.path}`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
const ctx = {
|
|
1571
|
+
pluginId: request.pluginId,
|
|
1572
|
+
method: request.method.toUpperCase(),
|
|
1573
|
+
path: request.path,
|
|
1574
|
+
query: request.query,
|
|
1575
|
+
headers: request.headers,
|
|
1576
|
+
signal: request.signal,
|
|
1577
|
+
body: request.body,
|
|
1578
|
+
logger: request.logger
|
|
1579
|
+
};
|
|
1580
|
+
try {
|
|
1581
|
+
return normalizeResponse(await route.handler(ctx));
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
if (error instanceof RuntimeBackendError) throw error;
|
|
1584
|
+
throw new RuntimeBackendError(
|
|
1585
|
+
ErrorCode.enum.RUNTIME_PLUGIN_HANDLER_FAILED,
|
|
1586
|
+
500,
|
|
1587
|
+
error instanceof Error ? error.message : String(error)
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
async reloadOnce(plugins) {
|
|
1592
|
+
const diagnostics = [];
|
|
1593
|
+
const externalRuntimePlugins = plugins.filter((plugin) => plugin.source.kind === "external" && plugin.serverPath);
|
|
1594
|
+
const nextIds = new Set(externalRuntimePlugins.map((plugin) => plugin.id));
|
|
1595
|
+
for (const id of [...this.snapshots.keys()]) {
|
|
1596
|
+
if (nextIds.has(id)) continue;
|
|
1597
|
+
const previous = this.snapshots.get(id);
|
|
1598
|
+
if (!previous) continue;
|
|
1599
|
+
this.snapshots.delete(id);
|
|
1600
|
+
diagnostics.push(...await disposeSnapshot(previous));
|
|
1601
|
+
}
|
|
1602
|
+
for (const plugin of externalRuntimePlugins) {
|
|
1603
|
+
const serverPath = plugin.serverPath;
|
|
1604
|
+
if (!serverPath) continue;
|
|
1605
|
+
try {
|
|
1606
|
+
const mod = await importServerModule(serverPath, true);
|
|
1607
|
+
const runtimePlugin = validateRuntimeServerPlugin(moduleValue(mod));
|
|
1608
|
+
const routes = await captureRuntimeRoutes((router) => runtimePlugin.routes(router));
|
|
1609
|
+
const nextSnapshot = {
|
|
1610
|
+
pluginId: plugin.id,
|
|
1611
|
+
source: plugin.source,
|
|
1612
|
+
module: runtimePlugin,
|
|
1613
|
+
routes: toRouteMap(routes)
|
|
1614
|
+
};
|
|
1615
|
+
const previous = this.snapshots.get(plugin.id);
|
|
1616
|
+
this.snapshots.set(plugin.id, nextSnapshot);
|
|
1617
|
+
if (previous) diagnostics.push(...await disposeSnapshot(previous));
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
diagnostics.push({
|
|
1620
|
+
pluginId: plugin.id,
|
|
1621
|
+
source: `runtime backend (${plugin.id})`,
|
|
1622
|
+
code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
|
|
1623
|
+
message: errorMessage(error)
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
this.lastDiagnostics = diagnostics;
|
|
1628
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
1629
|
+
}
|
|
1630
|
+
async closeOnce() {
|
|
1631
|
+
const diagnostics = [];
|
|
1632
|
+
for (const snapshot of this.snapshots.values()) {
|
|
1633
|
+
diagnostics.push(...await disposeSnapshot(snapshot));
|
|
1634
|
+
}
|
|
1635
|
+
this.snapshots.clear();
|
|
1636
|
+
this.lastDiagnostics = diagnostics;
|
|
1637
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
// src/server/runtimeBackend/runtimeBackendGateway.ts
|
|
1642
|
+
import { ErrorCode as ErrorCode2 } from "@hachej/boring-agent/shared";
|
|
1643
|
+
var GATEWAY_PREFIX = "/api/v1/plugins/";
|
|
1644
|
+
function rawPathFromRequest(request) {
|
|
1645
|
+
const url = request.raw.url ?? request.url;
|
|
1646
|
+
const queryIndex = url.indexOf("?");
|
|
1647
|
+
return queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
1648
|
+
}
|
|
1649
|
+
function rawGatewayTail(request, pluginId) {
|
|
1650
|
+
const rawPath = rawPathFromRequest(request);
|
|
1651
|
+
const prefix = `${GATEWAY_PREFIX}${pluginId}`;
|
|
1652
|
+
if (rawPath === prefix || rawPath === `${prefix}/`) return "/";
|
|
1653
|
+
if (!rawPath.startsWith(`${prefix}/`)) {
|
|
1654
|
+
throw new RuntimeBackendError(
|
|
1655
|
+
ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
|
|
1656
|
+
404,
|
|
1657
|
+
"runtime backend route not found"
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
return rawPath.slice(prefix.length);
|
|
1661
|
+
}
|
|
1662
|
+
function normalizeGatewayPath(rawTail) {
|
|
1663
|
+
let path;
|
|
1664
|
+
try {
|
|
1665
|
+
path = decodeURIComponent(rawTail);
|
|
1666
|
+
} catch {
|
|
1667
|
+
throw new RuntimeBackendError(
|
|
1668
|
+
ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
|
|
1669
|
+
404,
|
|
1670
|
+
"runtime backend route path is not valid percent-encoding"
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
if (path.length === 0) path = "/";
|
|
1674
|
+
const unsafeSegment = findUnsafeRuntimePathSegment(path);
|
|
1675
|
+
if (unsafeSegment) {
|
|
1676
|
+
throw new RuntimeBackendError(
|
|
1677
|
+
ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
|
|
1678
|
+
404,
|
|
1679
|
+
`runtime backend route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}`
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
return path;
|
|
1683
|
+
}
|
|
1684
|
+
function firstString(value) {
|
|
1685
|
+
if (Array.isArray(value)) return value[0];
|
|
1686
|
+
return value;
|
|
1687
|
+
}
|
|
1688
|
+
function headersFromRequest(request) {
|
|
1689
|
+
const headers = new Headers();
|
|
1690
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
1691
|
+
if (value === void 0) continue;
|
|
1692
|
+
if (Array.isArray(value)) {
|
|
1693
|
+
for (const item of value) headers.append(name, item);
|
|
1694
|
+
} else {
|
|
1695
|
+
headers.set(name, String(value));
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return headers;
|
|
1699
|
+
}
|
|
1700
|
+
function loggerFromRequest(request) {
|
|
1701
|
+
return {
|
|
1702
|
+
debug: (arg, message) => {
|
|
1703
|
+
if (message === void 0) request.log.debug(arg);
|
|
1704
|
+
else request.log.debug(arg, message);
|
|
1705
|
+
},
|
|
1706
|
+
info: (arg, message) => {
|
|
1707
|
+
if (message === void 0) request.log.info(arg);
|
|
1708
|
+
else request.log.info(arg, message);
|
|
1709
|
+
},
|
|
1710
|
+
warn: (arg, message) => {
|
|
1711
|
+
if (message === void 0) request.log.warn(arg);
|
|
1712
|
+
else request.log.warn(arg, message);
|
|
1713
|
+
},
|
|
1714
|
+
error: (arg, message) => {
|
|
1715
|
+
if (message === void 0) request.log.error(arg);
|
|
1716
|
+
else request.log.error(arg, message);
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function sendDispatchResponse(reply, response) {
|
|
1721
|
+
reply.status(response.status);
|
|
1722
|
+
for (const [name, value] of Object.entries(response.headers)) reply.header(name, value);
|
|
1723
|
+
if (response.body === void 0 || response.body === null) return reply.send();
|
|
1724
|
+
return reply.send(response.body);
|
|
1725
|
+
}
|
|
1726
|
+
function sendError(reply, error) {
|
|
1727
|
+
if (error instanceof RuntimeBackendError) {
|
|
1728
|
+
return reply.status(error.statusCode).send({
|
|
1729
|
+
error: {
|
|
1730
|
+
code: error.code,
|
|
1731
|
+
message: error.message,
|
|
1732
|
+
...error.details ? { details: error.details } : {}
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
return reply.status(500).send({
|
|
1737
|
+
error: {
|
|
1738
|
+
code: ErrorCode2.enum.INTERNAL_ERROR,
|
|
1739
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
async function runtimeBackendGateway(app, opts) {
|
|
1744
|
+
const handle = async (request, reply) => {
|
|
1745
|
+
const { pluginId } = request.params;
|
|
1746
|
+
if (!isValidBoringPluginId(pluginId)) {
|
|
1747
|
+
return sendError(reply, new RuntimeBackendError(
|
|
1748
|
+
ErrorCode2.enum.RUNTIME_PLUGIN_NOT_FOUND,
|
|
1749
|
+
404,
|
|
1750
|
+
"runtime backend plugin not found"
|
|
1751
|
+
));
|
|
1752
|
+
}
|
|
1753
|
+
let path;
|
|
1754
|
+
try {
|
|
1755
|
+
path = normalizeGatewayPath(rawGatewayTail(request, pluginId));
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
return sendError(reply, error);
|
|
1758
|
+
}
|
|
1759
|
+
const abort = new AbortController();
|
|
1760
|
+
const close = () => abort.abort();
|
|
1761
|
+
request.raw.on("close", close);
|
|
1762
|
+
try {
|
|
1763
|
+
const response = await opts.registry.dispatch({
|
|
1764
|
+
pluginId,
|
|
1765
|
+
method: request.method,
|
|
1766
|
+
path,
|
|
1767
|
+
query: new URLSearchParams(request.query),
|
|
1768
|
+
headers: headersFromRequest(request),
|
|
1769
|
+
signal: abort.signal,
|
|
1770
|
+
body: request.body,
|
|
1771
|
+
logger: loggerFromRequest(request),
|
|
1772
|
+
...firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId ? { workspaceId: firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId } : {}
|
|
1773
|
+
});
|
|
1774
|
+
return sendDispatchResponse(reply, response);
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
return sendError(reply, error);
|
|
1777
|
+
} finally {
|
|
1778
|
+
request.raw.off("close", close);
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
app.all("/api/v1/plugins/:pluginId", handle);
|
|
1782
|
+
app.all("/api/v1/plugins/:pluginId/*", handle);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/server/agentPlugins/aggregatePluginPrompts.ts
|
|
1786
|
+
function aggregatePluginPrompts(manager) {
|
|
1787
|
+
const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
1788
|
+
if (prompts.length === 0) return void 0;
|
|
1789
|
+
return `# Loaded boring-ui plugin context
|
|
1790
|
+
|
|
1791
|
+
${prompts.join("\n\n")}`;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// src/app/server/pluginEntryResolver.ts
|
|
1795
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1796
|
+
import { join as join5, resolve as resolve5 } from "path";
|
|
1797
|
+
function readPluginPackageJson(dir) {
|
|
1798
|
+
const pkgPath = resolve5(dir, "package.json");
|
|
1799
|
+
if (!existsSync5(pkgPath)) return null;
|
|
1800
|
+
try {
|
|
1801
|
+
return JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
1802
|
+
} catch {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1340
1806
|
function resolveDirServerEntryPath(dir) {
|
|
1341
1807
|
const rootDir = resolve5(dir);
|
|
1342
1808
|
const pkg = readPluginPackageJson(rootDir);
|
|
@@ -2283,7 +2749,50 @@ async function provisionWorkspaceAgentServer(opts) {
|
|
|
2283
2749
|
force: opts.force
|
|
2284
2750
|
});
|
|
2285
2751
|
}
|
|
2286
|
-
function
|
|
2752
|
+
function uniquePluginSources(sources) {
|
|
2753
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
2754
|
+
for (const source of sources) {
|
|
2755
|
+
const existing = byRoot.get(source.rootDir);
|
|
2756
|
+
if (!existing || !existing.workspaceId && source.workspaceId) byRoot.set(source.rootDir, source);
|
|
2757
|
+
}
|
|
2758
|
+
return [...byRoot.values()];
|
|
2759
|
+
}
|
|
2760
|
+
var REMOTE_PI_PACKAGE_SOURCE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
|
|
2761
|
+
function piPackageSourceValue(entry) {
|
|
2762
|
+
if (typeof entry === "string") return entry;
|
|
2763
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
2764
|
+
const source = entry.source;
|
|
2765
|
+
return typeof source === "string" ? source : void 0;
|
|
2766
|
+
}
|
|
2767
|
+
return void 0;
|
|
2768
|
+
}
|
|
2769
|
+
function resolveLocalPiPackageSource(settingsDir, source) {
|
|
2770
|
+
const path = source.startsWith("file:") ? source.slice("file:".length) : source;
|
|
2771
|
+
if (!path) return void 0;
|
|
2772
|
+
if (REMOTE_PI_PACKAGE_SOURCE_PREFIXES.some((prefix) => path.startsWith(prefix))) return void 0;
|
|
2773
|
+
if (!isAbsolute5(path) && path !== "." && path !== "./" && !path.startsWith("./") && !path.startsWith("../")) return void 0;
|
|
2774
|
+
return resolve7(settingsDir, path);
|
|
2775
|
+
}
|
|
2776
|
+
function readPiSettingsBoringPluginSources(settingsPath, workspaceId) {
|
|
2777
|
+
let raw;
|
|
2778
|
+
try {
|
|
2779
|
+
raw = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
2780
|
+
} catch {
|
|
2781
|
+
return [];
|
|
2782
|
+
}
|
|
2783
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
2784
|
+
const packages = raw.packages;
|
|
2785
|
+
if (!Array.isArray(packages)) return [];
|
|
2786
|
+
const settingsDir = dirname7(settingsPath);
|
|
2787
|
+
return uniquePluginSources(
|
|
2788
|
+
packages.map(piPackageSourceValue).map((source) => source ? resolveLocalPiPackageSource(settingsDir, source) : void 0).filter((rootDir) => Boolean(rootDir)).map((rootDir) => ({
|
|
2789
|
+
rootDir,
|
|
2790
|
+
kind: "external",
|
|
2791
|
+
...workspaceId ? { workspaceId } : {}
|
|
2792
|
+
}))
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
function collectBoringPluginSources(workspaceRoot, pluginCollection, additionalPluginDirs = []) {
|
|
2287
2796
|
const extensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
|
|
2288
2797
|
const pluginRoots = extensionPaths.flatMap((path) => {
|
|
2289
2798
|
try {
|
|
@@ -2292,11 +2801,16 @@ function collectBoringPluginDirs(workspaceRoot, pluginCollection, additionalPlug
|
|
|
2292
2801
|
return [];
|
|
2293
2802
|
}
|
|
2294
2803
|
});
|
|
2295
|
-
return
|
|
2296
|
-
join7(workspaceRoot, ".pi", "extensions"),
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2804
|
+
return uniquePluginSources([
|
|
2805
|
+
{ rootDir: join7(workspaceRoot, ".pi", "extensions"), kind: "external", workspaceId: workspaceRoot },
|
|
2806
|
+
{ rootDir: join7(workspaceRoot, ".pi", "npm"), kind: "external", workspaceId: workspaceRoot },
|
|
2807
|
+
{ rootDir: join7(workspaceRoot, ".pi", "git"), kind: "external", workspaceId: workspaceRoot },
|
|
2808
|
+
{ rootDir: join7(homedir(), ".pi", "agent", "extensions"), kind: "external" },
|
|
2809
|
+
...readPiSettingsBoringPluginSources(join7(workspaceRoot, ".pi", "settings.json"), workspaceRoot),
|
|
2810
|
+
...readPiSettingsBoringPluginSources(join7(homedir(), ".pi", "agent", "settings.json")),
|
|
2811
|
+
...pluginRoots.map((rootDir) => ({ rootDir, kind: "internal" })),
|
|
2812
|
+
...additionalPluginDirs.map((entry) => typeof entry === "string" ? { rootDir: entry, kind: "internal" } : entry)
|
|
2813
|
+
]);
|
|
2300
2814
|
}
|
|
2301
2815
|
function mergeRuntimeProvisioningInputs(plugins) {
|
|
2302
2816
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -2409,11 +2923,17 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2409
2923
|
...pluginCollection.agentOptions.pi?.packages ?? []
|
|
2410
2924
|
];
|
|
2411
2925
|
const baseStaticPiExtensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
|
|
2412
|
-
const boringPluginDirs = [
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2926
|
+
const boringPluginDirs = [];
|
|
2927
|
+
const refreshBoringPluginDirs = () => {
|
|
2928
|
+
const next = uniquePluginSources([
|
|
2929
|
+
...defaultPluginPackagePaths.map((rootDir) => ({ rootDir, kind: "internal" })),
|
|
2930
|
+
...collectBoringPluginSources(workspaceRoot, pluginCollection, opts.additionalBoringPluginDirs)
|
|
2931
|
+
]);
|
|
2932
|
+
boringPluginDirs.splice(0, boringPluginDirs.length, ...next);
|
|
2933
|
+
return boringPluginDirs;
|
|
2934
|
+
};
|
|
2935
|
+
refreshBoringPluginDirs();
|
|
2936
|
+
const staticPluginPackagePiSnapshot = pluginHotReload ? emptyPackageJsonPiSnapshot() : readWorkspacePluginPackagePiSnapshot(refreshBoringPluginDirs());
|
|
2417
2937
|
const staticPiSkillPaths = [
|
|
2418
2938
|
...baseStaticPiSkillPaths,
|
|
2419
2939
|
...staticPluginPackagePiSnapshot.additionalSkillPaths
|
|
@@ -2426,17 +2946,18 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2426
2946
|
...baseStaticPiExtensionPaths,
|
|
2427
2947
|
...staticPluginPackagePiSnapshot.extensionPaths
|
|
2428
2948
|
];
|
|
2429
|
-
const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(
|
|
2949
|
+
const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(refreshBoringPluginDirs()) : void 0;
|
|
2430
2950
|
const boringAssetManager = new BoringPluginAssetManager({
|
|
2431
2951
|
pluginDirs: boringPluginDirs,
|
|
2432
2952
|
errorRoot: join7(workspaceRoot, ".pi", "extensions"),
|
|
2433
2953
|
frontTargetResolver: opts.boringPluginFrontTargetResolver,
|
|
2434
2954
|
includeLegacyFrontUrl: opts.boringPluginIncludeLegacyFrontUrl
|
|
2435
2955
|
});
|
|
2956
|
+
const runtimeBackendRegistry = new RuntimeBackendRegistry();
|
|
2436
2957
|
const buildRuntimeProvisioningInputs = () => {
|
|
2437
2958
|
const inputs = mergeRuntimeProvisioningInputs([
|
|
2438
2959
|
...pluginCollection.runtimePlugins,
|
|
2439
|
-
...readWorkspacePluginPackageRuntimePlugins(
|
|
2960
|
+
...readWorkspacePluginPackageRuntimePlugins(refreshBoringPluginDirs())
|
|
2440
2961
|
]);
|
|
2441
2962
|
if (resolvedMode === "direct") return omitPluginAuthoringProvisioning(inputs);
|
|
2442
2963
|
return inputs;
|
|
@@ -2499,7 +3020,9 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2499
3020
|
let restart_warnings = [];
|
|
2500
3021
|
let diagnostics = [];
|
|
2501
3022
|
if (pluginHotReload) {
|
|
3023
|
+
refreshBoringPluginDirs();
|
|
2502
3024
|
const scan = await boringAssetManager.load();
|
|
3025
|
+
const backendReload = await runtimeBackendRegistry.reloadFromLoadedPlugins(boringAssetManager.inspectLoaded());
|
|
2503
3026
|
restart_warnings = collectRestartWarnings(scan.events);
|
|
2504
3027
|
const scanDiagnostics = scan.errors.map((error) => ({
|
|
2505
3028
|
source: `boring plugin asset scan (${error.id})`,
|
|
@@ -2507,7 +3030,7 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2507
3030
|
pluginId: error.id
|
|
2508
3031
|
}));
|
|
2509
3032
|
const rebuild = await rebuildPlugins();
|
|
2510
|
-
diagnostics = [...scanDiagnostics, ...rebuild.diagnostics];
|
|
3033
|
+
diagnostics = [...scanDiagnostics, ...backendReload.diagnostics, ...rebuild.diagnostics];
|
|
2511
3034
|
}
|
|
2512
3035
|
await runRuntimeProvisioning();
|
|
2513
3036
|
const callerResult = await opts.beforeReload?.();
|
|
@@ -2533,19 +3056,26 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2533
3056
|
},
|
|
2534
3057
|
systemPromptDynamic: pluginHotReload ? () => aggregatePluginPrompts(boringAssetManager) : void 0
|
|
2535
3058
|
});
|
|
3059
|
+
refreshBoringPluginDirs();
|
|
2536
3060
|
await boringAssetManager.load();
|
|
3061
|
+
await runtimeBackendRegistry.reloadFromLoadedPlugins(boringAssetManager.inspectLoaded());
|
|
3062
|
+
if (typeof app.addHook === "function") {
|
|
3063
|
+
app.addHook("onClose", async () => {
|
|
3064
|
+
await runtimeBackendRegistry.close();
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
2537
3067
|
await app.register(uiRoutes, { bridge, preserveStateKeys: pluginCollection.preservedUiStateKeys });
|
|
2538
3068
|
await app.register(boringPluginRoutes, {
|
|
2539
|
-
manager: boringAssetManager
|
|
2540
|
-
rebuildPlugins,
|
|
2541
|
-
enableReloadRoute: pluginHotReload
|
|
3069
|
+
manager: boringAssetManager
|
|
2542
3070
|
});
|
|
3071
|
+
await app.register(runtimeBackendGateway, { registry: runtimeBackendRegistry, defaultWorkspaceId: workspaceRoot });
|
|
2543
3072
|
for (const { routes } of pluginCollection.routeContributions) {
|
|
2544
3073
|
await app.register(routes);
|
|
2545
3074
|
}
|
|
2546
3075
|
;
|
|
2547
3076
|
app.__boringRebuildPlugins = rebuildPlugins;
|
|
2548
3077
|
app.__boringAssetManager = boringAssetManager;
|
|
3078
|
+
app.__boringRuntimeBackendRegistry = runtimeBackendRegistry;
|
|
2549
3079
|
return app;
|
|
2550
3080
|
}
|
|
2551
3081
|
export {
|