grokbox 0.0.1 → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,34 +1,67 @@
1
1
  {
2
2
  "name": "grokbox",
3
- "version": "0.0.1",
4
- "description": "Unofficial local CLI for inspecting and messaging a running Grok Bot host",
3
+ "version": "0.1.0-alpha.0",
4
+ "packageManager": "bun@1.3.14",
5
+ "description": "Unofficial CLI and control plane for Grok Bot cloud computers",
6
+ "license": "MIT",
7
+ "author": "grokbox contributors",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/yoyooyooo/grokbox.git"
11
+ },
12
+ "homepage": "https://github.com/yoyooyooo/grokbox#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/yoyooyooo/grokbox/issues"
15
+ },
5
16
  "type": "module",
6
17
  "bin": {
7
- "grok-bot": "bin/grok-bot"
18
+ "grokbox": "bin/grokbox",
19
+ "gbox": "bin/grokbox"
8
20
  },
9
21
  "files": [
10
22
  "bin",
23
+ "dist",
11
24
  "skills",
12
- "src"
25
+ "LICENSE",
26
+ "THIRD_PARTY_NOTICES"
13
27
  ],
14
28
  "keywords": [
29
+ "grokbox",
15
30
  "grok-bot",
16
31
  "cli",
17
32
  "agents",
33
+ "cloud-computer",
18
34
  "automation"
19
35
  ],
20
36
  "engines": {
21
- "bun": ">=1.3.0"
37
+ "node": ">=20.0.0"
22
38
  },
23
39
  "publishConfig": {
24
- "access": "public"
40
+ "access": "public",
41
+ "provenance": true
25
42
  },
26
43
  "scripts": {
27
- "grok-bot": "bun src/index.ts",
44
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --outfile=dist/index.js",
45
+ "grokbox": "bun run src/index.ts",
46
+ "gbox": "bun run src/index.ts",
47
+ "shim:install": "bun run scripts/install-local-shim.mjs",
48
+ "shim:verify": "bun test test/local-shim.test.ts",
49
+ "prepack": "bun run build",
28
50
  "test": "bun test",
29
- "typecheck": "tsc --noEmit -p tsconfig.json"
51
+ "typecheck": "tsc --noEmit -p tsconfig.json",
52
+ "verify:package": "bun test test/packaging.test.ts",
53
+ "release:check": "bun run typecheck && bun test && bun run verify:package",
54
+ "verify:external": "node scripts/verify-external.mjs",
55
+ "observe:sandbox": "node scripts/observe-sandbox.mjs",
56
+ "experiment:sandbox-ssh-activity": "node scripts/experiment-sandbox-ssh-activity.mjs",
57
+ "monitor:sandbox-ssh-activity": "node scripts/monitor-sandbox-ssh-activity.mjs"
30
58
  },
31
- "dependencies": {
32
- "commander": "^14.0.0"
59
+ "dependencies": {},
60
+ "devDependencies": {
61
+ "commander": "^14.0.0",
62
+ "@types/bun": "^1.2.21",
63
+ "@types/node": "^26.4.0",
64
+ "esbuild": "^0.28.2",
65
+ "typescript": "^5.9.2"
33
66
  }
34
67
  }
package/skills/core.md CHANGED
@@ -1,81 +1,127 @@
1
- # grok-bot core
1
+ # grokbox core
2
2
 
3
- Version-matched usage guide for the box-local Grok Bot CLI. Load this file from the installed
4
- binary (`grok-bot skills get core --full`) instead of copying command docs into an Agent skill
3
+ Version-matched usage guide for the local/remote Grok Bot CLI and box daemon. Load this file from the installed
4
+ binary (`grokbox skills get core --full`) instead of copying command docs into an Agent skill
5
5
  stub.
6
6
 
7
7
  ## What this CLI is
8
8
 
9
- `grok-bot` is an agent-first adapter, not a 151-method Gateway wrapper.
9
+ `grokbox` is an agent-first adapter, not a raw Gateway wrapper. The published package requires Node.js 20+; both `grokbox` and `gbox` resolve to the same shim, which returns stable `runtime_unsupported` before loading the bundle on an older runtime. Bun is development tooling only.
10
10
 
11
11
  ```text
12
12
  Agent / Skill
13
- -> grok-bot
14
- -> /home/box/sand-data/gateway.json
15
- -> loopback HTTP + Bearer (in-process only)
13
+ -> grokbox + selected Profile
14
+ -> local or remote daemon (finite Grok + governed host capabilities)
15
+ -> Gateway discovery + loopback HTTP
16
+ -> admitted named filesystem roots
17
+ -> executable policy + durable Jobs
18
+ -> direct Gateway discovery + loopback HTTP (Grok-only local compatibility fallback)
19
+ -> external Cursor Sandbox control plane
20
+ -> read-only run state
21
+ -> EnsureSandBox + brokered exec no-op
22
+ -> explicit Cursor web quota source
23
+ -> one fresh sanitized account-quota snapshot
24
+ -> daemon desktop classifier / idle prune
16
25
  -> Grok Bot host Gateway
17
26
  ```
18
27
 
19
- It does two jobs: read a narrow state surface, and append one Human message to a 1:1 agent or a
20
- product group. It does not impersonate peer delivery.
28
+ The current source reads and manages a narrow product roster surface, appends one Human message to an ordinary agent or product group, reads explicitly admitted cloud-computer files, runs allowlisted structured processes as durable Jobs, queries an explicitly configured fresh account-quota source, and controls the optional Cursor Sandbox lifecycle from outside the box. In `auto` mode, finite Grok commands prefer the local daemon, then direct local Gateway, then a configured remote daemon. Filesystem and process/Job commands require their live daemon capabilities and never fall back to Gateway, SSH, or direct local host access.
21
29
 
22
30
  ## Start here
23
31
 
24
32
  ```bash
25
- grok-bot doctor
26
- grok-bot agents list --kind all --table
27
- grok-bot send --agent <id> --text "hello"
28
- grok-bot history tail <id> --limit 50
33
+ grokbox init
34
+ grokbox profile list --table
35
+ grokbox daemon status
36
+ grokbox doctor
37
+ grokbox quota
38
+ grokbox recover
39
+ grokbox box status
40
+ grokbox box wake
41
+ grokbox box keepalive status
42
+ grokbox agents list --table
43
+ grokbox groups list --table
44
+ grokbox send <target> --text "hello"
45
+ grokbox history tail <target> --limit 50
46
+ grokbox fs stat home:/artifact.txt
47
+ grokbox fs download home:/artifact.bin ./artifact.bin
48
+ grokbox fs write workspace:/status.txt --text "ready"
49
+ grokbox fs upload ./artifact.bin workspace:/artifact.bin
50
+ grokbox fs remove workspace:/obsolete.txt --yes
51
+ grokbox exec run --cwd workspace:/ --detach -- node -e "console.log('ready')"
52
+ grokbox jobs show <job-id>
53
+ grokbox jobs logs <job-id> --follow
54
+ grokbox jobs cancel <job-id>
55
+ grokbox desktop status --table
56
+ grokbox desktop keep add <agent>
57
+ grokbox desktop prune run
58
+ grokbox desktop prune enable
29
59
  ```
30
60
 
31
- Get `<id>` from `agents list`. Do not pass a token, URL, host, or port flag. The CLI reads
32
- discovery itself.
33
-
34
- ## 80% paths
35
-
36
- 1. **Discover** — `doctor` proves loopback mapping and `GET /health`. Health does **not** prove
37
- the discovery Bearer can call authenticated commands; `checks.authenticatedCommand` stays
38
- `not-probed`.
39
- 2. **Pick a target** — `agents list` (roster) or `agents show <id>`. `agents search` searches
40
- transcript text, not bot names.
41
- 3. **Speak as Human** — `send --agent` or `send --group`. Both map to one `sendPrompt`. Success
42
- means accepted, not completed. There is no `--wait`.
43
- 4. **Watch** — `events` (NDJSON) for incremental roster/transcript; `history tail` to reconcile
44
- display entries; `is running <id>` for that row's roster projection. `isRunning=false` does
45
- not prove a send finished.
46
- 5. **Memory metadata** — `memory list --agent <id>` strips `content` unless you pass `--content`.
61
+ Targets accept an exact ID first, then an unambiguous case-insensitive name/title. The built-in
62
+ `default` Profile works without a file; use `grokbox init` for local onboarding and
63
+ `grokbox profile ...` for explicit selection/configuration. Remote bootstrap preserves any existing filesystem policy; add `--admit-home-read` only when the user explicitly authorizes the peer home read/download root. Use `--expect-kind agent|group` on
64
+ `send` when the caller needs a kind guard.
65
+
66
+ ## Common paths
67
+
68
+ 1. **Discover and recover**: `doctor` performs a read-only staged check of Profile/secret source, optional Sandbox state, tailnet path, exact Serve ownership, daemon HTTP/auth/capabilities, and Gateway health. The diagnostic returns exit 0 with `data.ok: false` when it successfully describes an unhealthy target; inspect each check's stable code/action. `daemon status` is only a local daemon handshake, not a second doctor. `recover` is the explicit mutating composition and may wake the configured Sandbox, wait for Tailscale/IPv4, restore only the bootstrap-recorded mapping, and start an installed daemon through declared BatchMode SSH before rerunning doctor. `daemon ensure` is narrower and never wakes or repairs Serve; `--bootstrap` is the separately confirmed install/replace and credential-rotation transition.
69
+ 2. **Manage the roster**: `agents list/show/create/update/delete` only own non-group agents.
70
+ `groups list/show/create/update/delete` only own product groups. Membership is only
71
+ `groups members list/add/remove/set`; every member must resolve to a non-group agent and a group
72
+ must retain 1-6 unique members.
73
+ 3. **Search/read transcript**: `history search <query>`, `history tail <target>`, and
74
+ `history thread <target> --root <entry-id>` own transcript access. Object `show` commands do
75
+ not add transcript side projections.
76
+ 4. **Speak as Human**: `send <target>` maps to one `sendPrompt`. Success means accepted, not
77
+ completed. There is no `--wait`.
78
+ 5. **Watch**: `events` emits unified NDJSON from `gateway`, `job`, and `daemon` sources. Daemon cursors resume a bounded journal; direct Gateway reconnects and all unobserved intervals emit explicit gaps. `is running <target>` reads the roster projection.
79
+ 6. **Read account quota**: `quota` requires a selected Profile with `quota.source:"cursor-web"` and its independent `quota.access_token_ref`. It performs one fixed bounded HTTPS request and returns only a fresh sanitized DTO. It has no cache, fallback, Gateway/daemon route, Sandbox lifecycle effect, or App-private credential discovery. Missing quota configuration fails closed before discovery/SSH/host/App side effects even when the CLI runs inside the box. Copying an external OAuth token onto the box is not the in-box quota path. Static `profile capabilities` reports `provider-authorization-dependent`; only a successful call proves quota authority.
80
+ 7. **Memory metadata**: `memory list <agent>` strips `content` unless `--content` is explicit.
81
+ 8. **Use governed files**: `fs stat/list/read` use `root:/relative/path`; `fs download` writes a new local destination only after size and SHA-256 verification. `fs write` accepts explicit `--text` or stdin and supports `--expected-sha256`; `fs upload` uses bounded verified chunks; `fs mkdir` creates one level; `fs remove` moves content to recoverable trash and requires confirmation. `--recursive` additionally requires elevated live root policy. JSON `read` returns UTF-8 or explicit base64 and never writes arbitrary binary to stdout.
82
+
83
+ 9. **Run governed processes**: `exec run -- <argv...>` resolves argv[0] only as a configured executable alias, preserves all remaining arguments literally, and returns a durable Job. `--run-timeout-ms` governs process lifetime while `--timeout-ms` bounds the client wait; `--detach` returns after admission. `jobs list/show/logs/cancel` require `host.process.manage`; logs are bounded base64 NDJSON and resumable by offset. Shell is separately privileged and normally unavailable. An admitted executable runs as the daemon user and is not sandboxed by cwd.
84
+ 10. **Control Sandbox lifecycle**: `box status` reads the Cursor run state without waking it. `box wake` performs one broker Ensure plus a bounded exec no-op. `box keepalive run` is an external foreground lease loop; `box keepalive status` reads its redacted protected state. These commands require an explicit `sandbox.access_token_ref` and do not depend on daemon, SSH, Tailscale, Gateway health, or an in-box process.
85
+ 11. **Idle desktop forks**: `desktop status` classifies seated forks and prints keep/floor ids. `desktop keep add|remove` persists Chrome keep ids on the box daemon config, not the client Profile. `desktop prune run` dry-runs by default; `--yes` and the daemon tick call official `stop-window`, which deletes `chrome-profile-N`. Keep must-keep agents before `prune enable`. Never edit the seating table or kill host/Xvfb. Display 1 is always kept.
47
86
 
48
87
  Sensitive or multiline prompts should go on stdin:
49
88
 
50
89
  ```bash
51
- printf '%s' "$prompt" | grok-bot send --agent <id>
90
+ printf '%s' "$prompt" | grokbox send <target>
52
91
  ```
53
92
 
54
- If you retry the same send, reuse `--nonce <uuid>` and the same target/prompt. Do not mint a new
55
- nonce after `send_delivery_unknown`.
93
+ An explicit `--text` is authoritative even in a non-TTY runner and suppresses stdin reads. If you
94
+ retry the same send, reuse `--nonce <uuid>` and the same target/prompt.
56
95
 
57
96
  ## Safety
58
97
 
59
- - No `--token`, `--gateway-url`, `--host`, `--port`. No `SAND_GATEWAY_TOKEN` env fallback.
60
- - HTTP has no `Origin` header. Token never belongs on argv, URL, stdout, stderr, or logs.
61
- - `send` is Human `sendPrompt` only. There is no peer, broadcast, raw, delete, Memory write,
62
- approval, secret, or prepare-upgrade command.
63
- - Default stdout redacts roster `path` / `avatarDataUrl` / `lastMessagePreview` and Memory
98
+ - Agent/group deletion is permanent and removes that object's product data. In a headless runner,
99
+ pass `--yes`; in a TTY, review the confirmation prompt. Resolve and inspect the object first.
100
+ - Interrupted management writes return `operation_outcome_unknown` with an operation ID and are not replayed automatically; inspect the roster before deciding whether to retry. If create succeeded before a later settings/projection failure, the error also names the created object ID and `post-create` phase.
101
+ - `doctor` never wakes, starts, installs, changes Tailscale, or accepts a repair option. `profile doctor`, `daemon doctor`, and `doctor --repair` are not commands. `recover` refuses unresolved required credentials, unrecorded/drifted/occupied Serve handlers, missing installed runtime, and unavailable declared SSH rather than broadening authority. It never runs `tailscale serve reset`, `tailscale up`, Funnel, ACL/tag changes, or ordinary business RPC over SSH.
102
+ - Local daemon access is gated by its `0600` Unix socket; remote daemon access uses the selected Profile credential. Handshake returns daemon generation, capability names, named filesystem roots/operations, and Gateway generation, never physical root paths, the Gateway token, or raw discovery JSON.
103
+ - Filesystem paths must use a handshake-advertised named root. Parent traversal, symlink escape, pseudo-filesystems, daemon/Gateway state, and known credential/session paths fail closed. `read` is bounded to 1 MiB; `download` is bounded to 64 MiB in 256 KiB chunks and refuses to overwrite an existing local destination.
104
+ - Job metadata excludes argv, environment values, and output content. Children receive a fixed minimal environment plus allowlisted additions, never daemon `process.env`; output is bounded and binary-safe. Transport timeout or a log follower disconnect never cancels the Job. Reconcile uncertain submission with its original Job ID and lifecycle through `jobs show`.
105
+ - Event cursors are opaque and should be persisted only for reconnect. Daemon restart/eviction and Gateway disconnect emit explicit gap events. Event payloads are channel allowlists; Memory content requires both the memory channel and `--include-memory-content`. Prompt, transcript, filesystem/process, environment, and auth content are never general event fields.
106
+ - Sandbox and quota access tokens must use separate explicit `env:`, protected absolute `file:`, or macOS `keychain:` Profile references. A `file:` ref must be a current-user-owned regular file with no group/other permissions; symbolic links and unverifiable ownership are rejected. A quota failure never falls back to the Sandbox ref, App Gateway descriptor, private App persistence, Gateway, daemon, SSH, or wake. There is no credential-sync command. Quota output excludes account identity, raw provider body, token, Machine ID, headers, and usage events.
107
+ - App Gateway descriptors and private App persistence are not wake credentials. Brokered exec, network, Gateway, and VNC descriptors stay inside the Sandbox adapter. The keeper never calls `sendPrompt` or creates model turns; its local success does not prove the required App-closed 24-72-hour lease experiment.
108
+ - No `--token`, `--gateway-url`, `--host`, `--port`, or raw API route.
109
+ - Gateway HTTP has no `Origin` header. The quota adapter is the explicit exception: it sends only the fixed provider-required `Origin: https://cursor.com` to its fixed endpoint. Tokens never belong on argv, URL, stdout, stderr, or logs.
110
+ - Default output redacts roster `path` / `avatarDataUrl` / `lastMessagePreview` and Memory
64
111
  `content`.
65
- - `history` and `agents search` return private product text. Do not copy them into ordinary logs.
66
- - `--table` is only valid on commands that declare it (`skills list`, `doctor`, `agents list`,
67
- `is running`). `--content` / `--include-memory-content` cannot combine with `--table`.
112
+ - Transcript and Memory content are private product data; do not copy them into ordinary logs.
113
+ - Unsupported options are absent from each leaf command and fail before any network request.
68
114
 
69
115
  ## Output
70
116
 
71
- Finite commands write one JSON object plus a trailing newline (except `skills get`, which is
72
- Markdown unless `--json`). `events` writes NDJSON. On failure, stdout is empty and stderr is one
117
+ Finite commands write one JSON object plus a trailing newline, except `skills get`, which is
118
+ Markdown unless `--json`. `events`, `jobs logs`, and `box keepalive run` write NDJSON. On failure, stdout is empty and stderr is one
73
119
  JSON error object. `is running` returning false is still exit 0.
74
120
 
75
121
  ## External skill stub
76
122
 
77
- Harness-installed skills should not copy this file. They should only say:
123
+ Harness-installed skills should only say:
78
124
 
79
125
  ```text
80
- Run `grok-bot skills get core --full` before first use in this session.
126
+ Run `grokbox skills get core --full` before first use in this session.
81
127
  ```
package/bin/grok-bot DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bun
2
- import "../src/index.ts";
@@ -1,110 +0,0 @@
1
- import type { CliDeps } from "../deps.ts";
2
- import { usage } from "../errors.ts";
3
- import { GatewayClient, gatewayMeta } from "../gateway.ts";
4
- import { formatTable, writeSuccess } from "../output.ts";
5
- import { ioFromOpts } from "../opts.ts";
6
- import { compactRosterRow, detailRosterRow, projectSearchHit } from "../redaction.ts";
7
- import { isRecord, parseInteger } from "../util.ts";
8
- import { findRosterRow } from "./roster.ts";
9
-
10
- export async function runAgentsList(
11
- deps: CliDeps,
12
- raw: {
13
- json?: boolean;
14
- table?: boolean;
15
- timeoutMs?: string;
16
- kind?: string;
17
- includeHidden?: boolean;
18
- },
19
- ): Promise<void> {
20
- const io = ioFromOpts(raw);
21
- const kind = raw.kind ?? "all";
22
- if (kind !== "all" && kind !== "agent" && kind !== "group") {
23
- throw usage("--kind must be all, agent, or group.");
24
- }
25
- const client = new GatewayClient(deps);
26
- const { agents, discovery } = await client.listAgents(io.timeoutMs);
27
- const includeHidden = Boolean(raw.includeHidden);
28
- const projected = agents
29
- .filter(isRecord)
30
- .map(compactRosterRow)
31
- .filter((row) => (includeHidden ? true : !row.isHidden))
32
- .filter((row) => (kind === "all" ? true : row.kind === kind));
33
- const data = { count: projected.length, agents: projected };
34
- if (io.table) {
35
- deps.stdout.write(
36
- formatTable(
37
- projected.map((row) => ({
38
- id: row.id,
39
- name: row.name,
40
- kind: row.kind,
41
- running: String(row.isRunning),
42
- unread: String(row.hasUnread),
43
- })),
44
- ),
45
- );
46
- return;
47
- }
48
- writeSuccess(deps.stdout, data, gatewayMeta(discovery));
49
- }
50
-
51
- export async function runAgentsSearch(
52
- deps: CliDeps,
53
- query: string,
54
- raw: { json?: boolean; table?: boolean; timeoutMs?: string; limit?: string },
55
- ): Promise<void> {
56
- const io = ioFromOpts(raw);
57
- if (io.table) throw usage("This command does not support --table.");
58
- if (query.trim().length === 0) throw usage("Search query must not be blank.");
59
- const limit = parseInteger(raw.limit, { name: "--limit", min: 1, max: 100, defaultValue: 20 });
60
- const client = new GatewayClient(deps);
61
- const { matches, discovery } = await client.searchAgents(query, limit, io.timeoutMs);
62
- writeSuccess(
63
- deps.stdout,
64
- { matches: matches.map(projectSearchHit) },
65
- gatewayMeta(discovery),
66
- );
67
- }
68
-
69
- export async function runAgentsShow(
70
- deps: CliDeps,
71
- id: string,
72
- raw: { json?: boolean; table?: boolean; timeoutMs?: string; history?: string },
73
- ): Promise<void> {
74
- const io = ioFromOpts(raw);
75
- if (io.table) throw usage("This command does not support --table.");
76
- const history = parseInteger(raw.history, {
77
- name: "--history",
78
- min: 0,
79
- max: 100,
80
- defaultValue: 20,
81
- });
82
- const client = new GatewayClient(deps);
83
- const { agents, discovery } = await client.listAgents(io.timeoutMs);
84
- const row = findRosterRow(agents, id);
85
- let historyView = {
86
- sampledEntries: 0,
87
- hasEarlier: false,
88
- nextBeforeSeq: null as number | null,
89
- };
90
- let meta = gatewayMeta(discovery);
91
- if (history > 0) {
92
- const tail = await client.getAgentTranscriptTail({ id, limit: history }, io.timeoutMs);
93
- meta = gatewayMeta(tail.discovery);
94
- const payload = isRecord(tail.result) ? tail.result : {};
95
- const entries = Array.isArray(payload.entries) ? payload.entries : [];
96
- historyView = {
97
- sampledEntries: entries.length,
98
- hasEarlier: payload.nextBeforeSeq !== undefined && payload.nextBeforeSeq !== null,
99
- nextBeforeSeq: typeof payload.nextBeforeSeq === "number" ? payload.nextBeforeSeq : null,
100
- };
101
- }
102
- writeSuccess(
103
- deps.stdout,
104
- {
105
- agent: detailRosterRow(row),
106
- history: historyView,
107
- },
108
- meta,
109
- );
110
- }
@@ -1,50 +0,0 @@
1
- import type { CliDeps } from "../deps.ts";
2
- import { GatewayClient, gatewayMeta } from "../gateway.ts";
3
- import { flattenRows, formatTable, writeSuccess } from "../output.ts";
4
- import { ioFromOpts } from "../opts.ts";
5
- import { asBoolean, asNumber } from "../util.ts";
6
-
7
- export async function runDoctor(
8
- deps: CliDeps,
9
- raw: { json?: boolean; table?: boolean; timeoutMs?: string },
10
- ): Promise<void> {
11
- const io = ioFromOpts(raw);
12
- const client = new GatewayClient(deps);
13
- const discovery = await client.load();
14
- const { health } = await client.health(io.timeoutMs);
15
- const healthPid = asNumber(health.pid, Number.NaN);
16
- const healthStarted = asNumber(health.startedAt, Number.NaN);
17
- const healthView: Record<string, unknown> = {
18
- ok: asBoolean(health.ok),
19
- pid: health.pid ?? null,
20
- isBusy: asBoolean(health.isBusy),
21
- activeAgentId: health.activeAgentId ?? null,
22
- startedAt: health.startedAt ?? null,
23
- lastBusyAtMs: health.lastBusyAtMs ?? null,
24
- };
25
- if ("busyOnlyAwaitingApproval" in health) {
26
- healthView.busyOnlyAwaitingApproval = health.busyOnlyAwaitingApproval;
27
- }
28
- const data = {
29
- discovery: {
30
- scheme: discovery.scheme,
31
- bindHost: discovery.bindHost,
32
- dialHost: discovery.dialHost,
33
- port: discovery.port,
34
- pid: discovery.pid,
35
- startedAt: discovery.startedAt,
36
- tokenPresent: discovery.tokenPresent,
37
- },
38
- health: healthView,
39
- checks: {
40
- loopbackTarget: discovery.dialHost === "127.0.0.1" || discovery.dialHost === "::1" || discovery.dialHost === "[::1]",
41
- generationMatches: healthPid === discovery.pid && healthStarted === discovery.startedAt,
42
- authenticatedCommand: "not-probed",
43
- },
44
- };
45
- if (io.table) {
46
- deps.stdout.write(formatTable(flattenRows(data as unknown as Record<string, unknown>)));
47
- return;
48
- }
49
- writeSuccess(deps.stdout, data, gatewayMeta(discovery));
50
- }
@@ -1,123 +0,0 @@
1
- import type { CliDeps } from "../deps.ts";
2
- import { CliError, usage } from "../errors.ts";
3
- import { GatewayClient, parseSse } from "../gateway.ts";
4
- import { writeJsonLine } from "../output.ts";
5
- import { ioFromOpts } from "../opts.ts";
6
- import { redactEventPayload } from "../redaction.ts";
7
- import { ALLOWED_EVENT_CHANNELS, DEFAULT_EVENT_CHANNELS } from "../registry.ts";
8
- import { asString, isRecord } from "../util.ts";
9
-
10
- const ALLOWED = new Set<string>(ALLOWED_EVENT_CHANNELS);
11
-
12
- function parseChannels(csv: string | undefined): string[] {
13
- if (csv === undefined || csv.trim().length === 0) return [...DEFAULT_EVENT_CHANNELS];
14
- const channels = csv
15
- .split(",")
16
- .map((item) => item.trim())
17
- .filter((item) => item.length > 0);
18
- if (channels.length === 0) throw usage("--channels must list at least one channel.");
19
- for (const channel of channels) {
20
- if (!ALLOWED.has(channel)) {
21
- throw usage(`Channel '${channel}' is not in the v1 allowlist.`);
22
- }
23
- }
24
- return channels;
25
- }
26
-
27
- function sleep(ms: number): Promise<void> {
28
- return new Promise((resolve) => setTimeout(resolve, ms));
29
- }
30
-
31
- export async function runEvents(
32
- deps: CliDeps,
33
- raw: {
34
- json?: boolean;
35
- table?: boolean;
36
- timeoutMs?: string;
37
- channels?: string;
38
- once?: boolean;
39
- includeMemoryContent?: boolean;
40
- },
41
- ): Promise<void> {
42
- const io = ioFromOpts(raw);
43
- if (io.table) throw usage("events does not support --table.");
44
- const channels = parseChannels(raw.channels);
45
- const includeMemoryContent = Boolean(raw.includeMemoryContent);
46
- if (includeMemoryContent && !channels.includes("memory")) {
47
- throw usage("--include-memory-content requires the memory channel.");
48
- }
49
- const once = Boolean(raw.once);
50
- const client = new GatewayClient(deps);
51
- const stop = deps.signal;
52
- let emitted = false;
53
- let first = true;
54
-
55
- while (!stop?.aborted) {
56
- let response: Response;
57
- let discoveryPid: number;
58
- let discoveryStarted: number;
59
- try {
60
- const opened = await client.openEventStream(channels, io.timeoutMs);
61
- response = opened.response;
62
- discoveryPid = opened.discovery.pid;
63
- discoveryStarted = opened.discovery.startedAt;
64
- } catch (error) {
65
- if (stop?.aborted) return;
66
- if (first) throw error;
67
- deps.stderr.write("grok-bot: event stream reconnect failed; retrying\n");
68
- await sleep(1000);
69
- continue;
70
- }
71
- first = false;
72
- if (!response.body) {
73
- throw new CliError("gateway_unreachable", "Gateway event stream had no body.");
74
- }
75
-
76
- let lastChunk = deps.now();
77
- const watchdog = setInterval(() => {
78
- if (deps.now() - lastChunk > deps.idleWatchdogMs) {
79
- void response.body?.cancel();
80
- }
81
- }, 1000);
82
-
83
- try {
84
- for await (const frame of parseSse(response.body, {
85
- onChunk: () => {
86
- lastChunk = deps.now();
87
- },
88
- signal: stop,
89
- })) {
90
- if (stop?.aborted) return;
91
- const rec = isRecord(frame) ? frame : {};
92
- const channel = asString(rec.channel);
93
- if (!channels.includes(channel)) continue;
94
- const payload = redactEventPayload(channel, rec.payload, includeMemoryContent);
95
- writeJsonLine(deps.stdout, {
96
- ok: true,
97
- event: { channel, payload },
98
- meta: {
99
- observedAtMs: deps.now(),
100
- gateway: { pid: discoveryPid, startedAt: discoveryStarted },
101
- },
102
- });
103
- emitted = true;
104
- if (once) return;
105
- }
106
- } finally {
107
- clearInterval(watchdog);
108
- }
109
-
110
- if (stop?.aborted) return;
111
- if (once) break;
112
- deps.stderr.write("grok-bot: event stream disconnected; rereading discovery\n");
113
- try {
114
- await client.listAgents(io.timeoutMs);
115
- } catch {
116
- // reconciliation is best-effort; do not claim the gap was empty
117
- }
118
- }
119
-
120
- if (once && !emitted) {
121
- throw new CliError("gateway_unreachable", "Event stream ended before the first allowed event.");
122
- }
123
- }
@@ -1,65 +0,0 @@
1
- import type { CliDeps } from "../deps.ts";
2
- import { usage } from "../errors.ts";
3
- import { GatewayClient, gatewayMeta } from "../gateway.ts";
4
- import { writeSuccess } from "../output.ts";
5
- import { ioFromOpts } from "../opts.ts";
6
- import { isRecord, parseInteger } from "../util.ts";
7
-
8
- export async function runHistoryTail(
9
- deps: CliDeps,
10
- id: string,
11
- raw: {
12
- json?: boolean;
13
- table?: boolean;
14
- timeoutMs?: string;
15
- limit?: string;
16
- beforeSeq?: string;
17
- },
18
- ): Promise<void> {
19
- const io = ioFromOpts(raw);
20
- if (io.table) throw usage("This command does not support --table.");
21
- if (id.length === 0) throw usage("id is required.");
22
- const limit = parseInteger(raw.limit, { name: "--limit", min: 1, max: 200, defaultValue: 50 });
23
- const beforeSeq =
24
- raw.beforeSeq === undefined
25
- ? undefined
26
- : parseInteger(raw.beforeSeq, { name: "--before-seq", min: 0, max: Number.MAX_SAFE_INTEGER });
27
- const client = new GatewayClient(deps);
28
- const { result, discovery } = await client.getAgentTranscriptTail(
29
- beforeSeq === undefined ? { id, limit } : { id, limit, beforeSeq },
30
- io.timeoutMs,
31
- );
32
- const payload = isRecord(result) ? result : {};
33
- const data: Record<string, unknown> = {
34
- id,
35
- entries: Array.isArray(payload.entries) ? payload.entries : [],
36
- };
37
- if (payload.nextBeforeSeq !== undefined && payload.nextBeforeSeq !== null) {
38
- data.nextBeforeSeq = payload.nextBeforeSeq;
39
- }
40
- writeSuccess(deps.stdout, data, gatewayMeta(discovery));
41
- }
42
-
43
- export async function runHistoryThread(
44
- deps: CliDeps,
45
- id: string,
46
- raw: { json?: boolean; table?: boolean; timeoutMs?: string; root?: string },
47
- ): Promise<void> {
48
- const io = ioFromOpts(raw);
49
- if (io.table) throw usage("This command does not support --table.");
50
- if (id.length === 0) throw usage("id is required.");
51
- const rootId = raw.root;
52
- if (!rootId) throw usage("--root is required.");
53
- const client = new GatewayClient(deps);
54
- const { result, discovery } = await client.getAgentThread({ id, rootId }, io.timeoutMs);
55
- const payload = isRecord(result) ? result : {};
56
- writeSuccess(
57
- deps.stdout,
58
- {
59
- id,
60
- rootId,
61
- entries: Array.isArray(payload.entries) ? payload.entries : [],
62
- },
63
- gatewayMeta(discovery),
64
- );
65
- }
@@ -1,23 +0,0 @@
1
- import type { CliDeps } from "../deps.ts";
2
- import { GatewayClient, gatewayMeta } from "../gateway.ts";
3
- import { flattenRows, formatTable, writeSuccess } from "../output.ts";
4
- import { ioFromOpts } from "../opts.ts";
5
- import { runningProjection } from "../redaction.ts";
6
- import { findRosterRow } from "./roster.ts";
7
-
8
- export async function runIsRunning(
9
- deps: CliDeps,
10
- id: string,
11
- raw: { json?: boolean; table?: boolean; timeoutMs?: string },
12
- ): Promise<void> {
13
- const io = ioFromOpts(raw);
14
- const client = new GatewayClient(deps);
15
- const { agents, discovery } = await client.listAgents(io.timeoutMs);
16
- const row = findRosterRow(agents, id);
17
- const data = runningProjection(row, deps.now());
18
- if (io.table) {
19
- deps.stdout.write(formatTable(flattenRows(data as unknown as Record<string, unknown>)));
20
- return;
21
- }
22
- writeSuccess(deps.stdout, data, gatewayMeta(discovery));
23
- }