grok-bot-cli 0.4.2 → 0.4.5
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/CHANGELOG.md +19 -0
- package/README.md +7 -3
- package/dist/.claude-plugin/marketplace.json +1 -1
- package/dist/.claude-plugin/plugin.json +1 -1
- package/dist/.codex-plugin/plugin.json +1 -1
- package/dist/.cursor-plugin/plugin.json +1 -1
- package/dist/INSTALL.md +1 -1
- package/dist/README.md +7 -3
- package/dist/agent-bundle.compile-evidence.json +1 -1
- package/dist/agent-bundle.manifest.json +1 -1
- package/dist/bin/gbot-flight.mjs +167 -55
- package/dist/bin/gbot.mjs +168 -56
- package/dist/install.mjs +1 -1
- package/dist/mcp/mcp-grok-bot-b8c2461e-flight.mjs +12 -1
- package/dist/mcp/mcp-grok-bot-b8c2461e.mjs +13 -2
- package/dist/package.json +1 -1
- package/dist/plugin.json +1 -1
- package/package.json +1 -1
package/dist/bin/gbot.mjs
CHANGED
|
@@ -84503,7 +84503,7 @@ if (import.meta.main) {
|
|
|
84503
84503
|
execute,
|
|
84504
84504
|
name: "gbot",
|
|
84505
84505
|
render,
|
|
84506
|
-
version: "0.4.
|
|
84506
|
+
version: "0.4.5"
|
|
84507
84507
|
});
|
|
84508
84508
|
}
|
|
84509
84509
|
|
|
@@ -84741,6 +84741,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
84741
84741
|
/* import */ var node_child_process__rspack_import_5 = __webpack_require__("node:child_process");
|
|
84742
84742
|
/* import */ var node_module__rspack_import_6 = __webpack_require__("node:module");
|
|
84743
84743
|
/* import */ var _codex_contract_js__rspack_import_7 = __webpack_require__("./src/core/codex/contract.js");
|
|
84744
|
+
/* import */ var _desktop_shim_js__rspack_import_8 = __webpack_require__("./src/core/desktop-shim.js");
|
|
84745
|
+
|
|
84744
84746
|
|
|
84745
84747
|
|
|
84746
84748
|
|
|
@@ -84999,11 +85001,42 @@ function isObject(value) {
|
|
|
84999
85001
|
const client = {
|
|
85000
85002
|
refused,
|
|
85001
85003
|
// Server-initiated requests are only *answered* once our own turn/start
|
|
85002
|
-
// is in flight
|
|
85003
|
-
//
|
|
85004
|
-
//
|
|
85005
|
-
//
|
|
85004
|
+
// is in flight, and then only when they name our own thread/turn:
|
|
85005
|
+
// anything earlier — or naming another thread or Desktop turn — belongs
|
|
85006
|
+
// to another client and must never be rejected on their behalf. Early
|
|
85007
|
+
// and foreign requests are still recorded; the connection closes right
|
|
85008
|
+
// after, so a pending request simply dies with it instead of denying
|
|
85009
|
+
// someone's approval.
|
|
85006
85010
|
answerServerRequests: false,
|
|
85011
|
+
expectedThreadId: null,
|
|
85012
|
+
expectedTurnId: null,
|
|
85013
|
+
// Approvals that name a turn while our own turn id is still unknown
|
|
85014
|
+
// (arriving before — or in the same socket batch as — the turn/start
|
|
85015
|
+
// acknowledgment) wait here instead of being classified foreign: once
|
|
85016
|
+
// the acknowledgment supplies the turn id, matching ones are answered
|
|
85017
|
+
// and the rest stay silent.
|
|
85018
|
+
deferred: [],
|
|
85019
|
+
// Adopt the acknowledged turn id, answering deferred requests that
|
|
85020
|
+
// prove to be ours. Runs inside the connection closure so it can send.
|
|
85021
|
+
_adoptTurn (turnId) {
|
|
85022
|
+
client.expectedTurnId = turnId;
|
|
85023
|
+
for (const entry of client.deferred.splice(0)){
|
|
85024
|
+
const params = entry.params && typeof entry.params === "object" && !Array.isArray(entry.params) ? entry.params : null;
|
|
85025
|
+
const threadId = params ? params.threadId ?? params.thread_id : null;
|
|
85026
|
+
const namedTurnId = params ? params.turnId ?? params.turn_id ?? (params.turn && params.turn.id) : null;
|
|
85027
|
+
if (threadId === client.expectedThreadId && namedTurnId === turnId) {
|
|
85028
|
+
entry.answered = true;
|
|
85029
|
+
sendJson({
|
|
85030
|
+
jsonrpc: "2.0",
|
|
85031
|
+
id: entry.id,
|
|
85032
|
+
error: {
|
|
85033
|
+
code: -32601,
|
|
85034
|
+
message: "gbot codex does not answer " + entry.method + "; configure approval_policy on the daemon"
|
|
85035
|
+
}
|
|
85036
|
+
});
|
|
85037
|
+
}
|
|
85038
|
+
}
|
|
85039
|
+
},
|
|
85007
85040
|
request (method, params) {
|
|
85008
85041
|
const id = nextId++;
|
|
85009
85042
|
return new Promise((res, rej)=>{
|
|
@@ -85067,13 +85100,36 @@ function isObject(value) {
|
|
|
85067
85100
|
};
|
|
85068
85101
|
const onMessage = (msg)=>{
|
|
85069
85102
|
if (msg.id != null && msg.method) {
|
|
85070
|
-
|
|
85071
|
-
|
|
85103
|
+
let answered = client.answerServerRequests;
|
|
85104
|
+
let defer = false;
|
|
85105
|
+
// Approval ownership: only answer requests for the turn gbot itself
|
|
85106
|
+
// started. A request naming another thread — or naming a turn that is
|
|
85107
|
+
// not ours — is recorded foreign-silent, never rejected. A request
|
|
85108
|
+
// naming a turn while our turn id is still unknown is deferred, not
|
|
85109
|
+
// classified: the turn/start acknowledgment decides it.
|
|
85110
|
+
if (answered) {
|
|
85111
|
+
const params = msg.params && typeof msg.params === "object" && !Array.isArray(msg.params) ? msg.params : null;
|
|
85112
|
+
if (params) {
|
|
85113
|
+
const threadId = params.threadId ?? params.thread_id;
|
|
85114
|
+
const turnId = params.turnId ?? params.turn_id ?? (params.turn && params.turn.id);
|
|
85115
|
+
if (threadId !== client.expectedThreadId || turnId == null) answered = false;
|
|
85116
|
+
else if (client.expectedTurnId == null) {
|
|
85117
|
+
answered = false;
|
|
85118
|
+
defer = true;
|
|
85119
|
+
} else if (turnId !== client.expectedTurnId) answered = false;
|
|
85120
|
+
} else answered = false;
|
|
85121
|
+
}
|
|
85122
|
+
const entry = {
|
|
85072
85123
|
id: msg.id,
|
|
85073
85124
|
method: msg.method,
|
|
85074
85125
|
params: msg.params,
|
|
85075
85126
|
answered
|
|
85076
|
-
}
|
|
85127
|
+
};
|
|
85128
|
+
refused.push(entry);
|
|
85129
|
+
if (defer) {
|
|
85130
|
+
client.deferred.push(entry);
|
|
85131
|
+
return;
|
|
85132
|
+
}
|
|
85077
85133
|
if (!answered) return;
|
|
85078
85134
|
sendJson({
|
|
85079
85135
|
jsonrpc: "2.0",
|
|
@@ -85318,17 +85374,30 @@ function listDesktopProcesses(timeoutMs = DESKTOP_PROCESS_LIST_TIMEOUT_MS) {
|
|
|
85318
85374
|
/**
|
|
85319
85375
|
* Status contract: `reachable` is endpoint reachability only. `schema.compatibility` is
|
|
85320
85376
|
* `exact` when the daemon reports the pinned version, otherwise `unverified` (methods
|
|
85321
|
-
* usually survive upgrades) or `unknown`. `
|
|
85322
|
-
* Desktop-
|
|
85323
|
-
*
|
|
85324
|
-
*
|
|
85377
|
+
* usually survive upgrades) or `unknown`. `desktopShimConfigured` records an installed
|
|
85378
|
+
* wrapper selected by Desktop-facing CODEX_CLI_PATH (the GUI domain on Darwin).
|
|
85379
|
+
* Configuration does not prove a running Desktop inherited it or connected.
|
|
85380
|
+
* `desktopAttached` is `"private-stdio"` when a Desktop-bundled app-server process
|
|
85381
|
+
* is visible, otherwise `"unknown"`. Managed attachment is not observable here.
|
|
85325
85382
|
*
|
|
85326
85383
|
* @param {NodeJS.ProcessEnv} [env]
|
|
85327
|
-
* @param {{ listProcesses?: string | string[] }} [opts]
|
|
85328
|
-
* defaults to a live `ps` snapshot
|
|
85329
|
-
|
|
85384
|
+
* @param {{ listProcesses?: string | string[], shim?: { installed?: boolean, wrapperPointsAtShim?: boolean } | null }} [opts]
|
|
85385
|
+
* injected process list and shim state for tests; defaults to a live `ps` snapshot and
|
|
85386
|
+
* `desktopShimStatus`. Never reads pipes or connects to Desktop.
|
|
85387
|
+
*/ async function codexStatus(env = process.env, { listProcesses, shim } = {}) {
|
|
85330
85388
|
const path = codexSocketPath(env);
|
|
85331
85389
|
const cli = probeLocalCodexVersion();
|
|
85390
|
+
let desktopShimConfigured = Boolean(shim && shim.installed && shim.wrapperPointsAtShim);
|
|
85391
|
+
if (shim === undefined) {
|
|
85392
|
+
try {
|
|
85393
|
+
const live = (0,_desktop_shim_js__rspack_import_8/* .desktopShimStatus */.em)({
|
|
85394
|
+
env
|
|
85395
|
+
});
|
|
85396
|
+
desktopShimConfigured = Boolean(live.installed && live.wrapperPointsAtShim);
|
|
85397
|
+
} catch {
|
|
85398
|
+
desktopShimConfigured = false;
|
|
85399
|
+
}
|
|
85400
|
+
}
|
|
85332
85401
|
const desktopAttached = detectDesktopPrivateAppServer({
|
|
85333
85402
|
platform: process.platform,
|
|
85334
85403
|
listProcesses: listProcesses ?? listDesktopProcesses()
|
|
@@ -85339,7 +85408,8 @@ function listDesktopProcesses(timeoutMs = DESKTOP_PROCESS_LIST_TIMEOUT_MS) {
|
|
|
85339
85408
|
pinnedVersion: PINNED_CODEX_VERSION,
|
|
85340
85409
|
cliVersion: cli.version,
|
|
85341
85410
|
cliVersionProbe: cli.probe,
|
|
85342
|
-
desktopAttached
|
|
85411
|
+
desktopAttached,
|
|
85412
|
+
desktopShimConfigured
|
|
85343
85413
|
};
|
|
85344
85414
|
let session;
|
|
85345
85415
|
try {
|
|
@@ -85741,11 +85811,15 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85741
85811
|
// the daemon exposes no compare-and-start request. Upgrade path: thread/queue/add + thread/queue/start once stable.
|
|
85742
85812
|
// Scope refusals to this turn: server requests from earlier calls belong to another context.
|
|
85743
85813
|
const seenRefused = client.refused.length;
|
|
85744
|
-
// Arm refusal replies only now that our own turn/start is in flight
|
|
85745
|
-
//
|
|
85746
|
-
//
|
|
85747
|
-
// never rejects another client's approval.
|
|
85748
|
-
//
|
|
85814
|
+
// Arm refusal replies only now that our own turn/start is in flight, scoped
|
|
85815
|
+
// to our own thread/turn: anything recorded earlier (resume, busy reject,
|
|
85816
|
+
// queue) — or naming another thread or Desktop turn — stays unanswered so
|
|
85817
|
+
// gbot never rejects another client's approval. Requests naming a turn
|
|
85818
|
+
// that arrives before the acknowledgment supplies our turn id wait in
|
|
85819
|
+
// client.deferred and are adopted only on a match. Requests missing
|
|
85820
|
+
// thread or turn IDs remain unanswered because ownership is unknown.
|
|
85821
|
+
client.expectedThreadId = threadId;
|
|
85822
|
+
client.expectedTurnId = null;
|
|
85749
85823
|
client.answerServerRequests = true;
|
|
85750
85824
|
let turn;
|
|
85751
85825
|
try {
|
|
@@ -85781,7 +85855,17 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85781
85855
|
envelope
|
|
85782
85856
|
});
|
|
85783
85857
|
}
|
|
85784
|
-
|
|
85858
|
+
client._adoptTurn(turnId);
|
|
85859
|
+
const freshRefused = client.refused.slice(seenRefused).filter((r)=>{
|
|
85860
|
+
if (!r.answered) return false;
|
|
85861
|
+
const params = r.params && typeof r.params === "object" && !Array.isArray(r.params) ? r.params : null;
|
|
85862
|
+
if (!params) return true;
|
|
85863
|
+
const refusedThreadId = params.threadId ?? params.thread_id;
|
|
85864
|
+
const refusedTurnId = params.turnId ?? params.turn_id ?? (params.turn && params.turn.id);
|
|
85865
|
+
if (refusedThreadId != null && refusedThreadId !== threadId) return false;
|
|
85866
|
+
if (refusedTurnId != null && refusedTurnId !== turnId) return false;
|
|
85867
|
+
return true;
|
|
85868
|
+
});
|
|
85785
85869
|
if (freshRefused.length) {
|
|
85786
85870
|
const methods = freshRefused.map((r)=>r.method).join(", ");
|
|
85787
85871
|
throw new CodexSendError("Turn " + turnId + " started on thread " + threadId + " but Codex asked for " + methods + ", which gbot refused. " + "Answer it in a Codex client, or set `approval_policy = \"never\"` in the daemon's config.toml for unattended sends.", {
|
|
@@ -85811,8 +85895,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
85811
85895
|
X$: () => (listCodexThreads),
|
|
85812
85896
|
ZS: () => (sendToCodexThread),
|
|
85813
85897
|
nq: () => (buildEnvelope),
|
|
85814
|
-
pF: () => (singleLine)
|
|
85815
|
-
ql: () => (codexSocketPath)
|
|
85898
|
+
pF: () => (singleLine)
|
|
85816
85899
|
});
|
|
85817
85900
|
|
|
85818
85901
|
|
|
@@ -86024,24 +86107,36 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
86024
86107
|
// Codex app-server control socket. Vendored from uploads/codex-stdio-to-daemon-ws.py
|
|
86025
86108
|
// (ChatGPT Desktop -> managed daemon shim) with fail-open deltas, marked below:
|
|
86026
86109
|
// (1) ws_connect enforces one absolute monotonic deadline for connect + HTTP
|
|
86027
|
-
// upgrade (CODEX_BRIDGE_CONNECT_TIMEOUT, default 10s),
|
|
86028
|
-
//
|
|
86029
|
-
//
|
|
86110
|
+
// upgrade (CODEX_BRIDGE_CONNECT_TIMEOUT, default 10s), requires HTTP/1.1 101
|
|
86111
|
+
// plus a matching Sec-WebSocket-Accept (a 200 fails even with a valid hash),
|
|
86112
|
+
// and retains bytes coalesced after the upgrade headers instead of discarding
|
|
86113
|
+
// the first data frame.
|
|
86030
86114
|
// (2) read_message assembles continuation frames; control frames answer inline;
|
|
86031
|
-
// socket
|
|
86032
|
-
//
|
|
86033
|
-
//
|
|
86034
|
-
//
|
|
86035
|
-
// daemon
|
|
86036
|
-
//
|
|
86037
|
-
// (
|
|
86038
|
-
//
|
|
86039
|
-
//
|
|
86040
|
-
//
|
|
86115
|
+
// every socket send/lock shares one monotonic budget (remaining first-RPC
|
|
86116
|
+
// budget, else the mid-session write budget) so a non-reading peer cannot
|
|
86117
|
+
// wedge pong writes or timeout cleanup; timeouts close and exit. Reads wait
|
|
86118
|
+
// with no timeout after the handshake so healthy idle sessions survive.
|
|
86119
|
+
// The wrapper never starts the daemon on the spawn path, so no daemon lock can
|
|
86120
|
+
// block pre-stdin Desktop.
|
|
86121
|
+
// (3) A first-response deadline (CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT, default
|
|
86122
|
+
// 30s) bounds connect-to-matching-daemon-response covering the first RPC;
|
|
86123
|
+
// unrelated notifications and foreign responses never satisfy it, only the
|
|
86124
|
+
// matching response/error (or timeout) clears it. The preflight (3s) +
|
|
86125
|
+
// upgrade (10s) + first RPC (30s) budget stays far below a daemon-lock hang.
|
|
86126
|
+
// Exits are 0 served, 1 pre-stdio (wrapper falls back to real Codex on pristine
|
|
86127
|
+
// stdio+stdout), 2 mid-session (wrapper exits so Desktop reconnects).
|
|
86128
|
+
// (4) Byte-precise commit on input OR output, committed BEFORE the stdout
|
|
86129
|
+
// write: os.read into a userspace line buffer counts every stdin byte (even
|
|
86130
|
+
// an unforwarded blank line), and any daemon payload marks stdout used before
|
|
86131
|
+
// it is written via a bounded stdout write, so the fallback only ever runs
|
|
86132
|
+
// on truly pristine stdio+stdout and never spawns a second Codex after used
|
|
86133
|
+
// stdout. Buffered EOF-tail data flushes before the WS Close, which drains
|
|
86134
|
+
// via shutdown+close with a bounded join. The reader signals a done event so
|
|
86135
|
+
// WS close wakes the select loop; no path leaves a thread hung.
|
|
86041
86136
|
// Desktop stdio hangs with stock `codex app-server proxy`, so this custom
|
|
86042
86137
|
// bridge is required. Install writes it to the CODEX_HOME bin dir. Never reads
|
|
86043
86138
|
// Desktop private pipes (CODEX_APP_TOOLS_PIPE_PATH, /tmp/codex-browser-use/).
|
|
86044
|
-
const BRIDGE_SOURCE = "#!/usr/bin/env python3\n\"\"\"Stdio JSONL (Desktop) <-> WebSocket-over-unix (managed app-server daemon).\nReversible: remove CODEX_CLI_PATH wrapper. Does not read Desktop private pipes.\n\"\"\"\nfrom __future__ import annotations\nimport base64, hashlib, json, os, select, socket, struct, sys, threading, time\n\nSOCK = os.environ.get(\n \"CODEX_APP_SERVER_SOCK\",\n os.path.expanduser(\"~/.codex/app-server-control/app-server-control.sock\"),\n)\nLOG = os.environ.get(\"CODEX_STDIO_BRIDGE_LOG\", os.path.expanduser(\"~/.codex/bin/codex-stdio-to-daemon-ws.log\"))\nWS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n\n# Wrapper contract: 0 served the session, 1 failed before any stdin was\n# consumed (wrapper falls back to real Codex on pristine stdio), 2 failed\n# mid-session (wrapper exits promptly so Desktop reconnects; the fallback\n# must never run on half-consumed stdin).\nEXIT_SERVED = 0\nEXIT_PRE_STDIO = 1\nEXIT_MID_SESSION = 2\n\nCONNECT_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_CONNECT_TIMEOUT\", \"10\"))\n# Bound from connect to the first daemon message: a daemon that completes the\n# upgrade but never answers the first RPC must not hold Desktop past this.\nFIRST_MESSAGE_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT\", \"30\"))\n\ndef log(msg: str) -> None:\n try:\n with open(LOG, \"a\") as f:\n f.write(time.strftime(\"%Y-%m-%dT%H:%M:%SZ\", time.gmtime()) + \" \" + msg + \"\\n\")\n except OSError:\n pass\n\ndef ws_connect(path: str, timeout: float = CONNECT_TIMEOUT_S):\n \"\"\"Connect + HTTP upgrade under one absolute monotonic deadline.\n\n Returns (socket, trailing) where trailing holds bytes coalesced after the\n upgrade headers (TCP may deliver the 101 and the first WS frame together;\n discarding them would lose the first message). Validates the 101 status\n and Sec-WebSocket-Accept so a non-WebSocket listener fails fast.\n \"\"\"\n deadline = time.monotonic() + timeout\n s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n s.settimeout(timeout)\n try:\n s.connect(path)\n key = base64.b64encode(os.urandom(16)).decode()\n expected = base64.b64encode(hashlib.sha1((key + WS_GUID).encode()).digest()).decode()\n req = (\n f\"GET /rpc HTTP/1.1\\r\\nHost: localhost\\r\\nUpgrade: websocket\\r\\n\"\n f\"Connection: Upgrade\\r\\nSec-WebSocket-Key: {key}\\r\\nSec-WebSocket-Version: 13\\r\\n\\r\\n\"\n ).encode()\n s.sendall(req)\n buf = b\"\"\n while True:\n if b\"\\r\\n\\r\\n\" in buf:\n break\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"WebSocket upgrade timed out\")\n if len(buf) > 65536:\n raise ConnectionError(\"WS upgrade headers too large\")\n s.settimeout(remaining)\n chunk = s.recv(4096)\n if not chunk:\n raise ConnectionError(\"daemon closed during WS upgrade\")\n buf += chunk\n head, _, trailing = buf.partition(b\"\\r\\n\\r\\n\")\n lines = head.split(b\"\\r\\n\")\n if b\"101\" not in lines[0]:\n raise ConnectionError(f\"WS upgrade failed: {head[:200]!r}\")\n accept = None\n for line in lines[1:]:\n name, sep, value = line.partition(b\":\")\n if sep and name.strip().lower() == b\"sec-websocket-accept\":\n accept = value.strip().decode(\"latin-1\")\n if accept != expected:\n raise ConnectionError(\"WS upgrade accept mismatch\")\n except Exception:\n try:\n s.close()\n except OSError:\n pass\n raise\n s.settimeout(None)\n return s, trailing\n\ndef mask_send(sock: socket.socket, payload: bytes, opcode: int = 1) -> None:\n mask = os.urandom(4)\n ln = len(payload)\n if ln < 126:\n hdr = bytes([0x80 | opcode, 0x80 | ln])\n elif ln < 65536:\n hdr = bytes([0x80 | opcode, 0x80 | 126]) + struct.pack(\"!H\", ln)\n else:\n hdr = bytes([0x80 | opcode, 0x80 | 127]) + struct.pack(\"!Q\", ln)\n sock.sendall(hdr + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))\n\ndef locked_send(sock: socket.socket, lock: threading.Lock, payload: bytes, opcode: int = 1) -> None:\n with lock:\n mask_send(sock, payload, opcode=opcode)\n\ndef _buffered_reader(sock: socket.socket, initial: bytes = b\"\"):\n buf = bytearray(initial)\n def recv(n):\n while len(buf) < n:\n chunk = sock.recv(n - len(buf))\n if not chunk:\n raise ConnectionError(\"eof\")\n buf.extend(chunk)\n out = bytes(buf[:n])\n del buf[:n]\n return out\n return recv\n\ndef read_frame(recv):\n hdr = recv(2)\n fin = bool(hdr[0] & 0x80)\n opcode = hdr[0] & 0x0F\n masked = bool(hdr[1] & 0x80)\n ln = hdr[1] & 0x7F\n if ln == 126:\n ln = struct.unpack(\"!H\", recv(2))[0]\n elif ln == 127:\n ln = struct.unpack(\"!Q\", recv(8))[0]\n mask = recv(4) if masked else b\"\"\n payload = recv(ln)\n if masked:\n payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))\n return fin, opcode, payload\n\ndef read_message(recv, send_pong):\n \"\"\"Next complete data message, assembling continuation frames.\n\n Control frames are handled inline (pong answered, close reported) so a\n fragmented message split around a ping still reassembles.\n \"\"\"\n opcode = None\n parts = []\n while True:\n fin, op, payload = read_frame(recv)\n if op == 0x8:\n return (\"close\", None, b\"\")\n if op == 0x9:\n send_pong(payload)\n continue\n if op == 0xA:\n continue\n if op in (0x1, 0x2):\n if opcode is not None:\n raise ConnectionError(\"data frame inside fragmented message\")\n if fin:\n return (\"data\", op, payload)\n opcode = op\n parts.append(payload)\n continue\n if op == 0x0:\n if opcode is None:\n raise ConnectionError(\"stray continuation frame\")\n parts.append(payload)\n if fin:\n return (\"data\", opcode, b\"\".join(parts))\n continue\n raise ConnectionError(f\"unsupported opcode {op}\")\n\ndef stdout_writer(sock: socket.socket, recv, send_pong, done: threading.Event, first_msg: threading.Event) -> None:\n out = sys.stdout.buffer\n try:\n while True:\n kind, opcode, payload = read_message(recv, send_pong)\n if kind == \"close\":\n log(\"daemon WS close\")\n return\n if opcode == 0x1:\n # Desktop StdioConnection expects newline-delimited JSON text\n if not payload.endswith(b\"\\n\"):\n payload += b\"\\n\"\n out.write(payload)\n out.flush()\n else:\n out.write(payload)\n out.flush()\n first_msg.set()\n except Exception as e:\n log(f\"stdout_writer exit: {e}\")\n finally:\n done.set()\n\ndef main() -> int:\n log(f\"start sock={SOCK}\")\n try:\n sock, pending = ws_connect(SOCK)\n except Exception as e:\n log(f\"connect failed: {e}\")\n sys.stderr.write(f\"codex-stdio-to-daemon-ws: {e}\\n\")\n return EXIT_PRE_STDIO\n log(\"connected\")\n connected_at = time.monotonic()\n send_lock = threading.Lock()\n def send_pong(payload: bytes) -> None:\n locked_send(sock, send_lock, payload, opcode=0xA)\n recv = _buffered_reader(sock, pending)\n done = threading.Event()\n first_msg = threading.Event()\n t = threading.Thread(target=stdout_writer, args=(sock, recv, send_pong, done, first_msg), daemon=True)\n t.start()\n # Byte-precise commit point: the fallback may run only while zero stdin\n # bytes have left the pipe. Reads use os.read into a userspace line buffer\n # (buffered readline could readahead past the commit point), and every\n # byte read counts — even a blank line. first_msg bounds the first RPC.\n stdin_bytes = 0\n pending_in = b\"\"\n clean_eof = False\n try:\n fd = sys.stdin.fileno()\n while not done.is_set():\n if not first_msg.is_set() and time.monotonic() - connected_at > FIRST_MESSAGE_TIMEOUT_S:\n log(\"first daemon message timed out\")\n break\n ready, _, _ = select.select([fd], [], [], 0.5)\n if not ready:\n continue\n chunk = os.read(fd, 65536)\n if not chunk:\n clean_eof = True\n break\n stdin_bytes += len(chunk)\n pending_in += chunk\n while b\"\\n\" in pending_in:\n line, _, pending_in = pending_in.partition(b\"\\n\")\n line = line.strip()\n if not line:\n continue\n locked_send(sock, send_lock, line)\n except Exception as e:\n log(f\"stdin loop exit: {e}\")\n try:\n locked_send(sock, send_lock, b\"\", opcode=0x8)\n except Exception:\n pass\n if clean_eof:\n tail = pending_in.strip()\n if tail and not done.is_set():\n try:\n locked_send(sock, send_lock, tail)\n except Exception as e:\n log(f\"eof flush failed: {e}\")\n log(\"exit\")\n return EXIT_SERVED\n # Reader died, first RPC timed out, or stdin broke: fail open only if no\n # stdin byte was ever consumed.\n rc = EXIT_MID_SESSION if stdin_bytes else EXIT_PRE_STDIO\n log(f\"exit rc={rc}\")\n return rc\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n";
|
|
86139
|
+
const BRIDGE_SOURCE = "#!/usr/bin/env python3\n\"\"\"Stdio JSONL (Desktop) <-> WebSocket-over-unix (managed app-server daemon).\nReversible: remove CODEX_CLI_PATH wrapper. Does not read Desktop private pipes.\n\"\"\"\nfrom __future__ import annotations\nimport base64, hashlib, json, os, select, socket, struct, sys, threading, time\n\nSOCK = os.environ.get(\n \"CODEX_APP_SERVER_SOCK\",\n os.path.expanduser(\"~/.codex/app-server-control/app-server-control.sock\"),\n)\nLOG = os.environ.get(\"CODEX_STDIO_BRIDGE_LOG\", os.path.expanduser(\"~/.codex/bin/codex-stdio-to-daemon-ws.log\"))\nWS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n\n# Wrapper contract: 0 served the session, 1 failed while stdio was still\n# pristine -- no stdin byte consumed and no stdout frame committed (wrapper\n# falls back to real Codex), 2 failed mid-session (wrapper exits promptly so\n# Desktop reconnects; the fallback must never run on half-consumed stdin or\n# dirty stdout).\nEXIT_SERVED = 0\nEXIT_PRE_STDIO = 1\nEXIT_MID_SESSION = 2\n\nCONNECT_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_CONNECT_TIMEOUT\", \"10\"))\n# Bound from connect to the matching first daemon response: a daemon that\n# completes the upgrade but never answers the first RPC must not hold Desktop\n# past this. Only the matching initialize response/error (or the deadline\n# itself) clears it; notifications never do.\nFIRST_MESSAGE_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT\", \"30\"))\n# Bound on every mid-session socket send and stdout write: a wedged or\n# non-reading peer must not hang Desktop past this. Reads after a successful\n# init block with no timeout so healthy idle sessions survive silence.\nIO_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_IO_TIMEOUT\", \"30\"))\n\ndef log(msg: str) -> None:\n try:\n with open(LOG, \"a\") as f:\n f.write(time.strftime(\"%Y-%m-%dT%H:%M:%SZ\", time.gmtime()) + \" \" + msg + \"\\n\")\n except OSError:\n pass\n\ndef ws_connect(path: str, timeout: float = CONNECT_TIMEOUT_S):\n \"\"\"Connect + HTTP upgrade under one absolute monotonic deadline.\n\n Returns (socket, trailing) where trailing holds bytes coalesced after the\n upgrade headers (TCP may deliver the 101 and the first WS frame together;\n discarding them would lose the first message). Requires HTTP/1.1 101 plus\n a matching Sec-WebSocket-Accept so a 200 (or any non-101) fails fast even\n when the accept hash happens to be valid.\n \"\"\"\n deadline = time.monotonic() + timeout\n s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n s.settimeout(timeout)\n try:\n s.connect(path)\n key = base64.b64encode(os.urandom(16)).decode()\n expected = base64.b64encode(hashlib.sha1((key + WS_GUID).encode()).digest()).decode()\n req = (\n f\"GET /rpc HTTP/1.1\\r\\nHost: localhost\\r\\nUpgrade: websocket\\r\\n\"\n f\"Connection: Upgrade\\r\\nSec-WebSocket-Key: {key}\\r\\nSec-WebSocket-Version: 13\\r\\n\\r\\n\"\n ).encode()\n s.settimeout(max(0.0, deadline - time.monotonic()))\n s.sendall(req)\n buf = b\"\"\n while True:\n if b\"\\r\\n\\r\\n\" in buf:\n break\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"WebSocket upgrade timed out\")\n if len(buf) > 65536:\n raise ConnectionError(\"WS upgrade headers too large\")\n s.settimeout(remaining)\n chunk = s.recv(4096)\n if not chunk:\n raise ConnectionError(\"daemon closed during WS upgrade\")\n buf += chunk\n head, _, trailing = buf.partition(b\"\\r\\n\\r\\n\")\n lines = head.split(b\"\\r\\n\")\n if not lines or lines[0].split(b\" \", 2)[:2] != [b\"HTTP/1.1\", b\"101\"]:\n raise ConnectionError(f\"WS upgrade failed: {head[:200]!r}\")\n accept = None\n for line in lines[1:]:\n name, sep, value = line.partition(b\":\")\n if sep and name.strip().lower() == b\"sec-websocket-accept\":\n accept = value.strip().decode(\"latin-1\")\n if accept != expected:\n raise ConnectionError(\"WS upgrade accept mismatch\")\n except Exception:\n try:\n s.close()\n except OSError:\n pass\n raise\n # Keep one socket mode across reader and writer threads. Reads wait\n # indefinitely in select; sends use their own absolute deadline.\n s.setblocking(False)\n return s, trailing\n\ndef mask_send(sock: socket.socket, payload: bytes, opcode: int = 1, deadline=None) -> None:\n mask = os.urandom(4)\n ln = len(payload)\n if ln < 126:\n hdr = bytes([0x80 | opcode, 0x80 | ln])\n elif ln < 65536:\n hdr = bytes([0x80 | opcode, 0x80 | 126]) + struct.pack(\"!H\", ln)\n else:\n hdr = bytes([0x80 | opcode, 0x80 | 127]) + struct.pack(\"!Q\", ln)\n view = memoryview(hdr + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))\n while view:\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"socket write timed out\")\n _, writable, _ = select.select([], [sock], [], remaining)\n if not writable:\n raise TimeoutError(\"socket write timed out\")\n try:\n n = sock.send(view)\n except BlockingIOError:\n continue\n if not n:\n raise ConnectionError(\"socket closed during write\")\n view = view[n:]\n\ndef locked_send(sock: socket.socket, lock: threading.Lock, payload: bytes, opcode: int = 1, timeout: float = CONNECT_TIMEOUT_S) -> None:\n \"\"\"Serialized send sharing one monotonic budget across lock wait + send.\n\n A non-reading peer must never wedge pong writes or timeout cleanup: on\n expiry raise TimeoutError so the caller closes and exits without hanging.\n \"\"\"\n if timeout is None:\n timeout = CONNECT_TIMEOUT_S\n deadline = time.monotonic() + timeout\n if timeout <= 0:\n raise TimeoutError(\"send budget expired\")\n if not lock.acquire(timeout=timeout):\n raise TimeoutError(\"send lock timed out\")\n try:\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"send budget expired\")\n mask_send(sock, payload, opcode=opcode, deadline=deadline)\n finally:\n lock.release()\n\ndef _buffered_reader(sock: socket.socket, initial: bytes = b\"\"):\n buf = bytearray(initial)\n def recv(n):\n while len(buf) < n:\n select.select([sock], [], [])\n try:\n chunk = sock.recv(n - len(buf))\n except BlockingIOError:\n continue\n if not chunk:\n raise ConnectionError(\"eof\")\n buf.extend(chunk)\n out = bytes(buf[:n])\n del buf[:n]\n return out\n return recv\n\ndef read_frame(recv):\n hdr = recv(2)\n fin = bool(hdr[0] & 0x80)\n opcode = hdr[0] & 0x0F\n masked = bool(hdr[1] & 0x80)\n ln = hdr[1] & 0x7F\n if ln == 126:\n ln = struct.unpack(\"!H\", recv(2))[0]\n elif ln == 127:\n ln = struct.unpack(\"!Q\", recv(8))[0]\n mask = recv(4) if masked else b\"\"\n payload = recv(ln)\n if masked:\n payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))\n return fin, opcode, payload\n\ndef read_message(recv, send_pong):\n \"\"\"Next complete data message, assembling continuation frames.\n\n Control frames are handled inline (pong answered, close reported) so a\n fragmented message split around a ping still reassembles.\n \"\"\"\n opcode = None\n parts = []\n while True:\n fin, op, payload = read_frame(recv)\n if op == 0x8:\n return (\"close\", None, b\"\")\n if op == 0x9:\n send_pong(payload)\n continue\n if op == 0xA:\n continue\n if op in (0x1, 0x2):\n if opcode is not None:\n raise ConnectionError(\"data frame inside fragmented message\")\n if fin:\n return (\"data\", op, payload)\n opcode = op\n parts.append(payload)\n continue\n if op == 0x0:\n if opcode is None:\n raise ConnectionError(\"stray continuation frame\")\n parts.append(payload)\n if fin:\n return (\"data\", opcode, b\"\".join(parts))\n continue\n raise ConnectionError(f\"unsupported opcode {op}\")\n\ndef _is_daemon_response(payload: bytes, expect_id=None, known_ids=None) -> bool:\n \"\"\"True only for the matching JSON-RPC response/error, never a notification.\n\n The first-RPC timer must survive unrelated daemon notifications and foreign\n responses: only the response/error whose id matches the client's initialize\n (or, before it is known, any id the client already sent) proves the first\n RPC got an answer. Timeouts clear it via the deadline, never via data.\n \"\"\"\n try:\n text = payload.decode(\"utf-8\").strip()\n msg = json.loads(text)\n except Exception:\n return False\n if not isinstance(msg, dict):\n return False\n rid = msg.get(\"id\")\n if rid is None or \"method\" in msg or (\"result\" not in msg and \"error\" not in msg):\n return False\n if expect_id is not None:\n return rid == expect_id\n if known_ids:\n return rid in known_ids\n return False\n\ndef _bounded_stdout_write(data: bytes, timeout: float = IO_TIMEOUT_S) -> None:\n \"\"\"Write one daemon frame to stdout without hanging on a non-reading peer.\n\n Waits for writability under the mid-session write budget and writes via\n os.write in a loop, so a Desktop that stopped reading cannot hang the\n bridge and then exit 1 on dirty stdout.\n \"\"\"\n if timeout is None:\n timeout = IO_TIMEOUT_S\n if timeout <= 0:\n raise TimeoutError(\"stdout budget expired\")\n fd = sys.stdout.fileno()\n deadline = time.monotonic() + timeout\n was_blocking = os.get_blocking(fd)\n os.set_blocking(fd, False)\n try:\n view = memoryview(data)\n while view:\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"stdout write timed out\")\n _, writable, _ = select.select([], [fd], [], remaining)\n if not writable:\n raise TimeoutError(\"stdout write timed out\")\n try:\n n = os.write(fd, view)\n except BlockingIOError:\n continue\n view = view[n:]\n finally:\n os.set_blocking(fd, was_blocking)\n\ndef stdout_writer(sock: socket.socket, recv, send_pong, done: threading.Event, first_msg: threading.Event, output_forwarded: threading.Event, client_ids=None, init_state=None) -> None:\n try:\n while True:\n kind, opcode, payload = read_message(recv, send_pong)\n if kind == \"close\":\n log(\"daemon WS close\")\n return\n if opcode == 0x1:\n # Desktop StdioConnection expects newline-delimited JSON text\n if not payload.endswith(b\"\\n\"):\n payload += b\"\\n\"\n # Commit BEFORE writing: once a daemon byte is about to hit\n # stdout, the session is dirty and the fallback must never run,\n # even if the write itself blocks or fails.\n output_forwarded.set()\n _bounded_stdout_write(payload)\n if opcode == 0x1:\n expect = (init_state or {}).get(\"initialize_id\") if isinstance(init_state, dict) else None\n if _is_daemon_response(payload, expect, client_ids or ()):\n first_msg.set()\n except Exception as e:\n log(f\"stdout_writer exit: {e}\")\n finally:\n done.set()\n\ndef main() -> int:\n log(f\"start sock={SOCK}\")\n try:\n sock, pending = ws_connect(SOCK)\n except Exception as e:\n log(f\"connect failed: {e}\")\n sys.stderr.write(f\"codex-stdio-to-daemon-ws: {e}\\n\")\n return EXIT_PRE_STDIO\n log(\"connected\")\n connected_at = time.monotonic()\n first_deadline = connected_at + FIRST_MESSAGE_TIMEOUT_S\n send_lock = threading.Lock()\n done = threading.Event()\n first_msg = threading.Event()\n output_forwarded = threading.Event()\n client_ids = set()\n init_state = {\"initialize_id\": None}\n\n def _send_timeout() -> float:\n # Before the first matching daemon response the remaining first-RPC\n # budget bounds every send; after it each send gets the mid-session\n # write budget. Idle reads are never timed out.\n if not first_msg.is_set():\n return max(0.0, first_deadline - time.monotonic())\n return IO_TIMEOUT_S\n\n def _cleanup_timeout() -> float:\n # Timeout cleanup stays within the expired session budget: once the\n # first-RPC deadline has passed, close directly instead of opening\n # fresh windows that let a short budget take several timeout periods.\n if not first_msg.is_set():\n return max(0.0, first_deadline - time.monotonic())\n return CONNECT_TIMEOUT_S\n\n def send_pong(payload: bytes) -> None:\n locked_send(sock, send_lock, payload, opcode=0xA, timeout=_send_timeout())\n recv = _buffered_reader(sock, pending)\n t = threading.Thread(target=stdout_writer, args=(sock, recv, send_pong, done, first_msg, output_forwarded, client_ids, init_state), daemon=True)\n t.start()\n # Byte-precise commit point: the fallback may run only while zero stdin\n # bytes have left the pipe and no daemon payload was committed to stdout.\n # Reads use os.read into a userspace line buffer (buffered readline could\n # readahead past the commit point), and every byte read counts -- even a\n # blank line. first_msg tracks the matching daemon response/error, never\n # a notification.\n stdin_bytes = 0\n pending_in = b\"\"\n clean_eof = False\n try:\n fd = sys.stdin.fileno()\n while not done.is_set():\n if not first_msg.is_set() and time.monotonic() >= first_deadline:\n log(\"first daemon message timed out\")\n break\n ready, _, _ = select.select([fd], [], [], 0.5)\n if not ready:\n continue\n chunk = os.read(fd, 65536)\n if not chunk:\n clean_eof = True\n break\n stdin_bytes += len(chunk)\n pending_in += chunk\n while b\"\\n\" in pending_in:\n line, _, pending_in = pending_in.partition(b\"\\n\")\n line = line.strip()\n if not line:\n continue\n try:\n try:\n obj = json.loads(line.decode(\"utf-8\"))\n except Exception:\n obj = None\n if isinstance(obj, dict) and obj.get(\"id\") is not None:\n client_ids.add(obj.get(\"id\"))\n if obj.get(\"method\") == \"initialize\" and init_state[\"initialize_id\"] is None:\n init_state[\"initialize_id\"] = obj.get(\"id\")\n except Exception:\n pass\n try:\n locked_send(sock, send_lock, line, timeout=_send_timeout())\n except TimeoutError as e:\n log(f\"stdin forward timed out: {e}\")\n break\n except Exception as e:\n log(f\"stdin forward failed: {e}\")\n break\n else:\n continue\n break\n except Exception as e:\n log(f\"stdin loop exit: {e}\")\n # Flush any buffered EOF-tail data BEFORE the WS Close: sending Close\n # first would let the daemon discard the tail.\n if clean_eof:\n tail = pending_in.strip()\n if tail and not done.is_set():\n try:\n locked_send(sock, send_lock, tail, timeout=_cleanup_timeout())\n except Exception as e:\n log(f\"eof flush failed: {e}\")\n try:\n locked_send(sock, send_lock, b\"\", opcode=0x8, timeout=_cleanup_timeout())\n except Exception as e:\n log(f\"close send failed: {e}\")\n try:\n try:\n sock.shutdown(socket.SHUT_RDWR)\n except OSError:\n pass\n sock.close()\n except OSError:\n pass\n # Bound termination: wake the reader so no thread is left hung on recv.\n t.join(timeout=0.5)\n # A reader still exiting could commit output after this check. In that\n # case pristine stdio cannot be proven, so never permit fallback.\n if t.is_alive():\n output_forwarded.set()\n if clean_eof:\n log(\"exit\")\n return EXIT_SERVED\n # Reader died, first RPC timed out, post-upgrade I/O timed out, or stdin\n # broke: fail open only while stdio is still pristine -- no stdin byte\n # ever consumed AND no daemon payload ever committed to stdout. Committed\n # output forces a mid-session exit so Desktop reconnects instead of\n # falling back to stock Codex on a corrupted stream.\n rc = EXIT_MID_SESSION if (stdin_bytes or output_forwarded.is_set()) else EXIT_PRE_STDIO\n log(f\"exit rc={rc}\")\n return rc\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n";
|
|
86045
86140
|
|
|
86046
86141
|
__webpack_require__.d(__webpack_exports__, {
|
|
86047
86142
|
}, {
|
|
@@ -86055,9 +86150,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
86055
86150
|
/* import */ var node_fs__rspack_import_1 = __webpack_require__("node:fs");
|
|
86056
86151
|
/* import */ var node_os__rspack_import_2 = __webpack_require__("node:os");
|
|
86057
86152
|
/* import */ var node_path__rspack_import_3 = __webpack_require__("node:path");
|
|
86058
|
-
/* import */ var
|
|
86059
|
-
/* import */ var _codex_bridge_js__rspack_import_4 = __webpack_require__("./src/core/codex-bridge.js");
|
|
86060
|
-
|
|
86153
|
+
/* import */ var _desktop_shim_bridge_js__rspack_import_4 = __webpack_require__("./src/core/desktop-shim-bridge.js");
|
|
86061
86154
|
|
|
86062
86155
|
|
|
86063
86156
|
|
|
@@ -86135,11 +86228,12 @@ function renderWrapperScript({ realPath, bridgePath, wrapperLogPath, codexHome,
|
|
|
86135
86228
|
# daemon's control socket. Reversible: \`gbot codex desktop-shim uninstall\`.
|
|
86136
86229
|
# The daemon is kept up by the LaunchAgent login script, not here: this hot path
|
|
86137
86230
|
# never starts the daemon itself, so a wedged daemon lock cannot hang
|
|
86138
|
-
# Desktop. Fail-open runs ONLY
|
|
86139
|
-
#
|
|
86231
|
+
# Desktop. Fail-open runs ONLY while stdio is still pristine (no stdin
|
|
86232
|
+
# consumed, no stdout written): an absent socket fails the preflight and a
|
|
86233
|
+
# pre-session bridge failure (exit 1) falls through
|
|
86140
86234
|
# to the real standalone codex on pristine stdio. A mid-session bridge failure
|
|
86141
86235
|
# (exit 2+) exits promptly so Desktop reconnects; the fallback never runs on
|
|
86142
|
-
# half-consumed stdin. Bridge exit 0 means it served the session. Never touches
|
|
86236
|
+
# half-consumed stdin or dirty stdout. Bridge exit 0 means it served the session. Never touches
|
|
86143
86237
|
# Desktop binaries or its private tool pipe.
|
|
86144
86238
|
set -u
|
|
86145
86239
|
REAL="\${CODEX_DESKTOP_WRAPPER_REAL:-${realPath}}"
|
|
@@ -86156,7 +86250,11 @@ echo "$(ts) argv: $*" >>"$LOG" 2>/dev/null || true
|
|
|
86156
86250
|
if [[ ! -x "$REAL" ]]; then
|
|
86157
86251
|
FALLBACK="$(command -v codex 2>/dev/null || true)"
|
|
86158
86252
|
if [[ -n "$FALLBACK" && -x "$FALLBACK" ]]; then
|
|
86159
|
-
|
|
86253
|
+
if [[ "$FALLBACK" -ef "$0" ]] 2>/dev/null || { [[ -n "\${CODEX_CLI_PATH:-}" ]] && [[ "$FALLBACK" -ef "\${CODEX_CLI_PATH}" ]] 2>/dev/null; }; then
|
|
86254
|
+
echo "$(ts) refusing self fallback $FALLBACK" >>"$LOG" 2>/dev/null || true
|
|
86255
|
+
else
|
|
86256
|
+
REAL="$FALLBACK"
|
|
86257
|
+
fi
|
|
86160
86258
|
fi
|
|
86161
86259
|
fi
|
|
86162
86260
|
|
|
@@ -86177,7 +86275,7 @@ preflight() {
|
|
|
86177
86275
|
}
|
|
86178
86276
|
|
|
86179
86277
|
if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 ]]; then
|
|
86180
|
-
if [[ -
|
|
86278
|
+
if [[ -f "$BRIDGE" ]] && command -v python3 >/dev/null 2>&1; then
|
|
86181
86279
|
if preflight; then
|
|
86182
86280
|
echo "$(ts) rewrite -> stdio-ws bridge" >>"$LOG" 2>/dev/null || true
|
|
86183
86281
|
python3 "$BRIDGE"
|
|
@@ -86327,7 +86425,7 @@ function fileExists(path) {
|
|
|
86327
86425
|
recursive: true
|
|
86328
86426
|
});
|
|
86329
86427
|
writeExecutable(paths.wrapperPath, renderWrapperScript(paths));
|
|
86330
|
-
(0,node_fs__rspack_import_1.writeFileSync)(paths.bridgePath,
|
|
86428
|
+
(0,node_fs__rspack_import_1.writeFileSync)(paths.bridgePath, _desktop_shim_bridge_js__rspack_import_4/* .BRIDGE_SOURCE.endsWith */.w.endsWith("\n") ? _desktop_shim_bridge_js__rspack_import_4/* .BRIDGE_SOURCE */.w : `${_desktop_shim_bridge_js__rspack_import_4/* .BRIDGE_SOURCE */.w}\n`, {
|
|
86331
86429
|
mode: 420
|
|
86332
86430
|
});
|
|
86333
86431
|
writeExecutable(paths.envScriptPath, renderEnvScript(paths));
|
|
@@ -86407,11 +86505,12 @@ function fileExists(path) {
|
|
|
86407
86505
|
paths.bridgePath,
|
|
86408
86506
|
paths.envScriptPath
|
|
86409
86507
|
]){
|
|
86508
|
+
const existed = fileExists(path);
|
|
86410
86509
|
try {
|
|
86411
86510
|
if ((0,node_fs__rspack_import_1.rmSync)(path, {
|
|
86412
86511
|
force: true
|
|
86413
86512
|
}) === undefined && fileExists(path)) warnings.push(`could not remove ${path}`);
|
|
86414
|
-
else if (!fileExists(path)) removed.push(path);
|
|
86513
|
+
else if (existed && !fileExists(path)) removed.push(path);
|
|
86415
86514
|
} catch (error) {
|
|
86416
86515
|
warnings.push(`could not remove ${path} (${error instanceof Error ? error.message : String(error)})`);
|
|
86417
86516
|
}
|
|
@@ -86505,12 +86604,10 @@ function socketStateOf(path) {
|
|
|
86505
86604
|
// Desktop-facing value: the GUI domain on macOS, the caller env elsewhere.
|
|
86506
86605
|
const desktopCliPath = platform === "darwin" ? guiCliPath : cliPath;
|
|
86507
86606
|
const installed = wrapperPresent && isExecutable(paths.wrapperPath) && bridgePresent;
|
|
86508
|
-
// Effective socket, resolved exactly like gbot's own client
|
|
86509
|
-
// send/queue/status never
|
|
86510
|
-
|
|
86511
|
-
|
|
86512
|
-
CODEX_HOME: paths.codexHome
|
|
86513
|
-
});
|
|
86607
|
+
// Effective socket, resolved exactly like gbot's own client (CODEX_APP_SERVER_SOCK
|
|
86608
|
+
// wins, else the install-time CODEX_HOME) so status and send/queue/status never
|
|
86609
|
+
// disagree. Inlined here so codex-bridge can reuse desktopShimStatus without a cycle.
|
|
86610
|
+
const socketPath = env.CODEX_APP_SERVER_SOCK || (0,node_path__rspack_import_3.join)(paths.codexHome, "app-server-control", "app-server-control.sock");
|
|
86514
86611
|
return {
|
|
86515
86612
|
action: "status",
|
|
86516
86613
|
bridgePath: paths.bridgePath,
|
|
@@ -86618,6 +86715,7 @@ function formatCodexStatus(s) {
|
|
|
86618
86715
|
const lines = [
|
|
86619
86716
|
"socket: " + s.socketPath + " (" + s.socketState + ")"
|
|
86620
86717
|
];
|
|
86718
|
+
if (s.desktopShimConfigured) lines.push("desktop shim: configured (managed attachment unverified)");
|
|
86621
86719
|
if (!s.reachable) return lines.concat("reachable: no (" + s.mode + ")", s.message).join("\n");
|
|
86622
86720
|
if (s.mode !== "daemon") {
|
|
86623
86721
|
return lines.concat("reachable: yes, but unusable (" + s.mode + ")", s.message).join("\n");
|
|
@@ -86625,7 +86723,7 @@ function formatCodexStatus(s) {
|
|
|
86625
86723
|
lines.push("reachable: yes (daemon)");
|
|
86626
86724
|
const cli = s.cliVersion ?? (s.cliVersionProbe === "ok" ? "unknown" : "unknown, probe " + s.cliVersionProbe);
|
|
86627
86725
|
lines.push("daemon version: " + (s.daemonVersion ?? "unknown") + " cli version: " + cli + " pinned schema: " + s.pinnedVersion + " (" + s.schema.compatibility + ")");
|
|
86628
|
-
lines.push(s.desktopAttached === "private-stdio" ? "desktop attached: private-stdio (
|
|
86726
|
+
lines.push(s.desktopAttached === "private-stdio" ? "desktop attached: private-stdio (Desktop-bundled app-server process observed; managed attachment unverified)" : "desktop attached: unknown (not observable from the socket)");
|
|
86629
86727
|
if (s.versionMismatch) {
|
|
86630
86728
|
lines.push("warning: daemon and CLI versions differ; `codex app-server daemon restart` picks up the installed CLI");
|
|
86631
86729
|
}
|
|
@@ -86640,6 +86738,9 @@ function formatCodexThread(t) {
|
|
|
86640
86738
|
const preview = t.preview ? "\n " + stripTerminalControls(String(t.preview).replace(/\s+/g, " ")).slice(0, 200) : "";
|
|
86641
86739
|
return stripTerminalControls(t.id) + " " + stripTerminalControls(t.status) + title + "\n " + stripTerminalControls(t.cwd ?? "") + preview;
|
|
86642
86740
|
}
|
|
86741
|
+
/** Shell-safe single-quoting for copy-pasteable export lines (spaces, quotes, $). */ function shellQuote(value) {
|
|
86742
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
86743
|
+
}
|
|
86643
86744
|
function formatDesktopShimStatus(s) {
|
|
86644
86745
|
const mark = (ok)=>ok ? "yes" : "no";
|
|
86645
86746
|
const cliLine = s.platform === "darwin" ? "CODEX_CLI_PATH: GUI domain " + (s.guiCliPath ?? "(unset)") + " / this shell " + (s.cliPath ?? "(unset)") + (s.wrapperPointsAtShim ? " (Desktop-facing value points at shim)" : "") : "CODEX_CLI_PATH: " + (s.cliPath ?? "(unset)") + (s.wrapperPointsAtShim ? " (points at shim)" : "");
|
|
@@ -86654,7 +86755,7 @@ function formatDesktopShimStatus(s) {
|
|
|
86654
86755
|
if (!s.installed) {
|
|
86655
86756
|
lines.push("shim: not installed — run `gbot codex desktop-shim install` (Desktop keeps stock behavior until then)");
|
|
86656
86757
|
} else if (!s.wrapperPointsAtShim) {
|
|
86657
|
-
lines.push("shim: installed but CODEX_CLI_PATH does not point at it — reinstall or relaunch ChatGPT.app after login");
|
|
86758
|
+
lines.push(s.platform === "darwin" ? "shim: installed but CODEX_CLI_PATH does not point at it — reinstall or relaunch ChatGPT.app after login" : "shim: installed but CODEX_CLI_PATH does not point at it — export CODEX_CLI_PATH=" + shellQuote(s.wrapperPath));
|
|
86658
86759
|
} else {
|
|
86659
86760
|
lines.push("shim: active — Desktop app-server spawns bridge onto the managed daemon");
|
|
86660
86761
|
}
|
|
@@ -87751,6 +87852,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
87751
87852
|
* never *.cursorvm.com, so a CURSOR_ACCESS_TOKEN cannot be pointed at a box host.
|
|
87752
87853
|
* Local/dev gateways: http(s)://127.0.0.1|localhost|::1 when GROK_BOT_ALLOW_LOCAL_GATEWAY=1.
|
|
87753
87854
|
* Escape hatch: GROK_BOT_ALLOW_ANY_GATEWAY=1 (unsafe; disables host checks; warns once).
|
|
87855
|
+
* Test mode (GROK_BOT_TEST=1 or NODE_ENV=test): loopback only, for gateway and backend
|
|
87856
|
+
* alike, and the escape hatches are ignored — a test can never reach a live thread.
|
|
87754
87857
|
*/ function truthyEnv(name) {
|
|
87755
87858
|
const v = (process.env[name] || "").trim().toLowerCase();
|
|
87756
87859
|
return v === "1" || v === "true" || v === "yes";
|
|
@@ -87761,6 +87864,9 @@ function allowAnyGateway() {
|
|
|
87761
87864
|
function allowLocalGateway() {
|
|
87762
87865
|
return truthyEnv("GROK_BOT_ALLOW_LOCAL_GATEWAY");
|
|
87763
87866
|
}
|
|
87867
|
+
function testMode() {
|
|
87868
|
+
return truthyEnv("GROK_BOT_TEST") || process.env.NODE_ENV === "test";
|
|
87869
|
+
}
|
|
87764
87870
|
const warned = new Set();
|
|
87765
87871
|
function warnOnce(key, message) {
|
|
87766
87872
|
if (warned.has(key)) return;
|
|
@@ -87800,6 +87906,12 @@ function isCursorGatewayHostname(hostname) {
|
|
|
87800
87906
|
if (parsed.username || parsed.password) {
|
|
87801
87907
|
throw new Error("Rejected " + label + ": userinfo is not allowed.");
|
|
87802
87908
|
}
|
|
87909
|
+
if (testMode()) {
|
|
87910
|
+
if (!isLocalHostname(parsed.hostname) || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
87911
|
+
throw new Error("Rejected " + label + " host \"" + parsed.hostname + "\": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.");
|
|
87912
|
+
}
|
|
87913
|
+
return String(rawUrl).replace(/\/$/, "");
|
|
87914
|
+
}
|
|
87803
87915
|
if (allowAnyGateway()) {
|
|
87804
87916
|
warnOnce("ALLOW_ANY", "warning: GROK_BOT_ALLOW_ANY_GATEWAY is set; credential host checks are disabled.");
|
|
87805
87917
|
return String(rawUrl).replace(/\/$/, "");
|
package/dist/install.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'nod
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
9
|
const pluginName = "gbot";
|
|
10
|
-
const pluginVersion = "0.4.
|
|
10
|
+
const pluginVersion = "0.4.5";
|
|
11
11
|
const receiptFile = ".agent-bundle-install.json";
|
|
12
12
|
const receiptFormat = "agent-bundle-install-receipt/2";
|
|
13
13
|
const legacyReceiptFormat = "agent-bundle-install-receipt/1";
|
|
@@ -25987,6 +25987,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
25987
25987
|
* never *.cursorvm.com, so a CURSOR_ACCESS_TOKEN cannot be pointed at a box host.
|
|
25988
25988
|
* Local/dev gateways: http(s)://127.0.0.1|localhost|::1 when GROK_BOT_ALLOW_LOCAL_GATEWAY=1.
|
|
25989
25989
|
* Escape hatch: GROK_BOT_ALLOW_ANY_GATEWAY=1 (unsafe; disables host checks; warns once).
|
|
25990
|
+
* Test mode (GROK_BOT_TEST=1 or NODE_ENV=test): loopback only, for gateway and backend
|
|
25991
|
+
* alike, and the escape hatches are ignored — a test can never reach a live thread.
|
|
25990
25992
|
*/ function truthyEnv(name) {
|
|
25991
25993
|
const v = (process.env[name] || "").trim().toLowerCase();
|
|
25992
25994
|
return v === "1" || v === "true" || v === "yes";
|
|
@@ -25997,6 +25999,9 @@ function allowAnyGateway() {
|
|
|
25997
25999
|
function allowLocalGateway() {
|
|
25998
26000
|
return truthyEnv("GROK_BOT_ALLOW_LOCAL_GATEWAY");
|
|
25999
26001
|
}
|
|
26002
|
+
function testMode() {
|
|
26003
|
+
return truthyEnv("GROK_BOT_TEST") || process.env.NODE_ENV === "test";
|
|
26004
|
+
}
|
|
26000
26005
|
const warned = new Set();
|
|
26001
26006
|
function warnOnce(key, message) {
|
|
26002
26007
|
if (warned.has(key)) return;
|
|
@@ -26036,6 +26041,12 @@ function isCursorGatewayHostname(hostname) {
|
|
|
26036
26041
|
if (parsed.username || parsed.password) {
|
|
26037
26042
|
throw new Error("Rejected " + label + ": userinfo is not allowed.");
|
|
26038
26043
|
}
|
|
26044
|
+
if (testMode()) {
|
|
26045
|
+
if (!isLocalHostname(parsed.hostname) || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
26046
|
+
throw new Error("Rejected " + label + " host \"" + parsed.hostname + "\": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.");
|
|
26047
|
+
}
|
|
26048
|
+
return String(rawUrl).replace(/\/$/, "");
|
|
26049
|
+
}
|
|
26039
26050
|
if (allowAnyGateway()) {
|
|
26040
26051
|
warnOnce("ALLOW_ANY", "warning: GROK_BOT_ALLOW_ANY_GATEWAY is set; credential host checks are disabled.");
|
|
26041
26052
|
return String(rawUrl).replace(/\/$/, "");
|
|
@@ -26173,7 +26184,7 @@ globalThis.__rspack_rsc_manifest__ ??= Object.freeze({
|
|
|
26173
26184
|
});
|
|
26174
26185
|
if (node_worker_threads__rspack_import_0.parentPort === null) throw new Error('Generated Flight worker requires a parent port.');
|
|
26175
26186
|
process.stdout.write = process.stderr.write.bind(process.stderr);
|
|
26176
|
-
const ARTIFACT_EPOCH = "gbot@0.4.
|
|
26187
|
+
const ARTIFACT_EPOCH = "gbot@0.4.5";
|
|
26177
26188
|
const processLifetime = {
|
|
26178
26189
|
hits: 0,
|
|
26179
26190
|
instanceId: crypto.randomUUID(),
|
|
@@ -106202,7 +106202,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
106202
106202
|
const route0 = Object.assign({}, Reflect.get(_home_runner_work_grok_bot_cli_grok_bot_cli_src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_3, 'default'), _home_runner_work_grok_bot_cli_grok_bot_cli_src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_3);
|
|
106203
106203
|
|
|
106204
106204
|
const route1 = Object.assign({}, Reflect.get(_home_runner_work_grok_bot_cli_grok_bot_cli_src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_4, 'default'), _home_runner_work_grok_bot_cli_grok_bot_cli_src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_4);
|
|
106205
|
-
const ARTIFACT_EPOCH = "gbot@0.4.
|
|
106205
|
+
const ARTIFACT_EPOCH = "gbot@0.4.5";
|
|
106206
106206
|
const pluginRoot = (0,_agent_bundle_runtime__rspack_import_5/* .resolvePluginRoot */.E7)({
|
|
106207
106207
|
fallback: (0,node_url__rspack_import_0.fileURLToPath)(new URL('..', import.meta.url)),
|
|
106208
106208
|
stateAnchor: 'user-data'
|
|
@@ -106291,7 +106291,7 @@ const routes = Object.freeze({
|
|
|
106291
106291
|
lineage: lineage.registry,
|
|
106292
106292
|
plugin: {
|
|
106293
106293
|
"name": "gbot",
|
|
106294
|
-
"version": "0.4.
|
|
106294
|
+
"version": "0.4.5"
|
|
106295
106295
|
},
|
|
106296
106296
|
pluginRoot: pluginRoot.identity,
|
|
106297
106297
|
routes
|
|
@@ -107476,6 +107476,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
107476
107476
|
* never *.cursorvm.com, so a CURSOR_ACCESS_TOKEN cannot be pointed at a box host.
|
|
107477
107477
|
* Local/dev gateways: http(s)://127.0.0.1|localhost|::1 when GROK_BOT_ALLOW_LOCAL_GATEWAY=1.
|
|
107478
107478
|
* Escape hatch: GROK_BOT_ALLOW_ANY_GATEWAY=1 (unsafe; disables host checks; warns once).
|
|
107479
|
+
* Test mode (GROK_BOT_TEST=1 or NODE_ENV=test): loopback only, for gateway and backend
|
|
107480
|
+
* alike, and the escape hatches are ignored — a test can never reach a live thread.
|
|
107479
107481
|
*/ function truthyEnv(name) {
|
|
107480
107482
|
const v = (process.env[name] || "").trim().toLowerCase();
|
|
107481
107483
|
return v === "1" || v === "true" || v === "yes";
|
|
@@ -107486,6 +107488,9 @@ function allowAnyGateway() {
|
|
|
107486
107488
|
function allowLocalGateway() {
|
|
107487
107489
|
return truthyEnv("GROK_BOT_ALLOW_LOCAL_GATEWAY");
|
|
107488
107490
|
}
|
|
107491
|
+
function testMode() {
|
|
107492
|
+
return truthyEnv("GROK_BOT_TEST") || process.env.NODE_ENV === "test";
|
|
107493
|
+
}
|
|
107489
107494
|
const warned = new Set();
|
|
107490
107495
|
function warnOnce(key, message) {
|
|
107491
107496
|
if (warned.has(key)) return;
|
|
@@ -107525,6 +107530,12 @@ function isCursorGatewayHostname(hostname) {
|
|
|
107525
107530
|
if (parsed.username || parsed.password) {
|
|
107526
107531
|
throw new Error("Rejected " + label + ": userinfo is not allowed.");
|
|
107527
107532
|
}
|
|
107533
|
+
if (testMode()) {
|
|
107534
|
+
if (!isLocalHostname(parsed.hostname) || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
107535
|
+
throw new Error("Rejected " + label + " host \"" + parsed.hostname + "\": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.");
|
|
107536
|
+
}
|
|
107537
|
+
return String(rawUrl).replace(/\/$/, "");
|
|
107538
|
+
}
|
|
107528
107539
|
if (allowAnyGateway()) {
|
|
107529
107540
|
warnOnce("ALLOW_ANY", "warning: GROK_BOT_ALLOW_ANY_GATEWAY is set; credential host checks are disabled.");
|
|
107530
107541
|
return String(rawUrl).replace(/\/$/, "");
|
package/dist/package.json
CHANGED
package/dist/plugin.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","author":{"name":"Zack Jackson","url":"https://github.com/ScriptedAlchemy"},"description":"Message Grok Bot bots and groups and read their threads from Codex, Claude Code, and Cursor.","homepage":"https://github.com/ScriptedAlchemy/grok-bot-cli#readme","keywords":["grok","grok-bot","grokbot","gbot","cursor","ai-agents","automation","terminal","cli","agent-bundle"],"license":"MIT","name":"gbot","repository":"https://github.com/ScriptedAlchemy/grok-bot-cli","version":"0.4.
|
|
1
|
+
{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","author":{"name":"Zack Jackson","url":"https://github.com/ScriptedAlchemy"},"description":"Message Grok Bot bots and groups and read their threads from Codex, Claude Code, and Cursor.","homepage":"https://github.com/ScriptedAlchemy/grok-bot-cli#readme","keywords":["grok","grok-bot","grokbot","gbot","cursor","ai-agents","automation","terminal","cli","agent-bundle"],"license":"MIT","name":"gbot","repository":"https://github.com/ScriptedAlchemy/grok-bot-cli","version":"0.4.5"}
|
package/package.json
CHANGED