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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/index.ts";
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
+ }