@hachej/boring-ui-cli 0.1.40 → 0.1.42

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 (64) hide show
  1. package/README.md +74 -178
  2. package/dist/server/modeApps.js +75 -2
  3. package/dist/server/pluginDiscovery.js +5 -4
  4. package/dist/server/pluginFrontRuntime.js +88 -0
  5. package/package.json +7 -6
  6. package/public/assets/{DebugDrawer-BKnTv0ta.js → DebugDrawer-gDfs30E4.js} +1 -1
  7. package/public/assets/{_baseUniq-BLLh7JYg.js → _baseUniq-Cq5XC-pK.js} +1 -1
  8. package/public/assets/{arc-CCKoqe23.js → arc-BmAfBjRZ.js} +1 -1
  9. package/public/assets/{architectureDiagram-Q4EWVU46-BiezsVHo.js → architectureDiagram-Q4EWVU46-Eklp421i.js} +1 -1
  10. package/public/assets/{blockDiagram-DXYQGD6D-BiontjNM.js → blockDiagram-DXYQGD6D-DtGFygpY.js} +1 -1
  11. package/public/assets/{c4Diagram-AHTNJAMY-BBwk8oFU.js → c4Diagram-AHTNJAMY-BZswzGDM.js} +1 -1
  12. package/public/assets/channel-Dq1UFgB_.js +1 -0
  13. package/public/assets/{chunk-4BX2VUAB-DPKS56FY.js → chunk-4BX2VUAB-F_g9lHXM.js} +1 -1
  14. package/public/assets/{chunk-4TB4RGXK-BQsSw8RM.js → chunk-4TB4RGXK-BTm8fprd.js} +1 -1
  15. package/public/assets/{chunk-55IACEB6-Ci72MzDy.js → chunk-55IACEB6-BcF-RbgG.js} +1 -1
  16. package/public/assets/{chunk-EDXVE4YY-DEmniXcF.js → chunk-EDXVE4YY-jdXrQWwg.js} +1 -1
  17. package/public/assets/{chunk-FMBD7UC4-BlX3J66_.js → chunk-FMBD7UC4-Bg13Lq7M.js} +1 -1
  18. package/public/assets/{chunk-OYMX7WX6-CVgYvnDy.js → chunk-OYMX7WX6-BPLuWVtk.js} +1 -1
  19. package/public/assets/{chunk-QZHKN3VN-CByUXwie.js → chunk-QZHKN3VN-CKAy04iy.js} +1 -1
  20. package/public/assets/{chunk-YZCP3GAM-kVASw0Ps.js → chunk-YZCP3GAM-bw0yarJe.js} +1 -1
  21. package/public/assets/classDiagram-6PBFFD2Q-BJUw8mCg.js +1 -0
  22. package/public/assets/classDiagram-v2-HSJHXN6E-BJUw8mCg.js +1 -0
  23. package/public/assets/clone-S7Dw6doI.js +1 -0
  24. package/public/assets/{cose-bilkent-S5V4N54A-P1wJoSYn.js → cose-bilkent-S5V4N54A-BhFSor4L.js} +1 -1
  25. package/public/assets/{dagre-KV5264BT-COwIFIWR.js → dagre-KV5264BT-BP-JJ5-u.js} +1 -1
  26. package/public/assets/{diagram-5BDNPKRD-Cu1uZiY8.js → diagram-5BDNPKRD-rEEUrLN8.js} +1 -1
  27. package/public/assets/{diagram-G4DWMVQ6-LYnPYs25.js → diagram-G4DWMVQ6-kLIqIjZl.js} +1 -1
  28. package/public/assets/{diagram-MMDJMWI5-BHdCIIZQ.js → diagram-MMDJMWI5-Ba5AbEKB.js} +1 -1
  29. package/public/assets/{diagram-TYMM5635-DOP9v9nq.js → diagram-TYMM5635-evStAp2X.js} +1 -1
  30. package/public/assets/{erDiagram-SMLLAGMA-r1ECuX9K.js → erDiagram-SMLLAGMA-D-bE3gq8.js} +1 -1
  31. package/public/assets/{flowDiagram-DWJPFMVM-BGfCv6DR.js → flowDiagram-DWJPFMVM-CaJjL3ue.js} +1 -1
  32. package/public/assets/{ganttDiagram-T4ZO3ILL-BT1XjcZL.js → ganttDiagram-T4ZO3ILL-D5RchBme.js} +1 -1
  33. package/public/assets/{gitGraphDiagram-UUTBAWPF-CI1rp0X1.js → gitGraphDiagram-UUTBAWPF-Qf423d8K.js} +1 -1
  34. package/public/assets/{graph-DI_xMEn_.js → graph-DCHxrEzC.js} +1 -1
  35. package/public/assets/{highlighted-body-OFNGDK62-8n743sC6.js → highlighted-body-OFNGDK62-C5r0PLJ8.js} +1 -1
  36. package/public/assets/index-BMbY-d9g.css +1 -0
  37. package/public/assets/{index-BbHTuYde.js → index-CyW2LgWg.js} +375 -375
  38. package/public/assets/{infoDiagram-42DDH7IO-BpRHk6VQ.js → infoDiagram-42DDH7IO-D-904xJW.js} +1 -1
  39. package/public/assets/{ishikawaDiagram-UXIWVN3A-DKm4l4s3.js → ishikawaDiagram-UXIWVN3A-BRPxrqA5.js} +1 -1
  40. package/public/assets/{journeyDiagram-VCZTEJTY-BF6q66EE.js → journeyDiagram-VCZTEJTY-DEsDr2q-.js} +1 -1
  41. package/public/assets/{kanban-definition-6JOO6SKY-ghGywoOn.js → kanban-definition-6JOO6SKY-8wHPWuXt.js} +1 -1
  42. package/public/assets/{layout-rNIh0dh7.js → layout-ChfR3Hlu.js} +1 -1
  43. package/public/assets/{linear-uZeMuiY-.js → linear-CL3edB6-.js} +1 -1
  44. package/public/assets/{min-CUa0xbzO.js → min-CyD06bdU.js} +1 -1
  45. package/public/assets/{mindmap-definition-QFDTVHPH-CqAtG59w.js → mindmap-definition-QFDTVHPH-BQaZ0zVu.js} +1 -1
  46. package/public/assets/{pieDiagram-DEJITSTG-p9Q1gHs7.js → pieDiagram-DEJITSTG-DQSS1DuA.js} +1 -1
  47. package/public/assets/{quadrantDiagram-34T5L4WZ-CXVkk7JI.js → quadrantDiagram-34T5L4WZ-BpHur_Z8.js} +1 -1
  48. package/public/assets/{requirementDiagram-MS252O5E-Cjtu6Q56.js → requirementDiagram-MS252O5E-C8Op3GXH.js} +1 -1
  49. package/public/assets/{sankeyDiagram-XADWPNL6-BpnNV-v0.js → sankeyDiagram-XADWPNL6-C-L9CUps.js} +1 -1
  50. package/public/assets/{sequenceDiagram-FGHM5R23-Dvp42ano.js → sequenceDiagram-FGHM5R23-Po3qK1MI.js} +1 -1
  51. package/public/assets/{stateDiagram-FHFEXIEX-Cr5QWnxH.js → stateDiagram-FHFEXIEX-CJSrVG_f.js} +1 -1
  52. package/public/assets/stateDiagram-v2-QKLJ7IA2-CLnVqZGL.js +1 -0
  53. package/public/assets/{timeline-definition-GMOUNBTQ-zevXOd6g.js → timeline-definition-GMOUNBTQ-DtezkzAy.js} +1 -1
  54. package/public/assets/{vennDiagram-DHZGUBPP-6KiZ2GA5.js → vennDiagram-DHZGUBPP-BVQGCd3J.js} +1 -1
  55. package/public/assets/{wardley-RL74JXVD-DiCKkv9h.js → wardley-RL74JXVD-CjGArPjb.js} +1 -1
  56. package/public/assets/{wardleyDiagram-NUSXRM2D-BV7_buYN.js → wardleyDiagram-NUSXRM2D-4A2kwq7j.js} +1 -1
  57. package/public/assets/{xychartDiagram-5P7HB3ND-C-mSzQZo.js → xychartDiagram-5P7HB3ND-zHlCL4kA.js} +1 -1
  58. package/public/index.html +2 -2
  59. package/public/assets/channel-B1YUFW3e.js +0 -1
  60. package/public/assets/classDiagram-6PBFFD2Q-CcrldmCi.js +0 -1
  61. package/public/assets/classDiagram-v2-HSJHXN6E-CcrldmCi.js +0 -1
  62. package/public/assets/clone-BUiJZ_PH.js +0 -1
  63. package/public/assets/index-o6MvaI3V.css +0 -1
  64. package/public/assets/stateDiagram-v2-QKLJ7IA2-Fp_LJrX2.js +0 -1
package/README.md CHANGED
@@ -7,237 +7,133 @@
7
7
 
8
8
  </div>
9
9
 
10
- **Turn an agent into an app — in one command.** Start a full IDE-style workbench pointed at your current directory: chat, file tree, editor, command palette. No clone. No database. No config.
10
+ **Turn an agent into an app — in one command.** Start a full IDE-style workbench pointed at a folder: chat, file tree, editor, command palette, plugins. No clone, no database, no config.
11
11
 
12
12
  ```bash
13
13
  npx @hachej/boring-ui-cli
14
14
  ```
15
15
 
16
- ---
17
-
18
- ## TL;DR
19
-
20
- **The Problem**: You want a coding agent in a browser IDE — but you don't want to clone a repo, set up Postgres, configure auth, or deploy anything. You just want to talk to an AI about your code.
21
-
22
- **The Solution**: `npx @hachej/boring-ui-cli` starts a full workbench locally, using your current directory as the workspace. It opens a browser tab with chat, file explorer, and panels. Zero setup, zero config, zero deploy.
23
-
24
- ### Why Use @hachej/boring-ui-cli?
25
-
26
- | Feature | What It Does |
27
- |---------|--------------|
28
- | **Zero-config startup** | `npx @hachej/boring-ui-cli` — that's it. Opens your browser to a full agent workbench. |
29
- | **Simple auth** | Set `ANTHROPIC_API_KEY` in your environment. The agent runs with direct filesystem access to your cwd. |
30
- | **Full workspace** | Chat, file tree, editor panels, command palette — all running against your real directory. |
31
- | **No database** | Runs in-memory. State persists for the session. No external dependencies. |
32
- | **Customizable port + root** | `PORT=8080` and `BORING_AGENT_WORKSPACE_ROOT=/path` env vars for power users. |
16
+ The binary is `boring-ui`.
33
17
 
34
18
  ---
35
19
 
36
- ## Quick Example
20
+ ## Quick start
37
21
 
38
22
  ```bash
39
- # Navigate to any project
40
- cd /path/to/my-project
41
-
42
- # Start the CLI — opens browser at localhost:5200
23
+ # Open the current folder as a workspace (browser opens at localhost:5200)
43
24
  npx @hachej/boring-ui-cli
44
25
 
45
- # With a custom port
46
- PORT=8080 npx @hachej/boring-ui-cli
47
-
48
- # Point at a specific directory
49
- BORING_AGENT_WORKSPACE_ROOT=/path/to/project npx @hachej/boring-ui-cli
26
+ # Open a specific folder
27
+ npx @hachej/boring-ui-cli ~/projects/foo
50
28
 
51
- # With API key
52
- ANTHROPIC_API_KEY=sk-ant-... npx @hachej/boring-ui-cli
29
+ # Custom port / host
30
+ npx @hachej/boring-ui-cli --port 8080 --host 127.0.0.1
53
31
  ```
54
32
 
55
- Once the browser opens, you can:
56
- ```
57
- # In the chat box:
58
- "read my README and suggest improvements"
59
- "find all TypeScript files that import 'react'"
60
- "rewrite the test file to use vitest"
61
- ```
33
+ The CLI does not take an API key flag. On first run, if no LLM provider is
34
+ configured it prints a guide: in another terminal run `pi` (or
35
+ `npx @earendil-works/pi-coding-agent`) and use `/login` to add an API key or
36
+ sign in to a subscription (Claude Pro/Max, ChatGPT Plus, Copilot). Credentials
37
+ are saved at `~/.pi/agent/auth.json`; refresh the browser afterward.
62
38
 
63
39
  ---
64
40
 
65
- ## Installation
66
-
67
- No installation needed — use `npx`:
41
+ ## Commands
68
42
 
69
- ```bash
70
- npx @hachej/boring-ui-cli
71
43
  ```
72
-
73
- Or install globally for repeated use:
74
-
75
- ```bash
76
- # npm
77
- npm install -g @hachej/boring-ui-cli
78
-
79
- # pnpm
80
- pnpm add -g @hachej/boring-ui-cli
81
-
82
- # Then just run:
83
- boring-ui
44
+ boring-ui [folder] [options] Open a single folder as a workspace (folder mode)
45
+ boring-ui workspaces Start the multi-workspace hub (workspaces mode)
46
+ boring-ui workspaces add <folder> Register a folder as a saved workspace
47
+ boring-ui workspaces list List saved workspaces
48
+ boring-ui workspaces remove <id> Remove a saved workspace
49
+ boring-ui workspaces rename <id> <name> Rename a saved workspace
50
+ boring-ui plugin <subcommand> … Plugin authoring (delegates to boring-ui-plugin)
84
51
  ```
85
52
 
86
- ### From Source
53
+ `boring-ui plugin …` forwards to `@hachej/boring-ui-plugin-cli`; run
54
+ `boring-ui plugin` with no subcommand for its usage.
87
55
 
88
- ```bash
89
- git clone https://github.com/hachej/boring-ui.git
90
- cd boring-ui && pnpm install
91
- pnpm --filter @hachej/boring-ui-cli build
92
- npx ./packages/cli/dist/index.js
93
- ```
56
+ ### Options
94
57
 
95
- ### Plugins
58
+ | Flag | Default | Description |
59
+ |------|---------|-------------|
60
+ | `-p, --port <port>` | `5200` (or `$PORT`) | HTTP port |
61
+ | `--host <host>` | `0.0.0.0` (or `$HOST`) | Listen host |
62
+ | `-m, --mode <mode>` | `local` | `local` (no sandbox, full network) or `local-sandbox` (bwrap-isolated, no network, Linux only) |
63
+ | `-h, --help` | | Show help |
96
64
 
97
- Plugin authoring operations live in the dedicated plugin CLI:
65
+ ### Environment variables
98
66
 
99
- ```bash
100
- boring-ui-plugin create my-package-plugin --path plugins
101
- boring-ui-plugin scaffold my-runtime-plugin "$BORING_AGENT_WORKSPACE_ROOT"
102
- boring-ui-plugin verify my-runtime-plugin "$BORING_AGENT_WORKSPACE_ROOT"
103
- boring-ui-plugin test my-runtime-plugin
104
- ```
67
+ | Variable | Description |
68
+ |----------|-------------|
69
+ | `PORT`, `HOST` | Fallbacks for `--port` / `--host` |
70
+ | `BORING_MODE` | Fallback for `--mode` |
71
+ | `BORING_AGENT_WORKSPACE_ROOT` | Overrides the folder argument in folder mode |
72
+ | `BORING_UI_WORKSPACES_PATH` | Path to the workspaces registry (default `~/.boring-ui/workspaces.yaml`) |
73
+ | `BORING_USE_LOCAL_PACKAGES` | `1` to resolve the bundled plugin-cli runtime from the local monorepo checkout |
105
74
 
106
75
  ---
107
76
 
108
- ## Authentication
77
+ ## Installation
109
78
 
110
- The CLI talks to Anthropic Claude via the agent runtime. You need a valid API key:
79
+ No install needed `npx @hachej/boring-ui-cli`. Or install globally:
111
80
 
112
81
  ```bash
113
- ANTHROPIC_API_KEY=sk-ant-... npx @hachej/boring-ui-cli
82
+ npm install -g @hachej/boring-ui-cli # or: pnpm add -g @hachej/boring-ui-cli
83
+ boring-ui
114
84
  ```
115
85
 
116
- Only Anthropic Claude is wired in v1. The agent harness supports other providers via the `AgentHarness` interface, but only Anthropic is implemented.
117
-
118
- ---
119
-
120
- ## Options
121
-
122
- | Environment Variable | Default | Description |
123
- |---------------------|---------|-------------|
124
- | `ANTHROPIC_API_KEY` | Yes | Anthropic API key. The agent requires a valid key to function. |
125
- | `PORT` | `5200` | Port to run the server on |
126
- | `BORING_AGENT_WORKSPACE_ROOT` | `.` (cwd) | Root directory for the workspace. The agent sees this as its filesystem. |
127
- | `BORING_AGENT_MODE` | `direct` | `direct` (no sandbox) or `local` (bwrap sandbox, Linux only) |
128
- | `BORING_AGENT_DEFAULT_MODEL_ID` | `claude-sonnet-4-6` | Default model to use |
129
-
130
- ---
131
-
132
- ## Architecture
86
+ ### From source
133
87
 
88
+ ```bash
89
+ git clone https://github.com/hachej/boring-ui.git
90
+ cd boring-ui && pnpm install
91
+ pnpm --filter @hachej/boring-ui-cli build:full # builds front bundle + server
92
+ node packages/cli/dist/index.js
134
93
  ```
135
- npx @hachej/boring-ui-cli
136
- ├── Boot Fastify server (direct mode, in-memory)
137
- ├── Serve frontend SPA (Vite-built bundle)
138
- ├── Open browser → http://localhost:5200
139
- └── Workspace = your current directory (or $BORING_AGENT_WORKSPACE_ROOT)
140
- ```
141
-
142
- The CLI is the zero-config entry point to the full boring-ui stack. Under the hood it wires together:
143
-
144
- - `@hachej/boring-agent` — agent runtime, tools, chat UI
145
- - `@hachej/boring-workspace` — file tree, panels, command palette, plugins
146
- - `@hachej/boring-ui-kit` — shared UI primitives
147
-
148
- All running locally against your real filesystem with no database.
149
-
150
- ---
151
94
 
152
- ## How @hachej/boring-ui-cli Compares
153
-
154
- | Feature | @hachej/boring-ui-cli | Claude Code | Codex CLI | Cursor |
155
- |---------|------------------------|-------------|-----------|--------|
156
- | Browser UI | ✅ Full IDE with panels | ❌ Terminal only | ❌ Terminal only | ✅ Desktop app |
157
- | File tree | ✅ Side panel | ❌ | ❌ | ✅ |
158
- | Zero setup | ✅ `npx` anywhere | ⚠️ Install + login | ⚠️ Install + login | ❌ Desktop app download |
159
- | Panel system | ✅ Dockview splittable panels | ❌ | ❌ | ❌ |
160
- | Plugin extensibility | ✅ Panels, commands, catalogs | ❌ | ❌ | ⚠️ Extensions |
161
- | Local filesystem | ✅ Direct access | ✅ | ✅ | ✅ |
162
- | Database required | ❌ None | ❌ | ❌ | ❌ |
163
-
164
- **When to use @hachej/boring-ui-cli:**
165
- - You want a browser-based coding agent with file tree and panels
166
- - You don't want to install anything — just `npx`
167
- - You want plugin extensibility (custom panels, data catalogs, etc.)
168
-
169
- **When it might not fit:**
170
- - You prefer terminal-only agent workflows (use Claude Code or Codex CLI)
171
- - You need multi-user auth, workspaces, or a database (use `@hachej/boring-core`)
172
- - You want a full desktop IDE with LSP, debugging, and git (use Cursor or VS Code)
173
-
174
- ---
175
-
176
- ## Troubleshooting
177
-
178
- | Error | Cause | Fix |
179
- |-------|-------|-----|
180
- | `ANTHROPIC_API_KEY not set` | No API key | `export ANTHROPIC_API_KEY=sk-ant-...` before running |
181
- | `port already in use` | Port 5200 occupied | `PORT=5201 npx @hachej/boring-ui-cli` |
182
- | Browser doesn't open | `BROWSER=none` or no display | Manually navigate to `http://localhost:5200` |
183
- | Agent returns errors | Invalid API key | Verify your Anthropic API key is valid and has quota |
184
- | `workspace root not found` | `BORING_AGENT_WORKSPACE_ROOT` points to non-existent dir | Create the directory or unset the variable to use cwd |
95
+ `build:full` is required from source: the server refuses to start without a
96
+ built frontend under `public/`.
185
97
 
186
98
  ---
187
99
 
188
- ## Limitations
100
+ ## Two modes
189
101
 
190
- - **In-memory only**: No database, no persistent workspaces. State is lost when the CLI exits.
191
- - **Single workspace**: Points at one directory. No multi-workspace switching.
192
- - **No auth management**: No user accounts, invites, or role-based access.
193
- - **Direct mode only**: No bwrap sandbox by default (use `BORING_AGENT_MODE=local` on Linux with bubblewrap installed).
194
- - **Not for production**: This is a developer tool, not a deployment target. Use `@hachej/boring-core` for multi-user apps.
195
- - **Only Anthropic Claude**: No OpenAI, Google, or other model providers wired in v1.
102
+ - **Folder mode** (`boring-ui [folder]`) one folder, one workspace. The fast
103
+ editor-launcher path, like `code .`.
104
+ - **Workspaces mode** (`boring-ui workspaces`) a persistent local hub serving
105
+ multiple folder-backed workspaces, with a workspace switcher in the UI. The
106
+ registry is a user-local YAML file, not a database.
196
107
 
197
- ---
108
+ Both run a Fastify server that serves the prebuilt React/Vite SPA plus the agent
109
+ and workspace API routes against your real filesystem. There is no database.
198
110
 
199
- ## FAQ
111
+ ## Plugins
200
112
 
201
- **Q: Do I need to install anything first?**
202
- A: No. `npx` downloads and runs the package on first use. Subsequent runs use the cached version.
113
+ The CLI discovers plugins from Pi-shaped roots — `~/.pi/agent/extensions/*`
114
+ (global) and `<workspace>/.pi/extensions/*` (workspace-local) plus
115
+ CLI-bundled defaults (e.g. `@hachej/boring-ask-user`). Authoring is handled by
116
+ the bundled `boring-ui-plugin` CLI:
203
117
 
204
- **Q: What happens when I close the browser?**
205
- A: The server keeps running. Stop it with `Ctrl+C` in the terminal.
206
-
207
- **Q: Can I use this with OpenAI models?**
208
- A: Only Anthropic Claude is wired in v1. Additional providers may be supported in future versions.
209
-
210
- **Q: Is my code sent to the cloud?**
211
- A: Yes — the agent sends file contents and chat messages to the LLM provider (e.g. Anthropic). The filesystem operations run locally on your machine.
212
-
213
- **Q: How is this different from `npx @hachej/boring-agent`?**
214
- A: `@hachej/boring-ui-cli` ships the full workbench (file tree, editor, command palette, plugins). `@hachej/boring-agent` is just the agent + chat. The CLI is the batteries-included zero-config entry point.
118
+ ```bash
119
+ boring-ui-plugin create <name> --path plugins # npm-package plugin (build step)
120
+ boring-ui-plugin scaffold <name> # workspace runtime plugin (.pi/extensions, hot-reload)
121
+ boring-ui-plugin verify [name]
122
+ boring-ui-plugin test <name>
123
+ ```
215
124
 
216
- **Q: Can I extend the CLI with plugins?**
217
- A: Not directly in v1. The CLI uses the default agent + workspace configuration. For plugin extensibility, build a custom app using `@hachej/boring-workspace` + `@hachej/boring-core`.
125
+ See `@hachej/boring-ui-plugin-cli` for the full plugin authoring workflow.
218
126
 
219
127
  ---
220
128
 
221
- ## Building Something Bigger?
222
-
223
- `@hachej/boring-ui-cli` is the zero-config entry point. For a full app with:
224
- - Multi-user authentication
225
- - Persistent workspaces with Postgres
226
- - Email invites and password resets
227
- - Custom domain plugins
228
-
229
- See the [boring-ui monorepo](https://github.com/hachej/boring-ui) and its packages:
129
+ ## Documentation
230
130
 
231
- | Package | Purpose |
232
- |---------|---------|
233
- | `@hachej/boring-core` | Auth, DB, app factory, multi-user |
234
- | `@hachej/boring-workspace` | Plugin system, panels, layouts |
235
- | `@hachej/boring-agent` | Agent runtime, tools, chat UI |
236
- | `@hachej/boring-ui-kit` | Shared React UI primitives |
131
+ - [`docs/README.md`](./docs/README.md) architecture and key abstractions
132
+ - [`docs/plans/archive/`](./docs/plans/archive/) — historical design plans (not current docs)
237
133
 
238
134
  ---
239
135
 
240
- *About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
136
+ *About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings.
241
137
 
242
138
  ---
243
139
 
@@ -108,6 +108,7 @@ const RUNTIME_PLUGIN_TRUST_LABEL = "Trusted local runtime plugins";
108
108
  const RUNTIME_PLUGIN_TRUST_DESCRIPTION = "Loads plugin UI code from trusted local Pi extension roots through the CLI-owned runtime module host.";
109
109
  function createRuntimePluginDiagnosticsStore() {
110
110
  const byWorkspace = /* @__PURE__ */ new Map();
111
+ const frontErrorsByWorkspace = /* @__PURE__ */ new Map();
111
112
  function upsert(workspaceId, pluginId) {
112
113
  const workspace = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
113
114
  byWorkspace.set(workspaceId, workspace);
@@ -137,6 +138,10 @@ function createRuntimePluginDiagnosticsStore() {
137
138
  entry.lastErrorCode = void 0;
138
139
  entry.lastErrorMessage = void 0;
139
140
  entry.lastErrorStage = void 0;
141
+ const storedFrontError = frontErrorsByWorkspace.get(diagnostic.workspaceId)?.get(diagnostic.pluginId);
142
+ if (storedFrontError && diagnostic.revision !== void 0 && storedFrontError.revision < diagnostic.revision) {
143
+ frontErrorsByWorkspace.get(diagnostic.workspaceId)?.delete(diagnostic.pluginId);
144
+ }
140
145
  }
141
146
  if (diagnostic.stage === "cache") entry.lastRequestAt = now;
142
147
  if (diagnostic.stage === "transform" && diagnostic.outcome === "served") {
@@ -163,8 +168,22 @@ function createRuntimePluginDiagnosticsStore() {
163
168
  snapshot(workspaceId) {
164
169
  return [...byWorkspace.get(workspaceId)?.values() ?? []].map((entry) => ({ ...entry, recent: [...entry.recent] })).sort((a, b) => a.pluginId.localeCompare(b.pluginId));
165
170
  },
171
+ recordFrontError(workspaceId, error) {
172
+ const forWorkspace = frontErrorsByWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
173
+ frontErrorsByWorkspace.set(workspaceId, forWorkspace);
174
+ const existing = forWorkspace.get(error.pluginId);
175
+ if (existing && existing.revision > error.revision) return;
176
+ forWorkspace.set(error.pluginId, error);
177
+ },
178
+ clearFrontError(workspaceId, pluginId) {
179
+ frontErrorsByWorkspace.get(workspaceId)?.delete(pluginId);
180
+ },
181
+ frontErrors(workspaceId) {
182
+ return [...frontErrorsByWorkspace.get(workspaceId)?.values() ?? []].sort((a, b) => a.pluginId.localeCompare(b.pluginId));
183
+ },
166
184
  disposeWorkspace(workspaceId) {
167
185
  byWorkspace.delete(workspaceId);
186
+ frontErrorsByWorkspace.delete(workspaceId);
168
187
  }
169
188
  };
170
189
  }
@@ -204,11 +223,33 @@ function buildRuntimePluginDiagnosticsResponse(args) {
204
223
  host: hostEntry
205
224
  });
206
225
  }
226
+ for (const frontError of args.frontErrors ?? []) {
227
+ const current = byPlugin.get(frontError.pluginId) ?? { id: frontError.pluginId };
228
+ byPlugin.set(frontError.pluginId, {
229
+ ...current,
230
+ frontError
231
+ });
232
+ }
207
233
  return {
208
234
  workspaceId: args.workspaceId,
209
235
  plugins: [...byPlugin.values()].sort((a, b) => a.id.localeCompare(b.id))
210
236
  };
211
237
  }
238
+ function parseFrontErrorReport(pluginId, body) {
239
+ if (typeof body !== "object" || body === null) return null;
240
+ const record = body;
241
+ const message = typeof record.message === "string" ? record.message : "";
242
+ if (!pluginId || !message) return null;
243
+ const revisionRaw = record.revision;
244
+ const revision = typeof revisionRaw === "number" && Number.isFinite(revisionRaw) ? revisionRaw : 0;
245
+ return {
246
+ pluginId,
247
+ revision,
248
+ message,
249
+ ...typeof record.url === "string" ? { url: record.url } : {},
250
+ reportedAt: Date.now()
251
+ };
252
+ }
212
253
  async function createFolderModeApp(opts) {
213
254
  const workspaceRoot = resolve(opts.workspaceRoot);
214
255
  const projectName = opts.projectName ?? (basename(workspaceRoot) || "workspace");
@@ -234,6 +275,10 @@ async function createFolderModeApp(opts) {
234
275
  logger: false,
235
276
  provisionWorkspace: false,
236
277
  runtimeProvisioning,
278
+ // The standalone CLI runs on the user's own machine, so ambient skill
279
+ // discovery (workspace + user-global ~/.pi skills) is on. The library
280
+ // default is off (withPiHarnessDefaults) to keep hosted agents isolated.
281
+ pi: { noSkills: false },
237
282
  // CLI-bundled internal plugins, resolved to absolute package dirs. This
238
283
  // drives the server-side install array (boot-time routes/agentTools);
239
284
  // additionalBoringPluginDirs only feeds the asset-manager scan.
@@ -257,9 +302,17 @@ async function createFolderModeApp(opts) {
257
302
  workspaceId: FOLDER_RUNTIME_PLUGIN_WORKSPACE_ID,
258
303
  loaded: manager?.inspectLoaded() ?? [],
259
304
  errors: manager?.getErrors() ?? [],
260
- host: diagnosticsStore.snapshot(FOLDER_RUNTIME_PLUGIN_WORKSPACE_ID)
305
+ host: diagnosticsStore.snapshot(FOLDER_RUNTIME_PLUGIN_WORKSPACE_ID),
306
+ frontErrors: diagnosticsStore.frontErrors(FOLDER_RUNTIME_PLUGIN_WORKSPACE_ID)
261
307
  });
262
308
  });
309
+ app.post("/api/v1/agent-plugins/:id/front-error", async (request, reply) => {
310
+ const { id } = request.params;
311
+ const report = parseFrontErrorReport(id, request.body);
312
+ if (!report) return reply.code(400).send({ error: "invalid_front_error_report" });
313
+ diagnosticsStore.recordFrontError(FOLDER_RUNTIME_PLUGIN_WORKSPACE_ID, report);
314
+ return reply.code(204).send();
315
+ });
263
316
  app.get("/api/v1/workspace/meta", async () => ({
264
317
  workspaceId: "default",
265
318
  workspaceRoot,
@@ -497,6 +550,14 @@ async function createWorkspacesModeApp(opts) {
497
550
  source: "plugin-preflight",
498
551
  message: `${error.code}: ${error.message} (${error.pluginDir})`,
499
552
  ...error.pluginId ? { pluginId: error.pluginId } : {}
553
+ })),
554
+ // Browser-reported front import failures: the server scan/transform is
555
+ // green, so without these a plugin that never renders looks healthy to
556
+ // the agent. The plugin_diagnostics tool consumes this array.
557
+ ...diagnosticsStore.frontErrors(workspace.id).map((error) => ({
558
+ source: "plugin-front",
559
+ message: error.message,
560
+ pluginId: error.pluginId
500
561
  }))
501
562
  ];
502
563
  },
@@ -504,6 +565,9 @@ async function createWorkspacesModeApp(opts) {
504
565
  const workspace = await requireWorkspace(workspaceId);
505
566
  await getLoadedPluginRuntime(workspace);
506
567
  return {
568
+ // Same policy as folder mode: the local hub runs on the user's own
569
+ // machine, so ambient skill discovery is on (library default is off).
570
+ noSkills: false,
507
571
  additionalSkillPaths: [join(workspaceRoot, ".agents", "skills")],
508
572
  packages: [],
509
573
  extensionPaths: [],
@@ -527,9 +591,18 @@ async function createWorkspacesModeApp(opts) {
527
591
  workspaceId: workspace.id,
528
592
  loaded: runtime.manager.inspectLoaded(),
529
593
  errors: runtime.manager.getErrors(),
530
- host: diagnosticsStore.snapshot(workspace.id)
594
+ host: diagnosticsStore.snapshot(workspace.id),
595
+ frontErrors: diagnosticsStore.frontErrors(workspace.id)
531
596
  });
532
597
  });
598
+ app.post("/api/v1/agent-plugins/:id/front-error", async (request, reply) => {
599
+ const workspace = await workspaceFromRequest(request);
600
+ const { id } = request.params;
601
+ const report = parseFrontErrorReport(id, request.body);
602
+ if (!report) return reply.code(400).send({ error: "invalid_front_error_report" });
603
+ diagnosticsStore.recordFrontError(workspace.id, report);
604
+ return reply.code(204).send();
605
+ });
533
606
  app.get("/api/v1/agent-plugins", async (request) => {
534
607
  const workspace = await workspaceFromRequest(request);
535
608
  const runtime = await getLoadedPluginRuntime(workspace);
@@ -10,8 +10,8 @@ import {
10
10
  resolveDefaultWorkspacePluginPackagePaths
11
11
  } from "@hachej/boring-workspace/app/server";
12
12
  import {
13
- readPluginSourceRecords,
14
- resolvePluginSourceScopePaths
13
+ resolvePluginSourceScopePaths,
14
+ resolveRegisteredPluginSourceDirs
15
15
  } from "@hachej/boring-ui-plugin-cli";
16
16
  function resolveBoringUiCliPackageRoot() {
17
17
  const here = dirname(fileURLToPath(import.meta.url));
@@ -41,8 +41,8 @@ function resolveCliBoringPluginDirs(workspaceRoot, options = {}) {
41
41
  const globalScope = resolvePluginSourceScopePaths("global", { globalRoot: globalAgentRoot });
42
42
  const localScope = resolvePluginSourceScopePaths("local", { workspaceRoot: resolvedWorkspaceRoot });
43
43
  const packageSources = [
44
- ...readPluginSourceRecords(globalScope),
45
- ...readPluginSourceRecords(localScope)
44
+ ...resolveRegisteredPluginSourceDirs(globalScope).map((dir) => ({ ...dir, scope: "global" })),
45
+ ...resolveRegisteredPluginSourceDirs(localScope).map((dir) => ({ ...dir, scope: "local" }))
46
46
  ];
47
47
  const includeDefaultPackages = options.includeDefaultPackages ?? true;
48
48
  const roots = [
@@ -56,6 +56,7 @@ function resolveCliBoringPluginDirs(workspaceRoot, options = {}) {
56
56
  ...packageSources.map((record) => ({
57
57
  rootDir: record.rootDir,
58
58
  kind: "external",
59
+ registered: true,
59
60
  ...record.scope === "local" ? { workspaceId: resolvedWorkspaceRoot } : {}
60
61
  }))
61
62
  ];
@@ -8,6 +8,7 @@ import react from "@vitejs/plugin-react";
8
8
  import { ErrorCode } from "@hachej/boring-agent/shared";
9
9
  import ts from "typescript";
10
10
  import { createServer } from "vite";
11
+ import { init as initCjsLexer, parse as parseCjsExports } from "cjs-module-lexer";
11
12
  const PLUGIN_FRONT_RUNTIME_BASE_PATH = "/api/v1/agent-plugins/runtime";
12
13
  const HOST_VIRTUAL_SINGLETON_MODULES = [
13
14
  "react",
@@ -817,6 +818,86 @@ function runtimeAssetContentType(path) {
817
818
  return "application/octet-stream";
818
819
  }
819
820
  }
821
+ const CJS_REQUIRE_PLACEHOLDER = "__boringCjsRequire";
822
+ let cjsLexerReady;
823
+ function ensureCjsLexer() {
824
+ if (!cjsLexerReady) cjsLexerReady = initCjsLexer();
825
+ return cjsLexerReady;
826
+ }
827
+ function hasEsmSyntax(sourceText) {
828
+ const sourceFile = ts.createSourceFile("cjs-probe.js", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
829
+ let esm = false;
830
+ const visit = (node) => {
831
+ if (esm) return;
832
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isExportAssignment(node) || ts.isFunctionDeclaration(node) && node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) || (ts.isVariableStatement(node) || ts.isClassDeclaration(node)) && node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
833
+ esm = true;
834
+ return;
835
+ }
836
+ ts.forEachChild(node, visit);
837
+ };
838
+ visit(sourceFile);
839
+ return esm;
840
+ }
841
+ function looksLikeCommonJs(sourceText) {
842
+ if (hasEsmSyntax(sourceText)) return false;
843
+ return /\b(?:module\.exports|exports\.|exports\[)/.test(sourceText) || /\brequire\s*\(/.test(sourceText);
844
+ }
845
+ function collectRequireSpecifiers(sourceFile) {
846
+ const specifiers = /* @__PURE__ */ new Set();
847
+ const visit = (node) => {
848
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0])) {
849
+ specifiers.add(node.arguments[0].text);
850
+ }
851
+ ts.forEachChild(node, visit);
852
+ };
853
+ visit(sourceFile);
854
+ return [...specifiers];
855
+ }
856
+ async function cjsDependencyToEsm(sourceText, modulePath) {
857
+ await ensureCjsLexer();
858
+ const sourceFile = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
859
+ const requireSpecifiers = collectRequireSpecifiers(sourceFile);
860
+ const importLines = [];
861
+ const requireMapEntries = [];
862
+ requireSpecifiers.forEach((specifier, index) => {
863
+ const binding = `${CJS_REQUIRE_PLACEHOLDER}_${index}`;
864
+ importLines.push(`import * as ${binding} from ${JSON.stringify(specifier)};`);
865
+ requireMapEntries.push(` [${JSON.stringify(specifier)}]: ${binding},`);
866
+ });
867
+ let exportNames = [];
868
+ try {
869
+ const parsed = parseCjsExports(sourceText, modulePath);
870
+ exportNames = [...parsed.exports];
871
+ } catch {
872
+ exportNames = [];
873
+ }
874
+ const safeExportNames = exportNames.filter(
875
+ (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && name !== "default" && name !== "__esModule"
876
+ );
877
+ const namedExportLines = safeExportNames.map(
878
+ (name) => `export const ${name} = ${JSON.stringify(name)} in __boringCjsExports ? __boringCjsExports[${JSON.stringify(name)}] : undefined;`
879
+ );
880
+ return [
881
+ ...importLines,
882
+ `const ${CJS_REQUIRE_PLACEHOLDER}_modules = {`,
883
+ ...requireMapEntries,
884
+ "};",
885
+ `function ${CJS_REQUIRE_PLACEHOLDER}(id) {`,
886
+ ` const ns = ${CJS_REQUIRE_PLACEHOLDER}_modules[id];`,
887
+ ` if (!ns) throw new Error("runtime plugin CommonJS require could not be resolved: " + id);`,
888
+ " const def = ns.default;",
889
+ " if (def !== undefined && (ns.__esModule || typeof def !== 'object' || Object.keys(ns).length === 1)) return def;",
890
+ " return ns.default !== undefined ? ns.default : ns;",
891
+ "}",
892
+ "const __boringCjsModule = { exports: {} };",
893
+ `(function (module, exports, require) {`,
894
+ sourceText,
895
+ `})(__boringCjsModule, __boringCjsModule.exports, ${CJS_REQUIRE_PLACEHOLDER});`,
896
+ "const __boringCjsExports = __boringCjsModule.exports;",
897
+ "export default __boringCjsExports;",
898
+ ...namedExportLines
899
+ ].join("\n");
900
+ }
820
901
  function runtimeAssetModuleCode(path, bytes) {
821
902
  const dataUrl = `data:${runtimeAssetContentType(path)};base64,${Buffer.from(bytes).toString("base64")}`;
822
903
  return `export default ${JSON.stringify(dataUrl)};`;
@@ -1093,6 +1174,13 @@ async function createPluginFrontRuntimeHost(options = {}) {
1093
1174
  return runtimeAssetModuleCode(resolvedPath2, await readFile(resolvedPath2));
1094
1175
  }
1095
1176
  const sourceText2 = await readFile(resolvedPath2, "utf8");
1177
+ const extension = extname(resolvedPath2).toLowerCase();
1178
+ const isCommonJs = extension === ".cjs" || extension !== ".mjs" && looksLikeCommonJs(sourceText2);
1179
+ if (isCommonJs) {
1180
+ const interop = await cjsDependencyToEsm(sourceText2, resolvedPath2);
1181
+ validateSourceImports(interop, resolvedPath2, basePath);
1182
+ return interop;
1183
+ }
1096
1184
  validateSourceImports(sourceText2, resolvedPath2, basePath);
1097
1185
  return sourceText2;
1098
1186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-ui-cli",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "description": "Turn an agent into an app",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,15 +15,16 @@
15
15
  "@fastify/compress": "^8.3.1",
16
16
  "@fastify/static": "^9.1.1",
17
17
  "@vitejs/plugin-react": "^4.0.0",
18
+ "cjs-module-lexer": "^2.0.0",
18
19
  "fastify": "^5.4.0",
19
20
  "lucide-react": "^1.8.0",
20
21
  "typescript": "^5.8.3",
21
22
  "vite": "^6.0.0",
22
- "@hachej/boring-agent": "0.1.40",
23
- "@hachej/boring-ui-kit": "0.1.40",
24
- "@hachej/boring-ask-user": "0.1.40",
25
- "@hachej/boring-ui-plugin-cli": "0.1.40",
26
- "@hachej/boring-workspace": "0.1.40"
23
+ "@hachej/boring-agent": "0.1.42",
24
+ "@hachej/boring-ask-user": "0.1.42",
25
+ "@hachej/boring-workspace": "0.1.42",
26
+ "@hachej/boring-ui-kit": "0.1.42",
27
+ "@hachej/boring-ui-plugin-cli": "0.1.42"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@tailwindcss/vite": "^4.0.0",