first commit

This commit is contained in:
iceBear67
2026-08-09 04:00:13 +00:00
commit b3b6bf3f70
46 changed files with 7146 additions and 0 deletions
Executable
+247
View File
@@ -0,0 +1,247 @@
#!/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 } 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;
}
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}`);
console.log(` events kept : ${s.events}`);
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": {
const file = path.join(PLUGIN_ROOT, "hooks", "hooks.json");
const doc = JSON.parse(fs.readFileSync(file, "utf8"));
let changed = 0;
for (const groups of Object.values(doc.hooks ?? {})) {
for (const group of groups) {
for (const h of group.hooks ?? []) {
if (h.type === "http" && typeof h.url === "string") {
const next = h.url.replace(/127\.0\.0\.1:\d+/, `127.0.0.1:${cfg.port}`);
if (next !== h.url) changed++;
h.url = next;
}
}
}
}
fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
console.log(`rewrote ${changed} hook url(s) to port ${cfg.port}`);
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 rewrite hook urls after a port change
glance logs tail the daemon log
`);
}
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env node
/**
* PreToolUse hook: the remote approval gate.
*
* Asks the daemon what to do with this tool call. The daemon holds the request open while
* your phone decides, then answers allow/deny. Every failure path here is fail-open —
* a daemon that is down, slow, or confused must not be able to block your agent.
*
* Denying is the only outcome that changes behaviour, and approving only lets through a
* call Grok was already about to make. This hook can never introduce a new command.
*/
import { baseUrl, envEnvelope, postJson, readConfig, readStdinJson } from "./glance-lib.mjs";
function allow() {
process.exit(0);
}
function deny(reason) {
// Belt and braces: the documented deny signals are a JSON decision on stdout *and*
// exit code 2. We emit both so a change in precedence cannot silently allow.
process.stdout.write(
JSON.stringify({ decision: "deny", reason: reason || "Denied from grok-glance" }),
);
process.exit(2);
}
const payload = envEnvelope(await readStdinJson());
const cfg = readConfig();
// Stay inside the hook timeout declared in hooks/hooks.json (125s).
const waitMs = Math.min(115_000, Number(cfg.approval?.timeoutMs ?? 90_000) + 15_000);
try {
const { data } = await postJson(`${baseUrl(cfg)}/hook/approve`, payload, waitMs);
if (data && data.decision === "deny") deny(data.reason);
allow();
} catch {
allow();
}
+107
View File
@@ -0,0 +1,107 @@
/**
* Shared helpers for the grok-glance hook scripts and CLI.
*
* Deliberately dependency-free and stdlib-only: these run on the critical path of
* every Grok Build tool call, so they must start fast and never wedge a session.
*/
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { fileURLToPath } from "node:url";
export const PLUGIN_ROOT = path.resolve(fileURLToPath(import.meta.url), "../..");
export const DEFAULT_PORT = 8791;
/**
* State lives in one fixed place so that hooks (which get GROK_PLUGIN_DATA) and the
* CLI (which does not) always agree on where config, credentials and events are.
*/
export function glanceHome() {
if (process.env.GLANCE_HOME) return path.resolve(process.env.GLANCE_HOME);
return path.join(os.homedir(), ".grok", "glance");
}
export function readConfig() {
const file = path.join(glanceHome(), "config.json");
let raw = {};
try {
raw = JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
// No config yet, or unreadable: defaults are always usable.
}
const port = Number(process.env.GLANCE_PORT ?? raw.port ?? DEFAULT_PORT);
return {
...raw,
port: Number.isFinite(port) ? port : DEFAULT_PORT,
host: raw.host ?? "127.0.0.1",
};
}
export function baseUrl(cfg = readConfig()) {
return `http://127.0.0.1:${cfg.port}`;
}
/** Read the hook payload that Grok Build writes to stdin. Returns {} if there is none. */
export async function readStdinJson() {
if (process.stdin.isTTY) return {};
const chunks = [];
try {
for await (const chunk of process.stdin) chunks.push(chunk);
} catch {
return {};
}
const text = Buffer.concat(chunks).toString("utf8").trim();
if (!text) return {};
try {
return JSON.parse(text);
} catch {
return {};
}
}
/**
* Grok Build also passes the event in the environment. We merge it in so a payload that
* is missing fields (or absent entirely) still produces a usable event.
*/
export function envEnvelope(payload) {
return {
hookEventName: payload.hookEventName ?? process.env.GROK_HOOK_EVENT ?? "Unknown",
sessionId: payload.sessionId ?? process.env.GROK_SESSION_ID ?? "unknown",
workspaceRoot:
payload.workspaceRoot ?? process.env.GROK_WORKSPACE_ROOT ?? payload.cwd ?? process.cwd(),
cwd: payload.cwd ?? process.cwd(),
hookName: process.env.GROK_HOOK_NAME ?? undefined,
...payload,
};
}
export async function postJson(url, body, timeoutMs) {
const res = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeoutMs),
});
const text = await res.text();
if (!text) return { status: res.status, data: null };
try {
return { status: res.status, data: JSON.parse(text) };
} catch {
return { status: res.status, data: null };
}
}
export async function isDaemonUp(cfg = readConfig(), timeoutMs = 400) {
try {
const res = await fetch(`${baseUrl(cfg)}/healthz`, {
signal: AbortSignal.timeout(timeoutMs),
});
return res.ok;
} catch {
return false;
}
}
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env node
/**
* SessionStart hook: make sure the glance daemon is running, then record the event.
*
* This is the only hook that spawns anything. It always exits 0 — a monitoring
* dashboard must never be the reason a Grok Build session fails to start.
*/
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import {
PLUGIN_ROOT,
baseUrl,
envEnvelope,
glanceHome,
isDaemonUp,
postJson,
readConfig,
readStdinJson,
sleep,
} from "./glance-lib.mjs";
const SERVER_ENTRY = path.join(PLUGIN_ROOT, "dist", "server", "index.js");
async function ensureDaemon(cfg) {
if (await isDaemonUp(cfg)) return true;
if (!fs.existsSync(SERVER_ENTRY)) {
// Not built yet. Say so once, on stderr, where it is recorded but harmless.
process.stderr.write(
`[grok-glance] not built yet - run \`npm install && npm run build\` in ${PLUGIN_ROOT}\n`,
);
return false;
}
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],
env: { ...process.env, GLANCE_STARTED_BY: "hook" },
});
child.unref();
// Give it a moment to bind before the first http hook fires.
for (let i = 0; i < 40; i++) {
await sleep(200);
if (await isDaemonUp(cfg, 300)) return true;
}
return false;
}
const payload = envEnvelope(await readStdinJson());
const cfg = readConfig();
try {
const up = await ensureDaemon(cfg);
if (up) {
await postJson(`${baseUrl(cfg)}/hook/record`, payload, 2500);
}
} catch {
// Fail open, always.
}
process.exit(0);