grokbox 0.0.1
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/README.md +34 -0
- package/bin/grok-bot +2 -0
- package/package.json +34 -0
- package/skills/core.md +81 -0
- package/src/commands/agents.ts +110 -0
- package/src/commands/doctor.ts +50 -0
- package/src/commands/events.ts +123 -0
- package/src/commands/history.ts +65 -0
- package/src/commands/is.ts +23 -0
- package/src/commands/memory.ts +47 -0
- package/src/commands/roster.ts +11 -0
- package/src/commands/send.ts +72 -0
- package/src/deps.ts +49 -0
- package/src/errors.ts +132 -0
- package/src/gateway.ts +421 -0
- package/src/index.ts +6 -0
- package/src/opts.ts +52 -0
- package/src/output.ts +58 -0
- package/src/program.ts +213 -0
- package/src/redaction.ts +130 -0
- package/src/registry.ts +202 -0
- package/src/skills.ts +55 -0
- package/src/util.ts +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# grokbox
|
|
2
|
+
|
|
3
|
+
Unofficial local CLI for inspecting and messaging a running Grok Bot host.
|
|
4
|
+
It is not affiliated with or endorsed by Anysphere, Cursor, or xAI.
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
|
|
8
|
+
- Bun 1.3 or newer
|
|
9
|
+
- A reachable Grok Bot host on the same machine or network
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun add --global grokbox
|
|
15
|
+
grok-bot --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For a one-off run:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bunx grokbox --help
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Common commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
grok-bot doctor
|
|
28
|
+
grok-bot agents list
|
|
29
|
+
grok-bot send <agent> "status only"
|
|
30
|
+
grok-bot history tail <agent>
|
|
31
|
+
grok-bot skills list
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run `grok-bot --help` or `grok-bot <command> --help` for the complete command reference.
|
package/bin/grok-bot
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "grokbox",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Unofficial local CLI for inspecting and messaging a running Grok Bot host",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"grok-bot": "bin/grok-bot"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"skills",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"grok-bot",
|
|
16
|
+
"cli",
|
|
17
|
+
"agents",
|
|
18
|
+
"automation"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.3.0"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"grok-bot": "bun src/index.ts",
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": "^14.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/skills/core.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# grok-bot core
|
|
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
|
|
5
|
+
stub.
|
|
6
|
+
|
|
7
|
+
## What this CLI is
|
|
8
|
+
|
|
9
|
+
`grok-bot` is an agent-first adapter, not a 151-method Gateway wrapper.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Agent / Skill
|
|
13
|
+
-> grok-bot
|
|
14
|
+
-> /home/box/sand-data/gateway.json
|
|
15
|
+
-> loopback HTTP + Bearer (in-process only)
|
|
16
|
+
-> Grok Bot host Gateway
|
|
17
|
+
```
|
|
18
|
+
|
|
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.
|
|
21
|
+
|
|
22
|
+
## Start here
|
|
23
|
+
|
|
24
|
+
```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
|
|
29
|
+
```
|
|
30
|
+
|
|
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`.
|
|
47
|
+
|
|
48
|
+
Sensitive or multiline prompts should go on stdin:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
printf '%s' "$prompt" | grok-bot send --agent <id>
|
|
52
|
+
```
|
|
53
|
+
|
|
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`.
|
|
56
|
+
|
|
57
|
+
## Safety
|
|
58
|
+
|
|
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
|
|
64
|
+
`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`.
|
|
68
|
+
|
|
69
|
+
## Output
|
|
70
|
+
|
|
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
|
|
73
|
+
JSON error object. `is running` returning false is still exit 0.
|
|
74
|
+
|
|
75
|
+
## External skill stub
|
|
76
|
+
|
|
77
|
+
Harness-installed skills should not copy this file. They should only say:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
Run `grok-bot skills get core --full` before first use in this session.
|
|
81
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { projectMemory } from "../redaction.ts";
|
|
7
|
+
import { MEMORY_UPSTREAM_CAP } from "../registry.ts";
|
|
8
|
+
import { parseInteger } from "../util.ts";
|
|
9
|
+
|
|
10
|
+
export async function runMemoryList(
|
|
11
|
+
deps: CliDeps,
|
|
12
|
+
raw: {
|
|
13
|
+
json?: boolean;
|
|
14
|
+
table?: boolean;
|
|
15
|
+
timeoutMs?: string;
|
|
16
|
+
agent?: string;
|
|
17
|
+
limit?: string;
|
|
18
|
+
content?: boolean;
|
|
19
|
+
},
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const io = ioFromOpts(raw);
|
|
22
|
+
const includeContent = Boolean(raw.content);
|
|
23
|
+
if (io.table) throw usage("memory list does not support --table.");
|
|
24
|
+
const agentId = raw.agent;
|
|
25
|
+
if (!agentId) throw usage("--agent is required.");
|
|
26
|
+
const limit = parseInteger(raw.limit, {
|
|
27
|
+
name: "--limit",
|
|
28
|
+
min: 1,
|
|
29
|
+
max: MEMORY_UPSTREAM_CAP,
|
|
30
|
+
defaultValue: 100,
|
|
31
|
+
});
|
|
32
|
+
const client = new GatewayClient(deps);
|
|
33
|
+
const { result, discovery } = await client.getAgentMemories(agentId, io.timeoutMs);
|
|
34
|
+
const source = Array.isArray(result) ? result : [];
|
|
35
|
+
const sliced = source.slice(0, limit);
|
|
36
|
+
writeSuccess(
|
|
37
|
+
deps.stdout,
|
|
38
|
+
{
|
|
39
|
+
agentId,
|
|
40
|
+
returned: sliced.length,
|
|
41
|
+
sourceCount: source.length,
|
|
42
|
+
sourceMayBeTruncated: source.length === MEMORY_UPSTREAM_CAP,
|
|
43
|
+
memories: sliced.map((row) => projectMemory(row, includeContent)),
|
|
44
|
+
},
|
|
45
|
+
gatewayMeta(discovery),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CliError, usage } from "../errors.ts";
|
|
2
|
+
import { isRecord } from "../util.ts";
|
|
3
|
+
|
|
4
|
+
export function findRosterRow(agents: unknown[], id: string): Record<string, unknown> {
|
|
5
|
+
if (id.length === 0) throw usage("Agent id is required.");
|
|
6
|
+
const row = agents.find((item): item is Record<string, unknown> => isRecord(item) && item.id === id);
|
|
7
|
+
if (!row) {
|
|
8
|
+
throw new CliError("target_not_found", "No roster row matched that id.");
|
|
9
|
+
}
|
|
10
|
+
return row;
|
|
11
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CliDeps } from "../deps.ts";
|
|
2
|
+
import { CliError, usage } from "../errors.ts";
|
|
3
|
+
import { GatewayClient, gatewayMeta } from "../gateway.ts";
|
|
4
|
+
import { writeSuccess } from "../output.ts";
|
|
5
|
+
import { ioFromOpts, readPrompt } from "../opts.ts";
|
|
6
|
+
import { agentKind } from "../redaction.ts";
|
|
7
|
+
import { assertUuidV4, isRecord } from "../util.ts";
|
|
8
|
+
import { findRosterRow } from "./roster.ts";
|
|
9
|
+
|
|
10
|
+
export async function runSend(
|
|
11
|
+
deps: CliDeps,
|
|
12
|
+
raw: {
|
|
13
|
+
json?: boolean;
|
|
14
|
+
table?: boolean;
|
|
15
|
+
timeoutMs?: string;
|
|
16
|
+
agent?: string;
|
|
17
|
+
group?: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
nonce?: string;
|
|
20
|
+
},
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const io = ioFromOpts(raw);
|
|
23
|
+
if (io.table) throw usage("This command does not support --table.");
|
|
24
|
+
const agentId = raw.agent;
|
|
25
|
+
const groupId = raw.group;
|
|
26
|
+
if ((agentId === undefined) === (groupId === undefined)) {
|
|
27
|
+
throw usage("Provide exactly one of --agent or --group.");
|
|
28
|
+
}
|
|
29
|
+
const wantedKind = agentId !== undefined ? "agent" : "group";
|
|
30
|
+
const targetId = (agentId ?? groupId) as string;
|
|
31
|
+
const prompt = await readPrompt(raw.text, deps);
|
|
32
|
+
const clientNonce = raw.nonce === undefined ? deps.randomUUID() : assertUuidV4(raw.nonce, "--nonce");
|
|
33
|
+
const client = new GatewayClient(deps);
|
|
34
|
+
const { agents } = await client.listAgents(io.timeoutMs);
|
|
35
|
+
const row = findRosterRow(agents, targetId);
|
|
36
|
+
const actualKind = agentKind(row);
|
|
37
|
+
if (actualKind !== wantedKind) {
|
|
38
|
+
throw new CliError(
|
|
39
|
+
"target_kind_mismatch",
|
|
40
|
+
wantedKind === "agent"
|
|
41
|
+
? "--agent matched a group id."
|
|
42
|
+
: "--group matched a non-group agent id.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const { result, discovery } = await client.sendPrompt(
|
|
47
|
+
{ agentId: String(row.id), prompt, clientNonce },
|
|
48
|
+
io.timeoutMs,
|
|
49
|
+
);
|
|
50
|
+
const accepted = !isRecord(result) || result.accepted !== false;
|
|
51
|
+
if (!accepted) {
|
|
52
|
+
throw new CliError("gateway_internal", "Gateway did not accept the send.");
|
|
53
|
+
}
|
|
54
|
+
writeSuccess(
|
|
55
|
+
deps.stdout,
|
|
56
|
+
{
|
|
57
|
+
status: "accepted",
|
|
58
|
+
accepted: true,
|
|
59
|
+
target: { id: String(row.id), kind: actualKind },
|
|
60
|
+
clientNonce,
|
|
61
|
+
},
|
|
62
|
+
gatewayMeta(discovery),
|
|
63
|
+
);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof CliError && error.code === "send_delivery_unknown") {
|
|
66
|
+
throw new CliError("send_delivery_unknown", error.message, {
|
|
67
|
+
context: { clientNonce, target: { id: String(row.id), kind: actualKind } },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|