glimmervoid 0.0.0-stage → 0.30.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.
- package/LICENSE +48 -0
- package/README.md +195 -2
- package/dist/bin/glimmervoid.js +275 -0
- package/dist/chunks/adapters-Dho1VSY5.js +2 -0
- package/dist/chunks/adapters-zPDx1Xox.js +575 -0
- package/dist/chunks/agent-api-cli-D0MmtEdz.js +71 -0
- package/dist/chunks/agent-api-core-Dz0r21y9.js +103 -0
- package/dist/chunks/agent-setup-cli-B5Atxr3F.js +139 -0
- package/dist/chunks/ascii-figure-core-pDAwtlKO.js +45 -0
- package/dist/chunks/child-process-safe-DRGsp0TE.js +69 -0
- package/dist/chunks/child-process-safe-DvQjXsgz.js +2 -0
- package/dist/chunks/config-path-core-DFt-TemS.js +38 -0
- package/dist/chunks/config-store-CC8T3okC.js +2 -0
- package/dist/chunks/config-store-mm7MgzoL.js +1412 -0
- package/dist/chunks/contracts-DGkKNh9W.js +3020 -0
- package/dist/chunks/grok-DuU8xVLZ.js +436 -0
- package/dist/chunks/hook-command-core-DclEoE9e.js +14 -0
- package/dist/chunks/hook-relay-core-BHfYFnYV.js +88 -0
- package/dist/chunks/hook-tools-pSJia7ks.js +74 -0
- package/dist/chunks/ingest-core-DWI1Mbu4.js +342 -0
- package/dist/chunks/ingest-editor-core-BuI1gMF_.js +445 -0
- package/dist/chunks/loopback-post-C7C22eYi.js +79 -0
- package/dist/chunks/main-CuIZQPhZ.js +43559 -0
- package/dist/chunks/node-pty-preflight-CK1knGIR.js +162 -0
- package/dist/chunks/pair-cli-w-gCAF-1.js +110 -0
- package/dist/chunks/path-doctor-C23VAqeu.js +63 -0
- package/dist/chunks/refocus-core-BKQKllq7.js +38 -0
- package/dist/chunks/relay-stdin-CpHvLu6M.js +20 -0
- package/dist/chunks/remote-config-C2ytQwrs.js +525 -0
- package/dist/chunks/rtk-hook-core-Bl6gYHPN.js +133 -0
- package/dist/chunks/rtk-resolver-CLo-gZzw.js +21 -0
- package/dist/chunks/rtk-resolver-CMQAidlI.js +2 -0
- package/dist/chunks/runtime-paths-Ci_ksSkj.js +75 -0
- package/dist/chunks/runtime-paths-JLrZ3wyi.js +2 -0
- package/dist/chunks/sandbox-deps-Dd5-kZnf.js +67 -0
- package/dist/chunks/session-Cez8aIWg.js +164 -0
- package/dist/chunks/spawn-command-Cc40FiZI.js +134 -0
- package/dist/chunks/visions-cli-BXQFIBCX.js +98 -0
- package/dist/chunks/visions-setup-CvVnOe1r.js +1753 -0
- package/dist/client/assets/favicon-CKygeLwW.svg +7 -0
- package/dist/client/assets/index-B1MFijjb.css +1 -0
- package/dist/client/assets/index-CU1iPPdn.js +249 -0
- package/dist/client/index.html +108 -0
- package/dist/package.json +3 -0
- package/dist/scripts/postinstall-path-check.js +36 -0
- package/dist/server/index.js +92 -0
- package/dist/session/command-hook-relay.js +16 -0
- package/dist/session/hook-relay.js +24 -0
- package/dist/session/hook-tool-relay.js +107 -0
- package/dist/session/statusline-relay.js +82 -0
- package/dist/session/visions-relay.js +627 -0
- package/dist/tools/vscode-visions/extension.js +289 -0
- package/dist/tools/vscode-visions/lsp-convert.js +80 -0
- package/dist/tools/vscode-visions/package.json +32 -0
- package/dist/tools/vscode-visions/visions-lsp-core.js +112 -0
- package/package.json +96 -4
- package/scripts/postinstall.mjs +4 -0
- package/scripts/prepare-build.js +38 -0
- package/scripts/recover-handoff.mjs +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John C. Waters
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Third-party notices
|
|
24
|
+
|
|
25
|
+
mdxcn (https://github.com/keshav-exe/mdxcn)
|
|
26
|
+
Applies to server/core/ascii-figure-core.ts
|
|
27
|
+
|
|
28
|
+
MIT License
|
|
29
|
+
|
|
30
|
+
Copyright (c) 2026 Keshav Bagaade
|
|
31
|
+
|
|
32
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
33
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
34
|
+
in the Software without restriction, including without limitation the rights
|
|
35
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
36
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
37
|
+
furnished to do so, subject to the following conditions:
|
|
38
|
+
|
|
39
|
+
The above copyright notice and this permission notice shall be included in all
|
|
40
|
+
copies or substantial portions of the Software.
|
|
41
|
+
|
|
42
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
43
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
44
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
45
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
46
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
47
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
48
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,196 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Glimmervoid
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/johncwaters/glimmervoid/actions/workflows/test.yml)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](https://github.com/johncwaters/glimmervoid)
|
|
7
|
+
|
|
8
|
+
**Mission control for your coding agents. Run dozens of Claude Code, Codex and Grok sessions at once. See every one. Miss nothing.**
|
|
9
|
+
|
|
10
|
+
Running more than a couple of coding agents at once turns into alt-tabbing between terminal windows, missing the exact moment one finishes or silently blocks on a prompt, and merging work you never watched happen. Glimmervoid is one browser dashboard with live terminal output for every session, exact status instead of a guess, a notification the moment an agent needs you, and per-agent git worktrees you can review and merge without leaving the page.
|
|
11
|
+
|
|
12
|
+
Glimmervoid is an agent orchestrator, sometimes called an agent manager or an agentic development environment. It is not an agent harness: Claude Code, Codex and Grok are the harnesses, the loop that turns a model into an agent. Glimmervoid runs those harnesses unmodified, as real processes in real terminals on your own machine, and supervises all of them from one screen. No cloud service, no account.
|
|
13
|
+
|
|
14
|
+
Glimmervoid is developed inside Glimmervoid.
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
Windows and macOS:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx glimmervoid@latest
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Linux (node-pty ships no Linux prebuilds, so it compiles at install time, and npm runs that build only when allowed):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
sudo apt install build-essential python3 git
|
|
30
|
+
npx --allow-scripts=node-pty glimmervoid@latest
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
To install it globally instead, run `npm install -g glimmervoid` (add `--allow-scripts=node-pty` on Linux); if that fails with `EACCES`, see [docs/troubleshooting.md](docs/troubleshooting.md#eacces-on-a-global-install) and never use `sudo npm`.
|
|
34
|
+
|
|
35
|
+
Open http://localhost:3000. A fresh install knows no projects, so **+ Session** shows "No projects found" until you say where your repositories live: open **Settings**, then **Repositories**, and add the folder that holds your git checkouts under **Repository roots** (each subfolder becomes a project). For a one-off, **+ Session** then **Advanced options** takes a name and a path directly. That is the whole setup; everything else is optional.
|
|
36
|
+
|
|
37
|
+
To check notifications, allow them when the dashboard asks (or turn on **Desktop notifications** under **Settings**, **Appearance and alerts**) and keep a dashboard tab open. A notification only appears while that tab is not focused, since a focused tab already shows the change. With no dashboard tab open anywhere, only Telegram reaches you (the `telegram` keys in [docs/configuration.md](docs/configuration.md)).
|
|
38
|
+
|
|
39
|
+
The Linux flag matters on npm 12, which blocks dependency install scripts by default; npm 10 and 11 run them (npm 11 prints a notice), so there it is harmless. Keep `@latest` so npx checks for a new release instead of rerunning the version it cached first. If the native module still fails to load, the server refuses to start and prints the repair command; `glimmervoid doctor` runs the same check. See [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
40
|
+
|
|
41
|
+
### From source
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/johncwaters/glimmervoid.git
|
|
45
|
+
cd glimmervoid
|
|
46
|
+
npm ci
|
|
47
|
+
npm run build
|
|
48
|
+
npm start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Update a clone with `git pull --ff-only && npm ci && npm run build`, then restart the server. The dashboard's update check prints the right command for whichever way you installed.
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
- **Node.js >= 22.18.0** (the `engines` floor). npm 12 itself needs Node 22.22.2 or newer; distro-packaged Node is usually older than either, so use nodesource, nvm or the official installer.
|
|
56
|
+
- **Windows 11, Linux or macOS.** CI runs on Linux; Windows and macOS are tested by hand.
|
|
57
|
+
- **Claude Code CLI** on PATH, or another supported agent below.
|
|
58
|
+
- **git** on PATH, for per-session worktrees.
|
|
59
|
+
- **Linux only:** `build-essential` and `python3` for node-pty.
|
|
60
|
+
- **Linux only:** `bubblewrap` (`bwrap`) and `socat` for Claude Code's sandbox, which team review, Keep mergeable, workflow sessions and benchmark runs use (`sudo apt install bubblewrap socat`). `glimmervoid doctor` reports both.
|
|
61
|
+
|
|
62
|
+
## Supported agents
|
|
63
|
+
|
|
64
|
+
- **Claude Code** (`claude`), the default and the most complete: hooks, background agent gating, auto-resume.
|
|
65
|
+
- **Codex CLI** (`codex`), with hook-based status. Codex has no notification event, so a question it asks in prose looks like a finished turn.
|
|
66
|
+
- **Grok Build** (`grok`). Run `glimmervoid agent setup grok` once to install its hook relay.
|
|
67
|
+
- **Any other terminal agent**, declared under `customAgents` in `config.json` with an `id`, `label`, `command` and optional `args`, `idleTitle` and `busyTitle`. Custom agents get status from the terminal title only.
|
|
68
|
+
|
|
69
|
+
The Add Session dialog only offers agents whose command resolves on PATH, and `glimmervoid doctor` lists what it found. A project can default to an agent with `projects[].agent`.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
glimmervoid # start the server on port 3000
|
|
75
|
+
glimmervoid --port 3001 # start on another port
|
|
76
|
+
glimmervoid doctor # diagnose the install, PATH, agents and native module
|
|
77
|
+
glimmervoid pair --name phone # mint a single-use pairing link for a remote device
|
|
78
|
+
glimmervoid --version
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`glimmervoid --help` lists every command.
|
|
82
|
+
|
|
83
|
+
## Features
|
|
84
|
+
|
|
85
|
+
- Focus view: a roster rail of every session, the selected session's live terminal in the center, and a worktree review sidebar
|
|
86
|
+
- Per-session git worktree isolation: review and merge each agent's committed work from the dashboard while it keeps running
|
|
87
|
+
- Real-time terminal output via xterm.js with WebGL rendering
|
|
88
|
+
- Structural status detection: hooks as the authoritative signal, an OSC-0 title fallback, never screen scraping (see below)
|
|
89
|
+
- Background sub-agent completion gate: a session with live background agents or tasks stays out of Complete until they finish
|
|
90
|
+
- Browser notifications when a session needs input, finishes or fails
|
|
91
|
+
- Custom alert sounds: drop your own `.ogg`, `.mp3`, `.wav`, `.m4a` or `.webm` files into `~/.glimmervoid/sounds/` (or `$GLIMMERVOID_HOME/sounds/`) and pick one in Settings
|
|
92
|
+
- Phone layout as a first-class second layout: board, terminal and review screens, attention-first ordering, and a terminal that resizes around the soft keyboard instead of hiding behind it
|
|
93
|
+
- Remote access (opt-in): a separate listener with single-use device pairing and cookie auth
|
|
94
|
+
- Telegram notifications (opt-in), sent only when no dashboard tab is open anywhere
|
|
95
|
+
- Plan review: read, edit and approve a Claude Code plan from the dashboard or phone
|
|
96
|
+
- Usage tracking: token use and estimated cost from local Claude Code, Codex and Grok transcripts, with optional budgets
|
|
97
|
+
- Radar (opt-in): polls PostHog error tracking, pings Telegram when an issue spikes, regresses or first appears, and sends an agent to investigate; optional auto-fix opens a pull request the agent itself can never push or merge
|
|
98
|
+
- Team PR review (opt-in): drafts a review of each open pull request from a GitHub team in a sandboxed agent, optionally with your own Claude Code review skill (`teamReview.skill`); nothing posts until you choose Approve or Comment
|
|
99
|
+
- Auto-resume: sessions that were live when Glimmervoid stopped come back with their conversation resumed
|
|
100
|
+
- Hot-reloaded configuration and a Settings view for most of it
|
|
101
|
+
|
|
102
|
+
## Why the status detection is hard (and how Glimmervoid does it)
|
|
103
|
+
|
|
104
|
+
The obvious way to know if a Claude Code session finished, is waiting on you, or is still working is to scrape the terminal: watch for a prompt string, a spinner glyph, some text pattern. It breaks constantly. Every TUI redraw, every theme change, every Claude Code release that adjusts spacing invalidates the scrape. Glimmervoid never does this.
|
|
105
|
+
|
|
106
|
+
Instead, at spawn Glimmervoid injects Claude Code hooks scoped to that one session, no changes to the target repo, that POST to a local HTTP endpoint on every lifecycle event: prompt submitted, turn stopped, notification raised, sub-agent started or finished. These hooks are the authoritative signal. An OSC-0 terminal title fallback (spinner glyph = working, idle glyph = ready) covers the gap for anything that predates or bypasses the hooks. The two are merged with explicit precedence (hook beats title) and a short conflict window so a racing signal can still win before the UI settles.
|
|
107
|
+
|
|
108
|
+
That design didn't arrive whole. Three incidents shaped it:
|
|
109
|
+
|
|
110
|
+
`/clear` and `/compact` fire no `UserPromptSubmit` and no `Stop`, but the terminal redraw briefly flashes a spinner then an idle glyph in the title. Early on, that flash looked exactly like a finished work cycle, so Glimmervoid fired a "session complete" notification on every `/clear`. (The bug report was, more or less, "why did my terminal congratulate me for clearing the screen.") The fix: on a detected clear/compact, reset both signal sources and mute title-only signals until the next real prompt.
|
|
111
|
+
|
|
112
|
+
A background sub-agent (launched via `Task` with `run_in_background`, or Ctrl+B) can still be running when the main agent's own turn ends and its `Stop` hook fires. Treating that `Stop` as completion closed the card while real work was still happening in the background. The fix is a completion gate: Glimmervoid counts live sub-agents from `SubagentStart`/`SubagentStop` and reconciles that count against `background_tasks`, a field Claude Code's own hook payloads declare independently. Staleness between the two is resolved by a sequence number, not a timestamp, because concurrent signals routinely land in the same millisecond.
|
|
113
|
+
|
|
114
|
+
Boot auto-resume (reattaching a session's Claude conversation after Glimmervoid restarts) depends on capturing Claude's session id from a hook. It was wired to capture that id from `SessionStart`, which seemed reasonable until testing showed Claude Code doesn't reliably fire `SessionStart` on interactive startup at all, silently dead in production, no error, resume just never happened. The fix: capture the session id from whichever main-agent hook arrives first, since they all carry it.
|
|
115
|
+
|
|
116
|
+
Every session also writes a JSONL forensic recording by default (hook payloads and state transitions, not raw terminal bytes), and a version-aware replay harness drives recorded traffic back through the detection code as regression fixtures. That's how bugs like the ones above get diagnosed from real session data instead of guesswork, and how they stay caught if the logic regresses.
|
|
117
|
+
|
|
118
|
+
## Focus
|
|
119
|
+
|
|
120
|
+
Glimmervoid centers on one session at a time. A left **roster rail** lists one pill per session (grouped by project, with a live working heartbeat and a "needs you" queue); the **center** borrows that session's live terminal as the work surface; a right **review sidebar** shows its changes.
|
|
121
|
+
|
|
122
|
+
Every git-repo session runs in its own git worktree forked from the integration branch, so an agent's edits stay out of your main checkout until you review them. `integrationBranch` is unset by default, which means each repo's own default branch: `origin/HEAD`, then `main`, then `master`. The sidebar splits **Committed** (the mergeable unit) from **Uncommitted** work, keeps the diff live, and merges into the integration branch with one click while the session keeps running. If a merge hits conflicts it parks, and **Resolve in session** hands the conflict back to the agent that owns the worktree with a ready-to-run prompt.
|
|
123
|
+
|
|
124
|
+
Navigate it from the keyboard with Cmd on a Mac and Alt elsewhere, never more than two keys: `Cmd/Alt+J` steps through the sessions needing attention, `Cmd/Alt+1`..`9` jump to a session, `Cmd/Alt+Up`/`Down` move through the rail, `Cmd/Alt+I` merges the selected one, `Cmd/Alt+U` resolves or resyncs it, and `Cmd/Alt+0` opens Add Session.
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
On first run Glimmervoid creates `~/.glimmervoid/config.json`. Most settings are edited from the dashboard's Settings view, and the server reloads the file when you edit it by hand. Every key, its default and every environment variable are in [docs/configuration.md](docs/configuration.md), which is generated from the code.
|
|
129
|
+
|
|
130
|
+
Glimmervoid sends anonymous usage and error data by default; what it sends and how to turn it off are in [Telemetry](docs/configuration.md#telemetry).
|
|
131
|
+
|
|
132
|
+
## Remote access
|
|
133
|
+
|
|
134
|
+
Remote access is off by default. It is a second loopback listener meant to sit behind an HTTPS reverse proxy, never a wider bind, and it is created only at startup:
|
|
135
|
+
|
|
136
|
+
1. Add a `remote` block to `~/.glimmervoid/config.json`. `enabled` defaults to `false`; `port` has no default and is required once enabled (it must differ from the local port); `publicHost` defaults to empty and is the hostname your proxy serves, used for shareable pairing links and the allowed origin.
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"remote": { "enabled": true, "port": 3456, "publicHost": "my-machine.example.ts.net" }
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
2. Restart Glimmervoid (stop and start it, or `systemctl --user restart glimmervoid` when it runs as a service). The log line `Glimmervoid remote listener on http://127.0.0.1:3456 (paired devices only)` confirms the listener is up.
|
|
145
|
+
3. Point an HTTPS reverse proxy at that port. With Tailscale, `tailscale serve --bg 3456` serves `https://<machine>.<tailnet>.ts.net`, which is the name `publicHost` should hold. When the port is reached solely through HTTP-mode `tailscale serve`, also set `"ownerLogin": "you@example.com"` in the `remote` block: the listener then refuses every request and WebSocket whose `Tailscale-User-Login` is not that login, including Funnel and tagged-device traffic, which carry none. Only HTTP-mode `tailscale serve` strips a client-sent `Tailscale-User-Login`; behind any other proxy, or `tailscale serve --tcp` or `--tls-terminated-tcp`, a client can forge it, so `ownerLogin` proves nothing there. It works on top of pairing, never instead of it.
|
|
146
|
+
4. Verify from the device: `https://<publicHost>/` should answer with Glimmervoid's "Pairing required" page.
|
|
147
|
+
5. On the host, run `glimmervoid pair --name phone`. It prints a single-use URL valid for 10 minutes that sets an auth cookie when opened on the device.
|
|
148
|
+
|
|
149
|
+
`glimmervoid pair --list` and `glimmervoid pair --revoke <id>` manage devices, and a revoke applies without a restart. A pairing grants full control of the machine as the server account, so treat pairing URLs as passwords.
|
|
150
|
+
|
|
151
|
+
## Security
|
|
152
|
+
|
|
153
|
+
Glimmervoid runs agents with your account's privileges and trusts every local process. Read [SECURITY.md](SECURITY.md) for the trust model and how to report a vulnerability, and [Limitations](#limitations) for the short version.
|
|
154
|
+
|
|
155
|
+
## Running as a service (Linux)
|
|
156
|
+
|
|
157
|
+
A systemd user unit keeps Glimmervoid running after you log out:
|
|
158
|
+
|
|
159
|
+
```ini
|
|
160
|
+
# ~/.config/systemd/user/glimmervoid.service
|
|
161
|
+
[Unit]
|
|
162
|
+
Description=Glimmervoid
|
|
163
|
+
|
|
164
|
+
[Service]
|
|
165
|
+
ExecStart=/usr/bin/env glimmervoid
|
|
166
|
+
EnvironmentFile=-%h/.glimmervoid/secrets.env
|
|
167
|
+
Restart=on-failure
|
|
168
|
+
|
|
169
|
+
[Install]
|
|
170
|
+
WantedBy=default.target
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
systemctl --user daemon-reload
|
|
175
|
+
systemctl --user enable --now glimmervoid
|
|
176
|
+
loginctl enable-linger "$USER"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`secrets.env` is optional and can hold `GLIMMERVOID_POSTHOG_API_KEY` and `GLIMMERVOID_TELEGRAM_BOT_TOKEN`, which keeps both out of `config.json`. The unit's `PATH` must reach `node`, `glimmervoid` and your agent CLIs; with nvm, set `Environment=PATH=...` or point `ExecStart` at absolute paths. For a source checkout, use `ExecStart=/usr/bin/env node dist/server/index.js` with `WorkingDirectory=` set to the checkout.
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
184
|
+
|
|
185
|
+
## Limitations
|
|
186
|
+
|
|
187
|
+
- **Local-first, with an opt-in remote door.** By default Glimmervoid binds `127.0.0.1` and has no login on the local listener, so any local process can drive it. That is a deliberate single-user choice, and it means the local port must never be exposed to the network. Remote access is a separate listener gated by single-use pairing tokens and cookies, meant to sit behind a reverse proxy; a pairing cookie grants full code execution as the server account.
|
|
188
|
+
- **Requires an agent CLI.** Glimmervoid spawns and supervises Claude Code, Codex, Grok or another terminal agent; it does not replace them.
|
|
189
|
+
|
|
190
|
+
## Changelog
|
|
191
|
+
|
|
192
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { o as nodePtyRebuildHint, t as probeNodePty } from "../chunks/node-pty-preflight-CK1knGIR.js";
|
|
3
|
+
import { a as packageRoot } from "../chunks/runtime-paths-Ci_ksSkj.js";
|
|
4
|
+
import { t as AGENT_API_VERBS } from "../chunks/session-Cez8aIWg.js";
|
|
5
|
+
import { i as execSync } from "../chunks/child-process-safe-DRGsp0TE.js";
|
|
6
|
+
import { t as renderTable } from "../chunks/ascii-figure-core-pDAwtlKO.js";
|
|
7
|
+
import { n as glimmervoidHomeDir, t as decideConfigPath } from "../chunks/config-path-core-DFt-TemS.js";
|
|
8
|
+
import { i as sandboxDoctorRows, r as probeSandboxDependencies } from "../chunks/sandbox-deps-Dd5-kZnf.js";
|
|
9
|
+
import { i as pnpmGlobalBinDir, n as npmGlobalBinDir, r as onPath, t as formatPathNotice } from "../chunks/path-doctor-C23VAqeu.js";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
//#region bin/glimmervoid.ts
|
|
14
|
+
var args = process.argv.slice(2);
|
|
15
|
+
var pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
16
|
+
var USAGE = `Usage: glimmervoid [command] [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
doctor Diagnose install / PATH issues and exit
|
|
20
|
+
agent setup grok Install Glimmervoid's env-inert Grok hook relay
|
|
21
|
+
pair Mint a single-use pairing link for a remote device
|
|
22
|
+
pair --list List paired devices
|
|
23
|
+
pair --revoke <id> Revoke a paired device
|
|
24
|
+
visions relay Run the Visions LSP relay on stdio (what an editor's LSP client spawns)
|
|
25
|
+
visions install Install the Visions extension into every VS Code family editor on PATH
|
|
26
|
+
visions setup Print LSP client config for Neovim, Helix, Emacs, Kate, Sublime, JetBrains
|
|
27
|
+
visions status Report the relay path and which editors carry the extension
|
|
28
|
+
spawn <prompt> From inside a Glimmervoid session, start a sibling session on that prompt
|
|
29
|
+
attention <note> From inside a Glimmervoid session, flag it as needing the operator
|
|
30
|
+
board From inside a Glimmervoid session, list the live sessions
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--name <label> Label for the device being paired (with: pair)
|
|
34
|
+
--port <number> Override the server port (default: 3000)
|
|
35
|
+
--config <path> Path to config file (default: ~/.glimmervoid/config.json)
|
|
36
|
+
--version Show version number
|
|
37
|
+
--help, -h Show this help message`;
|
|
38
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
39
|
+
console.log(USAGE);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
if (args.includes("--version")) {
|
|
43
|
+
console.log(pkg.version);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
function getArgValue(flag) {
|
|
47
|
+
const idx = args.indexOf(flag);
|
|
48
|
+
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
var configArg = getArgValue("--config");
|
|
52
|
+
if (configArg) process.env.GLIMMERVOID_CONFIG = configArg;
|
|
53
|
+
var portArg = getArgValue("--port");
|
|
54
|
+
if (portArg) process.env.GLIMMERVOID_PORT = portArg;
|
|
55
|
+
var SUBCOMMAND_RUNNERS = new Map([
|
|
56
|
+
["doctor", async () => {
|
|
57
|
+
await runDoctor();
|
|
58
|
+
return 0;
|
|
59
|
+
}],
|
|
60
|
+
["pair", async (commandArgs) => {
|
|
61
|
+
const { runPairCli } = await import("../chunks/pair-cli-w-gCAF-1.js");
|
|
62
|
+
return runPairCli(commandArgs.slice(1));
|
|
63
|
+
}],
|
|
64
|
+
["agent", async (commandArgs) => {
|
|
65
|
+
const { runAgentSetupCli } = await import("../chunks/agent-setup-cli-B5Atxr3F.js");
|
|
66
|
+
return runAgentSetupCli(commandArgs.slice(1));
|
|
67
|
+
}],
|
|
68
|
+
["visions", async (commandArgs) => {
|
|
69
|
+
const { runVisionsCli } = await import("../chunks/visions-cli-BXQFIBCX.js");
|
|
70
|
+
return runVisionsCli(commandArgs.slice(1));
|
|
71
|
+
}],
|
|
72
|
+
...AGENT_API_VERBS.map((verb) => [verb, async (commandArgs) => {
|
|
73
|
+
const { runAgentApiCli } = await import("../chunks/agent-api-cli-D0MmtEdz.js");
|
|
74
|
+
return runAgentApiCli(commandArgs);
|
|
75
|
+
}])
|
|
76
|
+
]);
|
|
77
|
+
async function dispatchCommandLine() {
|
|
78
|
+
const requestedSubcommand = args.includes("--doctor") ? "doctor" : args[0];
|
|
79
|
+
if (!requestedSubcommand || requestedSubcommand.startsWith("-")) {
|
|
80
|
+
await import("../server/index.js");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const runSubcommand = SUBCOMMAND_RUNNERS.get(requestedSubcommand);
|
|
84
|
+
if (!runSubcommand) {
|
|
85
|
+
console.error(`Unknown command: ${requestedSubcommand}\n${USAGE}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
process.exit(await runSubcommand(args));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(messageOf(err));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
await dispatchCommandLine();
|
|
96
|
+
function messageOf(error) {
|
|
97
|
+
if (error instanceof Error && error.message) return error.message;
|
|
98
|
+
return String(error);
|
|
99
|
+
}
|
|
100
|
+
function firstLineOf(error) {
|
|
101
|
+
return messageOf(error).split("\n")[0];
|
|
102
|
+
}
|
|
103
|
+
function decideReadOnlyConfigPath() {
|
|
104
|
+
return decideConfigPath({
|
|
105
|
+
env: process.env,
|
|
106
|
+
homeDir: glimmervoidHomeDir(os.homedir(), process.env)
|
|
107
|
+
}, (candidate) => fs.existsSync(candidate));
|
|
108
|
+
}
|
|
109
|
+
function resolveConfigPathReadOnly() {
|
|
110
|
+
const decided = decideReadOnlyConfigPath();
|
|
111
|
+
if (decided.path) return decided.path;
|
|
112
|
+
if (decided.source === "env") return `${decided.envPath} (set via GLIMMERVOID_CONFIG, but NOT found)`;
|
|
113
|
+
return `${decided.homePath} (created on first run)`;
|
|
114
|
+
}
|
|
115
|
+
async function readDeclaredCustomAgents() {
|
|
116
|
+
const decided = decideReadOnlyConfigPath();
|
|
117
|
+
if (!decided.path) return {
|
|
118
|
+
declared: [],
|
|
119
|
+
error: null
|
|
120
|
+
};
|
|
121
|
+
const { loadConfigFile } = await import("../chunks/config-store-CC8T3okC.js");
|
|
122
|
+
try {
|
|
123
|
+
const loaded = loadConfigFile(decided.path, { exitOnError: false });
|
|
124
|
+
if (!loaded.config) return {
|
|
125
|
+
declared: [],
|
|
126
|
+
error: loaded.message
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
declared: loaded.config.customAgents ?? [],
|
|
130
|
+
error: null
|
|
131
|
+
};
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return {
|
|
134
|
+
declared: [],
|
|
135
|
+
error: `could not read ${decided.path}: ${firstLineOf(err)}`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function runDoctor() {
|
|
140
|
+
const platform = process.platform;
|
|
141
|
+
const homedir = os.homedir();
|
|
142
|
+
const pathEnv = process.env.PATH || process.env.Path || "";
|
|
143
|
+
let section = "Versions";
|
|
144
|
+
let rows = [];
|
|
145
|
+
const line = (label, value) => rows.push([label, value]);
|
|
146
|
+
const flush = () => {
|
|
147
|
+
console.log(renderTable({
|
|
148
|
+
title: section,
|
|
149
|
+
rows,
|
|
150
|
+
terminalColumns: process.stdout.columns
|
|
151
|
+
}));
|
|
152
|
+
rows = [];
|
|
153
|
+
};
|
|
154
|
+
const switchSection = (nextSection) => {
|
|
155
|
+
flush();
|
|
156
|
+
console.log("");
|
|
157
|
+
section = nextSection;
|
|
158
|
+
};
|
|
159
|
+
console.log("glimmervoid doctor\n");
|
|
160
|
+
line("glimmervoid", pkg.version);
|
|
161
|
+
line("node", process.version);
|
|
162
|
+
line("platform", `${platform} ${process.arch}`);
|
|
163
|
+
switchSection("This CLI");
|
|
164
|
+
line("running from", process.argv[1] || "(unknown)");
|
|
165
|
+
line("package dir", packageRoot);
|
|
166
|
+
switchSection("PATH registration");
|
|
167
|
+
const npmBin = npmGlobalBinDir({
|
|
168
|
+
env: process.env,
|
|
169
|
+
platform,
|
|
170
|
+
homedir
|
|
171
|
+
}) || npmGlobalBinDir({
|
|
172
|
+
env: process.env,
|
|
173
|
+
platform,
|
|
174
|
+
homedir,
|
|
175
|
+
resolvedPrefix: resolveNpmGlobalPrefix(execSync)
|
|
176
|
+
});
|
|
177
|
+
const npmOn = npmBin ? onPath(npmBin, {
|
|
178
|
+
pathEnv,
|
|
179
|
+
platform
|
|
180
|
+
}) : false;
|
|
181
|
+
line("npm global bin", npmBin || "(unknown)");
|
|
182
|
+
line("on PATH", npmBin ? npmOn ? "yes" : "NO" : "unknown");
|
|
183
|
+
const pnpmBin = pnpmGlobalBinDir({
|
|
184
|
+
env: process.env,
|
|
185
|
+
platform,
|
|
186
|
+
homedir
|
|
187
|
+
});
|
|
188
|
+
if (pnpmBin && fs.existsSync(pnpmBin)) {
|
|
189
|
+
line("pnpm global bin", pnpmBin);
|
|
190
|
+
line("on PATH", onPath(pnpmBin, {
|
|
191
|
+
pathEnv,
|
|
192
|
+
platform
|
|
193
|
+
}) ? "yes" : "NO");
|
|
194
|
+
}
|
|
195
|
+
switchSection("Agents");
|
|
196
|
+
try {
|
|
197
|
+
const { listAgentIds, describeAgentResolvability, setCustomAgents } = await import("../chunks/adapters-Dho1VSY5.js");
|
|
198
|
+
const declaredCustomAgents = await readDeclaredCustomAgents();
|
|
199
|
+
if (declaredCustomAgents.error) line("config", declaredCustomAgents.error);
|
|
200
|
+
setCustomAgents(declaredCustomAgents.declared);
|
|
201
|
+
for (const id of listAgentIds()) {
|
|
202
|
+
const { label, path: resolvedPath } = describeAgentResolvability(id);
|
|
203
|
+
line(`${id} (${label})`, resolvedPath ?? "not found on PATH");
|
|
204
|
+
}
|
|
205
|
+
const { inspectGrokAgentSetup } = await import("../chunks/agent-setup-cli-B5Atxr3F.js");
|
|
206
|
+
const grokSetup = inspectGrokAgentSetup();
|
|
207
|
+
line("grok hook setup", `${grokSetup.classification}: ${grokSetup.filePath}`);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
line("agents", `probe failed: ${firstLineOf(err)}`);
|
|
210
|
+
}
|
|
211
|
+
switchSection("rtk");
|
|
212
|
+
try {
|
|
213
|
+
const { getRtkPath } = await import("../chunks/rtk-resolver-CMQAidlI.js");
|
|
214
|
+
line("rtk", getRtkPath() || "not installed (Glimmervoid installs it when the rtk setting is on)");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
line("rtk", `probe failed: ${firstLineOf(err)}`);
|
|
217
|
+
}
|
|
218
|
+
switchSection("Sane YOLO");
|
|
219
|
+
try {
|
|
220
|
+
const { resolvePackageBin } = await import("../chunks/runtime-paths-JLrZ3wyi.js");
|
|
221
|
+
const { execFileSync } = await import("../chunks/child-process-safe-DvQjXsgz.js");
|
|
222
|
+
const { inspectGrokAgentSetup } = await import("../chunks/agent-setup-cli-B5Atxr3F.js");
|
|
223
|
+
const binPath = resolvePackageBin("cc-safety-net", "cc-safety-net");
|
|
224
|
+
line("Sane YOLO", binPath || "not installed");
|
|
225
|
+
if (binPath) line("cc-safety-net version", execFileSync(process.execPath, [binPath, "--version"], {
|
|
226
|
+
encoding: "utf8",
|
|
227
|
+
timeout: 3e3
|
|
228
|
+
}).trim());
|
|
229
|
+
line("Codex hook trust", "Sane YOLO is inactive unless Codex trusts the injected hooks.");
|
|
230
|
+
line("Grok Sane YOLO", inspectGrokAgentSetup().saneYoloReady ? "installed" : "inactive; run glimmervoid agent setup grok");
|
|
231
|
+
} catch (err) {
|
|
232
|
+
line("Sane YOLO", `probe failed: ${firstLineOf(err)}`);
|
|
233
|
+
}
|
|
234
|
+
switchSection("Native module");
|
|
235
|
+
const nodePty = await probeNodePty();
|
|
236
|
+
if (nodePty.ok) line("node-pty", "loads OK");
|
|
237
|
+
if (!nodePty.ok) {
|
|
238
|
+
line("node-pty", "FAILED to load");
|
|
239
|
+
line("reason", nodePty.reason);
|
|
240
|
+
line("hint", nodePtyRebuildHint(platform));
|
|
241
|
+
}
|
|
242
|
+
switchSection("Sandbox");
|
|
243
|
+
for (const [label, value] of sandboxDoctorRows(probeSandboxDependencies({
|
|
244
|
+
platform,
|
|
245
|
+
exec: execSync
|
|
246
|
+
}))) line(label, value);
|
|
247
|
+
switchSection("Config");
|
|
248
|
+
line("resolved config", resolveConfigPathReadOnly());
|
|
249
|
+
flush();
|
|
250
|
+
if (npmBin && !npmOn) console.log(`\n${formatPathNotice({
|
|
251
|
+
installedBinDir: npmBin,
|
|
252
|
+
onPathFlag: false,
|
|
253
|
+
platform
|
|
254
|
+
})}`);
|
|
255
|
+
}
|
|
256
|
+
function resolveNpmGlobalPrefix(exec) {
|
|
257
|
+
try {
|
|
258
|
+
const out = exec("npm prefix -g", {
|
|
259
|
+
encoding: "utf8",
|
|
260
|
+
stdio: [
|
|
261
|
+
"ignore",
|
|
262
|
+
"pipe",
|
|
263
|
+
"ignore"
|
|
264
|
+
],
|
|
265
|
+
timeout: 2e3
|
|
266
|
+
});
|
|
267
|
+
const prefix = String(out || "").trim();
|
|
268
|
+
if (prefix) return prefix;
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
export {};
|