glimmervoid 0.43.2 → 0.43.3
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/glimmervoid.js +14 -14
- package/dist/chunks/{adapters-CjtgWEdg.js → adapters-BrJVO21n.js} +16 -10
- package/dist/chunks/{adapters-Ce0I__TH.js → adapters-CxM7iyI_.js} +1 -1
- package/dist/chunks/{agent-api-cli-B0fpc41n.js → agent-api-cli-DQDeXBP_.js} +2 -2
- package/dist/chunks/{agent-api-cli-DYwXyAmN.js → agent-api-cli-DbipciSb.js} +1 -1
- package/dist/chunks/agent-api-core-Da2UAN7U.js +782 -0
- package/dist/chunks/{agent-setup-cli-NfJQxNdp.js → agent-setup-cli-DAPHGpRA.js} +1 -1
- package/dist/chunks/{control-messages-CD99vE0V.js → config-DkjpKAoc.js} +556 -2
- package/dist/chunks/{config-store-2noiIsTg.js → config-store-DJ_imLTg.js} +39 -5
- package/dist/chunks/config-store-Wbcvb0f3.js +1 -0
- package/dist/chunks/contracts-D5KEXrcX.js +293 -0
- package/dist/chunks/{grok-DPmX1ozS.js → grok-2z-LqPHN.js} +2 -2
- package/dist/chunks/{main-BGJ7PAA2.js → main-BPyE0fvz.js} +194 -465
- package/dist/chunks/{pair-cli-CPv7mDTg.js → pair-cli-CKBA5XVU.js} +2 -2
- package/dist/chunks/{remote-config-DZWZ5fVJ.js → remote-config-C-Rc55Vf.js} +1 -1
- package/dist/chunks/runtime-paths-Cc0_ZR6V.js +1 -0
- package/dist/chunks/{runtime-paths-BkeftE60.js → runtime-paths-ghcH9CeW.js} +1 -1
- package/dist/chunks/{session-routes-DdeSkLqC.js → session-routes-BQPy81dm.js} +6 -3
- package/dist/chunks/{settings-cli-4fX1olS3.js → settings-cli-bDyDiLRy.js} +5 -5
- package/dist/chunks/{settings-readout-core-NoTHBGL6.js → settings-readout-core-BKSVNO7q.js} +1 -1
- package/dist/chunks/{ssh-sign-cli-CvqtQNQe.js → ssh-sign-cli-COMPYoNe.js} +1 -1
- package/dist/chunks/{visions-cli-CVRB9IWA.js → visions-cli-COYRIdXy.js} +1 -1
- package/dist/chunks/{visions-setup-DB2P8JUL.js → visions-setup-VMMyqOpl.js} +2 -2
- package/dist/client/assets/index-BA0smAG8.js +177 -0
- package/dist/client/assets/{index-DEbBUagP.css → index-CFJ6Pl08.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +2 -2
- package/dist/session/remote-setup/SKILL.md +120 -0
- package/dist/session/remote-setup/glimmervoid.service +20 -0
- package/dist/session/statusline-relay.js +1 -1
- package/dist/session/visions-relay.js +1 -1
- package/package.json +1 -1
- package/dist/chunks/agent-api-core-BeSecYrA.js +0 -336
- package/dist/chunks/config-store-DW0ONWV5.js +0 -1
- package/dist/chunks/contracts-CFm0dprb.js +0 -835
- package/dist/chunks/runtime-paths-DSmormp7.js +0 -1
- package/dist/client/assets/index-CNDdyYn0.js +0 -177
package/dist/client/index.html
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon-CKygeLwW.svg">
|
|
8
8
|
<title>Glimmervoid</title>
|
|
9
9
|
<style>html,body{background:var(--bg,#0a0810);height:100%;color:var(--text,#c8c0e0);font-family:var(--font-mono,"CommitMono", "Cascadia Code", "Fira Code", "Consolas", "Menlo", monospace);margin:0;padding:0}.app-shell{display:none}html:not([data-layout=phone]) .app-ready .app-shell{display:contents}.loading-screen{z-index:9999;background:#0a0810;flex-direction:column;justify-content:center;align-items:center;gap:20px;transition:opacity .3s;display:flex;position:fixed;inset:0}.loading-screen.fade-out{opacity:0;pointer-events:none}.loading-title{letter-spacing:.1em;color:#e8e0ff;text-transform:uppercase;font-family:CommitMono,Cascadia Code,Fira Code,Consolas,Menlo,monospace;font-size:20px;font-weight:700}.loading-title:before{content:"▸";color:#c084fc}.loading-status{color:#8276b0;letter-spacing:.04em;font-family:CommitMono,Cascadia Code,Fira Code,Consolas,Menlo,monospace;font-size:12px}.loading-detail{color:#5e5584;text-align:center;text-wrap:balance;max-width:min(34ch,100vw - 32px);margin-top:-8px;font-family:CommitMono,Cascadia Code,Fira Code,Consolas,Menlo,monospace;font-size:12px;line-height:1.6}.loading-spinner{border:2px solid #2a2440;border-top-color:#c084fc;border-radius:50%;width:28px;height:28px;animation:.8s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.shutdown-screen:not(.active) .loading-spinner{animation:none}.shutdown-screen{z-index:9999;-webkit-backdrop-filter:blur();backdrop-filter:blur();pointer-events:none;opacity:0;background:#0a081000;flex-direction:column;justify-content:center;align-items:center;gap:20px;transition:background .4s,-webkit-backdrop-filter .4s,backdrop-filter .4s,opacity .3s;display:flex;position:fixed;inset:0}.shutdown-screen.active{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);pointer-events:auto;opacity:1;background:#0a0810eb}.shutdown-status{color:#8276b0;letter-spacing:.04em;font-family:CommitMono,Cascadia Code,Fira Code,Consolas,Menlo,monospace;font-size:12px;transition:color .3s}.shutdown-screen.done .loading-spinner{opacity:.3;border-color:#2a2440;animation:none}.shutdown-screen.done .shutdown-status{color:#4a4268}</style>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-BA0smAG8.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/zod-BRLv4KqJ.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm-kR64YvZ-.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CFJ6Pl08.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div class="loading-screen" id="loading-screen">
|
package/dist/server/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as formatNodePtyBootRefusal, t as probeNodePty } from "../chunks/node-pty-preflight-C8QIlrtU.js";
|
|
2
|
-
import {
|
|
2
|
+
import { o as packageRoot } from "../chunks/runtime-paths-ghcH9CeW.js";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -87,6 +87,6 @@ if (!nodePty.ok) {
|
|
|
87
87
|
}));
|
|
88
88
|
process.exit(1);
|
|
89
89
|
}
|
|
90
|
-
await import("../chunks/main-
|
|
90
|
+
await import("../chunks/main-BPyE0fvz.js");
|
|
91
91
|
//#endregion
|
|
92
92
|
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: remote-host-setup
|
|
3
|
+
description: Stand up a remote Linux host that runs Glimmervoid as a service, reached from the operator's laptop and phone. Use in the Glimmervoid remote setup session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remote host setup
|
|
7
|
+
|
|
8
|
+
You run on the operator's current Glimmervoid machine, outside the guardrails, with their ssh keys. You drive the new host over `ssh`. Every step that must happen on the operator's laptop or phone, or that needs an interactive login, you hand to the operator as an exact command and wait for them to confirm.
|
|
9
|
+
|
|
10
|
+
The end state:
|
|
11
|
+
|
|
12
|
+
- Glimmervoid runs on the remote host as a systemd user service that survives logout and reboot.
|
|
13
|
+
- The operator reaches its dashboard from the laptop, and from the phone through a paired device link.
|
|
14
|
+
- Sessions on the host can sign commits through the operator's forwarded ssh agent.
|
|
15
|
+
- Guarded sessions on the host can start their sandbox.
|
|
16
|
+
|
|
17
|
+
## Ground rules
|
|
18
|
+
|
|
19
|
+
- Ask before you assume. Start by asking for: the ssh host alias or `user@host`, whether it is a Coder workspace, whether the operator uses Tailscale, and which agent CLIs (Claude Code, Codex) they want on the host.
|
|
20
|
+
- Run one remote command per `ssh` call and read its output before the next. Never pipe a downloaded script into a shell without showing the operator the URL first.
|
|
21
|
+
- `sudo` on the remote needs the operator's approval every time. Say what it changes and how to revert it.
|
|
22
|
+
- Never restart, stop or reinstall a Glimmervoid server from a session that server hosts: the session is a child of the server and dies with it halfway through. This session runs on the operator's current machine, so restarting the NEW host's service is fine; restarting the server this session lives in is not.
|
|
23
|
+
- Never copy private keys, tokens or `~/.glimmervoid/config.json` secrets between machines. Secrets go into `~/.glimmervoid/secrets.env` on the host, typed by the operator.
|
|
24
|
+
- Never set `GLIMMERVOID_HOST` to a non-loopback address or set `GLIMMERVOID_INSECURE_BIND=1`. Remote access goes through the remote listener and an HTTPS proxy only.
|
|
25
|
+
|
|
26
|
+
## 1. Reach the host
|
|
27
|
+
|
|
28
|
+
- Plain host: confirm `ssh <host> true` works without a password prompt. If it does not, hand the operator `ssh-copy-id <host>` to run themselves.
|
|
29
|
+
- Coder workspace: the operator runs `coder login <deployment-url>` and `coder config-ssh` on the laptop and on this machine, then the host alias is `coder.<workspace>`. `CODER_SSH_DISABLE_AUTOSTART=true coder ssh <workspace> -- <command>` also works.
|
|
30
|
+
- Record the facts you find: `uname -a`, `cat /etc/os-release`, `systemctl --user status` (user systemd must be running), free disk, and memory.
|
|
31
|
+
|
|
32
|
+
## 2. Prerequisites on the host
|
|
33
|
+
|
|
34
|
+
- Node.js 22.18.0 or newer (`node --version`). If missing, propose a user-level install (nvm or fnm) rather than a system package, and note the absolute `node` path for the unit file.
|
|
35
|
+
- Build tools for `node-pty`: `make`, `g++`, `python3`. On Debian or Ubuntu that is `sudo apt-get install -y build-essential python3`.
|
|
36
|
+
- `git`, and `bubblewrap` (the `bwrap` binary) for the guardrails sandbox.
|
|
37
|
+
- The agent CLIs the operator chose: `npm install -g @anthropic-ai/claude-code` and or `npm install -g @openai/codex`. Logging in is interactive: hand the operator `ssh -t <host> claude` and `ssh -t <host> codex login` and wait.
|
|
38
|
+
|
|
39
|
+
### Ubuntu sandbox prerequisite
|
|
40
|
+
|
|
41
|
+
Ubuntu 23.10 and later set `kernel.apparmor_restrict_unprivileged_userns=1`, which makes `bwrap` fail with `setting up uid map: Permission denied` or `loopback: Failed RTM_NEWADDR`, and guarded sessions never start. Test it:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bwrap --ro-bind / / --dev /dev --unshare-net true && echo sandbox-ok
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If it fails, propose the narrow fix, which grants user namespaces to `bwrap` only, and get the operator's approval for the `sudo` steps:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
printf 'abi <abi/4.0>,\ninclude <tunables/global>\nprofile bwrap /usr/bin/bwrap flags=(unconfined) {\n userns,\n}\n' | sudo tee /etc/apparmor.d/bwrap
|
|
51
|
+
sudo apparmor_parser -r /etc/apparmor.d/bwrap
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Revert by deleting `/etc/apparmor.d/bwrap` and reloading AppArmor. Do not disable the sysctl machine wide.
|
|
55
|
+
|
|
56
|
+
## 3. Install Glimmervoid
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install -g glimmervoid
|
|
60
|
+
glimmervoid doctor
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`glimmervoid doctor` must pass before you go on. If `glimmervoid` is not on `PATH`, check `npm prefix -g`.
|
|
64
|
+
|
|
65
|
+
## 4. Run it as a service
|
|
66
|
+
|
|
67
|
+
`glimmervoid.service` beside this file is the unit template. Fill its placeholders on the host:
|
|
68
|
+
|
|
69
|
+
- `__NODE__`: absolute path of `node` (`command -v node` on the host).
|
|
70
|
+
- `__GLIMMERVOID__`: absolute path of the installed `glimmervoid` bin script (`command -v glimmervoid`).
|
|
71
|
+
- `__PATH__`: a `PATH` that reaches `node`, `glimmervoid`, `git` and the agent CLIs, built from absolute directories, never `$PATH`.
|
|
72
|
+
|
|
73
|
+
Write it to `~/.config/systemd/user/glimmervoid.service`, then:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
systemctl --user daemon-reload
|
|
77
|
+
systemctl --user enable --now glimmervoid
|
|
78
|
+
sudo loginctl enable-linger "$USER"
|
|
79
|
+
systemctl --user status glimmervoid
|
|
80
|
+
journalctl --user -u glimmervoid -n 50
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`enable-linger` keeps the service alive after the operator logs out; it needs `sudo` on most hosts. The log must show `Open http://localhost:<port>`, the local dashboard listener bound to loopback.
|
|
84
|
+
|
|
85
|
+
## 5. Reach the dashboard
|
|
86
|
+
|
|
87
|
+
The local listener has no authentication and stays on loopback. Pick one route with the operator:
|
|
88
|
+
|
|
89
|
+
- **Laptop over ssh**: `ssh -L 3000:127.0.0.1:3000 <host>` on the laptop, then open `http://localhost:3000`. Simplest, laptop only.
|
|
90
|
+
- **Laptop and phone over Tailscale** (the devbox route):
|
|
91
|
+
1. Install Tailscale on the host and join the tailnet: hand the operator `sudo tailscale up` since it prints a login URL.
|
|
92
|
+
2. On the host, edit `~/.glimmervoid/config.json` by hand (`glimmervoid settings` cannot change `remote`), keeping every other key. Set `"remote": { "enabled": true, "port": 3456, "publicHost": "<machine>.<tailnet>.ts.net" }`. `port` must differ from the dashboard port. Optionally add `"ownerLogin": "<their tailscale login>"`.
|
|
93
|
+
3. `systemctl --user restart glimmervoid`, then check the log for `remote listener on http://127.0.0.1:3456 (paired devices only)`.
|
|
94
|
+
4. `tailscale serve --bg 3456`. Use HTTP mode only: `ownerLogin` proves nothing behind `--tcp` or `--tls-terminated-tcp`.
|
|
95
|
+
5. Each device opens `https://<publicHost>/` and sees "Pairing required". On the host, `glimmervoid pair --name <device>` prints a single-use link that expires in 10 minutes. The operator opens it on that device. Treat the link like a password: show it only in this session, never write it to a file.
|
|
96
|
+
- **Coder workspace**: Coder proxies the remote listener port as a workspace app; the URL shape is the deployment's own.
|
|
97
|
+
1. On the host, edit `~/.glimmervoid/config.json` by hand (`glimmervoid settings` cannot change `remote`), keeping every other key. Set `"remote": { "enabled": true, "port": 3456, "publicHost": "<coder app hostname for that port>" }`. `port` must differ from the dashboard port. Use `"allowedOrigins": ["https://<coder app hostname>"]` instead of `publicHost` if the operator needs explicit origins. With neither set, dashboard WebSockets from the Coder app origin are rejected.
|
|
98
|
+
2. `systemctl --user restart glimmervoid`, then check the log for `remote listener on http://127.0.0.1:3456 (paired devices only)`.
|
|
99
|
+
3. Pair exactly as in the Tailscale route: open `https://<coder app hostname>/`, then `glimmervoid pair --name <device>` on the host.
|
|
100
|
+
|
|
101
|
+
## 6. Commit signing
|
|
102
|
+
|
|
103
|
+
If the operator signs commits with an ssh key held by an agent on the laptop (for example Secretive), no key file belongs on the host. Glimmervoid sessions sign through `glimmervoid ssh-sign`, which signs with the forwarded agent the server finds. That needs a live forwarded connection from the laptop:
|
|
104
|
+
|
|
105
|
+
- Plain host: `ssh -A <host>` kept open, or `ForwardAgent yes` for that host in the laptop's `~/.ssh/config`.
|
|
106
|
+
- Coder: `coder ssh -A <workspace>`; plain `coder ssh` forwards nothing.
|
|
107
|
+
|
|
108
|
+
Set git on the host to sign with the public key only: `git config --global gpg.format ssh`, `git config --global user.signingkey '<public key line>'`, `git config --global commit.gpgsign true`. "agent refused operation" means the laptop declined or timed out the approval.
|
|
109
|
+
|
|
110
|
+
## 7. Prove it
|
|
111
|
+
|
|
112
|
+
Finish only when each of these holds, and show the operator the evidence:
|
|
113
|
+
|
|
114
|
+
1. `systemctl --user is-active glimmervoid` prints `active`, and `systemctl --user is-enabled glimmervoid` prints `enabled`.
|
|
115
|
+
2. `glimmervoid doctor` passes on the host.
|
|
116
|
+
3. The operator opened the dashboard through the chosen route and sees it.
|
|
117
|
+
4. If Tailscale was set up: a paired device loads the dashboard.
|
|
118
|
+
5. If the sandbox check ran: it printed `sandbox-ok`.
|
|
119
|
+
|
|
120
|
+
Then summarize for the operator: the host, the dashboard URL, which steps they still own (logins, keeping `ssh -A` open for signing), and how to undo each change you made.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Glimmervoid
|
|
3
|
+
After=network.target
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=simple
|
|
7
|
+
ExecStart=__NODE__ __GLIMMERVOID__
|
|
8
|
+
EnvironmentFile=-%h/.glimmervoid/secrets.env
|
|
9
|
+
Environment=PATH=__PATH__
|
|
10
|
+
KillMode=control-group
|
|
11
|
+
TimeoutStopSec=10
|
|
12
|
+
LimitNOFILE=65536
|
|
13
|
+
Restart=on-failure
|
|
14
|
+
RestartSec=5
|
|
15
|
+
OOMPolicy=continue
|
|
16
|
+
MemoryHigh=50%
|
|
17
|
+
MemoryMax=60%
|
|
18
|
+
|
|
19
|
+
[Install]
|
|
20
|
+
WantedBy=default.target
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { o as spawn } from "../chunks/child-process-safe-D4hyVIXj.js";
|
|
2
|
+
import { n as isRecord } from "../chunks/coerce-NSst8oRP.js";
|
|
2
3
|
import { t as readStdin } from "../chunks/relay-stdin-CpHvLu6M.js";
|
|
3
4
|
import { t as postPayload } from "../chunks/loopback-post-DZK7YibD.js";
|
|
4
|
-
import { n as isRecord } from "../chunks/coerce-NSst8oRP.js";
|
|
5
5
|
import { n as parseJsonRecord } from "../chunks/json-core-dfa_7Fak.js";
|
|
6
6
|
//#region session/statusline-relay.ts
|
|
7
7
|
var NO_CHAIN = "-";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as glimmervoidHomeDir, t as decideConfigPath } from "../chunks/config-path-core-DFt-TemS.js";
|
|
2
1
|
import { n as isRecord } from "../chunks/coerce-NSst8oRP.js";
|
|
2
|
+
import { n as glimmervoidHomeDir, t as decideConfigPath } from "../chunks/config-path-core-DFt-TemS.js";
|
|
3
3
|
import { d as applyDidOpen, f as createDocStore, h as getDoc, l as applyDidChange, m as formatRange, t as ACTIVITY_METHOD, u as applyDidClose, v as listDocs, x as uriOfParams } from "../chunks/ingest-editor-core-B2tycW_m.js";
|
|
4
4
|
import { r as nextBackoffMs } from "../chunks/backoff-DjVpfHk7.js";
|
|
5
5
|
import { n as unrefTimer } from "../chunks/timer-deps-u2DAQLok.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glimmervoid",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.3",
|
|
4
4
|
"description": "Mission control that ships. Not a terminal multiplexer: Claude Code, Codex and Grok each work in their own git worktree, and the one session that needs you comes to the front",
|
|
5
5
|
"main": "dist/server/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import { a as AgentCloseRequest, c as AgentPullRequestRequest, f as AgentSettingsRequest, i as AgentAttentionRequest, l as AgentReadRequest, o as AgentEditRequest, p as AgentSpawnRequest, t as AGENT_API_VERBS, u as AgentSendRequest } from "./session-BUcx9uJa.js";
|
|
2
|
-
import { s as pullRequestKeyFromReference } from "./control-messages-CD99vE0V.js";
|
|
3
|
-
import { n as NO_OWN_WORKTREE_REASON, t as COMMAND_ROUTES } from "./session-routes-DdeSkLqC.js";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
var MAX_SPAWNS_IN_FLIGHT = 1;
|
|
6
|
-
var REFUSAL_REASON = "unknown session";
|
|
7
|
-
var INVALID_BODY_STATUS = 400;
|
|
8
|
-
var ROLE_REFUSED_STATUS = 403;
|
|
9
|
-
var PUSH_REFUSED_STATUS = 409;
|
|
10
|
-
var PUSH_REFUSAL_COPY = Object.freeze({
|
|
11
|
-
"no-worktree": () => NO_OWN_WORKTREE_REASON,
|
|
12
|
-
"merge-in-progress": () => "a merge for this session is already running, so try again when it ends",
|
|
13
|
-
"not-continuable": () => "this session cannot be merged in its current state",
|
|
14
|
-
destroyed: () => "this session is ending",
|
|
15
|
-
"nothing-to-commit": () => "no commit on this branch is missing from the integration branch, so commit your work first",
|
|
16
|
-
"rebase-conflict": ({ base, conflicts = [] }) => `this branch conflicts with ${base} in ${conflicts.join(", ") || "one or more files"}, so rebase onto ${base}, resolve the conflicts, commit, then push again`,
|
|
17
|
-
"not-fast-forward": ({ base }) => `${base} could not be fast-forwarded to this branch, and a push is never forced`,
|
|
18
|
-
"base-diverged": ({ base }) => `the local ${base} and origin/${base} have diverged, which the operator has to reconcile`
|
|
19
|
-
});
|
|
20
|
-
function tokensMatch(presented, expected) {
|
|
21
|
-
if (presented.length !== expected.length) return false;
|
|
22
|
-
try {
|
|
23
|
-
return crypto.timingSafeEqual(Buffer.from(presented, "utf8"), Buffer.from(expected, "utf8"));
|
|
24
|
-
} catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function refused() {
|
|
29
|
-
return {
|
|
30
|
-
ok: false,
|
|
31
|
-
status: 404,
|
|
32
|
-
reason: REFUSAL_REASON
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function decideAgentRequest({ sessionId, presentedToken, expectedToken, isLoopback }) {
|
|
36
|
-
if (!isLoopback) return refused();
|
|
37
|
-
if (!sessionId) return refused();
|
|
38
|
-
if (!expectedToken) return refused();
|
|
39
|
-
if (!presentedToken) return refused();
|
|
40
|
-
if (!tokensMatch(presentedToken, expectedToken)) return refused();
|
|
41
|
-
return { ok: true };
|
|
42
|
-
}
|
|
43
|
-
function decideSpawnAllowance({ liveChildren, lifetimeSpawns, inFlight }) {
|
|
44
|
-
if (inFlight >= MAX_SPAWNS_IN_FLIGHT) return {
|
|
45
|
-
ok: false,
|
|
46
|
-
status: 429,
|
|
47
|
-
reason: "a spawn is already in flight"
|
|
48
|
-
};
|
|
49
|
-
if (liveChildren >= 3) return {
|
|
50
|
-
ok: false,
|
|
51
|
-
status: 429,
|
|
52
|
-
reason: `at most 3 spawned sessions may be live at once`
|
|
53
|
-
};
|
|
54
|
-
if (lifetimeSpawns >= 10) return {
|
|
55
|
-
ok: false,
|
|
56
|
-
status: 429,
|
|
57
|
-
reason: `this session has used its 10 spawns`
|
|
58
|
-
};
|
|
59
|
-
return { ok: true };
|
|
60
|
-
}
|
|
61
|
-
function describeRoleRefusal(verb, caller) {
|
|
62
|
-
const route = COMMAND_ROUTES[verb](caller);
|
|
63
|
-
if (route.answers) return null;
|
|
64
|
-
if (route.code) return {
|
|
65
|
-
status: PUSH_REFUSED_STATUS,
|
|
66
|
-
body: {
|
|
67
|
-
ok: false,
|
|
68
|
-
reason: route.code,
|
|
69
|
-
error: route.reason
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
return {
|
|
73
|
-
status: ROLE_REFUSED_STATUS,
|
|
74
|
-
body: {
|
|
75
|
-
ok: false,
|
|
76
|
-
error: route.reason
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
function isAgentApiVerb(verb) {
|
|
81
|
-
return AGENT_API_VERBS.includes(verb);
|
|
82
|
-
}
|
|
83
|
-
function refuseInvalidBody(issues) {
|
|
84
|
-
return {
|
|
85
|
-
ok: false,
|
|
86
|
-
status: INVALID_BODY_STATUS,
|
|
87
|
-
error: issues[0]?.message || "invalid request"
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function bodyParser(schema) {
|
|
91
|
-
return (payload) => {
|
|
92
|
-
const parsed = schema.safeParse(payload);
|
|
93
|
-
if (!parsed.success) return refuseInvalidBody(parsed.error.issues);
|
|
94
|
-
return {
|
|
95
|
-
ok: true,
|
|
96
|
-
request: parsed.data
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
var parseEmptyBody = () => ({
|
|
101
|
-
ok: true,
|
|
102
|
-
request: {}
|
|
103
|
-
});
|
|
104
|
-
var parsePullRequestBody = (payload) => {
|
|
105
|
-
const parsed = AgentPullRequestRequest.safeParse(payload);
|
|
106
|
-
if (!parsed.success) return refuseInvalidBody(parsed.error.issues);
|
|
107
|
-
const pullRequestKey = pullRequestKeyFromReference(parsed.data.pr);
|
|
108
|
-
if (!pullRequestKey) return {
|
|
109
|
-
ok: false,
|
|
110
|
-
status: INVALID_BODY_STATUS,
|
|
111
|
-
error: "pr must be a GitHub pull request url or owner/repo#number"
|
|
112
|
-
};
|
|
113
|
-
return {
|
|
114
|
-
ok: true,
|
|
115
|
-
request: { pullRequestKey }
|
|
116
|
-
};
|
|
117
|
-
};
|
|
118
|
-
var REQUEST_PARSERS = Object.freeze({
|
|
119
|
-
spawn: bodyParser(AgentSpawnRequest),
|
|
120
|
-
send: bodyParser(AgentSendRequest),
|
|
121
|
-
read: bodyParser(AgentReadRequest),
|
|
122
|
-
close: bodyParser(AgentCloseRequest),
|
|
123
|
-
attention: bodyParser(AgentAttentionRequest),
|
|
124
|
-
board: parseEmptyBody,
|
|
125
|
-
push: parseEmptyBody,
|
|
126
|
-
edit: bodyParser(AgentEditRequest),
|
|
127
|
-
limits: parseEmptyBody,
|
|
128
|
-
review: parsePullRequestBody,
|
|
129
|
-
reviews: parseEmptyBody,
|
|
130
|
-
prs: parseEmptyBody,
|
|
131
|
-
"link-pr": parsePullRequestBody,
|
|
132
|
-
settings: bodyParser(AgentSettingsRequest)
|
|
133
|
-
});
|
|
134
|
-
function parseAgentVerb(verb, payload) {
|
|
135
|
-
if (!isAgentApiVerb(verb)) return {
|
|
136
|
-
ok: false,
|
|
137
|
-
status: 404,
|
|
138
|
-
error: REFUSAL_REASON
|
|
139
|
-
};
|
|
140
|
-
const parsed = REQUEST_PARSERS[verb](payload);
|
|
141
|
-
if (!parsed.ok) return parsed;
|
|
142
|
-
return {
|
|
143
|
-
ok: true,
|
|
144
|
-
verb,
|
|
145
|
-
request: parsed.request
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
function describeChildWorktree({ worktreeDir, branch }) {
|
|
149
|
-
return branch ? `branch ${branch} at ${worktreeDir}` : worktreeDir;
|
|
150
|
-
}
|
|
151
|
-
function describeUnpushedCloseRefusal(childName, childId, worktree) {
|
|
152
|
-
return `${childName} holds work that is not on the integration branch (${describeChildWorktree(worktree)}), so it was not closed. Ask it to land the work with glimmervoid push (glimmervoid send ${childId} <text>), or run glimmervoid close ${childId} --discard to end it and throw the work away`;
|
|
153
|
-
}
|
|
154
|
-
function describeChildEndNote(childName, outcome) {
|
|
155
|
-
if (outcome.kind === "kept") return `${childName} ended holding work that is not on the integration branch, so its worktree was kept: ${describeChildWorktree(outcome.worktree)}. The operator decides whether to merge or discard it`;
|
|
156
|
-
if (outcome.kind === "discarded") return `${childName} ended and its worktree was discarded`;
|
|
157
|
-
return `${childName} ended`;
|
|
158
|
-
}
|
|
159
|
-
function describeKeptChildAttention(childName, worktree) {
|
|
160
|
-
return `${childName}, a session this one spawned, ended holding unpushed work: ${describeChildWorktree(worktree)}`;
|
|
161
|
-
}
|
|
162
|
-
function describePushLanding(landing) {
|
|
163
|
-
if (!landing.merged) {
|
|
164
|
-
const reason = landing.reason || "unknown";
|
|
165
|
-
const explain = PUSH_REFUSAL_COPY[reason];
|
|
166
|
-
return {
|
|
167
|
-
status: PUSH_REFUSED_STATUS,
|
|
168
|
-
body: {
|
|
169
|
-
ok: false,
|
|
170
|
-
reason,
|
|
171
|
-
error: explain ? explain(landing) : `the merge stopped: ${reason}`
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
if (landing.pushed) return {
|
|
176
|
-
status: 200,
|
|
177
|
-
body: {
|
|
178
|
-
ok: true,
|
|
179
|
-
landedOn: landing.base ?? null,
|
|
180
|
-
pushed: true
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
return {
|
|
184
|
-
status: 200,
|
|
185
|
-
body: {
|
|
186
|
-
ok: true,
|
|
187
|
-
landedOn: landing.base ?? null,
|
|
188
|
-
pushed: false,
|
|
189
|
-
note: "landed locally and nothing was pushed, because the repository has no origin remote or origin did not take a fast-forward"
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
function projectReviews({ configured, reason, inFlight, queued, drafts }) {
|
|
194
|
-
const identity = ({ key, title, url, author }) => ({
|
|
195
|
-
key,
|
|
196
|
-
title,
|
|
197
|
-
url,
|
|
198
|
-
author
|
|
199
|
-
});
|
|
200
|
-
return {
|
|
201
|
-
ok: true,
|
|
202
|
-
configured,
|
|
203
|
-
reason: reason ?? null,
|
|
204
|
-
reviewing: inFlight.map((review) => ({
|
|
205
|
-
...identity(review),
|
|
206
|
-
phase: review.phase
|
|
207
|
-
})),
|
|
208
|
-
queued: queued.map(identity),
|
|
209
|
-
drafts: drafts.map((draft) => ({
|
|
210
|
-
...identity(draft),
|
|
211
|
-
status: draft.status,
|
|
212
|
-
verdict: draft.verdict,
|
|
213
|
-
summary: draft.summary,
|
|
214
|
-
comments: draft.comments.length,
|
|
215
|
-
isOutOfDate: draft.liveHead !== void 0 && draft.liveHead !== draft.reviewedHead
|
|
216
|
-
}))
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
function projectMyPrs(snapshot) {
|
|
220
|
-
if (!snapshot) return {
|
|
221
|
-
ok: true,
|
|
222
|
-
configured: false,
|
|
223
|
-
prs: []
|
|
224
|
-
};
|
|
225
|
-
return {
|
|
226
|
-
ok: true,
|
|
227
|
-
configured: snapshot.configured,
|
|
228
|
-
prs: snapshot.prs.map((pr) => ({
|
|
229
|
-
key: pr.key,
|
|
230
|
-
title: pr.title,
|
|
231
|
-
url: pr.url,
|
|
232
|
-
state: pr.state,
|
|
233
|
-
isDraft: pr.isDraft,
|
|
234
|
-
stage: pr.stage,
|
|
235
|
-
reviewDecision: pr.reviewDecision,
|
|
236
|
-
approvals: pr.approvals,
|
|
237
|
-
unresolvedThreads: pr.unresolvedThreads,
|
|
238
|
-
behindBy: pr.behindBy,
|
|
239
|
-
mergeable: pr.mergeable,
|
|
240
|
-
isInMergeQueue: pr.isInMergeQueue,
|
|
241
|
-
checks: {
|
|
242
|
-
state: pr.checks.state,
|
|
243
|
-
failing: pr.checks.failing
|
|
244
|
-
}
|
|
245
|
-
}))
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
function deriveChildSessionName(parentName, existingNames) {
|
|
249
|
-
const base = `${String(parentName || "").trim() || "session"} agent`;
|
|
250
|
-
const taken = new Set(existingNames);
|
|
251
|
-
let candidate = base;
|
|
252
|
-
let suffix = 1;
|
|
253
|
-
while (taken.has(candidate)) {
|
|
254
|
-
suffix += 1;
|
|
255
|
-
candidate = `${base} ${suffix}`;
|
|
256
|
-
}
|
|
257
|
-
return candidate;
|
|
258
|
-
}
|
|
259
|
-
var USAGE_COLUMN_WIDTH = 20;
|
|
260
|
-
var AGENT_COMMAND_HELP = Object.freeze({
|
|
261
|
-
spawn: {
|
|
262
|
-
usage: "spawn <prompt>",
|
|
263
|
-
summary: "Start a session on that prompt that this session owns until its task is done",
|
|
264
|
-
missingArgumentHint: "glimmervoid spawn: this command needs text, for example \"glimmervoid spawn check the failing test\""
|
|
265
|
-
},
|
|
266
|
-
send: {
|
|
267
|
-
usage: "send [child] <text>",
|
|
268
|
-
summary: "Message a session this one spawned; a spawned session messages its parent with send <text>",
|
|
269
|
-
missingArgumentHint: "glimmervoid send: this command needs text, for example \"glimmervoid send the tests pass\" or, to a spawned session, \"glimmervoid send <child> fix the failing test\""
|
|
270
|
-
},
|
|
271
|
-
read: {
|
|
272
|
-
usage: "read [child]",
|
|
273
|
-
summary: "Read what this session's spawned sessions sent; a spawned session reads its parent's messages",
|
|
274
|
-
missingArgumentHint: null
|
|
275
|
-
},
|
|
276
|
-
close: {
|
|
277
|
-
usage: "close <child>",
|
|
278
|
-
summary: "End a session this one spawned and discard its worktree; refused while it holds unpushed work unless --discard",
|
|
279
|
-
missingArgumentHint: "glimmervoid close: this command needs the spawned session, for example \"glimmervoid close <child>\""
|
|
280
|
-
},
|
|
281
|
-
attention: {
|
|
282
|
-
usage: "attention <note>",
|
|
283
|
-
summary: "Flag this session as needing the operator",
|
|
284
|
-
missingArgumentHint: "glimmervoid attention: this command needs text, for example \"glimmervoid attention check the failing test\""
|
|
285
|
-
},
|
|
286
|
-
board: {
|
|
287
|
-
usage: "board",
|
|
288
|
-
summary: "List the live sessions",
|
|
289
|
-
missingArgumentHint: null
|
|
290
|
-
},
|
|
291
|
-
push: {
|
|
292
|
-
usage: "push",
|
|
293
|
-
summary: "Land this session's commits on the integration branch and push it",
|
|
294
|
-
missingArgumentHint: null
|
|
295
|
-
},
|
|
296
|
-
edit: {
|
|
297
|
-
usage: "edit <path>",
|
|
298
|
-
summary: "Open a text file for the operator to edit in the dashboard",
|
|
299
|
-
missingArgumentHint: "glimmervoid edit: this command needs a file path, for example \"glimmervoid edit drafts/post.md\""
|
|
300
|
-
},
|
|
301
|
-
review: {
|
|
302
|
-
usage: "review <pr>",
|
|
303
|
-
summary: "Queue a team review of a pull request; the draft waits in the Reviews tab for the operator to post",
|
|
304
|
-
missingArgumentHint: "glimmervoid review: this command needs a pull request, for example \"glimmervoid review https://github.com/acme/app/pull/12\" or \"glimmervoid review acme/app#12\""
|
|
305
|
-
},
|
|
306
|
-
reviews: {
|
|
307
|
-
usage: "reviews",
|
|
308
|
-
summary: "List the Reviews tab: reviews running, queued and drafted",
|
|
309
|
-
missingArgumentHint: null
|
|
310
|
-
},
|
|
311
|
-
prs: {
|
|
312
|
-
usage: "prs",
|
|
313
|
-
summary: "List the operator's open pull requests with their checks, reviews and merge state",
|
|
314
|
-
missingArgumentHint: null
|
|
315
|
-
},
|
|
316
|
-
"link-pr": {
|
|
317
|
-
usage: "link-pr <pr>",
|
|
318
|
-
summary: "Link a pull request to this session's card",
|
|
319
|
-
missingArgumentHint: "glimmervoid link-pr: this command needs a pull request, for example \"glimmervoid link-pr acme/app#12\""
|
|
320
|
-
},
|
|
321
|
-
settings: {
|
|
322
|
-
usage: "settings list|get",
|
|
323
|
-
summary: "Read Glimmervoid's settings; a secret shows only as configured or not",
|
|
324
|
-
missingArgumentHint: null
|
|
325
|
-
},
|
|
326
|
-
limits: {
|
|
327
|
-
usage: "limits",
|
|
328
|
-
summary: "List which of these commands answer here and what the guardrails block",
|
|
329
|
-
missingArgumentHint: null
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
function agentCommandHelpLines() {
|
|
333
|
-
return Object.values(AGENT_COMMAND_HELP).map(({ usage, summary }) => ` ${usage.padEnd(USAGE_COLUMN_WIDTH)}${summary}`);
|
|
334
|
-
}
|
|
335
|
-
//#endregion
|
|
336
|
-
export { decideSpawnAllowance as a, describeKeptChildAttention as c, describeUnpushedCloseRefusal as d, isAgentApiVerb as f, projectReviews as h, decideAgentRequest as i, describePushLanding as l, projectMyPrs as m, REFUSAL_REASON as n, deriveChildSessionName as o, parseAgentVerb as p, agentCommandHelpLines as r, describeChildEndNote as s, AGENT_COMMAND_HELP as t, describeRoleRefusal as u };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { s as loadConfigFile } from "./config-store-2noiIsTg.js";
|