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/LICENSE +21 -0
- package/README.md +214 -16
- package/THIRD_PARTY_NOTICES +29 -0
- package/bin/grokbox +13 -0
- package/bin/runtime.d.ts +13 -0
- package/bin/runtime.js +27 -0
- package/dist/index.js +14104 -0
- package/package.json +43 -10
- package/skills/core.md +90 -44
- package/bin/grok-bot +0 -2
- package/src/commands/agents.ts +0 -110
- package/src/commands/doctor.ts +0 -50
- package/src/commands/events.ts +0 -123
- package/src/commands/history.ts +0 -65
- package/src/commands/is.ts +0 -23
- package/src/commands/memory.ts +0 -47
- package/src/commands/roster.ts +0 -11
- package/src/commands/send.ts +0 -72
- package/src/deps.ts +0 -49
- package/src/errors.ts +0 -132
- package/src/gateway.ts +0 -421
- package/src/index.ts +0 -6
- package/src/opts.ts +0 -52
- package/src/output.ts +0 -58
- package/src/program.ts +0 -213
- package/src/redaction.ts +0 -130
- package/src/registry.ts +0 -202
- package/src/skills.ts +0 -55
- package/src/util.ts +0 -59
package/package.json
CHANGED
|
@@ -1,34 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grokbox",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
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
|
-
"
|
|
18
|
+
"grokbox": "bin/grokbox",
|
|
19
|
+
"gbox": "bin/grokbox"
|
|
8
20
|
},
|
|
9
21
|
"files": [
|
|
10
22
|
"bin",
|
|
23
|
+
"dist",
|
|
11
24
|
"skills",
|
|
12
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
#
|
|
1
|
+
# grokbox core
|
|
2
2
|
|
|
3
|
-
Version-matched usage guide for the
|
|
4
|
-
binary (`
|
|
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
|
-
`
|
|
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
|
-
->
|
|
14
|
-
->
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
2. **
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
not
|
|
46
|
-
|
|
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" |
|
|
90
|
+
printf '%s' "$prompt" | grokbox send <target>
|
|
52
91
|
```
|
|
53
92
|
|
|
54
|
-
|
|
55
|
-
nonce
|
|
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
|
-
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
-
|
|
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
|
-
-
|
|
66
|
-
-
|
|
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
|
|
72
|
-
Markdown unless `--json
|
|
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
|
|
123
|
+
Harness-installed skills should only say:
|
|
78
124
|
|
|
79
125
|
```text
|
|
80
|
-
Run `
|
|
126
|
+
Run `grokbox skills get core --full` before first use in this session.
|
|
81
127
|
```
|
package/bin/grok-bot
DELETED
package/src/commands/agents.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/doctor.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/events.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/history.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/is.ts
DELETED
|
@@ -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
|
-
}
|