@hachej/boring-ui-plugin-cli 0.1.35 → 0.1.37
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/bin.js +2 -2
- package/dist/{chunk-6FD653KO.js → chunk-3WN35EV3.js} +23 -0
- package/dist/{chunk-GHTVKPQG.js → chunk-LWVRDO4C.js} +6 -1
- package/dist/index.js +2 -2
- package/dist/plugin-sources.js +1 -1
- package/package.json +1 -1
- package/templates/agent-canonical.ts +45 -0
- package/templates/front-file-visualizer.tsx +113 -0
- package/templates/package-canonical.json +5 -1
package/dist/bin.js
CHANGED
|
@@ -119,6 +119,28 @@ function validatePiPackages(issues, value) {
|
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
+
function validatePiSlashCommands(issues, value) {
|
|
123
|
+
if (value === void 0) return;
|
|
124
|
+
if (!Array.isArray(value)) {
|
|
125
|
+
issues.push(issue("INVALID_FIELD", "pi.slashCommands", "pi.slashCommands must be an array when provided"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
value.forEach((entry, index) => {
|
|
129
|
+
const field = `pi.slashCommands[${index}]`;
|
|
130
|
+
if (!isRecord(entry)) {
|
|
131
|
+
issues.push(issue("INVALID_FIELD", field, `${field} must be an object with name and description`));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (typeof entry.name !== "string" || entry.name.length === 0) {
|
|
135
|
+
issues.push(issue("INVALID_FIELD", `${field}.name`, `${field}.name must be a non-empty string (no leading slash)`));
|
|
136
|
+
} else if (entry.name.startsWith("/")) {
|
|
137
|
+
issues.push(issue("INVALID_FIELD", `${field}.name`, `${field}.name must not start with "/" \u2014 omit the slash`));
|
|
138
|
+
}
|
|
139
|
+
if (typeof entry.description !== "string" || entry.description.length === 0) {
|
|
140
|
+
issues.push(issue("INVALID_FIELD", `${field}.description`, `${field}.description must be a non-empty string`));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
122
144
|
function validatePiField(issues, pi) {
|
|
123
145
|
if (pi === void 0) return void 0;
|
|
124
146
|
if (!isRecord(pi)) {
|
|
@@ -131,6 +153,7 @@ function validatePiField(issues, pi) {
|
|
|
131
153
|
if (pi.systemPrompt !== void 0 && typeof pi.systemPrompt !== "string") {
|
|
132
154
|
issues.push(issue("INVALID_FIELD", "pi.systemPrompt", "pi.systemPrompt must be a string when provided"));
|
|
133
155
|
}
|
|
156
|
+
validatePiSlashCommands(issues, pi.slashCommands);
|
|
134
157
|
return pi;
|
|
135
158
|
}
|
|
136
159
|
function validateBoringPluginManifest(raw) {
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
listPluginSources,
|
|
6
6
|
removePluginSource,
|
|
7
7
|
validateBoringPluginManifest
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-3WN35EV3.js";
|
|
9
9
|
|
|
10
10
|
// src/server/index.ts
|
|
11
11
|
import { join as join4, resolve as resolve4 } from "path";
|
|
@@ -161,6 +161,7 @@ function scaffoldPlugin(opts) {
|
|
|
161
161
|
}
|
|
162
162
|
const tplFront = join2(templatesDir, "front-canonical.tsx");
|
|
163
163
|
const tplPackage = join2(templatesDir, "package-canonical.json");
|
|
164
|
+
const tplAgent = join2(templatesDir, "agent-canonical.ts");
|
|
164
165
|
for (const tpl of [tplFront, tplPackage]) {
|
|
165
166
|
if (!existsSync2(tpl)) {
|
|
166
167
|
throw new Error(`canonical template missing: ${tpl}`);
|
|
@@ -181,6 +182,10 @@ function scaffoldPlugin(opts) {
|
|
|
181
182
|
`);
|
|
182
183
|
const frontSource = substitute(readFileSync2(tplFront, "utf8"), opts.name, label);
|
|
183
184
|
write("front/index.tsx", frontSource);
|
|
185
|
+
if (existsSync2(tplAgent)) {
|
|
186
|
+
const agentSource = substitute(readFileSync2(tplAgent, "utf8"), opts.name, label);
|
|
187
|
+
write("agent/index.ts", agentSource);
|
|
188
|
+
}
|
|
184
189
|
write(".gitignore", "# Machine-managed sidecars written by the boring-ui plugin runtime.\n.boring-signature.json\n");
|
|
185
190
|
return { pluginDir, filesCreated };
|
|
186
191
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
runPluginSelfTest,
|
|
9
9
|
scaffoldPlugin,
|
|
10
10
|
verifyPlugin
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-LWVRDO4C.js";
|
|
12
12
|
import {
|
|
13
13
|
formatPluginSourceList,
|
|
14
14
|
installPluginSource,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
readPluginSourceRecordsForRoots,
|
|
18
18
|
removePluginSource,
|
|
19
19
|
resolvePluginSourceScopePaths
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-3WN35EV3.js";
|
|
21
21
|
export {
|
|
22
22
|
createPlugin,
|
|
23
23
|
findHintForError,
|
package/dist/plugin-sources.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// CANONICAL agent/index.ts for a boring-ui runtime plugin.
|
|
2
|
+
// Registers a hot-reloadable Pi slash command for deterministic plugin activation.
|
|
3
|
+
//
|
|
4
|
+
// The command opens its panel through the in-process workspace UI bridge via
|
|
5
|
+
// `openPanel` from "@hachej/boring-workspace/plugin" — the SAME path the agent's
|
|
6
|
+
// own `exec_ui` tool uses. No BORING_UI_URL, no env vars, no HTTP self-call:
|
|
7
|
+
// the bridge is already connected to the browser.
|
|
8
|
+
|
|
9
|
+
import { NoWorkspaceUiBridgeError, notify, openPanel } from "@hachej/boring-workspace/plugin"
|
|
10
|
+
|
|
11
|
+
const PLUGIN_ID = "<kebab-name>"
|
|
12
|
+
const PANEL_ID = "<kebab-name>.panel"
|
|
13
|
+
const PANEL_TITLE = "<Label>"
|
|
14
|
+
const OPEN_COMMAND = "open-<kebab-name>"
|
|
15
|
+
|
|
16
|
+
export default function (pi: any) {
|
|
17
|
+
// User-facing deterministic slash command. Add pi.registerTool(...) below
|
|
18
|
+
// when this plugin also needs an LLM-callable tool.
|
|
19
|
+
pi.registerCommand(OPEN_COMMAND, {
|
|
20
|
+
description: `Open the ${PANEL_TITLE} panel`,
|
|
21
|
+
handler: async () => {
|
|
22
|
+
try {
|
|
23
|
+
await openPanel({
|
|
24
|
+
id: `${PLUGIN_ID}.slash-open`,
|
|
25
|
+
component: PANEL_ID,
|
|
26
|
+
params: { source: `/${OPEN_COMMAND}` },
|
|
27
|
+
})
|
|
28
|
+
// Surfaces as a workspace toast (unlike Pi's ctx.ui.notify, which is a
|
|
29
|
+
// terminal notification that is swallowed in server/headless mode).
|
|
30
|
+
await notify(`Opened ${PANEL_TITLE}.`, "info")
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof NoWorkspaceUiBridgeError) {
|
|
33
|
+
// Running outside a workspace agent (e.g. a bare Pi CLI). Nothing to
|
|
34
|
+
// open; rethrow so the caller logs a clear reason.
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
await notify(
|
|
38
|
+
`Could not open ${PANEL_TITLE}: ${error instanceof Error ? error.message : String(error)}`,
|
|
39
|
+
"error",
|
|
40
|
+
).catch(() => {})
|
|
41
|
+
throw error
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// CANONICAL file visualizer front/index.tsx for a boring-ui runtime plugin.
|
|
2
|
+
// Recipe contract: the imports, surfaceResolvers shape, file fetch route, and
|
|
3
|
+
// workspace-id header below are supported public API. Do not grep workspace
|
|
4
|
+
// internals for these names unless `boring-ui-plugin test` fails. Change
|
|
5
|
+
// FILE_EXT and parser/rendering for your file type.
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useMemo, useState } from "react"
|
|
8
|
+
import { definePlugin, WORKSPACE_OPEN_PATH_SURFACE_KIND, type PaneProps } from "@hachej/boring-workspace/plugin"
|
|
9
|
+
import { useApiBaseUrl, useWorkspaceRequestId } from "@hachej/boring-workspace"
|
|
10
|
+
|
|
11
|
+
const MAIN_PANEL_ID = "<kebab-name>.panel"
|
|
12
|
+
const FILE_EXT = ".csv"
|
|
13
|
+
|
|
14
|
+
interface FilePaneParams {
|
|
15
|
+
path?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseCsv(text: string): string[][] {
|
|
19
|
+
return text
|
|
20
|
+
.trim()
|
|
21
|
+
.split(/\r?\n/)
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((row) => row.split(",").map((cell) => cell.trim()))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function FilePane({ params }: PaneProps<FilePaneParams>) {
|
|
27
|
+
const path = params?.path ?? ""
|
|
28
|
+
const apiBaseUrl = useApiBaseUrl()
|
|
29
|
+
const workspaceId = useWorkspaceRequestId()
|
|
30
|
+
const [text, setText] = useState("")
|
|
31
|
+
const [error, setError] = useState<string | null>(null)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!path) return
|
|
35
|
+
let cancelled = false
|
|
36
|
+
const url = `${apiBaseUrl}/api/v1/files/raw?path=${encodeURIComponent(path)}`
|
|
37
|
+
fetch(url, {
|
|
38
|
+
credentials: "include",
|
|
39
|
+
headers: workspaceId ? { "x-boring-workspace-id": workspaceId } : {},
|
|
40
|
+
})
|
|
41
|
+
.then((response) => {
|
|
42
|
+
if (!response.ok) throw new Error(`Failed to load ${path}: ${response.status}`)
|
|
43
|
+
return response.text()
|
|
44
|
+
})
|
|
45
|
+
.then((body) => { if (!cancelled) setText(body) })
|
|
46
|
+
.catch((err) => { if (!cancelled) setError(err instanceof Error ? err.message : String(err)) })
|
|
47
|
+
return () => { cancelled = true }
|
|
48
|
+
}, [apiBaseUrl, path, workspaceId])
|
|
49
|
+
|
|
50
|
+
const rows = useMemo(() => parseCsv(text), [text])
|
|
51
|
+
const headers = rows[0] ?? []
|
|
52
|
+
const dataRows = rows.slice(1)
|
|
53
|
+
const numericValues = dataRows
|
|
54
|
+
.map((row) => Number(row[1] ?? row[0]))
|
|
55
|
+
.filter((value) => Number.isFinite(value))
|
|
56
|
+
const max = Math.max(1, ...numericValues)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex h-full min-h-0 min-w-0 flex-col bg-background text-foreground">
|
|
60
|
+
<div className="border-b border-border px-4 py-3">
|
|
61
|
+
<div className="text-sm font-semibold"><Label></div>
|
|
62
|
+
<div className="text-xs text-muted-foreground">{path || `Open a ${FILE_EXT} file`}</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="min-h-0 flex-1 overflow-auto p-4">
|
|
65
|
+
{error ? <div className="text-sm text-destructive">{error}</div> : null}
|
|
66
|
+
<table className="w-full border-collapse text-sm">
|
|
67
|
+
<thead>
|
|
68
|
+
<tr>{headers.map((header, index) => <th key={index} className="border border-border px-2 py-1 text-left">{header}</th>)}</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
{dataRows.map((row, rowIndex) => (
|
|
72
|
+
<tr key={rowIndex}>{row.map((cell, cellIndex) => <td key={cellIndex} className="border border-border px-2 py-1">{cell}</td>)}</tr>
|
|
73
|
+
))}
|
|
74
|
+
</tbody>
|
|
75
|
+
</table>
|
|
76
|
+
<svg className="mt-4 h-32 w-full overflow-visible" viewBox="0 0 320 120" role="img" aria-label="CSV value chart">
|
|
77
|
+
{numericValues.map((value, index) => {
|
|
78
|
+
const width = 280 / Math.max(1, numericValues.length)
|
|
79
|
+
const height = (value / max) * 100
|
|
80
|
+
return <rect key={index} x={10 + index * width} y={110 - height} width={Math.max(2, width - 2)} height={height} fill="currentColor" opacity="0.7" />
|
|
81
|
+
})}
|
|
82
|
+
</svg>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default definePlugin({
|
|
89
|
+
id: "<kebab-name>",
|
|
90
|
+
label: "<Label>",
|
|
91
|
+
panels: [
|
|
92
|
+
{ id: MAIN_PANEL_ID, label: "<Label>", component: FilePane },
|
|
93
|
+
],
|
|
94
|
+
commands: [
|
|
95
|
+
{ id: "<kebab-name>.open", title: "Open <Label>", panelId: MAIN_PANEL_ID },
|
|
96
|
+
],
|
|
97
|
+
surfaceResolvers: [
|
|
98
|
+
{
|
|
99
|
+
id: "<kebab-name>.open-file",
|
|
100
|
+
kind: WORKSPACE_OPEN_PATH_SURFACE_KIND,
|
|
101
|
+
resolve: (request) => {
|
|
102
|
+
if (request.kind !== WORKSPACE_OPEN_PATH_SURFACE_KIND) return null
|
|
103
|
+
if (!request.target.endsWith(FILE_EXT)) return null
|
|
104
|
+
return {
|
|
105
|
+
id: `<kebab-name>:${request.target}`,
|
|
106
|
+
component: MAIN_PANEL_ID,
|
|
107
|
+
title: request.target.split("/").pop() ?? "<Label>",
|
|
108
|
+
params: { path: request.target },
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
})
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"front": "front/index.tsx"
|
|
15
15
|
},
|
|
16
16
|
"pi": {
|
|
17
|
-
"systemPrompt": "<Label> plugin — describe what it does so the agent knows when to use it."
|
|
17
|
+
"systemPrompt": "<Label> plugin — describe what it does so the agent knows when to use it.",
|
|
18
|
+
"extensions": ["agent/index.ts"],
|
|
19
|
+
"slashCommands": [
|
|
20
|
+
{ "name": "open-<kebab-name>", "description": "Open the <Label> panel" }
|
|
21
|
+
]
|
|
18
22
|
}
|
|
19
23
|
}
|