grok-bot-cli 0.3.0 → 0.3.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/package.json +1 -1
- package/src/cli.js +38 -6
- package/src/codex-bridge.js +23 -7
- package/src/gateway.js +29 -6
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -16,7 +16,16 @@ function print(value) {
|
|
|
16
16
|
function fail(err) {
|
|
17
17
|
let message = err instanceof Error ? err.message : String(err);
|
|
18
18
|
message = redactSecrets(message);
|
|
19
|
-
|
|
19
|
+
if (jsonErrors && err instanceof Error) {
|
|
20
|
+
const out = { error: message };
|
|
21
|
+
if (err.delivery !== undefined) out.delivery = err.delivery;
|
|
22
|
+
if (err.threadId !== undefined) out.threadId = err.threadId;
|
|
23
|
+
if (err.turnId !== undefined) out.turnId = err.turnId;
|
|
24
|
+
if (err.targetId !== undefined) out.targetId = err.targetId;
|
|
25
|
+
process.stderr.write(JSON.stringify(out) + "\n");
|
|
26
|
+
} else {
|
|
27
|
+
process.stderr.write(message + "\n");
|
|
28
|
+
}
|
|
20
29
|
process.exit(1);
|
|
21
30
|
}
|
|
22
31
|
|
|
@@ -118,7 +127,19 @@ function hasFlag(args, name) {
|
|
|
118
127
|
return true;
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
/** Peel a trailing flag that `--` does not protect; free-text commands keep mid-text tokens. */
|
|
131
|
+
function takeTrailingFlag(args, name) {
|
|
132
|
+
const stop = args.indexOf("--");
|
|
133
|
+
const end = stop === -1 ? args.length : stop;
|
|
134
|
+
if (end > 0 && args[end - 1] === name) {
|
|
135
|
+
args.splice(end - 1, 1);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
121
141
|
/** Peel global CLI options only from the leading argv (before the command). */
|
|
142
|
+
let jsonErrors = false;
|
|
122
143
|
function takeLeadingGlobals(args) {
|
|
123
144
|
let json = false;
|
|
124
145
|
let gateway = false;
|
|
@@ -134,6 +155,7 @@ function takeLeadingGlobals(args) {
|
|
|
134
155
|
}
|
|
135
156
|
if (a === "--json") {
|
|
136
157
|
json = true;
|
|
158
|
+
jsonErrors = true;
|
|
137
159
|
args.shift();
|
|
138
160
|
continue;
|
|
139
161
|
}
|
|
@@ -301,6 +323,11 @@ function formatCodexThread(t) {
|
|
|
301
323
|
}
|
|
302
324
|
|
|
303
325
|
async function runCodex(sub, rest, json) {
|
|
326
|
+
// Structured subcommands take no free text, so --json peels anywhere. Send
|
|
327
|
+
// peels a trailing --json only; mid-message tokens stay message content.
|
|
328
|
+
if (sub === "status" || sub === "list-threads") {
|
|
329
|
+
if (hasFlag(rest, "--json")) { json = true; jsonErrors = true; }
|
|
330
|
+
}
|
|
304
331
|
if (sub === "status") {
|
|
305
332
|
const status = await codexStatus();
|
|
306
333
|
print(json ? status : formatCodexStatus(status));
|
|
@@ -319,6 +346,7 @@ async function runCodex(sub, rest, json) {
|
|
|
319
346
|
}
|
|
320
347
|
if (sub === "send") {
|
|
321
348
|
const threadId = rest.shift();
|
|
349
|
+
if (takeTrailingFlag(rest, "--json")) { json = true; jsonErrors = true; }
|
|
322
350
|
if (rest[0] === "--") rest.shift();
|
|
323
351
|
const message = rest.join(" ").trim();
|
|
324
352
|
if (!threadId || threadId.startsWith("-") || !message) throw new StoreError("gbot codex send <threadId> <message...>");
|
|
@@ -379,7 +407,7 @@ async function main(argv) {
|
|
|
379
407
|
if (cmd === "history") {
|
|
380
408
|
const options = args.slice(1);
|
|
381
409
|
// Command-local flags (globals only peel from argv before the command).
|
|
382
|
-
if (hasFlag(options, "--json")) json = true;
|
|
410
|
+
if (hasFlag(options, "--json")) { json = true; jsonErrors = true; }
|
|
383
411
|
const showPath = hasFlag(options, "--path");
|
|
384
412
|
const search = takeFlag(options, "--search");
|
|
385
413
|
const limitRaw = takeFlag(options, "--limit");
|
|
@@ -405,6 +433,12 @@ async function main(argv) {
|
|
|
405
433
|
return;
|
|
406
434
|
}
|
|
407
435
|
|
|
436
|
+
// Peel command-local --json before touching the backend so auth/gateway
|
|
437
|
+
// failures honor it. Send peels a trailing --json only (mid-text tokens stay
|
|
438
|
+
// message content); `--` protects everything after it.
|
|
439
|
+
if (cmd === "send" && takeTrailingFlag(rest, "--json")) { json = true; jsonErrors = true; }
|
|
440
|
+
if ((cmd === "thread" || cmd === "chat") && hasFlag(rest, "--json")) { json = true; jsonErrors = true; }
|
|
441
|
+
|
|
408
442
|
const backend = await openBackend({ root: rootFlag, gateway, files: filesMode });
|
|
409
443
|
|
|
410
444
|
if (cmd === "bots" && sub === "list") {
|
|
@@ -514,7 +548,7 @@ async function main(argv) {
|
|
|
514
548
|
|
|
515
549
|
if (cmd === "send") {
|
|
516
550
|
const ref = sub;
|
|
517
|
-
if (hasFlag(rest, "--json")) json = true;
|
|
551
|
+
if (hasFlag(rest, "--json")) { json = true; jsonErrors = true; }
|
|
518
552
|
const message = rest.join(" ").trim();
|
|
519
553
|
if (!ref || !message) throw new StoreError("gbot send <bot-or-group> <message...>");
|
|
520
554
|
const out = await backend.send(ref, message);
|
|
@@ -528,9 +562,7 @@ async function main(argv) {
|
|
|
528
562
|
if (cmd === "thread" || cmd === "chat") {
|
|
529
563
|
const ref = sub;
|
|
530
564
|
if (!ref) throw new StoreError("gbot thread <bot-or-group> [--limit N] [--root MESSAGE_ID] [--full]");
|
|
531
|
-
|
|
532
|
-
const full = rest.includes("--full");
|
|
533
|
-
if (full) rest.splice(rest.indexOf("--full"), 1);
|
|
565
|
+
const full = hasFlag(rest, "--full");
|
|
534
566
|
const limitRaw = takeFlag(rest, "--limit");
|
|
535
567
|
const rootId = takeFlag(rest, "--root");
|
|
536
568
|
const limit = limitRaw ? Number(limitRaw) : 40;
|
package/src/codex-bridge.js
CHANGED
|
@@ -173,10 +173,15 @@ export function connectCodexAppServer(path, { timeoutMs = 15000 } = {}) {
|
|
|
173
173
|
let fragOpcode = null;
|
|
174
174
|
let fragParts = [];
|
|
175
175
|
let fragBytes = 0;
|
|
176
|
+
// Absolute handshake deadline: socket timeouts reset on any bytes, so trickled
|
|
177
|
+
// headers must not extend this. Per-request timers stay absolute after upgrade.
|
|
178
|
+
const handshakeTimer = setTimeout(() => failAll(new Error("Timed out connecting to Codex app-server at " + path)), timeoutMs);
|
|
179
|
+
if (typeof handshakeTimer.unref === "function") handshakeTimer.unref();
|
|
176
180
|
|
|
177
181
|
const failAll = (err) => {
|
|
178
182
|
if (closed) return;
|
|
179
183
|
closed = true;
|
|
184
|
+
clearTimeout(handshakeTimer);
|
|
180
185
|
if (err && err.delivery == null) err.delivery = pending.size ? "unknown" : "rejected";
|
|
181
186
|
for (const { reject: rej } of pending.values()) rej(err);
|
|
182
187
|
pending.clear();
|
|
@@ -212,15 +217,20 @@ export function connectCodexAppServer(path, { timeoutMs = 15000 } = {}) {
|
|
|
212
217
|
close() {
|
|
213
218
|
if (closed) return;
|
|
214
219
|
closed = true;
|
|
220
|
+
clearTimeout(handshakeTimer);
|
|
215
221
|
const err = new Error("Codex client closed");
|
|
216
222
|
err.delivery = pending.size ? "unknown" : "rejected";
|
|
217
223
|
for (const { reject: rej } of pending.values()) rej(err);
|
|
218
224
|
pending.clear();
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
// Best-effort close frame, then guaranteed destruction so no path leaks the socket.
|
|
226
|
+
if (!socket.destroyed && upgraded) write(0x8, Buffer.from([0x03, 0xe8]));
|
|
227
|
+
if (socket.destroyed) return;
|
|
228
|
+
const forceDestroy = setTimeout(() => { try { socket.destroy(); } catch { /* already gone */ } }, 1000);
|
|
229
|
+
if (typeof forceDestroy.unref === "function") forceDestroy.unref();
|
|
230
|
+
socket.end(() => {
|
|
231
|
+
clearTimeout(forceDestroy);
|
|
232
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
233
|
+
});
|
|
224
234
|
},
|
|
225
235
|
};
|
|
226
236
|
|
|
@@ -256,10 +266,13 @@ export function connectCodexAppServer(path, { timeoutMs = 15000 } = {}) {
|
|
|
256
266
|
failAll(new Error("Codex app-server sent an unreadable message: " + err.message));
|
|
257
267
|
return;
|
|
258
268
|
}
|
|
269
|
+
if (!msg || typeof msg !== "object" || Array.isArray(msg)) {
|
|
270
|
+
failAll(new Error("Codex app-server sent a malformed message"));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
259
273
|
onMessage(msg);
|
|
260
274
|
};
|
|
261
275
|
|
|
262
|
-
socket.setTimeout(timeoutMs, () => failAll(new Error("Timed out connecting to Codex app-server at " + path)));
|
|
263
276
|
socket.once("error", (err) => failAll(new Error("Could not connect to Codex app-server at " + path + ": " + err.message)));
|
|
264
277
|
socket.once("close", () => failAll(new Error("Codex app-server closed the connection")));
|
|
265
278
|
socket.once("connect", () => socket.write(upgradeRequest(key)));
|
|
@@ -271,11 +284,13 @@ export function connectCodexAppServer(path, { timeoutMs = 15000 } = {}) {
|
|
|
271
284
|
if (buf.length > WS_MAX_HEADER_BYTES) failAll(new Error("Codex app-server handshake headers exceed " + WS_MAX_HEADER_BYTES + " bytes"));
|
|
272
285
|
return;
|
|
273
286
|
}
|
|
287
|
+
// Terminated headers hit the cap too; size is checked before any decoding.
|
|
288
|
+
if (end > WS_MAX_HEADER_BYTES) return failAll(new Error("Codex app-server handshake headers exceed " + WS_MAX_HEADER_BYTES + " bytes"));
|
|
274
289
|
const head = buf.subarray(0, end).toString();
|
|
275
290
|
buf = buf.subarray(end + 4);
|
|
276
291
|
if (!validateUpgradeHead(head, key)) return failAll(new Error("Codex app-server refused the WebSocket upgrade: " + head.split("\r\n")[0]));
|
|
277
292
|
upgraded = true;
|
|
278
|
-
|
|
293
|
+
clearTimeout(handshakeTimer);
|
|
279
294
|
resolve(client);
|
|
280
295
|
}
|
|
281
296
|
for (;;) {
|
|
@@ -290,6 +305,7 @@ export function connectCodexAppServer(path, { timeoutMs = 15000 } = {}) {
|
|
|
290
305
|
return;
|
|
291
306
|
}
|
|
292
307
|
buf = frame.rest;
|
|
308
|
+
if (frame.payload.length > WS_MAX_MESSAGE_BYTES) return failAll(new Error("Codex app-server frame exceeds " + WS_MAX_MESSAGE_BYTES + " bytes"));
|
|
293
309
|
if (frame.masked) return failProtocol("server frames must not be masked");
|
|
294
310
|
if (frame.opcode >= 0x8) {
|
|
295
311
|
if (!frame.fin || frame.payload.length > 125) return failProtocol("bad control frame");
|
package/src/gateway.js
CHANGED
|
@@ -82,11 +82,32 @@ export function hasGatewayAuth() {
|
|
|
82
82
|
return Boolean(gatewayOverride() || accessTokenFromEnv() || hasGrokBotGatewaySession());
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
async function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
async function readTextCapped(res, maxBytes) {
|
|
86
|
+
if (!res.body || typeof res.body.getReader !== "function") {
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
if (Buffer.byteLength(text, "utf8") > maxBytes) {
|
|
89
|
+
throw new GatewayError("Gateway response too large (over " + maxBytes + " bytes)");
|
|
90
|
+
}
|
|
91
|
+
return text;
|
|
92
|
+
}
|
|
93
|
+
const reader = res.body.getReader();
|
|
94
|
+
const chunks = [];
|
|
95
|
+
let bytes = 0;
|
|
96
|
+
for (;;) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done) break;
|
|
99
|
+
bytes += value.byteLength ?? value.length;
|
|
100
|
+
if (bytes > maxBytes) {
|
|
101
|
+
try { await reader.cancel(); } catch { /* already closed */ }
|
|
102
|
+
throw new GatewayError("Gateway response too large (over " + maxBytes + " bytes)");
|
|
103
|
+
}
|
|
104
|
+
chunks.push(value);
|
|
89
105
|
}
|
|
106
|
+
return Buffer.concat(chunks.map((c) => Buffer.from(c.buffer ?? c, c.byteOffset ?? 0, c.byteLength ?? c.length))).toString("utf8");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readJson(res) {
|
|
110
|
+
const text = await readTextCapped(res, GATEWAY_MAX_RESPONSE_BYTES);
|
|
90
111
|
if (!text) return {};
|
|
91
112
|
try {
|
|
92
113
|
return JSON.parse(text);
|
|
@@ -343,8 +364,10 @@ export async function sendPrompt(session, ref, prompt, extra = {}) {
|
|
|
343
364
|
}
|
|
344
365
|
throw err;
|
|
345
366
|
}
|
|
346
|
-
const messageId = data && typeof data.messageId === "string" ? data.messageId : null;
|
|
347
|
-
|
|
367
|
+
const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
|
|
368
|
+
// Only a confirmed receipt counts as accepted; anything else is unknown, never a silent accept.
|
|
369
|
+
// ponytail: full send/execution correlation envelope stays in #37.
|
|
370
|
+
return { target: rec, result: data && typeof data === "object" ? data : {}, delivery: messageId ? "accepted" : "unknown", ...(messageId ? { messageId } : {}) };
|
|
348
371
|
}
|
|
349
372
|
|
|
350
373
|
export async function getTranscriptTail(session, ref, limit = 40) {
|