grok-bot-cli 0.4.2 → 0.5.0
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 +30 -0
- package/README.md +13 -9
- 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 +13 -9
- package/dist/agent-bundle.compile-evidence.json +1 -1
- package/dist/agent-bundle.manifest.json +1 -1
- package/dist/bin/gbot-flight.mjs +207 -112
- package/dist/bin/gbot.mjs +209 -116
- package/dist/install.mjs +1 -1
- package/dist/mcp/mcp-grok-bot-b8c2461e-flight.mjs +25 -22
- package/dist/mcp/mcp-grok-bot-b8c2461e.mjs +26 -23
- package/dist/package.json +2 -2
- package/dist/plugin.json +1 -1
- package/dist/skills/talk-to-grok-bot/SKILL.md +3 -1
- package/package.json +2 -2
package/dist/bin/gbot.mjs
CHANGED
|
@@ -12994,9 +12994,6 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
12994
12994
|
|
|
12995
12995
|
|
|
12996
12996
|
const config = {
|
|
12997
|
-
aliases: [
|
|
12998
|
-
'chat'
|
|
12999
|
-
],
|
|
13000
12997
|
description: 'Read the most recent messages in a Grok Bot bot or group thread.',
|
|
13001
12998
|
inputJsonSchema: {
|
|
13002
12999
|
additionalProperties: false,
|
|
@@ -13146,7 +13143,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
13146
13143
|
|
|
13147
13144
|
// Hosts and the CLI print thrown error text (and stack), and a fetch or proxy failure can echo
|
|
13148
13145
|
// a credential. Rewrap with a redacted message; keep `name` and own fields such as `reason`,
|
|
13149
|
-
// `delivery`, `mode`, and
|
|
13146
|
+
// `delivery`, `mode`, and correlation ids so outcome documents still classify the failure.
|
|
13150
13147
|
const withRedactedErrors = async (run)=>{
|
|
13151
13148
|
try {
|
|
13152
13149
|
return await run();
|
|
@@ -84199,9 +84196,7 @@ const commands = Object.freeze([
|
|
|
84199
84196
|
"routeId": "cli:send"
|
|
84200
84197
|
},
|
|
84201
84198
|
{
|
|
84202
|
-
"aliases": [
|
|
84203
|
-
"chat"
|
|
84204
|
-
],
|
|
84199
|
+
"aliases": [],
|
|
84205
84200
|
"description": "Read the most recent messages in a Grok Bot bot or group thread.",
|
|
84206
84201
|
"exitCode": "zero",
|
|
84207
84202
|
"options": [
|
|
@@ -84503,7 +84498,7 @@ if (import.meta.main) {
|
|
|
84503
84498
|
execute,
|
|
84504
84499
|
name: "gbot",
|
|
84505
84500
|
render,
|
|
84506
|
-
version: "0.
|
|
84501
|
+
version: "0.5.0"
|
|
84507
84502
|
});
|
|
84508
84503
|
}
|
|
84509
84504
|
|
|
@@ -84741,6 +84736,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
84741
84736
|
/* import */ var node_child_process__rspack_import_5 = __webpack_require__("node:child_process");
|
|
84742
84737
|
/* import */ var node_module__rspack_import_6 = __webpack_require__("node:module");
|
|
84743
84738
|
/* import */ var _codex_contract_js__rspack_import_7 = __webpack_require__("./src/core/codex/contract.js");
|
|
84739
|
+
/* import */ var _desktop_shim_js__rspack_import_8 = __webpack_require__("./src/core/desktop-shim.js");
|
|
84740
|
+
|
|
84744
84741
|
|
|
84745
84742
|
|
|
84746
84743
|
|
|
@@ -84903,15 +84900,27 @@ class CodexRpcError extends Error {
|
|
|
84903
84900
|
}
|
|
84904
84901
|
}
|
|
84905
84902
|
class CodexSendError extends Error {
|
|
84906
|
-
constructor(message, { delivery,
|
|
84903
|
+
constructor(message, { correlationId, delivery, hop, messageId, reason, refused, threadId, turnId } = {}){
|
|
84907
84904
|
super(message);
|
|
84908
84905
|
this.name = "CodexSendError";
|
|
84909
84906
|
this.delivery = delivery;
|
|
84907
|
+
if (correlationId !== undefined) this.correlationId = correlationId;
|
|
84908
|
+
if (hop !== undefined) this.hop = hop;
|
|
84909
|
+
if (messageId !== undefined) this.messageId = messageId;
|
|
84910
84910
|
if (reason !== undefined) this.reason = reason;
|
|
84911
|
+
if (refused !== undefined) this.refused = refused;
|
|
84911
84912
|
if (threadId !== undefined) this.threadId = threadId;
|
|
84912
84913
|
if (turnId !== undefined) this.turnId = turnId;
|
|
84913
|
-
|
|
84914
|
-
|
|
84914
|
+
}
|
|
84915
|
+
}
|
|
84916
|
+
function attachEnvelope(error, envelope) {
|
|
84917
|
+
if (!error || typeof error !== "object") return;
|
|
84918
|
+
for (const key of [
|
|
84919
|
+
"messageId",
|
|
84920
|
+
"correlationId",
|
|
84921
|
+
"hop"
|
|
84922
|
+
]){
|
|
84923
|
+
if (error[key] === undefined && envelope[key] !== undefined) error[key] = envelope[key];
|
|
84915
84924
|
}
|
|
84916
84925
|
}
|
|
84917
84926
|
/** The route to the app-server is unavailable; `mode` is a stable machine-readable state. */ class CodexRouteError extends Error {
|
|
@@ -84999,11 +85008,42 @@ function isObject(value) {
|
|
|
84999
85008
|
const client = {
|
|
85000
85009
|
refused,
|
|
85001
85010
|
// Server-initiated requests are only *answered* once our own turn/start
|
|
85002
|
-
// is in flight
|
|
85003
|
-
//
|
|
85004
|
-
//
|
|
85005
|
-
//
|
|
85011
|
+
// is in flight, and then only when they name our own thread/turn:
|
|
85012
|
+
// anything earlier — or naming another thread or Desktop turn — belongs
|
|
85013
|
+
// to another client and must never be rejected on their behalf. Early
|
|
85014
|
+
// and foreign requests are still recorded; the connection closes right
|
|
85015
|
+
// after, so a pending request simply dies with it instead of denying
|
|
85016
|
+
// someone's approval.
|
|
85006
85017
|
answerServerRequests: false,
|
|
85018
|
+
expectedThreadId: null,
|
|
85019
|
+
expectedTurnId: null,
|
|
85020
|
+
// Approvals that name a turn while our own turn id is still unknown
|
|
85021
|
+
// (arriving before — or in the same socket batch as — the turn/start
|
|
85022
|
+
// acknowledgment) wait here instead of being classified foreign: once
|
|
85023
|
+
// the acknowledgment supplies the turn id, matching ones are answered
|
|
85024
|
+
// and the rest stay silent.
|
|
85025
|
+
deferred: [],
|
|
85026
|
+
// Adopt the acknowledged turn id, answering deferred requests that
|
|
85027
|
+
// prove to be ours. Runs inside the connection closure so it can send.
|
|
85028
|
+
_adoptTurn (turnId) {
|
|
85029
|
+
client.expectedTurnId = turnId;
|
|
85030
|
+
for (const entry of client.deferred.splice(0)){
|
|
85031
|
+
const params = entry.params && typeof entry.params === "object" && !Array.isArray(entry.params) ? entry.params : null;
|
|
85032
|
+
const threadId = params ? params.threadId ?? params.thread_id : null;
|
|
85033
|
+
const namedTurnId = params ? params.turnId ?? params.turn_id ?? (params.turn && params.turn.id) : null;
|
|
85034
|
+
if (threadId === client.expectedThreadId && namedTurnId === turnId) {
|
|
85035
|
+
entry.answered = true;
|
|
85036
|
+
sendJson({
|
|
85037
|
+
jsonrpc: "2.0",
|
|
85038
|
+
id: entry.id,
|
|
85039
|
+
error: {
|
|
85040
|
+
code: -32601,
|
|
85041
|
+
message: "gbot codex does not answer " + entry.method + "; configure approval_policy on the daemon"
|
|
85042
|
+
}
|
|
85043
|
+
});
|
|
85044
|
+
}
|
|
85045
|
+
}
|
|
85046
|
+
},
|
|
85007
85047
|
request (method, params) {
|
|
85008
85048
|
const id = nextId++;
|
|
85009
85049
|
return new Promise((res, rej)=>{
|
|
@@ -85067,13 +85107,36 @@ function isObject(value) {
|
|
|
85067
85107
|
};
|
|
85068
85108
|
const onMessage = (msg)=>{
|
|
85069
85109
|
if (msg.id != null && msg.method) {
|
|
85070
|
-
|
|
85071
|
-
|
|
85110
|
+
let answered = client.answerServerRequests;
|
|
85111
|
+
let defer = false;
|
|
85112
|
+
// Approval ownership: only answer requests for the turn gbot itself
|
|
85113
|
+
// started. A request naming another thread — or naming a turn that is
|
|
85114
|
+
// not ours — is recorded foreign-silent, never rejected. A request
|
|
85115
|
+
// naming a turn while our turn id is still unknown is deferred, not
|
|
85116
|
+
// classified: the turn/start acknowledgment decides it.
|
|
85117
|
+
if (answered) {
|
|
85118
|
+
const params = msg.params && typeof msg.params === "object" && !Array.isArray(msg.params) ? msg.params : null;
|
|
85119
|
+
if (params) {
|
|
85120
|
+
const threadId = params.threadId ?? params.thread_id;
|
|
85121
|
+
const turnId = params.turnId ?? params.turn_id ?? (params.turn && params.turn.id);
|
|
85122
|
+
if (threadId !== client.expectedThreadId || turnId == null) answered = false;
|
|
85123
|
+
else if (client.expectedTurnId == null) {
|
|
85124
|
+
answered = false;
|
|
85125
|
+
defer = true;
|
|
85126
|
+
} else if (turnId !== client.expectedTurnId) answered = false;
|
|
85127
|
+
} else answered = false;
|
|
85128
|
+
}
|
|
85129
|
+
const entry = {
|
|
85072
85130
|
id: msg.id,
|
|
85073
85131
|
method: msg.method,
|
|
85074
85132
|
params: msg.params,
|
|
85075
85133
|
answered
|
|
85076
|
-
}
|
|
85134
|
+
};
|
|
85135
|
+
refused.push(entry);
|
|
85136
|
+
if (defer) {
|
|
85137
|
+
client.deferred.push(entry);
|
|
85138
|
+
return;
|
|
85139
|
+
}
|
|
85077
85140
|
if (!answered) return;
|
|
85078
85141
|
sendJson({
|
|
85079
85142
|
jsonrpc: "2.0",
|
|
@@ -85318,17 +85381,30 @@ function listDesktopProcesses(timeoutMs = DESKTOP_PROCESS_LIST_TIMEOUT_MS) {
|
|
|
85318
85381
|
/**
|
|
85319
85382
|
* Status contract: `reachable` is endpoint reachability only. `schema.compatibility` is
|
|
85320
85383
|
* `exact` when the daemon reports the pinned version, otherwise `unverified` (methods
|
|
85321
|
-
* usually survive upgrades) or `unknown`. `
|
|
85322
|
-
* Desktop-
|
|
85323
|
-
*
|
|
85324
|
-
*
|
|
85384
|
+
* usually survive upgrades) or `unknown`. `desktopShimConfigured` records an installed
|
|
85385
|
+
* wrapper selected by Desktop-facing CODEX_CLI_PATH (the GUI domain on Darwin).
|
|
85386
|
+
* Configuration does not prove a running Desktop inherited it or connected.
|
|
85387
|
+
* `desktopAttached` is `"private-stdio"` when a Desktop-bundled app-server process
|
|
85388
|
+
* is visible, otherwise `"unknown"`. Managed attachment is not observable here.
|
|
85325
85389
|
*
|
|
85326
85390
|
* @param {NodeJS.ProcessEnv} [env]
|
|
85327
|
-
* @param {{ listProcesses?: string | string[] }} [opts]
|
|
85328
|
-
* defaults to a live `ps` snapshot
|
|
85329
|
-
|
|
85391
|
+
* @param {{ listProcesses?: string | string[], shim?: { installed?: boolean, wrapperPointsAtShim?: boolean } | null }} [opts]
|
|
85392
|
+
* injected process list and shim state for tests; defaults to a live `ps` snapshot and
|
|
85393
|
+
* `desktopShimStatus`. Never reads pipes or connects to Desktop.
|
|
85394
|
+
*/ async function codexStatus(env = process.env, { listProcesses, shim } = {}) {
|
|
85330
85395
|
const path = codexSocketPath(env);
|
|
85331
85396
|
const cli = probeLocalCodexVersion();
|
|
85397
|
+
let desktopShimConfigured = Boolean(shim && shim.installed && shim.wrapperPointsAtShim);
|
|
85398
|
+
if (shim === undefined) {
|
|
85399
|
+
try {
|
|
85400
|
+
const live = (0,_desktop_shim_js__rspack_import_8/* .desktopShimStatus */.em)({
|
|
85401
|
+
env
|
|
85402
|
+
});
|
|
85403
|
+
desktopShimConfigured = Boolean(live.installed && live.wrapperPointsAtShim);
|
|
85404
|
+
} catch {
|
|
85405
|
+
desktopShimConfigured = false;
|
|
85406
|
+
}
|
|
85407
|
+
}
|
|
85332
85408
|
const desktopAttached = detectDesktopPrivateAppServer({
|
|
85333
85409
|
platform: process.platform,
|
|
85334
85410
|
listProcesses: listProcesses ?? listDesktopProcesses()
|
|
@@ -85339,7 +85415,8 @@ function listDesktopProcesses(timeoutMs = DESKTOP_PROCESS_LIST_TIMEOUT_MS) {
|
|
|
85339
85415
|
pinnedVersion: PINNED_CODEX_VERSION,
|
|
85340
85416
|
cliVersion: cli.version,
|
|
85341
85417
|
cliVersionProbe: cli.probe,
|
|
85342
|
-
desktopAttached
|
|
85418
|
+
desktopAttached,
|
|
85419
|
+
desktopShimConfigured
|
|
85343
85420
|
};
|
|
85344
85421
|
let session;
|
|
85345
85422
|
try {
|
|
@@ -85498,7 +85575,7 @@ const ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
|
85498
85575
|
throw new CodexSendError("Refusing to send: hop " + hopCount + " reaches the relay bound " + bound + " (GROK_BOT_MAX_HOPS). " + "This message is an agent-to-agent relay that has already been forwarded too many times.", {
|
|
85499
85576
|
delivery: "rejected",
|
|
85500
85577
|
reason: "hop-limit",
|
|
85501
|
-
|
|
85578
|
+
...out
|
|
85502
85579
|
});
|
|
85503
85580
|
}
|
|
85504
85581
|
return out;
|
|
@@ -85586,20 +85663,20 @@ function unsupportedOrRpc(err, method, threadId, envelope) {
|
|
|
85586
85663
|
delivery: "rejected",
|
|
85587
85664
|
reason: "unsupported",
|
|
85588
85665
|
threadId,
|
|
85589
|
-
envelope
|
|
85666
|
+
...envelope
|
|
85590
85667
|
});
|
|
85591
85668
|
}
|
|
85592
85669
|
if (err instanceof CodexRpcError) return new CodexSendError(err.message, {
|
|
85593
85670
|
delivery: "rejected",
|
|
85594
85671
|
reason: "rejected",
|
|
85595
85672
|
threadId,
|
|
85596
|
-
envelope
|
|
85673
|
+
...envelope
|
|
85597
85674
|
});
|
|
85598
85675
|
return new CodexSendError("Lost the Codex " + method + " response for thread " + threadId + ": " + (err && err.message || err) + ". Delivery is unknown; list the queue before resending.", {
|
|
85599
85676
|
delivery: err && err.delivery || "unknown",
|
|
85600
85677
|
reason: "transport",
|
|
85601
85678
|
threadId,
|
|
85602
|
-
envelope
|
|
85679
|
+
...envelope
|
|
85603
85680
|
});
|
|
85604
85681
|
}
|
|
85605
85682
|
/** Read the daemon's queue for one thread (experimental `thread/queue/list`). */ async function listCodexQueue(threadId, { env = process.env, limit = 50, cursor } = {}) {
|
|
@@ -85650,7 +85727,7 @@ function unsupportedOrRpc(err, method, threadId, envelope) {
|
|
|
85650
85727
|
return (0,_codex_contract_js__rspack_import_7/* .outcomeFromReceipt */.Dz)(receipt);
|
|
85651
85728
|
} catch (err) {
|
|
85652
85729
|
// Every receipt names the message, including refusals that never reached the daemon.
|
|
85653
|
-
if (
|
|
85730
|
+
if (err instanceof CodexSendError || err instanceof CodexRouteError || err instanceof CodexProtocolError) attachEnvelope(err, envelope);
|
|
85654
85731
|
return (0,_codex_contract_js__rspack_import_7/* .outcomeFromError */.DG)(err);
|
|
85655
85732
|
}
|
|
85656
85733
|
}
|
|
@@ -85675,7 +85752,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85675
85752
|
delivery: err instanceof CodexRpcError ? "rejected" : err && err.delivery || "unknown",
|
|
85676
85753
|
reason: err instanceof CodexRpcError ? /no rollout found|thread not found/i.test(String(err.rpc && err.rpc.message)) ? "unknown-thread" : /active writer/i.test(String(err.rpc && err.rpc.message)) ? "external-owner" : "rejected" : "transport",
|
|
85677
85754
|
threadId,
|
|
85678
|
-
envelope
|
|
85755
|
+
...envelope
|
|
85679
85756
|
});
|
|
85680
85757
|
}
|
|
85681
85758
|
if (!isObject(resumed) || !isObject(resumed.thread) || typeof resumed.thread.id !== "string") {
|
|
@@ -85702,7 +85779,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85702
85779
|
delivery: "rejected",
|
|
85703
85780
|
reason: "busy",
|
|
85704
85781
|
threadId,
|
|
85705
|
-
envelope
|
|
85782
|
+
...envelope
|
|
85706
85783
|
});
|
|
85707
85784
|
}
|
|
85708
85785
|
if (state.busy) {
|
|
@@ -85727,7 +85804,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85727
85804
|
delivery: "unknown",
|
|
85728
85805
|
reason: "bad-response",
|
|
85729
85806
|
threadId,
|
|
85730
|
-
envelope
|
|
85807
|
+
...envelope
|
|
85731
85808
|
});
|
|
85732
85809
|
}
|
|
85733
85810
|
return {
|
|
@@ -85741,11 +85818,15 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85741
85818
|
// the daemon exposes no compare-and-start request. Upgrade path: thread/queue/add + thread/queue/start once stable.
|
|
85742
85819
|
// Scope refusals to this turn: server requests from earlier calls belong to another context.
|
|
85743
85820
|
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
|
-
//
|
|
85821
|
+
// Arm refusal replies only now that our own turn/start is in flight, scoped
|
|
85822
|
+
// to our own thread/turn: anything recorded earlier (resume, busy reject,
|
|
85823
|
+
// queue) — or naming another thread or Desktop turn — stays unanswered so
|
|
85824
|
+
// gbot never rejects another client's approval. Requests naming a turn
|
|
85825
|
+
// that arrives before the acknowledgment supplies our turn id wait in
|
|
85826
|
+
// client.deferred and are adopted only on a match. Requests missing
|
|
85827
|
+
// thread or turn IDs remain unanswered because ownership is unknown.
|
|
85828
|
+
client.expectedThreadId = threadId;
|
|
85829
|
+
client.expectedTurnId = null;
|
|
85749
85830
|
client.answerServerRequests = true;
|
|
85750
85831
|
let turn;
|
|
85751
85832
|
try {
|
|
@@ -85769,7 +85850,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85769
85850
|
delivery,
|
|
85770
85851
|
reason: delivery === "rejected" ? "rejected" : "transport",
|
|
85771
85852
|
threadId,
|
|
85772
|
-
envelope
|
|
85853
|
+
...envelope
|
|
85773
85854
|
});
|
|
85774
85855
|
}
|
|
85775
85856
|
const turnId = turn && turn.turn && typeof turn.turn.id === "string" && turn.turn.id ? turn.turn.id : null;
|
|
@@ -85778,10 +85859,20 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85778
85859
|
delivery: "unknown",
|
|
85779
85860
|
reason: "bad-response",
|
|
85780
85861
|
threadId,
|
|
85781
|
-
envelope
|
|
85862
|
+
...envelope
|
|
85782
85863
|
});
|
|
85783
85864
|
}
|
|
85784
|
-
|
|
85865
|
+
client._adoptTurn(turnId);
|
|
85866
|
+
const freshRefused = client.refused.slice(seenRefused).filter((r)=>{
|
|
85867
|
+
if (!r.answered) return false;
|
|
85868
|
+
const params = r.params && typeof r.params === "object" && !Array.isArray(r.params) ? r.params : null;
|
|
85869
|
+
if (!params) return true;
|
|
85870
|
+
const refusedThreadId = params.threadId ?? params.thread_id;
|
|
85871
|
+
const refusedTurnId = params.turnId ?? params.turn_id ?? (params.turn && params.turn.id);
|
|
85872
|
+
if (refusedThreadId != null && refusedThreadId !== threadId) return false;
|
|
85873
|
+
if (refusedTurnId != null && refusedTurnId !== turnId) return false;
|
|
85874
|
+
return true;
|
|
85875
|
+
});
|
|
85785
85876
|
if (freshRefused.length) {
|
|
85786
85877
|
const methods = freshRefused.map((r)=>r.method).join(", ");
|
|
85787
85878
|
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.", {
|
|
@@ -85790,7 +85881,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
85790
85881
|
threadId,
|
|
85791
85882
|
turnId,
|
|
85792
85883
|
refused: freshRefused.map((r)=>r.method),
|
|
85793
|
-
envelope
|
|
85884
|
+
...envelope
|
|
85794
85885
|
});
|
|
85795
85886
|
}
|
|
85796
85887
|
return {
|
|
@@ -85811,8 +85902,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
85811
85902
|
X$: () => (listCodexThreads),
|
|
85812
85903
|
ZS: () => (sendToCodexThread),
|
|
85813
85904
|
nq: () => (buildEnvelope),
|
|
85814
|
-
pF: () => (singleLine)
|
|
85815
|
-
ql: () => (codexSocketPath)
|
|
85905
|
+
pF: () => (singleLine)
|
|
85816
85906
|
});
|
|
85817
85907
|
|
|
85818
85908
|
|
|
@@ -85877,8 +85967,6 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
85877
85967
|
}
|
|
85878
85968
|
/**
|
|
85879
85969
|
* Flatten a thrown error into the #46 send/failure document (plus exitCode).
|
|
85880
|
-
* Preserves the legacy failure-document fields, then adds exitCode for the
|
|
85881
|
-
* generated CLI's `exitCode: 'result'` policy.
|
|
85882
85970
|
*
|
|
85883
85971
|
* @param {unknown} error
|
|
85884
85972
|
* @param {{ isUsage?: (err: unknown) => boolean }} [opts]
|
|
@@ -85905,12 +85993,6 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
85905
85993
|
if (error[key] !== undefined) out[key] = error[key];
|
|
85906
85994
|
}
|
|
85907
85995
|
if (out.reason === undefined && isUsage(error)) out.reason = "usage";
|
|
85908
|
-
const envelope = /** @type {{ messageId?: string, correlationId?: string, hop?: number }} */ error.envelope;
|
|
85909
|
-
if (envelope && typeof envelope === "object") {
|
|
85910
|
-
if (envelope.messageId !== undefined) out.messageId = envelope.messageId;
|
|
85911
|
-
if (envelope.correlationId !== undefined) out.correlationId = envelope.correlationId;
|
|
85912
|
-
if (envelope.hop !== undefined) out.hop = envelope.hop;
|
|
85913
|
-
}
|
|
85914
85996
|
}
|
|
85915
85997
|
if (out.delivery === undefined) out.delivery = "rejected";
|
|
85916
85998
|
return /** @type {CodexSendOutcome & { error: string, exitCode: 1 }} */ {
|
|
@@ -85927,14 +86009,6 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
85927
86009
|
...receipt
|
|
85928
86010
|
};
|
|
85929
86011
|
if (base.delivery === undefined) base.delivery = "accepted";
|
|
85930
|
-
// Flatten envelope onto the document when present (matches fail() + json print).
|
|
85931
|
-
const envelope = base.envelope;
|
|
85932
|
-
if (envelope && typeof envelope === "object") {
|
|
85933
|
-
const e = /** @type {Record<string, unknown>} */ envelope;
|
|
85934
|
-
if (base.messageId === undefined && e.messageId !== undefined) base.messageId = e.messageId;
|
|
85935
|
-
if (base.correlationId === undefined && e.correlationId !== undefined) base.correlationId = e.correlationId;
|
|
85936
|
-
if (base.hop === undefined && e.hop !== undefined) base.hop = e.hop;
|
|
85937
|
-
}
|
|
85938
86012
|
const exitCode = sendExitCode(/** @type {{ delivery: CodexDelivery, reason?: string, error?: string }} */ base);
|
|
85939
86013
|
if (exitCode === 1 && base.error === undefined && base.reason === "approval-refused") {
|
|
85940
86014
|
base.error = typeof base.message === "string" ? base.message : "Codex refused one or more approvals.";
|
|
@@ -85983,7 +86057,7 @@ async function openBackend(opts) {
|
|
|
85983
86057
|
setGroupMembers: (group, members)=>_gateway_js__rspack_import_1/* .setGroupMembers */.Td(session, group, members),
|
|
85984
86058
|
addGroupMember: (group, bot)=>_gateway_js__rspack_import_1/* .addGroupMember */.zr(session, group, bot),
|
|
85985
86059
|
removeGroupMember: (group, bot)=>_gateway_js__rspack_import_1/* .removeGroupMember */.A0(session, group, bot),
|
|
85986
|
-
send: (ref, prompt
|
|
86060
|
+
send: (ref, prompt)=>_gateway_js__rspack_import_1/* .sendPrompt */.LV(session, ref, prompt),
|
|
85987
86061
|
transcript: (ref, limit)=>_gateway_js__rspack_import_1/* .getTranscriptTail */.bA(session, ref, limit),
|
|
85988
86062
|
thread: (ref, rootId)=>_gateway_js__rspack_import_1/* .getThread */.fG(session, ref, rootId)
|
|
85989
86063
|
};
|
|
@@ -86024,24 +86098,36 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
86024
86098
|
// Codex app-server control socket. Vendored from uploads/codex-stdio-to-daemon-ws.py
|
|
86025
86099
|
// (ChatGPT Desktop -> managed daemon shim) with fail-open deltas, marked below:
|
|
86026
86100
|
// (1) ws_connect enforces one absolute monotonic deadline for connect + HTTP
|
|
86027
|
-
// upgrade (CODEX_BRIDGE_CONNECT_TIMEOUT, default 10s),
|
|
86028
|
-
//
|
|
86029
|
-
//
|
|
86101
|
+
// upgrade (CODEX_BRIDGE_CONNECT_TIMEOUT, default 10s), requires HTTP/1.1 101
|
|
86102
|
+
// plus a matching Sec-WebSocket-Accept (a 200 fails even with a valid hash),
|
|
86103
|
+
// and retains bytes coalesced after the upgrade headers instead of discarding
|
|
86104
|
+
// the first data frame.
|
|
86030
86105
|
// (2) read_message assembles continuation frames; control frames answer inline;
|
|
86031
|
-
// socket
|
|
86032
|
-
//
|
|
86033
|
-
//
|
|
86034
|
-
//
|
|
86035
|
-
// daemon
|
|
86036
|
-
//
|
|
86037
|
-
// (
|
|
86038
|
-
//
|
|
86039
|
-
//
|
|
86040
|
-
//
|
|
86106
|
+
// every socket send/lock shares one monotonic budget (remaining first-RPC
|
|
86107
|
+
// budget, else the mid-session write budget) so a non-reading peer cannot
|
|
86108
|
+
// wedge pong writes or timeout cleanup; timeouts close and exit. Reads wait
|
|
86109
|
+
// with no timeout after the handshake so healthy idle sessions survive.
|
|
86110
|
+
// The wrapper never starts the daemon on the spawn path, so no daemon lock can
|
|
86111
|
+
// block pre-stdin Desktop.
|
|
86112
|
+
// (3) A first-response deadline (CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT, default
|
|
86113
|
+
// 30s) bounds connect-to-matching-daemon-response covering the first RPC;
|
|
86114
|
+
// unrelated notifications and foreign responses never satisfy it, only the
|
|
86115
|
+
// matching response/error (or timeout) clears it. The preflight (3s) +
|
|
86116
|
+
// upgrade (10s) + first RPC (30s) budget stays far below a daemon-lock hang.
|
|
86117
|
+
// Exits are 0 served, 1 pre-stdio (wrapper falls back to real Codex on pristine
|
|
86118
|
+
// stdio+stdout), 2 mid-session (wrapper exits so Desktop reconnects).
|
|
86119
|
+
// (4) Byte-precise commit on input OR output, committed BEFORE the stdout
|
|
86120
|
+
// write: os.read into a userspace line buffer counts every stdin byte (even
|
|
86121
|
+
// an unforwarded blank line), and any daemon payload marks stdout used before
|
|
86122
|
+
// it is written via a bounded stdout write, so the fallback only ever runs
|
|
86123
|
+
// on truly pristine stdio+stdout and never spawns a second Codex after used
|
|
86124
|
+
// stdout. Buffered EOF-tail data flushes before the WS Close, which drains
|
|
86125
|
+
// via shutdown+close with a bounded join. The reader signals a done event so
|
|
86126
|
+
// WS close wakes the select loop; no path leaves a thread hung.
|
|
86041
86127
|
// Desktop stdio hangs with stock `codex app-server proxy`, so this custom
|
|
86042
86128
|
// bridge is required. Install writes it to the CODEX_HOME bin dir. Never reads
|
|
86043
86129
|
// 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";
|
|
86130
|
+
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
86131
|
|
|
86046
86132
|
__webpack_require__.d(__webpack_exports__, {
|
|
86047
86133
|
}, {
|
|
@@ -86055,9 +86141,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
86055
86141
|
/* import */ var node_fs__rspack_import_1 = __webpack_require__("node:fs");
|
|
86056
86142
|
/* import */ var node_os__rspack_import_2 = __webpack_require__("node:os");
|
|
86057
86143
|
/* 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
|
-
|
|
86144
|
+
/* import */ var _desktop_shim_bridge_js__rspack_import_4 = __webpack_require__("./src/core/desktop-shim-bridge.js");
|
|
86061
86145
|
|
|
86062
86146
|
|
|
86063
86147
|
|
|
@@ -86135,11 +86219,12 @@ function renderWrapperScript({ realPath, bridgePath, wrapperLogPath, codexHome,
|
|
|
86135
86219
|
# daemon's control socket. Reversible: \`gbot codex desktop-shim uninstall\`.
|
|
86136
86220
|
# The daemon is kept up by the LaunchAgent login script, not here: this hot path
|
|
86137
86221
|
# never starts the daemon itself, so a wedged daemon lock cannot hang
|
|
86138
|
-
# Desktop. Fail-open runs ONLY
|
|
86139
|
-
#
|
|
86222
|
+
# Desktop. Fail-open runs ONLY while stdio is still pristine (no stdin
|
|
86223
|
+
# consumed, no stdout written): an absent socket fails the preflight and a
|
|
86224
|
+
# pre-session bridge failure (exit 1) falls through
|
|
86140
86225
|
# to the real standalone codex on pristine stdio. A mid-session bridge failure
|
|
86141
86226
|
# (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
|
|
86227
|
+
# half-consumed stdin or dirty stdout. Bridge exit 0 means it served the session. Never touches
|
|
86143
86228
|
# Desktop binaries or its private tool pipe.
|
|
86144
86229
|
set -u
|
|
86145
86230
|
REAL="\${CODEX_DESKTOP_WRAPPER_REAL:-${realPath}}"
|
|
@@ -86156,7 +86241,11 @@ echo "$(ts) argv: $*" >>"$LOG" 2>/dev/null || true
|
|
|
86156
86241
|
if [[ ! -x "$REAL" ]]; then
|
|
86157
86242
|
FALLBACK="$(command -v codex 2>/dev/null || true)"
|
|
86158
86243
|
if [[ -n "$FALLBACK" && -x "$FALLBACK" ]]; then
|
|
86159
|
-
|
|
86244
|
+
if [[ "$FALLBACK" -ef "$0" ]] 2>/dev/null || { [[ -n "\${CODEX_CLI_PATH:-}" ]] && [[ "$FALLBACK" -ef "\${CODEX_CLI_PATH}" ]] 2>/dev/null; }; then
|
|
86245
|
+
echo "$(ts) refusing self fallback $FALLBACK" >>"$LOG" 2>/dev/null || true
|
|
86246
|
+
else
|
|
86247
|
+
REAL="$FALLBACK"
|
|
86248
|
+
fi
|
|
86160
86249
|
fi
|
|
86161
86250
|
fi
|
|
86162
86251
|
|
|
@@ -86177,7 +86266,7 @@ preflight() {
|
|
|
86177
86266
|
}
|
|
86178
86267
|
|
|
86179
86268
|
if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 ]]; then
|
|
86180
|
-
if [[ -
|
|
86269
|
+
if [[ -f "$BRIDGE" ]] && command -v python3 >/dev/null 2>&1; then
|
|
86181
86270
|
if preflight; then
|
|
86182
86271
|
echo "$(ts) rewrite -> stdio-ws bridge" >>"$LOG" 2>/dev/null || true
|
|
86183
86272
|
python3 "$BRIDGE"
|
|
@@ -86327,7 +86416,7 @@ function fileExists(path) {
|
|
|
86327
86416
|
recursive: true
|
|
86328
86417
|
});
|
|
86329
86418
|
writeExecutable(paths.wrapperPath, renderWrapperScript(paths));
|
|
86330
|
-
(0,node_fs__rspack_import_1.writeFileSync)(paths.bridgePath,
|
|
86419
|
+
(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
86420
|
mode: 420
|
|
86332
86421
|
});
|
|
86333
86422
|
writeExecutable(paths.envScriptPath, renderEnvScript(paths));
|
|
@@ -86407,11 +86496,12 @@ function fileExists(path) {
|
|
|
86407
86496
|
paths.bridgePath,
|
|
86408
86497
|
paths.envScriptPath
|
|
86409
86498
|
]){
|
|
86499
|
+
const existed = fileExists(path);
|
|
86410
86500
|
try {
|
|
86411
86501
|
if ((0,node_fs__rspack_import_1.rmSync)(path, {
|
|
86412
86502
|
force: true
|
|
86413
86503
|
}) === undefined && fileExists(path)) warnings.push(`could not remove ${path}`);
|
|
86414
|
-
else if (!fileExists(path)) removed.push(path);
|
|
86504
|
+
else if (existed && !fileExists(path)) removed.push(path);
|
|
86415
86505
|
} catch (error) {
|
|
86416
86506
|
warnings.push(`could not remove ${path} (${error instanceof Error ? error.message : String(error)})`);
|
|
86417
86507
|
}
|
|
@@ -86505,12 +86595,10 @@ function socketStateOf(path) {
|
|
|
86505
86595
|
// Desktop-facing value: the GUI domain on macOS, the caller env elsewhere.
|
|
86506
86596
|
const desktopCliPath = platform === "darwin" ? guiCliPath : cliPath;
|
|
86507
86597
|
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
|
-
});
|
|
86598
|
+
// Effective socket, resolved exactly like gbot's own client (CODEX_APP_SERVER_SOCK
|
|
86599
|
+
// wins, else the install-time CODEX_HOME) so status and send/queue/status never
|
|
86600
|
+
// disagree. Inlined here so codex-bridge can reuse desktopShimStatus without a cycle.
|
|
86601
|
+
const socketPath = env.CODEX_APP_SERVER_SOCK || (0,node_path__rspack_import_3.join)(paths.codexHome, "app-server-control", "app-server-control.sock");
|
|
86514
86602
|
return {
|
|
86515
86603
|
action: "status",
|
|
86516
86604
|
bridgePath: paths.bridgePath,
|
|
@@ -86605,7 +86693,7 @@ function formatTranscript(out, { full = false } = {}) {
|
|
|
86605
86693
|
for (const e of entries){
|
|
86606
86694
|
const role = e.role || e.kind || e.sender || e.type || "msg";
|
|
86607
86695
|
const text = (0,_transcript_js__rspack_import_0/* .entryText */.u0)(e);
|
|
86608
|
-
const id = e.id ||
|
|
86696
|
+
const id = e.id || "";
|
|
86609
86697
|
lines.push("[" + role + (id ? " " + id : "") + "] " + (full ? text : truncateCliText(text)));
|
|
86610
86698
|
}
|
|
86611
86699
|
return lines.join("\n");
|
|
@@ -86618,6 +86706,7 @@ function formatCodexStatus(s) {
|
|
|
86618
86706
|
const lines = [
|
|
86619
86707
|
"socket: " + s.socketPath + " (" + s.socketState + ")"
|
|
86620
86708
|
];
|
|
86709
|
+
if (s.desktopShimConfigured) lines.push("desktop shim: configured (managed attachment unverified)");
|
|
86621
86710
|
if (!s.reachable) return lines.concat("reachable: no (" + s.mode + ")", s.message).join("\n");
|
|
86622
86711
|
if (s.mode !== "daemon") {
|
|
86623
86712
|
return lines.concat("reachable: yes, but unusable (" + s.mode + ")", s.message).join("\n");
|
|
@@ -86625,7 +86714,7 @@ function formatCodexStatus(s) {
|
|
|
86625
86714
|
lines.push("reachable: yes (daemon)");
|
|
86626
86715
|
const cli = s.cliVersion ?? (s.cliVersionProbe === "ok" ? "unknown" : "unknown, probe " + s.cliVersionProbe);
|
|
86627
86716
|
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 (
|
|
86717
|
+
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
86718
|
if (s.versionMismatch) {
|
|
86630
86719
|
lines.push("warning: daemon and CLI versions differ; `codex app-server daemon restart` picks up the installed CLI");
|
|
86631
86720
|
}
|
|
@@ -86640,6 +86729,9 @@ function formatCodexThread(t) {
|
|
|
86640
86729
|
const preview = t.preview ? "\n " + stripTerminalControls(String(t.preview).replace(/\s+/g, " ")).slice(0, 200) : "";
|
|
86641
86730
|
return stripTerminalControls(t.id) + " " + stripTerminalControls(t.status) + title + "\n " + stripTerminalControls(t.cwd ?? "") + preview;
|
|
86642
86731
|
}
|
|
86732
|
+
/** Shell-safe single-quoting for copy-pasteable export lines (spaces, quotes, $). */ function shellQuote(value) {
|
|
86733
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
86734
|
+
}
|
|
86643
86735
|
function formatDesktopShimStatus(s) {
|
|
86644
86736
|
const mark = (ok)=>ok ? "yes" : "no";
|
|
86645
86737
|
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 +86746,7 @@ function formatDesktopShimStatus(s) {
|
|
|
86654
86746
|
if (!s.installed) {
|
|
86655
86747
|
lines.push("shim: not installed — run `gbot codex desktop-shim install` (Desktop keeps stock behavior until then)");
|
|
86656
86748
|
} 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");
|
|
86749
|
+
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
86750
|
} else {
|
|
86659
86751
|
lines.push("shim: active — Desktop app-server spawns bridge onto the managed daemon");
|
|
86660
86752
|
}
|
|
@@ -86697,19 +86789,17 @@ class GatewayError extends Error {
|
|
|
86697
86789
|
const GATEWAY_TIMEOUT_MS = 30000;
|
|
86698
86790
|
const GATEWAY_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
86699
86791
|
function backendBase() {
|
|
86700
|
-
return (process.env.
|
|
86792
|
+
return (process.env.CURSOR_API_BASE_URL || "https://api2.cursor.sh").replace(/\/$/, "");
|
|
86701
86793
|
}
|
|
86702
86794
|
function accessTokenFromEnv() {
|
|
86703
|
-
return (process.env.CURSOR_ACCESS_TOKEN ||
|
|
86795
|
+
return (process.env.CURSOR_ACCESS_TOKEN || "").trim();
|
|
86704
86796
|
}
|
|
86705
86797
|
function gatewayTokenFromEnv() {
|
|
86706
|
-
return (process.env.GROK_BOT_GATEWAY_TOKEN ||
|
|
86798
|
+
return (process.env.GROK_BOT_GATEWAY_TOKEN || "").trim();
|
|
86707
86799
|
}
|
|
86708
86800
|
function gatewayOverride() {
|
|
86709
86801
|
const token = gatewayTokenFromEnv();
|
|
86710
|
-
const
|
|
86711
|
-
const localUrl = token ? "http://127.0.0.1:" + (process.env.SAND_HOST_PORT || "1340") : "";
|
|
86712
|
-
const url = explicitUrl || localUrl;
|
|
86802
|
+
const url = (process.env.GROK_BOT_GATEWAY_URL || "").trim();
|
|
86713
86803
|
if (url && token) {
|
|
86714
86804
|
return {
|
|
86715
86805
|
gatewayUrl: (0,_url_policy_js__rspack_import_3/* .assertAllowedCredentialUrl */.Re)(url.replace(/\/$/, ""), {
|
|
@@ -87029,14 +87119,13 @@ async function removeGroupMember(session, groupRef, memberRef) {
|
|
|
87029
87119
|
const next = group.memberIds.filter((id)=>id !== member.id);
|
|
87030
87120
|
return setGroupMembers(session, group.id, next);
|
|
87031
87121
|
}
|
|
87032
|
-
async function sendPrompt(session, ref, prompt
|
|
87122
|
+
async function sendPrompt(session, ref, prompt) {
|
|
87033
87123
|
const rec = await resolveRef(session, ref);
|
|
87034
87124
|
const body = {
|
|
87035
87125
|
agentId: rec.id,
|
|
87036
87126
|
prompt,
|
|
87037
|
-
clientNonce:
|
|
87127
|
+
clientNonce: (0,node_crypto__rspack_import_0.randomUUID)()
|
|
87038
87128
|
};
|
|
87039
|
-
if (extra.replyToId) body.replyToId = extra.replyToId;
|
|
87040
87129
|
let data;
|
|
87041
87130
|
try {
|
|
87042
87131
|
data = await gatewayCall(session, "sendPrompt", body);
|
|
@@ -87213,8 +87302,8 @@ function saveHistory(out, { dir, disabled, event, prompt, rootId } = {}) {
|
|
|
87213
87302
|
rootId
|
|
87214
87303
|
} : {},
|
|
87215
87304
|
role: String(entry.role || entry.kind || entry.sender || entry.type || "msg"),
|
|
87216
|
-
...entry.id
|
|
87217
|
-
messageId: String(entry.id
|
|
87305
|
+
...entry.id ? {
|
|
87306
|
+
messageId: String(entry.id)
|
|
87218
87307
|
} : {},
|
|
87219
87308
|
...entry.timestamp || entry.createdAt ? {
|
|
87220
87309
|
timestamp: String(entry.timestamp || entry.createdAt)
|
|
@@ -87358,10 +87447,7 @@ function isUuid(value) {
|
|
|
87358
87447
|
function defaultCandidateRoots() {
|
|
87359
87448
|
const home = (0,node_os__rspack_import_2.homedir)();
|
|
87360
87449
|
const env = [
|
|
87361
|
-
process.env.GROK_BOT_AGENTS_DIR
|
|
87362
|
-
process.env.SAND_AGENTS_DIR,
|
|
87363
|
-
process.env.SAND_DATA_ROOT && (0,node_path__rspack_import_3.join)(process.env.SAND_DATA_ROOT, "agents"),
|
|
87364
|
-
process.env.SAND_DATA_ROOT && (0,node_path__rspack_import_3.join)(process.env.SAND_DATA_ROOT, "agent-data", "agents")
|
|
87450
|
+
process.env.GROK_BOT_AGENTS_DIR
|
|
87365
87451
|
].filter(Boolean);
|
|
87366
87452
|
return [
|
|
87367
87453
|
...env,
|
|
@@ -87640,7 +87726,6 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
87640
87726
|
|
|
87641
87727
|
},
|
|
87642
87728
|
"./src/core/transcript.js"(__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) {
|
|
87643
|
-
// Shared by `gbot thread` and the grok-bot plugin's gbot_thread tool.
|
|
87644
87729
|
/** Coerce anything to a string without throwing (numbers, BigInt, unserializable objects). */ function toSafeText(value) {
|
|
87645
87730
|
if (typeof value === "string") return value;
|
|
87646
87731
|
if (value == null) return "";
|
|
@@ -87678,15 +87763,12 @@ function entryTextRaw(e) {
|
|
|
87678
87763
|
return typeof e.preview === "string" ? e.preview : "";
|
|
87679
87764
|
}
|
|
87680
87765
|
function transcriptEntries(payload) {
|
|
87681
|
-
if (Array.isArray(payload)) return payload;
|
|
87682
87766
|
if (!payload || typeof payload !== "object") return [];
|
|
87683
|
-
|
|
87684
|
-
return Array.isArray(entries) ? entries : [];
|
|
87767
|
+
return Array.isArray(payload.entries) ? payload.entries : [];
|
|
87685
87768
|
}
|
|
87686
87769
|
function sourceEntryId(entry) {
|
|
87687
87770
|
if (!entry || typeof entry !== "object") return "";
|
|
87688
|
-
|
|
87689
|
-
return typeof entry.messageId === "string" ? entry.messageId : "";
|
|
87771
|
+
return typeof entry.id === "string" ? entry.id : "";
|
|
87690
87772
|
}
|
|
87691
87773
|
function lastSourceId(entries, fallback = "") {
|
|
87692
87774
|
for(let index = entries.length - 1; index >= 0; index -= 1){
|
|
@@ -87751,6 +87833,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
87751
87833
|
* never *.cursorvm.com, so a CURSOR_ACCESS_TOKEN cannot be pointed at a box host.
|
|
87752
87834
|
* Local/dev gateways: http(s)://127.0.0.1|localhost|::1 when GROK_BOT_ALLOW_LOCAL_GATEWAY=1.
|
|
87753
87835
|
* Escape hatch: GROK_BOT_ALLOW_ANY_GATEWAY=1 (unsafe; disables host checks; warns once).
|
|
87836
|
+
* Test mode (GROK_BOT_TEST=1 or NODE_ENV=test): loopback only, for gateway and backend
|
|
87837
|
+
* alike, and the escape hatches are ignored — a test can never reach a live thread.
|
|
87754
87838
|
*/ function truthyEnv(name) {
|
|
87755
87839
|
const v = (process.env[name] || "").trim().toLowerCase();
|
|
87756
87840
|
return v === "1" || v === "true" || v === "yes";
|
|
@@ -87761,6 +87845,9 @@ function allowAnyGateway() {
|
|
|
87761
87845
|
function allowLocalGateway() {
|
|
87762
87846
|
return truthyEnv("GROK_BOT_ALLOW_LOCAL_GATEWAY");
|
|
87763
87847
|
}
|
|
87848
|
+
function testMode() {
|
|
87849
|
+
return truthyEnv("GROK_BOT_TEST") || process.env.NODE_ENV === "test";
|
|
87850
|
+
}
|
|
87764
87851
|
const warned = new Set();
|
|
87765
87852
|
function warnOnce(key, message) {
|
|
87766
87853
|
if (warned.has(key)) return;
|
|
@@ -87800,6 +87887,12 @@ function isCursorGatewayHostname(hostname) {
|
|
|
87800
87887
|
if (parsed.username || parsed.password) {
|
|
87801
87888
|
throw new Error("Rejected " + label + ": userinfo is not allowed.");
|
|
87802
87889
|
}
|
|
87890
|
+
if (testMode()) {
|
|
87891
|
+
if (!isLocalHostname(parsed.hostname) || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
87892
|
+
throw new Error("Rejected " + label + " host \"" + parsed.hostname + "\": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.");
|
|
87893
|
+
}
|
|
87894
|
+
return String(rawUrl).replace(/\/$/, "");
|
|
87895
|
+
}
|
|
87803
87896
|
if (allowAnyGateway()) {
|
|
87804
87897
|
warnOnce("ALLOW_ANY", "warning: GROK_BOT_ALLOW_ANY_GATEWAY is set; credential host checks are disabled.");
|
|
87805
87898
|
return String(rawUrl).replace(/\/$/, "");
|