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/src/deps.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { join } from "node:path";
2
+ import cliPackage from "../package.json" with { type: "json" };
3
+ import { DEFAULT_DISCOVERY_PATH } from "./registry.ts";
4
+ import type { Writable } from "./output.ts";
5
+
6
+ export type FetchFn = typeof fetch;
7
+
8
+ export type CliDeps = {
9
+ discoveryPath: string;
10
+ fetch: FetchFn;
11
+ now: () => number;
12
+ randomUUID: () => string;
13
+ readFile: (path: string) => Promise<string>;
14
+ stdout: Writable;
15
+ stderr: Writable;
16
+ stdinIsTTY: boolean;
17
+ readStdin: () => Promise<string>;
18
+ skillsDir: string;
19
+ cliVersion: string;
20
+ idleWatchdogMs: number;
21
+ signal?: AbortSignal;
22
+ };
23
+
24
+ export const CLI_VERSION: string = cliPackage.version;
25
+
26
+ export function createProductionDeps(): CliDeps {
27
+ return {
28
+ discoveryPath: DEFAULT_DISCOVERY_PATH,
29
+ fetch: globalThis.fetch.bind(globalThis),
30
+ now: () => Date.now(),
31
+ randomUUID: () => crypto.randomUUID(),
32
+ readFile: async (path) => await Bun.file(path).text(),
33
+ stdout: {
34
+ write(chunk) {
35
+ process.stdout.write(chunk);
36
+ },
37
+ },
38
+ stderr: {
39
+ write(chunk) {
40
+ process.stderr.write(chunk);
41
+ },
42
+ },
43
+ stdinIsTTY: Boolean(process.stdin.isTTY),
44
+ readStdin: async () => await Bun.stdin.text(),
45
+ skillsDir: join(import.meta.dir, "..", "skills"),
46
+ cliVersion: CLI_VERSION,
47
+ idleWatchdogMs: 45_000,
48
+ };
49
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,132 @@
1
+ export const EXIT_CODES = {
2
+ ok: 0,
3
+ invalid_usage: 2,
4
+ discovery_unavailable: 3,
5
+ gateway_unreachable: 4,
6
+ gateway_bad_request: 10,
7
+ gateway_unauthorized: 11,
8
+ gateway_forbidden: 12,
9
+ gateway_not_found: 13,
10
+ gateway_conflict: 14,
11
+ gateway_internal: 15,
12
+ send_delivery_unknown: 16,
13
+ target_not_found: 17,
14
+ target_kind_mismatch: 18,
15
+ } as const;
16
+
17
+ export type ErrorCode = Exclude<keyof typeof EXIT_CODES, "ok">;
18
+
19
+ export type ErrorBody = {
20
+ code: ErrorCode;
21
+ message: string;
22
+ httpStatus?: number;
23
+ failureCode?: string;
24
+ retryable: boolean;
25
+ context?: {
26
+ clientNonce: string;
27
+ target: { id: string; kind: "agent" | "group" };
28
+ };
29
+ };
30
+
31
+ const RETRYABLE: Record<ErrorCode, boolean> = {
32
+ invalid_usage: false,
33
+ discovery_unavailable: false,
34
+ gateway_unreachable: true,
35
+ gateway_bad_request: false,
36
+ gateway_unauthorized: false,
37
+ gateway_forbidden: false,
38
+ gateway_not_found: false,
39
+ gateway_conflict: false,
40
+ gateway_internal: true,
41
+ send_delivery_unknown: false,
42
+ target_not_found: false,
43
+ target_kind_mismatch: false,
44
+ };
45
+
46
+ export class CliError extends Error {
47
+ readonly code: ErrorCode;
48
+ readonly httpStatus?: number;
49
+ readonly failureCode?: string;
50
+ readonly retryable: boolean;
51
+ readonly context?: ErrorBody["context"];
52
+
53
+ constructor(
54
+ code: ErrorCode,
55
+ message: string,
56
+ extras: {
57
+ httpStatus?: number;
58
+ failureCode?: string;
59
+ retryable?: boolean;
60
+ context?: ErrorBody["context"];
61
+ } = {},
62
+ ) {
63
+ super(message);
64
+ this.name = "CliError";
65
+ this.code = code;
66
+ this.httpStatus = extras.httpStatus;
67
+ this.failureCode = extras.failureCode;
68
+ this.retryable = extras.retryable ?? RETRYABLE[code];
69
+ this.context = extras.context;
70
+ }
71
+
72
+ get exitCode(): number {
73
+ return EXIT_CODES[this.code];
74
+ }
75
+
76
+ toErrorBody(): ErrorBody {
77
+ const body: ErrorBody = {
78
+ code: this.code,
79
+ message: this.message,
80
+ retryable: this.retryable,
81
+ };
82
+ if (this.httpStatus !== undefined) body.httpStatus = this.httpStatus;
83
+ if (this.failureCode !== undefined) body.failureCode = this.failureCode;
84
+ if (this.context !== undefined) body.context = this.context;
85
+ return body;
86
+ }
87
+ }
88
+
89
+ export function usage(message: string): CliError {
90
+ return new CliError("invalid_usage", message);
91
+ }
92
+
93
+ export function httpStatusToError(
94
+ status: number,
95
+ failureCode: string | undefined,
96
+ fallbackMessage: string,
97
+ ): CliError {
98
+ if (status === 400) {
99
+ return new CliError("gateway_bad_request", "Gateway rejected the request body.", {
100
+ httpStatus: 400,
101
+ failureCode,
102
+ });
103
+ }
104
+ if (status === 401) {
105
+ return new CliError("gateway_unauthorized", "Gateway rejected the credentials.", {
106
+ httpStatus: 401,
107
+ failureCode,
108
+ });
109
+ }
110
+ if (status === 403) {
111
+ return new CliError("gateway_forbidden", "Gateway forbade the request.", {
112
+ httpStatus: 403,
113
+ failureCode,
114
+ });
115
+ }
116
+ if (status === 404) {
117
+ return new CliError("gateway_not_found", "Gateway method or path was not found.", {
118
+ httpStatus: 404,
119
+ failureCode,
120
+ });
121
+ }
122
+ if (status === 409) {
123
+ return new CliError("gateway_conflict", "Gateway rejected the request.", {
124
+ httpStatus: 409,
125
+ failureCode,
126
+ });
127
+ }
128
+ return new CliError("gateway_internal", fallbackMessage, {
129
+ httpStatus: status,
130
+ failureCode,
131
+ });
132
+ }
package/src/gateway.ts ADDED
@@ -0,0 +1,421 @@
1
+ import type { CliDeps } from "./deps.ts";
2
+ import { CliError, httpStatusToError } from "./errors.ts";
3
+ import type { GatewayMethod } from "./registry.ts";
4
+ import { isRecord } from "./util.ts";
5
+
6
+ const WILDCARD_HOSTS = new Set(["0.0.0.0", "::", "[::]", "*"]);
7
+
8
+ export type Discovery = {
9
+ scheme: string;
10
+ bindHost: string;
11
+ dialHost: string;
12
+ port: number;
13
+ pid: number;
14
+ startedAt: number;
15
+ token: string;
16
+ tokenPresent: boolean;
17
+ baseUrl: string;
18
+ };
19
+
20
+ export type GatewayMeta = { pid: number; startedAt: number };
21
+
22
+ type RequestKind = "read" | "write" | "health";
23
+
24
+ function isWildcardHost(host: string): boolean {
25
+ return WILDCARD_HOSTS.has(host.trim().toLowerCase());
26
+ }
27
+
28
+ function isLoopbackHost(host: string): boolean {
29
+ const h = host.trim().toLowerCase();
30
+ if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]") return true;
31
+ if (/^127(?:\.\d{1,3}){3}$/.test(h)) return true;
32
+ return false;
33
+ }
34
+
35
+ function formatHostForUrl(host: string): string {
36
+ if (host.includes(":") && !host.startsWith("[")) return `[${host}]`;
37
+ return host;
38
+ }
39
+
40
+ export function resolveDialHost(bindHost: string): string {
41
+ const host = bindHost.trim();
42
+ if (host.length === 0) {
43
+ throw new CliError("discovery_unavailable", "Discovery host is missing.");
44
+ }
45
+ if (isWildcardHost(host)) return "127.0.0.1";
46
+ if (isLoopbackHost(host)) {
47
+ if (host.toLowerCase() === "localhost") return "127.0.0.1";
48
+ return host;
49
+ }
50
+ throw new CliError(
51
+ "discovery_unavailable",
52
+ "Discovery host is not loopback; grok-bot refuses to dial off-box.",
53
+ );
54
+ }
55
+
56
+ export async function readDiscovery(deps: CliDeps): Promise<Discovery> {
57
+ let raw: string;
58
+ try {
59
+ raw = await deps.readFile(deps.discoveryPath);
60
+ } catch {
61
+ throw new CliError("discovery_unavailable", "Gateway discovery file is missing or unreadable.");
62
+ }
63
+ let parsed: unknown;
64
+ try {
65
+ parsed = JSON.parse(raw);
66
+ } catch {
67
+ throw new CliError("discovery_unavailable", "Gateway discovery file is not valid JSON.");
68
+ }
69
+ if (!isRecord(parsed)) {
70
+ throw new CliError("discovery_unavailable", "Gateway discovery file has the wrong shape.");
71
+ }
72
+ const scheme = parsed.scheme;
73
+ const host = parsed.host;
74
+ const port = parsed.port;
75
+ const pid = parsed.pid;
76
+ const startedAt = parsed.startedAt;
77
+ const token = parsed.token;
78
+ if (scheme !== "http" && scheme !== "https") {
79
+ throw new CliError("discovery_unavailable", "Discovery scheme must be http or https.");
80
+ }
81
+ if (typeof host !== "string") {
82
+ throw new CliError("discovery_unavailable", "Discovery host is missing.");
83
+ }
84
+ if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535) {
85
+ throw new CliError("discovery_unavailable", "Discovery port is invalid.");
86
+ }
87
+ if (typeof pid !== "number" || !Number.isFinite(pid)) {
88
+ throw new CliError("discovery_unavailable", "Discovery pid is invalid.");
89
+ }
90
+ if (typeof startedAt !== "number" || !Number.isFinite(startedAt)) {
91
+ throw new CliError("discovery_unavailable", "Discovery startedAt is invalid.");
92
+ }
93
+ if (typeof token !== "string") {
94
+ throw new CliError("discovery_unavailable", "Discovery token is invalid.");
95
+ }
96
+ const dialHost = resolveDialHost(host);
97
+ return {
98
+ scheme,
99
+ bindHost: host,
100
+ dialHost,
101
+ port,
102
+ pid,
103
+ startedAt,
104
+ token,
105
+ tokenPresent: token.length > 0,
106
+ baseUrl: `${scheme}://${formatHostForUrl(dialHost)}:${port}`,
107
+ };
108
+ }
109
+
110
+ export function gatewayMeta(discovery: Discovery): GatewayMeta {
111
+ return { pid: discovery.pid, startedAt: discovery.startedAt };
112
+ }
113
+
114
+ function parseFailureCode(body: unknown): string | undefined {
115
+ if (!isRecord(body) || typeof body.failureCode !== "string") return undefined;
116
+ return body.failureCode;
117
+ }
118
+
119
+ function isAbortError(error: unknown): boolean {
120
+ return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
121
+ }
122
+
123
+ type HttpResult = {
124
+ status: number;
125
+ body: unknown;
126
+ text: string;
127
+ response: Response;
128
+ };
129
+
130
+ export class GatewayClient {
131
+ lastDiscovery: Discovery | null = null;
132
+
133
+ constructor(private readonly deps: CliDeps) {}
134
+
135
+ async load(): Promise<Discovery> {
136
+ const discovery = await readDiscovery(this.deps);
137
+ this.lastDiscovery = discovery;
138
+ return discovery;
139
+ }
140
+
141
+ requireToken(discovery: Discovery): void {
142
+ if (!discovery.tokenPresent) {
143
+ throw new CliError("discovery_unavailable", "Discovery token is missing.");
144
+ }
145
+ }
146
+
147
+ async health(timeoutMs: number): Promise<{ discovery: Discovery; health: Record<string, unknown> }> {
148
+ const result = await this.request({
149
+ kind: "health",
150
+ path: "/health",
151
+ method: "GET",
152
+ auth: false,
153
+ timeoutMs,
154
+ });
155
+ if (!isRecord(result.body)) {
156
+ throw new CliError("gateway_internal", "Gateway /health did not return an object.", {
157
+ httpStatus: result.status,
158
+ });
159
+ }
160
+ return { discovery: this.lastDiscovery!, health: result.body };
161
+ }
162
+
163
+ async rpc(
164
+ method: GatewayMethod,
165
+ body: Record<string, unknown>,
166
+ options: { timeoutMs: number; write?: boolean; slim?: boolean },
167
+ ): Promise<{ result: unknown; discovery: Discovery }> {
168
+ const result = await this.request({
169
+ kind: options.write ? "write" : "read",
170
+ path: `/api/${method}`,
171
+ method: "POST",
172
+ auth: true,
173
+ timeoutMs: options.timeoutMs,
174
+ jsonBody: body,
175
+ slim: options.slim === true,
176
+ write: options.write === true,
177
+ });
178
+ return { result: result.body, discovery: this.lastDiscovery! };
179
+ }
180
+
181
+ async listAgents(timeoutMs: number): Promise<{ agents: unknown[]; discovery: Discovery }> {
182
+ const { result, discovery } = await this.rpc("listAgents", {}, { timeoutMs, slim: true });
183
+ if (!Array.isArray(result)) {
184
+ throw new CliError("gateway_internal", "Gateway listAgents did not return an array.");
185
+ }
186
+ return { agents: result, discovery };
187
+ }
188
+
189
+ async searchAgents(
190
+ query: string,
191
+ limit: number,
192
+ timeoutMs: number,
193
+ ): Promise<{ matches: unknown[]; discovery: Discovery }> {
194
+ const { result, discovery } = await this.rpc("searchAgents", { query, limit }, { timeoutMs });
195
+ if (!Array.isArray(result)) {
196
+ throw new CliError("gateway_internal", "Gateway searchAgents did not return an array.");
197
+ }
198
+ return { matches: result, discovery };
199
+ }
200
+
201
+ async getAgentTranscriptTail(
202
+ body: { id: string; limit: number; beforeSeq?: number },
203
+ timeoutMs: number,
204
+ ): Promise<{ result: unknown; discovery: Discovery }> {
205
+ const payload: Record<string, unknown> = { id: body.id, limit: body.limit };
206
+ if (body.beforeSeq !== undefined) payload.beforeSeq = body.beforeSeq;
207
+ return await this.rpc("getAgentTranscriptTail", payload, { timeoutMs });
208
+ }
209
+
210
+ async getAgentThread(
211
+ body: { id: string; rootId: string },
212
+ timeoutMs: number,
213
+ ): Promise<{ result: unknown; discovery: Discovery }> {
214
+ return await this.rpc("getAgentThread", { id: body.id, rootId: body.rootId }, { timeoutMs });
215
+ }
216
+
217
+ async getAgentMemories(id: string, timeoutMs: number): Promise<{ result: unknown; discovery: Discovery }> {
218
+ return await this.rpc("getAgentMemories", { id }, { timeoutMs });
219
+ }
220
+
221
+ async sendPrompt(
222
+ body: { agentId: string; prompt: string; clientNonce: string },
223
+ timeoutMs: number,
224
+ ): Promise<{ result: unknown; discovery: Discovery }> {
225
+ return await this.rpc(
226
+ "sendPrompt",
227
+ { agentId: body.agentId, prompt: body.prompt, clientNonce: body.clientNonce },
228
+ { timeoutMs, write: true },
229
+ );
230
+ }
231
+
232
+ async openEventStream(
233
+ channels: string[],
234
+ timeoutMs: number,
235
+ ): Promise<{ response: Response; discovery: Discovery }> {
236
+ const qs = channels.map((c) => encodeURIComponent(c)).join(",");
237
+ const result = await this.request({
238
+ kind: "read",
239
+ path: `/events?channels=${qs}`,
240
+ method: "GET",
241
+ auth: true,
242
+ timeoutMs,
243
+ slim: true,
244
+ accept: "text/event-stream",
245
+ stream: true,
246
+ });
247
+ return { response: result.response, discovery: this.lastDiscovery! };
248
+ }
249
+
250
+ private async request(input: {
251
+ kind: RequestKind;
252
+ path: string;
253
+ method: "GET" | "POST";
254
+ auth: boolean;
255
+ timeoutMs: number;
256
+ jsonBody?: Record<string, unknown>;
257
+ slim?: boolean;
258
+ write?: boolean;
259
+ accept?: string;
260
+ stream?: boolean;
261
+ }): Promise<HttpResult> {
262
+ const serialized = input.jsonBody === undefined ? undefined : JSON.stringify(input.jsonBody);
263
+ const write = input.write === true;
264
+ const attempt = async (): Promise<HttpResult> => {
265
+ const discovery = await this.load();
266
+ if (input.auth) this.requireToken(discovery);
267
+ return await this.sendOnce(discovery, input, serialized, write);
268
+ };
269
+
270
+ try {
271
+ return await this.interpret(await attempt());
272
+ } catch (error) {
273
+ if (error instanceof CliError && error.code === "gateway_unauthorized") {
274
+ return await this.interpret(await attempt());
275
+ }
276
+ if (error instanceof CliError && error.code === "gateway_unreachable") {
277
+ return await this.interpret(await attempt());
278
+ }
279
+ throw error;
280
+ }
281
+ }
282
+
283
+ private async sendOnce(
284
+ discovery: Discovery,
285
+ input: {
286
+ path: string;
287
+ method: "GET" | "POST";
288
+ auth: boolean;
289
+ timeoutMs: number;
290
+ slim?: boolean;
291
+ accept?: string;
292
+ stream?: boolean;
293
+ },
294
+ serialized: string | undefined,
295
+ write: boolean,
296
+ ): Promise<HttpResult> {
297
+ const headers = new Headers();
298
+ if (input.auth) headers.set("Authorization", `Bearer ${discovery.token}`);
299
+ if (serialized !== undefined) headers.set("Content-Type", "application/json");
300
+ headers.set("Accept", input.accept ?? "application/json");
301
+ if (input.slim) headers.set("x-sand-slim-avatars", "1");
302
+ const ac = new AbortController();
303
+ const timer = setTimeout(() => ac.abort(), input.timeoutMs);
304
+ let response: Response;
305
+ try {
306
+ response = await this.deps.fetch(`${discovery.baseUrl}${input.path}`, {
307
+ method: input.method,
308
+ headers,
309
+ body: serialized,
310
+ signal: ac.signal,
311
+ redirect: "manual",
312
+ });
313
+ } catch (error) {
314
+ clearTimeout(timer);
315
+ if (isAbortError(error)) {
316
+ if (write) {
317
+ throw new CliError(
318
+ "send_delivery_unknown",
319
+ "Send may have reached Gateway before the response was lost.",
320
+ );
321
+ }
322
+ throw new CliError("gateway_unreachable", "Gateway request timed out.");
323
+ }
324
+ throw new CliError("gateway_unreachable", "Gateway is unreachable.", {
325
+ retryable: true,
326
+ });
327
+ }
328
+ if (input.stream) {
329
+ clearTimeout(timer);
330
+ if (!response.ok) {
331
+ const text = await response.text();
332
+ let parsed: unknown;
333
+ try {
334
+ parsed = text.length === 0 ? null : JSON.parse(text);
335
+ } catch {
336
+ parsed = null;
337
+ }
338
+ throw httpStatusToError(response.status, parseFailureCode(parsed), "Gateway request failed.");
339
+ }
340
+ return { status: response.status, body: null, text: "", response };
341
+ }
342
+ let text = "";
343
+ try {
344
+ text = await response.text();
345
+ } catch {
346
+ clearTimeout(timer);
347
+ if (write) {
348
+ throw new CliError(
349
+ "send_delivery_unknown",
350
+ "Send may have reached Gateway before the response was lost.",
351
+ );
352
+ }
353
+ throw new CliError("gateway_unreachable", "Gateway is unreachable.");
354
+ } finally {
355
+ clearTimeout(timer);
356
+ }
357
+ let body: unknown = null;
358
+ if (text.length > 0) {
359
+ try {
360
+ body = JSON.parse(text);
361
+ } catch {
362
+ body = null;
363
+ }
364
+ }
365
+ return { status: response.status, body, text, response };
366
+ }
367
+
368
+ private interpret(result: HttpResult): HttpResult {
369
+ if (result.status >= 200 && result.status < 300) return result;
370
+ throw httpStatusToError(result.status, parseFailureCode(result.body), "Gateway request failed.");
371
+ }
372
+ }
373
+
374
+ export async function* parseSse(
375
+ stream: ReadableStream<Uint8Array>,
376
+ options: { onChunk?: () => void; signal?: AbortSignal } = {},
377
+ ): AsyncGenerator<unknown> {
378
+ const reader = stream.getReader();
379
+ const decoder = new TextDecoder();
380
+ let buf = "";
381
+ const abort = () => {
382
+ void reader.cancel();
383
+ };
384
+ options.signal?.addEventListener("abort", abort, { once: true });
385
+ try {
386
+ while (true) {
387
+ if (options.signal?.aborted) return;
388
+ const { done, value } = await reader.read();
389
+ if (done) {
390
+ buf += decoder.decode();
391
+ if (buf.length > 0 && !buf.endsWith("\n\n")) buf += "\n\n";
392
+ } else {
393
+ options.onChunk?.();
394
+ buf += decoder.decode(value, { stream: true });
395
+ }
396
+ buf = buf.replace(/\r\n/g, "\n");
397
+ let idx = buf.indexOf("\n\n");
398
+ while (idx >= 0) {
399
+ const block = buf.slice(0, idx);
400
+ buf = buf.slice(idx + 2);
401
+ const data = block
402
+ .split("\n")
403
+ .filter((line) => line.startsWith("data:"))
404
+ .map((line) => line.slice(5).replace(/^ /, ""))
405
+ .join("\n");
406
+ if (data.length > 0) {
407
+ try {
408
+ yield JSON.parse(data) as unknown;
409
+ } catch {
410
+ // skip malformed event frames
411
+ }
412
+ }
413
+ idx = buf.indexOf("\n\n");
414
+ }
415
+ if (done) break;
416
+ }
417
+ } finally {
418
+ options.signal?.removeEventListener("abort", abort);
419
+ reader.releaseLock();
420
+ }
421
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bun
2
+ import { createProductionDeps } from "./deps.ts";
3
+ import { runCli } from "./program.ts";
4
+
5
+ const code = await runCli(process.argv.slice(2), createProductionDeps());
6
+ process.exit(code);
package/src/opts.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { CliDeps } from "./deps.ts";
2
+ import { usage } from "./errors.ts";
3
+ import { DEFAULT_TIMEOUT_MS, PROMPT_MAX_BYTES } from "./registry.ts";
4
+ import { parseInteger, stripOneTrailingNewline, utf8Bytes } from "./util.ts";
5
+
6
+ export type IoOpts = {
7
+ json: boolean;
8
+ table: boolean;
9
+ timeoutMs: number;
10
+ };
11
+
12
+ export function ioFromOpts(opts: {
13
+ json?: boolean;
14
+ table?: boolean;
15
+ timeoutMs?: string | number;
16
+ }): IoOpts {
17
+ const json = Boolean(opts.json);
18
+ const table = Boolean(opts.table);
19
+ if (json && table) throw usage("--json and --table cannot be used together.");
20
+ return {
21
+ json,
22
+ table,
23
+ timeoutMs: parseInteger(opts.timeoutMs, {
24
+ name: "--timeout-ms",
25
+ min: 1,
26
+ max: 300_000,
27
+ defaultValue: DEFAULT_TIMEOUT_MS,
28
+ }),
29
+ };
30
+ }
31
+
32
+ export function rejectTable(table: boolean, allowed: boolean): void {
33
+ if (table && !allowed) throw usage("This command does not support --table.");
34
+ }
35
+
36
+ export async function readPrompt(text: string | undefined, deps: CliDeps): Promise<string> {
37
+ if (text !== undefined && !deps.stdinIsTTY) {
38
+ throw usage("Provide --text or stdin, not both.");
39
+ }
40
+ let prompt: string;
41
+ if (text !== undefined) {
42
+ prompt = text;
43
+ } else if (deps.stdinIsTTY) {
44
+ throw usage("Provide --text or pipe stdin. Interactive prompts are not supported.");
45
+ } else {
46
+ prompt = await deps.readStdin();
47
+ }
48
+ prompt = stripOneTrailingNewline(prompt);
49
+ if (prompt.length === 0) throw usage("Prompt is empty.");
50
+ if (utf8Bytes(prompt) > PROMPT_MAX_BYTES) throw usage("Prompt exceeds the 64 KiB CLI limit.");
51
+ return prompt;
52
+ }