Ship prebuilt, so a git URL is the whole install
Grok Build loads plugins out of ~/.grok/plugins/ and clones marketplace sources straight from git; it never runs npm install or a build for you. So a plugin that ships TypeScript is a plugin you have to build by hand before it does anything, and the SessionStart hook's first act is to print "not built yet". dist/ is now committed, and the tree is arranged so that a bare clone can run: - The daemon is bundled to a single ESM file with rolldown, platform node. Its one runtime dependency (@simplewebauthn/server, plus the asn1/cbor tree under it) is inlined; the only imports left in the output are node: builtins. Not minified — a committed blob nobody can read is worse than no committed blob. - tsc no longer emits for the server, it only typechecks (noEmit). rolldown emits. - dist/web was already a self-contained static bundle. - The hook scripts under bin/ were stdlib-only from the start. .grok-plugin/marketplace.json makes the repo its own one-entry catalog with a local source of "./", so `grok plugin marketplace add <git-url>` followed by `grok plugin install grok-glance` works without pinning a SHA of itself. `npm run check:dist` rebuilds and fails if the committed output is stale — the one real hazard of checking in build output. Also drops the daemon's "non-default port, run `glance sync-hooks`" startup note, which the previous commit should have taken with the rest of that scheme; the hook scripts read config.json themselves, so a non-default port needs nothing. The e2e suite now takes GLANCE_ROOT and was run twice: once against the repo, once against a copy containing only tracked files plus dist/ and no node_modules — which is what actually demonstrates the claim, passkey registration and assertion included. 233 checks, both runs green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d20eb9255c
commit
3cfa011d0c
@@ -1,4 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "grok-glance",
|
||||||
|
"description": "One-entry marketplace: this repo is both the catalog and the plugin, so `grok plugin marketplace add <git-url>` is enough to install it.",
|
||||||
|
"owner": {
|
||||||
|
"name": "grok-glance"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "grok-glance",
|
||||||
|
"description": "A passkey-guarded web dashboard that lets you glance at what Grok Build is doing from your phone, and approve or deny risky tool calls remotely.",
|
||||||
|
"category": "monitoring",
|
||||||
|
"keywords": [
|
||||||
|
"grok-glance",
|
||||||
|
"glance dashboard",
|
||||||
|
"webauthn passkey",
|
||||||
|
"remote approval",
|
||||||
|
"session monitor"
|
||||||
|
],
|
||||||
|
"source": { "type": "local", "path": "./" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -37,49 +37,63 @@ or drive a session.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Node.js 20 or newer, and npm.
|
- Node.js 20 or newer. **npm is only needed to develop it** — `dist/` is committed, and the
|
||||||
|
daemon bundle carries its one runtime dependency inside it, so an installed copy never runs
|
||||||
|
a build or an install step.
|
||||||
- Grok Build.
|
- Grok Build.
|
||||||
- For phone access: [Tailscale](https://tailscale.com/) on both the machine and the phone. See
|
- For phone access: [Tailscale](https://tailscale.com/) on both the machine and the phone. See
|
||||||
[Why Tailscale](#why-tailscale-and-not-just-the-lan-ip) — a LAN IP genuinely cannot work.
|
[Why Tailscale](#why-tailscale-and-not-just-the-lan-ip) — a LAN IP genuinely cannot work.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
Grok Build loads plugins straight out of `~/.grok/plugins/`, so the shortest install is a clone:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone <this repo> grok-glance
|
git clone <this repo> ~/.grok/plugins/grok-glance
|
||||||
cd grok-glance
|
|
||||||
npm install && npm run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The build produces `dist/server` (the daemon) and `dist/web` (the dashboard). Both are required;
|
That is the whole thing — no `npm install`, no build. Open `/plugins` in Grok Build and enable
|
||||||
the daemon serves the dashboard itself. `hooks/hooks.json` is checked in as-is — nothing about it
|
**grok-glance**. On the next session start its `SessionStart` hook boots the daemon in the
|
||||||
is generated or machine-specific. The shared secret the hook scripts authenticate with lives in
|
background, and the dashboard is on `http://127.0.0.1:8791`.
|
||||||
`~/.grok/glance/hook.secret` (mode 0600) and is created by the daemon on first start; it never
|
|
||||||
appears in `hooks.json`.
|
|
||||||
|
|
||||||
Then register the directory with Grok Build. Plugins are installed from a marketplace catalog, so
|
### …or from a marketplace, by URL
|
||||||
for a local checkout the shortest path is a one-entry catalog. Create
|
|
||||||
`.grok-plugin/marketplace.json` in a directory that contains your checkout:
|
|
||||||
|
|
||||||
```json
|
If you would rather install it the way marketplace plugins are installed — or share it with other
|
||||||
{
|
machines — this repo is also its own one-entry marketplace (`.grok-plugin/marketplace.json`). Add
|
||||||
"name": "local",
|
it as a marketplace source and install from it:
|
||||||
"description": "Local plugins",
|
|
||||||
"owner": { "name": "me" },
|
```sh
|
||||||
"plugins": [
|
grok plugin marketplace add https://your-git-host/you/grok-glance.git
|
||||||
{
|
grok plugin install grok-glance --trust
|
||||||
"name": "grok-glance",
|
|
||||||
"description": "Passkey-guarded phone dashboard for Grok Build.",
|
|
||||||
"category": "monitoring",
|
|
||||||
"source": { "type": "local", "path": "./grok-glance" }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
…then add that marketplace and install `grok-glance` from Grok Build's `/plugin` interface.
|
Marketplace sources also live in `~/.grok/config.toml` under `[[marketplace.sources]]` and in
|
||||||
|
`~/.grok/plugins/known_marketplaces.json`, if you prefer to write them there directly. The TUI's
|
||||||
|
Marketplace tab reads the same list.
|
||||||
|
|
||||||
Once installed, the daemon starts by itself: the `SessionStart` hook boots it in the background on
|
### Why there is no build step
|
||||||
the first session after installation.
|
|
||||||
|
`dist/` is checked in:
|
||||||
|
|
||||||
|
- `dist/server/index.js` — the daemon, bundled to a single dependency-free ESM file. Its only
|
||||||
|
runtime dependency, `@simplewebauthn/server`, is inlined; everything else it uses is the Node
|
||||||
|
standard library. It is *not* minified, so what ships is what you can read.
|
||||||
|
- `dist/web/` — the dashboard, already a static bundle, which the daemon serves itself.
|
||||||
|
|
||||||
|
The hook scripts under `bin/` were stdlib-only from the start. So a clone has nothing to resolve
|
||||||
|
and nothing to compile, which is what makes a bare git URL enough.
|
||||||
|
|
||||||
|
Working on it instead? Then you do need the toolchain:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm run build # tsc typechecks, rolldown bundles the server, vite builds the web app
|
||||||
|
npm run check:dist # rebuilds and fails if the committed dist/ is stale
|
||||||
|
```
|
||||||
|
|
||||||
|
`hooks/hooks.json` is checked in as-is — nothing about it is generated or machine-specific. The
|
||||||
|
shared secret the hook scripts authenticate with lives in `~/.grok/glance/hook.secret` (mode 0600)
|
||||||
|
and is created by the daemon on first start; it never appears in `hooks.json`.
|
||||||
|
|
||||||
## Get it onto your phone
|
## Get it onto your phone
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -77,7 +77,11 @@ function sessionBreakdown(states) {
|
|||||||
|
|
||||||
function requireBuild() {
|
function requireBuild() {
|
||||||
if (!fs.existsSync(SERVER_ENTRY)) {
|
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`);
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -28,9 +28,10 @@ async function ensureDaemon(cfg) {
|
|||||||
if (await isDaemonUp(cfg)) return true;
|
if (await isDaemonUp(cfg)) return true;
|
||||||
|
|
||||||
if (!fs.existsSync(SERVER_ENTRY)) {
|
if (!fs.existsSync(SERVER_ENTRY)) {
|
||||||
// Not built yet. Say so once, on stderr, where it is recorded but harmless.
|
// dist/ ships with the plugin, so this means an incomplete checkout. Say so once, on
|
||||||
|
// stderr, where it is recorded but harmless — a hook must never fail a session.
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`[grok-glance] not built yet - run \`npm install && npm run build\` in ${PLUGIN_ROOT}\n`,
|
`[grok-glance] dist/ is missing - run \`npm install && npm run build\` in ${PLUGIN_ROOT}\n`,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -12,8 +12,9 @@ If no argument was given, treat it as `status`.
|
|||||||
Then:
|
Then:
|
||||||
|
|
||||||
1. Run `node "$GROK_PLUGIN_ROOT/bin/glance" $ARGUMENTS`.
|
1. Run `node "$GROK_PLUGIN_ROOT/bin/glance" $ARGUMENTS`.
|
||||||
2. If it says the plugin is not built, run `npm install && npm run build` in `$GROK_PLUGIN_ROOT`
|
2. The plugin ships prebuilt, so this should just work. If it does say the plugin is not built,
|
||||||
(this takes a minute or two) and try again.
|
`dist/` is missing from the checkout: run `npm install && npm run build` in
|
||||||
|
`$GROK_PLUGIN_ROOT` (a minute or two) and try again.
|
||||||
3. Report what came back. For `enroll`, show the URL and the code verbatim — the user needs to
|
3. Report what came back. For `enroll`, show the URL and the code verbatim — the user needs to
|
||||||
type them on their phone, so do not paraphrase or reformat them.
|
type them on their phone, so do not paraphrase or reformat them.
|
||||||
4. If the output mentions that no public origin is configured, explain the Tailscale Serve setup:
|
4. If the output mentions that no public origin is configured, explain the Tailscale Serve setup:
|
||||||
|
|||||||
Vendored
+23758
File diff suppressed because it is too large
Load Diff
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+15
File diff suppressed because one or more lines are too long
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||||
|
<rect width="64" height="64" rx="14" fill="#09090b" />
|
||||||
|
<circle cx="32" cy="32" r="15" fill="none" stroke="#fafafa" stroke-width="3.5" />
|
||||||
|
<circle cx="32" cy="32" r="5.5" fill="#fafafa" />
|
||||||
|
<path d="M32 9v5M32 50v5M9 32h5M50 32h5" stroke="#71717a" stroke-width="3.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 389 B |
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<!-- viewport-fit=cover so the sticky header sits under the notch rather than beside it. -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
||||||
|
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme: dark)" />
|
||||||
|
<title>grok-glance</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
|
<link rel="apple-touch-icon" href="/icon.svg" />
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="glance" />
|
||||||
|
<script type="module" crossorigin src="/assets/index-D-dJ5pn0.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-BZSiLyex.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "grok-glance",
|
||||||
|
"short_name": "glance",
|
||||||
|
"description": "Glance at what Grok Build is doing.",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"background_color": "#09090b",
|
||||||
|
"theme_color": "#09090b",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+1
@@ -21,6 +21,7 @@
|
|||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "6.0.2",
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
|
"rolldown": "1.0.3",
|
||||||
"tailwind-variants": "3.3.0",
|
"tailwind-variants": "3.3.0",
|
||||||
"tailwindcss": "4.3.1",
|
"tailwindcss": "4.3.1",
|
||||||
"typescript": "5.6.3",
|
"typescript": "5.6.3",
|
||||||
|
|||||||
+4
-2
@@ -9,11 +9,12 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:server && npm run build:web",
|
"build": "npm run build:server && npm run build:web",
|
||||||
"build:server": "tsc -p tsconfig.server.json",
|
"build:server": "tsc -p tsconfig.server.json && rolldown server/src/index.ts -o dist/server/index.js -f esm -p node",
|
||||||
"build:web": "tsc -p tsconfig.web.json && vite build",
|
"build:web": "tsc -p tsconfig.web.json && vite build",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"start": "node dist/server/index.js",
|
"start": "node dist/server/index.js",
|
||||||
"glance": "node bin/glance"
|
"glance": "node bin/glance",
|
||||||
|
"check:dist": "npm run build && test -z \"$(git status --porcelain dist)\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroui/react": "3.2.4",
|
"@heroui/react": "3.2.4",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "6.0.2",
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
|
"rolldown": "1.0.3",
|
||||||
"tailwind-variants": "3.3.0",
|
"tailwind-variants": "3.3.0",
|
||||||
"tailwindcss": "4.3.1",
|
"tailwindcss": "4.3.1",
|
||||||
"typescript": "5.6.3",
|
"typescript": "5.6.3",
|
||||||
|
|||||||
+3
-3
@@ -18,7 +18,6 @@ import http from "node:http";
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { URL } from "node:url";
|
import { URL } from "node:url";
|
||||||
import {
|
import {
|
||||||
DEFAULT_PORT,
|
|
||||||
VERSION,
|
VERSION,
|
||||||
deriveRpId,
|
deriveRpId,
|
||||||
ensureHome,
|
ensureHome,
|
||||||
@@ -564,8 +563,9 @@ server.listen(cfg.port, cfg.host, () => {
|
|||||||
console.log(`[glance] state: ${paths.home}`);
|
console.log(`[glance] state: ${paths.home}`);
|
||||||
console.log(`[glance] origin: ${cfg.origin ?? "(none set - see README)"} rpId: ${cfg.rpId ?? "localhost"}`);
|
console.log(`[glance] origin: ${cfg.origin ?? "(none set - see README)"} rpId: ${cfg.rpId ?? "localhost"}`);
|
||||||
console.log(`[glance] accepts assertions from: ${expectedOrigins(cfg).join(", ")}`);
|
console.log(`[glance] accepts assertions from: ${expectedOrigins(cfg).join(", ")}`);
|
||||||
if (!webBuildExists()) console.log("[glance] web app not built yet: npm install && npm run build");
|
// dist/ is committed, so this only fires for a developer who deleted it. Nothing warns about a
|
||||||
if (cfg.port !== DEFAULT_PORT) console.log(`[glance] note: non-default port, run \`glance sync-hooks\``);
|
// non-default port any more: the hook scripts read config.json themselves.
|
||||||
|
if (!webBuildExists()) console.log("[glance] web app missing from dist/: npm install && npm run build");
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("error", (err) => {
|
server.on("error", (err) => {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function webBuildExists(): boolean {
|
|||||||
export function serveStatic(urlPath: string, res: ServerResponse): void {
|
export function serveStatic(urlPath: string, res: ServerResponse): void {
|
||||||
if (!webBuildExists()) {
|
if (!webBuildExists()) {
|
||||||
res.writeHead(503, { ...SECURITY_HEADERS, "content-type": "text/plain; charset=utf-8" });
|
res.writeHead(503, { ...SECURITY_HEADERS, "content-type": "text/plain; charset=utf-8" });
|
||||||
res.end("grok-glance: web app not built yet. Run `npm install && npm run build`.\n");
|
res.end("grok-glance: dist/web is missing. Run `npm install && npm run build`.\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-10
@@ -14,16 +14,12 @@ same dashboard — no per-session setup.
|
|||||||
The daemon is started automatically by the `SessionStart` hook. Everything below is done through
|
The daemon is started automatically by the `SessionStart` hook. Everything below is done through
|
||||||
the `glance` CLI at `$GROK_PLUGIN_ROOT/bin/glance`.
|
the `glance` CLI at `$GROK_PLUGIN_ROOT/bin/glance`.
|
||||||
|
|
||||||
## First check whether it is even built
|
## No build step
|
||||||
|
|
||||||
The plugin ships as TypeScript and must be built once:
|
The plugin ships prebuilt: `dist/` is committed, and the daemon is a single dependency-free
|
||||||
|
bundle. A clone is ready to run. Only reach for `npm install && npm run build` in
|
||||||
```sh
|
`$GROK_PLUGIN_ROOT` if `glance status` actually says it is not built, which means `dist/` was
|
||||||
cd "$GROK_PLUGIN_ROOT" && npm install && npm run build
|
deleted from the checkout.
|
||||||
```
|
|
||||||
|
|
||||||
`glance status` prints a "not built" error with this same instruction if it is missing. Do not
|
|
||||||
attempt to skip the build — the daemon entry point is `dist/server/index.js`.
|
|
||||||
|
|
||||||
## The commands
|
## The commands
|
||||||
|
|
||||||
@@ -99,7 +95,8 @@ and wait for a tap on the phone. Defaults that matter:
|
|||||||
secret in `$GLANCE_HOME/hook.secret` no longer matches the one the daemon loaded (events are being
|
secret in `$GLANCE_HOME/hook.secret` no longer matches the one the daemon loaded (events are being
|
||||||
dropped with a 403); `glance stop && glance up` fixes that. Otherwise check that
|
dropped with a 403); `glance stop && glance up` fixes that. Otherwise check that
|
||||||
`hooks/hooks.json` exists and that the plugin is registered with Grok Build.
|
`hooks/hooks.json` exists and that the plugin is registered with Grok Build.
|
||||||
- **Page says "run npm install && npm run build"** → the web bundle is missing; build it.
|
- **Page says "run npm install && npm run build"** → `dist/web` is missing from the checkout,
|
||||||
|
which should not happen in a clone. Re-clone, or build it.
|
||||||
- **Only one agent shows up** → the others were started before the plugin was installed, or in an
|
- **Only one agent shows up** → the others were started before the plugin was installed, or in an
|
||||||
environment where the hooks are not registered. A session appears on its next hook event; nothing
|
environment where the hooks are not registered. A session appears on its next hook event; nothing
|
||||||
can be back-filled for one that already ran.
|
can be back-filled for one that already ran.
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"outDir": "dist/server",
|
"noEmit": true,
|
||||||
"rootDir": "server/src",
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
@@ -15,8 +14,7 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"declaration": false,
|
"declaration": false
|
||||||
"sourceMap": true
|
|
||||||
},
|
},
|
||||||
"include": ["server/src"]
|
"include": ["server/src"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user