@hachej/boring-workspace 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/dist/CodeEditor-DQqOn4xz.js +266 -0
  4. package/dist/CommandPalette-aM61U-b0.js +5229 -0
  5. package/dist/FileTree-DRq_bfue.js +245 -0
  6. package/dist/MarkdownEditor-DjiHxnRv.js +349 -0
  7. package/dist/WorkspaceLoadingState-By0dZoPD.js +568 -0
  8. package/dist/agent-tool-NvxKfist.d.ts +28 -0
  9. package/dist/app-front.d.ts +485 -0
  10. package/dist/app-front.js +452 -0
  11. package/dist/app-server.d.ts +53 -0
  12. package/dist/app-server.js +769 -0
  13. package/dist/bootstrapServer-BRUqUpVW.d.ts +66 -0
  14. package/dist/boring-workspace.css +1 -0
  15. package/dist/charts.d.ts +114 -0
  16. package/dist/charts.js +143 -0
  17. package/dist/events.d.ts +178 -0
  18. package/dist/events.js +88 -0
  19. package/dist/explorer-DtLUnuah.d.ts +129 -0
  20. package/dist/panel-DnvDNQac.js +6 -0
  21. package/dist/server.d.ts +84 -0
  22. package/dist/server.js +811 -0
  23. package/dist/shared.d.ts +113 -0
  24. package/dist/shared.js +11 -0
  25. package/dist/testing-e2e.d.ts +68 -0
  26. package/dist/testing-e2e.js +45 -0
  27. package/dist/testing.d.ts +464 -0
  28. package/dist/testing.js +10984 -0
  29. package/dist/utils-B6yFEsav.js +8 -0
  30. package/dist/workspace.css +5780 -0
  31. package/dist/workspace.d.ts +2119 -0
  32. package/dist/workspace.js +1884 -0
  33. package/docs/INTERFACES.md +58 -0
  34. package/docs/PLUGIN_STRUCTURE.md +162 -0
  35. package/docs/README.md +19 -0
  36. package/docs/bridge.md +135 -0
  37. package/docs/panels.md +102 -0
  38. package/docs/plans/GENERIC_EXPLORER_PLUGIN_PLAN.md +455 -0
  39. package/docs/plans/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md +962 -0
  40. package/docs/plans/PLUGIN_OUTPUTS_ISOLATION_PLAN.md +301 -0
  41. package/docs/plans/README.md +9 -0
  42. package/docs/plans/UI_BRIDGE_OWNERSHIP_REFACTOR.md +303 -0
  43. package/docs/plans/archive/CODE_OWNERSHIP_CLEANUP_PLAN.md +387 -0
  44. package/docs/plans/archive/COMMAND_PALETTE_REGISTRY.md +814 -0
  45. package/docs/plans/archive/DECLARATIVE_LAYOUT_MIGRATION.md +277 -0
  46. package/docs/plans/archive/PLUGIN_MODEL.md +3674 -0
  47. package/docs/plans/archive/SRC_FOLDER_REORG_PLAN.md +307 -0
  48. package/docs/plans/archive/UNIFIED_EVENT_BUS.md +647 -0
  49. package/docs/plans/archive/WORKSPACE_V2_PLAN.md +2489 -0
  50. package/docs/plugins.md +158 -0
  51. package/package.json +164 -0
package/dist/server.js ADDED
@@ -0,0 +1,811 @@
1
+ // src/server/bridge/createInMemoryBridge.ts
2
+ var MAX_PENDING_COMMANDS = 1e3;
3
+ function createInMemoryBridge() {
4
+ let state = null;
5
+ let nextSeq = 1;
6
+ const subscribers = /* @__PURE__ */ new Set();
7
+ const pendingCommands = [];
8
+ function enqueuePending(command) {
9
+ pendingCommands.push(command);
10
+ if (pendingCommands.length > MAX_PENDING_COMMANDS) {
11
+ pendingCommands.splice(0, pendingCommands.length - MAX_PENDING_COMMANDS);
12
+ }
13
+ }
14
+ return {
15
+ async getState() {
16
+ return state;
17
+ },
18
+ async setState(s) {
19
+ state = s;
20
+ },
21
+ async postCommand(cmd) {
22
+ const seq = nextSeq++;
23
+ const annotated = { ...cmd, seq };
24
+ if (subscribers.size === 0) enqueuePending(annotated);
25
+ for (const handler of subscribers) {
26
+ handler(annotated);
27
+ }
28
+ return { seq, status: "ok" };
29
+ },
30
+ subscribeCommands(handler) {
31
+ subscribers.add(handler);
32
+ return () => {
33
+ subscribers.delete(handler);
34
+ };
35
+ },
36
+ async drainCommands() {
37
+ if (pendingCommands.length === 0) return [];
38
+ return pendingCommands.splice(0, pendingCommands.length);
39
+ }
40
+ };
41
+ }
42
+
43
+ // src/server/ui-control/http/uiRoutes.ts
44
+ import { z } from "zod";
45
+ var UI_BRIDGE_PROTOCOL_VERSION = 1;
46
+ var HEARTBEAT_MS = 15e3;
47
+ var setStateBodySchema = z.object({
48
+ state: z.record(z.unknown()),
49
+ causedBy: z.enum(["user", "agent", "restore"]).optional()
50
+ });
51
+ var postCommandBodySchema = z.object({
52
+ kind: z.string().min(1),
53
+ params: z.record(z.unknown()).default({})
54
+ });
55
+ function createBodyValidator(schema) {
56
+ return async function validateBody(request, reply) {
57
+ const parsed = schema.safeParse(request.body);
58
+ if (!parsed.success) {
59
+ const firstIssue = parsed.error.issues[0];
60
+ const fieldName = firstIssue?.path?.map((segment) => String(segment)).join(".");
61
+ reply.code(400).send({
62
+ error: "validation_error",
63
+ message: firstIssue?.message ?? "Invalid request body",
64
+ field: fieldName || void 0
65
+ });
66
+ return;
67
+ }
68
+ request.body = parsed.data;
69
+ };
70
+ }
71
+ function uiRoutes(app, opts, done) {
72
+ const fallbackBridge = opts.bridge;
73
+ const validateSetState = createBodyValidator(setStateBodySchema);
74
+ const validatePostCommand = createBodyValidator(postCommandBodySchema);
75
+ const resolveBridge = async (request) => {
76
+ if (opts.getBridge) return await opts.getBridge(request);
77
+ if (fallbackBridge) return fallbackBridge;
78
+ throw new Error("uiRoutes requires bridge or getBridge");
79
+ };
80
+ const encodeCommand = (cmd) => ({
81
+ v: UI_BRIDGE_PROTOCOL_VERSION,
82
+ seq: cmd.seq,
83
+ kind: cmd.kind,
84
+ params: cmd.params
85
+ });
86
+ app.get("/api/v1/ui/state", async (request) => {
87
+ const bridge = await resolveBridge(request);
88
+ return await bridge.getState() ?? {};
89
+ });
90
+ app.put(
91
+ "/api/v1/ui/state",
92
+ { preHandler: validateSetState },
93
+ async (request, reply) => {
94
+ const body = request.body;
95
+ const bridge = await resolveBridge(request);
96
+ await bridge.setState(body.state);
97
+ return reply.code(204).send();
98
+ }
99
+ );
100
+ app.post(
101
+ "/api/v1/ui/commands",
102
+ { preHandler: validatePostCommand },
103
+ async (request) => {
104
+ const body = request.body;
105
+ const bridge = await resolveBridge(request);
106
+ const cmd = { kind: body.kind, params: body.params };
107
+ return await bridge.postCommand(cmd);
108
+ }
109
+ );
110
+ app.get("/api/v1/ui/commands/next", async (request, reply) => {
111
+ const bridge = await resolveBridge(request);
112
+ const query = request.query;
113
+ if (query.poll === "true") {
114
+ const batch = bridge.drainCommands ? await bridge.drainCommands() : [];
115
+ return batch.map(encodeCommand);
116
+ }
117
+ reply.raw.writeHead(200, {
118
+ "Content-Type": "text/event-stream",
119
+ "Cache-Control": "no-cache",
120
+ Connection: "keep-alive"
121
+ });
122
+ reply.raw.write(
123
+ `event: init
124
+ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
125
+
126
+ `
127
+ );
128
+ if (bridge.drainCommands) {
129
+ const queued = await bridge.drainCommands();
130
+ for (const cmd of queued) {
131
+ reply.raw.write(
132
+ `event: command
133
+ data: ${JSON.stringify(encodeCommand(cmd))}
134
+
135
+ `
136
+ );
137
+ }
138
+ }
139
+ const unsub = bridge.subscribeCommands((cmd) => {
140
+ reply.raw.write(`event: command
141
+ data: ${JSON.stringify(encodeCommand(cmd))}
142
+
143
+ `);
144
+ });
145
+ const heartbeat = setInterval(() => {
146
+ if (reply.raw.writableEnded) return;
147
+ reply.raw.write(
148
+ `event: heartbeat
149
+ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
150
+
151
+ `
152
+ );
153
+ }, HEARTBEAT_MS);
154
+ request.raw.on("close", () => {
155
+ clearInterval(heartbeat);
156
+ unsub();
157
+ });
158
+ reply.hijack();
159
+ });
160
+ done();
161
+ }
162
+
163
+ // src/server/ui-control/tools/uiTools.ts
164
+ import { access } from "fs/promises";
165
+ import { resolve, isAbsolute, relative } from "path";
166
+ function makeError(message) {
167
+ return {
168
+ content: [{ type: "text", text: message }],
169
+ isError: true
170
+ };
171
+ }
172
+ var PATH_BEARING_KINDS = /* @__PURE__ */ new Set(["openFile", "expandToFile", "navigateToLine"]);
173
+ function getPathParam(kind, params) {
174
+ const raw = kind === "navigateToLine" ? params.file : params.path;
175
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
176
+ }
177
+ async function validatePath(workspaceRoot, relPath) {
178
+ if (isAbsolute(relPath)) {
179
+ return {
180
+ ok: false,
181
+ reason: `path "${relPath}" is absolute \u2014 pass a path relative to the workspace root (${workspaceRoot}).`
182
+ };
183
+ }
184
+ const resolved = resolve(workspaceRoot, relPath);
185
+ const rel = relative(workspaceRoot, resolved);
186
+ if (rel.startsWith("..") || isAbsolute(rel)) {
187
+ return {
188
+ ok: false,
189
+ reason: `path "${relPath}" escapes the workspace root (${workspaceRoot}).`
190
+ };
191
+ }
192
+ try {
193
+ await access(resolved);
194
+ return { ok: true };
195
+ } catch {
196
+ return {
197
+ ok: false,
198
+ reason: `file not found at "${relPath}" (relative to workspace root ${workspaceRoot}). Try find or grep to locate the file before retrying openFile.`
199
+ };
200
+ }
201
+ }
202
+ function createGetUiStateTool(uiBridge) {
203
+ return {
204
+ name: "get_ui_state",
205
+ description: [
206
+ "Read the current workspace UI state. Returns a JSON object with:",
207
+ "- workbenchOpen (boolean): is the right-side workbench pane visible?",
208
+ "- drawerOpen (boolean): is the left-side sessions drawer visible?",
209
+ "- openTabs (array): tabs currently open in the workbench, each with",
210
+ " { id, title, params }. params.path is the file path for file tabs.",
211
+ "- activeTab (string|null): id of the currently focused tab.",
212
+ "- activeFile (string|null): convenience \u2014 params.path of activeTab.",
213
+ "- availablePanels (array of strings): every panel component the host",
214
+ " has registered. Use these names with exec_ui openPanel below.",
215
+ "",
216
+ "Call this BEFORE exec_ui openPanel to learn which `component` ids",
217
+ "are valid for this app. Common built-ins: code-editor, markdown-editor,",
218
+ "csv-viewer. Apps may register more (e.g. chart-canvas, series-viewer)."
219
+ ].join("\n"),
220
+ parameters: {
221
+ type: "object",
222
+ properties: {},
223
+ additionalProperties: false
224
+ },
225
+ async execute() {
226
+ try {
227
+ const state = await uiBridge.getState();
228
+ return {
229
+ content: [{ type: "text", text: JSON.stringify(state ?? {}) }],
230
+ details: state
231
+ };
232
+ } catch (error) {
233
+ const message = error instanceof Error ? error.message : "get_ui_state failed";
234
+ return makeError(message);
235
+ }
236
+ }
237
+ };
238
+ }
239
+ var VERIFIABLE_KINDS = /* @__PURE__ */ new Set(["openFile", "openPanel", "openSurface", "closePanel"]);
240
+ function isVerified(kind, params, state) {
241
+ if (!state) return false;
242
+ const tabs = state.openTabs ?? [];
243
+ if (kind === "openFile") {
244
+ const path = typeof params.path === "string" ? params.path : null;
245
+ return path !== null && tabs.some((t) => t.params?.path === path);
246
+ }
247
+ if (kind === "openPanel") {
248
+ const id = typeof params.id === "string" ? params.id : null;
249
+ return id !== null && tabs.some((t) => t.id === id);
250
+ }
251
+ if (kind === "closePanel") {
252
+ const id = typeof params.id === "string" ? params.id : null;
253
+ return id !== null && !tabs.some((t) => t.id === id);
254
+ }
255
+ return true;
256
+ }
257
+ function createExecUiTool(uiBridge, opts = {}) {
258
+ const { workspaceRoot } = opts;
259
+ const verifyDelayMs = opts.verifyDelayMs ?? 200;
260
+ const verifyRetries = opts.verifyRetries ?? 2;
261
+ const verifyIntervalMs = opts.verifyIntervalMs ?? 200;
262
+ return {
263
+ name: "exec_ui",
264
+ description: [
265
+ "Execute a UI command in the workspace. Use this to open files, panels,",
266
+ "navigate to lines, or show notifications.",
267
+ "",
268
+ "CRITICAL: When the user asks for a concrete UI action (open/show/",
269
+ "focus/navigate), execute it immediately via exec_ui. Do not ask a",
270
+ "confirmation question first unless the target is genuinely ambiguous",
271
+ "or unsafe.",
272
+ "",
273
+ "CRITICAL: When the user asks to open / show / display / navigate to a",
274
+ "file, ALWAYS call exec_ui openFile. Never skip the call based on",
275
+ "conversation history OR get_ui_state output \u2014 even if openTabs already",
276
+ "lists the file. State can drift (the user may have closed the tab,",
277
+ "the persisted state may be stale, the tab may not be focused). Calling",
278
+ "openFile when the file is already open is idempotent: it focuses the",
279
+ 'tab. Saying "already opened" without calling the tool is a bug \u2014 the',
280
+ "user explicitly requested an action; honor it.",
281
+ "",
282
+ "Supported `kind` values:",
283
+ "",
284
+ " openFile params: { path: string, mode?: 'view'|'edit'|'diff' }",
285
+ " \u2014 Open a file in the workbench. The workbench pane",
286
+ " auto-opens if collapsed. Path must be relative to the",
287
+ " workspace root (e.g. `src/foo.ts`, not `foo.ts` if it",
288
+ " lives under src/).",
289
+ " Recovery on file-not-found: this tool stat-checks the",
290
+ " path server-side and returns an error if it doesn't",
291
+ " exist. On that error, immediately call find (or",
292
+ " grep) to locate the file, then call exec_ui",
293
+ " openFile AGAIN using the EXACT path returned \u2014 don't",
294
+ " give up and don't switch to the read tool. Repeat",
295
+ " until openFile succeeds or no candidate is found.",
296
+ " Example: {kind:'openFile', params:{path:'README.md'}}",
297
+ "",
298
+ " openPanel params: { id: string, component: string,",
299
+ " title?: string, params?: object }",
300
+ " \u2014 Open an app-specific panel. `component` MUST be one",
301
+ " of the ids returned by get_ui_state's availablePanels.",
302
+ " `id` is the tab instance id (re-use the same id to",
303
+ " re-activate an existing tab; pick a unique id per",
304
+ " distinct artifact). `params` is forwarded to the",
305
+ " panel component.",
306
+ " Example: {kind:'openPanel', params:{id:'chart:GDPC1',",
307
+ " component:'chart-canvas',",
308
+ " params:{seriesId:'GDPC1'}}}",
309
+ "",
310
+ " openSurface params: { kind: string, target: string, meta?: object }",
311
+ " \u2014 Open a plugin-owned target through the workspace",
312
+ " surface resolver registry. Use this when a plugin",
313
+ " defines the mapping from domain target to panel",
314
+ " component, for example a data catalog row.",
315
+ " Example: {kind:'openSurface', params:{",
316
+ " kind:'data-catalog.open-row',",
317
+ " target:'orders_daily',",
318
+ " meta:{catalogId:'data-catalog'}}}",
319
+ "",
320
+ " closePanel params: { id: string }",
321
+ " closeWorkbenchLeftPane params: {}",
322
+ " \u2014 Hide the workbench's left sources/files pane while",
323
+ " keeping the workbench itself open.",
324
+ " navigateToLine params: { file: string, line: number }",
325
+ " expandToFile params: { path: string }",
326
+ " showNotification params: { msg: string, level?: 'info'|'warn'|'error' }",
327
+ "",
328
+ "Returns { seq, status, uiState? }. For openFile / openPanel / openSurface /",
329
+ "closePanel the response includes a `uiState` snapshot taken ~400ms after",
330
+ "dispatch \u2014 check uiState.openTabs to confirm the action took effect.",
331
+ "If the expected tab is missing from openTabs the frontend silently",
332
+ "rejected the command (unknown panel component, unregistered surface",
333
+ "resolver, or surface not yet ready). For other kinds only { seq, status }",
334
+ "is returned. To open a FILE prefer openFile (path-aware) over openPanel",
335
+ "(which is for non-file panes like charts)."
336
+ ].join("\n"),
337
+ parameters: {
338
+ type: "object",
339
+ properties: {
340
+ kind: {
341
+ type: "string",
342
+ enum: [
343
+ "openFile",
344
+ "openPanel",
345
+ "openSurface",
346
+ "closePanel",
347
+ "closeWorkbenchLeftPane",
348
+ "navigateToLine",
349
+ "expandToFile",
350
+ "showNotification"
351
+ ]
352
+ },
353
+ params: { type: "object" }
354
+ },
355
+ required: ["kind"],
356
+ additionalProperties: false
357
+ },
358
+ async execute(input) {
359
+ const kind = input.kind;
360
+ if (typeof kind !== "string" || kind.length === 0) {
361
+ return makeError("kind is required");
362
+ }
363
+ const params = input.params;
364
+ if (params !== void 0 && (typeof params !== "object" || params === null || Array.isArray(params))) {
365
+ return makeError("params must be an object when provided");
366
+ }
367
+ const cmdParams = params ?? {};
368
+ if (kind === "openSurface") {
369
+ if (typeof cmdParams.kind !== "string" || cmdParams.kind.length === 0) {
370
+ return makeError("openSurface: kind param is required");
371
+ }
372
+ if (typeof cmdParams.target !== "string" || cmdParams.target.length === 0) {
373
+ return makeError("openSurface: target param is required");
374
+ }
375
+ if (cmdParams.meta !== void 0 && (typeof cmdParams.meta !== "object" || cmdParams.meta === null || Array.isArray(cmdParams.meta))) {
376
+ return makeError("openSurface: meta must be an object when provided");
377
+ }
378
+ }
379
+ if (workspaceRoot && PATH_BEARING_KINDS.has(kind)) {
380
+ const relPath = getPathParam(kind, cmdParams);
381
+ if (!relPath) {
382
+ return makeError(
383
+ `${kind}: ${kind === "navigateToLine" ? "file" : "path"} param is required`
384
+ );
385
+ }
386
+ const check = await validatePath(workspaceRoot, relPath);
387
+ if (!check.ok) {
388
+ return makeError(check.reason);
389
+ }
390
+ }
391
+ try {
392
+ const command = { kind, params: cmdParams };
393
+ const result = await uiBridge.postCommand(command);
394
+ if (result.status === "error") {
395
+ return {
396
+ content: [{ type: "text", text: JSON.stringify(result) }],
397
+ isError: true,
398
+ details: result
399
+ };
400
+ }
401
+ if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(kind)) {
402
+ await new Promise((r) => setTimeout(r, verifyDelayMs));
403
+ let uiState = await uiBridge.getState();
404
+ for (let i = 0; i < verifyRetries; i++) {
405
+ if (isVerified(kind, cmdParams, uiState)) break;
406
+ await new Promise((r) => setTimeout(r, verifyIntervalMs));
407
+ uiState = await uiBridge.getState();
408
+ }
409
+ const combined = { ...result, uiState };
410
+ return {
411
+ content: [{ type: "text", text: JSON.stringify(combined) }],
412
+ details: combined
413
+ };
414
+ }
415
+ return {
416
+ content: [{ type: "text", text: JSON.stringify(result) }],
417
+ details: result
418
+ };
419
+ } catch (error) {
420
+ const message = error instanceof Error ? error.message : "exec_ui failed";
421
+ return makeError(message);
422
+ }
423
+ }
424
+ };
425
+ }
426
+ function createWorkspaceUiTools(uiBridge, opts = {}) {
427
+ return [createGetUiStateTool(uiBridge), createExecUiTool(uiBridge, opts)];
428
+ }
429
+
430
+ // src/server/plugins/piPackages.ts
431
+ import {
432
+ compactPiPackages,
433
+ mergePiPackageSources,
434
+ piPackageSourceKey,
435
+ PI_PACKAGE_RESOURCE_FILTERS
436
+ } from "@hachej/boring-agent/server";
437
+
438
+ // src/server/plugins/defineServerPlugin.ts
439
+ var ServerPluginError = class extends Error {
440
+ constructor(message) {
441
+ super(message);
442
+ }
443
+ };
444
+ function fail(pluginId, message) {
445
+ throw new ServerPluginError(`server plugin "${pluginId}": ${message}`);
446
+ }
447
+ function isUrl(value) {
448
+ return value instanceof URL;
449
+ }
450
+ function isPathLike(value) {
451
+ return typeof value === "string" && value.length > 0 || isUrl(value);
452
+ }
453
+ function validateAgentTool(pluginId, tool, index) {
454
+ if (!tool || typeof tool !== "object") {
455
+ fail(pluginId, `agentTools[${index}] must be an object`);
456
+ }
457
+ const candidate = tool;
458
+ if (!candidate.name || typeof candidate.name !== "string") {
459
+ fail(pluginId, `agentTools[${index}].name must be a non-empty string`);
460
+ }
461
+ if (typeof candidate.description !== "string") {
462
+ fail(pluginId, `agentTools[${index}].description must be a string`);
463
+ }
464
+ if (!candidate.parameters || typeof candidate.parameters !== "object") {
465
+ fail(pluginId, `agentTools[${index}].parameters must be an object`);
466
+ }
467
+ if (typeof candidate.execute !== "function") {
468
+ fail(pluginId, `agentTools[${index}].execute must be a function`);
469
+ }
470
+ }
471
+ function validatePiPackages(pluginId, piPackages) {
472
+ for (let i = 0; i < piPackages.length; i++) {
473
+ const source = piPackages[i];
474
+ if (typeof source === "string") {
475
+ if (source.length === 0) {
476
+ fail(pluginId, `piPackages[${i}] must be a non-empty string`);
477
+ }
478
+ continue;
479
+ }
480
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
481
+ fail(pluginId, `piPackages[${i}] must be a string or package source object`);
482
+ }
483
+ const candidate = source;
484
+ if (typeof candidate.source !== "string" || candidate.source.length === 0) {
485
+ fail(pluginId, `piPackages[${i}].source must be a non-empty string`);
486
+ }
487
+ for (const key of PI_PACKAGE_RESOURCE_FILTERS) {
488
+ const value = candidate[key];
489
+ if (value === void 0) continue;
490
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
491
+ fail(pluginId, `piPackages[${i}].${key} must be a string array when provided`);
492
+ }
493
+ }
494
+ }
495
+ }
496
+ function validateProvisioning(pluginId, provisioning) {
497
+ if (!provisioning || typeof provisioning !== "object") {
498
+ fail(pluginId, "provisioning must be an object");
499
+ }
500
+ if (provisioning.templateDirs !== void 0) {
501
+ if (!Array.isArray(provisioning.templateDirs)) {
502
+ fail(pluginId, "provisioning.templateDirs must be an array when provided");
503
+ }
504
+ for (let i = 0; i < provisioning.templateDirs.length; i++) {
505
+ const contribution = provisioning.templateDirs[i];
506
+ if (!contribution || typeof contribution !== "object") {
507
+ fail(pluginId, `provisioning.templateDirs[${i}] must be an object`);
508
+ }
509
+ if (!contribution.id || typeof contribution.id !== "string") {
510
+ fail(pluginId, `provisioning.templateDirs[${i}].id must be a non-empty string`);
511
+ }
512
+ if (!isPathLike(contribution.path)) {
513
+ fail(pluginId, `provisioning.templateDirs[${i}].path must be a string or URL`);
514
+ }
515
+ if (contribution.target !== void 0 && typeof contribution.target !== "string") {
516
+ fail(pluginId, `provisioning.templateDirs[${i}].target must be a string when provided`);
517
+ }
518
+ }
519
+ }
520
+ if (provisioning.python !== void 0) {
521
+ if (!Array.isArray(provisioning.python)) {
522
+ fail(pluginId, "provisioning.python must be an array when provided");
523
+ }
524
+ for (let i = 0; i < provisioning.python.length; i++) {
525
+ const spec = provisioning.python[i];
526
+ if (!spec || typeof spec !== "object") {
527
+ fail(pluginId, `provisioning.python[${i}] must be an object`);
528
+ }
529
+ if (!spec.id || typeof spec.id !== "string") {
530
+ fail(pluginId, `provisioning.python[${i}].id must be a non-empty string`);
531
+ }
532
+ if (!isPathLike(spec.projectFile)) {
533
+ fail(pluginId, `provisioning.python[${i}].projectFile must be a string or URL`);
534
+ }
535
+ if (spec.extraLibs !== void 0 && (!Array.isArray(spec.extraLibs) || spec.extraLibs.some((item) => typeof item !== "string"))) {
536
+ fail(pluginId, `provisioning.python[${i}].extraLibs must be a string array when provided`);
537
+ }
538
+ if (spec.env !== void 0) {
539
+ if (!spec.env || typeof spec.env !== "object" || Array.isArray(spec.env)) {
540
+ fail(pluginId, `provisioning.python[${i}].env must be an object when provided`);
541
+ }
542
+ for (const [key, value] of Object.entries(spec.env)) {
543
+ if (!key || !isPathLike(value)) {
544
+ fail(pluginId, `provisioning.python[${i}].env values must be strings or URLs`);
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ }
551
+ function validateServerPlugin(plugin) {
552
+ if (!plugin.id || typeof plugin.id !== "string") {
553
+ fail(plugin.id ?? "<unknown>", "id must be a non-empty string");
554
+ }
555
+ if (plugin.label !== void 0 && typeof plugin.label !== "string") {
556
+ fail(plugin.id, "label must be a string when provided");
557
+ }
558
+ if (plugin.systemPrompt !== void 0 && typeof plugin.systemPrompt !== "string") {
559
+ fail(plugin.id, "systemPrompt must be a string when provided");
560
+ }
561
+ if (plugin.piPackages !== void 0) {
562
+ if (!Array.isArray(plugin.piPackages)) {
563
+ fail(plugin.id, "piPackages must be an array when provided");
564
+ }
565
+ validatePiPackages(plugin.id, plugin.piPackages);
566
+ }
567
+ if (plugin.agentTools !== void 0) {
568
+ if (!Array.isArray(plugin.agentTools)) {
569
+ fail(plugin.id, "agentTools must be an array when provided");
570
+ }
571
+ plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
572
+ }
573
+ if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
574
+ fail(plugin.id, "routes must be a Fastify plugin function when provided");
575
+ }
576
+ if (plugin.provisioning !== void 0) {
577
+ validateProvisioning(plugin.id, plugin.provisioning);
578
+ }
579
+ }
580
+ function defineServerPlugin(plugin) {
581
+ validateServerPlugin(plugin);
582
+ return Object.assign({}, plugin);
583
+ }
584
+
585
+ // src/server/plugins/composeServerPlugins.ts
586
+ function compactPrompts(prompts) {
587
+ const text = prompts.map((prompt) => prompt?.trim()).filter((prompt) => Boolean(prompt)).join("\n\n");
588
+ return text || void 0;
589
+ }
590
+ function mergeProvisioning(contributions) {
591
+ const templateDirs = contributions.flatMap((entry) => entry?.templateDirs ?? []);
592
+ const python = contributions.flatMap((entry) => entry?.python ?? []);
593
+ if (templateDirs.length === 0 && python.length === 0) return void 0;
594
+ return {
595
+ ...templateDirs.length > 0 ? { templateDirs } : {},
596
+ ...python.length > 0 ? { python } : {}
597
+ };
598
+ }
599
+ function composeRoutes(routes) {
600
+ const routePlugins = routes.filter(
601
+ (route) => Boolean(route)
602
+ );
603
+ if (routePlugins.length === 0) return void 0;
604
+ return async (app) => {
605
+ for (const routes2 of routePlugins) {
606
+ await app.register(routes2);
607
+ }
608
+ };
609
+ }
610
+ function composeServerPlugins(options) {
611
+ const piPackages = compactPiPackages([
612
+ ...options.plugins.flatMap((plugin) => plugin.piPackages ?? []),
613
+ ...options.piPackages ?? []
614
+ ]);
615
+ const agentTools = [
616
+ ...options.plugins.flatMap((plugin) => plugin.agentTools ?? []),
617
+ ...options.agentTools ?? []
618
+ ];
619
+ const systemPrompt = compactPrompts([
620
+ ...options.plugins.map((plugin) => plugin.systemPrompt),
621
+ options.systemPrompt
622
+ ]);
623
+ const provisioning = mergeProvisioning([
624
+ ...options.plugins.map((plugin) => plugin.provisioning),
625
+ options.provisioning
626
+ ]);
627
+ const routes = composeRoutes([
628
+ ...options.plugins.map((plugin) => plugin.routes),
629
+ options.routes
630
+ ]);
631
+ return defineServerPlugin({
632
+ id: options.id,
633
+ ...options.label !== void 0 ? { label: options.label } : {},
634
+ ...piPackages.length > 0 ? { piPackages } : {},
635
+ ...systemPrompt ? { systemPrompt } : {},
636
+ ...agentTools.length > 0 ? { agentTools } : {},
637
+ ...provisioning ? { provisioning } : {},
638
+ ...routes ? { routes } : {}
639
+ });
640
+ }
641
+
642
+ // src/server/plugins/bootstrapServer.ts
643
+ function collectPiPackages(plugins) {
644
+ return compactPiPackages(plugins.flatMap((plugin) => plugin.piPackages ?? []));
645
+ }
646
+ function bootstrapServer(options) {
647
+ const excludedDefaults = new Set(options.excludeDefaults ?? []);
648
+ const finalPlugins = [
649
+ ...(options.defaults ?? []).filter((p) => !excludedDefaults.has(p.id)),
650
+ ...options.plugins ?? []
651
+ ];
652
+ const seenIds = /* @__PURE__ */ new Set();
653
+ for (const plugin of finalPlugins) {
654
+ validateServerPlugin(plugin);
655
+ if (seenIds.has(plugin.id)) {
656
+ throw new Error(`plugin "${plugin.id}" registered twice`);
657
+ }
658
+ seenIds.add(plugin.id);
659
+ }
660
+ const agentTools = [];
661
+ for (const plugin of finalPlugins) {
662
+ for (const tool of plugin.agentTools ?? []) {
663
+ agentTools.push(tool);
664
+ }
665
+ }
666
+ const systemPromptAppend = finalPlugins.filter((p) => p.systemPrompt && p.systemPrompt.trim()).map((p) => p.systemPrompt.trim()).join("\n\n");
667
+ const piPackages = collectPiPackages(finalPlugins);
668
+ const provisioningContributions = finalPlugins.filter((p) => p.provisioning).map((p) => ({ id: p.id, provisioning: p.provisioning }));
669
+ const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
670
+ return {
671
+ registered: finalPlugins.map((p) => p.id),
672
+ systemPromptAppend,
673
+ piPackages,
674
+ agentTools,
675
+ provisioningContributions,
676
+ routeContributions
677
+ };
678
+ }
679
+
680
+ // src/plugins/dataCatalogPlugin/shared/constants.ts
681
+ var DATA_CATALOG_PLUGIN_ID = "data-catalog";
682
+ var DATA_CATALOG_DEFAULT_TOOL_NAME = "query_data_catalog";
683
+ var DATA_CATALOG_ROW_SURFACE_KIND = "data-catalog.open-row";
684
+
685
+ // src/plugins/dataCatalogPlugin/server/index.ts
686
+ function textResult(text, details) {
687
+ return { content: [{ type: "text", text }], details };
688
+ }
689
+ function errorResult(text) {
690
+ return { content: [{ type: "text", text }], isError: true };
691
+ }
692
+ function clampLimit(value, fallback, max) {
693
+ const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
694
+ if (!Number.isFinite(numeric)) return fallback;
695
+ return Math.max(1, Math.min(max, Math.floor(numeric)));
696
+ }
697
+ function normalizeLimitOptions(options) {
698
+ const rawMax = options.maxLimit ?? 50;
699
+ const maxLimit = typeof rawMax === "number" && Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax)) : 50;
700
+ const rawDefault = options.defaultLimit ?? 20;
701
+ const defaultLimit = typeof rawDefault === "number" && Number.isFinite(rawDefault) ? Math.max(1, Math.min(maxLimit, Math.floor(rawDefault))) : Math.min(20, maxLimit);
702
+ return { defaultLimit, maxLimit };
703
+ }
704
+ function formatBadge(row) {
705
+ const parts = [
706
+ row.leading?.code,
707
+ ...(row.trailing ?? []).map((badge) => badge.code),
708
+ row.meta
709
+ ].filter(Boolean);
710
+ return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
711
+ }
712
+ function formatDataCatalogSearchResult(query, result) {
713
+ if (result.items.length === 0) {
714
+ return `No ${query ? `results for "${query}"` : "catalog results"}.`;
715
+ }
716
+ const lines = result.items.map((row) => {
717
+ const subtitle = row.subtitle ? ` \u2014 ${row.subtitle}` : "";
718
+ return `${row.id}: ${row.title}${subtitle}${formatBadge(row)}`;
719
+ });
720
+ const total = Number.isFinite(result.total) ? result.total : result.items.length;
721
+ return `Found ${total} results (showing ${result.items.length}):
722
+
723
+ ${lines.join("\n")}`;
724
+ }
725
+ function createDataCatalogAgentTool(options) {
726
+ const name = options.name ?? DATA_CATALOG_DEFAULT_TOOL_NAME;
727
+ const label = options.label ?? "data catalog";
728
+ const { defaultLimit, maxLimit } = normalizeLimitOptions(options);
729
+ return {
730
+ name,
731
+ description: `Search the ${label}. Use this before opening data visualizations or asking for a specific dataset.`,
732
+ parameters: {
733
+ type: "object",
734
+ properties: {
735
+ query: {
736
+ type: "string",
737
+ description: "Search keywords for datasets, series, tables, or metrics."
738
+ },
739
+ limit: {
740
+ type: "number",
741
+ description: `Maximum number of results. Default ${defaultLimit}, max ${maxLimit}.`,
742
+ minimum: 1,
743
+ maximum: maxLimit
744
+ }
745
+ },
746
+ required: ["query"],
747
+ additionalProperties: false
748
+ },
749
+ async execute(params, ctx) {
750
+ const query = String(params.query ?? "").trim();
751
+ if (!query) return errorResult("query is required");
752
+ const limit = clampLimit(params.limit, defaultLimit, maxLimit);
753
+ try {
754
+ const result = await options.adapter.search({
755
+ query,
756
+ filters: {},
757
+ limit,
758
+ offset: 0,
759
+ signal: ctx.abortSignal
760
+ });
761
+ return textResult(formatDataCatalogSearchResult(query, result), result);
762
+ } catch (error) {
763
+ return errorResult(error instanceof Error ? error.message : String(error));
764
+ }
765
+ }
766
+ };
767
+ }
768
+ function createDataCatalogSkillPrompt(options = {}) {
769
+ const label = options.label ?? "data catalog";
770
+ const toolName = options.toolName ?? DATA_CATALOG_DEFAULT_TOOL_NAME;
771
+ const surfaceKind = options.surfaceKind ?? DATA_CATALOG_ROW_SURFACE_KIND;
772
+ const guidance = options.guidance?.trim();
773
+ return [
774
+ "## Data Catalog Plugin",
775
+ "",
776
+ `Use \`${toolName}\` to search the ${label} before referencing datasets, series, tables, or metrics.`,
777
+ `When you need to show a catalog row to the user, use the workspace UI bridge \`openSurface\` command with \`{ kind: '${surfaceKind}', target: row.id, meta: { catalogId, row } }\` so the client plugin resolver chooses the panel.`,
778
+ guidance ? "" : void 0,
779
+ guidance || void 0
780
+ ].filter((line) => line !== void 0).join("\n");
781
+ }
782
+ function createDataCatalogServerPlugin(options) {
783
+ const tool = createDataCatalogAgentTool(options);
784
+ return defineServerPlugin({
785
+ id: options.id ?? DATA_CATALOG_PLUGIN_ID,
786
+ label: options.label ?? "Data Catalog",
787
+ agentTools: [tool],
788
+ systemPrompt: createDataCatalogSkillPrompt({
789
+ label: options.label,
790
+ toolName: tool.name,
791
+ surfaceKind: options.surfaceKind,
792
+ guidance: options.guidance
793
+ })
794
+ });
795
+ }
796
+ export {
797
+ ServerPluginError,
798
+ bootstrapServer,
799
+ composeServerPlugins,
800
+ createDataCatalogAgentTool,
801
+ createDataCatalogServerPlugin,
802
+ createDataCatalogSkillPrompt,
803
+ createExecUiTool,
804
+ createGetUiStateTool,
805
+ createInMemoryBridge,
806
+ createWorkspaceUiTools,
807
+ defineServerPlugin,
808
+ formatDataCatalogSearchResult,
809
+ uiRoutes,
810
+ validateServerPlugin
811
+ };