#!/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://..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 "); 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 "); 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 set the public https origin (and webauthn rp id) glance devices list enrolled devices glance revoke revoke a device glance approval remote approval policy glance logs tail the daemon log `); }