Files
iceBear67andClaude Opus 5 966133eeda Start the daemon from whichever hook fires first
A plugin's SessionStart hook never runs, so nothing was starting the
daemon: no daemon.log, nothing on :8791, and a manual `glance up`
working perfectly.

Grok Build dispatches SessionStart from inside session creation
(xai-grok-shell, agent_ops.rs -> DispatchSessionStartHook) and resolves
it against the session's hook registry as it stands at that moment.
That registry comes from discover_hooks(), whose sources are the config
layers and the global/project settings files; plugin directories are not
among them. Plugin hooks are appended later, under a plugin/ prefix, by
reload_hooks_impl and reload_plugins_impl - which run in response to a
plugin action, a /hooks reload, or a folder-trust grant. So the entry is
always registered after the event it subscribes to has been dispatched.
The other thirteen events work because they happen later in the session.

There is no boot event to move to, so every recorder boots the daemon
instead and whichever fires first wins. The cost is one loopback request
to /healthz per event once it is up, which is the steady state. A
daemon.lock (O_EXCL, 15s staleness takeover) keeps a burst of concurrent
events from starting five daemons and leaving four to die on EADDRINUSE.

glance-up.mjs stays wired: it costs nothing when it does not fire, and
it is the right hook for the job if that ordering is ever fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 08:04:03 +00:00

241 lines
7.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 {
PLUGIN_ROOT,
SERVER_ENTRY,
baseUrl,
ensureDaemon,
glanceHome,
isDaemonUp,
readConfig,
} from "./glance-lib.mjs";
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: ${SERVER_ENTRY} is missing.\n\n` +
`dist/ ships with the plugin, so this checkout is incomplete. Rebuild it:\n\n` +
` cd ${PLUGIN_ROOT}\n npm install && npm run build\n`,
);
process.exit(1);
}
}
async function ensureUp() {
requireBuild();
// No hook timeout to fit inside here, so wait long enough that a slow cold start still counts.
if (await ensureDaemon(cfg, { waitMs: 8000, startedBy: "cli" })) return true;
console.error(`daemon did not come up; see ${path.join(glanceHome(), "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 "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 logs tail the daemon log
`);
}