@hachej/boring-workspace 0.1.33 → 0.1.34
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-DWbh0xNI.js} +1 -1
- package/dist/{MarkdownEditor-zbp8ezds.js → MarkdownEditor-BhLjIyPQ.js} +1 -1
- package/dist/{WorkspaceLoadingState-BlvZXmFg.js → WorkspaceLoadingState-RVGURLJy.js} +1 -1
- package/dist/{WorkspaceProvider-CuIZx1ua.js → WorkspaceProvider-hCE2wXKZ.js} +1029 -1015
- package/dist/app-front.js +3 -3
- package/dist/app-server.d.ts +11 -8
- package/dist/app-server.js +603 -79
- package/dist/{createInMemoryBridge-HJopAIbo.d.ts → createInMemoryBridge-zb8MpO60.d.ts} +11 -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 +549 -46
- package/dist/testing.js +1 -1
- package/dist/workspace.js +5 -5
- package/docs/PLUGIN_SYSTEM.md +5 -6
- package/package.json +9 -3
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,7 +104,7 @@ 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`.",
|
|
@@ -348,6 +349,14 @@ function resolveSafePluginEntryPath({
|
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
// src/server/agentPlugins/scan.ts
|
|
352
|
+
function normalizeBoringPluginSource(input) {
|
|
353
|
+
if (typeof input === "string") return { rootDir: resolve2(input), kind: "internal" };
|
|
354
|
+
return {
|
|
355
|
+
rootDir: resolve2(input.rootDir),
|
|
356
|
+
kind: input.kind,
|
|
357
|
+
...input.workspaceId ? { workspaceId: input.workspaceId } : {}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
351
360
|
function pluginIdFromPackageJson(pkg, rootDir) {
|
|
352
361
|
const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
|
|
353
362
|
if (explicitId) return explicitId;
|
|
@@ -406,10 +415,11 @@ function packagePathContainmentIssues(rootDir, pkg) {
|
|
|
406
415
|
return issues;
|
|
407
416
|
}
|
|
408
417
|
function discoverBoringPluginDirs(pluginDirs) {
|
|
409
|
-
const out = /* @__PURE__ */ new
|
|
418
|
+
const out = /* @__PURE__ */ new Map();
|
|
410
419
|
const missingPackageJson = [];
|
|
411
420
|
for (const raw of pluginDirs) {
|
|
412
|
-
const
|
|
421
|
+
const source = normalizeBoringPluginSource(raw);
|
|
422
|
+
const dir = source.rootDir;
|
|
413
423
|
if (!existsSync2(dir)) continue;
|
|
414
424
|
const info = statSync(dir);
|
|
415
425
|
if (!info.isDirectory()) continue;
|
|
@@ -420,13 +430,19 @@ function discoverBoringPluginDirs(pluginDirs) {
|
|
|
420
430
|
const child = join2(dir, entry.name);
|
|
421
431
|
if (existsSync2(join2(child, "package.json"))) childPackageDirs.push(child);
|
|
422
432
|
}
|
|
423
|
-
if (hasPackageJson) out.
|
|
424
|
-
for (const child of childPackageDirs)
|
|
425
|
-
|
|
433
|
+
if (hasPackageJson && !out.has(dir)) out.set(dir, source);
|
|
434
|
+
for (const child of childPackageDirs) {
|
|
435
|
+
if (!out.has(child)) out.set(child, { ...source, rootDir: child });
|
|
436
|
+
}
|
|
437
|
+
const collectionDirNames = /* @__PURE__ */ new Set(["extensions", "npm", "git"]);
|
|
438
|
+
if (!hasPackageJson && childPackageDirs.length === 0 && !collectionDirNames.has(basename(dir))) {
|
|
426
439
|
missingPackageJson.push(dir);
|
|
427
440
|
}
|
|
428
441
|
}
|
|
429
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
sources: [...out.values()].sort((a, b) => a.rootDir.localeCompare(b.rootDir)),
|
|
444
|
+
missingPackageJson: [...new Set(missingPackageJson)].sort()
|
|
445
|
+
};
|
|
430
446
|
}
|
|
431
447
|
function scanBoringPlugins(pluginDirs) {
|
|
432
448
|
const errors = [];
|
|
@@ -436,7 +452,8 @@ function scanBoringPlugins(pluginDirs) {
|
|
|
436
452
|
for (const pluginDir of discovered.missingPackageJson) {
|
|
437
453
|
errors.push({ pluginDir, code: "MISSING_PACKAGE_JSON", message: "package.json is missing" });
|
|
438
454
|
}
|
|
439
|
-
for (const
|
|
455
|
+
for (const source of discovered.sources) {
|
|
456
|
+
const rootDir = source.rootDir;
|
|
440
457
|
let raw;
|
|
441
458
|
try {
|
|
442
459
|
raw = parsePackageJson(rootDir);
|
|
@@ -474,15 +491,26 @@ function scanBoringPlugins(pluginDirs) {
|
|
|
474
491
|
} else {
|
|
475
492
|
const previous = seenIds.get(id);
|
|
476
493
|
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
494
|
const previousPluginIndex = plugins.findIndex((plugin) => plugin.id === id);
|
|
484
|
-
|
|
485
|
-
|
|
495
|
+
const previousPlugin = previousPluginIndex >= 0 ? plugins[previousPluginIndex] : void 0;
|
|
496
|
+
const currentIsWorkspaceLocal = Boolean(source.workspaceId);
|
|
497
|
+
const previousIsWorkspaceLocal = Boolean(previousPlugin?.source.workspaceId);
|
|
498
|
+
const currentMayShadowPrevious = source.kind === "external" && currentIsWorkspaceLocal && previousPlugin?.source.kind === "external" && !previousIsWorkspaceLocal;
|
|
499
|
+
if (currentMayShadowPrevious) {
|
|
500
|
+
if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
|
|
501
|
+
seenIds.set(id, rootDir);
|
|
502
|
+
} else if (!currentIsWorkspaceLocal && previousIsWorkspaceLocal) {
|
|
503
|
+
canAddPlugin = false;
|
|
504
|
+
} else {
|
|
505
|
+
errors.push({
|
|
506
|
+
pluginDir: rootDir,
|
|
507
|
+
pluginId: id,
|
|
508
|
+
code: "INVALID_PLUGIN_METADATA",
|
|
509
|
+
message: `duplicate plugin id "${id}" also declared by ${previous}`
|
|
510
|
+
});
|
|
511
|
+
if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
|
|
512
|
+
canAddPlugin = false;
|
|
513
|
+
}
|
|
486
514
|
} else {
|
|
487
515
|
seenIds.set(id, rootDir);
|
|
488
516
|
}
|
|
@@ -494,6 +522,7 @@ function scanBoringPlugins(pluginDirs) {
|
|
|
494
522
|
}
|
|
495
523
|
if (!canAddPlugin) continue;
|
|
496
524
|
const pkg = result.packageJson;
|
|
525
|
+
const hasBoring = pkg.boring !== void 0;
|
|
497
526
|
const boring = pkg.boring ?? {};
|
|
498
527
|
const pi = pkg.pi;
|
|
499
528
|
const frontPath = resolvePluginPath(rootDir, boring.front, { mustExist: true });
|
|
@@ -506,11 +535,13 @@ function scanBoringPlugins(pluginDirs) {
|
|
|
506
535
|
rootDir,
|
|
507
536
|
version,
|
|
508
537
|
boring,
|
|
538
|
+
hasBoring,
|
|
509
539
|
...pi ? { pi } : {},
|
|
510
540
|
...frontPath ? { frontPath, frontUrl: `/@fs/${frontPath}` } : {},
|
|
511
541
|
...serverPath ? { serverPath } : {},
|
|
512
542
|
...extensionPaths.length > 0 ? { extensionPaths } : {},
|
|
513
|
-
...skillPaths.length > 0 ? { skillPaths } : {}
|
|
543
|
+
...skillPaths.length > 0 ? { skillPaths } : {},
|
|
544
|
+
source
|
|
514
545
|
});
|
|
515
546
|
}
|
|
516
547
|
const preflight = { ok: errors.length === 0, errors };
|
|
@@ -933,10 +964,11 @@ function frontSignatureRoot(plugin) {
|
|
|
933
964
|
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel) ? frontRoot : dirname5(plugin.frontPath);
|
|
934
965
|
}
|
|
935
966
|
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");
|
|
967
|
+
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
968
|
}
|
|
938
969
|
function computeRequiresRestart(previous, next) {
|
|
939
970
|
if (!previous) return [];
|
|
971
|
+
if (previous.source.kind === "external" && next.source.kind === "external") return [];
|
|
940
972
|
const prevHasServer = !!previous.serverPath;
|
|
941
973
|
const nextHasServer = !!next.serverPath;
|
|
942
974
|
if (!prevHasServer && !nextHasServer) return [];
|
|
@@ -982,8 +1014,10 @@ var BoringPluginAssetManager = class {
|
|
|
982
1014
|
version: plugin.version,
|
|
983
1015
|
revision: plugin.revision,
|
|
984
1016
|
rootDir: plugin.rootDir,
|
|
1017
|
+
source: plugin.source,
|
|
985
1018
|
...plugin.frontPath ? { frontPath: plugin.frontPath } : {},
|
|
986
|
-
...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
|
|
1019
|
+
...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {},
|
|
1020
|
+
...plugin.serverPath ? { serverPath: plugin.serverPath } : {}
|
|
987
1021
|
}));
|
|
988
1022
|
}
|
|
989
1023
|
inspectLoadedPiSnapshot() {
|
|
@@ -1023,7 +1057,7 @@ ${prompts.join("\n\n")}` } : {}
|
|
|
1023
1057
|
async doLoadOnce() {
|
|
1024
1058
|
this.lastErrors.clear();
|
|
1025
1059
|
const scan = scanBoringPlugins(this.pluginDirs);
|
|
1026
|
-
const nextPlugins = scan.plugins;
|
|
1060
|
+
const nextPlugins = scan.plugins.filter((plugin) => plugin.hasBoring);
|
|
1027
1061
|
const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
|
|
1028
1062
|
const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve4(error.pluginDir)));
|
|
1029
1063
|
const events = [];
|
|
@@ -1187,31 +1221,7 @@ function collectRestartWarnings(events) {
|
|
|
1187
1221
|
return warnings;
|
|
1188
1222
|
}
|
|
1189
1223
|
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
|
-
}
|
|
1224
|
+
const { manager } = opts;
|
|
1215
1225
|
const listPlugins = async () => manager.list();
|
|
1216
1226
|
app.get("/api/v1/agent-plugins", listPlugins);
|
|
1217
1227
|
const getPluginError = async (request, reply) => {
|
|
@@ -1280,29 +1290,106 @@ async function boringPluginRoutes(app, opts) {
|
|
|
1280
1290
|
});
|
|
1281
1291
|
}
|
|
1282
1292
|
|
|
1283
|
-
// src/server/
|
|
1284
|
-
function
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
return
|
|
1293
|
+
// src/server/runtimeBackend/defineRuntimeServerPlugin.ts
|
|
1294
|
+
function isPlainObject(value) {
|
|
1295
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1296
|
+
const proto = Object.getPrototypeOf(value);
|
|
1297
|
+
return proto === Object.prototype || proto === null;
|
|
1298
|
+
}
|
|
1299
|
+
function validateRuntimeServerPlugin(value) {
|
|
1300
|
+
if (!isPlainObject(value)) {
|
|
1301
|
+
throw new Error("runtime server plugin default export must be a plain object");
|
|
1302
|
+
}
|
|
1303
|
+
if ("id" in value) {
|
|
1304
|
+
throw new Error("runtime server plugin must not declare id; the host supplies plugin id from package metadata");
|
|
1305
|
+
}
|
|
1306
|
+
if (typeof value.routes !== "function") {
|
|
1307
|
+
throw new Error("runtime server plugin default export must define routes(router)");
|
|
1308
|
+
}
|
|
1309
|
+
if (value.dispose !== void 0 && typeof value.dispose !== "function") {
|
|
1310
|
+
throw new Error("runtime server plugin dispose must be a function when provided");
|
|
1311
|
+
}
|
|
1312
|
+
return value;
|
|
1313
|
+
}
|
|
1314
|
+
function isRuntimePluginResponse(value) {
|
|
1315
|
+
return isPlainObject(value) && value.kind === "response";
|
|
1316
|
+
}
|
|
1288
1317
|
|
|
1289
|
-
|
|
1318
|
+
// src/server/runtimeBackend/runtimePathSegments.ts
|
|
1319
|
+
function findUnsafeRuntimePathSegment(path) {
|
|
1320
|
+
if (path.includes("\\")) return "\\";
|
|
1321
|
+
for (const segment of path.split("/")) {
|
|
1322
|
+
if (segment === "." || segment === "..") return segment;
|
|
1323
|
+
}
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
function describeUnsafeRuntimePathSegment(segment) {
|
|
1327
|
+
if (segment === "\\") return "backslashes";
|
|
1328
|
+
return `${segment} segments`;
|
|
1290
1329
|
}
|
|
1291
1330
|
|
|
1292
|
-
// src/
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
function
|
|
1298
|
-
|
|
1299
|
-
|
|
1331
|
+
// src/server/runtimeBackend/routerCapture.ts
|
|
1332
|
+
var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "ALL"];
|
|
1333
|
+
function routeKey(method, path) {
|
|
1334
|
+
return `${method} ${path}`;
|
|
1335
|
+
}
|
|
1336
|
+
function runtimeRouteKey(method, path) {
|
|
1337
|
+
return routeKey(method.toUpperCase(), path);
|
|
1338
|
+
}
|
|
1339
|
+
function validateRuntimeRoutePath(path) {
|
|
1340
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1341
|
+
throw new Error("runtime route path must be a non-empty string");
|
|
1342
|
+
}
|
|
1343
|
+
if (!path.startsWith("/")) {
|
|
1344
|
+
throw new Error(`runtime route path must start with /: ${path}`);
|
|
1345
|
+
}
|
|
1346
|
+
if (path.includes("?") || path.includes("#")) {
|
|
1347
|
+
throw new Error(`runtime route path must not include query strings or fragments: ${path}`);
|
|
1348
|
+
}
|
|
1349
|
+
let decodedPath;
|
|
1300
1350
|
try {
|
|
1301
|
-
|
|
1351
|
+
decodedPath = decodeURIComponent(path);
|
|
1302
1352
|
} catch {
|
|
1303
|
-
|
|
1353
|
+
throw new Error(`runtime route path must be valid percent-encoding: ${path}`);
|
|
1354
|
+
}
|
|
1355
|
+
const unsafeSegment = findUnsafeRuntimePathSegment(decodedPath);
|
|
1356
|
+
if (unsafeSegment) {
|
|
1357
|
+
throw new Error(`runtime route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}: ${path}`);
|
|
1304
1358
|
}
|
|
1359
|
+
if (path.includes(":")) {
|
|
1360
|
+
throw new Error(`runtime route path must be exact and must not contain params: ${path}`);
|
|
1361
|
+
}
|
|
1362
|
+
if (path.includes("*")) {
|
|
1363
|
+
throw new Error(`runtime route path must be exact and must not contain wildcards: ${path}`);
|
|
1364
|
+
}
|
|
1365
|
+
return path;
|
|
1366
|
+
}
|
|
1367
|
+
async function captureRuntimeRoutes(register) {
|
|
1368
|
+
const routes = [];
|
|
1369
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1370
|
+
const add = (method, path, handler) => {
|
|
1371
|
+
if (typeof handler !== "function") {
|
|
1372
|
+
throw new Error(`runtime route ${method} ${path} handler must be a function`);
|
|
1373
|
+
}
|
|
1374
|
+
const normalizedPath = validateRuntimeRoutePath(path);
|
|
1375
|
+
const key = routeKey(method, normalizedPath);
|
|
1376
|
+
if (seen.has(key)) throw new Error(`duplicate runtime route: ${key}`);
|
|
1377
|
+
seen.add(key);
|
|
1378
|
+
routes.push({ method, path: normalizedPath, handler });
|
|
1379
|
+
};
|
|
1380
|
+
const router = Object.fromEntries(
|
|
1381
|
+
METHODS.map((method) => [method.toLowerCase(), (path, handler) => add(method, path, handler)])
|
|
1382
|
+
);
|
|
1383
|
+
await register(router);
|
|
1384
|
+
return routes;
|
|
1305
1385
|
}
|
|
1386
|
+
|
|
1387
|
+
// src/server/runtimeBackend/runtimeBackendRegistry.ts
|
|
1388
|
+
import { ErrorCode } from "@hachej/boring-agent/shared";
|
|
1389
|
+
|
|
1390
|
+
// src/server/pluginImports/importServerModule.ts
|
|
1391
|
+
import { createRequire as createRequire2 } from "module";
|
|
1392
|
+
import { pathToFileURL } from "url";
|
|
1306
1393
|
var require3 = createRequire2(import.meta.url);
|
|
1307
1394
|
var warnedJitiMissing = false;
|
|
1308
1395
|
function warnJitiUnavailable(serverPath, reason) {
|
|
@@ -1337,6 +1424,379 @@ async function importServerModule(serverPath, hotReload) {
|
|
|
1337
1424
|
href
|
|
1338
1425
|
);
|
|
1339
1426
|
}
|
|
1427
|
+
|
|
1428
|
+
// src/server/runtimeBackend/runtimeBackendRegistry.ts
|
|
1429
|
+
var RuntimeBackendError = class extends Error {
|
|
1430
|
+
constructor(code, statusCode, message, details) {
|
|
1431
|
+
super(message);
|
|
1432
|
+
this.code = code;
|
|
1433
|
+
this.statusCode = statusCode;
|
|
1434
|
+
this.details = details;
|
|
1435
|
+
this.name = "RuntimeBackendError";
|
|
1436
|
+
}
|
|
1437
|
+
code;
|
|
1438
|
+
statusCode;
|
|
1439
|
+
details;
|
|
1440
|
+
};
|
|
1441
|
+
function errorMessage(error) {
|
|
1442
|
+
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1443
|
+
}
|
|
1444
|
+
function moduleValue(mod) {
|
|
1445
|
+
return typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
|
|
1446
|
+
}
|
|
1447
|
+
function toRouteMap(routes) {
|
|
1448
|
+
const map = /* @__PURE__ */ new Map();
|
|
1449
|
+
for (const route of routes) map.set(runtimeRouteKey(route.method, route.path), route);
|
|
1450
|
+
return map;
|
|
1451
|
+
}
|
|
1452
|
+
function assertJsonSerializable(value) {
|
|
1453
|
+
if (value === void 0 || value === null) return;
|
|
1454
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
|
1455
|
+
throw new RuntimeBackendError(
|
|
1456
|
+
ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
|
|
1457
|
+
500,
|
|
1458
|
+
"runtime plugin response is not JSON-serializable"
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
JSON.stringify(value);
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
throw new RuntimeBackendError(
|
|
1465
|
+
ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
|
|
1466
|
+
500,
|
|
1467
|
+
`runtime plugin response is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function normalizeResponse(value) {
|
|
1472
|
+
if (value === void 0 || value === null) return { status: 204, headers: {} };
|
|
1473
|
+
if (isRuntimePluginResponse(value)) return normalizeExplicitResponse(value);
|
|
1474
|
+
assertJsonSerializable(value);
|
|
1475
|
+
return { status: 200, headers: { "content-type": "application/json; charset=utf-8" }, body: value };
|
|
1476
|
+
}
|
|
1477
|
+
function normalizeExplicitResponse(value) {
|
|
1478
|
+
const status = value.status ?? (value.body === void 0 || value.body === null ? 204 : 200);
|
|
1479
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
1480
|
+
throw new RuntimeBackendError(
|
|
1481
|
+
ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
|
|
1482
|
+
500,
|
|
1483
|
+
"runtime plugin response status must be an integer HTTP status code"
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
const headers = {};
|
|
1487
|
+
if (value.headers !== void 0) {
|
|
1488
|
+
for (const [name, headerValue] of Object.entries(value.headers)) {
|
|
1489
|
+
if (typeof headerValue !== "string") {
|
|
1490
|
+
throw new RuntimeBackendError(
|
|
1491
|
+
ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
|
|
1492
|
+
500,
|
|
1493
|
+
"runtime plugin response headers must be strings"
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
headers[name] = headerValue;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (value.body === void 0 || value.body === null) return { status, headers };
|
|
1500
|
+
assertJsonSerializable(value.body);
|
|
1501
|
+
if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
|
|
1502
|
+
headers["content-type"] = "application/json; charset=utf-8";
|
|
1503
|
+
}
|
|
1504
|
+
return { status, headers, body: value.body };
|
|
1505
|
+
}
|
|
1506
|
+
async function disposeSnapshot(snapshot) {
|
|
1507
|
+
if (!snapshot.module.dispose) return [];
|
|
1508
|
+
try {
|
|
1509
|
+
await snapshot.module.dispose();
|
|
1510
|
+
return [];
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
return [{
|
|
1513
|
+
pluginId: snapshot.pluginId,
|
|
1514
|
+
source: `runtime backend dispose (${snapshot.pluginId})`,
|
|
1515
|
+
code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
|
|
1516
|
+
message: errorMessage(error)
|
|
1517
|
+
}];
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
var RuntimeBackendRegistry = class {
|
|
1521
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1522
|
+
lastDiagnostics = [];
|
|
1523
|
+
reloadQueue = Promise.resolve({ ok: true, diagnostics: [] });
|
|
1524
|
+
getDiagnostics() {
|
|
1525
|
+
return [...this.lastDiagnostics];
|
|
1526
|
+
}
|
|
1527
|
+
listPluginIds() {
|
|
1528
|
+
return [...this.snapshots.keys()].sort();
|
|
1529
|
+
}
|
|
1530
|
+
async reloadFromLoadedPlugins(plugins) {
|
|
1531
|
+
const run = this.reloadQueue.then(() => this.reloadOnce(plugins), () => this.reloadOnce(plugins));
|
|
1532
|
+
this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
|
|
1533
|
+
return run;
|
|
1534
|
+
}
|
|
1535
|
+
async close() {
|
|
1536
|
+
const run = this.reloadQueue.then(() => this.closeOnce(), () => this.closeOnce());
|
|
1537
|
+
this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
|
|
1538
|
+
return run;
|
|
1539
|
+
}
|
|
1540
|
+
async dispatch(request) {
|
|
1541
|
+
const snapshot = this.snapshots.get(request.pluginId);
|
|
1542
|
+
if (!snapshot) {
|
|
1543
|
+
throw new RuntimeBackendError(
|
|
1544
|
+
ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
|
|
1545
|
+
404,
|
|
1546
|
+
`runtime backend plugin not found: ${request.pluginId}`
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
if (snapshot.source.workspaceId && snapshot.source.workspaceId !== request.workspaceId) {
|
|
1550
|
+
throw new RuntimeBackendError(
|
|
1551
|
+
ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
|
|
1552
|
+
404,
|
|
1553
|
+
`runtime backend plugin not found in workspace: ${request.pluginId}`
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
const route = snapshot.routes.get(runtimeRouteKey(request.method, request.path)) ?? snapshot.routes.get(runtimeRouteKey("ALL", request.path));
|
|
1557
|
+
if (!route) {
|
|
1558
|
+
throw new RuntimeBackendError(
|
|
1559
|
+
ErrorCode.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
|
|
1560
|
+
404,
|
|
1561
|
+
`runtime backend route not found: ${request.method.toUpperCase()} ${request.path}`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
const ctx = {
|
|
1565
|
+
pluginId: request.pluginId,
|
|
1566
|
+
method: request.method.toUpperCase(),
|
|
1567
|
+
path: request.path,
|
|
1568
|
+
query: request.query,
|
|
1569
|
+
headers: request.headers,
|
|
1570
|
+
signal: request.signal,
|
|
1571
|
+
body: request.body,
|
|
1572
|
+
logger: request.logger
|
|
1573
|
+
};
|
|
1574
|
+
try {
|
|
1575
|
+
return normalizeResponse(await route.handler(ctx));
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
if (error instanceof RuntimeBackendError) throw error;
|
|
1578
|
+
throw new RuntimeBackendError(
|
|
1579
|
+
ErrorCode.enum.RUNTIME_PLUGIN_HANDLER_FAILED,
|
|
1580
|
+
500,
|
|
1581
|
+
error instanceof Error ? error.message : String(error)
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async reloadOnce(plugins) {
|
|
1586
|
+
const diagnostics = [];
|
|
1587
|
+
const externalRuntimePlugins = plugins.filter((plugin) => plugin.source.kind === "external" && plugin.serverPath);
|
|
1588
|
+
const nextIds = new Set(externalRuntimePlugins.map((plugin) => plugin.id));
|
|
1589
|
+
for (const id of [...this.snapshots.keys()]) {
|
|
1590
|
+
if (nextIds.has(id)) continue;
|
|
1591
|
+
const previous = this.snapshots.get(id);
|
|
1592
|
+
if (!previous) continue;
|
|
1593
|
+
this.snapshots.delete(id);
|
|
1594
|
+
diagnostics.push(...await disposeSnapshot(previous));
|
|
1595
|
+
}
|
|
1596
|
+
for (const plugin of externalRuntimePlugins) {
|
|
1597
|
+
const serverPath = plugin.serverPath;
|
|
1598
|
+
if (!serverPath) continue;
|
|
1599
|
+
try {
|
|
1600
|
+
const mod = await importServerModule(serverPath, true);
|
|
1601
|
+
const runtimePlugin = validateRuntimeServerPlugin(moduleValue(mod));
|
|
1602
|
+
const routes = await captureRuntimeRoutes((router) => runtimePlugin.routes(router));
|
|
1603
|
+
const nextSnapshot = {
|
|
1604
|
+
pluginId: plugin.id,
|
|
1605
|
+
source: plugin.source,
|
|
1606
|
+
module: runtimePlugin,
|
|
1607
|
+
routes: toRouteMap(routes)
|
|
1608
|
+
};
|
|
1609
|
+
const previous = this.snapshots.get(plugin.id);
|
|
1610
|
+
this.snapshots.set(plugin.id, nextSnapshot);
|
|
1611
|
+
if (previous) diagnostics.push(...await disposeSnapshot(previous));
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
diagnostics.push({
|
|
1614
|
+
pluginId: plugin.id,
|
|
1615
|
+
source: `runtime backend (${plugin.id})`,
|
|
1616
|
+
code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
|
|
1617
|
+
message: errorMessage(error)
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
this.lastDiagnostics = diagnostics;
|
|
1622
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
1623
|
+
}
|
|
1624
|
+
async closeOnce() {
|
|
1625
|
+
const diagnostics = [];
|
|
1626
|
+
for (const snapshot of this.snapshots.values()) {
|
|
1627
|
+
diagnostics.push(...await disposeSnapshot(snapshot));
|
|
1628
|
+
}
|
|
1629
|
+
this.snapshots.clear();
|
|
1630
|
+
this.lastDiagnostics = diagnostics;
|
|
1631
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
// src/server/runtimeBackend/runtimeBackendGateway.ts
|
|
1636
|
+
import { ErrorCode as ErrorCode2 } from "@hachej/boring-agent/shared";
|
|
1637
|
+
var GATEWAY_PREFIX = "/api/v1/plugins/";
|
|
1638
|
+
function rawPathFromRequest(request) {
|
|
1639
|
+
const url = request.raw.url ?? request.url;
|
|
1640
|
+
const queryIndex = url.indexOf("?");
|
|
1641
|
+
return queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
1642
|
+
}
|
|
1643
|
+
function rawGatewayTail(request, pluginId) {
|
|
1644
|
+
const rawPath = rawPathFromRequest(request);
|
|
1645
|
+
const prefix = `${GATEWAY_PREFIX}${pluginId}`;
|
|
1646
|
+
if (rawPath === prefix || rawPath === `${prefix}/`) return "/";
|
|
1647
|
+
if (!rawPath.startsWith(`${prefix}/`)) {
|
|
1648
|
+
throw new RuntimeBackendError(
|
|
1649
|
+
ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
|
|
1650
|
+
404,
|
|
1651
|
+
"runtime backend route not found"
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
return rawPath.slice(prefix.length);
|
|
1655
|
+
}
|
|
1656
|
+
function normalizeGatewayPath(rawTail) {
|
|
1657
|
+
let path;
|
|
1658
|
+
try {
|
|
1659
|
+
path = decodeURIComponent(rawTail);
|
|
1660
|
+
} catch {
|
|
1661
|
+
throw new RuntimeBackendError(
|
|
1662
|
+
ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
|
|
1663
|
+
404,
|
|
1664
|
+
"runtime backend route path is not valid percent-encoding"
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
if (path.length === 0) path = "/";
|
|
1668
|
+
const unsafeSegment = findUnsafeRuntimePathSegment(path);
|
|
1669
|
+
if (unsafeSegment) {
|
|
1670
|
+
throw new RuntimeBackendError(
|
|
1671
|
+
ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
|
|
1672
|
+
404,
|
|
1673
|
+
`runtime backend route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}`
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
return path;
|
|
1677
|
+
}
|
|
1678
|
+
function firstString(value) {
|
|
1679
|
+
if (Array.isArray(value)) return value[0];
|
|
1680
|
+
return value;
|
|
1681
|
+
}
|
|
1682
|
+
function headersFromRequest(request) {
|
|
1683
|
+
const headers = new Headers();
|
|
1684
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
1685
|
+
if (value === void 0) continue;
|
|
1686
|
+
if (Array.isArray(value)) {
|
|
1687
|
+
for (const item of value) headers.append(name, item);
|
|
1688
|
+
} else {
|
|
1689
|
+
headers.set(name, String(value));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return headers;
|
|
1693
|
+
}
|
|
1694
|
+
function loggerFromRequest(request) {
|
|
1695
|
+
return {
|
|
1696
|
+
debug: (arg, message) => {
|
|
1697
|
+
if (message === void 0) request.log.debug(arg);
|
|
1698
|
+
else request.log.debug(arg, message);
|
|
1699
|
+
},
|
|
1700
|
+
info: (arg, message) => {
|
|
1701
|
+
if (message === void 0) request.log.info(arg);
|
|
1702
|
+
else request.log.info(arg, message);
|
|
1703
|
+
},
|
|
1704
|
+
warn: (arg, message) => {
|
|
1705
|
+
if (message === void 0) request.log.warn(arg);
|
|
1706
|
+
else request.log.warn(arg, message);
|
|
1707
|
+
},
|
|
1708
|
+
error: (arg, message) => {
|
|
1709
|
+
if (message === void 0) request.log.error(arg);
|
|
1710
|
+
else request.log.error(arg, message);
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
function sendDispatchResponse(reply, response) {
|
|
1715
|
+
reply.status(response.status);
|
|
1716
|
+
for (const [name, value] of Object.entries(response.headers)) reply.header(name, value);
|
|
1717
|
+
if (response.body === void 0 || response.body === null) return reply.send();
|
|
1718
|
+
return reply.send(response.body);
|
|
1719
|
+
}
|
|
1720
|
+
function sendError(reply, error) {
|
|
1721
|
+
if (error instanceof RuntimeBackendError) {
|
|
1722
|
+
return reply.status(error.statusCode).send({
|
|
1723
|
+
error: {
|
|
1724
|
+
code: error.code,
|
|
1725
|
+
message: error.message,
|
|
1726
|
+
...error.details ? { details: error.details } : {}
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
return reply.status(500).send({
|
|
1731
|
+
error: {
|
|
1732
|
+
code: ErrorCode2.enum.INTERNAL_ERROR,
|
|
1733
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
async function runtimeBackendGateway(app, opts) {
|
|
1738
|
+
const handle = async (request, reply) => {
|
|
1739
|
+
const { pluginId } = request.params;
|
|
1740
|
+
if (!isValidBoringPluginId(pluginId)) {
|
|
1741
|
+
return sendError(reply, new RuntimeBackendError(
|
|
1742
|
+
ErrorCode2.enum.RUNTIME_PLUGIN_NOT_FOUND,
|
|
1743
|
+
404,
|
|
1744
|
+
"runtime backend plugin not found"
|
|
1745
|
+
));
|
|
1746
|
+
}
|
|
1747
|
+
let path;
|
|
1748
|
+
try {
|
|
1749
|
+
path = normalizeGatewayPath(rawGatewayTail(request, pluginId));
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
return sendError(reply, error);
|
|
1752
|
+
}
|
|
1753
|
+
const abort = new AbortController();
|
|
1754
|
+
const close = () => abort.abort();
|
|
1755
|
+
request.raw.on("close", close);
|
|
1756
|
+
try {
|
|
1757
|
+
const response = await opts.registry.dispatch({
|
|
1758
|
+
pluginId,
|
|
1759
|
+
method: request.method,
|
|
1760
|
+
path,
|
|
1761
|
+
query: new URLSearchParams(request.query),
|
|
1762
|
+
headers: headersFromRequest(request),
|
|
1763
|
+
signal: abort.signal,
|
|
1764
|
+
body: request.body,
|
|
1765
|
+
logger: loggerFromRequest(request),
|
|
1766
|
+
...firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId ? { workspaceId: firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId } : {}
|
|
1767
|
+
});
|
|
1768
|
+
return sendDispatchResponse(reply, response);
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
return sendError(reply, error);
|
|
1771
|
+
} finally {
|
|
1772
|
+
request.raw.off("close", close);
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
app.all("/api/v1/plugins/:pluginId", handle);
|
|
1776
|
+
app.all("/api/v1/plugins/:pluginId/*", handle);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/server/agentPlugins/aggregatePluginPrompts.ts
|
|
1780
|
+
function aggregatePluginPrompts(manager) {
|
|
1781
|
+
const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
1782
|
+
if (prompts.length === 0) return void 0;
|
|
1783
|
+
return `# Loaded boring-ui plugin context
|
|
1784
|
+
|
|
1785
|
+
${prompts.join("\n\n")}`;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/app/server/pluginEntryResolver.ts
|
|
1789
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1790
|
+
import { join as join5, resolve as resolve5 } from "path";
|
|
1791
|
+
function readPluginPackageJson(dir) {
|
|
1792
|
+
const pkgPath = resolve5(dir, "package.json");
|
|
1793
|
+
if (!existsSync5(pkgPath)) return null;
|
|
1794
|
+
try {
|
|
1795
|
+
return JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
1796
|
+
} catch {
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1340
1800
|
function resolveDirServerEntryPath(dir) {
|
|
1341
1801
|
const rootDir = resolve5(dir);
|
|
1342
1802
|
const pkg = readPluginPackageJson(rootDir);
|
|
@@ -2283,7 +2743,50 @@ async function provisionWorkspaceAgentServer(opts) {
|
|
|
2283
2743
|
force: opts.force
|
|
2284
2744
|
});
|
|
2285
2745
|
}
|
|
2286
|
-
function
|
|
2746
|
+
function uniquePluginSources(sources) {
|
|
2747
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
2748
|
+
for (const source of sources) {
|
|
2749
|
+
const existing = byRoot.get(source.rootDir);
|
|
2750
|
+
if (!existing || !existing.workspaceId && source.workspaceId) byRoot.set(source.rootDir, source);
|
|
2751
|
+
}
|
|
2752
|
+
return [...byRoot.values()];
|
|
2753
|
+
}
|
|
2754
|
+
var REMOTE_PI_PACKAGE_SOURCE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
|
|
2755
|
+
function piPackageSourceValue(entry) {
|
|
2756
|
+
if (typeof entry === "string") return entry;
|
|
2757
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
2758
|
+
const source = entry.source;
|
|
2759
|
+
return typeof source === "string" ? source : void 0;
|
|
2760
|
+
}
|
|
2761
|
+
return void 0;
|
|
2762
|
+
}
|
|
2763
|
+
function resolveLocalPiPackageSource(settingsDir, source) {
|
|
2764
|
+
const path = source.startsWith("file:") ? source.slice("file:".length) : source;
|
|
2765
|
+
if (!path) return void 0;
|
|
2766
|
+
if (REMOTE_PI_PACKAGE_SOURCE_PREFIXES.some((prefix) => path.startsWith(prefix))) return void 0;
|
|
2767
|
+
if (!isAbsolute5(path) && path !== "." && path !== "./" && !path.startsWith("./") && !path.startsWith("../")) return void 0;
|
|
2768
|
+
return resolve7(settingsDir, path);
|
|
2769
|
+
}
|
|
2770
|
+
function readPiSettingsBoringPluginSources(settingsPath, workspaceId) {
|
|
2771
|
+
let raw;
|
|
2772
|
+
try {
|
|
2773
|
+
raw = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
2774
|
+
} catch {
|
|
2775
|
+
return [];
|
|
2776
|
+
}
|
|
2777
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
2778
|
+
const packages = raw.packages;
|
|
2779
|
+
if (!Array.isArray(packages)) return [];
|
|
2780
|
+
const settingsDir = dirname7(settingsPath);
|
|
2781
|
+
return uniquePluginSources(
|
|
2782
|
+
packages.map(piPackageSourceValue).map((source) => source ? resolveLocalPiPackageSource(settingsDir, source) : void 0).filter((rootDir) => Boolean(rootDir)).map((rootDir) => ({
|
|
2783
|
+
rootDir,
|
|
2784
|
+
kind: "external",
|
|
2785
|
+
...workspaceId ? { workspaceId } : {}
|
|
2786
|
+
}))
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
function collectBoringPluginSources(workspaceRoot, pluginCollection, additionalPluginDirs = []) {
|
|
2287
2790
|
const extensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
|
|
2288
2791
|
const pluginRoots = extensionPaths.flatMap((path) => {
|
|
2289
2792
|
try {
|
|
@@ -2292,11 +2795,16 @@ function collectBoringPluginDirs(workspaceRoot, pluginCollection, additionalPlug
|
|
|
2292
2795
|
return [];
|
|
2293
2796
|
}
|
|
2294
2797
|
});
|
|
2295
|
-
return
|
|
2296
|
-
join7(workspaceRoot, ".pi", "extensions"),
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2798
|
+
return uniquePluginSources([
|
|
2799
|
+
{ rootDir: join7(workspaceRoot, ".pi", "extensions"), kind: "external", workspaceId: workspaceRoot },
|
|
2800
|
+
{ rootDir: join7(workspaceRoot, ".pi", "npm"), kind: "external", workspaceId: workspaceRoot },
|
|
2801
|
+
{ rootDir: join7(workspaceRoot, ".pi", "git"), kind: "external", workspaceId: workspaceRoot },
|
|
2802
|
+
{ rootDir: join7(homedir(), ".pi", "agent", "extensions"), kind: "external" },
|
|
2803
|
+
...readPiSettingsBoringPluginSources(join7(workspaceRoot, ".pi", "settings.json"), workspaceRoot),
|
|
2804
|
+
...readPiSettingsBoringPluginSources(join7(homedir(), ".pi", "agent", "settings.json")),
|
|
2805
|
+
...pluginRoots.map((rootDir) => ({ rootDir, kind: "internal" })),
|
|
2806
|
+
...additionalPluginDirs.map((entry) => typeof entry === "string" ? { rootDir: entry, kind: "internal" } : entry)
|
|
2807
|
+
]);
|
|
2300
2808
|
}
|
|
2301
2809
|
function mergeRuntimeProvisioningInputs(plugins) {
|
|
2302
2810
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -2409,11 +2917,17 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2409
2917
|
...pluginCollection.agentOptions.pi?.packages ?? []
|
|
2410
2918
|
];
|
|
2411
2919
|
const baseStaticPiExtensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
|
|
2412
|
-
const boringPluginDirs = [
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2920
|
+
const boringPluginDirs = [];
|
|
2921
|
+
const refreshBoringPluginDirs = () => {
|
|
2922
|
+
const next = uniquePluginSources([
|
|
2923
|
+
...defaultPluginPackagePaths.map((rootDir) => ({ rootDir, kind: "internal" })),
|
|
2924
|
+
...collectBoringPluginSources(workspaceRoot, pluginCollection, opts.additionalBoringPluginDirs)
|
|
2925
|
+
]);
|
|
2926
|
+
boringPluginDirs.splice(0, boringPluginDirs.length, ...next);
|
|
2927
|
+
return boringPluginDirs;
|
|
2928
|
+
};
|
|
2929
|
+
refreshBoringPluginDirs();
|
|
2930
|
+
const staticPluginPackagePiSnapshot = pluginHotReload ? emptyPackageJsonPiSnapshot() : readWorkspacePluginPackagePiSnapshot(refreshBoringPluginDirs());
|
|
2417
2931
|
const staticPiSkillPaths = [
|
|
2418
2932
|
...baseStaticPiSkillPaths,
|
|
2419
2933
|
...staticPluginPackagePiSnapshot.additionalSkillPaths
|
|
@@ -2426,17 +2940,18 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2426
2940
|
...baseStaticPiExtensionPaths,
|
|
2427
2941
|
...staticPluginPackagePiSnapshot.extensionPaths
|
|
2428
2942
|
];
|
|
2429
|
-
const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(
|
|
2943
|
+
const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(refreshBoringPluginDirs()) : void 0;
|
|
2430
2944
|
const boringAssetManager = new BoringPluginAssetManager({
|
|
2431
2945
|
pluginDirs: boringPluginDirs,
|
|
2432
2946
|
errorRoot: join7(workspaceRoot, ".pi", "extensions"),
|
|
2433
2947
|
frontTargetResolver: opts.boringPluginFrontTargetResolver,
|
|
2434
2948
|
includeLegacyFrontUrl: opts.boringPluginIncludeLegacyFrontUrl
|
|
2435
2949
|
});
|
|
2950
|
+
const runtimeBackendRegistry = new RuntimeBackendRegistry();
|
|
2436
2951
|
const buildRuntimeProvisioningInputs = () => {
|
|
2437
2952
|
const inputs = mergeRuntimeProvisioningInputs([
|
|
2438
2953
|
...pluginCollection.runtimePlugins,
|
|
2439
|
-
...readWorkspacePluginPackageRuntimePlugins(
|
|
2954
|
+
...readWorkspacePluginPackageRuntimePlugins(refreshBoringPluginDirs())
|
|
2440
2955
|
]);
|
|
2441
2956
|
if (resolvedMode === "direct") return omitPluginAuthoringProvisioning(inputs);
|
|
2442
2957
|
return inputs;
|
|
@@ -2499,7 +3014,9 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2499
3014
|
let restart_warnings = [];
|
|
2500
3015
|
let diagnostics = [];
|
|
2501
3016
|
if (pluginHotReload) {
|
|
3017
|
+
refreshBoringPluginDirs();
|
|
2502
3018
|
const scan = await boringAssetManager.load();
|
|
3019
|
+
const backendReload = await runtimeBackendRegistry.reloadFromLoadedPlugins(boringAssetManager.inspectLoaded());
|
|
2503
3020
|
restart_warnings = collectRestartWarnings(scan.events);
|
|
2504
3021
|
const scanDiagnostics = scan.errors.map((error) => ({
|
|
2505
3022
|
source: `boring plugin asset scan (${error.id})`,
|
|
@@ -2507,7 +3024,7 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2507
3024
|
pluginId: error.id
|
|
2508
3025
|
}));
|
|
2509
3026
|
const rebuild = await rebuildPlugins();
|
|
2510
|
-
diagnostics = [...scanDiagnostics, ...rebuild.diagnostics];
|
|
3027
|
+
diagnostics = [...scanDiagnostics, ...backendReload.diagnostics, ...rebuild.diagnostics];
|
|
2511
3028
|
}
|
|
2512
3029
|
await runRuntimeProvisioning();
|
|
2513
3030
|
const callerResult = await opts.beforeReload?.();
|
|
@@ -2533,19 +3050,26 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2533
3050
|
},
|
|
2534
3051
|
systemPromptDynamic: pluginHotReload ? () => aggregatePluginPrompts(boringAssetManager) : void 0
|
|
2535
3052
|
});
|
|
3053
|
+
refreshBoringPluginDirs();
|
|
2536
3054
|
await boringAssetManager.load();
|
|
3055
|
+
await runtimeBackendRegistry.reloadFromLoadedPlugins(boringAssetManager.inspectLoaded());
|
|
3056
|
+
if (typeof app.addHook === "function") {
|
|
3057
|
+
app.addHook("onClose", async () => {
|
|
3058
|
+
await runtimeBackendRegistry.close();
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
2537
3061
|
await app.register(uiRoutes, { bridge, preserveStateKeys: pluginCollection.preservedUiStateKeys });
|
|
2538
3062
|
await app.register(boringPluginRoutes, {
|
|
2539
|
-
manager: boringAssetManager
|
|
2540
|
-
rebuildPlugins,
|
|
2541
|
-
enableReloadRoute: pluginHotReload
|
|
3063
|
+
manager: boringAssetManager
|
|
2542
3064
|
});
|
|
3065
|
+
await app.register(runtimeBackendGateway, { registry: runtimeBackendRegistry, defaultWorkspaceId: workspaceRoot });
|
|
2543
3066
|
for (const { routes } of pluginCollection.routeContributions) {
|
|
2544
3067
|
await app.register(routes);
|
|
2545
3068
|
}
|
|
2546
3069
|
;
|
|
2547
3070
|
app.__boringRebuildPlugins = rebuildPlugins;
|
|
2548
3071
|
app.__boringAssetManager = boringAssetManager;
|
|
3072
|
+
app.__boringRuntimeBackendRegistry = runtimeBackendRegistry;
|
|
2549
3073
|
return app;
|
|
2550
3074
|
}
|
|
2551
3075
|
export {
|