Files
grok-glance/bin/glance
T
iceBear67 6d34a17d9d Supervise several agents at once
One daemon already saw every session; the dashboard only ever showed one of
them well. Under four parallel agents it failed in specific ways, each fixed
here:

- Sessions were identified by workspace basename, so two agents in one repo
  were indistinguishable. The daemon now hands out a small ordinal badge in
  arrival order and the web UI colours each agent by it — rows, timeline
  lines, and approval cards, which previously asked you to approve `rm -rf`
  "in remote-grok" without saying which one.
- `currentTool` held a single call, so parallel tools overwrote each other.
  It is now a list; durations are matched FIFO per tool name, since hook
  payloads carry no call id.
- One 400-event ring, evicted oldest-first, let a chatty agent blank
  everyone else's history. Eviction now takes from whichever session holds
  the most of the ring.
- Sessions sorted by recency jumped under a moving thumb. They are ordered
  waiting -> error -> working -> idle -> ended, ties on badge, so an agent
  keeps its slot.
- Events carry no workspace root, so a restart came back with a full
  timeline and an empty roster. The session map is persisted to
  sessions.json (debounced, flushed on shutdown, 12h cutoff on load).
  In-flight tools are dropped on the way out: they belonged to a process
  that no longer exists.
- Snapshot pushes now back off to 1s once a snapshot exceeds 24KB, since
  full-snapshot SSE cost scales with agent count.
- Pending approvals are ordered by which expires first, not by arrival.

`glance status` and /local/status report the roster by state. e2e suite:
220 passed, 0 failed.
2026-08-09 05:27:00 +00:00

267 lines
8.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* grok-glance CLI.
*
* Privileged operations (minting enrollment codes, revoking devices) are authenticated with
* a local admin token read from $GLANCE_HOME/admin.token, not by "is this request from
* localhost". That distinction matters: `tailscale serve` proxies remote traffic to
* 127.0.0.1, so the daemon cannot tell a local caller from a tunnelled one by address alone.
*/
import fs from "node:fs";
import path from "node:path";
import { spawn, spawnSync } from "node:child_process";
import {
PLUGIN_ROOT,
baseUrl,
glanceHome,
isDaemonUp,
readConfig,
sleep,
} from "./glance-lib.mjs";
const SERVER_ENTRY = path.join(PLUGIN_ROOT, "dist", "server", "index.js");
const cfg = readConfig();
const cmd = process.argv[2] ?? "status";
const args = process.argv.slice(3);
function adminToken() {
try {
return fs.readFileSync(path.join(glanceHome(), "admin.token"), "utf8").trim();
} catch {
return null;
}
}
async function api(pathname, { method = "GET", body, admin = false } = {}) {
const headers = { "content-type": "application/json" };
if (admin) {
const token = adminToken();
if (!token) {
throw new Error("no admin token found - is the daemon running? try `glance up`");
}
headers["x-glance-admin"] = token;
}
const res = await fetch(`${baseUrl(cfg)}${pathname}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(10_000),
});
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = null;
}
if (!res.ok) throw new Error(data?.error ?? `${res.status} ${res.statusText}`);
return data;
}
/** "3 (2 working, 1 waiting on you)" — a raw count says nothing when you watch several agents. */
function sessionBreakdown(states) {
if (!states || typeof states !== "object") return "";
const order = [
["waiting", "waiting on you"],
["error", "error"],
["working", "working"],
["idle", "idle"],
["ended", "ended"],
];
const parts = order
.filter(([key]) => Number(states[key]) > 0)
.map(([key, label]) => `${states[key]} ${label}`);
return parts.length ? ` (${parts.join(", ")})` : "";
}
function requireBuild() {
if (!fs.existsSync(SERVER_ENTRY)) {
console.error(`grok-glance is not built yet.\n\n cd ${PLUGIN_ROOT}\n npm install && npm run build\n`);
process.exit(1);
}
}
async function ensureUp() {
if (await isDaemonUp(cfg)) return true;
requireBuild();
const home = glanceHome();
fs.mkdirSync(home, { recursive: true });
const logFd = fs.openSync(path.join(home, "daemon.log"), "a");
const child = spawn(process.execPath, [SERVER_ENTRY], {
detached: true,
stdio: ["ignore", logFd, logFd],
});
child.unref();
for (let i = 0; i < 40; i++) {
await sleep(200);
if (await isDaemonUp(cfg, 300)) return true;
}
console.error(`daemon did not come up; see ${path.join(home, "daemon.log")}`);
return false;
}
switch (cmd) {
case "serve":
case "start": {
requireBuild();
await import(SERVER_ENTRY);
break;
}
case "up": {
if (await ensureUp()) console.log(`grok-glance running on ${baseUrl(cfg)}`);
else process.exit(1);
break;
}
case "stop": {
if (!(await isDaemonUp(cfg))) {
console.log("not running");
break;
}
await api("/local/shutdown", { method: "POST", admin: true });
console.log("stopped");
break;
}
case "status": {
if (!(await isDaemonUp(cfg))) {
console.log(`grok-glance: not running (port ${cfg.port})`);
console.log("start it with: glance up");
break;
}
const s = await api("/local/status", { admin: true });
console.log(`grok-glance ${s.version} on ${baseUrl(cfg)}`);
console.log(` public origin : ${s.origin ?? "(not configured - see README)"}`);
console.log(` rp id : ${s.rpId ?? "(not configured)"}`);
console.log(` devices : ${s.devices}`);
console.log(` approval mode : ${s.approval.mode}`);
console.log(` watchers : ${s.watchers}`);
console.log(` sessions : ${s.sessions}${sessionBreakdown(s.sessionStates)}`);
console.log(` events kept : ${s.events}`);
if (s.hookAuthOk === false) {
console.log(
"\n ! hook auth mismatch: $GLANCE_HOME/hook.secret no longer matches what the daemon" +
"\n loaded, so events are being dropped. Restart it: glance stop && glance up",
);
}
if (s.devices === 0) console.log("\nNo device enrolled yet. Run: glance enroll");
break;
}
case "enroll": {
if (!(await ensureUp())) process.exit(1);
const out = await api("/local/enroll", { method: "POST", admin: true });
console.log("\n Open this on your phone:\n");
console.log(` ${out.url}\n`);
console.log(` Enrollment code: ${out.code}`);
console.log(` Valid for: ${Math.round(out.expiresInMs / 60000)} minutes (single use)\n`);
if (!out.originConfigured) {
console.log(" Note: no public origin configured yet, so the URL above is localhost.");
console.log(" Set one up first (see README), e.g.:\n");
console.log(" tailscale serve --bg 127.0.0.1:" + cfg.port);
console.log(" glance set-origin https://<your-box>.<tailnet>.ts.net\n");
}
break;
}
case "set-origin": {
const origin = args[0];
if (!origin) {
console.error("usage: glance set-origin https://your-box.tailnet.ts.net");
process.exit(1);
}
if (!(await ensureUp())) process.exit(1);
const out = await api("/local/origin", { method: "POST", admin: true, body: { origin } });
console.log(`origin : ${out.origin}`);
console.log(`rp id : ${out.rpId}`);
console.log("\nEnrolled devices are bound to the rp id. Changing it invalidates them.");
break;
}
case "devices": {
if (!(await isDaemonUp(cfg))) {
console.error("not running");
process.exit(1);
}
const out = await api("/local/devices", { admin: true });
if (!out.devices.length) {
console.log("no devices enrolled - run: glance enroll");
break;
}
for (const d of out.devices) {
console.log(`${d.id.slice(0, 16)}${d.label.padEnd(24)} added ${new Date(d.createdAt).toISOString().slice(0, 10)} last seen ${d.lastUsedAt ? new Date(d.lastUsedAt).toISOString().slice(0, 16).replace("T", " ") : "never"}`);
}
break;
}
case "revoke": {
if (!args[0]) {
console.error("usage: glance revoke <device-id-prefix>");
process.exit(1);
}
const out = await api("/local/devices/revoke", {
method: "POST",
admin: true,
body: { idPrefix: args[0] },
});
console.log(`revoked ${out.revoked} device(s)`);
break;
}
case "approval": {
const mode = args[0];
if (!["off", "risky", "all"].includes(mode)) {
console.error("usage: glance approval <off|risky|all>");
process.exit(1);
}
const out = await api("/local/approval", { method: "POST", admin: true, body: { mode } });
console.log(`approval mode: ${out.mode}`);
break;
}
case "sync-hooks": {
// hooks.json is generated, not edited: regenerate it from hooks/hooks.template.json.
// The port is not baked into it any more — the hook scripts read config.json themselves —
// so the thing this actually refreshes is the approval gate's timeout, plus the hook
// secret if it has gone missing.
const { status } = spawnSync(
process.execPath,
[path.join(PLUGIN_ROOT, "scripts", "gen-hooks.mjs")],
{ stdio: "inherit" },
);
if (status !== 0) process.exit(status ?? 1);
if (await isDaemonUp(cfg)) {
console.log("restart the daemon to pick up config changes: glance stop && glance up");
}
break;
}
case "logs": {
const file = path.join(glanceHome(), "daemon.log");
if (!fs.existsSync(file)) {
console.log("no log yet");
break;
}
process.stdout.write(fs.readFileSync(file, "utf8").split("\n").slice(-60).join("\n") + "\n");
break;
}
default:
console.log(`grok-glance - glance at Grok Build from your phone
glance up start the daemon in the background
glance serve run it in the foreground
glance stop stop it
glance status show what is running
glance enroll mint a one-time code to enrol a phone
glance set-origin <url> set the public https origin (and webauthn rp id)
glance devices list enrolled devices
glance revoke <id-prefix> revoke a device
glance approval <off|risky|all> remote approval policy
glance sync-hooks regenerate hooks/hooks.json from the template
glance logs tail the daemon log
`);
}