diff --git a/.changeset/chatgpt-desktop-cdp.md b/.changeset/chatgpt-desktop-cdp.md new file mode 100644 index 0000000..86c2736 --- /dev/null +++ b/.changeset/chatgpt-desktop-cdp.md @@ -0,0 +1,7 @@ +--- +"grok-bot-cli": minor +--- + +Scaffold ChatGPT Desktop CDP adapter and MCP/CLI tools (`chatgpt_desktop_*`). List/search keep separate `host` / `modelProvider` filters; `list_hosts` discovers hosts and providers. Codex list-threads always passes `modelProviders: []`, pages via `nextCursor`, and falls back from `useStateDbOnly` when empty. Codex send/read/wait strip Desktop `local:` ids; remote-control "thread not loaded" maps to `REMOTE_THREAD_NOT_LOADED` with host. Thread reads use `thread/read` metadata + `thread/turns/list` (never deprecated `includeTurns`). + +Keep CDP loopback-only across discovery, bound and close sessions, serialize UI operations, distinguish partial replies and uncertain sends, and preserve dynamic remote/provider filtering and ordered empty-safe history reads. diff --git a/.changeset/desktop-e2e-reliability.md b/.changeset/desktop-e2e-reliability.md new file mode 100644 index 0000000..0ae216a --- /dev/null +++ b/.changeset/desktop-e2e-reliability.md @@ -0,0 +1,7 @@ +--- +"grok-bot-cli": patch +--- + +Fix ChatGPT Desktop host discovery, bounded thread reads and searches, list pagination, CDP navigation and project clicks, status reachability, and result provenance. Add complete MCP and skill guidance, project filters, archived-thread reads and explicit unarchive-on-send support. + +Keep MCP Desktop results below the Agent Bundle document cap with resumable, lossless read fragments and byte-bounded list/search pages. Compact display titles while matching search against full titles, and align remote host counts with filtered inventory. diff --git a/.changeset/desktop-merged-pagination-projects.md b/.changeset/desktop-merged-pagination-projects.md new file mode 100644 index 0000000..8667a8d --- /dev/null +++ b/.changeset/desktop-merged-pagination-projects.md @@ -0,0 +1,5 @@ +--- +"grok-bot-cli": patch +--- + +Page ChatGPT Desktop threads from a stable, sorted merged inventory so CDP and remote rows appear once in their timestamp range and requested page sizes are filled. Resolve project labels from Desktop assignments and workspace roots. Verify new-chat navigation and project scope before sending, and report unavailable project roots promptly. diff --git a/.gitignore b/.gitignore index 18e51f5..94e82da 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ tmp-agents/ *.log .DS_Store *.tsbuildinfo + +# Local plugin checkout is not part of the shipped source snapshot. +/plugin/ diff --git a/README.md b/README.md index 9e40093..e9ced7d 100644 --- a/README.md +++ b/README.md @@ -228,11 +228,27 @@ Residual risks, stated honestly: requests without both matching thread and turn Current shim limitation: Desktop's spawn-time app-tools MCP `-c` overrides are not forwarded to the already-running managed daemon. No restoration path or app-tools parity has been demonstrated here; this is not a claim of permanent protocol impossibility. Fully quit and relaunch ChatGPT.app after install (or login) so it inherits `CODEX_CLI_PATH`. `status` reads the macOS GUI-domain value via `launchctl getenv` (what Desktop actually inherits) alongside the calling shell's value. LaunchAgent persistence is macOS-first; elsewhere install still writes the wrapper and bridge but leaves `CODEX_CLI_PATH` for you to export. `~/.codex/bin` holds scripts only — there is no extra revert note to clean up; revert is `gbot codex desktop-shim uninstall` plus this section. +**ChatGPT Desktop over CDP (scaffold).** Prefer the Desktop UI surface via Chrome DevTools Protocol when you need more than the app-server socket. CDP is **local-only** (`127.0.0.1`); there is no remote transport. Relaunch the macOS app with a debug port (Electron fuses block Node inspect, not Chromium's flag): + +```sh +osascript -e 'tell application "ChatGPT" to quit' \ + && open -a /Applications/ChatGPT.app --args --remote-debugging-port=9222 +gbot chatgpt-desktop status +gbot chatgpt-desktop threads --limit 20 +gbot chatgpt-desktop search "launch" +gbot chatgpt-desktop read +gbot chatgpt-desktop read --full --limit 200 +gbot chatgpt-desktop send --project Launch "hello" +gbot chatgpt-desktop send --thread-id "hello" +``` + +MCP tools: `chatgpt_desktop_status`, `chatgpt_desktop_list_threads`, `chatgpt_desktop_search_threads`, `chatgpt_desktop_read_thread`, `chatgpt_desktop_send`, `chatgpt_desktop_wait_reply`. List/search/read prefer the Codex app-server (`thread/list` with `modelProviders: []`, `thread/read` + `thread/turns/list`; never `thread/resume` for reads). Send / new-thread / wait / selected-thread stay on CDP; DOM read is fallback only. Desktop `local:` strips to the bare app-server id. Results report `backend: "cdp" | "app-server"`. Details: [docs/chatgpt-desktop-cdp.md](docs/chatgpt-desktop-cdp.md). + **Status contract (`gbot codex status --json`).** `reachable` is endpoint reachability only. `socketState` is `socket`, `absent`, `permission-denied`, or `not-a-socket`; `mode` is `daemon` for a usable daemon, otherwise the failure: `socket-absent`, `permission-denied` (the file or the connect refused this user), `not-a-socket`, `connect-failed` (socket present, nothing completed the WebSocket upgrade), `handshake-failed` (upgrade or `initialize` failed), `windows-unsupported`, or `bad-response` (reachable, but `initialize` returned something off-schema — `reachable` stays `true`). `schema.compatibility` is `exact` when the daemon reports the pinned version, `unverified` when it differs (methods usually survive upgrades, but the shapes are not re-checked), or `unknown`. `cliVersionProbe` reports whether `codex --version` answered (`ok`, `missing`, `timeout` after 3 s, `error`). The document is always written to stdout and includes `exitCode`; it is `0` only for a usable daemon. `desktopShimConfigured` is true when the installed wrapper is selected by Desktop-facing `CODEX_CLI_PATH` (the GUI domain on macOS). This describes configuration for future launches; a running Desktop may not have inherited it, and the wrapper may have fallen back to stock Codex. `desktopAttached` is `"private-stdio"` when a Desktop-bundled app-server process is observed, otherwise `"unknown"`. That process observation and shim configuration can both be present. Neither a configured shim nor a reachable daemon proves Desktop is attached to that daemon. Verify actual attachment with a controlled shared-thread interaction and matching thread/turn IDs. -**Thread discovery.** `list-threads --limit N` (1–200) pages with the opaque `--cursor` from the previous `nextCursor`; JSON keeps the cursor verbatim, text output prints a sanitized `more: --cursor …` hint. Text fields are stripped of terminal control sequences in both outputs (single-line fields also lose line breaks; `preview` keeps its newlines; a structured `source` such as `{ "custom": … }` passes through unchanged), `status` is one of `notLoaded | idle | active | systemError | unknown`, and non-numeric `updatedAt` becomes `null`. Unknown arguments are rejected before the socket is touched; a response that does not match the pinned schema (including an entry without a string `id`) fails with `reason: "bad-response"`. +**Thread discovery.** `list-threads --limit N` (1–200) pages with the opaque `--cursor` from the previous `nextCursor`; JSON keeps the cursor verbatim, text output prints a sanitized `more: --cursor …` hint. Requests pass `modelProviders: []` so threads from every provider appear (the daemon otherwise filters to the current provider) and `useStateDbOnly: true` for a fast state-DB listing, falling back to a full scan when that first page is empty. Text fields are stripped of terminal control sequences in both outputs (single-line fields also lose line breaks; `preview` keeps its newlines; a structured `source` such as `{ "custom": … }` passes through unchanged), `status` is one of `notLoaded | idle | active | systemError | unknown`, and non-numeric `updatedAt` / `createdAt` become `null`. Extra fields when present: `section`, `projectId`, `modelProvider`, `model`, `originator`. Unknown arguments are rejected before the socket is touched; a response that does not match the pinned schema (including an entry without a string `id`) fails with `reason: "bad-response"`. **Routes, attribution, and loops.** `gbot codex send` runs on the machine that owns `CODEX_HOME`, as the user who owns the socket, with that user's Codex credentials; the socket path comes only from `CODEX_HOME` or `CODEX_APP_SERVER_SOCK`, never from the message or an agent-supplied argument. gbot has no remote transport. A cloud-hosted Grok Bot on the box cannot reach a desktop socket at `/home/box/.codex/...` — use Grok Bot Shell with a machineId to run `gbot` on the user's registered machine (after `codex app-server daemon start` / bootstrap there). `GROK_BOT_CODEX_THREADS=id,id` lets the operator pin `send` to approved threads (`reason: "route-not-allowed"` otherwise). Every send gets a delivery envelope: `messageId` (also sent as Codex's native `clientUserMessageId`), `correlationId` (defaults to the message id), optional `replyTo`, and `hop`. A reply passes the original correlation id and `hop` + 1: @@ -262,7 +278,10 @@ The npm package is also an [Agent Bundle](https://scriptedalchemy.github.io/agen that gives Codex, Claude Code, and Cursor a `grok-bot` MCP server with messaging, Codex conversation, and managed bridge tools, plus a `talk-to-grok-bot` skill. `gbot_send` and `gbot_thread` handle Grok conversations; `codex_threads`, -`codex_send`, `codex_new`, `codex_wait`, and `codex_watch` handle Codex conversations. Claude Code and Cursor bundles also include `/gbot:codex-send`, `/gbot:codex-threads`, and `/gbot:codex-wait` commands under `commands/`. +`codex_send`, `codex_new`, `codex_wait`, and `codex_watch` handle Codex conversations. +`chatgpt_desktop_*` tools drive ChatGPT Desktop over local CDP (with app-server +fallback). Claude Code and Cursor bundles also include `/gbot:codex-send`, +`/gbot:codex-threads`, and `/gbot:codex-wait` commands under `commands/`. `gbot_bridge_start`, `gbot_bridge_status`, `gbot_bridge_stop`, and `gbot_codex_respond` manage automatic delivery and scoped operator responses. The tools bundle this repository's gateway client and worker, so the installed diff --git a/artifact/agent-bundle.compile-evidence.json b/artifact/agent-bundle.compile-evidence.json index 3356b2b..19df1e0 100644 --- a/artifact/agent-bundle.compile-evidence.json +++ b/artifact/agent-bundle.compile-evidence.json @@ -1 +1 @@ -{"assets":[{"externals":[{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"async_hooks","userRequest":"async_hooks"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"crypto","userRequest":"crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/cli/codex/bridge/run.tsx","src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/desktop-shim.js","src/core/history.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/40.js","src/cli/skills/add.tsx","src/core/claude-channel.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js","src/core/codex-bridge.js","src/core/relay/control.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/history.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/cli/skills/add.tsx","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/history.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["src/core/history.js"],"kind":"builtin","request":"node:readline","userRequest":"node:readline"},{"externalType":"module","issuers":["src/core/relay/control.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"},{"externalType":"module","issuers":[".agent-bundle-virtual/bin-gbot-flight-entry.mjs","src/core/relay/managed.js"],"kind":"builtin","request":"node:url","userRequest":"node:url"},{"externalType":"module","issuers":[".agent-bundle-virtual/bin-gbot-flight-entry.mjs"],"kind":"builtin","request":"node:worker_threads","userRequest":"node:worker_threads"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"stream","userRequest":"stream"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.production.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"util","userRequest":"util"}],"packages":["@agent-bundle/runtime","effect","react","react-dom","react-server-dom-rspack","rsc-markdown-stream","zod"],"path":"bin/gbot-flight.mjs","sha256":"6512082aecad31bdef60eef2788e8f427aa542a5ece7dae9f2a4a560f071c682"},{"externals":[{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/cli/codex/bridge/run.tsx","src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/667~1.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/launch-env.js","node_modules/agent-bundle/dist/terminal-capability.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/desktop-shim.js","src/core/history.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/40.js","node_modules/agent-bundle/dist/667~1.js","src/cli/skills/add.tsx","src/core/claude-channel.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js","src/core/codex-bridge.js","src/core/relay/control.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/history.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/launch-env.js","src/cli/skills/add.tsx","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/history.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["src/core/history.js"],"kind":"builtin","request":"node:readline","userRequest":"node:readline"},{"externalType":"module","issuers":["src/core/relay/control.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"},{"externalType":"module","issuers":[".agent-bundle-virtual/bin-gbot-0.mjs",".agent-bundle-virtual/bin-gbot-entry.mjs","src/core/relay/managed.js"],"kind":"builtin","request":"node:url","userRequest":"node:url"},{"externalType":"module","issuers":[".agent-bundle-virtual/bin-gbot-entry.mjs"],"kind":"builtin","request":"node:worker_threads","userRequest":"node:worker_threads"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.production.js"],"kind":"builtin","request":"util","userRequest":"util"}],"packages":["@agent-bundle/runtime","agent-bundle","effect","react","react-dom","react-server-dom-rspack","rsc-markdown-stream","zod"],"path":"bin/gbot.mjs","sha256":"e66fbcc6432392f078d773bb5eb468c88ffc4f21d4b746cccd4d48ad206604fe"},{"externals":[{"externalType":"module","issuers":["src/core/claude-channel.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/agent-bundle/dist/launch-env.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["src/core/claude-channel.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["src/core/claude-channel.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/agent-bundle/dist/launch-env.js","src/core/claude-channel.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs","node_modules/agent-bundle/node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs"],"kind":"builtin","request":"node:process","userRequest":"node:process"},{"externalType":"module","issuers":[".agent-bundle-virtual/mcp-claude-channel-8029413c-1.mjs"],"kind":"builtin","request":"node:url","userRequest":"node:url"}],"packages":["@modelcontextprotocol/core","@modelcontextprotocol/server","agent-bundle","zod"],"path":"mcp/mcp-claude-channel-8029413c.mjs","sha256":"3a966160704621cd614ae9217051b2a681c0c913dd7507020c71c79f02161486"},{"externals":[{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"async_hooks","userRequest":"async_hooks"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"crypto","userRequest":"crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/desktop-shim.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/40.js","src/core/claude-channel.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js","src/core/codex-bridge.js","src/core/relay/control.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["src/core/relay/control.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"},{"externalType":"module","issuers":[".agent-bundle-virtual/mcp-grok-bot-b8c2461e-flight-entry.mjs","src/core/relay/managed.js"],"kind":"builtin","request":"node:url","userRequest":"node:url"},{"externalType":"module","issuers":[".agent-bundle-virtual/mcp-grok-bot-b8c2461e-flight-entry.mjs"],"kind":"builtin","request":"node:worker_threads","userRequest":"node:worker_threads"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"stream","userRequest":"stream"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.production.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"util","userRequest":"util"}],"packages":["@agent-bundle/runtime","agent-bundle","effect","react","react-dom","react-server-dom-rspack","rsc-markdown-stream","zod"],"path":"mcp/mcp-grok-bot-b8c2461e-flight.mjs","sha256":"228cc31c20dad9ca169646101e127bc4fecf5bb941b8014753c66d1bf6f06452"},{"externals":[{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/@agent-bundle/runtime/dist/lineage.js","node_modules/agent-bundle/dist/667~1.js","node_modules/agent-bundle/dist/mcp-server-runtime.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/launch-env.js","node_modules/agent-bundle/dist/terminal-capability.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/desktop-shim.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/40.js","node_modules/agent-bundle/dist/667~1.js","src/core/claude-channel.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js","src/core/codex-bridge.js","src/core/relay/control.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/launch-env.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["node_modules/agent-bundle/node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs"],"kind":"builtin","request":"node:process","userRequest":"node:process"},{"externalType":"module","issuers":["src/core/relay/control.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"},{"externalType":"module","issuers":[".agent-bundle-virtual/mcp-grok-bot-b8c2461e-1.mjs",".agent-bundle-virtual/mcp-grok-bot-b8c2461e-2.mjs","src/core/relay/managed.js"],"kind":"builtin","request":"node:url","userRequest":"node:url"},{"externalType":"module","issuers":["node_modules/agent-bundle/dist/mcp-server-runtime.js"],"kind":"builtin","request":"node:worker_threads","userRequest":"node:worker_threads"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.production.js"],"kind":"builtin","request":"util","userRequest":"util"}],"packages":["@agent-bundle/runtime","@modelcontextprotocol/core","@modelcontextprotocol/server","agent-bundle","effect","react","react-dom","react-server-dom-rspack","rsc-markdown-stream","zod"],"path":"mcp/mcp-grok-bot-b8c2461e.mjs","sha256":"b51d83893aa2ce106a036de71a3649f880d16462a4b67f1362659e57f148563a"},{"externals":[{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state.js"],"kind":"builtin","request":"node:assert/strict","userRequest":"node:assert/strict"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state/sqlite.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/codex.js","src/core/relay/control.js","src/core/relay/engine.js","src/core/relay/profile.js","src/core/relay/records.js","src/core/relay/state.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state/sqlite.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/desktop-shim.js","src/core/relay/codex.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["src/core/relay/ownership.js","src/core/relay/state.js","src/core/relay/worker.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/codex-bridge.js","src/core/relay/control.js","src/core/relay/ownership.js","src/core/relay/worker.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/engine.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state/sqlite.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/engine.js","src/core/relay/ownership.js","src/core/relay/profile.js","src/core/relay/state.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"import","issuers":["src/core/relay/ownership.js"],"kind":"builtin","request":"node:sqlite","userRequest":"node:sqlite"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state/sqlite.js"],"kind":"builtin","request":"node:sqlite","userRequest":"node:sqlite"},{"externalType":"module","issuers":["src/core/relay/control.js","src/core/relay/worker.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"}],"packages":["@agent-bundle/runtime","effect","zod"],"path":"scripts/gbot-relay.mjs","sha256":"9445a1735c380cd065a513eb2f795e6f8a5c6623092d96447a1bcfd28f6fd333"}],"coverage":{"rewritable":false,"unobserved":["import()","require()","require.resolve(…)","createRequire(…)(…)","import.meta.resolve(…)"]},"policy":{"name":"closed-world-externals","revision":1},"producer":{"name":"agent-bundle","rspack":"2.2.8","version":"0.3.1"}} +{"assets":[{"externals":[{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"async_hooks","userRequest":"async_hooks"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"crypto","userRequest":"crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/cli/codex/bridge/run.tsx","src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/chatgpt-desktop/facade.ts","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/chatgpt-desktop/cdp-adapter.ts","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/history.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/40.js","src/cli/skills/add.tsx","src/core/claude-channel.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js","src/core/codex-bridge.js","src/core/relay/control.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/history.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/cli/skills/add.tsx","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/history.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["src/core/history.js"],"kind":"builtin","request":"node:readline","userRequest":"node:readline"},{"externalType":"module","issuers":["src/core/relay/control.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"},{"externalType":"module","issuers":[".agent-bundle-virtual/bin-gbot-flight-entry.mjs","src/core/relay/managed.js"],"kind":"builtin","request":"node:url","userRequest":"node:url"},{"externalType":"module","issuers":[".agent-bundle-virtual/bin-gbot-flight-entry.mjs"],"kind":"builtin","request":"node:worker_threads","userRequest":"node:worker_threads"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"stream","userRequest":"stream"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.production.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"util","userRequest":"util"}],"packages":["@agent-bundle/runtime","effect","react","react-dom","react-server-dom-rspack","rsc-markdown-stream","zod"],"path":"bin/gbot-flight.mjs","sha256":"b0a314b26965679b91a06be788337603c12006f3061beebb5f4a682e6e26a581"},{"externals":[{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/cli/codex/bridge/run.tsx","src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/667~1.js","src/core/app-session.js","src/core/chatgpt-desktop/facade.ts","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/launch-env.js","node_modules/agent-bundle/dist/terminal-capability.js","src/core/app-session.js","src/core/chatgpt-desktop/cdp-adapter.ts","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/history.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/40.js","node_modules/agent-bundle/dist/667~1.js","src/cli/skills/add.tsx","src/core/claude-channel.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js","src/core/codex-bridge.js","src/core/relay/control.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/history.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/launch-env.js","src/cli/skills/add.tsx","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/history.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["src/core/history.js"],"kind":"builtin","request":"node:readline","userRequest":"node:readline"},{"externalType":"module","issuers":["src/core/relay/control.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"},{"externalType":"module","issuers":[".agent-bundle-virtual/bin-gbot-0.mjs",".agent-bundle-virtual/bin-gbot-entry.mjs","src/core/relay/managed.js"],"kind":"builtin","request":"node:url","userRequest":"node:url"},{"externalType":"module","issuers":[".agent-bundle-virtual/bin-gbot-entry.mjs"],"kind":"builtin","request":"node:worker_threads","userRequest":"node:worker_threads"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.production.js"],"kind":"builtin","request":"util","userRequest":"util"}],"packages":["@agent-bundle/runtime","agent-bundle","effect","react","react-dom","react-server-dom-rspack","rsc-markdown-stream","zod"],"path":"bin/gbot.mjs","sha256":"1da1b6468f12930e44b9be5422ada37905e0ea0a558569d98d8da17df9e2362f"},{"externals":[{"externalType":"module","issuers":["src/core/claude-channel.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/agent-bundle/dist/launch-env.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["src/core/claude-channel.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["src/core/claude-channel.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/agent-bundle/dist/launch-env.js","src/core/claude-channel.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs","node_modules/agent-bundle/node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs"],"kind":"builtin","request":"node:process","userRequest":"node:process"},{"externalType":"module","issuers":[".agent-bundle-virtual/mcp-claude-channel-8029413c-1.mjs"],"kind":"builtin","request":"node:url","userRequest":"node:url"}],"packages":["@modelcontextprotocol/core","@modelcontextprotocol/server","agent-bundle","zod"],"path":"mcp/mcp-claude-channel-8029413c.mjs","sha256":"3a966160704621cd614ae9217051b2a681c0c913dd7507020c71c79f02161486"},{"externals":[{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"async_hooks","userRequest":"async_hooks"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"crypto","userRequest":"crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/chatgpt-desktop/facade.ts","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/chatgpt-desktop/cdp-adapter.ts","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/40.js","src/core/claude-channel.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js","src/core/codex-bridge.js","src/core/relay/control.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["src/core/relay/control.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"},{"externalType":"module","issuers":[".agent-bundle-virtual/mcp-grok-bot-b8c2461e-flight-entry.mjs","src/core/relay/managed.js"],"kind":"builtin","request":"node:url","userRequest":"node:url"},{"externalType":"module","issuers":[".agent-bundle-virtual/mcp-grok-bot-b8c2461e-flight-entry.mjs"],"kind":"builtin","request":"node:worker_threads","userRequest":"node:worker_threads"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"stream","userRequest":"stream"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.production.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-server.node.production.js"],"kind":"builtin","request":"util","userRequest":"util"}],"packages":["@agent-bundle/runtime","agent-bundle","effect","react","react-dom","react-server-dom-rspack","rsc-markdown-stream","zod"],"path":"mcp/mcp-grok-bot-b8c2461e-flight.mjs","sha256":"0c9ab3f71ae43f71263cae3d55b241e731ae663ecbe580c31e3a50d63e3a3a3d"},{"externals":[{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/@agent-bundle/runtime/dist/lineage.js","node_modules/agent-bundle/dist/667~1.js","node_modules/agent-bundle/dist/mcp-server-runtime.js","src/core/app-session.js","src/core/chatgpt-desktop/facade.ts","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/launch-env.js","node_modules/agent-bundle/dist/terminal-capability.js","src/core/app-session.js","src/core/chatgpt-desktop/cdp-adapter.ts","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/40.js","node_modules/agent-bundle/dist/667~1.js","src/core/claude-channel.js","src/core/relay/managed.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/claude-channel.js","src/core/codex-bridge.js","src/core/relay/control.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/49.js","node_modules/agent-bundle/dist/launch-env.js","src/core/app-session.js","src/core/claude-channel.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"module","issuers":["node_modules/agent-bundle/node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs"],"kind":"builtin","request":"node:process","userRequest":"node:process"},{"externalType":"module","issuers":["src/core/relay/control.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"},{"externalType":"module","issuers":[".agent-bundle-virtual/mcp-grok-bot-b8c2461e-1.mjs",".agent-bundle-virtual/mcp-grok-bot-b8c2461e-2.mjs","src/core/relay/managed.js"],"kind":"builtin","request":"node:url","userRequest":"node:url"},{"externalType":"module","issuers":["node_modules/agent-bundle/dist/mcp-server-runtime.js"],"kind":"builtin","request":"node:worker_threads","userRequest":"node:worker_threads"},{"externalType":"node-commonjs","issuers":["node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.development.js","node_modules/react-server-dom-rspack/cjs/react-server-dom-rspack-client.node.production.js"],"kind":"builtin","request":"util","userRequest":"util"}],"packages":["@agent-bundle/runtime","@modelcontextprotocol/core","@modelcontextprotocol/server","agent-bundle","effect","react","react-dom","react-server-dom-rspack","rsc-markdown-stream","zod"],"path":"mcp/mcp-grok-bot-b8c2461e.mjs","sha256":"44bb2d1883bb081e78d216914a6adcb54b5d2d3ddb811f52bb486dcee63c6fd7"},{"externals":[{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state.js"],"kind":"builtin","request":"node:assert/strict","userRequest":"node:assert/strict"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/736.js"],"kind":"builtin","request":"node:async_hooks","userRequest":"node:async_hooks"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/302.js"],"kind":"builtin","request":"node:buffer","userRequest":"node:buffer"},{"externalType":"module","issuers":["src/core/app-session.js","src/core/codex-bridge.js","src/core/desktop-shim.js"],"kind":"builtin","request":"node:child_process","userRequest":"node:child_process"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state/sqlite.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/gateway.js","src/core/relay/codex.js","src/core/relay/control.js","src/core/relay/engine.js","src/core/relay/profile.js","src/core/relay/records.js","src/core/relay/state.js","src/core/store.js"],"kind":"builtin","request":"node:crypto","userRequest":"node:crypto"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state/sqlite.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/relay/codex.js","src/core/store.js"],"kind":"builtin","request":"node:fs","userRequest":"node:fs"},{"externalType":"module","issuers":["src/core/relay/ownership.js","src/core/relay/state.js","src/core/relay/worker.js"],"kind":"builtin","request":"node:fs/promises","userRequest":"node:fs/promises"},{"externalType":"module","issuers":["src/core/codex-bridge.js","src/core/relay/control.js","src/core/relay/ownership.js","src/core/relay/worker.js"],"kind":"builtin","request":"node:net","userRequest":"node:net"},{"externalType":"module","issuers":["src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/relay/engine.js","src/core/relay/profile.js","src/core/store.js"],"kind":"builtin","request":"node:os","userRequest":"node:os"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state/sqlite.js","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/remote-control.js","src/core/desktop-shim.js","src/core/relay/engine.js","src/core/relay/ownership.js","src/core/relay/profile.js","src/core/relay/state.js","src/core/store.js"],"kind":"builtin","request":"node:path","userRequest":"node:path"},{"externalType":"import","issuers":["src/core/relay/ownership.js"],"kind":"builtin","request":"node:sqlite","userRequest":"node:sqlite"},{"externalType":"module","issuers":["node_modules/@agent-bundle/runtime/dist/state/sqlite.js"],"kind":"builtin","request":"node:sqlite","userRequest":"node:sqlite"},{"externalType":"module","issuers":["src/core/relay/control.js","src/core/relay/worker.js"],"kind":"builtin","request":"node:string_decoder","userRequest":"node:string_decoder"}],"packages":["@agent-bundle/runtime","effect","zod"],"path":"scripts/gbot-relay.mjs","sha256":"faf9fe6e3f82f9ae4a7ea61d04c2099c98057e325888e8769aa552b339c046cb"}],"coverage":{"rewritable":false,"unobserved":["import()","require()","require.resolve(…)","createRequire(…)(…)","import.meta.resolve(…)"]},"policy":{"name":"closed-world-externals","revision":1},"producer":{"name":"agent-bundle","rspack":"2.2.8","version":"0.3.1"}} diff --git a/artifact/agent-bundle.manifest.json b/artifact/agent-bundle.manifest.json index 0e0a6c6..042b022 100644 --- a/artifact/agent-bundle.manifest.json +++ b/artifact/agent-bundle.manifest.json @@ -1 +1 @@ -{"application":{"description":"Message Grok Bot from Codex, Claude Code, and Cursor. Codex/Claude tools use local sockets on the user's registered machines only (no remote transport); from the Grok Bot box, run gbot via Grok Bot Shell with a machineId.","id":"plugin:gbot","name":"gbot","version":"0.10.2"},"compiler":{"adapters":[{"adapterRevision":"1.28.0","host":"claude","observedVersion":"2.1.260","schemas":[{"name":"hooks","revision":"2.1.260","sha256":"0cdfc5eb5201f2c3091768559ac82f4c563ccb1b7bce7a39d5f99e5f404654cb"},{"name":"lsp","revision":"2.1.260","sha256":"b4419c5d857267c7e2b21e3e1eb98b4fdc302c87109007190d3258e6ba7096e4"},{"name":"marketplace","revision":"2.1.260","sha256":"31ee4cc43ba5ce2be248030a69da2d31171550b2cc51c1fcb9f788a8ab92783d"},{"name":"mcp","revision":"2.1.260","sha256":"edd4770e41d6aee5beae1ff918d33139613a243e454ecbd52b72fa824be4a662"},{"name":"monitors","revision":"2.1.260","sha256":"6378d94b51fb7c784eaee178237008a03da0f889d0520d57486784954a1d484c"},{"name":"plugin","revision":"2.1.260","sha256":"2a976091b81ad07ae8eca57f6f9c5749efeba17fa089a0c12b6ca84d6e70f118"},{"name":"settings","revision":"2.1.260","sha256":"2bbca553621dbf9433a9b7d1ff7952543a368434f2d999a4e4e360b95b6d4c3d"},{"name":"theme","revision":"2.1.260","sha256":"9931264e6f5a1d4b3b854ce7a17d602c9ba1b57cc1991f1d3991b316eaa81ac2"}]},{"adapterRevision":"1.13.0","host":"codex","observedVersion":"0.147.0","schemas":[{"name":"app","revision":"0.147.0","sha256":"01c720a645e437bf0c4f8c26fd4cb5a13988e5649e4a8562ee23a1d4b7355c6a"},{"name":"hooks","revision":"0.147.0","sha256":"175b859eb8e85bd287d85ee840d97c3f5c2d0dda3223507a796d158e3770eeba"},{"name":"marketplace","revision":"0.147.0","sha256":"fbccce3ade39e1b077fcb440e60260de6e811e8c2d222b2ae0fe8fe47706b470"},{"name":"mcp","revision":"0.147.0","sha256":"75bd50f9fcb85c2e8d43bc132d61c172a02f28ea8bb77389816ae77b14a4257e"},{"name":"plugin","revision":"0.147.0","sha256":"074c6c71966a3e6560ccbceb8d82ec6a40cb1eccee2f2d863fb4ef1e2276a814"}]},{"adapterRevision":"1.13.0","host":"cursor","observedVersion":"2026-08-28","schemas":[{"name":"hooks","revision":"2026-08-28","sha256":"06154b7afa0861df462130b988912b897e7ccf962b8dd20c09193100bcde5d81"},{"name":"marketplace","revision":"2026-08-28","sha256":"1aae96a24c2796419933bc8bfe3a1255394e7199c35740b36325e0ce6dbc253d"},{"name":"mcp","revision":"2026-08-28","sha256":"f3fa4615afefe004c4fbcc09e635d890df0f1ec0cb39540feab72cbd3a31d844"},{"name":"plugin","revision":"2026-08-28","sha256":"a393b758901803fcf5cfe0d77bda8a83e987d32c3377dfce2d9edf445af884ed"}]},{"adapterRevision":"1.10.0","host":"portable","observedVersion":"1.0.0","schemas":[{"name":"mcp","revision":"1.0.0","sha256":"6539175bfcdf43085855183e86da40ea94b166547a72b47ae9a0a390516d3acb"},{"name":"plugin","revision":"1.0.0","sha256":"0a4aad95ce337878ad38802ebf0daa3fde76abe3f65400c86bcbb1ec0b3ab883"}]}],"agentSkills":{"schemaSha256":"6d06b61e423317421de2295ea1fd0c7b4491bfa36c5b7705c28616dfe76d4047","sourceRevision":"69ef37e9424c0a7ea9dd2293b559e43ec8176379","specification":"https://raw.githubusercontent.com/agentskills/agentskills/69ef37e9424c0a7ea9dd2293b559e43ec8176379/docs/specification.mdx"},"producer":{"name":"agent-bundle","version":"0.3.1"},"project":{"configDigest":"7718be28a54eb7ec9e28b9653ef671683604436af21e1eda99c11098f43e278f","configPath":"agent-bundle.config.ts","modelDigest":"f865870d42a142c8638ec85438bd5d1507438ca432666f45b0f318fde99a7855","packageName":"grok-bot-cli","packageVersion":"0.10.2","revision":"b6545a92c9126fec30105693e1311f25a6ecc5415bcf79de59206bc0d4caf773","sourceInputs":[{"executable":false,"path":".changeset/config.json","sha256":"0a97ce5d0488bada887ee9ea3726edecc727454b7da0294f1b5fe4947f48ee7c"},{"executable":false,"path":".changeset/README.md","sha256":"495bd4805f8362853bce99689c1cd75db8c475dfa38930619d2e197d7554db27"},{"executable":false,"path":".github/workflows/changeset.yml","sha256":"0139beb8a3dc0e95126c9701d09251a879331b1e8c5540bbfc69848525b40f23"},{"executable":false,"path":".github/workflows/ci.yml","sha256":"17a58cf5ceff03b7922d15c9b0eea11ae3e0fd00a581ff6881f69cf795f3ec0b"},{"executable":false,"path":".github/workflows/release.yml","sha256":"9c97360a2221fca9317edcac0eb343298c765e3b34bd048f916c47533dd90311"},{"executable":false,"path":".gitignore","sha256":"3e8fdb390e0a40ebc680e14f015cfc607d92061f174dcea0c559ecad0da5058e"},{"executable":false,"path":".npmrc","sha256":"da47110e4c2d7cd216cd495b604ca78bd0f5caaca32da4837ec0efa6f967bba0"},{"executable":false,"path":"agent-bundle.config.ts","sha256":"7718be28a54eb7ec9e28b9653ef671683604436af21e1eda99c11098f43e278f"},{"executable":false,"path":"AGENTS.md","sha256":"e18ce90a031c533e7226789b97f6bce4aa7994b672544e0f425d20cc3bc15d29"},{"executable":false,"path":"CHANGELOG.md","sha256":"49d0b5b880153fa7be882f31c8edf73cf04fe0a5f83e2bb880a48ccda495044d"},{"executable":false,"path":"demo/grok-bot-cli-demo.gif","sha256":"0fe01e15fd2abbbc5cc3313e42931d404d3a320764e22fdcfb995f1a45dd73cd"},{"executable":false,"path":"demo/grok-bot-cli-demo.mp4","sha256":"b6ed66e43616674096c2f0a4a4da46fe4693c681833eab013f872a66b9ff933c"},{"executable":false,"path":"docs/codex-busy-threads.md","sha256":"42509e8e4c2b519949f06d4e7028b8973d56f8fc0cd98decdb231375bb49d38d"},{"executable":false,"path":"docs/codex-duplex-architecture.md","sha256":"792f1be8bad508b042c5813c1fc3b7ea5ac87032803227830666727306e55fa2"},{"executable":false,"path":"docs/verification/2026-09-15-duplex.md","sha256":"c7d97f709a3c2d851c0a24690321bbda161ec925b4143a456b9951e174bf75df"},{"executable":false,"path":"LICENSE","sha256":"bc48c8a0f6b6e45842e574c79a6b955fe683ed13f48a50cc08bcccdb1a59fe59"},{"executable":false,"path":"package-lock.json","sha256":"fc92e6f62433580416ad0af5f6b7f83d422f111e5e0c2535f7934c54f9066953"},{"executable":false,"path":"package.json","sha256":"4894d2ae20d895b1b213c0bd7eb7a6c483a7ae632e49ffeaa4eb7564bc71abde"},{"executable":false,"path":"README.md","sha256":"621de240b10dfc913fc0c0cd44c1291c7c56415025ccec7d79255ee75faa20ad"},{"executable":false,"path":"renovate.json","sha256":"e80b4e42a3043bc12fa0640db4bac392d2bf770acf841360d7c8ceeeac2ec1a9"},{"executable":false,"path":"rstest.route-unit.config.ts","sha256":"7286e20dc3d4162673e7c3dfe824db00307e1fda178410f23044a97286d141f2"},{"executable":false,"path":"scripts/run-unit-tests.mjs","sha256":"483f91b4856ce294a483dd2e62ca53bf09301adbc4c5fc9571da0e1d28049bc6"},{"executable":false,"path":"src/cli/_shared.ts","sha256":"555210759d9b2e2e60a01452c137abd73bad42bfab8b2311b66a3ab97f4ee5e9"},{"executable":false,"path":"src/cli/approvals/list.tsx","sha256":"8d21320b6cd9d6b2a266fd30196b8a439960f865a6f0a5dacf5845d64ed7388c"},{"executable":false,"path":"src/cli/approvals/respond.tsx","sha256":"7d9fe6e641deb08f444684733daac6da47fb472d90ed0e9ff77892f376a87255"},{"executable":false,"path":"src/cli/bots/create.tsx","sha256":"9f7a73a0595b642dd77f008ad596bcafd00fc5846ec1abff2ce62819d983eb84"},{"executable":false,"path":"src/cli/bots/delete.tsx","sha256":"126b53ac1da4a657f22d1a6770ddbab2cc66715c811de0873d50016bbe39cbf2"},{"executable":false,"path":"src/cli/bots/get.tsx","sha256":"f0c752a3b498819083a8ee5aa918d275727f1cc94d8774d8c84ac74db66e8a71"},{"executable":false,"path":"src/cli/bots/list.tsx","sha256":"21c0f7e39005860dfc2514b71f745bb4cab29d409ef2cff26f8759bb0fd261c1"},{"executable":false,"path":"src/cli/bots/update.tsx","sha256":"33b7f5aafbf931cf3cb5ecf423a76dacd09adb117a7d5490050639ae7f4210dd"},{"executable":false,"path":"src/cli/claude/send.tsx","sha256":"d650caadec248aa59530d124ca6186c45d9f9fe1fb0f9c805e9eef6dc9f06d92"},{"executable":false,"path":"src/cli/codex/bridge/respond.tsx","sha256":"9d3a6af4904af6e44583dfb4ef317b3fe1045a78e765fc27406eaca9bcadcd13"},{"executable":false,"path":"src/cli/codex/bridge/run.tsx","sha256":"a49d334bebb00db62ad2c54370a47ec2c9119b4c07eb6ba11fc4bba4dd8e4217"},{"executable":false,"path":"src/cli/codex/bridge/start.tsx","sha256":"9e423cf5d65168606f6235baa2bcd127eb949647f7a4dee284d75f831474524d"},{"executable":false,"path":"src/cli/codex/bridge/status.tsx","sha256":"c801f69080985c87025429fb136487acdfac093acb07241b3417ea9a83915a18"},{"executable":false,"path":"src/cli/codex/bridge/stop.tsx","sha256":"6710e298a21d124c442d5aa1a3744280e5cd4df93213797f2ea3c0654174184a"},{"executable":false,"path":"src/cli/codex/desktop-shim.tsx","sha256":"fa3d88d11895bc4a15700bd590c055aa392b2b0e0c1185c52da233a23589aba0"},{"executable":false,"path":"src/cli/codex/list-threads.tsx","sha256":"4419b29c5bc56f019e2835e3c8dbf4bc72291150900cfe2b8781a94c1482fe0f"},{"executable":false,"path":"src/cli/codex/new.tsx","sha256":"11d0658c1a3e7173d53b71446b184fccccd867baefc6f733e43a9d543ca26ec9"},{"executable":false,"path":"src/cli/codex/queue.tsx","sha256":"7993ac5b827042cd95041775b183fde2d112b5ba7d5c82098e778a44e473826d"},{"executable":false,"path":"src/cli/codex/send.tsx","sha256":"a1b30f053a9e65bac561cd04d1361e292e2c672bbf69a6d76d2bdda441c46757"},{"executable":false,"path":"src/cli/codex/status.tsx","sha256":"b8c31001cb2892c1bfccf502f9776ee06998eac4fc0356f92936fa2c34f272d8"},{"executable":false,"path":"src/cli/codex/wait.tsx","sha256":"0a4cc6c440ad496682031658bda375fee29f5c346d833618d4f143de29b351cd"},{"executable":false,"path":"src/cli/codex/watch.tsx","sha256":"3280f42d15941cfd00b3727d9e271972c39165bd28db8f56acca5664d777cb4d"},{"executable":false,"path":"src/cli/doctor.tsx","sha256":"c30ce4e57fe707d36de29700abf5a2c56834bb4fb3adf2a70bb1089b4150ddc8"},{"executable":false,"path":"src/cli/groups/add.tsx","sha256":"2a5de115852f0ca6a6638300bd5006862a02a2f1e2f527b6fd680f610c1d777a"},{"executable":false,"path":"src/cli/groups/create.tsx","sha256":"da3f2539f338063daf51b80d86c27b2752f0e4a0334078d11508e27981590fa3"},{"executable":false,"path":"src/cli/groups/delete.tsx","sha256":"2d4e91113a2c24cefefcfd464513918791592b5e8734a4ef901eabe42c9c7289"},{"executable":false,"path":"src/cli/groups/get.tsx","sha256":"efed88fb3ab7292d60d6532f1f966f784ba3376b7826c9f3e5028467f86241e9"},{"executable":false,"path":"src/cli/groups/list.tsx","sha256":"f3cd229d41dd389b767447a04267700ab2293eae87a878f7d88149cb5798f4a6"},{"executable":false,"path":"src/cli/groups/members.tsx","sha256":"9b9eaecb65ea995006eff1b24a287110d16bf4830a0b0d07221c2f1317853641"},{"executable":false,"path":"src/cli/groups/remove.tsx","sha256":"96dd85d1b9732681ce9dc126ac3ae562d3f287f4c28e36b9a8f8c301dc93f63d"},{"executable":false,"path":"src/cli/groups/set.tsx","sha256":"4f9d90db612f14ddbb6d2afa5e45b66f044aed661ff95415e567e1be3b6108c6"},{"executable":false,"path":"src/cli/groups/update.tsx","sha256":"f44305815ddb6e27286f97ef68c04cfa5548acd5f04b1aba10ee7ce3cdbfc851"},{"executable":false,"path":"src/cli/history.tsx","sha256":"5151b48113b4ba0516bacabfabcd48e3256ead807c34d914deb84f31ded0b062"},{"executable":false,"path":"src/cli/send.tsx","sha256":"fd16790cfcde1c15328c3979acc0664112d5cf40d3f00a24da906e00dd63153e"},{"executable":false,"path":"src/cli/skills/add.tsx","sha256":"fbde237d172c026d7912174e705c03cbc9d74dad7806fc58c6ceb43127c69ba6"},{"executable":false,"path":"src/cli/skills/list.tsx","sha256":"34f255687c69aaf598a1dc54220453b57c46ed9ab4a168648d9c7eca6535ebae"},{"executable":false,"path":"src/cli/skills/remove.tsx","sha256":"21662c80a62f276c2dfd840a328e9665c0c5f63959a239d4dd877372c46f60d0"},{"executable":false,"path":"src/cli/thread.tsx","sha256":"29ab775936fa2987cb789754a44888fc52bdaad07180c5471122dda508cfd0c2"},{"executable":false,"path":"src/commands/codex-send.md","sha256":"ef29096075a13a28be9c2ee7a6226f169ffd330a7f21698b3927273070a6086d"},{"executable":false,"path":"src/commands/codex-threads.md","sha256":"bacac58bb4e61766ea76f9abad71c25207192bc609ce3bf3f122686c00d674a4"},{"executable":false,"path":"src/commands/codex-wait.md","sha256":"ef224414fe7a9f2aaaae2fba8b83f0600cb3589dfc20920a69b29db3a5f2d880"},{"executable":false,"path":"src/core/app-session.js","sha256":"2b2eb53ec3f4c7ea0dccf269fa9720af6f1ce36b8ca36e889d2bb271a092717d"},{"executable":false,"path":"src/core/claude-channel.js","sha256":"01ac7daef748a11bc99e132950ed7c59f50442e417eecf894b59cb4bb484202f"},{"executable":false,"path":"src/core/claude-routes.ts","sha256":"d34ddf25b8634ef86d46ab663f30a49aaedf87f8dc98e4036f84c1c88efa4083"},{"executable":false,"path":"src/core/codex-bridge.js","sha256":"01f4ba71251348cdfb41b19244d0b3df2d2c9818fa31fc6186bc8cc1a0ea518b"},{"executable":false,"path":"src/core/codex/contract.js","sha256":"96e80f91c323df150d08a477abe6697b287cd7a1b11e98d17cb15f682fa46012"},{"executable":false,"path":"src/core/codex/conversation.js","sha256":"49836864594322fd948ae4374ae0cee12d3d694a03b7cecbd505004bd12879ca"},{"executable":false,"path":"src/core/codex/routes.ts","sha256":"82691c8644e75f4a1762be6a29e6a8d8089f58cad2cfc6987a9659475f89785f"},{"executable":false,"path":"src/core/commands.js","sha256":"3ad8fafb86744a57992da93ad838ade0e9acb1b7ca09b829cefadbf2781be0c9"},{"executable":false,"path":"src/core/desktop-shim-bridge.js","sha256":"58f3f39b8527adf58f57313e02e34322b203b8c38d1acfbad419a8b9af214443"},{"executable":false,"path":"src/core/desktop-shim.js","sha256":"500b8617bade6fa1a9c395826e197892fb864067044cac80ab025849f8fd6f82"},{"executable":false,"path":"src/core/format.js","sha256":"83ee15f73a87ef24b75f7782f3b61dd4ef4cc1f2e01e38cdf19825b805fab3be"},{"executable":false,"path":"src/core/gateway.js","sha256":"3a4f838ee423600d29e05f58060dcaf81cdcea4ac93b36dd6108363ac24bc606"},{"executable":false,"path":"src/core/grok-approval-routes.ts","sha256":"a33daa4d05e0c2594c795bd7654c22f3e1003e4aaad41cd70034d03651dd01f9"},{"executable":false,"path":"src/core/grok-approvals.js","sha256":"6b651a65bdd17241dd5b7a01d94a318ece9356c1c9b965b0578e08250d74e8fe"},{"executable":false,"path":"src/core/headers.js","sha256":"e229f2826ebdfb52949dab500798c0fa8a5ff7c4e63de91a63103bf45d2ae3cc"},{"executable":false,"path":"src/core/history.js","sha256":"38ca8e8f8dcd2ad42952c424ea9f00be5972d8751e4c0ac87214171903d82d89"},{"executable":false,"path":"src/core/relay/codex.js","sha256":"ff6e8f3158d171911a35b829cd76e1f5fd8319f8143f727a4854448fc09edeec"},{"executable":false,"path":"src/core/relay/completion.js","sha256":"9d1a47f92716d1ccc7c4f44d92a580084f3b622c6ac0f20c6391be81abeb56e4"},{"executable":false,"path":"src/core/relay/control.js","sha256":"3645540f9f799b8742e0d5782b6324454a1486a780f6cb97260b96ef62411771"},{"executable":false,"path":"src/core/relay/engine.js","sha256":"205dd43b337439a56c7b1521b2603679ebc7fd3b2da430f8a477f83549efd453"},{"executable":false,"path":"src/core/relay/intake.js","sha256":"b31146fe3c9edd2f09323a454687403666591b6c12483a55b07ac610d2e3610a"},{"executable":false,"path":"src/core/relay/interactions.js","sha256":"6b576ed3a5b6659bc17e384d26e40d6ccb41b118b3a91cf5c902e9758ccbc1b1"},{"executable":false,"path":"src/core/relay/managed.js","sha256":"dbe03ed964bd09739e580de73b7e9f90508062fa2e53b8244c6ca7f78fc7a60d"},{"executable":false,"path":"src/core/relay/ownership.js","sha256":"74805f04f6a8bc02b2951614c4814aa42f54aaa3abca027a2413ed5a54814caf"},{"executable":false,"path":"src/core/relay/profile.js","sha256":"3347d1d493d3470d82722468d31571464d8e11aa0b4deac5bda75f574078dfe5"},{"executable":false,"path":"src/core/relay/records.js","sha256":"5b80f7dfe7e438811d9f27c1f9038d7631f8c73266d2ec7b1108c1ce9ecbbb47"},{"executable":false,"path":"src/core/relay/routes.ts","sha256":"4007e9908e285036786b7b0291dd480d03d51ad03f973fa578444accffa1f416"},{"executable":false,"path":"src/core/relay/state.js","sha256":"c18eda2f956944d0f318910de5239b954090eab81f7c77b4683e326a6d34b2b8"},{"executable":false,"path":"src/core/relay/worker.js","sha256":"4176ad91872f36f7ea6565d9f30d23d9d7a80ef27d7af975af760457132ba148"},{"executable":false,"path":"src/core/store.js","sha256":"caefeeebf686cecd37ef19585ef0a5d3acba9dbf1564fe128c3afe4de067305c"},{"executable":false,"path":"src/core/transcript.js","sha256":"0355f0978d2d126b90858e3f661130243fc4730ee81ba13080af1dfa39d8b081"},{"executable":false,"path":"src/core/url-policy.js","sha256":"62bf6d3c8af1f5840d5e2d682d8f2e52fcbfbee6695728b47ed31816f1736089"},{"executable":false,"path":"src/core/user-machine-guidance.js","sha256":"c8409ae9679246d8f907f1e0d16ec1c415d6697f2ce048fb64a599734486b9a6"},{"executable":false,"path":"src/gbot-install.ts","sha256":"e3c833d7662610e70e65232e541017229fb9a049eacee0bdf31e2c33c6ab7189"},{"executable":false,"path":"src/gbot.ts","sha256":"c89a37b764956ceacb2917a991a6d82d987ed8d8e453c8d553485bbd7f3edcc0"},{"executable":false,"path":"src/mcp/claude-channel.ts","sha256":"2d52e0d862a492026b6cb408e3177c994996e2b4292903da5bbf5bc191019913"},{"executable":false,"path":"src/mcp/grok-bot/tools/claude_send.tsx","sha256":"c02924049f370d9cb5ae459d6275c6cae2c9f4f1328cb5fa8e2f3e6d12a6d656"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_new.tsx","sha256":"556c8d6f5f2b07b20e9daae7aa8008ee3775582a4d034415fc86fecda0c63866"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_send.tsx","sha256":"5c02dc335a0354850d05e5827f6124d0ddc08b9b654059984887c55e4cd778c9"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_threads.tsx","sha256":"2585ca95ded13c334b95ab969054eb72353475b224a181fe710f7cb591ef939b"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_wait.tsx","sha256":"cb08b094860f3a09c937eb8339108015fe0de57d9f6c6886aea2028cd33ef207"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_watch.tsx","sha256":"1bc603d36ea7542468e7e3eadbb86072990282e09e209f13ebfc64389d207c49"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_bridge_start.tsx","sha256":"c75c5c8a30ec43709a4d32baa8c3bc9796595381bb04fd2499ca30cd515c5a0b"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_bridge_status.tsx","sha256":"486ce643b35cfa2eabf78f0deeabce7198adc66133669eec630e1d12206ad642"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_bridge_stop.tsx","sha256":"371428a6e4b4e1312dd95d9681c29e765aec55e65d80d7c474535df838a43457"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_codex_respond.tsx","sha256":"030e9c2081ed4ed40732b098c929022af3083f609f10934b35e1f428d33ae637"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_grok_approvals.tsx","sha256":"17f164acdc48d6bc79a4c5044c7082760dd2a1646e5b56f6bff54c53fed96451"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_grok_respond.tsx","sha256":"41964bb8cf0eb0f8493afa1dde16cd94c4f589d1a7e410577d1ed63ec4648542"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_send.tsx","sha256":"2101d279a0e6bda2d57ba3da5366411df460a95692d4e64a1096880a89121d9a"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_thread.tsx","sha256":"ef4aefec7243efaad18335e8508c7c17af0cd9b0f8a4b3693a7af9f666807fd6"},{"executable":false,"path":"src/rules/codex-claude-on-user-machines.mdc","sha256":"a50ec93d021a807e0a18bf9311cdf103bc0ae3947e4c6a0bd78cbb941e8e5b24"},{"executable":false,"path":"src/scripts/gbot-relay.ts","sha256":"6690e316189ed8ebba43ea67a495338c927e4c0c21a9f896b7b2fecc8c6eb656"},{"executable":false,"path":"src/skills/talk-to-grok-bot/references/bridge-administration.md","sha256":"3fc7d335822d9021cbd3770af6d7e95734d9213cd7331f41602598911c15ddf6"},{"executable":false,"path":"src/skills/talk-to-grok-bot/SKILL.md","sha256":"b80a88ea029564447a79dbc1a533191dc4839791368c6248c904bcfaf3ccb770"},{"executable":false,"path":"test/app-session.test.js","sha256":"0756b691703dd8c84bf0823f2afab16c6b158398e4ce6be229db6f459a0bbdb1"},{"executable":false,"path":"test/claude-channel.test.js","sha256":"c63337698001b9739414203d70b6d54d0794ec5970bd90d327923f50ab2355dc"},{"executable":false,"path":"test/codex-bridge.test.js","sha256":"7671a03e5f290c07d4614ed4a0f6683bd85e2fd8361c819ddd06813aec63466d"},{"executable":false,"path":"test/codex-commands.test.js","sha256":"ab15a53a76eb82dc9c51c9fabd0affd2a1037cde27765fef08ecd0012a2e4495"},{"executable":false,"path":"test/codex-contract.test.js","sha256":"926c7609679d8b6ac3b568d56ba5efa20df86d016a9031f085d19adbfb0e9064"},{"executable":false,"path":"test/codex-conversation.test.js","sha256":"789d0245a8d470860f7535c6ef3849313205097f6df517ab9946e47a789f8534"},{"executable":false,"path":"test/codex-session.test.js","sha256":"b39fe6afe0784b16e37b208089279dea01095d33e06ae26a1b60871796c7738a"},{"executable":false,"path":"test/codex-surfaces.test.js","sha256":"56d1b6550cc69d462a05e892cf82aa6a41495857b753215104e03207db514c81"},{"executable":false,"path":"test/connect-gateway.test.js","sha256":"5fabd87640b39e75c218300ca6be278268f248f3ff75e8cb72f9fade9eb0f7f3"},{"executable":false,"path":"test/desktop-shim.test.js","sha256":"89fb6c38a5fca5d41a96434cf9e8d51ae1d9992cf4f7930df0cc5840ef985705"},{"executable":false,"path":"test/doctor.test.js","sha256":"9685885df9e7363d3e077d246b1a17803c8899e9613b4a4d52ee4b9275e20694"},{"executable":false,"path":"test/gateway-groups.test.js","sha256":"14e227453e3e28c7cb60f91dba55733834c023ce9a914e40e22c68c899512bdc"},{"executable":false,"path":"test/gateway-send.test.js","sha256":"d059166153dde975f457e4e8c32ea08b0fc857b58fd48eab7da1959638e4fb05"},{"executable":false,"path":"test/gateway-skills.test.js","sha256":"feed62a884d1b7d12e358a27d73fb5587bcf761d4867ab1755d8bc34a4380069"},{"executable":false,"path":"test/gateway.test.js","sha256":"e5a7acd7ae347c82ef1e2a6000562767168e56d6f9d86aca8da46141731d1309"},{"executable":false,"path":"test/grok-approvals.test.js","sha256":"5ad4c548060a08dbe57949ab5ae240383fdb1709cd3694a909f5e1354fee3f73"},{"executable":false,"path":"test/helpers/codex-server.js","sha256":"432b4570601e94cf08c4861133d328404a97066642edb7ec0dbc314411a4d848"},{"executable":false,"path":"test/history.test.js","sha256":"eac4811f1b12d427df66996f33dd46bc3cbe31fc9fa030b3f32c571d5379f8dd"},{"executable":false,"path":"test/host-tool-inventory.test.js","sha256":"8bedb7772de24b8580eaeace9ff27480e1bb8d4ebddd2932b4d0dea149bf798f"},{"executable":false,"path":"test/pr69-integration.test.js","sha256":"a5dc99c11e2d3fcf18233c595f9c28301e5939f4b3657125eb3f970df2598840"},{"executable":false,"path":"test/relay-auth-recovery.test.js","sha256":"399c064f550fe33e166187a51da527728a97946d213377c8fcc8231378f5bbab"},{"executable":false,"path":"test/relay-codex.test.js","sha256":"43cb313e748e187f798a8a0bce5a01cce9ba360a52985b05ecededee9ad10581"},{"executable":false,"path":"test/relay-completion.test.js","sha256":"a8cc2402f82ce4b458074eafd32c8fa18f7ced7d85fdb6b7fa68f7b18b6a8c2e"},{"executable":false,"path":"test/relay-engine.test.js","sha256":"a8c06138d895ffaa30476f3a2ddf1ef50ef73a816a665f749f451d01d9cc2551"},{"executable":false,"path":"test/relay-gateway-lifecycle.test.js","sha256":"e16e7a34f7b6bb07965003e3018aa3a43ff192602db1c177bec9b6268fddb78b"},{"executable":false,"path":"test/relay-interactions.test.js","sha256":"83d70b1a813923fc6bae02b18d328107d0d04a6a761ab73b2051e3d114c7eaa5"},{"executable":false,"path":"test/relay-lifecycle.test.js","sha256":"e90130e1f609839bfd3f74c3edc333a22f37fe20dcde0146daa8d497133da4b7"},{"executable":false,"path":"test/relay-state.test.js","sha256":"c9143d5fa150b1ebcef9d515e161e25af3370b71eaf800a7b2bb1da1678b0bd3"},{"executable":false,"path":"test/relay-surfaces.test.js","sha256":"ba17ac4f1ffc02d4e06cd73eda143422a3d4d2f750630af9ed4e828dc9d7d2be"},{"executable":false,"path":"test/relay-worker.test.js","sha256":"6bd1bdaaaae59997132f289f822b6c4b8c9616c268fb2b45861e03f697c08197"},{"executable":false,"path":"test/release-config.test.js","sha256":"f3078277086d722f21d07e4134e10b89d2e849a94a0fa40f30025a5cb6fa6a15"},{"executable":false,"path":"test/store.test.js","sha256":"5d4ae4295287539ef4b7f98d0ca0cd42a931a3c7b25b092fbd2b8f1a08ca4bac"},{"executable":false,"path":"test/test-runner.test.js","sha256":"966ff3b67edb8e1f03b50120d4d820ab0f871121e9b359609e73faae8446454f"},{"executable":false,"path":"test/transcript.test.js","sha256":"28795d9cc8e42653ac430d18e17ff1e6bc2feb1b7935fe5c69b336d58e1793ae"},{"executable":false,"path":"test/url-policy.test.js","sha256":"dacfe322e0cbff178c0d67a5f76b6bd8d9fef14aa57c09c697eb69fa0019629c"},{"executable":false,"path":"tests/route-unit/tools.test.ts","sha256":"a57ff0ca43dbcbdd0be9eb2b9261164ea96183007a1cd4e89683fbfbc3d1ae18"},{"executable":false,"path":"tsconfig.json","sha256":"fa2ced6d1721e8280a6aed615717cffcef25f6e2d16e15889b2be2a9df4fc226"}]},"provenance":[{"path":".agents/plugins/marketplace.json","sourceInputs":["agent-bundle.config.ts"]},{"path":".claude-plugin/marketplace.json","sourceInputs":["agent-bundle.config.ts","package.json"]},{"path":".claude-plugin/plugin.json","sourceInputs":["agent-bundle.config.ts","src/mcp/claude-channel.ts","src/mcp/grok-bot/tools/claude_send.tsx","src/skills/talk-to-grok-bot/SKILL.md"]},{"path":".codex-plugin/mcp.json","sourceInputs":["agent-bundle.config.ts","src/mcp/grok-bot/tools/claude_send.tsx"]},{"path":".codex-plugin/plugin.json","sourceInputs":["agent-bundle.config.ts","package.json","src/mcp/grok-bot/tools/claude_send.tsx","src/skills/talk-to-grok-bot/SKILL.md"]},{"path":".cursor-plugin/marketplace.json","sourceInputs":["agent-bundle.config.ts"]},{"path":".cursor-plugin/mcp.json","sourceInputs":["agent-bundle.config.ts","src/mcp/grok-bot/tools/claude_send.tsx"]},{"path":".cursor-plugin/plugin.json","sourceInputs":["agent-bundle.config.ts","package.json","src/commands/codex-send.md","src/commands/codex-threads.md","src/commands/codex-wait.md","src/mcp/grok-bot/tools/claude_send.tsx","src/rules/codex-claude-on-user-machines.mdc","src/skills/talk-to-grok-bot/SKILL.md"]},{"path":".mcp.json","sourceInputs":["agent-bundle.config.ts","src/mcp/claude-channel.ts","src/mcp/grok-bot/tools/claude_send.tsx"]},{"path":"agent-bundle.compile-evidence.json","sourceInputs":["agent-bundle.config.ts"]},{"path":"bin/gbot-flight.mjs","sourceInputs":["agent-bundle.config.ts","package.json","src/cli/_shared.ts","src/cli/approvals/list.tsx","src/cli/approvals/respond.tsx","src/cli/bots/create.tsx","src/cli/bots/delete.tsx","src/cli/bots/get.tsx","src/cli/bots/list.tsx","src/cli/bots/update.tsx","src/cli/claude/send.tsx","src/cli/codex/bridge/respond.tsx","src/cli/codex/bridge/run.tsx","src/cli/codex/bridge/start.tsx","src/cli/codex/bridge/status.tsx","src/cli/codex/bridge/stop.tsx","src/cli/codex/desktop-shim.tsx","src/cli/codex/list-threads.tsx","src/cli/codex/new.tsx","src/cli/codex/queue.tsx","src/cli/codex/send.tsx","src/cli/codex/status.tsx","src/cli/codex/wait.tsx","src/cli/codex/watch.tsx","src/cli/doctor.tsx","src/cli/groups/add.tsx","src/cli/groups/create.tsx","src/cli/groups/delete.tsx","src/cli/groups/get.tsx","src/cli/groups/list.tsx","src/cli/groups/members.tsx","src/cli/groups/remove.tsx","src/cli/groups/set.tsx","src/cli/groups/update.tsx","src/cli/history.tsx","src/cli/send.tsx","src/cli/skills/add.tsx","src/cli/skills/list.tsx","src/cli/skills/remove.tsx","src/cli/thread.tsx","src/core/app-session.js","src/core/claude-channel.js","src/core/claude-routes.ts","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/routes.ts","src/core/commands.js","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/format.js","src/core/gateway.js","src/core/grok-approval-routes.ts","src/core/grok-approvals.js","src/core/headers.js","src/core/history.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/relay/routes.ts","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/gbot.ts"]},{"path":"bin/gbot.mjs","sourceInputs":["agent-bundle.config.ts","package.json","src/cli/_shared.ts","src/cli/approvals/list.tsx","src/cli/approvals/respond.tsx","src/cli/bots/create.tsx","src/cli/bots/delete.tsx","src/cli/bots/get.tsx","src/cli/bots/list.tsx","src/cli/bots/update.tsx","src/cli/claude/send.tsx","src/cli/codex/bridge/respond.tsx","src/cli/codex/bridge/run.tsx","src/cli/codex/bridge/start.tsx","src/cli/codex/bridge/status.tsx","src/cli/codex/bridge/stop.tsx","src/cli/codex/desktop-shim.tsx","src/cli/codex/list-threads.tsx","src/cli/codex/new.tsx","src/cli/codex/queue.tsx","src/cli/codex/send.tsx","src/cli/codex/status.tsx","src/cli/codex/wait.tsx","src/cli/codex/watch.tsx","src/cli/doctor.tsx","src/cli/groups/add.tsx","src/cli/groups/create.tsx","src/cli/groups/delete.tsx","src/cli/groups/get.tsx","src/cli/groups/list.tsx","src/cli/groups/members.tsx","src/cli/groups/remove.tsx","src/cli/groups/set.tsx","src/cli/groups/update.tsx","src/cli/history.tsx","src/cli/send.tsx","src/cli/skills/add.tsx","src/cli/skills/list.tsx","src/cli/skills/remove.tsx","src/cli/thread.tsx","src/core/app-session.js","src/core/claude-channel.js","src/core/claude-routes.ts","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/routes.ts","src/core/commands.js","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/format.js","src/core/gateway.js","src/core/grok-approval-routes.ts","src/core/grok-approvals.js","src/core/headers.js","src/core/history.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/relay/routes.ts","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/gbot.ts"]},{"path":"commands/codex-send.md","sourceInputs":["src/commands/codex-send.md"]},{"path":"commands/codex-threads.md","sourceInputs":["src/commands/codex-threads.md"]},{"path":"commands/codex-wait.md","sourceInputs":["src/commands/codex-wait.md"]},{"path":"INSTALL.md","sourceInputs":["agent-bundle.config.ts"]},{"path":"install.mjs","sourceInputs":["agent-bundle.config.ts"]},{"path":"mcp.json","sourceInputs":["src/mcp/grok-bot/tools/claude_send.tsx"]},{"path":"mcp/mcp-claude-channel-8029413c.mjs","sourceInputs":["src/core/claude-channel.js","src/core/user-machine-guidance.js","src/mcp/claude-channel.ts"]},{"path":"mcp/mcp-grok-bot-b8c2461e-flight.mjs","sourceInputs":["package.json","src/core/app-session.js","src/core/claude-channel.js","src/core/claude-routes.ts","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/routes.ts","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/gateway.js","src/core/grok-approval-routes.ts","src/core/grok-approvals.js","src/core/headers.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/relay/routes.ts","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/gbot.ts","src/mcp/grok-bot/tools/claude_send.tsx","src/mcp/grok-bot/tools/codex_new.tsx","src/mcp/grok-bot/tools/codex_send.tsx","src/mcp/grok-bot/tools/codex_threads.tsx","src/mcp/grok-bot/tools/codex_wait.tsx","src/mcp/grok-bot/tools/codex_watch.tsx","src/mcp/grok-bot/tools/gbot_bridge_start.tsx","src/mcp/grok-bot/tools/gbot_bridge_status.tsx","src/mcp/grok-bot/tools/gbot_bridge_stop.tsx","src/mcp/grok-bot/tools/gbot_codex_respond.tsx","src/mcp/grok-bot/tools/gbot_grok_approvals.tsx","src/mcp/grok-bot/tools/gbot_grok_respond.tsx","src/mcp/grok-bot/tools/gbot_send.tsx","src/mcp/grok-bot/tools/gbot_thread.tsx"]},{"path":"mcp/mcp-grok-bot-b8c2461e.mjs","sourceInputs":["package.json","src/core/app-session.js","src/core/claude-channel.js","src/core/claude-routes.ts","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/routes.ts","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/gateway.js","src/core/grok-approval-routes.ts","src/core/grok-approvals.js","src/core/headers.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/relay/routes.ts","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/gbot.ts","src/mcp/grok-bot/tools/claude_send.tsx","src/mcp/grok-bot/tools/codex_new.tsx","src/mcp/grok-bot/tools/codex_send.tsx","src/mcp/grok-bot/tools/codex_threads.tsx","src/mcp/grok-bot/tools/codex_wait.tsx","src/mcp/grok-bot/tools/codex_watch.tsx","src/mcp/grok-bot/tools/gbot_bridge_start.tsx","src/mcp/grok-bot/tools/gbot_bridge_status.tsx","src/mcp/grok-bot/tools/gbot_bridge_stop.tsx","src/mcp/grok-bot/tools/gbot_codex_respond.tsx","src/mcp/grok-bot/tools/gbot_grok_approvals.tsx","src/mcp/grok-bot/tools/gbot_grok_respond.tsx","src/mcp/grok-bot/tools/gbot_send.tsx","src/mcp/grok-bot/tools/gbot_thread.tsx"]},{"path":"plugin.json","sourceInputs":["agent-bundle.config.ts","package.json"]},{"path":"rules/codex-claude-on-user-machines.mdc","sourceInputs":["src/rules/codex-claude-on-user-machines.mdc"]},{"path":"scripts/gbot-relay.mjs","sourceInputs":["package.json","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/gateway.js","src/core/grok-approvals.js","src/core/headers.js","src/core/relay/codex.js","src/core/relay/completion.js","src/core/relay/control.js","src/core/relay/engine.js","src/core/relay/intake.js","src/core/relay/interactions.js","src/core/relay/ownership.js","src/core/relay/profile.js","src/core/relay/records.js","src/core/relay/state.js","src/core/relay/worker.js","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/scripts/gbot-relay.ts"]},{"path":"skills/talk-to-grok-bot/references/bridge-administration.md","sourceInputs":["src/skills/talk-to-grok-bot/references/bridge-administration.md","src/skills/talk-to-grok-bot/SKILL.md"]},{"path":"skills/talk-to-grok-bot/SKILL.md","sourceInputs":["src/skills/talk-to-grok-bot/SKILL.md"]}],"recordVersion":1,"validation":{"artifact":{"status":"passed"},"projections":[{"host":"claude","status":"passed"},{"host":"codex","status":"passed"},{"host":"cursor","status":"passed"},{"host":"portable","status":"passed"}],"source":{"status":"passed"}}},"distribution":{"channels":["local","npm"],"install":{"instructions":"INSTALL.md","script":"install.mjs"},"payloads":[]},"executables":{"bins":[{"hosts":["claude","codex","cursor","portable"],"name":"gbot","path":"bin/gbot.mjs","worker":"bin/gbot-flight.mjs"}],"hooks":[],"mcpServers":[{"apps":[],"hosts":["claude"],"id":"mcp:claude-channel","kind":"compiled","launch":{"args":[],"entry":"mcp/mcp-claude-channel-8029413c.mjs","env":{}},"name":"claude-channel","transport":"stdio"},{"apps":[],"hosts":["claude","codex","cursor","portable"],"id":"mcp:grok-bot","kind":"compiled","launch":{"args":[],"entry":"mcp/mcp-grok-bot-b8c2461e.mjs","env":{},"worker":"mcp/mcp-grok-bot-b8c2461e-flight.mjs"},"name":"grok-bot","transport":"stdio"}],"scripts":[{"hosts":["claude","codex","cursor","portable"],"id":"script:gbot-relay","mode":"bundle","name":"gbot-relay","path":"scripts/gbot-relay.mjs"}]},"files":[{"bytes":225,"kind":"generated","path":".agents/plugins/marketplace.json","sha256":"54d575719e003eb71ba2d7fb7379e12098243e701646e0ca83dba38ef2a78b1d"},{"bytes":926,"kind":"generated","path":".claude-plugin/marketplace.json","sha256":"a8f5bf1f109d8f88b4195f3baea389e942af2f0ef544ecc22502326bcf3e742e"},{"bytes":339,"kind":"generated","path":".claude-plugin/plugin.json","sha256":"1420b97dfe289c97c24e855ba758439c94fa0e92aa38aae467312fbe341d5d15"},{"bytes":156,"kind":"generated","path":".codex-plugin/mcp.json","sha256":"4b442017d0d30ec207cef7a4e5d2cd9dca246080008633459a27e3a5f2f7935b"},{"bytes":1310,"kind":"generated","path":".codex-plugin/plugin.json","sha256":"a3ab79672b9ab74affae23c25ef06ddc926da90d0ff2499e9471c3e4c6ba475f"},{"bytes":332,"kind":"generated","path":".cursor-plugin/marketplace.json","sha256":"297ba8aebcdb8bcac814b520bf21d9d6acc9c8c94a1ecf3ca442a49d834aab59"},{"bytes":169,"kind":"generated","path":".cursor-plugin/mcp.json","sha256":"95c87d0f2ed3a4f6df7d0e519454b1234672eca030db3b8cd8776ef0f35dee74"},{"bytes":694,"kind":"generated","path":".cursor-plugin/plugin.json","sha256":"12321577920803f3d60bf071167f8b7888d94d720d06c5caa38f3b30c8bfda73"},{"bytes":363,"kind":"generated","path":".mcp.json","sha256":"5c56ac183c3d4fcbfa9386a73ab805495c6ab613b96bf71dbd5be80093e1a10b"},{"bytes":21751,"kind":"generated","path":"agent-bundle.compile-evidence.json","sha256":"35ac2ac3d7f412b81bbf5414125ebc6e6de49cf0904df563206a862a9e65c454"},{"bytes":1366663,"kind":"bundle","path":"bin/gbot-flight.mjs","sha256":"6512082aecad31bdef60eef2788e8f427aa542a5ece7dae9f2a4a560f071c682"},{"bytes":2221588,"kind":"bundle","mode":493,"path":"bin/gbot.mjs","sha256":"e66fbcc6432392f078d773bb5eb468c88ffc4f21d4b746cccd4d48ad206604fe"},{"bytes":290,"kind":"generated","path":"commands/codex-send.md","sha256":"6d188a21596115f4aa65e39718cede5eff837749c8652a12e2a5b25d8a3e0ef3"},{"bytes":195,"kind":"generated","path":"commands/codex-threads.md","sha256":"0db562fff831ede9dd300552aecc5b29d347c94ebeb3df9f488cb1a87b990cf8"},{"bytes":205,"kind":"generated","path":"commands/codex-wait.md","sha256":"fb04448a474b2f14ddb59739d5f79bcf251a3e1136ffe9b3e50ca5654aa6070b"},{"bytes":22762,"kind":"generated","path":"INSTALL.md","sha256":"5fdafbf8a3fa3d2205e6a308d37deb8ad31910927334a855f70e11977d43f6a9"},{"bytes":78560,"kind":"generated","path":"install.mjs","sha256":"d5c328ca82057cd05b90372a8cfa8ed3e8c6f7f1b14b1f579ae82250722c3d93"},{"bytes":246,"kind":"generated","path":"mcp.json","sha256":"c06a01fa851792f42e0cc807a444e454c3ea2fd164dc1c2adef455acc93879e2"},{"bytes":2270116,"kind":"bundle","path":"mcp/mcp-claude-channel-8029413c.mjs","sha256":"3a966160704621cd614ae9217051b2a681c0c913dd7507020c71c79f02161486"},{"bytes":1274365,"kind":"bundle","path":"mcp/mcp-grok-bot-b8c2461e-flight.mjs","sha256":"228cc31c20dad9ca169646101e127bc4fecf5bb941b8014753c66d1bf6f06452"},{"bytes":3099541,"kind":"bundle","path":"mcp/mcp-grok-bot-b8c2461e.mjs","sha256":"b51d83893aa2ce106a036de71a3649f880d16462a4b67f1362659e57f148563a"},{"bytes":681,"kind":"generated","path":"plugin.json","sha256":"862f9ff4c106a5b4c3bb09d498a2f395e3450074c8c79525c3a34aa67179d594"},{"bytes":953,"kind":"generated","path":"rules/codex-claude-on-user-machines.mdc","sha256":"a50ec93d021a807e0a18bf9311cdf103bc0ae3947e4c6a0bd78cbb941e8e5b24"},{"bytes":1287739,"kind":"bundle","path":"scripts/gbot-relay.mjs","sha256":"9445a1735c380cd065a513eb2f795e6f8a5c6623092d96447a1bcfd28f6fd333"},{"bytes":6007,"kind":"copy","path":"skills/talk-to-grok-bot/references/bridge-administration.md","sha256":"3fc7d335822d9021cbd3770af6d7e95734d9213cd7331f41602598911c15ddf6"},{"bytes":4067,"kind":"copy","path":"skills/talk-to-grok-bot/SKILL.md","sha256":"b80a88ea029564447a79dbc1a533191dc4839791368c6248c904bcfaf3ccb770"}],"manifestVersion":6,"projections":[{"builtInHost":"claude","documents":{"marketplace":".claude-plugin/marketplace.json","mcp":".mcp.json","plugin":".claude-plugin/plugin.json"},"host":"claude","marketplace":{"name":"gbot-marketplace"}},{"builtInHost":"codex","documents":{"marketplace":".agents/plugins/marketplace.json","mcp":".codex-plugin/mcp.json","plugin":".codex-plugin/plugin.json"},"host":"codex","marketplace":{"name":"gbot-marketplace"}},{"builtInHost":"cursor","documents":{"marketplace":".cursor-plugin/marketplace.json","mcp":".cursor-plugin/mcp.json","plugin":".cursor-plugin/plugin.json"},"host":"cursor","marketplace":{"name":"gbot-marketplace"}},{"builtInHost":"portable","documents":{"mcp":"mcp.json","plugin":"plugin.json"},"host":"portable"}],"routes":{"cli":{"commands":[{"aliases":[],"description":"List current Grok approval cards (latest 200 entries).","exitCode":"zero","options":[{"key":"target","kind":"string","option":"target","positional":0,"repeated":false,"required":true}],"path":["approvals","list"],"routeId":"cli:approvals/list"},{"aliases":[],"description":"Explicit user decision: accept a Grok request once or decline; never grant persistent permissions.","exitCode":"zero","options":[{"choices":["accept","decline"],"key":"decision","kind":"enum","option":"decision","repeated":false,"required":true},{"key":"entryId","kind":"string","option":"entry-id","repeated":false,"required":true},{"key":"requestId","kind":"string","option":"request-id","repeated":false,"required":true},{"key":"target","kind":"string","option":"target","repeated":false,"required":true}],"path":["approvals","respond"],"routeId":"cli:approvals/respond"},{"aliases":[],"description":"Create a Grok Bot bot.","exitCode":"zero","options":[{"key":"avatarColor","kind":"string","option":"avatar-color","repeated":false,"required":false},{"key":"avatarShape","kind":"string","option":"avatar-shape","repeated":false,"required":false},{"key":"description","kind":"string","option":"description","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"name","kind":"string","option":"name","repeated":false,"required":true},{"key":"title","kind":"string","option":"title","repeated":false,"required":false}],"path":["bots","create"],"routeId":"cli:bots/create"},{"aliases":[],"description":"Delete a bot or group by id or name.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Bot or group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["bots","delete"],"routeId":"cli:bots/delete"},{"aliases":[],"description":"Show one bot or group by id or name.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Bot or group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["bots","get"],"routeId":"cli:bots/get"},{"aliases":[],"description":"List Grok Bot bots (not groups).","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false}],"path":["bots","list"],"routeId":"cli:bots/list"},{"aliases":[],"description":"Update a bot or group profile.","exitCode":"zero","options":[{"key":"avatarColor","kind":"string","option":"avatar-color","repeated":false,"required":false},{"key":"avatarShape","kind":"string","option":"avatar-shape","repeated":false,"required":false},{"key":"description","kind":"string","option":"description","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"hidden","kind":"string","option":"hidden","repeated":false,"required":false},{"key":"name","kind":"string","option":"name","repeated":false,"required":false},{"key":"notify","kind":"string","option":"notify","repeated":false,"required":false},{"description":"Bot or group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true},{"key":"title","kind":"string","option":"title","repeated":false,"required":false}],"path":["bots","update"],"routeId":"cli:bots/update"},{"aliases":[],"description":"Send to an explicitly enabled live Claude Code channel and wait for its reply.","exitCode":"result","options":[{"key":"message","kind":"string","option":"message","positional":1,"repeated":false,"required":true},{"key":"name","kind":"string","option":"name","positional":0,"repeated":false,"required":true},{"key":"timeoutMs","kind":"number","option":"timeout-ms","repeated":false,"required":false}],"path":["claude","send"],"routeId":"cli:claude/send"},{"aliases":[],"description":"Managed Grok/Codex bridge respond.","exitCode":"result","options":[{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","key":"answersJson","kind":"string","option":"answers-json","repeated":false,"required":false},{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"choices":["accept","decline","cancel"],"key":"decision","kind":"enum","option":"decision","repeated":false,"required":false},{"key":"exchangeId","kind":"string","option":"exchange-id","repeated":false,"required":false},{"key":"generation","kind":"string","option":"generation","repeated":false,"required":true},{"key":"interactionId","kind":"string","option":"interaction-id","repeated":false,"required":true},{"key":"threadId","kind":"string","option":"thread-id","repeated":false,"required":true},{"key":"turnId","kind":"string","option":"turn-id","repeated":false,"required":true}],"path":["codex","bridge","respond"],"routeId":"cli:codex/bridge/respond"},{"aliases":[],"description":"Run a bounded foreground relay (up to 23 hours). Use the packaged gbot-relay.mjs script for unlimited service lifetime.","exitCode":"result","options":[{"description":"Foreground lifetime in milliseconds (100..82800000; default 23h).","key":"lifetimeMs","kind":"number","option":"lifetime-ms","repeated":false,"required":false}],"path":["codex","bridge","run"],"routeId":"cli:codex/bridge/run"},{"aliases":[],"description":"Managed Grok/Codex bridge start.","exitCode":"result","options":[{"choices":["steer","reject"],"key":"busyPolicy","kind":"enum","option":"busy-policy","repeated":false,"required":false},{"key":"codexThreadId","kind":"string","option":"codex-thread-id","repeated":false,"required":false},{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"key":"grokTarget","kind":"string","option":"grok-target","positional":0,"repeated":false,"required":true},{"key":"requestId","kind":"string","option":"request-id","repeated":false,"required":false}],"path":["codex","bridge","start"],"routeId":"cli:codex/bridge/start"},{"aliases":[],"description":"Managed Grok/Codex bridge status.","exitCode":"result","options":[{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"key":"limit","kind":"number","option":"limit","repeated":false,"required":false}],"path":["codex","bridge","status"],"routeId":"cli:codex/bridge/status"},{"aliases":[],"description":"Managed Grok/Codex bridge stop.","exitCode":"result","options":[{"key":"all","kind":"boolean","option":"all","repeated":false,"required":false},{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"key":"worker","kind":"boolean","option":"worker","repeated":false,"required":false}],"path":["codex","bridge","stop"],"routeId":"cli:codex/bridge/stop"},{"aliases":[],"description":"Install, remove, or check the ChatGPT Desktop shim: a CODEX_CLI_PATH wrapper that bridges Desktop stdio onto the managed Codex daemon (stock app-server proxy hangs). macOS persists via LaunchAgent; always fails open to real Codex.","exitCode":"result","options":[{"choices":["install","uninstall","status"],"description":"install | uninstall | status","key":"action","kind":"enum","option":"action","positional":0,"repeated":false,"required":true}],"path":["codex","desktop-shim"],"routeId":"cli:codex/desktop-shim"},{"aliases":[],"description":"List Codex daemon-managed threads.","exitCode":"result","options":[{"description":"Opaque pagination cursor (may start with -)","key":"cursor","kind":"string","option":"cursor","repeated":false,"required":false},{"description":"Max threads to list (1-200)","key":"limit","kind":"number","option":"limit","repeated":false,"required":false}],"path":["codex","list-threads"],"routeId":"cli:codex/list-threads"},{"aliases":[],"description":"Start a new Codex thread in --cwd, optionally with --model, --effort and a message.","exitCode":"result","options":[{"key":"cwd","kind":"string","option":"cwd","repeated":false,"required":true},{"choices":["none","minimal","low","medium","high","xhigh"],"key":"effort","kind":"enum","option":"effort","repeated":false,"required":false},{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"key":"message","kind":"string","option":"message","positional":0,"repeated":true,"required":false},{"key":"model","kind":"string","option":"model","repeated":false,"required":false}],"path":["codex","new"],"routeId":"cli:codex/new"},{"aliases":[],"description":"List the experimental Codex thread queue (GROK_BOT_CODEX_EXPERIMENTAL=1).","exitCode":"result","options":[{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true}],"path":["codex","queue"],"routeId":"cli:codex/queue"},{"aliases":[],"description":"Send to Codex; acceptance is distinct from completion. Options precede threadId.","exitCode":"result","options":[{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"key":"correlationId","kind":"string","option":"correlation-id","repeated":false,"required":false},{"choices":["none","minimal","low","medium","high","xhigh"],"key":"effort","kind":"enum","option":"effort","repeated":false,"required":false},{"key":"envelope","kind":"boolean","option":"envelope","repeated":false,"required":false},{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"description":"Required active-turn guard for steer; stale guards reject.","key":"expectedTurnId","kind":"string","option":"expected-turn-id","repeated":false,"required":false},{"key":"hop","kind":"number","option":"hop","repeated":false,"required":false},{"description":"Reply budget: 1-4194304 bytes.","key":"maxOutputBytes","kind":"number","option":"max-output-bytes","repeated":false,"required":false},{"key":"message","kind":"string","option":"message","positional":1,"repeated":true,"required":true},{"key":"model","kind":"string","option":"model","repeated":false,"required":false},{"key":"replyTo","kind":"string","option":"reply-to","repeated":false,"required":false},{"key":"replyToGrok","kind":"string","option":"reply-to-grok","repeated":false,"required":false},{"key":"requestId","kind":"string","option":"request-id","repeated":false,"required":false},{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true},{"description":"Observation timeout: 1-7200000 milliseconds.","key":"timeoutMs","kind":"number","option":"timeout-ms","repeated":false,"required":false},{"key":"wait","kind":"boolean","option":"wait","repeated":false,"required":false},{"choices":["reject","queue","steer"],"key":"whenBusy","kind":"enum","option":"when-busy","repeated":false,"required":false}],"path":["codex","send"],"routeId":"cli:codex/send"},{"aliases":[],"description":"Probe the local Codex app-server daemon. Exit 0 only when it is usable.","exitCode":"result","options":[],"path":["codex","status"],"routeId":"cli:codex/status"},{"aliases":[],"description":"Bounded Codex wait observation; never interrupts execution.","exitCode":"result","options":[{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"description":"Reply budget: 1-4194304 bytes.","key":"maxOutputBytes","kind":"number","option":"max-output-bytes","repeated":false,"required":false},{"key":"messageId","kind":"string","option":"message-id","repeated":false,"required":false},{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true},{"description":"Observation timeout: 1-7200000 milliseconds.","key":"timeoutMs","kind":"number","option":"timeout-ms","repeated":false,"required":false},{"key":"turnId","kind":"string","option":"turn-id","positional":1,"repeated":false,"required":true}],"path":["codex","wait"],"routeId":"cli:codex/wait"},{"aliases":[],"description":"Bounded Codex watch observation; never interrupts execution.","exitCode":"result","options":[{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"description":"Maximum observed events: 1-500.","key":"maxEvents","kind":"number","option":"max-events","repeated":false,"required":false},{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true},{"description":"Observation timeout: 1-600000 milliseconds.","key":"timeoutMs","kind":"number","option":"timeout-ms","repeated":false,"required":false}],"path":["codex","watch"],"routeId":"cli:codex/watch"},{"aliases":[],"description":"Show which agents root and auth sources gbot can see.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false}],"path":["doctor"],"routeId":"cli:doctor"},{"aliases":[],"description":"Add a bot to a group.","exitCode":"zero","options":[{"key":"bot","kind":"string","option":"bot","positional":1,"repeated":false,"required":true},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"group","kind":"string","option":"group","positional":0,"repeated":false,"required":true}],"path":["groups","add"],"routeId":"cli:groups/add"},{"aliases":[],"description":"Create a Grok Bot group.","exitCode":"zero","options":[{"key":"avatarColor","kind":"string","option":"avatar-color","repeated":false,"required":false},{"key":"avatarShape","kind":"string","option":"avatar-shape","repeated":false,"required":false},{"key":"description","kind":"string","option":"description","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Member bot id or name (repeatable)","key":"member","kind":"string","option":"member","repeated":true,"required":true},{"key":"name","kind":"string","option":"name","repeated":false,"required":true},{"key":"title","kind":"string","option":"title","repeated":false,"required":false}],"path":["groups","create"],"routeId":"cli:groups/create"},{"aliases":[],"description":"Delete a group by id or name.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["groups","delete"],"routeId":"cli:groups/delete"},{"aliases":[],"description":"Show one group by id or name.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["groups","get"],"routeId":"cli:groups/get"},{"aliases":[],"description":"List Grok Bot groups.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false}],"path":["groups","list"],"routeId":"cli:groups/list"},{"aliases":[],"description":"Show members of a group (same payload as groups get).","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["groups","members"],"routeId":"cli:groups/members"},{"aliases":[],"description":"Remove a bot from a group.","exitCode":"zero","options":[{"key":"bot","kind":"string","option":"bot","positional":1,"repeated":false,"required":true},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"group","kind":"string","option":"group","positional":0,"repeated":false,"required":true}],"path":["groups","remove"],"routeId":"cli:groups/remove"},{"aliases":[],"description":"Replace a group's member list.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"group","kind":"string","option":"group","positional":0,"repeated":false,"required":true},{"description":"Member bot id or name (repeatable)","key":"member","kind":"string","option":"member","repeated":true,"required":true}],"path":["groups","set"],"routeId":"cli:groups/set"},{"aliases":[],"description":"Update a group profile (members stay on set/add/remove).","exitCode":"zero","options":[{"key":"avatarColor","kind":"string","option":"avatar-color","repeated":false,"required":false},{"key":"avatarShape","kind":"string","option":"avatar-shape","repeated":false,"required":false},{"key":"description","kind":"string","option":"description","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"hidden","kind":"string","option":"hidden","repeated":false,"required":false},{"key":"name","kind":"string","option":"name","repeated":false,"required":false},{"key":"notify","kind":"string","option":"notify","repeated":false,"required":false},{"description":"Group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true},{"key":"title","kind":"string","option":"title","repeated":false,"required":false}],"path":["groups","update"],"routeId":"cli:groups/update"},{"aliases":[],"description":"Read the opt-in local JSONL history without contacting the gateway.","exitCode":"zero","options":[{"description":"Directory containing history.jsonl","key":"historyDir","kind":"string","option":"history-dir","repeated":false,"required":false},{"description":"Maximum matching rows (1 or more)","key":"limit","kind":"number","option":"limit","repeated":false,"required":false},{"description":"Print the history file path","key":"path","kind":"boolean","option":"path","repeated":false,"required":false},{"description":"Bot or group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":false},{"description":"Case-insensitive message text filter","key":"search","kind":"string","option":"search","repeated":false,"required":false}],"path":["history"],"routeId":"cli:history"},{"aliases":[],"description":"Send a message to a Grok Bot bot or group by name or id.","exitCode":"result","options":[{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"key":"codexThreadId","kind":"string","option":"codex-thread-id","repeated":false,"required":false},{"description":"Stable correlation id for multi-hop replies","key":"correlationId","kind":"string","option":"correlation-id","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Prepend the [gbot …] header","key":"envelope","kind":"boolean","option":"envelope","repeated":false,"required":false},{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Directory containing history.jsonl","key":"historyDir","kind":"string","option":"history-dir","repeated":false,"required":false},{"description":"Hop count; refused at GROK_BOT_MAX_HOPS","key":"hop","kind":"number","option":"hop","repeated":false,"required":false},{"key":"message","kind":"string","option":"message","positional":1,"repeated":true,"required":true},{"description":"Skip local history for this command","key":"noHistory","kind":"boolean","option":"no-history","repeated":false,"required":false},{"choices":["auto","manual"],"key":"replyMode","kind":"enum","option":"reply-mode","repeated":false,"required":false},{"description":"Prior message id this send replies to","key":"replyTo","kind":"string","option":"reply-to","repeated":false,"required":false},{"key":"requestId","kind":"string","option":"request-id","repeated":false,"required":false},{"key":"target","kind":"string","option":"target","positional":0,"repeated":false,"required":true}],"path":["send"],"routeId":"cli:send"},{"aliases":[],"description":"Add a SKILL.md to the shared skill library. Every bot sees it.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"SKILL.md file, or a directory holding one","key":"path","kind":"string","option":"path","positional":0,"repeated":false,"required":true}],"path":["skills","add"],"routeId":"cli:skills/add"},{"aliases":[],"description":"List the skill library every bot in this Grok Bot shares.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false}],"path":["skills","list"],"routeId":"cli:skills/list"},{"aliases":[],"description":"Remove a library skill by id or name. Every bot loses it.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Skill id or name","key":"skill","kind":"string","option":"skill","positional":0,"repeated":false,"required":true}],"path":["skills","remove"],"routeId":"cli:skills/remove"},{"aliases":[],"description":"Read the most recent messages in a Grok Bot bot or group thread.","exitCode":"zero","options":[{"description":"Return entries after this opaque entry id","key":"after","kind":"string","option":"after","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Show full entry text in human output","key":"full","kind":"boolean","option":"full","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Directory containing history.jsonl","key":"historyDir","kind":"string","option":"history-dir","repeated":false,"required":false},{"description":"How many trailing entries to return (1-200)","key":"limit","kind":"number","option":"limit","repeated":false,"required":false},{"description":"Skip local history for this command","key":"noHistory","kind":"boolean","option":"no-history","repeated":false,"required":false},{"description":"Read one rooted thread by message id","key":"root","kind":"string","option":"root","repeated":false,"required":false},{"key":"target","kind":"string","option":"target","positional":0,"repeated":false,"required":true}],"path":["thread"],"routeId":"cli:thread"}],"mode":"generated","routes":[{"contract":"contract:src/cli/approvals/list.tsx#inputJsonSchema","description":"List current Grok approval cards (latest 200 entries).","id":"cli:approvals/list","inputSchema":{"additionalProperties":false,"properties":{"target":{"type":"string"}},"required":["target"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/approvals/list.tsx"},{"contract":"contract:src/cli/approvals/respond.tsx#inputJsonSchema","description":"Explicit user decision: accept a Grok request once or decline; never grant persistent permissions.","id":"cli:approvals/respond","inputSchema":{"additionalProperties":false,"properties":{"decision":{"enum":["accept","decline"],"type":"string"},"entryId":{"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","entryId","requestId","decision"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/approvals/respond.tsx"},{"contract":"contract:src/cli/bots/create.tsx#inputJsonSchema","description":"Create a Grok Bot bot.","id":"cli:bots/create","inputSchema":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"name":{"type":"string"},"title":{"type":"string"}},"required":["name"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/create.tsx"},{"contract":"contract:src/cli/bots/delete.tsx#inputJsonSchema","description":"Delete a bot or group by id or name.","id":"cli:bots/delete","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/delete.tsx"},{"contract":"contract:src/cli/bots/get.tsx#inputJsonSchema","description":"Show one bot or group by id or name.","id":"cli:bots/get","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/get.tsx"},{"contract":"contract:src/cli/bots/list.tsx#inputJsonSchema","description":"List Grok Bot bots (not groups).","id":"cli:bots/list","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/list.tsx"},{"contract":"contract:src/cli/bots/update.tsx#inputJsonSchema","description":"Update a bot or group profile.","id":"cli:bots/update","inputSchema":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"hidden":{"type":"string"},"name":{"type":"string"},"notify":{"type":"string"},"ref":{"description":"Bot or group id or name","type":"string"},"title":{"type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/update.tsx"},{"contract":"contract:src/cli/claude/send.tsx#inputJsonSchema","description":"Send to an explicitly enabled live Claude Code channel and wait for its reply.","id":"cli:claude/send","inputSchema":{"additionalProperties":false,"properties":{"message":{"type":"string"},"name":{"type":"string"},"timeoutMs":{"type":"number"}},"required":["name","message"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/claude/send.tsx"},{"contract":"contract:src/cli/codex/bridge/respond.tsx#inputJsonSchema","description":"Managed Grok/Codex bridge respond.","id":"cli:codex/bridge/respond","inputSchema":{"additionalProperties":false,"properties":{"answersJson":{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","type":"string"},"bindingId":{"type":"string"},"decision":{"enum":["accept","decline","cancel"],"type":"string"},"exchangeId":{"type":"string"},"generation":{"type":"string"},"interactionId":{"type":"string"},"threadId":{"type":"string"},"turnId":{"type":"string"}},"required":["interactionId","generation","threadId","turnId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/respond.tsx"},{"contract":"contract:src/cli/codex/bridge/run.tsx#inputJsonSchema","description":"Run a bounded foreground relay (up to 23 hours). Use the packaged gbot-relay.mjs script for unlimited service lifetime.","id":"cli:codex/bridge/run","inputSchema":{"additionalProperties":false,"properties":{"lifetimeMs":{"description":"Foreground lifetime in milliseconds (100..82800000; default 23h).","type":"number"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/run.tsx"},{"contract":"contract:src/cli/codex/bridge/start.tsx#inputJsonSchema","description":"Managed Grok/Codex bridge start.","id":"cli:codex/bridge/start","inputSchema":{"additionalProperties":false,"properties":{"busyPolicy":{"enum":["steer","reject"],"type":"string"},"codexThreadId":{"type":"string"},"expectedCwd":{"type":"string"},"grokTarget":{"type":"string"},"requestId":{"type":"string"}},"required":["grokTarget"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/start.tsx"},{"contract":"contract:src/cli/codex/bridge/status.tsx#inputJsonSchema","description":"Managed Grok/Codex bridge status.","id":"cli:codex/bridge/status","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"limit":{"type":"number"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/status.tsx"},{"contract":"contract:src/cli/codex/bridge/stop.tsx#inputJsonSchema","description":"Managed Grok/Codex bridge stop.","id":"cli:codex/bridge/stop","inputSchema":{"additionalProperties":false,"properties":{"all":{"type":"boolean"},"bindingId":{"type":"string"},"worker":{"type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/stop.tsx"},{"contract":"contract:src/cli/codex/desktop-shim.tsx#inputJsonSchema","description":"Install, remove, or check the ChatGPT Desktop shim: a CODEX_CLI_PATH wrapper that bridges Desktop stdio onto the managed Codex daemon (stock app-server proxy hangs). macOS persists via LaunchAgent; always fails open to real Codex.","id":"cli:codex/desktop-shim","inputSchema":{"additionalProperties":false,"properties":{"action":{"description":"install | uninstall | status","enum":["install","uninstall","status"],"type":"string"}},"required":["action"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/desktop-shim.tsx"},{"contract":"contract:src/cli/codex/list-threads.tsx#inputJsonSchema","description":"List Codex daemon-managed threads.","id":"cli:codex/list-threads","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque pagination cursor (may start with -)","type":"string"},"limit":{"default":20,"description":"Max threads to list (1-200)","type":"number"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/list-threads.tsx"},{"contract":"contract:src/cli/codex/new.tsx#inputJsonSchema","description":"Start a new Codex thread in --cwd, optionally with --model, --effort and a message.","id":"cli:codex/new","inputSchema":{"additionalProperties":false,"properties":{"cwd":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"expectedCwd":{"type":"string"},"message":{"items":{"type":"string"},"type":"array"},"model":{"type":"string"}},"required":["cwd"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/new.tsx"},{"contract":"contract:src/cli/codex/queue.tsx#inputJsonSchema","description":"List the experimental Codex thread queue (GROK_BOT_CODEX_EXPERIMENTAL=1).","id":"cli:codex/queue","inputSchema":{"additionalProperties":false,"properties":{"threadId":{"type":"string"}},"required":["threadId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/queue.tsx"},{"contract":"contract:src/cli/codex/send.tsx#inputJsonSchema","description":"Send to Codex; acceptance is distinct from completion. Options precede threadId.","id":"cli:codex/send","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"correlationId":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"envelope":{"type":"boolean"},"expectedCwd":{"type":"string"},"expectedTurnId":{"description":"Required active-turn guard for steer; stale guards reject.","type":"string"},"hop":{"type":"number"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"message":{"items":{"type":"string"},"type":"array"},"model":{"type":"string"},"replyTo":{"type":"string"},"replyToGrok":{"type":"string"},"requestId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"wait":{"type":"boolean"},"whenBusy":{"enum":["reject","queue","steer"],"type":"string"}},"required":["threadId","message"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/send.tsx"},{"contract":"contract:src/cli/codex/status.tsx#inputJsonSchema","description":"Probe the local Codex app-server daemon. Exit 0 only when it is usable.","id":"cli:codex/status","inputSchema":{"additionalProperties":false,"properties":{},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/status.tsx"},{"contract":"contract:src/cli/codex/wait.tsx#inputJsonSchema","description":"Bounded Codex wait observation; never interrupts execution.","id":"cli:codex/wait","inputSchema":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"messageId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"turnId":{"type":"string"}},"required":["threadId","turnId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/wait.tsx"},{"contract":"contract:src/cli/codex/watch.tsx#inputJsonSchema","description":"Bounded Codex watch observation; never interrupts execution.","id":"cli:codex/watch","inputSchema":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxEvents":{"description":"Maximum observed events: 1-500.","type":"number"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-600000 milliseconds.","type":"number"}},"required":["threadId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/watch.tsx"},{"contract":"contract:src/cli/doctor.tsx#inputJsonSchema","description":"Show which agents root and auth sources gbot can see.","id":"cli:doctor","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/doctor.tsx"},{"contract":"contract:src/cli/groups/add.tsx#inputJsonSchema","description":"Add a bot to a group.","id":"cli:groups/add","inputSchema":{"additionalProperties":false,"properties":{"bot":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"}},"required":["group","bot"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/add.tsx"},{"contract":"contract:src/cli/groups/create.tsx#inputJsonSchema","description":"Create a Grok Bot group.","id":"cli:groups/create","inputSchema":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"member":{"description":"Member bot id or name (repeatable)","items":{"type":"string"},"type":"array"},"name":{"type":"string"},"title":{"type":"string"}},"required":["name","member"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/create.tsx"},{"contract":"contract:src/cli/groups/delete.tsx#inputJsonSchema","description":"Delete a group by id or name.","id":"cli:groups/delete","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/delete.tsx"},{"contract":"contract:src/cli/groups/get.tsx#inputJsonSchema","description":"Show one group by id or name.","id":"cli:groups/get","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/get.tsx"},{"contract":"contract:src/cli/groups/list.tsx#inputJsonSchema","description":"List Grok Bot groups.","id":"cli:groups/list","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/list.tsx"},{"contract":"contract:src/cli/groups/members.tsx#inputJsonSchema","description":"Show members of a group (same payload as groups get).","id":"cli:groups/members","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/members.tsx"},{"contract":"contract:src/cli/groups/remove.tsx#inputJsonSchema","description":"Remove a bot from a group.","id":"cli:groups/remove","inputSchema":{"additionalProperties":false,"properties":{"bot":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"}},"required":["group","bot"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/remove.tsx"},{"contract":"contract:src/cli/groups/set.tsx#inputJsonSchema","description":"Replace a group's member list.","id":"cli:groups/set","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"},"member":{"description":"Member bot id or name (repeatable)","items":{"type":"string"},"type":"array"}},"required":["group","member"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/set.tsx"},{"contract":"contract:src/cli/groups/update.tsx#inputJsonSchema","description":"Update a group profile (members stay on set/add/remove).","id":"cli:groups/update","inputSchema":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"hidden":{"type":"string"},"name":{"type":"string"},"notify":{"type":"string"},"ref":{"description":"Group id or name","type":"string"},"title":{"type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/update.tsx"},{"contract":"contract:src/cli/history.tsx#inputJsonSchema","description":"Read the opt-in local JSONL history without contacting the gateway.","id":"cli:history","inputSchema":{"additionalProperties":false,"properties":{"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"limit":{"default":40,"description":"Maximum matching rows (1 or more)","type":"number"},"path":{"description":"Print the history file path","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"},"search":{"description":"Case-insensitive message text filter","type":"string"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/history.tsx"},{"contract":"contract:src/cli/send.tsx#inputJsonSchema","description":"Send a message to a Grok Bot bot or group by name or id.","id":"cli:send","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"codexThreadId":{"type":"string"},"correlationId":{"description":"Stable correlation id for multi-hop replies","type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"envelope":{"description":"Prepend the [gbot …] header","type":"boolean"},"expectedCwd":{"type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"hop":{"description":"Hop count; refused at GROK_BOT_MAX_HOPS","type":"number"},"message":{"items":{"type":"string"},"type":"array"},"noHistory":{"description":"Skip local history for this command","type":"boolean"},"replyMode":{"enum":["auto","manual"],"type":"string"},"replyTo":{"description":"Prior message id this send replies to","type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","message"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/send.tsx"},{"contract":"contract:src/cli/skills/add.tsx#inputJsonSchema","description":"Add a SKILL.md to the shared skill library. Every bot sees it.","id":"cli:skills/add","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"path":{"description":"SKILL.md file, or a directory holding one","type":"string"}},"required":["path"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/skills/add.tsx"},{"contract":"contract:src/cli/skills/list.tsx#inputJsonSchema","description":"List the skill library every bot in this Grok Bot shares.","id":"cli:skills/list","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/skills/list.tsx"},{"contract":"contract:src/cli/skills/remove.tsx#inputJsonSchema","description":"Remove a library skill by id or name. Every bot loses it.","id":"cli:skills/remove","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"skill":{"description":"Skill id or name","type":"string"}},"required":["skill"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/skills/remove.tsx"},{"contract":"contract:src/cli/thread.tsx#inputJsonSchema","description":"Read the most recent messages in a Grok Bot bot or group thread.","id":"cli:thread","inputSchema":{"additionalProperties":false,"properties":{"after":{"description":"Return entries after this opaque entry id","type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"full":{"description":"Show full entry text in human output","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"limit":{"default":40,"description":"How many trailing entries to return (1-200)","type":"number"},"noHistory":{"description":"Skip local history for this command","type":"boolean"},"root":{"description":"Read one rooted thread by message id","type":"string"},"target":{"type":"string"}},"required":["target"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/thread.tsx"}]},"contracts":[{"id":"contract:src/cli/approvals/list.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"target":{"type":"string"}},"required":["target"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/approvals/list.tsx"},"routes":["cli:approvals/list"]},{"id":"contract:src/cli/approvals/respond.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"decision":{"enum":["accept","decline"],"type":"string"},"entryId":{"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","entryId","requestId","decision"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/approvals/respond.tsx"},"routes":["cli:approvals/respond"]},{"id":"contract:src/cli/bots/create.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"name":{"type":"string"},"title":{"type":"string"}},"required":["name"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/create.tsx"},"routes":["cli:bots/create"]},{"id":"contract:src/cli/bots/delete.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/delete.tsx"},"routes":["cli:bots/delete"]},{"id":"contract:src/cli/bots/get.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/get.tsx"},"routes":["cli:bots/get"]},{"id":"contract:src/cli/bots/list.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/list.tsx"},"routes":["cli:bots/list"]},{"id":"contract:src/cli/bots/update.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"hidden":{"type":"string"},"name":{"type":"string"},"notify":{"type":"string"},"ref":{"description":"Bot or group id or name","type":"string"},"title":{"type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/update.tsx"},"routes":["cli:bots/update"]},{"id":"contract:src/cli/claude/send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"message":{"type":"string"},"name":{"type":"string"},"timeoutMs":{"type":"number"}},"required":["name","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/claude/send.tsx"},"routes":["cli:claude/send"]},{"id":"contract:src/cli/codex/bridge/respond.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"answersJson":{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","type":"string"},"bindingId":{"type":"string"},"decision":{"enum":["accept","decline","cancel"],"type":"string"},"exchangeId":{"type":"string"},"generation":{"type":"string"},"interactionId":{"type":"string"},"threadId":{"type":"string"},"turnId":{"type":"string"}},"required":["interactionId","generation","threadId","turnId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/respond.tsx"},"routes":["cli:codex/bridge/respond"]},{"id":"contract:src/cli/codex/bridge/run.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"lifetimeMs":{"description":"Foreground lifetime in milliseconds (100..82800000; default 23h).","type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/run.tsx"},"routes":["cli:codex/bridge/run"]},{"id":"contract:src/cli/codex/bridge/start.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"busyPolicy":{"enum":["steer","reject"],"type":"string"},"codexThreadId":{"type":"string"},"expectedCwd":{"type":"string"},"grokTarget":{"type":"string"},"requestId":{"type":"string"}},"required":["grokTarget"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/start.tsx"},"routes":["cli:codex/bridge/start"]},{"id":"contract:src/cli/codex/bridge/status.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"limit":{"type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/status.tsx"},"routes":["cli:codex/bridge/status"]},{"id":"contract:src/cli/codex/bridge/stop.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"all":{"type":"boolean"},"bindingId":{"type":"string"},"worker":{"type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/stop.tsx"},"routes":["cli:codex/bridge/stop"]},{"id":"contract:src/cli/codex/desktop-shim.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"action":{"description":"install | uninstall | status","enum":["install","uninstall","status"],"type":"string"}},"required":["action"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/desktop-shim.tsx"},"routes":["cli:codex/desktop-shim"]},{"id":"contract:src/cli/codex/list-threads.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque pagination cursor (may start with -)","type":"string"},"limit":{"default":20,"description":"Max threads to list (1-200)","type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/list-threads.tsx"},"routes":["cli:codex/list-threads"]},{"id":"contract:src/cli/codex/new.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cwd":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"expectedCwd":{"type":"string"},"message":{"items":{"type":"string"},"type":"array"},"model":{"type":"string"}},"required":["cwd"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/new.tsx"},"routes":["cli:codex/new"]},{"id":"contract:src/cli/codex/queue.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"threadId":{"type":"string"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/queue.tsx"},"routes":["cli:codex/queue"]},{"id":"contract:src/cli/codex/send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"correlationId":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"envelope":{"type":"boolean"},"expectedCwd":{"type":"string"},"expectedTurnId":{"description":"Required active-turn guard for steer; stale guards reject.","type":"string"},"hop":{"type":"number"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"message":{"items":{"type":"string"},"type":"array"},"model":{"type":"string"},"replyTo":{"type":"string"},"replyToGrok":{"type":"string"},"requestId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"wait":{"type":"boolean"},"whenBusy":{"enum":["reject","queue","steer"],"type":"string"}},"required":["threadId","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/send.tsx"},"routes":["cli:codex/send"]},{"id":"contract:src/cli/codex/status.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/status.tsx"},"routes":["cli:codex/status"]},{"id":"contract:src/cli/codex/wait.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"messageId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"turnId":{"type":"string"}},"required":["threadId","turnId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/wait.tsx"},"routes":["cli:codex/wait"]},{"id":"contract:src/cli/codex/watch.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxEvents":{"description":"Maximum observed events: 1-500.","type":"number"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-600000 milliseconds.","type":"number"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/watch.tsx"},"routes":["cli:codex/watch"]},{"id":"contract:src/cli/doctor.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/doctor.tsx"},"routes":["cli:doctor"]},{"id":"contract:src/cli/groups/add.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bot":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"}},"required":["group","bot"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/add.tsx"},"routes":["cli:groups/add"]},{"id":"contract:src/cli/groups/create.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"member":{"description":"Member bot id or name (repeatable)","items":{"type":"string"},"type":"array"},"name":{"type":"string"},"title":{"type":"string"}},"required":["name","member"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/create.tsx"},"routes":["cli:groups/create"]},{"id":"contract:src/cli/groups/delete.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/delete.tsx"},"routes":["cli:groups/delete"]},{"id":"contract:src/cli/groups/get.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/get.tsx"},"routes":["cli:groups/get"]},{"id":"contract:src/cli/groups/list.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/list.tsx"},"routes":["cli:groups/list"]},{"id":"contract:src/cli/groups/members.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/members.tsx"},"routes":["cli:groups/members"]},{"id":"contract:src/cli/groups/remove.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bot":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"}},"required":["group","bot"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/remove.tsx"},"routes":["cli:groups/remove"]},{"id":"contract:src/cli/groups/set.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"},"member":{"description":"Member bot id or name (repeatable)","items":{"type":"string"},"type":"array"}},"required":["group","member"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/set.tsx"},"routes":["cli:groups/set"]},{"id":"contract:src/cli/groups/update.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"hidden":{"type":"string"},"name":{"type":"string"},"notify":{"type":"string"},"ref":{"description":"Group id or name","type":"string"},"title":{"type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/update.tsx"},"routes":["cli:groups/update"]},{"id":"contract:src/cli/history.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"limit":{"default":40,"description":"Maximum matching rows (1 or more)","type":"number"},"path":{"description":"Print the history file path","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"},"search":{"description":"Case-insensitive message text filter","type":"string"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/history.tsx"},"routes":["cli:history"]},{"id":"contract:src/cli/send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"codexThreadId":{"type":"string"},"correlationId":{"description":"Stable correlation id for multi-hop replies","type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"envelope":{"description":"Prepend the [gbot …] header","type":"boolean"},"expectedCwd":{"type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"hop":{"description":"Hop count; refused at GROK_BOT_MAX_HOPS","type":"number"},"message":{"items":{"type":"string"},"type":"array"},"noHistory":{"description":"Skip local history for this command","type":"boolean"},"replyMode":{"enum":["auto","manual"],"type":"string"},"replyTo":{"description":"Prior message id this send replies to","type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/send.tsx"},"routes":["cli:send"]},{"id":"contract:src/cli/skills/add.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"path":{"description":"SKILL.md file, or a directory holding one","type":"string"}},"required":["path"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/skills/add.tsx"},"routes":["cli:skills/add"]},{"id":"contract:src/cli/skills/list.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/skills/list.tsx"},"routes":["cli:skills/list"]},{"id":"contract:src/cli/skills/remove.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"skill":{"description":"Skill id or name","type":"string"}},"required":["skill"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/skills/remove.tsx"},"routes":["cli:skills/remove"]},{"id":"contract:src/cli/thread.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"after":{"description":"Return entries after this opaque entry id","type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"full":{"description":"Show full entry text in human output","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"limit":{"default":40,"description":"How many trailing entries to return (1-200)","type":"number"},"noHistory":{"description":"Skip local history for this command","type":"boolean"},"root":{"description":"Read one rooted thread by message id","type":"string"},"target":{"type":"string"}},"required":["target"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/thread.tsx"},"routes":["cli:thread"]},{"id":"contract:src/mcp/grok-bot/tools/claude_send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"message":{"type":"string"},"name":{"type":"string"},"timeoutMs":{"type":"number"}},"required":["name","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/claude_send.tsx"},"routes":["tool:grok-bot/claude_send"]},{"id":"contract:src/mcp/grok-bot/tools/codex_new.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cwd":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"expectedCwd":{"type":"string"},"message":{"type":"string"},"model":{"type":"string"}},"required":["cwd"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_new.tsx"},"routes":["tool:grok-bot/codex_new"]},{"id":"contract:src/mcp/grok-bot/tools/codex_send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"correlationId":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"envelope":{"type":"boolean"},"expectedCwd":{"type":"string"},"expectedTurnId":{"type":"string"},"hop":{"type":"number"},"maxOutputBytes":{"type":"number"},"message":{"type":"string"},"model":{"type":"string"},"replyTo":{"type":"string"},"replyToGrok":{"type":"string"},"requestId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"type":"number"},"wait":{"default":false,"type":"boolean"},"whenBusy":{"enum":["reject","queue","steer"],"type":"string"}},"required":["threadId","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_send.tsx"},"routes":["tool:grok-bot/codex_send"]},{"id":"contract:src/mcp/grok-bot/tools/codex_threads.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"type":"string"},"limit":{"description":"Maximum threads in this page: 1-200.","type":"number"}},"required":[],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_threads.tsx"},"routes":["tool:grok-bot/codex_threads"]},{"id":"contract:src/mcp/grok-bot/tools/codex_wait.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"messageId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"turnId":{"type":"string"}},"required":["threadId","turnId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_wait.tsx"},"routes":["tool:grok-bot/codex_wait"]},{"id":"contract:src/mcp/grok-bot/tools/codex_watch.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxEvents":{"description":"Maximum observed events: 1-500.","type":"number"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-600000 milliseconds.","type":"number"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_watch.tsx"},"routes":["tool:grok-bot/codex_watch"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_bridge_start.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"busyPolicy":{"enum":["steer","reject"],"type":"string"},"codexThreadId":{"type":"string"},"expectedCwd":{"type":"string"},"grokTarget":{"type":"string"},"requestId":{"type":"string"}},"required":["grokTarget"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_bridge_start.tsx"},"routes":["tool:grok-bot/gbot_bridge_start"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_bridge_status.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"limit":{"type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_bridge_status.tsx"},"routes":["tool:grok-bot/gbot_bridge_status"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_bridge_stop.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"all":{"type":"boolean"},"bindingId":{"type":"string"},"worker":{"type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_bridge_stop.tsx"},"routes":["tool:grok-bot/gbot_bridge_stop"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_codex_respond.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"answersJson":{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","type":"string"},"bindingId":{"type":"string"},"decision":{"enum":["accept","decline","cancel"],"type":"string"},"exchangeId":{"type":"string"},"generation":{"type":"string"},"interactionId":{"type":"string"},"threadId":{"type":"string"},"turnId":{"type":"string"}},"required":["interactionId","generation","threadId","turnId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_codex_respond.tsx"},"routes":["tool:grok-bot/gbot_codex_respond"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_grok_approvals.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"target":{"type":"string"}},"required":["target"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_grok_approvals.tsx"},"routes":["tool:grok-bot/gbot_grok_approvals"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_grok_respond.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"decision":{"enum":["accept","decline"],"type":"string"},"entryId":{"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","entryId","requestId","decision"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_grok_respond.tsx"},"routes":["tool:grok-bot/gbot_grok_respond"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"codexThreadId":{"type":"string"},"correlationId":{"type":"string"},"expectedCwd":{"type":"string"},"hop":{"description":"Explicit chain hop count, refused at the configured bound.","type":"number"},"message":{"type":"string"},"replyMode":{"enum":["auto","manual"],"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_send.tsx"},"routes":["tool:grok-bot/gbot_send"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_thread.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"after":{"description":"Opaque cursor from the previous call. Returns entries strictly after it; an unknown cursor resets with a bounded snapshot.","type":"string"},"full":{"default":false,"description":"Return entries with text up to bounded budgets (20k chars per entry, 200k total). Every entry reports truncated/fullLength; read any remainder with `gbot thread --full` / `--json` on the machine.","type":"boolean"},"limit":{"default":40,"description":"How many trailing entries to inspect (1-200). Entries are returned only with full:true.","type":"number"},"target":{"description":"Bot or group name or id, for example \"General\".","type":"string"}},"required":["target"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_thread.tsx"},"routes":["tool:grok-bot/gbot_thread"]}],"digest":"4e5c8be282c178457bafe177ecc68ca162cdf566f6b507b92736f858afa74213","events":[],"layouts":[],"providers":[],"scripts":[{"id":"script:gbot-relay","kind":"script","provenance":{"kind":"conventional"},"source":"src/scripts/gbot-relay.ts"}],"servers":[{"id":"mcp:grok-bot","mode":"generated","name":"grok-bot","routes":[{"contract":"contract:src/mcp/grok-bot/tools/claude_send.tsx#inputJsonSchema","description":"Send to a named, opted-in live Claude Code channel and wait for its reply. Unknown delivery must not be retried automatically. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport.","id":"tool:grok-bot/claude_send","inputSchema":{"additionalProperties":false,"properties":{"message":{"type":"string"},"name":{"type":"string"},"timeoutMs":{"type":"number"}},"required":["name","message"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/claude_send.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_new.tsx#inputJsonSchema","description":"Start a new Codex thread on this machine in cwd, optionally sending its first message. Codex must run locally on the same machine as gbot.","id":"tool:grok-bot/codex_new","inputSchema":{"additionalProperties":false,"properties":{"cwd":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"expectedCwd":{"type":"string"},"message":{"type":"string"},"model":{"type":"string"}},"required":["cwd"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_new.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_send.tsx#inputJsonSchema","description":"Send to a Codex thread. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport. With replyToGrok or bindingId, managed delivery returns the terminal answer to Grok automatically. Otherwise optional wait observes completion and explicit steer requires expectedTurnId. Acceptance is not completion.","id":"tool:grok-bot/codex_send","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"correlationId":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"envelope":{"type":"boolean"},"expectedCwd":{"type":"string"},"expectedTurnId":{"type":"string"},"hop":{"type":"number"},"maxOutputBytes":{"type":"number"},"message":{"type":"string"},"model":{"type":"string"},"replyTo":{"type":"string"},"replyToGrok":{"type":"string"},"requestId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"type":"number"},"wait":{"default":false,"type":"boolean"},"whenBusy":{"enum":["reject","queue","steer"],"type":"string"}},"required":["threadId","message"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_send.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_threads.tsx#inputJsonSchema","description":"Discover a bounded page of Codex daemon threads. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport.","id":"tool:grok-bot/codex_threads","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"type":"string"},"limit":{"description":"Maximum threads in this page: 1-200.","type":"number"}},"required":[],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_threads.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_wait.tsx#inputJsonSchema","description":"Explicit diagnostic observation of one Codex turn; returns execution and final reply without interrupting it. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport.","id":"tool:grok-bot/codex_wait","inputSchema":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"messageId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"turnId":{"type":"string"}},"required":["threadId","turnId"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_wait.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_watch.tsx#inputJsonSchema","description":"Watch bounded Codex thread events for diagnostics without answering approvals. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport.","id":"tool:grok-bot/codex_watch","inputSchema":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxEvents":{"description":"Maximum observed events: 1-500.","type":"number"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-600000 milliseconds.","type":"number"}},"required":["threadId"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_watch.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_bridge_start.tsx#inputJsonSchema","description":"Link a Grok conversation to Codex once. New visible Grok messages arrive automatically and Codex final answers return to Grok.","id":"tool:grok-bot/gbot_bridge_start","inputSchema":{"additionalProperties":false,"properties":{"busyPolicy":{"enum":["steer","reject"],"type":"string"},"codexThreadId":{"type":"string"},"expectedCwd":{"type":"string"},"grokTarget":{"type":"string"},"requestId":{"type":"string"}},"required":["grokTarget"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_bridge_start.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_bridge_status.tsx#inputJsonSchema","description":"Inspect worker health, route coverage, bounded receipts and scoped pending operator interactions.","id":"tool:grok-bot/gbot_bridge_status","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"limit":{"type":"number"}},"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_bridge_status.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_bridge_stop.tsx#inputJsonSchema","description":"Stop a binding without deleting receipts or interrupting Codex. all stops all bindings; worker explicitly shuts down the worker.","id":"tool:grok-bot/gbot_bridge_stop","inputSchema":{"additionalProperties":false,"properties":{"all":{"type":"boolean"},"bindingId":{"type":"string"},"worker":{"type":"boolean"}},"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_bridge_stop.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_codex_respond.tsx#inputJsonSchema","description":"Explicit operator response to a current scoped Codex interaction. Supports only one-time accept/decline/cancel or exact question-ID answers. Never auto-approve.","id":"tool:grok-bot/gbot_codex_respond","inputSchema":{"additionalProperties":false,"properties":{"answersJson":{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","type":"string"},"bindingId":{"type":"string"},"decision":{"enum":["accept","decline","cancel"],"type":"string"},"exchangeId":{"type":"string"},"generation":{"type":"string"},"interactionId":{"type":"string"},"threadId":{"type":"string"},"turnId":{"type":"string"}},"required":["interactionId","generation","threadId","turnId"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_codex_respond.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_grok_approvals.tsx#inputJsonSchema","description":"List pending auto-review and local-tool approval cards in the latest 200 entries for a Grok bot. Older or unsupported requests require the owning Grok UI.","id":"tool:grok-bot/gbot_grok_approvals","inputSchema":{"additionalProperties":false,"properties":{"target":{"type":"string"}},"required":["target"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_grok_approvals.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_grok_respond.tsx#inputJsonSchema","description":"Only after an explicit user decision: accept one current Grok approval once or decline it. Exact target, entryId and approval requestId required. Never auto-approve or grant persistent permissions. Success acknowledges response delivery, not execution.","id":"tool:grok-bot/gbot_grok_respond","inputSchema":{"additionalProperties":false,"properties":{"decision":{"enum":["accept","decline"],"type":"string"},"entryId":{"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","entryId","requestId","decision"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_grok_respond.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_send.tsx#inputJsonSchema","description":"Send to Grok Bot. Native Codex calls automatically receive replies in their originating thread; send once and continue work. Without a native source, supply codexThreadId or use manual gbot_thread reading.","id":"tool:grok-bot/gbot_send","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"codexThreadId":{"type":"string"},"correlationId":{"type":"string"},"expectedCwd":{"type":"string"},"hop":{"description":"Explicit chain hop count, refused at the configured bound.","type":"number"},"message":{"type":"string"},"replyMode":{"enum":["auto","manual"],"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","message"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_send.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_thread.tsx#inputJsonSchema","description":"Read a bounded Grok Bot thread tail. Returns a small receipt by default; pass the last cursor as after for an exclusive client-side delta, or full:true to include bounded entry text.","id":"tool:grok-bot/gbot_thread","inputSchema":{"additionalProperties":false,"properties":{"after":{"description":"Opaque cursor from the previous call. Returns entries strictly after it; an unknown cursor resets with a bounded snapshot.","type":"string"},"full":{"default":false,"description":"Return entries with text up to bounded budgets (20k chars per entry, 200k total). Every entry reports truncated/fullLength; read any remainder with `gbot thread --full` / `--json` on the machine.","type":"boolean"},"limit":{"default":40,"description":"How many trailing entries to inspect (1-200). Entries are returned only with full:true.","type":"number"},"target":{"description":"Bot or group name or id, for example \"General\".","type":"string"}},"required":["target"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_thread.tsx"}]}]},"runtime":{"node":"22.19.0"}} +{"application":{"description":"Message Grok Bot from Codex, Claude Code, and Cursor. Codex/Claude tools use local sockets on the user's registered machines only (no remote transport); from the Grok Bot box, run gbot via Grok Bot Shell with a machineId.","id":"plugin:gbot","name":"gbot","version":"0.10.2"},"compiler":{"adapters":[{"adapterRevision":"1.28.0","host":"claude","observedVersion":"2.1.260","schemas":[{"name":"hooks","revision":"2.1.260","sha256":"0cdfc5eb5201f2c3091768559ac82f4c563ccb1b7bce7a39d5f99e5f404654cb"},{"name":"lsp","revision":"2.1.260","sha256":"b4419c5d857267c7e2b21e3e1eb98b4fdc302c87109007190d3258e6ba7096e4"},{"name":"marketplace","revision":"2.1.260","sha256":"31ee4cc43ba5ce2be248030a69da2d31171550b2cc51c1fcb9f788a8ab92783d"},{"name":"mcp","revision":"2.1.260","sha256":"edd4770e41d6aee5beae1ff918d33139613a243e454ecbd52b72fa824be4a662"},{"name":"monitors","revision":"2.1.260","sha256":"6378d94b51fb7c784eaee178237008a03da0f889d0520d57486784954a1d484c"},{"name":"plugin","revision":"2.1.260","sha256":"2a976091b81ad07ae8eca57f6f9c5749efeba17fa089a0c12b6ca84d6e70f118"},{"name":"settings","revision":"2.1.260","sha256":"2bbca553621dbf9433a9b7d1ff7952543a368434f2d999a4e4e360b95b6d4c3d"},{"name":"theme","revision":"2.1.260","sha256":"9931264e6f5a1d4b3b854ce7a17d602c9ba1b57cc1991f1d3991b316eaa81ac2"}]},{"adapterRevision":"1.13.0","host":"codex","observedVersion":"0.147.0","schemas":[{"name":"app","revision":"0.147.0","sha256":"01c720a645e437bf0c4f8c26fd4cb5a13988e5649e4a8562ee23a1d4b7355c6a"},{"name":"hooks","revision":"0.147.0","sha256":"175b859eb8e85bd287d85ee840d97c3f5c2d0dda3223507a796d158e3770eeba"},{"name":"marketplace","revision":"0.147.0","sha256":"fbccce3ade39e1b077fcb440e60260de6e811e8c2d222b2ae0fe8fe47706b470"},{"name":"mcp","revision":"0.147.0","sha256":"75bd50f9fcb85c2e8d43bc132d61c172a02f28ea8bb77389816ae77b14a4257e"},{"name":"plugin","revision":"0.147.0","sha256":"074c6c71966a3e6560ccbceb8d82ec6a40cb1eccee2f2d863fb4ef1e2276a814"}]},{"adapterRevision":"1.13.0","host":"cursor","observedVersion":"2026-08-28","schemas":[{"name":"hooks","revision":"2026-08-28","sha256":"06154b7afa0861df462130b988912b897e7ccf962b8dd20c09193100bcde5d81"},{"name":"marketplace","revision":"2026-08-28","sha256":"1aae96a24c2796419933bc8bfe3a1255394e7199c35740b36325e0ce6dbc253d"},{"name":"mcp","revision":"2026-08-28","sha256":"f3fa4615afefe004c4fbcc09e635d890df0f1ec0cb39540feab72cbd3a31d844"},{"name":"plugin","revision":"2026-08-28","sha256":"a393b758901803fcf5cfe0d77bda8a83e987d32c3377dfce2d9edf445af884ed"}]},{"adapterRevision":"1.10.0","host":"portable","observedVersion":"1.0.0","schemas":[{"name":"mcp","revision":"1.0.0","sha256":"6539175bfcdf43085855183e86da40ea94b166547a72b47ae9a0a390516d3acb"},{"name":"plugin","revision":"1.0.0","sha256":"0a4aad95ce337878ad38802ebf0daa3fde76abe3f65400c86bcbb1ec0b3ab883"}]}],"agentSkills":{"schemaSha256":"6d06b61e423317421de2295ea1fd0c7b4491bfa36c5b7705c28616dfe76d4047","sourceRevision":"69ef37e9424c0a7ea9dd2293b559e43ec8176379","specification":"https://raw.githubusercontent.com/agentskills/agentskills/69ef37e9424c0a7ea9dd2293b559e43ec8176379/docs/specification.mdx"},"producer":{"name":"agent-bundle","version":"0.3.1"},"project":{"configDigest":"7718be28a54eb7ec9e28b9653ef671683604436af21e1eda99c11098f43e278f","configPath":"agent-bundle.config.ts","modelDigest":"b462f5594e8174bb3c565276aa2dcb5f56094d68add7f8b991b09701ed1417a6","packageName":"grok-bot-cli","packageVersion":"0.10.2","revision":"ee63b1fe252603936f09265b7ea1f2d4a0d4092ffc862bc6ac68d393375afa06","sourceInputs":[{"executable":false,"path":".changeset/chatgpt-desktop-cdp.md","sha256":"7b79eb902697f8d0e805a98466e3d57c7a2f588b0daefbd3b83559f66e325ad9"},{"executable":false,"path":".changeset/config.json","sha256":"0a97ce5d0488bada887ee9ea3726edecc727454b7da0294f1b5fe4947f48ee7c"},{"executable":false,"path":".changeset/desktop-e2e-reliability.md","sha256":"ebb912d56ba3321df13a9d611780129af8f4c264dbc5e841df6c1a8a838dfc10"},{"executable":false,"path":".changeset/desktop-merged-pagination-projects.md","sha256":"686b95a887f227380fce54a4a36c227b8a53b924038ddaad82d1c2a778699168"},{"executable":false,"path":".changeset/README.md","sha256":"495bd4805f8362853bce99689c1cd75db8c475dfa38930619d2e197d7554db27"},{"executable":false,"path":".github/workflows/changeset.yml","sha256":"0139beb8a3dc0e95126c9701d09251a879331b1e8c5540bbfc69848525b40f23"},{"executable":false,"path":".github/workflows/ci.yml","sha256":"17a58cf5ceff03b7922d15c9b0eea11ae3e0fd00a581ff6881f69cf795f3ec0b"},{"executable":false,"path":".github/workflows/release.yml","sha256":"9c97360a2221fca9317edcac0eb343298c765e3b34bd048f916c47533dd90311"},{"executable":false,"path":".gitignore","sha256":"30aaeac24084f0a642d811aa493fc51b0fd3eddff322d359c39c573d8394ff3e"},{"executable":false,"path":".npmrc","sha256":"da47110e4c2d7cd216cd495b604ca78bd0f5caaca32da4837ec0efa6f967bba0"},{"executable":false,"path":"agent-bundle.config.ts","sha256":"7718be28a54eb7ec9e28b9653ef671683604436af21e1eda99c11098f43e278f"},{"executable":false,"path":"AGENTS.md","sha256":"e18ce90a031c533e7226789b97f6bce4aa7994b672544e0f425d20cc3bc15d29"},{"executable":false,"path":"CHANGELOG.md","sha256":"49d0b5b880153fa7be882f31c8edf73cf04fe0a5f83e2bb880a48ccda495044d"},{"executable":false,"path":"demo/grok-bot-cli-demo.gif","sha256":"0fe01e15fd2abbbc5cc3313e42931d404d3a320764e22fdcfb995f1a45dd73cd"},{"executable":false,"path":"demo/grok-bot-cli-demo.mp4","sha256":"b6ed66e43616674096c2f0a4a4da46fe4693c681833eab013f872a66b9ff933c"},{"executable":false,"path":"docs/chatgpt-desktop-cdp.md","sha256":"6dd6cfe00e9a888c1ced79f1db85744f9c92affef8a78126e22ea5205e76b732"},{"executable":false,"path":"docs/codex-busy-threads.md","sha256":"42509e8e4c2b519949f06d4e7028b8973d56f8fc0cd98decdb231375bb49d38d"},{"executable":false,"path":"docs/codex-duplex-architecture.md","sha256":"792f1be8bad508b042c5813c1fc3b7ea5ac87032803227830666727306e55fa2"},{"executable":false,"path":"docs/verification/2026-09-15-duplex.md","sha256":"c7d97f709a3c2d851c0a24690321bbda161ec925b4143a456b9951e174bf75df"},{"executable":false,"path":"LICENSE","sha256":"bc48c8a0f6b6e45842e574c79a6b955fe683ed13f48a50cc08bcccdb1a59fe59"},{"executable":false,"path":"package-lock.json","sha256":"fc92e6f62433580416ad0af5f6b7f83d422f111e5e0c2535f7934c54f9066953"},{"executable":false,"path":"package.json","sha256":"4894d2ae20d895b1b213c0bd7eb7a6c483a7ae632e49ffeaa4eb7564bc71abde"},{"executable":false,"path":"README.md","sha256":"efe1a601a87648defca4b223f5b6828ead8f17fc15ad1fb8eeac23b9b61b8b29"},{"executable":false,"path":"renovate.json","sha256":"e80b4e42a3043bc12fa0640db4bac392d2bf770acf841360d7c8ceeeac2ec1a9"},{"executable":false,"path":"rstest.route-unit.config.ts","sha256":"7286e20dc3d4162673e7c3dfe824db00307e1fda178410f23044a97286d141f2"},{"executable":false,"path":"scripts/run-unit-tests.mjs","sha256":"483f91b4856ce294a483dd2e62ca53bf09301adbc4c5fc9571da0e1d28049bc6"},{"executable":false,"path":"src/cli/_shared.ts","sha256":"555210759d9b2e2e60a01452c137abd73bad42bfab8b2311b66a3ab97f4ee5e9"},{"executable":false,"path":"src/cli/approvals/list.tsx","sha256":"8d21320b6cd9d6b2a266fd30196b8a439960f865a6f0a5dacf5845d64ed7388c"},{"executable":false,"path":"src/cli/approvals/respond.tsx","sha256":"7d9fe6e641deb08f444684733daac6da47fb472d90ed0e9ff77892f376a87255"},{"executable":false,"path":"src/cli/bots/create.tsx","sha256":"9f7a73a0595b642dd77f008ad596bcafd00fc5846ec1abff2ce62819d983eb84"},{"executable":false,"path":"src/cli/bots/delete.tsx","sha256":"126b53ac1da4a657f22d1a6770ddbab2cc66715c811de0873d50016bbe39cbf2"},{"executable":false,"path":"src/cli/bots/get.tsx","sha256":"f0c752a3b498819083a8ee5aa918d275727f1cc94d8774d8c84ac74db66e8a71"},{"executable":false,"path":"src/cli/bots/list.tsx","sha256":"21c0f7e39005860dfc2514b71f745bb4cab29d409ef2cff26f8759bb0fd261c1"},{"executable":false,"path":"src/cli/bots/update.tsx","sha256":"33b7f5aafbf931cf3cb5ecf423a76dacd09adb117a7d5490050639ae7f4210dd"},{"executable":false,"path":"src/cli/chatgpt-desktop/hosts.tsx","sha256":"64e3e873115a4f5c2b6a7fd68236dbf2ca4e4a880ba97bdd7409b48931bca632"},{"executable":false,"path":"src/cli/chatgpt-desktop/read.tsx","sha256":"11c86f604c2ee405f7544394db60ee1b36917d0b90769d441fcd361b5f0fe038"},{"executable":false,"path":"src/cli/chatgpt-desktop/search.tsx","sha256":"0dc3acdea178ff14d6c1c4b8b3bf1e18af47984f63b8ed8c41483e4da4b72f08"},{"executable":false,"path":"src/cli/chatgpt-desktop/send.tsx","sha256":"b801a028980e60fe37dca56e86887a1e2b0b8ce80ce77b379e7375d2e4544d5e"},{"executable":false,"path":"src/cli/chatgpt-desktop/status.tsx","sha256":"141b7b6db6d6f9236dd5343ca9495200752937b05764ad7fa7261631c2db5bbc"},{"executable":false,"path":"src/cli/chatgpt-desktop/threads.tsx","sha256":"d0a33935d992af349c52ef448622da5e460b4af1a9474aac60994295245b43ad"},{"executable":false,"path":"src/cli/claude/send.tsx","sha256":"d650caadec248aa59530d124ca6186c45d9f9fe1fb0f9c805e9eef6dc9f06d92"},{"executable":false,"path":"src/cli/codex/bridge/respond.tsx","sha256":"9d3a6af4904af6e44583dfb4ef317b3fe1045a78e765fc27406eaca9bcadcd13"},{"executable":false,"path":"src/cli/codex/bridge/run.tsx","sha256":"a49d334bebb00db62ad2c54370a47ec2c9119b4c07eb6ba11fc4bba4dd8e4217"},{"executable":false,"path":"src/cli/codex/bridge/start.tsx","sha256":"9e423cf5d65168606f6235baa2bcd127eb949647f7a4dee284d75f831474524d"},{"executable":false,"path":"src/cli/codex/bridge/status.tsx","sha256":"c801f69080985c87025429fb136487acdfac093acb07241b3417ea9a83915a18"},{"executable":false,"path":"src/cli/codex/bridge/stop.tsx","sha256":"6710e298a21d124c442d5aa1a3744280e5cd4df93213797f2ea3c0654174184a"},{"executable":false,"path":"src/cli/codex/desktop-shim.tsx","sha256":"fa3d88d11895bc4a15700bd590c055aa392b2b0e0c1185c52da233a23589aba0"},{"executable":false,"path":"src/cli/codex/list-threads.tsx","sha256":"6d754db238457ec63af76c0b34af47f9dc3888b2bae7cebaa7ed01135c7946e0"},{"executable":false,"path":"src/cli/codex/new.tsx","sha256":"11d0658c1a3e7173d53b71446b184fccccd867baefc6f733e43a9d543ca26ec9"},{"executable":false,"path":"src/cli/codex/queue.tsx","sha256":"7993ac5b827042cd95041775b183fde2d112b5ba7d5c82098e778a44e473826d"},{"executable":false,"path":"src/cli/codex/send.tsx","sha256":"a1b30f053a9e65bac561cd04d1361e292e2c672bbf69a6d76d2bdda441c46757"},{"executable":false,"path":"src/cli/codex/status.tsx","sha256":"b8c31001cb2892c1bfccf502f9776ee06998eac4fc0356f92936fa2c34f272d8"},{"executable":false,"path":"src/cli/codex/wait.tsx","sha256":"0a4cc6c440ad496682031658bda375fee29f5c346d833618d4f143de29b351cd"},{"executable":false,"path":"src/cli/codex/watch.tsx","sha256":"3280f42d15941cfd00b3727d9e271972c39165bd28db8f56acca5664d777cb4d"},{"executable":false,"path":"src/cli/doctor.tsx","sha256":"c30ce4e57fe707d36de29700abf5a2c56834bb4fb3adf2a70bb1089b4150ddc8"},{"executable":false,"path":"src/cli/groups/add.tsx","sha256":"2a5de115852f0ca6a6638300bd5006862a02a2f1e2f527b6fd680f610c1d777a"},{"executable":false,"path":"src/cli/groups/create.tsx","sha256":"da3f2539f338063daf51b80d86c27b2752f0e4a0334078d11508e27981590fa3"},{"executable":false,"path":"src/cli/groups/delete.tsx","sha256":"2d4e91113a2c24cefefcfd464513918791592b5e8734a4ef901eabe42c9c7289"},{"executable":false,"path":"src/cli/groups/get.tsx","sha256":"efed88fb3ab7292d60d6532f1f966f784ba3376b7826c9f3e5028467f86241e9"},{"executable":false,"path":"src/cli/groups/list.tsx","sha256":"f3cd229d41dd389b767447a04267700ab2293eae87a878f7d88149cb5798f4a6"},{"executable":false,"path":"src/cli/groups/members.tsx","sha256":"9b9eaecb65ea995006eff1b24a287110d16bf4830a0b0d07221c2f1317853641"},{"executable":false,"path":"src/cli/groups/remove.tsx","sha256":"96dd85d1b9732681ce9dc126ac3ae562d3f287f4c28e36b9a8f8c301dc93f63d"},{"executable":false,"path":"src/cli/groups/set.tsx","sha256":"4f9d90db612f14ddbb6d2afa5e45b66f044aed661ff95415e567e1be3b6108c6"},{"executable":false,"path":"src/cli/groups/update.tsx","sha256":"f44305815ddb6e27286f97ef68c04cfa5548acd5f04b1aba10ee7ce3cdbfc851"},{"executable":false,"path":"src/cli/history.tsx","sha256":"5151b48113b4ba0516bacabfabcd48e3256ead807c34d914deb84f31ded0b062"},{"executable":false,"path":"src/cli/send.tsx","sha256":"fd16790cfcde1c15328c3979acc0664112d5cf40d3f00a24da906e00dd63153e"},{"executable":false,"path":"src/cli/skills/add.tsx","sha256":"fbde237d172c026d7912174e705c03cbc9d74dad7806fc58c6ceb43127c69ba6"},{"executable":false,"path":"src/cli/skills/list.tsx","sha256":"34f255687c69aaf598a1dc54220453b57c46ed9ab4a168648d9c7eca6535ebae"},{"executable":false,"path":"src/cli/skills/remove.tsx","sha256":"21662c80a62f276c2dfd840a328e9665c0c5f63959a239d4dd877372c46f60d0"},{"executable":false,"path":"src/cli/thread.tsx","sha256":"29ab775936fa2987cb789754a44888fc52bdaad07180c5471122dda508cfd0c2"},{"executable":false,"path":"src/commands/codex-send.md","sha256":"ef29096075a13a28be9c2ee7a6226f169ffd330a7f21698b3927273070a6086d"},{"executable":false,"path":"src/commands/codex-threads.md","sha256":"bacac58bb4e61766ea76f9abad71c25207192bc609ce3bf3f122686c00d674a4"},{"executable":false,"path":"src/commands/codex-wait.md","sha256":"ef224414fe7a9f2aaaae2fba8b83f0600cb3589dfc20920a69b29db3a5f2d880"},{"executable":false,"path":"src/core/app-session.js","sha256":"2b2eb53ec3f4c7ea0dccf269fa9720af6f1ce36b8ca36e889d2bb271a092717d"},{"executable":false,"path":"src/core/chatgpt-desktop/adapter.ts","sha256":"5a0a4f2cea9f1fc1a739c534aed3979c28e0bd9c057b8475e6251974ad44ed7a"},{"executable":false,"path":"src/core/chatgpt-desktop/app-server-fallback.ts","sha256":"44002096eea29b5ea26d5b3d186b5616fcdab570888e3cb86fd4b5c5804af04c"},{"executable":false,"path":"src/core/chatgpt-desktop/cdp-adapter.ts","sha256":"db60fdd3be0259a6c76cad7d8ff86eddc6acbbd21c5f20d6aeffff5390063efa"},{"executable":false,"path":"src/core/chatgpt-desktop/cdp-dom.ts","sha256":"a122fb76626006fa8dedb7088f9b5533a8058c3a58838692b168e0753b7c89eb"},{"executable":false,"path":"src/core/chatgpt-desktop/cdp-session.ts","sha256":"8d6512ee80b179eedf54504110c1e88c5233b0b922667b00f87751452164cc00"},{"executable":false,"path":"src/core/chatgpt-desktop/errors.ts","sha256":"941ce4a9148fc53086cce64336dba473c1f7e4138774fd50a8ae524dcc05d34d"},{"executable":false,"path":"src/core/chatgpt-desktop/facade.ts","sha256":"bc9ac3bfd24db8458af6d20893fb3ab2ed701778f709aadbd141d61ed6c6c29b"},{"executable":false,"path":"src/core/chatgpt-desktop/index.ts","sha256":"591e6968c008cc75827da94b91c0ea15e573640876814e35e2470342dc8bccf9"},{"executable":false,"path":"src/core/chatgpt-desktop/loopback.ts","sha256":"b746681e5c45c67445e6620f850eff7c4627e13979747a7f686374c10a51f52f"},{"executable":false,"path":"src/core/chatgpt-desktop/payload-budget.ts","sha256":"89410474315b3769bad58d3ab61999e169410b7d1fdc20819efb4ee9c82fe1ac"},{"executable":false,"path":"src/core/chatgpt-desktop/project-state.ts","sha256":"b33e55b6d6abf15f3d7d1489b8b178d4e907ea83ed1a47d88f8f178a574709f8"},{"executable":false,"path":"src/core/chatgpt-desktop/relaunch.ts","sha256":"14851e115de7c45e256c4b9c3829dc4e796f1d989cf67d65115701a30b6d3a81"},{"executable":false,"path":"src/core/chatgpt-desktop/remote-threads.ts","sha256":"ed93418ff5d2bb51016783e7b0a458805e709b212974a66be804ddb9f7038033"},{"executable":false,"path":"src/core/chatgpt-desktop/routes.ts","sha256":"ccf8cde780da6e6e03f11e4dcc0d9d6bc4a6a3fd20d3210e74f0217e3660b7e1"},{"executable":false,"path":"src/core/chatgpt-desktop/thread-ids.ts","sha256":"0fe25e9d32c078bd9e7a171ab4bc4ee0b15190cd6d6c4d3b345926b3c608e082"},{"executable":false,"path":"src/core/chatgpt-desktop/types.ts","sha256":"59f89694eed34206e32db2a0afb48670bb164a4ffffacb907c8629bb0ce4bcca"},{"executable":false,"path":"src/core/claude-channel.js","sha256":"01ac7daef748a11bc99e132950ed7c59f50442e417eecf894b59cb4bb484202f"},{"executable":false,"path":"src/core/claude-routes.ts","sha256":"d34ddf25b8634ef86d46ab663f30a49aaedf87f8dc98e4036f84c1c88efa4083"},{"executable":false,"path":"src/core/codex-bridge.js","sha256":"78f0fbba7a5053eb78717c5885a9306871f08beaf39da90df1f815a66b1288d9"},{"executable":false,"path":"src/core/codex/contract.js","sha256":"16bcbd0fc09844166c52694ebb1075e60b91296a8f04fb02ad061c30b0e9c591"},{"executable":false,"path":"src/core/codex/conversation.js","sha256":"05abb2253480d0793bf5f6b3db17c47e7d154973747074c7c76a79e6f61b7deb"},{"executable":false,"path":"src/core/codex/remote-control.js","sha256":"52565007799e98c3bff4297287d8e53005e285a95bab9e4e4d0a088bded4fa8e"},{"executable":false,"path":"src/core/codex/routes.ts","sha256":"ae29d05e06cdd8e90bcdd723d277af748e16dc676db1e4b938b7cdd36340ed17"},{"executable":false,"path":"src/core/codex/thread-id.js","sha256":"4eb6c1929c2e7687ed6c1422789fa01c3e1c556ead7d16355eafbae46a7126f4"},{"executable":false,"path":"src/core/commands.js","sha256":"3ad8fafb86744a57992da93ad838ade0e9acb1b7ca09b829cefadbf2781be0c9"},{"executable":false,"path":"src/core/desktop-shim-bridge.js","sha256":"58f3f39b8527adf58f57313e02e34322b203b8c38d1acfbad419a8b9af214443"},{"executable":false,"path":"src/core/desktop-shim.js","sha256":"500b8617bade6fa1a9c395826e197892fb864067044cac80ab025849f8fd6f82"},{"executable":false,"path":"src/core/format.js","sha256":"83ee15f73a87ef24b75f7782f3b61dd4ef4cc1f2e01e38cdf19825b805fab3be"},{"executable":false,"path":"src/core/gateway.js","sha256":"3a4f838ee423600d29e05f58060dcaf81cdcea4ac93b36dd6108363ac24bc606"},{"executable":false,"path":"src/core/grok-approval-routes.ts","sha256":"a33daa4d05e0c2594c795bd7654c22f3e1003e4aaad41cd70034d03651dd01f9"},{"executable":false,"path":"src/core/grok-approvals.js","sha256":"6b651a65bdd17241dd5b7a01d94a318ece9356c1c9b965b0578e08250d74e8fe"},{"executable":false,"path":"src/core/headers.js","sha256":"e229f2826ebdfb52949dab500798c0fa8a5ff7c4e63de91a63103bf45d2ae3cc"},{"executable":false,"path":"src/core/history.js","sha256":"38ca8e8f8dcd2ad42952c424ea9f00be5972d8751e4c0ac87214171903d82d89"},{"executable":false,"path":"src/core/relay/codex.js","sha256":"ff6e8f3158d171911a35b829cd76e1f5fd8319f8143f727a4854448fc09edeec"},{"executable":false,"path":"src/core/relay/completion.js","sha256":"9d1a47f92716d1ccc7c4f44d92a580084f3b622c6ac0f20c6391be81abeb56e4"},{"executable":false,"path":"src/core/relay/control.js","sha256":"3645540f9f799b8742e0d5782b6324454a1486a780f6cb97260b96ef62411771"},{"executable":false,"path":"src/core/relay/engine.js","sha256":"205dd43b337439a56c7b1521b2603679ebc7fd3b2da430f8a477f83549efd453"},{"executable":false,"path":"src/core/relay/intake.js","sha256":"b31146fe3c9edd2f09323a454687403666591b6c12483a55b07ac610d2e3610a"},{"executable":false,"path":"src/core/relay/interactions.js","sha256":"6b576ed3a5b6659bc17e384d26e40d6ccb41b118b3a91cf5c902e9758ccbc1b1"},{"executable":false,"path":"src/core/relay/managed.js","sha256":"dbe03ed964bd09739e580de73b7e9f90508062fa2e53b8244c6ca7f78fc7a60d"},{"executable":false,"path":"src/core/relay/ownership.js","sha256":"74805f04f6a8bc02b2951614c4814aa42f54aaa3abca027a2413ed5a54814caf"},{"executable":false,"path":"src/core/relay/profile.js","sha256":"3347d1d493d3470d82722468d31571464d8e11aa0b4deac5bda75f574078dfe5"},{"executable":false,"path":"src/core/relay/records.js","sha256":"5b80f7dfe7e438811d9f27c1f9038d7631f8c73266d2ec7b1108c1ce9ecbbb47"},{"executable":false,"path":"src/core/relay/routes.ts","sha256":"4007e9908e285036786b7b0291dd480d03d51ad03f973fa578444accffa1f416"},{"executable":false,"path":"src/core/relay/state.js","sha256":"c18eda2f956944d0f318910de5239b954090eab81f7c77b4683e326a6d34b2b8"},{"executable":false,"path":"src/core/relay/worker.js","sha256":"4176ad91872f36f7ea6565d9f30d23d9d7a80ef27d7af975af760457132ba148"},{"executable":false,"path":"src/core/store.js","sha256":"caefeeebf686cecd37ef19585ef0a5d3acba9dbf1564fe128c3afe4de067305c"},{"executable":false,"path":"src/core/transcript.js","sha256":"0355f0978d2d126b90858e3f661130243fc4730ee81ba13080af1dfa39d8b081"},{"executable":false,"path":"src/core/url-policy.js","sha256":"62bf6d3c8af1f5840d5e2d682d8f2e52fcbfbee6695728b47ed31816f1736089"},{"executable":false,"path":"src/core/user-machine-guidance.js","sha256":"c8409ae9679246d8f907f1e0d16ec1c415d6697f2ce048fb64a599734486b9a6"},{"executable":false,"path":"src/gbot-install.ts","sha256":"e3c833d7662610e70e65232e541017229fb9a049eacee0bdf31e2c33c6ab7189"},{"executable":false,"path":"src/gbot.ts","sha256":"c89a37b764956ceacb2917a991a6d82d987ed8d8e453c8d553485bbd7f3edcc0"},{"executable":false,"path":"src/mcp/claude-channel.ts","sha256":"2d52e0d862a492026b6cb408e3177c994996e2b4292903da5bbf5bc191019913"},{"executable":false,"path":"src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx","sha256":"86334421686350ea044fb1160c19b6f5889425feb78fd608675feb8137492090"},{"executable":false,"path":"src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx","sha256":"5f24cfdace92522891dfbaed0bd90e74b7de6bb8d193e0ef3bd5582c05a06914"},{"executable":false,"path":"src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx","sha256":"b415d5bd303693649abae96b6b9d2920d29c797f5367ae8fbb2b62b635528e37"},{"executable":false,"path":"src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx","sha256":"6adc4318f4b6c90b1d3970046266197348f79757c0d52ffe991d6338eb1fed6c"},{"executable":false,"path":"src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx","sha256":"1d50db88cf7d06a74524c8937ed9045eaaf2560f6766d6c411b3bb55575d1d3f"},{"executable":false,"path":"src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx","sha256":"9e1b93735b9a38216c7e9aac4c5b62bc2ac20b60e1979a19c4610f0208961af5"},{"executable":false,"path":"src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx","sha256":"681e50ae6c7d3a04a76ad8eadf865ab9d793b5eb3c78f961a42d0902adacc306"},{"executable":false,"path":"src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx","sha256":"2786b62ff2a1a4e68229199935ac63c15614a0f0bdbef74b2d06e0c405516272"},{"executable":false,"path":"src/mcp/grok-bot/tools/claude_send.tsx","sha256":"c02924049f370d9cb5ae459d6275c6cae2c9f4f1328cb5fa8e2f3e6d12a6d656"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_new.tsx","sha256":"556c8d6f5f2b07b20e9daae7aa8008ee3775582a4d034415fc86fecda0c63866"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_send.tsx","sha256":"5c02dc335a0354850d05e5827f6124d0ddc08b9b654059984887c55e4cd778c9"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_threads.tsx","sha256":"0164fc0c7b49df6524d56380763349cdd5ca26c68cffa5979b4e3346593ba2e4"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_wait.tsx","sha256":"cb08b094860f3a09c937eb8339108015fe0de57d9f6c6886aea2028cd33ef207"},{"executable":false,"path":"src/mcp/grok-bot/tools/codex_watch.tsx","sha256":"1bc603d36ea7542468e7e3eadbb86072990282e09e209f13ebfc64389d207c49"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_bridge_start.tsx","sha256":"c75c5c8a30ec43709a4d32baa8c3bc9796595381bb04fd2499ca30cd515c5a0b"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_bridge_status.tsx","sha256":"486ce643b35cfa2eabf78f0deeabce7198adc66133669eec630e1d12206ad642"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_bridge_stop.tsx","sha256":"371428a6e4b4e1312dd95d9681c29e765aec55e65d80d7c474535df838a43457"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_codex_respond.tsx","sha256":"030e9c2081ed4ed40732b098c929022af3083f609f10934b35e1f428d33ae637"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_grok_approvals.tsx","sha256":"17f164acdc48d6bc79a4c5044c7082760dd2a1646e5b56f6bff54c53fed96451"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_grok_respond.tsx","sha256":"41964bb8cf0eb0f8493afa1dde16cd94c4f589d1a7e410577d1ed63ec4648542"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_send.tsx","sha256":"2101d279a0e6bda2d57ba3da5366411df460a95692d4e64a1096880a89121d9a"},{"executable":false,"path":"src/mcp/grok-bot/tools/gbot_thread.tsx","sha256":"ef4aefec7243efaad18335e8508c7c17af0cd9b0f8a4b3693a7af9f666807fd6"},{"executable":false,"path":"src/rules/codex-claude-on-user-machines.mdc","sha256":"a50ec93d021a807e0a18bf9311cdf103bc0ae3947e4c6a0bd78cbb941e8e5b24"},{"executable":false,"path":"src/scripts/gbot-relay.ts","sha256":"6690e316189ed8ebba43ea67a495338c927e4c0c21a9f896b7b2fecc8c6eb656"},{"executable":false,"path":"src/skills/chatgpt-desktop/SKILL.md","sha256":"0fe2560037f761203a317bf8755b0476ce864b7b1b13708559448d300aa4635c"},{"executable":false,"path":"src/skills/talk-to-grok-bot/references/bridge-administration.md","sha256":"3fc7d335822d9021cbd3770af6d7e95734d9213cd7331f41602598911c15ddf6"},{"executable":false,"path":"src/skills/talk-to-grok-bot/SKILL.md","sha256":"b80a88ea029564447a79dbc1a533191dc4839791368c6248c904bcfaf3ccb770"},{"executable":false,"path":"test/app-session.test.js","sha256":"0756b691703dd8c84bf0823f2afab16c6b158398e4ce6be229db6f459a0bbdb1"},{"executable":false,"path":"test/claude-channel.test.js","sha256":"c63337698001b9739414203d70b6d54d0794ec5970bd90d327923f50ab2355dc"},{"executable":false,"path":"test/codex-bridge.test.js","sha256":"55a1afcdea07f325b3528dd0c546ecc4cbfd6069610ecc4f773f049cd597e69c"},{"executable":false,"path":"test/codex-commands.test.js","sha256":"ab15a53a76eb82dc9c51c9fabd0affd2a1037cde27765fef08ecd0012a2e4495"},{"executable":false,"path":"test/codex-contract.test.js","sha256":"926c7609679d8b6ac3b568d56ba5efa20df86d016a9031f085d19adbfb0e9064"},{"executable":false,"path":"test/codex-conversation.test.js","sha256":"789d0245a8d470860f7535c6ef3849313205097f6df517ab9946e47a789f8534"},{"executable":false,"path":"test/codex-session.test.js","sha256":"b39fe6afe0784b16e37b208089279dea01095d33e06ae26a1b60871796c7738a"},{"executable":false,"path":"test/codex-surfaces.test.js","sha256":"56d1b6550cc69d462a05e892cf82aa6a41495857b753215104e03207db514c81"},{"executable":false,"path":"test/connect-gateway.test.js","sha256":"5fabd87640b39e75c218300ca6be278268f248f3ff75e8cb72f9fade9eb0f7f3"},{"executable":false,"path":"test/desktop-shim.test.js","sha256":"89fb6c38a5fca5d41a96434cf9e8d51ae1d9992cf4f7930df0cc5840ef985705"},{"executable":false,"path":"test/doctor.test.js","sha256":"9685885df9e7363d3e077d246b1a17803c8899e9613b4a4d52ee4b9275e20694"},{"executable":false,"path":"test/gateway-groups.test.js","sha256":"14e227453e3e28c7cb60f91dba55733834c023ce9a914e40e22c68c899512bdc"},{"executable":false,"path":"test/gateway-send.test.js","sha256":"d059166153dde975f457e4e8c32ea08b0fc857b58fd48eab7da1959638e4fb05"},{"executable":false,"path":"test/gateway-skills.test.js","sha256":"feed62a884d1b7d12e358a27d73fb5587bcf761d4867ab1755d8bc34a4380069"},{"executable":false,"path":"test/gateway.test.js","sha256":"e5a7acd7ae347c82ef1e2a6000562767168e56d6f9d86aca8da46141731d1309"},{"executable":false,"path":"test/grok-approvals.test.js","sha256":"5ad4c548060a08dbe57949ab5ae240383fdb1709cd3694a909f5e1354fee3f73"},{"executable":false,"path":"test/helpers/codex-server.js","sha256":"432b4570601e94cf08c4861133d328404a97066642edb7ec0dbc314411a4d848"},{"executable":false,"path":"test/history.test.js","sha256":"eac4811f1b12d427df66996f33dd46bc3cbe31fc9fa030b3f32c571d5379f8dd"},{"executable":false,"path":"test/host-tool-inventory.test.js","sha256":"fa44f15f46d0cae486c13be3fae09568f17f8581001ec50a61478bec03f4c28c"},{"executable":false,"path":"test/pr69-integration.test.js","sha256":"a5dc99c11e2d3fcf18233c595f9c28301e5939f4b3657125eb3f970df2598840"},{"executable":false,"path":"test/relay-auth-recovery.test.js","sha256":"399c064f550fe33e166187a51da527728a97946d213377c8fcc8231378f5bbab"},{"executable":false,"path":"test/relay-codex.test.js","sha256":"43cb313e748e187f798a8a0bce5a01cce9ba360a52985b05ecededee9ad10581"},{"executable":false,"path":"test/relay-completion.test.js","sha256":"a8cc2402f82ce4b458074eafd32c8fa18f7ced7d85fdb6b7fa68f7b18b6a8c2e"},{"executable":false,"path":"test/relay-engine.test.js","sha256":"a8c06138d895ffaa30476f3a2ddf1ef50ef73a816a665f749f451d01d9cc2551"},{"executable":false,"path":"test/relay-gateway-lifecycle.test.js","sha256":"e16e7a34f7b6bb07965003e3018aa3a43ff192602db1c177bec9b6268fddb78b"},{"executable":false,"path":"test/relay-interactions.test.js","sha256":"83d70b1a813923fc6bae02b18d328107d0d04a6a761ab73b2051e3d114c7eaa5"},{"executable":false,"path":"test/relay-lifecycle.test.js","sha256":"e90130e1f609839bfd3f74c3edc333a22f37fe20dcde0146daa8d497133da4b7"},{"executable":false,"path":"test/relay-state.test.js","sha256":"c9143d5fa150b1ebcef9d515e161e25af3370b71eaf800a7b2bb1da1678b0bd3"},{"executable":false,"path":"test/relay-surfaces.test.js","sha256":"ba17ac4f1ffc02d4e06cd73eda143422a3d4d2f750630af9ed4e828dc9d7d2be"},{"executable":false,"path":"test/relay-worker.test.js","sha256":"6bd1bdaaaae59997132f289f822b6c4b8c9616c268fb2b45861e03f697c08197"},{"executable":false,"path":"test/release-config.test.js","sha256":"f3078277086d722f21d07e4134e10b89d2e849a94a0fa40f30025a5cb6fa6a15"},{"executable":false,"path":"test/store.test.js","sha256":"5d4ae4295287539ef4b7f98d0ca0cd42a931a3c7b25b092fbd2b8f1a08ca4bac"},{"executable":false,"path":"test/test-runner.test.js","sha256":"966ff3b67edb8e1f03b50120d4d820ab0f871121e9b359609e73faae8446454f"},{"executable":false,"path":"test/transcript.test.js","sha256":"28795d9cc8e42653ac430d18e17ff1e6bc2feb1b7935fe5c69b336d58e1793ae"},{"executable":false,"path":"test/url-policy.test.js","sha256":"dacfe322e0cbff178c0d67a5f76b6bd8d9fef14aa57c09c697eb69fa0019629c"},{"executable":false,"path":"tests/fixtures/codex-global-state-remote.json","sha256":"e59377d0f03cb24d57b55558e9c91dd363619a8a92566835f1ad49b639f99525"},{"executable":false,"path":"tests/route-unit/chatgpt-desktop-e2e-regressions.test.ts","sha256":"9cfc7f538899954970fe643c13571f03d417450c519525e91d382a0b5f0b086a"},{"executable":false,"path":"tests/route-unit/chatgpt-desktop-mcp-followup.test.ts","sha256":"4a2db11a642b8daef05285f096ce026ca5b9b6394c31c0966d7e5f8c1d49f4ca"},{"executable":false,"path":"tests/route-unit/chatgpt-desktop-review.test.ts","sha256":"208dfdea18ccc408a279666a521d2f70fdce9bdaf28f86bc5d053f7ad09a7dd2"},{"executable":false,"path":"tests/route-unit/chatgpt-desktop.test.ts","sha256":"5cf61e174b1d4a7c0f2209d6746bb1880a96fe550f7d2c4ea94e849dd7f21299"},{"executable":false,"path":"tests/route-unit/tools.test.ts","sha256":"7c7d971edaa7ef134d0dfba12208f88ec7f69c11dca79ad61d6e6cfae6d34c43"},{"executable":false,"path":"tsconfig.json","sha256":"fa2ced6d1721e8280a6aed615717cffcef25f6e2d16e15889b2be2a9df4fc226"}]},"provenance":[{"path":".agents/plugins/marketplace.json","sourceInputs":["agent-bundle.config.ts"]},{"path":".claude-plugin/marketplace.json","sourceInputs":["agent-bundle.config.ts","package.json"]},{"path":".claude-plugin/plugin.json","sourceInputs":["agent-bundle.config.ts","src/mcp/claude-channel.ts","src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx","src/skills/chatgpt-desktop/SKILL.md","src/skills/talk-to-grok-bot/SKILL.md"]},{"path":".codex-plugin/mcp.json","sourceInputs":["agent-bundle.config.ts","src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx"]},{"path":".codex-plugin/plugin.json","sourceInputs":["agent-bundle.config.ts","package.json","src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx","src/skills/chatgpt-desktop/SKILL.md","src/skills/talk-to-grok-bot/SKILL.md"]},{"path":".cursor-plugin/marketplace.json","sourceInputs":["agent-bundle.config.ts"]},{"path":".cursor-plugin/mcp.json","sourceInputs":["agent-bundle.config.ts","src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx"]},{"path":".cursor-plugin/plugin.json","sourceInputs":["agent-bundle.config.ts","package.json","src/commands/codex-send.md","src/commands/codex-threads.md","src/commands/codex-wait.md","src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx","src/rules/codex-claude-on-user-machines.mdc","src/skills/chatgpt-desktop/SKILL.md","src/skills/talk-to-grok-bot/SKILL.md"]},{"path":".mcp.json","sourceInputs":["agent-bundle.config.ts","src/mcp/claude-channel.ts","src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx"]},{"path":"agent-bundle.compile-evidence.json","sourceInputs":["agent-bundle.config.ts"]},{"path":"bin/gbot-flight.mjs","sourceInputs":["agent-bundle.config.ts","package.json","src/cli/_shared.ts","src/cli/approvals/list.tsx","src/cli/approvals/respond.tsx","src/cli/bots/create.tsx","src/cli/bots/delete.tsx","src/cli/bots/get.tsx","src/cli/bots/list.tsx","src/cli/bots/update.tsx","src/cli/chatgpt-desktop/hosts.tsx","src/cli/chatgpt-desktop/read.tsx","src/cli/chatgpt-desktop/search.tsx","src/cli/chatgpt-desktop/send.tsx","src/cli/chatgpt-desktop/status.tsx","src/cli/chatgpt-desktop/threads.tsx","src/cli/claude/send.tsx","src/cli/codex/bridge/respond.tsx","src/cli/codex/bridge/run.tsx","src/cli/codex/bridge/start.tsx","src/cli/codex/bridge/status.tsx","src/cli/codex/bridge/stop.tsx","src/cli/codex/desktop-shim.tsx","src/cli/codex/list-threads.tsx","src/cli/codex/new.tsx","src/cli/codex/queue.tsx","src/cli/codex/send.tsx","src/cli/codex/status.tsx","src/cli/codex/wait.tsx","src/cli/codex/watch.tsx","src/cli/doctor.tsx","src/cli/groups/add.tsx","src/cli/groups/create.tsx","src/cli/groups/delete.tsx","src/cli/groups/get.tsx","src/cli/groups/list.tsx","src/cli/groups/members.tsx","src/cli/groups/remove.tsx","src/cli/groups/set.tsx","src/cli/groups/update.tsx","src/cli/history.tsx","src/cli/send.tsx","src/cli/skills/add.tsx","src/cli/skills/list.tsx","src/cli/skills/remove.tsx","src/cli/thread.tsx","src/core/app-session.js","src/core/chatgpt-desktop/app-server-fallback.ts","src/core/chatgpt-desktop/cdp-adapter.ts","src/core/chatgpt-desktop/cdp-dom.ts","src/core/chatgpt-desktop/cdp-session.ts","src/core/chatgpt-desktop/errors.ts","src/core/chatgpt-desktop/facade.ts","src/core/chatgpt-desktop/loopback.ts","src/core/chatgpt-desktop/payload-budget.ts","src/core/chatgpt-desktop/project-state.ts","src/core/chatgpt-desktop/remote-threads.ts","src/core/chatgpt-desktop/routes.ts","src/core/chatgpt-desktop/thread-ids.ts","src/core/claude-channel.js","src/core/claude-routes.ts","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/codex/routes.ts","src/core/codex/thread-id.js","src/core/commands.js","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/format.js","src/core/gateway.js","src/core/grok-approval-routes.ts","src/core/grok-approvals.js","src/core/headers.js","src/core/history.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/relay/routes.ts","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/gbot.ts"]},{"path":"bin/gbot.mjs","sourceInputs":["agent-bundle.config.ts","package.json","src/cli/_shared.ts","src/cli/approvals/list.tsx","src/cli/approvals/respond.tsx","src/cli/bots/create.tsx","src/cli/bots/delete.tsx","src/cli/bots/get.tsx","src/cli/bots/list.tsx","src/cli/bots/update.tsx","src/cli/chatgpt-desktop/hosts.tsx","src/cli/chatgpt-desktop/read.tsx","src/cli/chatgpt-desktop/search.tsx","src/cli/chatgpt-desktop/send.tsx","src/cli/chatgpt-desktop/status.tsx","src/cli/chatgpt-desktop/threads.tsx","src/cli/claude/send.tsx","src/cli/codex/bridge/respond.tsx","src/cli/codex/bridge/run.tsx","src/cli/codex/bridge/start.tsx","src/cli/codex/bridge/status.tsx","src/cli/codex/bridge/stop.tsx","src/cli/codex/desktop-shim.tsx","src/cli/codex/list-threads.tsx","src/cli/codex/new.tsx","src/cli/codex/queue.tsx","src/cli/codex/send.tsx","src/cli/codex/status.tsx","src/cli/codex/wait.tsx","src/cli/codex/watch.tsx","src/cli/doctor.tsx","src/cli/groups/add.tsx","src/cli/groups/create.tsx","src/cli/groups/delete.tsx","src/cli/groups/get.tsx","src/cli/groups/list.tsx","src/cli/groups/members.tsx","src/cli/groups/remove.tsx","src/cli/groups/set.tsx","src/cli/groups/update.tsx","src/cli/history.tsx","src/cli/send.tsx","src/cli/skills/add.tsx","src/cli/skills/list.tsx","src/cli/skills/remove.tsx","src/cli/thread.tsx","src/core/app-session.js","src/core/chatgpt-desktop/app-server-fallback.ts","src/core/chatgpt-desktop/cdp-adapter.ts","src/core/chatgpt-desktop/cdp-dom.ts","src/core/chatgpt-desktop/cdp-session.ts","src/core/chatgpt-desktop/errors.ts","src/core/chatgpt-desktop/facade.ts","src/core/chatgpt-desktop/loopback.ts","src/core/chatgpt-desktop/payload-budget.ts","src/core/chatgpt-desktop/project-state.ts","src/core/chatgpt-desktop/remote-threads.ts","src/core/chatgpt-desktop/routes.ts","src/core/chatgpt-desktop/thread-ids.ts","src/core/claude-channel.js","src/core/claude-routes.ts","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/codex/routes.ts","src/core/codex/thread-id.js","src/core/commands.js","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/format.js","src/core/gateway.js","src/core/grok-approval-routes.ts","src/core/grok-approvals.js","src/core/headers.js","src/core/history.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/relay/routes.ts","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/gbot.ts"]},{"path":"commands/codex-send.md","sourceInputs":["src/commands/codex-send.md"]},{"path":"commands/codex-threads.md","sourceInputs":["src/commands/codex-threads.md"]},{"path":"commands/codex-wait.md","sourceInputs":["src/commands/codex-wait.md"]},{"path":"INSTALL.md","sourceInputs":["agent-bundle.config.ts"]},{"path":"install.mjs","sourceInputs":["agent-bundle.config.ts"]},{"path":"mcp.json","sourceInputs":["src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx"]},{"path":"mcp/mcp-claude-channel-8029413c.mjs","sourceInputs":["src/core/claude-channel.js","src/core/user-machine-guidance.js","src/mcp/claude-channel.ts"]},{"path":"mcp/mcp-grok-bot-b8c2461e-flight.mjs","sourceInputs":["package.json","src/core/app-session.js","src/core/chatgpt-desktop/app-server-fallback.ts","src/core/chatgpt-desktop/cdp-adapter.ts","src/core/chatgpt-desktop/cdp-dom.ts","src/core/chatgpt-desktop/cdp-session.ts","src/core/chatgpt-desktop/errors.ts","src/core/chatgpt-desktop/facade.ts","src/core/chatgpt-desktop/loopback.ts","src/core/chatgpt-desktop/payload-budget.ts","src/core/chatgpt-desktop/project-state.ts","src/core/chatgpt-desktop/remote-threads.ts","src/core/chatgpt-desktop/routes.ts","src/core/chatgpt-desktop/thread-ids.ts","src/core/claude-channel.js","src/core/claude-routes.ts","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/codex/routes.ts","src/core/codex/thread-id.js","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/gateway.js","src/core/grok-approval-routes.ts","src/core/grok-approvals.js","src/core/headers.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/relay/routes.ts","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/gbot.ts","src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx","src/mcp/grok-bot/tools/claude_send.tsx","src/mcp/grok-bot/tools/codex_new.tsx","src/mcp/grok-bot/tools/codex_send.tsx","src/mcp/grok-bot/tools/codex_threads.tsx","src/mcp/grok-bot/tools/codex_wait.tsx","src/mcp/grok-bot/tools/codex_watch.tsx","src/mcp/grok-bot/tools/gbot_bridge_start.tsx","src/mcp/grok-bot/tools/gbot_bridge_status.tsx","src/mcp/grok-bot/tools/gbot_bridge_stop.tsx","src/mcp/grok-bot/tools/gbot_codex_respond.tsx","src/mcp/grok-bot/tools/gbot_grok_approvals.tsx","src/mcp/grok-bot/tools/gbot_grok_respond.tsx","src/mcp/grok-bot/tools/gbot_send.tsx","src/mcp/grok-bot/tools/gbot_thread.tsx"]},{"path":"mcp/mcp-grok-bot-b8c2461e.mjs","sourceInputs":["package.json","src/core/app-session.js","src/core/chatgpt-desktop/app-server-fallback.ts","src/core/chatgpt-desktop/cdp-adapter.ts","src/core/chatgpt-desktop/cdp-dom.ts","src/core/chatgpt-desktop/cdp-session.ts","src/core/chatgpt-desktop/errors.ts","src/core/chatgpt-desktop/facade.ts","src/core/chatgpt-desktop/loopback.ts","src/core/chatgpt-desktop/payload-budget.ts","src/core/chatgpt-desktop/project-state.ts","src/core/chatgpt-desktop/remote-threads.ts","src/core/chatgpt-desktop/routes.ts","src/core/chatgpt-desktop/thread-ids.ts","src/core/claude-channel.js","src/core/claude-routes.ts","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/codex/routes.ts","src/core/codex/thread-id.js","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/gateway.js","src/core/grok-approval-routes.ts","src/core/grok-approvals.js","src/core/headers.js","src/core/relay/control.js","src/core/relay/managed.js","src/core/relay/profile.js","src/core/relay/routes.ts","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/gbot.ts","src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx","src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx","src/mcp/grok-bot/tools/claude_send.tsx","src/mcp/grok-bot/tools/codex_new.tsx","src/mcp/grok-bot/tools/codex_send.tsx","src/mcp/grok-bot/tools/codex_threads.tsx","src/mcp/grok-bot/tools/codex_wait.tsx","src/mcp/grok-bot/tools/codex_watch.tsx","src/mcp/grok-bot/tools/gbot_bridge_start.tsx","src/mcp/grok-bot/tools/gbot_bridge_status.tsx","src/mcp/grok-bot/tools/gbot_bridge_stop.tsx","src/mcp/grok-bot/tools/gbot_codex_respond.tsx","src/mcp/grok-bot/tools/gbot_grok_approvals.tsx","src/mcp/grok-bot/tools/gbot_grok_respond.tsx","src/mcp/grok-bot/tools/gbot_send.tsx","src/mcp/grok-bot/tools/gbot_thread.tsx"]},{"path":"plugin.json","sourceInputs":["agent-bundle.config.ts","package.json"]},{"path":"rules/codex-claude-on-user-machines.mdc","sourceInputs":["src/rules/codex-claude-on-user-machines.mdc"]},{"path":"scripts/gbot-relay.mjs","sourceInputs":["package.json","src/core/app-session.js","src/core/codex-bridge.js","src/core/codex/contract.js","src/core/codex/conversation.js","src/core/codex/remote-control.js","src/core/codex/thread-id.js","src/core/desktop-shim-bridge.js","src/core/desktop-shim.js","src/core/gateway.js","src/core/grok-approvals.js","src/core/headers.js","src/core/relay/codex.js","src/core/relay/completion.js","src/core/relay/control.js","src/core/relay/engine.js","src/core/relay/intake.js","src/core/relay/interactions.js","src/core/relay/ownership.js","src/core/relay/profile.js","src/core/relay/records.js","src/core/relay/state.js","src/core/relay/worker.js","src/core/store.js","src/core/transcript.js","src/core/url-policy.js","src/core/user-machine-guidance.js","src/scripts/gbot-relay.ts"]},{"path":"skills/chatgpt-desktop/SKILL.md","sourceInputs":["src/skills/chatgpt-desktop/SKILL.md"]},{"path":"skills/talk-to-grok-bot/references/bridge-administration.md","sourceInputs":["src/skills/talk-to-grok-bot/references/bridge-administration.md","src/skills/talk-to-grok-bot/SKILL.md"]},{"path":"skills/talk-to-grok-bot/SKILL.md","sourceInputs":["src/skills/talk-to-grok-bot/SKILL.md"]}],"recordVersion":1,"validation":{"artifact":{"status":"passed"},"projections":[{"host":"claude","status":"passed"},{"host":"codex","status":"passed"},{"host":"cursor","status":"passed"},{"host":"portable","status":"passed"}],"source":{"status":"passed"}}},"distribution":{"channels":["local","npm"],"install":{"instructions":"INSTALL.md","script":"install.mjs"},"payloads":[]},"executables":{"bins":[{"hosts":["claude","codex","cursor","portable"],"name":"gbot","path":"bin/gbot.mjs","worker":"bin/gbot-flight.mjs"}],"hooks":[],"mcpServers":[{"apps":[],"hosts":["claude"],"id":"mcp:claude-channel","kind":"compiled","launch":{"args":[],"entry":"mcp/mcp-claude-channel-8029413c.mjs","env":{}},"name":"claude-channel","transport":"stdio"},{"apps":[],"hosts":["claude","codex","cursor","portable"],"id":"mcp:grok-bot","kind":"compiled","launch":{"args":[],"entry":"mcp/mcp-grok-bot-b8c2461e.mjs","env":{},"worker":"mcp/mcp-grok-bot-b8c2461e-flight.mjs"},"name":"grok-bot","transport":"stdio"}],"scripts":[{"hosts":["claude","codex","cursor","portable"],"id":"script:gbot-relay","mode":"bundle","name":"gbot-relay","path":"scripts/gbot-relay.mjs"}]},"files":[{"bytes":225,"kind":"generated","path":".agents/plugins/marketplace.json","sha256":"54d575719e003eb71ba2d7fb7379e12098243e701646e0ca83dba38ef2a78b1d"},{"bytes":926,"kind":"generated","path":".claude-plugin/marketplace.json","sha256":"a8f5bf1f109d8f88b4195f3baea389e942af2f0ef544ecc22502326bcf3e742e"},{"bytes":339,"kind":"generated","path":".claude-plugin/plugin.json","sha256":"1420b97dfe289c97c24e855ba758439c94fa0e92aa38aae467312fbe341d5d15"},{"bytes":156,"kind":"generated","path":".codex-plugin/mcp.json","sha256":"4b442017d0d30ec207cef7a4e5d2cd9dca246080008633459a27e3a5f2f7935b"},{"bytes":1310,"kind":"generated","path":".codex-plugin/plugin.json","sha256":"a3ab79672b9ab74affae23c25ef06ddc926da90d0ff2499e9471c3e4c6ba475f"},{"bytes":332,"kind":"generated","path":".cursor-plugin/marketplace.json","sha256":"297ba8aebcdb8bcac814b520bf21d9d6acc9c8c94a1ecf3ca442a49d834aab59"},{"bytes":169,"kind":"generated","path":".cursor-plugin/mcp.json","sha256":"95c87d0f2ed3a4f6df7d0e519454b1234672eca030db3b8cd8776ef0f35dee74"},{"bytes":694,"kind":"generated","path":".cursor-plugin/plugin.json","sha256":"12321577920803f3d60bf071167f8b7888d94d720d06c5caa38f3b30c8bfda73"},{"bytes":363,"kind":"generated","path":".mcp.json","sha256":"5c56ac183c3d4fcbfa9386a73ab805495c6ab613b96bf71dbd5be80093e1a10b"},{"bytes":22592,"kind":"generated","path":"agent-bundle.compile-evidence.json","sha256":"0a1a4dbdf3fcea9ed7e5f636963c12067c66ad28f0433b35a9b1d99385982c3b"},{"bytes":1592195,"kind":"bundle","path":"bin/gbot-flight.mjs","sha256":"b0a314b26965679b91a06be788337603c12006f3061beebb5f4a682e6e26a581"},{"bytes":2462573,"kind":"bundle","mode":493,"path":"bin/gbot.mjs","sha256":"1da1b6468f12930e44b9be5422ada37905e0ea0a558569d98d8da17df9e2362f"},{"bytes":290,"kind":"generated","path":"commands/codex-send.md","sha256":"6d188a21596115f4aa65e39718cede5eff837749c8652a12e2a5b25d8a3e0ef3"},{"bytes":195,"kind":"generated","path":"commands/codex-threads.md","sha256":"0db562fff831ede9dd300552aecc5b29d347c94ebeb3df9f488cb1a87b990cf8"},{"bytes":205,"kind":"generated","path":"commands/codex-wait.md","sha256":"fb04448a474b2f14ddb59739d5f79bcf251a3e1136ffe9b3e50ca5654aa6070b"},{"bytes":22762,"kind":"generated","path":"INSTALL.md","sha256":"5fdafbf8a3fa3d2205e6a308d37deb8ad31910927334a855f70e11977d43f6a9"},{"bytes":78560,"kind":"generated","path":"install.mjs","sha256":"d5c328ca82057cd05b90372a8cfa8ed3e8c6f7f1b14b1f579ae82250722c3d93"},{"bytes":246,"kind":"generated","path":"mcp.json","sha256":"c06a01fa851792f42e0cc807a444e454c3ea2fd164dc1c2adef455acc93879e2"},{"bytes":2270116,"kind":"bundle","path":"mcp/mcp-claude-channel-8029413c.mjs","sha256":"3a966160704621cd614ae9217051b2a681c0c913dd7507020c71c79f02161486"},{"bytes":1523717,"kind":"bundle","path":"mcp/mcp-grok-bot-b8c2461e-flight.mjs","sha256":"0c9ab3f71ae43f71263cae3d55b241e731ae663ecbe580c31e3a50d63e3a3a3d"},{"bytes":3351174,"kind":"bundle","path":"mcp/mcp-grok-bot-b8c2461e.mjs","sha256":"44bb2d1883bb081e78d216914a6adcb54b5d2d3ddb811f52bb486dcee63c6fd7"},{"bytes":681,"kind":"generated","path":"plugin.json","sha256":"862f9ff4c106a5b4c3bb09d498a2f395e3450074c8c79525c3a34aa67179d594"},{"bytes":953,"kind":"generated","path":"rules/codex-claude-on-user-machines.mdc","sha256":"a50ec93d021a807e0a18bf9311cdf103bc0ae3947e4c6a0bd78cbb941e8e5b24"},{"bytes":1305648,"kind":"bundle","path":"scripts/gbot-relay.mjs","sha256":"faf9fe6e3f82f9ae4a7ea61d04c2099c98057e325888e8769aa552b339c046cb"},{"bytes":4856,"kind":"copy","path":"skills/chatgpt-desktop/SKILL.md","sha256":"0fe2560037f761203a317bf8755b0476ce864b7b1b13708559448d300aa4635c"},{"bytes":6007,"kind":"copy","path":"skills/talk-to-grok-bot/references/bridge-administration.md","sha256":"3fc7d335822d9021cbd3770af6d7e95734d9213cd7331f41602598911c15ddf6"},{"bytes":4067,"kind":"copy","path":"skills/talk-to-grok-bot/SKILL.md","sha256":"b80a88ea029564447a79dbc1a533191dc4839791368c6248c904bcfaf3ccb770"}],"manifestVersion":6,"projections":[{"builtInHost":"claude","documents":{"marketplace":".claude-plugin/marketplace.json","mcp":".mcp.json","plugin":".claude-plugin/plugin.json"},"host":"claude","marketplace":{"name":"gbot-marketplace"}},{"builtInHost":"codex","documents":{"marketplace":".agents/plugins/marketplace.json","mcp":".codex-plugin/mcp.json","plugin":".codex-plugin/plugin.json"},"host":"codex","marketplace":{"name":"gbot-marketplace"}},{"builtInHost":"cursor","documents":{"marketplace":".cursor-plugin/marketplace.json","mcp":".cursor-plugin/mcp.json","plugin":".cursor-plugin/plugin.json"},"host":"cursor","marketplace":{"name":"gbot-marketplace"}},{"builtInHost":"portable","documents":{"mcp":"mcp.json","plugin":"plugin.json"},"host":"portable"}],"routes":{"cli":{"commands":[{"aliases":[],"description":"List current Grok approval cards (latest 200 entries).","exitCode":"zero","options":[{"key":"target","kind":"string","option":"target","positional":0,"repeated":false,"required":true}],"path":["approvals","list"],"routeId":"cli:approvals/list"},{"aliases":[],"description":"Explicit user decision: accept a Grok request once or decline; never grant persistent permissions.","exitCode":"zero","options":[{"choices":["accept","decline"],"key":"decision","kind":"enum","option":"decision","repeated":false,"required":true},{"key":"entryId","kind":"string","option":"entry-id","repeated":false,"required":true},{"key":"requestId","kind":"string","option":"request-id","repeated":false,"required":true},{"key":"target","kind":"string","option":"target","repeated":false,"required":true}],"path":["approvals","respond"],"routeId":"cli:approvals/respond"},{"aliases":[],"description":"Create a Grok Bot bot.","exitCode":"zero","options":[{"key":"avatarColor","kind":"string","option":"avatar-color","repeated":false,"required":false},{"key":"avatarShape","kind":"string","option":"avatar-shape","repeated":false,"required":false},{"key":"description","kind":"string","option":"description","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"name","kind":"string","option":"name","repeated":false,"required":true},{"key":"title","kind":"string","option":"title","repeated":false,"required":false}],"path":["bots","create"],"routeId":"cli:bots/create"},{"aliases":[],"description":"Delete a bot or group by id or name.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Bot or group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["bots","delete"],"routeId":"cli:bots/delete"},{"aliases":[],"description":"Show one bot or group by id or name.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Bot or group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["bots","get"],"routeId":"cli:bots/get"},{"aliases":[],"description":"List Grok Bot bots (not groups).","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false}],"path":["bots","list"],"routeId":"cli:bots/list"},{"aliases":[],"description":"Update a bot or group profile.","exitCode":"zero","options":[{"key":"avatarColor","kind":"string","option":"avatar-color","repeated":false,"required":false},{"key":"avatarShape","kind":"string","option":"avatar-shape","repeated":false,"required":false},{"key":"description","kind":"string","option":"description","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"hidden","kind":"string","option":"hidden","repeated":false,"required":false},{"key":"name","kind":"string","option":"name","repeated":false,"required":false},{"key":"notify","kind":"string","option":"notify","repeated":false,"required":false},{"description":"Bot or group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true},{"key":"title","kind":"string","option":"title","repeated":false,"required":false}],"path":["bots","update"],"routeId":"cli:bots/update"},{"aliases":[],"description":"Discover ChatGPT Desktop hosts and modelProviders at runtime (never hardcode). Always includes local plus every remote-thread-summaries-v3:; Providers come from distinct thread/list and remote-summary values.","exitCode":"result","options":[{"description":"Local CDP port","key":"port","kind":"number","option":"port","repeated":false,"required":false}],"path":["chatgpt-desktop","hosts"],"routeId":"cli:chatgpt-desktop/hosts"},{"aliases":[],"description":"Read a ChatGPT Desktop thread via app-server (turns/list); CDP DOM is fallback only.","exitCode":"result","options":[{"description":"Opaque nextCursor from the prior read page","key":"cursor","kind":"string","option":"cursor","repeated":false,"required":false},{"key":"full","kind":"boolean","option":"full","repeated":false,"required":false},{"description":"Recent default 100; full default 2000","key":"limit","kind":"number","option":"limit","repeated":false,"required":false},{"key":"openTimeoutMs","kind":"number","option":"open-timeout-ms","repeated":false,"required":false},{"key":"port","kind":"number","option":"port","repeated":false,"required":false},{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true}],"path":["chatgpt-desktop","read"],"routeId":"cli:chatgpt-desktop/read"},{"aliases":[],"description":"Search ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.","exitCode":"result","options":[{"description":"Opaque nextCursor from a prior search page","key":"cursor","kind":"string","option":"cursor","repeated":false,"required":false},{"choices":["host"],"description":"Group results by host (grouping mode, not a host id)","key":"groupBy","kind":"enum","option":"group-by","repeated":false,"required":false},{"description":"Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.","key":"host","kind":"string","option":"host","repeated":false,"required":false},{"description":"Max matches (1-200)","key":"limit","kind":"number","option":"limit","repeated":false,"required":false},{"description":"Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.","key":"modelProvider","kind":"string","option":"model-provider","repeated":false,"required":false},{"description":"Local CDP port","key":"port","kind":"number","option":"port","repeated":false,"required":false},{"description":"Project label or projectId filter.","key":"project","kind":"string","option":"project","repeated":false,"required":false},{"key":"query","kind":"string","option":"query","positional":0,"repeated":false,"required":true}],"path":["chatgpt-desktop","search"],"routeId":"cli:chatgpt-desktop/search"},{"aliases":[],"description":"Send to ChatGPT Desktop via CDP composer. Omit --thread-id for a new chat; --project prefers Start new chat in .","exitCode":"result","options":[{"key":"openTimeoutMs","kind":"number","option":"open-timeout-ms","repeated":false,"required":false},{"key":"port","kind":"number","option":"port","repeated":false,"required":false},{"key":"project","kind":"string","option":"project","repeated":false,"required":false},{"key":"text","kind":"string","option":"text","positional":0,"repeated":true,"required":true},{"key":"threadId","kind":"string","option":"thread-id","repeated":false,"required":false},{"description":"Explicitly unarchive an existing archived thread before sending.","key":"unarchive","kind":"boolean","option":"unarchive","repeated":false,"required":false}],"path":["chatgpt-desktop","send"],"routeId":"cli:chatgpt-desktop/send"},{"aliases":[],"description":"Probe local ChatGPT Desktop CDP on 127.0.0.1 and report app-server availability for list/deep-read.","exitCode":"result","options":[{"description":"Local CDP port (default 9222)","key":"port","kind":"number","option":"port","repeated":false,"required":false}],"path":["chatgpt-desktop","status"],"routeId":"cli:chatgpt-desktop/status"},{"aliases":[],"description":"List local + remote-control ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.","exitCode":"result","options":[{"description":"Opaque nextCursor from a previous page","key":"cursor","kind":"string","option":"cursor","repeated":false,"required":false},{"choices":["host"],"description":"Group results by host (grouping mode, not a host id)","key":"groupBy","kind":"enum","option":"group-by","repeated":false,"required":false},{"description":"Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.","key":"host","kind":"string","option":"host","repeated":false,"required":false},{"description":"Max threads (1-200)","key":"limit","kind":"number","option":"limit","repeated":false,"required":false},{"description":"Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.","key":"modelProvider","kind":"string","option":"model-provider","repeated":false,"required":false},{"description":"Local CDP port","key":"port","kind":"number","option":"port","repeated":false,"required":false},{"description":"Project label or projectId filter.","key":"project","kind":"string","option":"project","repeated":false,"required":false}],"path":["chatgpt-desktop","threads"],"routeId":"cli:chatgpt-desktop/threads"},{"aliases":[],"description":"Send to an explicitly enabled live Claude Code channel and wait for its reply.","exitCode":"result","options":[{"key":"message","kind":"string","option":"message","positional":1,"repeated":false,"required":true},{"key":"name","kind":"string","option":"name","positional":0,"repeated":false,"required":true},{"key":"timeoutMs","kind":"number","option":"timeout-ms","repeated":false,"required":false}],"path":["claude","send"],"routeId":"cli:claude/send"},{"aliases":[],"description":"Managed Grok/Codex bridge respond.","exitCode":"result","options":[{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","key":"answersJson","kind":"string","option":"answers-json","repeated":false,"required":false},{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"choices":["accept","decline","cancel"],"key":"decision","kind":"enum","option":"decision","repeated":false,"required":false},{"key":"exchangeId","kind":"string","option":"exchange-id","repeated":false,"required":false},{"key":"generation","kind":"string","option":"generation","repeated":false,"required":true},{"key":"interactionId","kind":"string","option":"interaction-id","repeated":false,"required":true},{"key":"threadId","kind":"string","option":"thread-id","repeated":false,"required":true},{"key":"turnId","kind":"string","option":"turn-id","repeated":false,"required":true}],"path":["codex","bridge","respond"],"routeId":"cli:codex/bridge/respond"},{"aliases":[],"description":"Run a bounded foreground relay (up to 23 hours). Use the packaged gbot-relay.mjs script for unlimited service lifetime.","exitCode":"result","options":[{"description":"Foreground lifetime in milliseconds (100..82800000; default 23h).","key":"lifetimeMs","kind":"number","option":"lifetime-ms","repeated":false,"required":false}],"path":["codex","bridge","run"],"routeId":"cli:codex/bridge/run"},{"aliases":[],"description":"Managed Grok/Codex bridge start.","exitCode":"result","options":[{"choices":["steer","reject"],"key":"busyPolicy","kind":"enum","option":"busy-policy","repeated":false,"required":false},{"key":"codexThreadId","kind":"string","option":"codex-thread-id","repeated":false,"required":false},{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"key":"grokTarget","kind":"string","option":"grok-target","positional":0,"repeated":false,"required":true},{"key":"requestId","kind":"string","option":"request-id","repeated":false,"required":false}],"path":["codex","bridge","start"],"routeId":"cli:codex/bridge/start"},{"aliases":[],"description":"Managed Grok/Codex bridge status.","exitCode":"result","options":[{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"key":"limit","kind":"number","option":"limit","repeated":false,"required":false}],"path":["codex","bridge","status"],"routeId":"cli:codex/bridge/status"},{"aliases":[],"description":"Managed Grok/Codex bridge stop.","exitCode":"result","options":[{"key":"all","kind":"boolean","option":"all","repeated":false,"required":false},{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"key":"worker","kind":"boolean","option":"worker","repeated":false,"required":false}],"path":["codex","bridge","stop"],"routeId":"cli:codex/bridge/stop"},{"aliases":[],"description":"Install, remove, or check the ChatGPT Desktop shim: a CODEX_CLI_PATH wrapper that bridges Desktop stdio onto the managed Codex daemon (stock app-server proxy hangs). macOS persists via LaunchAgent; always fails open to real Codex.","exitCode":"result","options":[{"choices":["install","uninstall","status"],"description":"install | uninstall | status","key":"action","kind":"enum","option":"action","positional":0,"repeated":false,"required":true}],"path":["codex","desktop-shim"],"routeId":"cli:codex/desktop-shim"},{"aliases":[],"description":"List Codex daemon-managed threads (all modelProviders; pages via --cursor / nextCursor).","exitCode":"result","options":[{"description":"Opaque pagination cursor from a previous nextCursor (may start with -)","key":"cursor","kind":"string","option":"cursor","repeated":false,"required":false},{"description":"Max threads to list (1-200)","key":"limit","kind":"number","option":"limit","repeated":false,"required":false}],"path":["codex","list-threads"],"routeId":"cli:codex/list-threads"},{"aliases":[],"description":"Start a new Codex thread in --cwd, optionally with --model, --effort and a message.","exitCode":"result","options":[{"key":"cwd","kind":"string","option":"cwd","repeated":false,"required":true},{"choices":["none","minimal","low","medium","high","xhigh"],"key":"effort","kind":"enum","option":"effort","repeated":false,"required":false},{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"key":"message","kind":"string","option":"message","positional":0,"repeated":true,"required":false},{"key":"model","kind":"string","option":"model","repeated":false,"required":false}],"path":["codex","new"],"routeId":"cli:codex/new"},{"aliases":[],"description":"List the experimental Codex thread queue (GROK_BOT_CODEX_EXPERIMENTAL=1).","exitCode":"result","options":[{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true}],"path":["codex","queue"],"routeId":"cli:codex/queue"},{"aliases":[],"description":"Send to Codex; acceptance is distinct from completion. Options precede threadId.","exitCode":"result","options":[{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"key":"correlationId","kind":"string","option":"correlation-id","repeated":false,"required":false},{"choices":["none","minimal","low","medium","high","xhigh"],"key":"effort","kind":"enum","option":"effort","repeated":false,"required":false},{"key":"envelope","kind":"boolean","option":"envelope","repeated":false,"required":false},{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"description":"Required active-turn guard for steer; stale guards reject.","key":"expectedTurnId","kind":"string","option":"expected-turn-id","repeated":false,"required":false},{"key":"hop","kind":"number","option":"hop","repeated":false,"required":false},{"description":"Reply budget: 1-4194304 bytes.","key":"maxOutputBytes","kind":"number","option":"max-output-bytes","repeated":false,"required":false},{"key":"message","kind":"string","option":"message","positional":1,"repeated":true,"required":true},{"key":"model","kind":"string","option":"model","repeated":false,"required":false},{"key":"replyTo","kind":"string","option":"reply-to","repeated":false,"required":false},{"key":"replyToGrok","kind":"string","option":"reply-to-grok","repeated":false,"required":false},{"key":"requestId","kind":"string","option":"request-id","repeated":false,"required":false},{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true},{"description":"Observation timeout: 1-7200000 milliseconds.","key":"timeoutMs","kind":"number","option":"timeout-ms","repeated":false,"required":false},{"key":"wait","kind":"boolean","option":"wait","repeated":false,"required":false},{"choices":["reject","queue","steer"],"key":"whenBusy","kind":"enum","option":"when-busy","repeated":false,"required":false}],"path":["codex","send"],"routeId":"cli:codex/send"},{"aliases":[],"description":"Probe the local Codex app-server daemon. Exit 0 only when it is usable.","exitCode":"result","options":[],"path":["codex","status"],"routeId":"cli:codex/status"},{"aliases":[],"description":"Bounded Codex wait observation; never interrupts execution.","exitCode":"result","options":[{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"description":"Reply budget: 1-4194304 bytes.","key":"maxOutputBytes","kind":"number","option":"max-output-bytes","repeated":false,"required":false},{"key":"messageId","kind":"string","option":"message-id","repeated":false,"required":false},{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true},{"description":"Observation timeout: 1-7200000 milliseconds.","key":"timeoutMs","kind":"number","option":"timeout-ms","repeated":false,"required":false},{"key":"turnId","kind":"string","option":"turn-id","positional":1,"repeated":false,"required":true}],"path":["codex","wait"],"routeId":"cli:codex/wait"},{"aliases":[],"description":"Bounded Codex watch observation; never interrupts execution.","exitCode":"result","options":[{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"description":"Maximum observed events: 1-500.","key":"maxEvents","kind":"number","option":"max-events","repeated":false,"required":false},{"key":"threadId","kind":"string","option":"thread-id","positional":0,"repeated":false,"required":true},{"description":"Observation timeout: 1-600000 milliseconds.","key":"timeoutMs","kind":"number","option":"timeout-ms","repeated":false,"required":false}],"path":["codex","watch"],"routeId":"cli:codex/watch"},{"aliases":[],"description":"Show which agents root and auth sources gbot can see.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false}],"path":["doctor"],"routeId":"cli:doctor"},{"aliases":[],"description":"Add a bot to a group.","exitCode":"zero","options":[{"key":"bot","kind":"string","option":"bot","positional":1,"repeated":false,"required":true},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"group","kind":"string","option":"group","positional":0,"repeated":false,"required":true}],"path":["groups","add"],"routeId":"cli:groups/add"},{"aliases":[],"description":"Create a Grok Bot group.","exitCode":"zero","options":[{"key":"avatarColor","kind":"string","option":"avatar-color","repeated":false,"required":false},{"key":"avatarShape","kind":"string","option":"avatar-shape","repeated":false,"required":false},{"key":"description","kind":"string","option":"description","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Member bot id or name (repeatable)","key":"member","kind":"string","option":"member","repeated":true,"required":true},{"key":"name","kind":"string","option":"name","repeated":false,"required":true},{"key":"title","kind":"string","option":"title","repeated":false,"required":false}],"path":["groups","create"],"routeId":"cli:groups/create"},{"aliases":[],"description":"Delete a group by id or name.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["groups","delete"],"routeId":"cli:groups/delete"},{"aliases":[],"description":"Show one group by id or name.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["groups","get"],"routeId":"cli:groups/get"},{"aliases":[],"description":"List Grok Bot groups.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false}],"path":["groups","list"],"routeId":"cli:groups/list"},{"aliases":[],"description":"Show members of a group (same payload as groups get).","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true}],"path":["groups","members"],"routeId":"cli:groups/members"},{"aliases":[],"description":"Remove a bot from a group.","exitCode":"zero","options":[{"key":"bot","kind":"string","option":"bot","positional":1,"repeated":false,"required":true},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"group","kind":"string","option":"group","positional":0,"repeated":false,"required":true}],"path":["groups","remove"],"routeId":"cli:groups/remove"},{"aliases":[],"description":"Replace a group's member list.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"group","kind":"string","option":"group","positional":0,"repeated":false,"required":true},{"description":"Member bot id or name (repeatable)","key":"member","kind":"string","option":"member","repeated":true,"required":true}],"path":["groups","set"],"routeId":"cli:groups/set"},{"aliases":[],"description":"Update a group profile (members stay on set/add/remove).","exitCode":"zero","options":[{"key":"avatarColor","kind":"string","option":"avatar-color","repeated":false,"required":false},{"key":"avatarShape","kind":"string","option":"avatar-shape","repeated":false,"required":false},{"key":"description","kind":"string","option":"description","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"key":"hidden","kind":"string","option":"hidden","repeated":false,"required":false},{"key":"name","kind":"string","option":"name","repeated":false,"required":false},{"key":"notify","kind":"string","option":"notify","repeated":false,"required":false},{"description":"Group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":true},{"key":"title","kind":"string","option":"title","repeated":false,"required":false}],"path":["groups","update"],"routeId":"cli:groups/update"},{"aliases":[],"description":"Read the opt-in local JSONL history without contacting the gateway.","exitCode":"zero","options":[{"description":"Directory containing history.jsonl","key":"historyDir","kind":"string","option":"history-dir","repeated":false,"required":false},{"description":"Maximum matching rows (1 or more)","key":"limit","kind":"number","option":"limit","repeated":false,"required":false},{"description":"Print the history file path","key":"path","kind":"boolean","option":"path","repeated":false,"required":false},{"description":"Bot or group id or name","key":"ref","kind":"string","option":"ref","positional":0,"repeated":false,"required":false},{"description":"Case-insensitive message text filter","key":"search","kind":"string","option":"search","repeated":false,"required":false}],"path":["history"],"routeId":"cli:history"},{"aliases":[],"description":"Send a message to a Grok Bot bot or group by name or id.","exitCode":"result","options":[{"key":"bindingId","kind":"string","option":"binding-id","repeated":false,"required":false},{"key":"codexThreadId","kind":"string","option":"codex-thread-id","repeated":false,"required":false},{"description":"Stable correlation id for multi-hop replies","key":"correlationId","kind":"string","option":"correlation-id","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Prepend the [gbot …] header","key":"envelope","kind":"boolean","option":"envelope","repeated":false,"required":false},{"key":"expectedCwd","kind":"string","option":"expected-cwd","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Directory containing history.jsonl","key":"historyDir","kind":"string","option":"history-dir","repeated":false,"required":false},{"description":"Hop count; refused at GROK_BOT_MAX_HOPS","key":"hop","kind":"number","option":"hop","repeated":false,"required":false},{"key":"message","kind":"string","option":"message","positional":1,"repeated":true,"required":true},{"description":"Skip local history for this command","key":"noHistory","kind":"boolean","option":"no-history","repeated":false,"required":false},{"choices":["auto","manual"],"key":"replyMode","kind":"enum","option":"reply-mode","repeated":false,"required":false},{"description":"Prior message id this send replies to","key":"replyTo","kind":"string","option":"reply-to","repeated":false,"required":false},{"key":"requestId","kind":"string","option":"request-id","repeated":false,"required":false},{"key":"target","kind":"string","option":"target","positional":0,"repeated":false,"required":true}],"path":["send"],"routeId":"cli:send"},{"aliases":[],"description":"Add a SKILL.md to the shared skill library. Every bot sees it.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"SKILL.md file, or a directory holding one","key":"path","kind":"string","option":"path","positional":0,"repeated":false,"required":true}],"path":["skills","add"],"routeId":"cli:skills/add"},{"aliases":[],"description":"List the skill library every bot in this Grok Bot shares.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false}],"path":["skills","list"],"routeId":"cli:skills/list"},{"aliases":[],"description":"Remove a library skill by id or name. Every bot loses it.","exitCode":"zero","options":[{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Skill id or name","key":"skill","kind":"string","option":"skill","positional":0,"repeated":false,"required":true}],"path":["skills","remove"],"routeId":"cli:skills/remove"},{"aliases":[],"description":"Read the most recent messages in a Grok Bot bot or group thread.","exitCode":"zero","options":[{"description":"Return entries after this opaque entry id","key":"after","kind":"string","option":"after","repeated":false,"required":false},{"description":"Agents directory for --files mode","key":"dir","kind":"string","option":"dir","repeated":false,"required":false},{"description":"Force the on-disk agents store","key":"files","kind":"boolean","option":"files","repeated":false,"required":false},{"description":"Show full entry text in human output","key":"full","kind":"boolean","option":"full","repeated":false,"required":false},{"description":"Force the live gateway","key":"gateway","kind":"boolean","option":"gateway","repeated":false,"required":false},{"description":"Directory containing history.jsonl","key":"historyDir","kind":"string","option":"history-dir","repeated":false,"required":false},{"description":"How many trailing entries to return (1-200)","key":"limit","kind":"number","option":"limit","repeated":false,"required":false},{"description":"Skip local history for this command","key":"noHistory","kind":"boolean","option":"no-history","repeated":false,"required":false},{"description":"Read one rooted thread by message id","key":"root","kind":"string","option":"root","repeated":false,"required":false},{"key":"target","kind":"string","option":"target","positional":0,"repeated":false,"required":true}],"path":["thread"],"routeId":"cli:thread"}],"mode":"generated","routes":[{"contract":"contract:src/cli/approvals/list.tsx#inputJsonSchema","description":"List current Grok approval cards (latest 200 entries).","id":"cli:approvals/list","inputSchema":{"additionalProperties":false,"properties":{"target":{"type":"string"}},"required":["target"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/approvals/list.tsx"},{"contract":"contract:src/cli/approvals/respond.tsx#inputJsonSchema","description":"Explicit user decision: accept a Grok request once or decline; never grant persistent permissions.","id":"cli:approvals/respond","inputSchema":{"additionalProperties":false,"properties":{"decision":{"enum":["accept","decline"],"type":"string"},"entryId":{"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","entryId","requestId","decision"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/approvals/respond.tsx"},{"contract":"contract:src/cli/bots/create.tsx#inputJsonSchema","description":"Create a Grok Bot bot.","id":"cli:bots/create","inputSchema":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"name":{"type":"string"},"title":{"type":"string"}},"required":["name"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/create.tsx"},{"contract":"contract:src/cli/bots/delete.tsx#inputJsonSchema","description":"Delete a bot or group by id or name.","id":"cli:bots/delete","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/delete.tsx"},{"contract":"contract:src/cli/bots/get.tsx#inputJsonSchema","description":"Show one bot or group by id or name.","id":"cli:bots/get","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/get.tsx"},{"contract":"contract:src/cli/bots/list.tsx#inputJsonSchema","description":"List Grok Bot bots (not groups).","id":"cli:bots/list","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/list.tsx"},{"contract":"contract:src/cli/bots/update.tsx#inputJsonSchema","description":"Update a bot or group profile.","id":"cli:bots/update","inputSchema":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"hidden":{"type":"string"},"name":{"type":"string"},"notify":{"type":"string"},"ref":{"description":"Bot or group id or name","type":"string"},"title":{"type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/bots/update.tsx"},{"contract":"contract:src/cli/chatgpt-desktop/hosts.tsx#inputJsonSchema","description":"Discover ChatGPT Desktop hosts and modelProviders at runtime (never hardcode). Always includes local plus every remote-thread-summaries-v3:; Providers come from distinct thread/list and remote-summary values.","id":"cli:chatgpt-desktop/hosts","inputSchema":{"additionalProperties":false,"properties":{"port":{"description":"Local CDP port","type":"number"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/chatgpt-desktop/hosts.tsx"},{"contract":"contract:src/cli/chatgpt-desktop/read.tsx#inputJsonSchema","description":"Read a ChatGPT Desktop thread via app-server (turns/list); CDP DOM is fallback only.","id":"cli:chatgpt-desktop/read","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from the prior read page","type":"string"},"full":{"default":false,"type":"boolean"},"limit":{"description":"Recent default 100; full default 2000","type":"number"},"openTimeoutMs":{"default":90000,"type":"number"},"port":{"type":"number"},"threadId":{"type":"string"}},"required":["threadId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/chatgpt-desktop/read.tsx"},{"contract":"contract:src/cli/chatgpt-desktop/search.tsx#inputJsonSchema","description":"Search ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.","id":"cli:chatgpt-desktop/search","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from a prior search page","type":"string"},"groupBy":{"description":"Group results by host (grouping mode, not a host id)","enum":["host"],"type":"string"},"host":{"description":"Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.","type":"string"},"limit":{"default":50,"description":"Max matches (1-200)","type":"number"},"modelProvider":{"description":"Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.","type":"string"},"port":{"description":"Local CDP port","type":"number"},"project":{"description":"Project label or projectId filter.","type":"string"},"query":{"type":"string"}},"required":["query"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/chatgpt-desktop/search.tsx"},{"contract":"contract:src/cli/chatgpt-desktop/send.tsx#inputJsonSchema","description":"Send to ChatGPT Desktop via CDP composer. Omit --thread-id for a new chat; --project prefers Start new chat in .","id":"cli:chatgpt-desktop/send","inputSchema":{"additionalProperties":false,"properties":{"openTimeoutMs":{"type":"number"},"port":{"type":"number"},"project":{"type":"string"},"text":{"items":{"type":"string"},"type":"array"},"threadId":{"type":"string"},"unarchive":{"description":"Explicitly unarchive an existing archived thread before sending.","type":"boolean"}},"required":["text"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/chatgpt-desktop/send.tsx"},{"contract":"contract:src/cli/chatgpt-desktop/status.tsx#inputJsonSchema","description":"Probe local ChatGPT Desktop CDP on 127.0.0.1 and report app-server availability for list/deep-read.","id":"cli:chatgpt-desktop/status","inputSchema":{"additionalProperties":false,"properties":{"port":{"description":"Local CDP port (default 9222)","type":"number"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/chatgpt-desktop/status.tsx"},{"contract":"contract:src/cli/chatgpt-desktop/threads.tsx#inputJsonSchema","description":"List local + remote-control ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.","id":"cli:chatgpt-desktop/threads","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from a previous page","type":"string"},"groupBy":{"description":"Group results by host (grouping mode, not a host id)","enum":["host"],"type":"string"},"host":{"description":"Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.","type":"string"},"limit":{"default":50,"description":"Max threads (1-200)","type":"number"},"modelProvider":{"description":"Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.","type":"string"},"port":{"description":"Local CDP port","type":"number"},"project":{"description":"Project label or projectId filter.","type":"string"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/chatgpt-desktop/threads.tsx"},{"contract":"contract:src/cli/claude/send.tsx#inputJsonSchema","description":"Send to an explicitly enabled live Claude Code channel and wait for its reply.","id":"cli:claude/send","inputSchema":{"additionalProperties":false,"properties":{"message":{"type":"string"},"name":{"type":"string"},"timeoutMs":{"type":"number"}},"required":["name","message"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/claude/send.tsx"},{"contract":"contract:src/cli/codex/bridge/respond.tsx#inputJsonSchema","description":"Managed Grok/Codex bridge respond.","id":"cli:codex/bridge/respond","inputSchema":{"additionalProperties":false,"properties":{"answersJson":{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","type":"string"},"bindingId":{"type":"string"},"decision":{"enum":["accept","decline","cancel"],"type":"string"},"exchangeId":{"type":"string"},"generation":{"type":"string"},"interactionId":{"type":"string"},"threadId":{"type":"string"},"turnId":{"type":"string"}},"required":["interactionId","generation","threadId","turnId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/respond.tsx"},{"contract":"contract:src/cli/codex/bridge/run.tsx#inputJsonSchema","description":"Run a bounded foreground relay (up to 23 hours). Use the packaged gbot-relay.mjs script for unlimited service lifetime.","id":"cli:codex/bridge/run","inputSchema":{"additionalProperties":false,"properties":{"lifetimeMs":{"description":"Foreground lifetime in milliseconds (100..82800000; default 23h).","type":"number"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/run.tsx"},{"contract":"contract:src/cli/codex/bridge/start.tsx#inputJsonSchema","description":"Managed Grok/Codex bridge start.","id":"cli:codex/bridge/start","inputSchema":{"additionalProperties":false,"properties":{"busyPolicy":{"enum":["steer","reject"],"type":"string"},"codexThreadId":{"type":"string"},"expectedCwd":{"type":"string"},"grokTarget":{"type":"string"},"requestId":{"type":"string"}},"required":["grokTarget"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/start.tsx"},{"contract":"contract:src/cli/codex/bridge/status.tsx#inputJsonSchema","description":"Managed Grok/Codex bridge status.","id":"cli:codex/bridge/status","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"limit":{"type":"number"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/status.tsx"},{"contract":"contract:src/cli/codex/bridge/stop.tsx#inputJsonSchema","description":"Managed Grok/Codex bridge stop.","id":"cli:codex/bridge/stop","inputSchema":{"additionalProperties":false,"properties":{"all":{"type":"boolean"},"bindingId":{"type":"string"},"worker":{"type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/bridge/stop.tsx"},{"contract":"contract:src/cli/codex/desktop-shim.tsx#inputJsonSchema","description":"Install, remove, or check the ChatGPT Desktop shim: a CODEX_CLI_PATH wrapper that bridges Desktop stdio onto the managed Codex daemon (stock app-server proxy hangs). macOS persists via LaunchAgent; always fails open to real Codex.","id":"cli:codex/desktop-shim","inputSchema":{"additionalProperties":false,"properties":{"action":{"description":"install | uninstall | status","enum":["install","uninstall","status"],"type":"string"}},"required":["action"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/desktop-shim.tsx"},{"contract":"contract:src/cli/codex/list-threads.tsx#inputJsonSchema","description":"List Codex daemon-managed threads (all modelProviders; pages via --cursor / nextCursor).","id":"cli:codex/list-threads","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque pagination cursor from a previous nextCursor (may start with -)","type":"string"},"limit":{"default":20,"description":"Max threads to list (1-200)","type":"number"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/list-threads.tsx"},{"contract":"contract:src/cli/codex/new.tsx#inputJsonSchema","description":"Start a new Codex thread in --cwd, optionally with --model, --effort and a message.","id":"cli:codex/new","inputSchema":{"additionalProperties":false,"properties":{"cwd":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"expectedCwd":{"type":"string"},"message":{"items":{"type":"string"},"type":"array"},"model":{"type":"string"}},"required":["cwd"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/new.tsx"},{"contract":"contract:src/cli/codex/queue.tsx#inputJsonSchema","description":"List the experimental Codex thread queue (GROK_BOT_CODEX_EXPERIMENTAL=1).","id":"cli:codex/queue","inputSchema":{"additionalProperties":false,"properties":{"threadId":{"type":"string"}},"required":["threadId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/queue.tsx"},{"contract":"contract:src/cli/codex/send.tsx#inputJsonSchema","description":"Send to Codex; acceptance is distinct from completion. Options precede threadId.","id":"cli:codex/send","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"correlationId":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"envelope":{"type":"boolean"},"expectedCwd":{"type":"string"},"expectedTurnId":{"description":"Required active-turn guard for steer; stale guards reject.","type":"string"},"hop":{"type":"number"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"message":{"items":{"type":"string"},"type":"array"},"model":{"type":"string"},"replyTo":{"type":"string"},"replyToGrok":{"type":"string"},"requestId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"wait":{"type":"boolean"},"whenBusy":{"enum":["reject","queue","steer"],"type":"string"}},"required":["threadId","message"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/send.tsx"},{"contract":"contract:src/cli/codex/status.tsx#inputJsonSchema","description":"Probe the local Codex app-server daemon. Exit 0 only when it is usable.","id":"cli:codex/status","inputSchema":{"additionalProperties":false,"properties":{},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/status.tsx"},{"contract":"contract:src/cli/codex/wait.tsx#inputJsonSchema","description":"Bounded Codex wait observation; never interrupts execution.","id":"cli:codex/wait","inputSchema":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"messageId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"turnId":{"type":"string"}},"required":["threadId","turnId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/wait.tsx"},{"contract":"contract:src/cli/codex/watch.tsx#inputJsonSchema","description":"Bounded Codex watch observation; never interrupts execution.","id":"cli:codex/watch","inputSchema":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxEvents":{"description":"Maximum observed events: 1-500.","type":"number"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-600000 milliseconds.","type":"number"}},"required":["threadId"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/codex/watch.tsx"},{"contract":"contract:src/cli/doctor.tsx#inputJsonSchema","description":"Show which agents root and auth sources gbot can see.","id":"cli:doctor","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/doctor.tsx"},{"contract":"contract:src/cli/groups/add.tsx#inputJsonSchema","description":"Add a bot to a group.","id":"cli:groups/add","inputSchema":{"additionalProperties":false,"properties":{"bot":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"}},"required":["group","bot"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/add.tsx"},{"contract":"contract:src/cli/groups/create.tsx#inputJsonSchema","description":"Create a Grok Bot group.","id":"cli:groups/create","inputSchema":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"member":{"description":"Member bot id or name (repeatable)","items":{"type":"string"},"type":"array"},"name":{"type":"string"},"title":{"type":"string"}},"required":["name","member"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/create.tsx"},{"contract":"contract:src/cli/groups/delete.tsx#inputJsonSchema","description":"Delete a group by id or name.","id":"cli:groups/delete","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/delete.tsx"},{"contract":"contract:src/cli/groups/get.tsx#inputJsonSchema","description":"Show one group by id or name.","id":"cli:groups/get","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/get.tsx"},{"contract":"contract:src/cli/groups/list.tsx#inputJsonSchema","description":"List Grok Bot groups.","id":"cli:groups/list","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/list.tsx"},{"contract":"contract:src/cli/groups/members.tsx#inputJsonSchema","description":"Show members of a group (same payload as groups get).","id":"cli:groups/members","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/members.tsx"},{"contract":"contract:src/cli/groups/remove.tsx#inputJsonSchema","description":"Remove a bot from a group.","id":"cli:groups/remove","inputSchema":{"additionalProperties":false,"properties":{"bot":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"}},"required":["group","bot"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/remove.tsx"},{"contract":"contract:src/cli/groups/set.tsx#inputJsonSchema","description":"Replace a group's member list.","id":"cli:groups/set","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"},"member":{"description":"Member bot id or name (repeatable)","items":{"type":"string"},"type":"array"}},"required":["group","member"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/set.tsx"},{"contract":"contract:src/cli/groups/update.tsx#inputJsonSchema","description":"Update a group profile (members stay on set/add/remove).","id":"cli:groups/update","inputSchema":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"hidden":{"type":"string"},"name":{"type":"string"},"notify":{"type":"string"},"ref":{"description":"Group id or name","type":"string"},"title":{"type":"string"}},"required":["ref"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/groups/update.tsx"},{"contract":"contract:src/cli/history.tsx#inputJsonSchema","description":"Read the opt-in local JSONL history without contacting the gateway.","id":"cli:history","inputSchema":{"additionalProperties":false,"properties":{"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"limit":{"default":40,"description":"Maximum matching rows (1 or more)","type":"number"},"path":{"description":"Print the history file path","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"},"search":{"description":"Case-insensitive message text filter","type":"string"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/history.tsx"},{"contract":"contract:src/cli/send.tsx#inputJsonSchema","description":"Send a message to a Grok Bot bot or group by name or id.","id":"cli:send","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"codexThreadId":{"type":"string"},"correlationId":{"description":"Stable correlation id for multi-hop replies","type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"envelope":{"description":"Prepend the [gbot …] header","type":"boolean"},"expectedCwd":{"type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"hop":{"description":"Hop count; refused at GROK_BOT_MAX_HOPS","type":"number"},"message":{"items":{"type":"string"},"type":"array"},"noHistory":{"description":"Skip local history for this command","type":"boolean"},"replyMode":{"enum":["auto","manual"],"type":"string"},"replyTo":{"description":"Prior message id this send replies to","type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","message"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/send.tsx"},{"contract":"contract:src/cli/skills/add.tsx#inputJsonSchema","description":"Add a SKILL.md to the shared skill library. Every bot sees it.","id":"cli:skills/add","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"path":{"description":"SKILL.md file, or a directory holding one","type":"string"}},"required":["path"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/skills/add.tsx"},{"contract":"contract:src/cli/skills/list.tsx#inputJsonSchema","description":"List the skill library every bot in this Grok Bot shares.","id":"cli:skills/list","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/skills/list.tsx"},{"contract":"contract:src/cli/skills/remove.tsx#inputJsonSchema","description":"Remove a library skill by id or name. Every bot loses it.","id":"cli:skills/remove","inputSchema":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"skill":{"description":"Skill id or name","type":"string"}},"required":["skill"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/skills/remove.tsx"},{"contract":"contract:src/cli/thread.tsx#inputJsonSchema","description":"Read the most recent messages in a Grok Bot bot or group thread.","id":"cli:thread","inputSchema":{"additionalProperties":false,"properties":{"after":{"description":"Return entries after this opaque entry id","type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"full":{"description":"Show full entry text in human output","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"limit":{"default":40,"description":"How many trailing entries to return (1-200)","type":"number"},"noHistory":{"description":"Skip local history for this command","type":"boolean"},"root":{"description":"Read one rooted thread by message id","type":"string"},"target":{"type":"string"}},"required":["target"],"type":"object"},"kind":"cli","provenance":{"kind":"conventional"},"source":"src/cli/thread.tsx"}]},"contracts":[{"id":"contract:src/cli/approvals/list.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"target":{"type":"string"}},"required":["target"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/approvals/list.tsx"},"routes":["cli:approvals/list"]},{"id":"contract:src/cli/approvals/respond.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"decision":{"enum":["accept","decline"],"type":"string"},"entryId":{"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","entryId","requestId","decision"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/approvals/respond.tsx"},"routes":["cli:approvals/respond"]},{"id":"contract:src/cli/bots/create.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"name":{"type":"string"},"title":{"type":"string"}},"required":["name"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/create.tsx"},"routes":["cli:bots/create"]},{"id":"contract:src/cli/bots/delete.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/delete.tsx"},"routes":["cli:bots/delete"]},{"id":"contract:src/cli/bots/get.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/get.tsx"},"routes":["cli:bots/get"]},{"id":"contract:src/cli/bots/list.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/list.tsx"},"routes":["cli:bots/list"]},{"id":"contract:src/cli/bots/update.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"hidden":{"type":"string"},"name":{"type":"string"},"notify":{"type":"string"},"ref":{"description":"Bot or group id or name","type":"string"},"title":{"type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/bots/update.tsx"},"routes":["cli:bots/update"]},{"id":"contract:src/cli/chatgpt-desktop/hosts.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"port":{"description":"Local CDP port","type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/chatgpt-desktop/hosts.tsx"},"routes":["cli:chatgpt-desktop/hosts"]},{"id":"contract:src/cli/chatgpt-desktop/read.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from the prior read page","type":"string"},"full":{"default":false,"type":"boolean"},"limit":{"description":"Recent default 100; full default 2000","type":"number"},"openTimeoutMs":{"default":90000,"type":"number"},"port":{"type":"number"},"threadId":{"type":"string"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/chatgpt-desktop/read.tsx"},"routes":["cli:chatgpt-desktop/read"]},{"id":"contract:src/cli/chatgpt-desktop/search.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from a prior search page","type":"string"},"groupBy":{"description":"Group results by host (grouping mode, not a host id)","enum":["host"],"type":"string"},"host":{"description":"Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.","type":"string"},"limit":{"default":50,"description":"Max matches (1-200)","type":"number"},"modelProvider":{"description":"Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.","type":"string"},"port":{"description":"Local CDP port","type":"number"},"project":{"description":"Project label or projectId filter.","type":"string"},"query":{"type":"string"}},"required":["query"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/chatgpt-desktop/search.tsx"},"routes":["cli:chatgpt-desktop/search"]},{"id":"contract:src/cli/chatgpt-desktop/send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"openTimeoutMs":{"type":"number"},"port":{"type":"number"},"project":{"type":"string"},"text":{"items":{"type":"string"},"type":"array"},"threadId":{"type":"string"},"unarchive":{"description":"Explicitly unarchive an existing archived thread before sending.","type":"boolean"}},"required":["text"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/chatgpt-desktop/send.tsx"},"routes":["cli:chatgpt-desktop/send"]},{"id":"contract:src/cli/chatgpt-desktop/status.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"port":{"description":"Local CDP port (default 9222)","type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/chatgpt-desktop/status.tsx"},"routes":["cli:chatgpt-desktop/status"]},{"id":"contract:src/cli/chatgpt-desktop/threads.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from a previous page","type":"string"},"groupBy":{"description":"Group results by host (grouping mode, not a host id)","enum":["host"],"type":"string"},"host":{"description":"Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.","type":"string"},"limit":{"default":50,"description":"Max threads (1-200)","type":"number"},"modelProvider":{"description":"Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.","type":"string"},"port":{"description":"Local CDP port","type":"number"},"project":{"description":"Project label or projectId filter.","type":"string"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/chatgpt-desktop/threads.tsx"},"routes":["cli:chatgpt-desktop/threads"]},{"id":"contract:src/cli/claude/send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"message":{"type":"string"},"name":{"type":"string"},"timeoutMs":{"type":"number"}},"required":["name","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/claude/send.tsx"},"routes":["cli:claude/send"]},{"id":"contract:src/cli/codex/bridge/respond.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"answersJson":{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","type":"string"},"bindingId":{"type":"string"},"decision":{"enum":["accept","decline","cancel"],"type":"string"},"exchangeId":{"type":"string"},"generation":{"type":"string"},"interactionId":{"type":"string"},"threadId":{"type":"string"},"turnId":{"type":"string"}},"required":["interactionId","generation","threadId","turnId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/respond.tsx"},"routes":["cli:codex/bridge/respond"]},{"id":"contract:src/cli/codex/bridge/run.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"lifetimeMs":{"description":"Foreground lifetime in milliseconds (100..82800000; default 23h).","type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/run.tsx"},"routes":["cli:codex/bridge/run"]},{"id":"contract:src/cli/codex/bridge/start.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"busyPolicy":{"enum":["steer","reject"],"type":"string"},"codexThreadId":{"type":"string"},"expectedCwd":{"type":"string"},"grokTarget":{"type":"string"},"requestId":{"type":"string"}},"required":["grokTarget"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/start.tsx"},"routes":["cli:codex/bridge/start"]},{"id":"contract:src/cli/codex/bridge/status.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"limit":{"type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/status.tsx"},"routes":["cli:codex/bridge/status"]},{"id":"contract:src/cli/codex/bridge/stop.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"all":{"type":"boolean"},"bindingId":{"type":"string"},"worker":{"type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/bridge/stop.tsx"},"routes":["cli:codex/bridge/stop"]},{"id":"contract:src/cli/codex/desktop-shim.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"action":{"description":"install | uninstall | status","enum":["install","uninstall","status"],"type":"string"}},"required":["action"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/desktop-shim.tsx"},"routes":["cli:codex/desktop-shim"]},{"id":"contract:src/cli/codex/list-threads.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque pagination cursor from a previous nextCursor (may start with -)","type":"string"},"limit":{"default":20,"description":"Max threads to list (1-200)","type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/list-threads.tsx"},"routes":["cli:codex/list-threads"]},{"id":"contract:src/cli/codex/new.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cwd":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"expectedCwd":{"type":"string"},"message":{"items":{"type":"string"},"type":"array"},"model":{"type":"string"}},"required":["cwd"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/new.tsx"},"routes":["cli:codex/new"]},{"id":"contract:src/cli/codex/queue.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"threadId":{"type":"string"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/queue.tsx"},"routes":["cli:codex/queue"]},{"id":"contract:src/cli/codex/send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"correlationId":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"envelope":{"type":"boolean"},"expectedCwd":{"type":"string"},"expectedTurnId":{"description":"Required active-turn guard for steer; stale guards reject.","type":"string"},"hop":{"type":"number"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"message":{"items":{"type":"string"},"type":"array"},"model":{"type":"string"},"replyTo":{"type":"string"},"replyToGrok":{"type":"string"},"requestId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"wait":{"type":"boolean"},"whenBusy":{"enum":["reject","queue","steer"],"type":"string"}},"required":["threadId","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/send.tsx"},"routes":["cli:codex/send"]},{"id":"contract:src/cli/codex/status.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/status.tsx"},"routes":["cli:codex/status"]},{"id":"contract:src/cli/codex/wait.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"messageId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"turnId":{"type":"string"}},"required":["threadId","turnId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/wait.tsx"},"routes":["cli:codex/wait"]},{"id":"contract:src/cli/codex/watch.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxEvents":{"description":"Maximum observed events: 1-500.","type":"number"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-600000 milliseconds.","type":"number"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/codex/watch.tsx"},"routes":["cli:codex/watch"]},{"id":"contract:src/cli/doctor.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/doctor.tsx"},"routes":["cli:doctor"]},{"id":"contract:src/cli/groups/add.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bot":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"}},"required":["group","bot"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/add.tsx"},"routes":["cli:groups/add"]},{"id":"contract:src/cli/groups/create.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"member":{"description":"Member bot id or name (repeatable)","items":{"type":"string"},"type":"array"},"name":{"type":"string"},"title":{"type":"string"}},"required":["name","member"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/create.tsx"},"routes":["cli:groups/create"]},{"id":"contract:src/cli/groups/delete.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/delete.tsx"},"routes":["cli:groups/delete"]},{"id":"contract:src/cli/groups/get.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/get.tsx"},"routes":["cli:groups/get"]},{"id":"contract:src/cli/groups/list.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/list.tsx"},"routes":["cli:groups/list"]},{"id":"contract:src/cli/groups/members.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"ref":{"description":"Group id or name","type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/members.tsx"},"routes":["cli:groups/members"]},{"id":"contract:src/cli/groups/remove.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bot":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"}},"required":["group","bot"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/remove.tsx"},"routes":["cli:groups/remove"]},{"id":"contract:src/cli/groups/set.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"group":{"type":"string"},"member":{"description":"Member bot id or name (repeatable)","items":{"type":"string"},"type":"array"}},"required":["group","member"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/set.tsx"},"routes":["cli:groups/set"]},{"id":"contract:src/cli/groups/update.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"avatarColor":{"type":"string"},"avatarShape":{"type":"string"},"description":{"type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"hidden":{"type":"string"},"name":{"type":"string"},"notify":{"type":"string"},"ref":{"description":"Group id or name","type":"string"},"title":{"type":"string"}},"required":["ref"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/groups/update.tsx"},"routes":["cli:groups/update"]},{"id":"contract:src/cli/history.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"limit":{"default":40,"description":"Maximum matching rows (1 or more)","type":"number"},"path":{"description":"Print the history file path","type":"boolean"},"ref":{"description":"Bot or group id or name","type":"string"},"search":{"description":"Case-insensitive message text filter","type":"string"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/history.tsx"},"routes":["cli:history"]},{"id":"contract:src/cli/send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"codexThreadId":{"type":"string"},"correlationId":{"description":"Stable correlation id for multi-hop replies","type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"envelope":{"description":"Prepend the [gbot …] header","type":"boolean"},"expectedCwd":{"type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"hop":{"description":"Hop count; refused at GROK_BOT_MAX_HOPS","type":"number"},"message":{"items":{"type":"string"},"type":"array"},"noHistory":{"description":"Skip local history for this command","type":"boolean"},"replyMode":{"enum":["auto","manual"],"type":"string"},"replyTo":{"description":"Prior message id this send replies to","type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/send.tsx"},"routes":["cli:send"]},{"id":"contract:src/cli/skills/add.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"path":{"description":"SKILL.md file, or a directory holding one","type":"string"}},"required":["path"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/skills/add.tsx"},"routes":["cli:skills/add"]},{"id":"contract:src/cli/skills/list.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/skills/list.tsx"},"routes":["cli:skills/list"]},{"id":"contract:src/cli/skills/remove.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"skill":{"description":"Skill id or name","type":"string"}},"required":["skill"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/skills/remove.tsx"},"routes":["cli:skills/remove"]},{"id":"contract:src/cli/thread.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"after":{"description":"Return entries after this opaque entry id","type":"string"},"dir":{"description":"Agents directory for --files mode","type":"string"},"files":{"description":"Force the on-disk agents store","type":"boolean"},"full":{"description":"Show full entry text in human output","type":"boolean"},"gateway":{"description":"Force the live gateway","type":"boolean"},"historyDir":{"description":"Directory containing history.jsonl","type":"string"},"limit":{"default":40,"description":"How many trailing entries to return (1-200)","type":"number"},"noHistory":{"description":"Skip local history for this command","type":"boolean"},"root":{"description":"Read one rooted thread by message id","type":"string"},"target":{"type":"string"}},"required":["target"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/cli/thread.tsx"},"routes":["cli:thread"]},{"id":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"port":{"type":"number"}},"required":[],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx"},"routes":["tool:grok-bot/chatgpt_desktop_list_hosts"]},{"id":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from the prior page; keep filters unchanged.","type":"string"},"groupBy":{"description":"When \"host\", also return groups[] keyed by host (grouping mode, not a host id).","enum":["host"],"type":"string"},"host":{"description":"Any string filter for machine/location. Reserved: \"all\" (default), \"local\". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.","type":"string"},"limit":{"default":50,"description":"Maximum threads per page.","type":"number"},"modelProvider":{"description":"Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.","type":"string"},"port":{"type":"number"},"project":{"description":"Match a Desktop project label or app-server projectId. Dynamic string, not an enum.","type":"string"}},"required":[],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx"},"routes":["tool:grok-bot/chatgpt_desktop_list_threads"]},{"id":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"openTimeoutMs":{"description":"Maximum time to wait for the requested conversation (1-600000 ms).","type":"number"},"port":{"description":"Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).","type":"number"},"threadId":{"description":"local: or bare durable id. Temporary local:client-new-thread:* works only while selected; remote ids require their owning host.","type":"string"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx"},"routes":["tool:grok-bot/chatgpt_desktop_open_thread"]},{"id":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from the prior read page. Keep threadId and full unchanged.","type":"string"},"full":{"description":"When true, page from the start through the complete history (up to 2000 turns by default); otherwise return the most recent 100 turns by default. Explicit limit bounds either mode.","type":"boolean"},"limit":{"type":"number"},"openTimeoutMs":{"description":"Max wait for Loading task… to clear after opening (default 90000).","type":"number"},"port":{"type":"number"},"threadId":{"description":"local:, bare durable id, or selected local:client-new-thread:* temporary id. Remote ids return REMOTE_THREAD_NOT_LOADED with hostId.","type":"string"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx"},"routes":["tool:grok-bot/chatgpt_desktop_read_thread"]},{"id":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from the previous search page; keep query and filters unchanged.","type":"string"},"groupBy":{"description":"When \"host\", also return groups[] keyed by host (grouping mode, not a host id).","enum":["host"],"type":"string"},"host":{"description":"Any string filter for machine/location. Reserved: \"all\" (default), \"local\". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.","type":"string"},"limit":{"default":50,"description":"Maximum matches.","type":"number"},"modelProvider":{"description":"Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.","type":"string"},"port":{"type":"number"},"project":{"description":"Match a Desktop project label or app-server projectId.","type":"string"},"query":{"description":"Case-insensitive substring over thread metadata.","type":"string"}},"required":["query"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx"},"routes":["tool:grok-bot/chatgpt_desktop_search_threads"]},{"id":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"openTimeoutMs":{"type":"number"},"port":{"type":"number"},"project":{"description":"With threadId omitted, create the new chat inside this project. Omit for a new chat outside projects.","type":"string"},"text":{"type":"string"},"threadId":{"description":"local:, bare durable id, or selected local:client-new-thread:*; omit for a new chat.","type":"string"},"unarchive":{"default":false,"description":"Existing threadId only: explicitly call thread/unarchive after ARCHIVED_THREAD, then retry the send once. Never implicit.","type":"boolean"}},"required":["text"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx"},"routes":["tool:grok-bot/chatgpt_desktop_send"]},{"id":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"port":{"description":"Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).","type":"number"}},"required":[],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx"},"routes":["tool:grok-bot/chatgpt_desktop_status"]},{"id":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"port":{"type":"number"},"threadId":{"description":"Durable local:, bare id, or selected local:client-new-thread:* from send.","type":"string"},"timeoutMs":{"default":120000,"type":"number"}},"required":[],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx"},"routes":["tool:grok-bot/chatgpt_desktop_wait_reply"]},{"id":"contract:src/mcp/grok-bot/tools/claude_send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"message":{"type":"string"},"name":{"type":"string"},"timeoutMs":{"type":"number"}},"required":["name","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/claude_send.tsx"},"routes":["tool:grok-bot/claude_send"]},{"id":"contract:src/mcp/grok-bot/tools/codex_new.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cwd":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"expectedCwd":{"type":"string"},"message":{"type":"string"},"model":{"type":"string"}},"required":["cwd"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_new.tsx"},"routes":["tool:grok-bot/codex_new"]},{"id":"contract:src/mcp/grok-bot/tools/codex_send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"correlationId":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"envelope":{"type":"boolean"},"expectedCwd":{"type":"string"},"expectedTurnId":{"type":"string"},"hop":{"type":"number"},"maxOutputBytes":{"type":"number"},"message":{"type":"string"},"model":{"type":"string"},"replyTo":{"type":"string"},"replyToGrok":{"type":"string"},"requestId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"type":"number"},"wait":{"default":false,"type":"boolean"},"whenBusy":{"enum":["reject","queue","steer"],"type":"string"}},"required":["threadId","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_send.tsx"},"routes":["tool:grok-bot/codex_send"]},{"id":"contract:src/mcp/grok-bot/tools/codex_threads.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from a previous page.","type":"string"},"limit":{"description":"Maximum threads in this page: 1-200. Pass modelProviders:[] internally so every provider is included; use cursor/nextCursor to page.","type":"number"}},"required":[],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_threads.tsx"},"routes":["tool:grok-bot/codex_threads"]},{"id":"contract:src/mcp/grok-bot/tools/codex_wait.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"messageId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"turnId":{"type":"string"}},"required":["threadId","turnId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_wait.tsx"},"routes":["tool:grok-bot/codex_wait"]},{"id":"contract:src/mcp/grok-bot/tools/codex_watch.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxEvents":{"description":"Maximum observed events: 1-500.","type":"number"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-600000 milliseconds.","type":"number"}},"required":["threadId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/codex_watch.tsx"},"routes":["tool:grok-bot/codex_watch"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_bridge_start.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"busyPolicy":{"enum":["steer","reject"],"type":"string"},"codexThreadId":{"type":"string"},"expectedCwd":{"type":"string"},"grokTarget":{"type":"string"},"requestId":{"type":"string"}},"required":["grokTarget"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_bridge_start.tsx"},"routes":["tool:grok-bot/gbot_bridge_start"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_bridge_status.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"limit":{"type":"number"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_bridge_status.tsx"},"routes":["tool:grok-bot/gbot_bridge_status"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_bridge_stop.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"all":{"type":"boolean"},"bindingId":{"type":"string"},"worker":{"type":"boolean"}},"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_bridge_stop.tsx"},"routes":["tool:grok-bot/gbot_bridge_stop"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_codex_respond.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"answersJson":{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","type":"string"},"bindingId":{"type":"string"},"decision":{"enum":["accept","decline","cancel"],"type":"string"},"exchangeId":{"type":"string"},"generation":{"type":"string"},"interactionId":{"type":"string"},"threadId":{"type":"string"},"turnId":{"type":"string"}},"required":["interactionId","generation","threadId","turnId"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_codex_respond.tsx"},"routes":["tool:grok-bot/gbot_codex_respond"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_grok_approvals.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"target":{"type":"string"}},"required":["target"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_grok_approvals.tsx"},"routes":["tool:grok-bot/gbot_grok_approvals"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_grok_respond.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"decision":{"enum":["accept","decline"],"type":"string"},"entryId":{"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","entryId","requestId","decision"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_grok_respond.tsx"},"routes":["tool:grok-bot/gbot_grok_respond"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_send.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"codexThreadId":{"type":"string"},"correlationId":{"type":"string"},"expectedCwd":{"type":"string"},"hop":{"description":"Explicit chain hop count, refused at the configured bound.","type":"number"},"message":{"type":"string"},"replyMode":{"enum":["auto","manual"],"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","message"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_send.tsx"},"routes":["tool:grok-bot/gbot_send"]},{"id":"contract:src/mcp/grok-bot/tools/gbot_thread.tsx#inputJsonSchema","input":{"additionalProperties":false,"properties":{"after":{"description":"Opaque cursor from the previous call. Returns entries strictly after it; an unknown cursor resets with a bounded snapshot.","type":"string"},"full":{"default":false,"description":"Return entries with text up to bounded budgets (20k chars per entry, 200k total). Every entry reports truncated/fullLength; read any remainder with `gbot thread --full` / `--json` on the machine.","type":"boolean"},"limit":{"default":40,"description":"How many trailing entries to inspect (1-200). Entries are returned only with full:true.","type":"number"},"target":{"description":"Bot or group name or id, for example \"General\".","type":"string"}},"required":["target"],"type":"object"},"origin":{"binding":"inputJsonSchema","module":"src/mcp/grok-bot/tools/gbot_thread.tsx"},"routes":["tool:grok-bot/gbot_thread"]}],"digest":"c0f041a6aedf5b5e8760c0125a69225707c642edb094259fa080c580fe89668c","events":[],"layouts":[],"providers":[],"scripts":[{"id":"script:gbot-relay","kind":"script","provenance":{"kind":"conventional"},"source":"src/scripts/gbot-relay.ts"}],"servers":[{"id":"mcp:grok-bot","mode":"generated","name":"grok-bot","routes":[{"contract":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx#inputJsonSchema","description":"Discover local, remote-control environment ids, managed SSH hosts, and modelProvider ids. hostName is null when Desktop has no real display name. Use hostId or label in list/search filters; remote thread ids must be read on the owning host. warnings report provider discovery failures.","id":"tool:grok-bot/chatgpt_desktop_list_hosts","inputSchema":{"additionalProperties":false,"properties":{"port":{"type":"number"}},"required":[],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx#inputJsonSchema","description":"List one sorted Desktop inventory without duplicate rows. Pass nextCursor as cursor with unchanged filters until null; pages fill limit unless the MCP byte budget binds. A changed/expired inventory cursor requires restarting. Display titles are capped at 200 characters with titleTruncated:true. Filter by dynamic hostId/hostName, modelProvider, or Desktop project label/projectId; projectRootPath identifies the resolved workspace. Rows expose local:, location, hostId, and provider; remote rows require their owning host for reading or sending. Backend and warnings identify partial sources.","id":"tool:grok-bot/chatgpt_desktop_list_threads","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from the prior page; keep filters unchanged.","type":"string"},"groupBy":{"description":"When \"host\", also return groups[] keyed by host (grouping mode, not a host id).","enum":["host"],"type":"string"},"host":{"description":"Any string filter for machine/location. Reserved: \"all\" (default), \"local\". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.","type":"string"},"limit":{"default":50,"description":"Maximum threads per page.","type":"number"},"modelProvider":{"description":"Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.","type":"string"},"port":{"type":"number"},"project":{"description":"Match a Desktop project label or app-server projectId. Dynamic string, not an enum.","type":"string"}},"required":[],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx#inputJsonSchema","description":"Navigate Desktop to an existing local: or bare conversation id, even when the row is absent from the sidebar. Archived routes return archived:true without unarchiving; read them with chatgpt_desktop_read_thread. A remote-control id is owned by its hostId and cannot be opened on this Mac. CDP_UNREACHABLE means Desktop must be reachable locally.","id":"tool:grok-bot/chatgpt_desktop_open_thread","inputSchema":{"additionalProperties":false,"properties":{"openTimeoutMs":{"description":"Maximum time to wait for the requested conversation (1-600000 ms).","type":"number"},"port":{"description":"Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).","type":"number"},"threadId":{"description":"local: or bare durable id. Temporary local:client-new-thread:* works only while selected; remote ids require their owning host.","type":"string"}},"required":["threadId"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx#inputJsonSchema","description":"Read a local:, bare durable id, or selected temporary local:client-new-thread:* id. Pass nextCursor back as cursor with the same threadId/full mode until complete:true. Pages stop at a byte budget; oversized turns use ordered continuation fragments with stable turnKey and field offsets, without dropping text. Archived threads remain readable. Warnings explain partial pages or app-server errors. REMOTE_THREAD_NOT_LOADED includes the owning hostId: call on that host.","id":"tool:grok-bot/chatgpt_desktop_read_thread","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from the prior read page. Keep threadId and full unchanged.","type":"string"},"full":{"description":"When true, page from the start through the complete history (up to 2000 turns by default); otherwise return the most recent 100 turns by default. Explicit limit bounds either mode.","type":"boolean"},"limit":{"type":"number"},"openTimeoutMs":{"description":"Max wait for Loading task… to clear after opening (default 90000).","type":"number"},"port":{"type":"number"},"threadId":{"description":"local:, bare durable id, or selected local:client-new-thread:* temporary id. Remote ids return REMOTE_THREAD_NOT_LOADED with hostId.","type":"string"}},"required":["threadId"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx#inputJsonSchema","description":"Search full Desktop thread metadata by case-insensitive substring. Display titles are capped at 200 characters with titleTruncated:true. Pass nextCursor as cursor with unchanged query and filters to retrieve all matches. Dynamic hostId/hostName, modelProvider, and project filters apply. Remote results identify hostId and cannot be read on this Mac.","id":"tool:grok-bot/chatgpt_desktop_search_threads","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from the previous search page; keep query and filters unchanged.","type":"string"},"groupBy":{"description":"When \"host\", also return groups[] keyed by host (grouping mode, not a host id).","enum":["host"],"type":"string"},"host":{"description":"Any string filter for machine/location. Reserved: \"all\" (default), \"local\". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.","type":"string"},"limit":{"default":50,"description":"Maximum matches.","type":"number"},"modelProvider":{"description":"Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.","type":"string"},"port":{"type":"number"},"project":{"description":"Match a Desktop project label or app-server projectId.","type":"string"},"query":{"description":"Case-insensitive substring over thread metadata.","type":"string"}},"required":["query"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx#inputJsonSchema","description":"Send once through local Desktop CDP. With threadId, accept local:, bare durable id, or currently selected local:client-new-thread:*; remote ids must be sent on their hostId. Omit threadId and project for a new chat outside projects; omit threadId and set project for a new chat inside it. The empty view and project scope are checked before typing. New-thread receipts include temporaryThreadId when seen and return durable local: once resolved. ARCHIVED_THREAD rejects without sending; unarchive:true explicitly unarchives that existing thread first. PROJECT_UNAVAILABLE identifies a disabled project action or missing configured root; NEW_CHAT_NAVIGATION_FAILED reports an unchanged view promptly. CDP_UNREACHABLE and COMPOSER_HAS_DRAFT require operator inspection, not blind retry.","id":"tool:grok-bot/chatgpt_desktop_send","inputSchema":{"additionalProperties":false,"properties":{"openTimeoutMs":{"type":"number"},"port":{"type":"number"},"project":{"description":"With threadId omitted, create the new chat inside this project. Omit for a new chat outside projects.","type":"string"},"text":{"type":"string"},"threadId":{"description":"local:, bare durable id, or selected local:client-new-thread:*; omit for a new chat.","type":"string"},"unarchive":{"default":false,"description":"Existing threadId only: explicitly call thread/unarchive after ARCHIVED_THREAD, then retry the send once. Never implicit.","type":"boolean"}},"required":["text"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx#inputJsonSchema","description":"Check local ChatGPT Desktop CDP before send, wait, or open. reachable:false and exitCode:1 mean CDP is down even if appServerFallback.reachable is true; list/search/read may still work through app-server. There is no remote CDP transport.","id":"tool:grok-bot/chatgpt_desktop_status","inputSchema":{"additionalProperties":false,"properties":{"port":{"description":"Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).","type":"number"}},"required":[],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx#inputJsonSchema","description":"After chatgpt_desktop_send, wait for the selected Desktop reply over CDP. Accept the returned durable local: or the temporary local:client-new-thread:* while selected; omit threadId only when staying on the same selected chat. Returns reply, durable threadId and conversationId. timeout is not proof the send failed; CDP_UNREACHABLE requires Desktop recovery. Remote ids must be handled on their hostId.","id":"tool:grok-bot/chatgpt_desktop_wait_reply","inputSchema":{"additionalProperties":false,"properties":{"port":{"type":"number"},"threadId":{"description":"Durable local:, bare id, or selected local:client-new-thread:* from send.","type":"string"},"timeoutMs":{"default":120000,"type":"number"}},"required":[],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/claude_send.tsx#inputJsonSchema","description":"Send to a named, opted-in live Claude Code channel and wait for its reply. Unknown delivery must not be retried automatically. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport.","id":"tool:grok-bot/claude_send","inputSchema":{"additionalProperties":false,"properties":{"message":{"type":"string"},"name":{"type":"string"},"timeoutMs":{"type":"number"}},"required":["name","message"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/claude_send.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_new.tsx#inputJsonSchema","description":"Start a new Codex thread on this machine in cwd, optionally sending its first message. Codex must run locally on the same machine as gbot.","id":"tool:grok-bot/codex_new","inputSchema":{"additionalProperties":false,"properties":{"cwd":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"expectedCwd":{"type":"string"},"message":{"type":"string"},"model":{"type":"string"}},"required":["cwd"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_new.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_send.tsx#inputJsonSchema","description":"Send to a Codex thread. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport. With replyToGrok or bindingId, managed delivery returns the terminal answer to Grok automatically. Otherwise optional wait observes completion and explicit steer requires expectedTurnId. Acceptance is not completion.","id":"tool:grok-bot/codex_send","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"correlationId":{"type":"string"},"effort":{"enum":["none","minimal","low","medium","high","xhigh"],"type":"string"},"envelope":{"type":"boolean"},"expectedCwd":{"type":"string"},"expectedTurnId":{"type":"string"},"hop":{"type":"number"},"maxOutputBytes":{"type":"number"},"message":{"type":"string"},"model":{"type":"string"},"replyTo":{"type":"string"},"replyToGrok":{"type":"string"},"requestId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"type":"number"},"wait":{"default":false,"type":"boolean"},"whenBusy":{"enum":["reject","queue","steer"],"type":"string"}},"required":["threadId","message"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_send.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_threads.tsx#inputJsonSchema","description":"Discover a bounded page of Codex daemon threads. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport.","id":"tool:grok-bot/codex_threads","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque nextCursor from a previous page.","type":"string"},"limit":{"description":"Maximum threads in this page: 1-200. Pass modelProviders:[] internally so every provider is included; use cursor/nextCursor to page.","type":"number"}},"required":[],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_threads.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_wait.tsx#inputJsonSchema","description":"Explicit diagnostic observation of one Codex turn; returns execution and final reply without interrupting it. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport.","id":"tool:grok-bot/codex_wait","inputSchema":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxOutputBytes":{"description":"Reply budget: 1-4194304 bytes.","type":"number"},"messageId":{"type":"string"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-7200000 milliseconds.","type":"number"},"turnId":{"type":"string"}},"required":["threadId","turnId"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_wait.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/codex_watch.tsx#inputJsonSchema","description":"Watch bounded Codex thread events for diagnostics without answering approvals. Runs on the user's registered machine via its local socket, not the Grok Bot box. From the box, use Grok Bot Shell with a machineId to run gbot there after codex app-server daemon start or bootstrap; gbot has no remote transport.","id":"tool:grok-bot/codex_watch","inputSchema":{"additionalProperties":false,"properties":{"expectedCwd":{"type":"string"},"maxEvents":{"description":"Maximum observed events: 1-500.","type":"number"},"threadId":{"type":"string"},"timeoutMs":{"description":"Observation timeout: 1-600000 milliseconds.","type":"number"}},"required":["threadId"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/codex_watch.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_bridge_start.tsx#inputJsonSchema","description":"Link a Grok conversation to Codex once. New visible Grok messages arrive automatically and Codex final answers return to Grok.","id":"tool:grok-bot/gbot_bridge_start","inputSchema":{"additionalProperties":false,"properties":{"busyPolicy":{"enum":["steer","reject"],"type":"string"},"codexThreadId":{"type":"string"},"expectedCwd":{"type":"string"},"grokTarget":{"type":"string"},"requestId":{"type":"string"}},"required":["grokTarget"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_bridge_start.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_bridge_status.tsx#inputJsonSchema","description":"Inspect worker health, route coverage, bounded receipts and scoped pending operator interactions.","id":"tool:grok-bot/gbot_bridge_status","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"limit":{"type":"number"}},"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_bridge_status.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_bridge_stop.tsx#inputJsonSchema","description":"Stop a binding without deleting receipts or interrupting Codex. all stops all bindings; worker explicitly shuts down the worker.","id":"tool:grok-bot/gbot_bridge_stop","inputSchema":{"additionalProperties":false,"properties":{"all":{"type":"boolean"},"bindingId":{"type":"string"},"worker":{"type":"boolean"}},"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_bridge_stop.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_codex_respond.tsx#inputJsonSchema","description":"Explicit operator response to a current scoped Codex interaction. Supports only one-time accept/decline/cancel or exact question-ID answers. Never auto-approve.","id":"tool:grok-bot/gbot_codex_respond","inputSchema":{"additionalProperties":false,"properties":{"answersJson":{"description":"JSON object mapping each advertised question ID to {\"answers\":[\"answer\"]}; exact IDs required.","type":"string"},"bindingId":{"type":"string"},"decision":{"enum":["accept","decline","cancel"],"type":"string"},"exchangeId":{"type":"string"},"generation":{"type":"string"},"interactionId":{"type":"string"},"threadId":{"type":"string"},"turnId":{"type":"string"}},"required":["interactionId","generation","threadId","turnId"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_codex_respond.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_grok_approvals.tsx#inputJsonSchema","description":"List pending auto-review and local-tool approval cards in the latest 200 entries for a Grok bot. Older or unsupported requests require the owning Grok UI.","id":"tool:grok-bot/gbot_grok_approvals","inputSchema":{"additionalProperties":false,"properties":{"target":{"type":"string"}},"required":["target"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_grok_approvals.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_grok_respond.tsx#inputJsonSchema","description":"Only after an explicit user decision: accept one current Grok approval once or decline it. Exact target, entryId and approval requestId required. Never auto-approve or grant persistent permissions. Success acknowledges response delivery, not execution.","id":"tool:grok-bot/gbot_grok_respond","inputSchema":{"additionalProperties":false,"properties":{"decision":{"enum":["accept","decline"],"type":"string"},"entryId":{"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","entryId","requestId","decision"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_grok_respond.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_send.tsx#inputJsonSchema","description":"Send to Grok Bot. Native Codex calls automatically receive replies in their originating thread; send once and continue work. Without a native source, supply codexThreadId or use manual gbot_thread reading.","id":"tool:grok-bot/gbot_send","inputSchema":{"additionalProperties":false,"properties":{"bindingId":{"type":"string"},"codexThreadId":{"type":"string"},"correlationId":{"type":"string"},"expectedCwd":{"type":"string"},"hop":{"description":"Explicit chain hop count, refused at the configured bound.","type":"number"},"message":{"type":"string"},"replyMode":{"enum":["auto","manual"],"type":"string"},"requestId":{"type":"string"},"target":{"type":"string"}},"required":["target","message"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_send.tsx"},{"contract":"contract:src/mcp/grok-bot/tools/gbot_thread.tsx#inputJsonSchema","description":"Read a bounded Grok Bot thread tail. Returns a small receipt by default; pass the last cursor as after for an exclusive client-side delta, or full:true to include bounded entry text.","id":"tool:grok-bot/gbot_thread","inputSchema":{"additionalProperties":false,"properties":{"after":{"description":"Opaque cursor from the previous call. Returns entries strictly after it; an unknown cursor resets with a bounded snapshot.","type":"string"},"full":{"default":false,"description":"Return entries with text up to bounded budgets (20k chars per entry, 200k total). Every entry reports truncated/fullLength; read any remainder with `gbot thread --full` / `--json` on the machine.","type":"boolean"},"limit":{"default":40,"description":"How many trailing entries to inspect (1-200). Entries are returned only with full:true.","type":"number"},"target":{"description":"Bot or group name or id, for example \"General\".","type":"string"}},"required":["target"],"type":"object"},"kind":"tool","provenance":{"kind":"conventional"},"serverId":"mcp:grok-bot","source":"src/mcp/grok-bot/tools/gbot_thread.tsx"}]}]},"runtime":{"node":"22.19.0"}} diff --git a/artifact/bin/gbot-flight.mjs b/artifact/bin/gbot-flight.mjs index 664e34f..1060ee8 100644 --- a/artifact/bin/gbot-flight.mjs +++ b/artifact/bin/gbot-flight.mjs @@ -8609,336 +8609,470 @@ var __webpack_modules__ = { resultSchema: resultSchema }); }, - "./src/cli/claude/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/chatgpt-desktop/hosts.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_2 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var _core_claude_routes_js__rspack_import_1 = __webpack_require__("./src/core/claude-routes.ts"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); const config = { - description: 'Send to an explicitly enabled live Claude Code channel and wait for its reply.', - positionals: [ - 'name', - 'message' - ], + description: 'Discover ChatGPT Desktop hosts and modelProviders at runtime (never hardcode). Always includes local plus every remote-thread-summaries-v3:; Providers come from distinct thread/list and remote-summary values.', exitCode: 'result', inputJsonSchema: { - type: 'object', additionalProperties: false, properties: { - name: { - type: 'string' - }, - message: { - type: 'string' - }, - timeoutMs: { + port: { + description: 'Local CDP port', type: 'number' } }, - required: [ - 'name', - 'message' - ] - }, - render: { - maxElapsedMs: 130000 + type: 'object' } }; - async function send({ input }) { - const result = await (0, _core_claude_routes_js__rspack_import_1.UP)(input); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_2.g.Result, { - value: result, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_2.g.Text, { - children: result.reply ?? result.error ?? result.delivery + const inputSchema = zod__rspack_import_2.Ikc({ + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional() + }).strict(); + async function chatgptDesktopHosts({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.Bm)(input); + const hosts = Array.isArray(out.hosts) ? out.hosts : []; + const text = hosts.length > 0 ? hosts.map((host)=>{ + const row = host; + const name = row.hostName && row.hostName !== row.hostId ? ` (${row.hostName})` : ''; + return `${row.hostId ?? '?'}${name} [${row.location ?? '?'}] threads=${row.threadCount ?? 0}`; + }).join('\n') + `\n\nhostsSource: ${String(out.hostsSource ?? '')}` + `\nmodelProviders: ${JSON.stringify(out.modelProviders ?? [])}` + `\nmodelProvidersSource: ${String(out.modelProvidersSource ?? '')}` : (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>send, - inputSchema: ()=>_core_claude_routes_js__rspack_import_1.is, - resultSchema: ()=>_core_claude_routes_js__rspack_import_1.FD + "default": ()=>chatgptDesktopHosts, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD }, { - config: config + config: config, + inputSchema: inputSchema }); }, - "./src/cli/codex/bridge/respond.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/chatgpt-desktop/read.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); - var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); const config = { - description: 'Managed Grok/Codex bridge respond.', + description: 'Read a ChatGPT Desktop thread via app-server (turns/list); CDP DOM is fallback only.', exitCode: 'result', + positionals: [ + 'threadId' + ], inputJsonSchema: { - type: 'object', + additionalProperties: false, properties: { - interactionId: { - type: 'string' + full: { + default: false, + type: 'boolean' }, - generation: { + cursor: { + description: 'Opaque nextCursor from the prior read page', type: 'string' }, - threadId: { - type: 'string' + limit: { + description: 'Recent default 100; full default 2000', + type: 'number' }, - turnId: { - type: 'string' + openTimeoutMs: { + default: 90000, + type: 'number' }, - bindingId: { - type: 'string' + port: { + type: 'number' }, - exchangeId: { + threadId: { type: 'string' - }, - decision: { - type: 'string', - enum: [ - 'accept', - 'decline', - 'cancel' - ] - }, - answersJson: { - type: 'string', - description: 'JSON object mapping each advertised question ID to {"answers":["answer"]}; exact IDs required.' } }, required: [ - 'interactionId', - 'generation', - 'threadId', - 'turnId' + 'threadId' ], - additionalProperties: false + type: 'object' + }, + render: { + maxElapsedMs: 660000 } }; - async function route({ input }) { - let out; - try { - out = { - ...await (0, _core_relay_routes_js__rspack_import_1.n1)('respond', input, await (0, _agent_bundle_runtime__rspack_import_3.MA)()), - exitCode: 0 - }; - } catch (error) { - out = (0, _core_codex_contract_js__rspack_import_2.DG)(error); - } - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { + const inputSchema = zod__rspack_import_2.Ikc({ + full: zod__rspack_import_2.zMY().default(false), + cursor: zod__rspack_import_2.YjP().min(1).max(4096).optional(), + limit: zod__rspack_import_2.aig().int().min(1).max(2000).optional(), + openTimeoutMs: zod__rspack_import_2.aig().int().min(1).max(600000).default(90000), + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional(), + threadId: zod__rspack_import_2.YjP().min(1).max(256) + }).strict(); + async function chatgptDesktopRead({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.UK)(input); + const turns = Array.isArray(out.turns) ? out.turns : []; + const text = turns.length > 0 ? turns.map((turn)=>{ + const row = turn; + const body = row.userText || row.assistantText ? [ + row.userText ? `user: ${row.userText}` : null, + row.assistantText ? `assistant: ${row.assistantText}` : null + ].filter(Boolean).join('\n') : row.text ?? ''; + return `[${row.role ?? '?'} ${row.turnKey ?? ''}]\n${body}`; + }).join('\n\n') + `\n\nbackend: ${String(out.backend ?? '')} full: ${String(out.full ?? false)}` : (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { - children: JSON.stringify(out) + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>route, - inputSchema: ()=>_core_relay_routes_js__rspack_import_1.v1, - resultSchema: ()=>_core_relay_routes_js__rspack_import_1.Ax + "default": ()=>chatgptDesktopRead, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD }, { - config: config + config: config, + inputSchema: inputSchema }); }, - "./src/cli/codex/bridge/run.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/chatgpt-desktop/search.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var node_child_process__rspack_import_1 = __webpack_require__("node:child_process"); - var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_relay_managed_js__rspack_import_2 = __webpack_require__("./src/core/relay/managed.js"); - const inputSchema = zod__rspack_import_3.Ikc({ - lifetimeMs: zod__rspack_import_3.aig().int().min(100).max(82800000).default(82800000) - }).strict(); - const resultSchema = zod__rspack_import_3.Ikc({ - state: zod__rspack_import_3.YjP(), - exitCode: zod__rspack_import_3.aig() - }); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); const config = { - description: "Run a bounded foreground relay (up to 23 hours). Use the packaged gbot-relay.mjs script for unlimited service lifetime.", + description: 'Search ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.', exitCode: 'result', - render: { - maxElapsedMs: 86400000 - }, + positionals: [ + 'query' + ], inputJsonSchema: { - type: 'object', + additionalProperties: false, properties: { - lifetimeMs: { - type: 'number', - description: 'Foreground lifetime in milliseconds (100..82800000; default 23h).' + groupBy: { + description: 'Group results by host (grouping mode, not a host id)', + enum: [ + 'host' + ], + type: 'string' + }, + host: { + description: 'Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string' + }, + limit: { + default: 50, + description: 'Max matches (1-200)', + type: 'number' + }, + cursor: { + description: 'Opaque nextCursor from a prior search page', + type: 'string' + }, + modelProvider: { + description: 'Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string' + }, + project: { + description: 'Project label or projectId filter.', + type: 'string' + }, + port: { + description: 'Local CDP port', + type: 'number' + }, + query: { + type: 'string' } }, - additionalProperties: false + required: [ + 'query' + ], + type: 'object' } }; - async function route({ signal, input }) { - const context = await (0, _agent_bundle_runtime__rspack_import_4.MA)(); - const script = await (0, _core_relay_managed_js__rspack_import_2._v)({ - pluginRoot: context.plugin.state === 'available' ? context.plugin.value.root : undefined - }); - const child = (0, node_child_process__rspack_import_1.spawn)(process.execPath, [ - script, - '--lifetime-ms', - String(input.lifetimeMs), - '--parent-pid', - String(process.pid) - ], { - stdio: [ - 'ignore', - 'ignore', - 'inherit' - ] - }); - const stop = ()=>{ - child.kill('SIGTERM'); - }; - signal.addEventListener('abort', stop, { - once: true - }); - if (signal.aborted) stop(); - let exitCode; - try { - exitCode = await new Promise((resolve, reject)=>{ - child.once('error', reject); - child.once('exit', (code)=>resolve(code ?? 1)); - }); - } finally{ - signal.removeEventListener('abort', stop); - } - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { - value: { - state: 'stopped', - exitCode - }, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: "Relay worker stopped." + const inputSchema = zod__rspack_import_2.Ikc({ + groupBy: zod__rspack_import_2.euz('host').optional(), + host: zod__rspack_import_2.YjP().min(1).max(256).optional(), + limit: zod__rspack_import_2.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_2.YjP().min(1).max(4096).optional(), + modelProvider: zod__rspack_import_2.YjP().min(1).max(256).optional(), + project: zod__rspack_import_2.YjP().min(1).max(256).optional(), + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional(), + query: zod__rspack_import_2.YjP().min(1).max(512) + }).strict(); + async function chatgptDesktopSearch({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.zg)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>route + "default": ()=>chatgptDesktopSearch, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD }, { config: config, - inputSchema: inputSchema, - resultSchema: resultSchema + inputSchema: inputSchema }); }, - "./src/cli/codex/bridge/start.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/chatgpt-desktop/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); - var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); const config = { - description: 'Managed Grok/Codex bridge start.', + description: 'Send to ChatGPT Desktop via CDP composer. Omit --thread-id for a new chat; --project prefers Start new chat in .', exitCode: 'result', positionals: [ - 'grokTarget' + 'text' ], inputJsonSchema: { - type: 'object', + additionalProperties: false, properties: { - grokTarget: { - type: 'string' + openTimeoutMs: { + type: 'number' }, - codexThreadId: { - type: 'string' + port: { + type: 'number' }, - expectedCwd: { + project: { type: 'string' }, - busyPolicy: { - type: 'string', - enum: [ - 'steer', - 'reject' - ] + text: { + type: 'array', + items: { + type: 'string' + } }, - requestId: { + threadId: { type: 'string' + }, + unarchive: { + type: 'boolean', + description: 'Explicitly unarchive an existing archived thread before sending.' } }, required: [ - 'grokTarget' + 'text' ], - additionalProperties: false - } - }; - async function route({ input }) { - let out; - try { - out = { - ...await (0, _core_relay_routes_js__rspack_import_1.n1)('startBinding', input, await (0, _agent_bundle_runtime__rspack_import_3.MA)()), - exitCode: 0 - }; - } catch (error) { - out = (0, _core_codex_contract_js__rspack_import_2.DG)(error); + type: 'object' + }, + render: { + maxElapsedMs: 660000 } - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { + }; + const inputSchema = zod__rspack_import_2.Ikc({ + openTimeoutMs: zod__rspack_import_2.aig().int().min(1).max(600000).default(90000), + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional(), + project: zod__rspack_import_2.YjP().min(1).max(256).optional(), + text: zod__rspack_import_2.YOg(zod__rspack_import_2.YjP()).min(1), + threadId: zod__rspack_import_2.YjP().min(1).max(256).optional(), + unarchive: zod__rspack_import_2.zMY().default(false) + }).strict(); + async function chatgptDesktopSend({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.UP)({ + port: input.port, + threadId: input.threadId, + text: input.text.join(' ').trim(), + project: input.project, + unarchive: input.unarchive, + openTimeoutMs: input.openTimeoutMs + }); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { - children: JSON.stringify(out) + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>route, - inputSchema: ()=>_core_relay_routes_js__rspack_import_1.xD, - resultSchema: ()=>_core_relay_routes_js__rspack_import_1.Ax + "default": ()=>chatgptDesktopSend, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD }, { - config: config + config: config, + inputSchema: inputSchema }); }, - "./src/cli/codex/bridge/status.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/chatgpt-desktop/status.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); - var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); const config = { - description: 'Managed Grok/Codex bridge status.', + description: 'Probe local ChatGPT Desktop CDP on 127.0.0.1 and report app-server availability for list/deep-read.', exitCode: 'result', inputJsonSchema: { - type: 'object', + additionalProperties: false, properties: { - bindingId: { + port: { + description: 'Local CDP port (default 9222)', + type: 'number' + } + }, + type: 'object' + } + }; + const inputSchema = zod__rspack_import_2.Ikc({ + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional() + }).strict(); + async function chatgptDesktopStatus({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.Uj)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out) + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>chatgptDesktopStatus, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD + }, { + config: config, + inputSchema: inputSchema + }); + }, + "./src/cli/chatgpt-desktop/threads.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const config = { + description: 'List local + remote-control ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.', + exitCode: 'result', + inputJsonSchema: { + additionalProperties: false, + properties: { + groupBy: { + description: 'Group results by host (grouping mode, not a host id)', + enum: [ + 'host' + ], + type: 'string' + }, + host: { + description: 'Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.', type: 'string' }, limit: { + default: 50, + description: 'Max threads (1-200)', + type: 'number' + }, + cursor: { + description: 'Opaque nextCursor from a previous page', + type: 'string' + }, + modelProvider: { + description: 'Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string' + }, + project: { + description: 'Project label or projectId filter.', + type: 'string' + }, + port: { + description: 'Local CDP port', type: 'number' } }, - additionalProperties: false + type: 'object' } }; - async function route({ input }) { - let out; - try { - out = { - ...await (0, _core_relay_routes_js__rspack_import_1.n1)('status', input, await (0, _agent_bundle_runtime__rspack_import_3.MA)()), - exitCode: 0 - }; - } catch (error) { - out = (0, _core_codex_contract_js__rspack_import_2.DG)(error); - } - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { + const inputSchema = zod__rspack_import_2.Ikc({ + groupBy: zod__rspack_import_2.euz('host').optional(), + host: zod__rspack_import_2.YjP().min(1).max(256).optional(), + limit: zod__rspack_import_2.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_2.YjP().min(1).max(4096).optional(), + modelProvider: zod__rspack_import_2.YjP().min(1).max(256).optional(), + project: zod__rspack_import_2.YjP().min(1).max(256).optional(), + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional() + }).strict(); + async function chatgptDesktopThreads({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.tJ)(input); + const threads = Array.isArray(out.threads) ? out.threads : []; + const text = threads.length > 0 ? threads.map((thread)=>{ + const row = thread; + const where = row.location === 'remote' ? ` [remote ${row.hostName || row.hostId || '?'}]` : ' [local]'; + const provider = row.modelProvider ? ` {${row.modelProvider}}` : ''; + return `${row.selected ? '* ' : ' '}${row.threadId ?? ''} ${row.title ?? ''}` + (row.pinned ? ' (pinned)' : '') + where + provider; + }).join('\n') + `\n\nbackend: ${String(out.backend ?? '')}` + (out.nextCursor ? `\nnextCursor: ${out.nextCursor}` : '') : (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { - children: JSON.stringify(out) + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>route, - inputSchema: ()=>_core_relay_routes_js__rspack_import_1.np, - resultSchema: ()=>_core_relay_routes_js__rspack_import_1.Ax + "default": ()=>chatgptDesktopThreads, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD + }, { + config: config, + inputSchema: inputSchema + }); + }, + "./src/cli/claude/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_2 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var _core_claude_routes_js__rspack_import_1 = __webpack_require__("./src/core/claude-routes.ts"); + const config = { + description: 'Send to an explicitly enabled live Claude Code channel and wait for its reply.', + positionals: [ + 'name', + 'message' + ], + exitCode: 'result', + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string' + }, + message: { + type: 'string' + }, + timeoutMs: { + type: 'number' + } + }, + required: [ + 'name', + 'message' + ] + }, + render: { + maxElapsedMs: 130000 + } + }; + async function send({ input }) { + const result = await (0, _core_claude_routes_js__rspack_import_1.UP)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_2.g.Result, { + value: result, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_2.g.Text, { + children: result.reply ?? result.error ?? result.delivery + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>send, + inputSchema: ()=>_core_claude_routes_js__rspack_import_1.is, + resultSchema: ()=>_core_claude_routes_js__rspack_import_1.FD }, { config: config }); }, - "./src/cli/codex/bridge/stop.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/bridge/respond.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); @@ -8946,21 +9080,48 @@ var __webpack_modules__ = { var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); const config = { - description: 'Managed Grok/Codex bridge stop.', + description: 'Managed Grok/Codex bridge respond.', exitCode: 'result', inputJsonSchema: { type: 'object', properties: { + interactionId: { + type: 'string' + }, + generation: { + type: 'string' + }, + threadId: { + type: 'string' + }, + turnId: { + type: 'string' + }, bindingId: { type: 'string' }, - all: { - type: 'boolean' + exchangeId: { + type: 'string' }, - worker: { - type: 'boolean' + decision: { + type: 'string', + enum: [ + 'accept', + 'decline', + 'cancel' + ] + }, + answersJson: { + type: 'string', + description: 'JSON object mapping each advertised question ID to {"answers":["answer"]}; exact IDs required.' } }, + required: [ + 'interactionId', + 'generation', + 'threadId', + 'turnId' + ], additionalProperties: false } }; @@ -8968,7 +9129,7 @@ var __webpack_modules__ = { let out; try { out = { - ...await (0, _core_relay_routes_js__rspack_import_1.n1)('stop', input, await (0, _agent_bundle_runtime__rspack_import_3.MA)()), + ...await (0, _core_relay_routes_js__rspack_import_1.n1)('respond', input, await (0, _agent_bundle_runtime__rspack_import_3.MA)()), exitCode: 0 }; } catch (error) { @@ -8983,1126 +9144,964 @@ var __webpack_modules__ = { } __webpack_require__.d(__webpack_exports__, { "default": ()=>route, - inputSchema: ()=>_core_relay_routes_js__rspack_import_1.d5, + inputSchema: ()=>_core_relay_routes_js__rspack_import_1.v1, resultSchema: ()=>_core_relay_routes_js__rspack_import_1.Ax }, { config: config }); }, - "./src/cli/codex/desktop-shim.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/bridge/run.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_desktop_shim_js__rspack_import_1 = __webpack_require__("./src/core/desktop-shim.js"); - var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); + var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); + var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var node_child_process__rspack_import_1 = __webpack_require__("node:child_process"); + var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_relay_managed_js__rspack_import_2 = __webpack_require__("./src/core/relay/managed.js"); + const inputSchema = zod__rspack_import_3.Ikc({ + lifetimeMs: zod__rspack_import_3.aig().int().min(100).max(82800000).default(82800000) + }).strict(); + const resultSchema = zod__rspack_import_3.Ikc({ + state: zod__rspack_import_3.YjP(), + exitCode: zod__rspack_import_3.aig() + }); const config = { - description: 'Install, remove, or check the ChatGPT Desktop shim: a CODEX_CLI_PATH wrapper that bridges Desktop stdio onto the managed Codex daemon (stock app-server proxy hangs). macOS persists via LaunchAgent; always fails open to real Codex.', + description: "Run a bounded foreground relay (up to 23 hours). Use the packaged gbot-relay.mjs script for unlimited service lifetime.", exitCode: 'result', + render: { + maxElapsedMs: 86400000 + }, inputJsonSchema: { - additionalProperties: false, + type: 'object', properties: { - action: { - description: 'install | uninstall | status', - enum: [ - 'install', - 'uninstall', - 'status' - ], - type: 'string' + lifetimeMs: { + type: 'number', + description: 'Foreground lifetime in milliseconds (100..82800000; default 23h).' } }, - required: [ - 'action' - ], - type: 'object' - }, - positionals: [ - 'action' - ] + additionalProperties: false + } }; - const inputSchema = zod__rspack_import_2.Ikc({ - action: zod__rspack_import_2.k5n([ - 'install', - 'uninstall', - 'status' - ]) - }).strict(); - const resultSchema = zod__rspack_import_2.Ikc({ - exitCode: zod__rspack_import_2.KCZ([ - zod__rspack_import_2.euz(0), - zod__rspack_import_2.euz(1) - ]) - }).passthrough(); - const formatWarnings = (warnings)=>warnings.length === 0 ? '' : `\nwarnings:\n${warnings.map((w)=>` - ${String(w)}`).join('\n')}`; - async function codexDesktopShim({ input }) { - switch(input.action){ - case 'install': - { - let out; - try { - out = (0, _core_desktop_shim_js__rspack_import_1.B0)(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: { - action: 'install', - error: message, - exitCode: 1 - }, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Desktop shim install failed: ${message}` - }) - }); - } - const text = `Installed ChatGPT Desktop shim:\n` + ` wrapper: ${out.wrapperPath}\n` + ` bridge: ${out.bridgePath}\n` + ` login env: ${out.envScriptPath}` + (out.plistPath ? `\n LaunchAgent: ${out.plistPath}` : '\n LaunchAgent: n/a (macOS-only)') + `\nFully quit and relaunch ChatGPT.app so it inherits CODEX_CLI_PATH; ` + `Desktop falls back to stock Codex if the shim is ever removed.` + formatWarnings(out.warnings); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: text - }) - }); - } - case 'uninstall': - { - let out; - try { - out = (0, _core_desktop_shim_js__rspack_import_1.gM)(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: { - action: 'uninstall', - error: message, - exitCode: 1 - }, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Desktop shim uninstall failed: ${message}` - }) - }); - } - const removed = out.removed.length === 0 ? '(nothing installed)' : out.removed.join(', '); - const text = `Removed ChatGPT Desktop shim: ${removed}. ` + `Desktop and Codex fall back to stock behavior; relaunch ChatGPT.app to pick it up.` + formatWarnings(out.warnings); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: text - }) - }); - } - case 'status': - { - const out = (0, _core_desktop_shim_js__rspack_import_1.em)(); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: (0, _core_format_js__rspack_import_4.fX)(out) - }) - }); - } - default: - { - const _exhaustive = input.action; - throw new Error(`unknown desktop-shim action: ${String(_exhaustive)}`); - } - } - } - __webpack_require__.d(__webpack_exports__, { - "default": ()=>codexDesktopShim - }, { - config: config, - inputSchema: inputSchema, - resultSchema: resultSchema - }); - }, - "./src/cli/codex/list-threads.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { - __webpack_require__.r(__webpack_exports__); - var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_codex_bridge_js__rspack_import_1 = __webpack_require__("./src/core/codex-bridge.js"); - var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); - var _core_format_js__rspack_import_6 = __webpack_require__("./src/core/format.js"); - var _shared_js__rspack_import_3 = __webpack_require__("./src/cli/_shared.ts"); - const config = { - description: 'List Codex daemon-managed threads.', - exitCode: 'result', - inputJsonSchema: { - additionalProperties: false, - properties: { - cursor: { - description: 'Opaque pagination cursor (may start with -)', - type: 'string' - }, - limit: { - default: 20, - description: 'Max threads to list (1-200)', - type: 'number' - } - }, - type: 'object' - } - }; - const inputSchema = zod__rspack_import_4.Ikc({ - cursor: zod__rspack_import_4.YjP().min(1).optional(), - limit: zod__rspack_import_4.aig().int().min(1).max(200).default(20) - }).strict(); - const resultSchema = zod__rspack_import_4.KCZ([ - zod__rspack_import_4.Ikc({ - exitCode: zod__rspack_import_4.euz(0), - limit: zod__rspack_import_4.aig().int().min(1).max(200), - nextCursor: zod__rspack_import_4.YjP().nullable(), - threads: zod__rspack_import_4.YOg(zod__rspack_import_4.g1P(zod__rspack_import_4.YjP(), zod__rspack_import_4.Pq9())) - }).strict(), - _shared_js__rspack_import_3.R9 - ]); - async function codexListThreads({ input }) { - let out; + async function route({ signal, input }) { + const context = await (0, _agent_bundle_runtime__rspack_import_4.MA)(); + const script = await (0, _core_relay_managed_js__rspack_import_2._v)({ + pluginRoot: context.plugin.state === 'available' ? context.plugin.value.root : undefined + }); + const child = (0, node_child_process__rspack_import_1.spawn)(process.execPath, [ + script, + '--lifetime-ms', + String(input.lifetimeMs), + '--parent-pid', + String(process.pid) + ], { + stdio: [ + 'ignore', + 'ignore', + 'inherit' + ] + }); + const stop = ()=>{ + child.kill('SIGTERM'); + }; + signal.addEventListener('abort', stop, { + once: true + }); + if (signal.aborted) stop(); + let exitCode; try { - out = await (0, _core_codex_bridge_js__rspack_import_1.X$)({ - cursor: input.cursor, - limit: input.limit - }); - } catch (error) { - const failure = (0, _core_codex_contract_js__rspack_import_2.DG)(error); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { - value: failure, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: failure.error - }) + exitCode = await new Promise((resolve, reject)=>{ + child.once('error', reject); + child.once('exit', (code)=>resolve(code ?? 1)); }); + } finally{ + signal.removeEventListener('abort', stop); } - const more = out.nextCursor ? `\n\nmore: --cursor ${JSON.stringify((0, _core_codex_bridge_js__rspack_import_1.pF)(out.nextCursor))}` : ''; - const text = out.threads.length === 0 ? 'No Codex threads.' : out.threads.map(_core_format_js__rspack_import_6.nz).join('\n\n') + more; return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { value: { - ...out, - exitCode: 0 + state: 'stopped', + exitCode }, children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: text + children: "Relay worker stopped." }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>codexListThreads + "default": ()=>route }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/codex/new.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/bridge/start.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_codex_bridge_js__rspack_import_1 = __webpack_require__("./src/core/codex-bridge.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); + var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); - var _core_codex_routes_js__rspack_import_3 = __webpack_require__("./src/core/codex/routes.ts"); - const inputSchema = zod__rspack_import_4.Ikc({ - cwd: zod__rspack_import_4.YjP().min(1), - expectedCwd: zod__rspack_import_4.YjP().min(1).optional(), - model: zod__rspack_import_4.YjP().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/).optional(), - effort: zod__rspack_import_4.k5n([ - 'none', - 'minimal', - 'low', - 'medium', - 'high', - 'xhigh' - ]).optional(), - message: zod__rspack_import_4.YOg(zod__rspack_import_4.YjP()).optional() - }).strict(); const config = { - description: 'Start a new Codex thread in --cwd, optionally with --model, --effort and a message.', + description: 'Managed Grok/Codex bridge start.', exitCode: 'result', positionals: [ - 'message' + 'grokTarget' ], inputJsonSchema: { type: 'object', - additionalProperties: false, properties: { - cwd: { + grokTarget: { type: 'string' }, - expectedCwd: { + codexThreadId: { type: 'string' }, - model: { + expectedCwd: { type: 'string' }, - effort: { + busyPolicy: { type: 'string', enum: [ - 'none', - 'minimal', - 'low', - 'medium', - 'high', - 'xhigh' + 'steer', + 'reject' ] }, - message: { - type: 'array', - items: { - type: 'string' - } + requestId: { + type: 'string' } }, required: [ - 'cwd' - ] + 'grokTarget' + ], + additionalProperties: false } }; - async function route({ input, signal }) { + async function route({ input }) { let out; try { - out = await (0, _core_codex_bridge_js__rspack_import_1.QC)({ - ...input, - message: input.message?.join(' ').trim() || undefined, - signal - }); + out = { + ...await (0, _core_relay_routes_js__rspack_import_1.n1)('startBinding', input, await (0, _agent_bundle_runtime__rspack_import_3.MA)()), + exitCode: 0 + }; } catch (error) { out = (0, _core_codex_contract_js__rspack_import_2.DG)(error); } - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: out.threadId && !('delivery' in out) ? `Started Codex thread ${out.threadId}` : (0, _core_codex_routes_js__rspack_import_3.D6)(out) + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { + children: JSON.stringify(out) }) }); } __webpack_require__.d(__webpack_exports__, { "default": ()=>route, - resultSchema: ()=>_core_codex_routes_js__rspack_import_3.FD + inputSchema: ()=>_core_relay_routes_js__rspack_import_1.xD, + resultSchema: ()=>_core_relay_routes_js__rspack_import_1.Ax }, { - config: config, - inputSchema: inputSchema + config: config }); }, - "./src/cli/codex/queue.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/bridge/status.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_codex_bridge_js__rspack_import_1 = __webpack_require__("./src/core/codex-bridge.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); + var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); - var _core_format_js__rspack_import_6 = __webpack_require__("./src/core/format.js"); - var _shared_js__rspack_import_3 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'List the experimental Codex thread queue (GROK_BOT_CODEX_EXPERIMENTAL=1).', + description: 'Managed Grok/Codex bridge status.', exitCode: 'result', inputJsonSchema: { - additionalProperties: false, + type: 'object', properties: { - threadId: { + bindingId: { type: 'string' + }, + limit: { + type: 'number' } }, - required: [ - 'threadId' - ], - type: 'object' - }, - positionals: [ - 'threadId' - ] + additionalProperties: false + } }; - const inputSchema = zod__rspack_import_4.Ikc({ - threadId: zod__rspack_import_4.YjP().min(1) - }).strict(); - const resultSchema = zod__rspack_import_4.KCZ([ - zod__rspack_import_4.Ikc({ - exitCode: zod__rspack_import_4.euz(0), - nextCursor: zod__rspack_import_4.YjP().nullable(), - queued: zod__rspack_import_4.YOg(zod__rspack_import_4.Ikc({ - clientUserMessageId: zod__rspack_import_4.YjP().nullable(), - id: zod__rspack_import_4.YjP().nullable(), - text: zod__rspack_import_4.YjP() - }).strict()), - threadId: zod__rspack_import_4.YjP() - }).strict(), - _shared_js__rspack_import_3.R9 - ]); - async function codexQueue({ input }) { + async function route({ input }) { let out; try { - out = await (0, _core_codex_bridge_js__rspack_import_1.Bt)(input.threadId); + out = { + ...await (0, _core_relay_routes_js__rspack_import_1.n1)('status', input, await (0, _agent_bundle_runtime__rspack_import_3.MA)()), + exitCode: 0 + }; } catch (error) { - const failure = (0, _core_codex_contract_js__rspack_import_2.DG)(error); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { - value: failure, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: failure.error - }) - }); + out = (0, _core_codex_contract_js__rspack_import_2.DG)(error); } - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { - value: { - ...out, - exitCode: 0 - }, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: (0, _core_format_js__rspack_import_6.e_)(out) + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { + children: JSON.stringify(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>codexQueue + "default": ()=>route, + inputSchema: ()=>_core_relay_routes_js__rspack_import_1.np, + resultSchema: ()=>_core_relay_routes_js__rspack_import_1.Ax }, { - config: config, - inputSchema: inputSchema, - resultSchema: resultSchema + config: config }); }, - "./src/cli/codex/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/bridge/stop.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_codex_routes_js__rspack_import_1 = __webpack_require__("./src/core/codex/routes.ts"); - var _core_relay_routes_js__rspack_import_2 = __webpack_require__("./src/core/relay/routes.ts"); - const resultSchema = zod__rspack_import_3.KCZ([ - _core_codex_routes_js__rspack_import_1.FD, - _core_relay_routes_js__rspack_import_2.Ax - ]); - const inputSchema = zod__rspack_import_3.Ikc({ - ..._core_codex_routes_js__rspack_import_1.at, - whenBusy: zod__rspack_import_3.k5n([ - 'reject', - 'queue', - 'steer' - ]).optional(), - replyToGrok: zod__rspack_import_3.YjP().min(1).optional(), - bindingId: zod__rspack_import_3.YjP().min(1).optional(), - requestId: zod__rspack_import_3.YjP().min(1).max(128).optional(), - correlationId: zod__rspack_import_3.YjP().min(1).optional(), - replyTo: zod__rspack_import_3.YjP().min(1).optional(), - message: zod__rspack_import_3.YOg(zod__rspack_import_3.YjP()).min(1) - }).strict(); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); + var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); + var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); const config = { - description: 'Send to Codex; acceptance is distinct from completion. Options precede threadId.', + description: 'Managed Grok/Codex bridge stop.', exitCode: 'result', - render: { - maxElapsedMs: 7260000 - }, - positionals: [ - 'threadId', - 'message' - ], inputJsonSchema: { type: 'object', - additionalProperties: false, properties: { - replyToGrok: { - type: 'string' - }, bindingId: { type: 'string' }, - requestId: { - type: 'string' - }, - threadId: { - type: 'string' - }, - expectedCwd: { - type: 'string' - }, - model: { - type: 'string' - }, - effort: { - type: 'string', - enum: [ - 'none', - 'minimal', - 'low', - 'medium', - 'high', - 'xhigh' - ] - }, - timeoutMs: { - type: 'number', - description: 'Observation timeout: 1-7200000 milliseconds.' - }, - correlationId: { - type: 'string' - }, - envelope: { + all: { type: 'boolean' }, - hop: { - type: 'number' - }, - replyTo: { - type: 'string' - }, - expectedTurnId: { - type: 'string', - description: 'Required active-turn guard for steer; stale guards reject.' - }, - wait: { + worker: { type: 'boolean' - }, - maxOutputBytes: { - type: 'number', - description: 'Reply budget: 1-4194304 bytes.' - }, - whenBusy: { - type: 'string', - enum: [ - 'reject', - 'queue', - 'steer' - ] - }, - message: { - type: 'array', - items: { - type: 'string' - } } }, - required: [ - 'threadId', - 'message' - ] + additionalProperties: false } }; - async function route({ input, signal }) { - const context = await (0, _agent_bundle_runtime__rspack_import_4.MA)(); - if (input.replyToGrok || input.bindingId) { - const out = await (0, _core_relay_routes_js__rspack_import_2.lt)({ - ...input, - message: input.message.join(' ').trim() - }, context); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { - value: { - ...out, - exitCode: out.delivery === 'rejected' ? 1 : 0 - }, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: `Delivery ${out.delivery}; terminal answer returns to Grok automatically.` - }) - }); + async function route({ input }) { + let out; + try { + out = { + ...await (0, _core_relay_routes_js__rspack_import_1.n1)('stop', input, await (0, _agent_bundle_runtime__rspack_import_3.MA)()), + exitCode: 0 + }; + } catch (error) { + out = (0, _core_codex_contract_js__rspack_import_2.DG)(error); } - const out = await (0, _core_codex_routes_js__rspack_import_1.UP)({ - ...input, - whenBusy: input.whenBusy ?? 'reject', - message: input.message.join(' ').trim() - }, signal, (message)=>context.progress.report({ - message - }), true); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: (0, _core_codex_routes_js__rspack_import_1.D6)(out) + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { + children: JSON.stringify(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>route + "default": ()=>route, + inputSchema: ()=>_core_relay_routes_js__rspack_import_1.d5, + resultSchema: ()=>_core_relay_routes_js__rspack_import_1.Ax }, { - config: config, - inputSchema: inputSchema, - resultSchema: resultSchema + config: config }); }, - "./src/cli/codex/status.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/desktop-shim.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_codex_bridge_js__rspack_import_1 = __webpack_require__("./src/core/codex-bridge.js"); + var _core_desktop_shim_js__rspack_import_1 = __webpack_require__("./src/core/desktop-shim.js"); var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); const config = { - description: 'Probe the local Codex app-server daemon. Exit 0 only when it is usable.', + description: 'Install, remove, or check the ChatGPT Desktop shim: a CODEX_CLI_PATH wrapper that bridges Desktop stdio onto the managed Codex daemon (stock app-server proxy hangs). macOS persists via LaunchAgent; always fails open to real Codex.', exitCode: 'result', inputJsonSchema: { additionalProperties: false, - properties: {}, + properties: { + action: { + description: 'install | uninstall | status', + enum: [ + 'install', + 'uninstall', + 'status' + ], + type: 'string' + } + }, + required: [ + 'action' + ], type: 'object' - } - }; - const inputSchema = zod__rspack_import_2.Ikc({}).strict(); + }, + positionals: [ + 'action' + ] + }; + const inputSchema = zod__rspack_import_2.Ikc({ + action: zod__rspack_import_2.k5n([ + 'install', + 'uninstall', + 'status' + ]) + }).strict(); const resultSchema = zod__rspack_import_2.Ikc({ exitCode: zod__rspack_import_2.KCZ([ zod__rspack_import_2.euz(0), zod__rspack_import_2.euz(1) ]) }).passthrough(); - async function codexStatusCmd(_props) { - const status = await (0, _core_codex_bridge_js__rspack_import_1.MU)(); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: status, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: (0, _core_format_js__rspack_import_4.P2)(status) - }) - }); + const formatWarnings = (warnings)=>warnings.length === 0 ? '' : `\nwarnings:\n${warnings.map((w)=>` - ${String(w)}`).join('\n')}`; + async function codexDesktopShim({ input }) { + switch(input.action){ + case 'install': + { + let out; + try { + out = (0, _core_desktop_shim_js__rspack_import_1.B0)(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: { + action: 'install', + error: message, + exitCode: 1 + }, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: `Desktop shim install failed: ${message}` + }) + }); + } + const text = `Installed ChatGPT Desktop shim:\n` + ` wrapper: ${out.wrapperPath}\n` + ` bridge: ${out.bridgePath}\n` + ` login env: ${out.envScriptPath}` + (out.plistPath ? `\n LaunchAgent: ${out.plistPath}` : '\n LaunchAgent: n/a (macOS-only)') + `\nFully quit and relaunch ChatGPT.app so it inherits CODEX_CLI_PATH; ` + `Desktop falls back to stock Codex if the shim is ever removed.` + formatWarnings(out.warnings); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text + }) + }); + } + case 'uninstall': + { + let out; + try { + out = (0, _core_desktop_shim_js__rspack_import_1.gM)(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: { + action: 'uninstall', + error: message, + exitCode: 1 + }, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: `Desktop shim uninstall failed: ${message}` + }) + }); + } + const removed = out.removed.length === 0 ? '(nothing installed)' : out.removed.join(', '); + const text = `Removed ChatGPT Desktop shim: ${removed}. ` + `Desktop and Codex fall back to stock behavior; relaunch ChatGPT.app to pick it up.` + formatWarnings(out.warnings); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text + }) + }); + } + case 'status': + { + const out = (0, _core_desktop_shim_js__rspack_import_1.em)(); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_format_js__rspack_import_4.fX)(out) + }) + }); + } + default: + { + const _exhaustive = input.action; + throw new Error(`unknown desktop-shim action: ${String(_exhaustive)}`); + } + } } __webpack_require__.d(__webpack_exports__, { - "default": ()=>codexStatusCmd + "default": ()=>codexDesktopShim }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/codex/wait.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/list-threads.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_2 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var _core_codex_routes_js__rspack_import_1 = __webpack_require__("./src/core/codex/routes.ts"); + var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_codex_bridge_js__rspack_import_1 = __webpack_require__("./src/core/codex-bridge.js"); + var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); + var _core_format_js__rspack_import_6 = __webpack_require__("./src/core/format.js"); + var _shared_js__rspack_import_3 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Bounded Codex wait observation; never interrupts execution.', + description: 'List Codex daemon-managed threads (all modelProviders; pages via --cursor / nextCursor).', exitCode: 'result', - render: { - maxElapsedMs: 7260000 - }, - positionals: [ - 'threadId', - 'turnId' - ], inputJsonSchema: { - type: 'object', additionalProperties: false, properties: { - "threadId": { - "type": "string" - }, - "expectedCwd": { - "type": "string" - }, - "timeoutMs": { - "type": "number", - "description": "Observation timeout: 1-7200000 milliseconds." - }, - "turnId": { - "type": "string" - }, - "messageId": { - "type": "string" + cursor: { + description: 'Opaque pagination cursor from a previous nextCursor (may start with -)', + type: 'string' }, - "maxOutputBytes": { - "type": "number", - "description": "Reply budget: 1-4194304 bytes." + limit: { + default: 20, + description: 'Max threads to list (1-200)', + type: 'number' } }, - required: [ - 'threadId', - 'turnId' - ] + type: 'object' } }; - async function route({ input, signal }) { - const context = await (0, _agent_bundle_runtime__rspack_import_2.MA)(); - const out = await (0, _core_codex_routes_js__rspack_import_1.gT)('wait', input, signal, (message)=>context.progress.report({ - message - })); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: (0, _core_codex_routes_js__rspack_import_1.D6)(out) + const inputSchema = zod__rspack_import_4.Ikc({ + cursor: zod__rspack_import_4.YjP().min(1).optional(), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(20) + }).strict(); + const resultSchema = zod__rspack_import_4.KCZ([ + zod__rspack_import_4.Ikc({ + exitCode: zod__rspack_import_4.euz(0), + limit: zod__rspack_import_4.aig().int().min(1).max(200), + nextCursor: zod__rspack_import_4.YjP().nullable(), + threads: zod__rspack_import_4.YOg(zod__rspack_import_4.g1P(zod__rspack_import_4.YjP(), zod__rspack_import_4.Pq9())), + useStateDbOnly: zod__rspack_import_4.zMY().optional() + }).strict(), + _shared_js__rspack_import_3.R9 + ]); + async function codexListThreads({ input }) { + let out; + try { + out = await (0, _core_codex_bridge_js__rspack_import_1.X$)({ + cursor: input.cursor, + limit: input.limit + }); + } catch (error) { + const failure = (0, _core_codex_contract_js__rspack_import_2.DG)(error); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + value: failure, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { + children: failure.error + }) + }); + } + const more = out.nextCursor ? `\n\nmore: --cursor ${JSON.stringify((0, _core_codex_bridge_js__rspack_import_1.pF)(out.nextCursor))}` : ''; + const text = out.threads.length === 0 ? 'No Codex threads.' : out.threads.map(_core_format_js__rspack_import_6.nz).join('\n\n') + more; + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + value: { + ...out, + exitCode: 0 + }, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { + children: text }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>route, - inputSchema: ()=>_core_codex_routes_js__rspack_import_1.jS, - resultSchema: ()=>_core_codex_routes_js__rspack_import_1.FD + "default": ()=>codexListThreads }, { - config: config + config: config, + inputSchema: inputSchema, + resultSchema: resultSchema }); }, - "./src/cli/codex/watch.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/new.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_2 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var _core_codex_routes_js__rspack_import_1 = __webpack_require__("./src/core/codex/routes.ts"); + var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_codex_bridge_js__rspack_import_1 = __webpack_require__("./src/core/codex-bridge.js"); + var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); + var _core_codex_routes_js__rspack_import_3 = __webpack_require__("./src/core/codex/routes.ts"); + const inputSchema = zod__rspack_import_4.Ikc({ + cwd: zod__rspack_import_4.YjP().min(1), + expectedCwd: zod__rspack_import_4.YjP().min(1).optional(), + model: zod__rspack_import_4.YjP().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/).optional(), + effort: zod__rspack_import_4.k5n([ + 'none', + 'minimal', + 'low', + 'medium', + 'high', + 'xhigh' + ]).optional(), + message: zod__rspack_import_4.YOg(zod__rspack_import_4.YjP()).optional() + }).strict(); const config = { - description: 'Bounded Codex watch observation; never interrupts execution.', + description: 'Start a new Codex thread in --cwd, optionally with --model, --effort and a message.', exitCode: 'result', - render: { - maxElapsedMs: 660000 - }, positionals: [ - 'threadId' + 'message' ], inputJsonSchema: { type: 'object', additionalProperties: false, properties: { - "threadId": { - "type": "string" + cwd: { + type: 'string' }, - "expectedCwd": { - "type": "string" + expectedCwd: { + type: 'string' }, - "timeoutMs": { - "type": "number", - "description": "Observation timeout: 1-600000 milliseconds." + model: { + type: 'string' }, - "maxEvents": { - "type": "number", - "description": "Maximum observed events: 1-500." - } - }, - required: [ - 'threadId' - ] - } - }; - async function route({ input, signal }) { - const context = await (0, _agent_bundle_runtime__rspack_import_2.MA)(); - const out = await (0, _core_codex_routes_js__rspack_import_1.gT)('watch', input, signal, (message)=>context.progress.report({ - message - })); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: (0, _core_codex_routes_js__rspack_import_1.D6)(out) - }) - }); - } - __webpack_require__.d(__webpack_exports__, { - "default": ()=>route, - inputSchema: ()=>_core_codex_routes_js__rspack_import_1.R9, - resultSchema: ()=>_core_codex_routes_js__rspack_import_1.FD - }, { - config: config - }); - }, - "./src/cli/doctor.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { - __webpack_require__.r(__webpack_exports__); - var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_6 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_5 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_app_session_js__rspack_import_1 = __webpack_require__("./src/core/app-session.js"); - var _core_gateway_js__rspack_import_2 = __webpack_require__("./src/core/gateway.js"); - var _core_store_js__rspack_import_3 = __webpack_require__("./src/core/store.js"); - var _shared_js__rspack_import_4 = __webpack_require__("./src/cli/_shared.ts"); - const config = { - description: 'Show which agents root and auth sources gbot can see.', - inputJsonSchema: { - additionalProperties: false, - properties: { - dir: { - description: 'Agents directory for --files mode', - type: 'string' - }, - files: { - description: 'Force the on-disk agents store', - type: 'boolean' + effort: { + type: 'string', + enum: [ + 'none', + 'minimal', + 'low', + 'medium', + 'high', + 'xhigh' + ] }, - gateway: { - description: 'Force the live gateway', - type: 'boolean' + message: { + type: 'array', + items: { + type: 'string' + } } }, - type: 'object' + required: [ + 'cwd' + ] } }; - const inputSchema = _shared_js__rspack_import_4.lK; - const resultSchema = zod__rspack_import_5.Ikc({ - candidates: zod__rspack_import_5.YOg(zod__rspack_import_5.YjP()), - found: zod__rspack_import_5.YOg(zod__rspack_import_5.YjP()), - gatewayAuthPresent: zod__rspack_import_5.zMY(), - grokBotAppSession: zod__rspack_import_5.g1P(zod__rspack_import_5.YjP(), zod__rspack_import_5.Pq9()), - note: zod__rspack_import_5.YjP(), - resolved: zod__rspack_import_5.YjP().nullable() - }).strict(); - async function doctor({ input }) { - const candidates = (0, _core_store_js__rspack_import_3.ye)().filter((c)=>typeof c === 'string'); - const found = candidates.filter(_core_store_js__rspack_import_3.A8); - let resolved = null; + async function route({ input, signal }) { + let out; try { - resolved = (0, _core_store_js__rspack_import_3.v1)(input.dir); - } catch (err) { - if (!(err instanceof _core_store_js__rspack_import_3.AD)) throw err; + out = await (0, _core_codex_bridge_js__rspack_import_1.QC)({ + ...input, + message: input.message?.join(' ').trim() || undefined, + signal + }); + } catch (error) { + out = (0, _core_codex_contract_js__rspack_import_2.DG)(error); } - const note = 'Live roster is on the box. Prefer CURSOR_ACCESS_TOKEN then EnsureSandBox then POST gateway /api/*.'; - const gatewayAuthPresent = (0, _core_gateway_js__rspack_import_2.nF)(); - const grokBotAppSession = (0, _core_app_session_js__rspack_import_1.hd)(); - const sessionJson = { - present: grokBotAppSession.present, - usable: grokBotAppSession.usable, - ...grokBotAppSession.error === undefined ? {} : { - error: grokBotAppSession.error - }, - ...'code' in grokBotAppSession && grokBotAppSession.code != null ? { - code: grokBotAppSession.code - } : {} - }; - const value = { - resolved, - found, - candidates, - gatewayAuthPresent, - grokBotAppSession: sessionJson, - note - }; - const sessionLine = grokBotAppSession.usable ? 'Grok Bot app session: usable' : grokBotAppSession.present ? `Grok Bot app session: present but unusable: ${grokBotAppSession.error}` : 'Grok Bot app session: not found'; - const text = [ - `resolved: ${resolved ?? '(none)'}`, - `gateway auth: ${gatewayAuthPresent ? 'present' : 'no'}`, - sessionLine, - 'found:', - found.length ? found.map((p)=>` ${p}`).join('\n') : ' (none)', - 'candidates:', - ...candidates.map((c)=>` ${c}`), - note - ].join('\n'); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_6.g.Result, { - value: value, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_6.g.Text, { - children: text + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { + children: out.threadId && !('delivery' in out) ? `Started Codex thread ${out.threadId}` : (0, _core_codex_routes_js__rspack_import_3.D6)(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>doctor + "default": ()=>route, + resultSchema: ()=>_core_codex_routes_js__rspack_import_3.FD }, { config: config, - inputSchema: inputSchema, - resultSchema: resultSchema + inputSchema: inputSchema }); }, - "./src/cli/groups/add.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/queue.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); - var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); + var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_codex_bridge_js__rspack_import_1 = __webpack_require__("./src/core/codex-bridge.js"); + var _core_codex_contract_js__rspack_import_2 = __webpack_require__("./src/core/codex/contract.js"); + var _core_format_js__rspack_import_6 = __webpack_require__("./src/core/format.js"); + var _shared_js__rspack_import_3 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Add a bot to a group.', + description: 'List the experimental Codex thread queue (GROK_BOT_CODEX_EXPERIMENTAL=1).', + exitCode: 'result', inputJsonSchema: { additionalProperties: false, properties: { - dir: { - description: 'Agents directory for --files mode', - type: 'string' - }, - files: { - description: 'Force the on-disk agents store', - type: 'boolean' - }, - gateway: { - description: 'Force the live gateway', - type: 'boolean' - }, - bot: { - type: 'string' - }, - group: { + threadId: { type: 'string' } }, required: [ - 'group', - 'bot' + 'threadId' ], type: 'object' }, positionals: [ - 'group', - 'bot' + 'threadId' ] }; - const inputSchema = _shared_js__rspack_import_1.lK.extend({ - bot: zod__rspack_import_2.YjP().min(1), - group: zod__rspack_import_2.YjP().min(1) + const inputSchema = zod__rspack_import_4.Ikc({ + threadId: zod__rspack_import_4.YjP().min(1) }).strict(); - const resultSchema = _shared_js__rspack_import_1.t_; - async function groupsAdd({ input }) { - const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const rec = await backend.addGroupMember(input.group, input.bot); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: (0, _core_format_js__rspack_import_4.eh)(rec), - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Added to ${rec.name}. Members: ${rec.memberIds.length}` + const resultSchema = zod__rspack_import_4.KCZ([ + zod__rspack_import_4.Ikc({ + exitCode: zod__rspack_import_4.euz(0), + nextCursor: zod__rspack_import_4.YjP().nullable(), + queued: zod__rspack_import_4.YOg(zod__rspack_import_4.Ikc({ + clientUserMessageId: zod__rspack_import_4.YjP().nullable(), + id: zod__rspack_import_4.YjP().nullable(), + text: zod__rspack_import_4.YjP() + }).strict()), + threadId: zod__rspack_import_4.YjP() + }).strict(), + _shared_js__rspack_import_3.R9 + ]); + async function codexQueue({ input }) { + let out; + try { + out = await (0, _core_codex_bridge_js__rspack_import_1.Bt)(input.threadId); + } catch (error) { + const failure = (0, _core_codex_contract_js__rspack_import_2.DG)(error); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + value: failure, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { + children: failure.error + }) + }); + } + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + value: { + ...out, + exitCode: 0 + }, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { + children: (0, _core_format_js__rspack_import_6.e_)(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsAdd + "default": ()=>codexQueue }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/groups/create.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); - var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); - var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); + var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_codex_routes_js__rspack_import_1 = __webpack_require__("./src/core/codex/routes.ts"); + var _core_relay_routes_js__rspack_import_2 = __webpack_require__("./src/core/relay/routes.ts"); + const resultSchema = zod__rspack_import_3.KCZ([ + _core_codex_routes_js__rspack_import_1.FD, + _core_relay_routes_js__rspack_import_2.Ax + ]); + const inputSchema = zod__rspack_import_3.Ikc({ + ..._core_codex_routes_js__rspack_import_1.at, + whenBusy: zod__rspack_import_3.k5n([ + 'reject', + 'queue', + 'steer' + ]).optional(), + replyToGrok: zod__rspack_import_3.YjP().min(1).optional(), + bindingId: zod__rspack_import_3.YjP().min(1).optional(), + requestId: zod__rspack_import_3.YjP().min(1).max(128).optional(), + correlationId: zod__rspack_import_3.YjP().min(1).optional(), + replyTo: zod__rspack_import_3.YjP().min(1).optional(), + message: zod__rspack_import_3.YOg(zod__rspack_import_3.YjP()).min(1) + }).strict(); const config = { - description: 'Create a Grok Bot group.', + description: 'Send to Codex; acceptance is distinct from completion. Options precede threadId.', + exitCode: 'result', + render: { + maxElapsedMs: 7260000 + }, + positionals: [ + 'threadId', + 'message' + ], inputJsonSchema: { + type: 'object', additionalProperties: false, properties: { - dir: { - description: 'Agents directory for --files mode', + replyToGrok: { type: 'string' }, - files: { - description: 'Force the on-disk agents store', - type: 'boolean' + bindingId: { + type: 'string' }, - gateway: { - description: 'Force the live gateway', - type: 'boolean' + requestId: { + type: 'string' }, - avatarColor: { + threadId: { type: 'string' }, - avatarShape: { + expectedCwd: { type: 'string' }, - description: { + model: { type: 'string' }, - member: { - description: 'Member bot id or name (repeatable)', - items: { - type: 'string' - }, - type: 'array' + effort: { + type: 'string', + enum: [ + 'none', + 'minimal', + 'low', + 'medium', + 'high', + 'xhigh' + ] }, - name: { + timeoutMs: { + type: 'number', + description: 'Observation timeout: 1-7200000 milliseconds.' + }, + correlationId: { type: 'string' }, - title: { + envelope: { + type: 'boolean' + }, + hop: { + type: 'number' + }, + replyTo: { type: 'string' + }, + expectedTurnId: { + type: 'string', + description: 'Required active-turn guard for steer; stale guards reject.' + }, + wait: { + type: 'boolean' + }, + maxOutputBytes: { + type: 'number', + description: 'Reply budget: 1-4194304 bytes.' + }, + whenBusy: { + type: 'string', + enum: [ + 'reject', + 'queue', + 'steer' + ] + }, + message: { + type: 'array', + items: { + type: 'string' + } } }, required: [ - 'name', - 'member' - ], - type: 'object' + 'threadId', + 'message' + ] } }; - const inputSchema = _shared_js__rspack_import_1.lK.merge(_shared_js__rspack_import_1.bY).extend({ - member: zod__rspack_import_2.YOg(zod__rspack_import_2.YjP().min(1)).min(1) - }).strict(); - const resultSchema = _shared_js__rspack_import_1.t_; - async function groupsCreate({ input }) { - const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const { dir: _d, files: _f, gateway: _g, member, ...fields } = input; - const rec = await backend.createGroup({ - ...(0, _shared_js__rspack_import_1.cp)(fields), - memberIds: member - }); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: (0, _core_format_js__rspack_import_4.eh)(rec), - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Created group ${rec.name} (${rec.id}) with ${rec.memberIds.length} members` + async function route({ input, signal }) { + const context = await (0, _agent_bundle_runtime__rspack_import_4.MA)(); + if (input.replyToGrok || input.bindingId) { + const out = await (0, _core_relay_routes_js__rspack_import_2.lt)({ + ...input, + message: input.message.join(' ').trim() + }, context); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + value: { + ...out, + exitCode: out.delivery === 'rejected' ? 1 : 0 + }, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { + children: `Delivery ${out.delivery}; terminal answer returns to Grok automatically.` + }) + }); + } + const out = await (0, _core_codex_routes_js__rspack_import_1.UP)({ + ...input, + whenBusy: input.whenBusy ?? 'reject', + message: input.message.join(' ').trim() + }, signal, (message)=>context.progress.report({ + message + }), true); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { + children: (0, _core_codex_routes_js__rspack_import_1.D6)(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsCreate + "default": ()=>route }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/groups/delete.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/status.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_codex_bridge_js__rspack_import_1 = __webpack_require__("./src/core/codex-bridge.js"); var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); - var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Delete a group by id or name.', + description: 'Probe the local Codex app-server daemon. Exit 0 only when it is usable.', + exitCode: 'result', inputJsonSchema: { additionalProperties: false, - properties: { - dir: { - description: 'Agents directory for --files mode', - type: 'string' - }, - files: { - description: 'Force the on-disk agents store', - type: 'boolean' - }, - gateway: { - description: 'Force the live gateway', - type: 'boolean' - }, - ref: { - description: 'Group id or name', - type: 'string' - } - }, - required: [ - 'ref' - ], + properties: {}, type: 'object' - }, - positionals: [ - 'ref' - ] + } }; - const inputSchema = _shared_js__rspack_import_1.lK.extend({ - ref: zod__rspack_import_2.YjP().min(1) - }).strict(); - const resultSchema = _shared_js__rspack_import_1.t_; - async function groupsDelete({ input }) { - const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const rec = await backend.resolve(input.ref); - if (!rec.isGroup) throw new Error(`"${rec.name}" is a bot, not a group. Use bots delete.`); - const deleted = await backend.deleteAgent(input.ref); + const inputSchema = zod__rspack_import_2.Ikc({}).strict(); + const resultSchema = zod__rspack_import_2.Ikc({ + exitCode: zod__rspack_import_2.KCZ([ + zod__rspack_import_2.euz(0), + zod__rspack_import_2.euz(1) + ]) + }).passthrough(); + async function codexStatusCmd(_props) { + const status = await (0, _core_codex_bridge_js__rspack_import_1.MU)(); return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: (0, _core_format_js__rspack_import_4.eh)(deleted), + value: status, children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Deleted group ${deleted.name} (${deleted.id})` + children: (0, _core_format_js__rspack_import_4.P2)(status) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsDelete + "default": ()=>codexStatusCmd }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/groups/get.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/wait.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_2 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); - var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); + var _core_codex_routes_js__rspack_import_1 = __webpack_require__("./src/core/codex/routes.ts"); const config = { - description: 'Show one group by id or name.', + description: 'Bounded Codex wait observation; never interrupts execution.', + exitCode: 'result', + render: { + maxElapsedMs: 7260000 + }, + positionals: [ + 'threadId', + 'turnId' + ], inputJsonSchema: { + type: 'object', additionalProperties: false, properties: { - dir: { - description: 'Agents directory for --files mode', - type: 'string' + "threadId": { + "type": "string" }, - files: { - description: 'Force the on-disk agents store', - type: 'boolean' + "expectedCwd": { + "type": "string" }, - gateway: { - description: 'Force the live gateway', - type: 'boolean' + "timeoutMs": { + "type": "number", + "description": "Observation timeout: 1-7200000 milliseconds." }, - ref: { - description: 'Group id or name', - type: 'string' + "turnId": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "maxOutputBytes": { + "type": "number", + "description": "Reply budget: 1-4194304 bytes." } }, required: [ - 'ref' - ], - type: 'object' - }, - positionals: [ - 'ref' - ] + 'threadId', + 'turnId' + ] + } }; - const inputSchema = _shared_js__rspack_import_1.lK.extend({ - ref: zod__rspack_import_2.YjP().min(1) - }).strict(); - const resultSchema = _shared_js__rspack_import_1.t_; - async function groupsGet({ input }) { - const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const rec = await backend.resolve(input.ref); - if (!rec.isGroup) throw new Error(`"${rec.name}" is a bot, not a group.`); - const all = await backend.list(); + async function route({ input, signal }) { + const context = await (0, _agent_bundle_runtime__rspack_import_2.MA)(); + const out = await (0, _core_codex_routes_js__rspack_import_1.gT)('wait', input, signal, (message)=>context.progress.report({ + message + })); return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: (0, _core_format_js__rspack_import_4.eh)(rec), + value: out, children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: (0, _core_format_js__rspack_import_4.bM)(rec, all) + children: (0, _core_codex_routes_js__rspack_import_1.D6)(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsGet + "default": ()=>route, + inputSchema: ()=>_core_codex_routes_js__rspack_import_1.jS, + resultSchema: ()=>_core_codex_routes_js__rspack_import_1.FD }, { - config: config, - inputSchema: inputSchema, - resultSchema: resultSchema + config: config }); }, - "./src/cli/groups/list.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/codex/watch.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_format_js__rspack_import_3 = __webpack_require__("./src/core/format.js"); - var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); + var _agent_bundle_runtime__rspack_import_2 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var _core_codex_routes_js__rspack_import_1 = __webpack_require__("./src/core/codex/routes.ts"); const config = { - description: 'List Grok Bot groups.', + description: 'Bounded Codex watch observation; never interrupts execution.', + exitCode: 'result', + render: { + maxElapsedMs: 660000 + }, + positionals: [ + 'threadId' + ], inputJsonSchema: { + type: 'object', additionalProperties: false, properties: { - dir: { - description: 'Agents directory for --files mode', - type: 'string' + "threadId": { + "type": "string" }, - files: { - description: 'Force the on-disk agents store', - type: 'boolean' + "expectedCwd": { + "type": "string" }, - gateway: { - description: 'Force the live gateway', - type: 'boolean' + "timeoutMs": { + "type": "number", + "description": "Observation timeout: 1-600000 milliseconds." + }, + "maxEvents": { + "type": "number", + "description": "Maximum observed events: 1-500." } }, - type: 'object' + required: [ + 'threadId' + ] } }; - const inputSchema = _shared_js__rspack_import_1.lK; - const resultSchema = zod__rspack_import_2.YOg(_shared_js__rspack_import_1.t_); - async function groupsList({ input }) { - const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const all = await backend.list(); - const rows = all.filter((r)=>r.isGroup); - const groups = rows.map(_core_format_js__rspack_import_3.eh); - const text = rows.length === 0 ? 'No groups.' : rows.map((r)=>(0, _core_format_js__rspack_import_3.bM)(r, all)).join('\n\n'); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { - value: groups, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { - children: text + async function route({ input, signal }) { + const context = await (0, _agent_bundle_runtime__rspack_import_2.MA)(); + const out = await (0, _core_codex_routes_js__rspack_import_1.gT)('watch', input, signal, (message)=>context.progress.report({ + message + })); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_codex_routes_js__rspack_import_1.D6)(out) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsList + "default": ()=>route, + inputSchema: ()=>_core_codex_routes_js__rspack_import_1.R9, + resultSchema: ()=>_core_codex_routes_js__rspack_import_1.FD }, { - config: config, - inputSchema: inputSchema, - resultSchema: resultSchema + config: config }); }, - "./src/cli/groups/members.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/doctor.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); - var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); + var _agent_bundle_runtime__rspack_import_6 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_5 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_app_session_js__rspack_import_1 = __webpack_require__("./src/core/app-session.js"); + var _core_gateway_js__rspack_import_2 = __webpack_require__("./src/core/gateway.js"); + var _core_store_js__rspack_import_3 = __webpack_require__("./src/core/store.js"); + var _shared_js__rspack_import_4 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Show members of a group (same payload as groups get).', + description: 'Show which agents root and auth sources gbot can see.', inputJsonSchema: { additionalProperties: false, properties: { @@ -10117,46 +10116,77 @@ var __webpack_modules__ = { gateway: { description: 'Force the live gateway', type: 'boolean' - }, - ref: { - description: 'Group id or name', - type: 'string' } }, - required: [ - 'ref' - ], type: 'object' - }, - positionals: [ - 'ref' - ] + } }; - const inputSchema = _shared_js__rspack_import_1.lK.extend({ - ref: zod__rspack_import_2.YjP().min(1) + const inputSchema = _shared_js__rspack_import_4.lK; + const resultSchema = zod__rspack_import_5.Ikc({ + candidates: zod__rspack_import_5.YOg(zod__rspack_import_5.YjP()), + found: zod__rspack_import_5.YOg(zod__rspack_import_5.YjP()), + gatewayAuthPresent: zod__rspack_import_5.zMY(), + grokBotAppSession: zod__rspack_import_5.g1P(zod__rspack_import_5.YjP(), zod__rspack_import_5.Pq9()), + note: zod__rspack_import_5.YjP(), + resolved: zod__rspack_import_5.YjP().nullable() }).strict(); - const resultSchema = _shared_js__rspack_import_1.t_; - async function groupsMembers({ input }) { - const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const rec = await backend.resolve(input.ref); - if (!rec.isGroup) throw new Error(`"${rec.name}" is a bot, not a group.`); - const all = await backend.list(); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: (0, _core_format_js__rspack_import_4.eh)(rec), - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: (0, _core_format_js__rspack_import_4.bM)(rec, all) + async function doctor({ input }) { + const candidates = (0, _core_store_js__rspack_import_3.ye)().filter((c)=>typeof c === 'string'); + const found = candidates.filter(_core_store_js__rspack_import_3.A8); + let resolved = null; + try { + resolved = (0, _core_store_js__rspack_import_3.v1)(input.dir); + } catch (err) { + if (!(err instanceof _core_store_js__rspack_import_3.AD)) throw err; + } + const note = 'Live roster is on the box. Prefer CURSOR_ACCESS_TOKEN then EnsureSandBox then POST gateway /api/*.'; + const gatewayAuthPresent = (0, _core_gateway_js__rspack_import_2.nF)(); + const grokBotAppSession = (0, _core_app_session_js__rspack_import_1.hd)(); + const sessionJson = { + present: grokBotAppSession.present, + usable: grokBotAppSession.usable, + ...grokBotAppSession.error === undefined ? {} : { + error: grokBotAppSession.error + }, + ...'code' in grokBotAppSession && grokBotAppSession.code != null ? { + code: grokBotAppSession.code + } : {} + }; + const value = { + resolved, + found, + candidates, + gatewayAuthPresent, + grokBotAppSession: sessionJson, + note + }; + const sessionLine = grokBotAppSession.usable ? 'Grok Bot app session: usable' : grokBotAppSession.present ? `Grok Bot app session: present but unusable: ${grokBotAppSession.error}` : 'Grok Bot app session: not found'; + const text = [ + `resolved: ${resolved ?? '(none)'}`, + `gateway auth: ${gatewayAuthPresent ? 'present' : 'no'}`, + sessionLine, + 'found:', + found.length ? found.map((p)=>` ${p}`).join('\n') : ' (none)', + 'candidates:', + ...candidates.map((c)=>` ${c}`), + note + ].join('\n'); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_6.g.Result, { + value: value, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_6.g.Text, { + children: text }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsMembers + "default": ()=>doctor }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/groups/remove.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/groups/add.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); @@ -10164,7 +10194,7 @@ var __webpack_modules__ = { var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Remove a bot from a group.', + description: 'Add a bot to a group.', inputJsonSchema: { additionalProperties: false, properties: { @@ -10203,33 +10233,33 @@ var __webpack_modules__ = { group: zod__rspack_import_2.YjP().min(1) }).strict(); const resultSchema = _shared_js__rspack_import_1.t_; - async function groupsRemove({ input }) { + async function groupsAdd({ input }) { const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const rec = await backend.removeGroupMember(input.group, input.bot); + const rec = await backend.addGroupMember(input.group, input.bot); return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { value: (0, _core_format_js__rspack_import_4.eh)(rec), children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Removed from ${rec.name}. Members: ${rec.memberIds.length}` + children: `Added to ${rec.name}. Members: ${rec.memberIds.length}` }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsRemove + "default": ()=>groupsAdd }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/groups/set.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/groups/create.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); const config = { - description: "Replace a group's member list.", + description: 'Create a Grok Bot group.', inputJsonSchema: { additionalProperties: false, properties: { @@ -10245,7 +10275,13 @@ var __webpack_modules__ = { description: 'Force the live gateway', type: 'boolean' }, - group: { + avatarColor: { + type: 'string' + }, + avatarShape: { + type: 'string' + }, + description: { type: 'string' }, member: { @@ -10254,42 +10290,48 @@ var __webpack_modules__ = { type: 'string' }, type: 'array' + }, + name: { + type: 'string' + }, + title: { + type: 'string' } }, required: [ - 'group', + 'name', 'member' ], type: 'object' - }, - positionals: [ - 'group' - ] + } }; - const inputSchema = _shared_js__rspack_import_1.lK.extend({ - group: zod__rspack_import_2.YjP().min(1), + const inputSchema = _shared_js__rspack_import_1.lK.merge(_shared_js__rspack_import_1.bY).extend({ member: zod__rspack_import_2.YOg(zod__rspack_import_2.YjP().min(1)).min(1) }).strict(); const resultSchema = _shared_js__rspack_import_1.t_; - async function groupsSet({ input }) { + async function groupsCreate({ input }) { const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const rec = await backend.setGroupMembers(input.group, input.member); + const { dir: _d, files: _f, gateway: _g, member, ...fields } = input; + const rec = await backend.createGroup({ + ...(0, _shared_js__rspack_import_1.cp)(fields), + memberIds: member + }); return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { value: (0, _core_format_js__rspack_import_4.eh)(rec), children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Updated ${rec.name}. Members: ${rec.memberIds.length}` + children: `Created group ${rec.name} (${rec.id}) with ${rec.memberIds.length} members` }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsSet + "default": ()=>groupsCreate }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/groups/update.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/groups/delete.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); @@ -10297,7 +10339,7 @@ var __webpack_modules__ = { var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Update a group profile (members stay on set/add/remove).', + description: 'Delete a group by id or name.', inputJsonSchema: { additionalProperties: false, properties: { @@ -10313,30 +10355,9 @@ var __webpack_modules__ = { description: 'Force the live gateway', type: 'boolean' }, - avatarColor: { - type: 'string' - }, - avatarShape: { - type: 'string' - }, - description: { - type: 'string' - }, - hidden: { - type: 'string' - }, - name: { - type: 'string' - }, - notify: { - type: 'string' - }, ref: { description: 'Group id or name', type: 'string' - }, - title: { - type: 'string' } }, required: [ @@ -10348,165 +10369,108 @@ var __webpack_modules__ = { 'ref' ] }; - const inputSchema = _shared_js__rspack_import_1.lK.merge(_shared_js__rspack_import_1.qP).extend({ + const inputSchema = _shared_js__rspack_import_1.lK.extend({ ref: zod__rspack_import_2.YjP().min(1) }).strict(); const resultSchema = _shared_js__rspack_import_1.t_; - async function groupsUpdate({ input }) { + async function groupsDelete({ input }) { const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const current = await backend.resolve(input.ref); - if (!current.isGroup) throw new Error(`"${current.name}" is a bot, not a group. Use bots update.`); - const { dir: _d, files: _f, gateway: _g, ref, ...fields } = input; - const rec = await backend.updateAgent(ref, (0, _shared_js__rspack_import_1.li)(fields)); + const rec = await backend.resolve(input.ref); + if (!rec.isGroup) throw new Error(`"${rec.name}" is a bot, not a group. Use bots delete.`); + const deleted = await backend.deleteAgent(input.ref); return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: (0, _core_format_js__rspack_import_4.eh)(rec), + value: (0, _core_format_js__rspack_import_4.eh)(deleted), children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Updated group ${rec.name} (${rec.id})` + children: `Deleted group ${deleted.name} (${deleted.id})` }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>groupsUpdate + "default": ()=>groupsDelete }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/history.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/groups/get.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_history_js__rspack_import_1 = __webpack_require__("./src/core/history.js"); + var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); + var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Read the opt-in local JSONL history without contacting the gateway.', + description: 'Show one group by id or name.', inputJsonSchema: { additionalProperties: false, properties: { - historyDir: { - description: 'Directory containing history.jsonl', + dir: { + description: 'Agents directory for --files mode', type: 'string' }, - limit: { - default: 40, - description: 'Maximum matching rows (1 or more)', - type: 'number' + files: { + description: 'Force the on-disk agents store', + type: 'boolean' }, - path: { - description: 'Print the history file path', + gateway: { + description: 'Force the live gateway', type: 'boolean' }, ref: { - description: 'Bot or group id or name', - type: 'string' - }, - search: { - description: 'Case-insensitive message text filter', + description: 'Group id or name', type: 'string' } }, + required: [ + 'ref' + ], type: 'object' }, positionals: [ 'ref' ] }; - const inputSchema = zod__rspack_import_2.Ikc({ - historyDir: zod__rspack_import_2.YjP().min(1).optional(), - limit: zod__rspack_import_2.aig().int().min(1).default(40), - path: zod__rspack_import_2.zMY().default(false), - ref: zod__rspack_import_2.YjP().min(1).optional(), - search: zod__rspack_import_2.YjP().optional() - }).strict().refine((input)=>!input.path || input.ref === undefined && input.search === undefined && input.limit === 40, '--path cannot be combined with a target, --search, or --limit'); - const historyRowSchema = zod__rspack_import_2.g1P(zod__rspack_import_2.YjP(), zod__rspack_import_2.Pq9()); - const resultSchema = zod__rspack_import_2.KCZ([ - zod__rspack_import_2.YOg(historyRowSchema), - zod__rspack_import_2.Ikc({ - path: zod__rspack_import_2.YjP() - }).strict() - ]); - async function history({ input }) { - const file = (0, _core_history_js__rspack_import_1.ae)(input.historyDir); - if (input.path) { - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: { - path: file - }, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: file - }) - }); - } - const rows = await (0, _core_history_js__rspack_import_1.Lu)(file, { - limit: input.limit, - ref: input.ref, - search: input.search - }); - const text = rows.length ? rows.map((row)=>`[${row.recordedAt}] ${row.target.name} (${row.target.id}) [${row.role}] ${row.text}`).join('\n') : 'No local history.'; + const inputSchema = _shared_js__rspack_import_1.lK.extend({ + ref: zod__rspack_import_2.YjP().min(1) + }).strict(); + const resultSchema = _shared_js__rspack_import_1.t_; + async function groupsGet({ input }) { + const backend = await (0, _shared_js__rspack_import_1.FS)(input); + const rec = await backend.resolve(input.ref); + if (!rec.isGroup) throw new Error(`"${rec.name}" is a bot, not a group.`); + const all = await backend.list(); return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: rows, + value: (0, _core_format_js__rspack_import_4.eh)(rec), children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: text + children: (0, _core_format_js__rspack_import_4.bM)(rec, all) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>history + "default": ()=>groupsGet }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/groups/list.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_7 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_8 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_6 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); - var _core_codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); - var _core_codex_contract_js__rspack_import_3 = __webpack_require__("./src/core/codex/contract.js"); - var _core_history_js__rspack_import_4 = __webpack_require__("./src/core/history.js"); - var _shared_js__rspack_import_5 = __webpack_require__("./src/cli/_shared.ts"); + var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_format_js__rspack_import_3 = __webpack_require__("./src/core/format.js"); + var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Send a message to a Grok Bot bot or group by name or id.', - exitCode: 'result', + description: 'List Grok Bot groups.', inputJsonSchema: { additionalProperties: false, properties: { - replyMode: { - type: 'string', - enum: [ - 'auto', - 'manual' - ] - }, - codexThreadId: { - type: 'string' - }, - bindingId: { - type: 'string' - }, - expectedCwd: { - type: 'string' - }, - requestId: { - type: 'string' - }, - correlationId: { - description: 'Stable correlation id for multi-hop replies', - type: 'string' - }, dir: { description: 'Agents directory for --files mode', type: 'string' }, - envelope: { - description: 'Prepend the [gbot …] header', - type: 'boolean' - }, files: { description: 'Force the on-disk agents store', type: 'boolean' @@ -10514,176 +10478,105 @@ var __webpack_modules__ = { gateway: { description: 'Force the live gateway', type: 'boolean' - }, - historyDir: { - description: 'Directory containing history.jsonl', + } + }, + type: 'object' + } + }; + const inputSchema = _shared_js__rspack_import_1.lK; + const resultSchema = zod__rspack_import_2.YOg(_shared_js__rspack_import_1.t_); + async function groupsList({ input }) { + const backend = await (0, _shared_js__rspack_import_1.FS)(input); + const all = await backend.list(); + const rows = all.filter((r)=>r.isGroup); + const groups = rows.map(_core_format_js__rspack_import_3.eh); + const text = rows.length === 0 ? 'No groups.' : rows.map((r)=>(0, _core_format_js__rspack_import_3.bM)(r, all)).join('\n\n'); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { + value: groups, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { + children: text + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>groupsList + }, { + config: config, + inputSchema: inputSchema, + resultSchema: resultSchema + }); + }, + "./src/cli/groups/members.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); + var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); + const config = { + description: 'Show members of a group (same payload as groups get).', + inputJsonSchema: { + additionalProperties: false, + properties: { + dir: { + description: 'Agents directory for --files mode', type: 'string' }, - hop: { - description: 'Hop count; refused at GROK_BOT_MAX_HOPS', - type: 'number' - }, - message: { - items: { - type: 'string' - }, - type: 'array' - }, - noHistory: { - description: 'Skip local history for this command', + files: { + description: 'Force the on-disk agents store', type: 'boolean' }, - replyTo: { - description: 'Prior message id this send replies to', - type: 'string' + gateway: { + description: 'Force the live gateway', + type: 'boolean' }, - target: { + ref: { + description: 'Group id or name', type: 'string' } }, required: [ - 'target', - 'message' + 'ref' ], type: 'object' }, positionals: [ - 'target', - 'message' + 'ref' ] }; - const inputSchema = _shared_js__rspack_import_5.lK.extend({ - ..._core_relay_routes_js__rspack_import_1.zK, - replyMode: zod__rspack_import_6.k5n([ - 'auto', - 'manual' - ]).optional(), - correlationId: zod__rspack_import_6.YjP().min(1).optional(), - envelope: zod__rspack_import_6.zMY().default(false), - historyDir: zod__rspack_import_6.YjP().min(1).optional(), - hop: zod__rspack_import_6.aig().int().min(0).optional(), - message: zod__rspack_import_6.YOg(zod__rspack_import_6.YjP()).min(1), - noHistory: zod__rspack_import_6.zMY().default(false), - replyTo: zod__rspack_import_6.YjP().min(1).optional(), - target: zod__rspack_import_6.YjP().min(1) - }).strict(); - const receiptSchema = zod__rspack_import_6.Ikc({ - correlationId: zod__rspack_import_6.YjP(), - delivery: zod__rspack_import_6.k5n([ - 'accepted', - 'unknown' - ]), - envelopeId: zod__rspack_import_6.YjP(), - exitCode: zod__rspack_import_6.euz(0), - hop: zod__rspack_import_6.aig().int().min(0), - id: zod__rspack_import_6.YjP(), - kind: zod__rspack_import_6.k5n([ - 'bot', - 'group' - ]), - maxHops: zod__rspack_import_6.aig().int().min(0), - messageId: zod__rspack_import_6.YjP().optional(), - name: zod__rspack_import_6.YjP(), - replyTo: zod__rspack_import_6.YjP().optional(), - result: zod__rspack_import_6.g1P(zod__rspack_import_6.YjP(), zod__rspack_import_6.Pq9()) + const inputSchema = _shared_js__rspack_import_1.lK.extend({ + ref: zod__rspack_import_2.YjP().min(1) }).strict(); - const resultSchema = zod__rspack_import_6.KCZ([ - receiptSchema, - _shared_js__rspack_import_5.R9, - _core_relay_routes_js__rspack_import_1.Ax - ]); - const deliver = async (input)=>{ - const envelope = (0, _core_codex_bridge_js__rspack_import_2.nq)({ - correlationId: input.correlationId, - envelope: input.envelope, - hop: input.hop, - replyTo: input.replyTo - }); - const message = input.message.join(' ').trim(); - const backend = await (0, _shared_js__rspack_import_5.FS)(input); - const out = await backend.send(input.target, (0, _core_codex_bridge_js__rspack_import_2.Mr)(message, envelope)); - (0, _core_history_js__rspack_import_4.QX)(out, { - dir: input.historyDir, - disabled: input.noHistory, - event: 'send', - prompt: message - }); - return { - id: out.target.id, - name: out.target.name, - kind: out.target.isGroup ? 'group' : 'bot', - result: out.result, - delivery: out.delivery === 'accepted' ? 'accepted' : 'unknown', - ...typeof out.messageId === 'string' ? { - messageId: out.messageId - } : {}, - envelopeId: envelope.messageId, - correlationId: envelope.correlationId, - ...input.replyTo ? { - replyTo: input.replyTo - } : {}, - hop: envelope.hop, - maxHops: envelope.maxHops, - exitCode: 0 - }; - }; - async function send({ input }) { - let value; - try { - if (input.replyMode === 'auto' || input.codexThreadId || input.bindingId) { - (0, _core_relay_routes_js__rspack_import_1.MB)(input); - if (input.files) throw Error('Automatic routes require the gateway backend'); - const out = await (0, _core_relay_routes_js__rspack_import_1.bv)({ - target: input.target, - message: input.message.join(' ').trim(), - replyMode: input.replyMode ?? 'auto', - codexThreadId: input.codexThreadId, - bindingId: input.bindingId, - expectedCwd: input.expectedCwd, - requestId: input.requestId, - hop: input.hop, - correlationId: input.correlationId - }, await (0, _agent_bundle_runtime__rspack_import_7.MA)()); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_8.g.Result, { - value: { - ...out, - exitCode: out.delivery === 'rejected' ? 1 : 0 - }, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_8.g.Text, { - children: `Delivery ${out.delivery}; inspect codex bridge status for automatic reply delivery.` - }) - }); - } - value = await deliver(input); - } catch (error) { - value = (0, _core_codex_contract_js__rspack_import_3.DG)(error); - } - const text = value.exitCode === 0 ? `Sent to ${value.kind} ${value.name} (${value.id})${value.messageId ? ` message ${value.messageId}` : ''}; envelope ${value.envelopeId}` : value.error; - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_8.g.Result, { - value: value, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_8.g.Text, { - children: text + const resultSchema = _shared_js__rspack_import_1.t_; + async function groupsMembers({ input }) { + const backend = await (0, _shared_js__rspack_import_1.FS)(input); + const rec = await backend.resolve(input.ref); + if (!rec.isGroup) throw new Error(`"${rec.name}" is a bot, not a group.`); + const all = await backend.list(); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: (0, _core_format_js__rspack_import_4.eh)(rec), + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_format_js__rspack_import_4.bM)(rec, all) }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>send + "default": ()=>groupsMembers }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/skills/add.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/groups/remove.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var node_fs_promises__rspack_import_1 = __webpack_require__("node:fs/promises"); - var node_path__rspack_import_2 = __webpack_require__("node:path"); - var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _shared_js__rspack_import_3 = __webpack_require__("./src/cli/_shared.ts"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); + var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Add a SKILL.md to the shared skill library. Every bot sees it.', + description: 'Remove a bot from a group.', inputJsonSchema: { additionalProperties: false, properties: { @@ -10699,55 +10592,56 @@ var __webpack_modules__ = { description: 'Force the live gateway', type: 'boolean' }, - path: { - description: 'SKILL.md file, or a directory holding one', + bot: { + type: 'string' + }, + group: { type: 'string' } }, required: [ - 'path' + 'group', + 'bot' ], type: 'object' }, positionals: [ - 'path' + 'group', + 'bot' ] }; - const inputSchema = _shared_js__rspack_import_3.lK.extend({ - path: zod__rspack_import_4.YjP().min(1) + const inputSchema = _shared_js__rspack_import_1.lK.extend({ + bot: zod__rspack_import_2.YjP().min(1), + group: zod__rspack_import_2.YjP().min(1) }).strict(); - const resultSchema = _shared_js__rspack_import_3.b7; - const skillFile = async (path)=>{ - const target = (0, node_path__rspack_import_2.resolve)(path); - return (await (0, node_fs_promises__rspack_import_1.stat)(target)).isDirectory() ? (0, node_path__rspack_import_2.join)(target, 'SKILL.md') : target; - }; - async function skillsAdd({ input }) { - const markdown = await (0, node_fs_promises__rspack_import_1.readFile)(await skillFile(input.path), 'utf8'); - const backend = await (0, _shared_js__rspack_import_3.FS)(input); - const skill = resultSchema.parse(await backend.addSkill(markdown)); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { - value: skill, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { - children: `Added skill ${skill.name} (${skill.id}) to the shared library` + const resultSchema = _shared_js__rspack_import_1.t_; + async function groupsRemove({ input }) { + const backend = await (0, _shared_js__rspack_import_1.FS)(input); + const rec = await backend.removeGroupMember(input.group, input.bot); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: (0, _core_format_js__rspack_import_4.eh)(rec), + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: `Removed from ${rec.name}. Members: ${rec.memberIds.length}` }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>skillsAdd + "default": ()=>groupsRemove }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/skills/list.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/groups/set.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'List the skill library every bot in this Grok Bot shares.', + description: "Replace a group's member list.", inputJsonSchema: { additionalProperties: false, properties: { @@ -10762,40 +10656,60 @@ var __webpack_modules__ = { gateway: { description: 'Force the live gateway', type: 'boolean' + }, + group: { + type: 'string' + }, + member: { + description: 'Member bot id or name (repeatable)', + items: { + type: 'string' + }, + type: 'array' } }, + required: [ + 'group', + 'member' + ], type: 'object' - } + }, + positionals: [ + 'group' + ] }; - const inputSchema = _shared_js__rspack_import_1.lK; - const resultSchema = zod__rspack_import_2.YOg(_shared_js__rspack_import_1.b7); - async function skillsList({ input }) { + const inputSchema = _shared_js__rspack_import_1.lK.extend({ + group: zod__rspack_import_2.YjP().min(1), + member: zod__rspack_import_2.YOg(zod__rspack_import_2.YjP().min(1)).min(1) + }).strict(); + const resultSchema = _shared_js__rspack_import_1.t_; + async function groupsSet({ input }) { const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const skills = resultSchema.parse(await backend.skills()); - const lines = skills.map((s)=>`${s.id} ${s.name} [${s.source}]${s.description ? ` ${s.description}` : ''}`); + const rec = await backend.setGroupMembers(input.group, input.member); return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: skills, + value: (0, _core_format_js__rspack_import_4.eh)(rec), children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: lines.length > 0 ? lines.join('\n') : 'No skills.' + children: `Updated ${rec.name}. Members: ${rec.memberIds.length}` }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>skillsList + "default": ()=>groupsSet }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/skills/remove.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/groups/update.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_format_js__rspack_import_4 = __webpack_require__("./src/core/format.js"); var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Remove a library skill by id or name. Every bot loses it.', + description: 'Update a group profile (members stay on set/add/remove).', inputJsonSchema: { additionalProperties: false, properties: { @@ -10811,71 +10725,203 @@ var __webpack_modules__ = { description: 'Force the live gateway', type: 'boolean' }, - skill: { - description: 'Skill id or name', + avatarColor: { + type: 'string' + }, + avatarShape: { + type: 'string' + }, + description: { + type: 'string' + }, + hidden: { + type: 'string' + }, + name: { + type: 'string' + }, + notify: { + type: 'string' + }, + ref: { + description: 'Group id or name', + type: 'string' + }, + title: { type: 'string' } }, required: [ - 'skill' + 'ref' ], type: 'object' }, positionals: [ - 'skill' + 'ref' ] }; - const inputSchema = _shared_js__rspack_import_1.lK.extend({ - skill: zod__rspack_import_2.YjP().min(1) + const inputSchema = _shared_js__rspack_import_1.lK.merge(_shared_js__rspack_import_1.qP).extend({ + ref: zod__rspack_import_2.YjP().min(1) }).strict(); - const resultSchema = _shared_js__rspack_import_1.b7; - async function skillsRemove({ input }) { + const resultSchema = _shared_js__rspack_import_1.t_; + async function groupsUpdate({ input }) { const backend = await (0, _shared_js__rspack_import_1.FS)(input); - const skill = resultSchema.parse(await backend.removeSkill(input.skill)); + const current = await backend.resolve(input.ref); + if (!current.isGroup) throw new Error(`"${current.name}" is a bot, not a group. Use bots update.`); + const { dir: _d, files: _f, gateway: _g, ref, ...fields } = input; + const rec = await backend.updateAgent(ref, (0, _shared_js__rspack_import_1.li)(fields)); return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { - value: skill, + value: (0, _core_format_js__rspack_import_4.eh)(rec), children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { - children: `Removed skill ${skill.name} (${skill.id}) from the shared library` + children: `Updated group ${rec.name} (${rec.id})` }) }); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>skillsRemove + "default": ()=>groupsUpdate }, { config: config, inputSchema: inputSchema, resultSchema: resultSchema }); }, - "./src/cli/thread.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + "./src/cli/history.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); - var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); - var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_format_js__rspack_import_5 = __webpack_require__("./src/core/format.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); var _core_history_js__rspack_import_1 = __webpack_require__("./src/core/history.js"); - var _core_transcript_js__rspack_import_6 = __webpack_require__("./src/core/transcript.js"); - var _shared_js__rspack_import_2 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'Read the most recent messages in a Grok Bot bot or group thread.', + description: 'Read the opt-in local JSONL history without contacting the gateway.', inputJsonSchema: { additionalProperties: false, properties: { - after: { - description: 'Return entries after this opaque entry id', + historyDir: { + description: 'Directory containing history.jsonl', type: 'string' }, - dir: { - description: 'Agents directory for --files mode', - type: 'string' + limit: { + default: 40, + description: 'Maximum matching rows (1 or more)', + type: 'number' }, - files: { - description: 'Force the on-disk agents store', + path: { + description: 'Print the history file path', type: 'boolean' }, - full: { - description: 'Show full entry text in human output', - type: 'boolean' + ref: { + description: 'Bot or group id or name', + type: 'string' + }, + search: { + description: 'Case-insensitive message text filter', + type: 'string' + } + }, + type: 'object' + }, + positionals: [ + 'ref' + ] + }; + const inputSchema = zod__rspack_import_2.Ikc({ + historyDir: zod__rspack_import_2.YjP().min(1).optional(), + limit: zod__rspack_import_2.aig().int().min(1).default(40), + path: zod__rspack_import_2.zMY().default(false), + ref: zod__rspack_import_2.YjP().min(1).optional(), + search: zod__rspack_import_2.YjP().optional() + }).strict().refine((input)=>!input.path || input.ref === undefined && input.search === undefined && input.limit === 40, '--path cannot be combined with a target, --search, or --limit'); + const historyRowSchema = zod__rspack_import_2.g1P(zod__rspack_import_2.YjP(), zod__rspack_import_2.Pq9()); + const resultSchema = zod__rspack_import_2.KCZ([ + zod__rspack_import_2.YOg(historyRowSchema), + zod__rspack_import_2.Ikc({ + path: zod__rspack_import_2.YjP() + }).strict() + ]); + async function history({ input }) { + const file = (0, _core_history_js__rspack_import_1.ae)(input.historyDir); + if (input.path) { + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: { + path: file + }, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: file + }) + }); + } + const rows = await (0, _core_history_js__rspack_import_1.Lu)(file, { + limit: input.limit, + ref: input.ref, + search: input.search + }); + const text = rows.length ? rows.map((row)=>`[${row.recordedAt}] ${row.target.name} (${row.target.id}) [${row.role}] ${row.text}`).join('\n') : 'No local history.'; + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: rows, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>history + }, { + config: config, + inputSchema: inputSchema, + resultSchema: resultSchema + }); + }, + "./src/cli/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_7 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); + var _agent_bundle_runtime__rspack_import_8 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_6 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_relay_routes_js__rspack_import_1 = __webpack_require__("./src/core/relay/routes.ts"); + var _core_codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); + var _core_codex_contract_js__rspack_import_3 = __webpack_require__("./src/core/codex/contract.js"); + var _core_history_js__rspack_import_4 = __webpack_require__("./src/core/history.js"); + var _shared_js__rspack_import_5 = __webpack_require__("./src/cli/_shared.ts"); + const config = { + description: 'Send a message to a Grok Bot bot or group by name or id.', + exitCode: 'result', + inputJsonSchema: { + additionalProperties: false, + properties: { + replyMode: { + type: 'string', + enum: [ + 'auto', + 'manual' + ] + }, + codexThreadId: { + type: 'string' + }, + bindingId: { + type: 'string' + }, + expectedCwd: { + type: 'string' + }, + requestId: { + type: 'string' + }, + correlationId: { + description: 'Stable correlation id for multi-hop replies', + type: 'string' + }, + dir: { + description: 'Agents directory for --files mode', + type: 'string' + }, + envelope: { + description: 'Prepend the [gbot …] header', + type: 'boolean' + }, + files: { + description: 'Force the on-disk agents store', + type: 'boolean' }, gateway: { description: 'Force the live gateway', @@ -10885,17 +10931,22 @@ var __webpack_modules__ = { description: 'Directory containing history.jsonl', type: 'string' }, - limit: { - default: 40, - description: 'How many trailing entries to return (1-200)', + hop: { + description: 'Hop count; refused at GROK_BOT_MAX_HOPS', type: 'number' }, + message: { + items: { + type: 'string' + }, + type: 'array' + }, noHistory: { description: 'Skip local history for this command', type: 'boolean' }, - root: { - description: 'Read one rooted thread by message id', + replyTo: { + description: 'Prior message id this send replies to', type: 'string' }, target: { @@ -10903,93 +10954,4266 @@ var __webpack_modules__ = { } }, required: [ - 'target' + 'target', + 'message' ], type: 'object' }, positionals: [ - 'target' + 'target', + 'message' ] }; - const inputSchema = _shared_js__rspack_import_2.lK.extend({ - after: zod__rspack_import_3.YjP().min(1).max(1024).optional(), - full: zod__rspack_import_3.zMY().default(false), - historyDir: zod__rspack_import_3.YjP().min(1).optional(), - limit: zod__rspack_import_3.aig().int().min(1).max(200).default(40), - noHistory: zod__rspack_import_3.zMY().default(false), - root: zod__rspack_import_3.YjP().min(1).optional(), - target: zod__rspack_import_3.YjP().min(1) - }).strict().refine((input)=>input.after === undefined || input.root === undefined, '--after cannot be combined with --root'); - const resultSchema = zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.Pq9()); - async function thread({ input }) { - const backend = await (0, _shared_js__rspack_import_2.FS)(input); - if (input.root !== undefined) { - const rooted = await backend.thread(input.target, input.root); - (0, _core_history_js__rspack_import_1.QX)(rooted, { - dir: input.historyDir, - disabled: input.noHistory, - event: 'thread', - rootId: input.root - }); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { - value: rooted, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { - children: (0, _core_format_js__rspack_import_5.oZ)(rooted, { - full: input.full - }) - }) - }); - } - const out = await backend.transcript(input.target, input.limit); - if (input.after !== undefined) { - const delta = (0, _core_transcript_js__rspack_import_6.N3)(out.transcript, { - after: input.after, - limit: input.limit - }); - const selected = { - ...out, - transcript: { - entries: delta.entries - }, - cursor: delta.cursor, - entryCount: delta.entryCount, - gapReset: delta.gapReset - }; - (0, _core_history_js__rspack_import_1.QX)(selected, { - dir: input.historyDir, - disabled: input.noHistory, - event: 'thread' - }); - const text = (0, _core_format_js__rspack_import_5.oZ)(selected, { - full: input.full + const inputSchema = _shared_js__rspack_import_5.lK.extend({ + ..._core_relay_routes_js__rspack_import_1.zK, + replyMode: zod__rspack_import_6.k5n([ + 'auto', + 'manual' + ]).optional(), + correlationId: zod__rspack_import_6.YjP().min(1).optional(), + envelope: zod__rspack_import_6.zMY().default(false), + historyDir: zod__rspack_import_6.YjP().min(1).optional(), + hop: zod__rspack_import_6.aig().int().min(0).optional(), + message: zod__rspack_import_6.YOg(zod__rspack_import_6.YjP()).min(1), + noHistory: zod__rspack_import_6.zMY().default(false), + replyTo: zod__rspack_import_6.YjP().min(1).optional(), + target: zod__rspack_import_6.YjP().min(1) + }).strict(); + const receiptSchema = zod__rspack_import_6.Ikc({ + correlationId: zod__rspack_import_6.YjP(), + delivery: zod__rspack_import_6.k5n([ + 'accepted', + 'unknown' + ]), + envelopeId: zod__rspack_import_6.YjP(), + exitCode: zod__rspack_import_6.euz(0), + hop: zod__rspack_import_6.aig().int().min(0), + id: zod__rspack_import_6.YjP(), + kind: zod__rspack_import_6.k5n([ + 'bot', + 'group' + ]), + maxHops: zod__rspack_import_6.aig().int().min(0), + messageId: zod__rspack_import_6.YjP().optional(), + name: zod__rspack_import_6.YjP(), + replyTo: zod__rspack_import_6.YjP().optional(), + result: zod__rspack_import_6.g1P(zod__rspack_import_6.YjP(), zod__rspack_import_6.Pq9()) + }).strict(); + const resultSchema = zod__rspack_import_6.KCZ([ + receiptSchema, + _shared_js__rspack_import_5.R9, + _core_relay_routes_js__rspack_import_1.Ax + ]); + const deliver = async (input)=>{ + const envelope = (0, _core_codex_bridge_js__rspack_import_2.nq)({ + correlationId: input.correlationId, + envelope: input.envelope, + hop: input.hop, + replyTo: input.replyTo + }); + const message = input.message.join(' ').trim(); + const backend = await (0, _shared_js__rspack_import_5.FS)(input); + const out = await backend.send(input.target, (0, _core_codex_bridge_js__rspack_import_2.Mr)(message, envelope)); + (0, _core_history_js__rspack_import_4.QX)(out, { + dir: input.historyDir, + disabled: input.noHistory, + event: 'send', + prompt: message + }); + return { + id: out.target.id, + name: out.target.name, + kind: out.target.isGroup ? 'group' : 'bot', + result: out.result, + delivery: out.delivery === 'accepted' ? 'accepted' : 'unknown', + ...typeof out.messageId === 'string' ? { + messageId: out.messageId + } : {}, + envelopeId: envelope.messageId, + correlationId: envelope.correlationId, + ...input.replyTo ? { + replyTo: input.replyTo + } : {}, + hop: envelope.hop, + maxHops: envelope.maxHops, + exitCode: 0 + }; + }; + async function send({ input }) { + let value; + try { + if (input.replyMode === 'auto' || input.codexThreadId || input.bindingId) { + (0, _core_relay_routes_js__rspack_import_1.MB)(input); + if (input.files) throw Error('Automatic routes require the gateway backend'); + const out = await (0, _core_relay_routes_js__rspack_import_1.bv)({ + target: input.target, + message: input.message.join(' ').trim(), + replyMode: input.replyMode ?? 'auto', + codexThreadId: input.codexThreadId, + bindingId: input.bindingId, + expectedCwd: input.expectedCwd, + requestId: input.requestId, + hop: input.hop, + correlationId: input.correlationId + }, await (0, _agent_bundle_runtime__rspack_import_7.MA)()); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_8.g.Result, { + value: { + ...out, + exitCode: out.delivery === 'rejected' ? 1 : 0 + }, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_8.g.Text, { + children: `Delivery ${out.delivery}; inspect codex bridge status for automatic reply delivery.` + }) + }); + } + value = await deliver(input); + } catch (error) { + value = (0, _core_codex_contract_js__rspack_import_3.DG)(error); + } + const text = value.exitCode === 0 ? `Sent to ${value.kind} ${value.name} (${value.id})${value.messageId ? ` message ${value.messageId}` : ''}; envelope ${value.envelopeId}` : value.error; + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_8.g.Result, { + value: value, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_8.g.Text, { + children: text + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>send + }, { + config: config, + inputSchema: inputSchema, + resultSchema: resultSchema + }); + }, + "./src/cli/skills/add.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var node_fs_promises__rspack_import_1 = __webpack_require__("node:fs/promises"); + var node_path__rspack_import_2 = __webpack_require__("node:path"); + var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _shared_js__rspack_import_3 = __webpack_require__("./src/cli/_shared.ts"); + const config = { + description: 'Add a SKILL.md to the shared skill library. Every bot sees it.', + inputJsonSchema: { + additionalProperties: false, + properties: { + dir: { + description: 'Agents directory for --files mode', + type: 'string' + }, + files: { + description: 'Force the on-disk agents store', + type: 'boolean' + }, + gateway: { + description: 'Force the live gateway', + type: 'boolean' + }, + path: { + description: 'SKILL.md file, or a directory holding one', + type: 'string' + } + }, + required: [ + 'path' + ], + type: 'object' + }, + positionals: [ + 'path' + ] + }; + const inputSchema = _shared_js__rspack_import_3.lK.extend({ + path: zod__rspack_import_4.YjP().min(1) + }).strict(); + const resultSchema = _shared_js__rspack_import_3.b7; + const skillFile = async (path)=>{ + const target = (0, node_path__rspack_import_2.resolve)(path); + return (await (0, node_fs_promises__rspack_import_1.stat)(target)).isDirectory() ? (0, node_path__rspack_import_2.join)(target, 'SKILL.md') : target; + }; + async function skillsAdd({ input }) { + const markdown = await (0, node_fs_promises__rspack_import_1.readFile)(await skillFile(input.path), 'utf8'); + const backend = await (0, _shared_js__rspack_import_3.FS)(input); + const skill = resultSchema.parse(await backend.addSkill(markdown)); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Result, { + value: skill, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, { + children: `Added skill ${skill.name} (${skill.id}) to the shared library` + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>skillsAdd + }, { + config: config, + inputSchema: inputSchema, + resultSchema: resultSchema + }); + }, + "./src/cli/skills/list.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); + const config = { + description: 'List the skill library every bot in this Grok Bot shares.', + inputJsonSchema: { + additionalProperties: false, + properties: { + dir: { + description: 'Agents directory for --files mode', + type: 'string' + }, + files: { + description: 'Force the on-disk agents store', + type: 'boolean' + }, + gateway: { + description: 'Force the live gateway', + type: 'boolean' + } + }, + type: 'object' + } + }; + const inputSchema = _shared_js__rspack_import_1.lK; + const resultSchema = zod__rspack_import_2.YOg(_shared_js__rspack_import_1.b7); + async function skillsList({ input }) { + const backend = await (0, _shared_js__rspack_import_1.FS)(input); + const skills = resultSchema.parse(await backend.skills()); + const lines = skills.map((s)=>`${s.id} ${s.name} [${s.source}]${s.description ? ` ${s.description}` : ''}`); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: skills, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: lines.length > 0 ? lines.join('\n') : 'No skills.' + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>skillsList + }, { + config: config, + inputSchema: inputSchema, + resultSchema: resultSchema + }); + }, + "./src/cli/skills/remove.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _shared_js__rspack_import_1 = __webpack_require__("./src/cli/_shared.ts"); + const config = { + description: 'Remove a library skill by id or name. Every bot loses it.', + inputJsonSchema: { + additionalProperties: false, + properties: { + dir: { + description: 'Agents directory for --files mode', + type: 'string' + }, + files: { + description: 'Force the on-disk agents store', + type: 'boolean' + }, + gateway: { + description: 'Force the live gateway', + type: 'boolean' + }, + skill: { + description: 'Skill id or name', + type: 'string' + } + }, + required: [ + 'skill' + ], + type: 'object' + }, + positionals: [ + 'skill' + ] + }; + const inputSchema = _shared_js__rspack_import_1.lK.extend({ + skill: zod__rspack_import_2.YjP().min(1) + }).strict(); + const resultSchema = _shared_js__rspack_import_1.b7; + async function skillsRemove({ input }) { + const backend = await (0, _shared_js__rspack_import_1.FS)(input); + const skill = resultSchema.parse(await backend.removeSkill(input.skill)); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: skill, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: `Removed skill ${skill.name} (${skill.id}) from the shared library` + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>skillsRemove + }, { + config: config, + inputSchema: inputSchema, + resultSchema: resultSchema + }); + }, + "./src/cli/thread.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_format_js__rspack_import_5 = __webpack_require__("./src/core/format.js"); + var _core_history_js__rspack_import_1 = __webpack_require__("./src/core/history.js"); + var _core_transcript_js__rspack_import_6 = __webpack_require__("./src/core/transcript.js"); + var _shared_js__rspack_import_2 = __webpack_require__("./src/cli/_shared.ts"); + const config = { + description: 'Read the most recent messages in a Grok Bot bot or group thread.', + inputJsonSchema: { + additionalProperties: false, + properties: { + after: { + description: 'Return entries after this opaque entry id', + type: 'string' + }, + dir: { + description: 'Agents directory for --files mode', + type: 'string' + }, + files: { + description: 'Force the on-disk agents store', + type: 'boolean' + }, + full: { + description: 'Show full entry text in human output', + type: 'boolean' + }, + gateway: { + description: 'Force the live gateway', + type: 'boolean' + }, + historyDir: { + description: 'Directory containing history.jsonl', + type: 'string' + }, + limit: { + default: 40, + description: 'How many trailing entries to return (1-200)', + type: 'number' + }, + noHistory: { + description: 'Skip local history for this command', + type: 'boolean' + }, + root: { + description: 'Read one rooted thread by message id', + type: 'string' + }, + target: { + type: 'string' + } + }, + required: [ + 'target' + ], + type: 'object' + }, + positionals: [ + 'target' + ] + }; + const inputSchema = _shared_js__rspack_import_2.lK.extend({ + after: zod__rspack_import_3.YjP().min(1).max(1024).optional(), + full: zod__rspack_import_3.zMY().default(false), + historyDir: zod__rspack_import_3.YjP().min(1).optional(), + limit: zod__rspack_import_3.aig().int().min(1).max(200).default(40), + noHistory: zod__rspack_import_3.zMY().default(false), + root: zod__rspack_import_3.YjP().min(1).optional(), + target: zod__rspack_import_3.YjP().min(1) + }).strict().refine((input)=>input.after === undefined || input.root === undefined, '--after cannot be combined with --root'); + const resultSchema = zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.Pq9()); + async function thread({ input }) { + const backend = await (0, _shared_js__rspack_import_2.FS)(input); + if (input.root !== undefined) { + const rooted = await backend.thread(input.target, input.root); + (0, _core_history_js__rspack_import_1.QX)(rooted, { + dir: input.historyDir, + disabled: input.noHistory, + event: 'thread', + rootId: input.root + }); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { + value: rooted, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { + children: (0, _core_format_js__rspack_import_5.oZ)(rooted, { + full: input.full + }) + }) + }); + } + const out = await backend.transcript(input.target, input.limit); + if (input.after !== undefined) { + const delta = (0, _core_transcript_js__rspack_import_6.N3)(out.transcript, { + after: input.after, + limit: input.limit + }); + const selected = { + ...out, + transcript: { + entries: delta.entries + }, + cursor: delta.cursor, + entryCount: delta.entryCount, + gapReset: delta.gapReset + }; + (0, _core_history_js__rspack_import_1.QX)(selected, { + dir: input.historyDir, + disabled: input.noHistory, + event: 'thread' + }); + const text = (0, _core_format_js__rspack_import_5.oZ)(selected, { + full: input.full + }); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { + value: selected, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { + children: `${text}\n\ncursor: ${delta.cursor}${delta.gapReset ? ' (gap reset)' : ''}` + }) + }); + } + (0, _core_history_js__rspack_import_1.QX)(out, { + dir: input.historyDir, + disabled: input.noHistory, + event: 'thread' + }); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { + children: (0, _core_format_js__rspack_import_5.oZ)(out, { + full: input.full + }) + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>thread + }, { + config: config, + inputSchema: inputSchema, + resultSchema: resultSchema + }); + }, + "./src/core/chatgpt-desktop/app-server-fallback.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_bridge_js__rspack_import_0 = __webpack_require__("./src/core/codex-bridge.js"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _remote_threads_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/remote-threads.ts"); + var _payload_budget_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/payload-budget.ts"); + var _thread_ids_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const LIST_PAGE_PREFIX = 'desktop-list-v1:'; + const SEARCH_PAGE_PREFIX = 'desktop-search-v1:'; + const READ_PAGE_PREFIX = 'desktop-read-v1:'; + async function appServerListThreads({ limit = 50, cursor, modelProviders = [] } = {}) { + let sourceCursor = cursor; + let skip = 0; + let fetchLimit = Math.min(limit, 25); + if (cursor?.startsWith(LIST_PAGE_PREFIX)) { + const state = (0, _payload_budget_js__rspack_import_4.i3)(LIST_PAGE_PREFIX, cursor); + if (!Number.isSafeInteger(state.skip) || state.skip < 0 || !Number.isSafeInteger(state.fetchLimit) || state.fetchLimit < 1 || state.fetchLimit > 25 || state.cursor !== null && typeof state.cursor !== 'string') { + throw new Error('Invalid ChatGPT Desktop list page cursor'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + fetchLimit = state.fetchLimit; + } + const base = { + backend: 'app-server', + limit, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined + }; + const threads = []; + const seen = new Set(); + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: fetchLimit, + cursor: sourceCursor, + modelProviders + }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop list page cursor is stale'); + for(let index = skip; index < out.threads.length; index++){ + let row = (0, _payload_budget_js__rspack_import_4.eX)(mapListedThread(out.threads[index])); + const probe = { + ...base, + threads: [ + ...threads, + row + ], + nextCursor: LIST_PAGE_PREFIX, + exitCode: 0 + }; + if ((0, _payload_budget_js__rspack_import_4.sQ)(probe) > 614400) { + if (threads.length) return { + ...base, + threads, + nextCursor: (0, _payload_budget_js__rspack_import_4.oI)(LIST_PAGE_PREFIX, { + cursor: sourceCursor ?? null, + skip: index, + fetchLimit + }), + warnings: [ + 'List page stopped at the MCP byte budget; continue with nextCursor' + ] + }; + row = (0, _payload_budget_js__rspack_import_4.MH)(row); + if ((0, _payload_budget_js__rspack_import_4.sQ)({ + ...base, + threads: [ + row + ], + nextCursor: LIST_PAGE_PREFIX, + exitCode: 0 + }) > 614400) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + threads.push(row); + if (threads.length >= limit) { + const nextCursor = index + 1 < out.threads.length ? (0, _payload_budget_js__rspack_import_4.oI)(LIST_PAGE_PREFIX, { + cursor: sourceCursor ?? null, + skip: index + 1, + fetchLimit + }) : out.nextCursor ?? null; + return { + ...base, + threads, + nextCursor + }; + } + } + if (out.nextCursor == null) return { + ...base, + threads, + nextCursor: null + }; + if (typeof out.nextCursor !== 'string' || !out.nextCursor || seen.has(out.nextCursor)) { + throw new Error('ChatGPT Desktop list returned an invalid or repeated cursor'); + } + seen.add(out.nextCursor); + sourceCursor = out.nextCursor; + skip = 0; + } + return { + ...base, + threads, + nextCursor: sourceCursor, + warnings: [ + 'List stopped after 400 app-server pages; continue with nextCursor' + ] + }; + } + async function appServerSearchThreads({ query, limit = 50, cursor, modelProviders = [], project }) { + const needle = query.trim().toLowerCase(); + if (!needle) { + return appServerListThreads({ + limit, + cursor, + modelProviders + }); + } + const matched = []; + let sourceCursor; + let skip = 0; + if (cursor) { + const state = (0, _payload_budget_js__rspack_import_4.i3)(SEARCH_PAGE_PREFIX, cursor); + if (state.query !== query || state.project !== project || JSON.stringify(state.modelProviders) !== JSON.stringify(modelProviders) || !Number.isSafeInteger(state.skip) || state.skip < 0) { + throw new Error('Search cursor does not match this query and its filters'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + } + const pageCursor = (rpcCursor, offset)=>(0, _payload_budget_js__rspack_import_4.oI)(SEARCH_PAGE_PREFIX, { + cursor: rpcCursor ?? null, + skip: offset, + query, + project, + modelProviders + }); + const base = { + backend: 'app-server', + limit, + query, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined + }; + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: 25, + cursor: sourceCursor, + modelProviders + }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop search cursor is stale'); + for(let index = skip; index < out.threads.length; index++){ + const mapped = mapListedThread(out.threads[index]); + if (!threadMatchesQuery(mapped, needle) || project && ![ + mapped.project, + mapped.projectId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(project.trim().toLowerCase()))) continue; + let row = (0, _payload_budget_js__rspack_import_4.eX)(mapped); + if ((0, _payload_budget_js__rspack_import_4.sQ)({ + ...base, + threads: [ + ...matched, + row + ], + nextCursor: SEARCH_PAGE_PREFIX, + exitCode: 0 + }) > 614400) { + if (matched.length) return { + ...base, + threads: matched, + nextCursor: pageCursor(sourceCursor, index), + warnings: [ + 'Search page stopped at the MCP byte budget; continue with nextCursor' + ] + }; + row = (0, _payload_budget_js__rspack_import_4.MH)(row); + } + matched.push(row); + if (matched.length >= limit) { + const nextCursor = index + 1 < out.threads.length ? pageCursor(sourceCursor, index + 1) : out.nextCursor ? pageCursor(out.nextCursor, 0) : null; + return { + ...base, + threads: matched, + nextCursor + }; + } + } + if (out.nextCursor == null) return { + ...base, + threads: matched, + nextCursor: null + }; + sourceCursor = out.nextCursor; + skip = 0; + } + return { + ...base, + threads: matched, + nextCursor: pageCursor(sourceCursor, 0), + warnings: [ + 'Search stopped after 400 app-server pages; continue with nextCursor' + ] + }; + } + async function discoverModelProviders() { + const providers = new Set(); + let threadCount = 0; + let cursor; + let complete = false; + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: 25, + cursor, + modelProviders: [] + }); + threadCount += out.threads.length; + for (const thread of out.threads){ + if (typeof thread.modelProvider === 'string' && thread.modelProvider) { + providers.add(thread.modelProvider); + } + } + if (out.nextCursor == null || typeof out.nextCursor !== 'string') { + complete = true; + break; + } + cursor = out.nextCursor; + } + return { + providers: [ + ...providers + ].sort(), + source: 'thread/list-distinct', + threadCount, + complete + }; + } + async function appServerReadThread({ threadId, limit, full = false, cursor }) { + limit ??= full ? 2000 : 100; + const bare = (0, _thread_ids_js__rspack_import_3.NM)(threadId); + const remote = (0, _remote_threads_js__rspack_import_2.KI)(bare); + if (remote) { + try { + return await readLocalThread(bare, { + limit, + full, + cursor + }); + } catch (error) { + if (isThreadNotLoaded(error) || isUnknownThread(error)) { + throw new _errors_js__rspack_import_1.Rg(bare, remote.hostId, remote.hostName); + } + throw maybeRemoteThreadError(error, bare); + } + } + try { + return await readLocalThread(bare, { + limit, + full, + cursor + }); + } catch (error) { + throw maybeRemoteThreadError(error, bare); + } + } + async function appServerUnarchiveThread(threadId) { + const bare = (0, _thread_ids_js__rspack_import_3.NM)(threadId); + const { client } = await (0, _codex_bridge_js__rspack_import_0.eC)(); + try { + const result = await client.request('thread/unarchive', { + threadId: bare + }); + if (!result || typeof result !== 'object' || !('thread' in result) || !result.thread || typeof result.thread !== 'object' || !('id' in result.thread) || result.thread.id !== bare) { + throw new Error(`thread/unarchive returned no thread for ${bare}`); + } + } finally{ + client.close(); + } + } + async function readLocalThread(bare, { limit, full, cursor }) { + const { client } = await (0, _codex_bridge_js__rspack_import_0.eC)(); + try { + const meta = await readThreadMetadata(client, bare); + const { turns, complete, nextCursor } = await listTurnsFull(client, bare, { + limit, + full, + cursor, + title: meta.title + }); + return { + threadId: (0, _thread_ids_js__rspack_import_3.BS)(bare), + turns, + backend: 'app-server', + limit, + full, + complete, + nextCursor, + ...complete ? {} : { + warnings: [ + 'Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment' + ] + }, + ...meta.title ? (0, _payload_budget_js__rspack_import_4.aA)(meta.title) : {}, + cwd: meta.cwd, + status: meta.status, + modelProvider: meta.modelProvider, + model: meta.model + }; + } finally{ + client.close(); + } + } + async function appServerStatusProbe() { + const status = await (0, _codex_bridge_js__rspack_import_0.MU)(); + return { + reachable: Boolean(status.reachable && status.mode === 'daemon'), + mode: typeof status.mode === 'string' ? status.mode : undefined, + socketPath: typeof status.socketPath === 'string' ? status.socketPath : undefined + }; + } + function mapListedThread(thread) { + const bare = String(thread.id ?? ''); + const section = thread.section && typeof thread.section.id === 'string' ? { + id: thread.section.id, + name: thread.section.name ?? null + } : null; + const pinned = section?.id === 'Pinned' || section?.name === 'Pinned' || false; + return { + threadId: bare ? (0, _thread_ids_js__rspack_import_3.BS)(bare) : '', + title: String(thread.name || thread.preview || thread.cwd || thread.id || ''), + pinned, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + preview: typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: thread.cwd ?? null, + createdAt: thread.createdAt ?? null, + updatedAt: thread.updatedAt ?? null, + section, + projectId: thread.projectId ?? null, + project: thread.projectId ?? undefined, + status: thread.status, + modelProvider: thread.modelProvider ?? null, + model: thread.model ?? null, + originator: thread.originator ?? null + }; + } + function threadMatchesQuery(thread, needle) { + const haystacks = [ + thread.threadId, + thread.title, + thread.preview, + thread.cwd, + thread.project, + thread.projectId, + thread.model, + thread.modelProvider, + thread.originator, + thread.section?.name, + thread.section?.id + ]; + return haystacks.some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + async function readThreadMetadata(client, threadId) { + try { + const read = await client.request('thread/read', { + threadId + }); + const thread = read.thread && typeof read.thread === 'object' ? read.thread : {}; + const statusObj = thread.status; + const statusType = statusObj && typeof statusObj === 'object' && typeof statusObj.type === 'string' ? String(statusObj.type) : typeof thread.status === 'string' ? thread.status : undefined; + return { + title: typeof thread.name === 'string' ? thread.name : typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: typeof thread.cwd === 'string' ? thread.cwd : null, + status: statusType, + modelProvider: typeof thread.modelProvider === 'string' ? thread.modelProvider : null, + model: typeof thread.model === 'string' ? thread.model : null + }; + } catch (error) { + throw maybeRemoteThreadError(error, threadId); + } + } + async function listTurnsFull(client, threadId, { limit, full, cursor, title }) { + const state = cursor ? (0, _payload_budget_js__rspack_import_4.i3)(READ_PAGE_PREFIX, cursor) : null; + if (state && (state.threadId !== threadId || state.full !== full || !Number.isSafeInteger(state.skip) || state.skip < 0 || state.rpcCursor !== null && typeof state.rpcCursor !== 'string' || state.field !== undefined && ![ + 'text', + 'userText', + 'assistantText' + ].includes(state.field) || state.offset !== undefined && (!Number.isSafeInteger(state.offset) || state.offset < 0))) { + throw new Error('Read cursor does not match this thread or mode'); + } + let rpcCursor = state?.rpcCursor ?? undefined; + let skip = state?.skip ?? 0; + let fragmentField = state?.field; + let fragmentOffset = state?.offset ?? 0; + let itemsView = true; + let method = 'thread/turns/list'; + const seen = new Set(); + const turns = []; + const pageCursor = (source, index, field, offset)=>(0, _payload_budget_js__rspack_import_4.oI)(READ_PAGE_PREFIX, { + threadId, + full, + rpcCursor: source ?? null, + skip: index, + ...field ? { + field, + offset + } : {} + }); + const fits = (candidate)=>(0, _payload_budget_js__rspack_import_4.sQ)({ + threadId, + turns: [ + ...turns, + candidate + ], + backend: 'app-server', + limit, + full, + complete: false, + nextCursor: READ_PAGE_PREFIX, + title: title ? (0, _payload_budget_js__rspack_import_4.aA)(title).title : undefined, + warnings: [ + 'Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment' + ], + exitCode: 0 + }) <= 614400; + while(turns.length < limit){ + let result; + try { + result = await client.request(method, { + threadId, + limit: Math.min(10, Math.max(limit - turns.length, skip + 1)), + sortDirection: full ? 'asc' : 'desc', + ...method === 'thread/turns/list' && itemsView ? { + itemsView: 'full' + } : {}, + ...rpcCursor !== undefined ? { + cursor: rpcCursor + } : {} + }); + } catch (error) { + const code = error?.rpc?.code; + if (turns.length === 0 && code === -32602 && itemsView) { + itemsView = false; + continue; + } + if (turns.length === 0 && code === -32601 && method === 'thread/turns/list') { + method = 'thread/items/list'; + continue; + } + throw maybeRemoteThreadError(error, threadId); + } + if (!Array.isArray(result?.data)) throw new Error(`${method} missing data array for ${threadId}`); + if (skip > result.data.length) throw new Error('Read cursor is stale'); + const pageTurns = normalizeTurns(result.data); + for(let index = skip; index < pageTurns.length; index++){ + const turn = pageTurns[index]; + if (!fragmentField && turns.length && !fits(turn)) { + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index) + }; + } + if (!fragmentField && fits(turn)) { + turns.push(turn); + } else { + const fields = [ + 'text', + 'userText', + 'assistantText' + ].filter((field)=>typeof turn[field] === 'string' && turn[field].length > 0); + let fieldIndex = fragmentField ? fields.indexOf(fragmentField) : 0; + if (fieldIndex < 0) throw new Error('Read fragment cursor is stale'); + for(; fieldIndex < fields.length; fieldIndex++){ + const field = fields[fieldIndex]; + const value = turn[field]; + let offset = field === fragmentField ? fragmentOffset : 0; + if (offset > value.length) throw new Error('Read fragment cursor is stale'); + while(offset < value.length){ + let size = Math.min(value.length - offset, 32768); + let fragment; + do { + let end = offset + size; + if (end < value.length && /[\uD800-\uDBFF]/u.test(value[end - 1])) end--; + const chunk = value.slice(offset, end); + fragment = { + turnKey: turn.turnKey, + role: turn.role, + text: field === 'text' ? chunk : '', + ...field === 'userText' ? { + userText: chunk + } : {}, + ...field === 'assistantText' ? { + assistantText: chunk + } : {}, + startedAt: turn.startedAt, + endedAt: turn.endedAt, + textTruncated: true, + continuation: { + field, + offsetChars: offset, + totalChars: value.length, + fieldComplete: end === value.length, + turnComplete: end === value.length && fieldIndex === fields.length - 1 + } + }; + if (fits(fragment)) { + size = end - offset; + break; + } + size = Math.floor(size / 2); + }while (size > 0); + if (size === 0) { + if (turns.length) return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) + }; + throw new Error('Read fragment metadata exceeds the MCP result byte budget'); + } + turns.push(fragment); + offset += size; + if (turns.length >= limit || offset < value.length && !fits({ + ...fragment, + text: 'x'.repeat(32768) + })) { + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) + }; + } + } + } + fragmentField = undefined; + fragmentOffset = 0; + } + if (turns.length >= limit) { + const nextCursor = index + 1 < pageTurns.length ? pageCursor(rpcCursor, index + 1) : result.nextCursor ? pageCursor(result.nextCursor, 0) : null; + return { + turns: chronologicalPage(turns, full), + complete: nextCursor === null, + nextCursor + }; + } + } + if (result.nextCursor == null) return { + turns: chronologicalPage(turns, full), + complete: true, + nextCursor: null + }; + if (typeof result.nextCursor !== 'string' || !result.nextCursor || seen.has(result.nextCursor)) { + throw new Error(`${method} returned an invalid or repeated cursor`); + } + seen.add(result.nextCursor); + rpcCursor = result.nextCursor; + skip = 0; + } + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, skip) + }; + } + function chronologicalPage(turns, full) { + if (full) return turns; + const groups = []; + for (const turn of turns){ + const last = groups.at(-1); + if (last?.[0]?.turnKey === turn.turnKey) last.push(turn); + else groups.push([ + turn + ]); + } + return groups.reverse().flat(); + } + function normalizeTurns(raw) { + const turns = []; + for (const [index, item] of raw.entries()){ + if (!item || typeof item !== 'object') continue; + const row = item; + const id = String(row.id ?? row.turnId ?? index); + const items = Array.isArray(row.items) ? row.items : null; + if (items) { + turns.push(normalizeTurnWithItems(id, row, items)); + continue; + } + turns.push(normalizeLeafItem(id, row)); + } + return turns; + } + function normalizeTurnWithItems(id, row, items) { + let userText = ''; + let assistantText = ''; + let toolText = ''; + let reasoningText = ''; + for (const entry of items){ + if (!entry || typeof entry !== 'object') continue; + const item = entry; + const type = String(item.type ?? item.role ?? ''); + const text = extractText(item); + if (/user/i.test(type)) userText = joinText(userText, text); + else if (/agent|assistant|message/i.test(type) && !/reasoning|tool/i.test(type)) { + assistantText = joinText(assistantText, text); + } else if (/tool/i.test(type)) toolText = joinText(toolText, text || type); + else if (/reasoning/i.test(type)) reasoningText = joinText(reasoningText, text); + } + let role = 'assistant'; + let text = assistantText || userText; + if (userText && !assistantText) { + role = 'user'; + text = userText; + } else if (!userText && !assistantText && toolText) { + role = 'tool'; + text = toolText; + } else if (!userText && !assistantText && reasoningText) { + role = 'reasoning'; + text = reasoningText; + } + return { + turnKey: (0, _thread_ids_js__rspack_import_3.Sr)(id), + role, + text, + ...userText ? { + userText + } : {}, + ...assistantText ? { + assistantText + } : {}, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null + }; + } + function normalizeLeafItem(id, row) { + const text = extractText(row); + const type = String(row.type ?? row.role ?? ''); + let role = 'assistant'; + if (/user/i.test(type)) role = 'user'; + else if (/tool/i.test(type)) role = 'tool'; + else if (/reasoning/i.test(type)) role = 'reasoning'; + else if (/status|system/i.test(type) && !text) role = 'status'; + return { + turnKey: (0, _thread_ids_js__rspack_import_3.Sr)(id), + role, + text, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null + }; + } + function extractText(row) { + if (typeof row.text === 'string') return row.text; + if (typeof row.content === 'string') return row.content; + if (Array.isArray(row.content)) return row.content.filter((part)=>Boolean(part) && typeof part === 'object' && !Array.isArray(part)).map((part)=>typeof part.text === 'string' ? part.text : '').filter(Boolean).join('\n'); + if (typeof row.preview === 'string') return row.preview; + return ''; + } + function joinText(left, right) { + if (!right) return left; + if (!left) return right; + return `${left}\n${right}`; + } + function isThreadNotLoaded(error) { + const message = errorMessage(error); + return /thread not loaded|not loaded|invalid thread id/i.test(message); + } + function isUnknownThread(error) { + const message = errorMessage(error); + return /no rollout found|thread not found|unknown.*thread/i.test(message); + } + function maybeRemoteThreadError(error, threadId) { + const message = errorMessage(error); + if (/thread not loaded|not loaded|no rollout found|thread not found/i.test(message)) { + const remote = (0, _remote_threads_js__rspack_import_2.KI)(threadId); + if (remote) { + return new _errors_js__rspack_import_1.Rg(threadId, remote.hostId, remote.hostName); + } + const hostId = (0, _remote_threads_js__rspack_import_2.XI)(threadId); + if (hostId) return new _errors_js__rspack_import_1.Rg(threadId, hostId, null); + } + if (error instanceof Error) return error; + return new Error(message || `app-server error for thread ${threadId}`); + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) { + return String(error.message); + } + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); + } + __webpack_require__.d(__webpack_exports__, { + Jf: ()=>appServerReadThread, + OI: ()=>appServerListThreads, + X: ()=>discoverModelProviders, + aI: ()=>appServerSearchThreads, + dM: ()=>appServerStatusProbe, + qL: ()=>appServerUnarchiveThread + }); + }, + "./src/core/chatgpt-desktop/cdp-adapter.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _cdp_session_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/cdp-session.ts"); + var _cdp_dom_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/cdp-dom.ts"); + var _thread_ids_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + var _loopback_js__rspack_import_5 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + var _project_state_js__rspack_import_6 = __webpack_require__("./src/core/chatgpt-desktop/project-state.ts"); + class CdpChatGptDesktopAdapter { + #port = (0, _loopback_js__rspack_import_5.L0)(); + #session = null; + #version = null; + #pageSessionId = null; + #mainTargetId = null; + #submittedTurn = null; + async connect({ port }) { + await this.close(); + this.#port = (0, _loopback_js__rspack_import_5.L0)(process.env, port); + const session = new _cdp_session_js__rspack_import_2.a(this.#port); + try { + this.#version = await session.connect(); + this.#session = session; + await this.#attachMainWindow(); + } catch (error) { + session.close(); + this.#session = null; + throw error; + } + } + async listTargets() { + const session = this.#requireSession(); + const result = await session.send('Target.getTargets'); + return (0, _cdp_dom_js__rspack_import_3.Mr)(result.targetInfos ?? []); + } + async listThreads({ limit = 50 } = {}) { + const sessionId = await this.#ensurePageSession(); + const threads = await (0, _cdp_dom_js__rspack_import_3.AY)(this.#requireSession(), sessionId, { + limit + }); + return { + threads, + backend: 'cdp', + limit + }; + } + async readThread({ threadId, limit = 100, full = false, openTimeoutMs = 90000 }) { + const sessionId = await this.#ensurePageSession(); + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + const turns = await (0, _cdp_dom_js__rspack_import_3.JU)(this.#requireSession(), sessionId, { + limit, + full, + threadId + }); + return { + threadId, + turns, + backend: 'cdp', + limit, + full, + complete: false + }; + } + async sendMessage({ threadId, text, project, openTimeoutMs = 90000 }) { + const sessionId = await this.#ensurePageSession(); + let temporaryThreadId; + const startedNew = !threadId; + if (threadId) { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } else { + const localProject = project ? (0, _project_state_js__rspack_import_6.q)(project) : null; + if (localProject && !localProject.rootPaths.some((path)=>(0, node_fs__rspack_import_0.existsSync)(path))) { + throw new _errors_js__rspack_import_1.Kn(localProject.label, `none of its configured workspace roots exists: ${localProject.rootPaths.map((path)=>JSON.stringify(path)).join(', ') || '(none)'}. Update the Desktop project root before sending.`); + } + await (0, _cdp_dom_js__rspack_import_3.s0)(this.#requireSession(), sessionId, { + project: localProject?.label ?? project, + projectId: localProject?.id + }); + const threads = await (0, _cdp_dom_js__rspack_import_3.AY)(this.#requireSession(), sessionId, { + limit: 20 + }); + const temp = threads.find((t)=>t.threadId.startsWith(_cdp_dom_js__rspack_import_3.hA) && t.selected) ?? threads.find((t)=>t.threadId.startsWith(_cdp_dom_js__rspack_import_3.hA)); + if (temp) temporaryThreadId = temp.threadId; + } + const before = await (0, _cdp_dom_js__rspack_import_3.YX)(this.#requireSession(), sessionId); + const { sentVia } = await (0, _cdp_dom_js__rspack_import_3.lh)(this.#requireSession(), sessionId, text); + let conversationId; + if (startedNew || threadId && (0, _thread_ids_js__rspack_import_4.vr)(threadId)) { + const resolveMs = Math.min(openTimeoutMs, 60000); + try { + conversationId = await (0, _cdp_dom_js__rspack_import_3.z9)(this.#requireSession(), sessionId, { + timeoutMs: resolveMs + }); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', + reason: 'conversation-id-unresolved' + }); + } + } + const durable = (0, _cdp_dom_js__rspack_import_3.X8)(conversationId, threadId); + if (!durable) { + throw new Error('ChatGPT Desktop send accepted but no durable conversation id was available; ' + 'refusing to return a local:client-new-thread:* id as threadId'); + } + this.#submittedTurn = { + threadId: durable, + turnKey: before.lastTurnKey + }; + return { + threadId: durable, + temporaryThreadId, + conversationId: conversationId || (0, _thread_ids_js__rspack_import_4.nn)(durable) || durable, + project, + backend: 'cdp', + experimental: false, + delivery: 'accepted', + sentVia, + message: 'Submitted via CDP composer (focus → insertText → Enter)' + }; + } + async waitForReply({ threadId, timeoutMs = 120000 }) { + const deadline = Date.now() + timeoutMs; + const sessionId = await this.#ensurePageSession(); + const expectedId = threadId && threadId !== 'new' ? (0, _thread_ids_js__rspack_import_4.nn)(threadId) ?? undefined : this.#submittedTurn ? (0, _thread_ids_js__rspack_import_4.nn)(this.#submittedTurn.threadId) ?? undefined : undefined; + if (threadId && !(0, _thread_ids_js__rspack_import_4.vr)(threadId) && threadId !== 'new') { + await this.#openAndWait(sessionId, threadId, Math.min(timeoutMs, 90000)); + } + try { + const done = await (0, _cdp_dom_js__rspack_import_3.FG)(this.#requireSession(), sessionId, { + timeoutMs: Math.max(0, deadline - Date.now()), + expectedConversationId: expectedId, + baselineTurnKey: this.#submittedTurn && (!threadId || (0, _thread_ids_js__rspack_import_4.BS)(threadId) === this.#submittedTurn.threadId) ? this.#submittedTurn.turnKey : undefined + }); + const durable = (0, _cdp_dom_js__rspack_import_3.X8)(done.conversationId, threadId) ?? (done.conversationId ? (0, _thread_ids_js__rspack_import_4.BS)(done.conversationId) : null); + if (!durable) { + throw new Error('ChatGPT Desktop reply finished without a durable conversation id on ' + '[data-response-annotation-conversation]'); + } + return { + threadId: durable, + conversationId: done.conversationId || (0, _thread_ids_js__rspack_import_4.nn)(durable) || durable, + reply: done.reply, + backend: 'cdp', + experimental: false, + delivery: 'replied' + }; + } catch (error) { + if (!(error instanceof _cdp_dom_js__rspack_import_3.aw)) throw error; + return { + threadId: threadId ?? 'unknown', + reply: '', + backend: 'cdp', + experimental: false, + delivery: 'timeout' + }; + } + } + async openThread(threadId, { openTimeoutMs = 90000 } = {}) { + const sessionId = await this.#ensurePageSession(); + try { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } catch (error) { + if (error instanceof _errors_js__rspack_import_1.fw) return { + threadId, + backend: 'cdp', + archived: true + }; + throw error; + } + return { + threadId, + backend: 'cdp' + }; + } + async status() { + try { + const version = this.#version ?? await _cdp_session_js__rspack_import_2.a.fetchVersion(this.#port); + let targetCount; + let mainWindowTargetId; + if (this.#session && !this.#session.closed) { + const targets = await this.listTargets(); + targetCount = targets.length; + mainWindowTargetId = (0, _cdp_dom_js__rspack_import_3.C2)(targets)?.targetId ?? null; + } else { + const list = await _cdp_session_js__rspack_import_2.a.fetchJsonList(this.#port); + targetCount = list.length; + } + return { + reachable: true, + port: this.#port, + host: '127.0.0.1', + browser: typeof version.Browser === 'string' ? version.Browser : undefined, + protocolVersion: typeof version['Protocol-Version'] === 'string' ? version['Protocol-Version'] : undefined, + webSocketDebuggerUrl: typeof version.webSocketDebuggerUrl === 'string' ? version.webSocketDebuggerUrl : undefined, + targetCount, + mainWindowTargetId: mainWindowTargetId ?? this.#mainTargetId, + message: 'ChatGPT Desktop CDP endpoint reachable on 127.0.0.1', + exitCode: 0 + }; + } catch (error) { + return { + reachable: false, + port: this.#port, + host: '127.0.0.1', + message: error instanceof Error ? error.message : 'ChatGPT Desktop CDP endpoint unreachable on 127.0.0.1', + exitCode: 1 + }; + } + } + async close() { + this.#pageSessionId = null; + this.#mainTargetId = null; + this.#version = null; + this.#session?.close(); + this.#session = null; + } + async #openAndWait(sessionId, threadId, openTimeoutMs) { + await (0, _cdp_dom_js__rspack_import_3.Kl)(this.#requireSession(), sessionId, threadId); + await (0, _cdp_dom_js__rspack_import_3.Zx)(this.#requireSession(), sessionId, { + timeoutMs: openTimeoutMs, + threadId + }); + } + async #attachMainWindow() { + const targets = await this.listTargets(); + const main = (0, _cdp_dom_js__rspack_import_3.C2)(targets); + if (!main) { + throw new _errors_js__rspack_import_1.sO(`No ChatGPT Desktop main window target with url ${JSON.stringify('app://-/index.html')}`); + } + const attached = await this.#requireSession().send('Target.attachToTarget', { + targetId: main.targetId, + flatten: true + }); + this.#mainTargetId = main.targetId; + this.#pageSessionId = attached.sessionId; + } + async #ensurePageSession() { + this.#requireSession(); + if (!this.#pageSessionId) await this.#attachMainWindow(); + if (!this.#pageSessionId) { + throw new _errors_js__rspack_import_1.sO('Failed to attach to ChatGPT Desktop main window'); + } + return this.#pageSessionId; + } + #requireSession() { + if (!this.#session || this.#session.closed) { + throw new _errors_js__rspack_import_1.sO('Not connected to ChatGPT Desktop CDP. Call connect({ port }) after relaunching with --remote-debugging-port.'); + } + return this.#session; + } + } + __webpack_require__.d(__webpack_exports__, { + a: ()=>CdpChatGptDesktopAdapter + }); + }, + "./src/core/chatgpt-desktop/cdp-dom.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _codex_thread_id_js__rspack_import_1 = __webpack_require__("./src/core/codex/thread-id.js"); + const MAIN_WINDOW_URL = 'app://-/index.html'; + const IGNORED_TARGET_URL_MARKERS = [ + 'initialRoute=%2Favatar-overlay', + 'detached-window.html', + 'chatgpt.com', + 'codex-sandbox' + ]; + const ATTR = { + threadRow: 'data-app-action-sidebar-thread-row', + threadId: 'data-app-action-sidebar-thread-id', + threadTitle: 'data-app-action-sidebar-thread-title', + threadPinned: 'data-app-action-sidebar-thread-pinned', + threadSelected: 'data-app-action-sidebar-thread-selected', + threadKind: 'data-app-action-sidebar-thread-kind', + turnKey: 'data-turn-key', + userBubble: 'data-user-message-bubble', + timelineScroll: 'data-app-action-timeline-scroll', + virtualizedTurn: 'data-virtualized-turn-content', + composer: 'data-codex-composer', + finalAssistant: 'data-local-conversation-final-assistant', + markdownTextStyle: 'data-markdown-text-style', + conversationAnnotation: 'data-response-annotation-conversation' + }; + const MAIN_CONTENT_SURFACE_CLASS_PREFIX = '_MainContentSurface'; + const HISTORY_GAP_PREFIX = 'history-gap:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const LOADING_TASK_TEXT = 'Loading task…'; + const DEFAULT_OPEN_TIMEOUT_MS = 90000; + const DEFAULT_CONVERSATION_RESOLVE_MS = 60000; + const DEFAULT_FULL_READ_IDLE_WHEELS = 8; + const DEFAULT_FULL_READ_MAX_WHEELS = 400; + const FULL_READ_WHEEL_DELTA_Y = -800; + const COMPOSER_SELECTOR = `[${ATTR.composer}="true"][contenteditable="true"]`; + const SEND_BUTTON_SELECTOR = 'button[aria-label="Send"]'; + const STOP_BUTTON_SELECTOR = 'main button[aria-label="Stop"]'; + const FINAL_ASSISTANT_SELECTOR = `[${ATTR.finalAssistant}="true"]`; + const ASSISTANT_MESSAGE_SELECTOR = `[${ATTR.finalAssistant}="true"] [${ATTR.markdownTextStyle}="assistant-message"]`; + const CONVERSATION_ANNOTATION_SELECTOR = `[${ATTR.conversationAnnotation}]`; + const SELECTORS = { + threadRow: `[role="button"][${ATTR.threadRow}]`, + turn: `[${ATTR.turnKey}]`, + userBubble: `[${ATTR.userBubble}]`, + timelineScroll: `[${ATTR.timelineScroll}]`, + composer: COMPOSER_SELECTOR, + sendButton: SEND_BUTTON_SELECTOR, + stopButton: STOP_BUTTON_SELECTOR, + finalAssistant: FINAL_ASSISTANT_SELECTOR, + assistantMessage: ASSISTANT_MESSAGE_SELECTOR, + conversationAnnotation: CONVERSATION_ANNOTATION_SELECTOR, + newChatFallback: 'button' + }; + function newChatInProjectSelector(project) { + return `button[aria-label=${JSON.stringify(`Start new chat in ${project}`)}]`; + } + function isMainWindowTarget(target) { + if (target.type !== 'page') return false; + const url = typeof target.url === 'string' ? target.url : ''; + if (url !== MAIN_WINDOW_URL) return false; + return !IGNORED_TARGET_URL_MARKERS.some((marker)=>url.includes(marker)); + } + function summarizeTargetInfos(targetInfos) { + return targetInfos.filter((t)=>typeof t.targetId === 'string' && typeof t.type === 'string').map((t)=>({ + targetId: String(t.targetId), + type: String(t.type), + title: typeof t.title === 'string' ? t.title : '', + url: typeof t.url === 'string' ? t.url : '', + attached: Boolean(t.attached) + })); + } + function pickMainWindowTarget(targets) { + return targets.find((t)=>isMainWindowTarget(t)) ?? null; + } + function dedupeThreadsById(threads) { + const byKey = new Map(); + for (const thread of threads){ + if (!thread.threadId) continue; + const key = durableThreadKey(thread.threadId); + const existing = byKey.get(key); + if (!existing) { + byKey.set(key, thread); + continue; + } + byKey.set(key, preferThreadRow(existing, thread)); + } + return [ + ...byKey.values() + ]; + } + function durableThreadKey(threadId) { + if ((0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) return threadId; + try { + return (0, _codex_thread_id_js__rspack_import_1.SX)(threadId); + } catch { + return threadId.startsWith("local:") ? threadId.slice("local:".length) || threadId : threadId; + } + } + function toDesktopForm(threadId) { + if (!threadId || (0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) return threadId; + if (threadId.startsWith("local:")) return threadId; + return `${"local:"}${threadId}`; + } + function preferThreadRow(left, right) { + const score = (row)=>(row.selected ? 4 : 0) + (row.pinned ? 2 : 0) + (row.threadId.startsWith("local:") && !(0, _codex_thread_id_js__rspack_import_1.vr)(row.threadId) ? 1 : 0); + return score(right) > score(left) ? right : left; + } + const LIST_THREADS_EXPRESSION = `(() => { + const skipProjectLabels = new Set(['Recent', 'Pinned', 'Threads', 'Chats']); + const projectFor = (row) => { + let el = row.parentElement; + for (let i = 0; i < 8 && el; i += 1, el = el.parentElement) { + const attr = + el.getAttribute('data-app-action-sidebar-project') || + el.getAttribute('data-app-action-sidebar-project-name') || + el.getAttribute('data-app-action-sidebar-section-title'); + if (attr && !skipProjectLabels.has(attr)) return attr; + } + return undefined; + }; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const seen = new Set(); + const out = []; + for (const row of rows) { + const threadId = row.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''; + if (!threadId || seen.has(threadId)) continue; + seen.add(threadId); + const project = projectFor(row); + out.push({ + threadId, + hostId: row.getAttribute('data-app-action-sidebar-thread-host-id') || null, + title: row.getAttribute(${JSON.stringify(ATTR.threadTitle)}) || (row.textContent || '').trim(), + pinned: row.getAttribute(${JSON.stringify(ATTR.threadPinned)}) === 'true', + selected: row.getAttribute(${JSON.stringify(ATTR.threadSelected)}) === 'true', + kind: row.getAttribute(${JSON.stringify(ATTR.threadKind)}) || 'unknown', + ...(project ? { project } : {}), + }); + } + return out; +})()`; + function openThreadExpression(threadId) { + const bare = durableThreadKey(threadId); + const desktop = toDesktopForm(bare); + const candidates = [ + ...new Set([ + threadId, + bare, + desktop + ].filter(Boolean)) + ]; + return `(() => { + const candidates = ${JSON.stringify(candidates)}; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const row = rows.find((el) => + candidates.includes(el.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''), + ); + if (!row) return { ok: false, error: 'thread-not-found' }; + row.click(); + return { ok: true, threadId: row.getAttribute(${JSON.stringify(ATTR.threadId)}) || candidates[0] }; + })()`; + } + function navigateToThreadExpression(threadId) { + const bare = durableThreadKey(threadId); + return `(async () => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; + let scanned = 0; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); + if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { + await router.navigate(${JSON.stringify(`/local/${bare}`)}); + return { ok: true, via: 'router' }; + } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + return { ok: false, error: 'router-not-found' }; + })()`; + } + function startNewChatExpression(project, projectId) { + const projectSel = project ? newChatInProjectSelector(project) : null; + return `(() => { + const projectSel = ${JSON.stringify(projectSel)}; + if (projectSel) { + const preferred = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(projectSel) ||` : ''} document.querySelector(projectSel); + if (preferred) { + const row = preferred.closest('[data-app-action-sidebar-project-row]') || preferred.parentElement; + row?.scrollIntoView({ block: 'center' }); + const rowBox = row?.getBoundingClientRect(); + return { ok: true, via: 'project', project: ${JSON.stringify(project ?? null)}, + disabled: preferred.disabled || preferred.getAttribute('aria-disabled') === 'true', + hoverX: rowBox?.left + rowBox?.width / 2, hoverY: rowBox?.top + rowBox?.height / 2 }; + } + return { ok: false, error: 'project-not-found' }; + } + const buttons = Array.from(document.querySelectorAll('button')); + const fallback = buttons.find((btn) => (btn.textContent || '').trim() === 'New chat'); + if (!fallback) return { ok: false, error: 'new-chat-not-found' }; + fallback.click(); + return { ok: true, via: 'fallback' }; + })()`; + } + function projectButtonBoxExpression(project, projectId) { + return `(() => { + const button = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(${JSON.stringify(newChatInProjectSelector(project))}) ||` : ''} document.querySelector(${JSON.stringify(newChatInProjectSelector(project))}); + if (!button) return null; + const box = button.getBoundingClientRect(); + return { x: box.left + box.width / 2, y: box.top + box.height / 2, + disabled: button.disabled || button.getAttribute('aria-disabled') === 'true', + visible: box.width > 0 && box.height > 0 && box.x >= 0 && box.y >= 0 + && box.x < innerWidth && box.y < innerHeight && getComputedStyle(button.parentElement).opacity !== '0' }; + })()`; + } + function newChatViewExpression(project) { + return `(() => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; let scanned = 0; let path = ''; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { path = router.state.location.pathname; break; } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + const composer = !!document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + const scope = document.querySelector('[data-composer-navigation-target="workspace-project"]')?.getAttribute('aria-label') || ''; + const expected = ${JSON.stringify(project ? `Change project: ${project}` : '')}; + return { ready: path === '/' && composer && (expected ? scope === expected : (!scope || scope === 'Choose project')), path, composer, scope }; + })()`; + } + const CLEAR_NEW_CHAT_PROJECT_EXPRESSION = `(() => { + const button = document.querySelector('[data-clear-project-button="true"]'); + if (!button || button.disabled) return false; + button.click(); + return true; +})()`; + const LOADING_TASK_GONE_EXPRESSION = `(() => { + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const text = (surface.innerText || '').replace(/\\u2026/g, '…'); + return !text.includes(${JSON.stringify(LOADING_TASK_TEXT)}); +})()`; + const ARCHIVED_THREAD_EXPRESSION = `(() => Array.from(document.querySelectorAll('main')) + .some((main) => (main.innerText || '').includes('This task is archived') + && (main.innerText || '').includes('Unarchive this task to open it')))()`; + const FOCUS_COMPOSER_EXPRESSION = `(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + if (!composer) return { ok: false, error: 'composer-not-found' }; + if ((composer.textContent || '').trim()) return { ok: false, error: 'composer-has-draft' }; + if (document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)})) return { ok: false, error: 'reply-in-progress' }; + composer.focus(); + return { ok: true }; +})()`; + const REPLY_STATE_EXPRESSION = `(() => { + const stop = document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)}); + const finals = Array.from(document.querySelectorAll(${JSON.stringify(FINAL_ASSISTANT_SELECTOR)})); + const lastFinal = finals.at(-1) || null; + const annotation = lastFinal + ? lastFinal.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + || lastFinal.closest(${JSON.stringify(SELECTORS.turn)})?.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + : document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + const conversationId = annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; + const messageEl = lastFinal + ? lastFinal.querySelector(${JSON.stringify(`[${ATTR.markdownTextStyle}="assistant-message"]`)}) + : null; + const reply = messageEl ? (messageEl.innerText || '').trim() : ''; + const turns = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.turn)})); + const lastTurn = turns.at(-1); + const lastTurnKey = lastTurn ? (lastTurn.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '') : ''; + return { + stopVisible: Boolean(stop), + finalAssistantCount: finals.length, + conversationId, + reply, + lastTurnKey, + finalTurnKey: lastFinal?.closest(${JSON.stringify(SELECTORS.turn)})?.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '', + }; +})()`; + const CONVERSATION_ID_EXPRESSION = `(() => { + const annotation = document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + return annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; +})()`; + const HARVEST_VISIBLE_TURNS_EXPRESSION = `(() => { + const gapPrefix = ${JSON.stringify(HISTORY_GAP_PREFIX)}; + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const scroll = + surface.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}) + || document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + const root = scroll || surface; + const out = []; + for (const el of root.querySelectorAll(${JSON.stringify(SELECTORS.turn)})) { + const turnKey = el.getAttribute(${JSON.stringify(ATTR.turnKey)}) || ''; + if (!turnKey || turnKey.startsWith(gapPrefix)) continue; + const user = el.querySelector(${JSON.stringify(SELECTORS.userBubble)}); + const assistant = el.querySelector(${JSON.stringify(ASSISTANT_MESSAGE_SELECTOR)}); + const userText = user ? (user.innerText || '').trim() : ''; + const assistantText = assistant ? (assistant.innerText || '').trim() : ''; + const raw = (el.innerText || '').trim(); + const statusRe = /^Worked for \\d+s$/i; + let role = 'assistant'; + let text = assistantText || userText || raw; + if (user && !assistantText) { + role = 'user'; + text = userText || raw; + } else if (assistantText) { + role = 'assistant'; + text = assistantText; + } else if (!raw || statusRe.test(raw)) { + role = 'status'; + text = raw; + } + out.push({ turnKey, role, text, userText, assistantText }); + } + return out; +})()`; + const TIMELINE_SCROLL_METRICS_EXPRESSION = `(() => { + const scroll = document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + if (!scroll) return { ok: false }; + const rect = scroll.getBoundingClientRect(); + return { + ok: true, + x: Math.floor(rect.left + rect.width / 2), + y: Math.floor(rect.top + Math.min(rect.height / 2, 120)), + scrollTop: scroll.scrollTop, + scrollHeight: scroll.scrollHeight, + clientHeight: scroll.clientHeight, + }; +})()`; + async function listThreadsFromDom(session, sessionId, { limit = 50 } = {}) { + const rows = await session.evaluate(LIST_THREADS_EXPRESSION, { + sessionId + }); + if (!Array.isArray(rows)) return []; + return dedupeThreadsById(rows).slice(0, limit).map((row)=>({ + threadId: String(row.threadId), + title: String(row.title ?? ''), + pinned: Boolean(row.pinned), + selected: Boolean(row.selected), + kind: String(row.kind ?? 'unknown'), + ...row.hostId ? { + hostId: row.hostId, + location: row.hostId === 'local' ? 'local' : 'remote' + } : {}, + ...row.project ? { + project: String(row.project) + } : {} + })); + } + async function openThreadInDom(session, sessionId, threadId) { + const current = await readConversationIdFromDom(session, sessionId); + if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + if ((0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { + limit: 200 + }); + if (rows.some((row)=>row.selected && row.threadId === threadId)) return; + } + const result = await session.evaluate(openThreadExpression(threadId), { + sessionId + }); + if (!result?.ok) { + if (result?.error === 'thread-not-found' && !(0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const navigated = await session.evaluate(navigateToThreadExpression(threadId), { + sessionId + }); + if (navigated?.ok) return; + } + throw new Error(result?.error === 'thread-not-found' ? `ChatGPT Desktop thread not found: ${threadId}` : `Failed to open ChatGPT Desktop thread ${threadId}`); + } + } + async function waitForLoadingTaskGone(session, sessionId, { timeoutMs = DEFAULT_OPEN_TIMEOUT_MS, threadId } = {}) { + const deadline = Date.now() + timeoutMs; + let loadingGone = false; + let lastConversationId = ''; + let archivedSince = null; + while(Date.now() < deadline){ + if (threadId && threadId !== 'new' && await session.evaluate(ARCHIVED_THREAD_EXPRESSION, { + sessionId + }) === true) { + archivedSince ??= Date.now(); + if (Date.now() - archivedSince >= 500) { + throw new _errors_js__rspack_import_0.fw(threadId); + } + } else { + archivedSince = null; + } + const gone = await session.evaluate(LOADING_TASK_GONE_EXPRESSION, { + sessionId + }); + loadingGone ||= gone; + if (gone) { + const current = threadId ? await readConversationIdFromDom(session, sessionId) : ''; + lastConversationId = current; + if (!threadId) return; + if (threadId === 'new' || (0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { + limit: 200 + }); + const selected = rows.find((row)=>row.selected); + if (!current && (!selected || (0, _codex_thread_id_js__rspack_import_1.vr)(selected.threadId))) return; + } else if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + } + await delay(250); + } + throw new Error(loadingGone ? `ChatGPT Desktop timed out waiting for conversation ${JSON.stringify(threadId)}; displayed conversation ${JSON.stringify(lastConversationId)} after ${timeoutMs}ms` : `ChatGPT Desktop thread still showing ${JSON.stringify(LOADING_TASK_TEXT)} after ${timeoutMs}ms`); + } + async function startNewChatInDom(session, sessionId, { project, projectId, navigationTimeoutMs = 3000 } = {}) { + const result = await session.evaluate(startNewChatExpression(project, projectId), { + sessionId + }); + if (!result?.ok) { + throw new Error(project ? `ChatGPT Desktop new-chat button not found for project ${JSON.stringify(project)}` : 'ChatGPT Desktop "New chat" button not found'); + } + if (result.via === 'project') { + if (result.disabled) throw new _errors_js__rspack_import_0.Kn(project, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (![ + result.hoverX, + result.hoverY + ].every(Number.isFinite)) { + throw new Error(`ChatGPT Desktop project button has no visible box for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: result.hoverX, + y: result.hoverY + }, { + sessionId + }); + const button = await session.evaluate(projectButtonBoxExpression(project, projectId), { + sessionId + }); + if (button?.disabled) throw new _errors_js__rspack_import_0.Kn(project, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (!button?.visible || !Number.isFinite(button.x) || !Number.isFinite(button.y)) { + throw new Error(`ChatGPT Desktop project button did not appear after hover for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: button.x, + y: button.y + }, { + sessionId + }); + await session.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: button.x, + y: button.y, + button: 'left', + clickCount: 1 + }, { + sessionId + }); + await session.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: button.x, + y: button.y, + button: 'left', + clickCount: 1 + }, { + sessionId + }); + } + let observed = { + ready: false, + path: '', + composer: false, + scope: '' + }; + const deadline = Date.now() + navigationTimeoutMs; + while(Date.now() < deadline){ + observed = await session.evaluate(newChatViewExpression(project), { + sessionId + }); + if (observed?.ready) return { + via: result.via ?? 'unknown' + }; + if (!project && observed?.path === '/' && observed?.composer && observed.scope.startsWith('Change project: ')) { + await session.evaluate(CLEAR_NEW_CHAT_PROJECT_EXPRESSION, { + sessionId + }); + } + await delay(100); + } + throw new _errors_js__rspack_import_0.XL(project, `route ${JSON.stringify(observed?.path)}, composer ${Boolean(observed?.composer)}, project ${JSON.stringify(observed?.scope)}`); + } + async function focusComposer(session, sessionId) { + const focused = await session.evaluate(FOCUS_COMPOSER_EXPRESSION, { + sessionId + }); + if (!focused?.ok) { + if (focused?.error === 'composer-has-draft') throw new _errors_js__rspack_import_0.oN(); + throw new Error(`ChatGPT Desktop composer unavailable: ${focused?.error || 'not found'}`); + } + } + async function submitComposerWithEnter(session, sessionId) { + const base = { + windowsVirtualKeyCode: 13, + code: 'Enter', + key: 'Enter', + text: '\r', + unmodifiedText: '\r' + }; + await session.send('Input.dispatchKeyEvent', { + type: 'rawKeyDown', + ...base + }, { + sessionId + }); + await session.send('Input.dispatchKeyEvent', { + type: 'keyUp', + ...base + }, { + sessionId + }); + } + async function sendMessageInDom(session, sessionId, text) { + await focusComposer(session, sessionId); + await session.send('Input.insertText', { + text + }, { + sessionId + }); + try { + await submitComposerWithEnter(session, sessionId); + const deadline = Date.now() + 3000; + while(Date.now() < deadline){ + const cleared = await session.evaluate(`(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + return Boolean(composer && !(composer.textContent || '').trim()); + })()`, { + sessionId + }); + if (cleared) return { + sentVia: 'enter' + }; + await delay(100); + } + throw new Error('Desktop composer did not acknowledge submission'); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', + reason: 'submission-unconfirmed' + }); + } + } + async function readReplyState(session, sessionId) { + const state = await session.evaluate(REPLY_STATE_EXPRESSION, { + sessionId + }); + return { + stopVisible: Boolean(state?.stopVisible), + finalAssistantCount: Number(state?.finalAssistantCount ?? 0), + conversationId: String(state?.conversationId ?? ''), + reply: String(state?.reply ?? ''), + lastTurnKey: String(state?.lastTurnKey ?? ''), + finalTurnKey: String(state?.finalTurnKey ?? '') + }; + } + async function readConversationIdFromDom(session, sessionId) { + const value = await session.evaluate(CONVERSATION_ID_EXPRESSION, { + sessionId + }); + return typeof value === 'string' ? value.trim() : ''; + } + async function waitForConversationId(session, sessionId, { timeoutMs = DEFAULT_CONVERSATION_RESOLVE_MS } = {}) { + const deadline = Date.now() + timeoutMs; + while(Date.now() < deadline){ + const id = await readConversationIdFromDom(session, sessionId); + if (id && !(0, _codex_thread_id_js__rspack_import_1.vr)(id) && !id.startsWith(TEMP_THREAD_ID_PREFIX)) { + return id; + } + await delay(200); + } + throw new Error(`ChatGPT Desktop conversation id did not appear on [data-response-annotation-conversation] within ${timeoutMs}ms`); + } + function resolveDurableThreadId(conversationId, fallbackThreadId) { + if (conversationId && !(0, _codex_thread_id_js__rspack_import_1.vr)(conversationId)) { + return toDesktopForm(conversationId); + } + if (fallbackThreadId && !(0, _codex_thread_id_js__rspack_import_1.vr)(fallbackThreadId) && fallbackThreadId !== 'new') { + return toDesktopForm(fallbackThreadId); + } + return null; + } + class ReplyTimeoutError extends Error { + } + async function waitForReplyDone(session, sessionId, { timeoutMs = 120000, baselineFinalCount = 0, expectedConversationId, baselineTurnKey } = {}) { + const deadline = Date.now() + timeoutMs; + let sawStop = false; + const started = Date.now(); + while(Date.now() < deadline){ + const state = await readReplyState(session, sessionId); + if (state.stopVisible) sawStop = true; + expectedConversationId ??= state.conversationId || undefined; + if (expectedConversationId && state.conversationId && durableThreadKey(state.conversationId) !== durableThreadKey(expectedConversationId)) { + throw new Error('ChatGPT Desktop selected conversation changed while waiting'); + } + const latestIsFinal = Boolean(state.finalTurnKey) && state.finalTurnKey === state.lastTurnKey; + const hasNewFinal = (state.finalAssistantCount > baselineFinalCount || sawStop) && Boolean(state.reply) && latestIsFinal && (!baselineTurnKey || state.lastTurnKey !== baselineTurnKey); + if (!state.stopVisible && hasNewFinal && (sawStop || Date.now() - started > 1500)) { + return { + reply: state.reply, + conversationId: state.conversationId + }; + } + await delay(200); + } + throw new ReplyTimeoutError(`ChatGPT Desktop reply did not finish within ${timeoutMs}ms`); + } + function normalizeTurn(raw) { + const role = raw.role === 'user' || raw.role === 'status' || raw.role === 'assistant' ? raw.role : raw.assistantText ? 'assistant' : raw.userText ? 'user' : 'assistant'; + return { + turnKey: String(raw.turnKey ?? ''), + role, + text: String(raw.text ?? raw.assistantText ?? raw.userText ?? ''), + userText: raw.userText ? String(raw.userText) : undefined, + assistantText: raw.assistantText ? String(raw.assistantText) : undefined + }; + } + async function harvestVisibleTurns(session, sessionId) { + const rows = await session.evaluate(HARVEST_VISIBLE_TURNS_EXPRESSION, { + sessionId + }); + if (!Array.isArray(rows)) return []; + return rows.map(normalizeTurn).filter((t)=>t.turnKey && !t.turnKey.startsWith(HISTORY_GAP_PREFIX)); + } + async function readTurnsFromDom(session, sessionId, { limit = 100, full = false, threadId, idleWheels = DEFAULT_FULL_READ_IDLE_WHEELS, maxWheels = DEFAULT_FULL_READ_MAX_WHEELS } = {}) { + const collected = new Map(); + const order = []; + const ingest = async ()=>{ + const older = []; + for (const turn of (await harvestVisibleTurns(session, sessionId))){ + if (!collected.has(turn.turnKey)) older.push(turn.turnKey); + collected.set(turn.turnKey, turn); + } + order.unshift(...older); + }; + await ingest(); + if (!full) { + const visible = order.map((key)=>collected.get(key)); + return limit > 0 ? visible.slice(-limit) : visible; + } + let idle = 0; + for(let step = 0; step < maxWheels; step++){ + const before = collected.size; + const metrics = await session.evaluate(TIMELINE_SCROLL_METRICS_EXPRESSION, { + sessionId + }); + if (!metrics?.ok || metrics.x == null || metrics.y == null) break; + await session.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: metrics.x, + y: metrics.y, + deltaX: 0, + deltaY: FULL_READ_WHEEL_DELTA_Y + }, { + sessionId + }); + await delay(120); + await ingest(); + if (collected.size === before) { + idle += 1; + if (idle >= idleWheels) break; + } else { + idle = 0; + } + if (threadId && collected.has(threadId)) break; + } + const all = order.map((key)=>collected.get(key)); + return limit > 0 ? all.slice(0, limit) : all; + } + function delay(ms) { + return new Promise((resolve)=>setTimeout(resolve, ms)); + } + __webpack_require__.d(__webpack_exports__, { + AY: ()=>listThreadsFromDom, + C2: ()=>pickMainWindowTarget, + FG: ()=>waitForReplyDone, + JU: ()=>readTurnsFromDom, + Kl: ()=>openThreadInDom, + Mr: ()=>summarizeTargetInfos, + X8: ()=>resolveDurableThreadId, + YX: ()=>readReplyState, + Zx: ()=>waitForLoadingTaskGone, + aw: ()=>ReplyTimeoutError, + lh: ()=>sendMessageInDom, + s0: ()=>startNewChatInDom, + z9: ()=>waitForConversationId + }, { + hA: TEMP_THREAD_ID_PREFIX + }); + }, + "./src/core/chatgpt-desktop/cdp-session.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _loopback_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + class CdpSession { + #port; + #ws = null; + #nextId = 1; + #pending = new Map(); + #closed = false; + constructor(port){ + this.#port = port; + } + get port() { + return this.#port; + } + get closed() { + return this.#closed || this.#ws === null || this.#ws.readyState === WebSocket.CLOSED; + } + static async fetchVersion(port, { signal } = {}) { + const base = (0, _loopback_js__rspack_import_1.NJ)(port); + let response; + try { + response = await fetch(`${base}/json/version`, { + signal: signal ? AbortSignal.any([ + signal, + AbortSignal.timeout(5000) + ]) : AbortSignal.timeout(5000), + redirect: 'error' + }); + } catch (error) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP not reachable at ${base}/json/version: ` + (error instanceof Error ? error.message : String(error))); + } + if (!response.ok) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP /json/version returned HTTP ${response.status}`); + } + const version = await response.json(); + if (typeof version.webSocketDebuggerUrl === 'string') { + (0, _loopback_js__rspack_import_1.jv)(version.webSocketDebuggerUrl); + } + return version; + } + static async fetchJsonList(port, { signal } = {}) { + const base = (0, _loopback_js__rspack_import_1.NJ)(port); + let response; + try { + response = await fetch(`${base}/json/list`, { + signal: signal ? AbortSignal.any([ + signal, + AbortSignal.timeout(5000) + ]) : AbortSignal.timeout(5000), + redirect: 'error' + }); + } catch (error) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP not reachable at ${base}/json/list: ` + (error instanceof Error ? error.message : String(error))); + } + if (!response.ok) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP /json/list returned HTTP ${response.status}`); + } + const body = await response.json(); + return Array.isArray(body) ? body : []; + } + async connect({ signal } = {}) { + if (this.#ws && !this.closed) { + throw new Error('CDP session already connected'); + } + const version = await CdpSession.fetchVersion(this.#port, { + signal + }); + const rawUrl = version.webSocketDebuggerUrl; + if (typeof rawUrl !== 'string' || !rawUrl) { + throw new _errors_js__rspack_import_0.sO('CDP /json/version omitted webSocketDebuggerUrl'); + } + const url = (0, _loopback_js__rspack_import_1.jv)(rawUrl); + signal?.throwIfAborted(); + await new Promise((resolve, reject)=>{ + const ws = new WebSocket(url); + this.#ws = ws; + const cleanup = ()=>{ + clearTimeout(timer); + signal?.removeEventListener('abort', onAbort); + }; + const onAbort = ()=>{ + cleanup(); + ws.close(); + reject(new _errors_js__rspack_import_0.sO('CDP connect aborted or timed out')); + }; + const timer = setTimeout(onAbort, 5000); + signal?.addEventListener('abort', onAbort, { + once: true + }); + ws.addEventListener('open', ()=>{ + cleanup(); + resolve(); + }, { + once: true + }); + ws.addEventListener('error', ()=>{ + cleanup(); + reject(new _errors_js__rspack_import_0.sO(`CDP WebSocket failed for ${url}`)); + }, { + once: true + }); + ws.addEventListener('message', (event)=>this.#onMessage(event.data)); + ws.addEventListener('close', ()=>{ + cleanup(); + reject(new _errors_js__rspack_import_0.sO('CDP WebSocket closed before connecting')); + this.#closed = true; + for (const [, pending] of this.#pending){ + clearTimeout(pending.timer); + pending.reject(new _errors_js__rspack_import_0.sO('CDP WebSocket closed')); + } + this.#pending.clear(); + }); + }); + this.#closed = false; + return version; + } + async send(method, params, { sessionId } = {}) { + if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) { + throw new _errors_js__rspack_import_0.sO('CDP session is not connected'); + } + const id = this.#nextId++; + const payload = { + id, + method + }; + if (params !== undefined) payload.params = params; + if (sessionId !== undefined) payload.sessionId = sessionId; + return await new Promise((resolve, reject)=>{ + const timer = setTimeout(()=>{ + this.#pending.delete(id); + reject(new _errors_js__rspack_import_0.sO(`CDP ${method} timed out`)); + }, 10000); + this.#pending.set(id, { + timer, + resolve: (value)=>resolve(value), + reject + }); + try { + this.#ws.send(JSON.stringify(payload)); + } catch (error) { + clearTimeout(timer); + this.#pending.delete(id); + reject(error); + } + }); + } + async evaluate(expression, { sessionId, awaitPromise = true } = {}) { + const result = await this.send('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise + }, { + sessionId + }); + if (result.exceptionDetails) { + const text = result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Runtime.evaluate failed'; + throw new Error(text); + } + return result.result?.value; + } + close() { + this.#closed = true; + for (const pending of this.#pending.values()){ + clearTimeout(pending.timer); + pending.reject(new _errors_js__rspack_import_0.sO('CDP session closed')); + } + this.#pending.clear(); + this.#ws?.close(); + this.#ws = null; + } + #onMessage(data) { + let message; + try { + message = JSON.parse(String(data)); + } catch { + return; + } + if (!message || typeof message !== 'object' || typeof message.id !== 'number') return; + const pending = this.#pending.get(message.id); + if (!pending) return; + this.#pending.delete(message.id); + clearTimeout(pending.timer); + if (message.error) { + pending.reject(new Error(message.error.message || 'CDP error')); + return; + } + pending.resolve(message.result); + } + } + __webpack_require__.d(__webpack_exports__, { + a: ()=>CdpSession + }); + }, + "./src/core/chatgpt-desktop/errors.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + class ArchivedThreadError extends Error { + code = 'ARCHIVED_THREAD'; + reason = 'archived-thread'; + delivery = 'rejected'; + threadId; + constructor(threadId){ + super(`ChatGPT Desktop thread ${JSON.stringify(threadId)} is archived. Read it through app-server, or pass unarchive: true on send to unarchive before sending.`); + this.name = 'ArchivedThreadError'; + this.threadId = threadId; + } + } + class ComposerDraftError extends Error { + code = 'COMPOSER_HAS_DRAFT'; + reason = 'composer-has-draft'; + delivery = 'rejected'; + constructor(){ + super('ChatGPT Desktop composer contains an unsent draft; inspect it in Desktop before sending.'); + this.name = 'ComposerDraftError'; + } + } + class ProjectUnavailableError extends Error { + code = 'PROJECT_UNAVAILABLE'; + reason = 'project-unavailable'; + delivery = 'rejected'; + constructor(project, detail){ + super(`ChatGPT Desktop cannot start a chat in project ${JSON.stringify(project)}: ${detail}`); + this.name = 'ProjectUnavailableError'; + } + } + class NewChatNavigationError extends Error { + code = 'NEW_CHAT_NAVIGATION_FAILED'; + reason = 'new-chat-navigation-failed'; + delivery = 'rejected'; + constructor(project, observed){ + super(`ChatGPT Desktop new-chat action did not open ${project ? `project ${JSON.stringify(project)}` : 'a projectless chat'}; observed ${observed}`); + this.name = 'NewChatNavigationError'; + } + } + class NotImplementedError extends Error { + code = 'NOT_IMPLEMENTED'; + delivery = 'rejected'; + reason = 'not-implemented'; + constructor(operation, detail){ + super(detail ? `ChatGPT Desktop ${operation} is not implemented: ${detail}` : `ChatGPT Desktop ${operation} is not implemented`); + this.name = 'NotImplementedError'; + } + } + class CdpHostRejectedError extends Error { + code = 'CDP_HOST_REJECTED'; + delivery = 'rejected'; + reason = 'cdp-host-rejected'; + constructor(host){ + super(`ChatGPT Desktop CDP rejects non-loopback host ${JSON.stringify(host)}. ` + 'Connect to 127.0.0.1 only; there is no remote transport.'); + this.name = 'CdpHostRejectedError'; + } + } + class CdpUnreachableError extends Error { + code = 'CDP_UNREACHABLE'; + delivery = 'rejected'; + reason = 'cdp-unreachable'; + constructor(message){ + super(message); + this.name = 'CdpUnreachableError'; + } + } + __webpack_require__.d(__webpack_exports__, { + EH: ()=>NotImplementedError, + Kn: ()=>ProjectUnavailableError, + Rg: ()=>_codex_remote_control_js__rspack_import_0.Rg, + XL: ()=>NewChatNavigationError, + c$: ()=>CdpHostRejectedError, + fw: ()=>ArchivedThreadError, + oN: ()=>ComposerDraftError, + sO: ()=>CdpUnreachableError + }); + }, + "./src/core/chatgpt-desktop/facade.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var node_crypto__rspack_import_0 = __webpack_require__("node:crypto"); + var _cdp_adapter_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/cdp-adapter.ts"); + var _app_server_fallback_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/app-server-fallback.ts"); + var _errors_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _loopback_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + var _remote_threads_js__rspack_import_5 = __webpack_require__("./src/core/chatgpt-desktop/remote-threads.ts"); + var _project_state_js__rspack_import_6 = __webpack_require__("./src/core/chatgpt-desktop/project-state.ts"); + var _payload_budget_js__rspack_import_8 = __webpack_require__("./src/core/chatgpt-desktop/payload-budget.ts"); + var _thread_ids_js__rspack_import_7 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const defaultFallbacks = { + listThreads: _app_server_fallback_js__rspack_import_2.OI, + searchThreads: _app_server_fallback_js__rspack_import_2.aI, + readThread: _app_server_fallback_js__rspack_import_2.Jf, + unarchiveThread: _app_server_fallback_js__rspack_import_2.qL, + statusProbe: _app_server_fallback_js__rspack_import_2.dM, + listRemoteThreads: _remote_threads_js__rspack_import_5.Hx, + listHosts: _remote_threads_js__rspack_import_5.Kl, + discoverModelProviders: _app_server_fallback_js__rspack_import_2.X + }; + const MERGED_LIST_CURSOR = 'desktop-merged-list-v1:'; + const MERGED_SEARCH_CURSOR = 'desktop-merged-search-v1:'; + class ChatGptDesktopFacade { + #cdp; + #port; + #cdpConnected = false; + #fallbacks; + #listSnapshots = new Map(); + constructor(cdp = new _cdp_adapter_js__rspack_import_1.a(), port = (0, _loopback_js__rspack_import_4.L0)(), fallbacks = defaultFallbacks){ + this.#cdp = cdp; + this.#port = port; + this.#fallbacks = fallbacks; + } + async connect({ port }) { + this.#port = (0, _loopback_js__rspack_import_4.L0)(process.env, port); + try { + await this.#cdp.connect({ + port: this.#port + }); + this.#cdpConnected = true; + } catch (error) { + this.#cdpConnected = false; + throw error; + } + } + async listTargets() { + await this.#ensureCdp(); + return this.#cdp.listTargets(); + } + async listThreads(options = {}) { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null + }); + let snapshot; + let offset = 0; + if (options.cursor) { + const state = (0, _payload_budget_js__rspack_import_8.i3)(MERGED_LIST_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop list cursor does not match the port or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildListSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop thread inventory changed; restart listing without a cursor'); + } + } else { + snapshot = await this.#buildListSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_LIST_CURSOR); + } + #pageSnapshot(snapshot, offset, limit, options, prefix, query) { + if (offset > snapshot.rows.length) throw new Error('ChatGPT Desktop cursor is out of range'); + const host = normalizeHostFilter(options.host); + const rows = []; + const base = { + backend: snapshot.backend, + limit, + host, + ...query !== undefined ? { + query + } : {}, + ...options.modelProvider?.trim() ? { + modelProvider: options.modelProvider.trim() + } : {}, + ...options.project ? { + project: options.project + } : {}, + ...options.groupBy === 'host' ? { + groupBy: 'host' + } : {} + }; + for (const source of snapshot.rows.slice(offset, offset + limit)){ + let row = source; + const candidate = [ + ...rows, + row + ]; + const probe = { + ...base, + threads: candidate, + nextCursor: prefix, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(candidate) + } : {}, + warnings: snapshot.warnings, + exitCode: 0 + }; + if ((0, _payload_budget_js__rspack_import_8.sQ)(probe) > 614400) { + if (rows.length) break; + row = (0, _payload_budget_js__rspack_import_8.MH)(row); + const minimal = [ + row + ]; + if ((0, _payload_budget_js__rspack_import_8.sQ)({ + ...base, + threads: minimal, + nextCursor: prefix, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(minimal) + } : {}, + warnings: snapshot.warnings, + exitCode: 0 + }) > 614400) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + rows.push(row); + } + const nextOffset = offset + rows.length; + const nextCursor = nextOffset < snapshot.rows.length ? (0, _payload_budget_js__rspack_import_8.oI)(prefix, { + fingerprint: snapshot.fingerprint, + offset: nextOffset, + port: this.#port, + filters: snapshot.filters + }) : null; + const warnings = [ + ...snapshot.warnings + ]; + if (rows.length < Math.min(limit, snapshot.rows.length - offset)) { + warnings.push(`${query === undefined ? 'List' : 'Search'} page stopped at the MCP byte budget; continue with nextCursor`); + } + return { + ...base, + threads: rows, + nextCursor, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(rows) + } : {}, + ...warnings.length ? { + warnings + } : {} + }; + } + async #buildListSnapshot(options, filters) { + const appRows = []; + const warnings = []; + let appCursor; + let appComplete = false; + const seenCursors = new Set(); + const modelProviders = modelProvidersFromFilter(options.modelProvider); + let appSucceeded = false; + try { + for(let page = 0; page < 400; page++){ + const out = await this.#fallbacks.listThreads({ + limit: 200, + cursor: appCursor, + modelProviders + }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) { + appComplete = true; + break; + } + if (seenCursors.has(out.nextCursor)) throw new Error('App-server list returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + } + if (!appComplete) warnings.push('App-server inventory stopped after 400 pages'); + } catch (error) { + warnings.push(`App-server list failed: ${error instanceof Error ? error.message : String(error)}`); + } + const remotes = this.#listRemotes(); + let cdpList = null; + try { + await this.#ensureCdp(); + cdpList = await this.#cdp.listThreads({ + limit: 200 + }); + } catch (error) { + if (isCdpFailure(error)) this.#cdpConnected = false; + else warnings.push(`CDP list failed: ${error instanceof Error ? error.message : String(error)}`); + } + if (!appRows.length && !cdpList && !remotes.length) { + throw new _errors_js__rspack_import_3.sO(warnings[0] ?? 'ChatGPT Desktop listThreads: CDP and app-server unreachable'); + } + const appList = { + backend: 'app-server', + limit: appRows.length, + threads: appRows + }; + const base = appSucceeded ? cdpList ? mergeThreadLists(appList, cdpList) : appList : cdpList ?? { + backend: 'remote-state', + limit: 0, + threads: [] + }; + const merged = finalizeThreadList({ + ...base, + threads: (0, _project_state_js__rspack_import_6.j)(base.threads) + }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes: (0, _project_state_js__rspack_import_6.j)(remotes) + }); + const rows = [ + ...new Map(merged.threads.map((row)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(row.threadId) ?? row.threadId, + row + ])).values() + ]; + rows.sort(compareDesktopThreads); + const fingerprint = (0, node_crypto__rspack_import_0.createHash)('sha256').update(JSON.stringify({ + filters, + port: this.#port, + rows: rows.map((row)=>[ + row.threadId, + row.updatedAt, + row.location, + row.hostId + ]) + })).digest('base64url'); + const snapshot = { + fingerprint, + port: this.#port, + filters, + createdAt: Date.now(), + rows, + backend: merged.backend, + warnings + }; + for (const [key, value] of this.#listSnapshots){ + if (Date.now() - value.createdAt > 15 * 60000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + async listHosts() { + const remotes = this.#listRemotes(); + const cdpList = typeof this.#cdp.listThreads === 'function' ? await this.#tryCdpList({ + limit: 200 + }) : null; + const cdpRows = cdpList?.threads ?? []; + let localThreadCount = 0; + let appSucceeded = false; + try { + const local = await this.#fallbacks.listThreads({ + limit: 25, + modelProviders: [] + }); + appSucceeded = true; + localThreadCount = local.threads.length; + } catch { + localThreadCount = 0; + } + const listHosts = this.#fallbacks.listHosts ?? _remote_threads_js__rspack_import_5.Kl; + const fromState = listHosts(process.env, { + localThreadCount + }); + const byId = new Map(fromState.map((host)=>[ + host.hostId, + { + ...host + } + ])); + for (const thread of remotes){ + const existing = byId.get(thread.hostId); + if (existing) { + byId.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName + }); + } else { + byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 0 + }); + } + } + for (const thread of cdpRows){ + if (thread.location !== 'remote' || !thread.hostId) continue; + if (!byId.has(thread.hostId)) byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName ?? null, + location: 'remote', + threadCount: 0 + }); + } + const hostsSource = 'local+remote-thread-summaries-v3+managed-connections' + (cdpRows.length ? '+cdp' : ''); + const remoteCounts = new Map(); + const remoteInventory = finalizeThreadList({ + backend: cdpRows.length ? 'cdp' : 'remote-state', + limit: 10000, + threads: cdpRows.filter((thread)=>thread.location === 'remote') + }, { + limit: 10000, + remotes + }); + for (const thread of remoteInventory.threads){ + if (thread.hostId) remoteCounts.set(thread.hostId, (remoteCounts.get(thread.hostId) ?? 0) + 1); + } + const withCounts = [ + ...byId.values() + ].map((host)=>host.hostId === 'local' ? { + ...host, + threadCount: localThreadCount + } : { + ...host, + threadCount: remoteCounts.get(host.hostId) ?? host.threadCount + }).sort((a, b)=>{ + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + let modelProviders = []; + let modelProvidersSource = 'thread/list-distinct'; + const warnings = []; + try { + const discover = this.#fallbacks.discoverModelProviders ?? _app_server_fallback_js__rspack_import_2.X; + const discovered = await discover(); + modelProviders = [ + ...discovered.providers + ]; + modelProvidersSource = discovered.source; + if (typeof discovered.threadCount === 'number') localThreadCount = discovered.threadCount; + if (discovered.complete === false) warnings.push('Provider discovery stopped before the final app-server page'); + } catch (error) { + warnings.push(`Model provider discovery failed: ${error instanceof Error ? error.message : String(error)}`); + modelProviders = [ + ...new Set(remotes.map((thread)=>thread.modelProvider).filter((value)=>typeof value === 'string' && Boolean(value))) + ].sort(); + modelProvidersSource = 'remote-summaries-distinct'; + } + modelProviders = [ + ...new Set([ + ...modelProviders, + ...remotes.map((thread)=>thread.modelProvider).filter((value)=>Boolean(value)) + ]) + ].sort(); + let inventoryCounts = null; + try { + const filters = JSON.stringify({ + host: 'all', + modelProvider: null, + project: null, + groupBy: null + }); + const cached = [ + ...this.#listSnapshots.values() + ].find((snapshot)=>snapshot.port === this.#port && snapshot.filters === filters && Date.now() - snapshot.createdAt < 15 * 60000); + const inventory = cached ?? await this.#buildListSnapshot({}, filters); + inventoryCounts = new Map(); + for (const thread of inventory.rows){ + const hostId = thread.location === 'remote' ? thread.hostId ?? 'unknown-remote' : 'local'; + inventoryCounts.set(hostId, (inventoryCounts.get(hostId) ?? 0) + 1); + } + warnings.push(...inventory.warnings); + } catch (error) { + warnings.push(`Merged inventory count failed: ${error instanceof Error ? error.message : String(error)}`); + } + return { + hosts: withCounts.map((host)=>{ + const counted = { + ...host, + threadCount: inventoryCounts?.get(host.hostId) ?? (host.hostId === 'local' ? localThreadCount : host.threadCount) + }; + if (!counted.hostName) return counted; + const name = (0, _payload_budget_js__rspack_import_8.aA)(counted.hostName); + return { + ...counted, + hostName: name.title, + ...name.titleTruncated ? { + hostNameTruncated: true + } : {} + }; + }), + hostsSource, + modelProviders, + modelProvidersSource, + backend: appSucceeded ? cdpRows.length ? remotes.length ? 'app-server+cdp+remote-state' : 'app-server+cdp' : remotes.length ? 'app-server+remote-state' : 'app-server' : cdpRows.length ? remotes.length ? 'cdp+remote-state' : 'cdp' : 'remote-state', + ...warnings.length ? { + warnings + } : {} + }; + } + async searchThreads(options) { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + query: options.query, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null + }); + let snapshot; + let offset = 0; + if (options.cursor) { + const state = (0, _payload_budget_js__rspack_import_8.i3)(MERGED_SEARCH_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop search cursor does not match the port, query or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildSearchSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop search results changed; restart without a cursor'); + } + } else { + snapshot = await this.#buildSearchSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_SEARCH_CURSOR, options.query); + } + async #buildSearchSnapshot(options, filters) { + const modelProviders = modelProvidersFromFilter(options.modelProvider); + const appRows = []; + const warnings = []; + const seenCursors = new Set(); + let appCursor; + let appSucceeded = false; + let appError; + try { + for(let page = 0; page < 400; page++){ + const out = await this.#fallbacks.searchThreads({ + query: options.query, + limit: 200, + cursor: appCursor, + modelProviders + }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) break; + if (seenCursors.has(out.nextCursor)) throw new Error('App-server search returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + if (page === 399) warnings.push('App-server search stopped after 400 pages'); + } + } catch (error) { + appError = error; + warnings.push(`App-server search failed: ${error instanceof Error ? error.message : String(error)}`); + } + const cdpList = await this.#tryCdpList({ + limit: 200 + }); + const remotes = (0, _project_state_js__rspack_import_6.j)(this.#listRemotes()); + if (!appSucceeded && !cdpList && !remotes.length) throw appError; + const appList = { + backend: 'app-server', + limit: appRows.length, + threads: appRows + }; + const base = appSucceeded ? cdpList ? mergeThreadLists(appList, cdpList) : appList : cdpList ?? { + backend: 'remote-state', + limit: 0, + threads: [] + }; + const appMatches = new Set(appRows.map((thread)=>(0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId)); + const merged = finalizeThreadList({ + ...base, + threads: (0, _project_state_js__rspack_import_6.j)(base.threads) + }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes + }); + const needle = options.query.trim().toLowerCase(); + const rows = [ + ...new Map(merged.threads.filter((thread)=>appMatches.has((0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId) || threadMatchesQuery(thread, needle)).map((thread)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId, + thread + ])).values() + ]; + rows.sort(compareDesktopThreads); + const fingerprint = (0, node_crypto__rspack_import_0.createHash)('sha256').update(JSON.stringify({ + filters, + port: this.#port, + rows: rows.map((row)=>[ + row.threadId, + row.updatedAt, + row.location, + row.hostId + ]) + })).digest('base64url'); + const snapshot = { + fingerprint, + port: this.#port, + filters, + createdAt: Date.now(), + rows, + backend: merged.backend, + warnings + }; + for (const [key, value] of this.#listSnapshots){ + if (Date.now() - value.createdAt > 15 * 60000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + async readThread(options) { + const limit = options.limit ?? (options.full ? 2000 : 100); + const full = options.full ?? false; + if ((0, _thread_ids_js__rspack_import_7.vr)(options.threadId)) { + if (options.cursor) throw new Error('Temporary Desktop threads do not support read cursors'); + await this.#ensureCdp(); + return this.#cdp.readThread({ + ...options, + limit, + full + }); + } + try { + return await this.#fallbacks.readThread({ + threadId: options.threadId, + limit, + full, + cursor: options.cursor + }); + } catch (appError) { + if (appError instanceof _errors_js__rspack_import_3.Rg) throw appError; + if (options.cursor) throw appError; + const cdpConnected = await this.#tryEnsureCdp(); + if (!cdpConnected) throw appError; + const fallback = await this.#cdp.readThread({ + ...options, + limit, + full + }); + return { + ...fallback, + complete: false, + warnings: [ + `App-server read failed: ${appError instanceof Error ? appError.message : String(appError)}` + ] + }; + } + } + async sendMessage(options) { + if (options.threadId) { + const remote = (0, _remote_threads_js__rspack_import_5.KI)(options.threadId); + if (remote) throw new _errors_js__rspack_import_3.Rg(options.threadId, remote.hostId, remote.hostName); + } + await this.#ensureCdp(); + try { + return await this.#cdp.sendMessage(options); + } catch (error) { + if (!(error instanceof _errors_js__rspack_import_3.fw) || !options.unarchive || !options.threadId) throw error; + await (this.#fallbacks.unarchiveThread ?? _app_server_fallback_js__rspack_import_2.qL)(options.threadId); + return this.#cdp.sendMessage({ + ...options, + unarchive: false + }); + } + } + async waitForReply(options) { + await this.#ensureCdp(); + return this.#cdp.waitForReply(options); + } + async openThread(threadId, options) { + const remote = (0, _remote_threads_js__rspack_import_5.KI)(threadId); + if (remote) throw new _errors_js__rspack_import_3.Rg(threadId, remote.hostId, remote.hostName); + await this.#ensureCdp(); + return this.#cdp.openThread(threadId, options); + } + async status() { + const cdpStatus = await this.#cdp.status(); + let appServerFallback; + try { + appServerFallback = await this.#fallbacks.statusProbe(); + } catch { + appServerFallback = { + reachable: false + }; + } + const reachable = cdpStatus.reachable; + return { + ...cdpStatus, + port: this.#port, + appServerFallback, + reachable, + message: cdpStatus.reachable ? cdpStatus.message : cdpStatus.message, + exitCode: reachable ? 0 : 1 + }; + } + async close() { + this.#cdpConnected = false; + await this.#cdp.close(); + } + #listRemotes() { + const list = this.#fallbacks.listRemoteThreads ?? _remote_threads_js__rspack_import_5.Hx; + try { + return list(); + } catch { + return []; + } + } + async #ensureCdp() { + if (this.#cdpConnected) return; + await this.#cdp.connect({ + port: this.#port + }); + this.#cdpConnected = true; + } + async #tryEnsureCdp() { + try { + await this.#ensureCdp(); + return true; + } catch (error) { + if (!isCdpFailure(error)) throw error; + this.#cdpConnected = false; + return false; + } + } + async #tryCdpList(options) { + try { + await this.#ensureCdp(); + return await this.#cdp.listThreads(options); + } catch (error) { + if (isCdpFailure(error) || error instanceof _errors_js__rspack_import_3.EH) { + if (isCdpFailure(error)) this.#cdpConnected = false; + return null; + } + throw error; + } + } + } + function mergeThreadLists(appList, cdpList) { + const cdpByKey = new Map(); + for (const thread of cdpList.threads){ + const key = (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId; + cdpByKey.set(key, thread); + } + const merged = appList.threads.map((thread)=>{ + const key = (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId; + const cdp = cdpByKey.get(key); + const withLocal = { + ...thread, + location: thread.location ?? 'local', + hostId: thread.hostId ?? null, + hostName: thread.hostName ?? null + }; + if (!cdp) return withLocal; + return { + ...withLocal, + title: withLocal.title || cdp.title, + pinned: withLocal.pinned || cdp.pinned, + selected: cdp.selected, + kind: cdp.kind || withLocal.kind, + project: cdp.project ?? withLocal.project, + location: cdp.location === 'remote' ? 'remote' : withLocal.location, + hostId: cdp.location === 'remote' ? cdp.hostId ?? null : withLocal.hostId, + hostName: cdp.location === 'remote' ? cdp.hostName ?? null : withLocal.hostName + }; + }); + for (const thread of cdpList.threads){ + if (merged.some((row)=>(0, _thread_ids_js__rspack_import_7.LV)(row.threadId, thread.threadId))) continue; + merged.push(thread); + } + return { + backend: 'app-server+cdp', + limit: appList.limit, + nextCursor: appList.nextCursor, + ...appList.query !== undefined ? { + query: appList.query + } : {}, + threads: merged + }; + } + function finalizeThreadList(base, { limit, host = 'all', modelProvider, project, groupBy, remotes }) { + const remoteById = new Map(remotes.map((thread)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId, + thread + ])); + const locals = base.threads.map((thread)=>{ + const remote = remoteById.get((0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId); + if (remote) { + return { + ...thread, + ...remote, + selected: thread.selected, + pinned: thread.pinned || remote.pinned + }; + } + const location = thread.location ?? 'local'; + return { + ...thread, + location, + hostId: location === 'remote' ? thread.hostId ?? null : null, + hostName: location === 'remote' ? thread.hostName ?? null : null + }; + }); + const localKeys = new Set(locals.map((thread)=>(0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId)); + const merged = [ + ...locals + ]; + for (const remote of remotes){ + const key = (0, _thread_ids_js__rspack_import_7.nn)(remote.threadId) ?? remote.threadId; + if (localKeys.has(key)) continue; + merged.push({ + ...remote, + location: remote.location ?? 'remote', + hostId: remote.hostId ?? null, + hostName: remote.hostName ?? null + }); + localKeys.add(key); + } + if (remotes.length) merged.sort((a, b)=>Number(b.location === 'remote') - Number(a.location === 'remote')); + const filtered = merged.filter((thread)=>{ + if (!hostMatches(thread, host)) return false; + if (!modelProviderMatches(thread, modelProvider)) return false; + if (!projectMatches(thread, project)) return false; + return true; + }); + const limited = filtered.slice(0, limit).map(_payload_budget_js__rspack_import_8.eX); + const result = { + backend: remotes.length ? base.backend === 'app-server+cdp' ? 'app-server+cdp+remote-state' : base.backend === 'app-server' ? 'app-server+remote-state' : base.backend === 'cdp' ? 'cdp+remote-state' : base.backend : base.backend, + limit, + threads: limited, + ...base.nextCursor !== undefined ? { + nextCursor: base.nextCursor + } : {}, + ...base.query !== undefined ? { + query: base.query + } : {}, + host, + ...modelProvider ? { + modelProvider + } : {}, + ...project ? { + project + } : {} + }; + if (groupBy === 'host') { + return { + ...result, + groupBy: 'host', + groups: groupThreadsByHost(limited) + }; + } + return result; + } + function compareDesktopThreads(a, b) { + const timestamp = (row)=>{ + const value = row.updatedAt ?? row.createdAt; + if (typeof value !== 'number' || !Number.isFinite(value)) return Number.NEGATIVE_INFINITY; + return value < 100000000000 ? value * 1000 : value; + }; + return timestamp(b) - timestamp(a) || a.threadId.localeCompare(b.threadId); + } + function groupThreadsByHost(threads) { + const groups = new Map(); + for (const thread of threads){ + const location = thread.location ?? 'local'; + const hostId = location === 'remote' ? thread.hostId || 'unknown-remote' : 'local'; + const key = `${location}:${hostId}`; + const existing = groups.get(key); + if (existing) { + existing.threads.push(thread); + continue; + } + groups.set(key, { + hostId, + hostName: thread.hostName ?? (location === 'local' ? 'local' : null), + location, + threads: [ + thread + ] + }); + } + return [ + ...groups.values() + ]; + } + function normalizeHostFilter(host) { + const value = (host ?? 'all').trim(); + return value || 'all'; + } + function modelProvidersFromFilter(modelProvider) { + if (!modelProvider || !modelProvider.trim()) return []; + return [ + modelProvider.trim() + ]; + } + function hostMatches(thread, host) { + const needle = host.trim().toLowerCase(); + if (!needle || needle === 'all') return true; + if (needle === 'local') return (thread.location ?? 'local') === 'local'; + return [ + thread.hostId, + thread.hostName + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function modelProviderMatches(thread, modelProvider) { + if (!modelProvider) return true; + return thread.modelProvider === modelProvider; + } + function projectMatches(thread, project) { + if (!project) return true; + const needle = project.trim().toLowerCase(); + return [ + thread.project, + thread.projectId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function threadMatchesQuery(thread, needle) { + if (!needle) return true; + return [ + thread.title, + thread.threadId, + thread.project, + thread.preview, + thread.hostName, + thread.hostId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function isCdpFailure(error) { + if (error instanceof _errors_js__rspack_import_3.sO) return true; + if (error instanceof _errors_js__rspack_import_3.EH) return false; + if (error && typeof error === 'object' && 'code' in error) { + const code = String(error.code); + return code === 'CDP_UNREACHABLE' || code === 'CDP_HOST_REJECTED'; + } + return false; + } + let activeAdapter = null; + function getChatGptDesktopAdapter() { + if (!activeAdapter) activeAdapter = new ChatGptDesktopFacade(); + return activeAdapter; + } + function setChatGptDesktopAdapterForTests(adapter) { + activeAdapter = adapter; + } + __webpack_require__.d(__webpack_exports__, { + PP: ()=>getChatGptDesktopAdapter + }); + }, + "./src/core/chatgpt-desktop/loopback.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + const LOOPBACK = new Set([ + '127.0.0.1', + 'localhost', + '::1' + ]); + const DEFAULT_CDP_PORT = 9222; + const CDP_LOOPBACK_HOST = '127.0.0.1'; + function isLoopbackHostname(hostname) { + const host = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, ''); + return LOOPBACK.has(host); + } + function assertLoopbackHostname(hostname) { + if (!isLoopbackHostname(hostname)) { + throw new _errors_js__rspack_import_0.c$(hostname); + } + } + function cdpHttpBase(port) { + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return `http://${CDP_LOOPBACK_HOST}:${port}`; + } + function forceLoopbackWebSocketUrl(raw) { + let url; + try { + url = new URL(raw); + } catch { + throw new _errors_js__rspack_import_0.c$(String(raw)); + } + assertLoopbackHostname(url.hostname); + if (url.username || url.password || url.hash) throw new _errors_js__rspack_import_0.c$(raw); + if (url.protocol !== 'ws:' && url.protocol !== 'wss:') { + throw new Error(`Expected ws(s) debugger URL, got ${url.protocol}`); + } + url.hostname = CDP_LOOPBACK_HOST; + url.protocol = 'ws:'; + return url.toString(); + } + function resolveCdpPort(env = process.env, explicit) { + if (explicit !== undefined) { + if (!Number.isInteger(explicit) || explicit < 1 || explicit > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return explicit; + } + const raw = (env.CHATGPT_DESKTOP_CDP_PORT || '').trim(); + if (!raw) return DEFAULT_CDP_PORT; + const port = /^\d+$/.test(raw) ? Number(raw) : NaN; + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CHATGPT_DESKTOP_CDP_PORT must be an integer 1-65535'); + } + return port; + } + __webpack_require__.d(__webpack_exports__, { + L0: ()=>resolveCdpPort, + NJ: ()=>cdpHttpBase, + jv: ()=>forceLoopbackWebSocketUrl + }); + }, + "./src/core/chatgpt-desktop/payload-budget.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + const DESKTOP_RESULT_BUDGET_BYTES = 600 * 1024; + const DESKTOP_THREAD_TITLE_CHARS = 200; + function serializedBytes(value) { + return Buffer.byteLength(JSON.stringify(value), 'utf8'); + } + function compact(value, maxChars) { + const normalized = value.replace(/\s+/gu, ' ').trim(); + const chars = Array.from(normalized); + return { + value: chars.slice(0, maxChars).join(''), + clipped: chars.length > maxChars + }; + } + function compactThreadRow(thread) { + const title = compact(thread.title, DESKTOP_THREAD_TITLE_CHARS); + const preview = typeof thread.preview === 'string' ? compact(thread.preview, 200) : null; + const cwd = typeof thread.cwd === 'string' ? compact(thread.cwd, 2048) : null; + const project = typeof thread.project === 'string' ? compact(thread.project, 256) : null; + const hostName = typeof thread.hostName === 'string' ? compact(thread.hostName, 256) : null; + return { + ...thread, + title: title.value, + ...title.clipped ? { + titleTruncated: true + } : {}, + ...preview ? { + preview: preview.value, + ...preview.clipped ? { + previewTruncated: true + } : {} + } : {}, + ...cwd ? { + cwd: cwd.value, + ...cwd.clipped ? { + cwdTruncated: true + } : {} + } : {}, + ...project ? { + project: project.value, + ...project.clipped ? { + projectTruncated: true + } : {} + } : {}, + ...hostName ? { + hostName: hostName.value, + ...hostName.clipped ? { + hostNameTruncated: true + } : {} + } : {} + }; + } + function compactThreadTitle(title) { + const out = compact(title, DESKTOP_THREAD_TITLE_CHARS); + return { + title: out.value, + titleTruncated: out.clipped + }; + } + function minimalThreadRow(thread) { + return { + threadId: thread.threadId, + title: compact(thread.title, DESKTOP_THREAD_TITLE_CHARS).value, + titleTruncated: true, + pinned: thread.pinned, + selected: thread.selected, + kind: thread.kind, + location: thread.location, + hostId: thread.hostId, + hostName: thread.hostName, + modelProvider: thread.modelProvider, + metadataTruncated: true + }; + } + function encodePageCursor(prefix, value) { + return prefix + Buffer.from(JSON.stringify(value)).toString('base64url'); + } + function decodePageCursor(prefix, cursor) { + if (!cursor.startsWith(prefix)) throw new Error('Invalid ChatGPT Desktop page cursor'); + try { + return JSON.parse(Buffer.from(cursor.slice(prefix.length), 'base64url').toString('utf8')); + } catch { + throw new Error('Invalid ChatGPT Desktop page cursor'); + } + } + __webpack_require__.d(__webpack_exports__, { + MH: ()=>minimalThreadRow, + aA: ()=>compactThreadTitle, + eX: ()=>compactThreadRow, + i3: ()=>decodePageCursor, + oI: ()=>encodePageCursor, + sQ: ()=>serializedBytes + }); + }, + "./src/core/chatgpt-desktop/project-state.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_ids_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + function findLocalProject(nameOrId, env = process.env) { + const state = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!state) return null; + for (const value of Object.values(record(stateValue(state, 'local-projects')) ?? {})){ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') continue; + if (row.id !== nameOrId && row.name.toLowerCase() !== nameOrId.toLowerCase()) continue; + return { + id: row.id, + label: row.name, + rootPaths: (Array.isArray(row.rootPaths) ? row.rootPaths : []).filter((path)=>typeof path === 'string' && Boolean(path)) + }; + } + return null; + } + function record(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : null; + } + function stateValue(state, key) { + for (const source of (0, _codex_remote_control_js__rspack_import_0.xr)(state))if (key in source) return source[key]; + return undefined; + } + function pathWithin(path, root) { + const normalized = root.replace(/\/+$/u, ''); + return path === normalized || path.startsWith(`${normalized}/`); + } + function resolveThreadProjects(threads, env = process.env) { + const state = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!state) return [ + ...threads + ]; + const localProjects = Object.values(record(stateValue(state, 'local-projects')) ?? {}).flatMap((value)=>{ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') return []; + return (Array.isArray(row.rootPaths) ? row.rootPaths : []).filter((path)=>typeof path === 'string' && Boolean(path)).map((rootPath)=>({ + id: row.id, + label: row.name, + rootPath + })); + }); + const remoteProjects = (Array.isArray(stateValue(state, 'remote-projects')) ? stateValue(state, 'remote-projects') : []).flatMap((value)=>{ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.label !== 'string' || typeof row.remotePath !== 'string' || typeof row.hostId !== 'string') return []; + return [ + { + id: row.id, + label: row.label, + rootPath: row.remotePath, + hostId: row.hostId + } + ]; + }); + const assignments = record(stateValue(state, 'thread-project-assignments')) ?? {}; + const hints = record(stateValue(state, 'thread-workspace-root-hints')) ?? {}; + const projectless = new Set(Array.isArray(stateValue(state, 'projectless-thread-ids')) ? stateValue(state, 'projectless-thread-ids') : []); + return threads.map((thread)=>{ + const bare = (0, _thread_ids_js__rspack_import_1.nn)(thread.threadId) ?? thread.threadId; + const assignment = record(assignments[bare]); + const remote = thread.location === 'remote'; + const projects = remote ? remoteProjects.filter((project)=>project.hostId === thread.hostId) : localProjects; + let project = assignment && typeof assignment.projectId === 'string' ? projects.find((entry)=>entry.id === assignment.projectId) : undefined; + if (!project && !projectless.has(bare) && thread.projectId) { + project = projects.find((entry)=>entry.id === thread.projectId); + } + if (!project && !assignment && !projectless.has(bare)) { + const path = typeof hints[bare] === 'string' ? hints[bare] : typeof thread.cwd === 'string' ? thread.cwd : null; + if (path) project = projects.filter((entry)=>pathWithin(path, entry.rootPath)).sort((a, b)=>b.rootPath.length - a.rootPath.length)[0]; + } + if (project) return { + ...thread, + project: project.label, + projectId: project.id, + projectRootPath: project.rootPath + }; + if (projectless.has(bare)) return { + ...thread, + project: null, + projectId: null + }; + if (remote && typeof thread.project === 'string' && thread.project.startsWith('/')) { + return { + ...thread, + project: null + }; + } + return thread; + }); + } + __webpack_require__.d(__webpack_exports__, { + j: ()=>resolveThreadProjects, + q: ()=>findLocalProject + }); + }, + "./src/core/chatgpt-desktop/remote-threads.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_ids_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + function listRemoteThreadsFromState(env = process.env) { + const data = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!data) return []; + const hostNames = collectHostNames(data); + const out = []; + const seen = new Set(); + for (const [key, value] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value); + const pinnedIds = extractPinnedIds(value); + for (const entry of extractThreadEntries(value)){ + const mapped = mapRemoteEntry(entry, { + hostId, + hostName, + pinnedIds + }); + if (!mapped) continue; + const bare = (0, _thread_ids_js__rspack_import_1.nn)(mapped.threadId) ?? mapped.threadId; + const dedupe = `${hostId}:${bare}`; + if (seen.has(dedupe)) continue; + seen.add(dedupe); + out.push(mapped); + } + } + return out; + } + function findRemoteThread(threadId, env = process.env) { + const bare = (0, _thread_ids_js__rspack_import_1.nn)(threadId) ?? threadId; + if (!bare) return null; + for (const thread of listRemoteThreadsFromState(env)){ + const id = (0, _thread_ids_js__rspack_import_1.nn)(thread.threadId) ?? thread.threadId; + if (id === bare) { + return { + hostId: thread.hostId, + hostName: thread.hostName, + thread + }; + } + } + return null; + } + function findRemoteThreadHostId(threadId, env = process.env) { + return findRemoteThread(threadId, env)?.hostId ?? null; + } + function listDiscoveredHosts(env = process.env, { localThreadCount = 0 } = {}) { + const remotes = listRemoteThreadsFromState(env); + const byHost = new Map(); + byHost.set('local', { + hostId: 'local', + hostName: 'local', + location: 'local', + threadCount: localThreadCount + }); + const data = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (data) { + const hostNames = collectHostNames(data); + for (const [key, summary] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(summary) ?? null; + if (byHost.has(hostId)) continue; + byHost.set(hostId, { + hostId, + hostName, + location: 'remote', + threadCount: 0 + }); + } + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections){ + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue; + const hostId = stringField(entry, [ + 'hostId' + ]); + if (hostId && !byHost.has(hostId)) byHost.set(hostId, { + hostId, + hostName: stringField(entry, [ + 'displayName', + 'alias' + ]), + location: 'remote', + threadCount: 0 + }); + } + } + for (const thread of remotes){ + const existing = byHost.get(thread.hostId); + if (existing) { + byHost.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName, + threadCount: existing.threadCount + 1 + }); + } else { + byHost.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 1 + }); + } + } + return [ + ...byHost.values() + ].sort((a, b)=>{ + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + } + function collectHostNames(data) { + const names = new Map(); + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections){ + const pair = hostNameFromEntry(entry); + if (pair) names.set(pair.hostId, pair.hostName); + } + for (const [key, value] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry; + const hostId = stringField(row, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(row, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractPinnedIds(value) { + const pinned = new Set(); + if (!value || typeof value !== 'object') return pinned; + const record = value; + for (const key of [ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'pinned_ids' + ]){ + const arr = record[key]; + if (!Array.isArray(arr)) continue; + for (const item of arr){ + if (typeof item === 'string' && item) pinned.add(item); + else if (item && typeof item === 'object') { + const id = stringField(item, [ + 'id', + 'threadId', + 'thread_id' + ]); + if (id) pinned.add(id); + } + } + } + return pinned; + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) { + continue; + } + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const row = entry; + if (!stringField(row, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ])) { + entries.push({ + ...row, + id: key + }); + } else { + entries.push(entry); + } + } + } + return entries; + } + function mapRemoteEntry(entry, { hostId, hostName, pinnedIds }) { + if (typeof entry === 'string' && entry) { + return { + threadId: (0, _thread_ids_js__rspack_import_1.BS)(entry), + title: entry, + pinned: pinnedIds.has(entry), + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName + }; + } + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry; + const id = stringField(row, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + if (!id) return null; + const title = stringField(row, [ + 'title', + 'name', + 'preview', + 'label' + ]) || id; + const projectId = stringField(row, [ + 'projectId', + 'project_id' + ]); + const project = stringField(row, [ + 'project', + 'projectName', + 'project_name', + 'cwd' + ]) || projectId || undefined; + const preview = stringField(row, [ + 'preview', + 'snippet' + ]); + const cwd = stringField(row, [ + 'cwd', + 'workingDirectory', + 'working_directory' + ]); + const modelProvider = stringField(row, [ + 'modelProvider', + 'model_provider', + 'provider' + ]); + const model = stringField(row, [ + 'model' + ]); + const originator = stringField(row, [ + 'originator', + 'source' + ]); + const createdAt = numberField(row, [ + 'createdAt', + 'created_at' + ]); + const updatedAt = numberField(row, [ + 'updatedAt', + 'updated_at' + ]); + const sectionName = stringField(row, [ + 'sectionName', + 'section' + ]); + const sectionId = stringField(row, [ + 'sectionId', + 'section_id' + ]) || sectionName; + const pinned = pinnedIds.has(id) || row.pinned === true || sectionId === 'Pinned' || sectionName === 'Pinned'; + return { + threadId: (0, _thread_ids_js__rspack_import_1.BS)(id), + title, + pinned, + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName, + ...project ? { + project + } : {}, + ...projectId ? { + projectId + } : {}, + ...preview ? { + preview + } : {}, + ...cwd ? { + cwd + } : { + cwd: null + }, + createdAt: createdAt ?? null, + updatedAt: updatedAt ?? null, + ...sectionId ? { + section: { + id: sectionId, + name: sectionName ?? sectionId + } + } : { + section: null + }, + ...modelProvider ? { + modelProvider + } : { + modelProvider: null + }, + ...model ? { + model + } : { + model: null + }, + ...originator ? { + originator + } : { + originator: null + } + }; + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + const record = row; + for (const key of keys){ + const value = record[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function numberField(row, keys) { + for (const key of keys){ + const value = row[key]; + if (typeof value === 'number' && Number.isFinite(value)) return value; + } + return null; + } + __webpack_require__.d(__webpack_exports__, { + Hx: ()=>listRemoteThreadsFromState, + KI: ()=>findRemoteThread, + Kl: ()=>listDiscoveredHosts, + XI: ()=>findRemoteThreadHostId + }); + }, + "./src/core/chatgpt-desktop/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _codex_contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _facade_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/facade.ts"); + var _loopback_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + const threadId = zod__rspack_import_4.YjP().min(1).max(256); + const statusSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional() + }).strict(); + const listThreadsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + host: zod__rspack_import_4.YjP().min(1).max(256).optional(), + modelProvider: zod__rspack_import_4.YjP().min(1).max(256).optional(), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + groupBy: zod__rspack_import_4.euz('host').optional() + }).strict(); + const searchThreadsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + query: zod__rspack_import_4.YjP().min(1).max(512), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + host: zod__rspack_import_4.YjP().min(1).max(256).optional(), + modelProvider: zod__rspack_import_4.YjP().min(1).max(256).optional(), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + groupBy: zod__rspack_import_4.euz('host').optional() + }).strict(); + const listHostsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional() + }).strict(); + const readThreadSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId, + limit: zod__rspack_import_4.aig().int().min(1).max(2000).optional(), + full: zod__rspack_import_4.zMY().default(false), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict(); + const openThreadSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId, + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict(); + const sendSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + text: zod__rspack_import_4.YjP().trim().min(1).max(100000), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + unarchive: zod__rspack_import_4.zMY().default(false), + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict().refine((value)=>!value.unarchive || Boolean(value.threadId), { + message: 'unarchive requires an existing threadId', + path: [ + 'unarchive' + ] + }).refine((value)=>!value.threadId || !value.project, { + message: 'project is only for a new thread; omit threadId', + path: [ + 'project' + ] + }); + const waitReplySchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + timeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(120000) + }).strict(); + const resultSchema = zod__rspack_import_4.Ikc({ + exitCode: zod__rspack_import_4.KCZ([ + zod__rspack_import_4.euz(0), + zod__rspack_import_4.euz(1) + ]) + }).catchall(zod__rspack_import_4.Pq9()); + let operationQueue = Promise.resolve(); + async function withAdapter(port, run) { + const operation = operationQueue.then(async ()=>{ + const adapter = (0, _facade_js__rspack_import_2.PP)(); + const resolved = (0, _loopback_js__rspack_import_3.L0)(process.env, port); + try { + try { + await adapter.connect({ + port: resolved + }); + } catch {} + return await run(adapter); + } finally{ + await adapter.close(); + } + }); + operationQueue = operation.catch(()=>{}); + return operation; + } + function asResult(value) { + return resultSchema.parse(JSON.parse(JSON.stringify(value))); + } + function mapError(error) { + if (error instanceof _errors_js__rspack_import_1.EH) { + return asResult({ + delivery: 'rejected', + reason: 'not-implemented', + code: 'NOT_IMPLEMENTED', + error: error.message, + exitCode: 1 + }); + } + if (error instanceof _errors_js__rspack_import_1.Rg) { + return asResult({ + delivery: error.delivery, + reason: error.reason, + code: error.code, + error: error.message, + threadId: error.threadId, + hint: error.hint, + ...error.hostId ? { + hostId: error.hostId + } : {}, + ...error.hostName ? { + hostName: error.hostName + } : {}, + exitCode: 1 + }); + } + const base = (0, _codex_contract_js__rspack_import_0.DG)(error); + if (error && typeof error === 'object' && 'code' in error) { + return asResult({ + ...base, + code: String(error.code) + }); + } + return asResult(base); + } + async function statusOperation(input) { + try { + const status = await withAdapter(input.port, (adapter)=>adapter.status()); + return asResult(status); + } catch (error) { + return mapError(error); + } + } + async function listThreadsOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.listThreads({ + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function searchThreadsOperation(input) { + try { + const out = await withAdapter(input.port, async (adapter)=>{ + if (typeof adapter.searchThreads !== 'function') { + throw new _errors_js__rspack_import_1.EH('searchThreads', 'adapter does not implement search'); + } + return adapter.searchThreads({ + query: input.query, + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy + }); + }); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function listHostsOperation(input) { + try { + const out = await withAdapter(input.port, async (adapter)=>{ + if (typeof adapter.listHosts !== 'function') { + throw new _errors_js__rspack_import_1.EH('listHosts', 'adapter does not implement listHosts'); + } + return adapter.listHosts(); + }); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function readThreadOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.readThread({ + threadId: input.threadId, + limit: input.limit ?? (input.full ? 2000 : 100), + full: input.full, + cursor: input.cursor, + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function openThreadOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.openThread(input.threadId, { + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: 0 }); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { - value: selected, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { - children: `${text}\n\ncursor: ${delta.cursor}${delta.gapReset ? ' (gap reset)' : ''}` - }) + } catch (error) { + return mapError(error); + } + } + async function sendOperation(input) { + try { + input = sendSchema.parse(input); + const out = await withAdapter(input.port, (adapter)=>adapter.sendMessage({ + threadId: input.threadId, + text: input.text, + project: input.project, + unarchive: input.unarchive, + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: out.delivery === 'accepted' ? 0 : 1 }); + } catch (error) { + return mapError(error); } - (0, _core_history_js__rspack_import_1.QX)(out, { - dir: input.historyDir, - disabled: input.noHistory, - event: 'thread' - }); - return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Result, { - value: out, - children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_4.g.Text, { - children: (0, _core_format_js__rspack_import_5.oZ)(out, { - full: input.full - }) - }) - }); + } + async function waitReplyOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.waitForReply({ + threadId: input.threadId, + timeoutMs: input.timeoutMs + })); + return asResult({ + ...out, + exitCode: out.delivery === 'replied' ? 0 : 1 + }); + } catch (error) { + return mapError(error); + } + } + function resultText(result) { + if (result.error) return String(result.error); + if (result.reachable === true) { + const backend = result.appServerFallback && typeof result.appServerFallback === 'object' ? ` cdp + app-server probe` : ''; + return result.message ? String(result.message) : `ChatGPT Desktop CDP reachable on 127.0.0.1:${result.port}${backend}`; + } + if (result.reachable === false) return String(result.message ?? 'ChatGPT Desktop CDP unreachable'); + if (Array.isArray(result.hosts)) { + const providers = Array.isArray(result.modelProviders) ? ` providers=${result.modelProviders.length} (${String(result.modelProvidersSource ?? '')})` : ''; + return `${result.hosts.length} hosts via ${String(result.hostsSource ?? result.backend ?? 'unknown')}${providers}`; + } + if (Array.isArray(result.threads)) { + const via = result.query ? ` matching ${JSON.stringify(result.query)}` : ''; + const host = result.host && result.host !== 'all' ? ` host=${result.host}` : ''; + const provider = result.modelProvider ? ` provider=${result.modelProvider}` : ''; + return `${result.threads.length} threads${via}${host}${provider} via ${result.backend ?? 'unknown'}`; + } + if (Array.isArray(result.turns)) { + const mode = result.full ? 'full' : 'page'; + return `${result.turns.length} turns (${mode}) on ${result.threadId} via ${result.backend ?? 'unknown'}`; + } + if (result.delivery === 'accepted') { + return `Sent on ${result.threadId} via ${result.backend}` + (result.conversationId ? ` (conversation ${result.conversationId})` : ''); + } + if (result.delivery === 'replied') { + const id = result.conversationId ? ` [${result.conversationId}]` : ''; + return `${String(result.reply ?? 'replied')}${id}`; + } + if (result.delivery) return `delivery ${result.delivery}`; + return JSON.stringify(result); } __webpack_require__.d(__webpack_exports__, { - "default": ()=>thread + Bm: ()=>listHostsOperation, + D6: ()=>resultText, + UK: ()=>readThreadOperation, + UP: ()=>sendOperation, + Uj: ()=>statusOperation, + tJ: ()=>listThreadsOperation, + zg: ()=>searchThreadsOperation }, { - config: config, - inputSchema: inputSchema, - resultSchema: resultSchema + FD: resultSchema + }); + }, + "./src/core/chatgpt-desktop/thread-ids.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_thread_id_js__rspack_import_1 = __webpack_require__("./src/core/codex/thread-id.js"); + var _cdp_dom_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/cdp-dom.ts"); + const LOCAL_THREAD_ID_PREFIX = "local:"; + const TEMP_THREAD_ID_PREFIX = null && CDP_TEMP_PREFIX; + const HISTORY_CONTENT_TURN_PREFIX = 'history-content:turn:'; + function isTemporaryDesktopThreadId(threadId) { + return (0, _codex_thread_id_js__rspack_import_1.vr)(threadId) || threadId.startsWith(_codex_thread_id_js__rspack_import_1.hA); + } + function toAppServerThreadId(threadId) { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return null; + try { + return (0, _codex_thread_id_js__rspack_import_1.SX)(threadId); + } catch { + return null; + } + } + function requireAppServerThreadId(threadId) { + const bare = toAppServerThreadId(threadId); + if (!bare) { + throw new Error(`No app-server thread id mapping for ${JSON.stringify(threadId)} ` + `(temporary Desktop rows need CDP; strip a leading "local:" for durable ids)`); + } + return bare; + } + function toDesktopThreadId(threadId) { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return threadId; + if (threadId.startsWith(LOCAL_THREAD_ID_PREFIX)) return threadId; + return `${LOCAL_THREAD_ID_PREFIX}${threadId}`; + } + function threadIdsEquivalent(a, b) { + const left = toAppServerThreadId(a) ?? a; + const right = toAppServerThreadId(b) ?? b; + return left === right; + } + function toDomTurnKey(turnId) { + if (!turnId) return turnId; + if (turnId.startsWith(HISTORY_CONTENT_TURN_PREFIX)) return turnId; + return `${HISTORY_CONTENT_TURN_PREFIX}${turnId}`; + } + function appServerThreadIdCandidates(threadId) { + const bare = toAppServerThreadId(threadId); + return bare ? [ + bare + ] : []; + } + __webpack_require__.d(__webpack_exports__, { + BS: ()=>toDesktopThreadId, + LV: ()=>threadIdsEquivalent, + NM: ()=>requireAppServerThreadId, + Sr: ()=>toDomTurnKey, + nn: ()=>toAppServerThreadId, + vr: ()=>isTemporaryDesktopThreadId }); }, "./src/core/claude-routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { @@ -11038,10 +15262,23 @@ var __webpack_modules__ = { }, "./src/core/codex/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var zod__rspack_import_5 = __webpack_require__("./node_modules/zod/v4/core/core.js"); var _codex_bridge_js__rspack_import_0 = __webpack_require__("./src/core/codex-bridge.js"); var _contract_js__rspack_import_1 = __webpack_require__("./src/core/codex/contract.js"); var _conversation_js__rspack_import_2 = __webpack_require__("./src/core/codex/conversation.js"); - const id = zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9_.:-]{1,128}$/); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); + const bareId = zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9_.:-]{1,128}$/); + const id = zod__rspack_import_3.YjP().min(1).max(160).transform((value, ctx)=>{ + try { + return (0, _thread_id_js__rspack_import_4.SX)(value); + } catch (error) { + ctx.addIssue({ + code: 'custom', + message: error instanceof Error ? error.message : 'Invalid threadId' + }); + return zod__rspack_import_5.tm; + } + }).pipe(bareId); const observationFields = { expectedCwd: zod__rspack_import_3.YjP().min(1).optional(), threadId: id, @@ -11049,7 +15286,7 @@ var __webpack_modules__ = { }; const sendFields = { ...observationFields, - correlationId: id.optional(), + correlationId: bareId.optional(), envelope: zod__rspack_import_3.zMY().optional(), hop: zod__rspack_import_3.aig().int().min(0).optional(), model: zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/).optional(), @@ -11061,8 +15298,8 @@ var __webpack_modules__ = { 'high', 'xhigh' ]).optional(), - replyTo: id.optional(), - expectedTurnId: id.optional(), + replyTo: bareId.optional(), + expectedTurnId: bareId.optional(), whenBusy: zod__rspack_import_3.k5n([ 'reject', 'queue', @@ -11077,8 +15314,8 @@ var __webpack_modules__ = { }).strict(); const waitSchema = zod__rspack_import_3.Ikc({ ...observationFields, - turnId: id, - messageId: id.optional(), + turnId: bareId, + messageId: bareId.optional(), maxOutputBytes: zod__rspack_import_3.aig().int().min(1).max(4194304).optional() }).strict(); const watchSchema = zod__rspack_import_3.Ikc({ @@ -15408,11 +19645,12 @@ var __webpack_modules__ = { GT: ()=>$ZodAsyncError, cV: ()=>$ZodEncodeError, cr: ()=>globalConfig, + tm: ()=>NEVER, xI: ()=>$constructor }); var _util_js__rspack_import_0 = __webpack_require__("./node_modules/zod/v4/core/util.js"); var _a; - const NEVER = null && Object.freeze({ + const NEVER = Object.freeze({ status: "aborted" }); const _zodDesc = { @@ -21898,8 +26136,10 @@ var __webpack_modules__ = { var node_child_process__rspack_import_5 = __webpack_require__("node:child_process"); var _package_json__rspack_import_6 = __webpack_require__("./package.json"); var _codex_contract_js__rspack_import_7 = __webpack_require__("./src/core/codex/contract.js"); - var _desktop_shim_js__rspack_import_8 = __webpack_require__("./src/core/desktop-shim.js"); - var _user_machine_guidance_js__rspack_import_9 = __webpack_require__("./src/core/user-machine-guidance.js"); + var _codex_remote_control_js__rspack_import_8 = __webpack_require__("./src/core/codex/remote-control.js"); + var _codex_thread_id_js__rspack_import_11 = __webpack_require__("./src/core/codex/thread-id.js"); + var _desktop_shim_js__rspack_import_9 = __webpack_require__("./src/core/desktop-shim.js"); + var _user_machine_guidance_js__rspack_import_10 = __webpack_require__("./src/core/user-machine-guidance.js"); const PINNED_CODEX_VERSION = "0.158.0"; const UPSTREAM_DESKTOP_ISSUES = [ "https://github.com/openai/codex/issues/41014", @@ -21907,8 +26147,9 @@ var __webpack_modules__ = { ]; const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; const WS_MAX_HEADER_BYTES = 16 * 1024; - const WS_MAX_MESSAGE_BYTES = 4 * 1024 * 1024; - const WS_MAX_BUFFER_BYTES = 8 * 1024 * 1024; + const WS_MAX_MESSAGE_BYTES = 16 * 1024 * 1024; + const WS_MAX_BUFFER_BYTES = 32 * 1024 * 1024; + const CODEX_SEND_MAX_BYTES = 4 * 1024 * 1024; const WS_MAX_WRITE_BYTES = 8 * 1024 * 1024; const CODEX_MAX_REQUESTS = 128; const CODEX_MAX_LISTENERS = 128; @@ -21936,11 +26177,11 @@ var __webpack_modules__ = { function boxUnreachableMessage(path) { return [ "No Codex app-server control socket at " + path + ".", - _user_machine_guidance_js__rspack_import_9.M + _user_machine_guidance_js__rspack_import_10.M ].join("\n"); } function unreachableMessage(path, desktopAttached = "unknown", env = process.env) { - if ((0, _user_machine_guidance_js__rspack_import_9.R)({ + if ((0, _user_machine_guidance_js__rspack_import_10.R)({ path, env, home: env.HOME || (0, node_os__rspack_import_3.homedir)() @@ -22708,7 +26949,7 @@ var __webpack_modules__ = { let desktopShimConfigured = Boolean(shim && shim.installed && shim.wrapperPointsAtShim); if (shim === undefined) { try { - const live = (0, _desktop_shim_js__rspack_import_8.em)({ + const live = (0, _desktop_shim_js__rspack_import_9.em)({ env }); desktopShimConfigured = Boolean(live.installed && live.wrapperPointsAtShim); @@ -22779,6 +27020,13 @@ var __webpack_modules__ = { if (value == null) return null; return typeof value === "string" ? singleLine(value) : value; } + function summarizeSection(section) { + if (!isObject(section) || typeof section.id !== "string" || !section.id) return null; + return { + id: singleLine(section.id), + name: lineField(section.name) + }; + } function summarizeThread(t) { if (!isObject(t) || typeof t.id !== "string" || !t.id) throw new CodexProtocolError("thread/list", "entry without a string `id`"); const type = isObject(t.status) && typeof t.status.type === "string" ? t.status.type : "unknown"; @@ -22790,43 +27038,72 @@ var __webpack_modules__ = { preview: typeof t.preview === "string" ? stripTerminalControls(t.preview) : "", cwd: lineField(t.cwd), source: sourceField(t.source), - updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null + createdAt: typeof t.createdAt === "number" ? t.createdAt : null, + updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null, + section: summarizeSection(t.section), + projectId: lineField(t.projectId), + modelProvider: lineField(t.modelProvider), + model: lineField(t.model), + originator: lineField(t.originator) }; } const THREAD_LIST_MAX_LIMIT = 200; - async function listCodexThreads1({ limit = 20, cursor, env = process.env } = {}) { + async function listCodexThreads1({ limit = 20, cursor, modelProviders = [], env = process.env } = {}) { if (!Number.isInteger(limit) || limit < 1 || limit > THREAD_LIST_MAX_LIMIT) { throw new RangeError("--limit must be an integer 1-" + THREAD_LIST_MAX_LIMIT); } if (cursor !== undefined && (typeof cursor !== "string" || !cursor)) throw new RangeError("--cursor must be a non-empty string"); + if (!Array.isArray(modelProviders) || modelProviders.some((p)=>typeof p !== "string")) { + throw new RangeError("modelProviders must be an array of strings (empty = all providers)"); + } const { client } = await openSession(env); try { - const params = { + const baseParams = { limit, - useStateDbOnly: true, + modelProviders, ...cursor !== undefined ? { cursor } : {} }; let out; + let usedStateDbOnly = true; try { - out = await client.request("thread/list", params); + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: true + }); } catch (err) { throw transportError(err); } if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + if (out.data.length === 0 && cursor === undefined && (out.nextCursor == null || out.nextCursor === "")) { + try { + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: false + }); + usedStateDbOnly = false; + } catch (err) { + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + } if (out.nextCursor != null && typeof out.nextCursor !== "string") throw new CodexProtocolError("thread/list", "`nextCursor` is not a string"); const threads = out.data.map(summarizeThread); return { threads, nextCursor: out.nextCursor ?? null, - limit + limit, + useStateDbOnly: usedStateDbOnly }; } finally{ client.close(); } } - function explainSendError(err, threadId) { + function explainSendError(err, threadId, env = process.env) { + if (err instanceof _codex_remote_control_js__rspack_import_8.Rg) return err; + const mapped = (0, _codex_remote_control_js__rspack_import_8.R3)(err, threadId, env); + if (mapped instanceof _codex_remote_control_js__rspack_import_8.Rg) return mapped; if (!(err instanceof CodexRpcError)) return err; const msg = String(err.rpc && err.rpc.message || ""); if (/no rollout found|thread not found/i.test(msg)) { @@ -22901,12 +27178,19 @@ var __webpack_modules__ = { function assertThreadAllowed(threadId, env = process.env) { const raw = env.GROK_BOT_CODEX_THREADS; if (raw == null || raw.trim() === "") return; - const allowed = raw.split(",").map((s)=>s.trim()).filter(Boolean); - if (!allowed.includes(threadId)) { - throw new CodexSendError("Codex thread " + threadId + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { + const bare = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + const allowed = raw.split(",").map((s)=>{ + try { + return (0, _codex_thread_id_js__rspack_import_11.SX)(s.trim()); + } catch { + return s.trim(); + } + }).filter(Boolean); + if (!allowed.includes(bare)) { + throw new CodexSendError("Codex thread " + bare + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { delivery: "rejected", reason: "route-not-allowed", - threadId + threadId: bare }); } } @@ -22976,6 +27260,7 @@ var __webpack_modules__ = { } async function listCodexQueue(threadId, { env = process.env, limit = 50, cursor } = {}) { requireExperimental(env, "gbot codex queue"); + threadId = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); const { client } = await openSession(env, { experimental: true }); @@ -23006,6 +27291,64 @@ var __webpack_modules__ = { client.close(); } } + async function readCodexThread(threadId, { limit = 100, env = process.env } = {}) { + if (!Number.isInteger(limit) || limit < 1 || limit > 2000) { + throw new RangeError("limit must be an integer 1-2000"); + } + const bare = normalizeCodexThreadId(threadId); + const { client } = await openSession(env); + try { + let meta; + try { + meta = await client.request("thread/read", { + threadId: bare + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + const turns = []; + let cursor; + const seen = new Set(); + for(let page = 0; page < 40 && turns.length < limit; page++){ + let out; + try { + out = await client.request("thread/turns/list", { + threadId: bare, + limit: Math.min(100, limit - turns.length), + itemsView: "full", + sortDirection: "desc", + ...cursor !== undefined ? { + cursor + } : {} + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) { + throw new CodexProtocolError("thread/turns/list", "missing `data` array"); + } + for (const row of out.data)turns.push(row); + cursor = out.nextCursor ?? null; + if (cursor === null) break; + if (typeof cursor !== "string" || !cursor || seen.has(cursor)) { + throw new CodexProtocolError("thread/turns/list", "invalid or repeated cursor"); + } + seen.add(cursor); + } + return { + threadId: bare, + thread: isObject(meta) && isObject(meta.thread) ? meta.thread : null, + turns: turns.slice(0, limit), + nextCursor: cursor ?? null + }; + } finally{ + client.close(); + } + } async function sendToCodexThread(threadId, text, { env = process.env, envelope = buildEnvelope({ env }), whenBusy = "reject", session, expectedTurnId, expectedCwd, model, effort, signal } = {}) { @@ -23051,8 +27394,8 @@ var __webpack_modules__ = { ] : [] ].includes(whenBusy)) throw new RangeError("--when-busy must be reject, queue, or persistent steer"); if (whenBusy === "steer" && (typeof expectedTurnId !== "string" || !ID_PATTERN.test(expectedTurnId))) throw new RangeError("steer requires expectedTurnId"); - if (typeof threadId !== "string" || !ID_PATTERN.test(threadId)) throw new RangeError("Invalid threadId"); - if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > WS_MAX_MESSAGE_BYTES) throw new RangeError("Message must contain text within 4 MiB"); + threadId = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > CODEX_SEND_MAX_BYTES) throw new RangeError("Message must contain text within 4 MiB"); if (!envelope || typeof envelope.messageId !== "string" || !ID_PATTERN.test(envelope.messageId)) throw new RangeError("Invalid envelope messageId"); const validated = buildEnvelope({ correlationId: envelope.correlationId, @@ -23100,8 +27443,10 @@ var __webpack_modules__ = { }); } catch (err) { assertNotCancelled(); - if (err instanceof CodexSendError) throw err; - throw new CodexSendError(explainSendError(err, threadId).message, { + if (err instanceof CodexSendError || err instanceof _codex_remote_control_js__rspack_import_8.Rg) throw err; + const explained = explainSendError(err, threadId, env); + if (explained instanceof _codex_remote_control_js__rspack_import_8.Rg) throw explained; + throw new CodexSendError(explained.message, { delivery: err instanceof CodexRpcError ? "rejected" : err && err.delivery || "unknown", 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", threadId, @@ -23338,7 +27683,11 @@ var __webpack_modules__ = { "messageId", "correlationId", "refused", - "hop" + "hop", + "hostId", + "hostName", + "hint", + "code" ]){ if (error[key] !== undefined) out[key] = error[key]; } @@ -23374,9 +27723,12 @@ var __webpack_modules__ = { var _contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); var node_fs__rspack_import_1 = __webpack_require__("node:fs"); var _codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); + var _remote_control_js__rspack_import_3 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); const ID = /^[A-Za-z0-9_.:-]{1,128}$/; const BUDGET = 4 * 1024 * 1024; function conversationId(value, name = 'threadId') { + if (name === 'threadId') return (0, _thread_id_js__rspack_import_4.SX)(value); if (typeof value !== 'string' || !ID.test(value)) throw new RangeError(`${name} must be 1-128 identifier characters`); return value; } @@ -23501,7 +27853,8 @@ var __webpack_modules__ = { if (expected !== undefined && (typeof cwd !== 'string' || (0, node_fs__rspack_import_1.realpathSync)(cwd) !== expected)) throw new Error('Codex thread cwd does not match expectedCwd'); } catch (error) { await close(); - throw error; + if (error instanceof _remote_control_js__rspack_import_3.Rg) throw error; + throw (0, _remote_control_js__rspack_import_3.R3)(error, threadId, env); } async function wait({ turnId, messageId, afterMessageId, timeoutMs = 120000, signal: waitSignal, maxOutputBytes = 1048576 }) { conversationId(turnId, 'turnId'); @@ -23851,6 +28204,269 @@ var __webpack_modules__ = { j: ()=>openCodexConversation }); }, + "./src/core/codex/remote-control.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var node_os__rspack_import_1 = __webpack_require__("node:os"); + var node_path__rspack_import_2 = __webpack_require__("node:path"); + var _thread_id_js__rspack_import_3 = __webpack_require__("./src/core/codex/thread-id.js"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + class RemoteThreadNotLoadedError1 extends Error { + constructor(threadId, hostId = null, hostName = null){ + const hostLabel = hostName ? `${JSON.stringify(hostName)} (${JSON.stringify(hostId)})` : hostId ? JSON.stringify(hostId) : null; + const owner = hostLabel ? `owned by remote-control host ${hostLabel}` : 'owned by a remote-control host (hostId unknown; check ~/.codex/.codex-global-state.json)'; + const hint = hostId ? `Read this thread via host ${JSON.stringify(hostId)}'s app-server` + (hostName ? ` (${hostName})` : '') + '. SSH remoting is not implemented in this adapter.' : "Read this thread via the owning host's app-server. SSH remoting is not implemented in this adapter."; + super(`Codex thread ${JSON.stringify(threadId)} is not loaded on the local app-server; ${owner}. ${hint}`); + this.name = 'RemoteThreadNotLoadedError'; + this.code = 'REMOTE_THREAD_NOT_LOADED'; + this.delivery = 'rejected'; + this.reason = 'remote-thread-not-loaded'; + this.threadId = threadId; + this.hostId = hostId; + this.hostName = hostName; + this.hint = hint; + } + } + function codexGlobalStatePath(env = process.env) { + const home = env.CODEX_HOME || (0, node_path__rspack_import_2.join)((0, node_os__rspack_import_1.homedir)(), '.codex'); + return (0, node_path__rspack_import_2.join)(home, '.codex-global-state.json'); + } + function loadCodexGlobalState(env = process.env) { + let raw; + try { + raw = (0, node_fs__rspack_import_0.readFileSync)(codexGlobalStatePath(env), 'utf8'); + } catch { + return null; + } + try { + const data = JSON.parse(raw); + if (!data || typeof data !== 'object' || Array.isArray(data)) return null; + return data; + } catch { + return null; + } + } + function codexStateEntries(data) { + const nested = data?.['electron-persisted-atom-state']; + return [ + nested, + data + ].filter((value)=>value && typeof value === 'object' && !Array.isArray(value)); + } + function findRemoteThreadHost(threadId, env = process.env) { + let bare; + try { + bare = (0, _thread_id_js__rspack_import_3.SX)(threadId); + } catch { + bare = typeof threadId === 'string' && threadId.startsWith("local:") ? threadId.slice("local:".length) : threadId; + } + if (!bare) return null; + const data = loadCodexGlobalState(env); + if (!data) return null; + const hostNames = collectHostNames(data); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value) ?? null; + for (const entry of extractThreadEntries(value)){ + const id = threadIdFromEntry(entry); + if (!id) continue; + const entryBare = id.startsWith("local:") ? id.slice("local:".length) : id; + if (entryBare === bare) return { + hostId, + hostName + }; + } + } + return null; + } + function mapRemoteThreadError(error, threadId, env = process.env) { + const message = errorMessage(error); + if (!/thread not loaded|not loaded|no rollout found|thread not found|invalid thread id/i.test(message)) { + return error instanceof Error ? error : new Error(message || String(error)); + } + const remote = findRemoteThreadHost(threadId, env); + if (remote) { + return new RemoteThreadNotLoadedError1(threadId, remote.hostId, remote.hostName); + } + if (/thread not loaded|not loaded/i.test(message)) { + return new RemoteThreadNotLoadedError1(threadId, null, null); + } + return error instanceof Error ? error : new Error(message || String(error)); + } + function collectHostNames(data) { + const names = new Map(); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const hostId = stringField(entry, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) continue; + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const id = stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + entries.push(id ? entry : { + ...entry, + id: key + }); + } + } + return entries; + } + function threadIdFromEntry(entry) { + if (typeof entry === 'string' && entry) return entry; + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + return stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + for (const key of keys){ + const value = row[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) return String(error.message); + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); + } + __webpack_require__.d(__webpack_exports__, { + IT: ()=>loadCodexGlobalState, + R3: ()=>mapRemoteThreadError, + Rg: ()=>RemoteThreadNotLoadedError1, + xr: ()=>codexStateEntries + }); + }, + "./src/core/codex/thread-id.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + const LOCAL_THREAD_ID_PREFIX = 'local:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/; + function isTemporaryDesktopThreadId(threadId) { + return typeof threadId === 'string' && (threadId.startsWith(TEMP_THREAD_ID_PREFIX) || threadId.startsWith('client-new-thread:')); + } + function normalizeCodexThreadId1(threadId) { + if (typeof threadId !== 'string' || !threadId) { + throw new RangeError('Invalid threadId'); + } + if (isTemporaryDesktopThreadId(threadId)) { + throw new RangeError('Temporary Desktop thread id ' + JSON.stringify(threadId) + ' has no app-server mapping; wait for the real conversation id'); + } + const bare = threadId.startsWith(LOCAL_THREAD_ID_PREFIX) ? threadId.slice(LOCAL_THREAD_ID_PREFIX.length) : threadId; + if (!bare || !ID_PATTERN.test(bare)) { + throw new RangeError('Invalid threadId'); + } + return bare; + } + __webpack_require__.d(__webpack_exports__, { + SX: ()=>normalizeCodexThreadId1, + vr: ()=>isTemporaryDesktopThreadId + }, { + hA: TEMP_THREAD_ID_PREFIX + }); + }, "./src/core/commands.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { var _store_js__rspack_import_0 = __webpack_require__("./src/core/store.js"); var _gateway_js__rspack_import_1 = __webpack_require__("./src/core/gateway.js"); @@ -26164,9 +30780,9 @@ function __webpack_require__(moduleId) { var __webpack_exports__ = {}; var node_worker_threads__rspack_import_0 = __webpack_require__("node:worker_threads"); var react__rspack_import_1 = __webpack_require__("./node_modules/react/react.react-server.js"); -var _agent_bundle_runtime_flight_server__rspack_import_42 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/flight/server.js"); -var _agent_bundle_runtime__rspack_import_40 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/49.js"); -var _agent_bundle_runtime__rspack_import_41 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); +var _agent_bundle_runtime_flight_server__rspack_import_48 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/flight/server.js"); +var _agent_bundle_runtime__rspack_import_46 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/49.js"); +var _agent_bundle_runtime__rspack_import_47 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); var node_url__rspack_import_2 = __webpack_require__("node:url"); var _src_cli_approvals_list_tsx__rspack_import_3 = __webpack_require__("./src/cli/approvals/list.tsx"); var _src_cli_approvals_respond_tsx__rspack_import_4 = __webpack_require__("./src/cli/approvals/respond.tsx"); @@ -26175,36 +30791,42 @@ var _src_cli_bots_delete_tsx__rspack_import_6 = __webpack_require__("./src/cli/b var _src_cli_bots_get_tsx__rspack_import_7 = __webpack_require__("./src/cli/bots/get.tsx"); var _src_cli_bots_list_tsx__rspack_import_8 = __webpack_require__("./src/cli/bots/list.tsx"); var _src_cli_bots_update_tsx__rspack_import_9 = __webpack_require__("./src/cli/bots/update.tsx"); -var _src_cli_claude_send_tsx__rspack_import_10 = __webpack_require__("./src/cli/claude/send.tsx"); -var _src_cli_codex_bridge_respond_tsx__rspack_import_11 = __webpack_require__("./src/cli/codex/bridge/respond.tsx"); -var _src_cli_codex_bridge_run_tsx__rspack_import_12 = __webpack_require__("./src/cli/codex/bridge/run.tsx"); -var _src_cli_codex_bridge_start_tsx__rspack_import_13 = __webpack_require__("./src/cli/codex/bridge/start.tsx"); -var _src_cli_codex_bridge_status_tsx__rspack_import_14 = __webpack_require__("./src/cli/codex/bridge/status.tsx"); -var _src_cli_codex_bridge_stop_tsx__rspack_import_15 = __webpack_require__("./src/cli/codex/bridge/stop.tsx"); -var _src_cli_codex_desktop_shim_tsx__rspack_import_16 = __webpack_require__("./src/cli/codex/desktop-shim.tsx"); -var _src_cli_codex_list_threads_tsx__rspack_import_17 = __webpack_require__("./src/cli/codex/list-threads.tsx"); -var _src_cli_codex_new_tsx__rspack_import_18 = __webpack_require__("./src/cli/codex/new.tsx"); -var _src_cli_codex_queue_tsx__rspack_import_19 = __webpack_require__("./src/cli/codex/queue.tsx"); -var _src_cli_codex_send_tsx__rspack_import_20 = __webpack_require__("./src/cli/codex/send.tsx"); -var _src_cli_codex_status_tsx__rspack_import_21 = __webpack_require__("./src/cli/codex/status.tsx"); -var _src_cli_codex_wait_tsx__rspack_import_22 = __webpack_require__("./src/cli/codex/wait.tsx"); -var _src_cli_codex_watch_tsx__rspack_import_23 = __webpack_require__("./src/cli/codex/watch.tsx"); -var _src_cli_doctor_tsx__rspack_import_24 = __webpack_require__("./src/cli/doctor.tsx"); -var _src_cli_groups_add_tsx__rspack_import_25 = __webpack_require__("./src/cli/groups/add.tsx"); -var _src_cli_groups_create_tsx__rspack_import_26 = __webpack_require__("./src/cli/groups/create.tsx"); -var _src_cli_groups_delete_tsx__rspack_import_27 = __webpack_require__("./src/cli/groups/delete.tsx"); -var _src_cli_groups_get_tsx__rspack_import_28 = __webpack_require__("./src/cli/groups/get.tsx"); -var _src_cli_groups_list_tsx__rspack_import_29 = __webpack_require__("./src/cli/groups/list.tsx"); -var _src_cli_groups_members_tsx__rspack_import_30 = __webpack_require__("./src/cli/groups/members.tsx"); -var _src_cli_groups_remove_tsx__rspack_import_31 = __webpack_require__("./src/cli/groups/remove.tsx"); -var _src_cli_groups_set_tsx__rspack_import_32 = __webpack_require__("./src/cli/groups/set.tsx"); -var _src_cli_groups_update_tsx__rspack_import_33 = __webpack_require__("./src/cli/groups/update.tsx"); -var _src_cli_history_tsx__rspack_import_34 = __webpack_require__("./src/cli/history.tsx"); -var _src_cli_send_tsx__rspack_import_35 = __webpack_require__("./src/cli/send.tsx"); -var _src_cli_skills_add_tsx__rspack_import_36 = __webpack_require__("./src/cli/skills/add.tsx"); -var _src_cli_skills_list_tsx__rspack_import_37 = __webpack_require__("./src/cli/skills/list.tsx"); -var _src_cli_skills_remove_tsx__rspack_import_38 = __webpack_require__("./src/cli/skills/remove.tsx"); -var _src_cli_thread_tsx__rspack_import_39 = __webpack_require__("./src/cli/thread.tsx"); +var _src_cli_chatgpt_desktop_hosts_tsx__rspack_import_10 = __webpack_require__("./src/cli/chatgpt-desktop/hosts.tsx"); +var _src_cli_chatgpt_desktop_read_tsx__rspack_import_11 = __webpack_require__("./src/cli/chatgpt-desktop/read.tsx"); +var _src_cli_chatgpt_desktop_search_tsx__rspack_import_12 = __webpack_require__("./src/cli/chatgpt-desktop/search.tsx"); +var _src_cli_chatgpt_desktop_send_tsx__rspack_import_13 = __webpack_require__("./src/cli/chatgpt-desktop/send.tsx"); +var _src_cli_chatgpt_desktop_status_tsx__rspack_import_14 = __webpack_require__("./src/cli/chatgpt-desktop/status.tsx"); +var _src_cli_chatgpt_desktop_threads_tsx__rspack_import_15 = __webpack_require__("./src/cli/chatgpt-desktop/threads.tsx"); +var _src_cli_claude_send_tsx__rspack_import_16 = __webpack_require__("./src/cli/claude/send.tsx"); +var _src_cli_codex_bridge_respond_tsx__rspack_import_17 = __webpack_require__("./src/cli/codex/bridge/respond.tsx"); +var _src_cli_codex_bridge_run_tsx__rspack_import_18 = __webpack_require__("./src/cli/codex/bridge/run.tsx"); +var _src_cli_codex_bridge_start_tsx__rspack_import_19 = __webpack_require__("./src/cli/codex/bridge/start.tsx"); +var _src_cli_codex_bridge_status_tsx__rspack_import_20 = __webpack_require__("./src/cli/codex/bridge/status.tsx"); +var _src_cli_codex_bridge_stop_tsx__rspack_import_21 = __webpack_require__("./src/cli/codex/bridge/stop.tsx"); +var _src_cli_codex_desktop_shim_tsx__rspack_import_22 = __webpack_require__("./src/cli/codex/desktop-shim.tsx"); +var _src_cli_codex_list_threads_tsx__rspack_import_23 = __webpack_require__("./src/cli/codex/list-threads.tsx"); +var _src_cli_codex_new_tsx__rspack_import_24 = __webpack_require__("./src/cli/codex/new.tsx"); +var _src_cli_codex_queue_tsx__rspack_import_25 = __webpack_require__("./src/cli/codex/queue.tsx"); +var _src_cli_codex_send_tsx__rspack_import_26 = __webpack_require__("./src/cli/codex/send.tsx"); +var _src_cli_codex_status_tsx__rspack_import_27 = __webpack_require__("./src/cli/codex/status.tsx"); +var _src_cli_codex_wait_tsx__rspack_import_28 = __webpack_require__("./src/cli/codex/wait.tsx"); +var _src_cli_codex_watch_tsx__rspack_import_29 = __webpack_require__("./src/cli/codex/watch.tsx"); +var _src_cli_doctor_tsx__rspack_import_30 = __webpack_require__("./src/cli/doctor.tsx"); +var _src_cli_groups_add_tsx__rspack_import_31 = __webpack_require__("./src/cli/groups/add.tsx"); +var _src_cli_groups_create_tsx__rspack_import_32 = __webpack_require__("./src/cli/groups/create.tsx"); +var _src_cli_groups_delete_tsx__rspack_import_33 = __webpack_require__("./src/cli/groups/delete.tsx"); +var _src_cli_groups_get_tsx__rspack_import_34 = __webpack_require__("./src/cli/groups/get.tsx"); +var _src_cli_groups_list_tsx__rspack_import_35 = __webpack_require__("./src/cli/groups/list.tsx"); +var _src_cli_groups_members_tsx__rspack_import_36 = __webpack_require__("./src/cli/groups/members.tsx"); +var _src_cli_groups_remove_tsx__rspack_import_37 = __webpack_require__("./src/cli/groups/remove.tsx"); +var _src_cli_groups_set_tsx__rspack_import_38 = __webpack_require__("./src/cli/groups/set.tsx"); +var _src_cli_groups_update_tsx__rspack_import_39 = __webpack_require__("./src/cli/groups/update.tsx"); +var _src_cli_history_tsx__rspack_import_40 = __webpack_require__("./src/cli/history.tsx"); +var _src_cli_send_tsx__rspack_import_41 = __webpack_require__("./src/cli/send.tsx"); +var _src_cli_skills_add_tsx__rspack_import_42 = __webpack_require__("./src/cli/skills/add.tsx"); +var _src_cli_skills_list_tsx__rspack_import_43 = __webpack_require__("./src/cli/skills/list.tsx"); +var _src_cli_skills_remove_tsx__rspack_import_44 = __webpack_require__("./src/cli/skills/remove.tsx"); +var _src_cli_thread_tsx__rspack_import_45 = __webpack_require__("./src/cli/thread.tsx"); const route0 = Object.assign({}, Reflect.get(_src_cli_approvals_list_tsx__rspack_import_3, 'default'), _src_cli_approvals_list_tsx__rspack_import_3); const route1 = Object.assign({}, Reflect.get(_src_cli_approvals_respond_tsx__rspack_import_4, 'default'), _src_cli_approvals_respond_tsx__rspack_import_4); const route2 = Object.assign({}, Reflect.get(_src_cli_bots_create_tsx__rspack_import_5, 'default'), _src_cli_bots_create_tsx__rspack_import_5); @@ -26212,37 +30834,43 @@ const route3 = Object.assign({}, Reflect.get(_src_cli_bots_delete_tsx__rspack_im const route4 = Object.assign({}, Reflect.get(_src_cli_bots_get_tsx__rspack_import_7, 'default'), _src_cli_bots_get_tsx__rspack_import_7); const route5 = Object.assign({}, Reflect.get(_src_cli_bots_list_tsx__rspack_import_8, 'default'), _src_cli_bots_list_tsx__rspack_import_8); const route6 = Object.assign({}, Reflect.get(_src_cli_bots_update_tsx__rspack_import_9, 'default'), _src_cli_bots_update_tsx__rspack_import_9); -const route7 = Object.assign({}, Reflect.get(_src_cli_claude_send_tsx__rspack_import_10, 'default'), _src_cli_claude_send_tsx__rspack_import_10); -const route8 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_respond_tsx__rspack_import_11, 'default'), _src_cli_codex_bridge_respond_tsx__rspack_import_11); -const route9 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_run_tsx__rspack_import_12, 'default'), _src_cli_codex_bridge_run_tsx__rspack_import_12); -const route10 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_start_tsx__rspack_import_13, 'default'), _src_cli_codex_bridge_start_tsx__rspack_import_13); -const route11 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_status_tsx__rspack_import_14, 'default'), _src_cli_codex_bridge_status_tsx__rspack_import_14); -const route12 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_stop_tsx__rspack_import_15, 'default'), _src_cli_codex_bridge_stop_tsx__rspack_import_15); -const route13 = Object.assign({}, Reflect.get(_src_cli_codex_desktop_shim_tsx__rspack_import_16, 'default'), _src_cli_codex_desktop_shim_tsx__rspack_import_16); -const route14 = Object.assign({}, Reflect.get(_src_cli_codex_list_threads_tsx__rspack_import_17, 'default'), _src_cli_codex_list_threads_tsx__rspack_import_17); -const route15 = Object.assign({}, Reflect.get(_src_cli_codex_new_tsx__rspack_import_18, 'default'), _src_cli_codex_new_tsx__rspack_import_18); -const route16 = Object.assign({}, Reflect.get(_src_cli_codex_queue_tsx__rspack_import_19, 'default'), _src_cli_codex_queue_tsx__rspack_import_19); -const route17 = Object.assign({}, Reflect.get(_src_cli_codex_send_tsx__rspack_import_20, 'default'), _src_cli_codex_send_tsx__rspack_import_20); -const route18 = Object.assign({}, Reflect.get(_src_cli_codex_status_tsx__rspack_import_21, 'default'), _src_cli_codex_status_tsx__rspack_import_21); -const route19 = Object.assign({}, Reflect.get(_src_cli_codex_wait_tsx__rspack_import_22, 'default'), _src_cli_codex_wait_tsx__rspack_import_22); -const route20 = Object.assign({}, Reflect.get(_src_cli_codex_watch_tsx__rspack_import_23, 'default'), _src_cli_codex_watch_tsx__rspack_import_23); -const route21 = Object.assign({}, Reflect.get(_src_cli_doctor_tsx__rspack_import_24, 'default'), _src_cli_doctor_tsx__rspack_import_24); -const route22 = Object.assign({}, Reflect.get(_src_cli_groups_add_tsx__rspack_import_25, 'default'), _src_cli_groups_add_tsx__rspack_import_25); -const route23 = Object.assign({}, Reflect.get(_src_cli_groups_create_tsx__rspack_import_26, 'default'), _src_cli_groups_create_tsx__rspack_import_26); -const route24 = Object.assign({}, Reflect.get(_src_cli_groups_delete_tsx__rspack_import_27, 'default'), _src_cli_groups_delete_tsx__rspack_import_27); -const route25 = Object.assign({}, Reflect.get(_src_cli_groups_get_tsx__rspack_import_28, 'default'), _src_cli_groups_get_tsx__rspack_import_28); -const route26 = Object.assign({}, Reflect.get(_src_cli_groups_list_tsx__rspack_import_29, 'default'), _src_cli_groups_list_tsx__rspack_import_29); -const route27 = Object.assign({}, Reflect.get(_src_cli_groups_members_tsx__rspack_import_30, 'default'), _src_cli_groups_members_tsx__rspack_import_30); -const route28 = Object.assign({}, Reflect.get(_src_cli_groups_remove_tsx__rspack_import_31, 'default'), _src_cli_groups_remove_tsx__rspack_import_31); -const route29 = Object.assign({}, Reflect.get(_src_cli_groups_set_tsx__rspack_import_32, 'default'), _src_cli_groups_set_tsx__rspack_import_32); -const route30 = Object.assign({}, Reflect.get(_src_cli_groups_update_tsx__rspack_import_33, 'default'), _src_cli_groups_update_tsx__rspack_import_33); -const route31 = Object.assign({}, Reflect.get(_src_cli_history_tsx__rspack_import_34, 'default'), _src_cli_history_tsx__rspack_import_34); -const route32 = Object.assign({}, Reflect.get(_src_cli_send_tsx__rspack_import_35, 'default'), _src_cli_send_tsx__rspack_import_35); -const route33 = Object.assign({}, Reflect.get(_src_cli_skills_add_tsx__rspack_import_36, 'default'), _src_cli_skills_add_tsx__rspack_import_36); -const route34 = Object.assign({}, Reflect.get(_src_cli_skills_list_tsx__rspack_import_37, 'default'), _src_cli_skills_list_tsx__rspack_import_37); -const route35 = Object.assign({}, Reflect.get(_src_cli_skills_remove_tsx__rspack_import_38, 'default'), _src_cli_skills_remove_tsx__rspack_import_38); -const route36 = Object.assign({}, Reflect.get(_src_cli_thread_tsx__rspack_import_39, 'default'), _src_cli_thread_tsx__rspack_import_39); -const pluginRoot = (0, _agent_bundle_runtime__rspack_import_40.E7)({ +const route7 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_hosts_tsx__rspack_import_10, 'default'), _src_cli_chatgpt_desktop_hosts_tsx__rspack_import_10); +const route8 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_read_tsx__rspack_import_11, 'default'), _src_cli_chatgpt_desktop_read_tsx__rspack_import_11); +const route9 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_search_tsx__rspack_import_12, 'default'), _src_cli_chatgpt_desktop_search_tsx__rspack_import_12); +const route10 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_send_tsx__rspack_import_13, 'default'), _src_cli_chatgpt_desktop_send_tsx__rspack_import_13); +const route11 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_status_tsx__rspack_import_14, 'default'), _src_cli_chatgpt_desktop_status_tsx__rspack_import_14); +const route12 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_threads_tsx__rspack_import_15, 'default'), _src_cli_chatgpt_desktop_threads_tsx__rspack_import_15); +const route13 = Object.assign({}, Reflect.get(_src_cli_claude_send_tsx__rspack_import_16, 'default'), _src_cli_claude_send_tsx__rspack_import_16); +const route14 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_respond_tsx__rspack_import_17, 'default'), _src_cli_codex_bridge_respond_tsx__rspack_import_17); +const route15 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_run_tsx__rspack_import_18, 'default'), _src_cli_codex_bridge_run_tsx__rspack_import_18); +const route16 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_start_tsx__rspack_import_19, 'default'), _src_cli_codex_bridge_start_tsx__rspack_import_19); +const route17 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_status_tsx__rspack_import_20, 'default'), _src_cli_codex_bridge_status_tsx__rspack_import_20); +const route18 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_stop_tsx__rspack_import_21, 'default'), _src_cli_codex_bridge_stop_tsx__rspack_import_21); +const route19 = Object.assign({}, Reflect.get(_src_cli_codex_desktop_shim_tsx__rspack_import_22, 'default'), _src_cli_codex_desktop_shim_tsx__rspack_import_22); +const route20 = Object.assign({}, Reflect.get(_src_cli_codex_list_threads_tsx__rspack_import_23, 'default'), _src_cli_codex_list_threads_tsx__rspack_import_23); +const route21 = Object.assign({}, Reflect.get(_src_cli_codex_new_tsx__rspack_import_24, 'default'), _src_cli_codex_new_tsx__rspack_import_24); +const route22 = Object.assign({}, Reflect.get(_src_cli_codex_queue_tsx__rspack_import_25, 'default'), _src_cli_codex_queue_tsx__rspack_import_25); +const route23 = Object.assign({}, Reflect.get(_src_cli_codex_send_tsx__rspack_import_26, 'default'), _src_cli_codex_send_tsx__rspack_import_26); +const route24 = Object.assign({}, Reflect.get(_src_cli_codex_status_tsx__rspack_import_27, 'default'), _src_cli_codex_status_tsx__rspack_import_27); +const route25 = Object.assign({}, Reflect.get(_src_cli_codex_wait_tsx__rspack_import_28, 'default'), _src_cli_codex_wait_tsx__rspack_import_28); +const route26 = Object.assign({}, Reflect.get(_src_cli_codex_watch_tsx__rspack_import_29, 'default'), _src_cli_codex_watch_tsx__rspack_import_29); +const route27 = Object.assign({}, Reflect.get(_src_cli_doctor_tsx__rspack_import_30, 'default'), _src_cli_doctor_tsx__rspack_import_30); +const route28 = Object.assign({}, Reflect.get(_src_cli_groups_add_tsx__rspack_import_31, 'default'), _src_cli_groups_add_tsx__rspack_import_31); +const route29 = Object.assign({}, Reflect.get(_src_cli_groups_create_tsx__rspack_import_32, 'default'), _src_cli_groups_create_tsx__rspack_import_32); +const route30 = Object.assign({}, Reflect.get(_src_cli_groups_delete_tsx__rspack_import_33, 'default'), _src_cli_groups_delete_tsx__rspack_import_33); +const route31 = Object.assign({}, Reflect.get(_src_cli_groups_get_tsx__rspack_import_34, 'default'), _src_cli_groups_get_tsx__rspack_import_34); +const route32 = Object.assign({}, Reflect.get(_src_cli_groups_list_tsx__rspack_import_35, 'default'), _src_cli_groups_list_tsx__rspack_import_35); +const route33 = Object.assign({}, Reflect.get(_src_cli_groups_members_tsx__rspack_import_36, 'default'), _src_cli_groups_members_tsx__rspack_import_36); +const route34 = Object.assign({}, Reflect.get(_src_cli_groups_remove_tsx__rspack_import_37, 'default'), _src_cli_groups_remove_tsx__rspack_import_37); +const route35 = Object.assign({}, Reflect.get(_src_cli_groups_set_tsx__rspack_import_38, 'default'), _src_cli_groups_set_tsx__rspack_import_38); +const route36 = Object.assign({}, Reflect.get(_src_cli_groups_update_tsx__rspack_import_39, 'default'), _src_cli_groups_update_tsx__rspack_import_39); +const route37 = Object.assign({}, Reflect.get(_src_cli_history_tsx__rspack_import_40, 'default'), _src_cli_history_tsx__rspack_import_40); +const route38 = Object.assign({}, Reflect.get(_src_cli_send_tsx__rspack_import_41, 'default'), _src_cli_send_tsx__rspack_import_41); +const route39 = Object.assign({}, Reflect.get(_src_cli_skills_add_tsx__rspack_import_42, 'default'), _src_cli_skills_add_tsx__rspack_import_42); +const route40 = Object.assign({}, Reflect.get(_src_cli_skills_list_tsx__rspack_import_43, 'default'), _src_cli_skills_list_tsx__rspack_import_43); +const route41 = Object.assign({}, Reflect.get(_src_cli_skills_remove_tsx__rspack_import_44, 'default'), _src_cli_skills_remove_tsx__rspack_import_44); +const route42 = Object.assign({}, Reflect.get(_src_cli_thread_tsx__rspack_import_45, 'default'), _src_cli_thread_tsx__rspack_import_45); +const pluginRoot = (0, _agent_bundle_runtime__rspack_import_46.E7)({ fallback: (0, node_url__rspack_import_2.fileURLToPath)(new URL('..', import.meta.url)), stateAnchor: 'user-data' }); @@ -26300,185 +30928,221 @@ const routes = Object.freeze({ name: "bots update", module: route6 }), + "cli:chatgpt-desktop/hosts": Object.freeze({ + id: "cli:chatgpt-desktop/hosts", + kind: "cli", + name: "chatgpt-desktop hosts", + module: route7 + }), + "cli:chatgpt-desktop/read": Object.freeze({ + id: "cli:chatgpt-desktop/read", + kind: "cli", + name: "chatgpt-desktop read", + module: route8 + }), + "cli:chatgpt-desktop/search": Object.freeze({ + id: "cli:chatgpt-desktop/search", + kind: "cli", + name: "chatgpt-desktop search", + module: route9 + }), + "cli:chatgpt-desktop/send": Object.freeze({ + id: "cli:chatgpt-desktop/send", + kind: "cli", + name: "chatgpt-desktop send", + module: route10 + }), + "cli:chatgpt-desktop/status": Object.freeze({ + id: "cli:chatgpt-desktop/status", + kind: "cli", + name: "chatgpt-desktop status", + module: route11 + }), + "cli:chatgpt-desktop/threads": Object.freeze({ + id: "cli:chatgpt-desktop/threads", + kind: "cli", + name: "chatgpt-desktop threads", + module: route12 + }), "cli:claude/send": Object.freeze({ id: "cli:claude/send", kind: "cli", name: "claude send", - module: route7 + module: route13 }), "cli:codex/bridge/respond": Object.freeze({ id: "cli:codex/bridge/respond", kind: "cli", name: "codex bridge respond", - module: route8 + module: route14 }), "cli:codex/bridge/run": Object.freeze({ id: "cli:codex/bridge/run", kind: "cli", name: "codex bridge run", - module: route9 + module: route15 }), "cli:codex/bridge/start": Object.freeze({ id: "cli:codex/bridge/start", kind: "cli", name: "codex bridge start", - module: route10 + module: route16 }), "cli:codex/bridge/status": Object.freeze({ id: "cli:codex/bridge/status", kind: "cli", name: "codex bridge status", - module: route11 + module: route17 }), "cli:codex/bridge/stop": Object.freeze({ id: "cli:codex/bridge/stop", kind: "cli", name: "codex bridge stop", - module: route12 + module: route18 }), "cli:codex/desktop-shim": Object.freeze({ id: "cli:codex/desktop-shim", kind: "cli", name: "codex desktop-shim", - module: route13 + module: route19 }), "cli:codex/list-threads": Object.freeze({ id: "cli:codex/list-threads", kind: "cli", name: "codex list-threads", - module: route14 + module: route20 }), "cli:codex/new": Object.freeze({ id: "cli:codex/new", kind: "cli", name: "codex new", - module: route15 + module: route21 }), "cli:codex/queue": Object.freeze({ id: "cli:codex/queue", kind: "cli", name: "codex queue", - module: route16 + module: route22 }), "cli:codex/send": Object.freeze({ id: "cli:codex/send", kind: "cli", name: "codex send", - module: route17 + module: route23 }), "cli:codex/status": Object.freeze({ id: "cli:codex/status", kind: "cli", name: "codex status", - module: route18 + module: route24 }), "cli:codex/wait": Object.freeze({ id: "cli:codex/wait", kind: "cli", name: "codex wait", - module: route19 + module: route25 }), "cli:codex/watch": Object.freeze({ id: "cli:codex/watch", kind: "cli", name: "codex watch", - module: route20 + module: route26 }), "cli:doctor": Object.freeze({ id: "cli:doctor", kind: "cli", name: "doctor", - module: route21 + module: route27 }), "cli:groups/add": Object.freeze({ id: "cli:groups/add", kind: "cli", name: "groups add", - module: route22 + module: route28 }), "cli:groups/create": Object.freeze({ id: "cli:groups/create", kind: "cli", name: "groups create", - module: route23 + module: route29 }), "cli:groups/delete": Object.freeze({ id: "cli:groups/delete", kind: "cli", name: "groups delete", - module: route24 + module: route30 }), "cli:groups/get": Object.freeze({ id: "cli:groups/get", kind: "cli", name: "groups get", - module: route25 + module: route31 }), "cli:groups/list": Object.freeze({ id: "cli:groups/list", kind: "cli", name: "groups list", - module: route26 + module: route32 }), "cli:groups/members": Object.freeze({ id: "cli:groups/members", kind: "cli", name: "groups members", - module: route27 + module: route33 }), "cli:groups/remove": Object.freeze({ id: "cli:groups/remove", kind: "cli", name: "groups remove", - module: route28 + module: route34 }), "cli:groups/set": Object.freeze({ id: "cli:groups/set", kind: "cli", name: "groups set", - module: route29 + module: route35 }), "cli:groups/update": Object.freeze({ id: "cli:groups/update", kind: "cli", name: "groups update", - module: route30 + module: route36 }), "cli:history": Object.freeze({ id: "cli:history", kind: "cli", name: "history", - module: route31 + module: route37 }), "cli:send": Object.freeze({ id: "cli:send", kind: "cli", name: "send", - module: route32 + module: route38 }), "cli:skills/add": Object.freeze({ id: "cli:skills/add", kind: "cli", name: "skills add", - module: route33 + module: route39 }), "cli:skills/list": Object.freeze({ id: "cli:skills/list", kind: "cli", name: "skills list", - module: route34 + module: route40 }), "cli:skills/remove": Object.freeze({ id: "cli:skills/remove", kind: "cli", name: "skills remove", - module: route35 + module: route41 }), "cli:thread": Object.freeze({ id: "cli:thread", kind: "cli", name: "thread", - module: route36 + module: route42 }) }); const requests = new Map(); @@ -26513,18 +31177,18 @@ const render = async (message)=>{ }; try { const cwd = process.cwd(); - await (0, _agent_bundle_runtime__rspack_import_41.iC)({ + await (0, _agent_bundle_runtime__rspack_import_47.iC)({ capabilities: { - command: (0, _agent_bundle_runtime__rspack_import_41.hU)(), - filesystem: (0, _agent_bundle_runtime__rspack_import_41.hU)(), - network: (0, _agent_bundle_runtime__rspack_import_41.hU)(), - projectRoot: (0, _agent_bundle_runtime__rspack_import_41.qC)({ + command: (0, _agent_bundle_runtime__rspack_import_47.hU)(), + filesystem: (0, _agent_bundle_runtime__rspack_import_47.hU)(), + network: (0, _agent_bundle_runtime__rspack_import_47.hU)(), + projectRoot: (0, _agent_bundle_runtime__rspack_import_47.qC)({ root: cwd }, 'derived') }, - host: (0, _agent_bundle_runtime__rspack_import_41.hU)('unsupported-surface'), + host: (0, _agent_bundle_runtime__rspack_import_47.hU)('unsupported-surface'), invocation: message.request, - lineage: (0, _agent_bundle_runtime__rspack_import_41.hU)('unsupported-surface'), + lineage: (0, _agent_bundle_runtime__rspack_import_47.hU)('unsupported-surface'), plugin: pluginRoot.identity, progress: { report: async (update)=>{ @@ -26537,8 +31201,8 @@ const render = async (message)=>{ }, process: processHit, signal: controller.signal, - terminal: message.terminal === undefined ? (0, _agent_bundle_runtime__rspack_import_41.hU)('not-provided') : (0, _agent_bundle_runtime__rspack_import_41.qC)(message.terminal, 'native'), - workspace: (0, _agent_bundle_runtime__rspack_import_41.qC)({ + terminal: message.terminal === undefined ? (0, _agent_bundle_runtime__rspack_import_47.hU)('not-provided') : (0, _agent_bundle_runtime__rspack_import_47.qC)(message.terminal, 'native'), + workspace: (0, _agent_bundle_runtime__rspack_import_47.qC)({ root: cwd }, 'derived') }, async ()=>{ @@ -26547,7 +31211,7 @@ const render = async (message)=>{ type: 'observed-render-start' }); const renderStartedAt = performance.now(); - const flight = (0, _agent_bundle_runtime_flight_server__rspack_import_42.y)(composeLayouts(observedRoute, { + const flight = (0, _agent_bundle_runtime_flight_server__rspack_import_48.y)(composeLayouts(observedRoute, { ...message.props, signal: controller.signal }, controller.signal), { diff --git a/artifact/bin/gbot.mjs b/artifact/bin/gbot.mjs index 0d15ba4..e076601 100755 --- a/artifact/bin/gbot.mjs +++ b/artifact/bin/gbot.mjs @@ -7017,6 +7017,417 @@ var __webpack_modules__ = { resultSchema: resultSchema }); }, + "./src/cli/chatgpt-desktop/hosts.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const config = { + description: 'Discover ChatGPT Desktop hosts and modelProviders at runtime (never hardcode). Always includes local plus every remote-thread-summaries-v3:; Providers come from distinct thread/list and remote-summary values.', + exitCode: 'result', + inputJsonSchema: { + additionalProperties: false, + properties: { + port: { + description: 'Local CDP port', + type: 'number' + } + }, + type: 'object' + } + }; + const inputSchema = zod__rspack_import_2.Ikc({ + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional() + }).strict(); + async function chatgptDesktopHosts({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.Bm)(input); + const hosts = Array.isArray(out.hosts) ? out.hosts : []; + const text = hosts.length > 0 ? hosts.map((host)=>{ + const row = host; + const name = row.hostName && row.hostName !== row.hostId ? ` (${row.hostName})` : ''; + return `${row.hostId ?? '?'}${name} [${row.location ?? '?'}] threads=${row.threadCount ?? 0}`; + }).join('\n') + `\n\nhostsSource: ${String(out.hostsSource ?? '')}` + `\nmodelProviders: ${JSON.stringify(out.modelProviders ?? [])}` + `\nmodelProvidersSource: ${String(out.modelProvidersSource ?? '')}` : (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>chatgptDesktopHosts, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD + }, { + config: config, + inputSchema: inputSchema + }); + }, + "./src/cli/chatgpt-desktop/read.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const config = { + description: 'Read a ChatGPT Desktop thread via app-server (turns/list); CDP DOM is fallback only.', + exitCode: 'result', + positionals: [ + 'threadId' + ], + inputJsonSchema: { + additionalProperties: false, + properties: { + full: { + default: false, + type: 'boolean' + }, + cursor: { + description: 'Opaque nextCursor from the prior read page', + type: 'string' + }, + limit: { + description: 'Recent default 100; full default 2000', + type: 'number' + }, + openTimeoutMs: { + default: 90000, + type: 'number' + }, + port: { + type: 'number' + }, + threadId: { + type: 'string' + } + }, + required: [ + 'threadId' + ], + type: 'object' + }, + render: { + maxElapsedMs: 660000 + } + }; + const inputSchema = zod__rspack_import_2.Ikc({ + full: zod__rspack_import_2.zMY().default(false), + cursor: zod__rspack_import_2.YjP().min(1).max(4096).optional(), + limit: zod__rspack_import_2.aig().int().min(1).max(2000).optional(), + openTimeoutMs: zod__rspack_import_2.aig().int().min(1).max(600000).default(90000), + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional(), + threadId: zod__rspack_import_2.YjP().min(1).max(256) + }).strict(); + async function chatgptDesktopRead({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.UK)(input); + const turns = Array.isArray(out.turns) ? out.turns : []; + const text = turns.length > 0 ? turns.map((turn)=>{ + const row = turn; + const body = row.userText || row.assistantText ? [ + row.userText ? `user: ${row.userText}` : null, + row.assistantText ? `assistant: ${row.assistantText}` : null + ].filter(Boolean).join('\n') : row.text ?? ''; + return `[${row.role ?? '?'} ${row.turnKey ?? ''}]\n${body}`; + }).join('\n\n') + `\n\nbackend: ${String(out.backend ?? '')} full: ${String(out.full ?? false)}` : (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>chatgptDesktopRead, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD + }, { + config: config, + inputSchema: inputSchema + }); + }, + "./src/cli/chatgpt-desktop/search.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const config = { + description: 'Search ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.', + exitCode: 'result', + positionals: [ + 'query' + ], + inputJsonSchema: { + additionalProperties: false, + properties: { + groupBy: { + description: 'Group results by host (grouping mode, not a host id)', + enum: [ + 'host' + ], + type: 'string' + }, + host: { + description: 'Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string' + }, + limit: { + default: 50, + description: 'Max matches (1-200)', + type: 'number' + }, + cursor: { + description: 'Opaque nextCursor from a prior search page', + type: 'string' + }, + modelProvider: { + description: 'Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string' + }, + project: { + description: 'Project label or projectId filter.', + type: 'string' + }, + port: { + description: 'Local CDP port', + type: 'number' + }, + query: { + type: 'string' + } + }, + required: [ + 'query' + ], + type: 'object' + } + }; + const inputSchema = zod__rspack_import_2.Ikc({ + groupBy: zod__rspack_import_2.euz('host').optional(), + host: zod__rspack_import_2.YjP().min(1).max(256).optional(), + limit: zod__rspack_import_2.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_2.YjP().min(1).max(4096).optional(), + modelProvider: zod__rspack_import_2.YjP().min(1).max(256).optional(), + project: zod__rspack_import_2.YjP().min(1).max(256).optional(), + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional(), + query: zod__rspack_import_2.YjP().min(1).max(512) + }).strict(); + async function chatgptDesktopSearch({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.zg)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out) + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>chatgptDesktopSearch, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD + }, { + config: config, + inputSchema: inputSchema + }); + }, + "./src/cli/chatgpt-desktop/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const config = { + description: 'Send to ChatGPT Desktop via CDP composer. Omit --thread-id for a new chat; --project prefers Start new chat in .', + exitCode: 'result', + positionals: [ + 'text' + ], + inputJsonSchema: { + additionalProperties: false, + properties: { + openTimeoutMs: { + type: 'number' + }, + port: { + type: 'number' + }, + project: { + type: 'string' + }, + text: { + type: 'array', + items: { + type: 'string' + } + }, + threadId: { + type: 'string' + }, + unarchive: { + type: 'boolean', + description: 'Explicitly unarchive an existing archived thread before sending.' + } + }, + required: [ + 'text' + ], + type: 'object' + }, + render: { + maxElapsedMs: 660000 + } + }; + const inputSchema = zod__rspack_import_2.Ikc({ + openTimeoutMs: zod__rspack_import_2.aig().int().min(1).max(600000).default(90000), + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional(), + project: zod__rspack_import_2.YjP().min(1).max(256).optional(), + text: zod__rspack_import_2.YOg(zod__rspack_import_2.YjP()).min(1), + threadId: zod__rspack_import_2.YjP().min(1).max(256).optional(), + unarchive: zod__rspack_import_2.zMY().default(false) + }).strict(); + async function chatgptDesktopSend({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.UP)({ + port: input.port, + threadId: input.threadId, + text: input.text.join(' ').trim(), + project: input.project, + unarchive: input.unarchive, + openTimeoutMs: input.openTimeoutMs + }); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out) + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>chatgptDesktopSend, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD + }, { + config: config, + inputSchema: inputSchema + }); + }, + "./src/cli/chatgpt-desktop/status.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const config = { + description: 'Probe local ChatGPT Desktop CDP on 127.0.0.1 and report app-server availability for list/deep-read.', + exitCode: 'result', + inputJsonSchema: { + additionalProperties: false, + properties: { + port: { + description: 'Local CDP port (default 9222)', + type: 'number' + } + }, + type: 'object' + } + }; + const inputSchema = zod__rspack_import_2.Ikc({ + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional() + }).strict(); + async function chatgptDesktopStatus({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.Uj)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out) + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>chatgptDesktopStatus, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD + }, { + config: config, + inputSchema: inputSchema + }); + }, + "./src/cli/chatgpt-desktop/threads.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var zod__rspack_import_2 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const config = { + description: 'List local + remote-control ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.', + exitCode: 'result', + inputJsonSchema: { + additionalProperties: false, + properties: { + groupBy: { + description: 'Group results by host (grouping mode, not a host id)', + enum: [ + 'host' + ], + type: 'string' + }, + host: { + description: 'Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string' + }, + limit: { + default: 50, + description: 'Max threads (1-200)', + type: 'number' + }, + cursor: { + description: 'Opaque nextCursor from a previous page', + type: 'string' + }, + modelProvider: { + description: 'Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string' + }, + project: { + description: 'Project label or projectId filter.', + type: 'string' + }, + port: { + description: 'Local CDP port', + type: 'number' + } + }, + type: 'object' + } + }; + const inputSchema = zod__rspack_import_2.Ikc({ + groupBy: zod__rspack_import_2.euz('host').optional(), + host: zod__rspack_import_2.YjP().min(1).max(256).optional(), + limit: zod__rspack_import_2.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_2.YjP().min(1).max(4096).optional(), + modelProvider: zod__rspack_import_2.YjP().min(1).max(256).optional(), + project: zod__rspack_import_2.YjP().min(1).max(256).optional(), + port: zod__rspack_import_2.aig().int().min(1).max(65535).optional() + }).strict(); + async function chatgptDesktopThreads({ input }) { + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_1.tJ)(input); + const threads = Array.isArray(out.threads) ? out.threads : []; + const text = threads.length > 0 ? threads.map((thread)=>{ + const row = thread; + const where = row.location === 'remote' ? ` [remote ${row.hostName || row.hostId || '?'}]` : ' [local]'; + const provider = row.modelProvider ? ` {${row.modelProvider}}` : ''; + return `${row.selected ? '* ' : ' '}${row.threadId ?? ''} ${row.title ?? ''}` + (row.pinned ? ' (pinned)' : '') + where + provider; + }).join('\n') + `\n\nbackend: ${String(out.backend ?? '')}` + (out.nextCursor ? `\nnextCursor: ${out.nextCursor}` : '') : (0, _core_chatgpt_desktop_routes_js__rspack_import_1.D6)(out); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: text + }) + }); + } + __webpack_require__.d(__webpack_exports__, { + "default": ()=>chatgptDesktopThreads, + resultSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_1.FD + }, { + config: config, + inputSchema: inputSchema + }); + }, "./src/cli/claude/send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); @@ -7533,13 +7944,13 @@ var __webpack_modules__ = { var _core_format_js__rspack_import_6 = __webpack_require__("./src/core/format.js"); var _shared_js__rspack_import_3 = __webpack_require__("./src/cli/_shared.ts"); const config = { - description: 'List Codex daemon-managed threads.', + description: 'List Codex daemon-managed threads (all modelProviders; pages via --cursor / nextCursor).', exitCode: 'result', inputJsonSchema: { additionalProperties: false, properties: { cursor: { - description: 'Opaque pagination cursor (may start with -)', + description: 'Opaque pagination cursor from a previous nextCursor (may start with -)', type: 'string' }, limit: { @@ -7560,7 +7971,8 @@ var __webpack_modules__ = { exitCode: zod__rspack_import_4.euz(0), limit: zod__rspack_import_4.aig().int().min(1).max(200), nextCursor: zod__rspack_import_4.YjP().nullable(), - threads: zod__rspack_import_4.YOg(zod__rspack_import_4.g1P(zod__rspack_import_4.YjP(), zod__rspack_import_4.Pq9())) + threads: zod__rspack_import_4.YOg(zod__rspack_import_4.g1P(zod__rspack_import_4.YjP(), zod__rspack_import_4.Pq9())), + useStateDbOnly: zod__rspack_import_4.zMY().optional() }).strict(), _shared_js__rspack_import_3.R9 ]); @@ -9400,6 +9812,3818 @@ var __webpack_modules__ = { resultSchema: resultSchema }); }, + "./src/core/chatgpt-desktop/app-server-fallback.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_bridge_js__rspack_import_0 = __webpack_require__("./src/core/codex-bridge.js"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _remote_threads_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/remote-threads.ts"); + var _payload_budget_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/payload-budget.ts"); + var _thread_ids_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const LIST_PAGE_PREFIX = 'desktop-list-v1:'; + const SEARCH_PAGE_PREFIX = 'desktop-search-v1:'; + const READ_PAGE_PREFIX = 'desktop-read-v1:'; + async function appServerListThreads({ limit = 50, cursor, modelProviders = [] } = {}) { + let sourceCursor = cursor; + let skip = 0; + let fetchLimit = Math.min(limit, 25); + if (cursor?.startsWith(LIST_PAGE_PREFIX)) { + const state = (0, _payload_budget_js__rspack_import_4.i3)(LIST_PAGE_PREFIX, cursor); + if (!Number.isSafeInteger(state.skip) || state.skip < 0 || !Number.isSafeInteger(state.fetchLimit) || state.fetchLimit < 1 || state.fetchLimit > 25 || state.cursor !== null && typeof state.cursor !== 'string') { + throw new Error('Invalid ChatGPT Desktop list page cursor'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + fetchLimit = state.fetchLimit; + } + const base = { + backend: 'app-server', + limit, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined + }; + const threads = []; + const seen = new Set(); + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: fetchLimit, + cursor: sourceCursor, + modelProviders + }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop list page cursor is stale'); + for(let index = skip; index < out.threads.length; index++){ + let row = (0, _payload_budget_js__rspack_import_4.eX)(mapListedThread(out.threads[index])); + const probe = { + ...base, + threads: [ + ...threads, + row + ], + nextCursor: LIST_PAGE_PREFIX, + exitCode: 0 + }; + if ((0, _payload_budget_js__rspack_import_4.sQ)(probe) > 614400) { + if (threads.length) return { + ...base, + threads, + nextCursor: (0, _payload_budget_js__rspack_import_4.oI)(LIST_PAGE_PREFIX, { + cursor: sourceCursor ?? null, + skip: index, + fetchLimit + }), + warnings: [ + 'List page stopped at the MCP byte budget; continue with nextCursor' + ] + }; + row = (0, _payload_budget_js__rspack_import_4.MH)(row); + if ((0, _payload_budget_js__rspack_import_4.sQ)({ + ...base, + threads: [ + row + ], + nextCursor: LIST_PAGE_PREFIX, + exitCode: 0 + }) > 614400) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + threads.push(row); + if (threads.length >= limit) { + const nextCursor = index + 1 < out.threads.length ? (0, _payload_budget_js__rspack_import_4.oI)(LIST_PAGE_PREFIX, { + cursor: sourceCursor ?? null, + skip: index + 1, + fetchLimit + }) : out.nextCursor ?? null; + return { + ...base, + threads, + nextCursor + }; + } + } + if (out.nextCursor == null) return { + ...base, + threads, + nextCursor: null + }; + if (typeof out.nextCursor !== 'string' || !out.nextCursor || seen.has(out.nextCursor)) { + throw new Error('ChatGPT Desktop list returned an invalid or repeated cursor'); + } + seen.add(out.nextCursor); + sourceCursor = out.nextCursor; + skip = 0; + } + return { + ...base, + threads, + nextCursor: sourceCursor, + warnings: [ + 'List stopped after 400 app-server pages; continue with nextCursor' + ] + }; + } + async function appServerSearchThreads({ query, limit = 50, cursor, modelProviders = [], project }) { + const needle = query.trim().toLowerCase(); + if (!needle) { + return appServerListThreads({ + limit, + cursor, + modelProviders + }); + } + const matched = []; + let sourceCursor; + let skip = 0; + if (cursor) { + const state = (0, _payload_budget_js__rspack_import_4.i3)(SEARCH_PAGE_PREFIX, cursor); + if (state.query !== query || state.project !== project || JSON.stringify(state.modelProviders) !== JSON.stringify(modelProviders) || !Number.isSafeInteger(state.skip) || state.skip < 0) { + throw new Error('Search cursor does not match this query and its filters'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + } + const pageCursor = (rpcCursor, offset)=>(0, _payload_budget_js__rspack_import_4.oI)(SEARCH_PAGE_PREFIX, { + cursor: rpcCursor ?? null, + skip: offset, + query, + project, + modelProviders + }); + const base = { + backend: 'app-server', + limit, + query, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined + }; + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: 25, + cursor: sourceCursor, + modelProviders + }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop search cursor is stale'); + for(let index = skip; index < out.threads.length; index++){ + const mapped = mapListedThread(out.threads[index]); + if (!threadMatchesQuery(mapped, needle) || project && ![ + mapped.project, + mapped.projectId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(project.trim().toLowerCase()))) continue; + let row = (0, _payload_budget_js__rspack_import_4.eX)(mapped); + if ((0, _payload_budget_js__rspack_import_4.sQ)({ + ...base, + threads: [ + ...matched, + row + ], + nextCursor: SEARCH_PAGE_PREFIX, + exitCode: 0 + }) > 614400) { + if (matched.length) return { + ...base, + threads: matched, + nextCursor: pageCursor(sourceCursor, index), + warnings: [ + 'Search page stopped at the MCP byte budget; continue with nextCursor' + ] + }; + row = (0, _payload_budget_js__rspack_import_4.MH)(row); + } + matched.push(row); + if (matched.length >= limit) { + const nextCursor = index + 1 < out.threads.length ? pageCursor(sourceCursor, index + 1) : out.nextCursor ? pageCursor(out.nextCursor, 0) : null; + return { + ...base, + threads: matched, + nextCursor + }; + } + } + if (out.nextCursor == null) return { + ...base, + threads: matched, + nextCursor: null + }; + sourceCursor = out.nextCursor; + skip = 0; + } + return { + ...base, + threads: matched, + nextCursor: pageCursor(sourceCursor, 0), + warnings: [ + 'Search stopped after 400 app-server pages; continue with nextCursor' + ] + }; + } + async function discoverModelProviders() { + const providers = new Set(); + let threadCount = 0; + let cursor; + let complete = false; + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: 25, + cursor, + modelProviders: [] + }); + threadCount += out.threads.length; + for (const thread of out.threads){ + if (typeof thread.modelProvider === 'string' && thread.modelProvider) { + providers.add(thread.modelProvider); + } + } + if (out.nextCursor == null || typeof out.nextCursor !== 'string') { + complete = true; + break; + } + cursor = out.nextCursor; + } + return { + providers: [ + ...providers + ].sort(), + source: 'thread/list-distinct', + threadCount, + complete + }; + } + async function appServerReadThread({ threadId, limit, full = false, cursor }) { + limit ??= full ? 2000 : 100; + const bare = (0, _thread_ids_js__rspack_import_3.NM)(threadId); + const remote = (0, _remote_threads_js__rspack_import_2.KI)(bare); + if (remote) { + try { + return await readLocalThread(bare, { + limit, + full, + cursor + }); + } catch (error) { + if (isThreadNotLoaded(error) || isUnknownThread(error)) { + throw new _errors_js__rspack_import_1.Rg(bare, remote.hostId, remote.hostName); + } + throw maybeRemoteThreadError(error, bare); + } + } + try { + return await readLocalThread(bare, { + limit, + full, + cursor + }); + } catch (error) { + throw maybeRemoteThreadError(error, bare); + } + } + async function appServerUnarchiveThread(threadId) { + const bare = (0, _thread_ids_js__rspack_import_3.NM)(threadId); + const { client } = await (0, _codex_bridge_js__rspack_import_0.eC)(); + try { + const result = await client.request('thread/unarchive', { + threadId: bare + }); + if (!result || typeof result !== 'object' || !('thread' in result) || !result.thread || typeof result.thread !== 'object' || !('id' in result.thread) || result.thread.id !== bare) { + throw new Error(`thread/unarchive returned no thread for ${bare}`); + } + } finally{ + client.close(); + } + } + async function readLocalThread(bare, { limit, full, cursor }) { + const { client } = await (0, _codex_bridge_js__rspack_import_0.eC)(); + try { + const meta = await readThreadMetadata(client, bare); + const { turns, complete, nextCursor } = await listTurnsFull(client, bare, { + limit, + full, + cursor, + title: meta.title + }); + return { + threadId: (0, _thread_ids_js__rspack_import_3.BS)(bare), + turns, + backend: 'app-server', + limit, + full, + complete, + nextCursor, + ...complete ? {} : { + warnings: [ + 'Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment' + ] + }, + ...meta.title ? (0, _payload_budget_js__rspack_import_4.aA)(meta.title) : {}, + cwd: meta.cwd, + status: meta.status, + modelProvider: meta.modelProvider, + model: meta.model + }; + } finally{ + client.close(); + } + } + async function appServerStatusProbe() { + const status = await (0, _codex_bridge_js__rspack_import_0.MU)(); + return { + reachable: Boolean(status.reachable && status.mode === 'daemon'), + mode: typeof status.mode === 'string' ? status.mode : undefined, + socketPath: typeof status.socketPath === 'string' ? status.socketPath : undefined + }; + } + function mapListedThread(thread) { + const bare = String(thread.id ?? ''); + const section = thread.section && typeof thread.section.id === 'string' ? { + id: thread.section.id, + name: thread.section.name ?? null + } : null; + const pinned = section?.id === 'Pinned' || section?.name === 'Pinned' || false; + return { + threadId: bare ? (0, _thread_ids_js__rspack_import_3.BS)(bare) : '', + title: String(thread.name || thread.preview || thread.cwd || thread.id || ''), + pinned, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + preview: typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: thread.cwd ?? null, + createdAt: thread.createdAt ?? null, + updatedAt: thread.updatedAt ?? null, + section, + projectId: thread.projectId ?? null, + project: thread.projectId ?? undefined, + status: thread.status, + modelProvider: thread.modelProvider ?? null, + model: thread.model ?? null, + originator: thread.originator ?? null + }; + } + function threadMatchesQuery(thread, needle) { + const haystacks = [ + thread.threadId, + thread.title, + thread.preview, + thread.cwd, + thread.project, + thread.projectId, + thread.model, + thread.modelProvider, + thread.originator, + thread.section?.name, + thread.section?.id + ]; + return haystacks.some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + async function readThreadMetadata(client, threadId) { + try { + const read = await client.request('thread/read', { + threadId + }); + const thread = read.thread && typeof read.thread === 'object' ? read.thread : {}; + const statusObj = thread.status; + const statusType = statusObj && typeof statusObj === 'object' && typeof statusObj.type === 'string' ? String(statusObj.type) : typeof thread.status === 'string' ? thread.status : undefined; + return { + title: typeof thread.name === 'string' ? thread.name : typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: typeof thread.cwd === 'string' ? thread.cwd : null, + status: statusType, + modelProvider: typeof thread.modelProvider === 'string' ? thread.modelProvider : null, + model: typeof thread.model === 'string' ? thread.model : null + }; + } catch (error) { + throw maybeRemoteThreadError(error, threadId); + } + } + async function listTurnsFull(client, threadId, { limit, full, cursor, title }) { + const state = cursor ? (0, _payload_budget_js__rspack_import_4.i3)(READ_PAGE_PREFIX, cursor) : null; + if (state && (state.threadId !== threadId || state.full !== full || !Number.isSafeInteger(state.skip) || state.skip < 0 || state.rpcCursor !== null && typeof state.rpcCursor !== 'string' || state.field !== undefined && ![ + 'text', + 'userText', + 'assistantText' + ].includes(state.field) || state.offset !== undefined && (!Number.isSafeInteger(state.offset) || state.offset < 0))) { + throw new Error('Read cursor does not match this thread or mode'); + } + let rpcCursor = state?.rpcCursor ?? undefined; + let skip = state?.skip ?? 0; + let fragmentField = state?.field; + let fragmentOffset = state?.offset ?? 0; + let itemsView = true; + let method = 'thread/turns/list'; + const seen = new Set(); + const turns = []; + const pageCursor = (source, index, field, offset)=>(0, _payload_budget_js__rspack_import_4.oI)(READ_PAGE_PREFIX, { + threadId, + full, + rpcCursor: source ?? null, + skip: index, + ...field ? { + field, + offset + } : {} + }); + const fits = (candidate)=>(0, _payload_budget_js__rspack_import_4.sQ)({ + threadId, + turns: [ + ...turns, + candidate + ], + backend: 'app-server', + limit, + full, + complete: false, + nextCursor: READ_PAGE_PREFIX, + title: title ? (0, _payload_budget_js__rspack_import_4.aA)(title).title : undefined, + warnings: [ + 'Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment' + ], + exitCode: 0 + }) <= 614400; + while(turns.length < limit){ + let result; + try { + result = await client.request(method, { + threadId, + limit: Math.min(10, Math.max(limit - turns.length, skip + 1)), + sortDirection: full ? 'asc' : 'desc', + ...method === 'thread/turns/list' && itemsView ? { + itemsView: 'full' + } : {}, + ...rpcCursor !== undefined ? { + cursor: rpcCursor + } : {} + }); + } catch (error) { + const code = error?.rpc?.code; + if (turns.length === 0 && code === -32602 && itemsView) { + itemsView = false; + continue; + } + if (turns.length === 0 && code === -32601 && method === 'thread/turns/list') { + method = 'thread/items/list'; + continue; + } + throw maybeRemoteThreadError(error, threadId); + } + if (!Array.isArray(result?.data)) throw new Error(`${method} missing data array for ${threadId}`); + if (skip > result.data.length) throw new Error('Read cursor is stale'); + const pageTurns = normalizeTurns(result.data); + for(let index = skip; index < pageTurns.length; index++){ + const turn = pageTurns[index]; + if (!fragmentField && turns.length && !fits(turn)) { + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index) + }; + } + if (!fragmentField && fits(turn)) { + turns.push(turn); + } else { + const fields = [ + 'text', + 'userText', + 'assistantText' + ].filter((field)=>typeof turn[field] === 'string' && turn[field].length > 0); + let fieldIndex = fragmentField ? fields.indexOf(fragmentField) : 0; + if (fieldIndex < 0) throw new Error('Read fragment cursor is stale'); + for(; fieldIndex < fields.length; fieldIndex++){ + const field = fields[fieldIndex]; + const value = turn[field]; + let offset = field === fragmentField ? fragmentOffset : 0; + if (offset > value.length) throw new Error('Read fragment cursor is stale'); + while(offset < value.length){ + let size = Math.min(value.length - offset, 32768); + let fragment; + do { + let end = offset + size; + if (end < value.length && /[\uD800-\uDBFF]/u.test(value[end - 1])) end--; + const chunk = value.slice(offset, end); + fragment = { + turnKey: turn.turnKey, + role: turn.role, + text: field === 'text' ? chunk : '', + ...field === 'userText' ? { + userText: chunk + } : {}, + ...field === 'assistantText' ? { + assistantText: chunk + } : {}, + startedAt: turn.startedAt, + endedAt: turn.endedAt, + textTruncated: true, + continuation: { + field, + offsetChars: offset, + totalChars: value.length, + fieldComplete: end === value.length, + turnComplete: end === value.length && fieldIndex === fields.length - 1 + } + }; + if (fits(fragment)) { + size = end - offset; + break; + } + size = Math.floor(size / 2); + }while (size > 0); + if (size === 0) { + if (turns.length) return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) + }; + throw new Error('Read fragment metadata exceeds the MCP result byte budget'); + } + turns.push(fragment); + offset += size; + if (turns.length >= limit || offset < value.length && !fits({ + ...fragment, + text: 'x'.repeat(32768) + })) { + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) + }; + } + } + } + fragmentField = undefined; + fragmentOffset = 0; + } + if (turns.length >= limit) { + const nextCursor = index + 1 < pageTurns.length ? pageCursor(rpcCursor, index + 1) : result.nextCursor ? pageCursor(result.nextCursor, 0) : null; + return { + turns: chronologicalPage(turns, full), + complete: nextCursor === null, + nextCursor + }; + } + } + if (result.nextCursor == null) return { + turns: chronologicalPage(turns, full), + complete: true, + nextCursor: null + }; + if (typeof result.nextCursor !== 'string' || !result.nextCursor || seen.has(result.nextCursor)) { + throw new Error(`${method} returned an invalid or repeated cursor`); + } + seen.add(result.nextCursor); + rpcCursor = result.nextCursor; + skip = 0; + } + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, skip) + }; + } + function chronologicalPage(turns, full) { + if (full) return turns; + const groups = []; + for (const turn of turns){ + const last = groups.at(-1); + if (last?.[0]?.turnKey === turn.turnKey) last.push(turn); + else groups.push([ + turn + ]); + } + return groups.reverse().flat(); + } + function normalizeTurns(raw) { + const turns = []; + for (const [index, item] of raw.entries()){ + if (!item || typeof item !== 'object') continue; + const row = item; + const id = String(row.id ?? row.turnId ?? index); + const items = Array.isArray(row.items) ? row.items : null; + if (items) { + turns.push(normalizeTurnWithItems(id, row, items)); + continue; + } + turns.push(normalizeLeafItem(id, row)); + } + return turns; + } + function normalizeTurnWithItems(id, row, items) { + let userText = ''; + let assistantText = ''; + let toolText = ''; + let reasoningText = ''; + for (const entry of items){ + if (!entry || typeof entry !== 'object') continue; + const item = entry; + const type = String(item.type ?? item.role ?? ''); + const text = extractText(item); + if (/user/i.test(type)) userText = joinText(userText, text); + else if (/agent|assistant|message/i.test(type) && !/reasoning|tool/i.test(type)) { + assistantText = joinText(assistantText, text); + } else if (/tool/i.test(type)) toolText = joinText(toolText, text || type); + else if (/reasoning/i.test(type)) reasoningText = joinText(reasoningText, text); + } + let role = 'assistant'; + let text = assistantText || userText; + if (userText && !assistantText) { + role = 'user'; + text = userText; + } else if (!userText && !assistantText && toolText) { + role = 'tool'; + text = toolText; + } else if (!userText && !assistantText && reasoningText) { + role = 'reasoning'; + text = reasoningText; + } + return { + turnKey: (0, _thread_ids_js__rspack_import_3.Sr)(id), + role, + text, + ...userText ? { + userText + } : {}, + ...assistantText ? { + assistantText + } : {}, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null + }; + } + function normalizeLeafItem(id, row) { + const text = extractText(row); + const type = String(row.type ?? row.role ?? ''); + let role = 'assistant'; + if (/user/i.test(type)) role = 'user'; + else if (/tool/i.test(type)) role = 'tool'; + else if (/reasoning/i.test(type)) role = 'reasoning'; + else if (/status|system/i.test(type) && !text) role = 'status'; + return { + turnKey: (0, _thread_ids_js__rspack_import_3.Sr)(id), + role, + text, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null + }; + } + function extractText(row) { + if (typeof row.text === 'string') return row.text; + if (typeof row.content === 'string') return row.content; + if (Array.isArray(row.content)) return row.content.filter((part)=>Boolean(part) && typeof part === 'object' && !Array.isArray(part)).map((part)=>typeof part.text === 'string' ? part.text : '').filter(Boolean).join('\n'); + if (typeof row.preview === 'string') return row.preview; + return ''; + } + function joinText(left, right) { + if (!right) return left; + if (!left) return right; + return `${left}\n${right}`; + } + function isThreadNotLoaded(error) { + const message = errorMessage(error); + return /thread not loaded|not loaded|invalid thread id/i.test(message); + } + function isUnknownThread(error) { + const message = errorMessage(error); + return /no rollout found|thread not found|unknown.*thread/i.test(message); + } + function maybeRemoteThreadError(error, threadId) { + const message = errorMessage(error); + if (/thread not loaded|not loaded|no rollout found|thread not found/i.test(message)) { + const remote = (0, _remote_threads_js__rspack_import_2.KI)(threadId); + if (remote) { + return new _errors_js__rspack_import_1.Rg(threadId, remote.hostId, remote.hostName); + } + const hostId = (0, _remote_threads_js__rspack_import_2.XI)(threadId); + if (hostId) return new _errors_js__rspack_import_1.Rg(threadId, hostId, null); + } + if (error instanceof Error) return error; + return new Error(message || `app-server error for thread ${threadId}`); + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) { + return String(error.message); + } + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); + } + __webpack_require__.d(__webpack_exports__, { + Jf: ()=>appServerReadThread, + OI: ()=>appServerListThreads, + X: ()=>discoverModelProviders, + aI: ()=>appServerSearchThreads, + dM: ()=>appServerStatusProbe, + qL: ()=>appServerUnarchiveThread + }); + }, + "./src/core/chatgpt-desktop/cdp-adapter.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _cdp_session_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/cdp-session.ts"); + var _cdp_dom_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/cdp-dom.ts"); + var _thread_ids_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + var _loopback_js__rspack_import_5 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + var _project_state_js__rspack_import_6 = __webpack_require__("./src/core/chatgpt-desktop/project-state.ts"); + class CdpChatGptDesktopAdapter { + #port = (0, _loopback_js__rspack_import_5.L0)(); + #session = null; + #version = null; + #pageSessionId = null; + #mainTargetId = null; + #submittedTurn = null; + async connect({ port }) { + await this.close(); + this.#port = (0, _loopback_js__rspack_import_5.L0)(process.env, port); + const session = new _cdp_session_js__rspack_import_2.a(this.#port); + try { + this.#version = await session.connect(); + this.#session = session; + await this.#attachMainWindow(); + } catch (error) { + session.close(); + this.#session = null; + throw error; + } + } + async listTargets() { + const session = this.#requireSession(); + const result = await session.send('Target.getTargets'); + return (0, _cdp_dom_js__rspack_import_3.Mr)(result.targetInfos ?? []); + } + async listThreads({ limit = 50 } = {}) { + const sessionId = await this.#ensurePageSession(); + const threads = await (0, _cdp_dom_js__rspack_import_3.AY)(this.#requireSession(), sessionId, { + limit + }); + return { + threads, + backend: 'cdp', + limit + }; + } + async readThread({ threadId, limit = 100, full = false, openTimeoutMs = 90000 }) { + const sessionId = await this.#ensurePageSession(); + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + const turns = await (0, _cdp_dom_js__rspack_import_3.JU)(this.#requireSession(), sessionId, { + limit, + full, + threadId + }); + return { + threadId, + turns, + backend: 'cdp', + limit, + full, + complete: false + }; + } + async sendMessage({ threadId, text, project, openTimeoutMs = 90000 }) { + const sessionId = await this.#ensurePageSession(); + let temporaryThreadId; + const startedNew = !threadId; + if (threadId) { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } else { + const localProject = project ? (0, _project_state_js__rspack_import_6.q)(project) : null; + if (localProject && !localProject.rootPaths.some((path)=>(0, node_fs__rspack_import_0.existsSync)(path))) { + throw new _errors_js__rspack_import_1.Kn(localProject.label, `none of its configured workspace roots exists: ${localProject.rootPaths.map((path)=>JSON.stringify(path)).join(', ') || '(none)'}. Update the Desktop project root before sending.`); + } + await (0, _cdp_dom_js__rspack_import_3.s0)(this.#requireSession(), sessionId, { + project: localProject?.label ?? project, + projectId: localProject?.id + }); + const threads = await (0, _cdp_dom_js__rspack_import_3.AY)(this.#requireSession(), sessionId, { + limit: 20 + }); + const temp = threads.find((t)=>t.threadId.startsWith(_cdp_dom_js__rspack_import_3.hA) && t.selected) ?? threads.find((t)=>t.threadId.startsWith(_cdp_dom_js__rspack_import_3.hA)); + if (temp) temporaryThreadId = temp.threadId; + } + const before = await (0, _cdp_dom_js__rspack_import_3.YX)(this.#requireSession(), sessionId); + const { sentVia } = await (0, _cdp_dom_js__rspack_import_3.lh)(this.#requireSession(), sessionId, text); + let conversationId; + if (startedNew || threadId && (0, _thread_ids_js__rspack_import_4.vr)(threadId)) { + const resolveMs = Math.min(openTimeoutMs, 60000); + try { + conversationId = await (0, _cdp_dom_js__rspack_import_3.z9)(this.#requireSession(), sessionId, { + timeoutMs: resolveMs + }); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', + reason: 'conversation-id-unresolved' + }); + } + } + const durable = (0, _cdp_dom_js__rspack_import_3.X8)(conversationId, threadId); + if (!durable) { + throw new Error('ChatGPT Desktop send accepted but no durable conversation id was available; ' + 'refusing to return a local:client-new-thread:* id as threadId'); + } + this.#submittedTurn = { + threadId: durable, + turnKey: before.lastTurnKey + }; + return { + threadId: durable, + temporaryThreadId, + conversationId: conversationId || (0, _thread_ids_js__rspack_import_4.nn)(durable) || durable, + project, + backend: 'cdp', + experimental: false, + delivery: 'accepted', + sentVia, + message: 'Submitted via CDP composer (focus → insertText → Enter)' + }; + } + async waitForReply({ threadId, timeoutMs = 120000 }) { + const deadline = Date.now() + timeoutMs; + const sessionId = await this.#ensurePageSession(); + const expectedId = threadId && threadId !== 'new' ? (0, _thread_ids_js__rspack_import_4.nn)(threadId) ?? undefined : this.#submittedTurn ? (0, _thread_ids_js__rspack_import_4.nn)(this.#submittedTurn.threadId) ?? undefined : undefined; + if (threadId && !(0, _thread_ids_js__rspack_import_4.vr)(threadId) && threadId !== 'new') { + await this.#openAndWait(sessionId, threadId, Math.min(timeoutMs, 90000)); + } + try { + const done = await (0, _cdp_dom_js__rspack_import_3.FG)(this.#requireSession(), sessionId, { + timeoutMs: Math.max(0, deadline - Date.now()), + expectedConversationId: expectedId, + baselineTurnKey: this.#submittedTurn && (!threadId || (0, _thread_ids_js__rspack_import_4.BS)(threadId) === this.#submittedTurn.threadId) ? this.#submittedTurn.turnKey : undefined + }); + const durable = (0, _cdp_dom_js__rspack_import_3.X8)(done.conversationId, threadId) ?? (done.conversationId ? (0, _thread_ids_js__rspack_import_4.BS)(done.conversationId) : null); + if (!durable) { + throw new Error('ChatGPT Desktop reply finished without a durable conversation id on ' + '[data-response-annotation-conversation]'); + } + return { + threadId: durable, + conversationId: done.conversationId || (0, _thread_ids_js__rspack_import_4.nn)(durable) || durable, + reply: done.reply, + backend: 'cdp', + experimental: false, + delivery: 'replied' + }; + } catch (error) { + if (!(error instanceof _cdp_dom_js__rspack_import_3.aw)) throw error; + return { + threadId: threadId ?? 'unknown', + reply: '', + backend: 'cdp', + experimental: false, + delivery: 'timeout' + }; + } + } + async openThread(threadId, { openTimeoutMs = 90000 } = {}) { + const sessionId = await this.#ensurePageSession(); + try { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } catch (error) { + if (error instanceof _errors_js__rspack_import_1.fw) return { + threadId, + backend: 'cdp', + archived: true + }; + throw error; + } + return { + threadId, + backend: 'cdp' + }; + } + async status() { + try { + const version = this.#version ?? await _cdp_session_js__rspack_import_2.a.fetchVersion(this.#port); + let targetCount; + let mainWindowTargetId; + if (this.#session && !this.#session.closed) { + const targets = await this.listTargets(); + targetCount = targets.length; + mainWindowTargetId = (0, _cdp_dom_js__rspack_import_3.C2)(targets)?.targetId ?? null; + } else { + const list = await _cdp_session_js__rspack_import_2.a.fetchJsonList(this.#port); + targetCount = list.length; + } + return { + reachable: true, + port: this.#port, + host: '127.0.0.1', + browser: typeof version.Browser === 'string' ? version.Browser : undefined, + protocolVersion: typeof version['Protocol-Version'] === 'string' ? version['Protocol-Version'] : undefined, + webSocketDebuggerUrl: typeof version.webSocketDebuggerUrl === 'string' ? version.webSocketDebuggerUrl : undefined, + targetCount, + mainWindowTargetId: mainWindowTargetId ?? this.#mainTargetId, + message: 'ChatGPT Desktop CDP endpoint reachable on 127.0.0.1', + exitCode: 0 + }; + } catch (error) { + return { + reachable: false, + port: this.#port, + host: '127.0.0.1', + message: error instanceof Error ? error.message : 'ChatGPT Desktop CDP endpoint unreachable on 127.0.0.1', + exitCode: 1 + }; + } + } + async close() { + this.#pageSessionId = null; + this.#mainTargetId = null; + this.#version = null; + this.#session?.close(); + this.#session = null; + } + async #openAndWait(sessionId, threadId, openTimeoutMs) { + await (0, _cdp_dom_js__rspack_import_3.Kl)(this.#requireSession(), sessionId, threadId); + await (0, _cdp_dom_js__rspack_import_3.Zx)(this.#requireSession(), sessionId, { + timeoutMs: openTimeoutMs, + threadId + }); + } + async #attachMainWindow() { + const targets = await this.listTargets(); + const main = (0, _cdp_dom_js__rspack_import_3.C2)(targets); + if (!main) { + throw new _errors_js__rspack_import_1.sO(`No ChatGPT Desktop main window target with url ${JSON.stringify('app://-/index.html')}`); + } + const attached = await this.#requireSession().send('Target.attachToTarget', { + targetId: main.targetId, + flatten: true + }); + this.#mainTargetId = main.targetId; + this.#pageSessionId = attached.sessionId; + } + async #ensurePageSession() { + this.#requireSession(); + if (!this.#pageSessionId) await this.#attachMainWindow(); + if (!this.#pageSessionId) { + throw new _errors_js__rspack_import_1.sO('Failed to attach to ChatGPT Desktop main window'); + } + return this.#pageSessionId; + } + #requireSession() { + if (!this.#session || this.#session.closed) { + throw new _errors_js__rspack_import_1.sO('Not connected to ChatGPT Desktop CDP. Call connect({ port }) after relaunching with --remote-debugging-port.'); + } + return this.#session; + } + } + __webpack_require__.d(__webpack_exports__, { + a: ()=>CdpChatGptDesktopAdapter + }); + }, + "./src/core/chatgpt-desktop/cdp-dom.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _codex_thread_id_js__rspack_import_1 = __webpack_require__("./src/core/codex/thread-id.js"); + const MAIN_WINDOW_URL = 'app://-/index.html'; + const IGNORED_TARGET_URL_MARKERS = [ + 'initialRoute=%2Favatar-overlay', + 'detached-window.html', + 'chatgpt.com', + 'codex-sandbox' + ]; + const ATTR = { + threadRow: 'data-app-action-sidebar-thread-row', + threadId: 'data-app-action-sidebar-thread-id', + threadTitle: 'data-app-action-sidebar-thread-title', + threadPinned: 'data-app-action-sidebar-thread-pinned', + threadSelected: 'data-app-action-sidebar-thread-selected', + threadKind: 'data-app-action-sidebar-thread-kind', + turnKey: 'data-turn-key', + userBubble: 'data-user-message-bubble', + timelineScroll: 'data-app-action-timeline-scroll', + virtualizedTurn: 'data-virtualized-turn-content', + composer: 'data-codex-composer', + finalAssistant: 'data-local-conversation-final-assistant', + markdownTextStyle: 'data-markdown-text-style', + conversationAnnotation: 'data-response-annotation-conversation' + }; + const MAIN_CONTENT_SURFACE_CLASS_PREFIX = '_MainContentSurface'; + const HISTORY_GAP_PREFIX = 'history-gap:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const LOADING_TASK_TEXT = 'Loading task…'; + const DEFAULT_OPEN_TIMEOUT_MS = 90000; + const DEFAULT_CONVERSATION_RESOLVE_MS = 60000; + const DEFAULT_FULL_READ_IDLE_WHEELS = 8; + const DEFAULT_FULL_READ_MAX_WHEELS = 400; + const FULL_READ_WHEEL_DELTA_Y = -800; + const COMPOSER_SELECTOR = `[${ATTR.composer}="true"][contenteditable="true"]`; + const SEND_BUTTON_SELECTOR = 'button[aria-label="Send"]'; + const STOP_BUTTON_SELECTOR = 'main button[aria-label="Stop"]'; + const FINAL_ASSISTANT_SELECTOR = `[${ATTR.finalAssistant}="true"]`; + const ASSISTANT_MESSAGE_SELECTOR = `[${ATTR.finalAssistant}="true"] [${ATTR.markdownTextStyle}="assistant-message"]`; + const CONVERSATION_ANNOTATION_SELECTOR = `[${ATTR.conversationAnnotation}]`; + const SELECTORS = { + threadRow: `[role="button"][${ATTR.threadRow}]`, + turn: `[${ATTR.turnKey}]`, + userBubble: `[${ATTR.userBubble}]`, + timelineScroll: `[${ATTR.timelineScroll}]`, + composer: COMPOSER_SELECTOR, + sendButton: SEND_BUTTON_SELECTOR, + stopButton: STOP_BUTTON_SELECTOR, + finalAssistant: FINAL_ASSISTANT_SELECTOR, + assistantMessage: ASSISTANT_MESSAGE_SELECTOR, + conversationAnnotation: CONVERSATION_ANNOTATION_SELECTOR, + newChatFallback: 'button' + }; + function newChatInProjectSelector(project) { + return `button[aria-label=${JSON.stringify(`Start new chat in ${project}`)}]`; + } + function isMainWindowTarget(target) { + if (target.type !== 'page') return false; + const url = typeof target.url === 'string' ? target.url : ''; + if (url !== MAIN_WINDOW_URL) return false; + return !IGNORED_TARGET_URL_MARKERS.some((marker)=>url.includes(marker)); + } + function summarizeTargetInfos(targetInfos) { + return targetInfos.filter((t)=>typeof t.targetId === 'string' && typeof t.type === 'string').map((t)=>({ + targetId: String(t.targetId), + type: String(t.type), + title: typeof t.title === 'string' ? t.title : '', + url: typeof t.url === 'string' ? t.url : '', + attached: Boolean(t.attached) + })); + } + function pickMainWindowTarget(targets) { + return targets.find((t)=>isMainWindowTarget(t)) ?? null; + } + function dedupeThreadsById(threads) { + const byKey = new Map(); + for (const thread of threads){ + if (!thread.threadId) continue; + const key = durableThreadKey(thread.threadId); + const existing = byKey.get(key); + if (!existing) { + byKey.set(key, thread); + continue; + } + byKey.set(key, preferThreadRow(existing, thread)); + } + return [ + ...byKey.values() + ]; + } + function durableThreadKey(threadId) { + if ((0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) return threadId; + try { + return (0, _codex_thread_id_js__rspack_import_1.SX)(threadId); + } catch { + return threadId.startsWith("local:") ? threadId.slice("local:".length) || threadId : threadId; + } + } + function toDesktopForm(threadId) { + if (!threadId || (0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) return threadId; + if (threadId.startsWith("local:")) return threadId; + return `${"local:"}${threadId}`; + } + function preferThreadRow(left, right) { + const score = (row)=>(row.selected ? 4 : 0) + (row.pinned ? 2 : 0) + (row.threadId.startsWith("local:") && !(0, _codex_thread_id_js__rspack_import_1.vr)(row.threadId) ? 1 : 0); + return score(right) > score(left) ? right : left; + } + const LIST_THREADS_EXPRESSION = `(() => { + const skipProjectLabels = new Set(['Recent', 'Pinned', 'Threads', 'Chats']); + const projectFor = (row) => { + let el = row.parentElement; + for (let i = 0; i < 8 && el; i += 1, el = el.parentElement) { + const attr = + el.getAttribute('data-app-action-sidebar-project') || + el.getAttribute('data-app-action-sidebar-project-name') || + el.getAttribute('data-app-action-sidebar-section-title'); + if (attr && !skipProjectLabels.has(attr)) return attr; + } + return undefined; + }; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const seen = new Set(); + const out = []; + for (const row of rows) { + const threadId = row.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''; + if (!threadId || seen.has(threadId)) continue; + seen.add(threadId); + const project = projectFor(row); + out.push({ + threadId, + hostId: row.getAttribute('data-app-action-sidebar-thread-host-id') || null, + title: row.getAttribute(${JSON.stringify(ATTR.threadTitle)}) || (row.textContent || '').trim(), + pinned: row.getAttribute(${JSON.stringify(ATTR.threadPinned)}) === 'true', + selected: row.getAttribute(${JSON.stringify(ATTR.threadSelected)}) === 'true', + kind: row.getAttribute(${JSON.stringify(ATTR.threadKind)}) || 'unknown', + ...(project ? { project } : {}), + }); + } + return out; +})()`; + function openThreadExpression(threadId) { + const bare = durableThreadKey(threadId); + const desktop = toDesktopForm(bare); + const candidates = [ + ...new Set([ + threadId, + bare, + desktop + ].filter(Boolean)) + ]; + return `(() => { + const candidates = ${JSON.stringify(candidates)}; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const row = rows.find((el) => + candidates.includes(el.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''), + ); + if (!row) return { ok: false, error: 'thread-not-found' }; + row.click(); + return { ok: true, threadId: row.getAttribute(${JSON.stringify(ATTR.threadId)}) || candidates[0] }; + })()`; + } + function navigateToThreadExpression(threadId) { + const bare = durableThreadKey(threadId); + return `(async () => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; + let scanned = 0; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); + if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { + await router.navigate(${JSON.stringify(`/local/${bare}`)}); + return { ok: true, via: 'router' }; + } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + return { ok: false, error: 'router-not-found' }; + })()`; + } + function startNewChatExpression(project, projectId) { + const projectSel = project ? newChatInProjectSelector(project) : null; + return `(() => { + const projectSel = ${JSON.stringify(projectSel)}; + if (projectSel) { + const preferred = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(projectSel) ||` : ''} document.querySelector(projectSel); + if (preferred) { + const row = preferred.closest('[data-app-action-sidebar-project-row]') || preferred.parentElement; + row?.scrollIntoView({ block: 'center' }); + const rowBox = row?.getBoundingClientRect(); + return { ok: true, via: 'project', project: ${JSON.stringify(project ?? null)}, + disabled: preferred.disabled || preferred.getAttribute('aria-disabled') === 'true', + hoverX: rowBox?.left + rowBox?.width / 2, hoverY: rowBox?.top + rowBox?.height / 2 }; + } + return { ok: false, error: 'project-not-found' }; + } + const buttons = Array.from(document.querySelectorAll('button')); + const fallback = buttons.find((btn) => (btn.textContent || '').trim() === 'New chat'); + if (!fallback) return { ok: false, error: 'new-chat-not-found' }; + fallback.click(); + return { ok: true, via: 'fallback' }; + })()`; + } + function projectButtonBoxExpression(project, projectId) { + return `(() => { + const button = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(${JSON.stringify(newChatInProjectSelector(project))}) ||` : ''} document.querySelector(${JSON.stringify(newChatInProjectSelector(project))}); + if (!button) return null; + const box = button.getBoundingClientRect(); + return { x: box.left + box.width / 2, y: box.top + box.height / 2, + disabled: button.disabled || button.getAttribute('aria-disabled') === 'true', + visible: box.width > 0 && box.height > 0 && box.x >= 0 && box.y >= 0 + && box.x < innerWidth && box.y < innerHeight && getComputedStyle(button.parentElement).opacity !== '0' }; + })()`; + } + function newChatViewExpression(project) { + return `(() => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; let scanned = 0; let path = ''; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { path = router.state.location.pathname; break; } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + const composer = !!document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + const scope = document.querySelector('[data-composer-navigation-target="workspace-project"]')?.getAttribute('aria-label') || ''; + const expected = ${JSON.stringify(project ? `Change project: ${project}` : '')}; + return { ready: path === '/' && composer && (expected ? scope === expected : (!scope || scope === 'Choose project')), path, composer, scope }; + })()`; + } + const CLEAR_NEW_CHAT_PROJECT_EXPRESSION = `(() => { + const button = document.querySelector('[data-clear-project-button="true"]'); + if (!button || button.disabled) return false; + button.click(); + return true; +})()`; + const LOADING_TASK_GONE_EXPRESSION = `(() => { + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const text = (surface.innerText || '').replace(/\\u2026/g, '…'); + return !text.includes(${JSON.stringify(LOADING_TASK_TEXT)}); +})()`; + const ARCHIVED_THREAD_EXPRESSION = `(() => Array.from(document.querySelectorAll('main')) + .some((main) => (main.innerText || '').includes('This task is archived') + && (main.innerText || '').includes('Unarchive this task to open it')))()`; + const FOCUS_COMPOSER_EXPRESSION = `(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + if (!composer) return { ok: false, error: 'composer-not-found' }; + if ((composer.textContent || '').trim()) return { ok: false, error: 'composer-has-draft' }; + if (document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)})) return { ok: false, error: 'reply-in-progress' }; + composer.focus(); + return { ok: true }; +})()`; + const REPLY_STATE_EXPRESSION = `(() => { + const stop = document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)}); + const finals = Array.from(document.querySelectorAll(${JSON.stringify(FINAL_ASSISTANT_SELECTOR)})); + const lastFinal = finals.at(-1) || null; + const annotation = lastFinal + ? lastFinal.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + || lastFinal.closest(${JSON.stringify(SELECTORS.turn)})?.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + : document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + const conversationId = annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; + const messageEl = lastFinal + ? lastFinal.querySelector(${JSON.stringify(`[${ATTR.markdownTextStyle}="assistant-message"]`)}) + : null; + const reply = messageEl ? (messageEl.innerText || '').trim() : ''; + const turns = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.turn)})); + const lastTurn = turns.at(-1); + const lastTurnKey = lastTurn ? (lastTurn.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '') : ''; + return { + stopVisible: Boolean(stop), + finalAssistantCount: finals.length, + conversationId, + reply, + lastTurnKey, + finalTurnKey: lastFinal?.closest(${JSON.stringify(SELECTORS.turn)})?.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '', + }; +})()`; + const CONVERSATION_ID_EXPRESSION = `(() => { + const annotation = document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + return annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; +})()`; + const HARVEST_VISIBLE_TURNS_EXPRESSION = `(() => { + const gapPrefix = ${JSON.stringify(HISTORY_GAP_PREFIX)}; + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const scroll = + surface.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}) + || document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + const root = scroll || surface; + const out = []; + for (const el of root.querySelectorAll(${JSON.stringify(SELECTORS.turn)})) { + const turnKey = el.getAttribute(${JSON.stringify(ATTR.turnKey)}) || ''; + if (!turnKey || turnKey.startsWith(gapPrefix)) continue; + const user = el.querySelector(${JSON.stringify(SELECTORS.userBubble)}); + const assistant = el.querySelector(${JSON.stringify(ASSISTANT_MESSAGE_SELECTOR)}); + const userText = user ? (user.innerText || '').trim() : ''; + const assistantText = assistant ? (assistant.innerText || '').trim() : ''; + const raw = (el.innerText || '').trim(); + const statusRe = /^Worked for \\d+s$/i; + let role = 'assistant'; + let text = assistantText || userText || raw; + if (user && !assistantText) { + role = 'user'; + text = userText || raw; + } else if (assistantText) { + role = 'assistant'; + text = assistantText; + } else if (!raw || statusRe.test(raw)) { + role = 'status'; + text = raw; + } + out.push({ turnKey, role, text, userText, assistantText }); + } + return out; +})()`; + const TIMELINE_SCROLL_METRICS_EXPRESSION = `(() => { + const scroll = document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + if (!scroll) return { ok: false }; + const rect = scroll.getBoundingClientRect(); + return { + ok: true, + x: Math.floor(rect.left + rect.width / 2), + y: Math.floor(rect.top + Math.min(rect.height / 2, 120)), + scrollTop: scroll.scrollTop, + scrollHeight: scroll.scrollHeight, + clientHeight: scroll.clientHeight, + }; +})()`; + async function listThreadsFromDom(session, sessionId, { limit = 50 } = {}) { + const rows = await session.evaluate(LIST_THREADS_EXPRESSION, { + sessionId + }); + if (!Array.isArray(rows)) return []; + return dedupeThreadsById(rows).slice(0, limit).map((row)=>({ + threadId: String(row.threadId), + title: String(row.title ?? ''), + pinned: Boolean(row.pinned), + selected: Boolean(row.selected), + kind: String(row.kind ?? 'unknown'), + ...row.hostId ? { + hostId: row.hostId, + location: row.hostId === 'local' ? 'local' : 'remote' + } : {}, + ...row.project ? { + project: String(row.project) + } : {} + })); + } + async function openThreadInDom(session, sessionId, threadId) { + const current = await readConversationIdFromDom(session, sessionId); + if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + if ((0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { + limit: 200 + }); + if (rows.some((row)=>row.selected && row.threadId === threadId)) return; + } + const result = await session.evaluate(openThreadExpression(threadId), { + sessionId + }); + if (!result?.ok) { + if (result?.error === 'thread-not-found' && !(0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const navigated = await session.evaluate(navigateToThreadExpression(threadId), { + sessionId + }); + if (navigated?.ok) return; + } + throw new Error(result?.error === 'thread-not-found' ? `ChatGPT Desktop thread not found: ${threadId}` : `Failed to open ChatGPT Desktop thread ${threadId}`); + } + } + async function waitForLoadingTaskGone(session, sessionId, { timeoutMs = DEFAULT_OPEN_TIMEOUT_MS, threadId } = {}) { + const deadline = Date.now() + timeoutMs; + let loadingGone = false; + let lastConversationId = ''; + let archivedSince = null; + while(Date.now() < deadline){ + if (threadId && threadId !== 'new' && await session.evaluate(ARCHIVED_THREAD_EXPRESSION, { + sessionId + }) === true) { + archivedSince ??= Date.now(); + if (Date.now() - archivedSince >= 500) { + throw new _errors_js__rspack_import_0.fw(threadId); + } + } else { + archivedSince = null; + } + const gone = await session.evaluate(LOADING_TASK_GONE_EXPRESSION, { + sessionId + }); + loadingGone ||= gone; + if (gone) { + const current = threadId ? await readConversationIdFromDom(session, sessionId) : ''; + lastConversationId = current; + if (!threadId) return; + if (threadId === 'new' || (0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { + limit: 200 + }); + const selected = rows.find((row)=>row.selected); + if (!current && (!selected || (0, _codex_thread_id_js__rspack_import_1.vr)(selected.threadId))) return; + } else if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + } + await delay(250); + } + throw new Error(loadingGone ? `ChatGPT Desktop timed out waiting for conversation ${JSON.stringify(threadId)}; displayed conversation ${JSON.stringify(lastConversationId)} after ${timeoutMs}ms` : `ChatGPT Desktop thread still showing ${JSON.stringify(LOADING_TASK_TEXT)} after ${timeoutMs}ms`); + } + async function startNewChatInDom(session, sessionId, { project, projectId, navigationTimeoutMs = 3000 } = {}) { + const result = await session.evaluate(startNewChatExpression(project, projectId), { + sessionId + }); + if (!result?.ok) { + throw new Error(project ? `ChatGPT Desktop new-chat button not found for project ${JSON.stringify(project)}` : 'ChatGPT Desktop "New chat" button not found'); + } + if (result.via === 'project') { + if (result.disabled) throw new _errors_js__rspack_import_0.Kn(project, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (![ + result.hoverX, + result.hoverY + ].every(Number.isFinite)) { + throw new Error(`ChatGPT Desktop project button has no visible box for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: result.hoverX, + y: result.hoverY + }, { + sessionId + }); + const button = await session.evaluate(projectButtonBoxExpression(project, projectId), { + sessionId + }); + if (button?.disabled) throw new _errors_js__rspack_import_0.Kn(project, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (!button?.visible || !Number.isFinite(button.x) || !Number.isFinite(button.y)) { + throw new Error(`ChatGPT Desktop project button did not appear after hover for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: button.x, + y: button.y + }, { + sessionId + }); + await session.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: button.x, + y: button.y, + button: 'left', + clickCount: 1 + }, { + sessionId + }); + await session.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: button.x, + y: button.y, + button: 'left', + clickCount: 1 + }, { + sessionId + }); + } + let observed = { + ready: false, + path: '', + composer: false, + scope: '' + }; + const deadline = Date.now() + navigationTimeoutMs; + while(Date.now() < deadline){ + observed = await session.evaluate(newChatViewExpression(project), { + sessionId + }); + if (observed?.ready) return { + via: result.via ?? 'unknown' + }; + if (!project && observed?.path === '/' && observed?.composer && observed.scope.startsWith('Change project: ')) { + await session.evaluate(CLEAR_NEW_CHAT_PROJECT_EXPRESSION, { + sessionId + }); + } + await delay(100); + } + throw new _errors_js__rspack_import_0.XL(project, `route ${JSON.stringify(observed?.path)}, composer ${Boolean(observed?.composer)}, project ${JSON.stringify(observed?.scope)}`); + } + async function focusComposer(session, sessionId) { + const focused = await session.evaluate(FOCUS_COMPOSER_EXPRESSION, { + sessionId + }); + if (!focused?.ok) { + if (focused?.error === 'composer-has-draft') throw new _errors_js__rspack_import_0.oN(); + throw new Error(`ChatGPT Desktop composer unavailable: ${focused?.error || 'not found'}`); + } + } + async function submitComposerWithEnter(session, sessionId) { + const base = { + windowsVirtualKeyCode: 13, + code: 'Enter', + key: 'Enter', + text: '\r', + unmodifiedText: '\r' + }; + await session.send('Input.dispatchKeyEvent', { + type: 'rawKeyDown', + ...base + }, { + sessionId + }); + await session.send('Input.dispatchKeyEvent', { + type: 'keyUp', + ...base + }, { + sessionId + }); + } + async function sendMessageInDom(session, sessionId, text) { + await focusComposer(session, sessionId); + await session.send('Input.insertText', { + text + }, { + sessionId + }); + try { + await submitComposerWithEnter(session, sessionId); + const deadline = Date.now() + 3000; + while(Date.now() < deadline){ + const cleared = await session.evaluate(`(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + return Boolean(composer && !(composer.textContent || '').trim()); + })()`, { + sessionId + }); + if (cleared) return { + sentVia: 'enter' + }; + await delay(100); + } + throw new Error('Desktop composer did not acknowledge submission'); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', + reason: 'submission-unconfirmed' + }); + } + } + async function readReplyState(session, sessionId) { + const state = await session.evaluate(REPLY_STATE_EXPRESSION, { + sessionId + }); + return { + stopVisible: Boolean(state?.stopVisible), + finalAssistantCount: Number(state?.finalAssistantCount ?? 0), + conversationId: String(state?.conversationId ?? ''), + reply: String(state?.reply ?? ''), + lastTurnKey: String(state?.lastTurnKey ?? ''), + finalTurnKey: String(state?.finalTurnKey ?? '') + }; + } + async function readConversationIdFromDom(session, sessionId) { + const value = await session.evaluate(CONVERSATION_ID_EXPRESSION, { + sessionId + }); + return typeof value === 'string' ? value.trim() : ''; + } + async function waitForConversationId(session, sessionId, { timeoutMs = DEFAULT_CONVERSATION_RESOLVE_MS } = {}) { + const deadline = Date.now() + timeoutMs; + while(Date.now() < deadline){ + const id = await readConversationIdFromDom(session, sessionId); + if (id && !(0, _codex_thread_id_js__rspack_import_1.vr)(id) && !id.startsWith(TEMP_THREAD_ID_PREFIX)) { + return id; + } + await delay(200); + } + throw new Error(`ChatGPT Desktop conversation id did not appear on [data-response-annotation-conversation] within ${timeoutMs}ms`); + } + function resolveDurableThreadId(conversationId, fallbackThreadId) { + if (conversationId && !(0, _codex_thread_id_js__rspack_import_1.vr)(conversationId)) { + return toDesktopForm(conversationId); + } + if (fallbackThreadId && !(0, _codex_thread_id_js__rspack_import_1.vr)(fallbackThreadId) && fallbackThreadId !== 'new') { + return toDesktopForm(fallbackThreadId); + } + return null; + } + class ReplyTimeoutError extends Error { + } + async function waitForReplyDone(session, sessionId, { timeoutMs = 120000, baselineFinalCount = 0, expectedConversationId, baselineTurnKey } = {}) { + const deadline = Date.now() + timeoutMs; + let sawStop = false; + const started = Date.now(); + while(Date.now() < deadline){ + const state = await readReplyState(session, sessionId); + if (state.stopVisible) sawStop = true; + expectedConversationId ??= state.conversationId || undefined; + if (expectedConversationId && state.conversationId && durableThreadKey(state.conversationId) !== durableThreadKey(expectedConversationId)) { + throw new Error('ChatGPT Desktop selected conversation changed while waiting'); + } + const latestIsFinal = Boolean(state.finalTurnKey) && state.finalTurnKey === state.lastTurnKey; + const hasNewFinal = (state.finalAssistantCount > baselineFinalCount || sawStop) && Boolean(state.reply) && latestIsFinal && (!baselineTurnKey || state.lastTurnKey !== baselineTurnKey); + if (!state.stopVisible && hasNewFinal && (sawStop || Date.now() - started > 1500)) { + return { + reply: state.reply, + conversationId: state.conversationId + }; + } + await delay(200); + } + throw new ReplyTimeoutError(`ChatGPT Desktop reply did not finish within ${timeoutMs}ms`); + } + function normalizeTurn(raw) { + const role = raw.role === 'user' || raw.role === 'status' || raw.role === 'assistant' ? raw.role : raw.assistantText ? 'assistant' : raw.userText ? 'user' : 'assistant'; + return { + turnKey: String(raw.turnKey ?? ''), + role, + text: String(raw.text ?? raw.assistantText ?? raw.userText ?? ''), + userText: raw.userText ? String(raw.userText) : undefined, + assistantText: raw.assistantText ? String(raw.assistantText) : undefined + }; + } + async function harvestVisibleTurns(session, sessionId) { + const rows = await session.evaluate(HARVEST_VISIBLE_TURNS_EXPRESSION, { + sessionId + }); + if (!Array.isArray(rows)) return []; + return rows.map(normalizeTurn).filter((t)=>t.turnKey && !t.turnKey.startsWith(HISTORY_GAP_PREFIX)); + } + async function readTurnsFromDom(session, sessionId, { limit = 100, full = false, threadId, idleWheels = DEFAULT_FULL_READ_IDLE_WHEELS, maxWheels = DEFAULT_FULL_READ_MAX_WHEELS } = {}) { + const collected = new Map(); + const order1 = []; + const ingest = async ()=>{ + const older = []; + for (const turn of (await harvestVisibleTurns(session, sessionId))){ + if (!collected.has(turn.turnKey)) older.push(turn.turnKey); + collected.set(turn.turnKey, turn); + } + order1.unshift(...older); + }; + await ingest(); + if (!full) { + const visible = order1.map((key)=>collected.get(key)); + return limit > 0 ? visible.slice(-limit) : visible; + } + let idle = 0; + for(let step = 0; step < maxWheels; step++){ + const before = collected.size; + const metrics = await session.evaluate(TIMELINE_SCROLL_METRICS_EXPRESSION, { + sessionId + }); + if (!metrics?.ok || metrics.x == null || metrics.y == null) break; + await session.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: metrics.x, + y: metrics.y, + deltaX: 0, + deltaY: FULL_READ_WHEEL_DELTA_Y + }, { + sessionId + }); + await delay(120); + await ingest(); + if (collected.size === before) { + idle += 1; + if (idle >= idleWheels) break; + } else { + idle = 0; + } + if (threadId && collected.has(threadId)) break; + } + const all = order1.map((key)=>collected.get(key)); + return limit > 0 ? all.slice(0, limit) : all; + } + function delay(ms) { + return new Promise((resolve)=>setTimeout(resolve, ms)); + } + __webpack_require__.d(__webpack_exports__, { + AY: ()=>listThreadsFromDom, + C2: ()=>pickMainWindowTarget, + FG: ()=>waitForReplyDone, + JU: ()=>readTurnsFromDom, + Kl: ()=>openThreadInDom, + Mr: ()=>summarizeTargetInfos, + X8: ()=>resolveDurableThreadId, + YX: ()=>readReplyState, + Zx: ()=>waitForLoadingTaskGone, + aw: ()=>ReplyTimeoutError, + lh: ()=>sendMessageInDom, + s0: ()=>startNewChatInDom, + z9: ()=>waitForConversationId + }, { + hA: TEMP_THREAD_ID_PREFIX + }); + }, + "./src/core/chatgpt-desktop/cdp-session.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _loopback_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + class CdpSession { + #port; + #ws = null; + #nextId = 1; + #pending = new Map(); + #closed = false; + constructor(port){ + this.#port = port; + } + get port() { + return this.#port; + } + get closed() { + return this.#closed || this.#ws === null || this.#ws.readyState === WebSocket.CLOSED; + } + static async fetchVersion(port, { signal } = {}) { + const base = (0, _loopback_js__rspack_import_1.NJ)(port); + let response; + try { + response = await fetch(`${base}/json/version`, { + signal: signal ? AbortSignal.any([ + signal, + AbortSignal.timeout(5000) + ]) : AbortSignal.timeout(5000), + redirect: 'error' + }); + } catch (error) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP not reachable at ${base}/json/version: ` + (error instanceof Error ? error.message : String(error))); + } + if (!response.ok) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP /json/version returned HTTP ${response.status}`); + } + const version = await response.json(); + if (typeof version.webSocketDebuggerUrl === 'string') { + (0, _loopback_js__rspack_import_1.jv)(version.webSocketDebuggerUrl); + } + return version; + } + static async fetchJsonList(port, { signal } = {}) { + const base = (0, _loopback_js__rspack_import_1.NJ)(port); + let response; + try { + response = await fetch(`${base}/json/list`, { + signal: signal ? AbortSignal.any([ + signal, + AbortSignal.timeout(5000) + ]) : AbortSignal.timeout(5000), + redirect: 'error' + }); + } catch (error) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP not reachable at ${base}/json/list: ` + (error instanceof Error ? error.message : String(error))); + } + if (!response.ok) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP /json/list returned HTTP ${response.status}`); + } + const body = await response.json(); + return Array.isArray(body) ? body : []; + } + async connect({ signal } = {}) { + if (this.#ws && !this.closed) { + throw new Error('CDP session already connected'); + } + const version = await CdpSession.fetchVersion(this.#port, { + signal + }); + const rawUrl = version.webSocketDebuggerUrl; + if (typeof rawUrl !== 'string' || !rawUrl) { + throw new _errors_js__rspack_import_0.sO('CDP /json/version omitted webSocketDebuggerUrl'); + } + const url = (0, _loopback_js__rspack_import_1.jv)(rawUrl); + signal?.throwIfAborted(); + await new Promise((resolve, reject)=>{ + const ws = new WebSocket(url); + this.#ws = ws; + const cleanup = ()=>{ + clearTimeout(timer); + signal?.removeEventListener('abort', onAbort); + }; + const onAbort = ()=>{ + cleanup(); + ws.close(); + reject(new _errors_js__rspack_import_0.sO('CDP connect aborted or timed out')); + }; + const timer = setTimeout(onAbort, 5000); + signal?.addEventListener('abort', onAbort, { + once: true + }); + ws.addEventListener('open', ()=>{ + cleanup(); + resolve(); + }, { + once: true + }); + ws.addEventListener('error', ()=>{ + cleanup(); + reject(new _errors_js__rspack_import_0.sO(`CDP WebSocket failed for ${url}`)); + }, { + once: true + }); + ws.addEventListener('message', (event)=>this.#onMessage(event.data)); + ws.addEventListener('close', ()=>{ + cleanup(); + reject(new _errors_js__rspack_import_0.sO('CDP WebSocket closed before connecting')); + this.#closed = true; + for (const [, pending] of this.#pending){ + clearTimeout(pending.timer); + pending.reject(new _errors_js__rspack_import_0.sO('CDP WebSocket closed')); + } + this.#pending.clear(); + }); + }); + this.#closed = false; + return version; + } + async send(method, params, { sessionId } = {}) { + if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) { + throw new _errors_js__rspack_import_0.sO('CDP session is not connected'); + } + const id = this.#nextId++; + const payload = { + id, + method + }; + if (params !== undefined) payload.params = params; + if (sessionId !== undefined) payload.sessionId = sessionId; + return await new Promise((resolve, reject)=>{ + const timer = setTimeout(()=>{ + this.#pending.delete(id); + reject(new _errors_js__rspack_import_0.sO(`CDP ${method} timed out`)); + }, 10000); + this.#pending.set(id, { + timer, + resolve: (value)=>resolve(value), + reject + }); + try { + this.#ws.send(JSON.stringify(payload)); + } catch (error) { + clearTimeout(timer); + this.#pending.delete(id); + reject(error); + } + }); + } + async evaluate(expression, { sessionId, awaitPromise = true } = {}) { + const result = await this.send('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise + }, { + sessionId + }); + if (result.exceptionDetails) { + const text = result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Runtime.evaluate failed'; + throw new Error(text); + } + return result.result?.value; + } + close() { + this.#closed = true; + for (const pending of this.#pending.values()){ + clearTimeout(pending.timer); + pending.reject(new _errors_js__rspack_import_0.sO('CDP session closed')); + } + this.#pending.clear(); + this.#ws?.close(); + this.#ws = null; + } + #onMessage(data) { + let message; + try { + message = JSON.parse(String(data)); + } catch { + return; + } + if (!message || typeof message !== 'object' || typeof message.id !== 'number') return; + const pending = this.#pending.get(message.id); + if (!pending) return; + this.#pending.delete(message.id); + clearTimeout(pending.timer); + if (message.error) { + pending.reject(new Error(message.error.message || 'CDP error')); + return; + } + pending.resolve(message.result); + } + } + __webpack_require__.d(__webpack_exports__, { + a: ()=>CdpSession + }); + }, + "./src/core/chatgpt-desktop/errors.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + class ArchivedThreadError extends Error { + code = 'ARCHIVED_THREAD'; + reason = 'archived-thread'; + delivery = 'rejected'; + threadId; + constructor(threadId){ + super(`ChatGPT Desktop thread ${JSON.stringify(threadId)} is archived. Read it through app-server, or pass unarchive: true on send to unarchive before sending.`); + this.name = 'ArchivedThreadError'; + this.threadId = threadId; + } + } + class ComposerDraftError extends Error { + code = 'COMPOSER_HAS_DRAFT'; + reason = 'composer-has-draft'; + delivery = 'rejected'; + constructor(){ + super('ChatGPT Desktop composer contains an unsent draft; inspect it in Desktop before sending.'); + this.name = 'ComposerDraftError'; + } + } + class ProjectUnavailableError extends Error { + code = 'PROJECT_UNAVAILABLE'; + reason = 'project-unavailable'; + delivery = 'rejected'; + constructor(project, detail){ + super(`ChatGPT Desktop cannot start a chat in project ${JSON.stringify(project)}: ${detail}`); + this.name = 'ProjectUnavailableError'; + } + } + class NewChatNavigationError extends Error { + code = 'NEW_CHAT_NAVIGATION_FAILED'; + reason = 'new-chat-navigation-failed'; + delivery = 'rejected'; + constructor(project, observed){ + super(`ChatGPT Desktop new-chat action did not open ${project ? `project ${JSON.stringify(project)}` : 'a projectless chat'}; observed ${observed}`); + this.name = 'NewChatNavigationError'; + } + } + class NotImplementedError extends Error { + code = 'NOT_IMPLEMENTED'; + delivery = 'rejected'; + reason = 'not-implemented'; + constructor(operation, detail){ + super(detail ? `ChatGPT Desktop ${operation} is not implemented: ${detail}` : `ChatGPT Desktop ${operation} is not implemented`); + this.name = 'NotImplementedError'; + } + } + class CdpHostRejectedError extends Error { + code = 'CDP_HOST_REJECTED'; + delivery = 'rejected'; + reason = 'cdp-host-rejected'; + constructor(host){ + super(`ChatGPT Desktop CDP rejects non-loopback host ${JSON.stringify(host)}. ` + 'Connect to 127.0.0.1 only; there is no remote transport.'); + this.name = 'CdpHostRejectedError'; + } + } + class CdpUnreachableError extends Error { + code = 'CDP_UNREACHABLE'; + delivery = 'rejected'; + reason = 'cdp-unreachable'; + constructor(message){ + super(message); + this.name = 'CdpUnreachableError'; + } + } + __webpack_require__.d(__webpack_exports__, { + EH: ()=>NotImplementedError, + Kn: ()=>ProjectUnavailableError, + Rg: ()=>_codex_remote_control_js__rspack_import_0.Rg, + XL: ()=>NewChatNavigationError, + c$: ()=>CdpHostRejectedError, + fw: ()=>ArchivedThreadError, + oN: ()=>ComposerDraftError, + sO: ()=>CdpUnreachableError + }); + }, + "./src/core/chatgpt-desktop/facade.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var node_crypto__rspack_import_0 = __webpack_require__("node:crypto"); + var _cdp_adapter_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/cdp-adapter.ts"); + var _app_server_fallback_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/app-server-fallback.ts"); + var _errors_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _loopback_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + var _remote_threads_js__rspack_import_5 = __webpack_require__("./src/core/chatgpt-desktop/remote-threads.ts"); + var _project_state_js__rspack_import_6 = __webpack_require__("./src/core/chatgpt-desktop/project-state.ts"); + var _payload_budget_js__rspack_import_8 = __webpack_require__("./src/core/chatgpt-desktop/payload-budget.ts"); + var _thread_ids_js__rspack_import_7 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const defaultFallbacks = { + listThreads: _app_server_fallback_js__rspack_import_2.OI, + searchThreads: _app_server_fallback_js__rspack_import_2.aI, + readThread: _app_server_fallback_js__rspack_import_2.Jf, + unarchiveThread: _app_server_fallback_js__rspack_import_2.qL, + statusProbe: _app_server_fallback_js__rspack_import_2.dM, + listRemoteThreads: _remote_threads_js__rspack_import_5.Hx, + listHosts: _remote_threads_js__rspack_import_5.Kl, + discoverModelProviders: _app_server_fallback_js__rspack_import_2.X + }; + const MERGED_LIST_CURSOR = 'desktop-merged-list-v1:'; + const MERGED_SEARCH_CURSOR = 'desktop-merged-search-v1:'; + class ChatGptDesktopFacade { + #cdp; + #port; + #cdpConnected = false; + #fallbacks; + #listSnapshots = new Map(); + constructor(cdp = new _cdp_adapter_js__rspack_import_1.a(), port = (0, _loopback_js__rspack_import_4.L0)(), fallbacks = defaultFallbacks){ + this.#cdp = cdp; + this.#port = port; + this.#fallbacks = fallbacks; + } + async connect({ port }) { + this.#port = (0, _loopback_js__rspack_import_4.L0)(process.env, port); + try { + await this.#cdp.connect({ + port: this.#port + }); + this.#cdpConnected = true; + } catch (error) { + this.#cdpConnected = false; + throw error; + } + } + async listTargets() { + await this.#ensureCdp(); + return this.#cdp.listTargets(); + } + async listThreads(options = {}) { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null + }); + let snapshot; + let offset = 0; + if (options.cursor) { + const state = (0, _payload_budget_js__rspack_import_8.i3)(MERGED_LIST_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop list cursor does not match the port or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildListSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop thread inventory changed; restart listing without a cursor'); + } + } else { + snapshot = await this.#buildListSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_LIST_CURSOR); + } + #pageSnapshot(snapshot, offset, limit, options, prefix, query) { + if (offset > snapshot.rows.length) throw new Error('ChatGPT Desktop cursor is out of range'); + const host = normalizeHostFilter(options.host); + const rows = []; + const base = { + backend: snapshot.backend, + limit, + host, + ...query !== undefined ? { + query + } : {}, + ...options.modelProvider?.trim() ? { + modelProvider: options.modelProvider.trim() + } : {}, + ...options.project ? { + project: options.project + } : {}, + ...options.groupBy === 'host' ? { + groupBy: 'host' + } : {} + }; + for (const source of snapshot.rows.slice(offset, offset + limit)){ + let row = source; + const candidate = [ + ...rows, + row + ]; + const probe = { + ...base, + threads: candidate, + nextCursor: prefix, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(candidate) + } : {}, + warnings: snapshot.warnings, + exitCode: 0 + }; + if ((0, _payload_budget_js__rspack_import_8.sQ)(probe) > 614400) { + if (rows.length) break; + row = (0, _payload_budget_js__rspack_import_8.MH)(row); + const minimal = [ + row + ]; + if ((0, _payload_budget_js__rspack_import_8.sQ)({ + ...base, + threads: minimal, + nextCursor: prefix, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(minimal) + } : {}, + warnings: snapshot.warnings, + exitCode: 0 + }) > 614400) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + rows.push(row); + } + const nextOffset = offset + rows.length; + const nextCursor = nextOffset < snapshot.rows.length ? (0, _payload_budget_js__rspack_import_8.oI)(prefix, { + fingerprint: snapshot.fingerprint, + offset: nextOffset, + port: this.#port, + filters: snapshot.filters + }) : null; + const warnings = [ + ...snapshot.warnings + ]; + if (rows.length < Math.min(limit, snapshot.rows.length - offset)) { + warnings.push(`${query === undefined ? 'List' : 'Search'} page stopped at the MCP byte budget; continue with nextCursor`); + } + return { + ...base, + threads: rows, + nextCursor, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(rows) + } : {}, + ...warnings.length ? { + warnings + } : {} + }; + } + async #buildListSnapshot(options, filters) { + const appRows = []; + const warnings = []; + let appCursor; + let appComplete = false; + const seenCursors = new Set(); + const modelProviders = modelProvidersFromFilter(options.modelProvider); + let appSucceeded = false; + try { + for(let page = 0; page < 400; page++){ + const out = await this.#fallbacks.listThreads({ + limit: 200, + cursor: appCursor, + modelProviders + }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) { + appComplete = true; + break; + } + if (seenCursors.has(out.nextCursor)) throw new Error('App-server list returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + } + if (!appComplete) warnings.push('App-server inventory stopped after 400 pages'); + } catch (error) { + warnings.push(`App-server list failed: ${error instanceof Error ? error.message : String(error)}`); + } + const remotes = this.#listRemotes(); + let cdpList = null; + try { + await this.#ensureCdp(); + cdpList = await this.#cdp.listThreads({ + limit: 200 + }); + } catch (error) { + if (isCdpFailure(error)) this.#cdpConnected = false; + else warnings.push(`CDP list failed: ${error instanceof Error ? error.message : String(error)}`); + } + if (!appRows.length && !cdpList && !remotes.length) { + throw new _errors_js__rspack_import_3.sO(warnings[0] ?? 'ChatGPT Desktop listThreads: CDP and app-server unreachable'); + } + const appList = { + backend: 'app-server', + limit: appRows.length, + threads: appRows + }; + const base = appSucceeded ? cdpList ? mergeThreadLists(appList, cdpList) : appList : cdpList ?? { + backend: 'remote-state', + limit: 0, + threads: [] + }; + const merged = finalizeThreadList({ + ...base, + threads: (0, _project_state_js__rspack_import_6.j)(base.threads) + }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes: (0, _project_state_js__rspack_import_6.j)(remotes) + }); + const rows = [ + ...new Map(merged.threads.map((row)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(row.threadId) ?? row.threadId, + row + ])).values() + ]; + rows.sort(compareDesktopThreads); + const fingerprint = (0, node_crypto__rspack_import_0.createHash)('sha256').update(JSON.stringify({ + filters, + port: this.#port, + rows: rows.map((row)=>[ + row.threadId, + row.updatedAt, + row.location, + row.hostId + ]) + })).digest('base64url'); + const snapshot = { + fingerprint, + port: this.#port, + filters, + createdAt: Date.now(), + rows, + backend: merged.backend, + warnings + }; + for (const [key, value] of this.#listSnapshots){ + if (Date.now() - value.createdAt > 15 * 60000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + async listHosts() { + const remotes = this.#listRemotes(); + const cdpList = typeof this.#cdp.listThreads === 'function' ? await this.#tryCdpList({ + limit: 200 + }) : null; + const cdpRows = cdpList?.threads ?? []; + let localThreadCount = 0; + let appSucceeded = false; + try { + const local = await this.#fallbacks.listThreads({ + limit: 25, + modelProviders: [] + }); + appSucceeded = true; + localThreadCount = local.threads.length; + } catch { + localThreadCount = 0; + } + const listHosts = this.#fallbacks.listHosts ?? _remote_threads_js__rspack_import_5.Kl; + const fromState = listHosts(process.env, { + localThreadCount + }); + const byId = new Map(fromState.map((host)=>[ + host.hostId, + { + ...host + } + ])); + for (const thread of remotes){ + const existing = byId.get(thread.hostId); + if (existing) { + byId.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName + }); + } else { + byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 0 + }); + } + } + for (const thread of cdpRows){ + if (thread.location !== 'remote' || !thread.hostId) continue; + if (!byId.has(thread.hostId)) byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName ?? null, + location: 'remote', + threadCount: 0 + }); + } + const hostsSource = 'local+remote-thread-summaries-v3+managed-connections' + (cdpRows.length ? '+cdp' : ''); + const remoteCounts = new Map(); + const remoteInventory = finalizeThreadList({ + backend: cdpRows.length ? 'cdp' : 'remote-state', + limit: 10000, + threads: cdpRows.filter((thread)=>thread.location === 'remote') + }, { + limit: 10000, + remotes + }); + for (const thread of remoteInventory.threads){ + if (thread.hostId) remoteCounts.set(thread.hostId, (remoteCounts.get(thread.hostId) ?? 0) + 1); + } + const withCounts = [ + ...byId.values() + ].map((host)=>host.hostId === 'local' ? { + ...host, + threadCount: localThreadCount + } : { + ...host, + threadCount: remoteCounts.get(host.hostId) ?? host.threadCount + }).sort((a, b)=>{ + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + let modelProviders = []; + let modelProvidersSource = 'thread/list-distinct'; + const warnings = []; + try { + const discover = this.#fallbacks.discoverModelProviders ?? _app_server_fallback_js__rspack_import_2.X; + const discovered = await discover(); + modelProviders = [ + ...discovered.providers + ]; + modelProvidersSource = discovered.source; + if (typeof discovered.threadCount === 'number') localThreadCount = discovered.threadCount; + if (discovered.complete === false) warnings.push('Provider discovery stopped before the final app-server page'); + } catch (error) { + warnings.push(`Model provider discovery failed: ${error instanceof Error ? error.message : String(error)}`); + modelProviders = [ + ...new Set(remotes.map((thread)=>thread.modelProvider).filter((value)=>typeof value === 'string' && Boolean(value))) + ].sort(); + modelProvidersSource = 'remote-summaries-distinct'; + } + modelProviders = [ + ...new Set([ + ...modelProviders, + ...remotes.map((thread)=>thread.modelProvider).filter((value)=>Boolean(value)) + ]) + ].sort(); + let inventoryCounts = null; + try { + const filters = JSON.stringify({ + host: 'all', + modelProvider: null, + project: null, + groupBy: null + }); + const cached = [ + ...this.#listSnapshots.values() + ].find((snapshot)=>snapshot.port === this.#port && snapshot.filters === filters && Date.now() - snapshot.createdAt < 15 * 60000); + const inventory = cached ?? await this.#buildListSnapshot({}, filters); + inventoryCounts = new Map(); + for (const thread of inventory.rows){ + const hostId = thread.location === 'remote' ? thread.hostId ?? 'unknown-remote' : 'local'; + inventoryCounts.set(hostId, (inventoryCounts.get(hostId) ?? 0) + 1); + } + warnings.push(...inventory.warnings); + } catch (error) { + warnings.push(`Merged inventory count failed: ${error instanceof Error ? error.message : String(error)}`); + } + return { + hosts: withCounts.map((host)=>{ + const counted = { + ...host, + threadCount: inventoryCounts?.get(host.hostId) ?? (host.hostId === 'local' ? localThreadCount : host.threadCount) + }; + if (!counted.hostName) return counted; + const name = (0, _payload_budget_js__rspack_import_8.aA)(counted.hostName); + return { + ...counted, + hostName: name.title, + ...name.titleTruncated ? { + hostNameTruncated: true + } : {} + }; + }), + hostsSource, + modelProviders, + modelProvidersSource, + backend: appSucceeded ? cdpRows.length ? remotes.length ? 'app-server+cdp+remote-state' : 'app-server+cdp' : remotes.length ? 'app-server+remote-state' : 'app-server' : cdpRows.length ? remotes.length ? 'cdp+remote-state' : 'cdp' : 'remote-state', + ...warnings.length ? { + warnings + } : {} + }; + } + async searchThreads(options) { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + query: options.query, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null + }); + let snapshot; + let offset = 0; + if (options.cursor) { + const state = (0, _payload_budget_js__rspack_import_8.i3)(MERGED_SEARCH_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop search cursor does not match the port, query or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildSearchSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop search results changed; restart without a cursor'); + } + } else { + snapshot = await this.#buildSearchSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_SEARCH_CURSOR, options.query); + } + async #buildSearchSnapshot(options, filters) { + const modelProviders = modelProvidersFromFilter(options.modelProvider); + const appRows = []; + const warnings = []; + const seenCursors = new Set(); + let appCursor; + let appSucceeded = false; + let appError; + try { + for(let page = 0; page < 400; page++){ + const out = await this.#fallbacks.searchThreads({ + query: options.query, + limit: 200, + cursor: appCursor, + modelProviders + }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) break; + if (seenCursors.has(out.nextCursor)) throw new Error('App-server search returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + if (page === 399) warnings.push('App-server search stopped after 400 pages'); + } + } catch (error) { + appError = error; + warnings.push(`App-server search failed: ${error instanceof Error ? error.message : String(error)}`); + } + const cdpList = await this.#tryCdpList({ + limit: 200 + }); + const remotes = (0, _project_state_js__rspack_import_6.j)(this.#listRemotes()); + if (!appSucceeded && !cdpList && !remotes.length) throw appError; + const appList = { + backend: 'app-server', + limit: appRows.length, + threads: appRows + }; + const base = appSucceeded ? cdpList ? mergeThreadLists(appList, cdpList) : appList : cdpList ?? { + backend: 'remote-state', + limit: 0, + threads: [] + }; + const appMatches = new Set(appRows.map((thread)=>(0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId)); + const merged = finalizeThreadList({ + ...base, + threads: (0, _project_state_js__rspack_import_6.j)(base.threads) + }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes + }); + const needle = options.query.trim().toLowerCase(); + const rows = [ + ...new Map(merged.threads.filter((thread)=>appMatches.has((0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId) || threadMatchesQuery(thread, needle)).map((thread)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId, + thread + ])).values() + ]; + rows.sort(compareDesktopThreads); + const fingerprint = (0, node_crypto__rspack_import_0.createHash)('sha256').update(JSON.stringify({ + filters, + port: this.#port, + rows: rows.map((row)=>[ + row.threadId, + row.updatedAt, + row.location, + row.hostId + ]) + })).digest('base64url'); + const snapshot = { + fingerprint, + port: this.#port, + filters, + createdAt: Date.now(), + rows, + backend: merged.backend, + warnings + }; + for (const [key, value] of this.#listSnapshots){ + if (Date.now() - value.createdAt > 15 * 60000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + async readThread(options) { + const limit = options.limit ?? (options.full ? 2000 : 100); + const full = options.full ?? false; + if ((0, _thread_ids_js__rspack_import_7.vr)(options.threadId)) { + if (options.cursor) throw new Error('Temporary Desktop threads do not support read cursors'); + await this.#ensureCdp(); + return this.#cdp.readThread({ + ...options, + limit, + full + }); + } + try { + return await this.#fallbacks.readThread({ + threadId: options.threadId, + limit, + full, + cursor: options.cursor + }); + } catch (appError) { + if (appError instanceof _errors_js__rspack_import_3.Rg) throw appError; + if (options.cursor) throw appError; + const cdpConnected = await this.#tryEnsureCdp(); + if (!cdpConnected) throw appError; + const fallback = await this.#cdp.readThread({ + ...options, + limit, + full + }); + return { + ...fallback, + complete: false, + warnings: [ + `App-server read failed: ${appError instanceof Error ? appError.message : String(appError)}` + ] + }; + } + } + async sendMessage(options) { + if (options.threadId) { + const remote = (0, _remote_threads_js__rspack_import_5.KI)(options.threadId); + if (remote) throw new _errors_js__rspack_import_3.Rg(options.threadId, remote.hostId, remote.hostName); + } + await this.#ensureCdp(); + try { + return await this.#cdp.sendMessage(options); + } catch (error) { + if (!(error instanceof _errors_js__rspack_import_3.fw) || !options.unarchive || !options.threadId) throw error; + await (this.#fallbacks.unarchiveThread ?? _app_server_fallback_js__rspack_import_2.qL)(options.threadId); + return this.#cdp.sendMessage({ + ...options, + unarchive: false + }); + } + } + async waitForReply(options) { + await this.#ensureCdp(); + return this.#cdp.waitForReply(options); + } + async openThread(threadId, options) { + const remote = (0, _remote_threads_js__rspack_import_5.KI)(threadId); + if (remote) throw new _errors_js__rspack_import_3.Rg(threadId, remote.hostId, remote.hostName); + await this.#ensureCdp(); + return this.#cdp.openThread(threadId, options); + } + async status() { + const cdpStatus = await this.#cdp.status(); + let appServerFallback; + try { + appServerFallback = await this.#fallbacks.statusProbe(); + } catch { + appServerFallback = { + reachable: false + }; + } + const reachable = cdpStatus.reachable; + return { + ...cdpStatus, + port: this.#port, + appServerFallback, + reachable, + message: cdpStatus.reachable ? cdpStatus.message : cdpStatus.message, + exitCode: reachable ? 0 : 1 + }; + } + async close() { + this.#cdpConnected = false; + await this.#cdp.close(); + } + #listRemotes() { + const list = this.#fallbacks.listRemoteThreads ?? _remote_threads_js__rspack_import_5.Hx; + try { + return list(); + } catch { + return []; + } + } + async #ensureCdp() { + if (this.#cdpConnected) return; + await this.#cdp.connect({ + port: this.#port + }); + this.#cdpConnected = true; + } + async #tryEnsureCdp() { + try { + await this.#ensureCdp(); + return true; + } catch (error) { + if (!isCdpFailure(error)) throw error; + this.#cdpConnected = false; + return false; + } + } + async #tryCdpList(options) { + try { + await this.#ensureCdp(); + return await this.#cdp.listThreads(options); + } catch (error) { + if (isCdpFailure(error) || error instanceof _errors_js__rspack_import_3.EH) { + if (isCdpFailure(error)) this.#cdpConnected = false; + return null; + } + throw error; + } + } + } + function mergeThreadLists(appList, cdpList) { + const cdpByKey = new Map(); + for (const thread of cdpList.threads){ + const key = (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId; + cdpByKey.set(key, thread); + } + const merged = appList.threads.map((thread)=>{ + const key = (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId; + const cdp = cdpByKey.get(key); + const withLocal = { + ...thread, + location: thread.location ?? 'local', + hostId: thread.hostId ?? null, + hostName: thread.hostName ?? null + }; + if (!cdp) return withLocal; + return { + ...withLocal, + title: withLocal.title || cdp.title, + pinned: withLocal.pinned || cdp.pinned, + selected: cdp.selected, + kind: cdp.kind || withLocal.kind, + project: cdp.project ?? withLocal.project, + location: cdp.location === 'remote' ? 'remote' : withLocal.location, + hostId: cdp.location === 'remote' ? cdp.hostId ?? null : withLocal.hostId, + hostName: cdp.location === 'remote' ? cdp.hostName ?? null : withLocal.hostName + }; + }); + for (const thread of cdpList.threads){ + if (merged.some((row)=>(0, _thread_ids_js__rspack_import_7.LV)(row.threadId, thread.threadId))) continue; + merged.push(thread); + } + return { + backend: 'app-server+cdp', + limit: appList.limit, + nextCursor: appList.nextCursor, + ...appList.query !== undefined ? { + query: appList.query + } : {}, + threads: merged + }; + } + function finalizeThreadList(base, { limit, host = 'all', modelProvider, project, groupBy, remotes }) { + const remoteById = new Map(remotes.map((thread)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId, + thread + ])); + const locals = base.threads.map((thread)=>{ + const remote = remoteById.get((0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId); + if (remote) { + return { + ...thread, + ...remote, + selected: thread.selected, + pinned: thread.pinned || remote.pinned + }; + } + const location = thread.location ?? 'local'; + return { + ...thread, + location, + hostId: location === 'remote' ? thread.hostId ?? null : null, + hostName: location === 'remote' ? thread.hostName ?? null : null + }; + }); + const localKeys = new Set(locals.map((thread)=>(0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId)); + const merged = [ + ...locals + ]; + for (const remote of remotes){ + const key = (0, _thread_ids_js__rspack_import_7.nn)(remote.threadId) ?? remote.threadId; + if (localKeys.has(key)) continue; + merged.push({ + ...remote, + location: remote.location ?? 'remote', + hostId: remote.hostId ?? null, + hostName: remote.hostName ?? null + }); + localKeys.add(key); + } + if (remotes.length) merged.sort((a, b)=>Number(b.location === 'remote') - Number(a.location === 'remote')); + const filtered = merged.filter((thread)=>{ + if (!hostMatches(thread, host)) return false; + if (!modelProviderMatches(thread, modelProvider)) return false; + if (!projectMatches(thread, project)) return false; + return true; + }); + const limited = filtered.slice(0, limit).map(_payload_budget_js__rspack_import_8.eX); + const result = { + backend: remotes.length ? base.backend === 'app-server+cdp' ? 'app-server+cdp+remote-state' : base.backend === 'app-server' ? 'app-server+remote-state' : base.backend === 'cdp' ? 'cdp+remote-state' : base.backend : base.backend, + limit, + threads: limited, + ...base.nextCursor !== undefined ? { + nextCursor: base.nextCursor + } : {}, + ...base.query !== undefined ? { + query: base.query + } : {}, + host, + ...modelProvider ? { + modelProvider + } : {}, + ...project ? { + project + } : {} + }; + if (groupBy === 'host') { + return { + ...result, + groupBy: 'host', + groups: groupThreadsByHost(limited) + }; + } + return result; + } + function compareDesktopThreads(a, b) { + const timestamp = (row)=>{ + const value = row.updatedAt ?? row.createdAt; + if (typeof value !== 'number' || !Number.isFinite(value)) return Number.NEGATIVE_INFINITY; + return value < 100000000000 ? value * 1000 : value; + }; + return timestamp(b) - timestamp(a) || a.threadId.localeCompare(b.threadId); + } + function groupThreadsByHost(threads) { + const groups = new Map(); + for (const thread of threads){ + const location = thread.location ?? 'local'; + const hostId = location === 'remote' ? thread.hostId || 'unknown-remote' : 'local'; + const key = `${location}:${hostId}`; + const existing = groups.get(key); + if (existing) { + existing.threads.push(thread); + continue; + } + groups.set(key, { + hostId, + hostName: thread.hostName ?? (location === 'local' ? 'local' : null), + location, + threads: [ + thread + ] + }); + } + return [ + ...groups.values() + ]; + } + function normalizeHostFilter(host) { + const value = (host ?? 'all').trim(); + return value || 'all'; + } + function modelProvidersFromFilter(modelProvider) { + if (!modelProvider || !modelProvider.trim()) return []; + return [ + modelProvider.trim() + ]; + } + function hostMatches(thread, host) { + const needle = host.trim().toLowerCase(); + if (!needle || needle === 'all') return true; + if (needle === 'local') return (thread.location ?? 'local') === 'local'; + return [ + thread.hostId, + thread.hostName + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function modelProviderMatches(thread, modelProvider) { + if (!modelProvider) return true; + return thread.modelProvider === modelProvider; + } + function projectMatches(thread, project) { + if (!project) return true; + const needle = project.trim().toLowerCase(); + return [ + thread.project, + thread.projectId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function threadMatchesQuery(thread, needle) { + if (!needle) return true; + return [ + thread.title, + thread.threadId, + thread.project, + thread.preview, + thread.hostName, + thread.hostId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function isCdpFailure(error) { + if (error instanceof _errors_js__rspack_import_3.sO) return true; + if (error instanceof _errors_js__rspack_import_3.EH) return false; + if (error && typeof error === 'object' && 'code' in error) { + const code = String(error.code); + return code === 'CDP_UNREACHABLE' || code === 'CDP_HOST_REJECTED'; + } + return false; + } + let activeAdapter = null; + function getChatGptDesktopAdapter() { + if (!activeAdapter) activeAdapter = new ChatGptDesktopFacade(); + return activeAdapter; + } + function setChatGptDesktopAdapterForTests(adapter) { + activeAdapter = adapter; + } + __webpack_require__.d(__webpack_exports__, { + PP: ()=>getChatGptDesktopAdapter + }); + }, + "./src/core/chatgpt-desktop/loopback.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + const LOOPBACK = new Set([ + '127.0.0.1', + 'localhost', + '::1' + ]); + const DEFAULT_CDP_PORT = 9222; + const CDP_LOOPBACK_HOST = '127.0.0.1'; + function isLoopbackHostname(hostname) { + const host = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, ''); + return LOOPBACK.has(host); + } + function assertLoopbackHostname(hostname) { + if (!isLoopbackHostname(hostname)) { + throw new _errors_js__rspack_import_0.c$(hostname); + } + } + function cdpHttpBase(port) { + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return `http://${CDP_LOOPBACK_HOST}:${port}`; + } + function forceLoopbackWebSocketUrl(raw) { + let url; + try { + url = new URL(raw); + } catch { + throw new _errors_js__rspack_import_0.c$(String(raw)); + } + assertLoopbackHostname(url.hostname); + if (url.username || url.password || url.hash) throw new _errors_js__rspack_import_0.c$(raw); + if (url.protocol !== 'ws:' && url.protocol !== 'wss:') { + throw new Error(`Expected ws(s) debugger URL, got ${url.protocol}`); + } + url.hostname = CDP_LOOPBACK_HOST; + url.protocol = 'ws:'; + return url.toString(); + } + function resolveCdpPort(env = process.env, explicit) { + if (explicit !== undefined) { + if (!Number.isInteger(explicit) || explicit < 1 || explicit > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return explicit; + } + const raw = (env.CHATGPT_DESKTOP_CDP_PORT || '').trim(); + if (!raw) return DEFAULT_CDP_PORT; + const port = /^\d+$/.test(raw) ? Number(raw) : NaN; + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CHATGPT_DESKTOP_CDP_PORT must be an integer 1-65535'); + } + return port; + } + __webpack_require__.d(__webpack_exports__, { + L0: ()=>resolveCdpPort, + NJ: ()=>cdpHttpBase, + jv: ()=>forceLoopbackWebSocketUrl + }); + }, + "./src/core/chatgpt-desktop/payload-budget.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + const DESKTOP_RESULT_BUDGET_BYTES = 600 * 1024; + const DESKTOP_THREAD_TITLE_CHARS = 200; + function serializedBytes(value) { + return Buffer.byteLength(JSON.stringify(value), 'utf8'); + } + function compact(value, maxChars) { + const normalized = value.replace(/\s+/gu, ' ').trim(); + const chars = Array.from(normalized); + return { + value: chars.slice(0, maxChars).join(''), + clipped: chars.length > maxChars + }; + } + function compactThreadRow(thread) { + const title = compact(thread.title, DESKTOP_THREAD_TITLE_CHARS); + const preview = typeof thread.preview === 'string' ? compact(thread.preview, 200) : null; + const cwd = typeof thread.cwd === 'string' ? compact(thread.cwd, 2048) : null; + const project = typeof thread.project === 'string' ? compact(thread.project, 256) : null; + const hostName = typeof thread.hostName === 'string' ? compact(thread.hostName, 256) : null; + return { + ...thread, + title: title.value, + ...title.clipped ? { + titleTruncated: true + } : {}, + ...preview ? { + preview: preview.value, + ...preview.clipped ? { + previewTruncated: true + } : {} + } : {}, + ...cwd ? { + cwd: cwd.value, + ...cwd.clipped ? { + cwdTruncated: true + } : {} + } : {}, + ...project ? { + project: project.value, + ...project.clipped ? { + projectTruncated: true + } : {} + } : {}, + ...hostName ? { + hostName: hostName.value, + ...hostName.clipped ? { + hostNameTruncated: true + } : {} + } : {} + }; + } + function compactThreadTitle(title) { + const out = compact(title, DESKTOP_THREAD_TITLE_CHARS); + return { + title: out.value, + titleTruncated: out.clipped + }; + } + function minimalThreadRow(thread) { + return { + threadId: thread.threadId, + title: compact(thread.title, DESKTOP_THREAD_TITLE_CHARS).value, + titleTruncated: true, + pinned: thread.pinned, + selected: thread.selected, + kind: thread.kind, + location: thread.location, + hostId: thread.hostId, + hostName: thread.hostName, + modelProvider: thread.modelProvider, + metadataTruncated: true + }; + } + function encodePageCursor(prefix, value) { + return prefix + Buffer.from(JSON.stringify(value)).toString('base64url'); + } + function decodePageCursor(prefix, cursor) { + if (!cursor.startsWith(prefix)) throw new Error('Invalid ChatGPT Desktop page cursor'); + try { + return JSON.parse(Buffer.from(cursor.slice(prefix.length), 'base64url').toString('utf8')); + } catch { + throw new Error('Invalid ChatGPT Desktop page cursor'); + } + } + __webpack_require__.d(__webpack_exports__, { + MH: ()=>minimalThreadRow, + aA: ()=>compactThreadTitle, + eX: ()=>compactThreadRow, + i3: ()=>decodePageCursor, + oI: ()=>encodePageCursor, + sQ: ()=>serializedBytes + }); + }, + "./src/core/chatgpt-desktop/project-state.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_ids_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + function findLocalProject(nameOrId, env = process.env) { + const state = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!state) return null; + for (const value of Object.values(record(stateValue(state, 'local-projects')) ?? {})){ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') continue; + if (row.id !== nameOrId && row.name.toLowerCase() !== nameOrId.toLowerCase()) continue; + return { + id: row.id, + label: row.name, + rootPaths: (Array.isArray(row.rootPaths) ? row.rootPaths : []).filter((path)=>typeof path === 'string' && Boolean(path)) + }; + } + return null; + } + function record(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : null; + } + function stateValue(state, key) { + for (const source of (0, _codex_remote_control_js__rspack_import_0.xr)(state))if (key in source) return source[key]; + return undefined; + } + function pathWithin(path, root) { + const normalized = root.replace(/\/+$/u, ''); + return path === normalized || path.startsWith(`${normalized}/`); + } + function resolveThreadProjects(threads, env = process.env) { + const state = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!state) return [ + ...threads + ]; + const localProjects = Object.values(record(stateValue(state, 'local-projects')) ?? {}).flatMap((value)=>{ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') return []; + return (Array.isArray(row.rootPaths) ? row.rootPaths : []).filter((path)=>typeof path === 'string' && Boolean(path)).map((rootPath)=>({ + id: row.id, + label: row.name, + rootPath + })); + }); + const remoteProjects = (Array.isArray(stateValue(state, 'remote-projects')) ? stateValue(state, 'remote-projects') : []).flatMap((value)=>{ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.label !== 'string' || typeof row.remotePath !== 'string' || typeof row.hostId !== 'string') return []; + return [ + { + id: row.id, + label: row.label, + rootPath: row.remotePath, + hostId: row.hostId + } + ]; + }); + const assignments = record(stateValue(state, 'thread-project-assignments')) ?? {}; + const hints = record(stateValue(state, 'thread-workspace-root-hints')) ?? {}; + const projectless = new Set(Array.isArray(stateValue(state, 'projectless-thread-ids')) ? stateValue(state, 'projectless-thread-ids') : []); + return threads.map((thread)=>{ + const bare = (0, _thread_ids_js__rspack_import_1.nn)(thread.threadId) ?? thread.threadId; + const assignment = record(assignments[bare]); + const remote = thread.location === 'remote'; + const projects = remote ? remoteProjects.filter((project)=>project.hostId === thread.hostId) : localProjects; + let project = assignment && typeof assignment.projectId === 'string' ? projects.find((entry)=>entry.id === assignment.projectId) : undefined; + if (!project && !projectless.has(bare) && thread.projectId) { + project = projects.find((entry)=>entry.id === thread.projectId); + } + if (!project && !assignment && !projectless.has(bare)) { + const path = typeof hints[bare] === 'string' ? hints[bare] : typeof thread.cwd === 'string' ? thread.cwd : null; + if (path) project = projects.filter((entry)=>pathWithin(path, entry.rootPath)).sort((a, b)=>b.rootPath.length - a.rootPath.length)[0]; + } + if (project) return { + ...thread, + project: project.label, + projectId: project.id, + projectRootPath: project.rootPath + }; + if (projectless.has(bare)) return { + ...thread, + project: null, + projectId: null + }; + if (remote && typeof thread.project === 'string' && thread.project.startsWith('/')) { + return { + ...thread, + project: null + }; + } + return thread; + }); + } + __webpack_require__.d(__webpack_exports__, { + j: ()=>resolveThreadProjects, + q: ()=>findLocalProject + }); + }, + "./src/core/chatgpt-desktop/remote-threads.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_ids_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + function listRemoteThreadsFromState(env = process.env) { + const data = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!data) return []; + const hostNames = collectHostNames(data); + const out = []; + const seen = new Set(); + for (const [key, value] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value); + const pinnedIds = extractPinnedIds(value); + for (const entry of extractThreadEntries(value)){ + const mapped = mapRemoteEntry(entry, { + hostId, + hostName, + pinnedIds + }); + if (!mapped) continue; + const bare = (0, _thread_ids_js__rspack_import_1.nn)(mapped.threadId) ?? mapped.threadId; + const dedupe = `${hostId}:${bare}`; + if (seen.has(dedupe)) continue; + seen.add(dedupe); + out.push(mapped); + } + } + return out; + } + function findRemoteThread(threadId, env = process.env) { + const bare = (0, _thread_ids_js__rspack_import_1.nn)(threadId) ?? threadId; + if (!bare) return null; + for (const thread of listRemoteThreadsFromState(env)){ + const id = (0, _thread_ids_js__rspack_import_1.nn)(thread.threadId) ?? thread.threadId; + if (id === bare) { + return { + hostId: thread.hostId, + hostName: thread.hostName, + thread + }; + } + } + return null; + } + function findRemoteThreadHostId(threadId, env = process.env) { + return findRemoteThread(threadId, env)?.hostId ?? null; + } + function listDiscoveredHosts(env = process.env, { localThreadCount = 0 } = {}) { + const remotes = listRemoteThreadsFromState(env); + const byHost = new Map(); + byHost.set('local', { + hostId: 'local', + hostName: 'local', + location: 'local', + threadCount: localThreadCount + }); + const data = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (data) { + const hostNames = collectHostNames(data); + for (const [key, summary] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(summary) ?? null; + if (byHost.has(hostId)) continue; + byHost.set(hostId, { + hostId, + hostName, + location: 'remote', + threadCount: 0 + }); + } + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections){ + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue; + const hostId = stringField(entry, [ + 'hostId' + ]); + if (hostId && !byHost.has(hostId)) byHost.set(hostId, { + hostId, + hostName: stringField(entry, [ + 'displayName', + 'alias' + ]), + location: 'remote', + threadCount: 0 + }); + } + } + for (const thread of remotes){ + const existing = byHost.get(thread.hostId); + if (existing) { + byHost.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName, + threadCount: existing.threadCount + 1 + }); + } else { + byHost.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 1 + }); + } + } + return [ + ...byHost.values() + ].sort((a, b)=>{ + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + } + function collectHostNames(data) { + const names = new Map(); + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections){ + const pair = hostNameFromEntry(entry); + if (pair) names.set(pair.hostId, pair.hostName); + } + for (const [key, value] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry; + const hostId = stringField(row, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(row, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractPinnedIds(value) { + const pinned = new Set(); + if (!value || typeof value !== 'object') return pinned; + const record = value; + for (const key of [ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'pinned_ids' + ]){ + const arr = record[key]; + if (!Array.isArray(arr)) continue; + for (const item of arr){ + if (typeof item === 'string' && item) pinned.add(item); + else if (item && typeof item === 'object') { + const id = stringField(item, [ + 'id', + 'threadId', + 'thread_id' + ]); + if (id) pinned.add(id); + } + } + } + return pinned; + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) { + continue; + } + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const row = entry; + if (!stringField(row, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ])) { + entries.push({ + ...row, + id: key + }); + } else { + entries.push(entry); + } + } + } + return entries; + } + function mapRemoteEntry(entry, { hostId, hostName, pinnedIds }) { + if (typeof entry === 'string' && entry) { + return { + threadId: (0, _thread_ids_js__rspack_import_1.BS)(entry), + title: entry, + pinned: pinnedIds.has(entry), + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName + }; + } + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry; + const id = stringField(row, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + if (!id) return null; + const title = stringField(row, [ + 'title', + 'name', + 'preview', + 'label' + ]) || id; + const projectId = stringField(row, [ + 'projectId', + 'project_id' + ]); + const project = stringField(row, [ + 'project', + 'projectName', + 'project_name', + 'cwd' + ]) || projectId || undefined; + const preview = stringField(row, [ + 'preview', + 'snippet' + ]); + const cwd = stringField(row, [ + 'cwd', + 'workingDirectory', + 'working_directory' + ]); + const modelProvider = stringField(row, [ + 'modelProvider', + 'model_provider', + 'provider' + ]); + const model = stringField(row, [ + 'model' + ]); + const originator = stringField(row, [ + 'originator', + 'source' + ]); + const createdAt = numberField(row, [ + 'createdAt', + 'created_at' + ]); + const updatedAt = numberField(row, [ + 'updatedAt', + 'updated_at' + ]); + const sectionName = stringField(row, [ + 'sectionName', + 'section' + ]); + const sectionId = stringField(row, [ + 'sectionId', + 'section_id' + ]) || sectionName; + const pinned = pinnedIds.has(id) || row.pinned === true || sectionId === 'Pinned' || sectionName === 'Pinned'; + return { + threadId: (0, _thread_ids_js__rspack_import_1.BS)(id), + title, + pinned, + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName, + ...project ? { + project + } : {}, + ...projectId ? { + projectId + } : {}, + ...preview ? { + preview + } : {}, + ...cwd ? { + cwd + } : { + cwd: null + }, + createdAt: createdAt ?? null, + updatedAt: updatedAt ?? null, + ...sectionId ? { + section: { + id: sectionId, + name: sectionName ?? sectionId + } + } : { + section: null + }, + ...modelProvider ? { + modelProvider + } : { + modelProvider: null + }, + ...model ? { + model + } : { + model: null + }, + ...originator ? { + originator + } : { + originator: null + } + }; + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + const record = row; + for (const key of keys){ + const value = record[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function numberField(row, keys) { + for (const key of keys){ + const value = row[key]; + if (typeof value === 'number' && Number.isFinite(value)) return value; + } + return null; + } + __webpack_require__.d(__webpack_exports__, { + Hx: ()=>listRemoteThreadsFromState, + KI: ()=>findRemoteThread, + Kl: ()=>listDiscoveredHosts, + XI: ()=>findRemoteThreadHostId + }); + }, + "./src/core/chatgpt-desktop/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _codex_contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _facade_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/facade.ts"); + var _loopback_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + const threadId = zod__rspack_import_4.YjP().min(1).max(256); + const statusSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional() + }).strict(); + const listThreadsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + host: zod__rspack_import_4.YjP().min(1).max(256).optional(), + modelProvider: zod__rspack_import_4.YjP().min(1).max(256).optional(), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + groupBy: zod__rspack_import_4.euz('host').optional() + }).strict(); + const searchThreadsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + query: zod__rspack_import_4.YjP().min(1).max(512), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + host: zod__rspack_import_4.YjP().min(1).max(256).optional(), + modelProvider: zod__rspack_import_4.YjP().min(1).max(256).optional(), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + groupBy: zod__rspack_import_4.euz('host').optional() + }).strict(); + const listHostsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional() + }).strict(); + const readThreadSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId, + limit: zod__rspack_import_4.aig().int().min(1).max(2000).optional(), + full: zod__rspack_import_4.zMY().default(false), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict(); + const openThreadSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId, + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict(); + const sendSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + text: zod__rspack_import_4.YjP().trim().min(1).max(100000), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + unarchive: zod__rspack_import_4.zMY().default(false), + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict().refine((value)=>!value.unarchive || Boolean(value.threadId), { + message: 'unarchive requires an existing threadId', + path: [ + 'unarchive' + ] + }).refine((value)=>!value.threadId || !value.project, { + message: 'project is only for a new thread; omit threadId', + path: [ + 'project' + ] + }); + const waitReplySchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + timeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(120000) + }).strict(); + const resultSchema = zod__rspack_import_4.Ikc({ + exitCode: zod__rspack_import_4.KCZ([ + zod__rspack_import_4.euz(0), + zod__rspack_import_4.euz(1) + ]) + }).catchall(zod__rspack_import_4.Pq9()); + let operationQueue = Promise.resolve(); + async function withAdapter(port, run) { + const operation = operationQueue.then(async ()=>{ + const adapter = (0, _facade_js__rspack_import_2.PP)(); + const resolved = (0, _loopback_js__rspack_import_3.L0)(process.env, port); + try { + try { + await adapter.connect({ + port: resolved + }); + } catch {} + return await run(adapter); + } finally{ + await adapter.close(); + } + }); + operationQueue = operation.catch(()=>{}); + return operation; + } + function asResult(value) { + return resultSchema.parse(JSON.parse(JSON.stringify(value))); + } + function mapError(error) { + if (error instanceof _errors_js__rspack_import_1.EH) { + return asResult({ + delivery: 'rejected', + reason: 'not-implemented', + code: 'NOT_IMPLEMENTED', + error: error.message, + exitCode: 1 + }); + } + if (error instanceof _errors_js__rspack_import_1.Rg) { + return asResult({ + delivery: error.delivery, + reason: error.reason, + code: error.code, + error: error.message, + threadId: error.threadId, + hint: error.hint, + ...error.hostId ? { + hostId: error.hostId + } : {}, + ...error.hostName ? { + hostName: error.hostName + } : {}, + exitCode: 1 + }); + } + const base = (0, _codex_contract_js__rspack_import_0.DG)(error); + if (error && typeof error === 'object' && 'code' in error) { + return asResult({ + ...base, + code: String(error.code) + }); + } + return asResult(base); + } + async function statusOperation(input) { + try { + const status = await withAdapter(input.port, (adapter)=>adapter.status()); + return asResult(status); + } catch (error) { + return mapError(error); + } + } + async function listThreadsOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.listThreads({ + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function searchThreadsOperation(input) { + try { + const out = await withAdapter(input.port, async (adapter)=>{ + if (typeof adapter.searchThreads !== 'function') { + throw new _errors_js__rspack_import_1.EH('searchThreads', 'adapter does not implement search'); + } + return adapter.searchThreads({ + query: input.query, + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy + }); + }); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function listHostsOperation(input) { + try { + const out = await withAdapter(input.port, async (adapter)=>{ + if (typeof adapter.listHosts !== 'function') { + throw new _errors_js__rspack_import_1.EH('listHosts', 'adapter does not implement listHosts'); + } + return adapter.listHosts(); + }); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function readThreadOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.readThread({ + threadId: input.threadId, + limit: input.limit ?? (input.full ? 2000 : 100), + full: input.full, + cursor: input.cursor, + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function openThreadOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.openThread(input.threadId, { + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function sendOperation(input) { + try { + input = sendSchema.parse(input); + const out = await withAdapter(input.port, (adapter)=>adapter.sendMessage({ + threadId: input.threadId, + text: input.text, + project: input.project, + unarchive: input.unarchive, + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: out.delivery === 'accepted' ? 0 : 1 + }); + } catch (error) { + return mapError(error); + } + } + async function waitReplyOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.waitForReply({ + threadId: input.threadId, + timeoutMs: input.timeoutMs + })); + return asResult({ + ...out, + exitCode: out.delivery === 'replied' ? 0 : 1 + }); + } catch (error) { + return mapError(error); + } + } + function resultText(result) { + if (result.error) return String(result.error); + if (result.reachable === true) { + const backend = result.appServerFallback && typeof result.appServerFallback === 'object' ? ` cdp + app-server probe` : ''; + return result.message ? String(result.message) : `ChatGPT Desktop CDP reachable on 127.0.0.1:${result.port}${backend}`; + } + if (result.reachable === false) return String(result.message ?? 'ChatGPT Desktop CDP unreachable'); + if (Array.isArray(result.hosts)) { + const providers = Array.isArray(result.modelProviders) ? ` providers=${result.modelProviders.length} (${String(result.modelProvidersSource ?? '')})` : ''; + return `${result.hosts.length} hosts via ${String(result.hostsSource ?? result.backend ?? 'unknown')}${providers}`; + } + if (Array.isArray(result.threads)) { + const via = result.query ? ` matching ${JSON.stringify(result.query)}` : ''; + const host = result.host && result.host !== 'all' ? ` host=${result.host}` : ''; + const provider = result.modelProvider ? ` provider=${result.modelProvider}` : ''; + return `${result.threads.length} threads${via}${host}${provider} via ${result.backend ?? 'unknown'}`; + } + if (Array.isArray(result.turns)) { + const mode = result.full ? 'full' : 'page'; + return `${result.turns.length} turns (${mode}) on ${result.threadId} via ${result.backend ?? 'unknown'}`; + } + if (result.delivery === 'accepted') { + return `Sent on ${result.threadId} via ${result.backend}` + (result.conversationId ? ` (conversation ${result.conversationId})` : ''); + } + if (result.delivery === 'replied') { + const id = result.conversationId ? ` [${result.conversationId}]` : ''; + return `${String(result.reply ?? 'replied')}${id}`; + } + if (result.delivery) return `delivery ${result.delivery}`; + return JSON.stringify(result); + } + __webpack_require__.d(__webpack_exports__, { + Bm: ()=>listHostsOperation, + D6: ()=>resultText, + UK: ()=>readThreadOperation, + UP: ()=>sendOperation, + Uj: ()=>statusOperation, + tJ: ()=>listThreadsOperation, + zg: ()=>searchThreadsOperation + }, { + FD: resultSchema + }); + }, + "./src/core/chatgpt-desktop/thread-ids.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_thread_id_js__rspack_import_1 = __webpack_require__("./src/core/codex/thread-id.js"); + var _cdp_dom_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/cdp-dom.ts"); + const LOCAL_THREAD_ID_PREFIX = "local:"; + const TEMP_THREAD_ID_PREFIX = null && CDP_TEMP_PREFIX; + const HISTORY_CONTENT_TURN_PREFIX = 'history-content:turn:'; + function isTemporaryDesktopThreadId(threadId) { + return (0, _codex_thread_id_js__rspack_import_1.vr)(threadId) || threadId.startsWith(_codex_thread_id_js__rspack_import_1.hA); + } + function toAppServerThreadId(threadId) { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return null; + try { + return (0, _codex_thread_id_js__rspack_import_1.SX)(threadId); + } catch { + return null; + } + } + function requireAppServerThreadId(threadId) { + const bare = toAppServerThreadId(threadId); + if (!bare) { + throw new Error(`No app-server thread id mapping for ${JSON.stringify(threadId)} ` + `(temporary Desktop rows need CDP; strip a leading "local:" for durable ids)`); + } + return bare; + } + function toDesktopThreadId(threadId) { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return threadId; + if (threadId.startsWith(LOCAL_THREAD_ID_PREFIX)) return threadId; + return `${LOCAL_THREAD_ID_PREFIX}${threadId}`; + } + function threadIdsEquivalent(a, b) { + const left = toAppServerThreadId(a) ?? a; + const right = toAppServerThreadId(b) ?? b; + return left === right; + } + function toDomTurnKey(turnId) { + if (!turnId) return turnId; + if (turnId.startsWith(HISTORY_CONTENT_TURN_PREFIX)) return turnId; + return `${HISTORY_CONTENT_TURN_PREFIX}${turnId}`; + } + function appServerThreadIdCandidates(threadId) { + const bare = toAppServerThreadId(threadId); + return bare ? [ + bare + ] : []; + } + __webpack_require__.d(__webpack_exports__, { + BS: ()=>toDesktopThreadId, + LV: ()=>threadIdsEquivalent, + NM: ()=>requireAppServerThreadId, + Sr: ()=>toDomTurnKey, + nn: ()=>toAppServerThreadId, + vr: ()=>isTemporaryDesktopThreadId + }); + }, "./src/core/claude-routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { var zod__rspack_import_1 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); var _claude_channel_js__rspack_import_0 = __webpack_require__("./src/core/claude-channel.js"); @@ -9446,10 +13670,23 @@ var __webpack_modules__ = { }, "./src/core/codex/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var zod__rspack_import_5 = __webpack_require__("./node_modules/zod/v4/core/core.js"); var _codex_bridge_js__rspack_import_0 = __webpack_require__("./src/core/codex-bridge.js"); var _contract_js__rspack_import_1 = __webpack_require__("./src/core/codex/contract.js"); var _conversation_js__rspack_import_2 = __webpack_require__("./src/core/codex/conversation.js"); - const id = zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9_.:-]{1,128}$/); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); + const bareId = zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9_.:-]{1,128}$/); + const id = zod__rspack_import_3.YjP().min(1).max(160).transform((value, ctx)=>{ + try { + return (0, _thread_id_js__rspack_import_4.SX)(value); + } catch (error) { + ctx.addIssue({ + code: 'custom', + message: error instanceof Error ? error.message : 'Invalid threadId' + }); + return zod__rspack_import_5.tm; + } + }).pipe(bareId); const observationFields = { expectedCwd: zod__rspack_import_3.YjP().min(1).optional(), threadId: id, @@ -9457,7 +13694,7 @@ var __webpack_modules__ = { }; const sendFields = { ...observationFields, - correlationId: id.optional(), + correlationId: bareId.optional(), envelope: zod__rspack_import_3.zMY().optional(), hop: zod__rspack_import_3.aig().int().min(0).optional(), model: zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/).optional(), @@ -9469,8 +13706,8 @@ var __webpack_modules__ = { 'high', 'xhigh' ]).optional(), - replyTo: id.optional(), - expectedTurnId: id.optional(), + replyTo: bareId.optional(), + expectedTurnId: bareId.optional(), whenBusy: zod__rspack_import_3.k5n([ 'reject', 'queue', @@ -9485,8 +13722,8 @@ var __webpack_modules__ = { }).strict(); const waitSchema = zod__rspack_import_3.Ikc({ ...observationFields, - turnId: id, - messageId: id.optional(), + turnId: bareId, + messageId: bareId.optional(), maxOutputBytes: zod__rspack_import_3.aig().int().min(1).max(4194304).optional() }).strict(); const watchSchema = zod__rspack_import_3.Ikc({ @@ -30981,11 +35218,12 @@ var __webpack_modules__ = { GT: ()=>$ZodAsyncError, cV: ()=>$ZodEncodeError, cr: ()=>globalConfig, + tm: ()=>NEVER, xI: ()=>$constructor }); var _util_js__rspack_import_0 = __webpack_require__("./node_modules/zod/v4/core/util.js"); var _a; - const NEVER = null && Object.freeze({ + const NEVER = Object.freeze({ status: "aborted" }); const _zodDesc = { @@ -37056,9 +41294,9 @@ var __webpack_modules__ = { try { var agent_bundle_launch_env_layer__rspack_import_0 = __webpack_require__("./.agent-bundle-virtual/bin-gbot-0.mjs"); var agent_bundle_cli_entry__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/cli-entry.js"); - var _agent_bundle_runtime__rspack_import_41 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/49.js"); - var _agent_bundle_runtime__rspack_import_42 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); - var _agent_bundle_runtime__rspack_import_43 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/index.js"); + var _agent_bundle_runtime__rspack_import_47 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/49.js"); + var _agent_bundle_runtime__rspack_import_48 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); + var _agent_bundle_runtime__rspack_import_49 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/index.js"); var node_url__rspack_import_2 = __webpack_require__("node:url"); var node_worker_threads__rspack_import_3 = __webpack_require__("node:worker_threads"); var _src_cli_approvals_list_tsx__rspack_import_4 = __webpack_require__("./src/cli/approvals/list.tsx"); @@ -37068,36 +41306,42 @@ var __webpack_modules__ = { var _src_cli_bots_get_tsx__rspack_import_8 = __webpack_require__("./src/cli/bots/get.tsx"); var _src_cli_bots_list_tsx__rspack_import_9 = __webpack_require__("./src/cli/bots/list.tsx"); var _src_cli_bots_update_tsx__rspack_import_10 = __webpack_require__("./src/cli/bots/update.tsx"); - var _src_cli_claude_send_tsx__rspack_import_11 = __webpack_require__("./src/cli/claude/send.tsx"); - var _src_cli_codex_bridge_respond_tsx__rspack_import_12 = __webpack_require__("./src/cli/codex/bridge/respond.tsx"); - var _src_cli_codex_bridge_run_tsx__rspack_import_13 = __webpack_require__("./src/cli/codex/bridge/run.tsx"); - var _src_cli_codex_bridge_start_tsx__rspack_import_14 = __webpack_require__("./src/cli/codex/bridge/start.tsx"); - var _src_cli_codex_bridge_status_tsx__rspack_import_15 = __webpack_require__("./src/cli/codex/bridge/status.tsx"); - var _src_cli_codex_bridge_stop_tsx__rspack_import_16 = __webpack_require__("./src/cli/codex/bridge/stop.tsx"); - var _src_cli_codex_desktop_shim_tsx__rspack_import_17 = __webpack_require__("./src/cli/codex/desktop-shim.tsx"); - var _src_cli_codex_list_threads_tsx__rspack_import_18 = __webpack_require__("./src/cli/codex/list-threads.tsx"); - var _src_cli_codex_new_tsx__rspack_import_19 = __webpack_require__("./src/cli/codex/new.tsx"); - var _src_cli_codex_queue_tsx__rspack_import_20 = __webpack_require__("./src/cli/codex/queue.tsx"); - var _src_cli_codex_send_tsx__rspack_import_21 = __webpack_require__("./src/cli/codex/send.tsx"); - var _src_cli_codex_status_tsx__rspack_import_22 = __webpack_require__("./src/cli/codex/status.tsx"); - var _src_cli_codex_wait_tsx__rspack_import_23 = __webpack_require__("./src/cli/codex/wait.tsx"); - var _src_cli_codex_watch_tsx__rspack_import_24 = __webpack_require__("./src/cli/codex/watch.tsx"); - var _src_cli_doctor_tsx__rspack_import_25 = __webpack_require__("./src/cli/doctor.tsx"); - var _src_cli_groups_add_tsx__rspack_import_26 = __webpack_require__("./src/cli/groups/add.tsx"); - var _src_cli_groups_create_tsx__rspack_import_27 = __webpack_require__("./src/cli/groups/create.tsx"); - var _src_cli_groups_delete_tsx__rspack_import_28 = __webpack_require__("./src/cli/groups/delete.tsx"); - var _src_cli_groups_get_tsx__rspack_import_29 = __webpack_require__("./src/cli/groups/get.tsx"); - var _src_cli_groups_list_tsx__rspack_import_30 = __webpack_require__("./src/cli/groups/list.tsx"); - var _src_cli_groups_members_tsx__rspack_import_31 = __webpack_require__("./src/cli/groups/members.tsx"); - var _src_cli_groups_remove_tsx__rspack_import_32 = __webpack_require__("./src/cli/groups/remove.tsx"); - var _src_cli_groups_set_tsx__rspack_import_33 = __webpack_require__("./src/cli/groups/set.tsx"); - var _src_cli_groups_update_tsx__rspack_import_34 = __webpack_require__("./src/cli/groups/update.tsx"); - var _src_cli_history_tsx__rspack_import_35 = __webpack_require__("./src/cli/history.tsx"); - var _src_cli_send_tsx__rspack_import_36 = __webpack_require__("./src/cli/send.tsx"); - var _src_cli_skills_add_tsx__rspack_import_37 = __webpack_require__("./src/cli/skills/add.tsx"); - var _src_cli_skills_list_tsx__rspack_import_38 = __webpack_require__("./src/cli/skills/list.tsx"); - var _src_cli_skills_remove_tsx__rspack_import_39 = __webpack_require__("./src/cli/skills/remove.tsx"); - var _src_cli_thread_tsx__rspack_import_40 = __webpack_require__("./src/cli/thread.tsx"); + var _src_cli_chatgpt_desktop_hosts_tsx__rspack_import_11 = __webpack_require__("./src/cli/chatgpt-desktop/hosts.tsx"); + var _src_cli_chatgpt_desktop_read_tsx__rspack_import_12 = __webpack_require__("./src/cli/chatgpt-desktop/read.tsx"); + var _src_cli_chatgpt_desktop_search_tsx__rspack_import_13 = __webpack_require__("./src/cli/chatgpt-desktop/search.tsx"); + var _src_cli_chatgpt_desktop_send_tsx__rspack_import_14 = __webpack_require__("./src/cli/chatgpt-desktop/send.tsx"); + var _src_cli_chatgpt_desktop_status_tsx__rspack_import_15 = __webpack_require__("./src/cli/chatgpt-desktop/status.tsx"); + var _src_cli_chatgpt_desktop_threads_tsx__rspack_import_16 = __webpack_require__("./src/cli/chatgpt-desktop/threads.tsx"); + var _src_cli_claude_send_tsx__rspack_import_17 = __webpack_require__("./src/cli/claude/send.tsx"); + var _src_cli_codex_bridge_respond_tsx__rspack_import_18 = __webpack_require__("./src/cli/codex/bridge/respond.tsx"); + var _src_cli_codex_bridge_run_tsx__rspack_import_19 = __webpack_require__("./src/cli/codex/bridge/run.tsx"); + var _src_cli_codex_bridge_start_tsx__rspack_import_20 = __webpack_require__("./src/cli/codex/bridge/start.tsx"); + var _src_cli_codex_bridge_status_tsx__rspack_import_21 = __webpack_require__("./src/cli/codex/bridge/status.tsx"); + var _src_cli_codex_bridge_stop_tsx__rspack_import_22 = __webpack_require__("./src/cli/codex/bridge/stop.tsx"); + var _src_cli_codex_desktop_shim_tsx__rspack_import_23 = __webpack_require__("./src/cli/codex/desktop-shim.tsx"); + var _src_cli_codex_list_threads_tsx__rspack_import_24 = __webpack_require__("./src/cli/codex/list-threads.tsx"); + var _src_cli_codex_new_tsx__rspack_import_25 = __webpack_require__("./src/cli/codex/new.tsx"); + var _src_cli_codex_queue_tsx__rspack_import_26 = __webpack_require__("./src/cli/codex/queue.tsx"); + var _src_cli_codex_send_tsx__rspack_import_27 = __webpack_require__("./src/cli/codex/send.tsx"); + var _src_cli_codex_status_tsx__rspack_import_28 = __webpack_require__("./src/cli/codex/status.tsx"); + var _src_cli_codex_wait_tsx__rspack_import_29 = __webpack_require__("./src/cli/codex/wait.tsx"); + var _src_cli_codex_watch_tsx__rspack_import_30 = __webpack_require__("./src/cli/codex/watch.tsx"); + var _src_cli_doctor_tsx__rspack_import_31 = __webpack_require__("./src/cli/doctor.tsx"); + var _src_cli_groups_add_tsx__rspack_import_32 = __webpack_require__("./src/cli/groups/add.tsx"); + var _src_cli_groups_create_tsx__rspack_import_33 = __webpack_require__("./src/cli/groups/create.tsx"); + var _src_cli_groups_delete_tsx__rspack_import_34 = __webpack_require__("./src/cli/groups/delete.tsx"); + var _src_cli_groups_get_tsx__rspack_import_35 = __webpack_require__("./src/cli/groups/get.tsx"); + var _src_cli_groups_list_tsx__rspack_import_36 = __webpack_require__("./src/cli/groups/list.tsx"); + var _src_cli_groups_members_tsx__rspack_import_37 = __webpack_require__("./src/cli/groups/members.tsx"); + var _src_cli_groups_remove_tsx__rspack_import_38 = __webpack_require__("./src/cli/groups/remove.tsx"); + var _src_cli_groups_set_tsx__rspack_import_39 = __webpack_require__("./src/cli/groups/set.tsx"); + var _src_cli_groups_update_tsx__rspack_import_40 = __webpack_require__("./src/cli/groups/update.tsx"); + var _src_cli_history_tsx__rspack_import_41 = __webpack_require__("./src/cli/history.tsx"); + var _src_cli_send_tsx__rspack_import_42 = __webpack_require__("./src/cli/send.tsx"); + var _src_cli_skills_add_tsx__rspack_import_43 = __webpack_require__("./src/cli/skills/add.tsx"); + var _src_cli_skills_list_tsx__rspack_import_44 = __webpack_require__("./src/cli/skills/list.tsx"); + var _src_cli_skills_remove_tsx__rspack_import_45 = __webpack_require__("./src/cli/skills/remove.tsx"); + var _src_cli_thread_tsx__rspack_import_46 = __webpack_require__("./src/cli/thread.tsx"); const route0 = Object.assign({}, Reflect.get(_src_cli_approvals_list_tsx__rspack_import_4, 'default'), _src_cli_approvals_list_tsx__rspack_import_4); const route1 = Object.assign({}, Reflect.get(_src_cli_approvals_respond_tsx__rspack_import_5, 'default'), _src_cli_approvals_respond_tsx__rspack_import_5); const route2 = Object.assign({}, Reflect.get(_src_cli_bots_create_tsx__rspack_import_6, 'default'), _src_cli_bots_create_tsx__rspack_import_6); @@ -37105,37 +41349,43 @@ var __webpack_modules__ = { const route4 = Object.assign({}, Reflect.get(_src_cli_bots_get_tsx__rspack_import_8, 'default'), _src_cli_bots_get_tsx__rspack_import_8); const route5 = Object.assign({}, Reflect.get(_src_cli_bots_list_tsx__rspack_import_9, 'default'), _src_cli_bots_list_tsx__rspack_import_9); const route6 = Object.assign({}, Reflect.get(_src_cli_bots_update_tsx__rspack_import_10, 'default'), _src_cli_bots_update_tsx__rspack_import_10); - const route7 = Object.assign({}, Reflect.get(_src_cli_claude_send_tsx__rspack_import_11, 'default'), _src_cli_claude_send_tsx__rspack_import_11); - const route8 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_respond_tsx__rspack_import_12, 'default'), _src_cli_codex_bridge_respond_tsx__rspack_import_12); - const route9 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_run_tsx__rspack_import_13, 'default'), _src_cli_codex_bridge_run_tsx__rspack_import_13); - const route10 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_start_tsx__rspack_import_14, 'default'), _src_cli_codex_bridge_start_tsx__rspack_import_14); - const route11 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_status_tsx__rspack_import_15, 'default'), _src_cli_codex_bridge_status_tsx__rspack_import_15); - const route12 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_stop_tsx__rspack_import_16, 'default'), _src_cli_codex_bridge_stop_tsx__rspack_import_16); - const route13 = Object.assign({}, Reflect.get(_src_cli_codex_desktop_shim_tsx__rspack_import_17, 'default'), _src_cli_codex_desktop_shim_tsx__rspack_import_17); - const route14 = Object.assign({}, Reflect.get(_src_cli_codex_list_threads_tsx__rspack_import_18, 'default'), _src_cli_codex_list_threads_tsx__rspack_import_18); - const route15 = Object.assign({}, Reflect.get(_src_cli_codex_new_tsx__rspack_import_19, 'default'), _src_cli_codex_new_tsx__rspack_import_19); - const route16 = Object.assign({}, Reflect.get(_src_cli_codex_queue_tsx__rspack_import_20, 'default'), _src_cli_codex_queue_tsx__rspack_import_20); - const route17 = Object.assign({}, Reflect.get(_src_cli_codex_send_tsx__rspack_import_21, 'default'), _src_cli_codex_send_tsx__rspack_import_21); - const route18 = Object.assign({}, Reflect.get(_src_cli_codex_status_tsx__rspack_import_22, 'default'), _src_cli_codex_status_tsx__rspack_import_22); - const route19 = Object.assign({}, Reflect.get(_src_cli_codex_wait_tsx__rspack_import_23, 'default'), _src_cli_codex_wait_tsx__rspack_import_23); - const route20 = Object.assign({}, Reflect.get(_src_cli_codex_watch_tsx__rspack_import_24, 'default'), _src_cli_codex_watch_tsx__rspack_import_24); - const route21 = Object.assign({}, Reflect.get(_src_cli_doctor_tsx__rspack_import_25, 'default'), _src_cli_doctor_tsx__rspack_import_25); - const route22 = Object.assign({}, Reflect.get(_src_cli_groups_add_tsx__rspack_import_26, 'default'), _src_cli_groups_add_tsx__rspack_import_26); - const route23 = Object.assign({}, Reflect.get(_src_cli_groups_create_tsx__rspack_import_27, 'default'), _src_cli_groups_create_tsx__rspack_import_27); - const route24 = Object.assign({}, Reflect.get(_src_cli_groups_delete_tsx__rspack_import_28, 'default'), _src_cli_groups_delete_tsx__rspack_import_28); - const route25 = Object.assign({}, Reflect.get(_src_cli_groups_get_tsx__rspack_import_29, 'default'), _src_cli_groups_get_tsx__rspack_import_29); - const route26 = Object.assign({}, Reflect.get(_src_cli_groups_list_tsx__rspack_import_30, 'default'), _src_cli_groups_list_tsx__rspack_import_30); - const route27 = Object.assign({}, Reflect.get(_src_cli_groups_members_tsx__rspack_import_31, 'default'), _src_cli_groups_members_tsx__rspack_import_31); - const route28 = Object.assign({}, Reflect.get(_src_cli_groups_remove_tsx__rspack_import_32, 'default'), _src_cli_groups_remove_tsx__rspack_import_32); - const route29 = Object.assign({}, Reflect.get(_src_cli_groups_set_tsx__rspack_import_33, 'default'), _src_cli_groups_set_tsx__rspack_import_33); - const route30 = Object.assign({}, Reflect.get(_src_cli_groups_update_tsx__rspack_import_34, 'default'), _src_cli_groups_update_tsx__rspack_import_34); - const route31 = Object.assign({}, Reflect.get(_src_cli_history_tsx__rspack_import_35, 'default'), _src_cli_history_tsx__rspack_import_35); - const route32 = Object.assign({}, Reflect.get(_src_cli_send_tsx__rspack_import_36, 'default'), _src_cli_send_tsx__rspack_import_36); - const route33 = Object.assign({}, Reflect.get(_src_cli_skills_add_tsx__rspack_import_37, 'default'), _src_cli_skills_add_tsx__rspack_import_37); - const route34 = Object.assign({}, Reflect.get(_src_cli_skills_list_tsx__rspack_import_38, 'default'), _src_cli_skills_list_tsx__rspack_import_38); - const route35 = Object.assign({}, Reflect.get(_src_cli_skills_remove_tsx__rspack_import_39, 'default'), _src_cli_skills_remove_tsx__rspack_import_39); - const route36 = Object.assign({}, Reflect.get(_src_cli_thread_tsx__rspack_import_40, 'default'), _src_cli_thread_tsx__rspack_import_40); - const pluginRoot = (0, _agent_bundle_runtime__rspack_import_41.E7)({ + const route7 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_hosts_tsx__rspack_import_11, 'default'), _src_cli_chatgpt_desktop_hosts_tsx__rspack_import_11); + const route8 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_read_tsx__rspack_import_12, 'default'), _src_cli_chatgpt_desktop_read_tsx__rspack_import_12); + const route9 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_search_tsx__rspack_import_13, 'default'), _src_cli_chatgpt_desktop_search_tsx__rspack_import_13); + const route10 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_send_tsx__rspack_import_14, 'default'), _src_cli_chatgpt_desktop_send_tsx__rspack_import_14); + const route11 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_status_tsx__rspack_import_15, 'default'), _src_cli_chatgpt_desktop_status_tsx__rspack_import_15); + const route12 = Object.assign({}, Reflect.get(_src_cli_chatgpt_desktop_threads_tsx__rspack_import_16, 'default'), _src_cli_chatgpt_desktop_threads_tsx__rspack_import_16); + const route13 = Object.assign({}, Reflect.get(_src_cli_claude_send_tsx__rspack_import_17, 'default'), _src_cli_claude_send_tsx__rspack_import_17); + const route14 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_respond_tsx__rspack_import_18, 'default'), _src_cli_codex_bridge_respond_tsx__rspack_import_18); + const route15 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_run_tsx__rspack_import_19, 'default'), _src_cli_codex_bridge_run_tsx__rspack_import_19); + const route16 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_start_tsx__rspack_import_20, 'default'), _src_cli_codex_bridge_start_tsx__rspack_import_20); + const route17 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_status_tsx__rspack_import_21, 'default'), _src_cli_codex_bridge_status_tsx__rspack_import_21); + const route18 = Object.assign({}, Reflect.get(_src_cli_codex_bridge_stop_tsx__rspack_import_22, 'default'), _src_cli_codex_bridge_stop_tsx__rspack_import_22); + const route19 = Object.assign({}, Reflect.get(_src_cli_codex_desktop_shim_tsx__rspack_import_23, 'default'), _src_cli_codex_desktop_shim_tsx__rspack_import_23); + const route20 = Object.assign({}, Reflect.get(_src_cli_codex_list_threads_tsx__rspack_import_24, 'default'), _src_cli_codex_list_threads_tsx__rspack_import_24); + const route21 = Object.assign({}, Reflect.get(_src_cli_codex_new_tsx__rspack_import_25, 'default'), _src_cli_codex_new_tsx__rspack_import_25); + const route22 = Object.assign({}, Reflect.get(_src_cli_codex_queue_tsx__rspack_import_26, 'default'), _src_cli_codex_queue_tsx__rspack_import_26); + const route23 = Object.assign({}, Reflect.get(_src_cli_codex_send_tsx__rspack_import_27, 'default'), _src_cli_codex_send_tsx__rspack_import_27); + const route24 = Object.assign({}, Reflect.get(_src_cli_codex_status_tsx__rspack_import_28, 'default'), _src_cli_codex_status_tsx__rspack_import_28); + const route25 = Object.assign({}, Reflect.get(_src_cli_codex_wait_tsx__rspack_import_29, 'default'), _src_cli_codex_wait_tsx__rspack_import_29); + const route26 = Object.assign({}, Reflect.get(_src_cli_codex_watch_tsx__rspack_import_30, 'default'), _src_cli_codex_watch_tsx__rspack_import_30); + const route27 = Object.assign({}, Reflect.get(_src_cli_doctor_tsx__rspack_import_31, 'default'), _src_cli_doctor_tsx__rspack_import_31); + const route28 = Object.assign({}, Reflect.get(_src_cli_groups_add_tsx__rspack_import_32, 'default'), _src_cli_groups_add_tsx__rspack_import_32); + const route29 = Object.assign({}, Reflect.get(_src_cli_groups_create_tsx__rspack_import_33, 'default'), _src_cli_groups_create_tsx__rspack_import_33); + const route30 = Object.assign({}, Reflect.get(_src_cli_groups_delete_tsx__rspack_import_34, 'default'), _src_cli_groups_delete_tsx__rspack_import_34); + const route31 = Object.assign({}, Reflect.get(_src_cli_groups_get_tsx__rspack_import_35, 'default'), _src_cli_groups_get_tsx__rspack_import_35); + const route32 = Object.assign({}, Reflect.get(_src_cli_groups_list_tsx__rspack_import_36, 'default'), _src_cli_groups_list_tsx__rspack_import_36); + const route33 = Object.assign({}, Reflect.get(_src_cli_groups_members_tsx__rspack_import_37, 'default'), _src_cli_groups_members_tsx__rspack_import_37); + const route34 = Object.assign({}, Reflect.get(_src_cli_groups_remove_tsx__rspack_import_38, 'default'), _src_cli_groups_remove_tsx__rspack_import_38); + const route35 = Object.assign({}, Reflect.get(_src_cli_groups_set_tsx__rspack_import_39, 'default'), _src_cli_groups_set_tsx__rspack_import_39); + const route36 = Object.assign({}, Reflect.get(_src_cli_groups_update_tsx__rspack_import_40, 'default'), _src_cli_groups_update_tsx__rspack_import_40); + const route37 = Object.assign({}, Reflect.get(_src_cli_history_tsx__rspack_import_41, 'default'), _src_cli_history_tsx__rspack_import_41); + const route38 = Object.assign({}, Reflect.get(_src_cli_send_tsx__rspack_import_42, 'default'), _src_cli_send_tsx__rspack_import_42); + const route39 = Object.assign({}, Reflect.get(_src_cli_skills_add_tsx__rspack_import_43, 'default'), _src_cli_skills_add_tsx__rspack_import_43); + const route40 = Object.assign({}, Reflect.get(_src_cli_skills_list_tsx__rspack_import_44, 'default'), _src_cli_skills_list_tsx__rspack_import_44); + const route41 = Object.assign({}, Reflect.get(_src_cli_skills_remove_tsx__rspack_import_45, 'default'), _src_cli_skills_remove_tsx__rspack_import_45); + const route42 = Object.assign({}, Reflect.get(_src_cli_thread_tsx__rspack_import_46, 'default'), _src_cli_thread_tsx__rspack_import_46); + const pluginRoot = (0, _agent_bundle_runtime__rspack_import_47.E7)({ fallback: (0, node_url__rspack_import_2.fileURLToPath)(new URL('..', import.meta.url)), stateAnchor: 'user-data' }); @@ -37166,95 +41416,113 @@ var __webpack_modules__ = { "cli:bots/update": Object.freeze({ module: route6 }), - "cli:claude/send": Object.freeze({ + "cli:chatgpt-desktop/hosts": Object.freeze({ module: route7 }), - "cli:codex/bridge/respond": Object.freeze({ + "cli:chatgpt-desktop/read": Object.freeze({ module: route8 }), - "cli:codex/bridge/run": Object.freeze({ + "cli:chatgpt-desktop/search": Object.freeze({ module: route9 }), - "cli:codex/bridge/start": Object.freeze({ + "cli:chatgpt-desktop/send": Object.freeze({ module: route10 }), - "cli:codex/bridge/status": Object.freeze({ + "cli:chatgpt-desktop/status": Object.freeze({ module: route11 }), - "cli:codex/bridge/stop": Object.freeze({ + "cli:chatgpt-desktop/threads": Object.freeze({ module: route12 }), - "cli:codex/desktop-shim": Object.freeze({ + "cli:claude/send": Object.freeze({ module: route13 }), - "cli:codex/list-threads": Object.freeze({ + "cli:codex/bridge/respond": Object.freeze({ module: route14 }), - "cli:codex/new": Object.freeze({ + "cli:codex/bridge/run": Object.freeze({ module: route15 }), - "cli:codex/queue": Object.freeze({ + "cli:codex/bridge/start": Object.freeze({ module: route16 }), - "cli:codex/send": Object.freeze({ + "cli:codex/bridge/status": Object.freeze({ module: route17 }), - "cli:codex/status": Object.freeze({ + "cli:codex/bridge/stop": Object.freeze({ module: route18 }), - "cli:codex/wait": Object.freeze({ + "cli:codex/desktop-shim": Object.freeze({ module: route19 }), - "cli:codex/watch": Object.freeze({ + "cli:codex/list-threads": Object.freeze({ module: route20 }), - "cli:doctor": Object.freeze({ + "cli:codex/new": Object.freeze({ module: route21 }), - "cli:groups/add": Object.freeze({ + "cli:codex/queue": Object.freeze({ module: route22 }), - "cli:groups/create": Object.freeze({ + "cli:codex/send": Object.freeze({ module: route23 }), - "cli:groups/delete": Object.freeze({ + "cli:codex/status": Object.freeze({ module: route24 }), - "cli:groups/get": Object.freeze({ + "cli:codex/wait": Object.freeze({ module: route25 }), - "cli:groups/list": Object.freeze({ + "cli:codex/watch": Object.freeze({ module: route26 }), - "cli:groups/members": Object.freeze({ + "cli:doctor": Object.freeze({ module: route27 }), - "cli:groups/remove": Object.freeze({ + "cli:groups/add": Object.freeze({ module: route28 }), - "cli:groups/set": Object.freeze({ + "cli:groups/create": Object.freeze({ module: route29 }), - "cli:groups/update": Object.freeze({ + "cli:groups/delete": Object.freeze({ module: route30 }), - "cli:history": Object.freeze({ + "cli:groups/get": Object.freeze({ module: route31 }), - "cli:send": Object.freeze({ + "cli:groups/list": Object.freeze({ module: route32 }), - "cli:skills/add": Object.freeze({ + "cli:groups/members": Object.freeze({ module: route33 }), - "cli:skills/list": Object.freeze({ + "cli:groups/remove": Object.freeze({ module: route34 }), - "cli:skills/remove": Object.freeze({ + "cli:groups/set": Object.freeze({ module: route35 }), - "cli:thread": Object.freeze({ + "cli:groups/update": Object.freeze({ module: route36 + }), + "cli:history": Object.freeze({ + module: route37 + }), + "cli:send": Object.freeze({ + module: route38 + }), + "cli:skills/add": Object.freeze({ + module: route39 + }), + "cli:skills/list": Object.freeze({ + module: route40 + }), + "cli:skills/remove": Object.freeze({ + module: route41 + }), + "cli:thread": Object.freeze({ + module: route42 }) }); const commands = Object.freeze([ @@ -37437,188 +41705,507 @@ var __webpack_modules__ = { ], "path": [ "bots", - "delete" + "delete" + ], + "rendered": true, + "routeId": "cli:bots/delete" + }, + { + "aliases": [], + "description": "Show one bot or group by id or name.", + "exitCode": "zero", + "options": [ + { + "description": "Agents directory for --files mode", + "key": "dir", + "kind": "string", + "option": "dir", + "repeated": false, + "required": false + }, + { + "description": "Force the on-disk agents store", + "key": "files", + "kind": "boolean", + "option": "files", + "repeated": false, + "required": false + }, + { + "description": "Force the live gateway", + "key": "gateway", + "kind": "boolean", + "option": "gateway", + "repeated": false, + "required": false + }, + { + "description": "Bot or group id or name", + "key": "ref", + "kind": "string", + "option": "ref", + "positional": 0, + "repeated": false, + "required": true + } + ], + "path": [ + "bots", + "get" + ], + "rendered": true, + "routeId": "cli:bots/get" + }, + { + "aliases": [], + "description": "List Grok Bot bots (not groups).", + "exitCode": "zero", + "options": [ + { + "description": "Agents directory for --files mode", + "key": "dir", + "kind": "string", + "option": "dir", + "repeated": false, + "required": false + }, + { + "description": "Force the on-disk agents store", + "key": "files", + "kind": "boolean", + "option": "files", + "repeated": false, + "required": false + }, + { + "description": "Force the live gateway", + "key": "gateway", + "kind": "boolean", + "option": "gateway", + "repeated": false, + "required": false + } + ], + "path": [ + "bots", + "list" + ], + "rendered": true, + "routeId": "cli:bots/list" + }, + { + "aliases": [], + "description": "Update a bot or group profile.", + "exitCode": "zero", + "options": [ + { + "key": "avatarColor", + "kind": "string", + "option": "avatar-color", + "repeated": false, + "required": false + }, + { + "key": "avatarShape", + "kind": "string", + "option": "avatar-shape", + "repeated": false, + "required": false + }, + { + "key": "description", + "kind": "string", + "option": "description", + "repeated": false, + "required": false + }, + { + "description": "Agents directory for --files mode", + "key": "dir", + "kind": "string", + "option": "dir", + "repeated": false, + "required": false + }, + { + "description": "Force the on-disk agents store", + "key": "files", + "kind": "boolean", + "option": "files", + "repeated": false, + "required": false + }, + { + "description": "Force the live gateway", + "key": "gateway", + "kind": "boolean", + "option": "gateway", + "repeated": false, + "required": false + }, + { + "key": "hidden", + "kind": "string", + "option": "hidden", + "repeated": false, + "required": false + }, + { + "key": "name", + "kind": "string", + "option": "name", + "repeated": false, + "required": false + }, + { + "key": "notify", + "kind": "string", + "option": "notify", + "repeated": false, + "required": false + }, + { + "description": "Bot or group id or name", + "key": "ref", + "kind": "string", + "option": "ref", + "positional": 0, + "repeated": false, + "required": true + }, + { + "key": "title", + "kind": "string", + "option": "title", + "repeated": false, + "required": false + } + ], + "path": [ + "bots", + "update" ], "rendered": true, - "routeId": "cli:bots/delete" + "routeId": "cli:bots/update" }, { "aliases": [], - "description": "Show one bot or group by id or name.", - "exitCode": "zero", + "description": "Discover ChatGPT Desktop hosts and modelProviders at runtime (never hardcode). Always includes local plus every remote-thread-summaries-v3:; Providers come from distinct thread/list and remote-summary values.", + "exitCode": "result", "options": [ { - "description": "Agents directory for --files mode", - "key": "dir", + "description": "Local CDP port", + "key": "port", + "kind": "number", + "option": "port", + "repeated": false, + "required": false + } + ], + "path": [ + "chatgpt-desktop", + "hosts" + ], + "rendered": true, + "routeId": "cli:chatgpt-desktop/hosts" + }, + { + "aliases": [], + "description": "Read a ChatGPT Desktop thread via app-server (turns/list); CDP DOM is fallback only.", + "exitCode": "result", + "options": [ + { + "description": "Opaque nextCursor from the prior read page", + "key": "cursor", "kind": "string", - "option": "dir", + "option": "cursor", "repeated": false, "required": false }, { - "description": "Force the on-disk agents store", - "key": "files", + "defaultValue": false, + "key": "full", "kind": "boolean", - "option": "files", + "option": "full", "repeated": false, "required": false }, { - "description": "Force the live gateway", - "key": "gateway", - "kind": "boolean", - "option": "gateway", + "description": "Recent default 100; full default 2000", + "key": "limit", + "kind": "number", + "option": "limit", "repeated": false, "required": false }, { - "description": "Bot or group id or name", - "key": "ref", + "defaultValue": 90000, + "key": "openTimeoutMs", + "kind": "number", + "option": "open-timeout-ms", + "repeated": false, + "required": false + }, + { + "key": "port", + "kind": "number", + "option": "port", + "repeated": false, + "required": false + }, + { + "key": "threadId", "kind": "string", - "option": "ref", + "option": "thread-id", "positional": 0, "repeated": false, "required": true } ], "path": [ - "bots", - "get" + "chatgpt-desktop", + "read" ], + "render": { + "maxElapsedMs": 660000 + }, "rendered": true, - "routeId": "cli:bots/get" + "routeId": "cli:chatgpt-desktop/read" }, { "aliases": [], - "description": "List Grok Bot bots (not groups).", - "exitCode": "zero", + "description": "Search ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.", + "exitCode": "result", "options": [ { - "description": "Agents directory for --files mode", - "key": "dir", + "description": "Opaque nextCursor from a prior search page", + "key": "cursor", "kind": "string", - "option": "dir", + "option": "cursor", "repeated": false, "required": false }, { - "description": "Force the on-disk agents store", - "key": "files", - "kind": "boolean", - "option": "files", + "choices": [ + "host" + ], + "description": "Group results by host (grouping mode, not a host id)", + "key": "groupBy", + "kind": "enum", + "option": "group-by", "repeated": false, "required": false }, { - "description": "Force the live gateway", - "key": "gateway", - "kind": "boolean", - "option": "gateway", + "description": "Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.", + "key": "host", + "kind": "string", + "option": "host", "repeated": false, "required": false + }, + { + "defaultValue": 50, + "description": "Max matches (1-200)", + "key": "limit", + "kind": "number", + "option": "limit", + "repeated": false, + "required": false + }, + { + "description": "Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.", + "key": "modelProvider", + "kind": "string", + "option": "model-provider", + "repeated": false, + "required": false + }, + { + "description": "Local CDP port", + "key": "port", + "kind": "number", + "option": "port", + "repeated": false, + "required": false + }, + { + "description": "Project label or projectId filter.", + "key": "project", + "kind": "string", + "option": "project", + "repeated": false, + "required": false + }, + { + "key": "query", + "kind": "string", + "option": "query", + "positional": 0, + "repeated": false, + "required": true } ], "path": [ - "bots", - "list" + "chatgpt-desktop", + "search" ], "rendered": true, - "routeId": "cli:bots/list" + "routeId": "cli:chatgpt-desktop/search" }, { "aliases": [], - "description": "Update a bot or group profile.", - "exitCode": "zero", + "description": "Send to ChatGPT Desktop via CDP composer. Omit --thread-id for a new chat; --project prefers Start new chat in .", + "exitCode": "result", "options": [ { - "key": "avatarColor", - "kind": "string", - "option": "avatar-color", + "key": "openTimeoutMs", + "kind": "number", + "option": "open-timeout-ms", "repeated": false, "required": false }, { - "key": "avatarShape", - "kind": "string", - "option": "avatar-shape", + "key": "port", + "kind": "number", + "option": "port", "repeated": false, "required": false }, { - "key": "description", + "key": "project", "kind": "string", - "option": "description", + "option": "project", "repeated": false, "required": false }, { - "description": "Agents directory for --files mode", - "key": "dir", + "key": "text", "kind": "string", - "option": "dir", + "option": "text", + "positional": 0, + "repeated": true, + "required": true + }, + { + "key": "threadId", + "kind": "string", + "option": "thread-id", "repeated": false, "required": false }, { - "description": "Force the on-disk agents store", - "key": "files", + "description": "Explicitly unarchive an existing archived thread before sending.", + "key": "unarchive", "kind": "boolean", - "option": "files", + "option": "unarchive", + "repeated": false, + "required": false + } + ], + "path": [ + "chatgpt-desktop", + "send" + ], + "render": { + "maxElapsedMs": 660000 + }, + "rendered": true, + "routeId": "cli:chatgpt-desktop/send" + }, + { + "aliases": [], + "description": "Probe local ChatGPT Desktop CDP on 127.0.0.1 and report app-server availability for list/deep-read.", + "exitCode": "result", + "options": [ + { + "description": "Local CDP port (default 9222)", + "key": "port", + "kind": "number", + "option": "port", + "repeated": false, + "required": false + } + ], + "path": [ + "chatgpt-desktop", + "status" + ], + "rendered": true, + "routeId": "cli:chatgpt-desktop/status" + }, + { + "aliases": [], + "description": "List local + remote-control ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.", + "exitCode": "result", + "options": [ + { + "description": "Opaque nextCursor from a previous page", + "key": "cursor", + "kind": "string", + "option": "cursor", "repeated": false, "required": false }, { - "description": "Force the live gateway", - "key": "gateway", - "kind": "boolean", - "option": "gateway", + "choices": [ + "host" + ], + "description": "Group results by host (grouping mode, not a host id)", + "key": "groupBy", + "kind": "enum", + "option": "group-by", "repeated": false, "required": false }, { - "key": "hidden", + "description": "Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.", + "key": "host", "kind": "string", - "option": "hidden", + "option": "host", "repeated": false, "required": false }, { - "key": "name", - "kind": "string", - "option": "name", + "defaultValue": 50, + "description": "Max threads (1-200)", + "key": "limit", + "kind": "number", + "option": "limit", "repeated": false, "required": false }, { - "key": "notify", + "description": "Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.", + "key": "modelProvider", "kind": "string", - "option": "notify", + "option": "model-provider", "repeated": false, "required": false }, { - "description": "Bot or group id or name", - "key": "ref", - "kind": "string", - "option": "ref", - "positional": 0, + "description": "Local CDP port", + "key": "port", + "kind": "number", + "option": "port", "repeated": false, - "required": true + "required": false }, { - "key": "title", + "description": "Project label or projectId filter.", + "key": "project", "kind": "string", - "option": "title", + "option": "project", "repeated": false, "required": false } ], "path": [ - "bots", - "update" + "chatgpt-desktop", + "threads" ], "rendered": true, - "routeId": "cli:bots/update" + "routeId": "cli:chatgpt-desktop/threads" }, { "aliases": [], @@ -37906,11 +42493,11 @@ var __webpack_modules__ = { }, { "aliases": [], - "description": "List Codex daemon-managed threads.", + "description": "List Codex daemon-managed threads (all modelProviders; pages via --cursor / nextCursor).", "exitCode": "result", "options": [ { - "description": "Opaque pagination cursor (may start with -)", + "description": "Opaque pagination cursor from a previous nextCursor (may start with -)", "key": "cursor", "kind": "string", "option": "cursor", @@ -39281,27 +43868,27 @@ var __webpack_modules__ = { instanceId: processLifetime.instanceId, pid: processLifetime.pid }; - const result = await (0, _agent_bundle_runtime__rspack_import_42.iC)({ + const result = await (0, _agent_bundle_runtime__rspack_import_48.iC)({ capabilities: { - command: (0, _agent_bundle_runtime__rspack_import_42.hU)(), - filesystem: (0, _agent_bundle_runtime__rspack_import_42.hU)(), - network: (0, _agent_bundle_runtime__rspack_import_42.hU)(), - projectRoot: (0, _agent_bundle_runtime__rspack_import_42.qC)({ + command: (0, _agent_bundle_runtime__rspack_import_48.hU)(), + filesystem: (0, _agent_bundle_runtime__rspack_import_48.hU)(), + network: (0, _agent_bundle_runtime__rspack_import_48.hU)(), + projectRoot: (0, _agent_bundle_runtime__rspack_import_48.qC)({ root: cwd }, 'derived') }, - host: (0, _agent_bundle_runtime__rspack_import_42.hU)('unsupported-surface'), + host: (0, _agent_bundle_runtime__rspack_import_48.hU)('unsupported-surface'), invocation: { kind: 'cli', operationId: command.routeId, surface: command.path.join(' ') }, - lineage: (0, _agent_bundle_runtime__rspack_import_42.hU)('unsupported-surface'), + lineage: (0, _agent_bundle_runtime__rspack_import_48.hU)('unsupported-surface'), plugin: pluginRoot.identity, process: processHit, signal: context.signal, - terminal: (0, _agent_bundle_runtime__rspack_import_42.qC)(context.terminal, 'native'), - workspace: (0, _agent_bundle_runtime__rspack_import_42.qC)({ + terminal: (0, _agent_bundle_runtime__rspack_import_48.qC)(context.terminal, 'native'), + workspace: (0, _agent_bundle_runtime__rspack_import_48.qC)({ root: cwd }, 'derived') }, async ()=>route.module.default({ @@ -39408,7 +43995,7 @@ var __webpack_modules__ = { return stream; } }); - const dispatcher = (0, _agent_bundle_runtime__rspack_import_43.jK)(host); + const dispatcher = (0, _agent_bundle_runtime__rspack_import_49.jK)(host); return Object.freeze({ close: async ()=>{ await worker.terminate(); @@ -39896,8 +44483,10 @@ var __webpack_modules__ = { var node_child_process__rspack_import_5 = __webpack_require__("node:child_process"); var _package_json__rspack_import_6 = __webpack_require__("./package.json"); var _codex_contract_js__rspack_import_7 = __webpack_require__("./src/core/codex/contract.js"); - var _desktop_shim_js__rspack_import_8 = __webpack_require__("./src/core/desktop-shim.js"); - var _user_machine_guidance_js__rspack_import_9 = __webpack_require__("./src/core/user-machine-guidance.js"); + var _codex_remote_control_js__rspack_import_8 = __webpack_require__("./src/core/codex/remote-control.js"); + var _codex_thread_id_js__rspack_import_11 = __webpack_require__("./src/core/codex/thread-id.js"); + var _desktop_shim_js__rspack_import_9 = __webpack_require__("./src/core/desktop-shim.js"); + var _user_machine_guidance_js__rspack_import_10 = __webpack_require__("./src/core/user-machine-guidance.js"); const PINNED_CODEX_VERSION = "0.158.0"; const UPSTREAM_DESKTOP_ISSUES = [ "https://github.com/openai/codex/issues/41014", @@ -39905,8 +44494,9 @@ var __webpack_modules__ = { ]; const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; const WS_MAX_HEADER_BYTES = 16 * 1024; - const WS_MAX_MESSAGE_BYTES = 4 * 1024 * 1024; - const WS_MAX_BUFFER_BYTES = 8 * 1024 * 1024; + const WS_MAX_MESSAGE_BYTES = 16 * 1024 * 1024; + const WS_MAX_BUFFER_BYTES = 32 * 1024 * 1024; + const CODEX_SEND_MAX_BYTES = 4 * 1024 * 1024; const WS_MAX_WRITE_BYTES = 8 * 1024 * 1024; const CODEX_MAX_REQUESTS = 128; const CODEX_MAX_LISTENERS = 128; @@ -39934,11 +44524,11 @@ var __webpack_modules__ = { function boxUnreachableMessage(path) { return [ "No Codex app-server control socket at " + path + ".", - _user_machine_guidance_js__rspack_import_9.M + _user_machine_guidance_js__rspack_import_10.M ].join("\n"); } function unreachableMessage(path, desktopAttached = "unknown", env = process.env) { - if ((0, _user_machine_guidance_js__rspack_import_9.R)({ + if ((0, _user_machine_guidance_js__rspack_import_10.R)({ path, env, home: env.HOME || (0, node_os__rspack_import_3.homedir)() @@ -40706,7 +45296,7 @@ var __webpack_modules__ = { let desktopShimConfigured = Boolean(shim && shim.installed && shim.wrapperPointsAtShim); if (shim === undefined) { try { - const live = (0, _desktop_shim_js__rspack_import_8.em)({ + const live = (0, _desktop_shim_js__rspack_import_9.em)({ env }); desktopShimConfigured = Boolean(live.installed && live.wrapperPointsAtShim); @@ -40777,6 +45367,13 @@ var __webpack_modules__ = { if (value == null) return null; return typeof value === "string" ? singleLine(value) : value; } + function summarizeSection(section) { + if (!isObject(section) || typeof section.id !== "string" || !section.id) return null; + return { + id: singleLine(section.id), + name: lineField(section.name) + }; + } function summarizeThread(t) { if (!isObject(t) || typeof t.id !== "string" || !t.id) throw new CodexProtocolError("thread/list", "entry without a string `id`"); const type = isObject(t.status) && typeof t.status.type === "string" ? t.status.type : "unknown"; @@ -40788,43 +45385,72 @@ var __webpack_modules__ = { preview: typeof t.preview === "string" ? stripTerminalControls(t.preview) : "", cwd: lineField(t.cwd), source: sourceField(t.source), - updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null + createdAt: typeof t.createdAt === "number" ? t.createdAt : null, + updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null, + section: summarizeSection(t.section), + projectId: lineField(t.projectId), + modelProvider: lineField(t.modelProvider), + model: lineField(t.model), + originator: lineField(t.originator) }; } const THREAD_LIST_MAX_LIMIT = 200; - async function listCodexThreads1({ limit = 20, cursor, env = process.env } = {}) { + async function listCodexThreads1({ limit = 20, cursor, modelProviders = [], env = process.env } = {}) { if (!Number.isInteger(limit) || limit < 1 || limit > THREAD_LIST_MAX_LIMIT) { throw new RangeError("--limit must be an integer 1-" + THREAD_LIST_MAX_LIMIT); } if (cursor !== undefined && (typeof cursor !== "string" || !cursor)) throw new RangeError("--cursor must be a non-empty string"); + if (!Array.isArray(modelProviders) || modelProviders.some((p)=>typeof p !== "string")) { + throw new RangeError("modelProviders must be an array of strings (empty = all providers)"); + } const { client } = await openSession(env); try { - const params = { + const baseParams = { limit, - useStateDbOnly: true, + modelProviders, ...cursor !== undefined ? { cursor } : {} }; let out; + let usedStateDbOnly = true; try { - out = await client.request("thread/list", params); + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: true + }); } catch (err) { throw transportError(err); } if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + if (out.data.length === 0 && cursor === undefined && (out.nextCursor == null || out.nextCursor === "")) { + try { + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: false + }); + usedStateDbOnly = false; + } catch (err) { + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + } if (out.nextCursor != null && typeof out.nextCursor !== "string") throw new CodexProtocolError("thread/list", "`nextCursor` is not a string"); const threads = out.data.map(summarizeThread); return { threads, nextCursor: out.nextCursor ?? null, - limit + limit, + useStateDbOnly: usedStateDbOnly }; } finally{ client.close(); } } - function explainSendError(err, threadId) { + function explainSendError(err, threadId, env = process.env) { + if (err instanceof _codex_remote_control_js__rspack_import_8.Rg) return err; + const mapped = (0, _codex_remote_control_js__rspack_import_8.R3)(err, threadId, env); + if (mapped instanceof _codex_remote_control_js__rspack_import_8.Rg) return mapped; if (!(err instanceof CodexRpcError)) return err; const msg = String(err.rpc && err.rpc.message || ""); if (/no rollout found|thread not found/i.test(msg)) { @@ -40899,12 +45525,19 @@ var __webpack_modules__ = { function assertThreadAllowed(threadId, env = process.env) { const raw = env.GROK_BOT_CODEX_THREADS; if (raw == null || raw.trim() === "") return; - const allowed = raw.split(",").map((s)=>s.trim()).filter(Boolean); - if (!allowed.includes(threadId)) { - throw new CodexSendError("Codex thread " + threadId + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { + const bare = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + const allowed = raw.split(",").map((s)=>{ + try { + return (0, _codex_thread_id_js__rspack_import_11.SX)(s.trim()); + } catch { + return s.trim(); + } + }).filter(Boolean); + if (!allowed.includes(bare)) { + throw new CodexSendError("Codex thread " + bare + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { delivery: "rejected", reason: "route-not-allowed", - threadId + threadId: bare }); } } @@ -40974,6 +45607,7 @@ var __webpack_modules__ = { } async function listCodexQueue(threadId, { env = process.env, limit = 50, cursor } = {}) { requireExperimental(env, "gbot codex queue"); + threadId = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); const { client } = await openSession(env, { experimental: true }); @@ -41004,6 +45638,64 @@ var __webpack_modules__ = { client.close(); } } + async function readCodexThread(threadId, { limit = 100, env = process.env } = {}) { + if (!Number.isInteger(limit) || limit < 1 || limit > 2000) { + throw new RangeError("limit must be an integer 1-2000"); + } + const bare = normalizeCodexThreadId(threadId); + const { client } = await openSession(env); + try { + let meta; + try { + meta = await client.request("thread/read", { + threadId: bare + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + const turns = []; + let cursor; + const seen = new Set(); + for(let page = 0; page < 40 && turns.length < limit; page++){ + let out; + try { + out = await client.request("thread/turns/list", { + threadId: bare, + limit: Math.min(100, limit - turns.length), + itemsView: "full", + sortDirection: "desc", + ...cursor !== undefined ? { + cursor + } : {} + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) { + throw new CodexProtocolError("thread/turns/list", "missing `data` array"); + } + for (const row of out.data)turns.push(row); + cursor = out.nextCursor ?? null; + if (cursor === null) break; + if (typeof cursor !== "string" || !cursor || seen.has(cursor)) { + throw new CodexProtocolError("thread/turns/list", "invalid or repeated cursor"); + } + seen.add(cursor); + } + return { + threadId: bare, + thread: isObject(meta) && isObject(meta.thread) ? meta.thread : null, + turns: turns.slice(0, limit), + nextCursor: cursor ?? null + }; + } finally{ + client.close(); + } + } async function sendToCodexThread(threadId, text, { env = process.env, envelope = buildEnvelope({ env }), whenBusy = "reject", session, expectedTurnId, expectedCwd, model, effort, signal } = {}) { @@ -41049,8 +45741,8 @@ var __webpack_modules__ = { ] : [] ].includes(whenBusy)) throw new RangeError("--when-busy must be reject, queue, or persistent steer"); if (whenBusy === "steer" && (typeof expectedTurnId !== "string" || !ID_PATTERN.test(expectedTurnId))) throw new RangeError("steer requires expectedTurnId"); - if (typeof threadId !== "string" || !ID_PATTERN.test(threadId)) throw new RangeError("Invalid threadId"); - if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > WS_MAX_MESSAGE_BYTES) throw new RangeError("Message must contain text within 4 MiB"); + threadId = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > CODEX_SEND_MAX_BYTES) throw new RangeError("Message must contain text within 4 MiB"); if (!envelope || typeof envelope.messageId !== "string" || !ID_PATTERN.test(envelope.messageId)) throw new RangeError("Invalid envelope messageId"); const validated = buildEnvelope({ correlationId: envelope.correlationId, @@ -41098,8 +45790,10 @@ var __webpack_modules__ = { }); } catch (err) { assertNotCancelled(); - if (err instanceof CodexSendError) throw err; - throw new CodexSendError(explainSendError(err, threadId).message, { + if (err instanceof CodexSendError || err instanceof _codex_remote_control_js__rspack_import_8.Rg) throw err; + const explained = explainSendError(err, threadId, env); + if (explained instanceof _codex_remote_control_js__rspack_import_8.Rg) throw explained; + throw new CodexSendError(explained.message, { delivery: err instanceof CodexRpcError ? "rejected" : err && err.delivery || "unknown", 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", threadId, @@ -41336,7 +46030,11 @@ var __webpack_modules__ = { "messageId", "correlationId", "refused", - "hop" + "hop", + "hostId", + "hostName", + "hint", + "code" ]){ if (error[key] !== undefined) out[key] = error[key]; } @@ -41372,9 +46070,12 @@ var __webpack_modules__ = { var _contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); var node_fs__rspack_import_1 = __webpack_require__("node:fs"); var _codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); + var _remote_control_js__rspack_import_3 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); const ID = /^[A-Za-z0-9_.:-]{1,128}$/; const BUDGET = 4 * 1024 * 1024; function conversationId(value, name = 'threadId') { + if (name === 'threadId') return (0, _thread_id_js__rspack_import_4.SX)(value); if (typeof value !== 'string' || !ID.test(value)) throw new RangeError(`${name} must be 1-128 identifier characters`); return value; } @@ -41499,7 +46200,8 @@ var __webpack_modules__ = { if (expected !== undefined && (typeof cwd !== 'string' || (0, node_fs__rspack_import_1.realpathSync)(cwd) !== expected)) throw new Error('Codex thread cwd does not match expectedCwd'); } catch (error) { await close(); - throw error; + if (error instanceof _remote_control_js__rspack_import_3.Rg) throw error; + throw (0, _remote_control_js__rspack_import_3.R3)(error, threadId, env); } async function wait({ turnId, messageId, afterMessageId, timeoutMs = 120000, signal: waitSignal, maxOutputBytes = 1048576 }) { conversationId(turnId, 'turnId'); @@ -41849,6 +46551,269 @@ var __webpack_modules__ = { j: ()=>openCodexConversation }); }, + "./src/core/codex/remote-control.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var node_os__rspack_import_1 = __webpack_require__("node:os"); + var node_path__rspack_import_2 = __webpack_require__("node:path"); + var _thread_id_js__rspack_import_3 = __webpack_require__("./src/core/codex/thread-id.js"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + class RemoteThreadNotLoadedError1 extends Error { + constructor(threadId, hostId = null, hostName = null){ + const hostLabel = hostName ? `${JSON.stringify(hostName)} (${JSON.stringify(hostId)})` : hostId ? JSON.stringify(hostId) : null; + const owner = hostLabel ? `owned by remote-control host ${hostLabel}` : 'owned by a remote-control host (hostId unknown; check ~/.codex/.codex-global-state.json)'; + const hint = hostId ? `Read this thread via host ${JSON.stringify(hostId)}'s app-server` + (hostName ? ` (${hostName})` : '') + '. SSH remoting is not implemented in this adapter.' : "Read this thread via the owning host's app-server. SSH remoting is not implemented in this adapter."; + super(`Codex thread ${JSON.stringify(threadId)} is not loaded on the local app-server; ${owner}. ${hint}`); + this.name = 'RemoteThreadNotLoadedError'; + this.code = 'REMOTE_THREAD_NOT_LOADED'; + this.delivery = 'rejected'; + this.reason = 'remote-thread-not-loaded'; + this.threadId = threadId; + this.hostId = hostId; + this.hostName = hostName; + this.hint = hint; + } + } + function codexGlobalStatePath(env = process.env) { + const home = env.CODEX_HOME || (0, node_path__rspack_import_2.join)((0, node_os__rspack_import_1.homedir)(), '.codex'); + return (0, node_path__rspack_import_2.join)(home, '.codex-global-state.json'); + } + function loadCodexGlobalState(env = process.env) { + let raw; + try { + raw = (0, node_fs__rspack_import_0.readFileSync)(codexGlobalStatePath(env), 'utf8'); + } catch { + return null; + } + try { + const data = JSON.parse(raw); + if (!data || typeof data !== 'object' || Array.isArray(data)) return null; + return data; + } catch { + return null; + } + } + function codexStateEntries(data) { + const nested = data?.['electron-persisted-atom-state']; + return [ + nested, + data + ].filter((value)=>value && typeof value === 'object' && !Array.isArray(value)); + } + function findRemoteThreadHost(threadId, env = process.env) { + let bare; + try { + bare = (0, _thread_id_js__rspack_import_3.SX)(threadId); + } catch { + bare = typeof threadId === 'string' && threadId.startsWith("local:") ? threadId.slice("local:".length) : threadId; + } + if (!bare) return null; + const data = loadCodexGlobalState(env); + if (!data) return null; + const hostNames = collectHostNames(data); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value) ?? null; + for (const entry of extractThreadEntries(value)){ + const id = threadIdFromEntry(entry); + if (!id) continue; + const entryBare = id.startsWith("local:") ? id.slice("local:".length) : id; + if (entryBare === bare) return { + hostId, + hostName + }; + } + } + return null; + } + function mapRemoteThreadError(error, threadId, env = process.env) { + const message = errorMessage(error); + if (!/thread not loaded|not loaded|no rollout found|thread not found|invalid thread id/i.test(message)) { + return error instanceof Error ? error : new Error(message || String(error)); + } + const remote = findRemoteThreadHost(threadId, env); + if (remote) { + return new RemoteThreadNotLoadedError1(threadId, remote.hostId, remote.hostName); + } + if (/thread not loaded|not loaded/i.test(message)) { + return new RemoteThreadNotLoadedError1(threadId, null, null); + } + return error instanceof Error ? error : new Error(message || String(error)); + } + function collectHostNames(data) { + const names = new Map(); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const hostId = stringField(entry, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) continue; + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const id = stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + entries.push(id ? entry : { + ...entry, + id: key + }); + } + } + return entries; + } + function threadIdFromEntry(entry) { + if (typeof entry === 'string' && entry) return entry; + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + return stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + for (const key of keys){ + const value = row[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) return String(error.message); + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); + } + __webpack_require__.d(__webpack_exports__, { + IT: ()=>loadCodexGlobalState, + R3: ()=>mapRemoteThreadError, + Rg: ()=>RemoteThreadNotLoadedError1, + xr: ()=>codexStateEntries + }); + }, + "./src/core/codex/thread-id.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + const LOCAL_THREAD_ID_PREFIX = 'local:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/; + function isTemporaryDesktopThreadId(threadId) { + return typeof threadId === 'string' && (threadId.startsWith(TEMP_THREAD_ID_PREFIX) || threadId.startsWith('client-new-thread:')); + } + function normalizeCodexThreadId1(threadId) { + if (typeof threadId !== 'string' || !threadId) { + throw new RangeError('Invalid threadId'); + } + if (isTemporaryDesktopThreadId(threadId)) { + throw new RangeError('Temporary Desktop thread id ' + JSON.stringify(threadId) + ' has no app-server mapping; wait for the real conversation id'); + } + const bare = threadId.startsWith(LOCAL_THREAD_ID_PREFIX) ? threadId.slice(LOCAL_THREAD_ID_PREFIX.length) : threadId; + if (!bare || !ID_PATTERN.test(bare)) { + throw new RangeError('Invalid threadId'); + } + return bare; + } + __webpack_require__.d(__webpack_exports__, { + SX: ()=>normalizeCodexThreadId1, + vr: ()=>isTemporaryDesktopThreadId + }, { + hA: TEMP_THREAD_ID_PREFIX + }); + }, "./src/core/commands.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { var _store_js__rspack_import_0 = __webpack_require__("./src/core/store.js"); var _gateway_js__rspack_import_1 = __webpack_require__("./src/core/gateway.js"); diff --git a/artifact/mcp/mcp-grok-bot-b8c2461e-flight.mjs b/artifact/mcp/mcp-grok-bot-b8c2461e-flight.mjs index cc30cd0..ec8d8ab 100644 --- a/artifact/mcp/mcp-grok-bot-b8c2461e-flight.mjs +++ b/artifact/mcp/mcp-grok-bot-b8c2461e-flight.mjs @@ -8084,583 +8084,4863 @@ var __webpack_modules__ = { module.exports = __webpack_require__("./node_modules/react/cjs/react.react-server.development.js"); } }, - "./src/core/claude-routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { - var zod__rspack_import_1 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _claude_channel_js__rspack_import_0 = __webpack_require__("./src/core/claude-channel.js"); - const inputSchema = zod__rspack_import_1.Ikc({ - name: zod__rspack_import_1.YjP().regex(/^[a-zA-Z0-9_-]{1,32}$/).describe('Explicit name of the live Claude channel.'), - message: zod__rspack_import_1.YjP().min(1).max(65536), - timeoutMs: zod__rspack_import_1.aig().int().min(1).max(120000).default(60000) - }).strict(); - const resultSchema = zod__rspack_import_1.Ikc({ - delivery: zod__rspack_import_1.k5n([ - 'replied', - 'unknown', - 'rejected' - ]), - requestId: zod__rspack_import_1.YjP().optional(), - reply: zod__rspack_import_1.YjP().optional(), - error: zod__rspack_import_1.YjP().optional(), - exitCode: zod__rspack_import_1.KCZ([ - zod__rspack_import_1.euz(0), - zod__rspack_import_1.euz(1) - ]) - }).strict(); - async function sendOperation(input) { - try { - const result = await (0, _claude_channel_js__rspack_import_0.s)(input); - return { - ...result, - exitCode: result.delivery === 'replied' ? 0 : 1 - }; - } catch (error) { - return { - delivery: 'rejected', - error: error instanceof Error ? error.message : String(error), - exitCode: 1 - }; - } - } - __webpack_require__.d(__webpack_exports__, { - UP: ()=>sendOperation - }, { - FD: resultSchema, - is: inputSchema - }); - }, - "./src/core/codex/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { - var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + "./src/core/chatgpt-desktop/app-server-fallback.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { var _codex_bridge_js__rspack_import_0 = __webpack_require__("./src/core/codex-bridge.js"); - var _contract_js__rspack_import_1 = __webpack_require__("./src/core/codex/contract.js"); - var _conversation_js__rspack_import_2 = __webpack_require__("./src/core/codex/conversation.js"); - const id = zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9_.:-]{1,128}$/); - const observationFields = { - expectedCwd: zod__rspack_import_3.YjP().min(1).optional(), - threadId: id, - timeoutMs: zod__rspack_import_3.aig().int().min(1).max(7200000).optional() - }; - const sendFields = { - ...observationFields, - correlationId: id.optional(), - envelope: zod__rspack_import_3.zMY().optional(), - hop: zod__rspack_import_3.aig().int().min(0).optional(), - model: zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/).optional(), - effort: zod__rspack_import_3.k5n([ - 'none', - 'minimal', - 'low', - 'medium', - 'high', - 'xhigh' - ]).optional(), - replyTo: id.optional(), - expectedTurnId: id.optional(), - whenBusy: zod__rspack_import_3.k5n([ - 'reject', - 'queue', - 'steer' - ]).default('reject'), - wait: zod__rspack_import_3.zMY().default(false), - maxOutputBytes: zod__rspack_import_3.aig().int().min(1).max(4194304).optional() - }; - const sendSchema = zod__rspack_import_3.Ikc({ - ...sendFields, - message: zod__rspack_import_3.YjP().min(1).max(4194304) - }).strict(); - const waitSchema = zod__rspack_import_3.Ikc({ - ...observationFields, - turnId: id, - messageId: id.optional(), - maxOutputBytes: zod__rspack_import_3.aig().int().min(1).max(4194304).optional() - }).strict(); - const watchSchema = zod__rspack_import_3.Ikc({ - expectedCwd: zod__rspack_import_3.YjP().min(1).optional(), - threadId: id, - timeoutMs: zod__rspack_import_3.aig().int().min(1).max(600000).optional(), - maxEvents: zod__rspack_import_3.aig().int().min(1).max(500).default(100) - }).strict(); - const threadsSchema = zod__rspack_import_3.Ikc({ - limit: zod__rspack_import_3.aig().int().min(1).max(200).default(20), - cursor: zod__rspack_import_3.YjP().min(1).max(4096).optional() - }).strict(); - const resultSchema = zod__rspack_import_3.Ikc({ - exitCode: zod__rspack_import_3.aig().int().min(0).max(1), - threadId: zod__rspack_import_3.YjP().optional(), - turnId: zod__rspack_import_3.YjP().optional(), - messageId: zod__rspack_import_3.YjP().optional(), - delivery: zod__rspack_import_3.k5n([ - 'accepted', - 'queued', - 'rejected', - 'unknown' - ]).optional(), - execution: zod__rspack_import_3.Ikc({ - state: zod__rspack_import_3.k5n([ - 'completed', - 'failed', - 'interrupted', - 'waiting-for-input', - 'timeout', - 'disconnected', - 'unknown' - ]), - error: zod__rspack_import_3.L5J().optional() - }).optional(), - reply: zod__rspack_import_3.Ikc({ - text: zod__rspack_import_3.YjP().max(4194304), - items: zod__rspack_import_3.YOg(zod__rspack_import_3.Ikc({ - id: zod__rspack_import_3.YjP(), - type: zod__rspack_import_3.euz('agentMessage'), - phase: zod__rspack_import_3.k5n([ - 'final_answer' - ]).nullable(), - text: zod__rspack_import_3.YjP().max(4194304) - })).max(2000), - truncated: zod__rspack_import_3.zMY() - }).optional(), - interactions: zod__rspack_import_3.YOg(zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.L5J())).max(500).optional(), - events: zod__rspack_import_3.YOg(zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.L5J())).max(500).optional(), - reason: zod__rspack_import_3.YjP().optional(), - truncated: zod__rspack_import_3.zMY().optional() - }).passthrough(); - async function sendOperation(input, signal, progress, oneShot = false) { - let envelope; - let receipt; - try { - envelope = (0, _codex_bridge_js__rspack_import_0.nq)(input); - if (oneShot && !input.wait && input.whenBusy !== 'steer') { - return await (0, _codex_bridge_js__rspack_import_0.ZS)(input.threadId, input.message, { - envelope, - whenBusy: input.whenBusy, - expectedCwd: input.expectedCwd, - model: input.model, - effort: input.effort, - signal - }); - } - await progress?.('Opening Codex conversation'); - const conversation = await (0, _conversation_js__rspack_import_2.j)(input.threadId, { - expectedCwd: input.expectedCwd, - signal + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _remote_threads_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/remote-threads.ts"); + var _payload_budget_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/payload-budget.ts"); + var _thread_ids_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const LIST_PAGE_PREFIX = 'desktop-list-v1:'; + const SEARCH_PAGE_PREFIX = 'desktop-search-v1:'; + const READ_PAGE_PREFIX = 'desktop-read-v1:'; + async function appServerListThreads({ limit = 50, cursor, modelProviders = [] } = {}) { + let sourceCursor = cursor; + let skip = 0; + let fetchLimit = Math.min(limit, 25); + if (cursor?.startsWith(LIST_PAGE_PREFIX)) { + const state = (0, _payload_budget_js__rspack_import_4.i3)(LIST_PAGE_PREFIX, cursor); + if (!Number.isSafeInteger(state.skip) || state.skip < 0 || !Number.isSafeInteger(state.fetchLimit) || state.fetchLimit < 1 || state.fetchLimit > 25 || state.cursor !== null && typeof state.cursor !== 'string') { + throw new Error('Invalid ChatGPT Desktop list page cursor'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + fetchLimit = state.fetchLimit; + } + const base = { + backend: 'app-server', + limit, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined + }; + const threads = []; + const seen = new Set(); + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: fetchLimit, + cursor: sourceCursor, + modelProviders }); - try { - receipt = await conversation.send(input.message, { - envelope, - whenBusy: input.whenBusy, - expectedTurnId: input.expectedTurnId, - model: input.model, - effort: input.effort - }); - if (!input.wait || receipt.delivery !== 'accepted' || !receipt.turnId) return receipt; - await progress?.(`Accepted message ${receipt.messageId}; observing turn ${receipt.turnId}`); - const result = await conversation.wait({ - turnId: receipt.turnId, - messageId: receipt.messageId, - timeoutMs: input.timeoutMs, - maxOutputBytes: input.maxOutputBytes, - signal - }); - return { - ...receipt, - execution: result.execution, - reply: result.reply, - interactions: result.interactions, - exitCode: result.execution.state === 'completed' ? 0 : 1 + if (skip > out.threads.length) throw new Error('ChatGPT Desktop list page cursor is stale'); + for(let index = skip; index < out.threads.length; index++){ + let row = (0, _payload_budget_js__rspack_import_4.eX)(mapListedThread(out.threads[index])); + const probe = { + ...base, + threads: [ + ...threads, + row + ], + nextCursor: LIST_PAGE_PREFIX, + exitCode: 0 }; - } finally{ - await conversation.close(); + if ((0, _payload_budget_js__rspack_import_4.sQ)(probe) > 614400) { + if (threads.length) return { + ...base, + threads, + nextCursor: (0, _payload_budget_js__rspack_import_4.oI)(LIST_PAGE_PREFIX, { + cursor: sourceCursor ?? null, + skip: index, + fetchLimit + }), + warnings: [ + 'List page stopped at the MCP byte budget; continue with nextCursor' + ] + }; + row = (0, _payload_budget_js__rspack_import_4.MH)(row); + if ((0, _payload_budget_js__rspack_import_4.sQ)({ + ...base, + threads: [ + row + ], + nextCursor: LIST_PAGE_PREFIX, + exitCode: 0 + }) > 614400) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + threads.push(row); + if (threads.length >= limit) { + const nextCursor = index + 1 < out.threads.length ? (0, _payload_budget_js__rspack_import_4.oI)(LIST_PAGE_PREFIX, { + cursor: sourceCursor ?? null, + skip: index + 1, + fetchLimit + }) : out.nextCursor ?? null; + return { + ...base, + threads, + nextCursor + }; + } } - } catch (error) { - if (receipt?.delivery === 'accepted') return { - ...receipt, - execution: { - state: 'unknown', - error: (0, _contract_js__rspack_import_1.DG)(error).error - }, - reply: { - text: '', - items: [], - truncated: true - }, - interactions: [], - exitCode: 1 - }; - return { - ...(0, _contract_js__rspack_import_1.DG)(error), - threadId: input.threadId, - ...envelope ? { - messageId: envelope.messageId, - correlationId: envelope.correlationId, - hop: envelope.hop - } : {} + if (out.nextCursor == null) return { + ...base, + threads, + nextCursor: null }; + if (typeof out.nextCursor !== 'string' || !out.nextCursor || seen.has(out.nextCursor)) { + throw new Error('ChatGPT Desktop list returned an invalid or repeated cursor'); + } + seen.add(out.nextCursor); + sourceCursor = out.nextCursor; + skip = 0; } + return { + ...base, + threads, + nextCursor: sourceCursor, + warnings: [ + 'List stopped after 400 app-server pages; continue with nextCursor' + ] + }; } - async function observeOperation(kind, input, signal, progress) { - try { - await progress?.(`Observing Codex thread ${input.threadId}`); - const conversation = await (0, _conversation_js__rspack_import_2.j)(input.threadId, { - expectedCwd: input.expectedCwd, - signal + async function appServerSearchThreads({ query, limit = 50, cursor, modelProviders = [], project }) { + const needle = query.trim().toLowerCase(); + if (!needle) { + return appServerListThreads({ + limit, + cursor, + modelProviders }); - try { - if (kind === 'wait') { - const result = await conversation.wait({ - ...input, - signal - }); + } + const matched = []; + let sourceCursor; + let skip = 0; + if (cursor) { + const state = (0, _payload_budget_js__rspack_import_4.i3)(SEARCH_PAGE_PREFIX, cursor); + if (state.query !== query || state.project !== project || JSON.stringify(state.modelProviders) !== JSON.stringify(modelProviders) || !Number.isSafeInteger(state.skip) || state.skip < 0) { + throw new Error('Search cursor does not match this query and its filters'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + } + const pageCursor = (rpcCursor, offset)=>(0, _payload_budget_js__rspack_import_4.oI)(SEARCH_PAGE_PREFIX, { + cursor: rpcCursor ?? null, + skip: offset, + query, + project, + modelProviders + }); + const base = { + backend: 'app-server', + limit, + query, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined + }; + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: 25, + cursor: sourceCursor, + modelProviders + }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop search cursor is stale'); + for(let index = skip; index < out.threads.length; index++){ + const mapped = mapListedThread(out.threads[index]); + if (!threadMatchesQuery(mapped, needle) || project && ![ + mapped.project, + mapped.projectId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(project.trim().toLowerCase()))) continue; + let row = (0, _payload_budget_js__rspack_import_4.eX)(mapped); + if ((0, _payload_budget_js__rspack_import_4.sQ)({ + ...base, + threads: [ + ...matched, + row + ], + nextCursor: SEARCH_PAGE_PREFIX, + exitCode: 0 + }) > 614400) { + if (matched.length) return { + ...base, + threads: matched, + nextCursor: pageCursor(sourceCursor, index), + warnings: [ + 'Search page stopped at the MCP byte budget; continue with nextCursor' + ] + }; + row = (0, _payload_budget_js__rspack_import_4.MH)(row); + } + matched.push(row); + if (matched.length >= limit) { + const nextCursor = index + 1 < out.threads.length ? pageCursor(sourceCursor, index + 1) : out.nextCursor ? pageCursor(out.nextCursor, 0) : null; return { - ...result, - exitCode: result.execution.state === 'completed' ? 0 : 1 + ...base, + threads: matched, + nextCursor }; } - const result = await conversation.watch({ - ...input, - signal - }); - return { - ...result, - exitCode: [ - 'timeout', - 'event-limit' - ].includes(result.reason) ? 0 : 1 - }; - } finally{ - await conversation.close(); } + if (out.nextCursor == null) return { + ...base, + threads: matched, + nextCursor: null + }; + sourceCursor = out.nextCursor; + skip = 0; + } + return { + ...base, + threads: matched, + nextCursor: pageCursor(sourceCursor, 0), + warnings: [ + 'Search stopped after 400 app-server pages; continue with nextCursor' + ] + }; + } + async function discoverModelProviders() { + const providers = new Set(); + let threadCount = 0; + let cursor; + let complete = false; + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: 25, + cursor, + modelProviders: [] + }); + threadCount += out.threads.length; + for (const thread of out.threads){ + if (typeof thread.modelProvider === 'string' && thread.modelProvider) { + providers.add(thread.modelProvider); + } + } + if (out.nextCursor == null || typeof out.nextCursor !== 'string') { + complete = true; + break; + } + cursor = out.nextCursor; + } + return { + providers: [ + ...providers + ].sort(), + source: 'thread/list-distinct', + threadCount, + complete + }; + } + async function appServerReadThread({ threadId, limit, full = false, cursor }) { + limit ??= full ? 2000 : 100; + const bare = (0, _thread_ids_js__rspack_import_3.NM)(threadId); + const remote = (0, _remote_threads_js__rspack_import_2.KI)(bare); + if (remote) { + try { + return await readLocalThread(bare, { + limit, + full, + cursor + }); + } catch (error) { + if (isThreadNotLoaded(error) || isUnknownThread(error)) { + throw new _errors_js__rspack_import_1.Rg(bare, remote.hostId, remote.hostName); + } + throw maybeRemoteThreadError(error, bare); + } + } + try { + return await readLocalThread(bare, { + limit, + full, + cursor + }); } catch (error) { + throw maybeRemoteThreadError(error, bare); + } + } + async function appServerUnarchiveThread(threadId) { + const bare = (0, _thread_ids_js__rspack_import_3.NM)(threadId); + const { client } = await (0, _codex_bridge_js__rspack_import_0.eC)(); + try { + const result = await client.request('thread/unarchive', { + threadId: bare + }); + if (!result || typeof result !== 'object' || !('thread' in result) || !result.thread || typeof result.thread !== 'object' || !('id' in result.thread) || result.thread.id !== bare) { + throw new Error(`thread/unarchive returned no thread for ${bare}`); + } + } finally{ + client.close(); + } + } + async function readLocalThread(bare, { limit, full, cursor }) { + const { client } = await (0, _codex_bridge_js__rspack_import_0.eC)(); + try { + const meta = await readThreadMetadata(client, bare); + const { turns, complete, nextCursor } = await listTurnsFull(client, bare, { + limit, + full, + cursor, + title: meta.title + }); return { - ...(0, _contract_js__rspack_import_1.DG)(error), - threadId: input.threadId, - ...'turnId' in input ? { - turnId: input.turnId, - messageId: input.messageId - } : {} + threadId: (0, _thread_ids_js__rspack_import_3.BS)(bare), + turns, + backend: 'app-server', + limit, + full, + complete, + nextCursor, + ...complete ? {} : { + warnings: [ + 'Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment' + ] + }, + ...meta.title ? (0, _payload_budget_js__rspack_import_4.aA)(meta.title) : {}, + cwd: meta.cwd, + status: meta.status, + modelProvider: meta.modelProvider, + model: meta.model }; + } finally{ + client.close(); } } - async function threadsOperation(input) { + async function appServerStatusProbe() { + const status = await (0, _codex_bridge_js__rspack_import_0.MU)(); + return { + reachable: Boolean(status.reachable && status.mode === 'daemon'), + mode: typeof status.mode === 'string' ? status.mode : undefined, + socketPath: typeof status.socketPath === 'string' ? status.socketPath : undefined + }; + } + function mapListedThread(thread) { + const bare = String(thread.id ?? ''); + const section = thread.section && typeof thread.section.id === 'string' ? { + id: thread.section.id, + name: thread.section.name ?? null + } : null; + const pinned = section?.id === 'Pinned' || section?.name === 'Pinned' || false; + return { + threadId: bare ? (0, _thread_ids_js__rspack_import_3.BS)(bare) : '', + title: String(thread.name || thread.preview || thread.cwd || thread.id || ''), + pinned, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + preview: typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: thread.cwd ?? null, + createdAt: thread.createdAt ?? null, + updatedAt: thread.updatedAt ?? null, + section, + projectId: thread.projectId ?? null, + project: thread.projectId ?? undefined, + status: thread.status, + modelProvider: thread.modelProvider ?? null, + model: thread.model ?? null, + originator: thread.originator ?? null + }; + } + function threadMatchesQuery(thread, needle) { + const haystacks = [ + thread.threadId, + thread.title, + thread.preview, + thread.cwd, + thread.project, + thread.projectId, + thread.model, + thread.modelProvider, + thread.originator, + thread.section?.name, + thread.section?.id + ]; + return haystacks.some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + async function readThreadMetadata(client, threadId) { try { + const read = await client.request('thread/read', { + threadId + }); + const thread = read.thread && typeof read.thread === 'object' ? read.thread : {}; + const statusObj = thread.status; + const statusType = statusObj && typeof statusObj === 'object' && typeof statusObj.type === 'string' ? String(statusObj.type) : typeof thread.status === 'string' ? thread.status : undefined; return { - ...await (0, _codex_bridge_js__rspack_import_0.X$)(input), - exitCode: 0 + title: typeof thread.name === 'string' ? thread.name : typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: typeof thread.cwd === 'string' ? thread.cwd : null, + status: statusType, + modelProvider: typeof thread.modelProvider === 'string' ? thread.modelProvider : null, + model: typeof thread.model === 'string' ? thread.model : null }; } catch (error) { - return (0, _contract_js__rspack_import_1.DG)(error); + throw maybeRemoteThreadError(error, threadId); + } + } + async function listTurnsFull(client, threadId, { limit, full, cursor, title }) { + const state = cursor ? (0, _payload_budget_js__rspack_import_4.i3)(READ_PAGE_PREFIX, cursor) : null; + if (state && (state.threadId !== threadId || state.full !== full || !Number.isSafeInteger(state.skip) || state.skip < 0 || state.rpcCursor !== null && typeof state.rpcCursor !== 'string' || state.field !== undefined && ![ + 'text', + 'userText', + 'assistantText' + ].includes(state.field) || state.offset !== undefined && (!Number.isSafeInteger(state.offset) || state.offset < 0))) { + throw new Error('Read cursor does not match this thread or mode'); + } + let rpcCursor = state?.rpcCursor ?? undefined; + let skip = state?.skip ?? 0; + let fragmentField = state?.field; + let fragmentOffset = state?.offset ?? 0; + let itemsView = true; + let method = 'thread/turns/list'; + const seen = new Set(); + const turns = []; + const pageCursor = (source, index, field, offset)=>(0, _payload_budget_js__rspack_import_4.oI)(READ_PAGE_PREFIX, { + threadId, + full, + rpcCursor: source ?? null, + skip: index, + ...field ? { + field, + offset + } : {} + }); + const fits = (candidate)=>(0, _payload_budget_js__rspack_import_4.sQ)({ + threadId, + turns: [ + ...turns, + candidate + ], + backend: 'app-server', + limit, + full, + complete: false, + nextCursor: READ_PAGE_PREFIX, + title: title ? (0, _payload_budget_js__rspack_import_4.aA)(title).title : undefined, + warnings: [ + 'Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment' + ], + exitCode: 0 + }) <= 614400; + while(turns.length < limit){ + let result; + try { + result = await client.request(method, { + threadId, + limit: Math.min(10, Math.max(limit - turns.length, skip + 1)), + sortDirection: full ? 'asc' : 'desc', + ...method === 'thread/turns/list' && itemsView ? { + itemsView: 'full' + } : {}, + ...rpcCursor !== undefined ? { + cursor: rpcCursor + } : {} + }); + } catch (error) { + const code = error?.rpc?.code; + if (turns.length === 0 && code === -32602 && itemsView) { + itemsView = false; + continue; + } + if (turns.length === 0 && code === -32601 && method === 'thread/turns/list') { + method = 'thread/items/list'; + continue; + } + throw maybeRemoteThreadError(error, threadId); + } + if (!Array.isArray(result?.data)) throw new Error(`${method} missing data array for ${threadId}`); + if (skip > result.data.length) throw new Error('Read cursor is stale'); + const pageTurns = normalizeTurns(result.data); + for(let index = skip; index < pageTurns.length; index++){ + const turn = pageTurns[index]; + if (!fragmentField && turns.length && !fits(turn)) { + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index) + }; + } + if (!fragmentField && fits(turn)) { + turns.push(turn); + } else { + const fields = [ + 'text', + 'userText', + 'assistantText' + ].filter((field)=>typeof turn[field] === 'string' && turn[field].length > 0); + let fieldIndex = fragmentField ? fields.indexOf(fragmentField) : 0; + if (fieldIndex < 0) throw new Error('Read fragment cursor is stale'); + for(; fieldIndex < fields.length; fieldIndex++){ + const field = fields[fieldIndex]; + const value = turn[field]; + let offset = field === fragmentField ? fragmentOffset : 0; + if (offset > value.length) throw new Error('Read fragment cursor is stale'); + while(offset < value.length){ + let size = Math.min(value.length - offset, 32768); + let fragment; + do { + let end = offset + size; + if (end < value.length && /[\uD800-\uDBFF]/u.test(value[end - 1])) end--; + const chunk = value.slice(offset, end); + fragment = { + turnKey: turn.turnKey, + role: turn.role, + text: field === 'text' ? chunk : '', + ...field === 'userText' ? { + userText: chunk + } : {}, + ...field === 'assistantText' ? { + assistantText: chunk + } : {}, + startedAt: turn.startedAt, + endedAt: turn.endedAt, + textTruncated: true, + continuation: { + field, + offsetChars: offset, + totalChars: value.length, + fieldComplete: end === value.length, + turnComplete: end === value.length && fieldIndex === fields.length - 1 + } + }; + if (fits(fragment)) { + size = end - offset; + break; + } + size = Math.floor(size / 2); + }while (size > 0); + if (size === 0) { + if (turns.length) return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) + }; + throw new Error('Read fragment metadata exceeds the MCP result byte budget'); + } + turns.push(fragment); + offset += size; + if (turns.length >= limit || offset < value.length && !fits({ + ...fragment, + text: 'x'.repeat(32768) + })) { + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) + }; + } + } + } + fragmentField = undefined; + fragmentOffset = 0; + } + if (turns.length >= limit) { + const nextCursor = index + 1 < pageTurns.length ? pageCursor(rpcCursor, index + 1) : result.nextCursor ? pageCursor(result.nextCursor, 0) : null; + return { + turns: chronologicalPage(turns, full), + complete: nextCursor === null, + nextCursor + }; + } + } + if (result.nextCursor == null) return { + turns: chronologicalPage(turns, full), + complete: true, + nextCursor: null + }; + if (typeof result.nextCursor !== 'string' || !result.nextCursor || seen.has(result.nextCursor)) { + throw new Error(`${method} returned an invalid or repeated cursor`); + } + seen.add(result.nextCursor); + rpcCursor = result.nextCursor; + skip = 0; + } + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, skip) + }; + } + function chronologicalPage(turns, full) { + if (full) return turns; + const groups = []; + for (const turn of turns){ + const last = groups.at(-1); + if (last?.[0]?.turnKey === turn.turnKey) last.push(turn); + else groups.push([ + turn + ]); + } + return groups.reverse().flat(); + } + function normalizeTurns(raw) { + const turns = []; + for (const [index, item] of raw.entries()){ + if (!item || typeof item !== 'object') continue; + const row = item; + const id = String(row.id ?? row.turnId ?? index); + const items = Array.isArray(row.items) ? row.items : null; + if (items) { + turns.push(normalizeTurnWithItems(id, row, items)); + continue; + } + turns.push(normalizeLeafItem(id, row)); + } + return turns; + } + function normalizeTurnWithItems(id, row, items) { + let userText = ''; + let assistantText = ''; + let toolText = ''; + let reasoningText = ''; + for (const entry of items){ + if (!entry || typeof entry !== 'object') continue; + const item = entry; + const type = String(item.type ?? item.role ?? ''); + const text = extractText(item); + if (/user/i.test(type)) userText = joinText(userText, text); + else if (/agent|assistant|message/i.test(type) && !/reasoning|tool/i.test(type)) { + assistantText = joinText(assistantText, text); + } else if (/tool/i.test(type)) toolText = joinText(toolText, text || type); + else if (/reasoning/i.test(type)) reasoningText = joinText(reasoningText, text); + } + let role = 'assistant'; + let text = assistantText || userText; + if (userText && !assistantText) { + role = 'user'; + text = userText; + } else if (!userText && !assistantText && toolText) { + role = 'tool'; + text = toolText; + } else if (!userText && !assistantText && reasoningText) { + role = 'reasoning'; + text = reasoningText; } + return { + turnKey: (0, _thread_ids_js__rspack_import_3.Sr)(id), + role, + text, + ...userText ? { + userText + } : {}, + ...assistantText ? { + assistantText + } : {}, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null + }; + } + function normalizeLeafItem(id, row) { + const text = extractText(row); + const type = String(row.type ?? row.role ?? ''); + let role = 'assistant'; + if (/user/i.test(type)) role = 'user'; + else if (/tool/i.test(type)) role = 'tool'; + else if (/reasoning/i.test(type)) role = 'reasoning'; + else if (/status|system/i.test(type) && !text) role = 'status'; + return { + turnKey: (0, _thread_ids_js__rspack_import_3.Sr)(id), + role, + text, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null + }; } - function resultText(result) { - if (result.execution && typeof result.execution === 'object' && 'state' in result.execution) return `Codex turn ${result.turnId}: ${result.execution.state}`; - if (result.delivery === 'queued') return `Queued ${result.queuedSubmissionId} on busy Codex thread ${result.threadId}; message ${result.messageId}`; - if (result.error) return String(result.error); - if (result.delivery === 'accepted') return `Started turn ${result.turnId} (${result.turnStatus}) on Codex thread ${result.threadId}; message ${result.messageId}`; - if (result.delivery) return `Codex delivery ${result.delivery} on thread ${result.threadId}`; - if (result.reason) return `Codex observation: ${result.reason}`; - if (Array.isArray(result.threads)) return `${result.threads.length} Codex threads`; - return String(result.error ?? 'Codex observation complete'); + function extractText(row) { + if (typeof row.text === 'string') return row.text; + if (typeof row.content === 'string') return row.content; + if (Array.isArray(row.content)) return row.content.filter((part)=>Boolean(part) && typeof part === 'object' && !Array.isArray(part)).map((part)=>typeof part.text === 'string' ? part.text : '').filter(Boolean).join('\n'); + if (typeof row.preview === 'string') return row.preview; + return ''; + } + function joinText(left, right) { + if (!right) return left; + if (!left) return right; + return `${left}\n${right}`; + } + function isThreadNotLoaded(error) { + const message = errorMessage(error); + return /thread not loaded|not loaded|invalid thread id/i.test(message); + } + function isUnknownThread(error) { + const message = errorMessage(error); + return /no rollout found|thread not found|unknown.*thread/i.test(message); + } + function maybeRemoteThreadError(error, threadId) { + const message = errorMessage(error); + if (/thread not loaded|not loaded|no rollout found|thread not found/i.test(message)) { + const remote = (0, _remote_threads_js__rspack_import_2.KI)(threadId); + if (remote) { + return new _errors_js__rspack_import_1.Rg(threadId, remote.hostId, remote.hostName); + } + const hostId = (0, _remote_threads_js__rspack_import_2.XI)(threadId); + if (hostId) return new _errors_js__rspack_import_1.Rg(threadId, hostId, null); + } + if (error instanceof Error) return error; + return new Error(message || `app-server error for thread ${threadId}`); + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) { + return String(error.message); + } + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); } __webpack_require__.d(__webpack_exports__, { - $c: ()=>threadsOperation, - D6: ()=>resultText, - UP: ()=>sendOperation, - gT: ()=>observeOperation - }, { - FD: resultSchema, - P3: threadsSchema, - R9: watchSchema, - Wi: sendSchema, - jS: waitSchema + Jf: ()=>appServerReadThread, + OI: ()=>appServerListThreads, + X: ()=>discoverModelProviders, + aI: ()=>appServerSearchThreads, + dM: ()=>appServerStatusProbe, + qL: ()=>appServerUnarchiveThread }); }, - "./src/core/grok-approval-routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { - var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _gateway_js__rspack_import_0 = __webpack_require__("./src/core/gateway.js"); - var _grok_approvals_js__rspack_import_1 = __webpack_require__("./src/core/grok-approvals.js"); - var _gbot_js__rspack_import_2 = __webpack_require__("./src/gbot.ts"); - const listSchema = zod__rspack_import_3.rej({ - target: zod__rspack_import_3.YjP().min(1).max(1024) - }); - const respondSchema = _grok_approvals_js__rspack_import_1.T5; - const resultSchema = zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.Pq9()); - const listOperation = (input)=>(0, _gbot_js__rspack_import_2.yF)(async ()=>(0, _gateway_js__rspack_import_0.MI)(await (0, _gateway_js__rspack_import_0.Vq)(), listSchema.parse(input).target)); - const respondOperation = (input)=>(0, _gbot_js__rspack_import_2.yF)(async ()=>{ - const { target, ...response } = respondSchema.parse(input); - return (0, _gateway_js__rspack_import_0.OR)(await (0, _gateway_js__rspack_import_0.Vq)(), target, response); - }); - __webpack_require__.d(__webpack_exports__, {}, { - FD: resultSchema, - cE: listSchema, - gi: listOperation, - lr: respondOperation, - v1: respondSchema + "./src/core/chatgpt-desktop/cdp-adapter.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _cdp_session_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/cdp-session.ts"); + var _cdp_dom_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/cdp-dom.ts"); + var _thread_ids_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + var _loopback_js__rspack_import_5 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + var _project_state_js__rspack_import_6 = __webpack_require__("./src/core/chatgpt-desktop/project-state.ts"); + class CdpChatGptDesktopAdapter { + #port = (0, _loopback_js__rspack_import_5.L0)(); + #session = null; + #version = null; + #pageSessionId = null; + #mainTargetId = null; + #submittedTurn = null; + async connect({ port }) { + await this.close(); + this.#port = (0, _loopback_js__rspack_import_5.L0)(process.env, port); + const session = new _cdp_session_js__rspack_import_2.a(this.#port); + try { + this.#version = await session.connect(); + this.#session = session; + await this.#attachMainWindow(); + } catch (error) { + session.close(); + this.#session = null; + throw error; + } + } + async listTargets() { + const session = this.#requireSession(); + const result = await session.send('Target.getTargets'); + return (0, _cdp_dom_js__rspack_import_3.Mr)(result.targetInfos ?? []); + } + async listThreads({ limit = 50 } = {}) { + const sessionId = await this.#ensurePageSession(); + const threads = await (0, _cdp_dom_js__rspack_import_3.AY)(this.#requireSession(), sessionId, { + limit + }); + return { + threads, + backend: 'cdp', + limit + }; + } + async readThread({ threadId, limit = 100, full = false, openTimeoutMs = 90000 }) { + const sessionId = await this.#ensurePageSession(); + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + const turns = await (0, _cdp_dom_js__rspack_import_3.JU)(this.#requireSession(), sessionId, { + limit, + full, + threadId + }); + return { + threadId, + turns, + backend: 'cdp', + limit, + full, + complete: false + }; + } + async sendMessage({ threadId, text, project, openTimeoutMs = 90000 }) { + const sessionId = await this.#ensurePageSession(); + let temporaryThreadId; + const startedNew = !threadId; + if (threadId) { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } else { + const localProject = project ? (0, _project_state_js__rspack_import_6.q)(project) : null; + if (localProject && !localProject.rootPaths.some((path)=>(0, node_fs__rspack_import_0.existsSync)(path))) { + throw new _errors_js__rspack_import_1.Kn(localProject.label, `none of its configured workspace roots exists: ${localProject.rootPaths.map((path)=>JSON.stringify(path)).join(', ') || '(none)'}. Update the Desktop project root before sending.`); + } + await (0, _cdp_dom_js__rspack_import_3.s0)(this.#requireSession(), sessionId, { + project: localProject?.label ?? project, + projectId: localProject?.id + }); + const threads = await (0, _cdp_dom_js__rspack_import_3.AY)(this.#requireSession(), sessionId, { + limit: 20 + }); + const temp = threads.find((t)=>t.threadId.startsWith(_cdp_dom_js__rspack_import_3.hA) && t.selected) ?? threads.find((t)=>t.threadId.startsWith(_cdp_dom_js__rspack_import_3.hA)); + if (temp) temporaryThreadId = temp.threadId; + } + const before = await (0, _cdp_dom_js__rspack_import_3.YX)(this.#requireSession(), sessionId); + const { sentVia } = await (0, _cdp_dom_js__rspack_import_3.lh)(this.#requireSession(), sessionId, text); + let conversationId; + if (startedNew || threadId && (0, _thread_ids_js__rspack_import_4.vr)(threadId)) { + const resolveMs = Math.min(openTimeoutMs, 60000); + try { + conversationId = await (0, _cdp_dom_js__rspack_import_3.z9)(this.#requireSession(), sessionId, { + timeoutMs: resolveMs + }); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', + reason: 'conversation-id-unresolved' + }); + } + } + const durable = (0, _cdp_dom_js__rspack_import_3.X8)(conversationId, threadId); + if (!durable) { + throw new Error('ChatGPT Desktop send accepted but no durable conversation id was available; ' + 'refusing to return a local:client-new-thread:* id as threadId'); + } + this.#submittedTurn = { + threadId: durable, + turnKey: before.lastTurnKey + }; + return { + threadId: durable, + temporaryThreadId, + conversationId: conversationId || (0, _thread_ids_js__rspack_import_4.nn)(durable) || durable, + project, + backend: 'cdp', + experimental: false, + delivery: 'accepted', + sentVia, + message: 'Submitted via CDP composer (focus → insertText → Enter)' + }; + } + async waitForReply({ threadId, timeoutMs = 120000 }) { + const deadline = Date.now() + timeoutMs; + const sessionId = await this.#ensurePageSession(); + const expectedId = threadId && threadId !== 'new' ? (0, _thread_ids_js__rspack_import_4.nn)(threadId) ?? undefined : this.#submittedTurn ? (0, _thread_ids_js__rspack_import_4.nn)(this.#submittedTurn.threadId) ?? undefined : undefined; + if (threadId && !(0, _thread_ids_js__rspack_import_4.vr)(threadId) && threadId !== 'new') { + await this.#openAndWait(sessionId, threadId, Math.min(timeoutMs, 90000)); + } + try { + const done = await (0, _cdp_dom_js__rspack_import_3.FG)(this.#requireSession(), sessionId, { + timeoutMs: Math.max(0, deadline - Date.now()), + expectedConversationId: expectedId, + baselineTurnKey: this.#submittedTurn && (!threadId || (0, _thread_ids_js__rspack_import_4.BS)(threadId) === this.#submittedTurn.threadId) ? this.#submittedTurn.turnKey : undefined + }); + const durable = (0, _cdp_dom_js__rspack_import_3.X8)(done.conversationId, threadId) ?? (done.conversationId ? (0, _thread_ids_js__rspack_import_4.BS)(done.conversationId) : null); + if (!durable) { + throw new Error('ChatGPT Desktop reply finished without a durable conversation id on ' + '[data-response-annotation-conversation]'); + } + return { + threadId: durable, + conversationId: done.conversationId || (0, _thread_ids_js__rspack_import_4.nn)(durable) || durable, + reply: done.reply, + backend: 'cdp', + experimental: false, + delivery: 'replied' + }; + } catch (error) { + if (!(error instanceof _cdp_dom_js__rspack_import_3.aw)) throw error; + return { + threadId: threadId ?? 'unknown', + reply: '', + backend: 'cdp', + experimental: false, + delivery: 'timeout' + }; + } + } + async openThread(threadId, { openTimeoutMs = 90000 } = {}) { + const sessionId = await this.#ensurePageSession(); + try { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } catch (error) { + if (error instanceof _errors_js__rspack_import_1.fw) return { + threadId, + backend: 'cdp', + archived: true + }; + throw error; + } + return { + threadId, + backend: 'cdp' + }; + } + async status() { + try { + const version = this.#version ?? await _cdp_session_js__rspack_import_2.a.fetchVersion(this.#port); + let targetCount; + let mainWindowTargetId; + if (this.#session && !this.#session.closed) { + const targets = await this.listTargets(); + targetCount = targets.length; + mainWindowTargetId = (0, _cdp_dom_js__rspack_import_3.C2)(targets)?.targetId ?? null; + } else { + const list = await _cdp_session_js__rspack_import_2.a.fetchJsonList(this.#port); + targetCount = list.length; + } + return { + reachable: true, + port: this.#port, + host: '127.0.0.1', + browser: typeof version.Browser === 'string' ? version.Browser : undefined, + protocolVersion: typeof version['Protocol-Version'] === 'string' ? version['Protocol-Version'] : undefined, + webSocketDebuggerUrl: typeof version.webSocketDebuggerUrl === 'string' ? version.webSocketDebuggerUrl : undefined, + targetCount, + mainWindowTargetId: mainWindowTargetId ?? this.#mainTargetId, + message: 'ChatGPT Desktop CDP endpoint reachable on 127.0.0.1', + exitCode: 0 + }; + } catch (error) { + return { + reachable: false, + port: this.#port, + host: '127.0.0.1', + message: error instanceof Error ? error.message : 'ChatGPT Desktop CDP endpoint unreachable on 127.0.0.1', + exitCode: 1 + }; + } + } + async close() { + this.#pageSessionId = null; + this.#mainTargetId = null; + this.#version = null; + this.#session?.close(); + this.#session = null; + } + async #openAndWait(sessionId, threadId, openTimeoutMs) { + await (0, _cdp_dom_js__rspack_import_3.Kl)(this.#requireSession(), sessionId, threadId); + await (0, _cdp_dom_js__rspack_import_3.Zx)(this.#requireSession(), sessionId, { + timeoutMs: openTimeoutMs, + threadId + }); + } + async #attachMainWindow() { + const targets = await this.listTargets(); + const main = (0, _cdp_dom_js__rspack_import_3.C2)(targets); + if (!main) { + throw new _errors_js__rspack_import_1.sO(`No ChatGPT Desktop main window target with url ${JSON.stringify('app://-/index.html')}`); + } + const attached = await this.#requireSession().send('Target.attachToTarget', { + targetId: main.targetId, + flatten: true + }); + this.#mainTargetId = main.targetId; + this.#pageSessionId = attached.sessionId; + } + async #ensurePageSession() { + this.#requireSession(); + if (!this.#pageSessionId) await this.#attachMainWindow(); + if (!this.#pageSessionId) { + throw new _errors_js__rspack_import_1.sO('Failed to attach to ChatGPT Desktop main window'); + } + return this.#pageSessionId; + } + #requireSession() { + if (!this.#session || this.#session.closed) { + throw new _errors_js__rspack_import_1.sO('Not connected to ChatGPT Desktop CDP. Call connect({ port }) after relaunching with --remote-debugging-port.'); + } + return this.#session; + } + } + __webpack_require__.d(__webpack_exports__, { + a: ()=>CdpChatGptDesktopAdapter }); }, - "./src/core/relay/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { - var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/40.js"); - var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _managed_js__rspack_import_0 = __webpack_require__("./src/core/relay/managed.js"); - var _gbot_js__rspack_import_1 = __webpack_require__("./src/gbot.ts"); - var _codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); - const id = zod__rspack_import_3.YjP().min(1).max(128); - const routeFields = { - codexThreadId: id.optional(), - expectedCwd: zod__rspack_import_3.YjP().min(1).max(4096).optional(), - bindingId: zod__rspack_import_3.YjP().min(1).max(512).optional(), - requestId: id.optional() + "./src/core/chatgpt-desktop/cdp-dom.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _codex_thread_id_js__rspack_import_1 = __webpack_require__("./src/core/codex/thread-id.js"); + const MAIN_WINDOW_URL = 'app://-/index.html'; + const IGNORED_TARGET_URL_MARKERS = [ + 'initialRoute=%2Favatar-overlay', + 'detached-window.html', + 'chatgpt.com', + 'codex-sandbox' + ]; + const ATTR = { + threadRow: 'data-app-action-sidebar-thread-row', + threadId: 'data-app-action-sidebar-thread-id', + threadTitle: 'data-app-action-sidebar-thread-title', + threadPinned: 'data-app-action-sidebar-thread-pinned', + threadSelected: 'data-app-action-sidebar-thread-selected', + threadKind: 'data-app-action-sidebar-thread-kind', + turnKey: 'data-turn-key', + userBubble: 'data-user-message-bubble', + timelineScroll: 'data-app-action-timeline-scroll', + virtualizedTurn: 'data-virtualized-turn-content', + composer: 'data-codex-composer', + finalAssistant: 'data-local-conversation-final-assistant', + markdownTextStyle: 'data-markdown-text-style', + conversationAnnotation: 'data-response-annotation-conversation' }; - const grokSendSchema = zod__rspack_import_3.Ikc({ - target: zod__rspack_import_3.YjP().min(1), - message: zod__rspack_import_3.YjP().min(1), - replyMode: zod__rspack_import_3.k5n([ - "auto", - "manual" - ]).optional(), - hop: zod__rspack_import_3.aig().int().min(0).optional(), - correlationId: id.optional(), - ...routeFields - }).strict(); - const bridgeStartSchema = zod__rspack_import_3.Ikc({ - grokTarget: zod__rspack_import_3.YjP().min(1), - codexThreadId: id.optional(), - expectedCwd: zod__rspack_import_3.YjP().min(1).max(4096).optional(), - busyPolicy: zod__rspack_import_3.k5n([ - "steer", - "reject" - ]).optional(), - requestId: id.optional() - }).strict(); - const bridgeStatusSchema = zod__rspack_import_3.Ikc({ - bindingId: zod__rspack_import_3.YjP().min(1).max(512).optional(), - limit: zod__rspack_import_3.aig().int().min(1).max(100).optional() - }).strict(); - const bridgeStopSchema = zod__rspack_import_3.Ikc({ - bindingId: zod__rspack_import_3.YjP().min(1).max(512).optional(), - all: zod__rspack_import_3.zMY().optional(), - worker: zod__rspack_import_3.zMY().optional() - }).strict().refine((x)=>!!x.bindingId || x.all === true || x.worker === true, "Specify bindingId, all or worker"); - const respondSchema = zod__rspack_import_3.Ikc({ - interactionId: zod__rspack_import_3.YjP().min(1).max(512), - generation: id, - threadId: id, - turnId: id, - bindingId: zod__rspack_import_3.YjP().min(1).max(512).optional(), - exchangeId: zod__rspack_import_3.YjP().min(1).max(512).optional(), - decision: zod__rspack_import_3.k5n([ - "accept", - "decline", - "cancel" - ]).optional(), - answersJson: zod__rspack_import_3.YjP().min(2).max(65536).optional() - }).strict().refine((x)=>!!x.bindingId || !!x.exchangeId, "Binding or exchange scope required").refine((x)=>!!x.decision !== !!x.answersJson, "Supply decision or answersJson"); - const answersSchema = zod__rspack_import_3.g1P(zod__rspack_import_3.YjP().min(1).max(128), zod__rspack_import_3.Ikc({ - answers: zod__rspack_import_3.YOg(zod__rspack_import_3.YjP().max(8192)).max(20) - }).strict()); - const relayResultSchema = zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.Pq9()); - function nativeCodexThread(context) { - return context.host.state === "available" && (0, _agent_bundle_runtime__rspack_import_4.bg)(context.host.value.name) === "codex" && context.lineage.state === "available" && context.lineage.source === "native" && context.lineage.value.resolution === "native" ? context.lineage.value.conversation : undefined; - } - function options(context) { - return { - pluginRoot: context?.plugin.state === "available" ? context.plugin.value.root : undefined, - signal: context?.signal - }; + const MAIN_CONTENT_SURFACE_CLASS_PREFIX = '_MainContentSurface'; + const HISTORY_GAP_PREFIX = 'history-gap:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const LOADING_TASK_TEXT = 'Loading task…'; + const DEFAULT_OPEN_TIMEOUT_MS = 90000; + const DEFAULT_CONVERSATION_RESOLVE_MS = 60000; + const DEFAULT_FULL_READ_IDLE_WHEELS = 8; + const DEFAULT_FULL_READ_MAX_WHEELS = 400; + const FULL_READ_WHEEL_DELTA_Y = -800; + const COMPOSER_SELECTOR = `[${ATTR.composer}="true"][contenteditable="true"]`; + const SEND_BUTTON_SELECTOR = 'button[aria-label="Send"]'; + const STOP_BUTTON_SELECTOR = 'main button[aria-label="Stop"]'; + const FINAL_ASSISTANT_SELECTOR = `[${ATTR.finalAssistant}="true"]`; + const ASSISTANT_MESSAGE_SELECTOR = `[${ATTR.finalAssistant}="true"] [${ATTR.markdownTextStyle}="assistant-message"]`; + const CONVERSATION_ANNOTATION_SELECTOR = `[${ATTR.conversationAnnotation}]`; + const SELECTORS = { + threadRow: `[role="button"][${ATTR.threadRow}]`, + turn: `[${ATTR.turnKey}]`, + userBubble: `[${ATTR.userBubble}]`, + timelineScroll: `[${ATTR.timelineScroll}]`, + composer: COMPOSER_SELECTOR, + sendButton: SEND_BUTTON_SELECTOR, + stopButton: STOP_BUTTON_SELECTOR, + finalAssistant: FINAL_ASSISTANT_SELECTOR, + assistantMessage: ASSISTANT_MESSAGE_SELECTOR, + conversationAnnotation: CONVERSATION_ANNOTATION_SELECTOR, + newChatFallback: 'button' + }; + function newChatInProjectSelector(project) { + return `button[aria-label=${JSON.stringify(`Start new chat in ${project}`)}]`; + } + function isMainWindowTarget(target) { + if (target.type !== 'page') return false; + const url = typeof target.url === 'string' ? target.url : ''; + if (url !== MAIN_WINDOW_URL) return false; + return !IGNORED_TARGET_URL_MARKERS.some((marker)=>url.includes(marker)); + } + function summarizeTargetInfos(targetInfos) { + return targetInfos.filter((t)=>typeof t.targetId === 'string' && typeof t.type === 'string').map((t)=>({ + targetId: String(t.targetId), + type: String(t.type), + title: typeof t.title === 'string' ? t.title : '', + url: typeof t.url === 'string' ? t.url : '', + attached: Boolean(t.attached) + })); } - function destination(input, context) { - const codexThreadId = input.codexThreadId ?? (context ? nativeCodexThread(context) : undefined); - const workspace = context?.workspace; - const expectedCwd = input.expectedCwd ?? (workspace?.state === "available" && workspace.source === "native" ? workspace.value.root : undefined); - return { - codexThreadId, - expectedCwd - }; + function pickMainWindowTarget(targets) { + return targets.find((t)=>isMainWindowTarget(t)) ?? null; + } + function dedupeThreadsById(threads) { + const byKey = new Map(); + for (const thread of threads){ + if (!thread.threadId) continue; + const key = durableThreadKey(thread.threadId); + const existing = byKey.get(key); + if (!existing) { + byKey.set(key, thread); + continue; + } + byKey.set(key, preferThreadRow(existing, thread)); + } + return [ + ...byKey.values() + ]; } - async function grokSendOperation(input, context) { - const route = destination(input, input.bindingId ? undefined : context); - if (input.replyMode !== "manual" && (input.bindingId || route.codexThreadId)) return (0, _gbot_js__rspack_import_1.yF)(()=>(0, _managed_js__rspack_import_0.Bb)("sendToGrok", { - grokTarget: input.target, - message: input.message, - hop: input.hop, - correlationId: input.correlationId, - ...route, - ...input.bindingId ? { - bindingId: input.bindingId + function durableThreadKey(threadId) { + if ((0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) return threadId; + try { + return (0, _codex_thread_id_js__rspack_import_1.SX)(threadId); + } catch { + return threadId.startsWith("local:") ? threadId.slice("local:".length) || threadId : threadId; + } + } + function toDesktopForm(threadId) { + if (!threadId || (0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) return threadId; + if (threadId.startsWith("local:")) return threadId; + return `${"local:"}${threadId}`; + } + function preferThreadRow(left, right) { + const score = (row)=>(row.selected ? 4 : 0) + (row.pinned ? 2 : 0) + (row.threadId.startsWith("local:") && !(0, _codex_thread_id_js__rspack_import_1.vr)(row.threadId) ? 1 : 0); + return score(right) > score(left) ? right : left; + } + const LIST_THREADS_EXPRESSION = `(() => { + const skipProjectLabels = new Set(['Recent', 'Pinned', 'Threads', 'Chats']); + const projectFor = (row) => { + let el = row.parentElement; + for (let i = 0; i < 8 && el; i += 1, el = el.parentElement) { + const attr = + el.getAttribute('data-app-action-sidebar-project') || + el.getAttribute('data-app-action-sidebar-project-name') || + el.getAttribute('data-app-action-sidebar-section-title'); + if (attr && !skipProjectLabels.has(attr)) return attr; + } + return undefined; + }; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const seen = new Set(); + const out = []; + for (const row of rows) { + const threadId = row.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''; + if (!threadId || seen.has(threadId)) continue; + seen.add(threadId); + const project = projectFor(row); + out.push({ + threadId, + hostId: row.getAttribute('data-app-action-sidebar-thread-host-id') || null, + title: row.getAttribute(${JSON.stringify(ATTR.threadTitle)}) || (row.textContent || '').trim(), + pinned: row.getAttribute(${JSON.stringify(ATTR.threadPinned)}) === 'true', + selected: row.getAttribute(${JSON.stringify(ATTR.threadSelected)}) === 'true', + kind: row.getAttribute(${JSON.stringify(ATTR.threadKind)}) || 'unknown', + ...(project ? { project } : {}), + }); + } + return out; +})()`; + function openThreadExpression(threadId) { + const bare = durableThreadKey(threadId); + const desktop = toDesktopForm(bare); + const candidates = [ + ...new Set([ + threadId, + bare, + desktop + ].filter(Boolean)) + ]; + return `(() => { + const candidates = ${JSON.stringify(candidates)}; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const row = rows.find((el) => + candidates.includes(el.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''), + ); + if (!row) return { ok: false, error: 'thread-not-found' }; + row.click(); + return { ok: true, threadId: row.getAttribute(${JSON.stringify(ATTR.threadId)}) || candidates[0] }; + })()`; + } + function navigateToThreadExpression(threadId) { + const bare = durableThreadKey(threadId); + return `(async () => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; + let scanned = 0; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); + if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { + await router.navigate(${JSON.stringify(`/local/${bare}`)}); + return { ok: true, via: 'router' }; + } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + return { ok: false, error: 'router-not-found' }; + })()`; + } + function startNewChatExpression(project, projectId) { + const projectSel = project ? newChatInProjectSelector(project) : null; + return `(() => { + const projectSel = ${JSON.stringify(projectSel)}; + if (projectSel) { + const preferred = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(projectSel) ||` : ''} document.querySelector(projectSel); + if (preferred) { + const row = preferred.closest('[data-app-action-sidebar-project-row]') || preferred.parentElement; + row?.scrollIntoView({ block: 'center' }); + const rowBox = row?.getBoundingClientRect(); + return { ok: true, via: 'project', project: ${JSON.stringify(project ?? null)}, + disabled: preferred.disabled || preferred.getAttribute('aria-disabled') === 'true', + hoverX: rowBox?.left + rowBox?.width / 2, hoverY: rowBox?.top + rowBox?.height / 2 }; + } + return { ok: false, error: 'project-not-found' }; + } + const buttons = Array.from(document.querySelectorAll('button')); + const fallback = buttons.find((btn) => (btn.textContent || '').trim() === 'New chat'); + if (!fallback) return { ok: false, error: 'new-chat-not-found' }; + fallback.click(); + return { ok: true, via: 'fallback' }; + })()`; + } + function projectButtonBoxExpression(project, projectId) { + return `(() => { + const button = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(${JSON.stringify(newChatInProjectSelector(project))}) ||` : ''} document.querySelector(${JSON.stringify(newChatInProjectSelector(project))}); + if (!button) return null; + const box = button.getBoundingClientRect(); + return { x: box.left + box.width / 2, y: box.top + box.height / 2, + disabled: button.disabled || button.getAttribute('aria-disabled') === 'true', + visible: box.width > 0 && box.height > 0 && box.x >= 0 && box.y >= 0 + && box.x < innerWidth && box.y < innerHeight && getComputedStyle(button.parentElement).opacity !== '0' }; + })()`; + } + function newChatViewExpression(project) { + return `(() => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; let scanned = 0; let path = ''; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { path = router.state.location.pathname; break; } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + const composer = !!document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + const scope = document.querySelector('[data-composer-navigation-target="workspace-project"]')?.getAttribute('aria-label') || ''; + const expected = ${JSON.stringify(project ? `Change project: ${project}` : '')}; + return { ready: path === '/' && composer && (expected ? scope === expected : (!scope || scope === 'Choose project')), path, composer, scope }; + })()`; + } + const CLEAR_NEW_CHAT_PROJECT_EXPRESSION = `(() => { + const button = document.querySelector('[data-clear-project-button="true"]'); + if (!button || button.disabled) return false; + button.click(); + return true; +})()`; + const LOADING_TASK_GONE_EXPRESSION = `(() => { + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const text = (surface.innerText || '').replace(/\\u2026/g, '…'); + return !text.includes(${JSON.stringify(LOADING_TASK_TEXT)}); +})()`; + const ARCHIVED_THREAD_EXPRESSION = `(() => Array.from(document.querySelectorAll('main')) + .some((main) => (main.innerText || '').includes('This task is archived') + && (main.innerText || '').includes('Unarchive this task to open it')))()`; + const FOCUS_COMPOSER_EXPRESSION = `(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + if (!composer) return { ok: false, error: 'composer-not-found' }; + if ((composer.textContent || '').trim()) return { ok: false, error: 'composer-has-draft' }; + if (document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)})) return { ok: false, error: 'reply-in-progress' }; + composer.focus(); + return { ok: true }; +})()`; + const REPLY_STATE_EXPRESSION = `(() => { + const stop = document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)}); + const finals = Array.from(document.querySelectorAll(${JSON.stringify(FINAL_ASSISTANT_SELECTOR)})); + const lastFinal = finals.at(-1) || null; + const annotation = lastFinal + ? lastFinal.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + || lastFinal.closest(${JSON.stringify(SELECTORS.turn)})?.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + : document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + const conversationId = annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; + const messageEl = lastFinal + ? lastFinal.querySelector(${JSON.stringify(`[${ATTR.markdownTextStyle}="assistant-message"]`)}) + : null; + const reply = messageEl ? (messageEl.innerText || '').trim() : ''; + const turns = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.turn)})); + const lastTurn = turns.at(-1); + const lastTurnKey = lastTurn ? (lastTurn.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '') : ''; + return { + stopVisible: Boolean(stop), + finalAssistantCount: finals.length, + conversationId, + reply, + lastTurnKey, + finalTurnKey: lastFinal?.closest(${JSON.stringify(SELECTORS.turn)})?.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '', + }; +})()`; + const CONVERSATION_ID_EXPRESSION = `(() => { + const annotation = document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + return annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; +})()`; + const HARVEST_VISIBLE_TURNS_EXPRESSION = `(() => { + const gapPrefix = ${JSON.stringify(HISTORY_GAP_PREFIX)}; + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const scroll = + surface.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}) + || document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + const root = scroll || surface; + const out = []; + for (const el of root.querySelectorAll(${JSON.stringify(SELECTORS.turn)})) { + const turnKey = el.getAttribute(${JSON.stringify(ATTR.turnKey)}) || ''; + if (!turnKey || turnKey.startsWith(gapPrefix)) continue; + const user = el.querySelector(${JSON.stringify(SELECTORS.userBubble)}); + const assistant = el.querySelector(${JSON.stringify(ASSISTANT_MESSAGE_SELECTOR)}); + const userText = user ? (user.innerText || '').trim() : ''; + const assistantText = assistant ? (assistant.innerText || '').trim() : ''; + const raw = (el.innerText || '').trim(); + const statusRe = /^Worked for \\d+s$/i; + let role = 'assistant'; + let text = assistantText || userText || raw; + if (user && !assistantText) { + role = 'user'; + text = userText || raw; + } else if (assistantText) { + role = 'assistant'; + text = assistantText; + } else if (!raw || statusRe.test(raw)) { + role = 'status'; + text = raw; + } + out.push({ turnKey, role, text, userText, assistantText }); + } + return out; +})()`; + const TIMELINE_SCROLL_METRICS_EXPRESSION = `(() => { + const scroll = document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + if (!scroll) return { ok: false }; + const rect = scroll.getBoundingClientRect(); + return { + ok: true, + x: Math.floor(rect.left + rect.width / 2), + y: Math.floor(rect.top + Math.min(rect.height / 2, 120)), + scrollTop: scroll.scrollTop, + scrollHeight: scroll.scrollHeight, + clientHeight: scroll.clientHeight, + }; +})()`; + async function listThreadsFromDom(session, sessionId, { limit = 50 } = {}) { + const rows = await session.evaluate(LIST_THREADS_EXPRESSION, { + sessionId + }); + if (!Array.isArray(rows)) return []; + return dedupeThreadsById(rows).slice(0, limit).map((row)=>({ + threadId: String(row.threadId), + title: String(row.title ?? ''), + pinned: Boolean(row.pinned), + selected: Boolean(row.selected), + kind: String(row.kind ?? 'unknown'), + ...row.hostId ? { + hostId: row.hostId, + location: row.hostId === 'local' ? 'local' : 'remote' } : {}, - ...input.requestId ? { - requestId: input.requestId + ...row.project ? { + project: String(row.project) } : {} - }, options(context))); - if (input.replyMode === "auto") throw Error("Automatic reply delivery requires a native Codex source, codexThreadId or bindingId"); - const message = input.hop !== undefined || input.correlationId !== undefined ? (0, _codex_bridge_js__rspack_import_2.Mr)(input.message, (0, _codex_bridge_js__rspack_import_2.nq)(input)) : input.message; - const sent = await (0, _gbot_js__rspack_import_1.yF)(async ()=>(0, _gbot_js__rspack_import_1.LV)(await (0, _gbot_js__rspack_import_1.Vq)(), input.target, message)); - return { - result: sent.result, - target: (0, _gbot_js__rspack_import_1.rW)(sent.target), - delivery: sent.delivery === "accepted" ? "accepted" : "unknown", - ...sent.delivery === "accepted" && typeof sent.messageId === "string" ? { - messageId: sent.messageId - } : {}, - replyRoute: { - mode: "manual", - reason: input.replyMode === "manual" ? "requested" : "source-unavailable" + })); + } + async function openThreadInDom(session, sessionId, threadId) { + const current = await readConversationIdFromDom(session, sessionId); + if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + if ((0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { + limit: 200 + }); + if (rows.some((row)=>row.selected && row.threadId === threadId)) return; + } + const result = await session.evaluate(openThreadExpression(threadId), { + sessionId + }); + if (!result?.ok) { + if (result?.error === 'thread-not-found' && !(0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const navigated = await session.evaluate(navigateToThreadExpression(threadId), { + sessionId + }); + if (navigated?.ok) return; } - }; + throw new Error(result?.error === 'thread-not-found' ? `ChatGPT Desktop thread not found: ${threadId}` : `Failed to open ChatGPT Desktop thread ${threadId}`); + } } - async function bridgeOperation(method, input, context) { - let routed = method === "startBinding" ? { - ...input, - ...destination(input, context) - } : input; - if (method === "respond") { - const { decision, answersJson, ...scope } = respondSchema.parse(input); - routed = { - ...scope, - result: decision ? { - decision - } : { - answers: answersSchema.parse(JSON.parse(answersJson)) + async function waitForLoadingTaskGone(session, sessionId, { timeoutMs = DEFAULT_OPEN_TIMEOUT_MS, threadId } = {}) { + const deadline = Date.now() + timeoutMs; + let loadingGone = false; + let lastConversationId = ''; + let archivedSince = null; + while(Date.now() < deadline){ + if (threadId && threadId !== 'new' && await session.evaluate(ARCHIVED_THREAD_EXPRESSION, { + sessionId + }) === true) { + archivedSince ??= Date.now(); + if (Date.now() - archivedSince >= 500) { + throw new _errors_js__rspack_import_0.fw(threadId); } + } else { + archivedSince = null; + } + const gone = await session.evaluate(LOADING_TASK_GONE_EXPRESSION, { + sessionId + }); + loadingGone ||= gone; + if (gone) { + const current = threadId ? await readConversationIdFromDom(session, sessionId) : ''; + lastConversationId = current; + if (!threadId) return; + if (threadId === 'new' || (0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { + limit: 200 + }); + const selected = rows.find((row)=>row.selected); + if (!current && (!selected || (0, _codex_thread_id_js__rspack_import_1.vr)(selected.threadId))) return; + } else if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + } + await delay(250); + } + throw new Error(loadingGone ? `ChatGPT Desktop timed out waiting for conversation ${JSON.stringify(threadId)}; displayed conversation ${JSON.stringify(lastConversationId)} after ${timeoutMs}ms` : `ChatGPT Desktop thread still showing ${JSON.stringify(LOADING_TASK_TEXT)} after ${timeoutMs}ms`); + } + async function startNewChatInDom(session, sessionId, { project, projectId, navigationTimeoutMs = 3000 } = {}) { + const result = await session.evaluate(startNewChatExpression(project, projectId), { + sessionId + }); + if (!result?.ok) { + throw new Error(project ? `ChatGPT Desktop new-chat button not found for project ${JSON.stringify(project)}` : 'ChatGPT Desktop "New chat" button not found'); + } + if (result.via === 'project') { + if (result.disabled) throw new _errors_js__rspack_import_0.Kn(project, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (![ + result.hoverX, + result.hoverY + ].every(Number.isFinite)) { + throw new Error(`ChatGPT Desktop project button has no visible box for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: result.hoverX, + y: result.hoverY + }, { + sessionId + }); + const button = await session.evaluate(projectButtonBoxExpression(project, projectId), { + sessionId + }); + if (button?.disabled) throw new _errors_js__rspack_import_0.Kn(project, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (!button?.visible || !Number.isFinite(button.x) || !Number.isFinite(button.y)) { + throw new Error(`ChatGPT Desktop project button did not appear after hover for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: button.x, + y: button.y + }, { + sessionId + }); + await session.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: button.x, + y: button.y, + button: 'left', + clickCount: 1 + }, { + sessionId + }); + await session.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: button.x, + y: button.y, + button: 'left', + clickCount: 1 + }, { + sessionId + }); + } + let observed = { + ready: false, + path: '', + composer: false, + scope: '' + }; + const deadline = Date.now() + navigationTimeoutMs; + while(Date.now() < deadline){ + observed = await session.evaluate(newChatViewExpression(project), { + sessionId + }); + if (observed?.ready) return { + via: result.via ?? 'unknown' }; + if (!project && observed?.path === '/' && observed?.composer && observed.scope.startsWith('Change project: ')) { + await session.evaluate(CLEAR_NEW_CHAT_PROJECT_EXPRESSION, { + sessionId + }); + } + await delay(100); } - if (method === "startBinding" && !routed.codexThreadId) throw Error("Specify codexThreadId when native Codex source identity is unavailable"); - return (0, _gbot_js__rspack_import_1.yF)(()=>(0, _managed_js__rspack_import_0.Bb)(method, routed, options(context))); + throw new _errors_js__rspack_import_0.XL(project, `route ${JSON.stringify(observed?.path)}, composer ${Boolean(observed?.composer)}, project ${JSON.stringify(observed?.scope)}`); } - function assertManagedSendOptions(input) { - if (input.expectedTurnId !== undefined) throw Error("Managed relay does not support expectedTurnId; omit it to use managed guarded steering, or use a plain Codex send for an explicit turn guard"); - if (input.replyTo !== undefined || input.envelope === true) throw Error("Managed relay does not support legacy replyTo/envelope options; omit them or use a plain send. Use hop/correlationId for explicit managed chains"); + async function focusComposer(session, sessionId) { + const focused = await session.evaluate(FOCUS_COMPOSER_EXPRESSION, { + sessionId + }); + if (!focused?.ok) { + if (focused?.error === 'composer-has-draft') throw new _errors_js__rspack_import_0.oN(); + throw new Error(`ChatGPT Desktop composer unavailable: ${focused?.error || 'not found'}`); + } } - async function codexReturnOperation(input, context) { - assertManagedSendOptions(input); - if (input.whenBusy === "queue") throw Error("Managed relay supports steer or reject, not experimental queue"); - return (0, _gbot_js__rspack_import_1.yF)(()=>(0, _managed_js__rspack_import_0.Bb)("sendToCodex", { - grokTarget: input.replyToGrok, - codexThreadId: input.threadId, - expectedCwd: input.expectedCwd, - bindingId: input.bindingId, - message: input.message, - requestId: input.requestId, - busyPolicy: input.whenBusy, - hop: input.hop, - correlationId: input.correlationId, - model: input.model, - effort: input.effort - }, options(context))); + async function submitComposerWithEnter(session, sessionId) { + const base = { + windowsVirtualKeyCode: 13, + code: 'Enter', + key: 'Enter', + text: '\r', + unmodifiedText: '\r' + }; + await session.send('Input.dispatchKeyEvent', { + type: 'rawKeyDown', + ...base + }, { + sessionId + }); + await session.send('Input.dispatchKeyEvent', { + type: 'keyUp', + ...base + }, { + sessionId + }); + } + async function sendMessageInDom(session, sessionId, text) { + await focusComposer(session, sessionId); + await session.send('Input.insertText', { + text + }, { + sessionId + }); + try { + await submitComposerWithEnter(session, sessionId); + const deadline = Date.now() + 3000; + while(Date.now() < deadline){ + const cleared = await session.evaluate(`(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + return Boolean(composer && !(composer.textContent || '').trim()); + })()`, { + sessionId + }); + if (cleared) return { + sentVia: 'enter' + }; + await delay(100); + } + throw new Error('Desktop composer did not acknowledge submission'); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', + reason: 'submission-unconfirmed' + }); + } + } + async function readReplyState(session, sessionId) { + const state = await session.evaluate(REPLY_STATE_EXPRESSION, { + sessionId + }); + return { + stopVisible: Boolean(state?.stopVisible), + finalAssistantCount: Number(state?.finalAssistantCount ?? 0), + conversationId: String(state?.conversationId ?? ''), + reply: String(state?.reply ?? ''), + lastTurnKey: String(state?.lastTurnKey ?? ''), + finalTurnKey: String(state?.finalTurnKey ?? '') + }; + } + async function readConversationIdFromDom(session, sessionId) { + const value = await session.evaluate(CONVERSATION_ID_EXPRESSION, { + sessionId + }); + return typeof value === 'string' ? value.trim() : ''; + } + async function waitForConversationId(session, sessionId, { timeoutMs = DEFAULT_CONVERSATION_RESOLVE_MS } = {}) { + const deadline = Date.now() + timeoutMs; + while(Date.now() < deadline){ + const id = await readConversationIdFromDom(session, sessionId); + if (id && !(0, _codex_thread_id_js__rspack_import_1.vr)(id) && !id.startsWith(TEMP_THREAD_ID_PREFIX)) { + return id; + } + await delay(200); + } + throw new Error(`ChatGPT Desktop conversation id did not appear on [data-response-annotation-conversation] within ${timeoutMs}ms`); + } + function resolveDurableThreadId(conversationId, fallbackThreadId) { + if (conversationId && !(0, _codex_thread_id_js__rspack_import_1.vr)(conversationId)) { + return toDesktopForm(conversationId); + } + if (fallbackThreadId && !(0, _codex_thread_id_js__rspack_import_1.vr)(fallbackThreadId) && fallbackThreadId !== 'new') { + return toDesktopForm(fallbackThreadId); + } + return null; + } + class ReplyTimeoutError extends Error { + } + async function waitForReplyDone(session, sessionId, { timeoutMs = 120000, baselineFinalCount = 0, expectedConversationId, baselineTurnKey } = {}) { + const deadline = Date.now() + timeoutMs; + let sawStop = false; + const started = Date.now(); + while(Date.now() < deadline){ + const state = await readReplyState(session, sessionId); + if (state.stopVisible) sawStop = true; + expectedConversationId ??= state.conversationId || undefined; + if (expectedConversationId && state.conversationId && durableThreadKey(state.conversationId) !== durableThreadKey(expectedConversationId)) { + throw new Error('ChatGPT Desktop selected conversation changed while waiting'); + } + const latestIsFinal = Boolean(state.finalTurnKey) && state.finalTurnKey === state.lastTurnKey; + const hasNewFinal = (state.finalAssistantCount > baselineFinalCount || sawStop) && Boolean(state.reply) && latestIsFinal && (!baselineTurnKey || state.lastTurnKey !== baselineTurnKey); + if (!state.stopVisible && hasNewFinal && (sawStop || Date.now() - started > 1500)) { + return { + reply: state.reply, + conversationId: state.conversationId + }; + } + await delay(200); + } + throw new ReplyTimeoutError(`ChatGPT Desktop reply did not finish within ${timeoutMs}ms`); + } + function normalizeTurn(raw) { + const role = raw.role === 'user' || raw.role === 'status' || raw.role === 'assistant' ? raw.role : raw.assistantText ? 'assistant' : raw.userText ? 'user' : 'assistant'; + return { + turnKey: String(raw.turnKey ?? ''), + role, + text: String(raw.text ?? raw.assistantText ?? raw.userText ?? ''), + userText: raw.userText ? String(raw.userText) : undefined, + assistantText: raw.assistantText ? String(raw.assistantText) : undefined + }; + } + async function harvestVisibleTurns(session, sessionId) { + const rows = await session.evaluate(HARVEST_VISIBLE_TURNS_EXPRESSION, { + sessionId + }); + if (!Array.isArray(rows)) return []; + return rows.map(normalizeTurn).filter((t)=>t.turnKey && !t.turnKey.startsWith(HISTORY_GAP_PREFIX)); + } + async function readTurnsFromDom(session, sessionId, { limit = 100, full = false, threadId, idleWheels = DEFAULT_FULL_READ_IDLE_WHEELS, maxWheels = DEFAULT_FULL_READ_MAX_WHEELS } = {}) { + const collected = new Map(); + const order = []; + const ingest = async ()=>{ + const older = []; + for (const turn of (await harvestVisibleTurns(session, sessionId))){ + if (!collected.has(turn.turnKey)) older.push(turn.turnKey); + collected.set(turn.turnKey, turn); + } + order.unshift(...older); + }; + await ingest(); + if (!full) { + const visible = order.map((key)=>collected.get(key)); + return limit > 0 ? visible.slice(-limit) : visible; + } + let idle = 0; + for(let step = 0; step < maxWheels; step++){ + const before = collected.size; + const metrics = await session.evaluate(TIMELINE_SCROLL_METRICS_EXPRESSION, { + sessionId + }); + if (!metrics?.ok || metrics.x == null || metrics.y == null) break; + await session.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: metrics.x, + y: metrics.y, + deltaX: 0, + deltaY: FULL_READ_WHEEL_DELTA_Y + }, { + sessionId + }); + await delay(120); + await ingest(); + if (collected.size === before) { + idle += 1; + if (idle >= idleWheels) break; + } else { + idle = 0; + } + if (threadId && collected.has(threadId)) break; + } + const all = order.map((key)=>collected.get(key)); + return limit > 0 ? all.slice(0, limit) : all; + } + function delay(ms) { + return new Promise((resolve)=>setTimeout(resolve, ms)); } __webpack_require__.d(__webpack_exports__, { - bv: ()=>grokSendOperation, - lt: ()=>codexReturnOperation, - n1: ()=>bridgeOperation + AY: ()=>listThreadsFromDom, + C2: ()=>pickMainWindowTarget, + FG: ()=>waitForReplyDone, + JU: ()=>readTurnsFromDom, + Kl: ()=>openThreadInDom, + Mr: ()=>summarizeTargetInfos, + X8: ()=>resolveDurableThreadId, + YX: ()=>readReplyState, + Zx: ()=>waitForLoadingTaskGone, + aw: ()=>ReplyTimeoutError, + lh: ()=>sendMessageInDom, + s0: ()=>startNewChatInDom, + z9: ()=>waitForConversationId + }, { + hA: TEMP_THREAD_ID_PREFIX + }); + }, + "./src/core/chatgpt-desktop/cdp-session.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _loopback_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + class CdpSession { + #port; + #ws = null; + #nextId = 1; + #pending = new Map(); + #closed = false; + constructor(port){ + this.#port = port; + } + get port() { + return this.#port; + } + get closed() { + return this.#closed || this.#ws === null || this.#ws.readyState === WebSocket.CLOSED; + } + static async fetchVersion(port, { signal } = {}) { + const base = (0, _loopback_js__rspack_import_1.NJ)(port); + let response; + try { + response = await fetch(`${base}/json/version`, { + signal: signal ? AbortSignal.any([ + signal, + AbortSignal.timeout(5000) + ]) : AbortSignal.timeout(5000), + redirect: 'error' + }); + } catch (error) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP not reachable at ${base}/json/version: ` + (error instanceof Error ? error.message : String(error))); + } + if (!response.ok) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP /json/version returned HTTP ${response.status}`); + } + const version = await response.json(); + if (typeof version.webSocketDebuggerUrl === 'string') { + (0, _loopback_js__rspack_import_1.jv)(version.webSocketDebuggerUrl); + } + return version; + } + static async fetchJsonList(port, { signal } = {}) { + const base = (0, _loopback_js__rspack_import_1.NJ)(port); + let response; + try { + response = await fetch(`${base}/json/list`, { + signal: signal ? AbortSignal.any([ + signal, + AbortSignal.timeout(5000) + ]) : AbortSignal.timeout(5000), + redirect: 'error' + }); + } catch (error) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP not reachable at ${base}/json/list: ` + (error instanceof Error ? error.message : String(error))); + } + if (!response.ok) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP /json/list returned HTTP ${response.status}`); + } + const body = await response.json(); + return Array.isArray(body) ? body : []; + } + async connect({ signal } = {}) { + if (this.#ws && !this.closed) { + throw new Error('CDP session already connected'); + } + const version = await CdpSession.fetchVersion(this.#port, { + signal + }); + const rawUrl = version.webSocketDebuggerUrl; + if (typeof rawUrl !== 'string' || !rawUrl) { + throw new _errors_js__rspack_import_0.sO('CDP /json/version omitted webSocketDebuggerUrl'); + } + const url = (0, _loopback_js__rspack_import_1.jv)(rawUrl); + signal?.throwIfAborted(); + await new Promise((resolve, reject)=>{ + const ws = new WebSocket(url); + this.#ws = ws; + const cleanup = ()=>{ + clearTimeout(timer); + signal?.removeEventListener('abort', onAbort); + }; + const onAbort = ()=>{ + cleanup(); + ws.close(); + reject(new _errors_js__rspack_import_0.sO('CDP connect aborted or timed out')); + }; + const timer = setTimeout(onAbort, 5000); + signal?.addEventListener('abort', onAbort, { + once: true + }); + ws.addEventListener('open', ()=>{ + cleanup(); + resolve(); + }, { + once: true + }); + ws.addEventListener('error', ()=>{ + cleanup(); + reject(new _errors_js__rspack_import_0.sO(`CDP WebSocket failed for ${url}`)); + }, { + once: true + }); + ws.addEventListener('message', (event)=>this.#onMessage(event.data)); + ws.addEventListener('close', ()=>{ + cleanup(); + reject(new _errors_js__rspack_import_0.sO('CDP WebSocket closed before connecting')); + this.#closed = true; + for (const [, pending] of this.#pending){ + clearTimeout(pending.timer); + pending.reject(new _errors_js__rspack_import_0.sO('CDP WebSocket closed')); + } + this.#pending.clear(); + }); + }); + this.#closed = false; + return version; + } + async send(method, params, { sessionId } = {}) { + if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) { + throw new _errors_js__rspack_import_0.sO('CDP session is not connected'); + } + const id = this.#nextId++; + const payload = { + id, + method + }; + if (params !== undefined) payload.params = params; + if (sessionId !== undefined) payload.sessionId = sessionId; + return await new Promise((resolve, reject)=>{ + const timer = setTimeout(()=>{ + this.#pending.delete(id); + reject(new _errors_js__rspack_import_0.sO(`CDP ${method} timed out`)); + }, 10000); + this.#pending.set(id, { + timer, + resolve: (value)=>resolve(value), + reject + }); + try { + this.#ws.send(JSON.stringify(payload)); + } catch (error) { + clearTimeout(timer); + this.#pending.delete(id); + reject(error); + } + }); + } + async evaluate(expression, { sessionId, awaitPromise = true } = {}) { + const result = await this.send('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise + }, { + sessionId + }); + if (result.exceptionDetails) { + const text = result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Runtime.evaluate failed'; + throw new Error(text); + } + return result.result?.value; + } + close() { + this.#closed = true; + for (const pending of this.#pending.values()){ + clearTimeout(pending.timer); + pending.reject(new _errors_js__rspack_import_0.sO('CDP session closed')); + } + this.#pending.clear(); + this.#ws?.close(); + this.#ws = null; + } + #onMessage(data) { + let message; + try { + message = JSON.parse(String(data)); + } catch { + return; + } + if (!message || typeof message !== 'object' || typeof message.id !== 'number') return; + const pending = this.#pending.get(message.id); + if (!pending) return; + this.#pending.delete(message.id); + clearTimeout(pending.timer); + if (message.error) { + pending.reject(new Error(message.error.message || 'CDP error')); + return; + } + pending.resolve(message.result); + } + } + __webpack_require__.d(__webpack_exports__, { + a: ()=>CdpSession + }); + }, + "./src/core/chatgpt-desktop/errors.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + class ArchivedThreadError extends Error { + code = 'ARCHIVED_THREAD'; + reason = 'archived-thread'; + delivery = 'rejected'; + threadId; + constructor(threadId){ + super(`ChatGPT Desktop thread ${JSON.stringify(threadId)} is archived. Read it through app-server, or pass unarchive: true on send to unarchive before sending.`); + this.name = 'ArchivedThreadError'; + this.threadId = threadId; + } + } + class ComposerDraftError extends Error { + code = 'COMPOSER_HAS_DRAFT'; + reason = 'composer-has-draft'; + delivery = 'rejected'; + constructor(){ + super('ChatGPT Desktop composer contains an unsent draft; inspect it in Desktop before sending.'); + this.name = 'ComposerDraftError'; + } + } + class ProjectUnavailableError extends Error { + code = 'PROJECT_UNAVAILABLE'; + reason = 'project-unavailable'; + delivery = 'rejected'; + constructor(project, detail){ + super(`ChatGPT Desktop cannot start a chat in project ${JSON.stringify(project)}: ${detail}`); + this.name = 'ProjectUnavailableError'; + } + } + class NewChatNavigationError extends Error { + code = 'NEW_CHAT_NAVIGATION_FAILED'; + reason = 'new-chat-navigation-failed'; + delivery = 'rejected'; + constructor(project, observed){ + super(`ChatGPT Desktop new-chat action did not open ${project ? `project ${JSON.stringify(project)}` : 'a projectless chat'}; observed ${observed}`); + this.name = 'NewChatNavigationError'; + } + } + class NotImplementedError extends Error { + code = 'NOT_IMPLEMENTED'; + delivery = 'rejected'; + reason = 'not-implemented'; + constructor(operation, detail){ + super(detail ? `ChatGPT Desktop ${operation} is not implemented: ${detail}` : `ChatGPT Desktop ${operation} is not implemented`); + this.name = 'NotImplementedError'; + } + } + class CdpHostRejectedError extends Error { + code = 'CDP_HOST_REJECTED'; + delivery = 'rejected'; + reason = 'cdp-host-rejected'; + constructor(host){ + super(`ChatGPT Desktop CDP rejects non-loopback host ${JSON.stringify(host)}. ` + 'Connect to 127.0.0.1 only; there is no remote transport.'); + this.name = 'CdpHostRejectedError'; + } + } + class CdpUnreachableError extends Error { + code = 'CDP_UNREACHABLE'; + delivery = 'rejected'; + reason = 'cdp-unreachable'; + constructor(message){ + super(message); + this.name = 'CdpUnreachableError'; + } + } + __webpack_require__.d(__webpack_exports__, { + EH: ()=>NotImplementedError, + Kn: ()=>ProjectUnavailableError, + Rg: ()=>_codex_remote_control_js__rspack_import_0.Rg, + XL: ()=>NewChatNavigationError, + c$: ()=>CdpHostRejectedError, + fw: ()=>ArchivedThreadError, + oN: ()=>ComposerDraftError, + sO: ()=>CdpUnreachableError + }); + }, + "./src/core/chatgpt-desktop/facade.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var node_crypto__rspack_import_0 = __webpack_require__("node:crypto"); + var _cdp_adapter_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/cdp-adapter.ts"); + var _app_server_fallback_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/app-server-fallback.ts"); + var _errors_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _loopback_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + var _remote_threads_js__rspack_import_5 = __webpack_require__("./src/core/chatgpt-desktop/remote-threads.ts"); + var _project_state_js__rspack_import_6 = __webpack_require__("./src/core/chatgpt-desktop/project-state.ts"); + var _payload_budget_js__rspack_import_8 = __webpack_require__("./src/core/chatgpt-desktop/payload-budget.ts"); + var _thread_ids_js__rspack_import_7 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const defaultFallbacks = { + listThreads: _app_server_fallback_js__rspack_import_2.OI, + searchThreads: _app_server_fallback_js__rspack_import_2.aI, + readThread: _app_server_fallback_js__rspack_import_2.Jf, + unarchiveThread: _app_server_fallback_js__rspack_import_2.qL, + statusProbe: _app_server_fallback_js__rspack_import_2.dM, + listRemoteThreads: _remote_threads_js__rspack_import_5.Hx, + listHosts: _remote_threads_js__rspack_import_5.Kl, + discoverModelProviders: _app_server_fallback_js__rspack_import_2.X + }; + const MERGED_LIST_CURSOR = 'desktop-merged-list-v1:'; + const MERGED_SEARCH_CURSOR = 'desktop-merged-search-v1:'; + class ChatGptDesktopFacade { + #cdp; + #port; + #cdpConnected = false; + #fallbacks; + #listSnapshots = new Map(); + constructor(cdp = new _cdp_adapter_js__rspack_import_1.a(), port = (0, _loopback_js__rspack_import_4.L0)(), fallbacks = defaultFallbacks){ + this.#cdp = cdp; + this.#port = port; + this.#fallbacks = fallbacks; + } + async connect({ port }) { + this.#port = (0, _loopback_js__rspack_import_4.L0)(process.env, port); + try { + await this.#cdp.connect({ + port: this.#port + }); + this.#cdpConnected = true; + } catch (error) { + this.#cdpConnected = false; + throw error; + } + } + async listTargets() { + await this.#ensureCdp(); + return this.#cdp.listTargets(); + } + async listThreads(options = {}) { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null + }); + let snapshot; + let offset = 0; + if (options.cursor) { + const state = (0, _payload_budget_js__rspack_import_8.i3)(MERGED_LIST_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop list cursor does not match the port or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildListSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop thread inventory changed; restart listing without a cursor'); + } + } else { + snapshot = await this.#buildListSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_LIST_CURSOR); + } + #pageSnapshot(snapshot, offset, limit, options, prefix, query) { + if (offset > snapshot.rows.length) throw new Error('ChatGPT Desktop cursor is out of range'); + const host = normalizeHostFilter(options.host); + const rows = []; + const base = { + backend: snapshot.backend, + limit, + host, + ...query !== undefined ? { + query + } : {}, + ...options.modelProvider?.trim() ? { + modelProvider: options.modelProvider.trim() + } : {}, + ...options.project ? { + project: options.project + } : {}, + ...options.groupBy === 'host' ? { + groupBy: 'host' + } : {} + }; + for (const source of snapshot.rows.slice(offset, offset + limit)){ + let row = source; + const candidate = [ + ...rows, + row + ]; + const probe = { + ...base, + threads: candidate, + nextCursor: prefix, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(candidate) + } : {}, + warnings: snapshot.warnings, + exitCode: 0 + }; + if ((0, _payload_budget_js__rspack_import_8.sQ)(probe) > 614400) { + if (rows.length) break; + row = (0, _payload_budget_js__rspack_import_8.MH)(row); + const minimal = [ + row + ]; + if ((0, _payload_budget_js__rspack_import_8.sQ)({ + ...base, + threads: minimal, + nextCursor: prefix, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(minimal) + } : {}, + warnings: snapshot.warnings, + exitCode: 0 + }) > 614400) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + rows.push(row); + } + const nextOffset = offset + rows.length; + const nextCursor = nextOffset < snapshot.rows.length ? (0, _payload_budget_js__rspack_import_8.oI)(prefix, { + fingerprint: snapshot.fingerprint, + offset: nextOffset, + port: this.#port, + filters: snapshot.filters + }) : null; + const warnings = [ + ...snapshot.warnings + ]; + if (rows.length < Math.min(limit, snapshot.rows.length - offset)) { + warnings.push(`${query === undefined ? 'List' : 'Search'} page stopped at the MCP byte budget; continue with nextCursor`); + } + return { + ...base, + threads: rows, + nextCursor, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(rows) + } : {}, + ...warnings.length ? { + warnings + } : {} + }; + } + async #buildListSnapshot(options, filters) { + const appRows = []; + const warnings = []; + let appCursor; + let appComplete = false; + const seenCursors = new Set(); + const modelProviders = modelProvidersFromFilter(options.modelProvider); + let appSucceeded = false; + try { + for(let page = 0; page < 400; page++){ + const out = await this.#fallbacks.listThreads({ + limit: 200, + cursor: appCursor, + modelProviders + }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) { + appComplete = true; + break; + } + if (seenCursors.has(out.nextCursor)) throw new Error('App-server list returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + } + if (!appComplete) warnings.push('App-server inventory stopped after 400 pages'); + } catch (error) { + warnings.push(`App-server list failed: ${error instanceof Error ? error.message : String(error)}`); + } + const remotes = this.#listRemotes(); + let cdpList = null; + try { + await this.#ensureCdp(); + cdpList = await this.#cdp.listThreads({ + limit: 200 + }); + } catch (error) { + if (isCdpFailure(error)) this.#cdpConnected = false; + else warnings.push(`CDP list failed: ${error instanceof Error ? error.message : String(error)}`); + } + if (!appRows.length && !cdpList && !remotes.length) { + throw new _errors_js__rspack_import_3.sO(warnings[0] ?? 'ChatGPT Desktop listThreads: CDP and app-server unreachable'); + } + const appList = { + backend: 'app-server', + limit: appRows.length, + threads: appRows + }; + const base = appSucceeded ? cdpList ? mergeThreadLists(appList, cdpList) : appList : cdpList ?? { + backend: 'remote-state', + limit: 0, + threads: [] + }; + const merged = finalizeThreadList({ + ...base, + threads: (0, _project_state_js__rspack_import_6.j)(base.threads) + }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes: (0, _project_state_js__rspack_import_6.j)(remotes) + }); + const rows = [ + ...new Map(merged.threads.map((row)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(row.threadId) ?? row.threadId, + row + ])).values() + ]; + rows.sort(compareDesktopThreads); + const fingerprint = (0, node_crypto__rspack_import_0.createHash)('sha256').update(JSON.stringify({ + filters, + port: this.#port, + rows: rows.map((row)=>[ + row.threadId, + row.updatedAt, + row.location, + row.hostId + ]) + })).digest('base64url'); + const snapshot = { + fingerprint, + port: this.#port, + filters, + createdAt: Date.now(), + rows, + backend: merged.backend, + warnings + }; + for (const [key, value] of this.#listSnapshots){ + if (Date.now() - value.createdAt > 15 * 60000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + async listHosts() { + const remotes = this.#listRemotes(); + const cdpList = typeof this.#cdp.listThreads === 'function' ? await this.#tryCdpList({ + limit: 200 + }) : null; + const cdpRows = cdpList?.threads ?? []; + let localThreadCount = 0; + let appSucceeded = false; + try { + const local = await this.#fallbacks.listThreads({ + limit: 25, + modelProviders: [] + }); + appSucceeded = true; + localThreadCount = local.threads.length; + } catch { + localThreadCount = 0; + } + const listHosts = this.#fallbacks.listHosts ?? _remote_threads_js__rspack_import_5.Kl; + const fromState = listHosts(process.env, { + localThreadCount + }); + const byId = new Map(fromState.map((host)=>[ + host.hostId, + { + ...host + } + ])); + for (const thread of remotes){ + const existing = byId.get(thread.hostId); + if (existing) { + byId.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName + }); + } else { + byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 0 + }); + } + } + for (const thread of cdpRows){ + if (thread.location !== 'remote' || !thread.hostId) continue; + if (!byId.has(thread.hostId)) byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName ?? null, + location: 'remote', + threadCount: 0 + }); + } + const hostsSource = 'local+remote-thread-summaries-v3+managed-connections' + (cdpRows.length ? '+cdp' : ''); + const remoteCounts = new Map(); + const remoteInventory = finalizeThreadList({ + backend: cdpRows.length ? 'cdp' : 'remote-state', + limit: 10000, + threads: cdpRows.filter((thread)=>thread.location === 'remote') + }, { + limit: 10000, + remotes + }); + for (const thread of remoteInventory.threads){ + if (thread.hostId) remoteCounts.set(thread.hostId, (remoteCounts.get(thread.hostId) ?? 0) + 1); + } + const withCounts = [ + ...byId.values() + ].map((host)=>host.hostId === 'local' ? { + ...host, + threadCount: localThreadCount + } : { + ...host, + threadCount: remoteCounts.get(host.hostId) ?? host.threadCount + }).sort((a, b)=>{ + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + let modelProviders = []; + let modelProvidersSource = 'thread/list-distinct'; + const warnings = []; + try { + const discover = this.#fallbacks.discoverModelProviders ?? _app_server_fallback_js__rspack_import_2.X; + const discovered = await discover(); + modelProviders = [ + ...discovered.providers + ]; + modelProvidersSource = discovered.source; + if (typeof discovered.threadCount === 'number') localThreadCount = discovered.threadCount; + if (discovered.complete === false) warnings.push('Provider discovery stopped before the final app-server page'); + } catch (error) { + warnings.push(`Model provider discovery failed: ${error instanceof Error ? error.message : String(error)}`); + modelProviders = [ + ...new Set(remotes.map((thread)=>thread.modelProvider).filter((value)=>typeof value === 'string' && Boolean(value))) + ].sort(); + modelProvidersSource = 'remote-summaries-distinct'; + } + modelProviders = [ + ...new Set([ + ...modelProviders, + ...remotes.map((thread)=>thread.modelProvider).filter((value)=>Boolean(value)) + ]) + ].sort(); + let inventoryCounts = null; + try { + const filters = JSON.stringify({ + host: 'all', + modelProvider: null, + project: null, + groupBy: null + }); + const cached = [ + ...this.#listSnapshots.values() + ].find((snapshot)=>snapshot.port === this.#port && snapshot.filters === filters && Date.now() - snapshot.createdAt < 15 * 60000); + const inventory = cached ?? await this.#buildListSnapshot({}, filters); + inventoryCounts = new Map(); + for (const thread of inventory.rows){ + const hostId = thread.location === 'remote' ? thread.hostId ?? 'unknown-remote' : 'local'; + inventoryCounts.set(hostId, (inventoryCounts.get(hostId) ?? 0) + 1); + } + warnings.push(...inventory.warnings); + } catch (error) { + warnings.push(`Merged inventory count failed: ${error instanceof Error ? error.message : String(error)}`); + } + return { + hosts: withCounts.map((host)=>{ + const counted = { + ...host, + threadCount: inventoryCounts?.get(host.hostId) ?? (host.hostId === 'local' ? localThreadCount : host.threadCount) + }; + if (!counted.hostName) return counted; + const name = (0, _payload_budget_js__rspack_import_8.aA)(counted.hostName); + return { + ...counted, + hostName: name.title, + ...name.titleTruncated ? { + hostNameTruncated: true + } : {} + }; + }), + hostsSource, + modelProviders, + modelProvidersSource, + backend: appSucceeded ? cdpRows.length ? remotes.length ? 'app-server+cdp+remote-state' : 'app-server+cdp' : remotes.length ? 'app-server+remote-state' : 'app-server' : cdpRows.length ? remotes.length ? 'cdp+remote-state' : 'cdp' : 'remote-state', + ...warnings.length ? { + warnings + } : {} + }; + } + async searchThreads(options) { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + query: options.query, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null + }); + let snapshot; + let offset = 0; + if (options.cursor) { + const state = (0, _payload_budget_js__rspack_import_8.i3)(MERGED_SEARCH_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop search cursor does not match the port, query or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildSearchSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop search results changed; restart without a cursor'); + } + } else { + snapshot = await this.#buildSearchSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_SEARCH_CURSOR, options.query); + } + async #buildSearchSnapshot(options, filters) { + const modelProviders = modelProvidersFromFilter(options.modelProvider); + const appRows = []; + const warnings = []; + const seenCursors = new Set(); + let appCursor; + let appSucceeded = false; + let appError; + try { + for(let page = 0; page < 400; page++){ + const out = await this.#fallbacks.searchThreads({ + query: options.query, + limit: 200, + cursor: appCursor, + modelProviders + }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) break; + if (seenCursors.has(out.nextCursor)) throw new Error('App-server search returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + if (page === 399) warnings.push('App-server search stopped after 400 pages'); + } + } catch (error) { + appError = error; + warnings.push(`App-server search failed: ${error instanceof Error ? error.message : String(error)}`); + } + const cdpList = await this.#tryCdpList({ + limit: 200 + }); + const remotes = (0, _project_state_js__rspack_import_6.j)(this.#listRemotes()); + if (!appSucceeded && !cdpList && !remotes.length) throw appError; + const appList = { + backend: 'app-server', + limit: appRows.length, + threads: appRows + }; + const base = appSucceeded ? cdpList ? mergeThreadLists(appList, cdpList) : appList : cdpList ?? { + backend: 'remote-state', + limit: 0, + threads: [] + }; + const appMatches = new Set(appRows.map((thread)=>(0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId)); + const merged = finalizeThreadList({ + ...base, + threads: (0, _project_state_js__rspack_import_6.j)(base.threads) + }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes + }); + const needle = options.query.trim().toLowerCase(); + const rows = [ + ...new Map(merged.threads.filter((thread)=>appMatches.has((0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId) || threadMatchesQuery(thread, needle)).map((thread)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId, + thread + ])).values() + ]; + rows.sort(compareDesktopThreads); + const fingerprint = (0, node_crypto__rspack_import_0.createHash)('sha256').update(JSON.stringify({ + filters, + port: this.#port, + rows: rows.map((row)=>[ + row.threadId, + row.updatedAt, + row.location, + row.hostId + ]) + })).digest('base64url'); + const snapshot = { + fingerprint, + port: this.#port, + filters, + createdAt: Date.now(), + rows, + backend: merged.backend, + warnings + }; + for (const [key, value] of this.#listSnapshots){ + if (Date.now() - value.createdAt > 15 * 60000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + async readThread(options) { + const limit = options.limit ?? (options.full ? 2000 : 100); + const full = options.full ?? false; + if ((0, _thread_ids_js__rspack_import_7.vr)(options.threadId)) { + if (options.cursor) throw new Error('Temporary Desktop threads do not support read cursors'); + await this.#ensureCdp(); + return this.#cdp.readThread({ + ...options, + limit, + full + }); + } + try { + return await this.#fallbacks.readThread({ + threadId: options.threadId, + limit, + full, + cursor: options.cursor + }); + } catch (appError) { + if (appError instanceof _errors_js__rspack_import_3.Rg) throw appError; + if (options.cursor) throw appError; + const cdpConnected = await this.#tryEnsureCdp(); + if (!cdpConnected) throw appError; + const fallback = await this.#cdp.readThread({ + ...options, + limit, + full + }); + return { + ...fallback, + complete: false, + warnings: [ + `App-server read failed: ${appError instanceof Error ? appError.message : String(appError)}` + ] + }; + } + } + async sendMessage(options) { + if (options.threadId) { + const remote = (0, _remote_threads_js__rspack_import_5.KI)(options.threadId); + if (remote) throw new _errors_js__rspack_import_3.Rg(options.threadId, remote.hostId, remote.hostName); + } + await this.#ensureCdp(); + try { + return await this.#cdp.sendMessage(options); + } catch (error) { + if (!(error instanceof _errors_js__rspack_import_3.fw) || !options.unarchive || !options.threadId) throw error; + await (this.#fallbacks.unarchiveThread ?? _app_server_fallback_js__rspack_import_2.qL)(options.threadId); + return this.#cdp.sendMessage({ + ...options, + unarchive: false + }); + } + } + async waitForReply(options) { + await this.#ensureCdp(); + return this.#cdp.waitForReply(options); + } + async openThread(threadId, options) { + const remote = (0, _remote_threads_js__rspack_import_5.KI)(threadId); + if (remote) throw new _errors_js__rspack_import_3.Rg(threadId, remote.hostId, remote.hostName); + await this.#ensureCdp(); + return this.#cdp.openThread(threadId, options); + } + async status() { + const cdpStatus = await this.#cdp.status(); + let appServerFallback; + try { + appServerFallback = await this.#fallbacks.statusProbe(); + } catch { + appServerFallback = { + reachable: false + }; + } + const reachable = cdpStatus.reachable; + return { + ...cdpStatus, + port: this.#port, + appServerFallback, + reachable, + message: cdpStatus.reachable ? cdpStatus.message : cdpStatus.message, + exitCode: reachable ? 0 : 1 + }; + } + async close() { + this.#cdpConnected = false; + await this.#cdp.close(); + } + #listRemotes() { + const list = this.#fallbacks.listRemoteThreads ?? _remote_threads_js__rspack_import_5.Hx; + try { + return list(); + } catch { + return []; + } + } + async #ensureCdp() { + if (this.#cdpConnected) return; + await this.#cdp.connect({ + port: this.#port + }); + this.#cdpConnected = true; + } + async #tryEnsureCdp() { + try { + await this.#ensureCdp(); + return true; + } catch (error) { + if (!isCdpFailure(error)) throw error; + this.#cdpConnected = false; + return false; + } + } + async #tryCdpList(options) { + try { + await this.#ensureCdp(); + return await this.#cdp.listThreads(options); + } catch (error) { + if (isCdpFailure(error) || error instanceof _errors_js__rspack_import_3.EH) { + if (isCdpFailure(error)) this.#cdpConnected = false; + return null; + } + throw error; + } + } + } + function mergeThreadLists(appList, cdpList) { + const cdpByKey = new Map(); + for (const thread of cdpList.threads){ + const key = (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId; + cdpByKey.set(key, thread); + } + const merged = appList.threads.map((thread)=>{ + const key = (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId; + const cdp = cdpByKey.get(key); + const withLocal = { + ...thread, + location: thread.location ?? 'local', + hostId: thread.hostId ?? null, + hostName: thread.hostName ?? null + }; + if (!cdp) return withLocal; + return { + ...withLocal, + title: withLocal.title || cdp.title, + pinned: withLocal.pinned || cdp.pinned, + selected: cdp.selected, + kind: cdp.kind || withLocal.kind, + project: cdp.project ?? withLocal.project, + location: cdp.location === 'remote' ? 'remote' : withLocal.location, + hostId: cdp.location === 'remote' ? cdp.hostId ?? null : withLocal.hostId, + hostName: cdp.location === 'remote' ? cdp.hostName ?? null : withLocal.hostName + }; + }); + for (const thread of cdpList.threads){ + if (merged.some((row)=>(0, _thread_ids_js__rspack_import_7.LV)(row.threadId, thread.threadId))) continue; + merged.push(thread); + } + return { + backend: 'app-server+cdp', + limit: appList.limit, + nextCursor: appList.nextCursor, + ...appList.query !== undefined ? { + query: appList.query + } : {}, + threads: merged + }; + } + function finalizeThreadList(base, { limit, host = 'all', modelProvider, project, groupBy, remotes }) { + const remoteById = new Map(remotes.map((thread)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId, + thread + ])); + const locals = base.threads.map((thread)=>{ + const remote = remoteById.get((0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId); + if (remote) { + return { + ...thread, + ...remote, + selected: thread.selected, + pinned: thread.pinned || remote.pinned + }; + } + const location = thread.location ?? 'local'; + return { + ...thread, + location, + hostId: location === 'remote' ? thread.hostId ?? null : null, + hostName: location === 'remote' ? thread.hostName ?? null : null + }; + }); + const localKeys = new Set(locals.map((thread)=>(0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId)); + const merged = [ + ...locals + ]; + for (const remote of remotes){ + const key = (0, _thread_ids_js__rspack_import_7.nn)(remote.threadId) ?? remote.threadId; + if (localKeys.has(key)) continue; + merged.push({ + ...remote, + location: remote.location ?? 'remote', + hostId: remote.hostId ?? null, + hostName: remote.hostName ?? null + }); + localKeys.add(key); + } + if (remotes.length) merged.sort((a, b)=>Number(b.location === 'remote') - Number(a.location === 'remote')); + const filtered = merged.filter((thread)=>{ + if (!hostMatches(thread, host)) return false; + if (!modelProviderMatches(thread, modelProvider)) return false; + if (!projectMatches(thread, project)) return false; + return true; + }); + const limited = filtered.slice(0, limit).map(_payload_budget_js__rspack_import_8.eX); + const result = { + backend: remotes.length ? base.backend === 'app-server+cdp' ? 'app-server+cdp+remote-state' : base.backend === 'app-server' ? 'app-server+remote-state' : base.backend === 'cdp' ? 'cdp+remote-state' : base.backend : base.backend, + limit, + threads: limited, + ...base.nextCursor !== undefined ? { + nextCursor: base.nextCursor + } : {}, + ...base.query !== undefined ? { + query: base.query + } : {}, + host, + ...modelProvider ? { + modelProvider + } : {}, + ...project ? { + project + } : {} + }; + if (groupBy === 'host') { + return { + ...result, + groupBy: 'host', + groups: groupThreadsByHost(limited) + }; + } + return result; + } + function compareDesktopThreads(a, b) { + const timestamp = (row)=>{ + const value = row.updatedAt ?? row.createdAt; + if (typeof value !== 'number' || !Number.isFinite(value)) return Number.NEGATIVE_INFINITY; + return value < 100000000000 ? value * 1000 : value; + }; + return timestamp(b) - timestamp(a) || a.threadId.localeCompare(b.threadId); + } + function groupThreadsByHost(threads) { + const groups = new Map(); + for (const thread of threads){ + const location = thread.location ?? 'local'; + const hostId = location === 'remote' ? thread.hostId || 'unknown-remote' : 'local'; + const key = `${location}:${hostId}`; + const existing = groups.get(key); + if (existing) { + existing.threads.push(thread); + continue; + } + groups.set(key, { + hostId, + hostName: thread.hostName ?? (location === 'local' ? 'local' : null), + location, + threads: [ + thread + ] + }); + } + return [ + ...groups.values() + ]; + } + function normalizeHostFilter(host) { + const value = (host ?? 'all').trim(); + return value || 'all'; + } + function modelProvidersFromFilter(modelProvider) { + if (!modelProvider || !modelProvider.trim()) return []; + return [ + modelProvider.trim() + ]; + } + function hostMatches(thread, host) { + const needle = host.trim().toLowerCase(); + if (!needle || needle === 'all') return true; + if (needle === 'local') return (thread.location ?? 'local') === 'local'; + return [ + thread.hostId, + thread.hostName + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function modelProviderMatches(thread, modelProvider) { + if (!modelProvider) return true; + return thread.modelProvider === modelProvider; + } + function projectMatches(thread, project) { + if (!project) return true; + const needle = project.trim().toLowerCase(); + return [ + thread.project, + thread.projectId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function threadMatchesQuery(thread, needle) { + if (!needle) return true; + return [ + thread.title, + thread.threadId, + thread.project, + thread.preview, + thread.hostName, + thread.hostId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function isCdpFailure(error) { + if (error instanceof _errors_js__rspack_import_3.sO) return true; + if (error instanceof _errors_js__rspack_import_3.EH) return false; + if (error && typeof error === 'object' && 'code' in error) { + const code = String(error.code); + return code === 'CDP_UNREACHABLE' || code === 'CDP_HOST_REJECTED'; + } + return false; + } + let activeAdapter = null; + function getChatGptDesktopAdapter() { + if (!activeAdapter) activeAdapter = new ChatGptDesktopFacade(); + return activeAdapter; + } + function setChatGptDesktopAdapterForTests(adapter) { + activeAdapter = adapter; + } + __webpack_require__.d(__webpack_exports__, { + PP: ()=>getChatGptDesktopAdapter + }); + }, + "./src/core/chatgpt-desktop/loopback.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + const LOOPBACK = new Set([ + '127.0.0.1', + 'localhost', + '::1' + ]); + const DEFAULT_CDP_PORT = 9222; + const CDP_LOOPBACK_HOST = '127.0.0.1'; + function isLoopbackHostname(hostname) { + const host = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, ''); + return LOOPBACK.has(host); + } + function assertLoopbackHostname(hostname) { + if (!isLoopbackHostname(hostname)) { + throw new _errors_js__rspack_import_0.c$(hostname); + } + } + function cdpHttpBase(port) { + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return `http://${CDP_LOOPBACK_HOST}:${port}`; + } + function forceLoopbackWebSocketUrl(raw) { + let url; + try { + url = new URL(raw); + } catch { + throw new _errors_js__rspack_import_0.c$(String(raw)); + } + assertLoopbackHostname(url.hostname); + if (url.username || url.password || url.hash) throw new _errors_js__rspack_import_0.c$(raw); + if (url.protocol !== 'ws:' && url.protocol !== 'wss:') { + throw new Error(`Expected ws(s) debugger URL, got ${url.protocol}`); + } + url.hostname = CDP_LOOPBACK_HOST; + url.protocol = 'ws:'; + return url.toString(); + } + function resolveCdpPort(env = process.env, explicit) { + if (explicit !== undefined) { + if (!Number.isInteger(explicit) || explicit < 1 || explicit > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return explicit; + } + const raw = (env.CHATGPT_DESKTOP_CDP_PORT || '').trim(); + if (!raw) return DEFAULT_CDP_PORT; + const port = /^\d+$/.test(raw) ? Number(raw) : NaN; + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CHATGPT_DESKTOP_CDP_PORT must be an integer 1-65535'); + } + return port; + } + __webpack_require__.d(__webpack_exports__, { + L0: ()=>resolveCdpPort, + NJ: ()=>cdpHttpBase, + jv: ()=>forceLoopbackWebSocketUrl + }); + }, + "./src/core/chatgpt-desktop/payload-budget.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + const DESKTOP_RESULT_BUDGET_BYTES = 600 * 1024; + const DESKTOP_THREAD_TITLE_CHARS = 200; + function serializedBytes(value) { + return Buffer.byteLength(JSON.stringify(value), 'utf8'); + } + function compact(value, maxChars) { + const normalized = value.replace(/\s+/gu, ' ').trim(); + const chars = Array.from(normalized); + return { + value: chars.slice(0, maxChars).join(''), + clipped: chars.length > maxChars + }; + } + function compactThreadRow(thread) { + const title = compact(thread.title, DESKTOP_THREAD_TITLE_CHARS); + const preview = typeof thread.preview === 'string' ? compact(thread.preview, 200) : null; + const cwd = typeof thread.cwd === 'string' ? compact(thread.cwd, 2048) : null; + const project = typeof thread.project === 'string' ? compact(thread.project, 256) : null; + const hostName = typeof thread.hostName === 'string' ? compact(thread.hostName, 256) : null; + return { + ...thread, + title: title.value, + ...title.clipped ? { + titleTruncated: true + } : {}, + ...preview ? { + preview: preview.value, + ...preview.clipped ? { + previewTruncated: true + } : {} + } : {}, + ...cwd ? { + cwd: cwd.value, + ...cwd.clipped ? { + cwdTruncated: true + } : {} + } : {}, + ...project ? { + project: project.value, + ...project.clipped ? { + projectTruncated: true + } : {} + } : {}, + ...hostName ? { + hostName: hostName.value, + ...hostName.clipped ? { + hostNameTruncated: true + } : {} + } : {} + }; + } + function compactThreadTitle(title) { + const out = compact(title, DESKTOP_THREAD_TITLE_CHARS); + return { + title: out.value, + titleTruncated: out.clipped + }; + } + function minimalThreadRow(thread) { + return { + threadId: thread.threadId, + title: compact(thread.title, DESKTOP_THREAD_TITLE_CHARS).value, + titleTruncated: true, + pinned: thread.pinned, + selected: thread.selected, + kind: thread.kind, + location: thread.location, + hostId: thread.hostId, + hostName: thread.hostName, + modelProvider: thread.modelProvider, + metadataTruncated: true + }; + } + function encodePageCursor(prefix, value) { + return prefix + Buffer.from(JSON.stringify(value)).toString('base64url'); + } + function decodePageCursor(prefix, cursor) { + if (!cursor.startsWith(prefix)) throw new Error('Invalid ChatGPT Desktop page cursor'); + try { + return JSON.parse(Buffer.from(cursor.slice(prefix.length), 'base64url').toString('utf8')); + } catch { + throw new Error('Invalid ChatGPT Desktop page cursor'); + } + } + __webpack_require__.d(__webpack_exports__, { + MH: ()=>minimalThreadRow, + aA: ()=>compactThreadTitle, + eX: ()=>compactThreadRow, + i3: ()=>decodePageCursor, + oI: ()=>encodePageCursor, + sQ: ()=>serializedBytes + }); + }, + "./src/core/chatgpt-desktop/project-state.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_ids_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + function findLocalProject(nameOrId, env = process.env) { + const state = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!state) return null; + for (const value of Object.values(record(stateValue(state, 'local-projects')) ?? {})){ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') continue; + if (row.id !== nameOrId && row.name.toLowerCase() !== nameOrId.toLowerCase()) continue; + return { + id: row.id, + label: row.name, + rootPaths: (Array.isArray(row.rootPaths) ? row.rootPaths : []).filter((path)=>typeof path === 'string' && Boolean(path)) + }; + } + return null; + } + function record(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : null; + } + function stateValue(state, key) { + for (const source of (0, _codex_remote_control_js__rspack_import_0.xr)(state))if (key in source) return source[key]; + return undefined; + } + function pathWithin(path, root) { + const normalized = root.replace(/\/+$/u, ''); + return path === normalized || path.startsWith(`${normalized}/`); + } + function resolveThreadProjects(threads, env = process.env) { + const state = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!state) return [ + ...threads + ]; + const localProjects = Object.values(record(stateValue(state, 'local-projects')) ?? {}).flatMap((value)=>{ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') return []; + return (Array.isArray(row.rootPaths) ? row.rootPaths : []).filter((path)=>typeof path === 'string' && Boolean(path)).map((rootPath)=>({ + id: row.id, + label: row.name, + rootPath + })); + }); + const remoteProjects = (Array.isArray(stateValue(state, 'remote-projects')) ? stateValue(state, 'remote-projects') : []).flatMap((value)=>{ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.label !== 'string' || typeof row.remotePath !== 'string' || typeof row.hostId !== 'string') return []; + return [ + { + id: row.id, + label: row.label, + rootPath: row.remotePath, + hostId: row.hostId + } + ]; + }); + const assignments = record(stateValue(state, 'thread-project-assignments')) ?? {}; + const hints = record(stateValue(state, 'thread-workspace-root-hints')) ?? {}; + const projectless = new Set(Array.isArray(stateValue(state, 'projectless-thread-ids')) ? stateValue(state, 'projectless-thread-ids') : []); + return threads.map((thread)=>{ + const bare = (0, _thread_ids_js__rspack_import_1.nn)(thread.threadId) ?? thread.threadId; + const assignment = record(assignments[bare]); + const remote = thread.location === 'remote'; + const projects = remote ? remoteProjects.filter((project)=>project.hostId === thread.hostId) : localProjects; + let project = assignment && typeof assignment.projectId === 'string' ? projects.find((entry)=>entry.id === assignment.projectId) : undefined; + if (!project && !projectless.has(bare) && thread.projectId) { + project = projects.find((entry)=>entry.id === thread.projectId); + } + if (!project && !assignment && !projectless.has(bare)) { + const path = typeof hints[bare] === 'string' ? hints[bare] : typeof thread.cwd === 'string' ? thread.cwd : null; + if (path) project = projects.filter((entry)=>pathWithin(path, entry.rootPath)).sort((a, b)=>b.rootPath.length - a.rootPath.length)[0]; + } + if (project) return { + ...thread, + project: project.label, + projectId: project.id, + projectRootPath: project.rootPath + }; + if (projectless.has(bare)) return { + ...thread, + project: null, + projectId: null + }; + if (remote && typeof thread.project === 'string' && thread.project.startsWith('/')) { + return { + ...thread, + project: null + }; + } + return thread; + }); + } + __webpack_require__.d(__webpack_exports__, { + j: ()=>resolveThreadProjects, + q: ()=>findLocalProject + }); + }, + "./src/core/chatgpt-desktop/remote-threads.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_ids_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + function listRemoteThreadsFromState(env = process.env) { + const data = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!data) return []; + const hostNames = collectHostNames(data); + const out = []; + const seen = new Set(); + for (const [key, value] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value); + const pinnedIds = extractPinnedIds(value); + for (const entry of extractThreadEntries(value)){ + const mapped = mapRemoteEntry(entry, { + hostId, + hostName, + pinnedIds + }); + if (!mapped) continue; + const bare = (0, _thread_ids_js__rspack_import_1.nn)(mapped.threadId) ?? mapped.threadId; + const dedupe = `${hostId}:${bare}`; + if (seen.has(dedupe)) continue; + seen.add(dedupe); + out.push(mapped); + } + } + return out; + } + function findRemoteThread(threadId, env = process.env) { + const bare = (0, _thread_ids_js__rspack_import_1.nn)(threadId) ?? threadId; + if (!bare) return null; + for (const thread of listRemoteThreadsFromState(env)){ + const id = (0, _thread_ids_js__rspack_import_1.nn)(thread.threadId) ?? thread.threadId; + if (id === bare) { + return { + hostId: thread.hostId, + hostName: thread.hostName, + thread + }; + } + } + return null; + } + function findRemoteThreadHostId(threadId, env = process.env) { + return findRemoteThread(threadId, env)?.hostId ?? null; + } + function listDiscoveredHosts(env = process.env, { localThreadCount = 0 } = {}) { + const remotes = listRemoteThreadsFromState(env); + const byHost = new Map(); + byHost.set('local', { + hostId: 'local', + hostName: 'local', + location: 'local', + threadCount: localThreadCount + }); + const data = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (data) { + const hostNames = collectHostNames(data); + for (const [key, summary] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(summary) ?? null; + if (byHost.has(hostId)) continue; + byHost.set(hostId, { + hostId, + hostName, + location: 'remote', + threadCount: 0 + }); + } + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections){ + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue; + const hostId = stringField(entry, [ + 'hostId' + ]); + if (hostId && !byHost.has(hostId)) byHost.set(hostId, { + hostId, + hostName: stringField(entry, [ + 'displayName', + 'alias' + ]), + location: 'remote', + threadCount: 0 + }); + } + } + for (const thread of remotes){ + const existing = byHost.get(thread.hostId); + if (existing) { + byHost.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName, + threadCount: existing.threadCount + 1 + }); + } else { + byHost.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 1 + }); + } + } + return [ + ...byHost.values() + ].sort((a, b)=>{ + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + } + function collectHostNames(data) { + const names = new Map(); + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections){ + const pair = hostNameFromEntry(entry); + if (pair) names.set(pair.hostId, pair.hostName); + } + for (const [key, value] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry; + const hostId = stringField(row, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(row, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractPinnedIds(value) { + const pinned = new Set(); + if (!value || typeof value !== 'object') return pinned; + const record = value; + for (const key of [ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'pinned_ids' + ]){ + const arr = record[key]; + if (!Array.isArray(arr)) continue; + for (const item of arr){ + if (typeof item === 'string' && item) pinned.add(item); + else if (item && typeof item === 'object') { + const id = stringField(item, [ + 'id', + 'threadId', + 'thread_id' + ]); + if (id) pinned.add(id); + } + } + } + return pinned; + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) { + continue; + } + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const row = entry; + if (!stringField(row, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ])) { + entries.push({ + ...row, + id: key + }); + } else { + entries.push(entry); + } + } + } + return entries; + } + function mapRemoteEntry(entry, { hostId, hostName, pinnedIds }) { + if (typeof entry === 'string' && entry) { + return { + threadId: (0, _thread_ids_js__rspack_import_1.BS)(entry), + title: entry, + pinned: pinnedIds.has(entry), + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName + }; + } + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry; + const id = stringField(row, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + if (!id) return null; + const title = stringField(row, [ + 'title', + 'name', + 'preview', + 'label' + ]) || id; + const projectId = stringField(row, [ + 'projectId', + 'project_id' + ]); + const project = stringField(row, [ + 'project', + 'projectName', + 'project_name', + 'cwd' + ]) || projectId || undefined; + const preview = stringField(row, [ + 'preview', + 'snippet' + ]); + const cwd = stringField(row, [ + 'cwd', + 'workingDirectory', + 'working_directory' + ]); + const modelProvider = stringField(row, [ + 'modelProvider', + 'model_provider', + 'provider' + ]); + const model = stringField(row, [ + 'model' + ]); + const originator = stringField(row, [ + 'originator', + 'source' + ]); + const createdAt = numberField(row, [ + 'createdAt', + 'created_at' + ]); + const updatedAt = numberField(row, [ + 'updatedAt', + 'updated_at' + ]); + const sectionName = stringField(row, [ + 'sectionName', + 'section' + ]); + const sectionId = stringField(row, [ + 'sectionId', + 'section_id' + ]) || sectionName; + const pinned = pinnedIds.has(id) || row.pinned === true || sectionId === 'Pinned' || sectionName === 'Pinned'; + return { + threadId: (0, _thread_ids_js__rspack_import_1.BS)(id), + title, + pinned, + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName, + ...project ? { + project + } : {}, + ...projectId ? { + projectId + } : {}, + ...preview ? { + preview + } : {}, + ...cwd ? { + cwd + } : { + cwd: null + }, + createdAt: createdAt ?? null, + updatedAt: updatedAt ?? null, + ...sectionId ? { + section: { + id: sectionId, + name: sectionName ?? sectionId + } + } : { + section: null + }, + ...modelProvider ? { + modelProvider + } : { + modelProvider: null + }, + ...model ? { + model + } : { + model: null + }, + ...originator ? { + originator + } : { + originator: null + } + }; + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + const record = row; + for (const key of keys){ + const value = record[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function numberField(row, keys) { + for (const key of keys){ + const value = row[key]; + if (typeof value === 'number' && Number.isFinite(value)) return value; + } + return null; + } + __webpack_require__.d(__webpack_exports__, { + Hx: ()=>listRemoteThreadsFromState, + KI: ()=>findRemoteThread, + Kl: ()=>listDiscoveredHosts, + XI: ()=>findRemoteThreadHostId + }); + }, + "./src/core/chatgpt-desktop/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _codex_contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _facade_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/facade.ts"); + var _loopback_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + const threadId = zod__rspack_import_4.YjP().min(1).max(256); + const statusSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional() + }).strict(); + const listThreadsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + host: zod__rspack_import_4.YjP().min(1).max(256).optional(), + modelProvider: zod__rspack_import_4.YjP().min(1).max(256).optional(), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + groupBy: zod__rspack_import_4.euz('host').optional() + }).strict(); + const searchThreadsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + query: zod__rspack_import_4.YjP().min(1).max(512), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + host: zod__rspack_import_4.YjP().min(1).max(256).optional(), + modelProvider: zod__rspack_import_4.YjP().min(1).max(256).optional(), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + groupBy: zod__rspack_import_4.euz('host').optional() + }).strict(); + const listHostsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional() + }).strict(); + const readThreadSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId, + limit: zod__rspack_import_4.aig().int().min(1).max(2000).optional(), + full: zod__rspack_import_4.zMY().default(false), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict(); + const openThreadSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId, + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict(); + const sendSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + text: zod__rspack_import_4.YjP().trim().min(1).max(100000), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + unarchive: zod__rspack_import_4.zMY().default(false), + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict().refine((value)=>!value.unarchive || Boolean(value.threadId), { + message: 'unarchive requires an existing threadId', + path: [ + 'unarchive' + ] + }).refine((value)=>!value.threadId || !value.project, { + message: 'project is only for a new thread; omit threadId', + path: [ + 'project' + ] + }); + const waitReplySchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + timeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(120000) + }).strict(); + const resultSchema = zod__rspack_import_4.Ikc({ + exitCode: zod__rspack_import_4.KCZ([ + zod__rspack_import_4.euz(0), + zod__rspack_import_4.euz(1) + ]) + }).catchall(zod__rspack_import_4.Pq9()); + let operationQueue = Promise.resolve(); + async function withAdapter(port, run) { + const operation = operationQueue.then(async ()=>{ + const adapter = (0, _facade_js__rspack_import_2.PP)(); + const resolved = (0, _loopback_js__rspack_import_3.L0)(process.env, port); + try { + try { + await adapter.connect({ + port: resolved + }); + } catch {} + return await run(adapter); + } finally{ + await adapter.close(); + } + }); + operationQueue = operation.catch(()=>{}); + return operation; + } + function asResult(value) { + return resultSchema.parse(JSON.parse(JSON.stringify(value))); + } + function mapError(error) { + if (error instanceof _errors_js__rspack_import_1.EH) { + return asResult({ + delivery: 'rejected', + reason: 'not-implemented', + code: 'NOT_IMPLEMENTED', + error: error.message, + exitCode: 1 + }); + } + if (error instanceof _errors_js__rspack_import_1.Rg) { + return asResult({ + delivery: error.delivery, + reason: error.reason, + code: error.code, + error: error.message, + threadId: error.threadId, + hint: error.hint, + ...error.hostId ? { + hostId: error.hostId + } : {}, + ...error.hostName ? { + hostName: error.hostName + } : {}, + exitCode: 1 + }); + } + const base = (0, _codex_contract_js__rspack_import_0.DG)(error); + if (error && typeof error === 'object' && 'code' in error) { + return asResult({ + ...base, + code: String(error.code) + }); + } + return asResult(base); + } + async function statusOperation(input) { + try { + const status = await withAdapter(input.port, (adapter)=>adapter.status()); + return asResult(status); + } catch (error) { + return mapError(error); + } + } + async function listThreadsOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.listThreads({ + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function searchThreadsOperation(input) { + try { + const out = await withAdapter(input.port, async (adapter)=>{ + if (typeof adapter.searchThreads !== 'function') { + throw new _errors_js__rspack_import_1.EH('searchThreads', 'adapter does not implement search'); + } + return adapter.searchThreads({ + query: input.query, + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy + }); + }); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function listHostsOperation(input) { + try { + const out = await withAdapter(input.port, async (adapter)=>{ + if (typeof adapter.listHosts !== 'function') { + throw new _errors_js__rspack_import_1.EH('listHosts', 'adapter does not implement listHosts'); + } + return adapter.listHosts(); + }); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function readThreadOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.readThread({ + threadId: input.threadId, + limit: input.limit ?? (input.full ? 2000 : 100), + full: input.full, + cursor: input.cursor, + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function openThreadOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.openThread(input.threadId, { + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function sendOperation(input) { + try { + input = sendSchema.parse(input); + const out = await withAdapter(input.port, (adapter)=>adapter.sendMessage({ + threadId: input.threadId, + text: input.text, + project: input.project, + unarchive: input.unarchive, + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: out.delivery === 'accepted' ? 0 : 1 + }); + } catch (error) { + return mapError(error); + } + } + async function waitReplyOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.waitForReply({ + threadId: input.threadId, + timeoutMs: input.timeoutMs + })); + return asResult({ + ...out, + exitCode: out.delivery === 'replied' ? 0 : 1 + }); + } catch (error) { + return mapError(error); + } + } + function resultText(result) { + if (result.error) return String(result.error); + if (result.reachable === true) { + const backend = result.appServerFallback && typeof result.appServerFallback === 'object' ? ` cdp + app-server probe` : ''; + return result.message ? String(result.message) : `ChatGPT Desktop CDP reachable on 127.0.0.1:${result.port}${backend}`; + } + if (result.reachable === false) return String(result.message ?? 'ChatGPT Desktop CDP unreachable'); + if (Array.isArray(result.hosts)) { + const providers = Array.isArray(result.modelProviders) ? ` providers=${result.modelProviders.length} (${String(result.modelProvidersSource ?? '')})` : ''; + return `${result.hosts.length} hosts via ${String(result.hostsSource ?? result.backend ?? 'unknown')}${providers}`; + } + if (Array.isArray(result.threads)) { + const via = result.query ? ` matching ${JSON.stringify(result.query)}` : ''; + const host = result.host && result.host !== 'all' ? ` host=${result.host}` : ''; + const provider = result.modelProvider ? ` provider=${result.modelProvider}` : ''; + return `${result.threads.length} threads${via}${host}${provider} via ${result.backend ?? 'unknown'}`; + } + if (Array.isArray(result.turns)) { + const mode = result.full ? 'full' : 'page'; + return `${result.turns.length} turns (${mode}) on ${result.threadId} via ${result.backend ?? 'unknown'}`; + } + if (result.delivery === 'accepted') { + return `Sent on ${result.threadId} via ${result.backend}` + (result.conversationId ? ` (conversation ${result.conversationId})` : ''); + } + if (result.delivery === 'replied') { + const id = result.conversationId ? ` [${result.conversationId}]` : ''; + return `${String(result.reply ?? 'replied')}${id}`; + } + if (result.delivery) return `delivery ${result.delivery}`; + return JSON.stringify(result); + } + __webpack_require__.d(__webpack_exports__, { + Bm: ()=>listHostsOperation, + D6: ()=>resultText, + Iu: ()=>openThreadOperation, + UK: ()=>readThreadOperation, + UP: ()=>sendOperation, + Uj: ()=>statusOperation, + Zg: ()=>waitReplyOperation, + tJ: ()=>listThreadsOperation, + zg: ()=>searchThreadsOperation + }, { + FC: listThreadsSchema, + FD: resultSchema, + Fn: waitReplySchema, + OR: openThreadSchema, + Rf: listHostsSchema, + Wi: sendSchema, + aW: statusSchema, + nl: searchThreadsSchema, + so: readThreadSchema + }); + }, + "./src/core/chatgpt-desktop/thread-ids.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_thread_id_js__rspack_import_1 = __webpack_require__("./src/core/codex/thread-id.js"); + var _cdp_dom_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/cdp-dom.ts"); + const LOCAL_THREAD_ID_PREFIX = "local:"; + const TEMP_THREAD_ID_PREFIX = null && CDP_TEMP_PREFIX; + const HISTORY_CONTENT_TURN_PREFIX = 'history-content:turn:'; + function isTemporaryDesktopThreadId(threadId) { + return (0, _codex_thread_id_js__rspack_import_1.vr)(threadId) || threadId.startsWith(_codex_thread_id_js__rspack_import_1.hA); + } + function toAppServerThreadId(threadId) { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return null; + try { + return (0, _codex_thread_id_js__rspack_import_1.SX)(threadId); + } catch { + return null; + } + } + function requireAppServerThreadId(threadId) { + const bare = toAppServerThreadId(threadId); + if (!bare) { + throw new Error(`No app-server thread id mapping for ${JSON.stringify(threadId)} ` + `(temporary Desktop rows need CDP; strip a leading "local:" for durable ids)`); + } + return bare; + } + function toDesktopThreadId(threadId) { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return threadId; + if (threadId.startsWith(LOCAL_THREAD_ID_PREFIX)) return threadId; + return `${LOCAL_THREAD_ID_PREFIX}${threadId}`; + } + function threadIdsEquivalent(a, b) { + const left = toAppServerThreadId(a) ?? a; + const right = toAppServerThreadId(b) ?? b; + return left === right; + } + function toDomTurnKey(turnId) { + if (!turnId) return turnId; + if (turnId.startsWith(HISTORY_CONTENT_TURN_PREFIX)) return turnId; + return `${HISTORY_CONTENT_TURN_PREFIX}${turnId}`; + } + function appServerThreadIdCandidates(threadId) { + const bare = toAppServerThreadId(threadId); + return bare ? [ + bare + ] : []; + } + __webpack_require__.d(__webpack_exports__, { + BS: ()=>toDesktopThreadId, + LV: ()=>threadIdsEquivalent, + NM: ()=>requireAppServerThreadId, + Sr: ()=>toDomTurnKey, + nn: ()=>toAppServerThreadId, + vr: ()=>isTemporaryDesktopThreadId + }); + }, + "./src/core/claude-routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var zod__rspack_import_1 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _claude_channel_js__rspack_import_0 = __webpack_require__("./src/core/claude-channel.js"); + const inputSchema = zod__rspack_import_1.Ikc({ + name: zod__rspack_import_1.YjP().regex(/^[a-zA-Z0-9_-]{1,32}$/).describe('Explicit name of the live Claude channel.'), + message: zod__rspack_import_1.YjP().min(1).max(65536), + timeoutMs: zod__rspack_import_1.aig().int().min(1).max(120000).default(60000) + }).strict(); + const resultSchema = zod__rspack_import_1.Ikc({ + delivery: zod__rspack_import_1.k5n([ + 'replied', + 'unknown', + 'rejected' + ]), + requestId: zod__rspack_import_1.YjP().optional(), + reply: zod__rspack_import_1.YjP().optional(), + error: zod__rspack_import_1.YjP().optional(), + exitCode: zod__rspack_import_1.KCZ([ + zod__rspack_import_1.euz(0), + zod__rspack_import_1.euz(1) + ]) + }).strict(); + async function sendOperation(input) { + try { + const result = await (0, _claude_channel_js__rspack_import_0.s)(input); + return { + ...result, + exitCode: result.delivery === 'replied' ? 0 : 1 + }; + } catch (error) { + return { + delivery: 'rejected', + error: error instanceof Error ? error.message : String(error), + exitCode: 1 + }; + } + } + __webpack_require__.d(__webpack_exports__, { + UP: ()=>sendOperation + }, { + FD: resultSchema, + is: inputSchema + }); + }, + "./src/core/codex/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var zod__rspack_import_5 = __webpack_require__("./node_modules/zod/v4/core/core.js"); + var _codex_bridge_js__rspack_import_0 = __webpack_require__("./src/core/codex-bridge.js"); + var _contract_js__rspack_import_1 = __webpack_require__("./src/core/codex/contract.js"); + var _conversation_js__rspack_import_2 = __webpack_require__("./src/core/codex/conversation.js"); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); + const bareId = zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9_.:-]{1,128}$/); + const id = zod__rspack_import_3.YjP().min(1).max(160).transform((value, ctx)=>{ + try { + return (0, _thread_id_js__rspack_import_4.SX)(value); + } catch (error) { + ctx.addIssue({ + code: 'custom', + message: error instanceof Error ? error.message : 'Invalid threadId' + }); + return zod__rspack_import_5.tm; + } + }).pipe(bareId); + const observationFields = { + expectedCwd: zod__rspack_import_3.YjP().min(1).optional(), + threadId: id, + timeoutMs: zod__rspack_import_3.aig().int().min(1).max(7200000).optional() + }; + const sendFields = { + ...observationFields, + correlationId: bareId.optional(), + envelope: zod__rspack_import_3.zMY().optional(), + hop: zod__rspack_import_3.aig().int().min(0).optional(), + model: zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/).optional(), + effort: zod__rspack_import_3.k5n([ + 'none', + 'minimal', + 'low', + 'medium', + 'high', + 'xhigh' + ]).optional(), + replyTo: bareId.optional(), + expectedTurnId: bareId.optional(), + whenBusy: zod__rspack_import_3.k5n([ + 'reject', + 'queue', + 'steer' + ]).default('reject'), + wait: zod__rspack_import_3.zMY().default(false), + maxOutputBytes: zod__rspack_import_3.aig().int().min(1).max(4194304).optional() + }; + const sendSchema = zod__rspack_import_3.Ikc({ + ...sendFields, + message: zod__rspack_import_3.YjP().min(1).max(4194304) + }).strict(); + const waitSchema = zod__rspack_import_3.Ikc({ + ...observationFields, + turnId: bareId, + messageId: bareId.optional(), + maxOutputBytes: zod__rspack_import_3.aig().int().min(1).max(4194304).optional() + }).strict(); + const watchSchema = zod__rspack_import_3.Ikc({ + expectedCwd: zod__rspack_import_3.YjP().min(1).optional(), + threadId: id, + timeoutMs: zod__rspack_import_3.aig().int().min(1).max(600000).optional(), + maxEvents: zod__rspack_import_3.aig().int().min(1).max(500).default(100) + }).strict(); + const threadsSchema = zod__rspack_import_3.Ikc({ + limit: zod__rspack_import_3.aig().int().min(1).max(200).default(20), + cursor: zod__rspack_import_3.YjP().min(1).max(4096).optional() + }).strict(); + const resultSchema = zod__rspack_import_3.Ikc({ + exitCode: zod__rspack_import_3.aig().int().min(0).max(1), + threadId: zod__rspack_import_3.YjP().optional(), + turnId: zod__rspack_import_3.YjP().optional(), + messageId: zod__rspack_import_3.YjP().optional(), + delivery: zod__rspack_import_3.k5n([ + 'accepted', + 'queued', + 'rejected', + 'unknown' + ]).optional(), + execution: zod__rspack_import_3.Ikc({ + state: zod__rspack_import_3.k5n([ + 'completed', + 'failed', + 'interrupted', + 'waiting-for-input', + 'timeout', + 'disconnected', + 'unknown' + ]), + error: zod__rspack_import_3.L5J().optional() + }).optional(), + reply: zod__rspack_import_3.Ikc({ + text: zod__rspack_import_3.YjP().max(4194304), + items: zod__rspack_import_3.YOg(zod__rspack_import_3.Ikc({ + id: zod__rspack_import_3.YjP(), + type: zod__rspack_import_3.euz('agentMessage'), + phase: zod__rspack_import_3.k5n([ + 'final_answer' + ]).nullable(), + text: zod__rspack_import_3.YjP().max(4194304) + })).max(2000), + truncated: zod__rspack_import_3.zMY() + }).optional(), + interactions: zod__rspack_import_3.YOg(zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.L5J())).max(500).optional(), + events: zod__rspack_import_3.YOg(zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.L5J())).max(500).optional(), + reason: zod__rspack_import_3.YjP().optional(), + truncated: zod__rspack_import_3.zMY().optional() + }).passthrough(); + async function sendOperation(input, signal, progress, oneShot = false) { + let envelope; + let receipt; + try { + envelope = (0, _codex_bridge_js__rspack_import_0.nq)(input); + if (oneShot && !input.wait && input.whenBusy !== 'steer') { + return await (0, _codex_bridge_js__rspack_import_0.ZS)(input.threadId, input.message, { + envelope, + whenBusy: input.whenBusy, + expectedCwd: input.expectedCwd, + model: input.model, + effort: input.effort, + signal + }); + } + await progress?.('Opening Codex conversation'); + const conversation = await (0, _conversation_js__rspack_import_2.j)(input.threadId, { + expectedCwd: input.expectedCwd, + signal + }); + try { + receipt = await conversation.send(input.message, { + envelope, + whenBusy: input.whenBusy, + expectedTurnId: input.expectedTurnId, + model: input.model, + effort: input.effort + }); + if (!input.wait || receipt.delivery !== 'accepted' || !receipt.turnId) return receipt; + await progress?.(`Accepted message ${receipt.messageId}; observing turn ${receipt.turnId}`); + const result = await conversation.wait({ + turnId: receipt.turnId, + messageId: receipt.messageId, + timeoutMs: input.timeoutMs, + maxOutputBytes: input.maxOutputBytes, + signal + }); + return { + ...receipt, + execution: result.execution, + reply: result.reply, + interactions: result.interactions, + exitCode: result.execution.state === 'completed' ? 0 : 1 + }; + } finally{ + await conversation.close(); + } + } catch (error) { + if (receipt?.delivery === 'accepted') return { + ...receipt, + execution: { + state: 'unknown', + error: (0, _contract_js__rspack_import_1.DG)(error).error + }, + reply: { + text: '', + items: [], + truncated: true + }, + interactions: [], + exitCode: 1 + }; + return { + ...(0, _contract_js__rspack_import_1.DG)(error), + threadId: input.threadId, + ...envelope ? { + messageId: envelope.messageId, + correlationId: envelope.correlationId, + hop: envelope.hop + } : {} + }; + } + } + async function observeOperation(kind, input, signal, progress) { + try { + await progress?.(`Observing Codex thread ${input.threadId}`); + const conversation = await (0, _conversation_js__rspack_import_2.j)(input.threadId, { + expectedCwd: input.expectedCwd, + signal + }); + try { + if (kind === 'wait') { + const result = await conversation.wait({ + ...input, + signal + }); + return { + ...result, + exitCode: result.execution.state === 'completed' ? 0 : 1 + }; + } + const result = await conversation.watch({ + ...input, + signal + }); + return { + ...result, + exitCode: [ + 'timeout', + 'event-limit' + ].includes(result.reason) ? 0 : 1 + }; + } finally{ + await conversation.close(); + } + } catch (error) { + return { + ...(0, _contract_js__rspack_import_1.DG)(error), + threadId: input.threadId, + ...'turnId' in input ? { + turnId: input.turnId, + messageId: input.messageId + } : {} + }; + } + } + async function threadsOperation(input) { + try { + return { + ...await (0, _codex_bridge_js__rspack_import_0.X$)(input), + exitCode: 0 + }; + } catch (error) { + return (0, _contract_js__rspack_import_1.DG)(error); + } + } + function resultText(result) { + if (result.execution && typeof result.execution === 'object' && 'state' in result.execution) return `Codex turn ${result.turnId}: ${result.execution.state}`; + if (result.delivery === 'queued') return `Queued ${result.queuedSubmissionId} on busy Codex thread ${result.threadId}; message ${result.messageId}`; + if (result.error) return String(result.error); + if (result.delivery === 'accepted') return `Started turn ${result.turnId} (${result.turnStatus}) on Codex thread ${result.threadId}; message ${result.messageId}`; + if (result.delivery) return `Codex delivery ${result.delivery} on thread ${result.threadId}`; + if (result.reason) return `Codex observation: ${result.reason}`; + if (Array.isArray(result.threads)) return `${result.threads.length} Codex threads`; + return String(result.error ?? 'Codex observation complete'); + } + __webpack_require__.d(__webpack_exports__, { + $c: ()=>threadsOperation, + D6: ()=>resultText, + UP: ()=>sendOperation, + gT: ()=>observeOperation + }, { + FD: resultSchema, + P3: threadsSchema, + R9: watchSchema, + Wi: sendSchema, + jS: waitSchema + }); + }, + "./src/core/grok-approval-routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _gateway_js__rspack_import_0 = __webpack_require__("./src/core/gateway.js"); + var _grok_approvals_js__rspack_import_1 = __webpack_require__("./src/core/grok-approvals.js"); + var _gbot_js__rspack_import_2 = __webpack_require__("./src/gbot.ts"); + const listSchema = zod__rspack_import_3.rej({ + target: zod__rspack_import_3.YjP().min(1).max(1024) + }); + const respondSchema = _grok_approvals_js__rspack_import_1.T5; + const resultSchema = zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.Pq9()); + const listOperation = (input)=>(0, _gbot_js__rspack_import_2.yF)(async ()=>(0, _gateway_js__rspack_import_0.MI)(await (0, _gateway_js__rspack_import_0.Vq)(), listSchema.parse(input).target)); + const respondOperation = (input)=>(0, _gbot_js__rspack_import_2.yF)(async ()=>{ + const { target, ...response } = respondSchema.parse(input); + return (0, _gateway_js__rspack_import_0.OR)(await (0, _gateway_js__rspack_import_0.Vq)(), target, response); + }); + __webpack_require__.d(__webpack_exports__, {}, { + FD: resultSchema, + cE: listSchema, + gi: listOperation, + lr: respondOperation, + v1: respondSchema + }); + }, + "./src/core/relay/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _agent_bundle_runtime__rspack_import_4 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/40.js"); + var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _managed_js__rspack_import_0 = __webpack_require__("./src/core/relay/managed.js"); + var _gbot_js__rspack_import_1 = __webpack_require__("./src/gbot.ts"); + var _codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); + const id = zod__rspack_import_3.YjP().min(1).max(128); + const routeFields = { + codexThreadId: id.optional(), + expectedCwd: zod__rspack_import_3.YjP().min(1).max(4096).optional(), + bindingId: zod__rspack_import_3.YjP().min(1).max(512).optional(), + requestId: id.optional() + }; + const grokSendSchema = zod__rspack_import_3.Ikc({ + target: zod__rspack_import_3.YjP().min(1), + message: zod__rspack_import_3.YjP().min(1), + replyMode: zod__rspack_import_3.k5n([ + "auto", + "manual" + ]).optional(), + hop: zod__rspack_import_3.aig().int().min(0).optional(), + correlationId: id.optional(), + ...routeFields + }).strict(); + const bridgeStartSchema = zod__rspack_import_3.Ikc({ + grokTarget: zod__rspack_import_3.YjP().min(1), + codexThreadId: id.optional(), + expectedCwd: zod__rspack_import_3.YjP().min(1).max(4096).optional(), + busyPolicy: zod__rspack_import_3.k5n([ + "steer", + "reject" + ]).optional(), + requestId: id.optional() + }).strict(); + const bridgeStatusSchema = zod__rspack_import_3.Ikc({ + bindingId: zod__rspack_import_3.YjP().min(1).max(512).optional(), + limit: zod__rspack_import_3.aig().int().min(1).max(100).optional() + }).strict(); + const bridgeStopSchema = zod__rspack_import_3.Ikc({ + bindingId: zod__rspack_import_3.YjP().min(1).max(512).optional(), + all: zod__rspack_import_3.zMY().optional(), + worker: zod__rspack_import_3.zMY().optional() + }).strict().refine((x)=>!!x.bindingId || x.all === true || x.worker === true, "Specify bindingId, all or worker"); + const respondSchema = zod__rspack_import_3.Ikc({ + interactionId: zod__rspack_import_3.YjP().min(1).max(512), + generation: id, + threadId: id, + turnId: id, + bindingId: zod__rspack_import_3.YjP().min(1).max(512).optional(), + exchangeId: zod__rspack_import_3.YjP().min(1).max(512).optional(), + decision: zod__rspack_import_3.k5n([ + "accept", + "decline", + "cancel" + ]).optional(), + answersJson: zod__rspack_import_3.YjP().min(2).max(65536).optional() + }).strict().refine((x)=>!!x.bindingId || !!x.exchangeId, "Binding or exchange scope required").refine((x)=>!!x.decision !== !!x.answersJson, "Supply decision or answersJson"); + const answersSchema = zod__rspack_import_3.g1P(zod__rspack_import_3.YjP().min(1).max(128), zod__rspack_import_3.Ikc({ + answers: zod__rspack_import_3.YOg(zod__rspack_import_3.YjP().max(8192)).max(20) + }).strict()); + const relayResultSchema = zod__rspack_import_3.g1P(zod__rspack_import_3.YjP(), zod__rspack_import_3.Pq9()); + function nativeCodexThread(context) { + return context.host.state === "available" && (0, _agent_bundle_runtime__rspack_import_4.bg)(context.host.value.name) === "codex" && context.lineage.state === "available" && context.lineage.source === "native" && context.lineage.value.resolution === "native" ? context.lineage.value.conversation : undefined; + } + function options(context) { + return { + pluginRoot: context?.plugin.state === "available" ? context.plugin.value.root : undefined, + signal: context?.signal + }; + } + function destination(input, context) { + const codexThreadId = input.codexThreadId ?? (context ? nativeCodexThread(context) : undefined); + const workspace = context?.workspace; + const expectedCwd = input.expectedCwd ?? (workspace?.state === "available" && workspace.source === "native" ? workspace.value.root : undefined); + return { + codexThreadId, + expectedCwd + }; + } + async function grokSendOperation(input, context) { + const route = destination(input, input.bindingId ? undefined : context); + if (input.replyMode !== "manual" && (input.bindingId || route.codexThreadId)) return (0, _gbot_js__rspack_import_1.yF)(()=>(0, _managed_js__rspack_import_0.Bb)("sendToGrok", { + grokTarget: input.target, + message: input.message, + hop: input.hop, + correlationId: input.correlationId, + ...route, + ...input.bindingId ? { + bindingId: input.bindingId + } : {}, + ...input.requestId ? { + requestId: input.requestId + } : {} + }, options(context))); + if (input.replyMode === "auto") throw Error("Automatic reply delivery requires a native Codex source, codexThreadId or bindingId"); + const message = input.hop !== undefined || input.correlationId !== undefined ? (0, _codex_bridge_js__rspack_import_2.Mr)(input.message, (0, _codex_bridge_js__rspack_import_2.nq)(input)) : input.message; + const sent = await (0, _gbot_js__rspack_import_1.yF)(async ()=>(0, _gbot_js__rspack_import_1.LV)(await (0, _gbot_js__rspack_import_1.Vq)(), input.target, message)); + return { + result: sent.result, + target: (0, _gbot_js__rspack_import_1.rW)(sent.target), + delivery: sent.delivery === "accepted" ? "accepted" : "unknown", + ...sent.delivery === "accepted" && typeof sent.messageId === "string" ? { + messageId: sent.messageId + } : {}, + replyRoute: { + mode: "manual", + reason: input.replyMode === "manual" ? "requested" : "source-unavailable" + } + }; + } + async function bridgeOperation(method, input, context) { + let routed = method === "startBinding" ? { + ...input, + ...destination(input, context) + } : input; + if (method === "respond") { + const { decision, answersJson, ...scope } = respondSchema.parse(input); + routed = { + ...scope, + result: decision ? { + decision + } : { + answers: answersSchema.parse(JSON.parse(answersJson)) + } + }; + } + if (method === "startBinding" && !routed.codexThreadId) throw Error("Specify codexThreadId when native Codex source identity is unavailable"); + return (0, _gbot_js__rspack_import_1.yF)(()=>(0, _managed_js__rspack_import_0.Bb)(method, routed, options(context))); + } + function assertManagedSendOptions(input) { + if (input.expectedTurnId !== undefined) throw Error("Managed relay does not support expectedTurnId; omit it to use managed guarded steering, or use a plain Codex send for an explicit turn guard"); + if (input.replyTo !== undefined || input.envelope === true) throw Error("Managed relay does not support legacy replyTo/envelope options; omit them or use a plain send. Use hop/correlationId for explicit managed chains"); + } + async function codexReturnOperation(input, context) { + assertManagedSendOptions(input); + if (input.whenBusy === "queue") throw Error("Managed relay supports steer or reject, not experimental queue"); + return (0, _gbot_js__rspack_import_1.yF)(()=>(0, _managed_js__rspack_import_0.Bb)("sendToCodex", { + grokTarget: input.replyToGrok, + codexThreadId: input.threadId, + expectedCwd: input.expectedCwd, + bindingId: input.bindingId, + message: input.message, + requestId: input.requestId, + busyPolicy: input.whenBusy, + hop: input.hop, + correlationId: input.correlationId, + model: input.model, + effort: input.effort + }, options(context))); + } + __webpack_require__.d(__webpack_exports__, { + bv: ()=>grokSendOperation, + lt: ()=>codexReturnOperation, + n1: ()=>bridgeOperation + }, { + Ax: relayResultSchema, + bw: grokSendSchema, + d5: bridgeStopSchema, + np: bridgeStatusSchema, + v1: respondSchema, + xD: bridgeStartSchema + }); + }, + "./src/gbot.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _core_gateway_js__rspack_import_0 = __webpack_require__("./src/core/gateway.js"); + var _core_transcript_js__rspack_import_2 = __webpack_require__("./src/core/transcript.js"); + var _core_url_policy_js__rspack_import_1 = __webpack_require__("./src/core/url-policy.js"); + const withRedactedErrors = async (run)=>{ + try { + return await run(); + } catch (error) { + const wrapped = new Error((0, _core_url_policy_js__rspack_import_1.fp)(error instanceof Error ? error.message : String(error))); + if (error instanceof Error) Object.assign(wrapped, error, { + name: error.name + }); + throw wrapped; + } + }; + const targetSchema = zod__rspack_import_3.Ikc({ + id: zod__rspack_import_3.YjP(), + kind: zod__rspack_import_3.k5n([ + 'bot', + 'group' + ]), + name: zod__rspack_import_3.YjP() + }); + const summarizeTarget = (record)=>({ + id: record.id, + kind: record.isGroup ? 'group' : 'bot', + name: record.name + }); + const entrySchema = zod__rspack_import_3.Ikc({ + id: zod__rspack_import_3.YjP(), + kind: zod__rspack_import_3.YjP(), + role: zod__rspack_import_3.YjP().optional(), + text: zod__rspack_import_3.YjP(), + truncated: zod__rspack_import_3.zMY(), + fullLength: zod__rspack_import_3.aig().int().min(0), + timestampMs: zod__rspack_import_3.aig().optional() + }); + const ENTRY_FULL_MAX = 20000; + const TRANSCRIPT_TOTAL_MAX = 200000; + const ENTRY_META_MAX = 200; + const RECEIPT_CURSOR_MAX = 1024; + const entryFields = zod__rspack_import_3.Ikc({ + id: zod__rspack_import_3.YjP().default(''), + kind: zod__rspack_import_3.YjP().default('message'), + role: zod__rspack_import_3.YjP().optional(), + timestampMs: zod__rspack_import_3.aig().optional() + }); + const capMeta = (value)=>value.length > ENTRY_META_MAX ? `${value.slice(0, ENTRY_META_MAX)}…` : value; + const threadEntry = (raw)=>{ + const fields = entryFields.safeParse(raw); + const full = (0, _core_transcript_js__rspack_import_2.u0)(raw); + const truncated = full.length > ENTRY_FULL_MAX; + const text = truncated ? full.slice(0, ENTRY_FULL_MAX) : full; + if (!fields.success) { + return { + id: '', + kind: 'unknown', + text, + truncated, + fullLength: full.length + }; + } + const { id, kind, role, timestampMs } = fields.data; + return { + id: capMeta(id), + kind: capMeta(kind), + ...role === undefined ? {} : { + role: capMeta(role) + }, + text, + truncated, + fullLength: full.length, + ...timestampMs === undefined ? {} : { + timestampMs + } + }; + }; + const metaLength = (entry)=>entry.id.length + entry.kind.length + (entry.role?.length ?? 0); + const transcriptEntries = (transcript)=>{ + const rows = (0, _core_transcript_js__rspack_import_2.xw)(transcript); + let remaining = TRANSCRIPT_TOTAL_MAX; + return rows.map((raw)=>{ + const entry = threadEntry(raw); + const allowText = Math.max(0, Math.min(entry.text.length, remaining - metaLength(entry))); + if (allowText < entry.text.length) { + entry.text = allowText <= 0 ? '' : entry.text.slice(0, allowText); + entry.truncated = entry.fullLength > entry.text.length; + } + remaining = Math.max(0, remaining - metaLength(entry) - entry.text.length); + return entry; + }); + }; + __webpack_require__.d(__webpack_exports__, { + LV: ()=>_core_gateway_js__rspack_import_0.LV, + N3: ()=>_core_transcript_js__rspack_import_2.N3, + Vq: ()=>_core_gateway_js__rspack_import_0.Vq, + bA: ()=>_core_gateway_js__rspack_import_0.bA + }, { + Oo: entrySchema, + rW: summarizeTarget, + xw: transcriptEntries, + yF: withRedactedErrors + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop list hosts', + description: 'Discover local, remote-control environment ids, managed SSH hosts, and modelProvider ids. hostName is null when Desktop has no real display name. Use hostId or label in list/search filters; remote thread ids must be read on the owning host. warnings report provider discovery failures.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.Rf, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + } + }, + required: [] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.Bm)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.Rf + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop list threads', + description: 'List one sorted Desktop inventory without duplicate rows. Pass nextCursor as cursor with unchanged filters until null; pages fill limit unless the MCP byte budget binds. A changed/expired inventory cursor requires restarting. Display titles are capped at 200 characters with titleTruncated:true. Filter by dynamic hostId/hostName, modelProvider, or Desktop project label/projectId; projectRootPath identifies the resolved workspace. Rows expose local:, location, hostId, and provider; remote rows require their owning host for reading or sending. Backend and warnings identify partial sources.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FC, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + limit: { + type: 'number', + default: 50, + description: 'Maximum threads per page.' + }, + cursor: { + type: 'string', + description: 'Opaque nextCursor from the prior page; keep filters unchanged.' + }, + host: { + type: 'string', + description: 'Any string filter for machine/location. Reserved: "all" (default), "local". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.' + }, + modelProvider: { + type: 'string', + description: 'Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.' + }, + project: { + type: 'string', + description: 'Match a Desktop project label or app-server projectId. Dynamic string, not an enum.' + }, + groupBy: { + type: 'string', + enum: [ + 'host' + ], + description: 'When "host", also return groups[] keyed by host (grouping mode, not a host id).' + } + }, + required: [] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.tJ)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.FC + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop open thread', + description: 'Navigate Desktop to an existing local: or bare conversation id, even when the row is absent from the sidebar. Archived routes return archived:true without unarchiving; read them with chatgpt_desktop_read_thread. A remote-control id is owned by its hostId and cannot be opened on this Mac. CDP_UNREACHABLE means Desktop must be reachable locally.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.OR, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number', + description: 'Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).' + }, + threadId: { + type: 'string', + description: 'local: or bare durable id. Temporary local:client-new-thread:* works only while selected; remote ids require their owning host.' + }, + openTimeoutMs: { + type: 'number', + description: 'Maximum time to wait for the requested conversation (1-600000 ms).' + } + }, + required: [ + 'threadId' + ] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.Iu)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.OR + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop read thread', + description: 'Read a local:, bare durable id, or selected temporary local:client-new-thread:* id. Pass nextCursor back as cursor with the same threadId/full mode until complete:true. Pages stop at a byte budget; oversized turns use ordered continuation fragments with stable turnKey and field offsets, without dropping text. Archived threads remain readable. Warnings explain partial pages or app-server errors. REMOTE_THREAD_NOT_LOADED includes the owning hostId: call on that host.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.so, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + render: { + maxElapsedMs: 660000 + }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + threadId: { + type: 'string', + description: 'local:, bare durable id, or selected local:client-new-thread:* temporary id. Remote ids return REMOTE_THREAD_NOT_LOADED with hostId.' + }, + limit: { + type: 'number' + }, + cursor: { + type: 'string', + description: 'Opaque nextCursor from the prior read page. Keep threadId and full unchanged.' + }, + full: { + type: 'boolean', + description: 'When true, page from the start through the complete history (up to 2000 turns by default); otherwise return the most recent 100 turns by default. Explicit limit bounds either mode.' + }, + openTimeoutMs: { + type: 'number', + description: 'Max wait for Loading task… to clear after opening (default 90000).' + } + }, + required: [ + 'threadId' + ] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.UK)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.so + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop search threads', + description: 'Search full Desktop thread metadata by case-insensitive substring. Display titles are capped at 200 characters with titleTruncated:true. Pass nextCursor as cursor with unchanged query and filters to retrieve all matches. Dynamic hostId/hostName, modelProvider, and project filters apply. Remote results identify hostId and cannot be read on this Mac.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.nl, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + query: { + type: 'string', + description: 'Case-insensitive substring over thread metadata.' + }, + limit: { + type: 'number', + default: 50, + description: 'Maximum matches.' + }, + cursor: { + type: 'string', + description: 'Opaque nextCursor from the previous search page; keep query and filters unchanged.' + }, + host: { + type: 'string', + description: 'Any string filter for machine/location. Reserved: "all" (default), "local". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.' + }, + modelProvider: { + type: 'string', + description: 'Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.' + }, + project: { + type: 'string', + description: 'Match a Desktop project label or app-server projectId.' + }, + groupBy: { + type: 'string', + enum: [ + 'host' + ], + description: 'When "host", also return groups[] keyed by host (grouping mode, not a host id).' + } + }, + required: [ + 'query' + ] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.zg)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.nl + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop send', + description: 'Send once through local Desktop CDP. With threadId, accept local:, bare durable id, or currently selected local:client-new-thread:*; remote ids must be sent on their hostId. Omit threadId and project for a new chat outside projects; omit threadId and set project for a new chat inside it. The empty view and project scope are checked before typing. New-thread receipts include temporaryThreadId when seen and return durable local: once resolved. ARCHIVED_THREAD rejects without sending; unarchive:true explicitly unarchives that existing thread first. PROJECT_UNAVAILABLE identifies a disabled project action or missing configured root; NEW_CHAT_NAVIGATION_FAILED reports an unchanged view promptly. CDP_UNREACHABLE and COMPOSER_HAS_DRAFT require operator inspection, not blind retry.', + annotations: { + readOnlyHint: false + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.Wi, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + render: { + maxElapsedMs: 180000 + }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + threadId: { + type: 'string', + description: 'local:, bare durable id, or selected local:client-new-thread:*; omit for a new chat.' + }, + text: { + type: 'string' + }, + project: { + type: 'string', + description: 'With threadId omitted, create the new chat inside this project. Omit for a new chat outside projects.' + }, + unarchive: { + type: 'boolean', + default: false, + description: 'Existing threadId only: explicitly call thread/unarchive after ARCHIVED_THREAD, then retry the send once. Never implicit.' + }, + openTimeoutMs: { + type: 'number' + } + }, + required: [ + 'text' + ] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.UP)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.Wi + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop status', + description: 'Check local ChatGPT Desktop CDP before send, wait, or open. reachable:false and exitCode:1 mean CDP is down even if appServerFallback.reachable is true; list/search/read may still work through app-server. There is no remote CDP transport.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.aW, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number', + description: 'Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).' + } + }, + required: [] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.Uj)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.aW }, { - Ax: relayResultSchema, - bw: grokSendSchema, - d5: bridgeStopSchema, - np: bridgeStatusSchema, - v1: respondSchema, - xD: bridgeStartSchema + "default": __rspack_default_export }); }, - "./src/gbot.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { - var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); - var _core_gateway_js__rspack_import_0 = __webpack_require__("./src/core/gateway.js"); - var _core_transcript_js__rspack_import_2 = __webpack_require__("./src/core/transcript.js"); - var _core_url_policy_js__rspack_import_1 = __webpack_require__("./src/core/url-policy.js"); - const withRedactedErrors = async (run)=>{ - try { - return await run(); - } catch (error) { - const wrapped = new Error((0, _core_url_policy_js__rspack_import_1.fp)(error instanceof Error ? error.message : String(error))); - if (error instanceof Error) Object.assign(wrapped, error, { - name: error.name - }); - throw wrapped; + "./src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.react-server.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop wait for reply', + description: 'After chatgpt_desktop_send, wait for the selected Desktop reply over CDP. Accept the returned durable local: or the temporary local:client-new-thread:* while selected; omit threadId only when staying on the same selected chat. Returns reply, durable threadId and conversationId. timeout is not proof the send failed; CDP_UNREACHABLE requires Desktop recovery. Remote ids must be handled on their hostId.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.Fn, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + render: { + maxElapsedMs: 660000 + }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + threadId: { + type: 'string', + description: 'Durable local:, bare id, or selected local:client-new-thread:* from send.' + }, + timeoutMs: { + type: 'number', + default: 120000 + } + }, + required: [] } - }; - const targetSchema = zod__rspack_import_3.Ikc({ - id: zod__rspack_import_3.YjP(), - kind: zod__rspack_import_3.k5n([ - 'bot', - 'group' - ]), - name: zod__rspack_import_3.YjP() - }); - const summarizeTarget = (record)=>({ - id: record.id, - kind: record.isGroup ? 'group' : 'bot', - name: record.name + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.Zg)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) }); - const entrySchema = zod__rspack_import_3.Ikc({ - id: zod__rspack_import_3.YjP(), - kind: zod__rspack_import_3.YjP(), - role: zod__rspack_import_3.YjP().optional(), - text: zod__rspack_import_3.YjP(), - truncated: zod__rspack_import_3.zMY(), - fullLength: zod__rspack_import_3.aig().int().min(0), - timestampMs: zod__rspack_import_3.aig().optional() - }); - const ENTRY_FULL_MAX = 20000; - const TRANSCRIPT_TOTAL_MAX = 200000; - const ENTRY_META_MAX = 200; - const RECEIPT_CURSOR_MAX = 1024; - const entryFields = zod__rspack_import_3.Ikc({ - id: zod__rspack_import_3.YjP().default(''), - kind: zod__rspack_import_3.YjP().default('message'), - role: zod__rspack_import_3.YjP().optional(), - timestampMs: zod__rspack_import_3.aig().optional() }); - const capMeta = (value)=>value.length > ENTRY_META_MAX ? `${value.slice(0, ENTRY_META_MAX)}…` : value; - const threadEntry = (raw)=>{ - const fields = entryFields.safeParse(raw); - const full = (0, _core_transcript_js__rspack_import_2.u0)(raw); - const truncated = full.length > ENTRY_FULL_MAX; - const text = truncated ? full.slice(0, ENTRY_FULL_MAX) : full; - if (!fields.success) { - return { - id: '', - kind: 'unknown', - text, - truncated, - fullLength: full.length - }; - } - const { id, kind, role, timestampMs } = fields.data; - return { - id: capMeta(id), - kind: capMeta(kind), - ...role === undefined ? {} : { - role: capMeta(role) - }, - text, - truncated, - fullLength: full.length, - ...timestampMs === undefined ? {} : { - timestampMs - } - }; - }; - const metaLength = (entry)=>entry.id.length + entry.kind.length + (entry.role?.length ?? 0); - const transcriptEntries = (transcript)=>{ - const rows = (0, _core_transcript_js__rspack_import_2.xw)(transcript); - let remaining = TRANSCRIPT_TOTAL_MAX; - return rows.map((raw)=>{ - const entry = threadEntry(raw); - const allowText = Math.max(0, Math.min(entry.text.length, remaining - metaLength(entry))); - if (allowText < entry.text.length) { - entry.text = allowText <= 0 ? '' : entry.text.slice(0, allowText); - entry.truncated = entry.fullLength > entry.text.length; - } - remaining = Math.max(0, remaining - metaLength(entry) - entry.text.length); - return entry; - }); - }; __webpack_require__.d(__webpack_exports__, { - LV: ()=>_core_gateway_js__rspack_import_0.LV, - N3: ()=>_core_transcript_js__rspack_import_2.N3, - Vq: ()=>_core_gateway_js__rspack_import_0.Vq, - bA: ()=>_core_gateway_js__rspack_import_0.bA + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.Fn }, { - Oo: entrySchema, - rW: summarizeTarget, - xw: transcriptEntries, - yF: withRedactedErrors + "default": __rspack_default_export }); }, "./src/mcp/grok-bot/tools/claude_send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { @@ -8967,10 +13247,11 @@ var __webpack_modules__ = { properties: { "limit": { "type": "number", - "description": "Maximum threads in this page: 1-200." + "description": "Maximum threads in this page: 1-200. Pass modelProviders:[] internally so every provider is included; use cursor/nextCursor to page." }, "cursor": { - "type": "string" + "type": "string", + "description": "Opaque nextCursor from a previous page." } }, required: [] @@ -14082,11 +18363,12 @@ var __webpack_modules__ = { GT: ()=>$ZodAsyncError, cV: ()=>$ZodEncodeError, cr: ()=>globalConfig, + tm: ()=>NEVER, xI: ()=>$constructor }); var _util_js__rspack_import_0 = __webpack_require__("./node_modules/zod/v4/core/util.js"); var _a; - const NEVER = null && Object.freeze({ + const NEVER = Object.freeze({ status: "aborted" }); const _zodDesc = { @@ -20570,8 +24852,10 @@ var __webpack_modules__ = { var node_child_process__rspack_import_5 = __webpack_require__("node:child_process"); var _package_json__rspack_import_6 = __webpack_require__("./package.json"); var _codex_contract_js__rspack_import_7 = __webpack_require__("./src/core/codex/contract.js"); - var _desktop_shim_js__rspack_import_8 = __webpack_require__("./src/core/desktop-shim.js"); - var _user_machine_guidance_js__rspack_import_9 = __webpack_require__("./src/core/user-machine-guidance.js"); + var _codex_remote_control_js__rspack_import_8 = __webpack_require__("./src/core/codex/remote-control.js"); + var _codex_thread_id_js__rspack_import_11 = __webpack_require__("./src/core/codex/thread-id.js"); + var _desktop_shim_js__rspack_import_9 = __webpack_require__("./src/core/desktop-shim.js"); + var _user_machine_guidance_js__rspack_import_10 = __webpack_require__("./src/core/user-machine-guidance.js"); const PINNED_CODEX_VERSION = "0.158.0"; const UPSTREAM_DESKTOP_ISSUES = [ "https://github.com/openai/codex/issues/41014", @@ -20579,8 +24863,9 @@ var __webpack_modules__ = { ]; const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; const WS_MAX_HEADER_BYTES = 16 * 1024; - const WS_MAX_MESSAGE_BYTES = 4 * 1024 * 1024; - const WS_MAX_BUFFER_BYTES = 8 * 1024 * 1024; + const WS_MAX_MESSAGE_BYTES = 16 * 1024 * 1024; + const WS_MAX_BUFFER_BYTES = 32 * 1024 * 1024; + const CODEX_SEND_MAX_BYTES = 4 * 1024 * 1024; const WS_MAX_WRITE_BYTES = 8 * 1024 * 1024; const CODEX_MAX_REQUESTS = 128; const CODEX_MAX_LISTENERS = 128; @@ -20608,11 +24893,11 @@ var __webpack_modules__ = { function boxUnreachableMessage(path) { return [ "No Codex app-server control socket at " + path + ".", - _user_machine_guidance_js__rspack_import_9.M + _user_machine_guidance_js__rspack_import_10.M ].join("\n"); } function unreachableMessage(path, desktopAttached = "unknown", env = process.env) { - if ((0, _user_machine_guidance_js__rspack_import_9.R)({ + if ((0, _user_machine_guidance_js__rspack_import_10.R)({ path, env, home: env.HOME || (0, node_os__rspack_import_3.homedir)() @@ -21319,7 +25604,7 @@ var __webpack_modules__ = { } const CODEX_VERSION_PROBE_TIMEOUT_MS = 3000; function probeLocalCodexVersion(timeoutMs = CODEX_VERSION_PROBE_TIMEOUT_MS) { - const out = spawnSync("codex", [ + const out = (0, node_child_process__rspack_import_5.spawnSync)("codex", [ "--version" ], { encoding: "utf8", @@ -21352,7 +25637,7 @@ var __webpack_modules__ = { function listDesktopProcesses(timeoutMs = DESKTOP_PROCESS_LIST_TIMEOUT_MS) { if (process.platform === "win32") return ""; try { - const out = spawnSync("ps", [ + const out = (0, node_child_process__rspack_import_5.spawnSync)("ps", [ "-eo", "args" ], { @@ -21380,7 +25665,7 @@ var __webpack_modules__ = { let desktopShimConfigured = Boolean(shim && shim.installed && shim.wrapperPointsAtShim); if (shim === undefined) { try { - const live = desktopShimStatus({ + const live = (0, _desktop_shim_js__rspack_import_9.em)({ env }); desktopShimConfigured = Boolean(live.installed && live.wrapperPointsAtShim); @@ -21407,14 +25692,14 @@ var __webpack_modules__ = { } catch (err) { if (err instanceof CodexRouteError) { const message = err.mode === "socket-absent" ? unreachableMessage(path, desktopAttached, env) : err.message; - return withStatusExitCode({ + return (0, _codex_contract_js__rspack_import_7.AQ)({ ...base, reachable: false, mode: err.mode, message }); } - if (err instanceof CodexProtocolError) return withStatusExitCode({ + if (err instanceof CodexProtocolError) return (0, _codex_contract_js__rspack_import_7.AQ)({ ...base, reachable: true, mode: err.mode, @@ -21424,7 +25709,7 @@ var __webpack_modules__ = { } session.client.close(); const daemonVersion = appServerVersion(session.init); - return withStatusExitCode({ + return (0, _codex_contract_js__rspack_import_7.AQ)({ ...base, reachable: true, mode: "daemon", @@ -21451,6 +25736,13 @@ var __webpack_modules__ = { if (value == null) return null; return typeof value === "string" ? singleLine(value) : value; } + function summarizeSection(section) { + if (!isObject(section) || typeof section.id !== "string" || !section.id) return null; + return { + id: singleLine(section.id), + name: lineField(section.name) + }; + } function summarizeThread(t) { if (!isObject(t) || typeof t.id !== "string" || !t.id) throw new CodexProtocolError("thread/list", "entry without a string `id`"); const type = isObject(t.status) && typeof t.status.type === "string" ? t.status.type : "unknown"; @@ -21462,43 +25754,72 @@ var __webpack_modules__ = { preview: typeof t.preview === "string" ? stripTerminalControls(t.preview) : "", cwd: lineField(t.cwd), source: sourceField(t.source), - updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null + createdAt: typeof t.createdAt === "number" ? t.createdAt : null, + updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null, + section: summarizeSection(t.section), + projectId: lineField(t.projectId), + modelProvider: lineField(t.modelProvider), + model: lineField(t.model), + originator: lineField(t.originator) }; } const THREAD_LIST_MAX_LIMIT = 200; - async function listCodexThreads({ limit = 20, cursor, env = process.env } = {}) { + async function listCodexThreads({ limit = 20, cursor, modelProviders = [], env = process.env } = {}) { if (!Number.isInteger(limit) || limit < 1 || limit > THREAD_LIST_MAX_LIMIT) { throw new RangeError("--limit must be an integer 1-" + THREAD_LIST_MAX_LIMIT); } if (cursor !== undefined && (typeof cursor !== "string" || !cursor)) throw new RangeError("--cursor must be a non-empty string"); + if (!Array.isArray(modelProviders) || modelProviders.some((p)=>typeof p !== "string")) { + throw new RangeError("modelProviders must be an array of strings (empty = all providers)"); + } const { client } = await openSession(env); try { - const params = { + const baseParams = { limit, - useStateDbOnly: true, + modelProviders, ...cursor !== undefined ? { cursor } : {} }; let out; + let usedStateDbOnly = true; try { - out = await client.request("thread/list", params); + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: true + }); } catch (err) { throw transportError(err); } if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + if (out.data.length === 0 && cursor === undefined && (out.nextCursor == null || out.nextCursor === "")) { + try { + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: false + }); + usedStateDbOnly = false; + } catch (err) { + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + } if (out.nextCursor != null && typeof out.nextCursor !== "string") throw new CodexProtocolError("thread/list", "`nextCursor` is not a string"); const threads = out.data.map(summarizeThread); return { threads, nextCursor: out.nextCursor ?? null, - limit + limit, + useStateDbOnly: usedStateDbOnly }; } finally{ client.close(); } } - function explainSendError(err, threadId) { + function explainSendError(err, threadId, env = process.env) { + if (err instanceof _codex_remote_control_js__rspack_import_8.Rg) return err; + const mapped = (0, _codex_remote_control_js__rspack_import_8.R3)(err, threadId, env); + if (mapped instanceof _codex_remote_control_js__rspack_import_8.Rg) return mapped; if (!(err instanceof CodexRpcError)) return err; const msg = String(err.rpc && err.rpc.message || ""); if (/no rollout found|thread not found/i.test(msg)) { @@ -21573,12 +25894,19 @@ var __webpack_modules__ = { function assertThreadAllowed(threadId, env = process.env) { const raw = env.GROK_BOT_CODEX_THREADS; if (raw == null || raw.trim() === "") return; - const allowed = raw.split(",").map((s)=>s.trim()).filter(Boolean); - if (!allowed.includes(threadId)) { - throw new CodexSendError("Codex thread " + threadId + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { + const bare = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + const allowed = raw.split(",").map((s)=>{ + try { + return (0, _codex_thread_id_js__rspack_import_11.SX)(s.trim()); + } catch { + return s.trim(); + } + }).filter(Boolean); + if (!allowed.includes(bare)) { + throw new CodexSendError("Codex thread " + bare + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { delivery: "rejected", reason: "route-not-allowed", - threadId + threadId: bare }); } } @@ -21648,6 +25976,7 @@ var __webpack_modules__ = { } async function listCodexQueue(threadId, { env = process.env, limit = 50, cursor } = {}) { requireExperimental(env, "gbot codex queue"); + threadId = normalizeCodexThreadId(threadId); const { client } = await openSession(env, { experimental: true }); @@ -21678,6 +26007,64 @@ var __webpack_modules__ = { client.close(); } } + async function readCodexThread(threadId, { limit = 100, env = process.env } = {}) { + if (!Number.isInteger(limit) || limit < 1 || limit > 2000) { + throw new RangeError("limit must be an integer 1-2000"); + } + const bare = normalizeCodexThreadId(threadId); + const { client } = await openSession(env); + try { + let meta; + try { + meta = await client.request("thread/read", { + threadId: bare + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + const turns = []; + let cursor; + const seen = new Set(); + for(let page = 0; page < 40 && turns.length < limit; page++){ + let out; + try { + out = await client.request("thread/turns/list", { + threadId: bare, + limit: Math.min(100, limit - turns.length), + itemsView: "full", + sortDirection: "desc", + ...cursor !== undefined ? { + cursor + } : {} + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) { + throw new CodexProtocolError("thread/turns/list", "missing `data` array"); + } + for (const row of out.data)turns.push(row); + cursor = out.nextCursor ?? null; + if (cursor === null) break; + if (typeof cursor !== "string" || !cursor || seen.has(cursor)) { + throw new CodexProtocolError("thread/turns/list", "invalid or repeated cursor"); + } + seen.add(cursor); + } + return { + threadId: bare, + thread: isObject(meta) && isObject(meta.thread) ? meta.thread : null, + turns: turns.slice(0, limit), + nextCursor: cursor ?? null + }; + } finally{ + client.close(); + } + } async function sendToCodexThread(threadId, text, { env = process.env, envelope = buildEnvelope({ env }), whenBusy = "reject", session, expectedTurnId, expectedCwd, model, effort, signal } = {}) { @@ -21723,8 +26110,8 @@ var __webpack_modules__ = { ] : [] ].includes(whenBusy)) throw new RangeError("--when-busy must be reject, queue, or persistent steer"); if (whenBusy === "steer" && (typeof expectedTurnId !== "string" || !ID_PATTERN.test(expectedTurnId))) throw new RangeError("steer requires expectedTurnId"); - if (typeof threadId !== "string" || !ID_PATTERN.test(threadId)) throw new RangeError("Invalid threadId"); - if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > WS_MAX_MESSAGE_BYTES) throw new RangeError("Message must contain text within 4 MiB"); + threadId = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > CODEX_SEND_MAX_BYTES) throw new RangeError("Message must contain text within 4 MiB"); if (!envelope || typeof envelope.messageId !== "string" || !ID_PATTERN.test(envelope.messageId)) throw new RangeError("Invalid envelope messageId"); const validated = buildEnvelope({ correlationId: envelope.correlationId, @@ -21772,8 +26159,10 @@ var __webpack_modules__ = { }); } catch (err) { assertNotCancelled(); - if (err instanceof CodexSendError) throw err; - throw new CodexSendError(explainSendError(err, threadId).message, { + if (err instanceof CodexSendError || err instanceof _codex_remote_control_js__rspack_import_8.Rg) throw err; + const explained = explainSendError(err, threadId, env); + if (explained instanceof _codex_remote_control_js__rspack_import_8.Rg) throw explained; + throw new CodexSendError(explained.message, { delivery: err instanceof CodexRpcError ? "rejected" : err && err.delivery || "unknown", 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", threadId, @@ -21963,6 +26352,7 @@ var __webpack_modules__ = { } __webpack_require__.d(__webpack_exports__, { $3: ()=>assertThreadAllowed, + MU: ()=>codexStatus, Mr: ()=>withEnvelopeHeader, QC: ()=>startCodexThread, X$: ()=>listCodexThreads, @@ -21978,7 +26368,7 @@ var __webpack_modules__ = { function statusExitCode(status) { return status.reachable && status.mode === "daemon" ? 0 : 1; } - function withStatusExitCode1(status) { + function withStatusExitCode(status) { return { ...status, exitCode: statusExitCode(status) @@ -22007,7 +26397,11 @@ var __webpack_modules__ = { "messageId", "correlationId", "refused", - "hop" + "hop", + "hostId", + "hostName", + "hint", + "code" ]){ if (error[key] !== undefined) out[key] = error[key]; } @@ -22034,6 +26428,7 @@ var __webpack_modules__ = { }; } __webpack_require__.d(__webpack_exports__, { + AQ: ()=>withStatusExitCode, DG: ()=>outcomeFromError, Dz: ()=>outcomeFromReceipt }); @@ -22042,9 +26437,12 @@ var __webpack_modules__ = { var _contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); var node_fs__rspack_import_1 = __webpack_require__("node:fs"); var _codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); + var _remote_control_js__rspack_import_3 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); const ID = /^[A-Za-z0-9_.:-]{1,128}$/; const BUDGET = 4 * 1024 * 1024; function conversationId(value, name = 'threadId') { + if (name === 'threadId') return (0, _thread_id_js__rspack_import_4.SX)(value); if (typeof value !== 'string' || !ID.test(value)) throw new RangeError(`${name} must be 1-128 identifier characters`); return value; } @@ -22169,7 +26567,8 @@ var __webpack_modules__ = { if (expected !== undefined && (typeof cwd !== 'string' || (0, node_fs__rspack_import_1.realpathSync)(cwd) !== expected)) throw new Error('Codex thread cwd does not match expectedCwd'); } catch (error) { await close(); - throw error; + if (error instanceof _remote_control_js__rspack_import_3.Rg) throw error; + throw (0, _remote_control_js__rspack_import_3.R3)(error, threadId, env); } async function wait({ turnId, messageId, afterMessageId, timeoutMs = 120000, signal: waitSignal, maxOutputBytes = 1048576 }) { conversationId(turnId, 'turnId'); @@ -22516,10 +26915,273 @@ var __webpack_modules__ = { }; } __webpack_require__.d(__webpack_exports__, { - j: ()=>openCodexConversation + j: ()=>openCodexConversation + }); + }, + "./src/core/codex/remote-control.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var node_os__rspack_import_1 = __webpack_require__("node:os"); + var node_path__rspack_import_2 = __webpack_require__("node:path"); + var _thread_id_js__rspack_import_3 = __webpack_require__("./src/core/codex/thread-id.js"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + class RemoteThreadNotLoadedError1 extends Error { + constructor(threadId, hostId = null, hostName = null){ + const hostLabel = hostName ? `${JSON.stringify(hostName)} (${JSON.stringify(hostId)})` : hostId ? JSON.stringify(hostId) : null; + const owner = hostLabel ? `owned by remote-control host ${hostLabel}` : 'owned by a remote-control host (hostId unknown; check ~/.codex/.codex-global-state.json)'; + const hint = hostId ? `Read this thread via host ${JSON.stringify(hostId)}'s app-server` + (hostName ? ` (${hostName})` : '') + '. SSH remoting is not implemented in this adapter.' : "Read this thread via the owning host's app-server. SSH remoting is not implemented in this adapter."; + super(`Codex thread ${JSON.stringify(threadId)} is not loaded on the local app-server; ${owner}. ${hint}`); + this.name = 'RemoteThreadNotLoadedError'; + this.code = 'REMOTE_THREAD_NOT_LOADED'; + this.delivery = 'rejected'; + this.reason = 'remote-thread-not-loaded'; + this.threadId = threadId; + this.hostId = hostId; + this.hostName = hostName; + this.hint = hint; + } + } + function codexGlobalStatePath(env = process.env) { + const home = env.CODEX_HOME || (0, node_path__rspack_import_2.join)((0, node_os__rspack_import_1.homedir)(), '.codex'); + return (0, node_path__rspack_import_2.join)(home, '.codex-global-state.json'); + } + function loadCodexGlobalState(env = process.env) { + let raw; + try { + raw = (0, node_fs__rspack_import_0.readFileSync)(codexGlobalStatePath(env), 'utf8'); + } catch { + return null; + } + try { + const data = JSON.parse(raw); + if (!data || typeof data !== 'object' || Array.isArray(data)) return null; + return data; + } catch { + return null; + } + } + function codexStateEntries(data) { + const nested = data?.['electron-persisted-atom-state']; + return [ + nested, + data + ].filter((value)=>value && typeof value === 'object' && !Array.isArray(value)); + } + function findRemoteThreadHost(threadId, env = process.env) { + let bare; + try { + bare = (0, _thread_id_js__rspack_import_3.SX)(threadId); + } catch { + bare = typeof threadId === 'string' && threadId.startsWith("local:") ? threadId.slice("local:".length) : threadId; + } + if (!bare) return null; + const data = loadCodexGlobalState(env); + if (!data) return null; + const hostNames = collectHostNames(data); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value) ?? null; + for (const entry of extractThreadEntries(value)){ + const id = threadIdFromEntry(entry); + if (!id) continue; + const entryBare = id.startsWith("local:") ? id.slice("local:".length) : id; + if (entryBare === bare) return { + hostId, + hostName + }; + } + } + return null; + } + function mapRemoteThreadError(error, threadId, env = process.env) { + const message = errorMessage(error); + if (!/thread not loaded|not loaded|no rollout found|thread not found|invalid thread id/i.test(message)) { + return error instanceof Error ? error : new Error(message || String(error)); + } + const remote = findRemoteThreadHost(threadId, env); + if (remote) { + return new RemoteThreadNotLoadedError1(threadId, remote.hostId, remote.hostName); + } + if (/thread not loaded|not loaded/i.test(message)) { + return new RemoteThreadNotLoadedError1(threadId, null, null); + } + return error instanceof Error ? error : new Error(message || String(error)); + } + function collectHostNames(data) { + const names = new Map(); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const hostId = stringField(entry, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) continue; + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const id = stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + entries.push(id ? entry : { + ...entry, + id: key + }); + } + } + return entries; + } + function threadIdFromEntry(entry) { + if (typeof entry === 'string' && entry) return entry; + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + return stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + for (const key of keys){ + const value = row[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) return String(error.message); + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); + } + __webpack_require__.d(__webpack_exports__, { + IT: ()=>loadCodexGlobalState, + R3: ()=>mapRemoteThreadError, + Rg: ()=>RemoteThreadNotLoadedError1, + xr: ()=>codexStateEntries + }); + }, + "./src/core/codex/thread-id.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + const LOCAL_THREAD_ID_PREFIX = 'local:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/; + function isTemporaryDesktopThreadId(threadId) { + return typeof threadId === 'string' && (threadId.startsWith(TEMP_THREAD_ID_PREFIX) || threadId.startsWith('client-new-thread:')); + } + function normalizeCodexThreadId1(threadId) { + if (typeof threadId !== 'string' || !threadId) { + throw new RangeError('Invalid threadId'); + } + if (isTemporaryDesktopThreadId(threadId)) { + throw new RangeError('Temporary Desktop thread id ' + JSON.stringify(threadId) + ' has no app-server mapping; wait for the real conversation id'); + } + const bare = threadId.startsWith(LOCAL_THREAD_ID_PREFIX) ? threadId.slice(LOCAL_THREAD_ID_PREFIX.length) : threadId; + if (!bare || !ID_PATTERN.test(bare)) { + throw new RangeError('Invalid threadId'); + } + return bare; + } + __webpack_require__.d(__webpack_exports__, { + SX: ()=>normalizeCodexThreadId1, + vr: ()=>isTemporaryDesktopThreadId + }, { + hA: TEMP_THREAD_ID_PREFIX }); }, - "./src/core/desktop-shim.js" (__unused_rspack___webpack_module__, __unused_rspack___webpack_exports__, __webpack_require__) { + "./src/core/desktop-shim.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { var node_child_process__rspack_import_0 = __webpack_require__("node:child_process"); var node_fs__rspack_import_1 = __webpack_require__("node:fs"); var node_os__rspack_import_2 = __webpack_require__("node:os"); @@ -22536,25 +27198,25 @@ var __webpack_modules__ = { return env.CODEX_HOME || join(env.HOME || homedir(), ".codex"); } function userHomeDir(env = process.env) { - return env.HOME || homedir(); + return env.HOME || (0, node_os__rspack_import_2.homedir)(); } function defaultPaths({ env = process.env, home = userHomeDir(env) } = {}) { - const codexHome = env.CODEX_HOME || join(home, ".codex"); - const binDir = join(codexHome, "bin"); + const codexHome = env.CODEX_HOME || (0, node_path__rspack_import_3.join)(home, ".codex"); + const binDir = (0, node_path__rspack_import_3.join)(codexHome, "bin"); return { binDir, - bridgeLogPath: join(binDir, BRIDGE_LOG_FILENAME), - bridgePath: join(binDir, BRIDGE_FILENAME), + bridgeLogPath: (0, node_path__rspack_import_3.join)(binDir, BRIDGE_LOG_FILENAME), + bridgePath: (0, node_path__rspack_import_3.join)(binDir, BRIDGE_FILENAME), codexHome, - envLogPath: join(binDir, ENV_LOG_FILENAME), - envScriptPath: join(binDir, ENV_SCRIPT_FILENAME), + envLogPath: (0, node_path__rspack_import_3.join)(binDir, ENV_LOG_FILENAME), + envScriptPath: (0, node_path__rspack_import_3.join)(binDir, ENV_SCRIPT_FILENAME), label: SHIM_LABEL, - launchAgentsDir: join(home, "Library", "LaunchAgents"), - plistPath: join(home, "Library", "LaunchAgents", `${SHIM_LABEL}.plist`), - realPath: join(codexHome, STANDALONE_REAL_SUFFIX), - socketPath: join(codexHome, "app-server-control", "app-server-control.sock"), - wrapperLogPath: join(binDir, WRAPPER_LOG_FILENAME), - wrapperPath: join(binDir, WRAPPER_FILENAME) + launchAgentsDir: (0, node_path__rspack_import_3.join)(home, "Library", "LaunchAgents"), + plistPath: (0, node_path__rspack_import_3.join)(home, "Library", "LaunchAgents", `${SHIM_LABEL}.plist`), + realPath: (0, node_path__rspack_import_3.join)(codexHome, STANDALONE_REAL_SUFFIX), + socketPath: (0, node_path__rspack_import_3.join)(codexHome, "app-server-control", "app-server-control.sock"), + wrapperLogPath: (0, node_path__rspack_import_3.join)(binDir, WRAPPER_LOG_FILENAME), + wrapperPath: (0, node_path__rspack_import_3.join)(binDir, WRAPPER_FILENAME) }; } function shouldBridge(argv) { @@ -22714,7 +27376,7 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } } const defaultRunner = (file, args)=>{ try { - return spawnSync(file, args, { + return (0, node_child_process__rspack_import_0.spawnSync)(file, args, { encoding: "utf8", timeout: 15000 }); @@ -22741,19 +27403,19 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } } function isExecutable(path) { try { - statSync(path); + (0, node_fs__rspack_import_1.statSync)(path); } catch { return false; } try { - return (statSync(path).mode & 73) !== 0; + return ((0, node_fs__rspack_import_1.statSync)(path).mode & 73) !== 0; } catch { return false; } } function fileExists(path) { try { - statSync(path); + (0, node_fs__rspack_import_1.statSync)(path); return true; } catch { return false; @@ -22905,12 +27567,12 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } } function socketStateOf(path) { try { - return statSync(path).isSocket() ? "socket" : "not-a-socket"; + return (0, node_fs__rspack_import_1.statSync)(path).isSocket() ? "socket" : "not-a-socket"; } catch (error) { return error && (error.code === "EACCES" || error.code === "EPERM") ? "permission-denied" : "absent"; } } - function desktopShimStatus1({ env = process.env, home = userHomeDir(env), platform = process.platform, runner = defaultRunner } = {}) { + function desktopShimStatus({ env = process.env, home = userHomeDir(env), platform = process.platform, runner = defaultRunner } = {}) { const paths = defaultPaths({ env, home @@ -22941,7 +27603,7 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } } const desktopCliPath = platform === "darwin" ? guiCliPath : cliPath; const installed = wrapperPresent && isExecutable(paths.wrapperPath) && bridgePresent; - const socketPath = env.CODEX_APP_SERVER_SOCK || join(paths.codexHome, "app-server-control", "app-server-control.sock"); + const socketPath = env.CODEX_APP_SERVER_SOCK || (0, node_path__rspack_import_3.join)(paths.codexHome, "app-server-control", "app-server-control.sock"); return { action: "status", bridgePath: paths.bridgePath, @@ -22969,6 +27631,9 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } wrapperPresent }; } + __webpack_require__.d(__webpack_exports__, { + em: ()=>desktopShimStatus + }); }, "./src/core/gateway.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { var node_crypto__rspack_import_0 = __webpack_require__("node:crypto"); @@ -24482,39 +29147,55 @@ function __webpack_require__(moduleId) { var __webpack_exports__ = {}; var node_worker_threads__rspack_import_0 = __webpack_require__("node:worker_threads"); var react__rspack_import_1 = __webpack_require__("./node_modules/react/react.react-server.js"); -var _agent_bundle_runtime_flight_server__rspack_import_20 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/flight/server.js"); -var _agent_bundle_runtime__rspack_import_17 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/49.js"); -var _agent_bundle_runtime__rspack_import_18 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); -var _agent_bundle_runtime__rspack_import_19 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); +var _agent_bundle_runtime_flight_server__rspack_import_28 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/flight/server.js"); +var _agent_bundle_runtime__rspack_import_25 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/49.js"); +var _agent_bundle_runtime__rspack_import_26 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/736.js"); +var _agent_bundle_runtime__rspack_import_27 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); var node_url__rspack_import_2 = __webpack_require__("node:url"); -var _src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_3 = __webpack_require__("./src/mcp/grok-bot/tools/claude_send.tsx"); -var _src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_4 = __webpack_require__("./src/mcp/grok-bot/tools/codex_new.tsx"); -var _src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_5 = __webpack_require__("./src/mcp/grok-bot/tools/codex_send.tsx"); -var _src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_6 = __webpack_require__("./src/mcp/grok-bot/tools/codex_threads.tsx"); -var _src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_7 = __webpack_require__("./src/mcp/grok-bot/tools/codex_wait.tsx"); -var _src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_8 = __webpack_require__("./src/mcp/grok-bot/tools/codex_watch.tsx"); -var _src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_9 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_start.tsx"); -var _src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_10 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_status.tsx"); -var _src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_11 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_stop.tsx"); -var _src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_12 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_codex_respond.tsx"); -var _src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_13 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_grok_approvals.tsx"); -var _src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_14 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_grok_respond.tsx"); -var _src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_15 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_send.tsx"); -var _src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_16 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_thread.tsx"); -const route0 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_3, 'default'), _src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_3); -const route1 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_4, 'default'), _src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_4); -const route2 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_5, 'default'), _src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_5); -const route3 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_6, 'default'), _src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_6); -const route4 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_7, 'default'), _src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_7); -const route5 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_8, 'default'), _src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_8); -const route6 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_9, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_9); -const route7 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_10, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_10); -const route8 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_11, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_11); -const route9 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_12, 'default'), _src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_12); -const route10 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_13, 'default'), _src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_13); -const route11 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_14, 'default'), _src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_14); -const route12 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_15, 'default'), _src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_15); -const route13 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_16, 'default'), _src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_16); +var _src_mcp_grok_bot_tools_chatgpt_desktop_list_hosts_tsx__rspack_import_3 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx"); +var _src_mcp_grok_bot_tools_chatgpt_desktop_list_threads_tsx__rspack_import_4 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx"); +var _src_mcp_grok_bot_tools_chatgpt_desktop_open_thread_tsx__rspack_import_5 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx"); +var _src_mcp_grok_bot_tools_chatgpt_desktop_read_thread_tsx__rspack_import_6 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx"); +var _src_mcp_grok_bot_tools_chatgpt_desktop_search_threads_tsx__rspack_import_7 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx"); +var _src_mcp_grok_bot_tools_chatgpt_desktop_send_tsx__rspack_import_8 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx"); +var _src_mcp_grok_bot_tools_chatgpt_desktop_status_tsx__rspack_import_9 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx"); +var _src_mcp_grok_bot_tools_chatgpt_desktop_wait_reply_tsx__rspack_import_10 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx"); +var _src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_11 = __webpack_require__("./src/mcp/grok-bot/tools/claude_send.tsx"); +var _src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_12 = __webpack_require__("./src/mcp/grok-bot/tools/codex_new.tsx"); +var _src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_13 = __webpack_require__("./src/mcp/grok-bot/tools/codex_send.tsx"); +var _src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_14 = __webpack_require__("./src/mcp/grok-bot/tools/codex_threads.tsx"); +var _src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_15 = __webpack_require__("./src/mcp/grok-bot/tools/codex_wait.tsx"); +var _src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_16 = __webpack_require__("./src/mcp/grok-bot/tools/codex_watch.tsx"); +var _src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_17 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_start.tsx"); +var _src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_18 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_status.tsx"); +var _src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_19 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_stop.tsx"); +var _src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_20 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_codex_respond.tsx"); +var _src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_21 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_grok_approvals.tsx"); +var _src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_22 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_grok_respond.tsx"); +var _src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_23 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_send.tsx"); +var _src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_24 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_thread.tsx"); +const route0 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_list_hosts_tsx__rspack_import_3, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_list_hosts_tsx__rspack_import_3); +const route1 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_list_threads_tsx__rspack_import_4, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_list_threads_tsx__rspack_import_4); +const route2 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_open_thread_tsx__rspack_import_5, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_open_thread_tsx__rspack_import_5); +const route3 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_read_thread_tsx__rspack_import_6, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_read_thread_tsx__rspack_import_6); +const route4 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_search_threads_tsx__rspack_import_7, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_search_threads_tsx__rspack_import_7); +const route5 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_send_tsx__rspack_import_8, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_send_tsx__rspack_import_8); +const route6 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_status_tsx__rspack_import_9, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_status_tsx__rspack_import_9); +const route7 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_wait_reply_tsx__rspack_import_10, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_wait_reply_tsx__rspack_import_10); +const route8 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_11, 'default'), _src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_11); +const route9 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_12, 'default'), _src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_12); +const route10 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_13, 'default'), _src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_13); +const route11 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_14, 'default'), _src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_14); +const route12 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_15, 'default'), _src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_15); +const route13 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_16, 'default'), _src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_16); +const route14 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_17, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_17); +const route15 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_18, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_18); +const route16 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_19, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_19); +const route17 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_20, 'default'), _src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_20); +const route18 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_21, 'default'), _src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_21); +const route19 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_22, 'default'), _src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_22); +const route20 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_23, 'default'), _src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_23); +const route21 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_24, 'default'), _src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_24); globalThis.__rspack_rsc_manifest__ ??= Object.freeze({ clientManifest: Object.freeze({}) }); @@ -24526,12 +29207,337 @@ const processLifetime = { instanceId: crypto.randomUUID(), pid: process.pid }; -const pluginRoot = (0, _agent_bundle_runtime__rspack_import_17.E7)({ +const pluginRoot = (0, _agent_bundle_runtime__rspack_import_25.E7)({ fallback: (0, node_url__rspack_import_2.fileURLToPath)(new URL('..', import.meta.url)), stateAnchor: 'user-data' }); const composeLayouts = (route, props)=>(0, react__rspack_import_1.createElement)(route.module.default, props); const routes = Object.freeze({ + "tool:grok-bot/chatgpt_desktop_list_hosts": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Discover local, remote-control environment ids, managed SSH hosts, and modelProvider ids. hostName is null when Desktop has no real display name. Use hostId or label in list/search filters; remote thread ids must be read on the owning host. warnings report provider discovery failures.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "port": { + "type": "number" + } + }, + "required": [], + "type": "object" + }, + "title": "ChatGPT Desktop list hosts" + }, + id: "tool:grok-bot/chatgpt_desktop_list_hosts", + kind: "tool", + module: route0, + name: "chatgpt_desktop_list_hosts", + serverId: "mcp:grok-bot" + }), + "tool:grok-bot/chatgpt_desktop_list_threads": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "List one sorted Desktop inventory without duplicate rows. Pass nextCursor as cursor with unchanged filters until null; pages fill limit unless the MCP byte budget binds. A changed/expired inventory cursor requires restarting. Display titles are capped at 200 characters with titleTruncated:true. Filter by dynamic hostId/hostName, modelProvider, or Desktop project label/projectId; projectRootPath identifies the resolved workspace. Rows expose local:, location, hostId, and provider; remote rows require their owning host for reading or sending. Backend and warnings identify partial sources.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "cursor": { + "description": "Opaque nextCursor from the prior page; keep filters unchanged.", + "type": "string" + }, + "groupBy": { + "description": "When \"host\", also return groups[] keyed by host (grouping mode, not a host id).", + "enum": [ + "host" + ], + "type": "string" + }, + "host": { + "description": "Any string filter for machine/location. Reserved: \"all\" (default), \"local\". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.", + "type": "string" + }, + "limit": { + "default": 50, + "description": "Maximum threads per page.", + "type": "number" + }, + "modelProvider": { + "description": "Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.", + "type": "string" + }, + "port": { + "type": "number" + }, + "project": { + "description": "Match a Desktop project label or app-server projectId. Dynamic string, not an enum.", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "title": "ChatGPT Desktop list threads" + }, + id: "tool:grok-bot/chatgpt_desktop_list_threads", + kind: "tool", + module: route1, + name: "chatgpt_desktop_list_threads", + serverId: "mcp:grok-bot" + }), + "tool:grok-bot/chatgpt_desktop_open_thread": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Navigate Desktop to an existing local: or bare conversation id, even when the row is absent from the sidebar. Archived routes return archived:true without unarchiving; read them with chatgpt_desktop_read_thread. A remote-control id is owned by its hostId and cannot be opened on this Mac. CDP_UNREACHABLE means Desktop must be reachable locally.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "openTimeoutMs": { + "description": "Maximum time to wait for the requested conversation (1-600000 ms).", + "type": "number" + }, + "port": { + "description": "Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).", + "type": "number" + }, + "threadId": { + "description": "local: or bare durable id. Temporary local:client-new-thread:* works only while selected; remote ids require their owning host.", + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "title": "ChatGPT Desktop open thread" + }, + id: "tool:grok-bot/chatgpt_desktop_open_thread", + kind: "tool", + module: route2, + name: "chatgpt_desktop_open_thread", + serverId: "mcp:grok-bot" + }), + "tool:grok-bot/chatgpt_desktop_read_thread": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Read a local:, bare durable id, or selected temporary local:client-new-thread:* id. Pass nextCursor back as cursor with the same threadId/full mode until complete:true. Pages stop at a byte budget; oversized turns use ordered continuation fragments with stable turnKey and field offsets, without dropping text. Archived threads remain readable. Warnings explain partial pages or app-server errors. REMOTE_THREAD_NOT_LOADED includes the owning hostId: call on that host.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "cursor": { + "description": "Opaque nextCursor from the prior read page. Keep threadId and full unchanged.", + "type": "string" + }, + "full": { + "description": "When true, page from the start through the complete history (up to 2000 turns by default); otherwise return the most recent 100 turns by default. Explicit limit bounds either mode.", + "type": "boolean" + }, + "limit": { + "type": "number" + }, + "openTimeoutMs": { + "description": "Max wait for Loading task… to clear after opening (default 90000).", + "type": "number" + }, + "port": { + "type": "number" + }, + "threadId": { + "description": "local:, bare durable id, or selected local:client-new-thread:* temporary id. Remote ids return REMOTE_THREAD_NOT_LOADED with hostId.", + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "render": { + "maxElapsedMs": 660000 + }, + "title": "ChatGPT Desktop read thread" + }, + id: "tool:grok-bot/chatgpt_desktop_read_thread", + kind: "tool", + module: route3, + name: "chatgpt_desktop_read_thread", + serverId: "mcp:grok-bot" + }), + "tool:grok-bot/chatgpt_desktop_search_threads": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Search full Desktop thread metadata by case-insensitive substring. Display titles are capped at 200 characters with titleTruncated:true. Pass nextCursor as cursor with unchanged query and filters to retrieve all matches. Dynamic hostId/hostName, modelProvider, and project filters apply. Remote results identify hostId and cannot be read on this Mac.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "cursor": { + "description": "Opaque nextCursor from the previous search page; keep query and filters unchanged.", + "type": "string" + }, + "groupBy": { + "description": "When \"host\", also return groups[] keyed by host (grouping mode, not a host id).", + "enum": [ + "host" + ], + "type": "string" + }, + "host": { + "description": "Any string filter for machine/location. Reserved: \"all\" (default), \"local\". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.", + "type": "string" + }, + "limit": { + "default": 50, + "description": "Maximum matches.", + "type": "number" + }, + "modelProvider": { + "description": "Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.", + "type": "string" + }, + "port": { + "type": "number" + }, + "project": { + "description": "Match a Desktop project label or app-server projectId.", + "type": "string" + }, + "query": { + "description": "Case-insensitive substring over thread metadata.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "title": "ChatGPT Desktop search threads" + }, + id: "tool:grok-bot/chatgpt_desktop_search_threads", + kind: "tool", + module: route4, + name: "chatgpt_desktop_search_threads", + serverId: "mcp:grok-bot" + }), + "tool:grok-bot/chatgpt_desktop_send": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": false + }, + "description": "Send once through local Desktop CDP. With threadId, accept local:, bare durable id, or currently selected local:client-new-thread:*; remote ids must be sent on their hostId. Omit threadId and project for a new chat outside projects; omit threadId and set project for a new chat inside it. The empty view and project scope are checked before typing. New-thread receipts include temporaryThreadId when seen and return durable local: once resolved. ARCHIVED_THREAD rejects without sending; unarchive:true explicitly unarchives that existing thread first. PROJECT_UNAVAILABLE identifies a disabled project action or missing configured root; NEW_CHAT_NAVIGATION_FAILED reports an unchanged view promptly. CDP_UNREACHABLE and COMPOSER_HAS_DRAFT require operator inspection, not blind retry.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "openTimeoutMs": { + "type": "number" + }, + "port": { + "type": "number" + }, + "project": { + "description": "With threadId omitted, create the new chat inside this project. Omit for a new chat outside projects.", + "type": "string" + }, + "text": { + "type": "string" + }, + "threadId": { + "description": "local:, bare durable id, or selected local:client-new-thread:*; omit for a new chat.", + "type": "string" + }, + "unarchive": { + "default": false, + "description": "Existing threadId only: explicitly call thread/unarchive after ARCHIVED_THREAD, then retry the send once. Never implicit.", + "type": "boolean" + } + }, + "required": [ + "text" + ], + "type": "object" + }, + "render": { + "maxElapsedMs": 180000 + }, + "title": "ChatGPT Desktop send" + }, + id: "tool:grok-bot/chatgpt_desktop_send", + kind: "tool", + module: route5, + name: "chatgpt_desktop_send", + serverId: "mcp:grok-bot" + }), + "tool:grok-bot/chatgpt_desktop_status": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Check local ChatGPT Desktop CDP before send, wait, or open. reachable:false and exitCode:1 mean CDP is down even if appServerFallback.reachable is true; list/search/read may still work through app-server. There is no remote CDP transport.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "port": { + "description": "Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).", + "type": "number" + } + }, + "required": [], + "type": "object" + }, + "title": "ChatGPT Desktop status" + }, + id: "tool:grok-bot/chatgpt_desktop_status", + kind: "tool", + module: route6, + name: "chatgpt_desktop_status", + serverId: "mcp:grok-bot" + }), + "tool:grok-bot/chatgpt_desktop_wait_reply": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "After chatgpt_desktop_send, wait for the selected Desktop reply over CDP. Accept the returned durable local: or the temporary local:client-new-thread:* while selected; omit threadId only when staying on the same selected chat. Returns reply, durable threadId and conversationId. timeout is not proof the send failed; CDP_UNREACHABLE requires Desktop recovery. Remote ids must be handled on their hostId.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "port": { + "type": "number" + }, + "threadId": { + "description": "Durable local:, bare id, or selected local:client-new-thread:* from send.", + "type": "string" + }, + "timeoutMs": { + "default": 120000, + "type": "number" + } + }, + "required": [], + "type": "object" + }, + "render": { + "maxElapsedMs": 660000 + }, + "title": "ChatGPT Desktop wait for reply" + }, + id: "tool:grok-bot/chatgpt_desktop_wait_reply", + kind: "tool", + module: route7, + name: "chatgpt_desktop_wait_reply", + serverId: "mcp:grok-bot" + }), "tool:grok-bot/claude_send": Object.freeze({ config: { "annotations": { @@ -24564,7 +29570,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/claude_send", kind: "tool", - module: route0, + module: route8, name: "claude_send", serverId: "mcp:grok-bot" }), @@ -24613,7 +29619,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/codex_new", kind: "tool", - module: route1, + module: route9, name: "codex_new", serverId: "mcp:grok-bot" }), @@ -24708,7 +29714,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/codex_send", kind: "tool", - module: route2, + module: route10, name: "codex_send", serverId: "mcp:grok-bot" }), @@ -24725,10 +29731,11 @@ const routes = Object.freeze({ "additionalProperties": false, "properties": { "cursor": { + "description": "Opaque nextCursor from a previous page.", "type": "string" }, "limit": { - "description": "Maximum threads in this page: 1-200.", + "description": "Maximum threads in this page: 1-200. Pass modelProviders:[] internally so every provider is included; use cursor/nextCursor to page.", "type": "number" } }, @@ -24742,7 +29749,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/codex_threads", kind: "tool", - module: route3, + module: route11, name: "codex_threads", serverId: "mcp:grok-bot" }), @@ -24792,7 +29799,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/codex_wait", kind: "tool", - module: route4, + module: route12, name: "codex_wait", serverId: "mcp:grok-bot" }), @@ -24835,7 +29842,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/codex_watch", kind: "tool", - module: route5, + module: route13, name: "codex_watch", serverId: "mcp:grok-bot" }), @@ -24877,7 +29884,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/gbot_bridge_start", kind: "tool", - module: route6, + module: route14, name: "gbot_bridge_start", serverId: "mcp:grok-bot" }), @@ -24903,7 +29910,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/gbot_bridge_status", kind: "tool", - module: route7, + module: route15, name: "gbot_bridge_status", serverId: "mcp:grok-bot" }), @@ -24932,7 +29939,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/gbot_bridge_stop", kind: "tool", - module: route8, + module: route16, name: "gbot_bridge_stop", serverId: "mcp:grok-bot" }), @@ -24991,7 +29998,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/gbot_codex_respond", kind: "tool", - module: route9, + module: route17, name: "gbot_codex_respond", serverId: "mcp:grok-bot" }), @@ -25022,7 +30029,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/gbot_grok_approvals", kind: "tool", - module: route10, + module: route18, name: "gbot_grok_approvals", serverId: "mcp:grok-bot" }), @@ -25069,7 +30076,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/gbot_grok_respond", kind: "tool", - module: route11, + module: route19, name: "gbot_grok_respond", serverId: "mcp:grok-bot" }), @@ -25130,7 +30137,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/gbot_send", kind: "tool", - module: route12, + module: route20, name: "gbot_send", serverId: "mcp:grok-bot" }), @@ -25176,7 +30183,7 @@ const routes = Object.freeze({ }, id: "tool:grok-bot/gbot_thread", kind: "tool", - module: route13, + module: route21, name: "gbot_thread", serverId: "mcp:grok-bot" }) @@ -25224,7 +30231,7 @@ const render = async (message)=>{ }; try { const plugin = message.plugin ?? pluginRoot.identity; - await (0, _agent_bundle_runtime__rspack_import_18.iC)({ + await (0, _agent_bundle_runtime__rspack_import_26.iC)({ ...message.actor === undefined ? {} : { actor: message.actor }, @@ -25238,7 +30245,7 @@ const render = async (message)=>{ operationId: route.id, surface: route.name }, - lineage: message.lineage ?? (0, _agent_bundle_runtime__rspack_import_18.hU)('not-provided'), + lineage: message.lineage ?? (0, _agent_bundle_runtime__rspack_import_26.hU)('not-provided'), plugin, progress: { report: async (update)=>{ @@ -25254,7 +30261,7 @@ const render = async (message)=>{ session: message.session }, signal: controller.signal, - terminal: message.terminal ?? (0, _agent_bundle_runtime__rspack_import_18.hU)('not-provided'), + terminal: message.terminal ?? (0, _agent_bundle_runtime__rspack_import_26.hU)('not-provided'), ...message.workspace === undefined ? {} : { workspace: message.workspace } @@ -25289,10 +30296,10 @@ const render = async (message)=>{ type: 'observed-render-start' }); const renderStartedAt = performance.now(); - const element = validationError === undefined ? composeLayouts(observedRoute, props, controller.signal) : (0, react__rspack_import_1.createElement)(_agent_bundle_runtime__rspack_import_19.g.Result, null, (0, react__rspack_import_1.createElement)(_agent_bundle_runtime__rspack_import_19.g.Error, { + const element = validationError === undefined ? composeLayouts(observedRoute, props, controller.signal) : (0, react__rspack_import_1.createElement)(_agent_bundle_runtime__rspack_import_27.g.Result, null, (0, react__rspack_import_1.createElement)(_agent_bundle_runtime__rspack_import_27.g.Error, { code: 'invalid-input' }, `Input validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`)); - const flight = (0, _agent_bundle_runtime_flight_server__rspack_import_20.y)(element, { + const flight = (0, _agent_bundle_runtime_flight_server__rspack_import_28.y)(element, { signal: controller.signal }); const reader = flight.getReader(); diff --git a/artifact/mcp/mcp-grok-bot-b8c2461e.mjs b/artifact/mcp/mcp-grok-bot-b8c2461e.mjs index cecbfcd..d244a3d 100644 --- a/artifact/mcp/mcp-grok-bot-b8c2461e.mjs +++ b/artifact/mcp/mcp-grok-bot-b8c2461e.mjs @@ -6492,6 +6492,3828 @@ var __webpack_modules__ = { module.exports = __webpack_require__("./node_modules/react/cjs/react-jsx-runtime.development.js"); } }, + "./src/core/chatgpt-desktop/app-server-fallback.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_bridge_js__rspack_import_0 = __webpack_require__("./src/core/codex-bridge.js"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _remote_threads_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/remote-threads.ts"); + var _payload_budget_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/payload-budget.ts"); + var _thread_ids_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const LIST_PAGE_PREFIX = 'desktop-list-v1:'; + const SEARCH_PAGE_PREFIX = 'desktop-search-v1:'; + const READ_PAGE_PREFIX = 'desktop-read-v1:'; + async function appServerListThreads({ limit = 50, cursor, modelProviders = [] } = {}) { + let sourceCursor = cursor; + let skip = 0; + let fetchLimit = Math.min(limit, 25); + if (cursor?.startsWith(LIST_PAGE_PREFIX)) { + const state = (0, _payload_budget_js__rspack_import_4.i3)(LIST_PAGE_PREFIX, cursor); + if (!Number.isSafeInteger(state.skip) || state.skip < 0 || !Number.isSafeInteger(state.fetchLimit) || state.fetchLimit < 1 || state.fetchLimit > 25 || state.cursor !== null && typeof state.cursor !== 'string') { + throw new Error('Invalid ChatGPT Desktop list page cursor'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + fetchLimit = state.fetchLimit; + } + const base = { + backend: 'app-server', + limit, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined + }; + const threads = []; + const seen = new Set(); + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: fetchLimit, + cursor: sourceCursor, + modelProviders + }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop list page cursor is stale'); + for(let index = skip; index < out.threads.length; index++){ + let row = (0, _payload_budget_js__rspack_import_4.eX)(mapListedThread(out.threads[index])); + const probe = { + ...base, + threads: [ + ...threads, + row + ], + nextCursor: LIST_PAGE_PREFIX, + exitCode: 0 + }; + if ((0, _payload_budget_js__rspack_import_4.sQ)(probe) > 614400) { + if (threads.length) return { + ...base, + threads, + nextCursor: (0, _payload_budget_js__rspack_import_4.oI)(LIST_PAGE_PREFIX, { + cursor: sourceCursor ?? null, + skip: index, + fetchLimit + }), + warnings: [ + 'List page stopped at the MCP byte budget; continue with nextCursor' + ] + }; + row = (0, _payload_budget_js__rspack_import_4.MH)(row); + if ((0, _payload_budget_js__rspack_import_4.sQ)({ + ...base, + threads: [ + row + ], + nextCursor: LIST_PAGE_PREFIX, + exitCode: 0 + }) > 614400) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + threads.push(row); + if (threads.length >= limit) { + const nextCursor = index + 1 < out.threads.length ? (0, _payload_budget_js__rspack_import_4.oI)(LIST_PAGE_PREFIX, { + cursor: sourceCursor ?? null, + skip: index + 1, + fetchLimit + }) : out.nextCursor ?? null; + return { + ...base, + threads, + nextCursor + }; + } + } + if (out.nextCursor == null) return { + ...base, + threads, + nextCursor: null + }; + if (typeof out.nextCursor !== 'string' || !out.nextCursor || seen.has(out.nextCursor)) { + throw new Error('ChatGPT Desktop list returned an invalid or repeated cursor'); + } + seen.add(out.nextCursor); + sourceCursor = out.nextCursor; + skip = 0; + } + return { + ...base, + threads, + nextCursor: sourceCursor, + warnings: [ + 'List stopped after 400 app-server pages; continue with nextCursor' + ] + }; + } + async function appServerSearchThreads({ query, limit = 50, cursor, modelProviders = [], project }) { + const needle = query.trim().toLowerCase(); + if (!needle) { + return appServerListThreads({ + limit, + cursor, + modelProviders + }); + } + const matched = []; + let sourceCursor; + let skip = 0; + if (cursor) { + const state = (0, _payload_budget_js__rspack_import_4.i3)(SEARCH_PAGE_PREFIX, cursor); + if (state.query !== query || state.project !== project || JSON.stringify(state.modelProviders) !== JSON.stringify(modelProviders) || !Number.isSafeInteger(state.skip) || state.skip < 0) { + throw new Error('Search cursor does not match this query and its filters'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + } + const pageCursor = (rpcCursor, offset)=>(0, _payload_budget_js__rspack_import_4.oI)(SEARCH_PAGE_PREFIX, { + cursor: rpcCursor ?? null, + skip: offset, + query, + project, + modelProviders + }); + const base = { + backend: 'app-server', + limit, + query, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined + }; + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: 25, + cursor: sourceCursor, + modelProviders + }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop search cursor is stale'); + for(let index = skip; index < out.threads.length; index++){ + const mapped = mapListedThread(out.threads[index]); + if (!threadMatchesQuery(mapped, needle) || project && ![ + mapped.project, + mapped.projectId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(project.trim().toLowerCase()))) continue; + let row = (0, _payload_budget_js__rspack_import_4.eX)(mapped); + if ((0, _payload_budget_js__rspack_import_4.sQ)({ + ...base, + threads: [ + ...matched, + row + ], + nextCursor: SEARCH_PAGE_PREFIX, + exitCode: 0 + }) > 614400) { + if (matched.length) return { + ...base, + threads: matched, + nextCursor: pageCursor(sourceCursor, index), + warnings: [ + 'Search page stopped at the MCP byte budget; continue with nextCursor' + ] + }; + row = (0, _payload_budget_js__rspack_import_4.MH)(row); + } + matched.push(row); + if (matched.length >= limit) { + const nextCursor = index + 1 < out.threads.length ? pageCursor(sourceCursor, index + 1) : out.nextCursor ? pageCursor(out.nextCursor, 0) : null; + return { + ...base, + threads: matched, + nextCursor + }; + } + } + if (out.nextCursor == null) return { + ...base, + threads: matched, + nextCursor: null + }; + sourceCursor = out.nextCursor; + skip = 0; + } + return { + ...base, + threads: matched, + nextCursor: pageCursor(sourceCursor, 0), + warnings: [ + 'Search stopped after 400 app-server pages; continue with nextCursor' + ] + }; + } + async function discoverModelProviders() { + const providers = new Set(); + let threadCount = 0; + let cursor; + let complete = false; + for(let page = 0; page < 400; page++){ + const out = await (0, _codex_bridge_js__rspack_import_0.X$)({ + limit: 25, + cursor, + modelProviders: [] + }); + threadCount += out.threads.length; + for (const thread of out.threads){ + if (typeof thread.modelProvider === 'string' && thread.modelProvider) { + providers.add(thread.modelProvider); + } + } + if (out.nextCursor == null || typeof out.nextCursor !== 'string') { + complete = true; + break; + } + cursor = out.nextCursor; + } + return { + providers: [ + ...providers + ].sort(), + source: 'thread/list-distinct', + threadCount, + complete + }; + } + async function appServerReadThread({ threadId, limit, full = false, cursor }) { + limit ??= full ? 2000 : 100; + const bare = (0, _thread_ids_js__rspack_import_3.NM)(threadId); + const remote = (0, _remote_threads_js__rspack_import_2.KI)(bare); + if (remote) { + try { + return await readLocalThread(bare, { + limit, + full, + cursor + }); + } catch (error) { + if (isThreadNotLoaded(error) || isUnknownThread(error)) { + throw new _errors_js__rspack_import_1.Rg(bare, remote.hostId, remote.hostName); + } + throw maybeRemoteThreadError(error, bare); + } + } + try { + return await readLocalThread(bare, { + limit, + full, + cursor + }); + } catch (error) { + throw maybeRemoteThreadError(error, bare); + } + } + async function appServerUnarchiveThread(threadId) { + const bare = (0, _thread_ids_js__rspack_import_3.NM)(threadId); + const { client } = await (0, _codex_bridge_js__rspack_import_0.eC)(); + try { + const result = await client.request('thread/unarchive', { + threadId: bare + }); + if (!result || typeof result !== 'object' || !('thread' in result) || !result.thread || typeof result.thread !== 'object' || !('id' in result.thread) || result.thread.id !== bare) { + throw new Error(`thread/unarchive returned no thread for ${bare}`); + } + } finally{ + client.close(); + } + } + async function readLocalThread(bare, { limit, full, cursor }) { + const { client } = await (0, _codex_bridge_js__rspack_import_0.eC)(); + try { + const meta = await readThreadMetadata(client, bare); + const { turns, complete, nextCursor } = await listTurnsFull(client, bare, { + limit, + full, + cursor, + title: meta.title + }); + return { + threadId: (0, _thread_ids_js__rspack_import_3.BS)(bare), + turns, + backend: 'app-server', + limit, + full, + complete, + nextCursor, + ...complete ? {} : { + warnings: [ + 'Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment' + ] + }, + ...meta.title ? (0, _payload_budget_js__rspack_import_4.aA)(meta.title) : {}, + cwd: meta.cwd, + status: meta.status, + modelProvider: meta.modelProvider, + model: meta.model + }; + } finally{ + client.close(); + } + } + async function appServerStatusProbe() { + const status = await (0, _codex_bridge_js__rspack_import_0.MU)(); + return { + reachable: Boolean(status.reachable && status.mode === 'daemon'), + mode: typeof status.mode === 'string' ? status.mode : undefined, + socketPath: typeof status.socketPath === 'string' ? status.socketPath : undefined + }; + } + function mapListedThread(thread) { + const bare = String(thread.id ?? ''); + const section = thread.section && typeof thread.section.id === 'string' ? { + id: thread.section.id, + name: thread.section.name ?? null + } : null; + const pinned = section?.id === 'Pinned' || section?.name === 'Pinned' || false; + return { + threadId: bare ? (0, _thread_ids_js__rspack_import_3.BS)(bare) : '', + title: String(thread.name || thread.preview || thread.cwd || thread.id || ''), + pinned, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + preview: typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: thread.cwd ?? null, + createdAt: thread.createdAt ?? null, + updatedAt: thread.updatedAt ?? null, + section, + projectId: thread.projectId ?? null, + project: thread.projectId ?? undefined, + status: thread.status, + modelProvider: thread.modelProvider ?? null, + model: thread.model ?? null, + originator: thread.originator ?? null + }; + } + function threadMatchesQuery(thread, needle) { + const haystacks = [ + thread.threadId, + thread.title, + thread.preview, + thread.cwd, + thread.project, + thread.projectId, + thread.model, + thread.modelProvider, + thread.originator, + thread.section?.name, + thread.section?.id + ]; + return haystacks.some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + async function readThreadMetadata(client, threadId) { + try { + const read = await client.request('thread/read', { + threadId + }); + const thread = read.thread && typeof read.thread === 'object' ? read.thread : {}; + const statusObj = thread.status; + const statusType = statusObj && typeof statusObj === 'object' && typeof statusObj.type === 'string' ? String(statusObj.type) : typeof thread.status === 'string' ? thread.status : undefined; + return { + title: typeof thread.name === 'string' ? thread.name : typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: typeof thread.cwd === 'string' ? thread.cwd : null, + status: statusType, + modelProvider: typeof thread.modelProvider === 'string' ? thread.modelProvider : null, + model: typeof thread.model === 'string' ? thread.model : null + }; + } catch (error) { + throw maybeRemoteThreadError(error, threadId); + } + } + async function listTurnsFull(client, threadId, { limit, full, cursor, title }) { + const state = cursor ? (0, _payload_budget_js__rspack_import_4.i3)(READ_PAGE_PREFIX, cursor) : null; + if (state && (state.threadId !== threadId || state.full !== full || !Number.isSafeInteger(state.skip) || state.skip < 0 || state.rpcCursor !== null && typeof state.rpcCursor !== 'string' || state.field !== undefined && ![ + 'text', + 'userText', + 'assistantText' + ].includes(state.field) || state.offset !== undefined && (!Number.isSafeInteger(state.offset) || state.offset < 0))) { + throw new Error('Read cursor does not match this thread or mode'); + } + let rpcCursor = state?.rpcCursor ?? undefined; + let skip = state?.skip ?? 0; + let fragmentField = state?.field; + let fragmentOffset = state?.offset ?? 0; + let itemsView = true; + let method = 'thread/turns/list'; + const seen = new Set(); + const turns = []; + const pageCursor = (source, index, field, offset)=>(0, _payload_budget_js__rspack_import_4.oI)(READ_PAGE_PREFIX, { + threadId, + full, + rpcCursor: source ?? null, + skip: index, + ...field ? { + field, + offset + } : {} + }); + const fits = (candidate)=>(0, _payload_budget_js__rspack_import_4.sQ)({ + threadId, + turns: [ + ...turns, + candidate + ], + backend: 'app-server', + limit, + full, + complete: false, + nextCursor: READ_PAGE_PREFIX, + title: title ? (0, _payload_budget_js__rspack_import_4.aA)(title).title : undefined, + warnings: [ + 'Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment' + ], + exitCode: 0 + }) <= 614400; + while(turns.length < limit){ + let result; + try { + result = await client.request(method, { + threadId, + limit: Math.min(10, Math.max(limit - turns.length, skip + 1)), + sortDirection: full ? 'asc' : 'desc', + ...method === 'thread/turns/list' && itemsView ? { + itemsView: 'full' + } : {}, + ...rpcCursor !== undefined ? { + cursor: rpcCursor + } : {} + }); + } catch (error) { + const code = error?.rpc?.code; + if (turns.length === 0 && code === -32602 && itemsView) { + itemsView = false; + continue; + } + if (turns.length === 0 && code === -32601 && method === 'thread/turns/list') { + method = 'thread/items/list'; + continue; + } + throw maybeRemoteThreadError(error, threadId); + } + if (!Array.isArray(result?.data)) throw new Error(`${method} missing data array for ${threadId}`); + if (skip > result.data.length) throw new Error('Read cursor is stale'); + const pageTurns = normalizeTurns(result.data); + for(let index = skip; index < pageTurns.length; index++){ + const turn = pageTurns[index]; + if (!fragmentField && turns.length && !fits(turn)) { + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index) + }; + } + if (!fragmentField && fits(turn)) { + turns.push(turn); + } else { + const fields = [ + 'text', + 'userText', + 'assistantText' + ].filter((field)=>typeof turn[field] === 'string' && turn[field].length > 0); + let fieldIndex = fragmentField ? fields.indexOf(fragmentField) : 0; + if (fieldIndex < 0) throw new Error('Read fragment cursor is stale'); + for(; fieldIndex < fields.length; fieldIndex++){ + const field = fields[fieldIndex]; + const value = turn[field]; + let offset = field === fragmentField ? fragmentOffset : 0; + if (offset > value.length) throw new Error('Read fragment cursor is stale'); + while(offset < value.length){ + let size = Math.min(value.length - offset, 32768); + let fragment; + do { + let end = offset + size; + if (end < value.length && /[\uD800-\uDBFF]/u.test(value[end - 1])) end--; + const chunk = value.slice(offset, end); + fragment = { + turnKey: turn.turnKey, + role: turn.role, + text: field === 'text' ? chunk : '', + ...field === 'userText' ? { + userText: chunk + } : {}, + ...field === 'assistantText' ? { + assistantText: chunk + } : {}, + startedAt: turn.startedAt, + endedAt: turn.endedAt, + textTruncated: true, + continuation: { + field, + offsetChars: offset, + totalChars: value.length, + fieldComplete: end === value.length, + turnComplete: end === value.length && fieldIndex === fields.length - 1 + } + }; + if (fits(fragment)) { + size = end - offset; + break; + } + size = Math.floor(size / 2); + }while (size > 0); + if (size === 0) { + if (turns.length) return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) + }; + throw new Error('Read fragment metadata exceeds the MCP result byte budget'); + } + turns.push(fragment); + offset += size; + if (turns.length >= limit || offset < value.length && !fits({ + ...fragment, + text: 'x'.repeat(32768) + })) { + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) + }; + } + } + } + fragmentField = undefined; + fragmentOffset = 0; + } + if (turns.length >= limit) { + const nextCursor = index + 1 < pageTurns.length ? pageCursor(rpcCursor, index + 1) : result.nextCursor ? pageCursor(result.nextCursor, 0) : null; + return { + turns: chronologicalPage(turns, full), + complete: nextCursor === null, + nextCursor + }; + } + } + if (result.nextCursor == null) return { + turns: chronologicalPage(turns, full), + complete: true, + nextCursor: null + }; + if (typeof result.nextCursor !== 'string' || !result.nextCursor || seen.has(result.nextCursor)) { + throw new Error(`${method} returned an invalid or repeated cursor`); + } + seen.add(result.nextCursor); + rpcCursor = result.nextCursor; + skip = 0; + } + return { + turns: chronologicalPage(turns, full), + complete: false, + nextCursor: pageCursor(rpcCursor, skip) + }; + } + function chronologicalPage(turns, full) { + if (full) return turns; + const groups = []; + for (const turn of turns){ + const last = groups.at(-1); + if (last?.[0]?.turnKey === turn.turnKey) last.push(turn); + else groups.push([ + turn + ]); + } + return groups.reverse().flat(); + } + function normalizeTurns(raw) { + const turns = []; + for (const [index, item] of raw.entries()){ + if (!item || typeof item !== 'object') continue; + const row = item; + const id = String(row.id ?? row.turnId ?? index); + const items = Array.isArray(row.items) ? row.items : null; + if (items) { + turns.push(normalizeTurnWithItems(id, row, items)); + continue; + } + turns.push(normalizeLeafItem(id, row)); + } + return turns; + } + function normalizeTurnWithItems(id, row, items) { + let userText = ''; + let assistantText = ''; + let toolText = ''; + let reasoningText = ''; + for (const entry of items){ + if (!entry || typeof entry !== 'object') continue; + const item = entry; + const type = String(item.type ?? item.role ?? ''); + const text = extractText(item); + if (/user/i.test(type)) userText = joinText(userText, text); + else if (/agent|assistant|message/i.test(type) && !/reasoning|tool/i.test(type)) { + assistantText = joinText(assistantText, text); + } else if (/tool/i.test(type)) toolText = joinText(toolText, text || type); + else if (/reasoning/i.test(type)) reasoningText = joinText(reasoningText, text); + } + let role = 'assistant'; + let text = assistantText || userText; + if (userText && !assistantText) { + role = 'user'; + text = userText; + } else if (!userText && !assistantText && toolText) { + role = 'tool'; + text = toolText; + } else if (!userText && !assistantText && reasoningText) { + role = 'reasoning'; + text = reasoningText; + } + return { + turnKey: (0, _thread_ids_js__rspack_import_3.Sr)(id), + role, + text, + ...userText ? { + userText + } : {}, + ...assistantText ? { + assistantText + } : {}, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null + }; + } + function normalizeLeafItem(id, row) { + const text = extractText(row); + const type = String(row.type ?? row.role ?? ''); + let role = 'assistant'; + if (/user/i.test(type)) role = 'user'; + else if (/tool/i.test(type)) role = 'tool'; + else if (/reasoning/i.test(type)) role = 'reasoning'; + else if (/status|system/i.test(type) && !text) role = 'status'; + return { + turnKey: (0, _thread_ids_js__rspack_import_3.Sr)(id), + role, + text, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null + }; + } + function extractText(row) { + if (typeof row.text === 'string') return row.text; + if (typeof row.content === 'string') return row.content; + if (Array.isArray(row.content)) return row.content.filter((part)=>Boolean(part) && typeof part === 'object' && !Array.isArray(part)).map((part)=>typeof part.text === 'string' ? part.text : '').filter(Boolean).join('\n'); + if (typeof row.preview === 'string') return row.preview; + return ''; + } + function joinText(left, right) { + if (!right) return left; + if (!left) return right; + return `${left}\n${right}`; + } + function isThreadNotLoaded(error) { + const message = errorMessage(error); + return /thread not loaded|not loaded|invalid thread id/i.test(message); + } + function isUnknownThread(error) { + const message = errorMessage(error); + return /no rollout found|thread not found|unknown.*thread/i.test(message); + } + function maybeRemoteThreadError(error, threadId) { + const message = errorMessage(error); + if (/thread not loaded|not loaded|no rollout found|thread not found/i.test(message)) { + const remote = (0, _remote_threads_js__rspack_import_2.KI)(threadId); + if (remote) { + return new _errors_js__rspack_import_1.Rg(threadId, remote.hostId, remote.hostName); + } + const hostId = (0, _remote_threads_js__rspack_import_2.XI)(threadId); + if (hostId) return new _errors_js__rspack_import_1.Rg(threadId, hostId, null); + } + if (error instanceof Error) return error; + return new Error(message || `app-server error for thread ${threadId}`); + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) { + return String(error.message); + } + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); + } + __webpack_require__.d(__webpack_exports__, { + Jf: ()=>appServerReadThread, + OI: ()=>appServerListThreads, + X: ()=>discoverModelProviders, + aI: ()=>appServerSearchThreads, + dM: ()=>appServerStatusProbe, + qL: ()=>appServerUnarchiveThread + }); + }, + "./src/core/chatgpt-desktop/cdp-adapter.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _cdp_session_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/cdp-session.ts"); + var _cdp_dom_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/cdp-dom.ts"); + var _thread_ids_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + var _loopback_js__rspack_import_5 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + var _project_state_js__rspack_import_6 = __webpack_require__("./src/core/chatgpt-desktop/project-state.ts"); + class CdpChatGptDesktopAdapter { + #port = (0, _loopback_js__rspack_import_5.L0)(); + #session = null; + #version = null; + #pageSessionId = null; + #mainTargetId = null; + #submittedTurn = null; + async connect({ port }) { + await this.close(); + this.#port = (0, _loopback_js__rspack_import_5.L0)(process.env, port); + const session = new _cdp_session_js__rspack_import_2.a(this.#port); + try { + this.#version = await session.connect(); + this.#session = session; + await this.#attachMainWindow(); + } catch (error) { + session.close(); + this.#session = null; + throw error; + } + } + async listTargets() { + const session = this.#requireSession(); + const result = await session.send('Target.getTargets'); + return (0, _cdp_dom_js__rspack_import_3.Mr)(result.targetInfos ?? []); + } + async listThreads({ limit = 50 } = {}) { + const sessionId = await this.#ensurePageSession(); + const threads = await (0, _cdp_dom_js__rspack_import_3.AY)(this.#requireSession(), sessionId, { + limit + }); + return { + threads, + backend: 'cdp', + limit + }; + } + async readThread({ threadId, limit = 100, full = false, openTimeoutMs = 90000 }) { + const sessionId = await this.#ensurePageSession(); + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + const turns = await (0, _cdp_dom_js__rspack_import_3.JU)(this.#requireSession(), sessionId, { + limit, + full, + threadId + }); + return { + threadId, + turns, + backend: 'cdp', + limit, + full, + complete: false + }; + } + async sendMessage({ threadId, text, project, openTimeoutMs = 90000 }) { + const sessionId = await this.#ensurePageSession(); + let temporaryThreadId; + const startedNew = !threadId; + if (threadId) { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } else { + const localProject = project ? (0, _project_state_js__rspack_import_6.q)(project) : null; + if (localProject && !localProject.rootPaths.some((path)=>(0, node_fs__rspack_import_0.existsSync)(path))) { + throw new _errors_js__rspack_import_1.Kn(localProject.label, `none of its configured workspace roots exists: ${localProject.rootPaths.map((path)=>JSON.stringify(path)).join(', ') || '(none)'}. Update the Desktop project root before sending.`); + } + await (0, _cdp_dom_js__rspack_import_3.s0)(this.#requireSession(), sessionId, { + project: localProject?.label ?? project, + projectId: localProject?.id + }); + const threads = await (0, _cdp_dom_js__rspack_import_3.AY)(this.#requireSession(), sessionId, { + limit: 20 + }); + const temp = threads.find((t)=>t.threadId.startsWith(_cdp_dom_js__rspack_import_3.hA) && t.selected) ?? threads.find((t)=>t.threadId.startsWith(_cdp_dom_js__rspack_import_3.hA)); + if (temp) temporaryThreadId = temp.threadId; + } + const before = await (0, _cdp_dom_js__rspack_import_3.YX)(this.#requireSession(), sessionId); + const { sentVia } = await (0, _cdp_dom_js__rspack_import_3.lh)(this.#requireSession(), sessionId, text); + let conversationId; + if (startedNew || threadId && (0, _thread_ids_js__rspack_import_4.vr)(threadId)) { + const resolveMs = Math.min(openTimeoutMs, 60000); + try { + conversationId = await (0, _cdp_dom_js__rspack_import_3.z9)(this.#requireSession(), sessionId, { + timeoutMs: resolveMs + }); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', + reason: 'conversation-id-unresolved' + }); + } + } + const durable = (0, _cdp_dom_js__rspack_import_3.X8)(conversationId, threadId); + if (!durable) { + throw new Error('ChatGPT Desktop send accepted but no durable conversation id was available; ' + 'refusing to return a local:client-new-thread:* id as threadId'); + } + this.#submittedTurn = { + threadId: durable, + turnKey: before.lastTurnKey + }; + return { + threadId: durable, + temporaryThreadId, + conversationId: conversationId || (0, _thread_ids_js__rspack_import_4.nn)(durable) || durable, + project, + backend: 'cdp', + experimental: false, + delivery: 'accepted', + sentVia, + message: 'Submitted via CDP composer (focus → insertText → Enter)' + }; + } + async waitForReply({ threadId, timeoutMs = 120000 }) { + const deadline = Date.now() + timeoutMs; + const sessionId = await this.#ensurePageSession(); + const expectedId = threadId && threadId !== 'new' ? (0, _thread_ids_js__rspack_import_4.nn)(threadId) ?? undefined : this.#submittedTurn ? (0, _thread_ids_js__rspack_import_4.nn)(this.#submittedTurn.threadId) ?? undefined : undefined; + if (threadId && !(0, _thread_ids_js__rspack_import_4.vr)(threadId) && threadId !== 'new') { + await this.#openAndWait(sessionId, threadId, Math.min(timeoutMs, 90000)); + } + try { + const done = await (0, _cdp_dom_js__rspack_import_3.FG)(this.#requireSession(), sessionId, { + timeoutMs: Math.max(0, deadline - Date.now()), + expectedConversationId: expectedId, + baselineTurnKey: this.#submittedTurn && (!threadId || (0, _thread_ids_js__rspack_import_4.BS)(threadId) === this.#submittedTurn.threadId) ? this.#submittedTurn.turnKey : undefined + }); + const durable = (0, _cdp_dom_js__rspack_import_3.X8)(done.conversationId, threadId) ?? (done.conversationId ? (0, _thread_ids_js__rspack_import_4.BS)(done.conversationId) : null); + if (!durable) { + throw new Error('ChatGPT Desktop reply finished without a durable conversation id on ' + '[data-response-annotation-conversation]'); + } + return { + threadId: durable, + conversationId: done.conversationId || (0, _thread_ids_js__rspack_import_4.nn)(durable) || durable, + reply: done.reply, + backend: 'cdp', + experimental: false, + delivery: 'replied' + }; + } catch (error) { + if (!(error instanceof _cdp_dom_js__rspack_import_3.aw)) throw error; + return { + threadId: threadId ?? 'unknown', + reply: '', + backend: 'cdp', + experimental: false, + delivery: 'timeout' + }; + } + } + async openThread(threadId, { openTimeoutMs = 90000 } = {}) { + const sessionId = await this.#ensurePageSession(); + try { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } catch (error) { + if (error instanceof _errors_js__rspack_import_1.fw) return { + threadId, + backend: 'cdp', + archived: true + }; + throw error; + } + return { + threadId, + backend: 'cdp' + }; + } + async status() { + try { + const version = this.#version ?? await _cdp_session_js__rspack_import_2.a.fetchVersion(this.#port); + let targetCount; + let mainWindowTargetId; + if (this.#session && !this.#session.closed) { + const targets = await this.listTargets(); + targetCount = targets.length; + mainWindowTargetId = (0, _cdp_dom_js__rspack_import_3.C2)(targets)?.targetId ?? null; + } else { + const list = await _cdp_session_js__rspack_import_2.a.fetchJsonList(this.#port); + targetCount = list.length; + } + return { + reachable: true, + port: this.#port, + host: '127.0.0.1', + browser: typeof version.Browser === 'string' ? version.Browser : undefined, + protocolVersion: typeof version['Protocol-Version'] === 'string' ? version['Protocol-Version'] : undefined, + webSocketDebuggerUrl: typeof version.webSocketDebuggerUrl === 'string' ? version.webSocketDebuggerUrl : undefined, + targetCount, + mainWindowTargetId: mainWindowTargetId ?? this.#mainTargetId, + message: 'ChatGPT Desktop CDP endpoint reachable on 127.0.0.1', + exitCode: 0 + }; + } catch (error) { + return { + reachable: false, + port: this.#port, + host: '127.0.0.1', + message: error instanceof Error ? error.message : 'ChatGPT Desktop CDP endpoint unreachable on 127.0.0.1', + exitCode: 1 + }; + } + } + async close() { + this.#pageSessionId = null; + this.#mainTargetId = null; + this.#version = null; + this.#session?.close(); + this.#session = null; + } + async #openAndWait(sessionId, threadId, openTimeoutMs) { + await (0, _cdp_dom_js__rspack_import_3.Kl)(this.#requireSession(), sessionId, threadId); + await (0, _cdp_dom_js__rspack_import_3.Zx)(this.#requireSession(), sessionId, { + timeoutMs: openTimeoutMs, + threadId + }); + } + async #attachMainWindow() { + const targets = await this.listTargets(); + const main = (0, _cdp_dom_js__rspack_import_3.C2)(targets); + if (!main) { + throw new _errors_js__rspack_import_1.sO(`No ChatGPT Desktop main window target with url ${JSON.stringify('app://-/index.html')}`); + } + const attached = await this.#requireSession().send('Target.attachToTarget', { + targetId: main.targetId, + flatten: true + }); + this.#mainTargetId = main.targetId; + this.#pageSessionId = attached.sessionId; + } + async #ensurePageSession() { + this.#requireSession(); + if (!this.#pageSessionId) await this.#attachMainWindow(); + if (!this.#pageSessionId) { + throw new _errors_js__rspack_import_1.sO('Failed to attach to ChatGPT Desktop main window'); + } + return this.#pageSessionId; + } + #requireSession() { + if (!this.#session || this.#session.closed) { + throw new _errors_js__rspack_import_1.sO('Not connected to ChatGPT Desktop CDP. Call connect({ port }) after relaunching with --remote-debugging-port.'); + } + return this.#session; + } + } + __webpack_require__.d(__webpack_exports__, { + a: ()=>CdpChatGptDesktopAdapter + }); + }, + "./src/core/chatgpt-desktop/cdp-dom.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _codex_thread_id_js__rspack_import_1 = __webpack_require__("./src/core/codex/thread-id.js"); + const MAIN_WINDOW_URL = 'app://-/index.html'; + const IGNORED_TARGET_URL_MARKERS = [ + 'initialRoute=%2Favatar-overlay', + 'detached-window.html', + 'chatgpt.com', + 'codex-sandbox' + ]; + const ATTR = { + threadRow: 'data-app-action-sidebar-thread-row', + threadId: 'data-app-action-sidebar-thread-id', + threadTitle: 'data-app-action-sidebar-thread-title', + threadPinned: 'data-app-action-sidebar-thread-pinned', + threadSelected: 'data-app-action-sidebar-thread-selected', + threadKind: 'data-app-action-sidebar-thread-kind', + turnKey: 'data-turn-key', + userBubble: 'data-user-message-bubble', + timelineScroll: 'data-app-action-timeline-scroll', + virtualizedTurn: 'data-virtualized-turn-content', + composer: 'data-codex-composer', + finalAssistant: 'data-local-conversation-final-assistant', + markdownTextStyle: 'data-markdown-text-style', + conversationAnnotation: 'data-response-annotation-conversation' + }; + const MAIN_CONTENT_SURFACE_CLASS_PREFIX = '_MainContentSurface'; + const HISTORY_GAP_PREFIX = 'history-gap:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const LOADING_TASK_TEXT = 'Loading task…'; + const DEFAULT_OPEN_TIMEOUT_MS = 90000; + const DEFAULT_CONVERSATION_RESOLVE_MS = 60000; + const DEFAULT_FULL_READ_IDLE_WHEELS = 8; + const DEFAULT_FULL_READ_MAX_WHEELS = 400; + const FULL_READ_WHEEL_DELTA_Y = -800; + const COMPOSER_SELECTOR = `[${ATTR.composer}="true"][contenteditable="true"]`; + const SEND_BUTTON_SELECTOR = 'button[aria-label="Send"]'; + const STOP_BUTTON_SELECTOR = 'main button[aria-label="Stop"]'; + const FINAL_ASSISTANT_SELECTOR = `[${ATTR.finalAssistant}="true"]`; + const ASSISTANT_MESSAGE_SELECTOR = `[${ATTR.finalAssistant}="true"] [${ATTR.markdownTextStyle}="assistant-message"]`; + const CONVERSATION_ANNOTATION_SELECTOR = `[${ATTR.conversationAnnotation}]`; + const SELECTORS = { + threadRow: `[role="button"][${ATTR.threadRow}]`, + turn: `[${ATTR.turnKey}]`, + userBubble: `[${ATTR.userBubble}]`, + timelineScroll: `[${ATTR.timelineScroll}]`, + composer: COMPOSER_SELECTOR, + sendButton: SEND_BUTTON_SELECTOR, + stopButton: STOP_BUTTON_SELECTOR, + finalAssistant: FINAL_ASSISTANT_SELECTOR, + assistantMessage: ASSISTANT_MESSAGE_SELECTOR, + conversationAnnotation: CONVERSATION_ANNOTATION_SELECTOR, + newChatFallback: 'button' + }; + function newChatInProjectSelector(project) { + return `button[aria-label=${JSON.stringify(`Start new chat in ${project}`)}]`; + } + function isMainWindowTarget(target) { + if (target.type !== 'page') return false; + const url = typeof target.url === 'string' ? target.url : ''; + if (url !== MAIN_WINDOW_URL) return false; + return !IGNORED_TARGET_URL_MARKERS.some((marker)=>url.includes(marker)); + } + function summarizeTargetInfos(targetInfos) { + return targetInfos.filter((t)=>typeof t.targetId === 'string' && typeof t.type === 'string').map((t)=>({ + targetId: String(t.targetId), + type: String(t.type), + title: typeof t.title === 'string' ? t.title : '', + url: typeof t.url === 'string' ? t.url : '', + attached: Boolean(t.attached) + })); + } + function pickMainWindowTarget(targets) { + return targets.find((t)=>isMainWindowTarget(t)) ?? null; + } + function dedupeThreadsById(threads) { + const byKey = new Map(); + for (const thread of threads){ + if (!thread.threadId) continue; + const key = durableThreadKey(thread.threadId); + const existing = byKey.get(key); + if (!existing) { + byKey.set(key, thread); + continue; + } + byKey.set(key, preferThreadRow(existing, thread)); + } + return [ + ...byKey.values() + ]; + } + function durableThreadKey(threadId) { + if ((0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) return threadId; + try { + return (0, _codex_thread_id_js__rspack_import_1.SX)(threadId); + } catch { + return threadId.startsWith("local:") ? threadId.slice("local:".length) || threadId : threadId; + } + } + function toDesktopForm(threadId) { + if (!threadId || (0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) return threadId; + if (threadId.startsWith("local:")) return threadId; + return `${"local:"}${threadId}`; + } + function preferThreadRow(left, right) { + const score = (row)=>(row.selected ? 4 : 0) + (row.pinned ? 2 : 0) + (row.threadId.startsWith("local:") && !(0, _codex_thread_id_js__rspack_import_1.vr)(row.threadId) ? 1 : 0); + return score(right) > score(left) ? right : left; + } + const LIST_THREADS_EXPRESSION = `(() => { + const skipProjectLabels = new Set(['Recent', 'Pinned', 'Threads', 'Chats']); + const projectFor = (row) => { + let el = row.parentElement; + for (let i = 0; i < 8 && el; i += 1, el = el.parentElement) { + const attr = + el.getAttribute('data-app-action-sidebar-project') || + el.getAttribute('data-app-action-sidebar-project-name') || + el.getAttribute('data-app-action-sidebar-section-title'); + if (attr && !skipProjectLabels.has(attr)) return attr; + } + return undefined; + }; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const seen = new Set(); + const out = []; + for (const row of rows) { + const threadId = row.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''; + if (!threadId || seen.has(threadId)) continue; + seen.add(threadId); + const project = projectFor(row); + out.push({ + threadId, + hostId: row.getAttribute('data-app-action-sidebar-thread-host-id') || null, + title: row.getAttribute(${JSON.stringify(ATTR.threadTitle)}) || (row.textContent || '').trim(), + pinned: row.getAttribute(${JSON.stringify(ATTR.threadPinned)}) === 'true', + selected: row.getAttribute(${JSON.stringify(ATTR.threadSelected)}) === 'true', + kind: row.getAttribute(${JSON.stringify(ATTR.threadKind)}) || 'unknown', + ...(project ? { project } : {}), + }); + } + return out; +})()`; + function openThreadExpression(threadId) { + const bare = durableThreadKey(threadId); + const desktop = toDesktopForm(bare); + const candidates = [ + ...new Set([ + threadId, + bare, + desktop + ].filter(Boolean)) + ]; + return `(() => { + const candidates = ${JSON.stringify(candidates)}; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const row = rows.find((el) => + candidates.includes(el.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''), + ); + if (!row) return { ok: false, error: 'thread-not-found' }; + row.click(); + return { ok: true, threadId: row.getAttribute(${JSON.stringify(ATTR.threadId)}) || candidates[0] }; + })()`; + } + function navigateToThreadExpression(threadId) { + const bare = durableThreadKey(threadId); + return `(async () => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; + let scanned = 0; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); + if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { + await router.navigate(${JSON.stringify(`/local/${bare}`)}); + return { ok: true, via: 'router' }; + } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + return { ok: false, error: 'router-not-found' }; + })()`; + } + function startNewChatExpression(project, projectId) { + const projectSel = project ? newChatInProjectSelector(project) : null; + return `(() => { + const projectSel = ${JSON.stringify(projectSel)}; + if (projectSel) { + const preferred = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(projectSel) ||` : ''} document.querySelector(projectSel); + if (preferred) { + const row = preferred.closest('[data-app-action-sidebar-project-row]') || preferred.parentElement; + row?.scrollIntoView({ block: 'center' }); + const rowBox = row?.getBoundingClientRect(); + return { ok: true, via: 'project', project: ${JSON.stringify(project ?? null)}, + disabled: preferred.disabled || preferred.getAttribute('aria-disabled') === 'true', + hoverX: rowBox?.left + rowBox?.width / 2, hoverY: rowBox?.top + rowBox?.height / 2 }; + } + return { ok: false, error: 'project-not-found' }; + } + const buttons = Array.from(document.querySelectorAll('button')); + const fallback = buttons.find((btn) => (btn.textContent || '').trim() === 'New chat'); + if (!fallback) return { ok: false, error: 'new-chat-not-found' }; + fallback.click(); + return { ok: true, via: 'fallback' }; + })()`; + } + function projectButtonBoxExpression(project, projectId) { + return `(() => { + const button = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(${JSON.stringify(newChatInProjectSelector(project))}) ||` : ''} document.querySelector(${JSON.stringify(newChatInProjectSelector(project))}); + if (!button) return null; + const box = button.getBoundingClientRect(); + return { x: box.left + box.width / 2, y: box.top + box.height / 2, + disabled: button.disabled || button.getAttribute('aria-disabled') === 'true', + visible: box.width > 0 && box.height > 0 && box.x >= 0 && box.y >= 0 + && box.x < innerWidth && box.y < innerHeight && getComputedStyle(button.parentElement).opacity !== '0' }; + })()`; + } + function newChatViewExpression(project) { + return `(() => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; let scanned = 0; let path = ''; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { path = router.state.location.pathname; break; } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + const composer = !!document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + const scope = document.querySelector('[data-composer-navigation-target="workspace-project"]')?.getAttribute('aria-label') || ''; + const expected = ${JSON.stringify(project ? `Change project: ${project}` : '')}; + return { ready: path === '/' && composer && (expected ? scope === expected : (!scope || scope === 'Choose project')), path, composer, scope }; + })()`; + } + const CLEAR_NEW_CHAT_PROJECT_EXPRESSION = `(() => { + const button = document.querySelector('[data-clear-project-button="true"]'); + if (!button || button.disabled) return false; + button.click(); + return true; +})()`; + const LOADING_TASK_GONE_EXPRESSION = `(() => { + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const text = (surface.innerText || '').replace(/\\u2026/g, '…'); + return !text.includes(${JSON.stringify(LOADING_TASK_TEXT)}); +})()`; + const ARCHIVED_THREAD_EXPRESSION = `(() => Array.from(document.querySelectorAll('main')) + .some((main) => (main.innerText || '').includes('This task is archived') + && (main.innerText || '').includes('Unarchive this task to open it')))()`; + const FOCUS_COMPOSER_EXPRESSION = `(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + if (!composer) return { ok: false, error: 'composer-not-found' }; + if ((composer.textContent || '').trim()) return { ok: false, error: 'composer-has-draft' }; + if (document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)})) return { ok: false, error: 'reply-in-progress' }; + composer.focus(); + return { ok: true }; +})()`; + const REPLY_STATE_EXPRESSION = `(() => { + const stop = document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)}); + const finals = Array.from(document.querySelectorAll(${JSON.stringify(FINAL_ASSISTANT_SELECTOR)})); + const lastFinal = finals.at(-1) || null; + const annotation = lastFinal + ? lastFinal.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + || lastFinal.closest(${JSON.stringify(SELECTORS.turn)})?.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + : document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + const conversationId = annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; + const messageEl = lastFinal + ? lastFinal.querySelector(${JSON.stringify(`[${ATTR.markdownTextStyle}="assistant-message"]`)}) + : null; + const reply = messageEl ? (messageEl.innerText || '').trim() : ''; + const turns = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.turn)})); + const lastTurn = turns.at(-1); + const lastTurnKey = lastTurn ? (lastTurn.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '') : ''; + return { + stopVisible: Boolean(stop), + finalAssistantCount: finals.length, + conversationId, + reply, + lastTurnKey, + finalTurnKey: lastFinal?.closest(${JSON.stringify(SELECTORS.turn)})?.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '', + }; +})()`; + const CONVERSATION_ID_EXPRESSION = `(() => { + const annotation = document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + return annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; +})()`; + const HARVEST_VISIBLE_TURNS_EXPRESSION = `(() => { + const gapPrefix = ${JSON.stringify(HISTORY_GAP_PREFIX)}; + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const scroll = + surface.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}) + || document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + const root = scroll || surface; + const out = []; + for (const el of root.querySelectorAll(${JSON.stringify(SELECTORS.turn)})) { + const turnKey = el.getAttribute(${JSON.stringify(ATTR.turnKey)}) || ''; + if (!turnKey || turnKey.startsWith(gapPrefix)) continue; + const user = el.querySelector(${JSON.stringify(SELECTORS.userBubble)}); + const assistant = el.querySelector(${JSON.stringify(ASSISTANT_MESSAGE_SELECTOR)}); + const userText = user ? (user.innerText || '').trim() : ''; + const assistantText = assistant ? (assistant.innerText || '').trim() : ''; + const raw = (el.innerText || '').trim(); + const statusRe = /^Worked for \\d+s$/i; + let role = 'assistant'; + let text = assistantText || userText || raw; + if (user && !assistantText) { + role = 'user'; + text = userText || raw; + } else if (assistantText) { + role = 'assistant'; + text = assistantText; + } else if (!raw || statusRe.test(raw)) { + role = 'status'; + text = raw; + } + out.push({ turnKey, role, text, userText, assistantText }); + } + return out; +})()`; + const TIMELINE_SCROLL_METRICS_EXPRESSION = `(() => { + const scroll = document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + if (!scroll) return { ok: false }; + const rect = scroll.getBoundingClientRect(); + return { + ok: true, + x: Math.floor(rect.left + rect.width / 2), + y: Math.floor(rect.top + Math.min(rect.height / 2, 120)), + scrollTop: scroll.scrollTop, + scrollHeight: scroll.scrollHeight, + clientHeight: scroll.clientHeight, + }; +})()`; + async function listThreadsFromDom(session, sessionId, { limit = 50 } = {}) { + const rows = await session.evaluate(LIST_THREADS_EXPRESSION, { + sessionId + }); + if (!Array.isArray(rows)) return []; + return dedupeThreadsById(rows).slice(0, limit).map((row)=>({ + threadId: String(row.threadId), + title: String(row.title ?? ''), + pinned: Boolean(row.pinned), + selected: Boolean(row.selected), + kind: String(row.kind ?? 'unknown'), + ...row.hostId ? { + hostId: row.hostId, + location: row.hostId === 'local' ? 'local' : 'remote' + } : {}, + ...row.project ? { + project: String(row.project) + } : {} + })); + } + async function openThreadInDom(session, sessionId, threadId) { + const current = await readConversationIdFromDom(session, sessionId); + if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + if ((0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { + limit: 200 + }); + if (rows.some((row)=>row.selected && row.threadId === threadId)) return; + } + const result = await session.evaluate(openThreadExpression(threadId), { + sessionId + }); + if (!result?.ok) { + if (result?.error === 'thread-not-found' && !(0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const navigated = await session.evaluate(navigateToThreadExpression(threadId), { + sessionId + }); + if (navigated?.ok) return; + } + throw new Error(result?.error === 'thread-not-found' ? `ChatGPT Desktop thread not found: ${threadId}` : `Failed to open ChatGPT Desktop thread ${threadId}`); + } + } + async function waitForLoadingTaskGone(session, sessionId, { timeoutMs = DEFAULT_OPEN_TIMEOUT_MS, threadId } = {}) { + const deadline = Date.now() + timeoutMs; + let loadingGone = false; + let lastConversationId = ''; + let archivedSince = null; + while(Date.now() < deadline){ + if (threadId && threadId !== 'new' && await session.evaluate(ARCHIVED_THREAD_EXPRESSION, { + sessionId + }) === true) { + archivedSince ??= Date.now(); + if (Date.now() - archivedSince >= 500) { + throw new _errors_js__rspack_import_0.fw(threadId); + } + } else { + archivedSince = null; + } + const gone = await session.evaluate(LOADING_TASK_GONE_EXPRESSION, { + sessionId + }); + loadingGone ||= gone; + if (gone) { + const current = threadId ? await readConversationIdFromDom(session, sessionId) : ''; + lastConversationId = current; + if (!threadId) return; + if (threadId === 'new' || (0, _codex_thread_id_js__rspack_import_1.vr)(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { + limit: 200 + }); + const selected = rows.find((row)=>row.selected); + if (!current && (!selected || (0, _codex_thread_id_js__rspack_import_1.vr)(selected.threadId))) return; + } else if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + } + await delay(250); + } + throw new Error(loadingGone ? `ChatGPT Desktop timed out waiting for conversation ${JSON.stringify(threadId)}; displayed conversation ${JSON.stringify(lastConversationId)} after ${timeoutMs}ms` : `ChatGPT Desktop thread still showing ${JSON.stringify(LOADING_TASK_TEXT)} after ${timeoutMs}ms`); + } + async function startNewChatInDom(session, sessionId, { project, projectId, navigationTimeoutMs = 3000 } = {}) { + const result = await session.evaluate(startNewChatExpression(project, projectId), { + sessionId + }); + if (!result?.ok) { + throw new Error(project ? `ChatGPT Desktop new-chat button not found for project ${JSON.stringify(project)}` : 'ChatGPT Desktop "New chat" button not found'); + } + if (result.via === 'project') { + if (result.disabled) throw new _errors_js__rspack_import_0.Kn(project, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (![ + result.hoverX, + result.hoverY + ].every(Number.isFinite)) { + throw new Error(`ChatGPT Desktop project button has no visible box for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: result.hoverX, + y: result.hoverY + }, { + sessionId + }); + const button = await session.evaluate(projectButtonBoxExpression(project, projectId), { + sessionId + }); + if (button?.disabled) throw new _errors_js__rspack_import_0.Kn(project, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (!button?.visible || !Number.isFinite(button.x) || !Number.isFinite(button.y)) { + throw new Error(`ChatGPT Desktop project button did not appear after hover for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: button.x, + y: button.y + }, { + sessionId + }); + await session.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: button.x, + y: button.y, + button: 'left', + clickCount: 1 + }, { + sessionId + }); + await session.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: button.x, + y: button.y, + button: 'left', + clickCount: 1 + }, { + sessionId + }); + } + let observed = { + ready: false, + path: '', + composer: false, + scope: '' + }; + const deadline = Date.now() + navigationTimeoutMs; + while(Date.now() < deadline){ + observed = await session.evaluate(newChatViewExpression(project), { + sessionId + }); + if (observed?.ready) return { + via: result.via ?? 'unknown' + }; + if (!project && observed?.path === '/' && observed?.composer && observed.scope.startsWith('Change project: ')) { + await session.evaluate(CLEAR_NEW_CHAT_PROJECT_EXPRESSION, { + sessionId + }); + } + await delay(100); + } + throw new _errors_js__rspack_import_0.XL(project, `route ${JSON.stringify(observed?.path)}, composer ${Boolean(observed?.composer)}, project ${JSON.stringify(observed?.scope)}`); + } + async function focusComposer(session, sessionId) { + const focused = await session.evaluate(FOCUS_COMPOSER_EXPRESSION, { + sessionId + }); + if (!focused?.ok) { + if (focused?.error === 'composer-has-draft') throw new _errors_js__rspack_import_0.oN(); + throw new Error(`ChatGPT Desktop composer unavailable: ${focused?.error || 'not found'}`); + } + } + async function submitComposerWithEnter(session, sessionId) { + const base = { + windowsVirtualKeyCode: 13, + code: 'Enter', + key: 'Enter', + text: '\r', + unmodifiedText: '\r' + }; + await session.send('Input.dispatchKeyEvent', { + type: 'rawKeyDown', + ...base + }, { + sessionId + }); + await session.send('Input.dispatchKeyEvent', { + type: 'keyUp', + ...base + }, { + sessionId + }); + } + async function sendMessageInDom(session, sessionId, text) { + await focusComposer(session, sessionId); + await session.send('Input.insertText', { + text + }, { + sessionId + }); + try { + await submitComposerWithEnter(session, sessionId); + const deadline = Date.now() + 3000; + while(Date.now() < deadline){ + const cleared = await session.evaluate(`(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + return Boolean(composer && !(composer.textContent || '').trim()); + })()`, { + sessionId + }); + if (cleared) return { + sentVia: 'enter' + }; + await delay(100); + } + throw new Error('Desktop composer did not acknowledge submission'); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', + reason: 'submission-unconfirmed' + }); + } + } + async function readReplyState(session, sessionId) { + const state = await session.evaluate(REPLY_STATE_EXPRESSION, { + sessionId + }); + return { + stopVisible: Boolean(state?.stopVisible), + finalAssistantCount: Number(state?.finalAssistantCount ?? 0), + conversationId: String(state?.conversationId ?? ''), + reply: String(state?.reply ?? ''), + lastTurnKey: String(state?.lastTurnKey ?? ''), + finalTurnKey: String(state?.finalTurnKey ?? '') + }; + } + async function readConversationIdFromDom(session, sessionId) { + const value = await session.evaluate(CONVERSATION_ID_EXPRESSION, { + sessionId + }); + return typeof value === 'string' ? value.trim() : ''; + } + async function waitForConversationId(session, sessionId, { timeoutMs = DEFAULT_CONVERSATION_RESOLVE_MS } = {}) { + const deadline = Date.now() + timeoutMs; + while(Date.now() < deadline){ + const id = await readConversationIdFromDom(session, sessionId); + if (id && !(0, _codex_thread_id_js__rspack_import_1.vr)(id) && !id.startsWith(TEMP_THREAD_ID_PREFIX)) { + return id; + } + await delay(200); + } + throw new Error(`ChatGPT Desktop conversation id did not appear on [data-response-annotation-conversation] within ${timeoutMs}ms`); + } + function resolveDurableThreadId(conversationId, fallbackThreadId) { + if (conversationId && !(0, _codex_thread_id_js__rspack_import_1.vr)(conversationId)) { + return toDesktopForm(conversationId); + } + if (fallbackThreadId && !(0, _codex_thread_id_js__rspack_import_1.vr)(fallbackThreadId) && fallbackThreadId !== 'new') { + return toDesktopForm(fallbackThreadId); + } + return null; + } + class ReplyTimeoutError extends Error { + } + async function waitForReplyDone(session, sessionId, { timeoutMs = 120000, baselineFinalCount = 0, expectedConversationId, baselineTurnKey } = {}) { + const deadline = Date.now() + timeoutMs; + let sawStop = false; + const started = Date.now(); + while(Date.now() < deadline){ + const state = await readReplyState(session, sessionId); + if (state.stopVisible) sawStop = true; + expectedConversationId ??= state.conversationId || undefined; + if (expectedConversationId && state.conversationId && durableThreadKey(state.conversationId) !== durableThreadKey(expectedConversationId)) { + throw new Error('ChatGPT Desktop selected conversation changed while waiting'); + } + const latestIsFinal = Boolean(state.finalTurnKey) && state.finalTurnKey === state.lastTurnKey; + const hasNewFinal = (state.finalAssistantCount > baselineFinalCount || sawStop) && Boolean(state.reply) && latestIsFinal && (!baselineTurnKey || state.lastTurnKey !== baselineTurnKey); + if (!state.stopVisible && hasNewFinal && (sawStop || Date.now() - started > 1500)) { + return { + reply: state.reply, + conversationId: state.conversationId + }; + } + await delay(200); + } + throw new ReplyTimeoutError(`ChatGPT Desktop reply did not finish within ${timeoutMs}ms`); + } + function normalizeTurn(raw) { + const role = raw.role === 'user' || raw.role === 'status' || raw.role === 'assistant' ? raw.role : raw.assistantText ? 'assistant' : raw.userText ? 'user' : 'assistant'; + return { + turnKey: String(raw.turnKey ?? ''), + role, + text: String(raw.text ?? raw.assistantText ?? raw.userText ?? ''), + userText: raw.userText ? String(raw.userText) : undefined, + assistantText: raw.assistantText ? String(raw.assistantText) : undefined + }; + } + async function harvestVisibleTurns(session, sessionId) { + const rows = await session.evaluate(HARVEST_VISIBLE_TURNS_EXPRESSION, { + sessionId + }); + if (!Array.isArray(rows)) return []; + return rows.map(normalizeTurn).filter((t)=>t.turnKey && !t.turnKey.startsWith(HISTORY_GAP_PREFIX)); + } + async function readTurnsFromDom(session, sessionId, { limit = 100, full = false, threadId, idleWheels = DEFAULT_FULL_READ_IDLE_WHEELS, maxWheels = DEFAULT_FULL_READ_MAX_WHEELS } = {}) { + const collected = new Map(); + const order1 = []; + const ingest = async ()=>{ + const older = []; + for (const turn of (await harvestVisibleTurns(session, sessionId))){ + if (!collected.has(turn.turnKey)) older.push(turn.turnKey); + collected.set(turn.turnKey, turn); + } + order1.unshift(...older); + }; + await ingest(); + if (!full) { + const visible = order1.map((key)=>collected.get(key)); + return limit > 0 ? visible.slice(-limit) : visible; + } + let idle = 0; + for(let step = 0; step < maxWheels; step++){ + const before = collected.size; + const metrics = await session.evaluate(TIMELINE_SCROLL_METRICS_EXPRESSION, { + sessionId + }); + if (!metrics?.ok || metrics.x == null || metrics.y == null) break; + await session.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: metrics.x, + y: metrics.y, + deltaX: 0, + deltaY: FULL_READ_WHEEL_DELTA_Y + }, { + sessionId + }); + await delay(120); + await ingest(); + if (collected.size === before) { + idle += 1; + if (idle >= idleWheels) break; + } else { + idle = 0; + } + if (threadId && collected.has(threadId)) break; + } + const all = order1.map((key)=>collected.get(key)); + return limit > 0 ? all.slice(0, limit) : all; + } + function delay(ms) { + return new Promise((resolve)=>setTimeout(resolve, ms)); + } + __webpack_require__.d(__webpack_exports__, { + AY: ()=>listThreadsFromDom, + C2: ()=>pickMainWindowTarget, + FG: ()=>waitForReplyDone, + JU: ()=>readTurnsFromDom, + Kl: ()=>openThreadInDom, + Mr: ()=>summarizeTargetInfos, + X8: ()=>resolveDurableThreadId, + YX: ()=>readReplyState, + Zx: ()=>waitForLoadingTaskGone, + aw: ()=>ReplyTimeoutError, + lh: ()=>sendMessageInDom, + s0: ()=>startNewChatInDom, + z9: ()=>waitForConversationId + }, { + hA: TEMP_THREAD_ID_PREFIX + }); + }, + "./src/core/chatgpt-desktop/cdp-session.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _loopback_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + class CdpSession { + #port; + #ws = null; + #nextId = 1; + #pending = new Map(); + #closed = false; + constructor(port){ + this.#port = port; + } + get port() { + return this.#port; + } + get closed() { + return this.#closed || this.#ws === null || this.#ws.readyState === WebSocket.CLOSED; + } + static async fetchVersion(port, { signal } = {}) { + const base = (0, _loopback_js__rspack_import_1.NJ)(port); + let response; + try { + response = await fetch(`${base}/json/version`, { + signal: signal ? AbortSignal.any([ + signal, + AbortSignal.timeout(5000) + ]) : AbortSignal.timeout(5000), + redirect: 'error' + }); + } catch (error) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP not reachable at ${base}/json/version: ` + (error instanceof Error ? error.message : String(error))); + } + if (!response.ok) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP /json/version returned HTTP ${response.status}`); + } + const version = await response.json(); + if (typeof version.webSocketDebuggerUrl === 'string') { + (0, _loopback_js__rspack_import_1.jv)(version.webSocketDebuggerUrl); + } + return version; + } + static async fetchJsonList(port, { signal } = {}) { + const base = (0, _loopback_js__rspack_import_1.NJ)(port); + let response; + try { + response = await fetch(`${base}/json/list`, { + signal: signal ? AbortSignal.any([ + signal, + AbortSignal.timeout(5000) + ]) : AbortSignal.timeout(5000), + redirect: 'error' + }); + } catch (error) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP not reachable at ${base}/json/list: ` + (error instanceof Error ? error.message : String(error))); + } + if (!response.ok) { + throw new _errors_js__rspack_import_0.sO(`ChatGPT Desktop CDP /json/list returned HTTP ${response.status}`); + } + const body = await response.json(); + return Array.isArray(body) ? body : []; + } + async connect({ signal } = {}) { + if (this.#ws && !this.closed) { + throw new Error('CDP session already connected'); + } + const version = await CdpSession.fetchVersion(this.#port, { + signal + }); + const rawUrl = version.webSocketDebuggerUrl; + if (typeof rawUrl !== 'string' || !rawUrl) { + throw new _errors_js__rspack_import_0.sO('CDP /json/version omitted webSocketDebuggerUrl'); + } + const url = (0, _loopback_js__rspack_import_1.jv)(rawUrl); + signal?.throwIfAborted(); + await new Promise((resolve, reject)=>{ + const ws = new WebSocket(url); + this.#ws = ws; + const cleanup = ()=>{ + clearTimeout(timer); + signal?.removeEventListener('abort', onAbort); + }; + const onAbort = ()=>{ + cleanup(); + ws.close(); + reject(new _errors_js__rspack_import_0.sO('CDP connect aborted or timed out')); + }; + const timer = setTimeout(onAbort, 5000); + signal?.addEventListener('abort', onAbort, { + once: true + }); + ws.addEventListener('open', ()=>{ + cleanup(); + resolve(); + }, { + once: true + }); + ws.addEventListener('error', ()=>{ + cleanup(); + reject(new _errors_js__rspack_import_0.sO(`CDP WebSocket failed for ${url}`)); + }, { + once: true + }); + ws.addEventListener('message', (event)=>this.#onMessage(event.data)); + ws.addEventListener('close', ()=>{ + cleanup(); + reject(new _errors_js__rspack_import_0.sO('CDP WebSocket closed before connecting')); + this.#closed = true; + for (const [, pending] of this.#pending){ + clearTimeout(pending.timer); + pending.reject(new _errors_js__rspack_import_0.sO('CDP WebSocket closed')); + } + this.#pending.clear(); + }); + }); + this.#closed = false; + return version; + } + async send(method, params, { sessionId } = {}) { + if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) { + throw new _errors_js__rspack_import_0.sO('CDP session is not connected'); + } + const id = this.#nextId++; + const payload = { + id, + method + }; + if (params !== undefined) payload.params = params; + if (sessionId !== undefined) payload.sessionId = sessionId; + return await new Promise((resolve, reject)=>{ + const timer = setTimeout(()=>{ + this.#pending.delete(id); + reject(new _errors_js__rspack_import_0.sO(`CDP ${method} timed out`)); + }, 10000); + this.#pending.set(id, { + timer, + resolve: (value)=>resolve(value), + reject + }); + try { + this.#ws.send(JSON.stringify(payload)); + } catch (error) { + clearTimeout(timer); + this.#pending.delete(id); + reject(error); + } + }); + } + async evaluate(expression, { sessionId, awaitPromise = true } = {}) { + const result = await this.send('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise + }, { + sessionId + }); + if (result.exceptionDetails) { + const text = result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Runtime.evaluate failed'; + throw new Error(text); + } + return result.result?.value; + } + close() { + this.#closed = true; + for (const pending of this.#pending.values()){ + clearTimeout(pending.timer); + pending.reject(new _errors_js__rspack_import_0.sO('CDP session closed')); + } + this.#pending.clear(); + this.#ws?.close(); + this.#ws = null; + } + #onMessage(data) { + let message; + try { + message = JSON.parse(String(data)); + } catch { + return; + } + if (!message || typeof message !== 'object' || typeof message.id !== 'number') return; + const pending = this.#pending.get(message.id); + if (!pending) return; + this.#pending.delete(message.id); + clearTimeout(pending.timer); + if (message.error) { + pending.reject(new Error(message.error.message || 'CDP error')); + return; + } + pending.resolve(message.result); + } + } + __webpack_require__.d(__webpack_exports__, { + a: ()=>CdpSession + }); + }, + "./src/core/chatgpt-desktop/errors.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + class ArchivedThreadError extends Error { + code = 'ARCHIVED_THREAD'; + reason = 'archived-thread'; + delivery = 'rejected'; + threadId; + constructor(threadId){ + super(`ChatGPT Desktop thread ${JSON.stringify(threadId)} is archived. Read it through app-server, or pass unarchive: true on send to unarchive before sending.`); + this.name = 'ArchivedThreadError'; + this.threadId = threadId; + } + } + class ComposerDraftError extends Error { + code = 'COMPOSER_HAS_DRAFT'; + reason = 'composer-has-draft'; + delivery = 'rejected'; + constructor(){ + super('ChatGPT Desktop composer contains an unsent draft; inspect it in Desktop before sending.'); + this.name = 'ComposerDraftError'; + } + } + class ProjectUnavailableError extends Error { + code = 'PROJECT_UNAVAILABLE'; + reason = 'project-unavailable'; + delivery = 'rejected'; + constructor(project, detail){ + super(`ChatGPT Desktop cannot start a chat in project ${JSON.stringify(project)}: ${detail}`); + this.name = 'ProjectUnavailableError'; + } + } + class NewChatNavigationError extends Error { + code = 'NEW_CHAT_NAVIGATION_FAILED'; + reason = 'new-chat-navigation-failed'; + delivery = 'rejected'; + constructor(project, observed){ + super(`ChatGPT Desktop new-chat action did not open ${project ? `project ${JSON.stringify(project)}` : 'a projectless chat'}; observed ${observed}`); + this.name = 'NewChatNavigationError'; + } + } + class NotImplementedError extends Error { + code = 'NOT_IMPLEMENTED'; + delivery = 'rejected'; + reason = 'not-implemented'; + constructor(operation, detail){ + super(detail ? `ChatGPT Desktop ${operation} is not implemented: ${detail}` : `ChatGPT Desktop ${operation} is not implemented`); + this.name = 'NotImplementedError'; + } + } + class CdpHostRejectedError extends Error { + code = 'CDP_HOST_REJECTED'; + delivery = 'rejected'; + reason = 'cdp-host-rejected'; + constructor(host){ + super(`ChatGPT Desktop CDP rejects non-loopback host ${JSON.stringify(host)}. ` + 'Connect to 127.0.0.1 only; there is no remote transport.'); + this.name = 'CdpHostRejectedError'; + } + } + class CdpUnreachableError extends Error { + code = 'CDP_UNREACHABLE'; + delivery = 'rejected'; + reason = 'cdp-unreachable'; + constructor(message){ + super(message); + this.name = 'CdpUnreachableError'; + } + } + __webpack_require__.d(__webpack_exports__, { + EH: ()=>NotImplementedError, + Kn: ()=>ProjectUnavailableError, + Rg: ()=>_codex_remote_control_js__rspack_import_0.Rg, + XL: ()=>NewChatNavigationError, + c$: ()=>CdpHostRejectedError, + fw: ()=>ArchivedThreadError, + oN: ()=>ComposerDraftError, + sO: ()=>CdpUnreachableError + }); + }, + "./src/core/chatgpt-desktop/facade.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var node_crypto__rspack_import_0 = __webpack_require__("node:crypto"); + var _cdp_adapter_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/cdp-adapter.ts"); + var _app_server_fallback_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/app-server-fallback.ts"); + var _errors_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _loopback_js__rspack_import_4 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + var _remote_threads_js__rspack_import_5 = __webpack_require__("./src/core/chatgpt-desktop/remote-threads.ts"); + var _project_state_js__rspack_import_6 = __webpack_require__("./src/core/chatgpt-desktop/project-state.ts"); + var _payload_budget_js__rspack_import_8 = __webpack_require__("./src/core/chatgpt-desktop/payload-budget.ts"); + var _thread_ids_js__rspack_import_7 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const defaultFallbacks = { + listThreads: _app_server_fallback_js__rspack_import_2.OI, + searchThreads: _app_server_fallback_js__rspack_import_2.aI, + readThread: _app_server_fallback_js__rspack_import_2.Jf, + unarchiveThread: _app_server_fallback_js__rspack_import_2.qL, + statusProbe: _app_server_fallback_js__rspack_import_2.dM, + listRemoteThreads: _remote_threads_js__rspack_import_5.Hx, + listHosts: _remote_threads_js__rspack_import_5.Kl, + discoverModelProviders: _app_server_fallback_js__rspack_import_2.X + }; + const MERGED_LIST_CURSOR = 'desktop-merged-list-v1:'; + const MERGED_SEARCH_CURSOR = 'desktop-merged-search-v1:'; + class ChatGptDesktopFacade { + #cdp; + #port; + #cdpConnected = false; + #fallbacks; + #listSnapshots = new Map(); + constructor(cdp = new _cdp_adapter_js__rspack_import_1.a(), port = (0, _loopback_js__rspack_import_4.L0)(), fallbacks = defaultFallbacks){ + this.#cdp = cdp; + this.#port = port; + this.#fallbacks = fallbacks; + } + async connect({ port }) { + this.#port = (0, _loopback_js__rspack_import_4.L0)(process.env, port); + try { + await this.#cdp.connect({ + port: this.#port + }); + this.#cdpConnected = true; + } catch (error) { + this.#cdpConnected = false; + throw error; + } + } + async listTargets() { + await this.#ensureCdp(); + return this.#cdp.listTargets(); + } + async listThreads(options = {}) { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null + }); + let snapshot; + let offset = 0; + if (options.cursor) { + const state = (0, _payload_budget_js__rspack_import_8.i3)(MERGED_LIST_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop list cursor does not match the port or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildListSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop thread inventory changed; restart listing without a cursor'); + } + } else { + snapshot = await this.#buildListSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_LIST_CURSOR); + } + #pageSnapshot(snapshot, offset, limit, options, prefix, query) { + if (offset > snapshot.rows.length) throw new Error('ChatGPT Desktop cursor is out of range'); + const host = normalizeHostFilter(options.host); + const rows = []; + const base = { + backend: snapshot.backend, + limit, + host, + ...query !== undefined ? { + query + } : {}, + ...options.modelProvider?.trim() ? { + modelProvider: options.modelProvider.trim() + } : {}, + ...options.project ? { + project: options.project + } : {}, + ...options.groupBy === 'host' ? { + groupBy: 'host' + } : {} + }; + for (const source of snapshot.rows.slice(offset, offset + limit)){ + let row = source; + const candidate = [ + ...rows, + row + ]; + const probe = { + ...base, + threads: candidate, + nextCursor: prefix, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(candidate) + } : {}, + warnings: snapshot.warnings, + exitCode: 0 + }; + if ((0, _payload_budget_js__rspack_import_8.sQ)(probe) > 614400) { + if (rows.length) break; + row = (0, _payload_budget_js__rspack_import_8.MH)(row); + const minimal = [ + row + ]; + if ((0, _payload_budget_js__rspack_import_8.sQ)({ + ...base, + threads: minimal, + nextCursor: prefix, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(minimal) + } : {}, + warnings: snapshot.warnings, + exitCode: 0 + }) > 614400) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + rows.push(row); + } + const nextOffset = offset + rows.length; + const nextCursor = nextOffset < snapshot.rows.length ? (0, _payload_budget_js__rspack_import_8.oI)(prefix, { + fingerprint: snapshot.fingerprint, + offset: nextOffset, + port: this.#port, + filters: snapshot.filters + }) : null; + const warnings = [ + ...snapshot.warnings + ]; + if (rows.length < Math.min(limit, snapshot.rows.length - offset)) { + warnings.push(`${query === undefined ? 'List' : 'Search'} page stopped at the MCP byte budget; continue with nextCursor`); + } + return { + ...base, + threads: rows, + nextCursor, + ...options.groupBy === 'host' ? { + groups: groupThreadsByHost(rows) + } : {}, + ...warnings.length ? { + warnings + } : {} + }; + } + async #buildListSnapshot(options, filters) { + const appRows = []; + const warnings = []; + let appCursor; + let appComplete = false; + const seenCursors = new Set(); + const modelProviders = modelProvidersFromFilter(options.modelProvider); + let appSucceeded = false; + try { + for(let page = 0; page < 400; page++){ + const out = await this.#fallbacks.listThreads({ + limit: 200, + cursor: appCursor, + modelProviders + }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) { + appComplete = true; + break; + } + if (seenCursors.has(out.nextCursor)) throw new Error('App-server list returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + } + if (!appComplete) warnings.push('App-server inventory stopped after 400 pages'); + } catch (error) { + warnings.push(`App-server list failed: ${error instanceof Error ? error.message : String(error)}`); + } + const remotes = this.#listRemotes(); + let cdpList = null; + try { + await this.#ensureCdp(); + cdpList = await this.#cdp.listThreads({ + limit: 200 + }); + } catch (error) { + if (isCdpFailure(error)) this.#cdpConnected = false; + else warnings.push(`CDP list failed: ${error instanceof Error ? error.message : String(error)}`); + } + if (!appRows.length && !cdpList && !remotes.length) { + throw new _errors_js__rspack_import_3.sO(warnings[0] ?? 'ChatGPT Desktop listThreads: CDP and app-server unreachable'); + } + const appList = { + backend: 'app-server', + limit: appRows.length, + threads: appRows + }; + const base = appSucceeded ? cdpList ? mergeThreadLists(appList, cdpList) : appList : cdpList ?? { + backend: 'remote-state', + limit: 0, + threads: [] + }; + const merged = finalizeThreadList({ + ...base, + threads: (0, _project_state_js__rspack_import_6.j)(base.threads) + }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes: (0, _project_state_js__rspack_import_6.j)(remotes) + }); + const rows = [ + ...new Map(merged.threads.map((row)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(row.threadId) ?? row.threadId, + row + ])).values() + ]; + rows.sort(compareDesktopThreads); + const fingerprint = (0, node_crypto__rspack_import_0.createHash)('sha256').update(JSON.stringify({ + filters, + port: this.#port, + rows: rows.map((row)=>[ + row.threadId, + row.updatedAt, + row.location, + row.hostId + ]) + })).digest('base64url'); + const snapshot = { + fingerprint, + port: this.#port, + filters, + createdAt: Date.now(), + rows, + backend: merged.backend, + warnings + }; + for (const [key, value] of this.#listSnapshots){ + if (Date.now() - value.createdAt > 15 * 60000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + async listHosts() { + const remotes = this.#listRemotes(); + const cdpList = typeof this.#cdp.listThreads === 'function' ? await this.#tryCdpList({ + limit: 200 + }) : null; + const cdpRows = cdpList?.threads ?? []; + let localThreadCount = 0; + let appSucceeded = false; + try { + const local = await this.#fallbacks.listThreads({ + limit: 25, + modelProviders: [] + }); + appSucceeded = true; + localThreadCount = local.threads.length; + } catch { + localThreadCount = 0; + } + const listHosts = this.#fallbacks.listHosts ?? _remote_threads_js__rspack_import_5.Kl; + const fromState = listHosts(process.env, { + localThreadCount + }); + const byId = new Map(fromState.map((host)=>[ + host.hostId, + { + ...host + } + ])); + for (const thread of remotes){ + const existing = byId.get(thread.hostId); + if (existing) { + byId.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName + }); + } else { + byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 0 + }); + } + } + for (const thread of cdpRows){ + if (thread.location !== 'remote' || !thread.hostId) continue; + if (!byId.has(thread.hostId)) byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName ?? null, + location: 'remote', + threadCount: 0 + }); + } + const hostsSource = 'local+remote-thread-summaries-v3+managed-connections' + (cdpRows.length ? '+cdp' : ''); + const remoteCounts = new Map(); + const remoteInventory = finalizeThreadList({ + backend: cdpRows.length ? 'cdp' : 'remote-state', + limit: 10000, + threads: cdpRows.filter((thread)=>thread.location === 'remote') + }, { + limit: 10000, + remotes + }); + for (const thread of remoteInventory.threads){ + if (thread.hostId) remoteCounts.set(thread.hostId, (remoteCounts.get(thread.hostId) ?? 0) + 1); + } + const withCounts = [ + ...byId.values() + ].map((host)=>host.hostId === 'local' ? { + ...host, + threadCount: localThreadCount + } : { + ...host, + threadCount: remoteCounts.get(host.hostId) ?? host.threadCount + }).sort((a, b)=>{ + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + let modelProviders = []; + let modelProvidersSource = 'thread/list-distinct'; + const warnings = []; + try { + const discover = this.#fallbacks.discoverModelProviders ?? _app_server_fallback_js__rspack_import_2.X; + const discovered = await discover(); + modelProviders = [ + ...discovered.providers + ]; + modelProvidersSource = discovered.source; + if (typeof discovered.threadCount === 'number') localThreadCount = discovered.threadCount; + if (discovered.complete === false) warnings.push('Provider discovery stopped before the final app-server page'); + } catch (error) { + warnings.push(`Model provider discovery failed: ${error instanceof Error ? error.message : String(error)}`); + modelProviders = [ + ...new Set(remotes.map((thread)=>thread.modelProvider).filter((value)=>typeof value === 'string' && Boolean(value))) + ].sort(); + modelProvidersSource = 'remote-summaries-distinct'; + } + modelProviders = [ + ...new Set([ + ...modelProviders, + ...remotes.map((thread)=>thread.modelProvider).filter((value)=>Boolean(value)) + ]) + ].sort(); + let inventoryCounts = null; + try { + const filters = JSON.stringify({ + host: 'all', + modelProvider: null, + project: null, + groupBy: null + }); + const cached = [ + ...this.#listSnapshots.values() + ].find((snapshot)=>snapshot.port === this.#port && snapshot.filters === filters && Date.now() - snapshot.createdAt < 15 * 60000); + const inventory = cached ?? await this.#buildListSnapshot({}, filters); + inventoryCounts = new Map(); + for (const thread of inventory.rows){ + const hostId = thread.location === 'remote' ? thread.hostId ?? 'unknown-remote' : 'local'; + inventoryCounts.set(hostId, (inventoryCounts.get(hostId) ?? 0) + 1); + } + warnings.push(...inventory.warnings); + } catch (error) { + warnings.push(`Merged inventory count failed: ${error instanceof Error ? error.message : String(error)}`); + } + return { + hosts: withCounts.map((host)=>{ + const counted = { + ...host, + threadCount: inventoryCounts?.get(host.hostId) ?? (host.hostId === 'local' ? localThreadCount : host.threadCount) + }; + if (!counted.hostName) return counted; + const name = (0, _payload_budget_js__rspack_import_8.aA)(counted.hostName); + return { + ...counted, + hostName: name.title, + ...name.titleTruncated ? { + hostNameTruncated: true + } : {} + }; + }), + hostsSource, + modelProviders, + modelProvidersSource, + backend: appSucceeded ? cdpRows.length ? remotes.length ? 'app-server+cdp+remote-state' : 'app-server+cdp' : remotes.length ? 'app-server+remote-state' : 'app-server' : cdpRows.length ? remotes.length ? 'cdp+remote-state' : 'cdp' : 'remote-state', + ...warnings.length ? { + warnings + } : {} + }; + } + async searchThreads(options) { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + query: options.query, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null + }); + let snapshot; + let offset = 0; + if (options.cursor) { + const state = (0, _payload_budget_js__rspack_import_8.i3)(MERGED_SEARCH_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop search cursor does not match the port, query or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildSearchSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop search results changed; restart without a cursor'); + } + } else { + snapshot = await this.#buildSearchSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_SEARCH_CURSOR, options.query); + } + async #buildSearchSnapshot(options, filters) { + const modelProviders = modelProvidersFromFilter(options.modelProvider); + const appRows = []; + const warnings = []; + const seenCursors = new Set(); + let appCursor; + let appSucceeded = false; + let appError; + try { + for(let page = 0; page < 400; page++){ + const out = await this.#fallbacks.searchThreads({ + query: options.query, + limit: 200, + cursor: appCursor, + modelProviders + }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) break; + if (seenCursors.has(out.nextCursor)) throw new Error('App-server search returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + if (page === 399) warnings.push('App-server search stopped after 400 pages'); + } + } catch (error) { + appError = error; + warnings.push(`App-server search failed: ${error instanceof Error ? error.message : String(error)}`); + } + const cdpList = await this.#tryCdpList({ + limit: 200 + }); + const remotes = (0, _project_state_js__rspack_import_6.j)(this.#listRemotes()); + if (!appSucceeded && !cdpList && !remotes.length) throw appError; + const appList = { + backend: 'app-server', + limit: appRows.length, + threads: appRows + }; + const base = appSucceeded ? cdpList ? mergeThreadLists(appList, cdpList) : appList : cdpList ?? { + backend: 'remote-state', + limit: 0, + threads: [] + }; + const appMatches = new Set(appRows.map((thread)=>(0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId)); + const merged = finalizeThreadList({ + ...base, + threads: (0, _project_state_js__rspack_import_6.j)(base.threads) + }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes + }); + const needle = options.query.trim().toLowerCase(); + const rows = [ + ...new Map(merged.threads.filter((thread)=>appMatches.has((0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId) || threadMatchesQuery(thread, needle)).map((thread)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId, + thread + ])).values() + ]; + rows.sort(compareDesktopThreads); + const fingerprint = (0, node_crypto__rspack_import_0.createHash)('sha256').update(JSON.stringify({ + filters, + port: this.#port, + rows: rows.map((row)=>[ + row.threadId, + row.updatedAt, + row.location, + row.hostId + ]) + })).digest('base64url'); + const snapshot = { + fingerprint, + port: this.#port, + filters, + createdAt: Date.now(), + rows, + backend: merged.backend, + warnings + }; + for (const [key, value] of this.#listSnapshots){ + if (Date.now() - value.createdAt > 15 * 60000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + async readThread(options) { + const limit = options.limit ?? (options.full ? 2000 : 100); + const full = options.full ?? false; + if ((0, _thread_ids_js__rspack_import_7.vr)(options.threadId)) { + if (options.cursor) throw new Error('Temporary Desktop threads do not support read cursors'); + await this.#ensureCdp(); + return this.#cdp.readThread({ + ...options, + limit, + full + }); + } + try { + return await this.#fallbacks.readThread({ + threadId: options.threadId, + limit, + full, + cursor: options.cursor + }); + } catch (appError) { + if (appError instanceof _errors_js__rspack_import_3.Rg) throw appError; + if (options.cursor) throw appError; + const cdpConnected = await this.#tryEnsureCdp(); + if (!cdpConnected) throw appError; + const fallback = await this.#cdp.readThread({ + ...options, + limit, + full + }); + return { + ...fallback, + complete: false, + warnings: [ + `App-server read failed: ${appError instanceof Error ? appError.message : String(appError)}` + ] + }; + } + } + async sendMessage(options) { + if (options.threadId) { + const remote = (0, _remote_threads_js__rspack_import_5.KI)(options.threadId); + if (remote) throw new _errors_js__rspack_import_3.Rg(options.threadId, remote.hostId, remote.hostName); + } + await this.#ensureCdp(); + try { + return await this.#cdp.sendMessage(options); + } catch (error) { + if (!(error instanceof _errors_js__rspack_import_3.fw) || !options.unarchive || !options.threadId) throw error; + await (this.#fallbacks.unarchiveThread ?? _app_server_fallback_js__rspack_import_2.qL)(options.threadId); + return this.#cdp.sendMessage({ + ...options, + unarchive: false + }); + } + } + async waitForReply(options) { + await this.#ensureCdp(); + return this.#cdp.waitForReply(options); + } + async openThread(threadId, options) { + const remote = (0, _remote_threads_js__rspack_import_5.KI)(threadId); + if (remote) throw new _errors_js__rspack_import_3.Rg(threadId, remote.hostId, remote.hostName); + await this.#ensureCdp(); + return this.#cdp.openThread(threadId, options); + } + async status() { + const cdpStatus = await this.#cdp.status(); + let appServerFallback; + try { + appServerFallback = await this.#fallbacks.statusProbe(); + } catch { + appServerFallback = { + reachable: false + }; + } + const reachable = cdpStatus.reachable; + return { + ...cdpStatus, + port: this.#port, + appServerFallback, + reachable, + message: cdpStatus.reachable ? cdpStatus.message : cdpStatus.message, + exitCode: reachable ? 0 : 1 + }; + } + async close() { + this.#cdpConnected = false; + await this.#cdp.close(); + } + #listRemotes() { + const list = this.#fallbacks.listRemoteThreads ?? _remote_threads_js__rspack_import_5.Hx; + try { + return list(); + } catch { + return []; + } + } + async #ensureCdp() { + if (this.#cdpConnected) return; + await this.#cdp.connect({ + port: this.#port + }); + this.#cdpConnected = true; + } + async #tryEnsureCdp() { + try { + await this.#ensureCdp(); + return true; + } catch (error) { + if (!isCdpFailure(error)) throw error; + this.#cdpConnected = false; + return false; + } + } + async #tryCdpList(options) { + try { + await this.#ensureCdp(); + return await this.#cdp.listThreads(options); + } catch (error) { + if (isCdpFailure(error) || error instanceof _errors_js__rspack_import_3.EH) { + if (isCdpFailure(error)) this.#cdpConnected = false; + return null; + } + throw error; + } + } + } + function mergeThreadLists(appList, cdpList) { + const cdpByKey = new Map(); + for (const thread of cdpList.threads){ + const key = (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId; + cdpByKey.set(key, thread); + } + const merged = appList.threads.map((thread)=>{ + const key = (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId; + const cdp = cdpByKey.get(key); + const withLocal = { + ...thread, + location: thread.location ?? 'local', + hostId: thread.hostId ?? null, + hostName: thread.hostName ?? null + }; + if (!cdp) return withLocal; + return { + ...withLocal, + title: withLocal.title || cdp.title, + pinned: withLocal.pinned || cdp.pinned, + selected: cdp.selected, + kind: cdp.kind || withLocal.kind, + project: cdp.project ?? withLocal.project, + location: cdp.location === 'remote' ? 'remote' : withLocal.location, + hostId: cdp.location === 'remote' ? cdp.hostId ?? null : withLocal.hostId, + hostName: cdp.location === 'remote' ? cdp.hostName ?? null : withLocal.hostName + }; + }); + for (const thread of cdpList.threads){ + if (merged.some((row)=>(0, _thread_ids_js__rspack_import_7.LV)(row.threadId, thread.threadId))) continue; + merged.push(thread); + } + return { + backend: 'app-server+cdp', + limit: appList.limit, + nextCursor: appList.nextCursor, + ...appList.query !== undefined ? { + query: appList.query + } : {}, + threads: merged + }; + } + function finalizeThreadList(base, { limit, host = 'all', modelProvider, project, groupBy, remotes }) { + const remoteById = new Map(remotes.map((thread)=>[ + (0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId, + thread + ])); + const locals = base.threads.map((thread)=>{ + const remote = remoteById.get((0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId); + if (remote) { + return { + ...thread, + ...remote, + selected: thread.selected, + pinned: thread.pinned || remote.pinned + }; + } + const location = thread.location ?? 'local'; + return { + ...thread, + location, + hostId: location === 'remote' ? thread.hostId ?? null : null, + hostName: location === 'remote' ? thread.hostName ?? null : null + }; + }); + const localKeys = new Set(locals.map((thread)=>(0, _thread_ids_js__rspack_import_7.nn)(thread.threadId) ?? thread.threadId)); + const merged = [ + ...locals + ]; + for (const remote of remotes){ + const key = (0, _thread_ids_js__rspack_import_7.nn)(remote.threadId) ?? remote.threadId; + if (localKeys.has(key)) continue; + merged.push({ + ...remote, + location: remote.location ?? 'remote', + hostId: remote.hostId ?? null, + hostName: remote.hostName ?? null + }); + localKeys.add(key); + } + if (remotes.length) merged.sort((a, b)=>Number(b.location === 'remote') - Number(a.location === 'remote')); + const filtered = merged.filter((thread)=>{ + if (!hostMatches(thread, host)) return false; + if (!modelProviderMatches(thread, modelProvider)) return false; + if (!projectMatches(thread, project)) return false; + return true; + }); + const limited = filtered.slice(0, limit).map(_payload_budget_js__rspack_import_8.eX); + const result = { + backend: remotes.length ? base.backend === 'app-server+cdp' ? 'app-server+cdp+remote-state' : base.backend === 'app-server' ? 'app-server+remote-state' : base.backend === 'cdp' ? 'cdp+remote-state' : base.backend : base.backend, + limit, + threads: limited, + ...base.nextCursor !== undefined ? { + nextCursor: base.nextCursor + } : {}, + ...base.query !== undefined ? { + query: base.query + } : {}, + host, + ...modelProvider ? { + modelProvider + } : {}, + ...project ? { + project + } : {} + }; + if (groupBy === 'host') { + return { + ...result, + groupBy: 'host', + groups: groupThreadsByHost(limited) + }; + } + return result; + } + function compareDesktopThreads(a, b) { + const timestamp = (row)=>{ + const value = row.updatedAt ?? row.createdAt; + if (typeof value !== 'number' || !Number.isFinite(value)) return Number.NEGATIVE_INFINITY; + return value < 100000000000 ? value * 1000 : value; + }; + return timestamp(b) - timestamp(a) || a.threadId.localeCompare(b.threadId); + } + function groupThreadsByHost(threads) { + const groups = new Map(); + for (const thread of threads){ + const location = thread.location ?? 'local'; + const hostId = location === 'remote' ? thread.hostId || 'unknown-remote' : 'local'; + const key = `${location}:${hostId}`; + const existing = groups.get(key); + if (existing) { + existing.threads.push(thread); + continue; + } + groups.set(key, { + hostId, + hostName: thread.hostName ?? (location === 'local' ? 'local' : null), + location, + threads: [ + thread + ] + }); + } + return [ + ...groups.values() + ]; + } + function normalizeHostFilter(host) { + const value = (host ?? 'all').trim(); + return value || 'all'; + } + function modelProvidersFromFilter(modelProvider) { + if (!modelProvider || !modelProvider.trim()) return []; + return [ + modelProvider.trim() + ]; + } + function hostMatches(thread, host) { + const needle = host.trim().toLowerCase(); + if (!needle || needle === 'all') return true; + if (needle === 'local') return (thread.location ?? 'local') === 'local'; + return [ + thread.hostId, + thread.hostName + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function modelProviderMatches(thread, modelProvider) { + if (!modelProvider) return true; + return thread.modelProvider === modelProvider; + } + function projectMatches(thread, project) { + if (!project) return true; + const needle = project.trim().toLowerCase(); + return [ + thread.project, + thread.projectId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function threadMatchesQuery(thread, needle) { + if (!needle) return true; + return [ + thread.title, + thread.threadId, + thread.project, + thread.preview, + thread.hostName, + thread.hostId + ].some((value)=>typeof value === 'string' && value.toLowerCase().includes(needle)); + } + function isCdpFailure(error) { + if (error instanceof _errors_js__rspack_import_3.sO) return true; + if (error instanceof _errors_js__rspack_import_3.EH) return false; + if (error && typeof error === 'object' && 'code' in error) { + const code = String(error.code); + return code === 'CDP_UNREACHABLE' || code === 'CDP_HOST_REJECTED'; + } + return false; + } + let activeAdapter = null; + function getChatGptDesktopAdapter() { + if (!activeAdapter) activeAdapter = new ChatGptDesktopFacade(); + return activeAdapter; + } + function setChatGptDesktopAdapterForTests(adapter) { + activeAdapter = adapter; + } + __webpack_require__.d(__webpack_exports__, { + PP: ()=>getChatGptDesktopAdapter + }); + }, + "./src/core/chatgpt-desktop/loopback.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _errors_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + const LOOPBACK = new Set([ + '127.0.0.1', + 'localhost', + '::1' + ]); + const DEFAULT_CDP_PORT = 9222; + const CDP_LOOPBACK_HOST = '127.0.0.1'; + function isLoopbackHostname(hostname) { + const host = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, ''); + return LOOPBACK.has(host); + } + function assertLoopbackHostname(hostname) { + if (!isLoopbackHostname(hostname)) { + throw new _errors_js__rspack_import_0.c$(hostname); + } + } + function cdpHttpBase(port) { + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return `http://${CDP_LOOPBACK_HOST}:${port}`; + } + function forceLoopbackWebSocketUrl(raw) { + let url; + try { + url = new URL(raw); + } catch { + throw new _errors_js__rspack_import_0.c$(String(raw)); + } + assertLoopbackHostname(url.hostname); + if (url.username || url.password || url.hash) throw new _errors_js__rspack_import_0.c$(raw); + if (url.protocol !== 'ws:' && url.protocol !== 'wss:') { + throw new Error(`Expected ws(s) debugger URL, got ${url.protocol}`); + } + url.hostname = CDP_LOOPBACK_HOST; + url.protocol = 'ws:'; + return url.toString(); + } + function resolveCdpPort(env = process.env, explicit) { + if (explicit !== undefined) { + if (!Number.isInteger(explicit) || explicit < 1 || explicit > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return explicit; + } + const raw = (env.CHATGPT_DESKTOP_CDP_PORT || '').trim(); + if (!raw) return DEFAULT_CDP_PORT; + const port = /^\d+$/.test(raw) ? Number(raw) : NaN; + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CHATGPT_DESKTOP_CDP_PORT must be an integer 1-65535'); + } + return port; + } + __webpack_require__.d(__webpack_exports__, { + L0: ()=>resolveCdpPort, + NJ: ()=>cdpHttpBase, + jv: ()=>forceLoopbackWebSocketUrl + }); + }, + "./src/core/chatgpt-desktop/payload-budget.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + const DESKTOP_RESULT_BUDGET_BYTES = 600 * 1024; + const DESKTOP_THREAD_TITLE_CHARS = 200; + function serializedBytes(value) { + return Buffer.byteLength(JSON.stringify(value), 'utf8'); + } + function compact(value, maxChars) { + const normalized = value.replace(/\s+/gu, ' ').trim(); + const chars = Array.from(normalized); + return { + value: chars.slice(0, maxChars).join(''), + clipped: chars.length > maxChars + }; + } + function compactThreadRow(thread) { + const title = compact(thread.title, DESKTOP_THREAD_TITLE_CHARS); + const preview = typeof thread.preview === 'string' ? compact(thread.preview, 200) : null; + const cwd = typeof thread.cwd === 'string' ? compact(thread.cwd, 2048) : null; + const project = typeof thread.project === 'string' ? compact(thread.project, 256) : null; + const hostName = typeof thread.hostName === 'string' ? compact(thread.hostName, 256) : null; + return { + ...thread, + title: title.value, + ...title.clipped ? { + titleTruncated: true + } : {}, + ...preview ? { + preview: preview.value, + ...preview.clipped ? { + previewTruncated: true + } : {} + } : {}, + ...cwd ? { + cwd: cwd.value, + ...cwd.clipped ? { + cwdTruncated: true + } : {} + } : {}, + ...project ? { + project: project.value, + ...project.clipped ? { + projectTruncated: true + } : {} + } : {}, + ...hostName ? { + hostName: hostName.value, + ...hostName.clipped ? { + hostNameTruncated: true + } : {} + } : {} + }; + } + function compactThreadTitle(title) { + const out = compact(title, DESKTOP_THREAD_TITLE_CHARS); + return { + title: out.value, + titleTruncated: out.clipped + }; + } + function minimalThreadRow(thread) { + return { + threadId: thread.threadId, + title: compact(thread.title, DESKTOP_THREAD_TITLE_CHARS).value, + titleTruncated: true, + pinned: thread.pinned, + selected: thread.selected, + kind: thread.kind, + location: thread.location, + hostId: thread.hostId, + hostName: thread.hostName, + modelProvider: thread.modelProvider, + metadataTruncated: true + }; + } + function encodePageCursor(prefix, value) { + return prefix + Buffer.from(JSON.stringify(value)).toString('base64url'); + } + function decodePageCursor(prefix, cursor) { + if (!cursor.startsWith(prefix)) throw new Error('Invalid ChatGPT Desktop page cursor'); + try { + return JSON.parse(Buffer.from(cursor.slice(prefix.length), 'base64url').toString('utf8')); + } catch { + throw new Error('Invalid ChatGPT Desktop page cursor'); + } + } + __webpack_require__.d(__webpack_exports__, { + MH: ()=>minimalThreadRow, + aA: ()=>compactThreadTitle, + eX: ()=>compactThreadRow, + i3: ()=>decodePageCursor, + oI: ()=>encodePageCursor, + sQ: ()=>serializedBytes + }); + }, + "./src/core/chatgpt-desktop/project-state.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_ids_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + function findLocalProject(nameOrId, env = process.env) { + const state = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!state) return null; + for (const value of Object.values(record(stateValue(state, 'local-projects')) ?? {})){ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') continue; + if (row.id !== nameOrId && row.name.toLowerCase() !== nameOrId.toLowerCase()) continue; + return { + id: row.id, + label: row.name, + rootPaths: (Array.isArray(row.rootPaths) ? row.rootPaths : []).filter((path)=>typeof path === 'string' && Boolean(path)) + }; + } + return null; + } + function record(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : null; + } + function stateValue(state, key) { + for (const source of (0, _codex_remote_control_js__rspack_import_0.xr)(state))if (key in source) return source[key]; + return undefined; + } + function pathWithin(path, root) { + const normalized = root.replace(/\/+$/u, ''); + return path === normalized || path.startsWith(`${normalized}/`); + } + function resolveThreadProjects(threads, env = process.env) { + const state = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!state) return [ + ...threads + ]; + const localProjects = Object.values(record(stateValue(state, 'local-projects')) ?? {}).flatMap((value)=>{ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') return []; + return (Array.isArray(row.rootPaths) ? row.rootPaths : []).filter((path)=>typeof path === 'string' && Boolean(path)).map((rootPath)=>({ + id: row.id, + label: row.name, + rootPath + })); + }); + const remoteProjects = (Array.isArray(stateValue(state, 'remote-projects')) ? stateValue(state, 'remote-projects') : []).flatMap((value)=>{ + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.label !== 'string' || typeof row.remotePath !== 'string' || typeof row.hostId !== 'string') return []; + return [ + { + id: row.id, + label: row.label, + rootPath: row.remotePath, + hostId: row.hostId + } + ]; + }); + const assignments = record(stateValue(state, 'thread-project-assignments')) ?? {}; + const hints = record(stateValue(state, 'thread-workspace-root-hints')) ?? {}; + const projectless = new Set(Array.isArray(stateValue(state, 'projectless-thread-ids')) ? stateValue(state, 'projectless-thread-ids') : []); + return threads.map((thread)=>{ + const bare = (0, _thread_ids_js__rspack_import_1.nn)(thread.threadId) ?? thread.threadId; + const assignment = record(assignments[bare]); + const remote = thread.location === 'remote'; + const projects = remote ? remoteProjects.filter((project)=>project.hostId === thread.hostId) : localProjects; + let project = assignment && typeof assignment.projectId === 'string' ? projects.find((entry)=>entry.id === assignment.projectId) : undefined; + if (!project && !projectless.has(bare) && thread.projectId) { + project = projects.find((entry)=>entry.id === thread.projectId); + } + if (!project && !assignment && !projectless.has(bare)) { + const path = typeof hints[bare] === 'string' ? hints[bare] : typeof thread.cwd === 'string' ? thread.cwd : null; + if (path) project = projects.filter((entry)=>pathWithin(path, entry.rootPath)).sort((a, b)=>b.rootPath.length - a.rootPath.length)[0]; + } + if (project) return { + ...thread, + project: project.label, + projectId: project.id, + projectRootPath: project.rootPath + }; + if (projectless.has(bare)) return { + ...thread, + project: null, + projectId: null + }; + if (remote && typeof thread.project === 'string' && thread.project.startsWith('/')) { + return { + ...thread, + project: null + }; + } + return thread; + }); + } + __webpack_require__.d(__webpack_exports__, { + j: ()=>resolveThreadProjects, + q: ()=>findLocalProject + }); + }, + "./src/core/chatgpt-desktop/remote-threads.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_remote_control_js__rspack_import_0 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_ids_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/thread-ids.ts"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + function listRemoteThreadsFromState(env = process.env) { + const data = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (!data) return []; + const hostNames = collectHostNames(data); + const out = []; + const seen = new Set(); + for (const [key, value] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value); + const pinnedIds = extractPinnedIds(value); + for (const entry of extractThreadEntries(value)){ + const mapped = mapRemoteEntry(entry, { + hostId, + hostName, + pinnedIds + }); + if (!mapped) continue; + const bare = (0, _thread_ids_js__rspack_import_1.nn)(mapped.threadId) ?? mapped.threadId; + const dedupe = `${hostId}:${bare}`; + if (seen.has(dedupe)) continue; + seen.add(dedupe); + out.push(mapped); + } + } + return out; + } + function findRemoteThread(threadId, env = process.env) { + const bare = (0, _thread_ids_js__rspack_import_1.nn)(threadId) ?? threadId; + if (!bare) return null; + for (const thread of listRemoteThreadsFromState(env)){ + const id = (0, _thread_ids_js__rspack_import_1.nn)(thread.threadId) ?? thread.threadId; + if (id === bare) { + return { + hostId: thread.hostId, + hostName: thread.hostName, + thread + }; + } + } + return null; + } + function findRemoteThreadHostId(threadId, env = process.env) { + return findRemoteThread(threadId, env)?.hostId ?? null; + } + function listDiscoveredHosts(env = process.env, { localThreadCount = 0 } = {}) { + const remotes = listRemoteThreadsFromState(env); + const byHost = new Map(); + byHost.set('local', { + hostId: 'local', + hostName: 'local', + location: 'local', + threadCount: localThreadCount + }); + const data = (0, _codex_remote_control_js__rspack_import_0.IT)(env); + if (data) { + const hostNames = collectHostNames(data); + for (const [key, summary] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(summary) ?? null; + if (byHost.has(hostId)) continue; + byHost.set(hostId, { + hostId, + hostName, + location: 'remote', + threadCount: 0 + }); + } + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections){ + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue; + const hostId = stringField(entry, [ + 'hostId' + ]); + if (hostId && !byHost.has(hostId)) byHost.set(hostId, { + hostId, + hostName: stringField(entry, [ + 'displayName', + 'alias' + ]), + location: 'remote', + threadCount: 0 + }); + } + } + for (const thread of remotes){ + const existing = byHost.get(thread.hostId); + if (existing) { + byHost.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName, + threadCount: existing.threadCount + 1 + }); + } else { + byHost.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 1 + }); + } + } + return [ + ...byHost.values() + ].sort((a, b)=>{ + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + } + function collectHostNames(data) { + const names = new Map(); + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections){ + const pair = hostNameFromEntry(entry); + if (pair) names.set(pair.hostId, pair.hostName); + } + for (const [key, value] of (0, _codex_remote_control_js__rspack_import_0.xr)(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry; + const hostId = stringField(row, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(row, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractPinnedIds(value) { + const pinned = new Set(); + if (!value || typeof value !== 'object') return pinned; + const record = value; + for (const key of [ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'pinned_ids' + ]){ + const arr = record[key]; + if (!Array.isArray(arr)) continue; + for (const item of arr){ + if (typeof item === 'string' && item) pinned.add(item); + else if (item && typeof item === 'object') { + const id = stringField(item, [ + 'id', + 'threadId', + 'thread_id' + ]); + if (id) pinned.add(id); + } + } + } + return pinned; + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) { + continue; + } + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const row = entry; + if (!stringField(row, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ])) { + entries.push({ + ...row, + id: key + }); + } else { + entries.push(entry); + } + } + } + return entries; + } + function mapRemoteEntry(entry, { hostId, hostName, pinnedIds }) { + if (typeof entry === 'string' && entry) { + return { + threadId: (0, _thread_ids_js__rspack_import_1.BS)(entry), + title: entry, + pinned: pinnedIds.has(entry), + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName + }; + } + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry; + const id = stringField(row, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + if (!id) return null; + const title = stringField(row, [ + 'title', + 'name', + 'preview', + 'label' + ]) || id; + const projectId = stringField(row, [ + 'projectId', + 'project_id' + ]); + const project = stringField(row, [ + 'project', + 'projectName', + 'project_name', + 'cwd' + ]) || projectId || undefined; + const preview = stringField(row, [ + 'preview', + 'snippet' + ]); + const cwd = stringField(row, [ + 'cwd', + 'workingDirectory', + 'working_directory' + ]); + const modelProvider = stringField(row, [ + 'modelProvider', + 'model_provider', + 'provider' + ]); + const model = stringField(row, [ + 'model' + ]); + const originator = stringField(row, [ + 'originator', + 'source' + ]); + const createdAt = numberField(row, [ + 'createdAt', + 'created_at' + ]); + const updatedAt = numberField(row, [ + 'updatedAt', + 'updated_at' + ]); + const sectionName = stringField(row, [ + 'sectionName', + 'section' + ]); + const sectionId = stringField(row, [ + 'sectionId', + 'section_id' + ]) || sectionName; + const pinned = pinnedIds.has(id) || row.pinned === true || sectionId === 'Pinned' || sectionName === 'Pinned'; + return { + threadId: (0, _thread_ids_js__rspack_import_1.BS)(id), + title, + pinned, + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName, + ...project ? { + project + } : {}, + ...projectId ? { + projectId + } : {}, + ...preview ? { + preview + } : {}, + ...cwd ? { + cwd + } : { + cwd: null + }, + createdAt: createdAt ?? null, + updatedAt: updatedAt ?? null, + ...sectionId ? { + section: { + id: sectionId, + name: sectionName ?? sectionId + } + } : { + section: null + }, + ...modelProvider ? { + modelProvider + } : { + modelProvider: null + }, + ...model ? { + model + } : { + model: null + }, + ...originator ? { + originator + } : { + originator: null + } + }; + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + const record = row; + for (const key of keys){ + const value = record[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function numberField(row, keys) { + for (const key of keys){ + const value = row[key]; + if (typeof value === 'number' && Number.isFinite(value)) return value; + } + return null; + } + __webpack_require__.d(__webpack_exports__, { + Hx: ()=>listRemoteThreadsFromState, + KI: ()=>findRemoteThread, + Kl: ()=>listDiscoveredHosts, + XI: ()=>findRemoteThreadHostId + }); + }, + "./src/core/chatgpt-desktop/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var zod__rspack_import_4 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var _codex_contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); + var _errors_js__rspack_import_1 = __webpack_require__("./src/core/chatgpt-desktop/errors.ts"); + var _facade_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/facade.ts"); + var _loopback_js__rspack_import_3 = __webpack_require__("./src/core/chatgpt-desktop/loopback.ts"); + const threadId = zod__rspack_import_4.YjP().min(1).max(256); + const statusSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional() + }).strict(); + const listThreadsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + host: zod__rspack_import_4.YjP().min(1).max(256).optional(), + modelProvider: zod__rspack_import_4.YjP().min(1).max(256).optional(), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + groupBy: zod__rspack_import_4.euz('host').optional() + }).strict(); + const searchThreadsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + query: zod__rspack_import_4.YjP().min(1).max(512), + limit: zod__rspack_import_4.aig().int().min(1).max(200).default(50), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + host: zod__rspack_import_4.YjP().min(1).max(256).optional(), + modelProvider: zod__rspack_import_4.YjP().min(1).max(256).optional(), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + groupBy: zod__rspack_import_4.euz('host').optional() + }).strict(); + const listHostsSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional() + }).strict(); + const readThreadSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId, + limit: zod__rspack_import_4.aig().int().min(1).max(2000).optional(), + full: zod__rspack_import_4.zMY().default(false), + cursor: zod__rspack_import_4.YjP().min(1).max(4096).optional(), + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict(); + const openThreadSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId, + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict(); + const sendSchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + text: zod__rspack_import_4.YjP().trim().min(1).max(100000), + project: zod__rspack_import_4.YjP().min(1).max(256).optional(), + unarchive: zod__rspack_import_4.zMY().default(false), + openTimeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(90000) + }).strict().refine((value)=>!value.unarchive || Boolean(value.threadId), { + message: 'unarchive requires an existing threadId', + path: [ + 'unarchive' + ] + }).refine((value)=>!value.threadId || !value.project, { + message: 'project is only for a new thread; omit threadId', + path: [ + 'project' + ] + }); + const waitReplySchema = zod__rspack_import_4.Ikc({ + port: zod__rspack_import_4.aig().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + timeoutMs: zod__rspack_import_4.aig().int().min(1).max(600000).default(120000) + }).strict(); + const resultSchema = zod__rspack_import_4.Ikc({ + exitCode: zod__rspack_import_4.KCZ([ + zod__rspack_import_4.euz(0), + zod__rspack_import_4.euz(1) + ]) + }).catchall(zod__rspack_import_4.Pq9()); + let operationQueue = Promise.resolve(); + async function withAdapter(port, run) { + const operation = operationQueue.then(async ()=>{ + const adapter = (0, _facade_js__rspack_import_2.PP)(); + const resolved = (0, _loopback_js__rspack_import_3.L0)(process.env, port); + try { + try { + await adapter.connect({ + port: resolved + }); + } catch {} + return await run(adapter); + } finally{ + await adapter.close(); + } + }); + operationQueue = operation.catch(()=>{}); + return operation; + } + function asResult(value) { + return resultSchema.parse(JSON.parse(JSON.stringify(value))); + } + function mapError(error) { + if (error instanceof _errors_js__rspack_import_1.EH) { + return asResult({ + delivery: 'rejected', + reason: 'not-implemented', + code: 'NOT_IMPLEMENTED', + error: error.message, + exitCode: 1 + }); + } + if (error instanceof _errors_js__rspack_import_1.Rg) { + return asResult({ + delivery: error.delivery, + reason: error.reason, + code: error.code, + error: error.message, + threadId: error.threadId, + hint: error.hint, + ...error.hostId ? { + hostId: error.hostId + } : {}, + ...error.hostName ? { + hostName: error.hostName + } : {}, + exitCode: 1 + }); + } + const base = (0, _codex_contract_js__rspack_import_0.DG)(error); + if (error && typeof error === 'object' && 'code' in error) { + return asResult({ + ...base, + code: String(error.code) + }); + } + return asResult(base); + } + async function statusOperation(input) { + try { + const status = await withAdapter(input.port, (adapter)=>adapter.status()); + return asResult(status); + } catch (error) { + return mapError(error); + } + } + async function listThreadsOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.listThreads({ + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function searchThreadsOperation(input) { + try { + const out = await withAdapter(input.port, async (adapter)=>{ + if (typeof adapter.searchThreads !== 'function') { + throw new _errors_js__rspack_import_1.EH('searchThreads', 'adapter does not implement search'); + } + return adapter.searchThreads({ + query: input.query, + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy + }); + }); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function listHostsOperation(input) { + try { + const out = await withAdapter(input.port, async (adapter)=>{ + if (typeof adapter.listHosts !== 'function') { + throw new _errors_js__rspack_import_1.EH('listHosts', 'adapter does not implement listHosts'); + } + return adapter.listHosts(); + }); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function readThreadOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.readThread({ + threadId: input.threadId, + limit: input.limit ?? (input.full ? 2000 : 100), + full: input.full, + cursor: input.cursor, + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function openThreadOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.openThread(input.threadId, { + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: 0 + }); + } catch (error) { + return mapError(error); + } + } + async function sendOperation(input) { + try { + input = sendSchema.parse(input); + const out = await withAdapter(input.port, (adapter)=>adapter.sendMessage({ + threadId: input.threadId, + text: input.text, + project: input.project, + unarchive: input.unarchive, + openTimeoutMs: input.openTimeoutMs + })); + return asResult({ + ...out, + exitCode: out.delivery === 'accepted' ? 0 : 1 + }); + } catch (error) { + return mapError(error); + } + } + async function waitReplyOperation(input) { + try { + const out = await withAdapter(input.port, (adapter)=>adapter.waitForReply({ + threadId: input.threadId, + timeoutMs: input.timeoutMs + })); + return asResult({ + ...out, + exitCode: out.delivery === 'replied' ? 0 : 1 + }); + } catch (error) { + return mapError(error); + } + } + function resultText(result) { + if (result.error) return String(result.error); + if (result.reachable === true) { + const backend = result.appServerFallback && typeof result.appServerFallback === 'object' ? ` cdp + app-server probe` : ''; + return result.message ? String(result.message) : `ChatGPT Desktop CDP reachable on 127.0.0.1:${result.port}${backend}`; + } + if (result.reachable === false) return String(result.message ?? 'ChatGPT Desktop CDP unreachable'); + if (Array.isArray(result.hosts)) { + const providers = Array.isArray(result.modelProviders) ? ` providers=${result.modelProviders.length} (${String(result.modelProvidersSource ?? '')})` : ''; + return `${result.hosts.length} hosts via ${String(result.hostsSource ?? result.backend ?? 'unknown')}${providers}`; + } + if (Array.isArray(result.threads)) { + const via = result.query ? ` matching ${JSON.stringify(result.query)}` : ''; + const host = result.host && result.host !== 'all' ? ` host=${result.host}` : ''; + const provider = result.modelProvider ? ` provider=${result.modelProvider}` : ''; + return `${result.threads.length} threads${via}${host}${provider} via ${result.backend ?? 'unknown'}`; + } + if (Array.isArray(result.turns)) { + const mode = result.full ? 'full' : 'page'; + return `${result.turns.length} turns (${mode}) on ${result.threadId} via ${result.backend ?? 'unknown'}`; + } + if (result.delivery === 'accepted') { + return `Sent on ${result.threadId} via ${result.backend}` + (result.conversationId ? ` (conversation ${result.conversationId})` : ''); + } + if (result.delivery === 'replied') { + const id = result.conversationId ? ` [${result.conversationId}]` : ''; + return `${String(result.reply ?? 'replied')}${id}`; + } + if (result.delivery) return `delivery ${result.delivery}`; + return JSON.stringify(result); + } + __webpack_require__.d(__webpack_exports__, { + Bm: ()=>listHostsOperation, + D6: ()=>resultText, + Iu: ()=>openThreadOperation, + UK: ()=>readThreadOperation, + UP: ()=>sendOperation, + Uj: ()=>statusOperation, + Zg: ()=>waitReplyOperation, + tJ: ()=>listThreadsOperation, + zg: ()=>searchThreadsOperation + }, { + FC: listThreadsSchema, + FD: resultSchema, + Fn: waitReplySchema, + OR: openThreadSchema, + Rf: listHostsSchema, + Wi: sendSchema, + aW: statusSchema, + nl: searchThreadsSchema, + so: readThreadSchema + }); + }, + "./src/core/chatgpt-desktop/thread-ids.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + var _codex_thread_id_js__rspack_import_1 = __webpack_require__("./src/core/codex/thread-id.js"); + var _cdp_dom_js__rspack_import_0 = __webpack_require__("./src/core/chatgpt-desktop/cdp-dom.ts"); + const LOCAL_THREAD_ID_PREFIX = "local:"; + const TEMP_THREAD_ID_PREFIX = null && CDP_TEMP_PREFIX; + const HISTORY_CONTENT_TURN_PREFIX = 'history-content:turn:'; + function isTemporaryDesktopThreadId(threadId) { + return (0, _codex_thread_id_js__rspack_import_1.vr)(threadId) || threadId.startsWith(_codex_thread_id_js__rspack_import_1.hA); + } + function toAppServerThreadId(threadId) { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return null; + try { + return (0, _codex_thread_id_js__rspack_import_1.SX)(threadId); + } catch { + return null; + } + } + function requireAppServerThreadId(threadId) { + const bare = toAppServerThreadId(threadId); + if (!bare) { + throw new Error(`No app-server thread id mapping for ${JSON.stringify(threadId)} ` + `(temporary Desktop rows need CDP; strip a leading "local:" for durable ids)`); + } + return bare; + } + function toDesktopThreadId(threadId) { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return threadId; + if (threadId.startsWith(LOCAL_THREAD_ID_PREFIX)) return threadId; + return `${LOCAL_THREAD_ID_PREFIX}${threadId}`; + } + function threadIdsEquivalent(a, b) { + const left = toAppServerThreadId(a) ?? a; + const right = toAppServerThreadId(b) ?? b; + return left === right; + } + function toDomTurnKey(turnId) { + if (!turnId) return turnId; + if (turnId.startsWith(HISTORY_CONTENT_TURN_PREFIX)) return turnId; + return `${HISTORY_CONTENT_TURN_PREFIX}${turnId}`; + } + function appServerThreadIdCandidates(threadId) { + const bare = toAppServerThreadId(threadId); + return bare ? [ + bare + ] : []; + } + __webpack_require__.d(__webpack_exports__, { + BS: ()=>toDesktopThreadId, + LV: ()=>threadIdsEquivalent, + NM: ()=>requireAppServerThreadId, + Sr: ()=>toDomTurnKey, + nn: ()=>toAppServerThreadId, + vr: ()=>isTemporaryDesktopThreadId + }); + }, "./src/core/claude-routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { var zod__rspack_import_1 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); var _claude_channel_js__rspack_import_0 = __webpack_require__("./src/core/claude-channel.js"); @@ -6538,10 +10360,23 @@ var __webpack_modules__ = { }, "./src/core/codex/routes.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js"); + var zod__rspack_import_5 = __webpack_require__("./node_modules/zod/v4/core/core.js"); var _codex_bridge_js__rspack_import_0 = __webpack_require__("./src/core/codex-bridge.js"); var _contract_js__rspack_import_1 = __webpack_require__("./src/core/codex/contract.js"); var _conversation_js__rspack_import_2 = __webpack_require__("./src/core/codex/conversation.js"); - const id = zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9_.:-]{1,128}$/); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); + const bareId = zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9_.:-]{1,128}$/); + const id = zod__rspack_import_3.YjP().min(1).max(160).transform((value, ctx)=>{ + try { + return (0, _thread_id_js__rspack_import_4.SX)(value); + } catch (error) { + ctx.addIssue({ + code: 'custom', + message: error instanceof Error ? error.message : 'Invalid threadId' + }); + return zod__rspack_import_5.tm; + } + }).pipe(bareId); const observationFields = { expectedCwd: zod__rspack_import_3.YjP().min(1).optional(), threadId: id, @@ -6549,7 +10384,7 @@ var __webpack_modules__ = { }; const sendFields = { ...observationFields, - correlationId: id.optional(), + correlationId: bareId.optional(), envelope: zod__rspack_import_3.zMY().optional(), hop: zod__rspack_import_3.aig().int().min(0).optional(), model: zod__rspack_import_3.YjP().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/).optional(), @@ -6561,8 +10396,8 @@ var __webpack_modules__ = { 'high', 'xhigh' ]).optional(), - replyTo: id.optional(), - expectedTurnId: id.optional(), + replyTo: bareId.optional(), + expectedTurnId: bareId.optional(), whenBusy: zod__rspack_import_3.k5n([ 'reject', 'queue', @@ -6577,8 +10412,8 @@ var __webpack_modules__ = { }).strict(); const waitSchema = zod__rspack_import_3.Ikc({ ...observationFields, - turnId: id, - messageId: id.optional(), + turnId: bareId, + messageId: bareId.optional(), maxOutputBytes: zod__rspack_import_3.aig().int().min(1).max(4194304).optional() }).strict(); const watchSchema = zod__rspack_import_3.Ikc({ @@ -7071,6 +10906,451 @@ var __webpack_modules__ = { yF: withRedactedErrors }); }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop list hosts', + description: 'Discover local, remote-control environment ids, managed SSH hosts, and modelProvider ids. hostName is null when Desktop has no real display name. Use hostId or label in list/search filters; remote thread ids must be read on the owning host. warnings report provider discovery failures.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.Rf, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + } + }, + required: [] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.Bm)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.Rf + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop list threads', + description: 'List one sorted Desktop inventory without duplicate rows. Pass nextCursor as cursor with unchanged filters until null; pages fill limit unless the MCP byte budget binds. A changed/expired inventory cursor requires restarting. Display titles are capped at 200 characters with titleTruncated:true. Filter by dynamic hostId/hostName, modelProvider, or Desktop project label/projectId; projectRootPath identifies the resolved workspace. Rows expose local:, location, hostId, and provider; remote rows require their owning host for reading or sending. Backend and warnings identify partial sources.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FC, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + limit: { + type: 'number', + default: 50, + description: 'Maximum threads per page.' + }, + cursor: { + type: 'string', + description: 'Opaque nextCursor from the prior page; keep filters unchanged.' + }, + host: { + type: 'string', + description: 'Any string filter for machine/location. Reserved: "all" (default), "local". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.' + }, + modelProvider: { + type: 'string', + description: 'Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.' + }, + project: { + type: 'string', + description: 'Match a Desktop project label or app-server projectId. Dynamic string, not an enum.' + }, + groupBy: { + type: 'string', + enum: [ + 'host' + ], + description: 'When "host", also return groups[] keyed by host (grouping mode, not a host id).' + } + }, + required: [] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.tJ)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.FC + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop open thread', + description: 'Navigate Desktop to an existing local: or bare conversation id, even when the row is absent from the sidebar. Archived routes return archived:true without unarchiving; read them with chatgpt_desktop_read_thread. A remote-control id is owned by its hostId and cannot be opened on this Mac. CDP_UNREACHABLE means Desktop must be reachable locally.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.OR, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number', + description: 'Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).' + }, + threadId: { + type: 'string', + description: 'local: or bare durable id. Temporary local:client-new-thread:* works only while selected; remote ids require their owning host.' + }, + openTimeoutMs: { + type: 'number', + description: 'Maximum time to wait for the requested conversation (1-600000 ms).' + } + }, + required: [ + 'threadId' + ] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.Iu)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.OR + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop read thread', + description: 'Read a local:, bare durable id, or selected temporary local:client-new-thread:* id. Pass nextCursor back as cursor with the same threadId/full mode until complete:true. Pages stop at a byte budget; oversized turns use ordered continuation fragments with stable turnKey and field offsets, without dropping text. Archived threads remain readable. Warnings explain partial pages or app-server errors. REMOTE_THREAD_NOT_LOADED includes the owning hostId: call on that host.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.so, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + render: { + maxElapsedMs: 660000 + }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + threadId: { + type: 'string', + description: 'local:, bare durable id, or selected local:client-new-thread:* temporary id. Remote ids return REMOTE_THREAD_NOT_LOADED with hostId.' + }, + limit: { + type: 'number' + }, + cursor: { + type: 'string', + description: 'Opaque nextCursor from the prior read page. Keep threadId and full unchanged.' + }, + full: { + type: 'boolean', + description: 'When true, page from the start through the complete history (up to 2000 turns by default); otherwise return the most recent 100 turns by default. Explicit limit bounds either mode.' + }, + openTimeoutMs: { + type: 'number', + description: 'Max wait for Loading task… to clear after opening (default 90000).' + } + }, + required: [ + 'threadId' + ] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.UK)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.so + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop search threads', + description: 'Search full Desktop thread metadata by case-insensitive substring. Display titles are capped at 200 characters with titleTruncated:true. Pass nextCursor as cursor with unchanged query and filters to retrieve all matches. Dynamic hostId/hostName, modelProvider, and project filters apply. Remote results identify hostId and cannot be read on this Mac.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.nl, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + query: { + type: 'string', + description: 'Case-insensitive substring over thread metadata.' + }, + limit: { + type: 'number', + default: 50, + description: 'Maximum matches.' + }, + cursor: { + type: 'string', + description: 'Opaque nextCursor from the previous search page; keep query and filters unchanged.' + }, + host: { + type: 'string', + description: 'Any string filter for machine/location. Reserved: "all" (default), "local". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.' + }, + modelProvider: { + type: 'string', + description: 'Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.' + }, + project: { + type: 'string', + description: 'Match a Desktop project label or app-server projectId.' + }, + groupBy: { + type: 'string', + enum: [ + 'host' + ], + description: 'When "host", also return groups[] keyed by host (grouping mode, not a host id).' + } + }, + required: [ + 'query' + ] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.zg)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.nl + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop send', + description: 'Send once through local Desktop CDP. With threadId, accept local:, bare durable id, or currently selected local:client-new-thread:*; remote ids must be sent on their hostId. Omit threadId and project for a new chat outside projects; omit threadId and set project for a new chat inside it. The empty view and project scope are checked before typing. New-thread receipts include temporaryThreadId when seen and return durable local: once resolved. ARCHIVED_THREAD rejects without sending; unarchive:true explicitly unarchives that existing thread first. PROJECT_UNAVAILABLE identifies a disabled project action or missing configured root; NEW_CHAT_NAVIGATION_FAILED reports an unchanged view promptly. CDP_UNREACHABLE and COMPOSER_HAS_DRAFT require operator inspection, not blind retry.', + annotations: { + readOnlyHint: false + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.Wi, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + render: { + maxElapsedMs: 180000 + }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + threadId: { + type: 'string', + description: 'local:, bare durable id, or selected local:client-new-thread:*; omit for a new chat.' + }, + text: { + type: 'string' + }, + project: { + type: 'string', + description: 'With threadId omitted, create the new chat inside this project. Omit for a new chat outside projects.' + }, + unarchive: { + type: 'boolean', + default: false, + description: 'Existing threadId only: explicitly call thread/unarchive after ARCHIVED_THREAD, then retry the send once. Never implicit.' + }, + openTimeoutMs: { + type: 'number' + } + }, + required: [ + 'text' + ] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.UP)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.Wi + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop status', + description: 'Check local ChatGPT Desktop CDP before send, wait, or open. reachable:false and exitCode:1 mean CDP is down even if appServerFallback.reachable is true; list/search/read may still work through app-server. There is no remote CDP transport.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.aW, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number', + description: 'Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).' + } + }, + required: [] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.Uj)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.aW + }, { + "default": __rspack_default_export + }); + }, + "./src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { + __webpack_require__.r(__webpack_exports__); + var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); + var _agent_bundle_runtime__rspack_import_3 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js"); + var agent_bundle_routes__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/routes.js"); + var _core_chatgpt_desktop_routes_js__rspack_import_2 = __webpack_require__("./src/core/chatgpt-desktop/routes.ts"); + const __rspack_default_export = (0, agent_bundle_routes__rspack_import_1.uO)({ + title: 'ChatGPT Desktop wait for reply', + description: 'After chatgpt_desktop_send, wait for the selected Desktop reply over CDP. Accept the returned durable local: or the temporary local:client-new-thread:* while selected; omit threadId only when staying on the same selected chat. Returns reply, durable threadId and conversationId. timeout is not proof the send failed; CDP_UNREACHABLE requires Desktop recovery. Remote ids must be handled on their hostId.', + annotations: { + readOnlyHint: true + }, + inputSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.Fn, + resultSchema: _core_chatgpt_desktop_routes_js__rspack_import_2.FD, + render: { + maxElapsedMs: 660000 + }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number' + }, + threadId: { + type: 'string', + description: 'Durable local:, bare id, or selected local:client-new-thread:* from send.' + }, + timeoutMs: { + type: 'number', + default: 120000 + } + }, + required: [] + } + }, async (input)=>{ + const out = await (0, _core_chatgpt_desktop_routes_js__rspack_import_2.Zg)(input); + return (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Result, { + value: out, + children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_3.g.Text, { + children: (0, _core_chatgpt_desktop_routes_js__rspack_import_2.D6)(out) + }) + }); + }); + __webpack_require__.d(__webpack_exports__, { + inputSchema: ()=>_core_chatgpt_desktop_routes_js__rspack_import_2.Fn + }, { + "default": __rspack_default_export + }); + }, "./src/mcp/grok-bot/tools/claude_send.tsx" (__unused_rspack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var react_jsx_runtime__rspack_import_0 = __webpack_require__("./node_modules/react/jsx-runtime.js"); @@ -7375,10 +11655,11 @@ var __webpack_modules__ = { properties: { "limit": { "type": "number", - "description": "Maximum threads in this page: 1-200." + "description": "Maximum threads in this page: 1-200. Pass modelProviders:[] internally so every provider is included; use cursor/nextCursor to page." }, "cursor": { - "type": "string" + "type": "string", + "description": "Opaque nextCursor from a previous page." } }, required: [] @@ -56259,48 +60540,381 @@ var __webpack_modules__ = { "./.agent-bundle-virtual/mcp-grok-bot-b8c2461e-1.mjs" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var node_url__rspack_import_0 = __webpack_require__("node:url"); - var _agent_bundle_runtime__rspack_import_17 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/49.js"); + var _agent_bundle_runtime__rspack_import_25 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/49.js"); var agent_bundle_mcp_server_runtime__rspack_import_1 = __webpack_require__("./node_modules/agent-bundle/dist/mcp-server-runtime.js"); - var _agent_bundle_runtime_lineage__rspack_import_18 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/lineage.js"); + var _agent_bundle_runtime_lineage__rspack_import_26 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/lineage.js"); var agent_bundle_mcp_apps__rspack_import_2 = __webpack_require__("./.agent-bundle-virtual/mcp-grok-bot-b8c2461e-0.mjs"); - var _src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_3 = __webpack_require__("./src/mcp/grok-bot/tools/claude_send.tsx"); - var _src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_4 = __webpack_require__("./src/mcp/grok-bot/tools/codex_new.tsx"); - var _src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_5 = __webpack_require__("./src/mcp/grok-bot/tools/codex_send.tsx"); - var _src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_6 = __webpack_require__("./src/mcp/grok-bot/tools/codex_threads.tsx"); - var _src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_7 = __webpack_require__("./src/mcp/grok-bot/tools/codex_wait.tsx"); - var _src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_8 = __webpack_require__("./src/mcp/grok-bot/tools/codex_watch.tsx"); - var _src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_9 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_start.tsx"); - var _src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_10 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_status.tsx"); - var _src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_11 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_stop.tsx"); - var _src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_12 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_codex_respond.tsx"); - var _src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_13 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_grok_approvals.tsx"); - var _src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_14 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_grok_respond.tsx"); - var _src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_15 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_send.tsx"); - var _src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_16 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_thread.tsx"); - const route0 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_3, 'default'), _src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_3); - const route1 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_4, 'default'), _src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_4); - const route2 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_5, 'default'), _src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_5); - const route3 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_6, 'default'), _src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_6); - const route4 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_7, 'default'), _src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_7); - const route5 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_8, 'default'), _src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_8); - const route6 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_9, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_9); - const route7 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_10, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_10); - const route8 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_11, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_11); - const route9 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_12, 'default'), _src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_12); - const route10 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_13, 'default'), _src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_13); - const route11 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_14, 'default'), _src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_14); - const route12 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_15, 'default'), _src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_15); - const route13 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_16, 'default'), _src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_16); + var _src_mcp_grok_bot_tools_chatgpt_desktop_list_hosts_tsx__rspack_import_3 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx"); + var _src_mcp_grok_bot_tools_chatgpt_desktop_list_threads_tsx__rspack_import_4 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx"); + var _src_mcp_grok_bot_tools_chatgpt_desktop_open_thread_tsx__rspack_import_5 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx"); + var _src_mcp_grok_bot_tools_chatgpt_desktop_read_thread_tsx__rspack_import_6 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx"); + var _src_mcp_grok_bot_tools_chatgpt_desktop_search_threads_tsx__rspack_import_7 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx"); + var _src_mcp_grok_bot_tools_chatgpt_desktop_send_tsx__rspack_import_8 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx"); + var _src_mcp_grok_bot_tools_chatgpt_desktop_status_tsx__rspack_import_9 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx"); + var _src_mcp_grok_bot_tools_chatgpt_desktop_wait_reply_tsx__rspack_import_10 = __webpack_require__("./src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx"); + var _src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_11 = __webpack_require__("./src/mcp/grok-bot/tools/claude_send.tsx"); + var _src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_12 = __webpack_require__("./src/mcp/grok-bot/tools/codex_new.tsx"); + var _src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_13 = __webpack_require__("./src/mcp/grok-bot/tools/codex_send.tsx"); + var _src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_14 = __webpack_require__("./src/mcp/grok-bot/tools/codex_threads.tsx"); + var _src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_15 = __webpack_require__("./src/mcp/grok-bot/tools/codex_wait.tsx"); + var _src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_16 = __webpack_require__("./src/mcp/grok-bot/tools/codex_watch.tsx"); + var _src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_17 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_start.tsx"); + var _src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_18 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_status.tsx"); + var _src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_19 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_bridge_stop.tsx"); + var _src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_20 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_codex_respond.tsx"); + var _src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_21 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_grok_approvals.tsx"); + var _src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_22 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_grok_respond.tsx"); + var _src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_23 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_send.tsx"); + var _src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_24 = __webpack_require__("./src/mcp/grok-bot/tools/gbot_thread.tsx"); + const route0 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_list_hosts_tsx__rspack_import_3, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_list_hosts_tsx__rspack_import_3); + const route1 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_list_threads_tsx__rspack_import_4, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_list_threads_tsx__rspack_import_4); + const route2 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_open_thread_tsx__rspack_import_5, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_open_thread_tsx__rspack_import_5); + const route3 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_read_thread_tsx__rspack_import_6, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_read_thread_tsx__rspack_import_6); + const route4 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_search_threads_tsx__rspack_import_7, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_search_threads_tsx__rspack_import_7); + const route5 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_send_tsx__rspack_import_8, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_send_tsx__rspack_import_8); + const route6 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_status_tsx__rspack_import_9, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_status_tsx__rspack_import_9); + const route7 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_chatgpt_desktop_wait_reply_tsx__rspack_import_10, 'default'), _src_mcp_grok_bot_tools_chatgpt_desktop_wait_reply_tsx__rspack_import_10); + const route8 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_11, 'default'), _src_mcp_grok_bot_tools_claude_send_tsx__rspack_import_11); + const route9 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_12, 'default'), _src_mcp_grok_bot_tools_codex_new_tsx__rspack_import_12); + const route10 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_13, 'default'), _src_mcp_grok_bot_tools_codex_send_tsx__rspack_import_13); + const route11 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_14, 'default'), _src_mcp_grok_bot_tools_codex_threads_tsx__rspack_import_14); + const route12 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_15, 'default'), _src_mcp_grok_bot_tools_codex_wait_tsx__rspack_import_15); + const route13 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_16, 'default'), _src_mcp_grok_bot_tools_codex_watch_tsx__rspack_import_16); + const route14 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_17, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_start_tsx__rspack_import_17); + const route15 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_18, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_status_tsx__rspack_import_18); + const route16 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_19, 'default'), _src_mcp_grok_bot_tools_gbot_bridge_stop_tsx__rspack_import_19); + const route17 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_20, 'default'), _src_mcp_grok_bot_tools_gbot_codex_respond_tsx__rspack_import_20); + const route18 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_21, 'default'), _src_mcp_grok_bot_tools_gbot_grok_approvals_tsx__rspack_import_21); + const route19 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_22, 'default'), _src_mcp_grok_bot_tools_gbot_grok_respond_tsx__rspack_import_22); + const route20 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_23, 'default'), _src_mcp_grok_bot_tools_gbot_send_tsx__rspack_import_23); + const route21 = Object.assign({}, Reflect.get(_src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_24, 'default'), _src_mcp_grok_bot_tools_gbot_thread_tsx__rspack_import_24); const ARTIFACT_EPOCH = "gbot@0.10.2"; - const pluginRoot = (0, _agent_bundle_runtime__rspack_import_17.E7)({ + const pluginRoot = (0, _agent_bundle_runtime__rspack_import_25.E7)({ fallback: (0, node_url__rspack_import_0.fileURLToPath)(new URL('..', import.meta.url)), stateAnchor: 'user-data' }); const openLineage = async ()=>({ dispose: async ()=>undefined, - registry: (0, _agent_bundle_runtime_lineage__rspack_import_18.Q5)() + registry: (0, _agent_bundle_runtime_lineage__rspack_import_26.Q5)() }); const routes = Object.freeze({ + "tool:grok-bot/chatgpt_desktop_list_hosts": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Discover local, remote-control environment ids, managed SSH hosts, and modelProvider ids. hostName is null when Desktop has no real display name. Use hostId or label in list/search filters; remote thread ids must be read on the owning host. warnings report provider discovery failures.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "port": { + "type": "number" + } + }, + "required": [], + "type": "object" + }, + "title": "ChatGPT Desktop list hosts" + }, + id: "tool:grok-bot/chatgpt_desktop_list_hosts", + kind: "tool", + module: route0, + name: "chatgpt_desktop_list_hosts" + }), + "tool:grok-bot/chatgpt_desktop_list_threads": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "List one sorted Desktop inventory without duplicate rows. Pass nextCursor as cursor with unchanged filters until null; pages fill limit unless the MCP byte budget binds. A changed/expired inventory cursor requires restarting. Display titles are capped at 200 characters with titleTruncated:true. Filter by dynamic hostId/hostName, modelProvider, or Desktop project label/projectId; projectRootPath identifies the resolved workspace. Rows expose local:, location, hostId, and provider; remote rows require their owning host for reading or sending. Backend and warnings identify partial sources.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "cursor": { + "description": "Opaque nextCursor from the prior page; keep filters unchanged.", + "type": "string" + }, + "groupBy": { + "description": "When \"host\", also return groups[] keyed by host (grouping mode, not a host id).", + "enum": [ + "host" + ], + "type": "string" + }, + "host": { + "description": "Any string filter for machine/location. Reserved: \"all\" (default), \"local\". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.", + "type": "string" + }, + "limit": { + "default": 50, + "description": "Maximum threads per page.", + "type": "number" + }, + "modelProvider": { + "description": "Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.", + "type": "string" + }, + "port": { + "type": "number" + }, + "project": { + "description": "Match a Desktop project label or app-server projectId. Dynamic string, not an enum.", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "title": "ChatGPT Desktop list threads" + }, + id: "tool:grok-bot/chatgpt_desktop_list_threads", + kind: "tool", + module: route1, + name: "chatgpt_desktop_list_threads" + }), + "tool:grok-bot/chatgpt_desktop_open_thread": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Navigate Desktop to an existing local: or bare conversation id, even when the row is absent from the sidebar. Archived routes return archived:true without unarchiving; read them with chatgpt_desktop_read_thread. A remote-control id is owned by its hostId and cannot be opened on this Mac. CDP_UNREACHABLE means Desktop must be reachable locally.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "openTimeoutMs": { + "description": "Maximum time to wait for the requested conversation (1-600000 ms).", + "type": "number" + }, + "port": { + "description": "Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).", + "type": "number" + }, + "threadId": { + "description": "local: or bare durable id. Temporary local:client-new-thread:* works only while selected; remote ids require their owning host.", + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "title": "ChatGPT Desktop open thread" + }, + id: "tool:grok-bot/chatgpt_desktop_open_thread", + kind: "tool", + module: route2, + name: "chatgpt_desktop_open_thread" + }), + "tool:grok-bot/chatgpt_desktop_read_thread": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Read a local:, bare durable id, or selected temporary local:client-new-thread:* id. Pass nextCursor back as cursor with the same threadId/full mode until complete:true. Pages stop at a byte budget; oversized turns use ordered continuation fragments with stable turnKey and field offsets, without dropping text. Archived threads remain readable. Warnings explain partial pages or app-server errors. REMOTE_THREAD_NOT_LOADED includes the owning hostId: call on that host.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "cursor": { + "description": "Opaque nextCursor from the prior read page. Keep threadId and full unchanged.", + "type": "string" + }, + "full": { + "description": "When true, page from the start through the complete history (up to 2000 turns by default); otherwise return the most recent 100 turns by default. Explicit limit bounds either mode.", + "type": "boolean" + }, + "limit": { + "type": "number" + }, + "openTimeoutMs": { + "description": "Max wait for Loading task… to clear after opening (default 90000).", + "type": "number" + }, + "port": { + "type": "number" + }, + "threadId": { + "description": "local:, bare durable id, or selected local:client-new-thread:* temporary id. Remote ids return REMOTE_THREAD_NOT_LOADED with hostId.", + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "render": { + "maxElapsedMs": 660000 + }, + "title": "ChatGPT Desktop read thread" + }, + id: "tool:grok-bot/chatgpt_desktop_read_thread", + kind: "tool", + module: route3, + name: "chatgpt_desktop_read_thread" + }), + "tool:grok-bot/chatgpt_desktop_search_threads": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Search full Desktop thread metadata by case-insensitive substring. Display titles are capped at 200 characters with titleTruncated:true. Pass nextCursor as cursor with unchanged query and filters to retrieve all matches. Dynamic hostId/hostName, modelProvider, and project filters apply. Remote results identify hostId and cannot be read on this Mac.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "cursor": { + "description": "Opaque nextCursor from the previous search page; keep query and filters unchanged.", + "type": "string" + }, + "groupBy": { + "description": "When \"host\", also return groups[] keyed by host (grouping mode, not a host id).", + "enum": [ + "host" + ], + "type": "string" + }, + "host": { + "description": "Any string filter for machine/location. Reserved: \"all\" (default), \"local\". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.", + "type": "string" + }, + "limit": { + "default": 50, + "description": "Maximum matches.", + "type": "number" + }, + "modelProvider": { + "description": "Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.", + "type": "string" + }, + "port": { + "type": "number" + }, + "project": { + "description": "Match a Desktop project label or app-server projectId.", + "type": "string" + }, + "query": { + "description": "Case-insensitive substring over thread metadata.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "title": "ChatGPT Desktop search threads" + }, + id: "tool:grok-bot/chatgpt_desktop_search_threads", + kind: "tool", + module: route4, + name: "chatgpt_desktop_search_threads" + }), + "tool:grok-bot/chatgpt_desktop_send": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": false + }, + "description": "Send once through local Desktop CDP. With threadId, accept local:, bare durable id, or currently selected local:client-new-thread:*; remote ids must be sent on their hostId. Omit threadId and project for a new chat outside projects; omit threadId and set project for a new chat inside it. The empty view and project scope are checked before typing. New-thread receipts include temporaryThreadId when seen and return durable local: once resolved. ARCHIVED_THREAD rejects without sending; unarchive:true explicitly unarchives that existing thread first. PROJECT_UNAVAILABLE identifies a disabled project action or missing configured root; NEW_CHAT_NAVIGATION_FAILED reports an unchanged view promptly. CDP_UNREACHABLE and COMPOSER_HAS_DRAFT require operator inspection, not blind retry.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "openTimeoutMs": { + "type": "number" + }, + "port": { + "type": "number" + }, + "project": { + "description": "With threadId omitted, create the new chat inside this project. Omit for a new chat outside projects.", + "type": "string" + }, + "text": { + "type": "string" + }, + "threadId": { + "description": "local:, bare durable id, or selected local:client-new-thread:*; omit for a new chat.", + "type": "string" + }, + "unarchive": { + "default": false, + "description": "Existing threadId only: explicitly call thread/unarchive after ARCHIVED_THREAD, then retry the send once. Never implicit.", + "type": "boolean" + } + }, + "required": [ + "text" + ], + "type": "object" + }, + "render": { + "maxElapsedMs": 180000 + }, + "title": "ChatGPT Desktop send" + }, + id: "tool:grok-bot/chatgpt_desktop_send", + kind: "tool", + module: route5, + name: "chatgpt_desktop_send" + }), + "tool:grok-bot/chatgpt_desktop_status": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "Check local ChatGPT Desktop CDP before send, wait, or open. reachable:false and exitCode:1 mean CDP is down even if appServerFallback.reachable is true; list/search/read may still work through app-server. There is no remote CDP transport.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "port": { + "description": "Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).", + "type": "number" + } + }, + "required": [], + "type": "object" + }, + "title": "ChatGPT Desktop status" + }, + id: "tool:grok-bot/chatgpt_desktop_status", + kind: "tool", + module: route6, + name: "chatgpt_desktop_status" + }), + "tool:grok-bot/chatgpt_desktop_wait_reply": Object.freeze({ + config: { + "annotations": { + "readOnlyHint": true + }, + "description": "After chatgpt_desktop_send, wait for the selected Desktop reply over CDP. Accept the returned durable local: or the temporary local:client-new-thread:* while selected; omit threadId only when staying on the same selected chat. Returns reply, durable threadId and conversationId. timeout is not proof the send failed; CDP_UNREACHABLE requires Desktop recovery. Remote ids must be handled on their hostId.", + "inputJsonSchema": { + "additionalProperties": false, + "properties": { + "port": { + "type": "number" + }, + "threadId": { + "description": "Durable local:, bare id, or selected local:client-new-thread:* from send.", + "type": "string" + }, + "timeoutMs": { + "default": 120000, + "type": "number" + } + }, + "required": [], + "type": "object" + }, + "render": { + "maxElapsedMs": 660000 + }, + "title": "ChatGPT Desktop wait for reply" + }, + id: "tool:grok-bot/chatgpt_desktop_wait_reply", + kind: "tool", + module: route7, + name: "chatgpt_desktop_wait_reply" + }), "tool:grok-bot/claude_send": Object.freeze({ config: { "annotations": { @@ -56333,7 +60947,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/claude_send", kind: "tool", - module: route0, + module: route8, name: "claude_send" }), "tool:grok-bot/codex_new": Object.freeze({ @@ -56381,7 +60995,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/codex_new", kind: "tool", - module: route1, + module: route9, name: "codex_new" }), "tool:grok-bot/codex_send": Object.freeze({ @@ -56475,7 +61089,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/codex_send", kind: "tool", - module: route2, + module: route10, name: "codex_send" }), "tool:grok-bot/codex_threads": Object.freeze({ @@ -56491,10 +61105,11 @@ var __webpack_modules__ = { "additionalProperties": false, "properties": { "cursor": { + "description": "Opaque nextCursor from a previous page.", "type": "string" }, "limit": { - "description": "Maximum threads in this page: 1-200.", + "description": "Maximum threads in this page: 1-200. Pass modelProviders:[] internally so every provider is included; use cursor/nextCursor to page.", "type": "number" } }, @@ -56508,7 +61123,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/codex_threads", kind: "tool", - module: route3, + module: route11, name: "codex_threads" }), "tool:grok-bot/codex_wait": Object.freeze({ @@ -56557,7 +61172,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/codex_wait", kind: "tool", - module: route4, + module: route12, name: "codex_wait" }), "tool:grok-bot/codex_watch": Object.freeze({ @@ -56599,7 +61214,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/codex_watch", kind: "tool", - module: route5, + module: route13, name: "codex_watch" }), "tool:grok-bot/gbot_bridge_start": Object.freeze({ @@ -56640,7 +61255,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/gbot_bridge_start", kind: "tool", - module: route6, + module: route14, name: "gbot_bridge_start" }), "tool:grok-bot/gbot_bridge_status": Object.freeze({ @@ -56665,7 +61280,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/gbot_bridge_status", kind: "tool", - module: route7, + module: route15, name: "gbot_bridge_status" }), "tool:grok-bot/gbot_bridge_stop": Object.freeze({ @@ -56693,7 +61308,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/gbot_bridge_stop", kind: "tool", - module: route8, + module: route16, name: "gbot_bridge_stop" }), "tool:grok-bot/gbot_codex_respond": Object.freeze({ @@ -56751,7 +61366,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/gbot_codex_respond", kind: "tool", - module: route9, + module: route17, name: "gbot_codex_respond" }), "tool:grok-bot/gbot_grok_approvals": Object.freeze({ @@ -56781,7 +61396,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/gbot_grok_approvals", kind: "tool", - module: route10, + module: route18, name: "gbot_grok_approvals" }), "tool:grok-bot/gbot_grok_respond": Object.freeze({ @@ -56827,7 +61442,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/gbot_grok_respond", kind: "tool", - module: route11, + module: route19, name: "gbot_grok_respond" }), "tool:grok-bot/gbot_send": Object.freeze({ @@ -56887,7 +61502,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/gbot_send", kind: "tool", - module: route12, + module: route20, name: "gbot_send" }), "tool:grok-bot/gbot_thread": Object.freeze({ @@ -56932,7 +61547,7 @@ var __webpack_modules__ = { }, id: "tool:grok-bot/gbot_thread", kind: "tool", - module: route13, + module: route21, name: "gbot_thread" }) }); @@ -57406,8 +62021,10 @@ var __webpack_modules__ = { var node_child_process__rspack_import_5 = __webpack_require__("node:child_process"); var _package_json__rspack_import_6 = __webpack_require__("./package.json"); var _codex_contract_js__rspack_import_7 = __webpack_require__("./src/core/codex/contract.js"); - var _desktop_shim_js__rspack_import_8 = __webpack_require__("./src/core/desktop-shim.js"); - var _user_machine_guidance_js__rspack_import_9 = __webpack_require__("./src/core/user-machine-guidance.js"); + var _codex_remote_control_js__rspack_import_8 = __webpack_require__("./src/core/codex/remote-control.js"); + var _codex_thread_id_js__rspack_import_11 = __webpack_require__("./src/core/codex/thread-id.js"); + var _desktop_shim_js__rspack_import_9 = __webpack_require__("./src/core/desktop-shim.js"); + var _user_machine_guidance_js__rspack_import_10 = __webpack_require__("./src/core/user-machine-guidance.js"); const PINNED_CODEX_VERSION = "0.158.0"; const UPSTREAM_DESKTOP_ISSUES = [ "https://github.com/openai/codex/issues/41014", @@ -57415,8 +62032,9 @@ var __webpack_modules__ = { ]; const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; const WS_MAX_HEADER_BYTES = 16 * 1024; - const WS_MAX_MESSAGE_BYTES = 4 * 1024 * 1024; - const WS_MAX_BUFFER_BYTES = 8 * 1024 * 1024; + const WS_MAX_MESSAGE_BYTES = 16 * 1024 * 1024; + const WS_MAX_BUFFER_BYTES = 32 * 1024 * 1024; + const CODEX_SEND_MAX_BYTES = 4 * 1024 * 1024; const WS_MAX_WRITE_BYTES = 8 * 1024 * 1024; const CODEX_MAX_REQUESTS = 128; const CODEX_MAX_LISTENERS = 128; @@ -57444,11 +62062,11 @@ var __webpack_modules__ = { function boxUnreachableMessage(path) { return [ "No Codex app-server control socket at " + path + ".", - _user_machine_guidance_js__rspack_import_9.M + _user_machine_guidance_js__rspack_import_10.M ].join("\n"); } function unreachableMessage(path, desktopAttached = "unknown", env = process.env) { - if ((0, _user_machine_guidance_js__rspack_import_9.R)({ + if ((0, _user_machine_guidance_js__rspack_import_10.R)({ path, env, home: env.HOME || (0, node_os__rspack_import_3.homedir)() @@ -58155,7 +62773,7 @@ var __webpack_modules__ = { } const CODEX_VERSION_PROBE_TIMEOUT_MS = 3000; function probeLocalCodexVersion(timeoutMs = CODEX_VERSION_PROBE_TIMEOUT_MS) { - const out = spawnSync("codex", [ + const out = (0, node_child_process__rspack_import_5.spawnSync)("codex", [ "--version" ], { encoding: "utf8", @@ -58188,7 +62806,7 @@ var __webpack_modules__ = { function listDesktopProcesses(timeoutMs = DESKTOP_PROCESS_LIST_TIMEOUT_MS) { if (process.platform === "win32") return ""; try { - const out = spawnSync("ps", [ + const out = (0, node_child_process__rspack_import_5.spawnSync)("ps", [ "-eo", "args" ], { @@ -58216,7 +62834,7 @@ var __webpack_modules__ = { let desktopShimConfigured = Boolean(shim && shim.installed && shim.wrapperPointsAtShim); if (shim === undefined) { try { - const live = desktopShimStatus({ + const live = (0, _desktop_shim_js__rspack_import_9.em)({ env }); desktopShimConfigured = Boolean(live.installed && live.wrapperPointsAtShim); @@ -58243,14 +62861,14 @@ var __webpack_modules__ = { } catch (err) { if (err instanceof CodexRouteError) { const message = err.mode === "socket-absent" ? unreachableMessage(path, desktopAttached, env) : err.message; - return withStatusExitCode({ + return (0, _codex_contract_js__rspack_import_7.AQ)({ ...base, reachable: false, mode: err.mode, message }); } - if (err instanceof CodexProtocolError) return withStatusExitCode({ + if (err instanceof CodexProtocolError) return (0, _codex_contract_js__rspack_import_7.AQ)({ ...base, reachable: true, mode: err.mode, @@ -58260,7 +62878,7 @@ var __webpack_modules__ = { } session.client.close(); const daemonVersion = appServerVersion(session.init); - return withStatusExitCode({ + return (0, _codex_contract_js__rspack_import_7.AQ)({ ...base, reachable: true, mode: "daemon", @@ -58287,6 +62905,13 @@ var __webpack_modules__ = { if (value == null) return null; return typeof value === "string" ? singleLine(value) : value; } + function summarizeSection(section) { + if (!isObject(section) || typeof section.id !== "string" || !section.id) return null; + return { + id: singleLine(section.id), + name: lineField(section.name) + }; + } function summarizeThread(t) { if (!isObject(t) || typeof t.id !== "string" || !t.id) throw new CodexProtocolError("thread/list", "entry without a string `id`"); const type = isObject(t.status) && typeof t.status.type === "string" ? t.status.type : "unknown"; @@ -58298,43 +62923,72 @@ var __webpack_modules__ = { preview: typeof t.preview === "string" ? stripTerminalControls(t.preview) : "", cwd: lineField(t.cwd), source: sourceField(t.source), - updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null + createdAt: typeof t.createdAt === "number" ? t.createdAt : null, + updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null, + section: summarizeSection(t.section), + projectId: lineField(t.projectId), + modelProvider: lineField(t.modelProvider), + model: lineField(t.model), + originator: lineField(t.originator) }; } const THREAD_LIST_MAX_LIMIT = 200; - async function listCodexThreads({ limit = 20, cursor, env = process.env } = {}) { + async function listCodexThreads({ limit = 20, cursor, modelProviders = [], env = process.env } = {}) { if (!Number.isInteger(limit) || limit < 1 || limit > THREAD_LIST_MAX_LIMIT) { throw new RangeError("--limit must be an integer 1-" + THREAD_LIST_MAX_LIMIT); } if (cursor !== undefined && (typeof cursor !== "string" || !cursor)) throw new RangeError("--cursor must be a non-empty string"); + if (!Array.isArray(modelProviders) || modelProviders.some((p)=>typeof p !== "string")) { + throw new RangeError("modelProviders must be an array of strings (empty = all providers)"); + } const { client } = await openSession(env); try { - const params = { + const baseParams = { limit, - useStateDbOnly: true, + modelProviders, ...cursor !== undefined ? { cursor } : {} }; let out; + let usedStateDbOnly = true; try { - out = await client.request("thread/list", params); + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: true + }); } catch (err) { throw transportError(err); } if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + if (out.data.length === 0 && cursor === undefined && (out.nextCursor == null || out.nextCursor === "")) { + try { + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: false + }); + usedStateDbOnly = false; + } catch (err) { + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + } if (out.nextCursor != null && typeof out.nextCursor !== "string") throw new CodexProtocolError("thread/list", "`nextCursor` is not a string"); const threads = out.data.map(summarizeThread); return { threads, nextCursor: out.nextCursor ?? null, - limit + limit, + useStateDbOnly: usedStateDbOnly }; } finally{ client.close(); } } - function explainSendError(err, threadId) { + function explainSendError(err, threadId, env = process.env) { + if (err instanceof _codex_remote_control_js__rspack_import_8.Rg) return err; + const mapped = (0, _codex_remote_control_js__rspack_import_8.R3)(err, threadId, env); + if (mapped instanceof _codex_remote_control_js__rspack_import_8.Rg) return mapped; if (!(err instanceof CodexRpcError)) return err; const msg = String(err.rpc && err.rpc.message || ""); if (/no rollout found|thread not found/i.test(msg)) { @@ -58409,12 +63063,19 @@ var __webpack_modules__ = { function assertThreadAllowed(threadId, env = process.env) { const raw = env.GROK_BOT_CODEX_THREADS; if (raw == null || raw.trim() === "") return; - const allowed = raw.split(",").map((s)=>s.trim()).filter(Boolean); - if (!allowed.includes(threadId)) { - throw new CodexSendError("Codex thread " + threadId + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { + const bare = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + const allowed = raw.split(",").map((s)=>{ + try { + return (0, _codex_thread_id_js__rspack_import_11.SX)(s.trim()); + } catch { + return s.trim(); + } + }).filter(Boolean); + if (!allowed.includes(bare)) { + throw new CodexSendError("Codex thread " + bare + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { delivery: "rejected", reason: "route-not-allowed", - threadId + threadId: bare }); } } @@ -58484,6 +63145,7 @@ var __webpack_modules__ = { } async function listCodexQueue(threadId, { env = process.env, limit = 50, cursor } = {}) { requireExperimental(env, "gbot codex queue"); + threadId = normalizeCodexThreadId(threadId); const { client } = await openSession(env, { experimental: true }); @@ -58514,6 +63176,64 @@ var __webpack_modules__ = { client.close(); } } + async function readCodexThread(threadId, { limit = 100, env = process.env } = {}) { + if (!Number.isInteger(limit) || limit < 1 || limit > 2000) { + throw new RangeError("limit must be an integer 1-2000"); + } + const bare = normalizeCodexThreadId(threadId); + const { client } = await openSession(env); + try { + let meta; + try { + meta = await client.request("thread/read", { + threadId: bare + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + const turns = []; + let cursor; + const seen = new Set(); + for(let page = 0; page < 40 && turns.length < limit; page++){ + let out; + try { + out = await client.request("thread/turns/list", { + threadId: bare, + limit: Math.min(100, limit - turns.length), + itemsView: "full", + sortDirection: "desc", + ...cursor !== undefined ? { + cursor + } : {} + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) { + throw new CodexProtocolError("thread/turns/list", "missing `data` array"); + } + for (const row of out.data)turns.push(row); + cursor = out.nextCursor ?? null; + if (cursor === null) break; + if (typeof cursor !== "string" || !cursor || seen.has(cursor)) { + throw new CodexProtocolError("thread/turns/list", "invalid or repeated cursor"); + } + seen.add(cursor); + } + return { + threadId: bare, + thread: isObject(meta) && isObject(meta.thread) ? meta.thread : null, + turns: turns.slice(0, limit), + nextCursor: cursor ?? null + }; + } finally{ + client.close(); + } + } async function sendToCodexThread(threadId, text, { env = process.env, envelope = buildEnvelope({ env }), whenBusy = "reject", session, expectedTurnId, expectedCwd, model, effort, signal } = {}) { @@ -58559,8 +63279,8 @@ var __webpack_modules__ = { ] : [] ].includes(whenBusy)) throw new RangeError("--when-busy must be reject, queue, or persistent steer"); if (whenBusy === "steer" && (typeof expectedTurnId !== "string" || !ID_PATTERN.test(expectedTurnId))) throw new RangeError("steer requires expectedTurnId"); - if (typeof threadId !== "string" || !ID_PATTERN.test(threadId)) throw new RangeError("Invalid threadId"); - if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > WS_MAX_MESSAGE_BYTES) throw new RangeError("Message must contain text within 4 MiB"); + threadId = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > CODEX_SEND_MAX_BYTES) throw new RangeError("Message must contain text within 4 MiB"); if (!envelope || typeof envelope.messageId !== "string" || !ID_PATTERN.test(envelope.messageId)) throw new RangeError("Invalid envelope messageId"); const validated = buildEnvelope({ correlationId: envelope.correlationId, @@ -58608,8 +63328,10 @@ var __webpack_modules__ = { }); } catch (err) { assertNotCancelled(); - if (err instanceof CodexSendError) throw err; - throw new CodexSendError(explainSendError(err, threadId).message, { + if (err instanceof CodexSendError || err instanceof _codex_remote_control_js__rspack_import_8.Rg) throw err; + const explained = explainSendError(err, threadId, env); + if (explained instanceof _codex_remote_control_js__rspack_import_8.Rg) throw explained; + throw new CodexSendError(explained.message, { delivery: err instanceof CodexRpcError ? "rejected" : err && err.delivery || "unknown", 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", threadId, @@ -58799,6 +63521,7 @@ var __webpack_modules__ = { } __webpack_require__.d(__webpack_exports__, { $3: ()=>assertThreadAllowed, + MU: ()=>codexStatus, Mr: ()=>withEnvelopeHeader, QC: ()=>startCodexThread, X$: ()=>listCodexThreads, @@ -58814,7 +63537,7 @@ var __webpack_modules__ = { function statusExitCode(status) { return status.reachable && status.mode === "daemon" ? 0 : 1; } - function withStatusExitCode1(status) { + function withStatusExitCode(status) { return { ...status, exitCode: statusExitCode(status) @@ -58843,7 +63566,11 @@ var __webpack_modules__ = { "messageId", "correlationId", "refused", - "hop" + "hop", + "hostId", + "hostName", + "hint", + "code" ]){ if (error[key] !== undefined) out[key] = error[key]; } @@ -58870,6 +63597,7 @@ var __webpack_modules__ = { }; } __webpack_require__.d(__webpack_exports__, { + AQ: ()=>withStatusExitCode, DG: ()=>outcomeFromError, Dz: ()=>outcomeFromReceipt }); @@ -58878,9 +63606,12 @@ var __webpack_modules__ = { var _contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); var node_fs__rspack_import_1 = __webpack_require__("node:fs"); var _codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); + var _remote_control_js__rspack_import_3 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); const ID = /^[A-Za-z0-9_.:-]{1,128}$/; const BUDGET = 4 * 1024 * 1024; function conversationId(value, name = 'threadId') { + if (name === 'threadId') return (0, _thread_id_js__rspack_import_4.SX)(value); if (typeof value !== 'string' || !ID.test(value)) throw new RangeError(`${name} must be 1-128 identifier characters`); return value; } @@ -59005,7 +63736,8 @@ var __webpack_modules__ = { if (expected !== undefined && (typeof cwd !== 'string' || (0, node_fs__rspack_import_1.realpathSync)(cwd) !== expected)) throw new Error('Codex thread cwd does not match expectedCwd'); } catch (error) { await close(); - throw error; + if (error instanceof _remote_control_js__rspack_import_3.Rg) throw error; + throw (0, _remote_control_js__rspack_import_3.R3)(error, threadId, env); } async function wait({ turnId, messageId, afterMessageId, timeoutMs = 120000, signal: waitSignal, maxOutputBytes = 1048576 }) { conversationId(turnId, 'turnId'); @@ -59355,7 +64087,270 @@ var __webpack_modules__ = { j: ()=>openCodexConversation }); }, - "./src/core/desktop-shim.js" (__unused_rspack___webpack_module__, __unused_rspack___webpack_exports__, __webpack_require__) { + "./src/core/codex/remote-control.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var node_os__rspack_import_1 = __webpack_require__("node:os"); + var node_path__rspack_import_2 = __webpack_require__("node:path"); + var _thread_id_js__rspack_import_3 = __webpack_require__("./src/core/codex/thread-id.js"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + class RemoteThreadNotLoadedError1 extends Error { + constructor(threadId, hostId = null, hostName = null){ + const hostLabel = hostName ? `${JSON.stringify(hostName)} (${JSON.stringify(hostId)})` : hostId ? JSON.stringify(hostId) : null; + const owner = hostLabel ? `owned by remote-control host ${hostLabel}` : 'owned by a remote-control host (hostId unknown; check ~/.codex/.codex-global-state.json)'; + const hint = hostId ? `Read this thread via host ${JSON.stringify(hostId)}'s app-server` + (hostName ? ` (${hostName})` : '') + '. SSH remoting is not implemented in this adapter.' : "Read this thread via the owning host's app-server. SSH remoting is not implemented in this adapter."; + super(`Codex thread ${JSON.stringify(threadId)} is not loaded on the local app-server; ${owner}. ${hint}`); + this.name = 'RemoteThreadNotLoadedError'; + this.code = 'REMOTE_THREAD_NOT_LOADED'; + this.delivery = 'rejected'; + this.reason = 'remote-thread-not-loaded'; + this.threadId = threadId; + this.hostId = hostId; + this.hostName = hostName; + this.hint = hint; + } + } + function codexGlobalStatePath(env = process.env) { + const home = env.CODEX_HOME || (0, node_path__rspack_import_2.join)((0, node_os__rspack_import_1.homedir)(), '.codex'); + return (0, node_path__rspack_import_2.join)(home, '.codex-global-state.json'); + } + function loadCodexGlobalState(env = process.env) { + let raw; + try { + raw = (0, node_fs__rspack_import_0.readFileSync)(codexGlobalStatePath(env), 'utf8'); + } catch { + return null; + } + try { + const data = JSON.parse(raw); + if (!data || typeof data !== 'object' || Array.isArray(data)) return null; + return data; + } catch { + return null; + } + } + function codexStateEntries(data) { + const nested = data?.['electron-persisted-atom-state']; + return [ + nested, + data + ].filter((value)=>value && typeof value === 'object' && !Array.isArray(value)); + } + function findRemoteThreadHost(threadId, env = process.env) { + let bare; + try { + bare = (0, _thread_id_js__rspack_import_3.SX)(threadId); + } catch { + bare = typeof threadId === 'string' && threadId.startsWith("local:") ? threadId.slice("local:".length) : threadId; + } + if (!bare) return null; + const data = loadCodexGlobalState(env); + if (!data) return null; + const hostNames = collectHostNames(data); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value) ?? null; + for (const entry of extractThreadEntries(value)){ + const id = threadIdFromEntry(entry); + if (!id) continue; + const entryBare = id.startsWith("local:") ? id.slice("local:".length) : id; + if (entryBare === bare) return { + hostId, + hostName + }; + } + } + return null; + } + function mapRemoteThreadError(error, threadId, env = process.env) { + const message = errorMessage(error); + if (!/thread not loaded|not loaded|no rollout found|thread not found|invalid thread id/i.test(message)) { + return error instanceof Error ? error : new Error(message || String(error)); + } + const remote = findRemoteThreadHost(threadId, env); + if (remote) { + return new RemoteThreadNotLoadedError1(threadId, remote.hostId, remote.hostName); + } + if (/thread not loaded|not loaded/i.test(message)) { + return new RemoteThreadNotLoadedError1(threadId, null, null); + } + return error instanceof Error ? error : new Error(message || String(error)); + } + function collectHostNames(data) { + const names = new Map(); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const hostId = stringField(entry, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) continue; + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const id = stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + entries.push(id ? entry : { + ...entry, + id: key + }); + } + } + return entries; + } + function threadIdFromEntry(entry) { + if (typeof entry === 'string' && entry) return entry; + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + return stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + for (const key of keys){ + const value = row[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) return String(error.message); + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); + } + __webpack_require__.d(__webpack_exports__, { + IT: ()=>loadCodexGlobalState, + R3: ()=>mapRemoteThreadError, + Rg: ()=>RemoteThreadNotLoadedError1, + xr: ()=>codexStateEntries + }); + }, + "./src/core/codex/thread-id.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + const LOCAL_THREAD_ID_PREFIX = 'local:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/; + function isTemporaryDesktopThreadId(threadId) { + return typeof threadId === 'string' && (threadId.startsWith(TEMP_THREAD_ID_PREFIX) || threadId.startsWith('client-new-thread:')); + } + function normalizeCodexThreadId1(threadId) { + if (typeof threadId !== 'string' || !threadId) { + throw new RangeError('Invalid threadId'); + } + if (isTemporaryDesktopThreadId(threadId)) { + throw new RangeError('Temporary Desktop thread id ' + JSON.stringify(threadId) + ' has no app-server mapping; wait for the real conversation id'); + } + const bare = threadId.startsWith(LOCAL_THREAD_ID_PREFIX) ? threadId.slice(LOCAL_THREAD_ID_PREFIX.length) : threadId; + if (!bare || !ID_PATTERN.test(bare)) { + throw new RangeError('Invalid threadId'); + } + return bare; + } + __webpack_require__.d(__webpack_exports__, { + SX: ()=>normalizeCodexThreadId1, + vr: ()=>isTemporaryDesktopThreadId + }, { + hA: TEMP_THREAD_ID_PREFIX + }); + }, + "./src/core/desktop-shim.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { var node_child_process__rspack_import_0 = __webpack_require__("node:child_process"); var node_fs__rspack_import_1 = __webpack_require__("node:fs"); var node_os__rspack_import_2 = __webpack_require__("node:os"); @@ -59372,25 +64367,25 @@ var __webpack_modules__ = { return env.CODEX_HOME || join(env.HOME || homedir(), ".codex"); } function userHomeDir(env = process.env) { - return env.HOME || homedir(); + return env.HOME || (0, node_os__rspack_import_2.homedir)(); } function defaultPaths({ env = process.env, home = userHomeDir(env) } = {}) { - const codexHome = env.CODEX_HOME || join(home, ".codex"); - const binDir = join(codexHome, "bin"); + const codexHome = env.CODEX_HOME || (0, node_path__rspack_import_3.join)(home, ".codex"); + const binDir = (0, node_path__rspack_import_3.join)(codexHome, "bin"); return { binDir, - bridgeLogPath: join(binDir, BRIDGE_LOG_FILENAME), - bridgePath: join(binDir, BRIDGE_FILENAME), + bridgeLogPath: (0, node_path__rspack_import_3.join)(binDir, BRIDGE_LOG_FILENAME), + bridgePath: (0, node_path__rspack_import_3.join)(binDir, BRIDGE_FILENAME), codexHome, - envLogPath: join(binDir, ENV_LOG_FILENAME), - envScriptPath: join(binDir, ENV_SCRIPT_FILENAME), + envLogPath: (0, node_path__rspack_import_3.join)(binDir, ENV_LOG_FILENAME), + envScriptPath: (0, node_path__rspack_import_3.join)(binDir, ENV_SCRIPT_FILENAME), label: SHIM_LABEL, - launchAgentsDir: join(home, "Library", "LaunchAgents"), - plistPath: join(home, "Library", "LaunchAgents", `${SHIM_LABEL}.plist`), - realPath: join(codexHome, STANDALONE_REAL_SUFFIX), - socketPath: join(codexHome, "app-server-control", "app-server-control.sock"), - wrapperLogPath: join(binDir, WRAPPER_LOG_FILENAME), - wrapperPath: join(binDir, WRAPPER_FILENAME) + launchAgentsDir: (0, node_path__rspack_import_3.join)(home, "Library", "LaunchAgents"), + plistPath: (0, node_path__rspack_import_3.join)(home, "Library", "LaunchAgents", `${SHIM_LABEL}.plist`), + realPath: (0, node_path__rspack_import_3.join)(codexHome, STANDALONE_REAL_SUFFIX), + socketPath: (0, node_path__rspack_import_3.join)(codexHome, "app-server-control", "app-server-control.sock"), + wrapperLogPath: (0, node_path__rspack_import_3.join)(binDir, WRAPPER_LOG_FILENAME), + wrapperPath: (0, node_path__rspack_import_3.join)(binDir, WRAPPER_FILENAME) }; } function shouldBridge(argv) { @@ -59550,7 +64545,7 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } } const defaultRunner = (file, args)=>{ try { - return spawnSync(file, args, { + return (0, node_child_process__rspack_import_0.spawnSync)(file, args, { encoding: "utf8", timeout: 15000 }); @@ -59577,19 +64572,19 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } } function isExecutable(path) { try { - statSync(path); + (0, node_fs__rspack_import_1.statSync)(path); } catch { return false; } try { - return (statSync(path).mode & 73) !== 0; + return ((0, node_fs__rspack_import_1.statSync)(path).mode & 73) !== 0; } catch { return false; } } function fileExists(path) { try { - statSync(path); + (0, node_fs__rspack_import_1.statSync)(path); return true; } catch { return false; @@ -59741,12 +64736,12 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } } function socketStateOf(path) { try { - return statSync(path).isSocket() ? "socket" : "not-a-socket"; + return (0, node_fs__rspack_import_1.statSync)(path).isSocket() ? "socket" : "not-a-socket"; } catch (error) { return error && (error.code === "EACCES" || error.code === "EPERM") ? "permission-denied" : "absent"; } } - function desktopShimStatus1({ env = process.env, home = userHomeDir(env), platform = process.platform, runner = defaultRunner } = {}) { + function desktopShimStatus({ env = process.env, home = userHomeDir(env), platform = process.platform, runner = defaultRunner } = {}) { const paths = defaultPaths({ env, home @@ -59777,7 +64772,7 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } } const desktopCliPath = platform === "darwin" ? guiCliPath : cliPath; const installed = wrapperPresent && isExecutable(paths.wrapperPath) && bridgePresent; - const socketPath = env.CODEX_APP_SERVER_SOCK || join(paths.codexHome, "app-server-control", "app-server-control.sock"); + const socketPath = env.CODEX_APP_SERVER_SOCK || (0, node_path__rspack_import_3.join)(paths.codexHome, "app-server-control", "app-server-control.sock"); return { action: "status", bridgePath: paths.bridgePath, @@ -59805,6 +64800,9 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } wrapperPresent }; } + __webpack_require__.d(__webpack_exports__, { + em: ()=>desktopShimStatus + }); }, "./src/core/gateway.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { var node_crypto__rspack_import_0 = __webpack_require__("node:crypto"); diff --git a/artifact/scripts/gbot-relay.mjs b/artifact/scripts/gbot-relay.mjs index 94bc505..caf9d36 100644 --- a/artifact/scripts/gbot-relay.mjs +++ b/artifact/scripts/gbot-relay.mjs @@ -22133,8 +22133,10 @@ var __webpack_modules__ = { var node_child_process__rspack_import_5 = __webpack_require__("node:child_process"); var _package_json__rspack_import_6 = __webpack_require__("./package.json"); var _codex_contract_js__rspack_import_7 = __webpack_require__("./src/core/codex/contract.js"); - var _desktop_shim_js__rspack_import_8 = __webpack_require__("./src/core/desktop-shim.js"); - var _user_machine_guidance_js__rspack_import_9 = __webpack_require__("./src/core/user-machine-guidance.js"); + var _codex_remote_control_js__rspack_import_8 = __webpack_require__("./src/core/codex/remote-control.js"); + var _codex_thread_id_js__rspack_import_11 = __webpack_require__("./src/core/codex/thread-id.js"); + var _desktop_shim_js__rspack_import_9 = __webpack_require__("./src/core/desktop-shim.js"); + var _user_machine_guidance_js__rspack_import_10 = __webpack_require__("./src/core/user-machine-guidance.js"); const PINNED_CODEX_VERSION = "0.158.0"; const UPSTREAM_DESKTOP_ISSUES = [ "https://github.com/openai/codex/issues/41014", @@ -22142,8 +22144,9 @@ var __webpack_modules__ = { ]; const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; const WS_MAX_HEADER_BYTES = 16 * 1024; - const WS_MAX_MESSAGE_BYTES = 4 * 1024 * 1024; - const WS_MAX_BUFFER_BYTES = 8 * 1024 * 1024; + const WS_MAX_MESSAGE_BYTES = 16 * 1024 * 1024; + const WS_MAX_BUFFER_BYTES = 32 * 1024 * 1024; + const CODEX_SEND_MAX_BYTES = 4 * 1024 * 1024; const WS_MAX_WRITE_BYTES = 8 * 1024 * 1024; const CODEX_MAX_REQUESTS = 128; const CODEX_MAX_LISTENERS = 128; @@ -22171,11 +22174,11 @@ var __webpack_modules__ = { function boxUnreachableMessage(path) { return [ "No Codex app-server control socket at " + path + ".", - _user_machine_guidance_js__rspack_import_9.M + _user_machine_guidance_js__rspack_import_10.M ].join("\n"); } function unreachableMessage(path, desktopAttached = "unknown", env = process.env) { - if ((0, _user_machine_guidance_js__rspack_import_9.R)({ + if ((0, _user_machine_guidance_js__rspack_import_10.R)({ path, env, home: env.HOME || (0, node_os__rspack_import_3.homedir)() @@ -23014,6 +23017,13 @@ var __webpack_modules__ = { if (value == null) return null; return typeof value === "string" ? singleLine(value) : value; } + function summarizeSection(section) { + if (!isObject(section) || typeof section.id !== "string" || !section.id) return null; + return { + id: singleLine(section.id), + name: lineField(section.name) + }; + } function summarizeThread(t) { if (!isObject(t) || typeof t.id !== "string" || !t.id) throw new CodexProtocolError("thread/list", "entry without a string `id`"); const type = isObject(t.status) && typeof t.status.type === "string" ? t.status.type : "unknown"; @@ -23025,43 +23035,72 @@ var __webpack_modules__ = { preview: typeof t.preview === "string" ? stripTerminalControls(t.preview) : "", cwd: lineField(t.cwd), source: sourceField(t.source), - updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null + createdAt: typeof t.createdAt === "number" ? t.createdAt : null, + updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null, + section: summarizeSection(t.section), + projectId: lineField(t.projectId), + modelProvider: lineField(t.modelProvider), + model: lineField(t.model), + originator: lineField(t.originator) }; } const THREAD_LIST_MAX_LIMIT = 200; - async function listCodexThreads({ limit = 20, cursor, env = process.env } = {}) { + async function listCodexThreads({ limit = 20, cursor, modelProviders = [], env = process.env } = {}) { if (!Number.isInteger(limit) || limit < 1 || limit > THREAD_LIST_MAX_LIMIT) { throw new RangeError("--limit must be an integer 1-" + THREAD_LIST_MAX_LIMIT); } if (cursor !== undefined && (typeof cursor !== "string" || !cursor)) throw new RangeError("--cursor must be a non-empty string"); + if (!Array.isArray(modelProviders) || modelProviders.some((p)=>typeof p !== "string")) { + throw new RangeError("modelProviders must be an array of strings (empty = all providers)"); + } const { client } = await openSession(env); try { - const params = { + const baseParams = { limit, - useStateDbOnly: true, + modelProviders, ...cursor !== undefined ? { cursor } : {} }; let out; + let usedStateDbOnly = true; try { - out = await client.request("thread/list", params); + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: true + }); } catch (err) { throw transportError(err); } if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + if (out.data.length === 0 && cursor === undefined && (out.nextCursor == null || out.nextCursor === "")) { + try { + out = await client.request("thread/list", { + ...baseParams, + useStateDbOnly: false + }); + usedStateDbOnly = false; + } catch (err) { + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + } if (out.nextCursor != null && typeof out.nextCursor !== "string") throw new CodexProtocolError("thread/list", "`nextCursor` is not a string"); const threads = out.data.map(summarizeThread); return { threads, nextCursor: out.nextCursor ?? null, - limit + limit, + useStateDbOnly: usedStateDbOnly }; } finally{ client.close(); } } - function explainSendError(err, threadId) { + function explainSendError(err, threadId, env = process.env) { + if (err instanceof _codex_remote_control_js__rspack_import_8.Rg) return err; + const mapped = (0, _codex_remote_control_js__rspack_import_8.R3)(err, threadId, env); + if (mapped instanceof _codex_remote_control_js__rspack_import_8.Rg) return mapped; if (!(err instanceof CodexRpcError)) return err; const msg = String(err.rpc && err.rpc.message || ""); if (/no rollout found|thread not found/i.test(msg)) { @@ -23136,12 +23175,19 @@ var __webpack_modules__ = { function assertThreadAllowed(threadId, env = process.env) { const raw = env.GROK_BOT_CODEX_THREADS; if (raw == null || raw.trim() === "") return; - const allowed = raw.split(",").map((s)=>s.trim()).filter(Boolean); - if (!allowed.includes(threadId)) { - throw new CodexSendError("Codex thread " + threadId + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { + const bare = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + const allowed = raw.split(",").map((s)=>{ + try { + return (0, _codex_thread_id_js__rspack_import_11.SX)(s.trim()); + } catch { + return s.trim(); + } + }).filter(Boolean); + if (!allowed.includes(bare)) { + throw new CodexSendError("Codex thread " + bare + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), { delivery: "rejected", reason: "route-not-allowed", - threadId + threadId: bare }); } } @@ -23211,6 +23257,7 @@ var __webpack_modules__ = { } async function listCodexQueue(threadId, { env = process.env, limit = 50, cursor } = {}) { requireExperimental(env, "gbot codex queue"); + threadId = normalizeCodexThreadId(threadId); const { client } = await openSession(env, { experimental: true }); @@ -23241,6 +23288,64 @@ var __webpack_modules__ = { client.close(); } } + async function readCodexThread(threadId, { limit = 100, env = process.env } = {}) { + if (!Number.isInteger(limit) || limit < 1 || limit > 2000) { + throw new RangeError("limit must be an integer 1-2000"); + } + const bare = normalizeCodexThreadId(threadId); + const { client } = await openSession(env); + try { + let meta; + try { + meta = await client.request("thread/read", { + threadId: bare + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + const turns = []; + let cursor; + const seen = new Set(); + for(let page = 0; page < 40 && turns.length < limit; page++){ + let out; + try { + out = await client.request("thread/turns/list", { + threadId: bare, + limit: Math.min(100, limit - turns.length), + itemsView: "full", + sortDirection: "desc", + ...cursor !== undefined ? { + cursor + } : {} + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) { + throw new CodexProtocolError("thread/turns/list", "missing `data` array"); + } + for (const row of out.data)turns.push(row); + cursor = out.nextCursor ?? null; + if (cursor === null) break; + if (typeof cursor !== "string" || !cursor || seen.has(cursor)) { + throw new CodexProtocolError("thread/turns/list", "invalid or repeated cursor"); + } + seen.add(cursor); + } + return { + threadId: bare, + thread: isObject(meta) && isObject(meta.thread) ? meta.thread : null, + turns: turns.slice(0, limit), + nextCursor: cursor ?? null + }; + } finally{ + client.close(); + } + } async function sendToCodexThread(threadId, text, { env = process.env, envelope = buildEnvelope({ env }), whenBusy = "reject", session, expectedTurnId, expectedCwd, model, effort, signal } = {}) { @@ -23286,8 +23391,8 @@ var __webpack_modules__ = { ] : [] ].includes(whenBusy)) throw new RangeError("--when-busy must be reject, queue, or persistent steer"); if (whenBusy === "steer" && (typeof expectedTurnId !== "string" || !ID_PATTERN.test(expectedTurnId))) throw new RangeError("steer requires expectedTurnId"); - if (typeof threadId !== "string" || !ID_PATTERN.test(threadId)) throw new RangeError("Invalid threadId"); - if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > WS_MAX_MESSAGE_BYTES) throw new RangeError("Message must contain text within 4 MiB"); + threadId = (0, _codex_thread_id_js__rspack_import_11.SX)(threadId); + if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > CODEX_SEND_MAX_BYTES) throw new RangeError("Message must contain text within 4 MiB"); if (!envelope || typeof envelope.messageId !== "string" || !ID_PATTERN.test(envelope.messageId)) throw new RangeError("Invalid envelope messageId"); const validated = buildEnvelope({ correlationId: envelope.correlationId, @@ -23335,8 +23440,10 @@ var __webpack_modules__ = { }); } catch (err) { assertNotCancelled(); - if (err instanceof CodexSendError) throw err; - throw new CodexSendError(explainSendError(err, threadId).message, { + if (err instanceof CodexSendError || err instanceof _codex_remote_control_js__rspack_import_8.Rg) throw err; + const explained = explainSendError(err, threadId, env); + if (explained instanceof _codex_remote_control_js__rspack_import_8.Rg) throw explained; + throw new CodexSendError(explained.message, { delivery: err instanceof CodexRpcError ? "rejected" : err && err.delivery || "unknown", 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", threadId, @@ -23567,7 +23674,11 @@ var __webpack_modules__ = { "messageId", "correlationId", "refused", - "hop" + "hop", + "hostId", + "hostName", + "hint", + "code" ]){ if (error[key] !== undefined) out[key] = error[key]; } @@ -23602,9 +23713,12 @@ var __webpack_modules__ = { var _contract_js__rspack_import_0 = __webpack_require__("./src/core/codex/contract.js"); var node_fs__rspack_import_1 = __webpack_require__("node:fs"); var _codex_bridge_js__rspack_import_2 = __webpack_require__("./src/core/codex-bridge.js"); + var _remote_control_js__rspack_import_3 = __webpack_require__("./src/core/codex/remote-control.js"); + var _thread_id_js__rspack_import_4 = __webpack_require__("./src/core/codex/thread-id.js"); const ID = /^[A-Za-z0-9_.:-]{1,128}$/; const BUDGET = 4 * 1024 * 1024; function conversationId(value, name = 'threadId') { + if (name === 'threadId') return (0, _thread_id_js__rspack_import_4.SX)(value); if (typeof value !== 'string' || !ID.test(value)) throw new RangeError(`${name} must be 1-128 identifier characters`); return value; } @@ -23729,7 +23843,8 @@ var __webpack_modules__ = { if (expected !== undefined && (typeof cwd !== 'string' || (0, node_fs__rspack_import_1.realpathSync)(cwd) !== expected)) throw new Error('Codex thread cwd does not match expectedCwd'); } catch (error) { await close(); - throw error; + if (error instanceof _remote_control_js__rspack_import_3.Rg) throw error; + throw (0, _remote_control_js__rspack_import_3.R3)(error, threadId, env); } async function wait({ turnId, messageId, afterMessageId, timeoutMs = 120000, signal: waitSignal, maxOutputBytes = 1048576 }) { conversationId(turnId, 'turnId'); @@ -24080,6 +24195,264 @@ var __webpack_modules__ = { j: ()=>openCodexConversation }); }, + "./src/core/codex/remote-control.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + var node_fs__rspack_import_0 = __webpack_require__("node:fs"); + var node_os__rspack_import_1 = __webpack_require__("node:os"); + var node_path__rspack_import_2 = __webpack_require__("node:path"); + var _thread_id_js__rspack_import_3 = __webpack_require__("./src/core/codex/thread-id.js"); + const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + class RemoteThreadNotLoadedError1 extends Error { + constructor(threadId, hostId = null, hostName = null){ + const hostLabel = hostName ? `${JSON.stringify(hostName)} (${JSON.stringify(hostId)})` : hostId ? JSON.stringify(hostId) : null; + const owner = hostLabel ? `owned by remote-control host ${hostLabel}` : 'owned by a remote-control host (hostId unknown; check ~/.codex/.codex-global-state.json)'; + const hint = hostId ? `Read this thread via host ${JSON.stringify(hostId)}'s app-server` + (hostName ? ` (${hostName})` : '') + '. SSH remoting is not implemented in this adapter.' : "Read this thread via the owning host's app-server. SSH remoting is not implemented in this adapter."; + super(`Codex thread ${JSON.stringify(threadId)} is not loaded on the local app-server; ${owner}. ${hint}`); + this.name = 'RemoteThreadNotLoadedError'; + this.code = 'REMOTE_THREAD_NOT_LOADED'; + this.delivery = 'rejected'; + this.reason = 'remote-thread-not-loaded'; + this.threadId = threadId; + this.hostId = hostId; + this.hostName = hostName; + this.hint = hint; + } + } + function codexGlobalStatePath(env = process.env) { + const home = env.CODEX_HOME || (0, node_path__rspack_import_2.join)((0, node_os__rspack_import_1.homedir)(), '.codex'); + return (0, node_path__rspack_import_2.join)(home, '.codex-global-state.json'); + } + function loadCodexGlobalState(env = process.env) { + let raw; + try { + raw = (0, node_fs__rspack_import_0.readFileSync)(codexGlobalStatePath(env), 'utf8'); + } catch { + return null; + } + try { + const data = JSON.parse(raw); + if (!data || typeof data !== 'object' || Array.isArray(data)) return null; + return data; + } catch { + return null; + } + } + function codexStateEntries(data) { + const nested = data?.['electron-persisted-atom-state']; + return [ + nested, + data + ].filter((value)=>value && typeof value === 'object' && !Array.isArray(value)); + } + function findRemoteThreadHost(threadId, env = process.env) { + let bare; + try { + bare = (0, _thread_id_js__rspack_import_3.SX)(threadId); + } catch { + bare = typeof threadId === 'string' && threadId.startsWith("local:") ? threadId.slice("local:".length) : threadId; + } + if (!bare) return null; + const data = loadCodexGlobalState(env); + if (!data) return null; + const hostNames = collectHostNames(data); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value) ?? null; + for (const entry of extractThreadEntries(value)){ + const id = threadIdFromEntry(entry); + if (!id) continue; + const entryBare = id.startsWith("local:") ? id.slice("local:".length) : id; + if (entryBare === bare) return { + hostId, + hostName + }; + } + } + return null; + } + function mapRemoteThreadError(error, threadId, env = process.env) { + const message = errorMessage(error); + if (!/thread not loaded|not loaded|no rollout found|thread not found|invalid thread id/i.test(message)) { + return error instanceof Error ? error : new Error(message || String(error)); + } + const remote = findRemoteThreadHost(threadId, env); + if (remote) { + return new RemoteThreadNotLoadedError1(threadId, remote.hostId, remote.hostName); + } + if (/thread not loaded|not loaded/i.test(message)) { + return new RemoteThreadNotLoadedError1(threadId, null, null); + } + return error instanceof Error ? error : new Error(message || String(error)); + } + function collectHostNames(data) { + const names = new Map(); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)){ + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value){ + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)){ + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; + } + function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const hostId = stringField(entry, [ + 'hostId', + 'id', + 'host_id', + 'uuid' + ]); + const hostName = stringField(entry, [ + 'name', + 'hostName', + 'displayName', + 'envName', + 'label', + 'title' + ]); + if (!hostId || !hostName) return null; + return { + hostId, + hostName + }; + } + function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName' + ]); + } + function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of [ + 'threads', + 'summaries', + 'items', + 'data', + 'conversations' + ]){ + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)){ + if ([ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version' + ].includes(key)) continue; + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const id = stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + entries.push(id ? entry : { + ...entry, + id: key + }); + } + } + return entries; + } + function threadIdFromEntry(entry) { + if (typeof entry === 'string' && entry) return entry; + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + return stringField(entry, [ + 'id', + 'threadId', + 'thread_id', + 'conversationId' + ]); + } + function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + for (const key of keys){ + const value = row[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + } + function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) return String(error.message); + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); + } + __webpack_require__.d(__webpack_exports__, { + R3: ()=>mapRemoteThreadError, + Rg: ()=>RemoteThreadNotLoadedError1 + }); + }, + "./src/core/codex/thread-id.js" (__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) { + const LOCAL_THREAD_ID_PREFIX = 'local:'; + const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + const ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/; + function isTemporaryDesktopThreadId(threadId) { + return typeof threadId === 'string' && (threadId.startsWith(TEMP_THREAD_ID_PREFIX) || threadId.startsWith('client-new-thread:')); + } + function normalizeCodexThreadId1(threadId) { + if (typeof threadId !== 'string' || !threadId) { + throw new RangeError('Invalid threadId'); + } + if (isTemporaryDesktopThreadId(threadId)) { + throw new RangeError('Temporary Desktop thread id ' + JSON.stringify(threadId) + ' has no app-server mapping; wait for the real conversation id'); + } + const bare = threadId.startsWith(LOCAL_THREAD_ID_PREFIX) ? threadId.slice(LOCAL_THREAD_ID_PREFIX.length) : threadId; + if (!bare || !ID_PATTERN.test(bare)) { + throw new RangeError('Invalid threadId'); + } + return bare; + } + __webpack_require__.d(__webpack_exports__, { + SX: ()=>normalizeCodexThreadId1 + }); + }, "./src/core/desktop-shim.js" (__unused_rspack___webpack_module__, __unused_rspack___webpack_exports__, __webpack_require__) { var node_child_process__rspack_import_0 = __webpack_require__("node:child_process"); var node_fs__rspack_import_1 = __webpack_require__("node:fs"); diff --git a/artifact/skills/chatgpt-desktop/SKILL.md b/artifact/skills/chatgpt-desktop/SKILL.md new file mode 100644 index 0000000..1306013 --- /dev/null +++ b/artifact/skills/chatgpt-desktop/SKILL.md @@ -0,0 +1,75 @@ +--- +name: chatgpt-desktop +description: Use the gbot ChatGPT Desktop MCP tools to discover hosts and threads, page/search/read conversations, and send or wait for replies on the user's Mac. Use for local Desktop CDP automation and remote-control thread routing; do not use for ordinary Grok Bot messaging. +--- +# ChatGPT Desktop through gbot + +Use the `chatgpt_desktop_*` MCP tools on the user's Mac. The CLI is a secondary +surface. CDP listens on `127.0.0.1` only; these tools do not transport CDP to +another machine. + +## Find the right conversation + +1. Call `chatgpt_desktop_status`. `reachable: false` or `exitCode: 1` means + CDP is down: open/send/wait cannot work. `appServerFallback.reachable` is a + separate list/search/read probe. Do not retry a send because CDP disappeared. +2. Call `chatgpt_desktop_list_hosts` for dynamic host IDs, labels, and + `modelProvider` values. `local` is this Mac. A remote-control host ID looks + like `remote-control:env_e_...`; its `hostName` may be `null`. Do not infer a + host name from a project label. Managed SSH hosts may have no threads. +3. Use `chatgpt_desktop_list_threads` with `host`, `modelProvider`, or `project` + as needed. Pass `nextCursor` back as `cursor` with the same filters until it + is `null`. The cursor refers to a sorted merged inventory; pages normally + fill `limit` and can stop earlier at the MCP document cap. If the inventory + changed after a cursor expired, restart without one. Project labels and IDs + come from Desktop project assignments and workspace roots; `projectRootPath` + is the resolved root when available. + `chatgpt_desktop_search_threads` also accepts `cursor`; keep its query and + filters unchanged. Search matches the full title even when the display row + has a 200-character `title` and `titleTruncated: true`. +4. Read with `chatgpt_desktop_read_thread`. `full: false` gets the recent tail; + `full: true` pages chronologically from the start. Pass `nextCursor` back + as `cursor` with the same thread ID and `full` value until `complete: true`. + The byte budget can stop before `limit`. An oversized turn is returned as + ordered fragments with one stable `turnKey`; use `continuation.field`, + `offsetChars`, and `totalChars` to reassemble `text`, `userText`, and + `assistantText`. `textTruncated: true` signals a fragment, not missing text. + Check `complete` and `warnings` before treating a page as full history. + +Durable local IDs are `local:` (a bare conversation ID also +works). `local:client-new-thread:*` is temporary and works only while that +conversation remains selected. `REMOTE_THREAD_NOT_LOADED` includes the +owning `hostId`: read or send through gbot running on that host's own Desktop +or app-server. The local Mac cannot fetch that remote history through CDP. + +## Open, send, and wait + +- `chatgpt_desktop_open_thread` navigates to a durable conversation even if it + is absent from the sidebar. An archived route returns `archived: true`; the + thread remains readable through `chatgpt_desktop_read_thread`. +- `chatgpt_desktop_send` with `threadId` sends to that conversation. Omit + `threadId` and `project` to start outside a project. Omit `threadId` and set + `project` to start inside that project. The tool verifies the empty new-chat + view and its project selection before typing. Send once. A new-thread receipt may + include `temporaryThreadId` and resolves `threadId` to the durable + `local:` before returning. Keep both until the durable ID is + confirmed; use the durable ID for later work. +- Then call `chatgpt_desktop_wait_reply` with the returned `threadId` (or the + selected temporary ID). It returns the reply and durable `conversationId`. + A timeout is an observation, not permission to resend. + +`ARCHIVED_THREAD` means send was rejected before submission. Read remains +available. Pass `unarchive: true` with an existing `threadId` on send only if +the user wants the archive state changed; this explicitly calls app-server +`thread/unarchive` and retries once. Never unarchive merely to read or open. + +`COMPOSER_HAS_DRAFT` means the user's draft would be disturbed; leave it for +the operator. `PROJECT_UNAVAILABLE` means Desktop disabled a project's new-chat +action, often because its configured workspace root no longer exists; correct +the project root in Desktop before retrying. `NEW_CHAT_NAVIGATION_FAILED` means +the button did not open the requested empty view within three seconds; inspect +Desktop before retrying. `CDP_UNREACHABLE` requires restoring the local Desktop CDP +endpoint. App-server failures, including oversized frames, appear as errors or +read `warnings` with `complete: false`; inspect the partial result and source +status before continuing. A send with unknown delivery must not be repeated +without checking the selected thread and reply state. diff --git a/docs/chatgpt-desktop-cdp.md b/docs/chatgpt-desktop-cdp.md new file mode 100644 index 0000000..dc58eca --- /dev/null +++ b/docs/chatgpt-desktop-cdp.md @@ -0,0 +1,254 @@ +# ChatGPT Desktop CDP adapter + +`gbot` talks to the ChatGPT / Codex desktop app over the Chrome DevTools +Protocol on **127.0.0.1 only**. There is no remote transport. CDP/DOM details +stay inside `src/core/chatgpt-desktop/`; MCP tools and CLI commands call the +`ChatGptDesktopAdapter` surface. + +List / search / read prefer the local Codex **app-server** (verified on +0.158.0). Send / new-thread-in-project / wait-for-reply / selected-thread stay +on CDP. DOM read is a fallback only when app-server is unavailable. + +## Enable remote debugging (macOS) + +Electron fuses on ChatGPT.app block Node inspect / `RunAsNode`, but they do +**not** block Chromium's `--remote-debugging-port`. The port binds to loopback. +Do not depend on `@electron/fuses read` — the framework is renamed +(`Codex Framework.framework`) and that tool fails. + +```sh +# Helper-printed command (also available from the module): +# chatgptDesktopRelaunchCommand({ port: 9222 }) +osascript -e 'tell application "ChatGPT" to quit' \ + && open -a /Applications/ChatGPT.app --args --remote-debugging-port=9222 +``` + +Bundle id: `com.openai.codex` (ChatGPT.app v26.924.22138 / Chrome 154 explored). + +Override the port with `CHATGPT_DESKTOP_CDP_PORT` or `--port`. + +## CLI + +```sh +gbot chatgpt-desktop status +gbot chatgpt-desktop hosts # discover hostIds + modelProviders at runtime +gbot chatgpt-desktop threads --limit 20 +gbot chatgpt-desktop threads --limit 20 --cursor +gbot chatgpt-desktop threads --host local +gbot chatgpt-desktop threads --host --model-provider +gbot chatgpt-desktop threads --project +gbot chatgpt-desktop search "launch" --host all +gbot chatgpt-desktop read +gbot chatgpt-desktop read --full --cursor +gbot chatgpt-desktop send --thread-id "hello" +gbot chatgpt-desktop send --thread-id --unarchive "hello" +gbot chatgpt-desktop send --project "new chat in project" +gbot chatgpt-desktop send "start a new chat with this text" +``` + +`--host` / `--model-provider` accept **any string** (not enums). Discover +allowed values with `gbot chatgpt-desktop hosts` / `chatgpt_desktop_list_hosts`. + +## MCP tools + +| Tool | Role | +| --- | --- | +| `chatgpt_desktop_status` | CDP `/json/version` (+ optional attach) and app-server probe | +| `chatgpt_desktop_list_hosts` | Discover hosts + modelProviders (no hardcoding) | +| `chatgpt_desktop_list_threads` | App-server `thread/list`; merge CDP UI + remotes | +| `chatgpt_desktop_open_thread` | Navigate an existing local thread by conversation id; report archived routes | +| `chatgpt_desktop_search_threads` | App-server list + metadata filter | +| `chatgpt_desktop_read_thread` | App-server `thread/read` + `thread/turns/list` (`itemsView: "full"`) | +| `chatgpt_desktop_send` | Composer submit over CDP; omit `threadId` for a new chat inside or outside a project | +| `chatgpt_desktop_wait_reply` | Poll for a new assistant turn over CDP | + +Results identify the contributing backends, including `app-server+cdp` and +`remote-state` when applicable. + +## Thread id mapping + +Desktop sidebar rows expose `data-app-action-sidebar-thread-id` as +`local:`. App-server rejects the prefixed form with +`invalid thread id`. Always strip `local:` before RPC calls +(`toAppServerThreadId` / `requireAppServerThreadId`). + +| Form | Example | Where | +| --- | --- | --- | +| Desktop sidebar | `local:11111111-1111-1111-1111-111111111111` | CDP DOM | +| App-server | `11111111-1111-1111-1111-111111111111` | JSON-RPC | +| Temporary (CDP-only) | `local:client-new-thread:…` | Composer before first reply | + +Turn ids from app-server match DOM keys `history-content:turn:` +(`toDomTurnKey`). App-server history is usually richer than the virtualized +DOM (tool calls, reasoning, timing, and sometimes more turns). + +## App-server read protocol (0.158.0) + +1. `initialize` then `initialized` notification (via `openCodexSession`) +2. `thread/read` for **metadata only** (`includeTurns` is deprecated for + paginated threads) +3. Page history with `thread/turns/list` (`itemsView: "full"`, `cursor` / + `nextCursor`); `thread/items/list` is a fallback only for unsupported RPCs. + Recent reads request descending pages and return chronological turns; + `full` reads request ascending pages, bounded by `limit`. Return pages below + the Agent Bundle 1 MiB document cap. Pass `nextCursor` back as `cursor` until + `complete: true`. A large turn uses ordered `continuation` fragments with + a stable `turnKey` and field offsets, so all text remains retrievable. + Empty history is valid. +4. **Do not** `thread/resume` for reads — resume attaches a live session + +`thread/list` filters to the current model provider by default. Pass +`modelProviders: []` for every provider, and `useStateDbOnly: true` for speed +(~0.02s; a cold rollout scan can take ~36s). Pagination uses `limit` / `cursor` +/ `nextCursor`. The same fix applies to `gbot codex list-threads` / +`codex_threads`. + +Surfaced list fields: `id`, `name`, `preview`, `cwd`, `createdAt` / +`updatedAt` (unix s), `section {id,name}` (Pinned → `pinned: true`), +`projectId`, `status`, `modelProvider`, `model`, `originator`. `archived` is a +list filter, not a field. +List pages use one merged, timestamp-sorted inventory with thread ID as a +deterministic tiebreak. CDP/sidebar and remote-summary rows are deduplicated +before paging, so a full cursor walk includes each ID once. The cursor refers +to that inventory; after it expires or the inventory changes during recovery, +restart without a cursor. List and search pages can stop before `limit` to stay +under the document cap. +Display titles collapse whitespace and stop at 200 characters with +`titleTruncated: true`; search still matches the full source title. Pass +`nextCursor` back with unchanged filters and query. + +Project labels and IDs are resolved from Desktop's local/remote projects, +thread-project assignments, workspace-root hints, and cwd. The row's +`projectRootPath` shows the matched root. A new chat is checked for an empty +composer and the requested project before text is inserted. Projectless new +chat clears a project retained from the previous thread. If Desktop disables +the project action because its configured root is unavailable, send returns +`PROJECT_UNAVAILABLE` before typing. A click that leaves the old view in place +returns `NEW_CHAT_NAVIGATION_FAILED` after a short observation. + +### Host and modelProvider filters (separate) + +`chatgpt_desktop_list_threads` / search accept these independent filters: + +| Filter | Values (any string — not an enum) | Default | Effect | +| --- | --- | --- | --- | +| `host` | `all` \| `local` \| any hostId / friendly name from `list_hosts` | `all` | Machine/location from remote summaries + local | +| `modelProvider` | any provider id from `list_hosts` | omit / `[]` = all | Passed through to app-server `modelProviders` | +| `project` | Desktop project label or app-server projectId | omit = all | Match rows with that project | + +New machines or connections appear in `list_hosts` with **no code change**. + +Optional `groupBy: "host"` returns `groups[]` keyed by host. + +### Host discovery (`chatgpt_desktop_list_hosts`) + +Hosts are discovered dynamically — do not hardcode them: + +1. Always include `local` +2. Every `remote-thread-summaries-v3:` key in + `~/.codex/.codex-global-state.json`, including keys under + `electron-persisted-atom-state`, with thread counts +3. Managed SSH connections in `codex-managed-remote-connections` + +`hostName` is the Desktop display name when present, otherwise `null`. +Project labels are not used as machine names. CDP sidebar rows expose their +host id, and matching remote summaries supply ownership when available. + +`modelProviders` combines distinct `modelProvider` values from `thread/list` +(`modelProviders: []`) and remote summaries. `modelProvidersSource` names the +local discovery path; if unavailable, remote summaries still contribute. +Failures and page-cap truncation appear in `warnings`. +Explicit provider filters exclude rows whose provider is unknown. + +### Remote-control threads + +`chatgpt_desktop_list_threads` merges local app-server threads with +remote-control summaries (`location: "local" | "remote"` plus `hostId` / +`hostName`). Remote rows appear in list/search. `chatgpt_desktop_read_thread` +on a remote-only thread raises `RemoteThreadNotLoadedError` +(`REMOTE_THREAD_NOT_LOADED`) with `hostId`, optional `hostName`, and a `hint` +to read it via that host's app-server. SSH remoting is **not** implemented in +this PR (follow-up). The global-state parser is defensive — unknown shapes are +skipped. + +## Backend capability matrix + +| Operation | CDP | App-server | +| --- | --- | --- | +| `status` | `/json/version` + attach | daemon probe | +| `list_hosts` | — | global-state keys + thread inventory provider discovery | +| `list_threads` | merge selected / UI overlay | primary inventory + remote summaries | +| `search_threads` | merge selected when connected | primary (list + filter, includes remotes) | +| `read_thread` | DOM harvest / wheel **fallback only** (local) | primary; remote → typed error + host hint | +| `open_thread` (selected) | click sidebar row or navigate through Desktop's memory router by conversation id | **not used** (resume attaches) | +| `send` / new-thread-in-project | composer + Enter / project button | **not used** | +| `wait_reply` | Stop gone + final-assistant | **not used** | + +`status.reachable` and its process exit code report CDP availability; the +app-server probe is separate. List/search results report `app-server+cdp` +when both contribute, and append `remote-state` when remote summaries +contribute. `read_thread` returns `complete` and `warnings` so a CDP fallback +cannot be mistaken for a complete app-server read. A turn may contain both +`userText` and `assistantText`; `endedAt` uses the app-server `completedAt`. +Full reads page from the oldest turn until completion, with a default cap of +2000 turns; an explicit `limit` can lower that cap. +Archived local threads remain readable through app-server. The open tool +navigates to their route and returns `archived: true`. Send returns +`ARCHIVED_THREAD` before submitting. Only `unarchive: true` with an existing +thread ID explicitly calls app-server `thread/unarchive`, then retries the +send once; neither open nor read changes archive state. New-thread sends return +a durable `local:` once resolved and include the temporary +`local:client-new-thread:*` ID when the sidebar exposes one. +Archived conversations can be read through app-server, but Desktop requires +an explicit unarchive before opening them for interaction. + +## Target selection + +Prefer CDP `Target.getTargets`. The main window is `type: "page"` with URL +exactly `app://-/index.html`. Ignore avatar-overlay, detached-window, and +chatgpt.com / sandbox webviews. `/json/list` is incomplete and order-unstable; +use it only as a cheap status hint. + +## DOM module + +All selectors live in `src/core/chatgpt-desktop/cdp-dom.ts`: + +- Composer: `[data-codex-composer=true][contenteditable=true]` — `focus()` then `Input.insertText` +- Submit: Enter via `Input.dispatchKeyEvent` (rawKeyDown/keyUp), waits for the composer to clear without resubmitting +- Reply done: `main button[aria-label="Stop"]` gone + new `[data-local-conversation-final-assistant=true]` +- Reply text: last turn’s `[data-local-conversation-final-assistant=true] [data-markdown-text-style=assistant-message]` +- New chat: `button[aria-label="Start new chat in "]` (preferred) or sidebar “New chat” +- Conversation id: `[data-response-annotation-conversation]` — after a new-thread send, `threadId` is this durable id (never `local:client-new-thread:…`; that may appear only as `temporaryThreadId`) +- Open / selected: click sidebar row (bare or `local:` id), wait until “Loading task…” clears (default 90s, timed out) +- Read fallback: harvest rendered turns; full history uses mouseWheel (`deltaY` negative) because the timeline is column-reverse (`scrollTop` 0 is newest and does not load older turns) +- Target attach: exact `app://-/index.html` via `Target.getTargets` (not `/json/list` order) +- CDP: 127.0.0.1 only + +## App-server reuse + +List / search / read reuse the existing client in `codex-bridge.js` +(`listCodexThreads`, `openCodexSession`, `codexStatus`) — no duplicated +JSON-RPC stack. Send / new-thread-in-project / wait-for-reply stay on CDP. + +## Operation boundaries + +CDP HTTP discovery rejects redirects and non-loopback debugger URLs. Discovery +and connection attempts time out after 5 seconds; individual CDP requests after +10 seconds. CLI/MCP calls close their connection on completion and serialize +access to the shared Desktop composer within one process. + +Opening a thread waits for its conversation annotation as well as the absence +of “Loading task…”, so a still-rendered previous chat is not used. Reply waits +require the latest turn to have a final reply with Stop absent. A timeout is +reported as a timeout, never as a successful partial reply. Changing the +selected conversation during a wait fails the operation. A requested project +must exist; it does not silently fall back to an unrelated new chat. + +Separate CLI processes and manual Desktop interaction can still race with UI +operations; avoid driving the same window concurrently. DOM selectors and the +remote-summary file format are app-internal and can change between releases. + +Sends refuse an existing composer draft or active reply. Once Enter is dispatched, +a missing acknowledgment or durable conversation ID reports unknown delivery; +it must not be retried blindly. A send/wait pair in the same MCP process tracks +the pre-submit turn key to avoid returning the previous answer. diff --git a/src/cli/chatgpt-desktop/hosts.tsx b/src/cli/chatgpt-desktop/hosts.tsx new file mode 100644 index 0000000..030e14b --- /dev/null +++ b/src/cli/chatgpt-desktop/hosts.tsx @@ -0,0 +1,60 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; +import { z } from 'zod'; + +import { + listHostsOperation, + resultSchema, + resultText, +} from '../../core/chatgpt-desktop/routes.js'; + +export const config = { + description: + 'Discover ChatGPT Desktop hosts and modelProviders at runtime (never hardcode). Always includes local plus every remote-thread-summaries-v3:; Providers come from distinct thread/list and remote-summary values.', + exitCode: 'result', + inputJsonSchema: { + additionalProperties: false, + properties: { + port: { description: 'Local CDP port', type: 'number' }, + }, + type: 'object', + }, +} satisfies CliRouteConfig; + +export const inputSchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + }) + .strict(); +export { resultSchema }; + +export default async function chatgptDesktopHosts({ + input, +}: CliRouteProps) { + const out = await listHostsOperation(input); + const hosts = Array.isArray(out.hosts) ? out.hosts : []; + const text = + hosts.length > 0 + ? hosts + .map((host) => { + const row = host as { + hostId?: string; + hostName?: string | null; + location?: string; + threadCount?: number; + }; + const name = + row.hostName && row.hostName !== row.hostId ? ` (${row.hostName})` : ''; + return `${row.hostId ?? '?'}${name} [${row.location ?? '?'}] threads=${row.threadCount ?? 0}`; + }) + .join('\n') + + `\n\nhostsSource: ${String(out.hostsSource ?? '')}` + + `\nmodelProviders: ${JSON.stringify(out.modelProviders ?? [])}` + + `\nmodelProvidersSource: ${String(out.modelProvidersSource ?? '')}` + : resultText(out); + return ( + + {text} + + ); +} diff --git a/src/cli/chatgpt-desktop/read.tsx b/src/cli/chatgpt-desktop/read.tsx new file mode 100644 index 0000000..6b60cb4 --- /dev/null +++ b/src/cli/chatgpt-desktop/read.tsx @@ -0,0 +1,78 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; +import { z } from 'zod'; + +import { + readThreadOperation, + resultSchema, + resultText, +} from '../../core/chatgpt-desktop/routes.js'; + +export const config = { + description: + 'Read a ChatGPT Desktop thread via app-server (turns/list); CDP DOM is fallback only.', + exitCode: 'result', + positionals: ['threadId'], + inputJsonSchema: { + additionalProperties: false, + properties: { + full: { default: false, type: 'boolean' }, + cursor: { description: 'Opaque nextCursor from the prior read page', type: 'string' }, + limit: { description: 'Recent default 100; full default 2000', type: 'number' }, + openTimeoutMs: { default: 90000, type: 'number' }, + port: { type: 'number' }, + threadId: { type: 'string' }, + }, + required: ['threadId'], + type: 'object', + }, + render: { maxElapsedMs: 660000 }, +} satisfies CliRouteConfig; + +export const inputSchema = z + .object({ + full: z.boolean().default(false), + cursor: z.string().min(1).max(4096).optional(), + limit: z.number().int().min(1).max(2000).optional(), + openTimeoutMs: z.number().int().min(1).max(600_000).default(90_000), + port: z.number().int().min(1).max(65535).optional(), + threadId: z.string().min(1).max(256), + }) + .strict(); +export { resultSchema }; + +export default async function chatgptDesktopRead({ + input, +}: CliRouteProps) { + const out = await readThreadOperation(input); + const turns = Array.isArray(out.turns) ? out.turns : []; + const text = + turns.length > 0 + ? turns + .map((turn) => { + const row = turn as { + role?: string; + text?: string; + turnKey?: string; + userText?: string; + assistantText?: string; + }; + const body = + row.userText || row.assistantText + ? [ + row.userText ? `user: ${row.userText}` : null, + row.assistantText ? `assistant: ${row.assistantText}` : null, + ] + .filter(Boolean) + .join('\n') + : row.text ?? ''; + return `[${row.role ?? '?'} ${row.turnKey ?? ''}]\n${body}`; + }) + .join('\n\n') + `\n\nbackend: ${String(out.backend ?? '')} full: ${String(out.full ?? false)}` + : resultText(out); + return ( + + {text} + + ); +} diff --git a/src/cli/chatgpt-desktop/search.tsx b/src/cli/chatgpt-desktop/search.tsx new file mode 100644 index 0000000..c5ff728 --- /dev/null +++ b/src/cli/chatgpt-desktop/search.tsx @@ -0,0 +1,68 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; +import { z } from 'zod'; + +import { + resultSchema, + resultText, + searchThreadsOperation, +} from '../../core/chatgpt-desktop/routes.js'; + +export const config = { + description: + 'Search ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.', + exitCode: 'result', + positionals: ['query'], + inputJsonSchema: { + additionalProperties: false, + properties: { + groupBy: { + description: 'Group results by host (grouping mode, not a host id)', + enum: ['host'], + type: 'string', + }, + host: { + description: + 'Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string', + }, + limit: { default: 50, description: 'Max matches (1-200)', type: 'number' }, + cursor: { description: 'Opaque nextCursor from a prior search page', type: 'string' }, + modelProvider: { + description: + 'Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string', + }, + project: { description: 'Project label or projectId filter.', type: 'string' }, + port: { description: 'Local CDP port', type: 'number' }, + query: { type: 'string' }, + }, + required: ['query'], + type: 'object', + }, +} satisfies CliRouteConfig; + +export const inputSchema = z + .object({ + groupBy: z.literal('host').optional(), + host: z.string().min(1).max(256).optional(), + limit: z.number().int().min(1).max(200).default(50), + cursor: z.string().min(1).max(4096).optional(), + modelProvider: z.string().min(1).max(256).optional(), + project: z.string().min(1).max(256).optional(), + port: z.number().int().min(1).max(65535).optional(), + query: z.string().min(1).max(512), + }) + .strict(); +export { resultSchema }; + +export default async function chatgptDesktopSearch({ + input, +}: CliRouteProps) { + const out = await searchThreadsOperation(input); + return ( + + {resultText(out)} + + ); +} diff --git a/src/cli/chatgpt-desktop/send.tsx b/src/cli/chatgpt-desktop/send.tsx new file mode 100644 index 0000000..2be5741 --- /dev/null +++ b/src/cli/chatgpt-desktop/send.tsx @@ -0,0 +1,60 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; +import { z } from 'zod'; + +import { + resultSchema, + resultText, + sendOperation, +} from '../../core/chatgpt-desktop/routes.js'; + +export const config = { + description: + 'Send to ChatGPT Desktop via CDP composer. Omit --thread-id for a new chat; --project prefers Start new chat in .', + exitCode: 'result', + positionals: ['text'], + inputJsonSchema: { + additionalProperties: false, + properties: { + openTimeoutMs: { type: 'number' }, + port: { type: 'number' }, + project: { type: 'string' }, + text: { type: 'array', items: { type: 'string' } }, + threadId: { type: 'string' }, + unarchive: { type: 'boolean', description: 'Explicitly unarchive an existing archived thread before sending.' }, + }, + required: ['text'], + type: 'object', + }, + render: { maxElapsedMs: 660000 }, +} satisfies CliRouteConfig; + +export const inputSchema = z + .object({ + openTimeoutMs: z.number().int().min(1).max(600_000).default(90_000), + port: z.number().int().min(1).max(65535).optional(), + project: z.string().min(1).max(256).optional(), + text: z.array(z.string()).min(1), + threadId: z.string().min(1).max(256).optional(), + unarchive: z.boolean().default(false), + }) + .strict(); +export { resultSchema }; + +export default async function chatgptDesktopSend({ + input, +}: CliRouteProps) { + const out = await sendOperation({ + port: input.port, + threadId: input.threadId, + text: input.text.join(' ').trim(), + project: input.project, + unarchive: input.unarchive, + openTimeoutMs: input.openTimeoutMs, + }); + return ( + + {resultText(out)} + + ); +} diff --git a/src/cli/chatgpt-desktop/status.tsx b/src/cli/chatgpt-desktop/status.tsx new file mode 100644 index 0000000..1eb5954 --- /dev/null +++ b/src/cli/chatgpt-desktop/status.tsx @@ -0,0 +1,40 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; +import { z } from 'zod'; + +import { + resultSchema, + resultText, + statusOperation, +} from '../../core/chatgpt-desktop/routes.js'; + +export const config = { + description: + 'Probe local ChatGPT Desktop CDP on 127.0.0.1 and report app-server availability for list/deep-read.', + exitCode: 'result', + inputJsonSchema: { + additionalProperties: false, + properties: { + port: { description: 'Local CDP port (default 9222)', type: 'number' }, + }, + type: 'object', + }, +} satisfies CliRouteConfig; + +export const inputSchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + }) + .strict(); +export { resultSchema }; + +export default async function chatgptDesktopStatus({ + input, +}: CliRouteProps) { + const out = await statusOperation(input); + return ( + + {resultText(out)} + + ); +} diff --git a/src/cli/chatgpt-desktop/threads.tsx b/src/cli/chatgpt-desktop/threads.tsx new file mode 100644 index 0000000..0a9201a --- /dev/null +++ b/src/cli/chatgpt-desktop/threads.tsx @@ -0,0 +1,93 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; +import { z } from 'zod'; + +import { + listThreadsOperation, + resultSchema, + resultText, +} from '../../core/chatgpt-desktop/routes.js'; + +export const config = { + description: + 'List local + remote-control ChatGPT Desktop threads. --host and --model-provider accept any string (see gbot chatgpt-desktop hosts); not enums.', + exitCode: 'result', + inputJsonSchema: { + additionalProperties: false, + properties: { + groupBy: { + description: 'Group results by host (grouping mode, not a host id)', + enum: ['host'], + type: 'string', + }, + host: { + description: + 'Any string: all (default), local, or a hostId/friendly name from `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string', + }, + limit: { default: 50, description: 'Max threads (1-200)', type: 'number' }, + cursor: { description: 'Opaque nextCursor from a previous page', type: 'string' }, + modelProvider: { + description: + 'Any modelProvider id (app-server modelProviders pass-through). Omit = all. See `gbot chatgpt-desktop hosts`. Not an enum.', + type: 'string', + }, + project: { description: 'Project label or projectId filter.', type: 'string' }, + port: { description: 'Local CDP port', type: 'number' }, + }, + type: 'object', + }, +} satisfies CliRouteConfig; + +export const inputSchema = z + .object({ + groupBy: z.literal('host').optional(), + host: z.string().min(1).max(256).optional(), + limit: z.number().int().min(1).max(200).default(50), + cursor: z.string().min(1).max(4096).optional(), + modelProvider: z.string().min(1).max(256).optional(), + project: z.string().min(1).max(256).optional(), + port: z.number().int().min(1).max(65535).optional(), + }) + .strict(); +export { resultSchema }; + +export default async function chatgptDesktopThreads({ + input, +}: CliRouteProps) { + const out = await listThreadsOperation(input); + const threads = Array.isArray(out.threads) ? out.threads : []; + const text = + threads.length > 0 + ? threads + .map((thread) => { + const row = thread as { + threadId?: string; + title?: string; + selected?: boolean; + pinned?: boolean; + location?: string; + hostId?: string | null; + hostName?: string | null; + modelProvider?: string | null; + }; + const where = + row.location === 'remote' + ? ` [remote ${row.hostName || row.hostId || '?'}]` + : ' [local]'; + const provider = row.modelProvider ? ` {${row.modelProvider}}` : ''; + return ( + `${row.selected ? '* ' : ' '}${row.threadId ?? ''} ${row.title ?? ''}` + + (row.pinned ? ' (pinned)' : '') + + where + + provider + ); + }) + .join('\n') + `\n\nbackend: ${String(out.backend ?? '')}` + (out.nextCursor ? `\nnextCursor: ${out.nextCursor}` : '') + : resultText(out); + return ( + + {text} + + ); +} diff --git a/src/cli/codex/list-threads.tsx b/src/cli/codex/list-threads.tsx index 60949b5..81588e3 100644 --- a/src/cli/codex/list-threads.tsx +++ b/src/cli/codex/list-threads.tsx @@ -8,13 +8,14 @@ import { formatCodexThread } from '../../core/format.js'; import { failureDocumentSchema } from '../_shared.js'; export const config = { - description: 'List Codex daemon-managed threads.', + description: + 'List Codex daemon-managed threads (all modelProviders; pages via --cursor / nextCursor).', exitCode: 'result', inputJsonSchema: { additionalProperties: false, properties: { cursor: { - description: 'Opaque pagination cursor (may start with -)', + description: 'Opaque pagination cursor from a previous nextCursor (may start with -)', type: 'string', }, limit: { default: 20, description: 'Max threads to list (1-200)', type: 'number' }, @@ -33,6 +34,7 @@ export const resultSchema = z.union([ limit: z.number().int().min(1).max(200), nextCursor: z.string().nullable(), threads: z.array(z.record(z.string(), z.json())), + useStateDbOnly: z.boolean().optional(), }).strict(), failureDocumentSchema, ]); diff --git a/src/core/chatgpt-desktop/adapter.ts b/src/core/chatgpt-desktop/adapter.ts new file mode 100644 index 0000000..dd0b4b0 --- /dev/null +++ b/src/core/chatgpt-desktop/adapter.ts @@ -0,0 +1,76 @@ +import type { + ChatGptDesktopStatus, + ChatGptDesktopTarget, + ListHostsResult, + ListThreadsResult, + OpenThreadResult, + ReadThreadResult, + SendMessageResult, + WaitForReplyResult, +} from './types.js'; + +/** + * Stable surface for ChatGPT Desktop automation. CDP/DOM details stay behind + * the implementation; callers use these methods only. + */ +export interface ChatGptDesktopAdapter { + connect(options: { port: number }): Promise; + listTargets(): Promise; + listThreads(options?: { + limit?: number; + cursor?: string; + /** Any string: `all` (default) | `local` | hostId / friendly name from list_hosts. Not an enum. */ + host?: string; + /** Any modelProvider id (app-server modelProviders). Omit = all. See list_hosts. Not an enum. */ + modelProvider?: string; + project?: string; + groupBy?: 'host'; + }): Promise; + /** Optional; facades that support app-server search implement this. */ + searchThreads?(options: { + query: string; + limit?: number; + cursor?: string; + /** Any string; see list_hosts. Not an enum. */ + host?: string; + /** Any modelProvider id; see list_hosts. Not an enum. */ + modelProvider?: string; + project?: string; + groupBy?: 'host'; + }): Promise; + /** + * Optional; facades that discover remote-control hosts implement this. + * Returns `local` + every `remote-thread-summaries-v3:` host with + * counts / friendly names, plus discovered `modelProviders`. Never hardcode + * those ids — new machines/connections appear with no code change. + */ + listHosts?(): Promise; + readThread(options: { + threadId: string; + limit?: number; + full?: boolean; + cursor?: string; + openTimeoutMs?: number; + }): Promise; + /** + * Send text. With `threadId`, opens that thread first. Without `threadId`, + * starts a new chat (`project` prefers "Start new chat in "). + */ + sendMessage(options: { + threadId?: string; + text: string; + project?: string; + unarchive?: boolean; + openTimeoutMs?: number; + }): Promise; + waitForReply(options: { + threadId?: string; + timeoutMs?: number; + }): Promise; + openThread( + threadId: string, + options?: { openTimeoutMs?: number }, + ): Promise; + status(): Promise; + close(): Promise; +} diff --git a/src/core/chatgpt-desktop/app-server-fallback.ts b/src/core/chatgpt-desktop/app-server-fallback.ts new file mode 100644 index 0000000..0460afb --- /dev/null +++ b/src/core/chatgpt-desktop/app-server-fallback.ts @@ -0,0 +1,676 @@ +/** + * App-server path for ChatGPT Desktop list / search / read. + * Reuses the existing codex-bridge client — does not duplicate the WebSocket/JSON-RPC stack. + * + * Verified against Codex app-server 0.158.0: + * - initialize → initialized → thread/list | thread/read | thread/turns/list | thread/items/list + * - Do not use thread/resume for reads (it attaches a live session). + * - includeTurns on thread/read is deprecated for paginated threads; read metadata, + * then page with thread/turns/list (itemsView: "full"). + * + * Send, new-thread-in-project, wait-for-reply, and selected-thread stay on CDP (see facade). + */ + +import { + codexStatus, + listCodexThreads, + openCodexSession, +} from '../codex-bridge.js'; +import { RemoteThreadNotLoadedError } from './errors.js'; +import { findRemoteThread, findRemoteThreadHostId } from './remote-threads.js'; +import { + DESKTOP_RESULT_BUDGET_BYTES, + compactThreadRow, + compactThreadTitle, + decodePageCursor, + encodePageCursor, + minimalThreadRow, + serializedBytes, +} from './payload-budget.js'; +import { + requireAppServerThreadId, + toDesktopThreadId, + toDomTurnKey, +} from './thread-ids.js'; +import type { + ChatGptDesktopThread, + ChatGptDesktopTurn, + ListThreadsResult, + ReadThreadResult, +} from './types.js'; + +const LIST_PAGE_PREFIX = 'desktop-list-v1:'; +const SEARCH_PAGE_PREFIX = 'desktop-search-v1:'; +const READ_PAGE_PREFIX = 'desktop-read-v1:'; +type ListPageCursor = { cursor: string | null; skip: number; fetchLimit: number }; + +export async function appServerListThreads({ + limit = 50, + cursor, + modelProviders = [], +}: { + limit?: number; + cursor?: string; + modelProviders?: string[]; +} = {}): Promise { + let sourceCursor = cursor; + let skip = 0; + let fetchLimit = Math.min(limit, 25); + if (cursor?.startsWith(LIST_PAGE_PREFIX)) { + const state = decodePageCursor(LIST_PAGE_PREFIX, cursor); + if (!Number.isSafeInteger(state.skip) || state.skip < 0 + || !Number.isSafeInteger(state.fetchLimit) || state.fetchLimit < 1 || state.fetchLimit > 25 + || (state.cursor !== null && typeof state.cursor !== 'string')) { + throw new Error('Invalid ChatGPT Desktop list page cursor'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + fetchLimit = state.fetchLimit; + } + const base = { + backend: 'app-server' as const, + limit, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined, + }; + const threads: ChatGptDesktopThread[] = []; + const seen = new Set(); + for (let page = 0; page < 400; page++) { + const out = await listCodexThreads({ limit: fetchLimit, cursor: sourceCursor, modelProviders }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop list page cursor is stale'); + for (let index = skip; index < out.threads.length; index++) { + let row = compactThreadRow(mapListedThread(out.threads[index])); + const probe = { ...base, threads: [...threads, row], nextCursor: LIST_PAGE_PREFIX, exitCode: 0 }; + if (serializedBytes(probe) > DESKTOP_RESULT_BUDGET_BYTES) { + if (threads.length) return { ...base, threads, + nextCursor: encodePageCursor(LIST_PAGE_PREFIX, { cursor: sourceCursor ?? null, skip: index, fetchLimit }), + warnings: ['List page stopped at the MCP byte budget; continue with nextCursor'] }; + row = minimalThreadRow(row); + if (serializedBytes({ ...base, threads: [row], nextCursor: LIST_PAGE_PREFIX, exitCode: 0 }) > DESKTOP_RESULT_BUDGET_BYTES) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + threads.push(row); + if (threads.length >= limit) { + const nextCursor = index + 1 < out.threads.length + ? encodePageCursor(LIST_PAGE_PREFIX, { cursor: sourceCursor ?? null, skip: index + 1, fetchLimit }) + : out.nextCursor ?? null; + return { ...base, threads, nextCursor }; + } + } + if (out.nextCursor == null) return { ...base, threads, nextCursor: null }; + if (typeof out.nextCursor !== 'string' || !out.nextCursor || seen.has(out.nextCursor)) { + throw new Error('ChatGPT Desktop list returned an invalid or repeated cursor'); + } + seen.add(out.nextCursor); + sourceCursor = out.nextCursor; + skip = 0; + } + return { ...base, threads, nextCursor: sourceCursor, + warnings: ['List stopped after 400 app-server pages; continue with nextCursor'] }; +} + +export async function appServerSearchThreads({ + query, + limit = 50, + cursor, + modelProviders = [], + project, +}: { + query: string; + limit?: number; + cursor?: string; + modelProviders?: string[]; + project?: string; +}): Promise { + const needle = query.trim().toLowerCase(); + if (!needle) { + return appServerListThreads({ limit, cursor, modelProviders }); + } + const matched: ChatGptDesktopThread[] = []; + let sourceCursor: string | undefined; + let skip = 0; + if (cursor) { + const state = decodePageCursor<{ cursor: string | null; skip: number; query: string; project?: string; modelProviders: string[] }>(SEARCH_PAGE_PREFIX, cursor); + if (state.query !== query || state.project !== project + || JSON.stringify(state.modelProviders) !== JSON.stringify(modelProviders) + || !Number.isSafeInteger(state.skip) || state.skip < 0) { + throw new Error('Search cursor does not match this query and its filters'); + } + sourceCursor = state.cursor ?? undefined; + skip = state.skip; + } + const pageCursor = (rpcCursor: string | undefined, offset: number) => encodePageCursor(SEARCH_PAGE_PREFIX, { + cursor: rpcCursor ?? null, skip: offset, query, project, modelProviders, + }); + const base = { backend: 'app-server' as const, limit, query, + modelProvider: modelProviders.length === 1 ? modelProviders[0] : undefined }; + for (let page = 0; page < 400; page++) { + const out = await listCodexThreads({ limit: 25, cursor: sourceCursor, modelProviders }); + if (skip > out.threads.length) throw new Error('ChatGPT Desktop search cursor is stale'); + for (let index = skip; index < out.threads.length; index++) { + const mapped = mapListedThread(out.threads[index]); + if (!threadMatchesQuery(mapped, needle) + || (project && ![mapped.project, mapped.projectId].some((value) => + typeof value === 'string' && value.toLowerCase().includes(project.trim().toLowerCase())))) continue; + let row = compactThreadRow(mapped); + if (serializedBytes({ ...base, threads: [...matched, row], nextCursor: SEARCH_PAGE_PREFIX, exitCode: 0 }) > DESKTOP_RESULT_BUDGET_BYTES) { + if (matched.length) return { ...base, threads: matched, nextCursor: pageCursor(sourceCursor, index), + warnings: ['Search page stopped at the MCP byte budget; continue with nextCursor'] }; + row = minimalThreadRow(row); + } + matched.push(row); + if (matched.length >= limit) { + const nextCursor = index + 1 < out.threads.length + ? pageCursor(sourceCursor, index + 1) : out.nextCursor ? pageCursor(out.nextCursor, 0) : null; + return { ...base, threads: matched, nextCursor }; + } + } + if (out.nextCursor == null) return { ...base, threads: matched, nextCursor: null }; + sourceCursor = out.nextCursor; + skip = 0; + } + return { ...base, threads: matched, nextCursor: pageCursor(sourceCursor, 0), + warnings: ['Search stopped after 400 app-server pages; continue with nextCursor'] }; +} + +export type ModelProvidersDiscovery = { + readonly providers: readonly string[]; + readonly source: 'thread/list-distinct'; + readonly threadCount?: number; + readonly complete?: boolean; +}; + +/** Discover provider IDs from the supported inventory, without guessing RPCs. */ +export async function discoverModelProviders(): Promise { + const providers = new Set(); + let threadCount = 0; + let cursor: string | undefined; + let complete = false; + for (let page = 0; page < 400; page++) { + const out = await listCodexThreads({ limit: 25, cursor, modelProviders: [] }); + threadCount += out.threads.length; + for (const thread of out.threads) { + if (typeof thread.modelProvider === 'string' && thread.modelProvider) { + providers.add(thread.modelProvider); + } + } + if (out.nextCursor == null || typeof out.nextCursor !== 'string') { complete = true; break; } + cursor = out.nextCursor; + } + return { + providers: [...providers].sort(), + source: 'thread/list-distinct', + threadCount, + complete, + }; +} + +export async function appServerReadThread({ + threadId, + limit, + full = false, + cursor, +}: { + threadId: string; + limit?: number; + full?: boolean; + cursor?: string; +}): Promise { + // Prefixed `local:` ids fail with `invalid thread id` — always strip first. + limit ??= full ? 2000 : 100; + const bare = requireAppServerThreadId(threadId); + + // Known remote-only threads: fail fast with a typed error + host hint. + const remote = findRemoteThread(bare); + if (remote) { + // Still try local app-server in case the thread was also loaded locally. + try { + return await readLocalThread(bare, { limit, full, cursor }); + } catch (error) { + if (isThreadNotLoaded(error) || isUnknownThread(error)) { + throw new RemoteThreadNotLoadedError(bare, remote.hostId, remote.hostName); + } + throw maybeRemoteThreadError(error, bare); + } + } + + try { + return await readLocalThread(bare, { limit, full, cursor }); + } catch (error) { + throw maybeRemoteThreadError(error, bare); + } +} + +/** Explicit opt-in only. The local app-server owns the archive mutation. */ +export async function appServerUnarchiveThread(threadId: string): Promise { + const bare = requireAppServerThreadId(threadId); + const { client } = await openCodexSession(); + try { + const result = await client.request('thread/unarchive', { threadId: bare }); + if (!result || typeof result !== 'object' || !('thread' in result) + || !result.thread || typeof result.thread !== 'object' + || !('id' in result.thread) || result.thread.id !== bare) { + throw new Error(`thread/unarchive returned no thread for ${bare}`); + } + } finally { + client.close(); + } +} + +async function readLocalThread( + bare: string, + { limit, full, cursor }: { limit: number; full: boolean; cursor?: string }, +): Promise { + const { client } = await openCodexSession(); + try { + const meta = await readThreadMetadata(client, bare); + const { turns, complete, nextCursor } = await listTurnsFull(client, bare, { limit, full, cursor, title: meta.title }); + return { + threadId: toDesktopThreadId(bare), + turns, + backend: 'app-server', + limit, + full, + complete, + nextCursor, + ...(complete ? {} : { warnings: ['Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment'] }), + ...(meta.title ? compactThreadTitle(meta.title) : {}), + cwd: meta.cwd, + status: meta.status, + modelProvider: meta.modelProvider, + model: meta.model, + }; + } finally { + client.close(); + } +} + +export async function appServerStatusProbe(): Promise<{ + reachable: boolean; + mode?: string; + socketPath?: string; +}> { + const status = (await codexStatus()) as { + reachable?: boolean; + mode?: string; + socketPath?: string; + }; + return { + reachable: Boolean(status.reachable && status.mode === 'daemon'), + mode: typeof status.mode === 'string' ? status.mode : undefined, + socketPath: typeof status.socketPath === 'string' ? status.socketPath : undefined, + }; +} + +type AppServerClient = { + request: (method: string, params?: Record) => Promise; +}; + +type ListedThread = { + id?: string; + name?: string | null; + preview?: string; + cwd?: string | null; + createdAt?: number | null; + updatedAt?: number | null; + section?: { id?: string; name?: string | null } | null; + projectId?: string | null; + status?: string; + modelProvider?: string | null; + model?: string | null; + originator?: string | null; +}; + +function mapListedThread(thread: ListedThread): ChatGptDesktopThread { + const bare = String(thread.id ?? ''); + const section = + thread.section && typeof thread.section.id === 'string' + ? { id: thread.section.id, name: thread.section.name ?? null } + : null; + const pinned = + section?.id === 'Pinned' || + section?.name === 'Pinned' || + false; + return { + threadId: bare ? toDesktopThreadId(bare) : '', + title: String(thread.name || thread.preview || thread.cwd || thread.id || ''), + pinned, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + preview: typeof thread.preview === 'string' ? thread.preview : undefined, + cwd: thread.cwd ?? null, + createdAt: thread.createdAt ?? null, + updatedAt: thread.updatedAt ?? null, + section, + projectId: thread.projectId ?? null, + project: thread.projectId ?? undefined, + status: thread.status, + modelProvider: thread.modelProvider ?? null, + model: thread.model ?? null, + originator: thread.originator ?? null, + }; +} + +function threadMatchesQuery(thread: ChatGptDesktopThread, needle: string): boolean { + const haystacks = [ + thread.threadId, + thread.title, + thread.preview, + thread.cwd, + thread.project, + thread.projectId, + thread.model, + thread.modelProvider, + thread.originator, + thread.section?.name, + thread.section?.id, + ]; + return haystacks.some((value) => typeof value === 'string' && value.toLowerCase().includes(needle)); +} + +async function readThreadMetadata( + client: AppServerClient, + threadId: string, +): Promise<{ + title?: string; + cwd?: string | null; + status?: string; + modelProvider?: string | null; + model?: string | null; +}> { + try { + // Metadata only — includeTurns is deprecated for paginated threads. + const read = await client.request('thread/read', { threadId }) as { + thread?: Record; + }; + const thread = read.thread && typeof read.thread === 'object' ? read.thread : {}; + const statusObj = thread.status; + const statusType = + statusObj && typeof statusObj === 'object' && typeof (statusObj as { type?: unknown }).type === 'string' + ? String((statusObj as { type: string }).type) + : typeof thread.status === 'string' + ? thread.status + : undefined; + return { + title: + typeof thread.name === 'string' + ? thread.name + : typeof thread.preview === 'string' + ? thread.preview + : undefined, + cwd: typeof thread.cwd === 'string' ? thread.cwd : null, + status: statusType, + modelProvider: typeof thread.modelProvider === 'string' ? thread.modelProvider : null, + model: typeof thread.model === 'string' ? thread.model : null, + }; + } catch (error) { + throw maybeRemoteThreadError(error, threadId); + } +} + +async function listTurnsFull( + client: AppServerClient, + threadId: string, + { limit, full, cursor, title }: { limit: number; full: boolean; cursor?: string; title?: string }, +): Promise<{ turns: ChatGptDesktopTurn[]; complete: boolean; nextCursor: string | null }> { + type ReadCursor = { threadId: string; full: boolean; rpcCursor: string | null; skip: number; field?: 'text' | 'userText' | 'assistantText'; offset?: number }; + const state = cursor ? decodePageCursor(READ_PAGE_PREFIX, cursor) : null; + if (state && (state.threadId !== threadId || state.full !== full || !Number.isSafeInteger(state.skip) || state.skip < 0 + || (state.rpcCursor !== null && typeof state.rpcCursor !== 'string') + || (state.field !== undefined && !['text', 'userText', 'assistantText'].includes(state.field)) + || (state.offset !== undefined && (!Number.isSafeInteger(state.offset) || state.offset < 0)))) { + throw new Error('Read cursor does not match this thread or mode'); + } + let rpcCursor = state?.rpcCursor ?? undefined; + let skip = state?.skip ?? 0; + let fragmentField = state?.field; + let fragmentOffset = state?.offset ?? 0; + let itemsView = true; + let method = 'thread/turns/list'; + const seen = new Set(); + const turns: ChatGptDesktopTurn[] = []; + const pageCursor = (source: string | undefined, index: number, field?: ReadCursor['field'], offset?: number) => + encodePageCursor(READ_PAGE_PREFIX, { threadId, full, rpcCursor: source ?? null, skip: index, + ...(field ? { field, offset } : {}) }); + const fits = (candidate: ChatGptDesktopTurn) => serializedBytes({ threadId, turns: [...turns, candidate], + backend: 'app-server', limit, full, complete: false, nextCursor: READ_PAGE_PREFIX, + title: title ? compactThreadTitle(title).title : undefined, + warnings: ['Read page stopped before the end of the thread; continue with nextCursor to retrieve every turn and text fragment'], + exitCode: 0 }) <= DESKTOP_RESULT_BUDGET_BYTES; + while (turns.length < limit) { + let result: { data?: unknown[]; nextCursor?: string | null }; + try { + result = await client.request(method, { + threadId, + limit: Math.min(10, Math.max(limit - turns.length, skip + 1)), + sortDirection: full ? 'asc' : 'desc', + ...(method === 'thread/turns/list' && itemsView ? { itemsView: 'full' } : {}), + ...(rpcCursor !== undefined ? { cursor: rpcCursor } : {}), + }) as typeof result; + } catch (error) { + const code = (error as { rpc?: { code?: number } })?.rpc?.code; + if (turns.length === 0 && code === -32602 && itemsView) { + itemsView = false; + continue; + } + if (turns.length === 0 && code === -32601 && method === 'thread/turns/list') { + method = 'thread/items/list'; + continue; + } + throw maybeRemoteThreadError(error, threadId); + } + if (!Array.isArray(result?.data)) throw new Error(`${method} missing data array for ${threadId}`); + if (skip > result.data.length) throw new Error('Read cursor is stale'); + const pageTurns = normalizeTurns(result.data); + for (let index = skip; index < pageTurns.length; index++) { + const turn = pageTurns[index]; + if (!fragmentField && turns.length && !fits(turn)) { + return { turns: chronologicalPage(turns, full), complete: false, + nextCursor: pageCursor(rpcCursor, index) }; + } + if (!fragmentField && fits(turn)) { + turns.push(turn); + } else { + const fields = (['text', 'userText', 'assistantText'] as const).filter((field) => + typeof turn[field] === 'string' && turn[field]!.length > 0); + let fieldIndex = fragmentField ? fields.indexOf(fragmentField) : 0; + if (fieldIndex < 0) throw new Error('Read fragment cursor is stale'); + for (; fieldIndex < fields.length; fieldIndex++) { + const field = fields[fieldIndex]; + const value = turn[field]!; + let offset = field === fragmentField ? fragmentOffset : 0; + if (offset > value.length) throw new Error('Read fragment cursor is stale'); + while (offset < value.length) { + let size = Math.min(value.length - offset, 32_768); + let fragment: ChatGptDesktopTurn; + do { + let end = offset + size; + if (end < value.length && /[\uD800-\uDBFF]/u.test(value[end - 1])) end--; + const chunk = value.slice(offset, end); + fragment = { turnKey: turn.turnKey, role: turn.role, text: field === 'text' ? chunk : '', + ...(field === 'userText' ? { userText: chunk } : {}), + ...(field === 'assistantText' ? { assistantText: chunk } : {}), + startedAt: turn.startedAt, endedAt: turn.endedAt, textTruncated: true, + continuation: { field, offsetChars: offset, totalChars: value.length, + fieldComplete: end === value.length, + turnComplete: end === value.length && fieldIndex === fields.length - 1 } }; + if (fits(fragment)) { size = end - offset; break; } + size = Math.floor(size / 2); + } while (size > 0); + if (size === 0) { + if (turns.length) return { turns: chronologicalPage(turns, full), complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) }; + throw new Error('Read fragment metadata exceeds the MCP result byte budget'); + } + turns.push(fragment); + offset += size; + if (turns.length >= limit || offset < value.length && !fits({ ...fragment, text: 'x'.repeat(32_768) })) { + return { turns: chronologicalPage(turns, full), complete: false, + nextCursor: pageCursor(rpcCursor, index, field, offset) }; + } + } + } + fragmentField = undefined; + fragmentOffset = 0; + } + if (turns.length >= limit) { + const nextCursor = index + 1 < pageTurns.length ? pageCursor(rpcCursor, index + 1) + : result.nextCursor ? pageCursor(result.nextCursor, 0) : null; + return { turns: chronologicalPage(turns, full), complete: nextCursor === null, nextCursor }; + } + } + if (result.nextCursor == null) return { turns: chronologicalPage(turns, full), complete: true, nextCursor: null }; + if (typeof result.nextCursor !== 'string' || !result.nextCursor || seen.has(result.nextCursor)) { + throw new Error(`${method} returned an invalid or repeated cursor`); + } + seen.add(result.nextCursor); + rpcCursor = result.nextCursor; + skip = 0; + } + return { turns: chronologicalPage(turns, full), complete: false, nextCursor: pageCursor(rpcCursor, skip) }; +} + +function chronologicalPage(turns: ChatGptDesktopTurn[], full: boolean): ChatGptDesktopTurn[] { + if (full) return turns; + const groups: ChatGptDesktopTurn[][] = []; + for (const turn of turns) { + const last = groups.at(-1); + if (last?.[0]?.turnKey === turn.turnKey) last.push(turn); + else groups.push([turn]); + } + return groups.reverse().flat(); +} + +function normalizeTurns(raw: unknown[]): ChatGptDesktopTurn[] { + const turns: ChatGptDesktopTurn[] = []; + for (const [index, item] of raw.entries()) { + if (!item || typeof item !== 'object') continue; + const row = item as Record; + const id = String(row.id ?? row.turnId ?? index); + const items = Array.isArray(row.items) ? row.items : null; + if (items) { + turns.push(normalizeTurnWithItems(id, row, items)); + continue; + } + turns.push(normalizeLeafItem(id, row)); + } + return turns; +} + +function normalizeTurnWithItems( + id: string, + row: Record, + items: unknown[], +): ChatGptDesktopTurn { + let userText = ''; + let assistantText = ''; + let toolText = ''; + let reasoningText = ''; + for (const entry of items) { + if (!entry || typeof entry !== 'object') continue; + const item = entry as Record; + const type = String(item.type ?? item.role ?? ''); + const text = extractText(item); + if (/user/i.test(type)) userText = joinText(userText, text); + else if (/agent|assistant|message/i.test(type) && !/reasoning|tool/i.test(type)) { + assistantText = joinText(assistantText, text); + } else if (/tool/i.test(type)) toolText = joinText(toolText, text || type); + else if (/reasoning/i.test(type)) reasoningText = joinText(reasoningText, text); + } + let role: ChatGptDesktopTurn['role'] = 'assistant'; + let text = assistantText || userText; + if (userText && !assistantText) { + role = 'user'; + text = userText; + } else if (!userText && !assistantText && toolText) { + role = 'tool'; + text = toolText; + } else if (!userText && !assistantText && reasoningText) { + role = 'reasoning'; + text = reasoningText; + } + return { + turnKey: toDomTurnKey(id), + role, + text, + ...(userText ? { userText } : {}), + ...(assistantText ? { assistantText } : {}), + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null, + }; +} + +function normalizeLeafItem(id: string, row: Record): ChatGptDesktopTurn { + const text = extractText(row); + const type = String(row.type ?? row.role ?? ''); + let role: ChatGptDesktopTurn['role'] = 'assistant'; + if (/user/i.test(type)) role = 'user'; + else if (/tool/i.test(type)) role = 'tool'; + else if (/reasoning/i.test(type)) role = 'reasoning'; + else if (/status|system/i.test(type) && !text) role = 'status'; + return { + turnKey: toDomTurnKey(id), + role, + text, + startedAt: typeof row.startedAt === 'number' ? row.startedAt : null, + endedAt: typeof row.completedAt === 'number' ? row.completedAt : typeof row.endedAt === 'number' ? row.endedAt : null, + }; +} + +function extractText(row: Record): string { + if (typeof row.text === 'string') return row.text; + if (typeof row.content === 'string') return row.content; + if (Array.isArray(row.content)) return row.content + .filter((part): part is Record => Boolean(part) && typeof part === 'object' && !Array.isArray(part)) + .map((part) => typeof part.text === 'string' ? part.text : '') + .filter(Boolean).join('\n'); + if (typeof row.preview === 'string') return row.preview; + return ''; +} + +function joinText(left: string, right: string): string { + if (!right) return left; + if (!left) return right; + return `${left}\n${right}`; +} + +function isThreadNotLoaded(error: unknown): boolean { + const message = errorMessage(error); + return /thread not loaded|not loaded|invalid thread id/i.test(message); +} + +function isUnknownThread(error: unknown): boolean { + const message = errorMessage(error); + return /no rollout found|thread not found|unknown.*thread/i.test(message); +} + +function maybeRemoteThreadError(error: unknown, threadId: string): Error { + const message = errorMessage(error); + if (/thread not loaded|not loaded|no rollout found|thread not found/i.test(message)) { + const remote = findRemoteThread(threadId); + if (remote) { + return new RemoteThreadNotLoadedError(threadId, remote.hostId, remote.hostName); + } + const hostId = findRemoteThreadHostId(threadId); + if (hostId) return new RemoteThreadNotLoadedError(threadId, hostId, null); + } + if (error instanceof Error) return error; + return new Error(message || `app-server error for thread ${threadId}`); +} + +function errorMessage(error: unknown): string { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) { + return String((error as { message: unknown }).message); + } + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = (error as { rpc?: { message?: unknown } }).rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); +} diff --git a/src/core/chatgpt-desktop/cdp-adapter.ts b/src/core/chatgpt-desktop/cdp-adapter.ts new file mode 100644 index 0000000..4b88fc8 --- /dev/null +++ b/src/core/chatgpt-desktop/cdp-adapter.ts @@ -0,0 +1,334 @@ +import { existsSync } from 'node:fs'; +import type { ChatGptDesktopAdapter } from './adapter.js'; +import { ArchivedThreadError, CdpUnreachableError, ProjectUnavailableError } from './errors.js'; +import { CdpSession } from './cdp-session.js'; +import { + DEFAULT_CONVERSATION_RESOLVE_MS, + DEFAULT_OPEN_TIMEOUT_MS, + TEMP_THREAD_ID_PREFIX, + listThreadsFromDom, + openThreadInDom, + pickMainWindowTarget, + ReplyTimeoutError, + readReplyState, + readTurnsFromDom, + resolveDurableThreadId, + sendMessageInDom, + startNewChatInDom, + summarizeTargetInfos, + waitForConversationId, + waitForLoadingTaskGone, + waitForReplyDone, +} from './cdp-dom.js'; +import { isTemporaryDesktopThreadId, toAppServerThreadId, toDesktopThreadId } from './thread-ids.js'; +import { resolveCdpPort } from './loopback.js'; +import { findLocalProject } from './project-state.js'; +import type { + ChatGptDesktopStatus, + ChatGptDesktopTarget, + ListThreadsResult, + OpenThreadResult, + ReadThreadResult, + SendMessageResult, + WaitForReplyResult, +} from './types.js'; + +type TargetGetTargetsResult = { + targetInfos?: Array<{ + targetId?: string; + type?: string; + title?: string; + url?: string; + attached?: boolean; + }>; +}; + +/** + * CDP-backed ChatGPT Desktop adapter. Selector/data logic stays in cdp-dom.ts. + * Prefer Target.getTargets over /json/list (incomplete + unstable order). + */ +export class CdpChatGptDesktopAdapter implements ChatGptDesktopAdapter { + #port = resolveCdpPort(); + #session: CdpSession | null = null; + #version: Awaited> | null = null; + #pageSessionId: string | null = null; + #mainTargetId: string | null = null; + #submittedTurn: { threadId: string; turnKey: string } | null = null; + + async connect({ port }: { port: number }): Promise { + await this.close(); + this.#port = resolveCdpPort(process.env, port); + const session = new CdpSession(this.#port); + try { + this.#version = await session.connect(); + this.#session = session; + await this.#attachMainWindow(); + } catch (error) { + session.close(); + this.#session = null; + throw error; + } + } + + async listTargets(): Promise { + const session = this.#requireSession(); + const result = await session.send('Target.getTargets'); + return summarizeTargetInfos(result.targetInfos ?? []); + } + + async listThreads({ limit = 50 }: { limit?: number } = {}): Promise { + const sessionId = await this.#ensurePageSession(); + const threads = await listThreadsFromDom(this.#requireSession(), sessionId, { limit }); + return { threads, backend: 'cdp', limit }; + } + + async readThread({ + threadId, + limit = 100, + full = false, + openTimeoutMs = DEFAULT_OPEN_TIMEOUT_MS, + }: { + threadId: string; + limit?: number; + full?: boolean; + openTimeoutMs?: number; + }): Promise { + const sessionId = await this.#ensurePageSession(); + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + const turns = await readTurnsFromDom(this.#requireSession(), sessionId, { + limit, + full, + threadId, + }); + return { threadId, turns, backend: 'cdp', limit, full, complete: false }; + } + + async sendMessage({ + threadId, + text, + project, + openTimeoutMs = DEFAULT_OPEN_TIMEOUT_MS, + }: { + threadId?: string; + text: string; + project?: string; + openTimeoutMs?: number; + }): Promise { + const sessionId = await this.#ensurePageSession(); + let temporaryThreadId: string | undefined; + const startedNew = !threadId; + + if (threadId) { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } else { + const localProject = project ? findLocalProject(project) : null; + if (localProject && !localProject.rootPaths.some((path) => existsSync(path))) { + throw new ProjectUnavailableError(localProject.label, + `none of its configured workspace roots exists: ${localProject.rootPaths.map((path) => JSON.stringify(path)).join(', ') || '(none)'}. Update the Desktop project root before sending.`); + } + await startNewChatInDom(this.#requireSession(), sessionId, { + project: localProject?.label ?? project, projectId: localProject?.id, + }); + const threads = await listThreadsFromDom(this.#requireSession(), sessionId, { limit: 20 }); + const temp = threads.find((t) => t.threadId.startsWith(TEMP_THREAD_ID_PREFIX) && t.selected) + ?? threads.find((t) => t.threadId.startsWith(TEMP_THREAD_ID_PREFIX)); + if (temp) temporaryThreadId = temp.threadId; + } + + const before = await readReplyState(this.#requireSession(), sessionId); + const { sentVia } = await sendMessageInDom(this.#requireSession(), sessionId, text); + + let conversationId: string | undefined; + if (startedNew || (threadId && isTemporaryDesktopThreadId(threadId))) { + const resolveMs = Math.min(openTimeoutMs, DEFAULT_CONVERSATION_RESOLVE_MS); + try { + conversationId = await waitForConversationId(this.#requireSession(), sessionId, { + timeoutMs: resolveMs, + }); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', reason: 'conversation-id-unresolved', + }); + } + } + + const durable = resolveDurableThreadId(conversationId, threadId); + if (!durable) { + throw new Error( + 'ChatGPT Desktop send accepted but no durable conversation id was available; ' + + 'refusing to return a local:client-new-thread:* id as threadId', + ); + } + + this.#submittedTurn = { threadId: durable, turnKey: before.lastTurnKey }; + return { + threadId: durable, + temporaryThreadId, + conversationId: conversationId || toAppServerThreadId(durable) || durable, + project, + backend: 'cdp', + experimental: false, + delivery: 'accepted', + sentVia, + message: 'Submitted via CDP composer (focus → insertText → Enter)', + }; + } + + async waitForReply({ + threadId, + timeoutMs = 120_000, + }: { + threadId?: string; + timeoutMs?: number; + }): Promise { + const deadline = Date.now() + timeoutMs; + const sessionId = await this.#ensurePageSession(); + const expectedId = threadId && threadId !== 'new' + ? toAppServerThreadId(threadId) ?? undefined + : this.#submittedTurn ? toAppServerThreadId(this.#submittedTurn.threadId) ?? undefined : undefined; + if (threadId && !isTemporaryDesktopThreadId(threadId) && threadId !== 'new') { + // Stay on the current chat for temp/new; otherwise ensure the thread is open. + await this.#openAndWait(sessionId, threadId, Math.min(timeoutMs, DEFAULT_OPEN_TIMEOUT_MS)); + } + + try { + const done = await waitForReplyDone(this.#requireSession(), sessionId, { + timeoutMs: Math.max(0, deadline - Date.now()), + expectedConversationId: expectedId, + baselineTurnKey: this.#submittedTurn && (!threadId || toDesktopThreadId(threadId) === this.#submittedTurn.threadId) + ? this.#submittedTurn.turnKey : undefined, + }); + const durable = + resolveDurableThreadId(done.conversationId, threadId) + ?? (done.conversationId ? toDesktopThreadId(done.conversationId) : null); + if (!durable) { + throw new Error( + 'ChatGPT Desktop reply finished without a durable conversation id on ' + + '[data-response-annotation-conversation]', + ); + } + return { + threadId: durable, + conversationId: done.conversationId || toAppServerThreadId(durable) || durable, + reply: done.reply, + backend: 'cdp', + experimental: false, + delivery: 'replied', + }; + } catch (error) { + if (!(error instanceof ReplyTimeoutError)) throw error; + return { + threadId: threadId ?? 'unknown', + reply: '', + backend: 'cdp', + experimental: false, + delivery: 'timeout', + }; + } + } + + async openThread( + threadId: string, + { openTimeoutMs = DEFAULT_OPEN_TIMEOUT_MS }: { openTimeoutMs?: number } = {}, + ): Promise { + const sessionId = await this.#ensurePageSession(); + try { + await this.#openAndWait(sessionId, threadId, openTimeoutMs); + } catch (error) { + if (error instanceof ArchivedThreadError) return { threadId, backend: 'cdp', archived: true }; + throw error; + } + return { threadId, backend: 'cdp' }; + } + + async status(): Promise { + try { + const version = this.#version ?? (await CdpSession.fetchVersion(this.#port)); + let targetCount: number | undefined; + let mainWindowTargetId: string | null | undefined; + if (this.#session && !this.#session.closed) { + const targets = await this.listTargets(); + targetCount = targets.length; + mainWindowTargetId = pickMainWindowTarget(targets)?.targetId ?? null; + } else { + const list = await CdpSession.fetchJsonList(this.#port); + targetCount = list.length; + } + return { + reachable: true, + port: this.#port, + host: '127.0.0.1', + browser: typeof version.Browser === 'string' ? version.Browser : undefined, + protocolVersion: + typeof version['Protocol-Version'] === 'string' ? version['Protocol-Version'] : undefined, + webSocketDebuggerUrl: + typeof version.webSocketDebuggerUrl === 'string' ? version.webSocketDebuggerUrl : undefined, + targetCount, + mainWindowTargetId: mainWindowTargetId ?? this.#mainTargetId, + message: 'ChatGPT Desktop CDP endpoint reachable on 127.0.0.1', + exitCode: 0, + }; + } catch (error) { + return { + reachable: false, + port: this.#port, + host: '127.0.0.1', + message: + error instanceof Error + ? error.message + : 'ChatGPT Desktop CDP endpoint unreachable on 127.0.0.1', + exitCode: 1, + }; + } + } + + async close(): Promise { + this.#pageSessionId = null; + this.#mainTargetId = null; + this.#version = null; + this.#session?.close(); + this.#session = null; + } + + async #openAndWait(sessionId: string, threadId: string, openTimeoutMs: number): Promise { + await openThreadInDom(this.#requireSession(), sessionId, threadId); + await waitForLoadingTaskGone(this.#requireSession(), sessionId, { + timeoutMs: openTimeoutMs, + threadId, + }); + } + + async #attachMainWindow(): Promise { + const targets = await this.listTargets(); + const main = pickMainWindowTarget(targets); + if (!main) { + throw new CdpUnreachableError( + `No ChatGPT Desktop main window target with url ${JSON.stringify('app://-/index.html')}`, + ); + } + const attached = await this.#requireSession().send<{ sessionId: string }>( + 'Target.attachToTarget', + { targetId: main.targetId, flatten: true }, + ); + this.#mainTargetId = main.targetId; + this.#pageSessionId = attached.sessionId; + } + + async #ensurePageSession(): Promise { + this.#requireSession(); + if (!this.#pageSessionId) await this.#attachMainWindow(); + if (!this.#pageSessionId) { + throw new CdpUnreachableError('Failed to attach to ChatGPT Desktop main window'); + } + return this.#pageSessionId; + } + + #requireSession(): CdpSession { + if (!this.#session || this.#session.closed) { + throw new CdpUnreachableError( + 'Not connected to ChatGPT Desktop CDP. Call connect({ port }) after relaunching with --remote-debugging-port.', + ); + } + return this.#session; + } +} diff --git a/src/core/chatgpt-desktop/cdp-dom.ts b/src/core/chatgpt-desktop/cdp-dom.ts new file mode 100644 index 0000000..a0961a8 --- /dev/null +++ b/src/core/chatgpt-desktop/cdp-dom.ts @@ -0,0 +1,852 @@ +/** + * ChatGPT Desktop DOM / data-layer for CDP Runtime.evaluate + Input events. + * + * ALL selector and in-page data extraction lives in this module. Verified live + * on macOS ChatGPT.app (com.openai.codex, Chrome 154). + */ + +import type { ChatGptDesktopTarget, ChatGptDesktopThread, ChatGptDesktopTurn } from './types.js'; +import type { CdpSession } from './cdp-session.js'; +import { ArchivedThreadError, ComposerDraftError, NewChatNavigationError, ProjectUnavailableError } from './errors.js'; +import { + LOCAL_THREAD_ID_PREFIX, + isTemporaryDesktopThreadId, + normalizeCodexThreadId, +} from '../codex/thread-id.js'; + +/** Exact main-window URL; ignore overlays / detached / webviews. */ +export const MAIN_WINDOW_URL = 'app://-/index.html'; + +export const IGNORED_TARGET_URL_MARKERS = [ + 'initialRoute=%2Favatar-overlay', + 'detached-window.html', + 'chatgpt.com', + 'codex-sandbox', +] as const; + +export const ATTR = { + threadRow: 'data-app-action-sidebar-thread-row', + threadId: 'data-app-action-sidebar-thread-id', + threadTitle: 'data-app-action-sidebar-thread-title', + threadPinned: 'data-app-action-sidebar-thread-pinned', + threadSelected: 'data-app-action-sidebar-thread-selected', + threadKind: 'data-app-action-sidebar-thread-kind', + turnKey: 'data-turn-key', + userBubble: 'data-user-message-bubble', + timelineScroll: 'data-app-action-timeline-scroll', + virtualizedTurn: 'data-virtualized-turn-content', + composer: 'data-codex-composer', + finalAssistant: 'data-local-conversation-final-assistant', + markdownTextStyle: 'data-markdown-text-style', + conversationAnnotation: 'data-response-annotation-conversation', +} as const; + +export const MAIN_CONTENT_SURFACE_CLASS_PREFIX = '_MainContentSurface'; +export const HISTORY_GAP_PREFIX = 'history-gap:'; +export const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; +export const LOADING_TASK_TEXT = 'Loading task…'; +export const DEFAULT_OPEN_TIMEOUT_MS = 90_000; +/** How long a new-thread send waits for the real conversation annotation. */ +export const DEFAULT_CONVERSATION_RESOLVE_MS = 60_000; +export const DEFAULT_FULL_READ_IDLE_WHEELS = 8; +export const DEFAULT_FULL_READ_MAX_WHEELS = 400; +/** + * Timeline is `column-reverse`: scrollTop 0 is the newest message. Setting + * scrollTop does not load older history — use mouseWheel with this deltaY + * (negative = toward older turns). + */ +export const FULL_READ_WHEEL_DELTA_Y = -800; + +/** Verified composer: contenteditable textbox. */ +export const COMPOSER_SELECTOR = + `[${ATTR.composer}="true"][contenteditable="true"]` as const; + +/** Verified send button (only present while the composer has text). */ +export const SEND_BUTTON_SELECTOR = 'button[aria-label="Send"]' as const; + +/** Stop button in main — present while a reply is streaming. */ +export const STOP_BUTTON_SELECTOR = 'main button[aria-label="Stop"]' as const; + +/** Final assistant marker + markdown body inside a turn. */ +export const FINAL_ASSISTANT_SELECTOR = + `[${ATTR.finalAssistant}="true"]` as const; +export const ASSISTANT_MESSAGE_SELECTOR = + `[${ATTR.finalAssistant}="true"] [${ATTR.markdownTextStyle}="assistant-message"]` as const; + +/** Conversation id annotation inside a reply. */ +export const CONVERSATION_ANNOTATION_SELECTOR = + `[${ATTR.conversationAnnotation}]` as const; + +export const SELECTORS = { + threadRow: `[role="button"][${ATTR.threadRow}]`, + turn: `[${ATTR.turnKey}]`, + userBubble: `[${ATTR.userBubble}]`, + timelineScroll: `[${ATTR.timelineScroll}]`, + composer: COMPOSER_SELECTOR, + sendButton: SEND_BUTTON_SELECTOR, + stopButton: STOP_BUTTON_SELECTOR, + finalAssistant: FINAL_ASSISTANT_SELECTOR, + assistantMessage: ASSISTANT_MESSAGE_SELECTOR, + conversationAnnotation: CONVERSATION_ANNOTATION_SELECTOR, + newChatFallback: 'button', +} as const; + +export function newChatInProjectSelector(project: string): string { + return `button[aria-label=${JSON.stringify(`Start new chat in ${project}`)}]`; +} + +export function isMainWindowTarget(target: { + type?: string; + url?: string; +}): boolean { + if (target.type !== 'page') return false; + const url = typeof target.url === 'string' ? target.url : ''; + if (url !== MAIN_WINDOW_URL) return false; + return !IGNORED_TARGET_URL_MARKERS.some((marker) => url.includes(marker)); +} + +export function summarizeTargetInfos( + targetInfos: readonly { + targetId?: string; + type?: string; + title?: string; + url?: string; + attached?: boolean; + }[], +): ChatGptDesktopTarget[] { + return targetInfos + .filter((t) => typeof t.targetId === 'string' && typeof t.type === 'string') + .map((t) => ({ + targetId: String(t.targetId), + type: String(t.type), + title: typeof t.title === 'string' ? t.title : '', + url: typeof t.url === 'string' ? t.url : '', + attached: Boolean(t.attached), + })); +} + +export function pickMainWindowTarget( + targets: readonly ChatGptDesktopTarget[], +): ChatGptDesktopTarget | null { + return targets.find((t) => isMainWindowTarget(t)) ?? null; +} + +/** Dedupe sidebar rows (Recent + project can both list the same id). */ +export function dedupeThreadsById( + threads: readonly ChatGptDesktopThread[], +): ChatGptDesktopThread[] { + const byKey = new Map(); + for (const thread of threads) { + if (!thread.threadId) continue; + const key = durableThreadKey(thread.threadId); + const existing = byKey.get(key); + if (!existing) { + byKey.set(key, thread); + continue; + } + byKey.set(key, preferThreadRow(existing, thread)); + } + return [...byKey.values()]; +} + +function durableThreadKey(threadId: string): string { + if (isTemporaryDesktopThreadId(threadId)) return threadId; + try { + return normalizeCodexThreadId(threadId); + } catch { + return threadId.startsWith(LOCAL_THREAD_ID_PREFIX) + ? threadId.slice(LOCAL_THREAD_ID_PREFIX.length) || threadId + : threadId; + } +} + +function toDesktopForm(threadId: string): string { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return threadId; + if (threadId.startsWith(LOCAL_THREAD_ID_PREFIX)) return threadId; + return `${LOCAL_THREAD_ID_PREFIX}${threadId}`; +} + +function preferThreadRow( + left: ChatGptDesktopThread, + right: ChatGptDesktopThread, +): ChatGptDesktopThread { + const score = (row: ChatGptDesktopThread) => + (row.selected ? 4 : 0) + + (row.pinned ? 2 : 0) + + (row.threadId.startsWith(LOCAL_THREAD_ID_PREFIX) && !isTemporaryDesktopThreadId(row.threadId) + ? 1 + : 0); + return score(right) > score(left) ? right : left; +} + +export const LIST_THREADS_EXPRESSION = `(() => { + const skipProjectLabels = new Set(['Recent', 'Pinned', 'Threads', 'Chats']); + const projectFor = (row) => { + let el = row.parentElement; + for (let i = 0; i < 8 && el; i += 1, el = el.parentElement) { + const attr = + el.getAttribute('data-app-action-sidebar-project') || + el.getAttribute('data-app-action-sidebar-project-name') || + el.getAttribute('data-app-action-sidebar-section-title'); + if (attr && !skipProjectLabels.has(attr)) return attr; + } + return undefined; + }; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const seen = new Set(); + const out = []; + for (const row of rows) { + const threadId = row.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''; + if (!threadId || seen.has(threadId)) continue; + seen.add(threadId); + const project = projectFor(row); + out.push({ + threadId, + hostId: row.getAttribute('data-app-action-sidebar-thread-host-id') || null, + title: row.getAttribute(${JSON.stringify(ATTR.threadTitle)}) || (row.textContent || '').trim(), + pinned: row.getAttribute(${JSON.stringify(ATTR.threadPinned)}) === 'true', + selected: row.getAttribute(${JSON.stringify(ATTR.threadSelected)}) === 'true', + kind: row.getAttribute(${JSON.stringify(ATTR.threadKind)}) || 'unknown', + ...(project ? { project } : {}), + }); + } + return out; +})()`; + +export function openThreadExpression(threadId: string): string { + const bare = durableThreadKey(threadId); + const desktop = toDesktopForm(bare); + const candidates = [...new Set([threadId, bare, desktop].filter(Boolean))]; + return `(() => { + const candidates = ${JSON.stringify(candidates)}; + const rows = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.threadRow)})); + const row = rows.find((el) => + candidates.includes(el.getAttribute(${JSON.stringify(ATTR.threadId)}) || ''), + ); + if (!row) return { ok: false, error: 'thread-not-found' }; + row.click(); + return { ok: true, threadId: row.getAttribute(${JSON.stringify(ATTR.threadId)}) || candidates[0] }; + })()`; +} + +/** Desktop uses a memory router; the browser URL stays app://-/index.html. */ +export function navigateToThreadExpression(threadId: string): string { + const bare = durableThreadKey(threadId); + return `(async () => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; + let scanned = 0; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); + if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { + await router.navigate(${JSON.stringify(`/local/${bare}`)}); + return { ok: true, via: 'router' }; + } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + return { ok: false, error: 'router-not-found' }; + })()`; +} + +export function startNewChatExpression(project?: string, projectId?: string): string { + const projectSel = project ? newChatInProjectSelector(project) : null; + return `(() => { + const projectSel = ${JSON.stringify(projectSel)}; + if (projectSel) { + const preferred = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(projectSel) ||` : ''} document.querySelector(projectSel); + if (preferred) { + const row = preferred.closest('[data-app-action-sidebar-project-row]') || preferred.parentElement; + row?.scrollIntoView({ block: 'center' }); + const rowBox = row?.getBoundingClientRect(); + return { ok: true, via: 'project', project: ${JSON.stringify(project ?? null)}, + disabled: preferred.disabled || preferred.getAttribute('aria-disabled') === 'true', + hoverX: rowBox?.left + rowBox?.width / 2, hoverY: rowBox?.top + rowBox?.height / 2 }; + } + return { ok: false, error: 'project-not-found' }; + } + const buttons = Array.from(document.querySelectorAll('button')); + const fallback = buttons.find((btn) => (btn.textContent || '').trim() === 'New chat'); + if (!fallback) return { ok: false, error: 'new-chat-not-found' }; + fallback.click(); + return { ok: true, via: 'fallback' }; + })()`; +} + +export function projectButtonBoxExpression(project: string, projectId?: string): string { + return `(() => { + const button = ${projectId ? `document.querySelector('[data-app-action-sidebar-project-id=${JSON.stringify(projectId)}]')?.querySelector(${JSON.stringify(newChatInProjectSelector(project))}) ||` : ''} document.querySelector(${JSON.stringify(newChatInProjectSelector(project))}); + if (!button) return null; + const box = button.getBoundingClientRect(); + return { x: box.left + box.width / 2, y: box.top + box.height / 2, + disabled: button.disabled || button.getAttribute('aria-disabled') === 'true', + visible: box.width > 0 && box.height > 0 && box.x >= 0 && box.y >= 0 + && box.x < innerWidth && box.y < innerHeight && getComputedStyle(button.parentElement).opacity !== '0' }; + })()`; +} + +/** The new-chat route is `/`; the composer exposes its selected project. */ +export function newChatViewExpression(project?: string): string { + return `(() => { + const root = window.__codexRoot?._internalRoot?.current; + const queue = [root]; let scanned = 0; let path = ''; + while (queue.length && scanned++ < 10000) { + const fiber = queue.shift(); if (!fiber) continue; + const router = fiber.memoizedProps?.router; + if (router?.navigate && router?.state?.location) { path = router.state.location.pathname; break; } + if (fiber.child) queue.push(fiber.child); + if (fiber.sibling) queue.push(fiber.sibling); + } + const composer = !!document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + const scope = document.querySelector('[data-composer-navigation-target="workspace-project"]')?.getAttribute('aria-label') || ''; + const expected = ${JSON.stringify(project ? `Change project: ${project}` : '')}; + return { ready: path === '/' && composer && (expected ? scope === expected : (!scope || scope === 'Choose project')), path, composer, scope }; + })()`; +} + +export const CLEAR_NEW_CHAT_PROJECT_EXPRESSION = `(() => { + const button = document.querySelector('[data-clear-project-button="true"]'); + if (!button || button.disabled) return false; + button.click(); + return true; +})()`; + +export const LOADING_TASK_GONE_EXPRESSION = `(() => { + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const text = (surface.innerText || '').replace(/\\u2026/g, '…'); + return !text.includes(${JSON.stringify(LOADING_TASK_TEXT)}); +})()`; + +export const ARCHIVED_THREAD_EXPRESSION = `(() => Array.from(document.querySelectorAll('main')) + .some((main) => (main.innerText || '').includes('This task is archived') + && (main.innerText || '').includes('Unarchive this task to open it')))()`; + +export const FOCUS_COMPOSER_EXPRESSION = `(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + if (!composer) return { ok: false, error: 'composer-not-found' }; + if ((composer.textContent || '').trim()) return { ok: false, error: 'composer-has-draft' }; + if (document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)})) return { ok: false, error: 'reply-in-progress' }; + composer.focus(); + return { ok: true }; +})()`; + +export const REPLY_STATE_EXPRESSION = `(() => { + const stop = document.querySelector(${JSON.stringify(STOP_BUTTON_SELECTOR)}); + const finals = Array.from(document.querySelectorAll(${JSON.stringify(FINAL_ASSISTANT_SELECTOR)})); + const lastFinal = finals.at(-1) || null; + const annotation = lastFinal + ? lastFinal.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + || lastFinal.closest(${JSON.stringify(SELECTORS.turn)})?.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}) + : document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + const conversationId = annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; + const messageEl = lastFinal + ? lastFinal.querySelector(${JSON.stringify(`[${ATTR.markdownTextStyle}="assistant-message"]`)}) + : null; + const reply = messageEl ? (messageEl.innerText || '').trim() : ''; + const turns = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.turn)})); + const lastTurn = turns.at(-1); + const lastTurnKey = lastTurn ? (lastTurn.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '') : ''; + return { + stopVisible: Boolean(stop), + finalAssistantCount: finals.length, + conversationId, + reply, + lastTurnKey, + finalTurnKey: lastFinal?.closest(${JSON.stringify(SELECTORS.turn)})?.getAttribute(${JSON.stringify(ATTR.turnKey)}) || '', + }; +})()`; + +/** Any conversation annotation on the page (user turn or assistant). */ +export const CONVERSATION_ID_EXPRESSION = `(() => { + const annotation = document.querySelector(${JSON.stringify(CONVERSATION_ANNOTATION_SELECTOR)}); + return annotation + ? (annotation.getAttribute(${JSON.stringify(ATTR.conversationAnnotation)}) || '') + : ''; +})()`; + +/** + * Harvest currently rendered turns (no wheel). One turn may hold both the user + * bubble and the final assistant reply. Skip history-gap placeholders. + */ +export const HARVEST_VISIBLE_TURNS_EXPRESSION = `(() => { + const gapPrefix = ${JSON.stringify(HISTORY_GAP_PREFIX)}; + const surfacePrefix = ${JSON.stringify(MAIN_CONTENT_SURFACE_CLASS_PREFIX)}; + const mains = Array.from(document.querySelectorAll('main')); + const surface = + mains.find((el) => typeof el.className === 'string' && el.className.includes(surfacePrefix)) + || (mains.length >= 2 ? mains[1] : null) + || mains[0] + || document.body; + const scroll = + surface.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}) + || document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + const root = scroll || surface; + const out = []; + for (const el of root.querySelectorAll(${JSON.stringify(SELECTORS.turn)})) { + const turnKey = el.getAttribute(${JSON.stringify(ATTR.turnKey)}) || ''; + if (!turnKey || turnKey.startsWith(gapPrefix)) continue; + const user = el.querySelector(${JSON.stringify(SELECTORS.userBubble)}); + const assistant = el.querySelector(${JSON.stringify(ASSISTANT_MESSAGE_SELECTOR)}); + const userText = user ? (user.innerText || '').trim() : ''; + const assistantText = assistant ? (assistant.innerText || '').trim() : ''; + const raw = (el.innerText || '').trim(); + const statusRe = /^Worked for \\d+s$/i; + let role = 'assistant'; + let text = assistantText || userText || raw; + if (user && !assistantText) { + role = 'user'; + text = userText || raw; + } else if (assistantText) { + role = 'assistant'; + text = assistantText; + } else if (!raw || statusRe.test(raw)) { + role = 'status'; + text = raw; + } + out.push({ turnKey, role, text, userText, assistantText }); + } + return out; +})()`; + +export const TIMELINE_SCROLL_METRICS_EXPRESSION = `(() => { + const scroll = document.querySelector(${JSON.stringify(SELECTORS.timelineScroll)}); + if (!scroll) return { ok: false }; + const rect = scroll.getBoundingClientRect(); + return { + ok: true, + x: Math.floor(rect.left + rect.width / 2), + y: Math.floor(rect.top + Math.min(rect.height / 2, 120)), + scrollTop: scroll.scrollTop, + scrollHeight: scroll.scrollHeight, + clientHeight: scroll.clientHeight, + }; +})()`; + +export async function listThreadsFromDom( + session: CdpSession, + sessionId: string, + { limit = 50 }: { limit?: number } = {}, +): Promise { + const rows = await session.evaluate(LIST_THREADS_EXPRESSION, { sessionId }); + if (!Array.isArray(rows)) return []; + return dedupeThreadsById(rows).slice(0, limit).map((row) => ({ + threadId: String(row.threadId), + title: String(row.title ?? ''), + pinned: Boolean(row.pinned), + selected: Boolean(row.selected), + kind: String(row.kind ?? 'unknown'), + ...(row.hostId ? { hostId: row.hostId, location: row.hostId === 'local' ? 'local' as const : 'remote' as const } : {}), + ...(row.project ? { project: String(row.project) } : {}), + })); +} + +export async function openThreadInDom( + session: CdpSession, + sessionId: string, + threadId: string, +): Promise { + const current = await readConversationIdFromDom(session, sessionId); + if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + if (isTemporaryDesktopThreadId(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { limit: 200 }); + if (rows.some((row) => row.selected && row.threadId === threadId)) return; + } + const result = await session.evaluate<{ ok: boolean; error?: string }>( + openThreadExpression(threadId), + { sessionId }, + ); + if (!result?.ok) { + if (result?.error === 'thread-not-found' && !isTemporaryDesktopThreadId(threadId)) { + const navigated = await session.evaluate<{ ok: boolean }>(navigateToThreadExpression(threadId), { sessionId }); + if (navigated?.ok) return; + } + throw new Error( + result?.error === 'thread-not-found' + ? `ChatGPT Desktop thread not found: ${threadId}` + : `Failed to open ChatGPT Desktop thread ${threadId}`, + ); + } +} + +export async function waitForLoadingTaskGone( + session: CdpSession, + sessionId: string, + { timeoutMs = DEFAULT_OPEN_TIMEOUT_MS, threadId }: { timeoutMs?: number; threadId?: string } = {}, +): Promise { + const deadline = Date.now() + timeoutMs; + let loadingGone = false; + let lastConversationId = ''; + let archivedSince: number | null = null; + while (Date.now() < deadline) { + if (threadId && threadId !== 'new' && await session.evaluate(ARCHIVED_THREAD_EXPRESSION, { sessionId }) === true) { + archivedSince ??= Date.now(); + if (Date.now() - archivedSince >= 500) { + throw new ArchivedThreadError(threadId); + } + } else { + archivedSince = null; + } + const gone = await session.evaluate(LOADING_TASK_GONE_EXPRESSION, { sessionId }); + loadingGone ||= gone; + if (gone) { + // A click can resolve before React replaces the previous timeline. + const current = threadId ? await readConversationIdFromDom(session, sessionId) : ''; + lastConversationId = current; + if (!threadId) return; + if (threadId === 'new' || isTemporaryDesktopThreadId(threadId)) { + const rows = await listThreadsFromDom(session, sessionId, { limit: 200 }); + const selected = rows.find((row) => row.selected); + if (!current && (!selected || isTemporaryDesktopThreadId(selected.threadId))) return; + } else if (current && durableThreadKey(current) === durableThreadKey(threadId)) return; + } + await delay(250); + } + throw new Error( + loadingGone + ? `ChatGPT Desktop timed out waiting for conversation ${JSON.stringify(threadId)}; displayed conversation ${JSON.stringify(lastConversationId)} after ${timeoutMs}ms` + : `ChatGPT Desktop thread still showing ${JSON.stringify(LOADING_TASK_TEXT)} after ${timeoutMs}ms`, + ); +} + +export async function startNewChatInDom( + session: CdpSession, + sessionId: string, + { project, projectId, navigationTimeoutMs = 3000 }: { project?: string; projectId?: string; navigationTimeoutMs?: number } = {}, +): Promise<{ via: string }> { + const result = await session.evaluate<{ ok: boolean; via?: string; error?: string; disabled?: boolean; hoverX?: number; hoverY?: number }>( + startNewChatExpression(project, projectId), + { sessionId }, + ); + if (!result?.ok) { + throw new Error( + project + ? `ChatGPT Desktop new-chat button not found for project ${JSON.stringify(project)}` + : 'ChatGPT Desktop "New chat" button not found', + ); + } + if (result.via === 'project') { + if (result.disabled) throw new ProjectUnavailableError(project!, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (![result.hoverX, result.hoverY].every(Number.isFinite)) { + throw new Error(`ChatGPT Desktop project button has no visible box for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: result.hoverX, y: result.hoverY }, { sessionId }); + const button = await session.evaluate<{ x: number; y: number; visible: boolean; disabled: boolean } | null>(projectButtonBoxExpression(project!, projectId), { sessionId }); + if (button?.disabled) throw new ProjectUnavailableError(project!, 'the Desktop project new-chat action is disabled; check its configured workspace root'); + if (!button?.visible || !Number.isFinite(button.x) || !Number.isFinite(button.y)) { + throw new Error(`ChatGPT Desktop project button did not appear after hover for ${JSON.stringify(project)}`); + } + await session.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: button.x, y: button.y }, { sessionId }); + await session.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: button.x, y: button.y, button: 'left', clickCount: 1 }, { sessionId }); + await session.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: button.x, y: button.y, button: 'left', clickCount: 1 }, { sessionId }); + } + let observed = { ready: false, path: '', composer: false, scope: '' }; + const deadline = Date.now() + navigationTimeoutMs; + while (Date.now() < deadline) { + observed = await session.evaluate(newChatViewExpression(project), { sessionId }); + if (observed?.ready) return { via: result.via ?? 'unknown' }; + if (!project && observed?.path === '/' && observed?.composer && observed.scope.startsWith('Change project: ')) { + await session.evaluate(CLEAR_NEW_CHAT_PROJECT_EXPRESSION, { sessionId }); + } + await delay(100); + } + throw new NewChatNavigationError(project, `route ${JSON.stringify(observed?.path)}, composer ${Boolean(observed?.composer)}, project ${JSON.stringify(observed?.scope)}`); +} + +export async function focusComposer( + session: CdpSession, + sessionId: string, +): Promise { + const focused = await session.evaluate<{ ok: boolean; error?: string }>( + FOCUS_COMPOSER_EXPRESSION, + { sessionId }, + ); + if (!focused?.ok) { + if (focused?.error === 'composer-has-draft') throw new ComposerDraftError(); + throw new Error(`ChatGPT Desktop composer unavailable: ${focused?.error || 'not found'}`); + } +} + +/** Submit via Enter key events (preferred). */ +export async function submitComposerWithEnter( + session: CdpSession, + sessionId: string, +): Promise { + const base = { + windowsVirtualKeyCode: 13, + code: 'Enter', + key: 'Enter', + text: '\r', + unmodifiedText: '\r', + }; + await session.send('Input.dispatchKeyEvent', { type: 'rawKeyDown', ...base }, { sessionId }); + await session.send('Input.dispatchKeyEvent', { type: 'keyUp', ...base }, { sessionId }); +} + +export async function sendMessageInDom( + session: CdpSession, + sessionId: string, + text: string, +): Promise<{ sentVia: 'enter' }> { + await focusComposer(session, sessionId); + await session.send('Input.insertText', { text }, { sessionId }); + try { + await submitComposerWithEnter(session, sessionId); + // Never retry submission just because React has not cleared the button yet. + const deadline = Date.now() + 3000; + while (Date.now() < deadline) { + const cleared = await session.evaluate(`(() => { + const composer = document.querySelector(${JSON.stringify(COMPOSER_SELECTOR)}); + return Boolean(composer && !(composer.textContent || '').trim()); + })()`, { sessionId }); + if (cleared) return { sentVia: 'enter' }; + await delay(100); + } + throw new Error('Desktop composer did not acknowledge submission'); + } catch (error) { + throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), { + delivery: 'unknown', reason: 'submission-unconfirmed', + }); + } +} + +export type ReplyState = { + stopVisible: boolean; + finalAssistantCount: number; + conversationId: string; + reply: string; + lastTurnKey: string; + finalTurnKey?: string; +}; + +export async function readReplyState( + session: CdpSession, + sessionId: string, +): Promise { + const state = await session.evaluate(REPLY_STATE_EXPRESSION, { sessionId }); + return { + stopVisible: Boolean(state?.stopVisible), + finalAssistantCount: Number(state?.finalAssistantCount ?? 0), + conversationId: String(state?.conversationId ?? ''), + reply: String(state?.reply ?? ''), + lastTurnKey: String(state?.lastTurnKey ?? ''), + finalTurnKey: String(state?.finalTurnKey ?? ''), + }; +} + +/** Read the current `data-response-annotation-conversation` value, if any. */ +export async function readConversationIdFromDom( + session: CdpSession, + sessionId: string, +): Promise { + const value = await session.evaluate(CONVERSATION_ID_EXPRESSION, { sessionId }); + return typeof value === 'string' ? value.trim() : ''; +} + +/** + * Wait until the real conversation id appears on + * `[data-response-annotation-conversation]`. Rejects temporary sidebar ids. + */ +export async function waitForConversationId( + session: CdpSession, + sessionId: string, + { timeoutMs = DEFAULT_CONVERSATION_RESOLVE_MS }: { timeoutMs?: number } = {}, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const id = await readConversationIdFromDom(session, sessionId); + if (id && !isTemporaryDesktopThreadId(id) && !id.startsWith(TEMP_THREAD_ID_PREFIX)) { + return id; + } + await delay(200); + } + throw new Error( + `ChatGPT Desktop conversation id did not appear on [data-response-annotation-conversation] within ${timeoutMs}ms`, + ); +} + +/** + * Prefer a durable Desktop thread id. Never returns `local:client-new-thread:*`. + */ +export function resolveDurableThreadId( + conversationId: string | null | undefined, + fallbackThreadId?: string | null, +): string | null { + if (conversationId && !isTemporaryDesktopThreadId(conversationId)) { + return toDesktopForm(conversationId); + } + if ( + fallbackThreadId + && !isTemporaryDesktopThreadId(fallbackThreadId) + && fallbackThreadId !== 'new' + ) { + return toDesktopForm(fallbackThreadId); + } + return null; +} + +/** + * Wait until Stop is gone and a new final-assistant marker exists. + * Returns reply text + conversation id from the annotation. + */ +export class ReplyTimeoutError extends Error {} + +export async function waitForReplyDone( + session: CdpSession, + sessionId: string, + { + timeoutMs = 120_000, + baselineFinalCount = 0, + expectedConversationId, + baselineTurnKey, + }: { timeoutMs?: number; baselineFinalCount?: number; expectedConversationId?: string; baselineTurnKey?: string } = {}, +): Promise<{ reply: string; conversationId: string }> { + const deadline = Date.now() + timeoutMs; + let sawStop = false; + const started = Date.now(); + while (Date.now() < deadline) { + const state = await readReplyState(session, sessionId); + if (state.stopVisible) sawStop = true; + expectedConversationId ??= state.conversationId || undefined; + if (expectedConversationId && state.conversationId + && durableThreadKey(state.conversationId) !== durableThreadKey(expectedConversationId)) { + throw new Error('ChatGPT Desktop selected conversation changed while waiting'); + } + const latestIsFinal = Boolean(state.finalTurnKey) && state.finalTurnKey === state.lastTurnKey; + const hasNewFinal = (state.finalAssistantCount > baselineFinalCount || sawStop) + && Boolean(state.reply) && latestIsFinal + && (!baselineTurnKey || state.lastTurnKey !== baselineTurnKey); + // Stop appears within ~1s of submit; done when Stop is gone AND a new final exists. + if (!state.stopVisible && hasNewFinal && (sawStop || Date.now() - started > 1500)) { + return { reply: state.reply, conversationId: state.conversationId }; + } + await delay(200); + } + throw new ReplyTimeoutError(`ChatGPT Desktop reply did not finish within ${timeoutMs}ms`); +} + +function normalizeTurn(raw: { + turnKey?: string; + role?: string; + text?: string; + userText?: string; + assistantText?: string; +}): ChatGptDesktopTurn { + const role = + raw.role === 'user' || raw.role === 'status' || raw.role === 'assistant' + ? raw.role + : raw.assistantText + ? 'assistant' + : raw.userText + ? 'user' + : 'assistant'; + return { + turnKey: String(raw.turnKey ?? ''), + role, + text: String(raw.text ?? raw.assistantText ?? raw.userText ?? ''), + userText: raw.userText ? String(raw.userText) : undefined, + assistantText: raw.assistantText ? String(raw.assistantText) : undefined, + }; +} + +export async function harvestVisibleTurns( + session: CdpSession, + sessionId: string, +): Promise { + const rows = await session.evaluate[0]>>( + HARVEST_VISIBLE_TURNS_EXPRESSION, + { sessionId }, + ); + if (!Array.isArray(rows)) return []; + return rows.map(normalizeTurn).filter((t) => t.turnKey && !t.turnKey.startsWith(HISTORY_GAP_PREFIX)); +} + +/** + * Full history crawl: the timeline is column-reverse; scrollTop scripting does + * not load older turns (yields history-gap placeholders). Wheel with negative + * deltaY over the container, collect by data-turn-key, skip history-gap keys, + * stop after idle wheels or when a turn key equals the thread id. + */ +export async function readTurnsFromDom( + session: CdpSession, + sessionId: string, + { + limit = 100, + full = false, + threadId, + idleWheels = DEFAULT_FULL_READ_IDLE_WHEELS, + maxWheels = DEFAULT_FULL_READ_MAX_WHEELS, + }: { + limit?: number; + full?: boolean; + threadId?: string; + idleWheels?: number; + maxWheels?: number; + } = {}, +): Promise { + const collected = new Map(); + const order: string[] = []; + const ingest = async () => { + const older: string[] = []; + for (const turn of await harvestVisibleTurns(session, sessionId)) { + if (!collected.has(turn.turnKey)) older.push(turn.turnKey); + collected.set(turn.turnKey, turn); + } + order.unshift(...older); + }; + + await ingest(); + if (!full) { + const visible = order.map((key) => collected.get(key)!); + return limit > 0 ? visible.slice(-limit) : visible; + } + + let idle = 0; + for (let step = 0; step < maxWheels; step++) { + const before = collected.size; + const metrics = await session.evaluate<{ + ok: boolean; + x?: number; + y?: number; + }>(TIMELINE_SCROLL_METRICS_EXPRESSION, { sessionId }); + if (!metrics?.ok || metrics.x == null || metrics.y == null) break; + + await session.send( + 'Input.dispatchMouseEvent', + { + type: 'mouseWheel', + x: metrics.x, + y: metrics.y, + deltaX: 0, + deltaY: FULL_READ_WHEEL_DELTA_Y, + }, + { sessionId }, + ); + await delay(120); + await ingest(); + + if (collected.size === before) { + idle += 1; + if (idle >= idleWheels) break; + } else { + idle = 0; + } + if (threadId && collected.has(threadId)) break; + } + + const all = order.map((key) => collected.get(key)!); + return limit > 0 ? all.slice(0, limit) : all; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/core/chatgpt-desktop/cdp-session.ts b/src/core/chatgpt-desktop/cdp-session.ts new file mode 100644 index 0000000..3b47bd4 --- /dev/null +++ b/src/core/chatgpt-desktop/cdp-session.ts @@ -0,0 +1,225 @@ +import { CdpUnreachableError } from './errors.js'; +import { + cdpHttpBase, + forceLoopbackWebSocketUrl, +} from './loopback.js'; + +export type CdpVersionInfo = { + readonly Browser?: string; + readonly 'Protocol-Version'?: string; + readonly webSocketDebuggerUrl?: string; + readonly [key: string]: unknown; +}; + +type Pending = { + resolve: (value: unknown) => void; + reject: (error: Error) => void; + timer: ReturnType; +}; + +/** + * Minimal local-only CDP client. Connects to 127.0.0.1 only; never accepts a + * remote host. Uses Node's built-in WebSocket (no extra dependency). + */ +export class CdpSession { + #port: number; + #ws: WebSocket | null = null; + #nextId = 1; + #pending = new Map(); + #closed = false; + + constructor(port: number) { + this.#port = port; + } + + get port(): number { + return this.#port; + } + + get closed(): boolean { + return this.#closed || this.#ws === null || this.#ws.readyState === WebSocket.CLOSED; + } + + static async fetchVersion(port: number, { signal }: { signal?: AbortSignal } = {}): Promise { + const base = cdpHttpBase(port); + let response: Response; + try { + response = await fetch(`${base}/json/version`, { + signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(5000)]) : AbortSignal.timeout(5000), + redirect: 'error', + }); + } catch (error) { + throw new CdpUnreachableError( + `ChatGPT Desktop CDP not reachable at ${base}/json/version: ` + + (error instanceof Error ? error.message : String(error)), + ); + } + if (!response.ok) { + throw new CdpUnreachableError( + `ChatGPT Desktop CDP /json/version returned HTTP ${response.status}`, + ); + } + const version = (await response.json()) as CdpVersionInfo; + if (typeof version.webSocketDebuggerUrl === 'string') { + forceLoopbackWebSocketUrl(version.webSocketDebuggerUrl); + } + return version; + } + + static async fetchJsonList(port: number, { signal }: { signal?: AbortSignal } = {}): Promise { + const base = cdpHttpBase(port); + let response: Response; + try { + response = await fetch(`${base}/json/list`, { + signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(5000)]) : AbortSignal.timeout(5000), + redirect: 'error', + }); + } catch (error) { + throw new CdpUnreachableError( + `ChatGPT Desktop CDP not reachable at ${base}/json/list: ` + + (error instanceof Error ? error.message : String(error)), + ); + } + if (!response.ok) { + throw new CdpUnreachableError( + `ChatGPT Desktop CDP /json/list returned HTTP ${response.status}`, + ); + } + const body: unknown = await response.json(); + return Array.isArray(body) ? body : []; + } + + async connect({ signal }: { signal?: AbortSignal } = {}): Promise { + if (this.#ws && !this.closed) { + throw new Error('CDP session already connected'); + } + const version = await CdpSession.fetchVersion(this.#port, { signal }); + const rawUrl = version.webSocketDebuggerUrl; + if (typeof rawUrl !== 'string' || !rawUrl) { + throw new CdpUnreachableError('CDP /json/version omitted webSocketDebuggerUrl'); + } + const url = forceLoopbackWebSocketUrl(rawUrl); + + signal?.throwIfAborted(); + await new Promise((resolve, reject) => { + const ws = new WebSocket(url); + this.#ws = ws; + const cleanup = () => { + clearTimeout(timer); + signal?.removeEventListener('abort', onAbort); + }; + const onAbort = () => { + cleanup(); + ws.close(); + reject(new CdpUnreachableError('CDP connect aborted or timed out')); + }; + const timer = setTimeout(onAbort, 5000); + signal?.addEventListener('abort', onAbort, { once: true }); + ws.addEventListener('open', () => { + cleanup(); + resolve(); + }, { once: true }); + ws.addEventListener('error', () => { + cleanup(); + reject(new CdpUnreachableError(`CDP WebSocket failed for ${url}`)); + }, { once: true }); + ws.addEventListener('message', (event) => this.#onMessage(event.data)); + ws.addEventListener('close', () => { + cleanup(); + reject(new CdpUnreachableError('CDP WebSocket closed before connecting')); + this.#closed = true; + for (const [, pending] of this.#pending) { + clearTimeout(pending.timer); + pending.reject(new CdpUnreachableError('CDP WebSocket closed')); + } + this.#pending.clear(); + }); + }); + this.#closed = false; + return version; + } + + async send( + method: string, + params?: Record, + { sessionId }: { sessionId?: string } = {}, + ): Promise { + if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) { + throw new CdpUnreachableError('CDP session is not connected'); + } + const id = this.#nextId++; + const payload: Record = { id, method }; + if (params !== undefined) payload.params = params; + if (sessionId !== undefined) payload.sessionId = sessionId; + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.#pending.delete(id); + reject(new CdpUnreachableError(`CDP ${method} timed out`)); + }, 10000); + this.#pending.set(id, { + timer, + resolve: (value) => resolve(value as T), + reject, + }); + try { + this.#ws!.send(JSON.stringify(payload)); + } catch (error) { + clearTimeout(timer); + this.#pending.delete(id); + reject(error); + } + }); + } + + async evaluate( + expression: string, + { sessionId, awaitPromise = true }: { sessionId?: string; awaitPromise?: boolean } = {}, + ): Promise { + const result = await this.send<{ + result?: { type?: string; value?: unknown; description?: string }; + exceptionDetails?: { text?: string; exception?: { description?: string } }; + }>('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise, + }, { sessionId }); + if (result.exceptionDetails) { + const text = + result.exceptionDetails.exception?.description || + result.exceptionDetails.text || + 'Runtime.evaluate failed'; + throw new Error(text); + } + return result.result?.value as T; + } + + close(): void { + this.#closed = true; + for (const pending of this.#pending.values()) { + clearTimeout(pending.timer); + pending.reject(new CdpUnreachableError('CDP session closed')); + } + this.#pending.clear(); + this.#ws?.close(); + this.#ws = null; + } + + #onMessage(data: unknown): void { + let message: { id?: number; result?: unknown; error?: { message?: string } }; + try { + message = JSON.parse(String(data)) as typeof message; + } catch { + return; + } + if (!message || typeof message !== 'object' || typeof message.id !== 'number') return; + const pending = this.#pending.get(message.id); + if (!pending) return; + this.#pending.delete(message.id); + clearTimeout(pending.timer); + if (message.error) { + pending.reject(new Error(message.error.message || 'CDP error')); + return; + } + pending.resolve(message.result); + } +} diff --git a/src/core/chatgpt-desktop/errors.ts b/src/core/chatgpt-desktop/errors.ts new file mode 100644 index 0000000..e4118ff --- /dev/null +++ b/src/core/chatgpt-desktop/errors.ts @@ -0,0 +1,93 @@ +export { RemoteThreadNotLoadedError } from '../codex/remote-control.js'; + +/** Desktop opened the route but requires explicit unarchive before interaction. */ +export class ArchivedThreadError extends Error { + readonly code = 'ARCHIVED_THREAD' as const; + readonly reason = 'archived-thread' as const; + readonly delivery = 'rejected' as const; + readonly threadId: string; + + constructor(threadId: string) { + super(`ChatGPT Desktop thread ${JSON.stringify(threadId)} is archived. Read it through app-server, or pass unarchive: true on send to unarchive before sending.`); + this.name = 'ArchivedThreadError'; + this.threadId = threadId; + } +} + +/** Sending would replace an operator's unsent composer draft. */ +export class ComposerDraftError extends Error { + readonly code = 'COMPOSER_HAS_DRAFT' as const; + readonly reason = 'composer-has-draft' as const; + readonly delivery = 'rejected' as const; + + constructor() { + super('ChatGPT Desktop composer contains an unsent draft; inspect it in Desktop before sending.'); + this.name = 'ComposerDraftError'; + } +} + +/** Desktop cannot start a chat in a project whose action is disabled or root is unavailable. */ +export class ProjectUnavailableError extends Error { + readonly code = 'PROJECT_UNAVAILABLE' as const; + readonly reason = 'project-unavailable' as const; + readonly delivery = 'rejected' as const; + + constructor(project: string, detail: string) { + super(`ChatGPT Desktop cannot start a chat in project ${JSON.stringify(project)}: ${detail}`); + this.name = 'ProjectUnavailableError'; + } +} + +export class NewChatNavigationError extends Error { + readonly code = 'NEW_CHAT_NAVIGATION_FAILED' as const; + readonly reason = 'new-chat-navigation-failed' as const; + readonly delivery = 'rejected' as const; + + constructor(project: string | undefined, observed: string) { + super(`ChatGPT Desktop new-chat action did not open ${project ? `project ${JSON.stringify(project)}` : 'a projectless chat'}; observed ${observed}`); + this.name = 'NewChatNavigationError'; + } +} + +/** Typed failure when a ChatGPT Desktop CDP/DOM operation is not yet implemented. */ +export class NotImplementedError extends Error { + readonly code = 'NOT_IMPLEMENTED' as const; + readonly delivery = 'rejected' as const; + readonly reason = 'not-implemented' as const; + + constructor(operation: string, detail?: string) { + super( + detail + ? `ChatGPT Desktop ${operation} is not implemented: ${detail}` + : `ChatGPT Desktop ${operation} is not implemented`, + ); + this.name = 'NotImplementedError'; + } +} + +/** Thrown when a CDP endpoint host is not loopback. */ +export class CdpHostRejectedError extends Error { + readonly code = 'CDP_HOST_REJECTED' as const; + readonly delivery = 'rejected' as const; + readonly reason = 'cdp-host-rejected' as const; + + constructor(host: string) { + super( + `ChatGPT Desktop CDP rejects non-loopback host ${JSON.stringify(host)}. ` + + 'Connect to 127.0.0.1 only; there is no remote transport.', + ); + this.name = 'CdpHostRejectedError'; + } +} + +/** Thrown when the local CDP endpoint is unreachable. */ +export class CdpUnreachableError extends Error { + readonly code = 'CDP_UNREACHABLE' as const; + readonly delivery = 'rejected' as const; + readonly reason = 'cdp-unreachable' as const; + + constructor(message: string) { + super(message); + this.name = 'CdpUnreachableError'; + } +} diff --git a/src/core/chatgpt-desktop/facade.ts b/src/core/chatgpt-desktop/facade.ts new file mode 100644 index 0000000..7dd5830 --- /dev/null +++ b/src/core/chatgpt-desktop/facade.ts @@ -0,0 +1,824 @@ +import { createHash } from 'node:crypto'; +import type { ChatGptDesktopAdapter } from './adapter.js'; +import { CdpChatGptDesktopAdapter } from './cdp-adapter.js'; +import { + appServerListThreads, + appServerReadThread, + appServerUnarchiveThread, + appServerSearchThreads, + appServerStatusProbe, + discoverModelProviders, +} from './app-server-fallback.js'; +import { ArchivedThreadError, CdpUnreachableError, NotImplementedError, RemoteThreadNotLoadedError } from './errors.js'; +import { resolveCdpPort } from './loopback.js'; +import { findRemoteThread, listDiscoveredHosts, listRemoteThreadsFromState } from './remote-threads.js'; +import { resolveThreadProjects } from './project-state.js'; +import { compactThreadRow, compactThreadTitle, decodePageCursor, DESKTOP_RESULT_BUDGET_BYTES, encodePageCursor, minimalThreadRow, serializedBytes } from './payload-budget.js'; +import { + isTemporaryDesktopThreadId, + threadIdsEquivalent, + toAppServerThreadId, +} from './thread-ids.js'; +import type { + ChatGptDesktopHostGroup, + ChatGptDesktopStatus, + ChatGptDesktopTarget, + ChatGptDesktopThread, + ListHostsResult, + ListThreadsResult, + OpenThreadResult, + ReadThreadResult, + SendMessageResult, + WaitForReplyResult, +} from './types.js'; + +/** Optional overrides so tests can prove app-server / CDP routing without a live daemon. */ +export type ChatGptDesktopFallbacks = { + listThreads: typeof appServerListThreads; + searchThreads: typeof appServerSearchThreads; + readThread: typeof appServerReadThread; + unarchiveThread?: typeof appServerUnarchiveThread; + statusProbe: typeof appServerStatusProbe; + listRemoteThreads?: typeof listRemoteThreadsFromState; + listHosts?: typeof listDiscoveredHosts; + discoverModelProviders?: typeof discoverModelProviders; +}; + +const defaultFallbacks: ChatGptDesktopFallbacks = { + listThreads: appServerListThreads, + searchThreads: appServerSearchThreads, + readThread: appServerReadThread, + unarchiveThread: appServerUnarchiveThread, + statusProbe: appServerStatusProbe, + listRemoteThreads: listRemoteThreadsFromState, + listHosts: listDiscoveredHosts, + discoverModelProviders, +}; + +const MERGED_LIST_CURSOR = 'desktop-merged-list-v1:'; +const MERGED_SEARCH_CURSOR = 'desktop-merged-search-v1:'; +type ListSnapshot = { + fingerprint: string; + port: number; + filters: string; + createdAt: number; + rows: readonly ChatGptDesktopThread[]; + backend: ListThreadsResult['backend']; + warnings: readonly string[]; +}; + +export type ListThreadsOptions = { + limit?: number; + cursor?: string; + /** + * Any string (not an enum). Reserved: `all` (default), `local`. Otherwise a + * hostId or friendly name discovered at runtime — see `listHosts` / + * `chatgpt_desktop_list_hosts`. New machines appear with no code change. + */ + host?: string; + /** + * Any modelProvider id string (not an enum). Passed through to app-server + * `thread/list` as `modelProviders`. Omit / empty = all. Discovered values: + * see `listHosts` / `chatgpt_desktop_list_hosts`. + */ + modelProvider?: string; + /** Match a Desktop project label or app-server projectId. */ + project?: string; + /** When `"host"`, also return `groups` keyed by host (grouping mode). */ + groupBy?: 'host'; +}; + +type SearchThreadsOptions = ListThreadsOptions & { query: string }; + +/** + * ChatGPT Desktop facade (verified against app-server 0.158.0). + * + * - List / search / read: app-server primary; list also merges remote-control + * summaries from `~/.codex/.codex-global-state.json`. + * - Send, new-thread-in-project, wait-for-reply, selected thread / UI state: CDP. + * - Every list/read/send/wait/open result reports `backend: "cdp" | "app-server"`. + */ +export class ChatGptDesktopFacade implements ChatGptDesktopAdapter { + #cdp: ChatGptDesktopAdapter; + #port: number; + #cdpConnected = false; + #fallbacks: ChatGptDesktopFallbacks; + #listSnapshots = new Map(); + + constructor( + cdp: ChatGptDesktopAdapter = new CdpChatGptDesktopAdapter(), + port = resolveCdpPort(), + fallbacks: ChatGptDesktopFallbacks = defaultFallbacks, + ) { + this.#cdp = cdp; + this.#port = port; + this.#fallbacks = fallbacks; + } + + async connect({ port }: { port: number }): Promise { + this.#port = resolveCdpPort(process.env, port); + try { + await this.#cdp.connect({ port: this.#port }); + this.#cdpConnected = true; + } catch (error) { + this.#cdpConnected = false; + throw error; + } + } + + async listTargets(): Promise { + await this.#ensureCdp(); + return this.#cdp.listTargets(); + } + + async listThreads(options: ListThreadsOptions = {}): Promise { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, + project: options.project?.trim() || null, + groupBy: options.groupBy ?? null, + }); + let snapshot: ListSnapshot; + let offset = 0; + if (options.cursor) { + const state = decodePageCursor<{ fingerprint: string; offset: number; port: number; filters: string }>( + MERGED_LIST_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) || state.offset < 1 + || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop list cursor does not match the port or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildListSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop thread inventory changed; restart listing without a cursor'); + } + } else { + snapshot = await this.#buildListSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_LIST_CURSOR); + } + + #pageSnapshot(snapshot: ListSnapshot, offset: number, limit: number, options: ListThreadsOptions, + prefix: string, query?: string): ListThreadsResult { + if (offset > snapshot.rows.length) throw new Error('ChatGPT Desktop cursor is out of range'); + const host = normalizeHostFilter(options.host); + const rows: ChatGptDesktopThread[] = []; + const base = { + backend: snapshot.backend, limit, host, + ...(query !== undefined ? { query } : {}), + ...(options.modelProvider?.trim() ? { modelProvider: options.modelProvider.trim() } : {}), + ...(options.project ? { project: options.project } : {}), + ...(options.groupBy === 'host' ? { groupBy: 'host' as const } : {}), + }; + for (const source of snapshot.rows.slice(offset, offset + limit)) { + let row = source; + const candidate = [...rows, row]; + const probe = { ...base, threads: candidate, nextCursor: prefix, + ...(options.groupBy === 'host' ? { groups: groupThreadsByHost(candidate) } : {}), + warnings: snapshot.warnings, exitCode: 0 }; + if (serializedBytes(probe) > DESKTOP_RESULT_BUDGET_BYTES) { + if (rows.length) break; + row = minimalThreadRow(row); + const minimal = [row]; + if (serializedBytes({ ...base, threads: minimal, nextCursor: prefix, + ...(options.groupBy === 'host' ? { groups: groupThreadsByHost(minimal) } : {}), + warnings: snapshot.warnings, exitCode: 0 }) > DESKTOP_RESULT_BUDGET_BYTES) { + throw new Error('ChatGPT Desktop thread row exceeds the MCP result byte budget'); + } + } + rows.push(row); + } + const nextOffset = offset + rows.length; + const nextCursor = nextOffset < snapshot.rows.length + ? encodePageCursor(prefix, { fingerprint: snapshot.fingerprint, offset: nextOffset, + port: this.#port, filters: snapshot.filters }) : null; + const warnings = [...snapshot.warnings]; + if (rows.length < Math.min(limit, snapshot.rows.length - offset)) { + warnings.push(`${query === undefined ? 'List' : 'Search'} page stopped at the MCP byte budget; continue with nextCursor`); + } + return { ...base, threads: rows, nextCursor, + ...(options.groupBy === 'host' ? { groups: groupThreadsByHost(rows) } : {}), + ...(warnings.length ? { warnings } : {}) }; + } + + async #buildListSnapshot(options: ListThreadsOptions, filters: string): Promise { + const appRows: ChatGptDesktopThread[] = []; + const warnings: string[] = []; + let appCursor: string | undefined; + let appComplete = false; + const seenCursors = new Set(); + const modelProviders = modelProvidersFromFilter(options.modelProvider); + let appSucceeded = false; + try { + for (let page = 0; page < 400; page++) { + const out = await this.#fallbacks.listThreads({ limit: 200, cursor: appCursor, modelProviders }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) { appComplete = true; break; } + if (seenCursors.has(out.nextCursor)) throw new Error('App-server list returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + } + if (!appComplete) warnings.push('App-server inventory stopped after 400 pages'); + } catch (error) { + warnings.push(`App-server list failed: ${error instanceof Error ? error.message : String(error)}`); + } + const remotes = this.#listRemotes(); + let cdpList: ListThreadsResult | null = null; + try { + await this.#ensureCdp(); + cdpList = await this.#cdp.listThreads({ limit: 200 }); + } catch (error) { + if (isCdpFailure(error)) this.#cdpConnected = false; + else warnings.push(`CDP list failed: ${error instanceof Error ? error.message : String(error)}`); + } + if (!appRows.length && !cdpList && !remotes.length) { + throw new CdpUnreachableError(warnings[0] ?? 'ChatGPT Desktop listThreads: CDP and app-server unreachable'); + } + const appList: ListThreadsResult = { backend: 'app-server', limit: appRows.length, threads: appRows }; + const base = appSucceeded + ? (cdpList ? mergeThreadLists(appList, cdpList) : appList) + : (cdpList ?? { backend: 'remote-state' as const, limit: 0, threads: [] }); + const merged = finalizeThreadList({ ...base, threads: resolveThreadProjects(base.threads) }, { + limit: Number.MAX_SAFE_INTEGER, + host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, + project: options.project, + remotes: resolveThreadProjects(remotes), + }); + const rows = [...new Map(merged.threads.map((row) => [toAppServerThreadId(row.threadId) ?? row.threadId, row])).values()]; + rows.sort(compareDesktopThreads); + const fingerprint = createHash('sha256').update(JSON.stringify({ filters, port: this.#port, + rows: rows.map((row) => [row.threadId, row.updatedAt, row.location, row.hostId]) })).digest('base64url'); + const snapshot: ListSnapshot = { fingerprint, port: this.#port, filters, createdAt: Date.now(), + rows, backend: merged.backend, warnings }; + for (const [key, value] of this.#listSnapshots) { + if (Date.now() - value.createdAt > 15 * 60_000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + + async listHosts(): Promise { + const remotes = this.#listRemotes(); + const cdpList = typeof this.#cdp.listThreads === 'function' + ? await this.#tryCdpList({ limit: 200 }) : null; + const cdpRows = cdpList?.threads ?? []; + let localThreadCount = 0; + let appSucceeded = false; + try { + const local = await this.#fallbacks.listThreads({ limit: 25, modelProviders: [] }); + appSucceeded = true; + localThreadCount = local.threads.length; + } catch { + localThreadCount = 0; + } + const listHosts = this.#fallbacks.listHosts ?? listDiscoveredHosts; + const fromState = listHosts(process.env, { localThreadCount }); + const byId = new Map(fromState.map((host) => [host.hostId, { ...host }])); + + // Ensure every host seen in merged remotes is present (tests / custom + // listRemoteThreads may supply rows without a matching global-state file). + for (const thread of remotes) { + const existing = byId.get(thread.hostId); + if (existing) { + byId.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName, + }); + } else { + byId.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 0, + }); + } + } + + for (const thread of cdpRows) { + if (thread.location !== 'remote' || !thread.hostId) continue; + if (!byId.has(thread.hostId)) byId.set(thread.hostId, { + hostId: thread.hostId, hostName: thread.hostName ?? null, location: 'remote', threadCount: 0, + }); + } + + const hostsSource = 'local+remote-thread-summaries-v3+managed-connections' + + (cdpRows.length ? '+cdp' : ''); + + // Recount remotes from the parsed summary rows for accuracy. + const remoteCounts = new Map(); + const remoteInventory = finalizeThreadList({ + backend: cdpRows.length ? 'cdp' : 'remote-state', limit: 10_000, + threads: cdpRows.filter((thread) => thread.location === 'remote'), + }, { limit: 10_000, remotes }); + for (const thread of remoteInventory.threads) { + if (thread.hostId) remoteCounts.set(thread.hostId, (remoteCounts.get(thread.hostId) ?? 0) + 1); + } + const withCounts = [...byId.values()] + .map((host) => + host.hostId === 'local' + ? { ...host, threadCount: localThreadCount } + : { ...host, threadCount: remoteCounts.get(host.hostId) ?? host.threadCount }, + ) + .sort((a, b) => { + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); + + let modelProviders: string[] = []; + let modelProvidersSource = 'thread/list-distinct'; + const warnings: string[] = []; + try { + const discover = this.#fallbacks.discoverModelProviders ?? discoverModelProviders; + const discovered = await discover(); + modelProviders = [...discovered.providers]; + modelProvidersSource = discovered.source; + if (typeof discovered.threadCount === 'number') localThreadCount = discovered.threadCount; + if (discovered.complete === false) warnings.push('Provider discovery stopped before the final app-server page'); + } catch (error) { + warnings.push(`Model provider discovery failed: ${error instanceof Error ? error.message : String(error)}`); + modelProviders = [...new Set( + remotes + .map((thread) => thread.modelProvider) + .filter((value): value is string => typeof value === 'string' && Boolean(value)), + )].sort(); + modelProvidersSource = 'remote-summaries-distinct'; + } + modelProviders = [...new Set([ + ...modelProviders, + ...remotes.map((thread) => thread.modelProvider).filter((value): value is string => Boolean(value)), + ])].sort(); + let inventoryCounts: Map | null = null; + try { + const filters = JSON.stringify({ host: 'all', modelProvider: null, project: null, groupBy: null }); + const cached = [...this.#listSnapshots.values()].find((snapshot) => + snapshot.port === this.#port && snapshot.filters === filters && Date.now() - snapshot.createdAt < 15 * 60_000); + const inventory = cached ?? await this.#buildListSnapshot({}, filters); + inventoryCounts = new Map(); + for (const thread of inventory.rows) { + const hostId = thread.location === 'remote' ? thread.hostId ?? 'unknown-remote' : 'local'; + inventoryCounts.set(hostId, (inventoryCounts.get(hostId) ?? 0) + 1); + } + warnings.push(...inventory.warnings); + } catch (error) { + warnings.push(`Merged inventory count failed: ${error instanceof Error ? error.message : String(error)}`); + } + return { + hosts: withCounts.map((host) => { + const counted = { ...host, threadCount: inventoryCounts?.get(host.hostId) + ?? (host.hostId === 'local' ? localThreadCount : host.threadCount) }; + if (!counted.hostName) return counted; + const name = compactThreadTitle(counted.hostName); + return { ...counted, hostName: name.title, + ...(name.titleTruncated ? { hostNameTruncated: true } : {}) }; + }), + hostsSource, + modelProviders, + modelProvidersSource, + backend: appSucceeded + ? (cdpRows.length ? (remotes.length ? 'app-server+cdp+remote-state' : 'app-server+cdp') + : (remotes.length ? 'app-server+remote-state' : 'app-server')) + : (cdpRows.length ? (remotes.length ? 'cdp+remote-state' : 'cdp') : 'remote-state'), + ...(warnings.length ? { warnings } : {}), + }; + } + + async searchThreads(options: SearchThreadsOptions): Promise { + const limit = options.limit ?? 50; + const filters = JSON.stringify({ query: options.query, host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || null, project: options.project?.trim() || null, + groupBy: options.groupBy ?? null }); + let snapshot: ListSnapshot; + let offset = 0; + if (options.cursor) { + const state = decodePageCursor<{ fingerprint: string; offset: number; port: number; filters: string }>( + MERGED_SEARCH_CURSOR, options.cursor); + if (state.port !== this.#port || state.filters !== filters || !Number.isSafeInteger(state.offset) + || state.offset < 1 || typeof state.fingerprint !== 'string') { + throw new Error('ChatGPT Desktop search cursor does not match the port, query or filters'); + } + offset = state.offset; + snapshot = this.#listSnapshots.get(state.fingerprint) ?? await this.#buildSearchSnapshot(options, filters); + if (snapshot.fingerprint !== state.fingerprint || snapshot.filters !== filters || snapshot.port !== this.#port) { + throw new Error('ChatGPT Desktop search results changed; restart without a cursor'); + } + } else { + snapshot = await this.#buildSearchSnapshot(options, filters); + } + return this.#pageSnapshot(snapshot, offset, limit, options, MERGED_SEARCH_CURSOR, options.query); + } + + async #buildSearchSnapshot(options: SearchThreadsOptions, filters: string): Promise { + const modelProviders = modelProvidersFromFilter(options.modelProvider); + const appRows: ChatGptDesktopThread[] = []; + const warnings: string[] = []; + const seenCursors = new Set(); + let appCursor: string | undefined; + let appSucceeded = false; + let appError: unknown; + try { + for (let page = 0; page < 400; page++) { + const out = await this.#fallbacks.searchThreads({ query: options.query, limit: 200, + cursor: appCursor, modelProviders }); + appSucceeded = true; + appRows.push(...out.threads); + if (out.warnings) warnings.push(...out.warnings); + if (!out.nextCursor) break; + if (seenCursors.has(out.nextCursor)) throw new Error('App-server search returned a repeated cursor'); + seenCursors.add(out.nextCursor); + appCursor = out.nextCursor; + if (page === 399) warnings.push('App-server search stopped after 400 pages'); + } + } catch (error) { + appError = error; + warnings.push(`App-server search failed: ${error instanceof Error ? error.message : String(error)}`); + } + const cdpList = await this.#tryCdpList({ limit: 200 }); + const remotes = resolveThreadProjects(this.#listRemotes()); + if (!appSucceeded && !cdpList && !remotes.length) throw appError; + const appList: ListThreadsResult = { backend: 'app-server', limit: appRows.length, threads: appRows }; + const base = appSucceeded + ? (cdpList ? mergeThreadLists(appList, cdpList) : appList) + : (cdpList ?? { backend: 'remote-state' as const, limit: 0, threads: [] }); + const appMatches = new Set(appRows.map((thread) => toAppServerThreadId(thread.threadId) ?? thread.threadId)); + const merged = finalizeThreadList({ ...base, threads: resolveThreadProjects(base.threads) }, { + limit: Number.MAX_SAFE_INTEGER, host: normalizeHostFilter(options.host), + modelProvider: options.modelProvider?.trim() || undefined, project: options.project, + remotes, + }); + const needle = options.query.trim().toLowerCase(); + const rows = [...new Map(merged.threads + .filter((thread) => appMatches.has(toAppServerThreadId(thread.threadId) ?? thread.threadId) + || threadMatchesQuery(thread, needle)) + .map((thread) => [toAppServerThreadId(thread.threadId) ?? thread.threadId, thread])).values()]; + rows.sort(compareDesktopThreads); + const fingerprint = createHash('sha256').update(JSON.stringify({ filters, port: this.#port, + rows: rows.map((row) => [row.threadId, row.updatedAt, row.location, row.hostId]) })).digest('base64url'); + const snapshot: ListSnapshot = { fingerprint, port: this.#port, filters, createdAt: Date.now(), + rows, backend: merged.backend, warnings }; + for (const [key, value] of this.#listSnapshots) { + if (Date.now() - value.createdAt > 15 * 60_000 || this.#listSnapshots.size >= 8) this.#listSnapshots.delete(key); + } + this.#listSnapshots.set(fingerprint, snapshot); + return snapshot; + } + + async readThread(options: { + threadId: string; + limit?: number; + full?: boolean; + cursor?: string; + openTimeoutMs?: number; + }): Promise { + const limit = options.limit ?? (options.full ? 2000 : 100); + const full = options.full ?? false; + + if (isTemporaryDesktopThreadId(options.threadId)) { + if (options.cursor) throw new Error('Temporary Desktop threads do not support read cursors'); + await this.#ensureCdp(); + return this.#cdp.readThread({ ...options, limit, full }); + } + + try { + return await this.#fallbacks.readThread({ + threadId: options.threadId, + limit, + full, + cursor: options.cursor, + }); + } catch (appError) { + if (appError instanceof RemoteThreadNotLoadedError) throw appError; + if (options.cursor) throw appError; + const cdpConnected = await this.#tryEnsureCdp(); + if (!cdpConnected) throw appError; + const fallback = await this.#cdp.readThread({ ...options, limit, full }); + return { ...fallback, complete: false, warnings: [`App-server read failed: ${appError instanceof Error ? appError.message : String(appError)}`] }; + } + } + + async sendMessage(options: { + threadId?: string; + text: string; + project?: string; + unarchive?: boolean; + openTimeoutMs?: number; + }): Promise { + if (options.threadId) { + const remote = findRemoteThread(options.threadId); + if (remote) throw new RemoteThreadNotLoadedError(options.threadId, remote.hostId, remote.hostName); + } + await this.#ensureCdp(); + try { + return await this.#cdp.sendMessage(options); + } catch (error) { + if (!(error instanceof ArchivedThreadError) || !options.unarchive || !options.threadId) throw error; + await (this.#fallbacks.unarchiveThread ?? appServerUnarchiveThread)(options.threadId); + return this.#cdp.sendMessage({ ...options, unarchive: false }); + } + } + + async waitForReply(options: { + threadId?: string; + timeoutMs?: number; + }): Promise { + await this.#ensureCdp(); + return this.#cdp.waitForReply(options); + } + + async openThread( + threadId: string, + options?: { openTimeoutMs?: number }, + ): Promise { + const remote = findRemoteThread(threadId); + if (remote) throw new RemoteThreadNotLoadedError(threadId, remote.hostId, remote.hostName); + await this.#ensureCdp(); + return this.#cdp.openThread(threadId, options); + } + + async status(): Promise { + const cdpStatus = await this.#cdp.status(); + let appServerFallback: ChatGptDesktopStatus['appServerFallback']; + try { + appServerFallback = await this.#fallbacks.statusProbe(); + } catch { + appServerFallback = { reachable: false }; + } + const reachable = cdpStatus.reachable; + return { + ...cdpStatus, + port: this.#port, + appServerFallback, + reachable, + message: cdpStatus.reachable + ? cdpStatus.message + : cdpStatus.message, + exitCode: reachable ? 0 : 1, + }; + } + + async close(): Promise { + this.#cdpConnected = false; + await this.#cdp.close(); + } + + #listRemotes() { + const list = this.#fallbacks.listRemoteThreads ?? listRemoteThreadsFromState; + try { + return list(); + } catch { + return []; + } + } + + async #ensureCdp(): Promise { + if (this.#cdpConnected) return; + await this.#cdp.connect({ port: this.#port }); + this.#cdpConnected = true; + } + + async #tryEnsureCdp(): Promise { + try { + await this.#ensureCdp(); + return true; + } catch (error) { + if (!isCdpFailure(error)) throw error; + this.#cdpConnected = false; + return false; + } + } + + async #tryCdpList(options?: { limit?: number }): Promise { + try { + await this.#ensureCdp(); + return await this.#cdp.listThreads(options); + } catch (error) { + if (isCdpFailure(error) || error instanceof NotImplementedError) { + if (isCdpFailure(error)) this.#cdpConnected = false; + return null; + } + throw error; + } + } +} + +/** Merge app-server thread inventory with CDP-only sidebar fields (selected, etc.). */ +export function mergeThreadLists( + appList: ListThreadsResult, + cdpList: ListThreadsResult, +): ListThreadsResult { + const cdpByKey = new Map(); + for (const thread of cdpList.threads) { + const key = toAppServerThreadId(thread.threadId) ?? thread.threadId; + cdpByKey.set(key, thread); + } + + const merged: ChatGptDesktopThread[] = appList.threads.map((thread) => { + const key = toAppServerThreadId(thread.threadId) ?? thread.threadId; + const cdp = cdpByKey.get(key); + const withLocal: ChatGptDesktopThread = { + ...thread, + location: thread.location ?? 'local', + hostId: thread.hostId ?? null, + hostName: thread.hostName ?? null, + }; + if (!cdp) return withLocal; + return { + ...withLocal, + title: withLocal.title || cdp.title, + pinned: withLocal.pinned || cdp.pinned, + selected: cdp.selected, + kind: cdp.kind || withLocal.kind, + project: cdp.project ?? withLocal.project, + location: cdp.location === 'remote' ? 'remote' : withLocal.location, + hostId: cdp.location === 'remote' ? cdp.hostId ?? null : withLocal.hostId, + hostName: cdp.location === 'remote' ? cdp.hostName ?? null : withLocal.hostName, + }; + }); + + for (const thread of cdpList.threads) { + if (merged.some((row) => threadIdsEquivalent(row.threadId, thread.threadId))) continue; + merged.push(thread); + } + + return { + backend: 'app-server+cdp', + limit: appList.limit, + nextCursor: appList.nextCursor, + ...(appList.query !== undefined ? { query: appList.query } : {}), + threads: merged, + }; +} + +/** Append remote-control summaries, apply host filter / groupBy / limit. */ +export function finalizeThreadList( + base: ListThreadsResult, + { + limit, + host = 'all', + modelProvider, + project, + groupBy, + remotes, + }: { + limit: number; + host?: string; + modelProvider?: string; + project?: string; + groupBy?: 'host'; + remotes: readonly ChatGptDesktopThread[]; + }, +): ListThreadsResult { + const remoteById = new Map(remotes.map((thread) => [ + toAppServerThreadId(thread.threadId) ?? thread.threadId, thread, + ])); + const locals = base.threads.map((thread) => { + const remote = remoteById.get(toAppServerThreadId(thread.threadId) ?? thread.threadId); + if (remote) { + return { ...thread, ...remote, selected: thread.selected, pinned: thread.pinned || remote.pinned }; + } + const location = thread.location ?? ('local' as const); + return { + ...thread, + location, + hostId: location === 'remote' ? thread.hostId ?? null : null, + hostName: location === 'remote' ? thread.hostName ?? null : null, + }; + }); + + const localKeys = new Set( + locals.map((thread) => toAppServerThreadId(thread.threadId) ?? thread.threadId), + ); + const merged: ChatGptDesktopThread[] = [...locals]; + for (const remote of remotes) { + const key = toAppServerThreadId(remote.threadId) ?? remote.threadId; + if (localKeys.has(key)) continue; + merged.push({ + ...remote, + location: remote.location ?? 'remote', + hostId: remote.hostId ?? null, + hostName: remote.hostName ?? null, + }); + localKeys.add(key); + } + if (remotes.length) merged.sort((a, b) => Number(b.location === 'remote') - Number(a.location === 'remote')); + + const filtered = merged.filter((thread) => { + if (!hostMatches(thread, host)) return false; + if (!modelProviderMatches(thread, modelProvider)) return false; + if (!projectMatches(thread, project)) return false; + return true; + }); + const limited = filtered.slice(0, limit).map(compactThreadRow); + const result: ListThreadsResult = { + backend: remotes.length + ? (base.backend === 'app-server+cdp' ? 'app-server+cdp+remote-state' + : base.backend === 'app-server' ? 'app-server+remote-state' + : base.backend === 'cdp' ? 'cdp+remote-state' : base.backend) + : base.backend, + limit, + threads: limited, + ...(base.nextCursor !== undefined ? { nextCursor: base.nextCursor } : {}), + ...(base.query !== undefined ? { query: base.query } : {}), + host, + ...(modelProvider ? { modelProvider } : {}), + ...(project ? { project } : {}), + }; + if (groupBy === 'host') { + return { ...result, groupBy: 'host', groups: groupThreadsByHost(limited) }; + } + return result; +} + +function compareDesktopThreads(a: ChatGptDesktopThread, b: ChatGptDesktopThread): number { + const timestamp = (row: ChatGptDesktopThread) => { + const value = row.updatedAt ?? row.createdAt; + if (typeof value !== 'number' || !Number.isFinite(value)) return Number.NEGATIVE_INFINITY; + return value < 100_000_000_000 ? value * 1000 : value; + }; + return timestamp(b) - timestamp(a) || a.threadId.localeCompare(b.threadId); +} + +export function groupThreadsByHost( + threads: readonly ChatGptDesktopThread[], +): ChatGptDesktopHostGroup[] { + const groups = new Map(); + for (const thread of threads) { + const location = thread.location ?? 'local'; + const hostId = location === 'remote' ? thread.hostId || 'unknown-remote' : 'local'; + const key = `${location}:${hostId}`; + const existing = groups.get(key); + if (existing) { + (existing.threads as ChatGptDesktopThread[]).push(thread); + continue; + } + groups.set(key, { + hostId, + hostName: thread.hostName ?? (location === 'local' ? 'local' : null), + location, + threads: [thread], + }); + } + return [...groups.values()]; +} + +function normalizeHostFilter(host?: string): string { + const value = (host ?? 'all').trim(); + return value || 'all'; +} + +function modelProvidersFromFilter(modelProvider?: string): string[] { + if (!modelProvider || !modelProvider.trim()) return []; + return [modelProvider.trim()]; +} + +function hostMatches(thread: ChatGptDesktopThread, host: string): boolean { + const needle = host.trim().toLowerCase(); + if (!needle || needle === 'all') return true; + if (needle === 'local') return (thread.location ?? 'local') === 'local'; + return [thread.hostId, thread.hostName] + .some((value) => typeof value === 'string' && value.toLowerCase().includes(needle)); +} + +function modelProviderMatches(thread: ChatGptDesktopThread, modelProvider?: string): boolean { + if (!modelProvider) return true; + return thread.modelProvider === modelProvider; +} + +function projectMatches(thread: ChatGptDesktopThread, project?: string): boolean { + if (!project) return true; + const needle = project.trim().toLowerCase(); + return [thread.project, thread.projectId].some((value) => + typeof value === 'string' && value.toLowerCase().includes(needle)); +} + +function threadMatchesQuery(thread: ChatGptDesktopThread, needle: string): boolean { + if (!needle) return true; + return [thread.title, thread.threadId, thread.project, thread.preview, thread.hostName, thread.hostId] + .some((value) => typeof value === 'string' && value.toLowerCase().includes(needle)); +} + +function isCdpFailure(error: unknown): boolean { + if (error instanceof CdpUnreachableError) return true; + if (error instanceof NotImplementedError) return false; + if (error && typeof error === 'object' && 'code' in error) { + const code = String((error as { code: unknown }).code); + return code === 'CDP_UNREACHABLE' || code === 'CDP_HOST_REJECTED'; + } + return false; +} + +let activeAdapter: ChatGptDesktopAdapter | null = null; + +export function getChatGptDesktopAdapter(): ChatGptDesktopAdapter { + if (!activeAdapter) activeAdapter = new ChatGptDesktopFacade(); + return activeAdapter; +} + +/** Test hook: inject a fake adapter (or null to restore the default facade). */ +export function setChatGptDesktopAdapterForTests(adapter: ChatGptDesktopAdapter | null): void { + activeAdapter = adapter; +} diff --git a/src/core/chatgpt-desktop/index.ts b/src/core/chatgpt-desktop/index.ts new file mode 100644 index 0000000..fd787b1 --- /dev/null +++ b/src/core/chatgpt-desktop/index.ts @@ -0,0 +1,115 @@ +export type { ChatGptDesktopAdapter } from './adapter.js'; +export { CdpChatGptDesktopAdapter } from './cdp-adapter.js'; +export { + ASSISTANT_MESSAGE_SELECTOR, + ATTR, + COMPOSER_SELECTOR, + DEFAULT_CONVERSATION_RESOLVE_MS, + DEFAULT_FULL_READ_IDLE_WHEELS, + DEFAULT_FULL_READ_MAX_WHEELS, + DEFAULT_OPEN_TIMEOUT_MS, + FINAL_ASSISTANT_SELECTOR, + FULL_READ_WHEEL_DELTA_Y, + HISTORY_GAP_PREFIX, + IGNORED_TARGET_URL_MARKERS, + LOADING_TASK_TEXT, + MAIN_CONTENT_SURFACE_CLASS_PREFIX, + MAIN_WINDOW_URL, + SELECTORS, + SEND_BUTTON_SELECTOR, + STOP_BUTTON_SELECTOR, + TEMP_THREAD_ID_PREFIX, + dedupeThreadsById, + isMainWindowTarget, + newChatInProjectSelector, + pickMainWindowTarget, + resolveDurableThreadId, + summarizeTargetInfos, +} from './cdp-dom.js'; +export { CdpSession } from './cdp-session.js'; +export { + ArchivedThreadError, + ComposerDraftError, + CdpHostRejectedError, + CdpUnreachableError, + NotImplementedError, + RemoteThreadNotLoadedError, +} from './errors.js'; +export { + ChatGptDesktopFacade, + finalizeThreadList, + getChatGptDesktopAdapter, + groupThreadsByHost, + mergeThreadLists, + setChatGptDesktopAdapterForTests, +} from './facade.js'; +export type { ChatGptDesktopFallbacks, ListThreadsOptions } from './facade.js'; +export { + HISTORY_CONTENT_TURN_PREFIX, + LOCAL_THREAD_ID_PREFIX, + appServerThreadIdCandidates, + isTemporaryDesktopThreadId, + requireAppServerThreadId, + threadIdsEquivalent, + toAppServerThreadId, + toDesktopThreadId, + toDomTurnKey, +} from './thread-ids.js'; +export { + codexGlobalStatePath, + findRemoteThread, + findRemoteThreadHostId, + listDiscoveredHosts, + listRemoteThreadsFromState, + loadCodexGlobalState, +} from './remote-threads.js'; +export { + CDP_LOOPBACK_HOST, + DEFAULT_CDP_PORT, + assertLoopbackHostname, + cdpHttpBase, + forceLoopbackWebSocketUrl, + isLoopbackHostname, + resolveCdpPort, +} from './loopback.js'; +export { + CHATGPT_APP_PATH, + CHATGPT_BUNDLE_ID, + chatgptDesktopRelaunchArgs, + chatgptDesktopRelaunchCommand, + relaunchChatGptDesktopWithCdp, +} from './relaunch.js'; +export { + listHostsOperation, + listHostsSchema, + listThreadsOperation, + listThreadsSchema, + readThreadOperation, + readThreadSchema, + resultSchema, + resultText, + searchThreadsOperation, + searchThreadsSchema, + sendOperation, + sendSchema, + statusOperation, + statusSchema, + waitReplyOperation, + waitReplySchema, +} from './routes.js'; +export type { + ChatGptDesktopBackend, + ChatGptDesktopHostGroup, + ChatGptDesktopStatus, + ChatGptDesktopTarget, + ChatGptDesktopThread, + ChatGptDesktopThreadLocation, + ChatGptDesktopThreadSection, + ChatGptDesktopTurn, + ListHostsResult, + ListThreadsResult, + OpenThreadResult, + ReadThreadResult, + SendMessageResult, + WaitForReplyResult, +} from './types.js'; diff --git a/src/core/chatgpt-desktop/loopback.ts b/src/core/chatgpt-desktop/loopback.ts new file mode 100644 index 0000000..b2b8f41 --- /dev/null +++ b/src/core/chatgpt-desktop/loopback.ts @@ -0,0 +1,65 @@ +import { CdpHostRejectedError } from './errors.js'; + +const LOOPBACK = new Set(['127.0.0.1', 'localhost', '::1']); + +export const DEFAULT_CDP_PORT = 9222; +export const CDP_LOOPBACK_HOST = '127.0.0.1' as const; + +export function isLoopbackHostname(hostname: string): boolean { + const host = String(hostname || '') + .toLowerCase() + .replace(/^\[|\]$/g, ''); + return LOOPBACK.has(host); +} + +/** Reject any non-loopback host. CDP stays local-only with no remote transport. */ +export function assertLoopbackHostname(hostname: string): void { + if (!isLoopbackHostname(hostname)) { + throw new CdpHostRejectedError(hostname); + } +} + +/** + * Build the HTTP base URL for the Chromium debug port. + * Always dials 127.0.0.1 — never a caller-supplied host. + */ +export function cdpHttpBase(port: number): string { + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return `http://${CDP_LOOPBACK_HOST}:${port}`; +} + +/** Rewrite a debugger WebSocket URL so the host is forced to 127.0.0.1. */ +export function forceLoopbackWebSocketUrl(raw: string): string { + let url: URL; + try { + url = new URL(raw); + } catch { + throw new CdpHostRejectedError(String(raw)); + } + assertLoopbackHostname(url.hostname); + if (url.username || url.password || url.hash) throw new CdpHostRejectedError(raw); + if (url.protocol !== 'ws:' && url.protocol !== 'wss:') { + throw new Error(`Expected ws(s) debugger URL, got ${url.protocol}`); + } + url.hostname = CDP_LOOPBACK_HOST; + url.protocol = 'ws:'; + return url.toString(); +} + +export function resolveCdpPort(env: NodeJS.ProcessEnv = process.env, explicit?: number): number { + if (explicit !== undefined) { + if (!Number.isInteger(explicit) || explicit < 1 || explicit > 65535) { + throw new RangeError('CDP port must be an integer 1-65535'); + } + return explicit; + } + const raw = (env.CHATGPT_DESKTOP_CDP_PORT || '').trim(); + if (!raw) return DEFAULT_CDP_PORT; + const port = /^\d+$/.test(raw) ? Number(raw) : NaN; + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new RangeError('CHATGPT_DESKTOP_CDP_PORT must be an integer 1-65535'); + } + return port; +} diff --git a/src/core/chatgpt-desktop/payload-budget.ts b/src/core/chatgpt-desktop/payload-budget.ts new file mode 100644 index 0000000..f4f4cff --- /dev/null +++ b/src/core/chatgpt-desktop/payload-budget.ts @@ -0,0 +1,67 @@ +import type { ChatGptDesktopThread } from './types.js'; + +/** Agent Bundle rejects documents above 1 MiB; leave room for its envelope. */ +export const DESKTOP_RESULT_BUDGET_BYTES = 600 * 1024; +export const DESKTOP_THREAD_TITLE_CHARS = 200; + +export function serializedBytes(value: unknown): number { + return Buffer.byteLength(JSON.stringify(value), 'utf8'); +} + +function compact(value: string, maxChars: number): { value: string; clipped: boolean } { + const normalized = value.replace(/\s+/gu, ' ').trim(); + const chars = Array.from(normalized); + return { value: chars.slice(0, maxChars).join(''), clipped: chars.length > maxChars }; +} + +/** Compact display metadata after search has matched the original full text. */ +export function compactThreadRow(thread: ChatGptDesktopThread): ChatGptDesktopThread { + const title = compact(thread.title, DESKTOP_THREAD_TITLE_CHARS); + const preview = typeof thread.preview === 'string' ? compact(thread.preview, 200) : null; + const cwd = typeof thread.cwd === 'string' ? compact(thread.cwd, 2048) : null; + const project = typeof thread.project === 'string' ? compact(thread.project, 256) : null; + const hostName = typeof thread.hostName === 'string' ? compact(thread.hostName, 256) : null; + return { + ...thread, + title: title.value, + ...(title.clipped ? { titleTruncated: true } : {}), + ...(preview ? { preview: preview.value, ...(preview.clipped ? { previewTruncated: true } : {}) } : {}), + ...(cwd ? { cwd: cwd.value, ...(cwd.clipped ? { cwdTruncated: true } : {}) } : {}), + ...(project ? { project: project.value, ...(project.clipped ? { projectTruncated: true } : {}) } : {}), + ...(hostName ? { hostName: hostName.value, ...(hostName.clipped ? { hostNameTruncated: true } : {}) } : {}), + }; +} + +export function compactThreadTitle(title: string): { title: string; titleTruncated: boolean } { + const out = compact(title, DESKTOP_THREAD_TITLE_CHARS); + return { title: out.value, titleTruncated: out.clipped }; +} + +export function minimalThreadRow(thread: ChatGptDesktopThread): ChatGptDesktopThread { + return { + threadId: thread.threadId, + title: compact(thread.title, DESKTOP_THREAD_TITLE_CHARS).value, + titleTruncated: true, + pinned: thread.pinned, + selected: thread.selected, + kind: thread.kind, + location: thread.location, + hostId: thread.hostId, + hostName: thread.hostName, + modelProvider: thread.modelProvider, + metadataTruncated: true, + }; +} + +export function encodePageCursor(prefix: string, value: object): string { + return prefix + Buffer.from(JSON.stringify(value)).toString('base64url'); +} + +export function decodePageCursor(prefix: string, cursor: string): T { + if (!cursor.startsWith(prefix)) throw new Error('Invalid ChatGPT Desktop page cursor'); + try { + return JSON.parse(Buffer.from(cursor.slice(prefix.length), 'base64url').toString('utf8')) as T; + } catch { + throw new Error('Invalid ChatGPT Desktop page cursor'); + } +} diff --git a/src/core/chatgpt-desktop/project-state.ts b/src/core/chatgpt-desktop/project-state.ts new file mode 100644 index 0000000..912db38 --- /dev/null +++ b/src/core/chatgpt-desktop/project-state.ts @@ -0,0 +1,88 @@ +/** Resolve Desktop project labels from its persisted project/assignment state. */ +import { codexStateEntries, loadCodexGlobalState } from '../codex/remote-control.js'; +import { toAppServerThreadId } from './thread-ids.js'; +import type { ChatGptDesktopThread } from './types.js'; + +type Project = { id: string; label: string; rootPath: string; hostId?: string }; + +export function findLocalProject( + nameOrId: string, + env: NodeJS.ProcessEnv = process.env, +): { id: string; label: string; rootPaths: string[] } | null { + const state = loadCodexGlobalState(env); + if (!state) return null; + for (const value of Object.values(record(stateValue(state, 'local-projects')) ?? {})) { + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') continue; + if (row.id !== nameOrId && row.name.toLowerCase() !== nameOrId.toLowerCase()) continue; + return { id: row.id, label: row.name, + rootPaths: (Array.isArray(row.rootPaths) ? row.rootPaths : []) + .filter((path): path is string => typeof path === 'string' && Boolean(path)) }; + } + return null; +} + +function record(value: unknown): Record | null { + return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; +} + +function stateValue(state: Record, key: string): unknown { + for (const source of codexStateEntries(state)) if (key in source) return source[key]; + return undefined; +} + +function pathWithin(path: string, root: string): boolean { + const normalized = root.replace(/\/+$/u, ''); + return path === normalized || path.startsWith(`${normalized}/`); +} + +export function resolveThreadProjects( + threads: readonly ChatGptDesktopThread[], + env: NodeJS.ProcessEnv = process.env, +): ChatGptDesktopThread[] { + const state = loadCodexGlobalState(env); + if (!state) return [...threads]; + const localProjects = Object.values(record(stateValue(state, 'local-projects')) ?? {}) + .flatMap((value): Project[] => { + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') return []; + return (Array.isArray(row.rootPaths) ? row.rootPaths : []) + .filter((path): path is string => typeof path === 'string' && Boolean(path)) + .map((rootPath) => ({ id: row.id as string, label: row.name as string, rootPath })); + }); + const remoteProjects = (Array.isArray(stateValue(state, 'remote-projects')) + ? stateValue(state, 'remote-projects') as unknown[] : []).flatMap((value): Project[] => { + const row = record(value); + if (!row || typeof row.id !== 'string' || typeof row.label !== 'string' + || typeof row.remotePath !== 'string' || typeof row.hostId !== 'string') return []; + return [{ id: row.id, label: row.label, rootPath: row.remotePath, hostId: row.hostId }]; + }); + const assignments = record(stateValue(state, 'thread-project-assignments')) ?? {}; + const hints = record(stateValue(state, 'thread-workspace-root-hints')) ?? {}; + const projectless = new Set(Array.isArray(stateValue(state, 'projectless-thread-ids')) + ? stateValue(state, 'projectless-thread-ids') as string[] : []); + return threads.map((thread) => { + const bare = toAppServerThreadId(thread.threadId) ?? thread.threadId; + const assignment = record(assignments[bare]); + const remote = thread.location === 'remote'; + const projects = remote ? remoteProjects.filter((project) => project.hostId === thread.hostId) : localProjects; + let project = assignment && typeof assignment.projectId === 'string' + ? projects.find((entry) => entry.id === assignment.projectId) : undefined; + if (!project && !projectless.has(bare) && thread.projectId) { + project = projects.find((entry) => entry.id === thread.projectId); + } + if (!project && !assignment && !projectless.has(bare)) { + const path = typeof hints[bare] === 'string' ? hints[bare] as string + : typeof thread.cwd === 'string' ? thread.cwd : null; + if (path) project = projects.filter((entry) => pathWithin(path, entry.rootPath)) + .sort((a, b) => b.rootPath.length - a.rootPath.length)[0]; + } + if (project) return { ...thread, project: project.label, projectId: project.id, + projectRootPath: project.rootPath }; + if (projectless.has(bare)) return { ...thread, project: null, projectId: null }; + if (remote && typeof thread.project === 'string' && thread.project.startsWith('/')) { + return { ...thread, project: null }; + } + return thread; + }); +} diff --git a/src/core/chatgpt-desktop/relaunch.ts b/src/core/chatgpt-desktop/relaunch.ts new file mode 100644 index 0000000..fda4fbe --- /dev/null +++ b/src/core/chatgpt-desktop/relaunch.ts @@ -0,0 +1,77 @@ +import { spawnSync } from 'node:child_process'; + +import { DEFAULT_CDP_PORT, resolveCdpPort } from './loopback.js'; + +export const CHATGPT_APP_PATH = '/Applications/ChatGPT.app'; +export const CHATGPT_BUNDLE_ID = 'com.openai.codex'; + +/** + * Shell command that quits ChatGPT Desktop and relaunches it with a local + * Chromium `--remote-debugging-port`. Electron fuses block Node inspect / + * RunAsNode, but not this Chromium flag. The port binds to 127.0.0.1 only. + * Do not depend on `@electron/fuses read` — the framework is renamed + * (`Codex Framework.framework`) and that tool fails. + */ +export function chatgptDesktopRelaunchCommand({ + port = DEFAULT_CDP_PORT, + appPath = CHATGPT_APP_PATH, +}: { + port?: number; + appPath?: string; +} = {}): string { + const resolved = resolveCdpPort(process.env, port); + // Match explored launch: quit, then `open -a … --args --remote-debugging-port=N`. + return [ + `osascript -e 'tell application "ChatGPT" to quit'`, + `open -a ${shellQuote(appPath)} --args --remote-debugging-port=${resolved}`, + ].join(' && '); +} + +export function chatgptDesktopRelaunchArgs(port = DEFAULT_CDP_PORT): string[] { + return ['--remote-debugging-port=' + resolveCdpPort(process.env, port)]; +} + +/** + * Best-effort macOS relaunch. Returns the command that was (or would be) run. + * Non-darwin platforms return `{ ran: false }` with the command for operators. + */ +export function relaunchChatGptDesktopWithCdp({ + port = DEFAULT_CDP_PORT, + appPath = CHATGPT_APP_PATH, + dryRun = false, + runner = spawnSync, +}: { + port?: number; + appPath?: string; + dryRun?: boolean; + runner?: typeof spawnSync; +} = {}): { + command: string; + ran: boolean; + platform: string; + port: number; + stdout?: string; + stderr?: string; + status?: number | null; +} { + const resolved = resolveCdpPort(process.env, port); + const command = chatgptDesktopRelaunchCommand({ port: resolved, appPath }); + if (dryRun || process.platform !== 'darwin') { + return { command, ran: false, platform: process.platform, port: resolved }; + } + const result = runner('bash', ['-lc', command], { encoding: 'utf8' }); + return { + command, + ran: true, + platform: process.platform, + port: resolved, + stdout: result.stdout || '', + stderr: result.stderr || '', + status: result.status, + }; +} + +function shellQuote(value: string): string { + if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value; + return `'${value.replace(/'/g, `'\\''`)}'`; +} diff --git a/src/core/chatgpt-desktop/remote-threads.ts b/src/core/chatgpt-desktop/remote-threads.ts new file mode 100644 index 0000000..5022ecb --- /dev/null +++ b/src/core/chatgpt-desktop/remote-threads.ts @@ -0,0 +1,371 @@ +/** + * Remote-control thread summaries from ChatGPT Desktop's + * `~/.codex/.codex-global-state.json`. + * + * Keys are shaped like `remote-thread-summaries-v3:` and may hold + * titles, ids, remote project info, and pinned ids. The file is app-internal + * and may change — parse defensively and ignore unknown shapes. + * + * SSH remoting is intentionally not implemented here (follow-up). + */ + +import { codexStateEntries, loadCodexGlobalState } from '../codex/remote-control.js'; +export { codexGlobalStatePath, loadCodexGlobalState } from '../codex/remote-control.js'; + +import { toAppServerThreadId, toDesktopThreadId } from './thread-ids.js'; +import type { ChatGptDesktopThread } from './types.js'; + +const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + +export type RemoteHostInfo = { + readonly hostId: string; + readonly hostName: string | null; +}; + +export type RemoteThreadRecord = ChatGptDesktopThread & { + readonly location: 'remote'; + readonly hostId: string; + readonly hostName: string | null; +}; + +export type RemoteThreadLookup = { + readonly hostId: string; + readonly hostName: string | null; + readonly thread: RemoteThreadRecord | null; +}; + +/** + * Parse every `remote-thread-summaries-v3:` entry into flat remote + * thread rows. Unknown/malformed entries are skipped. + */ +export function listRemoteThreadsFromState( + env: NodeJS.ProcessEnv = process.env, +): RemoteThreadRecord[] { + const data = loadCodexGlobalState(env); + if (!data) return []; + const hostNames = collectHostNames(data); + const out: RemoteThreadRecord[] = []; + const seen = new Set(); + + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)) { + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value); + const pinnedIds = extractPinnedIds(value); + for (const entry of extractThreadEntries(value)) { + const mapped = mapRemoteEntry(entry, { hostId, hostName, pinnedIds }); + if (!mapped) continue; + const bare = toAppServerThreadId(mapped.threadId) ?? mapped.threadId; + const dedupe = `${hostId}:${bare}`; + if (seen.has(dedupe)) continue; + seen.add(dedupe); + out.push(mapped); + } + } + return out; +} + +/** Best-effort hostId (+ optional summary row) for a thread under remote summaries. */ +export function findRemoteThread( + threadId: string, + env: NodeJS.ProcessEnv = process.env, +): RemoteThreadLookup | null { + const bare = toAppServerThreadId(threadId) ?? threadId; + if (!bare) return null; + for (const thread of listRemoteThreadsFromState(env)) { + const id = toAppServerThreadId(thread.threadId) ?? thread.threadId; + if (id === bare) { + return { hostId: thread.hostId, hostName: thread.hostName, thread }; + } + } + return null; +} + +/** Best-effort hostId for a thread listed under a remote-control summary. */ +export function findRemoteThreadHostId( + threadId: string, + env: NodeJS.ProcessEnv = process.env, +): string | null { + return findRemoteThread(threadId, env)?.hostId ?? null; +} + +export type DiscoveredHost = { + readonly hostId: string; + readonly hostName: string | null; + readonly location: 'local' | 'remote'; + readonly threadCount: number; +}; + +/** + * Discover hosts dynamically: always include `local`, plus every + * `remote-thread-summaries-v3:` key (with friendly names / counts). + */ +export function listDiscoveredHosts( + env: NodeJS.ProcessEnv = process.env, + { + localThreadCount = 0, + }: { + localThreadCount?: number; + } = {}, +): DiscoveredHost[] { + const remotes = listRemoteThreadsFromState(env); + const byHost = new Map(); + byHost.set('local', { + hostId: 'local', + hostName: 'local', + location: 'local', + threadCount: localThreadCount, + }); + + const data = loadCodexGlobalState(env); + if (data) { + const hostNames = collectHostNames(data); + for (const [key, summary] of codexStateEntries(data).flatMap(Object.entries)) { + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(summary) ?? null; + if (byHost.has(hostId)) continue; + byHost.set(hostId, { + hostId, + hostName, + location: 'remote', + threadCount: 0, + }); + } + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue; + const hostId = stringField(entry, ['hostId']); + if (hostId && !byHost.has(hostId)) byHost.set(hostId, { + hostId, hostName: stringField(entry, ['displayName', 'alias']), location: 'remote', threadCount: 0, + }); + } + } + + for (const thread of remotes) { + const existing = byHost.get(thread.hostId); + if (existing) { + byHost.set(thread.hostId, { + ...existing, + hostName: existing.hostName ?? thread.hostName, + threadCount: existing.threadCount + 1, + }); + } else { + byHost.set(thread.hostId, { + hostId: thread.hostId, + hostName: thread.hostName, + location: 'remote', + threadCount: 1, + }); + } + } + + return [...byHost.values()].sort((a, b) => { + if (a.hostId === 'local') return -1; + if (b.hostId === 'local') return 1; + return a.hostId.localeCompare(b.hostId); + }); +} + +function collectHostNames(data: Record): Map { + const names = new Map(); + const connections = data['codex-managed-remote-connections']; + if (Array.isArray(connections)) for (const entry of connections) { + const pair = hostNameFromEntry(entry); + if (pair) names.set(pair.hostId, pair.hostName); + } + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)) { + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value) { + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value as Record)) { + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, ['name', 'hostName', 'displayName', 'envName', 'label', 'title']); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; +} + +function hostNameFromEntry(entry: unknown): { hostId: string; hostName: string } | null { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry as Record; + const hostId = stringField(row, ['hostId', 'id', 'host_id', 'uuid']); + const hostName = stringField(row, ['name', 'hostName', 'displayName', 'envName', 'label', 'title']); + if (!hostId || !hostName) return null; + return { hostId, hostName }; +} + +function extractHostName(value: unknown): string | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value as Record, [ + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + ]); +} + +function extractPinnedIds(value: unknown): Set { + const pinned = new Set(); + if (!value || typeof value !== 'object') return pinned; + const record = value as Record; + for (const key of ['pinnedIds', 'pinned', 'pinnedThreadIds', 'pinned_ids']) { + const arr = record[key]; + if (!Array.isArray(arr)) continue; + for (const item of arr) { + if (typeof item === 'string' && item) pinned.add(item); + else if (item && typeof item === 'object') { + const id = stringField(item as Record, ['id', 'threadId', 'thread_id']); + if (id) pinned.add(id); + } + } + } + return pinned; +} + +function extractThreadEntries(value: unknown): unknown[] { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value as Record; + for (const key of ['threads', 'summaries', 'items', 'data', 'conversations']) { + if (Array.isArray(record[key])) return record[key] as unknown[]; + } + // Object map keyed by thread id → treat values (and string keys) as entries. + const entries: unknown[] = []; + for (const [key, entry] of Object.entries(record)) { + if ( + [ + 'pinnedIds', + 'pinned', + 'pinnedThreadIds', + 'hostName', + 'displayName', + 'name', + 'envName', + 'label', + 'title', + 'friendlyName', + 'hostId', + 'version', + ].includes(key) + ) { + continue; + } + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const row = entry as Record; + if (!stringField(row, ['id', 'threadId', 'thread_id', 'conversationId'])) { + entries.push({ ...row, id: key }); + } else { + entries.push(entry); + } + } + } + return entries; +} + +function mapRemoteEntry( + entry: unknown, + { + hostId, + hostName, + pinnedIds, + }: { + hostId: string; + hostName: string | null; + pinnedIds: Set; + }, +): RemoteThreadRecord | null { + if (typeof entry === 'string' && entry) { + return { + threadId: toDesktopThreadId(entry), + title: entry, + pinned: pinnedIds.has(entry), + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName, + }; + } + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const row = entry as Record; + const id = stringField(row, ['id', 'threadId', 'thread_id', 'conversationId']); + if (!id) return null; + const title = + stringField(row, ['title', 'name', 'preview', 'label']) || id; + const projectId = stringField(row, ['projectId', 'project_id']); + const project = + stringField(row, ['project', 'projectName', 'project_name', 'cwd']) || projectId || undefined; + const preview = stringField(row, ['preview', 'snippet']); + const cwd = stringField(row, ['cwd', 'workingDirectory', 'working_directory']); + const modelProvider = stringField(row, ['modelProvider', 'model_provider', 'provider']); + const model = stringField(row, ['model']); + const originator = stringField(row, ['originator', 'source']); + const createdAt = numberField(row, ['createdAt', 'created_at']); + const updatedAt = numberField(row, ['updatedAt', 'updated_at']); + const sectionName = stringField(row, ['sectionName', 'section']); + const sectionId = stringField(row, ['sectionId', 'section_id']) || sectionName; + const pinned = + pinnedIds.has(id) || + row.pinned === true || + sectionId === 'Pinned' || + sectionName === 'Pinned'; + + return { + threadId: toDesktopThreadId(id), + title, + pinned, + selected: false, + kind: 'remote', + location: 'remote', + hostId, + hostName, + ...(project ? { project } : {}), + ...(projectId ? { projectId } : {}), + ...(preview ? { preview } : {}), + ...(cwd ? { cwd } : { cwd: null }), + createdAt: createdAt ?? null, + updatedAt: updatedAt ?? null, + ...(sectionId + ? { section: { id: sectionId, name: sectionName ?? sectionId } } + : { section: null }), + ...(modelProvider ? { modelProvider } : { modelProvider: null }), + ...(model ? { model } : { model: null }), + ...(originator ? { originator } : { originator: null }), + }; +} + +function stringField(row: Record | unknown, keys: string[]): string | null { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + const record = row as Record; + for (const key of keys) { + const value = record[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; +} + +function numberField(row: Record, keys: string[]): number | null { + for (const key of keys) { + const value = row[key]; + if (typeof value === 'number' && Number.isFinite(value)) return value; + } + return null; +} diff --git a/src/core/chatgpt-desktop/routes.ts b/src/core/chatgpt-desktop/routes.ts new file mode 100644 index 0000000..d87d75a --- /dev/null +++ b/src/core/chatgpt-desktop/routes.ts @@ -0,0 +1,339 @@ +import { z } from 'zod'; + +import { outcomeFromError } from '../codex/contract.js'; +import { NotImplementedError, RemoteThreadNotLoadedError } from './errors.js'; +import { getChatGptDesktopAdapter } from './facade.js'; +import { resolveCdpPort } from './loopback.js'; + +const threadId = z.string().min(1).max(256); + +export const statusSchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + }) + .strict(); + +export const listThreadsSchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + limit: z.number().int().min(1).max(200).default(50), + cursor: z.string().min(1).max(4096).optional(), + /** + * Any string. Reserved: `all` (default), `local`. Otherwise a hostId or + * friendly name discovered at runtime — see `chatgpt_desktop_list_hosts`. + * Not an enum; new machines appear with no code change. + */ + host: z.string().min(1).max(256).optional(), + /** + * Any modelProvider id string (passed through as app-server + * `modelProviders`). Omit for all. Discovered values: see + * `chatgpt_desktop_list_hosts`. Not an enum. + */ + modelProvider: z.string().min(1).max(256).optional(), + project: z.string().min(1).max(256).optional(), + groupBy: z.literal('host').optional(), + }) + .strict(); + +export const searchThreadsSchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + query: z.string().min(1).max(512), + limit: z.number().int().min(1).max(200).default(50), + cursor: z.string().min(1).max(4096).optional(), + /** Any string; see `chatgpt_desktop_list_hosts`. Not an enum. */ + host: z.string().min(1).max(256).optional(), + /** Any modelProvider id; see `chatgpt_desktop_list_hosts`. Not an enum. */ + modelProvider: z.string().min(1).max(256).optional(), + project: z.string().min(1).max(256).optional(), + groupBy: z.literal('host').optional(), + }) + .strict(); + +export const listHostsSchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + }) + .strict(); + +export const readThreadSchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + threadId, + limit: z.number().int().min(1).max(2000).optional(), + full: z.boolean().default(false), + cursor: z.string().min(1).max(4096).optional(), + openTimeoutMs: z.number().int().min(1).max(600_000).default(90_000), + }) + .strict(); + +export const openThreadSchema = z.object({ + port: z.number().int().min(1).max(65535).optional(), + threadId, + openTimeoutMs: z.number().int().min(1).max(600_000).default(90_000), +}).strict(); + +export const sendSchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + text: z.string().trim().min(1).max(100_000), + project: z.string().min(1).max(256).optional(), + unarchive: z.boolean().default(false), + openTimeoutMs: z.number().int().min(1).max(600_000).default(90_000), + }) + .strict() + .refine((value) => !value.unarchive || Boolean(value.threadId), { + message: 'unarchive requires an existing threadId', path: ['unarchive'], + }) + .refine((value) => !value.threadId || !value.project, { + message: 'project is only for a new thread; omit threadId', path: ['project'], + }); + +export const waitReplySchema = z + .object({ + port: z.number().int().min(1).max(65535).optional(), + threadId: threadId.optional(), + timeoutMs: z.number().int().min(1).max(600_000).default(120_000), + }) + .strict(); + +export const resultSchema = z + .object({ + exitCode: z.union([z.literal(0), z.literal(1)]), + }) + .catchall(z.json()); + +export type OperationResult = z.infer; + +// The Desktop has one selected conversation and composer. Keep route calls +// from closing each other's sessions or typing into each other's chats. +let operationQueue: Promise = Promise.resolve(); + +async function withAdapter( + port: number | undefined, + run: (adapter: ReturnType) => Promise, +): Promise { + const operation = operationQueue.then(async () => { + const adapter = getChatGptDesktopAdapter(); + const resolved = resolveCdpPort(process.env, port); + try { + try { + await adapter.connect({ port: resolved }); + } catch { + // Reads may still succeed through the app-server. + } + return await run(adapter); + } finally { + await adapter.close(); + } + }); + operationQueue = operation.catch(() => {}); + return operation; +} + +function asResult(value: object): OperationResult { + // Match the wire representation, including optional fields nested in turns. + return resultSchema.parse(JSON.parse(JSON.stringify(value))); +} + +function mapError(error: unknown): OperationResult { + if (error instanceof NotImplementedError) { + return asResult({ + delivery: 'rejected', + reason: 'not-implemented', + code: 'NOT_IMPLEMENTED', + error: error.message, + exitCode: 1 as const, + }); + } + if (error instanceof RemoteThreadNotLoadedError) { + return asResult({ + delivery: error.delivery, + reason: error.reason, + code: error.code, + error: error.message, + threadId: error.threadId, + hint: error.hint, + ...(error.hostId ? { hostId: error.hostId } : {}), + ...(error.hostName ? { hostName: error.hostName } : {}), + exitCode: 1 as const, + }); + } + const base = outcomeFromError(error); + if (error && typeof error === 'object' && 'code' in error) { + return asResult({ ...base, code: String((error as { code: unknown }).code) }); + } + return asResult(base); +} + +export async function statusOperation(input: z.infer): Promise { + try { + const status = await withAdapter(input.port, (adapter) => adapter.status()); + return asResult(status); + } catch (error) { + return mapError(error); + } +} + +export async function listThreadsOperation( + input: z.infer, +): Promise { + try { + const out = await withAdapter(input.port, (adapter) => + adapter.listThreads({ + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy, + }), + ); + return asResult({ ...out, exitCode: 0 as const }); + } catch (error) { + return mapError(error); + } +} + +export async function searchThreadsOperation( + input: z.infer, +): Promise { + try { + const out = await withAdapter(input.port, async (adapter) => { + if (typeof adapter.searchThreads !== 'function') { + throw new NotImplementedError('searchThreads', 'adapter does not implement search'); + } + return adapter.searchThreads({ + query: input.query, + limit: input.limit, + cursor: input.cursor, + host: input.host, + modelProvider: input.modelProvider, + project: input.project, + groupBy: input.groupBy, + }); + }); + return asResult({ ...out, exitCode: 0 as const }); + } catch (error) { + return mapError(error); + } +} + +export async function listHostsOperation( + input: z.infer, +): Promise { + try { + const out = await withAdapter(input.port, async (adapter) => { + if (typeof adapter.listHosts !== 'function') { + throw new NotImplementedError('listHosts', 'adapter does not implement listHosts'); + } + return adapter.listHosts(); + }); + return asResult({ ...out, exitCode: 0 as const }); + } catch (error) { + return mapError(error); + } +} + +export async function readThreadOperation( + input: z.infer, +): Promise { + try { + const out = await withAdapter(input.port, (adapter) => + adapter.readThread({ + threadId: input.threadId, + limit: input.limit ?? (input.full ? 2000 : 100), + full: input.full, + cursor: input.cursor, + openTimeoutMs: input.openTimeoutMs, + }), + ); + return asResult({ ...out, exitCode: 0 as const }); + } catch (error) { + return mapError(error); + } +} + +export async function openThreadOperation(input: z.infer): Promise { + try { + const out = await withAdapter(input.port, (adapter) => adapter.openThread(input.threadId, { openTimeoutMs: input.openTimeoutMs })); + return asResult({ ...out, exitCode: 0 as const }); + } catch (error) { + return mapError(error); + } +} + +export async function sendOperation(input: z.infer): Promise { + try { + input = sendSchema.parse(input); + const out = await withAdapter(input.port, (adapter) => + adapter.sendMessage({ + threadId: input.threadId, + text: input.text, + project: input.project, + unarchive: input.unarchive, + openTimeoutMs: input.openTimeoutMs, + }), + ); + return asResult({ + ...out, + exitCode: out.delivery === 'accepted' ? (0 as const) : (1 as const), + }); + } catch (error) { + return mapError(error); + } +} + +export async function waitReplyOperation( + input: z.infer, +): Promise { + try { + const out = await withAdapter(input.port, (adapter) => + adapter.waitForReply({ threadId: input.threadId, timeoutMs: input.timeoutMs }), + ); + return asResult({ + ...out, + exitCode: out.delivery === 'replied' ? (0 as const) : (1 as const), + }); + } catch (error) { + return mapError(error); + } +} + +export function resultText(result: OperationResult): string { + if (result.error) return String(result.error); + if (result.reachable === true) { + const backend = result.appServerFallback && typeof result.appServerFallback === 'object' + ? ` cdp + app-server probe` + : ''; + return result.message ? String(result.message) : `ChatGPT Desktop CDP reachable on 127.0.0.1:${result.port}${backend}`; + } + if (result.reachable === false) return String(result.message ?? 'ChatGPT Desktop CDP unreachable'); + if (Array.isArray(result.hosts)) { + const providers = Array.isArray(result.modelProviders) + ? ` providers=${result.modelProviders.length} (${String(result.modelProvidersSource ?? '')})` + : ''; + return `${result.hosts.length} hosts via ${String(result.hostsSource ?? result.backend ?? 'unknown')}${providers}`; + } + if (Array.isArray(result.threads)) { + const via = result.query ? ` matching ${JSON.stringify(result.query)}` : ''; + const host = result.host && result.host !== 'all' ? ` host=${result.host}` : ''; + const provider = result.modelProvider ? ` provider=${result.modelProvider}` : ''; + return `${result.threads.length} threads${via}${host}${provider} via ${result.backend ?? 'unknown'}`; + } + if (Array.isArray(result.turns)) { + const mode = result.full ? 'full' : 'page'; + return `${result.turns.length} turns (${mode}) on ${result.threadId} via ${result.backend ?? 'unknown'}`; + } + if (result.delivery === 'accepted') { + return `Sent on ${result.threadId} via ${result.backend}` + + (result.conversationId ? ` (conversation ${result.conversationId})` : ''); + } + if (result.delivery === 'replied') { + const id = result.conversationId ? ` [${result.conversationId}]` : ''; + return `${String(result.reply ?? 'replied')}${id}`; + } + if (result.delivery) return `delivery ${result.delivery}`; + return JSON.stringify(result); +} diff --git a/src/core/chatgpt-desktop/thread-ids.ts b/src/core/chatgpt-desktop/thread-ids.ts new file mode 100644 index 0000000..bcaf880 --- /dev/null +++ b/src/core/chatgpt-desktop/thread-ids.ts @@ -0,0 +1,84 @@ +/** + * Desktop sidebar thread ids ↔ Codex app-server thread ids. + * + * ChatGPT Desktop sidebar rows use `data-app-action-sidebar-thread-id` values + * shaped like `local:`. App-server APIs use the bare + * `` only — the `local:` form fails with `invalid thread id`. + * + * Temporary composer rows (`local:client-new-thread:…`) are CDP-only until the + * first reply annotates a real conversation id — they do not map to app-server. + */ + +import { + LOCAL_THREAD_ID_PREFIX as SHARED_LOCAL_PREFIX, + TEMP_THREAD_ID_PREFIX as SHARED_TEMP_PREFIX, + isTemporaryDesktopThreadId as sharedIsTemporary, + normalizeCodexThreadId, +} from '../codex/thread-id.js'; +import { TEMP_THREAD_ID_PREFIX as CDP_TEMP_PREFIX } from './cdp-dom.js'; + +export const LOCAL_THREAD_ID_PREFIX = SHARED_LOCAL_PREFIX; +export const TEMP_THREAD_ID_PREFIX = CDP_TEMP_PREFIX; + +/** DOM `data-turn-key` prefix; app-server turn ids are the trailing segment. */ +export const HISTORY_CONTENT_TURN_PREFIX = 'history-content:turn:' as const; + +/** True when the id is a transient sidebar row, not a durable conversation. */ +export function isTemporaryDesktopThreadId(threadId: string): boolean { + return sharedIsTemporary(threadId) || threadId.startsWith(SHARED_TEMP_PREFIX); +} + +/** + * Map a Desktop sidebar id (or bare id) to the app-server thread id. + * Returns null when the id is temporary or empty (no app-server mapping). + */ +export function toAppServerThreadId(threadId: string): string | null { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return null; + try { + return normalizeCodexThreadId(threadId); + } catch { + return null; + } +} + +/** Require a bare app-server id; throws when the input cannot be mapped. */ +export function requireAppServerThreadId(threadId: string): string { + const bare = toAppServerThreadId(threadId); + if (!bare) { + throw new Error( + `No app-server thread id mapping for ${JSON.stringify(threadId)} ` + + `(temporary Desktop rows need CDP; strip a leading "local:" for durable ids)`, + ); + } + return bare; +} + +/** Present an app-server (or bare) id in Desktop sidebar form. */ +export function toDesktopThreadId(threadId: string): string { + if (!threadId || isTemporaryDesktopThreadId(threadId)) return threadId; + if (threadId.startsWith(LOCAL_THREAD_ID_PREFIX)) return threadId; + return `${LOCAL_THREAD_ID_PREFIX}${threadId}`; +} + +/** Compare Desktop and app-server id forms as the same conversation. */ +export function threadIdsEquivalent(a: string, b: string): boolean { + const left = toAppServerThreadId(a) ?? a; + const right = toAppServerThreadId(b) ?? b; + return left === right; +} + +/** Format an app-server turn id as the DOM `data-turn-key` value. */ +export function toDomTurnKey(turnId: string): string { + if (!turnId) return turnId; + if (turnId.startsWith(HISTORY_CONTENT_TURN_PREFIX)) return turnId; + return `${HISTORY_CONTENT_TURN_PREFIX}${turnId}`; +} + +/** + * @deprecated Prefer requireAppServerThreadId — the `local:` form is rejected + * by app-server (`invalid thread id`). Kept for callers that want a list. + */ +export function appServerThreadIdCandidates(threadId: string): string[] { + const bare = toAppServerThreadId(threadId); + return bare ? [bare] : []; +} diff --git a/src/core/chatgpt-desktop/types.ts b/src/core/chatgpt-desktop/types.ts new file mode 100644 index 0000000..e807c4b --- /dev/null +++ b/src/core/chatgpt-desktop/types.ts @@ -0,0 +1,183 @@ +export type ChatGptDesktopBackend = 'cdp' | 'app-server' | 'app-server+cdp' | 'app-server+remote-state' | 'app-server+cdp+remote-state' | 'cdp+remote-state' | 'remote-state'; + +export type ChatGptDesktopTarget = { + readonly targetId: string; + readonly type: string; + readonly title: string; + readonly titleTruncated?: boolean; + readonly url: string; + readonly attached: boolean; +}; + +export type ChatGptDesktopThreadLocation = 'local' | 'remote'; + +export type ChatGptDesktopThreadSection = { + readonly id: string; + readonly name: string | null; +}; + +export type ChatGptDesktopThread = { + readonly threadId: string; + readonly title: string; + readonly titleTruncated?: boolean; + readonly pinned: boolean; + /** CDP-only: currently selected sidebar row. */ + readonly selected: boolean; + readonly kind: string; + /** Project folder name when known (CDP sidebar or app-server projectId). */ + readonly project?: string | null; + readonly projectRootPath?: string; + readonly preview?: string; + readonly previewTruncated?: boolean; + readonly cwd?: string | null; + readonly cwdTruncated?: boolean; + readonly createdAt?: number | null; + readonly updatedAt?: number | null; + readonly section?: ChatGptDesktopThreadSection | null; + readonly projectId?: string | null; + readonly status?: string; + readonly modelProvider?: string | null; + readonly model?: string | null; + readonly originator?: string | null; + /** Whether the thread is on the local app-server or a remote-control host. */ + readonly location?: ChatGptDesktopThreadLocation; + /** Remote-control host id when `location` is `remote`; null for local. */ + readonly hostId?: string | null; + /** Friendly host / env name from Desktop global state when known. */ + readonly hostName?: string | null; + readonly projectTruncated?: boolean; + readonly hostNameTruncated?: boolean; + readonly metadataTruncated?: boolean; +}; + +export type ChatGptDesktopHostGroup = { + readonly hostId: string; + readonly hostName: string | null; + readonly location: ChatGptDesktopThreadLocation; + readonly threads: readonly ChatGptDesktopThread[]; +}; + +export type ChatGptDesktopTurn = { + /** Prefer DOM form `history-content:turn:` when the bare turn id is known. */ + readonly turnKey: string; + readonly role: 'user' | 'assistant' | 'status' | 'tool' | 'reasoning'; + readonly text: string; + /** User bubble text when the turn holds both sides. */ + readonly userText?: string; + /** Final assistant markdown when present on the same turn. */ + readonly assistantText?: string; + /** A large turn is split into ordered fragments sharing this stable turnKey. */ + readonly textTruncated?: boolean; + readonly continuation?: { + readonly field: 'text' | 'userText' | 'assistantText'; + readonly offsetChars: number; + readonly totalChars: number; + readonly fieldComplete: boolean; + readonly turnComplete: boolean; + }; + readonly startedAt?: number | null; + readonly endedAt?: number | null; +}; + +export type ChatGptDesktopStatus = { + readonly reachable: boolean; + readonly port: number; + readonly host: '127.0.0.1'; + readonly browser?: string; + readonly protocolVersion?: string; + readonly webSocketDebuggerUrl?: string; + readonly targetCount?: number; + readonly mainWindowTargetId?: string | null; + readonly appServerFallback?: { + readonly reachable: boolean; + readonly mode?: string; + readonly socketPath?: string; + }; + readonly message: string; + readonly exitCode: 0 | 1; +}; + +export type ListThreadsResult = { + readonly threads: readonly ChatGptDesktopThread[]; + readonly backend: ChatGptDesktopBackend; + readonly limit: number; + readonly nextCursor?: string | null; + readonly warnings?: readonly string[]; + readonly query?: string; + /** Host filter applied: any string (`all` | `local` | hostId / friendly name from list_hosts). */ + readonly host?: string; + /** Model-provider filter (any string; see list_hosts). Passed through to app-server `modelProviders`. */ + readonly modelProvider?: string; + readonly project?: string; + readonly groupBy?: 'host'; + /** Present when `groupBy: "host"`. */ + readonly groups?: readonly ChatGptDesktopHostGroup[]; +}; + +export type ListHostsResult = { + readonly hosts: readonly { + readonly hostId: string; + readonly hostName: string | null; + readonly hostNameTruncated?: boolean; + readonly location: ChatGptDesktopThreadLocation; + readonly threadCount: number; + }[]; + /** How hosts were discovered (e.g. remote-thread-summaries-v3+local). */ + readonly hostsSource: string; + readonly modelProviders: readonly string[]; + /** How model providers were discovered (app-server method or thread/list-distinct). */ + readonly modelProvidersSource: string; + readonly backend: ChatGptDesktopBackend; + readonly warnings?: readonly string[]; +}; + +export type ReadThreadResult = { + readonly threadId: string; + readonly turns: readonly ChatGptDesktopTurn[]; + readonly backend: ChatGptDesktopBackend; + readonly limit: number; + readonly full: boolean; + readonly nextCursor?: string | null; + readonly titleTruncated?: boolean; + readonly complete?: boolean; + readonly warnings?: readonly string[]; + readonly title?: string; + readonly cwd?: string | null; + readonly status?: string; + readonly modelProvider?: string | null; + readonly model?: string | null; +}; + +export type SendMessageResult = { + readonly threadId: string; + readonly backend: ChatGptDesktopBackend; + readonly experimental: boolean; + readonly delivery: 'accepted' | 'rejected' | 'unknown'; + readonly message?: string; + /** + * Temporary sidebar id before the real conversation annotation appears + * (`local:client-new-thread:…`). Never used as `threadId` once the real id + * is known from `data-response-annotation-conversation`. + */ + readonly temporaryThreadId?: string; + /** Real conversation id from `data-response-annotation-conversation`. */ + readonly conversationId?: string; + readonly project?: string; + readonly sentVia?: 'enter' | 'button'; +}; + +export type WaitForReplyResult = { + readonly threadId: string; + readonly reply: string; + readonly backend: ChatGptDesktopBackend; + readonly experimental: boolean; + readonly delivery: 'replied' | 'timeout' | 'rejected' | 'unknown'; + /** Real conversation id from `data-response-annotation-conversation`. */ + readonly conversationId?: string; +}; + +export type OpenThreadResult = { + readonly threadId: string; + readonly backend: ChatGptDesktopBackend; + readonly archived?: boolean; +}; diff --git a/src/core/codex-bridge.js b/src/core/codex-bridge.js index 839e40b..6ad8ff2 100644 --- a/src/core/codex-bridge.js +++ b/src/core/codex-bridge.js @@ -7,10 +7,14 @@ import { spawnSync } from "node:child_process"; import pkg from "../../package.json" with { type: "json" }; import { outcomeFromError, outcomeFromReceipt, withStatusExitCode } from "./codex/contract.js"; +import { mapRemoteThreadError, RemoteThreadNotLoadedError } from "./codex/remote-control.js"; +import { normalizeCodexThreadId } from "./codex/thread-id.js"; import { desktopShimStatus } from "./desktop-shim.js"; import { looksLikeGrokBotBox, USER_MACHINE_CODEX_CLAUDE_GUIDANCE } from "./user-machine-guidance.js"; export { looksLikeGrokBotBox } from "./user-machine-guidance.js"; +export { RemoteThreadNotLoadedError } from "./codex/remote-control.js"; +export { normalizeCodexThreadId, isTemporaryDesktopThreadId, LOCAL_THREAD_ID_PREFIX } from "./codex/thread-id.js"; // Method and param names below come from `codex app-server generate-json-schema` // of this Codex release. Newer daemons usually keep them; `gbot codex status` @@ -25,8 +29,9 @@ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // Transport budgets: fail fast instead of buffering unbounded attacker-controlled bytes. // ponytail: raise these only with streaming/pagination support; the app-server sends small JSON-RPC frames. const WS_MAX_HEADER_BYTES = 16 * 1024; -const WS_MAX_MESSAGE_BYTES = 4 * 1024 * 1024; -const WS_MAX_BUFFER_BYTES = 8 * 1024 * 1024; +const WS_MAX_MESSAGE_BYTES = 16 * 1024 * 1024; +const WS_MAX_BUFFER_BYTES = 32 * 1024 * 1024; +const CODEX_SEND_MAX_BYTES = 4 * 1024 * 1024; export const WS_MAX_WRITE_BYTES = 8 * 1024 * 1024; export const CODEX_MAX_REQUESTS = 128; export const CODEX_MAX_LISTENERS = 128; @@ -847,6 +852,14 @@ function sourceField(value) { return typeof value === "string" ? singleLine(value) : value; } +function summarizeSection(section) { + if (!isObject(section) || typeof section.id !== "string" || !section.id) return null; + return { + id: singleLine(section.id), + name: lineField(section.name), + }; +} + function summarizeThread(t) { if (!isObject(t) || typeof t.id !== "string" || !t.id) throw new CodexProtocolError("thread/list", "entry without a string `id`"); const type = isObject(t.status) && typeof t.status.type === "string" ? t.status.type : "unknown"; @@ -858,41 +871,83 @@ function summarizeThread(t) { preview: typeof t.preview === "string" ? stripTerminalControls(t.preview) : "", cwd: lineField(t.cwd), source: sourceField(t.source), + createdAt: typeof t.createdAt === "number" ? t.createdAt : null, updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : null, + section: summarizeSection(t.section), + projectId: lineField(t.projectId), + modelProvider: lineField(t.modelProvider), + model: lineField(t.model), + originator: lineField(t.originator), }; } const THREAD_LIST_MAX_LIMIT = 200; /** - * @param {{ limit?: number, cursor?: string, env?: NodeJS.ProcessEnv }} [opts] + * @param {{ limit?: number, cursor?: string, modelProviders?: string[], env?: NodeJS.ProcessEnv }} [opts] */ -export async function listCodexThreads({ limit = 20, cursor, env = process.env } = {}) { +export async function listCodexThreads({ + limit = 20, + cursor, + modelProviders = [], + env = process.env, +} = {}) { if (!Number.isInteger(limit) || limit < 1 || limit > THREAD_LIST_MAX_LIMIT) { throw new RangeError("--limit must be an integer 1-" + THREAD_LIST_MAX_LIMIT); } if (cursor !== undefined && (typeof cursor !== "string" || !cursor)) throw new RangeError("--cursor must be a non-empty string"); + if (!Array.isArray(modelProviders) || modelProviders.some((p) => typeof p !== "string")) { + throw new RangeError("modelProviders must be an array of strings (empty = all providers)"); + } const { client } = await openSession(env); try { - // The default listing rescans every rollout file to repair metadata (26 s on a busy machine); - // the state DB already holds what we print. - const params = { limit, useStateDbOnly: true, ...(cursor !== undefined ? { cursor } : {}) }; + // Default daemon filter is the current provider only. Pass modelProviders: [] for + // every provider, or a non-empty list to restrict. useStateDbOnly avoids a cold + // rollout rescan (tens of seconds); fall back to a full scan when the state DB + // returns nothing on the first page. + const baseParams = { + limit, + modelProviders, + ...(cursor !== undefined ? { cursor } : {}), + }; let out; + let usedStateDbOnly = true; try { - out = await client.request("thread/list", params); + out = await client.request("thread/list", { ...baseParams, useStateDbOnly: true }); } catch (err) { throw transportError(err); } if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + if ( + out.data.length === 0 + && cursor === undefined + && (out.nextCursor == null || out.nextCursor === "") + ) { + try { + out = await client.request("thread/list", { ...baseParams, useStateDbOnly: false }); + usedStateDbOnly = false; + } catch (err) { + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) throw new CodexProtocolError("thread/list", "missing `data` array"); + } if (out.nextCursor != null && typeof out.nextCursor !== "string") throw new CodexProtocolError("thread/list", "`nextCursor` is not a string"); const threads = out.data.map(summarizeThread); - return { threads, nextCursor: out.nextCursor ?? null, limit }; + return { + threads, + nextCursor: out.nextCursor ?? null, + limit, + useStateDbOnly: usedStateDbOnly, + }; } finally { client.close(); } } -function explainSendError(err, threadId) { +function explainSendError(err, threadId, env = process.env) { + if (err instanceof RemoteThreadNotLoadedError) return err; + const mapped = mapRemoteThreadError(err, threadId, env); + if (mapped instanceof RemoteThreadNotLoadedError) return mapped; if (!(err instanceof CodexRpcError)) return err; const msg = String(err.rpc && err.rpc.message || ""); if (/no rollout found|thread not found/i.test(msg)) { @@ -972,11 +1027,15 @@ export function withEnvelopeHeader(text, envelope, env = process.env) { export function assertThreadAllowed(threadId, env = process.env) { const raw = env.GROK_BOT_CODEX_THREADS; if (raw == null || raw.trim() === "") return; - const allowed = raw.split(",").map((s) => s.trim()).filter(Boolean); - if (!allowed.includes(threadId)) { + const bare = normalizeCodexThreadId(threadId); + const allowed = raw.split(",").map((s) => { + try { return normalizeCodexThreadId(s.trim()); } + catch { return s.trim(); } + }).filter(Boolean); + if (!allowed.includes(bare)) { throw new CodexSendError( - "Codex thread " + threadId + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), - { delivery: "rejected", reason: "route-not-allowed", threadId }, + "Codex thread " + bare + " is not in GROK_BOT_CODEX_THREADS; the operator allows only: " + allowed.join(", "), + { delivery: "rejected", reason: "route-not-allowed", threadId: bare }, ); } } @@ -1032,6 +1091,7 @@ function unsupportedOrRpc(err, method, threadId, envelope, turnId) { /** Read the daemon's queue for one thread (experimental `thread/queue/list`). */ export async function listCodexQueue(threadId, { env = process.env, limit = 50, cursor } = {}) { requireExperimental(env, "gbot codex queue"); + threadId = normalizeCodexThreadId(threadId); const { client } = await openSession(env, { experimental: true }); try { let out; @@ -1055,6 +1115,70 @@ export async function listCodexQueue(threadId, { env = process.env, limit = 50, } } +/** + * Read a thread's history without `thread/resume` and without deprecated + * `includeTurns` on `thread/read`. Metadata via `thread/read`, turns via + * paginated `thread/turns/list` (`itemsView: "full"`). + * + * @param {string} threadId + * @param {{ limit?: number, env?: NodeJS.ProcessEnv }} [opts] + */ +export async function readCodexThread(threadId, { limit = 100, env = process.env } = {}) { + if (!Number.isInteger(limit) || limit < 1 || limit > 2000) { + throw new RangeError("limit must be an integer 1-2000"); + } + const bare = normalizeCodexThreadId(threadId); + const { client } = await openSession(env); + try { + let meta; + try { + // Metadata only — never pass includeTurns (deprecated for paginated threads). + meta = await client.request("thread/read", { threadId: bare }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + const turns = []; + let cursor; + const seen = new Set(); + for (let page = 0; page < 40 && turns.length < limit; page++) { + let out; + try { + out = await client.request("thread/turns/list", { + threadId: bare, + limit: Math.min(100, limit - turns.length), + itemsView: "full", + sortDirection: "desc", + ...(cursor !== undefined ? { cursor } : {}), + }); + } catch (err) { + const mapped = explainSendError(err, bare, env); + if (mapped instanceof RemoteThreadNotLoadedError) throw mapped; + throw transportError(err); + } + if (!isObject(out) || !Array.isArray(out.data)) { + throw new CodexProtocolError("thread/turns/list", "missing `data` array"); + } + for (const row of out.data) turns.push(row); + cursor = out.nextCursor ?? null; + if (cursor === null) break; + if (typeof cursor !== "string" || !cursor || seen.has(cursor)) { + throw new CodexProtocolError("thread/turns/list", "invalid or repeated cursor"); + } + seen.add(cursor); + } + return { + threadId: bare, + thread: isObject(meta) && isObject(meta.thread) ? meta.thread : null, + turns: turns.slice(0, limit), + nextCursor: cursor ?? null, + }; + } finally { + client.close(); + } +} + /** * @param {string} threadId * @param {string} text @@ -1083,8 +1207,8 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy, validateModelEffort(model, effort); if (!["reject", "queue", ...(session ? ["steer"] : [])].includes(whenBusy)) throw new RangeError("--when-busy must be reject, queue, or persistent steer"); if (whenBusy === "steer" && (typeof expectedTurnId !== "string" || !ID_PATTERN.test(expectedTurnId))) throw new RangeError("steer requires expectedTurnId"); - if (typeof threadId !== "string" || !ID_PATTERN.test(threadId)) throw new RangeError("Invalid threadId"); - if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > WS_MAX_MESSAGE_BYTES) throw new RangeError("Message must contain text within 4 MiB"); + threadId = normalizeCodexThreadId(threadId); + if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > CODEX_SEND_MAX_BYTES) throw new RangeError("Message must contain text within 4 MiB"); if (!envelope || typeof envelope.messageId !== "string" || !ID_PATTERN.test(envelope.messageId)) throw new RangeError("Invalid envelope messageId"); const validated = buildEnvelope({ correlationId: envelope.correlationId, replyTo: envelope.replyTo, hop: envelope.hop, env }); envelope = { ...validated, messageId: envelope.messageId, header: Boolean(envelope.header) }; @@ -1106,8 +1230,10 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy, ...(model ? { model } : {}), ...(effort ? { config: { model_reasoning_effort: effort } } : {}) }); } catch (err) { assertNotCancelled(); - if (err instanceof CodexSendError) throw err; - throw new CodexSendError(explainSendError(err, threadId).message, { + if (err instanceof CodexSendError || err instanceof RemoteThreadNotLoadedError) throw err; + const explained = explainSendError(err, threadId, env); + if (explained instanceof RemoteThreadNotLoadedError) throw explained; + throw new CodexSendError(explained.message, { delivery: err instanceof CodexRpcError ? "rejected" : (err && err.delivery) || "unknown", 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", diff --git a/src/core/codex/contract.js b/src/core/codex/contract.js index 7dee301..af8f0b9 100644 --- a/src/core/codex/contract.js +++ b/src/core/codex/contract.js @@ -81,7 +81,10 @@ export function outcomeFromError(error, opts = {}) { const out = { error: message }; if (error && typeof error === "object") { - for (const key of ["delivery", "reason", "mode", "threadId", "turnId", "targetId", "messageId", "correlationId", "refused", "hop"]) { + for (const key of [ + "delivery", "reason", "mode", "threadId", "turnId", "targetId", "messageId", + "correlationId", "refused", "hop", "hostId", "hostName", "hint", "code", + ]) { if (error[key] !== undefined) out[key] = error[key]; } if (out.reason === undefined && isUsage(error)) out.reason = "usage"; diff --git a/src/core/codex/conversation.js b/src/core/codex/conversation.js index d815d88..c7d3601 100644 --- a/src/core/codex/conversation.js +++ b/src/core/codex/conversation.js @@ -1,10 +1,13 @@ import { outcomeFromError } from './contract.js'; import { realpathSync } from 'node:fs'; import { assertThreadAllowed, buildEnvelope, experimentalEnabled, openCodexSession, sendToCodexThread } from '../codex-bridge.js'; +import { mapRemoteThreadError, RemoteThreadNotLoadedError } from './remote-control.js'; +import { normalizeCodexThreadId } from './thread-id.js'; const ID = /^[A-Za-z0-9_.:-]{1,128}$/; const BUDGET = 4 * 1024 * 1024; function conversationId(value, name = 'threadId') { + if (name === 'threadId') return normalizeCodexThreadId(value); if (typeof value !== 'string' || !ID.test(value)) throw new RangeError(`${name} must be 1-128 identifier characters`); return value; } @@ -79,7 +82,11 @@ export async function openCodexConversation(threadId, options = {}) { if (resumed?.thread?.id !== threadId) throw new Error('thread/resume returned a different thread ID'); const cwd = resumed.cwd ?? resumed.thread.cwd; if (expected !== undefined && (typeof cwd !== 'string' || realpathSync(cwd) !== expected)) throw new Error('Codex thread cwd does not match expectedCwd'); - } catch (error) { await close(); throw error; } + } catch (error) { + await close(); + if (error instanceof RemoteThreadNotLoadedError) throw error; + throw mapRemoteThreadError(error, threadId, env); + } /** @param {{turnId: string, messageId?: string, afterMessageId?: string|string[], timeoutMs?: number, signal?: AbortSignal, maxOutputBytes?: number}} options */ async function wait({ turnId, messageId, afterMessageId, timeoutMs = 120000, signal: waitSignal, maxOutputBytes = 1048576 }) { diff --git a/src/core/codex/remote-control.js b/src/core/codex/remote-control.js new file mode 100644 index 0000000..2ccabbe --- /dev/null +++ b/src/core/codex/remote-control.js @@ -0,0 +1,230 @@ +/** + * Remote-control thread ownership from ChatGPT Desktop's + * `~/.codex/.codex-global-state.json` (`remote-thread-summaries-v3:`). + * + * Shared by codex-bridge send/read and the ChatGPT Desktop adapter so both + * surfaces map "thread not loaded" to the same typed error. + */ + +import { readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +import { LOCAL_THREAD_ID_PREFIX, normalizeCodexThreadId } from './thread-id.js'; + +const REMOTE_SUMMARY_KEY = /^remote-thread-summaries-v3:(.+)$/; + +/** + * Thread exists on a remote-control host and is not loaded on the local + * app-server. SSH remoting is a follow-up — this error names the owner. + */ +export class RemoteThreadNotLoadedError extends Error { + /** + * @param {string} threadId + * @param {string | null} [hostId] + * @param {string | null} [hostName] + */ + constructor(threadId, hostId = null, hostName = null) { + const hostLabel = hostName + ? `${JSON.stringify(hostName)} (${JSON.stringify(hostId)})` + : hostId + ? JSON.stringify(hostId) + : null; + const owner = hostLabel + ? `owned by remote-control host ${hostLabel}` + : 'owned by a remote-control host (hostId unknown; check ~/.codex/.codex-global-state.json)'; + const hint = hostId + ? `Read this thread via host ${JSON.stringify(hostId)}'s app-server` + + (hostName ? ` (${hostName})` : '') + + '. SSH remoting is not implemented in this adapter.' + : "Read this thread via the owning host's app-server. SSH remoting is not implemented in this adapter."; + super( + `Codex thread ${JSON.stringify(threadId)} is not loaded on the local app-server; ${owner}. ${hint}`, + ); + this.name = 'RemoteThreadNotLoadedError'; + this.code = 'REMOTE_THREAD_NOT_LOADED'; + this.delivery = 'rejected'; + this.reason = 'remote-thread-not-loaded'; + this.threadId = threadId; + this.hostId = hostId; + this.hostName = hostName; + this.hint = hint; + } +} + +export function codexGlobalStatePath(env = process.env) { + const home = env.CODEX_HOME || join(homedir(), '.codex'); + return join(home, '.codex-global-state.json'); +} + +/** @returns {Record | null} */ +export function loadCodexGlobalState(env = process.env) { + let raw; + try { + raw = readFileSync(codexGlobalStatePath(env), 'utf8'); + } catch { + return null; + } + try { + const data = JSON.parse(raw); + if (!data || typeof data !== 'object' || Array.isArray(data)) return null; + return data; + } catch { + return null; + } +} + +/** Desktop keeps atom keys one level below the global state root. */ +export function codexStateEntries(data) { + const nested = data?.['electron-persisted-atom-state']; + return [nested, data].filter((value) => value && typeof value === 'object' && !Array.isArray(value)); +} + +/** + * @param {string} threadId + * @param {NodeJS.ProcessEnv} [env] + * @returns {{ hostId: string, hostName: string | null } | null} + */ +export function findRemoteThreadHost(threadId, env = process.env) { + let bare; + try { + bare = normalizeCodexThreadId(threadId); + } catch { + bare = typeof threadId === 'string' && threadId.startsWith(LOCAL_THREAD_ID_PREFIX) + ? threadId.slice(LOCAL_THREAD_ID_PREFIX.length) + : threadId; + } + if (!bare) return null; + const data = loadCodexGlobalState(env); + if (!data) return null; + const hostNames = collectHostNames(data); + + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)) { + const match = REMOTE_SUMMARY_KEY.exec(key); + if (!match?.[1]) continue; + const hostId = match[1]; + const hostName = hostNames.get(hostId) ?? extractHostName(value) ?? null; + for (const entry of extractThreadEntries(value)) { + const id = threadIdFromEntry(entry); + if (!id) continue; + const entryBare = id.startsWith(LOCAL_THREAD_ID_PREFIX) + ? id.slice(LOCAL_THREAD_ID_PREFIX.length) + : id; + if (entryBare === bare) return { hostId, hostName }; + } + } + return null; +} + +/** + * Map app-server "thread not loaded" / unknown-thread errors to a typed remote + * error when the id appears under remote-control summaries. + * @param {unknown} error + * @param {string} threadId + * @param {NodeJS.ProcessEnv} [env] + * @returns {Error} + */ +export function mapRemoteThreadError(error, threadId, env = process.env) { + const message = errorMessage(error); + if (!/thread not loaded|not loaded|no rollout found|thread not found|invalid thread id/i.test(message)) { + return error instanceof Error ? error : new Error(message || String(error)); + } + const remote = findRemoteThreadHost(threadId, env); + if (remote) { + return new RemoteThreadNotLoadedError(threadId, remote.hostId, remote.hostName); + } + // "thread not loaded" without a known remote host still gets a typed hint. + if (/thread not loaded|not loaded/i.test(message)) { + return new RemoteThreadNotLoadedError(threadId, null, null); + } + return error instanceof Error ? error : new Error(message || String(error)); +} + +function collectHostNames(data) { + const names = new Map(); + for (const [key, value] of codexStateEntries(data).flatMap(Object.entries)) { + if (!/remote-host|hosts|host-meta|host_directory|environments/i.test(key)) continue; + if (!value || typeof value !== 'object') continue; + if (Array.isArray(value)) { + for (const entry of value) { + const pair = hostNameFromEntry(entry); + if (pair && !names.has(pair.hostId)) names.set(pair.hostId, pair.hostName); + } + continue; + } + for (const [hostId, entry] of Object.entries(value)) { + const fromEntry = hostNameFromEntry(entry); + if (fromEntry) { + if (!names.has(fromEntry.hostId)) names.set(fromEntry.hostId, fromEntry.hostName); + continue; + } + const label = stringField(entry, ['name', 'hostName', 'displayName', 'envName', 'label', 'title']); + if (label && !names.has(hostId)) names.set(hostId, label); + } + } + return names; +} + +function hostNameFromEntry(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + const hostId = stringField(entry, ['hostId', 'id', 'host_id', 'uuid']); + const hostName = stringField(entry, ['name', 'hostName', 'displayName', 'envName', 'label', 'title']); + if (!hostId || !hostName) return null; + return { hostId, hostName }; +} + +function extractHostName(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return stringField(value, [ + 'hostName', 'displayName', 'name', 'envName', 'label', 'title', 'friendlyName', + ]); +} + +function extractThreadEntries(value) { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + const record = value; + for (const key of ['threads', 'summaries', 'items', 'data', 'conversations']) { + if (Array.isArray(record[key])) return record[key]; + } + const entries = []; + for (const [key, entry] of Object.entries(record)) { + if ([ + 'pinnedIds', 'pinned', 'pinnedThreadIds', 'hostName', 'displayName', 'name', + 'envName', 'label', 'title', 'friendlyName', 'hostId', 'version', + ].includes(key)) continue; + if (typeof entry === 'string') continue; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const id = stringField(entry, ['id', 'threadId', 'thread_id', 'conversationId']); + entries.push(id ? entry : { ...entry, id: key }); + } + } + return entries; +} + +function threadIdFromEntry(entry) { + if (typeof entry === 'string' && entry) return entry; + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; + return stringField(entry, ['id', 'threadId', 'thread_id', 'conversationId']); +} + +function stringField(row, keys) { + if (!row || typeof row !== 'object' || Array.isArray(row)) return null; + for (const key of keys) { + const value = row[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; +} + +function errorMessage(error) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error && 'message' in error) return String(error.message); + if (typeof error === 'object' && error && 'rpc' in error) { + const rpc = error.rpc; + if (rpc && typeof rpc.message === 'string') return rpc.message; + } + return String(error); +} diff --git a/src/core/codex/routes.ts b/src/core/codex/routes.ts index dec5996..481e779 100644 --- a/src/core/codex/routes.ts +++ b/src/core/codex/routes.ts @@ -2,8 +2,21 @@ import { z } from 'zod'; import { buildEnvelope, listCodexThreads, sendToCodexThread } from '../codex-bridge.js'; import { outcomeFromError } from './contract.js'; import { openCodexConversation } from './conversation.js'; +import { normalizeCodexThreadId } from './thread-id.js'; -const id = z.string().regex(/^[A-Za-z0-9_.:-]{1,128}$/); +const bareId = z.string().regex(/^[A-Za-z0-9_.:-]{1,128}$/); +/** Accept Desktop `local:` and normalize before the bare-id check. */ +const id = z.string().min(1).max(160).transform((value, ctx) => { + try { + return normalizeCodexThreadId(value); + } catch (error) { + ctx.addIssue({ + code: 'custom', + message: error instanceof Error ? error.message : 'Invalid threadId', + }); + return z.NEVER; + } +}).pipe(bareId); export const observationFields = { expectedCwd: z.string().min(1).optional(), threadId: id, @@ -11,15 +24,15 @@ export const observationFields = { }; export const sendFields = { ...observationFields, - correlationId: id.optional(), envelope: z.boolean().optional(), hop: z.number().int().min(0).optional(), + correlationId: bareId.optional(), envelope: z.boolean().optional(), hop: z.number().int().min(0).optional(), model: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/).optional(), effort: z.enum(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']).optional(), - replyTo: id.optional(), expectedTurnId: id.optional(), + replyTo: bareId.optional(), expectedTurnId: bareId.optional(), whenBusy: z.enum(['reject', 'queue', 'steer']).default('reject'), wait: z.boolean().default(false), maxOutputBytes: z.number().int().min(1).max(4194304).optional(), }; export const sendSchema = z.object({ ...sendFields, message: z.string().min(1).max(4194304) }).strict(); -export const waitSchema = z.object({ ...observationFields, turnId: id, messageId: id.optional(), maxOutputBytes: z.number().int().min(1).max(4194304).optional() }).strict(); +export const waitSchema = z.object({ ...observationFields, turnId: bareId, messageId: bareId.optional(), maxOutputBytes: z.number().int().min(1).max(4194304).optional() }).strict(); export const watchSchema = z.object({ expectedCwd: z.string().min(1).optional(), threadId: id, timeoutMs: z.number().int().min(1).max(600000).optional(), maxEvents: z.number().int().min(1).max(500).default(100) }).strict(); export const threadsSchema = z.object({ limit: z.number().int().min(1).max(200).default(20), cursor: z.string().min(1).max(4096).optional() }).strict(); export const resultSchema = z.object({ diff --git a/src/core/codex/thread-id.js b/src/core/codex/thread-id.js new file mode 100644 index 0000000..9dfe6be --- /dev/null +++ b/src/core/codex/thread-id.js @@ -0,0 +1,41 @@ +/** + * Normalize Desktop sidebar thread ids for Codex app-server RPCs. + * + * ChatGPT Desktop sidebar rows use `local:`. App-server rejects + * the prefixed form with `invalid thread id`. Temporary composer rows + * (`local:client-new-thread:…`) have no app-server mapping. + */ + +export const LOCAL_THREAD_ID_PREFIX = 'local:'; +export const TEMP_THREAD_ID_PREFIX = 'local:client-new-thread:'; + +const ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/; + +export function isTemporaryDesktopThreadId(threadId) { + return typeof threadId === 'string' + && (threadId.startsWith(TEMP_THREAD_ID_PREFIX) || threadId.startsWith('client-new-thread:')); +} + +/** + * Strip a leading `local:` prefix. Temporary Desktop rows throw. + * @param {string} threadId + * @returns {string} + */ +export function normalizeCodexThreadId(threadId) { + if (typeof threadId !== 'string' || !threadId) { + throw new RangeError('Invalid threadId'); + } + if (isTemporaryDesktopThreadId(threadId)) { + throw new RangeError( + 'Temporary Desktop thread id ' + JSON.stringify(threadId) + + ' has no app-server mapping; wait for the real conversation id', + ); + } + const bare = threadId.startsWith(LOCAL_THREAD_ID_PREFIX) + ? threadId.slice(LOCAL_THREAD_ID_PREFIX.length) + : threadId; + if (!bare || !ID_PATTERN.test(bare)) { + throw new RangeError('Invalid threadId'); + } + return bare; +} diff --git a/src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx b/src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx new file mode 100644 index 0000000..ff0869f --- /dev/null +++ b/src/mcp/grok-bot/tools/chatgpt_desktop_list_hosts.tsx @@ -0,0 +1,37 @@ +import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; +import { + listHostsOperation, + listHostsSchema as inputSchema, + resultSchema, + resultText, +} from '../../../core/chatgpt-desktop/routes.js'; + +export { inputSchema }; + +export default defineTool( + { + title: 'ChatGPT Desktop list hosts', + description: + 'Discover local, remote-control environment ids, managed SSH hosts, and modelProvider ids. hostName is null when Desktop has no real display name. Use hostId or label in list/search filters; remote thread ids must be read on the owning host. warnings report provider discovery failures.', + annotations: { readOnlyHint: true }, + inputSchema, + resultSchema, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { type: 'number' }, + }, + required: [], + }, + }, + async (input) => { + const out = await listHostsOperation(input); + return ( + + {resultText(out)} + + ); + }, +); diff --git a/src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx b/src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx new file mode 100644 index 0000000..a19eead --- /dev/null +++ b/src/mcp/grok-bot/tools/chatgpt_desktop_list_threads.tsx @@ -0,0 +1,55 @@ +import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; +import { + listThreadsOperation, + listThreadsSchema as inputSchema, + resultSchema, + resultText, +} from '../../../core/chatgpt-desktop/routes.js'; + +export { inputSchema }; + +export default defineTool( + { + title: 'ChatGPT Desktop list threads', + description: + 'List one sorted Desktop inventory without duplicate rows. Pass nextCursor as cursor with unchanged filters until null; pages fill limit unless the MCP byte budget binds. A changed/expired inventory cursor requires restarting. Display titles are capped at 200 characters with titleTruncated:true. Filter by dynamic hostId/hostName, modelProvider, or Desktop project label/projectId; projectRootPath identifies the resolved workspace. Rows expose local:, location, hostId, and provider; remote rows require their owning host for reading or sending. Backend and warnings identify partial sources.', + annotations: { readOnlyHint: true }, + inputSchema, + resultSchema, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { type: 'number' }, + limit: { type: 'number', default: 50, description: 'Maximum threads per page.' }, + cursor: { type: 'string', description: 'Opaque nextCursor from the prior page; keep filters unchanged.' }, + host: { + type: 'string', + description: + 'Any string filter for machine/location. Reserved: "all" (default), "local". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.', + }, + modelProvider: { + type: 'string', + description: + 'Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.', + }, + project: { type: 'string', description: 'Match a Desktop project label or app-server projectId. Dynamic string, not an enum.' }, + groupBy: { + type: 'string', + enum: ['host'], + description: 'When "host", also return groups[] keyed by host (grouping mode, not a host id).', + }, + }, + required: [], + }, + }, + async (input) => { + const out = await listThreadsOperation(input); + return ( + + {resultText(out)} + + ); + }, +); diff --git a/src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx b/src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx new file mode 100644 index 0000000..8cfb196 --- /dev/null +++ b/src/mcp/grok-bot/tools/chatgpt_desktop_open_thread.tsx @@ -0,0 +1,39 @@ +import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; +import { + openThreadOperation, + openThreadSchema as inputSchema, + resultSchema, + resultText, +} from '../../../core/chatgpt-desktop/routes.js'; + +export { inputSchema }; + +export default defineTool( + { + title: 'ChatGPT Desktop open thread', + description: + 'Navigate Desktop to an existing local: or bare conversation id, even when the row is absent from the sidebar. Archived routes return archived:true without unarchiving; read them with chatgpt_desktop_read_thread. A remote-control id is owned by its hostId and cannot be opened on this Mac. CDP_UNREACHABLE means Desktop must be reachable locally.', + annotations: { readOnlyHint: true }, + inputSchema, + resultSchema, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { type: 'number', description: 'Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).' }, + threadId: { type: 'string', description: 'local: or bare durable id. Temporary local:client-new-thread:* works only while selected; remote ids require their owning host.' }, + openTimeoutMs: { type: 'number', description: 'Maximum time to wait for the requested conversation (1-600000 ms).' }, + }, + required: ['threadId'], + }, + }, + async (input) => { + const out = await openThreadOperation(input); + return ( + + {resultText(out)} + + ); + }, +); diff --git a/src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx b/src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx new file mode 100644 index 0000000..9131095 --- /dev/null +++ b/src/mcp/grok-bot/tools/chatgpt_desktop_read_thread.tsx @@ -0,0 +1,54 @@ +import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; +import { + readThreadOperation, + readThreadSchema as inputSchema, + resultSchema, + resultText, +} from '../../../core/chatgpt-desktop/routes.js'; + +export { inputSchema }; + +export default defineTool( + { + title: 'ChatGPT Desktop read thread', + description: + 'Read a local:, bare durable id, or selected temporary local:client-new-thread:* id. Pass nextCursor back as cursor with the same threadId/full mode until complete:true. Pages stop at a byte budget; oversized turns use ordered continuation fragments with stable turnKey and field offsets, without dropping text. Archived threads remain readable. Warnings explain partial pages or app-server errors. REMOTE_THREAD_NOT_LOADED includes the owning hostId: call on that host.', + annotations: { readOnlyHint: true }, + inputSchema, + resultSchema, + render: { maxElapsedMs: 660000 }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { type: 'number' }, + threadId: { + type: 'string', + description: + 'local:, bare durable id, or selected local:client-new-thread:* temporary id. Remote ids return REMOTE_THREAD_NOT_LOADED with hostId.', + }, + limit: { type: 'number' }, + cursor: { type: 'string', description: 'Opaque nextCursor from the prior read page. Keep threadId and full unchanged.' }, + full: { + type: 'boolean', + description: + 'When true, page from the start through the complete history (up to 2000 turns by default); otherwise return the most recent 100 turns by default. Explicit limit bounds either mode.', + }, + openTimeoutMs: { + type: 'number', + description: 'Max wait for Loading task… to clear after opening (default 90000).', + }, + }, + required: ['threadId'], + }, + }, + async (input) => { + const out = await readThreadOperation(input); + return ( + + {resultText(out)} + + ); + }, +); diff --git a/src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx b/src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx new file mode 100644 index 0000000..528a895 --- /dev/null +++ b/src/mcp/grok-bot/tools/chatgpt_desktop_search_threads.tsx @@ -0,0 +1,56 @@ +import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; +import { + resultSchema, + resultText, + searchThreadsOperation, + searchThreadsSchema as inputSchema, +} from '../../../core/chatgpt-desktop/routes.js'; + +export { inputSchema }; + +export default defineTool( + { + title: 'ChatGPT Desktop search threads', + description: + 'Search full Desktop thread metadata by case-insensitive substring. Display titles are capped at 200 characters with titleTruncated:true. Pass nextCursor as cursor with unchanged query and filters to retrieve all matches. Dynamic hostId/hostName, modelProvider, and project filters apply. Remote results identify hostId and cannot be read on this Mac.', + annotations: { readOnlyHint: true }, + inputSchema, + resultSchema, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { type: 'number' }, + query: { type: 'string', description: 'Case-insensitive substring over thread metadata.' }, + limit: { type: 'number', default: 50, description: 'Maximum matches.' }, + cursor: { type: 'string', description: 'Opaque nextCursor from the previous search page; keep query and filters unchanged.' }, + host: { + type: 'string', + description: + 'Any string filter for machine/location. Reserved: "all" (default), "local". Otherwise a hostId or friendly name discovered at runtime — see chatgpt_desktop_list_hosts. Not an enum; new machines/connections appear with no code change.', + }, + modelProvider: { + type: 'string', + description: + 'Any modelProvider id string (passed through to app-server modelProviders). Omit for all providers. Discovered values: see chatgpt_desktop_list_hosts. Not an enum.', + }, + project: { type: 'string', description: 'Match a Desktop project label or app-server projectId.' }, + groupBy: { + type: 'string', + enum: ['host'], + description: 'When "host", also return groups[] keyed by host (grouping mode, not a host id).', + }, + }, + required: ['query'], + }, + }, + async (input) => { + const out = await searchThreadsOperation(input); + return ( + + {resultText(out)} + + ); + }, +); diff --git a/src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx b/src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx new file mode 100644 index 0000000..a9804d7 --- /dev/null +++ b/src/mcp/grok-bot/tools/chatgpt_desktop_send.tsx @@ -0,0 +1,46 @@ +import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; +import { + resultSchema, + resultText, + sendOperation, + sendSchema as inputSchema, +} from '../../../core/chatgpt-desktop/routes.js'; + +export { inputSchema }; + +export default defineTool( + { + title: 'ChatGPT Desktop send', + description: + 'Send once through local Desktop CDP. With threadId, accept local:, bare durable id, or currently selected local:client-new-thread:*; remote ids must be sent on their hostId. Omit threadId and project for a new chat outside projects; omit threadId and set project for a new chat inside it. The empty view and project scope are checked before typing. New-thread receipts include temporaryThreadId when seen and return durable local: once resolved. ARCHIVED_THREAD rejects without sending; unarchive:true explicitly unarchives that existing thread first. PROJECT_UNAVAILABLE identifies a disabled project action or missing configured root; NEW_CHAT_NAVIGATION_FAILED reports an unchanged view promptly. CDP_UNREACHABLE and COMPOSER_HAS_DRAFT require operator inspection, not blind retry.', + annotations: { readOnlyHint: false }, + inputSchema, + resultSchema, + render: { maxElapsedMs: 180000 }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { type: 'number' }, + threadId: { type: 'string', description: 'local:, bare durable id, or selected local:client-new-thread:*; omit for a new chat.' }, + text: { type: 'string' }, + project: { + type: 'string', + description: 'With threadId omitted, create the new chat inside this project. Omit for a new chat outside projects.', + }, + unarchive: { type: 'boolean', default: false, description: 'Existing threadId only: explicitly call thread/unarchive after ARCHIVED_THREAD, then retry the send once. Never implicit.' }, + openTimeoutMs: { type: 'number' }, + }, + required: ['text'], + }, + }, + async (input) => { + const out = await sendOperation(input); + return ( + + {resultText(out)} + + ); + }, +); diff --git a/src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx b/src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx new file mode 100644 index 0000000..427b9c8 --- /dev/null +++ b/src/mcp/grok-bot/tools/chatgpt_desktop_status.tsx @@ -0,0 +1,40 @@ +import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; +import { + resultSchema, + resultText, + statusOperation, + statusSchema as inputSchema, +} from '../../../core/chatgpt-desktop/routes.js'; + +export { inputSchema }; + +export default defineTool( + { + title: 'ChatGPT Desktop status', + description: + 'Check local ChatGPT Desktop CDP before send, wait, or open. reachable:false and exitCode:1 mean CDP is down even if appServerFallback.reachable is true; list/search/read may still work through app-server. There is no remote CDP transport.', + annotations: { readOnlyHint: true }, + inputSchema, + resultSchema, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { + type: 'number', + description: 'Local CDP port (default 9222 or CHATGPT_DESKTOP_CDP_PORT).', + }, + }, + required: [], + }, + }, + async (input) => { + const out = await statusOperation(input); + return ( + + {resultText(out)} + + ); + }, +); diff --git a/src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx b/src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx new file mode 100644 index 0000000..0026005 --- /dev/null +++ b/src/mcp/grok-bot/tools/chatgpt_desktop_wait_reply.tsx @@ -0,0 +1,40 @@ +import { Agent } from '@agent-bundle/runtime'; +import { defineTool } from 'agent-bundle/routes'; +import { + resultSchema, + resultText, + waitReplyOperation, + waitReplySchema as inputSchema, +} from '../../../core/chatgpt-desktop/routes.js'; + +export { inputSchema }; + +export default defineTool( + { + title: 'ChatGPT Desktop wait for reply', + description: + 'After chatgpt_desktop_send, wait for the selected Desktop reply over CDP. Accept the returned durable local: or the temporary local:client-new-thread:* while selected; omit threadId only when staying on the same selected chat. Returns reply, durable threadId and conversationId. timeout is not proof the send failed; CDP_UNREACHABLE requires Desktop recovery. Remote ids must be handled on their hostId.', + annotations: { readOnlyHint: true }, + inputSchema, + resultSchema, + render: { maxElapsedMs: 660000 }, + inputJsonSchema: { + type: 'object', + additionalProperties: false, + properties: { + port: { type: 'number' }, + threadId: { type: 'string', description: 'Durable local:, bare id, or selected local:client-new-thread:* from send.' }, + timeoutMs: { type: 'number', default: 120000 }, + }, + required: [], + }, + }, + async (input) => { + const out = await waitReplyOperation(input); + return ( + + {resultText(out)} + + ); + }, +); diff --git a/src/mcp/grok-bot/tools/codex_threads.tsx b/src/mcp/grok-bot/tools/codex_threads.tsx index a7b5100..7350eca 100644 --- a/src/mcp/grok-bot/tools/codex_threads.tsx +++ b/src/mcp/grok-bot/tools/codex_threads.tsx @@ -10,10 +10,11 @@ export default defineTool({ inputJsonSchema: { type: 'object', additionalProperties: false, properties: { "limit": { "type": "number", - "description": "Maximum threads in this page: 1-200." + "description": "Maximum threads in this page: 1-200. Pass modelProviders:[] internally so every provider is included; use cursor/nextCursor to page." }, "cursor": { - "type": "string" + "type": "string", + "description": "Opaque nextCursor from a previous page." } }, required: [] }, }, async input => { diff --git a/src/skills/chatgpt-desktop/SKILL.md b/src/skills/chatgpt-desktop/SKILL.md new file mode 100644 index 0000000..1306013 --- /dev/null +++ b/src/skills/chatgpt-desktop/SKILL.md @@ -0,0 +1,75 @@ +--- +name: chatgpt-desktop +description: Use the gbot ChatGPT Desktop MCP tools to discover hosts and threads, page/search/read conversations, and send or wait for replies on the user's Mac. Use for local Desktop CDP automation and remote-control thread routing; do not use for ordinary Grok Bot messaging. +--- +# ChatGPT Desktop through gbot + +Use the `chatgpt_desktop_*` MCP tools on the user's Mac. The CLI is a secondary +surface. CDP listens on `127.0.0.1` only; these tools do not transport CDP to +another machine. + +## Find the right conversation + +1. Call `chatgpt_desktop_status`. `reachable: false` or `exitCode: 1` means + CDP is down: open/send/wait cannot work. `appServerFallback.reachable` is a + separate list/search/read probe. Do not retry a send because CDP disappeared. +2. Call `chatgpt_desktop_list_hosts` for dynamic host IDs, labels, and + `modelProvider` values. `local` is this Mac. A remote-control host ID looks + like `remote-control:env_e_...`; its `hostName` may be `null`. Do not infer a + host name from a project label. Managed SSH hosts may have no threads. +3. Use `chatgpt_desktop_list_threads` with `host`, `modelProvider`, or `project` + as needed. Pass `nextCursor` back as `cursor` with the same filters until it + is `null`. The cursor refers to a sorted merged inventory; pages normally + fill `limit` and can stop earlier at the MCP document cap. If the inventory + changed after a cursor expired, restart without one. Project labels and IDs + come from Desktop project assignments and workspace roots; `projectRootPath` + is the resolved root when available. + `chatgpt_desktop_search_threads` also accepts `cursor`; keep its query and + filters unchanged. Search matches the full title even when the display row + has a 200-character `title` and `titleTruncated: true`. +4. Read with `chatgpt_desktop_read_thread`. `full: false` gets the recent tail; + `full: true` pages chronologically from the start. Pass `nextCursor` back + as `cursor` with the same thread ID and `full` value until `complete: true`. + The byte budget can stop before `limit`. An oversized turn is returned as + ordered fragments with one stable `turnKey`; use `continuation.field`, + `offsetChars`, and `totalChars` to reassemble `text`, `userText`, and + `assistantText`. `textTruncated: true` signals a fragment, not missing text. + Check `complete` and `warnings` before treating a page as full history. + +Durable local IDs are `local:` (a bare conversation ID also +works). `local:client-new-thread:*` is temporary and works only while that +conversation remains selected. `REMOTE_THREAD_NOT_LOADED` includes the +owning `hostId`: read or send through gbot running on that host's own Desktop +or app-server. The local Mac cannot fetch that remote history through CDP. + +## Open, send, and wait + +- `chatgpt_desktop_open_thread` navigates to a durable conversation even if it + is absent from the sidebar. An archived route returns `archived: true`; the + thread remains readable through `chatgpt_desktop_read_thread`. +- `chatgpt_desktop_send` with `threadId` sends to that conversation. Omit + `threadId` and `project` to start outside a project. Omit `threadId` and set + `project` to start inside that project. The tool verifies the empty new-chat + view and its project selection before typing. Send once. A new-thread receipt may + include `temporaryThreadId` and resolves `threadId` to the durable + `local:` before returning. Keep both until the durable ID is + confirmed; use the durable ID for later work. +- Then call `chatgpt_desktop_wait_reply` with the returned `threadId` (or the + selected temporary ID). It returns the reply and durable `conversationId`. + A timeout is an observation, not permission to resend. + +`ARCHIVED_THREAD` means send was rejected before submission. Read remains +available. Pass `unarchive: true` with an existing `threadId` on send only if +the user wants the archive state changed; this explicitly calls app-server +`thread/unarchive` and retries once. Never unarchive merely to read or open. + +`COMPOSER_HAS_DRAFT` means the user's draft would be disturbed; leave it for +the operator. `PROJECT_UNAVAILABLE` means Desktop disabled a project's new-chat +action, often because its configured workspace root no longer exists; correct +the project root in Desktop before retrying. `NEW_CHAT_NAVIGATION_FAILED` means +the button did not open the requested empty view within three seconds; inspect +Desktop before retrying. `CDP_UNREACHABLE` requires restoring the local Desktop CDP +endpoint. App-server failures, including oversized frames, appear as errors or +read `warnings` with `complete: false`; inspect the partial result and source +status before continuing. A send with unknown delivery must not be repeated +without checking the selected thread and reply state. diff --git a/test/codex-bridge.test.js b/test/codex-bridge.test.js index c22bc89..5f229d9 100644 --- a/test/codex-bridge.test.js +++ b/test/codex-bridge.test.js @@ -143,20 +143,110 @@ test("codex list-threads passes --limit and prints threads", async () => { const text = await gbot(fake.home, "codex", "list-threads", "--limit", "1"); assert.equal(text.code, 0, text.err); assert.equal(text.out, "t-1 idle - Fix the build\n /repo/a\n please fix the build\n\nmore: --cursor \"cursor-2\"\n"); - assert.deepEqual(fake.received.find((m) => m.method === "thread/list").params, { limit: 1, useStateDbOnly: true }); + assert.deepEqual(fake.received.find((m) => m.method === "thread/list").params, { + limit: 1, + useStateDbOnly: true, + modelProviders: [], + }); const json = await gbot(fake.home, "codex", "list-threads", "--json"); assert.equal(json.code, 0, json.err); assert.deepEqual(JSON.parse(json.out), { threads: [ - { id: "t-1", status: "idle", activeFlags: [], name: "Fix the build", preview: "please fix the build", cwd: "/repo/a", source: "vscode", updatedAt: 1700000001 }, - { id: "t-2", status: "notLoaded", activeFlags: [], name: null, preview: "second thread\npreview", cwd: "/repo/b", source: "cli", updatedAt: 1700000000 }, + { + id: "t-1", + status: "idle", + activeFlags: [], + name: "Fix the build", + preview: "please fix the build", + cwd: "/repo/a", + source: "vscode", + createdAt: null, + updatedAt: 1700000001, + section: null, + projectId: null, + modelProvider: null, + model: null, + originator: null, + }, + { + id: "t-2", + status: "notLoaded", + activeFlags: [], + name: null, + preview: "second thread\npreview", + cwd: "/repo/b", + source: "cli", + createdAt: null, + updatedAt: 1700000000, + section: null, + projectId: null, + modelProvider: null, + model: null, + originator: null, + }, ], nextCursor: null, limit: 20, + useStateDbOnly: true, exitCode: 0, }); - assert.deepEqual(fake.received.at(-1).params, { limit: 20, useStateDbOnly: true }); + assert.deepEqual(fake.received.at(-1).params, { + limit: 20, + useStateDbOnly: true, + modelProviders: [], + }); + } finally { + await fake.close(); + } +}); + +test("codex list-threads passes modelProviders:[] so other providers are not filtered out", async () => { + const seen = []; + const fake = await fakeAppServer({ + ...baseHandlers, + "thread/list": (params, ok) => { + seen.push(params); + ok({ + data: [ + { + id: "openai-1", + status: { type: "idle" }, + name: "OpenAI thread", + preview: "a", + cwd: "/a", + modelProvider: "openai", + updatedAt: 1, + }, + { + id: "other-1", + status: { type: "idle" }, + name: "Other provider", + preview: "b", + cwd: "/b", + modelProvider: "azure", + section: { id: "Pinned", name: "Pinned" }, + projectId: "proj-1", + model: "gpt-x", + originator: "desktop", + createdAt: 10, + updatedAt: 20, + }, + ], + nextCursor: null, + }); + }, + }); + try { + const { code, out } = await gbot(fake.home, "codex", "list-threads", "--json"); + assert.equal(code, 0, out); + assert.deepEqual(seen[0], { limit: 20, useStateDbOnly: true, modelProviders: [] }); + const page = JSON.parse(out); + assert.deepEqual(page.threads.map((t) => t.id), ["openai-1", "other-1"]); + assert.equal(page.threads[1].modelProvider, "azure"); + assert.deepEqual(page.threads[1].section, { id: "Pinned", name: "Pinned" }); + assert.equal(page.threads[1].projectId, "proj-1"); + assert.equal(page.threads[1].createdAt, 10); } finally { await fake.close(); } @@ -626,7 +716,7 @@ test("codex status rejects a complete oversized frame without buffering it", asy ...baseHandlers, initialize: (params, ok, err, send, socket) => { void params; void ok; void err; void send; - socket.write(encodeFrame(0x1, Buffer.alloc(5 * 1024 * 1024))); + socket.write(encodeFrame(0x1, Buffer.alloc(17 * 1024 * 1024))); }, }); try { @@ -832,7 +922,7 @@ test("codex list-threads pages with --cursor, echoes nextCursor, and rejects unk const second = await gbot(fake.home, "codex", "list-threads", "--limit", "1", "--cursor", "page-2", "--json"); assert.deepEqual(JSON.parse(second.out).threads.map((t) => t.id), ["t-2"]); assert.equal(JSON.parse(second.out).nextCursor, null); - assert.deepEqual(seen.at(-1), { limit: 1, useStateDbOnly: true, cursor: "page-2" }); + assert.deepEqual(seen.at(-1), { limit: 1, useStateDbOnly: true, modelProviders: [], cursor: "page-2" }); const unknown = await gbot(fake.home, "codex", "list-threads", "--all"); assert.equal(unknown.code, 2); @@ -875,8 +965,38 @@ test("codex list-threads sanitizes text fields, keeps structured source, and rej assert.equal(code, 0); const { threads } = JSON.parse(out); assert.deepEqual(threads, [ - { id: "t-x line", status: "active", activeFlags: ["waitingOnApproval"], name: "a b", preview: "keep\nnewlines", cwd: "/r", source: { custom: "hauler\u001b[0m" }, updatedAt: null }, - { id: "t-odd", status: "unknown", activeFlags: [], name: null, preview: "", cwd: null, source: "cli", updatedAt: null }, + { + id: "t-x line", + status: "active", + activeFlags: ["waitingOnApproval"], + name: "a b", + preview: "keep\nnewlines", + cwd: "/r", + source: { custom: "hauler\u001b[0m" }, + createdAt: null, + updatedAt: null, + section: null, + projectId: null, + modelProvider: null, + model: null, + originator: null, + }, + { + id: "t-odd", + status: "unknown", + activeFlags: [], + name: null, + preview: "", + cwd: null, + source: "cli", + createdAt: null, + updatedAt: null, + section: null, + projectId: null, + modelProvider: null, + model: null, + originator: null, + }, ]); } finally { await fake.close(); @@ -1449,3 +1569,154 @@ for (const whenBusy of ["reject", "queue"]) { } }); } + +test("codex list-threads falls back to a full scan when useStateDbOnly returns nothing", async () => { + const seen = []; + const fake = await fakeAppServer({ + ...baseHandlers, + "thread/list": (params, ok) => { + seen.push(params); + if (params.useStateDbOnly) return ok({ data: [], nextCursor: null }); + ok({ data: [THREADS[0]], nextCursor: null }); + }, + }); + try { + const { code, out } = await gbot(fake.home, "codex", "list-threads", "--json"); + assert.equal(code, 0, out); + const page = JSON.parse(out); + assert.deepEqual(page.threads.map((t) => t.id), ["t-1"]); + assert.equal(page.useStateDbOnly, false); + assert.deepEqual(seen, [ + { limit: 20, modelProviders: [], useStateDbOnly: true }, + { limit: 20, modelProviders: [], useStateDbOnly: false }, + ]); + } finally { + await fake.close(); + } +}); + +test("codex send strips a Desktop local: thread id prefix before resume", async () => { + const fake = await fakeAppServer(baseHandlers); + try { + const { code, out } = await gbot( + fake.home, + "codex", + "send", + "local:t-1", + "hello", + "--json", + ); + assert.equal(code, 0, out); + const receipt = JSON.parse(out); + assert.equal(receipt.threadId, "t-1"); + const resume = fake.received.find((m) => m.method === "thread/resume"); + assert.deepEqual(resume.params, { threadId: "t-1", excludeTurns: true }); + const start = fake.received.find((m) => m.method === "turn/start"); + assert.equal(start.params.threadId, "t-1"); + } finally { + await fake.close(); + } +}); + +test("codex send maps remote-control thread not loaded to a typed host error", async () => { + const { copyFileSync } = await import("node:fs"); + const home = mkdtempSync(join(tmpdir(), "gbot-remote-send-")); + mkdirSync(home, { recursive: true }); + copyFileSync( + join(fileURLToPath(new URL(".", import.meta.url)), "../tests/fixtures/codex-global-state-remote.json"), + join(home, ".codex-global-state.json"), + ); + const fake = await fakeAppServer({ + ...baseHandlers, + "thread/resume": (params, ok, err) => + err({ code: -32600, message: "thread not loaded: " + params.threadId }), + }); + // Point CODEX_HOME at the fixture home for global-state lookup, but use the + // fake socket via CODEX_APP_SERVER_SOCK. + try { + const envHome = fake.home; + const { readFileSync, writeFileSync } = await import("node:fs"); + // Install the remote fixture into the fake home so findRemoteThreadHost sees it. + writeFileSync( + join(envHome, ".codex-global-state.json"), + readFileSync(join(home, ".codex-global-state.json")), + ); + const { code, out } = await gbot( + envHome, + "codex", + "send", + "local:aaaa1111-1111-1111-1111-111111111111", + "hi", + "--json", + ); + assert.equal(code, 1, out); + const failure = JSON.parse(out); + assert.equal(failure.reason, "remote-thread-not-loaded"); + assert.equal(failure.code, "REMOTE_THREAD_NOT_LOADED"); + assert.equal(failure.hostId, "host-macbook"); + assert.equal(failure.hostName, "Zack's MacBook"); + assert.match(failure.error, /host-macbook/); + assert.match(failure.hint, /host-macbook/); + assert.equal(failure.threadId, "aaaa1111-1111-1111-1111-111111111111"); + } finally { + await fake.close(); + } +}); + +test("readCodexThread uses thread/read without includeTurns and pages thread/turns/list", async () => { + const { readCodexThread } = await import("../src/core/codex-bridge.js"); + const seen = []; + const fake = await fakeAppServer({ + ...baseHandlers, + "thread/read": (params, ok) => { + seen.push({ method: "thread/read", params }); + ok({ thread: { id: params.threadId, name: "History", status: { type: "idle" } } }); + }, + "thread/turns/list": (params, ok) => { + seen.push({ method: "thread/turns/list", params }); + if (!params.cursor) { + return ok({ + data: [{ id: "turn-a", status: "completed", items: [] }], + nextCursor: "page-2", + }); + } + ok({ + data: [{ id: "turn-b", status: "completed", items: [] }], + nextCursor: null, + }); + }, + }); + try { + const out = await readCodexThread("local:t-1", { env: { CODEX_HOME: fake.home }, limit: 10 }); + assert.equal(out.threadId, "t-1"); + assert.equal(out.nextCursor, null); + assert.equal(out.thread.name, "History"); + assert.deepEqual(out.turns.map((t) => t.id), ["turn-a", "turn-b"]); + assert.deepEqual(seen[0], { method: "thread/read", params: { threadId: "t-1" } }); + assert.equal("includeTurns" in seen[0].params, false); + assert.deepEqual(seen[1].params, { + threadId: "t-1", + limit: 10, + itemsView: "full", + sortDirection: "desc", + }); + assert.deepEqual(seen[2].params, { + threadId: "t-1", + limit: 9, + itemsView: "full", + sortDirection: "desc", + cursor: "page-2", + }); + } finally { + await fake.close(); + } +}); + +test("normalizeCodexThreadId strips local: and rejects temporary Desktop rows", async () => { + const { normalizeCodexThreadId, isTemporaryDesktopThreadId } = await import("../src/core/codex-bridge.js"); + assert.equal(normalizeCodexThreadId("local:abc-123"), "abc-123"); + assert.equal(normalizeCodexThreadId("abc-123"), "abc-123"); + assert.equal(isTemporaryDesktopThreadId("local:client-new-thread:x"), true); + assert.throws(() => normalizeCodexThreadId("local:client-new-thread:x"), /Temporary Desktop/); + assert.throws(() => normalizeCodexThreadId("client-new-thread:x"), /Temporary Desktop/); +}); diff --git a/test/host-tool-inventory.test.js b/test/host-tool-inventory.test.js index a9cbd1c..8824daa 100644 --- a/test/host-tool-inventory.test.js +++ b/test/host-tool-inventory.test.js @@ -11,7 +11,20 @@ const config = JSON.parse(await readFile(new URL('../artifact/.mcp.json', import const entry = config.mcpServers['grok-bot'].args[0].replace('${CLAUDE_PLUGIN_ROOT}', resolve('artifact')); const codex = ['codex_new', 'codex_send', 'codex_threads', 'codex_wait', 'codex_watch', 'gbot_codex_respond']; const grok = ['gbot_send', 'gbot_thread', 'gbot_grok_approvals', 'gbot_grok_respond']; -const shared = ['claude_send', 'gbot_bridge_start', 'gbot_bridge_status', 'gbot_bridge_stop']; +const shared = [ + 'chatgpt_desktop_list_hosts', + 'chatgpt_desktop_list_threads', + 'chatgpt_desktop_open_thread', + 'chatgpt_desktop_read_thread', + 'chatgpt_desktop_search_threads', + 'chatgpt_desktop_send', + 'chatgpt_desktop_status', + 'chatgpt_desktop_wait_reply', + 'claude_send', + 'gbot_bridge_start', + 'gbot_bridge_status', + 'gbot_bridge_stop', +]; test('built MCP artifact exposes the other host tools and shared bridge controls', async () => { for (const [name, hidden] of [['codex_cli_rs', codex], ['Grok Bot', grok], ['Cursor', []]]) { @@ -28,3 +41,34 @@ test('built MCP artifact exposes the other host tools and shared bridge controls } } }); + +test('every host projection contains the Desktop skill and every Desktop MCP tool', async () => { + const root = resolve('artifact'); + const manifest = JSON.parse(await readFile(resolve(root, 'agent-bundle.manifest.json'), 'utf8')); + const skillPath = 'skills/chatgpt-desktop/SKILL.md'; + assert.ok(manifest.files.some((file) => file.path === skillPath)); + const skill = await readFile(resolve(root, skillPath), 'utf8'); + assert.match(skill, /^---\nname: chatgpt-desktop\n/); + const desktopTools = shared.filter((name) => name.startsWith('chatgpt_desktop_')); + const manifestTools = manifest.routes.servers.flatMap((server) => server.routes) + .map((route) => route.id?.split('/').at(-1)) + .filter((name) => name?.startsWith('chatgpt_desktop_')); + assert.deepEqual(manifestTools.sort(), [...desktopTools].sort()); + for (const projection of manifest.projections.filter((item) => ['claude', 'codex', 'cursor'].includes(item.host))) { + const plugin = JSON.parse(await readFile(resolve(root, projection.documents.plugin), 'utf8')); + if (projection.host !== 'claude') assert.equal(plugin.skills, './skills/'); + const mcp = JSON.parse(await readFile(resolve(root, projection.documents.mcp), 'utf8')); + const arg = mcp.mcpServers['grok-bot'].args[0]; + const hostEntry = arg.startsWith('./') + ? resolve(root, arg) + : arg.replace(/^\$\{(?:CLAUDE|CURSOR)_PLUGIN_ROOT\}/, root); + const client = new Client({ name: projection.host, version: '1.0.0' }); + try { + await client.connect(new StdioClientTransport({ command: process.execPath, args: [hostEntry], stderr: 'pipe' })); + const listed = new Set((await client.listTools()).tools.map((tool) => tool.name)); + for (const name of desktopTools) assert.ok(listed.has(name), `${projection.host} missing ${name}`); + } finally { + await client.close(); + } + } +}); diff --git a/tests/fixtures/codex-global-state-remote.json b/tests/fixtures/codex-global-state-remote.json new file mode 100644 index 0000000..6184382 --- /dev/null +++ b/tests/fixtures/codex-global-state-remote.json @@ -0,0 +1,56 @@ +{ + "remote-hosts-v1": { + "host-macbook": { + "name": "Zack's MacBook", + "envName": "desktop" + }, + "host-linux": { + "name": "build-box", + "envName": "ci" + } + }, + "remote-thread-summaries-v3:host-macbook": { + "hostName": "Zack's MacBook", + "pinnedIds": [ + "aaaa1111-1111-1111-1111-111111111111" + ], + "threads": [ + { + "id": "aaaa1111-1111-1111-1111-111111111111", + "title": "Pinned remote chat", + "projectId": "proj-launch", + "projectName": "Launch", + "updatedAt": 1700000100, + "createdAt": 1700000000, + "modelProvider": "openai", + "model": "gpt-5" + }, + { + "id": "bbbb2222-2222-2222-2222-222222222222", + "name": "Remote work thread", + "preview": "hello from remote", + "projectId": "proj-ops", + "updatedAt": 1700000200 + } + ] + }, + "remote-thread-summaries-v3:host-linux": [ + { + "id": "cccc3333-3333-3333-3333-333333333333", + "title": "Linux CI thread", + "project": "Infra", + "updatedAt": 1700000300 + } + ], + "unrelated-key": { + "should": "be ignored" + }, + "remote-thread-summaries-v3:broken-host": { + "threads": [ + { + "title": "missing id should be skipped" + }, + "dddd4444-4444-4444-4444-444444444444" + ] + } +} diff --git a/tests/route-unit/chatgpt-desktop-e2e-regressions.test.ts b/tests/route-unit/chatgpt-desktop-e2e-regressions.test.ts new file mode 100644 index 0000000..9ede747 --- /dev/null +++ b/tests/route-unit/chatgpt-desktop-e2e-regressions.test.ts @@ -0,0 +1,495 @@ +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, expect, it, rstest } from '@rstest/core'; +import { findRemoteThreadHost } from '../../src/core/codex/remote-control.js'; +import { listDiscoveredHosts, listRemoteThreadsFromState } from '../../src/core/chatgpt-desktop/remote-threads.js'; +import { ChatGptDesktopFacade, finalizeThreadList } from '../../src/core/chatgpt-desktop/facade.js'; +import { appServerListThreads, appServerReadThread, appServerSearchThreads, discoverModelProviders } from '../../src/core/chatgpt-desktop/app-server-fallback.js'; +import { DESKTOP_RESULT_BUDGET_BYTES, serializedBytes } from '../../src/core/chatgpt-desktop/payload-budget.js'; +import { findLocalProject, resolveThreadProjects } from '../../src/core/chatgpt-desktop/project-state.js'; +import { ARCHIVED_THREAD_EXPRESSION, CLEAR_NEW_CHAT_PROJECT_EXPRESSION, CONVERSATION_ID_EXPRESSION, LOADING_TASK_GONE_EXPRESSION, navigateToThreadExpression, openThreadInDom, readTurnsFromDom, startNewChatExpression, startNewChatInDom, waitForLoadingTaskGone } from '../../src/core/chatgpt-desktop/cdp-dom.js'; +import { listThreadsOperation, listThreadsSchema, readThreadOperation, readThreadSchema, searchThreadsSchema, statusOperation } from '../../src/core/chatgpt-desktop/routes.js'; +import { setChatGptDesktopAdapterForTests } from '../../src/core/chatgpt-desktop/facade.js'; +import type { ChatGptDesktopAdapter } from '../../src/core/chatgpt-desktop/adapter.js'; + +afterEach(() => { rstest.restoreAllMocks(); setChatGptDesktopAdapterForTests(null); }); + +it('reads nested remote summaries and SSH connections without inventing a display name', () => { + const home = mkdtempSync(join(tmpdir(), 'gbot-remote-state-')); + try { + writeFileSync(join(home, '.codex-global-state.json'), JSON.stringify({ + 'electron-persisted-atom-state': { + 'remote-thread-summaries-v3:remote-control:env_dynamic': [ + { conversationId: 'remote-thread', hostId: 'remote-control:env_dynamic', title: 'TD v2', modelProvider: 'openai' }, + ], + }, + 'codex-managed-remote-connections': [{ hostId: 'ssh:dynamic', displayName: 'ssh-box' }], + })); + const env = { CODEX_HOME: home }; + expect(listRemoteThreadsFromState(env)[0]).toMatchObject({ threadId: 'local:remote-thread', hostId: 'remote-control:env_dynamic', hostName: null }); + expect(findRemoteThreadHost('local:remote-thread', env)).toEqual({ hostId: 'remote-control:env_dynamic', hostName: null }); + expect(listDiscoveredHosts(env)).toEqual(expect.arrayContaining([ + expect.objectContaining({ hostId: 'remote-control:env_dynamic', hostName: null, threadCount: 1 }), + expect.objectContaining({ hostId: 'ssh:dynamic', hostName: 'ssh-box' }), + ])); + } finally { rmSync(home, { recursive: true, force: true }); } +}); + +it('joins a CDP row to remote ownership and reports mixed backends', () => { + const row = { threadId: 'local:remote-thread', title: 'TD v2', pinned: false, selected: true, kind: 'remote' }; + const out = finalizeThreadList({ backend: 'app-server+cdp', limit: 20, threads: [row] }, { + limit: 20, host: 'env_dynamic', remotes: [{ ...row, selected: false, location: 'remote', hostId: 'remote-control:env_dynamic', hostName: null }], + }); + expect(out).toMatchObject({ backend: 'app-server+cdp+remote-state', threads: [{ location: 'remote', hostId: 'remote-control:env_dynamic', selected: true }] }); +}); + +it('filters thread rows by dynamic project label or projectId', () => { + const row = (id: string, project: string, projectId: string) => ({ + threadId: `local:${id}`, title: id, project, projectId, pinned: false, selected: false, kind: 'codex', + }); + const base = { backend: 'app-server' as const, limit: 10, threads: [row('one', 'core', 'project-1'), row('two', 'other', 'project-2')] }; + expect(finalizeThreadList(base, { limit: 10, project: 'core', remotes: [] }).threads.map((thread) => thread.threadId)).toEqual(['local:one']); + expect(finalizeThreadList(base, { limit: 10, project: 'project-2', remotes: [] }).threads.map((thread) => thread.threadId)).toEqual(['local:two']); +}); + +it('resolves local and remote project labels from assignments, root hints, and cwd', () => { + const home = mkdtempSync(join(tmpdir(), 'gbot-project-state-')); + try { + writeFileSync(join(home, '.codex-global-state.json'), JSON.stringify({ + 'electron-persisted-atom-state': { + 'local-projects': { core: { id: 'core-id', name: 'core', rootPaths: ['/work/core'] }, + lra: { id: 'lra-id', name: 'codex-lra', rootPaths: ['/work/lra'] } }, + 'remote-projects': [{ id: 'remote-id', hostId: 'remote-host', remotePath: '/fast/core', label: 'remote-core' }], + 'thread-project-assignments': { assigned: { projectKind: 'local', projectId: 'core-id' }, + remote: { projectKind: 'remote', projectId: 'remote-id', hostId: 'remote-host' } }, + 'thread-workspace-root-hints': { hinted: '/work/lra/subfolder' }, + 'projectless-thread-ids': ['projectless'], + }, + })); + const row = (id: string, cwd: string) => ({ threadId: `local:${id}`, title: id, cwd, + pinned: false, selected: false, kind: 'codex' }); + const rows = resolveThreadProjects([row('assigned', '/elsewhere'), row('hinted', '/elsewhere'), + row('cwd', '/work/core/subfolder'), row('projectless', '/work/core'), + { ...row('remote', '/fast/core/subfolder'), location: 'remote', hostId: 'remote-host', hostName: null, + project: '/fast/core/subfolder' }], { CODEX_HOME: home }); + expect(rows.map((thread) => [thread.project, thread.projectId])).toEqual([ + ['core', 'core-id'], ['codex-lra', 'lra-id'], ['core', 'core-id'], [null, null], ['remote-core', 'remote-id'], + ]); + expect(rows[0].projectRootPath).toBe('/work/core'); + expect(findLocalProject('codex-lra', { CODEX_HOME: home })).toMatchObject({ id: 'lra-id', rootPaths: ['/work/lra'] }); + const filtered = finalizeThreadList({ backend: 'app-server', limit: 10, threads: rows }, + { limit: 10, project: 'core', remotes: [] }); + expect(filtered.threads.map((thread) => thread.threadId)).toEqual(['local:assigned', 'local:cwd', 'local:remote']); + expect(finalizeThreadList({ backend: 'app-server', limit: 10, threads: rows }, + { limit: 10, project: 'lra-id', remotes: [] }).threads.map((thread) => thread.threadId)).toEqual(['local:hinted']); + } finally { rmSync(home, { recursive: true, force: true }); } +}); + +it('counts CDP-only remote rows in host discovery', async () => { + const row = { threadId: 'local:cdp-only', title: 'remote', pinned: false, selected: false, kind: 'remote', location: 'remote' as const, hostId: 'remote:dynamic', hostName: null }; + const local = { threadId: 'local:local-cdp-only', title: 'local', pinned: false, selected: false, kind: 'codex', location: 'local' as const }; + const facade = new ChatGptDesktopFacade({ + connect: async () => {}, listThreads: async () => ({ backend: 'cdp', limit: 200, threads: [row, local] }), + } as unknown as ChatGptDesktopAdapter, 9222, { + listThreads: async () => ({ backend: 'app-server', limit: 25, threads: [] }), + listRemoteThreads: () => [], listHosts: () => [{ hostId: 'local', hostName: 'local', location: 'local', threadCount: 0 }], + discoverModelProviders: async () => ({ providers: [], source: 'thread/list-distinct', threadCount: 0, complete: true }), + } as never); + expect((await facade.listHosts()).hosts).toEqual(expect.arrayContaining([expect.objectContaining({ hostId: 'remote:dynamic', threadCount: 1 })])); + expect((await facade.listHosts()).hosts.find((host) => host.hostId === 'local')?.threadCount).toBe(1); +}); + +it('uses the same deduplicated remote rows for host count and host-filtered list', async () => { + const row = (id: string) => ({ threadId: `local:${id}`, title: id, pinned: false, selected: false, + kind: 'remote', location: 'remote' as const, hostId: 'remote:one', hostName: null }); + const facade = new ChatGptDesktopFacade({ + connect: async () => {}, listThreads: async () => ({ backend: 'cdp', limit: 200, + threads: [row('shared'), row('cdp-only')] }), + } as unknown as ChatGptDesktopAdapter, 9222, { + listThreads: async () => ({ backend: 'app-server', limit: 25, threads: [] }), + listRemoteThreads: () => [row('shared'), row('state-only')], + listHosts: () => [{ hostId: 'remote:one', hostName: 'host'.repeat(2000), location: 'remote', threadCount: 2 }], + discoverModelProviders: async () => ({ providers: [], source: 'thread/list-distinct', threadCount: 0, complete: true }), + } as never); + const hosts = await facade.listHosts(); + const listed = await facade.listThreads({ host: 'remote:one', limit: 100 }); + expect(hosts.hosts.find((host) => host.hostId === 'remote:one')?.threadCount).toBe(listed.threads.length); + expect(hosts.hosts.find((host) => host.hostId === 'remote:one')).toMatchObject({ hostNameTruncated: true }); + expect(listed.threads.map((thread) => thread.threadId).sort()).toEqual(['local:cdp-only', 'local:shared', 'local:state-only']); +}); + +it('surfaces provider discovery errors with the remote fallback', async () => { + const facade = new ChatGptDesktopFacade({} as ChatGptDesktopAdapter, 9222, { + listThreads: async () => ({ backend: 'app-server', limit: 25, threads: [] }), + listRemoteThreads: () => [], listHosts: () => [], + discoverModelProviders: async () => { throw new Error('frame too large'); }, + } as never); + expect((await facade.listHosts()).warnings).toEqual(expect.arrayContaining(['Model provider discovery failed: frame too large'])); +}); + +it('uses small app-server pages for provider discovery and search', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + const limits: number[] = []; + rstest.spyOn(bridge, 'listCodexThreads').mockImplementation(async ({ limit, cursor } = {}) => { + limits.push(limit!); + return { limit: limit!, nextCursor: cursor ? null : 'more', threads: [{ id: cursor ? 'b' : 'a', name: cursor ? 'needle' : 'first', modelProvider: cursor ? 'second-provider' : 'first-provider' }] } as never; + }); + expect((await discoverModelProviders()).providers).toEqual(['first-provider', 'second-provider']); + expect((await appServerSearchThreads({ query: 'needle' })).threads).toHaveLength(1); + expect(limits).toEqual([25, 25, 25, 25]); +}); + +it('pages full turn reads, extracts user content and completedAt, and keeps stable keys', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + const limits: number[] = []; + rstest.spyOn(bridge, 'openCodexSession').mockResolvedValue({ client: { + close() {}, + async request(method: string, params: Record) { + if (method === 'thread/read') return { thread: { id: 'long-thread' } }; + limits.push(params.limit as number); + const id = params.cursor ? 'second' : 'first'; + return { data: [{ id, startedAt: 10, completedAt: 20, items: [ + { type: 'userMessage', content: [{ type: 'text', text: `ask ${id}` }] }, + { type: 'agentMessage', text: `answer ${id}` }, + ] }], nextCursor: params.cursor ? null : 'more' }; + }, + } } as never); + const out = await appServerReadThread({ threadId: 'long-thread', full: true, limit: 20 }); + expect(out.complete).toBe(true); + expect(out.turns).toMatchObject([ + { turnKey: 'history-content:turn:first', userText: 'ask first', assistantText: 'answer first', endedAt: 20 }, + { turnKey: 'history-content:turn:second', userText: 'ask second', endedAt: 20 }, + ]); + expect(limits).toEqual([10, 10]); +}); + +it('completes a 146-turn full read in bounded pages', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + const pages: number[] = []; + rstest.spyOn(bridge, 'openCodexSession').mockResolvedValue({ client: { + close() {}, + async request(method: string, params: Record) { + if (method === 'thread/read') return { thread: { id: 'long-thread' } }; + const offset = Number(params.cursor ?? 0); + const size = Math.min(params.limit as number, 146 - offset); + pages.push(params.limit as number); + return { data: Array.from({ length: size }, (_, i) => ({ id: `turn-${offset + i}`, items: [] })), + nextCursor: offset + size < 146 ? String(offset + size) : null }; + }, + } } as never); + const out = await appServerReadThread({ threadId: 'long-thread', full: true, limit: 200 }); + expect(out.turns).toHaveLength(146); + expect(out.complete).toBe(true); + expect(pages).toHaveLength(15); + expect(Math.max(...pages)).toBe(10); +}); + +it('pages a serialized history above the document budget without losing turns', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + rstest.spyOn(bridge, 'openCodexSession').mockResolvedValue({ client: { + close() {}, + async request(method: string, params: Record) { + if (method === 'thread/read') return { thread: { id: 'huge' } }; + const offset = Number(params.cursor ?? 0); + return { data: Array.from({ length: Math.min(10, 30 - offset) }, (_, i) => ({ + id: `turn-${offset + i}`, items: [{ type: 'agentMessage', text: 'x'.repeat(30_000) }], + })), nextCursor: offset + 10 < 30 ? String(offset + 10) : null }; + }, + } } as never); + const keys: string[] = []; + let cursor: string | undefined; + for (let page = 0; page < 20; page++) { + const out = await appServerReadThread({ threadId: 'huge', full: true, cursor }); + expect(serializedBytes(out)).toBeLessThan(DESKTOP_RESULT_BUDGET_BYTES); + keys.push(...out.turns.map((turn) => turn.turnKey)); + if (out.complete) break; + expect(out.nextCursor).toBeTruthy(); + cursor = out.nextCursor!; + } + expect(keys).toEqual(Array.from({ length: 30 }, (_, i) => `history-content:turn:turn-${i}`)); +}); + +it('splits one oversized turn into lossless ordered field fragments with a stable key', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + const userText = 'u'.repeat(700_000); + const assistantText = 'a'.repeat(700_000); + rstest.spyOn(bridge, 'openCodexSession').mockResolvedValue({ client: { + close() {}, + async request(method: string) { + if (method === 'thread/read') return { thread: { id: 'giant' } }; + return { data: [{ id: 'giant-turn', items: [ + { type: 'userMessage', text: userText }, { type: 'agentMessage', text: assistantText }, + ] }], nextCursor: null }; + }, + } } as never); + const fields = { text: '', userText: '', assistantText: '' }; + let cursor: string | undefined; + for (let page = 0; page < 100; page++) { + const out = await appServerReadThread({ threadId: 'giant', full: true, cursor }); + expect(serializedBytes(out)).toBeLessThan(DESKTOP_RESULT_BUDGET_BYTES); + for (const turn of out.turns) { + expect(turn.turnKey).toBe('history-content:turn:giant-turn'); + expect(turn.textTruncated).toBe(true); + const field = turn.continuation!.field; + expect(turn.continuation!.offsetChars).toBe(fields[field].length); + fields[field] += turn[field] ?? ''; + } + if (out.complete) break; + cursor = out.nextCursor!; + } + expect(fields).toEqual({ text: assistantText, userText, assistantText }); +}); + +it('compacts oversized list titles across pages and searches the original full title', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + const title = `${'x'.repeat(7_400)} unique-tail`; + rstest.spyOn(bridge, 'listCodexThreads').mockImplementation(async ({ cursor } = {}) => { + const offset = Number(cursor ?? 0); + return { limit: 25, threads: Array.from({ length: Math.min(25, 60 - offset) }, (_, i) => ({ + id: `id-${offset + i}`, name: title, + })), nextCursor: offset + 25 < 60 ? String(offset + 25) : null } as never; + }); + const ids: string[] = []; + let cursor: string | undefined; + for (let page = 0; page < 10; page++) { + const out = await appServerListThreads({ limit: 100, cursor }); + expect(serializedBytes(out)).toBeLessThan(DESKTOP_RESULT_BUDGET_BYTES); + expect(out.threads.every((row) => row.title.length <= 200 && row.titleTruncated)).toBe(true); + ids.push(...out.threads.map((row) => row.threadId)); + if (!out.nextCursor) break; + cursor = out.nextCursor; + } + expect(ids).toEqual(Array.from({ length: 60 }, (_, i) => `local:id-${i}`)); + const search = await appServerSearchThreads({ query: 'unique-tail', limit: 10 }); + expect(search.threads).toHaveLength(10); + expect(search.threads[0].titleTruncated).toBe(true); + expect((await appServerSearchThreads({ query: 'unique-tail', limit: 10, cursor: search.nextCursor! })).threads[0].threadId).toBe('local:id-10'); + expect(searchThreadsSchema.parse({ query: 'unique-tail', cursor: search.nextCursor }).cursor).toBe(search.nextCursor); + expect(readThreadSchema.parse({ threadId: 'giant', cursor: 'opaque' }).cursor).toBe('opaque'); +}); + +it('defaults full reads to the bounded complete-history limit', async () => { + const limits: number[] = []; + setChatGptDesktopAdapterForTests({ + connect: async () => {}, close: async () => {}, + readThread: async (options: { threadId: string; limit?: number; full?: boolean }) => { + limits.push(options.limit!); + return { threadId: options.threadId, turns: [], backend: 'app-server', limit: options.limit!, full: Boolean(options.full), complete: true }; + }, + } as unknown as ChatGptDesktopAdapter); + await readThreadOperation(readThreadSchema.parse({ threadId: 'example', full: true })); + await readThreadOperation(readThreadSchema.parse({ threadId: 'example' })); + expect(limits).toEqual([2000, 100]); +}); + +it('marks a CDP read partial and preserves the app-server failure', async () => { + const facade = new ChatGptDesktopFacade({ + connect: async () => {}, + readThread: async () => ({ threadId: 'local:example', turns: [], backend: 'cdp', limit: 100, full: true }), + } as unknown as ChatGptDesktopAdapter, 9222, { + readThread: async () => { throw new Error('frame exceeds 16 MiB'); }, + } as never); + expect(await facade.readThread({ threadId: 'local:example', full: true })).toMatchObject({ + backend: 'cdp', complete: false, warnings: ['App-server read failed: frame exceeds 16 MiB'], + }); +}); + +it('falls back to CDP when a local thread is absent from app-server and remote state', async () => { + const home = mkdtempSync(join(tmpdir(), 'gbot-local-read-')); + const previousHome = process.env.CODEX_HOME; + process.env.CODEX_HOME = home; + try { + const bridge = await import('../../src/core/codex-bridge.js'); + rstest.spyOn(bridge, 'openCodexSession').mockResolvedValue({ client: { + close() {}, + async request() { throw new Error('no rollout found'); }, + } } as never); + const facade = new ChatGptDesktopFacade({ connect: async () => {}, + readThread: async () => ({ threadId: 'local:desktop-only', turns: [], backend: 'cdp', limit: 100, full: false }), + } as unknown as ChatGptDesktopAdapter, 9222, { readThread: appServerReadThread } as never); + expect(await facade.readThread({ threadId: 'local:desktop-only' })).toMatchObject({ + backend: 'cdp', warnings: ['App-server read failed: no rollout found'], + }); + } finally { + if (previousHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previousHome; + rmSync(home, { recursive: true, force: true }); + } +}); + +it('walks every app-server search match when a remote row displaces a full first page', async () => { + const row = (id: string, updatedAt: number) => ({ threadId: `local:${id}`, title: `needle ${id}`, + updatedAt, pinned: false, selected: false, kind: 'codex' }); + const app = [row('a', 3), row('b', 2), row('c', 1)]; + const remote = { ...row('remote', 4), location: 'remote' as const, hostId: 'remote:one', hostName: null }; + const facade = new ChatGptDesktopFacade({ connect: async () => {}, + listThreads: async () => ({ backend: 'cdp', limit: 200, threads: [] }), + } as unknown as ChatGptDesktopAdapter, 9222, { + searchThreads: async ({ cursor }: { cursor?: string }) => ({ backend: 'app-server', limit: 2, + threads: cursor ? app.slice(2) : app.slice(0, 2), nextCursor: cursor ? null : 'next' }), + listRemoteThreads: () => [remote], + } as never); + const ids: string[] = []; + let cursor: string | null | undefined; + do { + const page = await facade.searchThreads({ query: 'needle', limit: 2, ...(cursor ? { cursor } : {}) }); + ids.push(...page.threads.map((thread) => thread.threadId)); + cursor = page.nextCursor; + } while (cursor); + expect(ids).toEqual(['local:remote', 'local:a', 'local:b', 'local:c']); +}); + +it('reports only CDP and remote state when app-server inventory fails', async () => { + const row = (id: string) => ({ threadId: `local:${id}`, title: id, + pinned: false, selected: false, kind: 'codex' }); + const remote = { ...row('remote'), location: 'remote' as const, hostId: 'remote:one', hostName: null }; + const facade = new ChatGptDesktopFacade({ connect: async () => {}, + listThreads: async () => ({ backend: 'cdp', limit: 200, threads: [row('cdp')] }), + } as unknown as ChatGptDesktopAdapter, 9222, { + listThreads: async () => { throw new Error('offline'); }, + listRemoteThreads: () => [remote], listHosts: () => [], + discoverModelProviders: async () => ({ providers: [], source: 'thread/list-distinct', complete: true }), + } as never); + expect((await facade.listThreads()).backend).toBe('cdp+remote-state'); + expect((await facade.listHosts()).backend).toBe('cdp+remote-state'); +}); + +it('pages one sorted merged inventory exactly once across app, CDP and remote rows', async () => { + const row = (id: string, updatedAt: number) => ({ threadId: `local:${id}`, title: id, + updatedAt, pinned: false, selected: false, kind: 'codex' }); + const app = [row('a', 900), row('b', 800), row('c', 700), row('d', 600), row('e', 500), row('f', 400)]; + const cdp = [{ ...row('a', 900), selected: true }, { ...row('c', 700), location: 'remote' as const, hostId: 'host', hostName: null }, row('sidebar-only', 550)]; + const remote = [{ ...row('c', 700), location: 'remote' as const, hostId: 'host', hostName: null }, + { ...row('remote-only', 750), location: 'remote' as const, hostId: 'host', hostName: null }]; + const calls: unknown[] = []; + const facade = new ChatGptDesktopFacade({ connect: async () => {}, + listThreads: async () => ({ backend: 'cdp', limit: 200, threads: cdp }) } as unknown as ChatGptDesktopAdapter, 9222, { + listThreads: async ({ cursor }: { cursor?: string }) => { + calls.push(cursor); + const offset = Number(cursor ?? 0); + return { backend: 'app-server', limit: 200, threads: app.slice(offset, offset + 2), + nextCursor: offset + 2 < app.length ? String(offset + 2) : null }; + }, + listRemoteThreads: () => remote, + } as never); + const pages = []; + let cursor: string | null | undefined; + do { + const out = await facade.listThreads({ limit: 3, ...(cursor ? { cursor } : {}) }); + pages.push(out); + cursor = out.nextCursor; + } while (cursor); + expect(pages.map((page) => page.threads.length)).toEqual([3, 3, 2]); + expect(pages.flatMap((page) => page.threads.map((thread) => thread.threadId))).toEqual([ + 'local:a', 'local:b', 'local:remote-only', 'local:c', 'local:d', 'local:sidebar-only', 'local:e', 'local:f', + ]); + expect(pages[1].threads.find((thread) => thread.threadId === 'local:c')?.location).toBe('remote'); + expect(calls).toEqual([undefined, '2', '4']); + await expect(facade.listThreads({ limit: 3, cursor: pages[0].nextCursor!, host: 'local' })).rejects.toThrow('filters'); + expect(listThreadsSchema.parse({ cursor: pages[0].nextCursor }).cursor).toBe(pages[0].nextCursor); + setChatGptDesktopAdapterForTests({ connect: async () => {}, close: async () => {}, + listThreads: async (options: { cursor?: string } = {}) => ({ backend: 'app-server', limit: 1, nextCursor: options.cursor, threads: [] }) } as unknown as ChatGptDesktopAdapter); + expect(await listThreadsOperation(listThreadsSchema.parse({ cursor: pages[0].nextCursor }))).toMatchObject({ nextCursor: pages[0].nextCursor, exitCode: 0 }); +}); + +it('fills a 100-row app-server page using bounded 25-row RPC requests', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + const limits: number[] = []; + rstest.spyOn(bridge, 'listCodexThreads').mockImplementation(async ({ limit, cursor } = {}) => { + limits.push(limit!); + const offset = Number(cursor ?? 0); + return { limit: limit!, threads: Array.from({ length: Math.min(limit!, 120 - offset) }, (_, i) => ({ + id: `id-${offset + i}`, name: `title-${offset + i}` })), + nextCursor: offset + limit! < 120 ? String(offset + limit!) : null } as never; + }); + const first = await appServerListThreads({ limit: 100 }); + const second = await appServerListThreads({ limit: 100, cursor: first.nextCursor! }); + expect([first.threads.length, second.threads.length]).toEqual([100, 20]); + expect([...first.threads, ...second.threads].map((row) => row.threadId)).toEqual( + Array.from({ length: 120 }, (_, i) => `local:id-${i}`)); + expect(limits).toEqual([25, 25, 25, 25, 25]); +}); + +it('reports CDP unreachable even when app-server is reachable', async () => { + const facade = new ChatGptDesktopFacade({ status: async () => ({ reachable: false, host: '127.0.0.1', port: 9333, message: 'CDP down', exitCode: 1 }) } as unknown as ChatGptDesktopAdapter, 9333, { statusProbe: async () => ({ reachable: true }) } as never); + expect(await facade.status()).toMatchObject({ reachable: false, exitCode: 1, appServerFallback: { reachable: true } }); + setChatGptDesktopAdapterForTests({ connect: async () => {}, close: async () => {}, status: async () => facade.status() } as unknown as ChatGptDesktopAdapter); + expect(await statusOperation({})).toMatchObject({ reachable: false, exitCode: 1 }); +}); + +it('hovers then dispatches a real project button press and release', async () => { + expect(startNewChatExpression('core')).toContain('scrollIntoView'); + const sent: string[] = []; + const session = { + evaluate: async (expression: string) => expression.includes('getComputedStyle') + ? { x: 12, y: 14, visible: true, disabled: false } + : expression.includes('const expected =') ? { ready: true, path: '/', composer: true, scope: 'Change project: core' } + : { ok: true, via: 'project', hoverX: 8, hoverY: 10 }, + send: async (_method: string, params: { type: string }) => { sent.push(params.type); }, + }; + await startNewChatInDom(session as never, 'page', { project: 'core' }); + expect(sent).toEqual(['mouseMoved', 'mouseMoved', 'mousePressed', 'mouseReleased']); +}); + +it('rejects a disabled project action before sending and reports a failed navigation quickly', async () => { + const sent: string[] = []; + const disabled = { evaluate: async () => ({ ok: true, via: 'project', disabled: true, hoverX: 8, hoverY: 10 }), + send: async (method: string) => { sent.push(method); } }; + await expect(startNewChatInDom(disabled as never, 'page', { project: 'missing-root' })).rejects.toMatchObject({ code: 'PROJECT_UNAVAILABLE' }); + expect(sent).toEqual([]); + const noNavigation = { evaluate: async (expression: string) => expression.includes('const expected =') + ? { ready: false, path: '/local/old', composer: false, scope: '' } + : expression.includes('getComputedStyle') ? { x: 12, y: 14, visible: true, disabled: false } + : { ok: true, via: 'project', hoverX: 8, hoverY: 10 }, send: async () => {} }; + await expect(startNewChatInDom(noNavigation as never, 'page', { project: 'core', navigationTimeoutMs: 20 })) + .rejects.toMatchObject({ code: 'NEW_CHAT_NAVIGATION_FAILED' }); +}); + +it('clears a retained project before treating a new chat as projectless', async () => { + let cleared = false; + const session = { evaluate: async (expression: string) => { + if (expression === CLEAR_NEW_CHAT_PROJECT_EXPRESSION) { cleared = true; return true; } + if (expression.includes('const expected =')) return { ready: cleared, path: '/', composer: true, + scope: cleared ? 'Choose project' : 'Change project: core' }; + return { ok: true, via: 'fallback' }; + } }; + expect(await startNewChatInDom(session as never, 'page')).toEqual({ via: 'fallback' }); + expect(cleared).toBe(true); +}); + +it('uses the displayed durable id before sidebar lookup and routes missing rows', async () => { + const expressions: string[] = []; + const session = { evaluate: async (expression: string) => { + expressions.push(expression); + if (expression === CONVERSATION_ID_EXPRESSION) return expressions.length === 1 ? 'already-open' : ''; + if (expression.includes('router.navigate')) return { ok: true }; + return { ok: false, error: 'thread-not-found' }; + } }; + await openThreadInDom(session as never, 'page', 'local:already-open'); + expect(expressions).toEqual([CONVERSATION_ID_EXPRESSION]); + await openThreadInDom(session as never, 'page', 'local:missing-row'); + expect(expressions.at(-1)).toContain("/local/missing-row"); + expect(navigateToThreadExpression('local:missing-row')).toContain('router.navigate'); +}); + +it('orders full CDP history oldest first and deduplicates stable turn keys', async () => { + let harvests = 0; + const session = { + evaluate: async (expression: string) => { + if (expression.includes('getBoundingClientRect')) return { ok: true, x: 10, y: 10 }; + if (expression.includes('gapPrefix')) return harvests++ === 0 + ? [{ turnKey: 'history-content:turn:new', role: 'assistant', text: 'new' }] + : [{ turnKey: 'history-content:turn:old', role: 'user', text: 'old' }, { turnKey: 'history-content:turn:new', role: 'assistant', text: 'new' }]; + return {}; + }, + send: async () => {}, + }; + const turns = await readTurnsFromDom(session as never, 'page', { full: true, limit: 10, idleWheels: 1, maxWheels: 2 }); + expect(turns.map((turn) => turn.turnKey)).toEqual(['history-content:turn:old', 'history-content:turn:new']); +}); + +it('reports an archived route distinctly from a loading timeout', async () => { + const session = { evaluate: async (expression: string) => expression === ARCHIVED_THREAD_EXPRESSION ? true : expression === LOADING_TASK_GONE_EXPRESSION }; + await expect(waitForLoadingTaskGone(session as never, 'page', { threadId: 'local:archived', timeoutMs: 1000 })).rejects.toThrow('archived'); +}); diff --git a/tests/route-unit/chatgpt-desktop-mcp-followup.test.ts b/tests/route-unit/chatgpt-desktop-mcp-followup.test.ts new file mode 100644 index 0000000..5ccae37 --- /dev/null +++ b/tests/route-unit/chatgpt-desktop-mcp-followup.test.ts @@ -0,0 +1,190 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, expect, it, rstest } from '@rstest/core'; +import { invokeMcpTool, listMcpSurface } from 'agent-bundle/test'; +import { CdpSession } from '../../src/core/chatgpt-desktop/cdp-session.js'; +import { CdpChatGptDesktopAdapter } from '../../src/core/chatgpt-desktop/cdp-adapter.js'; +import { ARCHIVED_THREAD_EXPRESSION, CONVERSATION_ID_EXPRESSION, FOCUS_COMPOSER_EXPRESSION, LIST_THREADS_EXPRESSION, LOADING_TASK_GONE_EXPRESSION, REPLY_STATE_EXPRESSION, focusComposer, startNewChatExpression } from '../../src/core/chatgpt-desktop/cdp-dom.js'; +import { ArchivedThreadError, ComposerDraftError } from '../../src/core/chatgpt-desktop/errors.js'; +import { ChatGptDesktopFacade, setChatGptDesktopAdapterForTests } from '../../src/core/chatgpt-desktop/facade.js'; +import { appServerUnarchiveThread } from '../../src/core/chatgpt-desktop/app-server-fallback.js'; +import { listThreadsOperation, listThreadsSchema, openThreadOperation, openThreadSchema, sendOperation, sendSchema } from '../../src/core/chatgpt-desktop/routes.js'; +import type { ChatGptDesktopAdapter } from '../../src/core/chatgpt-desktop/adapter.js'; + +afterEach(() => { rstest.restoreAllMocks(); setChatGptDesktopAdapterForTests(null); }); + +function mockCdp(evaluate: (expression: string) => unknown, onSend?: (method: string, params?: Record) => void) { + rstest.spyOn(CdpSession.prototype, 'connect').mockResolvedValue({}); + rstest.spyOn(CdpSession.prototype, 'closed', 'get').mockReturnValue(false); + rstest.spyOn(CdpSession.prototype, 'send').mockImplementation(async (method, params) => { + onSend?.(method, params); + return (method === 'Target.getTargets' + ? { targetInfos: [{ targetId: 'main', type: 'page', url: 'app://-/index.html' }] } + : { sessionId: 'page' }) as never; + }); + rstest.spyOn(CdpSession.prototype, 'evaluate').mockImplementation(async (expression) => evaluate(expression) as never); +} + +it('returns archived:true after navigating an archived thread route without unarchiving', async () => { + let navigated = false; + mockCdp((expression) => { + if (expression === CONVERSATION_ID_EXPRESSION) return ''; + if (expression.includes('router.navigate')) { navigated = true; return { ok: true }; } + if (expression.includes('rows.find((el)')) return { ok: false, error: 'thread-not-found' }; + if (expression === ARCHIVED_THREAD_EXPRESSION) return true; + if (expression === LOADING_TASK_GONE_EXPRESSION) return true; + return null; + }); + const adapter = new CdpChatGptDesktopAdapter(); + await adapter.connect({ port: 9222 }); + expect(await adapter.openThread('local:archived', { openTimeoutMs: 1500 })).toMatchObject({ threadId: 'local:archived', archived: true }); + expect(navigated).toBe(true); +}); + +it('reads an archived thread through app-server and exposes an MCP open operation', async () => { + const facade = new ChatGptDesktopFacade({ + connect: async () => {}, close: async () => {}, + openThread: async (threadId: string) => ({ threadId, backend: 'cdp', archived: true }), + } as unknown as ChatGptDesktopAdapter, 9222, { + readThread: async ({ threadId }: { threadId: string }) => ({ threadId, backend: 'app-server', turns: [], limit: 100, full: false, complete: true }), + } as never); + expect(await facade.readThread({ threadId: 'local:archived' })).toMatchObject({ backend: 'app-server', complete: true }); + setChatGptDesktopAdapterForTests(facade); + expect(await openThreadOperation(openThreadSchema.parse({ threadId: 'local:archived' }))).toMatchObject({ archived: true, exitCode: 0 }); + expect((await listMcpSurface({ server: 'grok-bot' })).tools).toContain('chatgpt_desktop_open_thread'); + const opened = await invokeMcpTool('chatgpt_desktop_open_thread', { server: 'grok-bot', input: { threadId: 'local:archived' } }); + expect(opened.isError).toBe(false); +}); + +it('rejects archived send with a specific code and unarchives only on explicit opt-in', async () => { + let archived = true; + let unarchived = 0; + let sent = 0; + const facade = new ChatGptDesktopFacade({ + connect: async () => {}, close: async () => {}, + sendMessage: async ({ threadId }: { threadId?: string }) => { + if (archived) throw new ArchivedThreadError(threadId!); + sent++; + return { threadId: threadId!, backend: 'cdp', delivery: 'accepted', experimental: false }; + }, + } as unknown as ChatGptDesktopAdapter, 9222, { + unarchiveThread: async () => { unarchived++; archived = false; }, + } as never); + setChatGptDesktopAdapterForTests(facade); + expect(await sendOperation(sendSchema.parse({ threadId: 'local:archived', text: 'hello' }))).toMatchObject({ + code: 'ARCHIVED_THREAD', reason: 'archived-thread', delivery: 'rejected', exitCode: 1, + }); + expect([unarchived, sent]).toEqual([0, 0]); + expect(await sendOperation(sendSchema.parse({ threadId: 'local:archived', text: 'hello', unarchive: true }))).toMatchObject({ + delivery: 'accepted', exitCode: 0, + }); + expect([unarchived, sent]).toEqual([1, 1]); + expect(() => sendSchema.parse({ text: 'new chat', unarchive: true })).toThrow('unarchive requires an existing threadId'); + expect(() => sendSchema.parse({ text: 'ambiguous', threadId: 'local:existing', project: 'core' })).toThrow('project is only for a new thread'); +}); + +it('calls thread/unarchive with the bare id and validates its response', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + const calls: Array<[string, unknown]> = []; + rstest.spyOn(bridge, 'openCodexSession').mockResolvedValue({ client: { + close() {}, + async request(method: string, params: unknown) { calls.push([method, params]); return { thread: { id: 'archived' } }; }, + } } as never); + await appServerUnarchiveThread('local:archived'); + expect(calls).toEqual([['thread/unarchive', { threadId: 'archived' }]]); +}); + +it('rejects an unarchive response for another thread before retrying a send', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + rstest.spyOn(bridge, 'openCodexSession').mockResolvedValue({ client: { + close() {}, async request() { return { thread: { id: 'wrong-thread' } }; }, + } } as never); + await expect(appServerUnarchiveThread('local:archived')).rejects.toThrow('returned no thread'); +}); + +it('returns a durable id after one new-thread-in-project send while preserving the temporary id', async () => { + let submitted = false; + const events: string[] = []; + mockCdp((expression) => { + if (expression.includes('projectSel')) return { ok: true, via: 'project', hoverX: 8, hoverY: 10 }; + if (expression.includes('getComputedStyle')) return { x: 12, y: 14, visible: true, disabled: false }; + if (expression.includes('const expected =')) return { ready: true, path: '/', composer: true, scope: 'Change project: core' }; + if (expression === LOADING_TASK_GONE_EXPRESSION) return true; + if (expression === CONVERSATION_ID_EXPRESSION) return submitted ? 'durable-conversation' : ''; + if (expression === LIST_THREADS_EXPRESSION) return [{ threadId: 'local:client-new-thread:temporary', title: 'new', selected: true, pinned: false, kind: 'local' }]; + if (expression === REPLY_STATE_EXPRESSION) return { stopVisible: false, finalAssistantCount: 0, conversationId: '', reply: '', lastTurnKey: 'before' }; + if (expression === FOCUS_COMPOSER_EXPRESSION) return { ok: true }; + if (expression.includes('return Boolean(composer')) return true; + return false; + }, (method, params) => { + if (method === 'Input.dispatchMouseEvent' || method === 'Input.dispatchKeyEvent') events.push(String(params?.type)); + if (method === 'Input.dispatchKeyEvent' && params?.type === 'keyUp') submitted = true; + }); + const adapter = new CdpChatGptDesktopAdapter(); + await adapter.connect({ port: 9222 }); + const receipt = await adapter.sendMessage({ text: 'one message', project: 'core', openTimeoutMs: 1000 }); + expect(receipt).toMatchObject({ + delivery: 'accepted', threadId: 'local:durable-conversation', conversationId: 'durable-conversation', + temporaryThreadId: 'local:client-new-thread:temporary', project: 'core', + }); + expect(events.filter((event) => event === 'mousePressed')).toHaveLength(1); + expect(events.filter((event) => event === 'keyUp')).toHaveLength(1); +}); + +it('rejects a project whose configured root is missing before touching the composer', async () => { + const home = mkdtempSync(join(tmpdir(), 'gbot-unavailable-project-')); + const previous = process.env.CODEX_HOME; + let inserted = false; + try { + writeFileSync(join(home, '.codex-global-state.json'), JSON.stringify({ + 'electron-persisted-atom-state': { 'local-projects': { + missing: { id: 'missing', name: 'missing-project', rootPaths: ['/no/such/project/root'] }, + } }, + })); + process.env.CODEX_HOME = home; + mockCdp(() => false, (method) => { if (method === 'Input.insertText') inserted = true; }); + const adapter = new CdpChatGptDesktopAdapter(); + await adapter.connect({ port: 9222 }); + await expect(adapter.sendMessage({ project: 'missing-project', text: 'must not send' })) + .rejects.toMatchObject({ code: 'PROJECT_UNAVAILABLE', delivery: 'rejected' }); + expect(inserted).toBe(false); + } finally { + if (previous === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previous; + rmSync(home, { recursive: true, force: true }); + } +}); + +it('passes the dynamic project filter through the MCP list contract', async () => { + const calls: string[] = []; + setChatGptDesktopAdapterForTests({ + connect: async () => {}, close: async () => {}, + listThreads: async (options: { project?: string }) => { + calls.push(options.project!); + return { backend: 'app-server', limit: 50, threads: [] }; + }, + } as unknown as ChatGptDesktopAdapter); + expect(await listThreadsOperation(listThreadsSchema.parse({ project: 'core' }))).toMatchObject({ exitCode: 0 }); + expect(calls).toEqual(['core']); +}); + +it('selects the generic New chat action when no project is supplied', () => { + let clicked = 0; + const document = { querySelectorAll: () => [{ textContent: 'New chat', click: () => { clicked++; } }] }; + const result = new Function('document', `return ${startNewChatExpression()}`)(document); + expect(result).toEqual({ ok: true, via: 'fallback' }); + expect(clicked).toBe(1); +}); + +it('returns a typed draft error before any text insertion', async () => { + const session = { evaluate: async () => ({ ok: false, error: 'composer-has-draft' }) }; + await expect(focusComposer(session as never, 'page')).rejects.toMatchObject({ + code: 'COMPOSER_HAS_DRAFT', reason: 'composer-has-draft', delivery: 'rejected', + }); + setChatGptDesktopAdapterForTests({ connect: async () => {}, close: async () => {}, + sendMessage: async () => { throw new ComposerDraftError(); } } as unknown as ChatGptDesktopAdapter); + expect(await sendOperation(sendSchema.parse({ text: 'do not replace draft' }))).toMatchObject({ + code: 'COMPOSER_HAS_DRAFT', reason: 'composer-has-draft', exitCode: 1, + }); +}); diff --git a/tests/route-unit/chatgpt-desktop-review.test.ts b/tests/route-unit/chatgpt-desktop-review.test.ts new file mode 100644 index 0000000..f68bdab --- /dev/null +++ b/tests/route-unit/chatgpt-desktop-review.test.ts @@ -0,0 +1,247 @@ +import { createServer } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { afterEach, expect, it, rstest } from '@rstest/core'; +import { CdpSession } from '../../src/core/chatgpt-desktop/cdp-session.js'; +import { CdpChatGptDesktopAdapter } from '../../src/core/chatgpt-desktop/cdp-adapter.js'; +import { ChatGptDesktopFacade, finalizeThreadList, setChatGptDesktopAdapterForTests } from '../../src/core/chatgpt-desktop/facade.js'; +import { ARCHIVED_THREAD_EXPRESSION, CONVERSATION_ID_EXPRESSION, REPLY_STATE_EXPRESSION, LOADING_TASK_GONE_EXPRESSION, waitForReplyDone } from '../../src/core/chatgpt-desktop/cdp-dom.js'; +import { sendSchema, waitSchema } from '../../src/core/codex/routes.js'; +import { statusOperation } from '../../src/core/chatgpt-desktop/routes.js'; +import type { ChatGptDesktopAdapter } from '../../src/core/chatgpt-desktop/adapter.js'; + +const row = (threadId: string) => ({ threadId, title: threadId, pinned: false, selected: false, kind: 'codex' }); +afterEach(() => { rstest.restoreAllMocks(); setChatGptDesktopAdapterForTests(null); }); + +it('rejects CDP HTTP redirects without contacting their destination', async () => { + let redirected = 0; + const server = createServer((req, res) => { + if (req.url === '/escaped') { redirected++; res.end('{}'); } + else { res.writeHead(302, { location: '/escaped' }); res.end(); } + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + try { + const { port } = server.address() as AddressInfo; + await expect(CdpSession.fetchVersion(port)).rejects.toThrow(); + await expect(CdpSession.fetchJsonList(port)).rejects.toThrow(); + expect(redirected).toBe(0); + } finally { server.closeAllConnections(); await new Promise(resolve => server.close(() => resolve())); } +}); + +it('normalizes only thread IDs, preserving correlation, message and turn IDs', () => { + const sent = sendSchema.parse({ threadId: 'local:thread', message: 'hi', correlationId: 'local:corr', replyTo: 'local:msg', expectedTurnId: 'local:turn' }); + expect(sent).toMatchObject({ threadId: 'thread', correlationId: 'local:corr', replyTo: 'local:msg', expectedTurnId: 'local:turn' }); + expect(waitSchema.parse({ threadId: 'local:thread', turnId: 'local:turn', messageId: 'local:msg' })).toMatchObject({ threadId: 'thread', turnId: 'local:turn', messageId: 'local:msg' }); +}); + +it('keeps remote ownership for a CDP sidebar row and excludes unknown providers', () => { + const remote = { ...row('local:remote'), location: 'remote' as const, hostId: 'fixture-new-host', hostName: 'Fixture Host', modelProvider: 'fixture-new-provider' }; + const base = { backend: 'cdp' as const, limit: 50, threads: [row('local:remote')] }; + const listed = finalizeThreadList(base, { limit: 50, remotes: [remote], host: remote.hostId, modelProvider: remote.modelProvider }); + expect(listed.threads).toHaveLength(1); + expect(listed.threads[0]).toMatchObject({ location: 'remote', hostId: remote.hostId }); + expect(finalizeThreadList({ ...base, threads: [] }, { limit: 50, remotes: [{ ...remote, modelProvider: null }], modelProvider: remote.modelProvider }).threads).toEqual([]); +}); + +it('filters remote search results even when app-server fails', async () => { + const facade = new ChatGptDesktopFacade({ connect: async () => {}, listThreads: async () => ({ backend: 'cdp', limit: 50, threads: [] }) } as unknown as ChatGptDesktopAdapter, 9222, { + searchThreads: async () => { throw new Error('offline'); }, + listRemoteThreads: () => [{ ...row('local:unrelated'), location: 'remote', hostId: 'fixture', hostName: null }], + } as never); + expect((await facade.searchThreads({ query: 'needle' })).threads).toEqual([]); +}); + +async function connectedAdapter(state: Record, open = true) { + rstest.spyOn(CdpSession.prototype, 'connect').mockResolvedValue({}); + rstest.spyOn(CdpSession.prototype, 'closed', 'get').mockReturnValue(false); + rstest.spyOn(CdpSession.prototype, 'send').mockImplementation(async (method) => (method === 'Target.getTargets' + ? { targetInfos: [{ targetId: 'main', type: 'page', url: 'app://-/index.html' }] } + : { sessionId: 'page' }) as never); + rstest.spyOn(CdpSession.prototype, 'evaluate').mockImplementation(async expression => { + if (expression === REPLY_STATE_EXPRESSION) return state as never; + if (expression === CONVERSATION_ID_EXPRESSION) return (open ? 'thread' : 'other') as never; + if (expression === LOADING_TASK_GONE_EXPRESSION) return true as never; + if (expression.includes('router.navigate')) return { ok: false } as never; + return { ok: open, error: 'thread-not-found' } as never; + }); + const adapter = new CdpChatGptDesktopAdapter(); + await adapter.connect({ port: 9222 }); + return adapter; +} + +it('does not report a streaming partial reply as completed after timeout', async () => { + const adapter = await connectedAdapter({ stopVisible: true, finalAssistantCount: 1, reply: 'partial', conversationId: 'thread', lastTurnKey: 'turn', finalTurnKey: 'turn' }); + expect((await adapter.waitForReply({ threadId: 'local:thread', timeoutMs: 1 })).delivery).toBe('timeout'); +}); + +it('propagates failure to open the requested thread instead of reading another chat', async () => { + const adapter = await connectedAdapter({ stopVisible: false, finalAssistantCount: 1, reply: 'wrong chat', conversationId: 'other', lastTurnKey: 'turn', finalTurnKey: 'turn' }, false); + await expect(adapter.waitForReply({ threadId: 'local:thread', timeoutMs: 1 })).rejects.toThrow('not found'); +}); + +it('recognizes completion when a final marker already existed while streaming', async () => { + let call = 0; + const session = { evaluate: async () => ({ stopVisible: call++ === 0, finalAssistantCount: 1, reply: 'final', conversationId: 'thread', lastTurnKey: 'turn', finalTurnKey: 'turn' }) }; + await expect(waitForReplyDone(session as never, 'page', { timeoutMs: 300, baselineFinalCount: 1 })).resolves.toMatchObject({ reply: 'final' }); +}); + +it('serializes Desktop operations and closes each connection on completion', async () => { + let active = 0; + let maxActive = 0; + let closed = 0; + setChatGptDesktopAdapterForTests({ + connect: async () => { active++; maxActive = Math.max(maxActive, active); }, + status: async () => { await new Promise(resolve => setTimeout(resolve, 10)); return { reachable: true, port: 9222, host: '127.0.0.1', message: 'ok', exitCode: 0 }; }, + close: async () => { active--; closed++; }, + } as unknown as ChatGptDesktopAdapter); + await Promise.all([statusOperation({}), statusOperation({})]); + expect(maxActive).toBe(1); + expect(closed).toBe(2); +}); + +it('waits through an old conversation rendered before Loading task appears', async () => { + const { waitForLoadingTaskGone } = await import('../../src/core/chatgpt-desktop/cdp-dom.js'); + let reads = 0; + const session = { evaluate: async (expression: string) => expression === CONVERSATION_ID_EXPRESSION ? (++reads === 1 ? 'old-thread' : 'wanted-thread') : expression !== ARCHIVED_THREAD_EXPRESSION }; + await waitForLoadingTaskGone(session as never, 'page', { timeoutMs: 600, threadId: 'local:wanted-thread' }); + expect(reads).toBe(2); +}); + +it('does not accept an old final reply when the latest user turn is unanswered', async () => { + const session = { evaluate: async () => ({ stopVisible: false, finalAssistantCount: 1, reply: 'old', conversationId: 'thread', lastTurnKey: 'new-turn', finalTurnKey: 'old-turn' }) }; + await expect(waitForReplyDone(session as never, 'page', { timeoutMs: 10 })).rejects.toThrow('did not finish'); +}); + +it('rejects a conversation switch while waiting for a specific thread', async () => { + const session = { evaluate: async () => ({ stopVisible: false, finalAssistantCount: 1, reply: 'other', conversationId: 'other', lastTurnKey: 'turn', finalTurnKey: 'turn' }) }; + await expect(waitForReplyDone(session as never, 'page', { timeoutMs: 10, expectedConversationId: 'wanted' })).rejects.toThrow('changed'); +}); + +it('does not silently create a generic chat when the requested project is missing', async () => { + const { startNewChatExpression } = await import('../../src/core/chatgpt-desktop/cdp-dom.js'); + let clicks = 0; + const document = { querySelector: () => null, querySelectorAll: () => [{ textContent: 'New chat', click: () => clicks++ }] }; + const result = new Function('document', `return ${startNewChatExpression('fixture-project')}`)(document); + expect(result.ok).toBe(false); + expect(clicks).toBe(0); +}); + +it('rejects non-loopback debugger URLs even for HTTP-only status discovery', async () => { + const server = createServer((_req, res) => res.end(JSON.stringify({ webSocketDebuggerUrl: 'ws://192.0.2.1:9222/devtools/browser/x' }))); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + try { + await expect(CdpSession.fetchVersion((server.address() as AddressInfo).port)).rejects.toMatchObject({ code: 'CDP_HOST_REJECTED' }); + } finally { server.closeAllConnections(); await new Promise(resolve => server.close(() => resolve())); } +}); + +it('aborts a CDP HTTP response that never finishes its body', async () => { + const server = createServer((_req, res) => { res.writeHead(200); res.write('{'); }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + try { + await expect(CdpSession.fetchVersion((server.address() as AddressInfo).port, { signal: AbortSignal.timeout(30) })).rejects.toThrow(); + } finally { server.closeAllConnections(); await new Promise(resolve => server.close(() => resolve())); } +}); + +it('reads empty history and requests explicit ascending/full or descending/recent pages', async () => { + const bridge = await import('../../src/core/codex-bridge.js'); + const { appServerReadThread } = await import('../../src/core/chatgpt-desktop/app-server-fallback.js'); + const calls: Array> = []; + let empty = true; + rstest.spyOn(bridge, 'openCodexSession').mockResolvedValue({ client: { + close() {}, + async request(method: string, params: Record) { + if (method === 'thread/read') return { thread: { id: 'review-thread' } }; + calls.push(params); + const ids = params.sortDirection === 'asc' ? ['old', 'new'] : ['new', 'old']; + return { data: empty ? [] : ids.map(id => ({ id, items: [] })), nextCursor: null }; + }, + } } as never); + expect((await appServerReadThread({ threadId: 'review-thread' })).turns).toEqual([]); + empty = false; + expect((await appServerReadThread({ threadId: 'review-thread', limit: 2 })).turns.map(t => t.turnKey)).toEqual(['history-content:turn:old', 'history-content:turn:new']); + await appServerReadThread({ threadId: 'review-thread', limit: 2, full: true }); + expect(calls.map(c => c.sortDirection)).toEqual(['desc', 'desc', 'asc']); + expect(calls.at(-1)?.limit).toBe(2); +}); + +it('keeps provider discovery dynamic and combines local and remote provider IDs', async () => { + const provider = `provider-${crypto.randomUUID()}`; + const host = `host-${crypto.randomUUID()}`; + const remote = { ...row('local:remote'), location: 'remote' as const, hostId: host, hostName: host, modelProvider: provider }; + const facade = new ChatGptDesktopFacade({} as ChatGptDesktopAdapter, 9222, { + listThreads: async () => ({ threads: [], backend: 'app-server', limit: 50 }), + listRemoteThreads: () => [remote], + listHosts: () => [], + discoverModelProviders: async () => ({ providers: ['fixture-local-provider'], source: 'thread/list-distinct' }), + } as never); + const result = await facade.listHosts(); + expect(result.hosts.map(h => h.hostId)).toContain(host); + expect(result.modelProviders).toContain(provider); +}); + +it('bounds an unanswered CDP command and rejects pending commands on close', async () => { + let socket: EventTarget; + class SilentSocket extends EventTarget { + static OPEN = 1; + static CLOSED = 3; + readyState = 1; + constructor() { super(); socket = this; } + send() {} + close() { this.readyState = 3; this.dispatchEvent(new Event('close')); } + } + rstest.stubGlobal('WebSocket', SilentSocket); + rstest.useFakeTimers(); + rstest.spyOn(CdpSession, 'fetchVersion').mockResolvedValue({ webSocketDebuggerUrl: 'ws://127.0.0.1:9222/test' }); + const session = new CdpSession(9222); + try { + const connecting = session.connect(); + await Promise.resolve(); + socket!.dispatchEvent(new Event('open')); + await connecting; + const timedOut = expect(session.send('Runtime.evaluate')).rejects.toThrow('timed out'); + await rstest.advanceTimersByTimeAsync(10001); + await timedOut; + const closed = expect(session.send('Target.getTargets')).rejects.toThrow('closed'); + session.close(); + await closed; + } finally { session.close(); rstest.useRealTimers(); rstest.unstubAllGlobals(); } +}); + +it('submits once even while the Send button lingers, and refuses an existing draft', async () => { + const { FOCUS_COMPOSER_EXPRESSION, sendMessageInDom } = await import('../../src/core/chatgpt-desktop/cdp-dom.js'); + const commands: string[] = []; + const session = { + evaluate: async (expression: string) => expression === FOCUS_COMPOSER_EXPRESSION ? { ok: true } : true, + send: async (method: string, params: Record) => { commands.push(`${method}:${params.type || ''}`); }, + }; + expect(await sendMessageInDom(session as never, 'page', 'hello')).toEqual({ sentVia: 'enter' }); + expect(commands).toEqual(['Input.insertText:', 'Input.dispatchKeyEvent:rawKeyDown', 'Input.dispatchKeyEvent:keyUp']); + const document = { querySelector: () => ({ textContent: 'unsent draft', focus: () => { throw new Error('must not focus'); } }) }; + expect(new Function('document', `return ${FOCUS_COMPOSER_EXPRESSION}`)(document)).toEqual({ ok: false, error: 'composer-has-draft' }); +}); + +it('does not relabel a failed Enter acknowledgment as a safe rejection', async () => { + const { sendMessageInDom } = await import('../../src/core/chatgpt-desktop/cdp-dom.js'); + const session = { + evaluate: async () => ({ ok: true }), + send: async (method: string) => { if (method === 'Input.dispatchKeyEvent') throw new Error('lost connection'); }, + }; + await expect(sendMessageInDom(session as never, 'page', 'hello')).rejects.toMatchObject({ delivery: 'unknown', reason: 'submission-unconfirmed' }); +}); + +it('waits for the real conversation when given the special new-thread target', async () => { + const state = { stopVisible: true, finalAssistantCount: 1, reply: 'final', conversationId: 'thread', lastTurnKey: 'turn', finalTurnKey: 'turn' }; + const adapter = await connectedAdapter(state); + const timer = setTimeout(() => { state.stopVisible = false; }, 10); + try { + expect(await adapter.waitForReply({ threadId: 'new', timeoutMs: 500 })).toMatchObject({ threadId: 'local:thread', delivery: 'replied' }); + } finally { clearTimeout(timer); } +}); + +it('serializes nested optional fields without rejecting successful reads', async () => { + const { readThreadOperation } = await import('../../src/core/chatgpt-desktop/routes.js'); + setChatGptDesktopAdapterForTests({ + connect: async () => {}, close: async () => {}, + readThread: async () => ({ threadId: 'thread', backend: 'cdp', turns: [{ turnKey: 'turn', role: 'assistant', text: 'reply', userText: undefined }], limit: 1, full: false }), + } as unknown as ChatGptDesktopAdapter); + expect(await readThreadOperation({ threadId: 'thread', limit: 1, full: false, openTimeoutMs: 100 })).toMatchObject({ exitCode: 0, turns: [{ text: 'reply' }] }); +}); diff --git a/tests/route-unit/chatgpt-desktop.test.ts b/tests/route-unit/chatgpt-desktop.test.ts new file mode 100644 index 0000000..1bfce30 --- /dev/null +++ b/tests/route-unit/chatgpt-desktop.test.ts @@ -0,0 +1,1322 @@ +import { createServer, type Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; + +import { afterEach, describe, expect, it } from '@rstest/core'; +import { invokeCli, invokeMcpTool, listMcpSurface } from 'agent-bundle/test'; + +import type { ChatGptDesktopAdapter } from '../../src/core/chatgpt-desktop/adapter.js'; +import { + CdpHostRejectedError, + CdpUnreachableError, + NotImplementedError, + assertLoopbackHostname, + chatgptDesktopRelaunchCommand, + forceLoopbackWebSocketUrl, + setChatGptDesktopAdapterForTests, +} from '../../src/core/chatgpt-desktop/index.js'; +import { ChatGptDesktopFacade } from '../../src/core/chatgpt-desktop/facade.js'; + +const desktopTools = [ + 'chatgpt_desktop_list_hosts', + 'chatgpt_desktop_list_threads', + 'chatgpt_desktop_open_thread', + 'chatgpt_desktop_read_thread', + 'chatgpt_desktop_search_threads', + 'chatgpt_desktop_send', + 'chatgpt_desktop_status', + 'chatgpt_desktop_wait_reply', +] as const; + +function fakeAdapter(overrides: Partial = {}): ChatGptDesktopAdapter { + const base: ChatGptDesktopAdapter = { + async connect() {}, + async listTargets() { + return [ + { + targetId: 'main', + type: 'page', + title: 'ChatGPT', + url: 'app://-/index.html', + attached: true, + }, + ]; + }, + async listThreads({ limit = 50 } = {}) { + return { + backend: 'cdp' as const, + limit, + host: 'all', + threads: [ + { + threadId: 'local:11111111-1111-1111-1111-111111111111', + title: 'Scaffold thread', + pinned: false, + selected: true, + kind: 'local', + location: 'local' as const, + hostId: null, + hostName: null, + }, + ].slice(0, limit), + }; + }, + async listHosts() { + // Stub only — production discovery is dynamic (see listDiscoveredHosts / + // chatgpt_desktop_list_hosts). Do not treat these ids as an allowlist. + return { + backend: 'app-server' as const, + hostsSource: 'remote-thread-summaries-v3+local', + modelProviders: ['fixture-provider'], + modelProvidersSource: 'thread/list-distinct', + hosts: [ + { hostId: 'local', hostName: 'local', location: 'local' as const, threadCount: 1 }, + { + hostId: 'fixture-remote-a', + hostName: 'Fixture Remote A', + location: 'remote' as const, + threadCount: 2, + }, + ], + }; + }, + async readThread({ threadId, limit = 100, full = false }) { + return { + threadId, + backend: 'cdp', + limit, + full, + turns: [ + { + turnKey: 't1', + role: 'assistant', + text: 'hi there', + userText: 'hello', + assistantText: 'hi there', + }, + ], + }; + }, + async sendMessage({ threadId, text, project }) { + return { + threadId: threadId && !threadId.startsWith('local:client-new-thread:') + ? threadId + : 'local:11111111-1111-1111-1111-111111111111', + temporaryThreadId: threadId ? undefined : 'local:client-new-thread:deadbeef', + conversationId: '11111111-1111-1111-1111-111111111111', + project, + backend: 'cdp', + experimental: false, + delivery: 'accepted', + sentVia: 'enter', + message: text, + }; + }, + async waitForReply({ threadId }) { + return { + threadId: 'local:conv-real-id', + conversationId: 'conv-real-id', + reply: 'hi there', + backend: 'cdp', + experimental: false, + delivery: 'replied', + }; + }, + async openThread(threadId) { + return { threadId, backend: 'cdp' }; + }, + async status() { + return { + reachable: true, + port: 9222, + host: '127.0.0.1', + browser: 'fake', + message: 'ok', + exitCode: 0, + }; + }, + async close() {}, + }; + return { ...base, ...overrides }; +} + +afterEach(() => { + setChatGptDesktopAdapterForTests(null); +}); + +describe('chatgpt-desktop loopback + relaunch helpers', () => { + it('rejects non-loopback CDP hosts and rewrites debugger URLs to 127.0.0.1', () => { + expect(() => assertLoopbackHostname('example.com')).toThrow(CdpHostRejectedError); + expect(() => forceLoopbackWebSocketUrl('ws://10.0.0.2:9222/devtools/browser/x')).toThrow( + CdpHostRejectedError, + ); + expect(forceLoopbackWebSocketUrl('ws://localhost:9222/devtools/browser/x')).toBe( + 'ws://127.0.0.1:9222/devtools/browser/x', + ); + expect(chatgptDesktopRelaunchCommand({ port: 9333 })).toContain( + '--remote-debugging-port=9333', + ); + expect(chatgptDesktopRelaunchCommand({ port: 9222 })).toContain( + 'open -a /Applications/ChatGPT.app --args --remote-debugging-port=9222', + ); + }); + + it('selects only the exact main window URL and ignores overlays/webviews', async () => { + const { + IGNORED_TARGET_URL_MARKERS, + MAIN_WINDOW_URL, + COMPOSER_SELECTOR, + SEND_BUTTON_SELECTOR, + STOP_BUTTON_SELECTOR, + ASSISTANT_MESSAGE_SELECTOR, + dedupeThreadsById, + isMainWindowTarget, + newChatInProjectSelector, + pickMainWindowTarget, + summarizeTargetInfos, + } = await import('../../src/core/chatgpt-desktop/cdp-dom.js'); + + expect(isMainWindowTarget({ type: 'page', url: MAIN_WINDOW_URL })).toBe(true); + expect( + isMainWindowTarget({ + type: 'page', + url: 'app://-/index.html?initialRoute=%2Favatar-overlay', + }), + ).toBe(false); + expect(isMainWindowTarget({ type: 'page', url: 'app://-/detached-window.html' })).toBe(false); + expect(isMainWindowTarget({ type: 'page', url: 'https://chatgpt.com/pricing' })).toBe(false); + expect(IGNORED_TARGET_URL_MARKERS.length).toBeGreaterThan(0); + expect(COMPOSER_SELECTOR).toContain('data-codex-composer'); + expect(SEND_BUTTON_SELECTOR).toBe('button[aria-label="Send"]'); + expect(STOP_BUTTON_SELECTOR).toContain('Stop'); + expect(ASSISTANT_MESSAGE_SELECTOR).toContain('assistant-message'); + expect(newChatInProjectSelector('Launch')).toContain('Start new chat in Launch'); + expect( + dedupeThreadsById([ + { threadId: 'a', title: '1', pinned: false, selected: false, kind: 'local' }, + { threadId: 'a', title: '1b', pinned: false, selected: true, kind: 'local' }, + { threadId: 'b', title: '2', pinned: false, selected: false, kind: 'local' }, + { threadId: 'local:c', title: 'c-desktop', pinned: false, selected: false, kind: 'local' }, + { threadId: 'c', title: 'c-bare', pinned: true, selected: false, kind: 'local' }, + ]).map((t) => ({ id: t.threadId, pinned: t.pinned, selected: t.selected })), + ).toEqual([ + { id: 'a', pinned: false, selected: true }, + { id: 'b', pinned: false, selected: false }, + { id: 'c', pinned: true, selected: false }, + ]); + + const targets = summarizeTargetInfos([ + { targetId: 'overlay', type: 'page', url: 'app://-/index.html?initialRoute=%2Favatar-overlay' }, + { targetId: 'web', type: 'page', url: 'https://chatgpt.com/pricing' }, + { targetId: 'main', type: 'page', title: 'ChatGPT', url: MAIN_WINDOW_URL }, + ]); + // Exact URL wins regardless of /json/list-style order (overlay/web listed first). + expect(pickMainWindowTarget(targets)?.targetId).toBe('main'); + expect(pickMainWindowTarget(targets.slice().reverse())?.targetId).toBe('main'); + }); +}); + +describe('chatgpt-desktop thread id mapping', () => { + it('maps local: ↔ app-server bare ids; temp rows do not map', async () => { + const { + LOCAL_THREAD_ID_PREFIX, + TEMP_THREAD_ID_PREFIX, + appServerThreadIdCandidates, + isTemporaryDesktopThreadId, + threadIdsEquivalent, + toAppServerThreadId, + toDesktopThreadId, + toDomTurnKey, + } = await import('../../src/core/chatgpt-desktop/index.js'); + + const bare = '11111111-1111-1111-1111-111111111111'; + const local = `${LOCAL_THREAD_ID_PREFIX}${bare}`; + expect(toAppServerThreadId(local)).toBe(bare); + expect(toAppServerThreadId(bare)).toBe(bare); + expect(toDesktopThreadId(bare)).toBe(local); + expect(toDesktopThreadId(local)).toBe(local); + expect(threadIdsEquivalent(local, bare)).toBe(true); + expect(appServerThreadIdCandidates(local)[0]).toBe(bare); + expect(appServerThreadIdCandidates(local)).toEqual([bare]); + expect(toDomTurnKey('abc')).toBe('history-content:turn:abc'); + expect(toDomTurnKey('history-content:turn:abc')).toBe('history-content:turn:abc'); + + const temp = `${TEMP_THREAD_ID_PREFIX}deadbeef`; + expect(isTemporaryDesktopThreadId(temp)).toBe(true); + expect(toAppServerThreadId(temp)).toBeNull(); + expect(appServerThreadIdCandidates(temp)).toEqual([]); + }); + + it('mergeThreadLists overlays CDP pinned/selected/project onto app-server rows', async () => { + const { mergeThreadLists } = await import('../../src/core/chatgpt-desktop/facade.js'); + const merged = mergeThreadLists( + { + backend: 'app-server', + limit: 10, + threads: [ + { + threadId: 'local:11111111-1111-1111-1111-111111111111', + title: 'from app-server', + pinned: false, + selected: false, + kind: 'codex', + }, + ], + }, + { + backend: 'cdp', + limit: 10, + threads: [ + { + threadId: 'local:11111111-1111-1111-1111-111111111111', + title: 'sidebar title', + pinned: true, + selected: true, + kind: 'local', + project: 'Launch', + }, + { + threadId: 'local:client-new-thread:temp', + title: 'draft', + pinned: false, + selected: true, + kind: 'local', + }, + ], + }, + ); + expect(merged.backend).toBe('app-server+cdp'); + expect(merged.threads[0]).toMatchObject({ + threadId: 'local:11111111-1111-1111-1111-111111111111', + title: 'from app-server', + pinned: true, + selected: true, + kind: 'local', + project: 'Launch', + }); + expect(merged.threads.some((t) => t.threadId === 'local:client-new-thread:temp')).toBe(true); + }); +}); + +describe('chatgpt-desktop MCP tools', () => { + it('registers the chatgpt_desktop_* tools on the grok-bot server', async () => { + const surface = await listMcpSurface({ server: 'grok-bot' }); + for (const name of desktopTools) { + expect(surface.tools.includes(name)).toBe(true); + } + }); + + it('status / list / read / send / wait_reply use the injected adapter', async () => { + setChatGptDesktopAdapterForTests(fakeAdapter()); + + const status = await invokeMcpTool('chatgpt_desktop_status', { + server: 'grok-bot', + input: {}, + }); + expect(status.isError).toBe(false); + expect(status.structuredContent).toMatchObject({ + reachable: true, + host: '127.0.0.1', + exitCode: 0, + }); + + const hosts = await invokeMcpTool('chatgpt_desktop_list_hosts', { + server: 'grok-bot', + input: {}, + }); + expect(hosts.isError).toBe(false); + expect(hosts.structuredContent).toMatchObject({ + exitCode: 0, + hostsSource: 'remote-thread-summaries-v3+local', + modelProvidersSource: 'thread/list-distinct', + modelProviders: ['fixture-provider'], + hosts: [ + { hostId: 'local', location: 'local' }, + { hostId: 'fixture-remote-a', hostName: 'Fixture Remote A' }, + ], + }); + + const listed = await invokeMcpTool('chatgpt_desktop_list_threads', { + server: 'grok-bot', + input: { limit: 10 }, + }); + expect(listed.isError).toBe(false); + expect(listed.structuredContent).toMatchObject({ + backend: 'cdp', + exitCode: 0, + threads: [{ threadId: 'local:11111111-1111-1111-1111-111111111111' }], + }); + + const read = await invokeMcpTool('chatgpt_desktop_read_thread', { + server: 'grok-bot', + input: { threadId: 'local:11111111-1111-1111-1111-111111111111' }, + }); + expect(read.isError).toBe(false); + expect(read.structuredContent).toMatchObject({ + backend: 'cdp', + full: false, + turns: [{ role: 'assistant', userText: 'hello', assistantText: 'hi there' }], + }); + + const sent = await invokeMcpTool('chatgpt_desktop_send', { + server: 'grok-bot', + input: { text: 'ping', project: 'Launch' }, + }); + expect(sent.isError).toBe(false); + expect(sent.structuredContent).toMatchObject({ + delivery: 'accepted', + backend: 'cdp', + experimental: false, + threadId: 'local:11111111-1111-1111-1111-111111111111', + temporaryThreadId: 'local:client-new-thread:deadbeef', + conversationId: '11111111-1111-1111-1111-111111111111', + project: 'Launch', + exitCode: 0, + }); + expect(String((sent.structuredContent as { threadId?: string }).threadId ?? '')).not.toMatch( + /^local:client-new-thread:/, + ); + + const waited = await invokeMcpTool('chatgpt_desktop_wait_reply', { + server: 'grok-bot', + input: { threadId: 'local:client-new-thread:deadbeef', timeoutMs: 1000 }, + }); + expect(waited.isError).toBe(false); + expect(waited.structuredContent).toMatchObject({ + delivery: 'replied', + reply: 'hi there', + conversationId: 'conv-real-id', + threadId: 'local:conv-real-id', + exitCode: 0, + }); + }); + + it('maps NotImplementedError to a rejected not-implemented document', async () => { + setChatGptDesktopAdapterForTests( + fakeAdapter({ + async listThreads() { + throw new NotImplementedError('listThreads', 'DOM selectors pending'); + }, + }), + ); + const result = await invokeMcpTool('chatgpt_desktop_list_threads', { + server: 'grok-bot', + input: {}, + }); + expect(result.structuredContent).toMatchObject({ + delivery: 'rejected', + reason: 'not-implemented', + code: 'NOT_IMPLEMENTED', + exitCode: 1, + }); + const errorText = + result.structuredContent && + typeof result.structuredContent === 'object' && + 'error' in result.structuredContent + ? String((result.structuredContent as { error?: unknown }).error ?? '') + : ''; + expect(errorText).toMatch(/listThreads/); + }); +}); + +describe('chatgpt-desktop CLI', () => { + it('dispatches status|hosts|threads|read|send through the fake adapter', async () => { + setChatGptDesktopAdapterForTests(fakeAdapter()); + + const status = await invokeCli(['chatgpt-desktop', 'status', '--json']); + expect(status.exitCode).toBe(0); + expect(status.value).toMatchObject({ reachable: true, host: '127.0.0.1' }); + + const hosts = await invokeCli(['chatgpt-desktop', 'hosts', '--json']); + expect(hosts.exitCode).toBe(0); + expect(hosts.value).toMatchObject({ + hostsSource: 'remote-thread-summaries-v3+local', + modelProvidersSource: 'thread/list-distinct', + }); + + const threads = await invokeCli(['chatgpt-desktop', 'threads', '--limit', '5', '--json']); + expect(threads.exitCode).toBe(0); + expect(threads.value).toMatchObject({ backend: 'cdp' }); + + const read = await invokeCli([ + 'chatgpt-desktop', + 'read', + 'local:11111111-1111-1111-1111-111111111111', + '--json', + ]); + expect(read.exitCode).toBe(0); + expect(read.value).toMatchObject({ backend: 'cdp' }); + + const sent = await invokeCli([ + 'chatgpt-desktop', + 'send', + '--thread-id', + 'local:11111111-1111-1111-1111-111111111111', + 'hello from cli', + '--json', + ]); + expect(sent.exitCode).toBe(0); + expect(sent.value).toMatchObject({ delivery: 'accepted', backend: 'cdp' }); + }); + + it('maps CLI NotImplemented failures onto exitCode 1', async () => { + setChatGptDesktopAdapterForTests( + fakeAdapter({ + async readThread() { + throw new NotImplementedError('readThread'); + }, + }), + ); + const read = await invokeCli([ + 'chatgpt-desktop', + 'read', + 'local:missing', + '--json', + ]); + expect(read.exitCode).toBe(1); + expect(read.value).toMatchObject({ + reason: 'not-implemented', + code: 'NOT_IMPLEMENTED', + }); + }); +}); + +describe('chatgpt-desktop app-server list/read/search + CDP-only send/wait', () => { + const appFallbacks = (calls: string[]) => ({ + listThreads: async ({ limit = 50 } = {}) => { + calls.push('app-server.listThreads'); + return { + backend: 'app-server' as const, + limit, + threads: [ + { + threadId: 'local:11111111-1111-1111-1111-111111111111', + title: 'from app-server', + pinned: false, + selected: false, + kind: 'codex', + section: { id: 'Pinned', name: 'Pinned' }, + modelProvider: 'openai', + }, + ], + }; + }, + searchThreads: async ({ query, limit = 50 }: { query: string; limit?: number }) => { + calls.push('app-server.searchThreads'); + return { + backend: 'app-server' as const, + limit, + query, + threads: [ + { + threadId: 'local:11111111-1111-1111-1111-111111111111', + title: 'from app-server', + pinned: true, + selected: false, + kind: 'codex', + }, + ], + }; + }, + readThread: async ({ threadId, limit = 100, full = false }: { threadId: string; limit?: number; full?: boolean }) => { + calls.push('app-server.readThread'); + return { + threadId, + backend: 'app-server' as const, + limit, + full, + turns: [ + { turnKey: 'history-content:turn:t1', role: 'user' as const, text: 'hello' }, + { turnKey: 'history-content:turn:t2', role: 'assistant' as const, text: 'deep history' }, + ], + }; + }, + statusProbe: async () => ({ reachable: true, mode: 'daemon', socketPath: '/tmp/fake.sock' }), + listRemoteThreads: () => [], + }); + + it('lists/searches/reads via app-server (merged with CDP), keeps send/wait/open on CDP', async () => { + const calls: string[] = []; + const cdp = fakeAdapter({ + async connect() { + calls.push('cdp.connect'); + }, + async listThreads({ limit = 50 } = {}) { + calls.push('cdp.listThreads'); + return { + backend: 'cdp', + limit, + threads: [ + { + threadId: 'local:11111111-1111-1111-1111-111111111111', + title: 'sidebar', + pinned: true, + selected: true, + kind: 'local', + project: 'Launch', + }, + ], + }; + }, + async readThread({ threadId, limit = 100, full = false }) { + calls.push(`cdp.readThread:${full ? 'full' : 'visible'}`); + return { + threadId, + backend: 'cdp', + limit, + full, + turns: [{ turnKey: 'visible', role: 'assistant' as const, text: 'on screen' }], + }; + }, + async sendMessage({ threadId, text }) { + calls.push('cdp.sendMessage'); + return { + threadId: threadId && !threadId.startsWith('local:client-new-thread:') + ? threadId + : 'local:resolved-after-send', + temporaryThreadId: threadId ? undefined : 'local:client-new-thread:x', + conversationId: 'resolved-after-send', + backend: 'cdp', + experimental: false, + delivery: 'accepted' as const, + message: text, + }; + }, + async waitForReply({ threadId }) { + calls.push('cdp.waitForReply'); + return { + threadId: threadId && !threadId.startsWith('local:client-new-thread:') + ? threadId + : 'local:resolved-after-send', + conversationId: 'resolved-after-send', + reply: 'ok', + backend: 'cdp', + experimental: false, + delivery: 'replied' as const, + }; + }, + async openThread(threadId) { + calls.push('cdp.openThread'); + return { threadId, backend: 'cdp' as const }; + }, + async status() { + return { + reachable: true, + port: 9222, + host: '127.0.0.1' as const, + message: 'ok', + exitCode: 0 as const, + }; + }, + }); + + const facade = new ChatGptDesktopFacade(cdp, 9222, appFallbacks(calls)); + + const listed = await facade.listThreads({ limit: 5 }); + expect(listed).toMatchObject({ + backend: 'app-server+cdp', + threads: [ + { + threadId: 'local:11111111-1111-1111-1111-111111111111', + title: 'from app-server', + selected: true, + project: 'Launch', + }, + ], + }); + + const searched = await facade.searchThreads({ query: 'app-server', limit: 5 }); + expect(searched).toMatchObject({ + backend: 'app-server+cdp', + query: 'app-server', + threads: [{ selected: true, project: 'Launch' }], + }); + + // App-server is primary for every durable read (no visible-first CDP hop). + const deep = await facade.readThread({ + threadId: 'local:11111111-1111-1111-1111-111111111111', + limit: 100, + }); + expect(deep).toMatchObject({ + backend: 'app-server', + turns: [ + { turnKey: 'history-content:turn:t1', text: 'hello' }, + { turnKey: 'history-content:turn:t2', text: 'deep history' }, + ], + }); + + const full = await facade.readThread({ + threadId: 'local:11111111-1111-1111-1111-111111111111', + full: true, + }); + expect(full.backend).toBe('app-server'); + + const sent = await facade.sendMessage({ threadId: 'local:11111111-1111-1111-1111-111111111111', text: 'hi' }); + expect(sent).toMatchObject({ backend: 'cdp', delivery: 'accepted' }); + + const waited = await facade.waitForReply({ threadId: 'local:11111111-1111-1111-1111-111111111111' }); + expect(waited).toMatchObject({ backend: 'cdp', delivery: 'replied' }); + + const opened = await facade.openThread('local:11111111-1111-1111-1111-111111111111'); + expect(opened).toMatchObject({ backend: 'cdp' }); + + expect(calls).toEqual([ + 'app-server.listThreads', + 'cdp.connect', + 'cdp.listThreads', + 'app-server.searchThreads', + 'cdp.listThreads', + 'app-server.readThread', + 'app-server.readThread', + 'cdp.sendMessage', + 'cdp.waitForReply', + 'cdp.openThread', + ]); + }); + + it('falls back to CDP read when app-server is unavailable', async () => { + const calls: string[] = []; + const facade = new ChatGptDesktopFacade( + fakeAdapter({ + async connect() { + calls.push('cdp.connect'); + }, + async readThread({ threadId, limit = 100, full = false }) { + calls.push(`cdp.readThread:${full ? 'full' : 'visible'}`); + return { + threadId, + backend: 'cdp', + limit, + full, + turns: [{ turnKey: 'w', role: 'assistant', text: 'from wheel' }], + }; + }, + }), + 9222, + { + listThreads: async () => ({ backend: 'app-server', limit: 1, threads: [] }), + searchThreads: async () => ({ backend: 'app-server', limit: 1, query: '', threads: [] }), + readThread: async () => { + calls.push('app-server.readThread'); + throw new Error('unknown thread'); + }, + statusProbe: async () => ({ reachable: false }), + }, + ); + + const read = await facade.readThread({ + threadId: 'local:11111111-1111-1111-1111-111111111111', + full: true, + }); + expect(read).toMatchObject({ backend: 'cdp', turns: [{ text: 'from wheel' }] }); + expect(calls).toEqual(['app-server.readThread', 'cdp.connect', 'cdp.readThread:full']); + }); + + it('does not fall back send/wait to app-server when CDP is down', async () => { + const unreachable = new CdpUnreachableError('CDP down'); + const facade = new ChatGptDesktopFacade( + fakeAdapter({ + async connect() { + throw unreachable; + }, + async sendMessage() { + throw unreachable; + }, + async waitForReply() { + throw unreachable; + }, + async status() { + return { + reachable: false, + port: 9222, + host: '127.0.0.1', + message: 'CDP down', + exitCode: 1, + }; + }, + }), + 9222, + { + listThreads: async ({ limit = 50 } = {}) => ({ + backend: 'app-server', + limit, + threads: [ + { + threadId: 'local:codex-1', + title: 'from app-server', + pinned: false, + selected: false, + kind: 'codex', + }, + ], + }), + searchThreads: async ({ query, limit = 50 }) => ({ + backend: 'app-server', + limit, + query, + threads: [], + }), + readThread: async ({ threadId, limit = 100, full = false }) => ({ + threadId, + backend: 'app-server', + limit, + full, + turns: [{ turnKey: 't1', role: 'assistant', text: 'fallback reply' }], + }), + statusProbe: async () => ({ reachable: true, mode: 'daemon', socketPath: '/tmp/fake.sock' }), + listRemoteThreads: () => [], + }, + ); + + const listed = await facade.listThreads({ limit: 5 }); + expect(listed).toMatchObject({ backend: 'app-server', threads: [{ threadId: 'local:codex-1' }] }); + + const read = await facade.readThread({ threadId: 'local:codex-1', full: true }); + expect(read).toMatchObject({ backend: 'app-server', turns: [{ text: 'fallback reply' }] }); + + await expect(facade.sendMessage({ threadId: 'local:codex-1', text: 'hi' })).rejects.toBeInstanceOf( + CdpUnreachableError, + ); + await expect(facade.waitForReply({ threadId: 'local:codex-1' })).rejects.toBeInstanceOf( + CdpUnreachableError, + ); + + const status = await facade.status(); + expect(status).toMatchObject({ + reachable: false, + appServerFallback: { reachable: true, mode: 'daemon' }, + exitCode: 1, + }); + }); + + it('keeps app-server list when CDP merge hits NotImplemented', async () => { + const facade = new ChatGptDesktopFacade( + fakeAdapter({ + async connect() {}, + async listThreads() { + throw new NotImplementedError('listThreads'); + }, + }), + 9222, + { + listThreads: async ({ limit = 50 } = {}) => ({ + backend: 'app-server', + limit, + threads: [ + { + threadId: 'local:codex-1', + title: 'from app-server', + pinned: false, + selected: false, + kind: 'codex', + }, + ], + }), + searchThreads: async () => ({ backend: 'app-server', limit: 1, query: '', threads: [] }), + readThread: async () => { + throw new Error('unused'); + }, + statusProbe: async () => ({ reachable: false }), + listRemoteThreads: () => [], + }, + ); + const listed = await facade.listThreads(); + expect(listed).toMatchObject({ + backend: 'app-server', + threads: [{ threadId: 'local:codex-1' }], + }); + }); + + it('merges remote-control summaries from the global-state fixture', async () => { + const { copyFileSync, mkdtempSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { join } = await import('node:path'); + const { + finalizeThreadList, + findRemoteThread, + listRemoteThreadsFromState, + RemoteThreadNotLoadedError, + } = await import('../../src/core/chatgpt-desktop/index.js'); + + const home = mkdtempSync(join(tmpdir(), 'gbot-remote-fix-')); + copyFileSync( + join(process.cwd(), 'tests/fixtures/codex-global-state-remote.json'), + join(home, '.codex-global-state.json'), + ); + const env = { CODEX_HOME: home }; + const remotes = listRemoteThreadsFromState(env); + expect(remotes.map((t) => t.threadId).sort()).toEqual([ + 'local:aaaa1111-1111-1111-1111-111111111111', + 'local:bbbb2222-2222-2222-2222-222222222222', + 'local:cccc3333-3333-3333-3333-333333333333', + 'local:dddd4444-4444-4444-4444-444444444444', + ]); + const pinned = remotes.find((t) => t.threadId.includes('aaaa1111')); + expect(pinned).toMatchObject({ + location: 'remote', + hostId: 'host-macbook', + hostName: "Zack's MacBook", + pinned: true, + project: 'Launch', + projectId: 'proj-launch', + }); + const linux = remotes.find((t) => t.threadId.includes('cccc3333')); + expect(linux).toMatchObject({ + location: 'remote', + hostId: 'host-linux', + hostName: 'build-box', + project: 'Infra', + }); + + const listed = finalizeThreadList( + { + backend: 'app-server', + limit: 50, + threads: [ + { + threadId: 'local:local-thread-1', + title: 'Local only', + pinned: false, + selected: true, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + }, + ], + }, + { limit: 50, remotes, groupBy: 'host' }, + ); + expect(listed.threads.some((t) => t.location === 'local')).toBe(true); + expect(listed.threads.some((t) => t.location === 'remote')).toBe(true); + expect(listed.groups?.map((g) => g.hostId).sort()).toEqual([ + 'broken-host', + 'host-linux', + 'host-macbook', + 'local', + ]); + + const filtered = finalizeThreadList( + { + backend: 'app-server', + limit: 50, + threads: [ + { + threadId: 'local:local-thread-1', + title: 'Local only', + pinned: false, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + }, + ], + }, + { limit: 50, remotes, host: 'macbook' }, + ); + expect(filtered.host).toBe('macbook'); + expect(filtered.threads.every((t) => t.hostId === 'host-macbook')).toBe(true); + + const localOnly = finalizeThreadList( + { + backend: 'app-server', + limit: 50, + threads: [ + { + threadId: 'local:local-thread-1', + title: 'Local only', + pinned: false, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + modelProvider: 'openai', + }, + ], + }, + { limit: 50, remotes, host: 'local' }, + ); + expect(localOnly.threads).toHaveLength(1); + expect(localOnly.threads[0]?.location).toBe('local'); + + const byProvider = finalizeThreadList( + { + backend: 'app-server', + limit: 50, + threads: [ + { + threadId: 'local:local-thread-1', + title: 'Local only', + pinned: false, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + modelProvider: 'openai', + }, + { + threadId: 'local:local-thread-2', + title: 'Other provider', + pinned: false, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + modelProvider: 'anthropic', + }, + ], + }, + { limit: 50, remotes: [], host: 'all', modelProvider: 'openai' }, + ); + expect(byProvider.modelProvider).toBe('openai'); + expect(byProvider.threads).toHaveLength(1); + expect(byProvider.threads[0]?.modelProvider).toBe('openai'); + + const { + listDiscoveredHosts, + } = await import('../../src/core/chatgpt-desktop/index.js'); + const discovered = listDiscoveredHosts(env, { localThreadCount: 3 }); + expect(discovered.map((h) => h.hostId)).toEqual([ + 'local', + 'broken-host', + 'host-linux', + 'host-macbook', + ]); + expect(discovered.find((h) => h.hostId === 'local')?.threadCount).toBe(3); + expect(discovered.find((h) => h.hostId === 'host-macbook')).toMatchObject({ + hostName: "Zack's MacBook", + threadCount: 2, + location: 'remote', + }); + + const facade = new ChatGptDesktopFacade( + fakeAdapter({ + async connect() { + throw new CdpUnreachableError('offline'); + }, + async listThreads() { + throw new CdpUnreachableError('offline'); + }, + }), + 9222, + { + listThreads: async ({ limit = 50 } = {}) => ({ + backend: 'app-server' as const, + limit, + threads: [ + { + threadId: 'local:local-1', + title: 'Local', + pinned: false, + selected: false, + kind: 'codex', + modelProvider: 'openai', + }, + ], + }), + searchThreads: async () => ({ backend: 'app-server' as const, limit: 1, query: '', threads: [] }), + readThread: async () => { + throw new Error('unused'); + }, + statusProbe: async () => ({ reachable: true, mode: 'daemon' }), + listRemoteThreads: () => remotes, + discoverModelProviders: async () => ({ + providers: ['openai', 'anthropic'], + source: 'thread/list-distinct' as const, + }), + }, + ); + const hostList = await facade.listHosts(); + expect(hostList.hostsSource).toBe('local+remote-thread-summaries-v3+managed-connections'); + expect(hostList.modelProvidersSource).toBe('thread/list-distinct'); + expect(hostList.modelProviders).toEqual(['anthropic', 'openai']); + expect(hostList.hosts.find((h) => h.hostId === 'local')?.threadCount).toBe(1); + expect(hostList.hosts.find((h) => h.hostId === 'host-macbook')?.threadCount).toBe(2); + + const lookup = findRemoteThread('aaaa1111-1111-1111-1111-111111111111', env); + expect(lookup).toMatchObject({ hostId: 'host-macbook', hostName: "Zack's MacBook" }); + const err = new RemoteThreadNotLoadedError( + 'aaaa1111-1111-1111-1111-111111111111', + lookup?.hostId ?? null, + lookup?.hostName ?? null, + ); + expect(err.code).toBe('REMOTE_THREAD_NOT_LOADED'); + expect(err.hostId).toBe('host-macbook'); + expect(err.hostName).toBe("Zack's MacBook"); + expect(err.hint).toMatch(/host-macbook/); + expect(err.message).toMatch(/app-server/); + }); + + it('discovers an unknown hostId from global-state and filters by it with no code change', async () => { + const { copyFileSync, mkdtempSync, readFileSync, writeFileSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { join } = await import('node:path'); + const { randomUUID } = await import('node:crypto'); + const { + finalizeThreadList, + listDiscoveredHosts, + listRemoteThreadsFromState, + } = await import('../../src/core/chatgpt-desktop/index.js'); + + // Brand-new id never mentioned in source — only injected into the fixture. + const unknownHostId = `host-supercomputer-${randomUUID().slice(0, 8)}`; + const unknownFriendly = 'Supercomputer Lab 9000'; + const unknownThreadId = randomUUID(); + + const home = mkdtempSync(join(tmpdir(), 'gbot-remote-unknown-')); + const fixturePath = join(process.cwd(), 'tests/fixtures/codex-global-state-remote.json'); + const statePath = join(home, '.codex-global-state.json'); + const base = JSON.parse(readFileSync(fixturePath, 'utf8')) as Record; + const hostsV1 = (base['remote-hosts-v1'] ?? {}) as Record; + hostsV1[unknownHostId] = { name: unknownFriendly, envName: 'lab' }; + base['remote-hosts-v1'] = hostsV1; + base[`remote-thread-summaries-v3:${unknownHostId}`] = { + hostName: unknownFriendly, + threads: [ + { + id: unknownThreadId, + title: 'Brand new machine thread', + updatedAt: 1700000999, + modelProvider: 'brand-new-provider', + }, + ], + }; + writeFileSync(statePath, JSON.stringify(base, null, 2)); + // Keep a copy of the stock fixture nearby so the test still proves we + // started from fixture data and only added the unknown host. + copyFileSync(fixturePath, join(home, 'stock-fixture.json')); + + const env = { CODEX_HOME: home }; + const remotes = listRemoteThreadsFromState(env); + expect(remotes.some((t) => t.hostId === unknownHostId)).toBe(true); + + const discovered = listDiscoveredHosts(env, { localThreadCount: 0 }); + expect(discovered.map((h) => h.hostId)).toContain(unknownHostId); + expect(discovered.find((h) => h.hostId === unknownHostId)).toMatchObject({ + hostId: unknownHostId, + hostName: unknownFriendly, + location: 'remote', + threadCount: 1, + }); + + const filtered = finalizeThreadList( + { + backend: 'app-server', + limit: 50, + threads: [ + { + threadId: 'local:local-thread-1', + title: 'Local only', + pinned: false, + selected: false, + kind: 'codex', + location: 'local', + hostId: null, + hostName: null, + }, + ], + }, + { limit: 50, remotes, host: unknownHostId }, + ); + expect(filtered.host).toBe(unknownHostId); + expect(filtered.threads.length).toBeGreaterThan(0); + expect(filtered.threads.every((t) => t.hostId === unknownHostId)).toBe(true); + + const byFriendly = finalizeThreadList( + { + backend: 'app-server', + limit: 50, + threads: [], + }, + { limit: 50, remotes, host: 'Supercomputer Lab' }, + ); + expect(byFriendly.threads.every((t) => t.hostId === unknownHostId)).toBe(true); + expect(byFriendly.threads.length).toBeGreaterThan(0); + + const facade = new ChatGptDesktopFacade( + fakeAdapter({ + async connect() { + throw new CdpUnreachableError('offline'); + }, + async listThreads() { + throw new CdpUnreachableError('offline'); + }, + }), + 9222, + { + listThreads: async ({ limit = 50 } = {}) => ({ + backend: 'app-server' as const, + limit, + threads: [], + }), + searchThreads: async () => ({ + backend: 'app-server' as const, + limit: 1, + query: '', + threads: [], + }), + readThread: async () => { + throw new Error('unused'); + }, + statusProbe: async () => ({ reachable: true, mode: 'daemon' }), + listRemoteThreads: () => remotes, + discoverModelProviders: async () => ({ + providers: ['brand-new-provider'], + source: 'thread/list-distinct' as const, + }), + listHosts: (processEnv, opts) => listDiscoveredHosts(env, opts), + }, + ); + const hostList = await facade.listHosts(); + expect(hostList.hosts.some((h) => h.hostId === unknownHostId)).toBe(true); + expect(hostList.hosts.find((h) => h.hostId === unknownHostId)).toMatchObject({ + hostName: unknownFriendly, + threadCount: 1, + location: 'remote', + }); + expect(hostList.modelProviders).toContain('brand-new-provider'); + }); + + it('skips malformed global-state files without throwing', async () => { + const { mkdtempSync, writeFileSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { join } = await import('node:path'); + const { listRemoteThreadsFromState } = await import('../../src/core/chatgpt-desktop/index.js'); + const home = mkdtempSync(join(tmpdir(), 'gbot-remote-bad-')); + writeFileSync(join(home, '.codex-global-state.json'), '{not-json'); + expect(listRemoteThreadsFromState({ CODEX_HOME: home })).toEqual([]); + writeFileSync(join(home, '.codex-global-state.json'), JSON.stringify(['array'])); + expect(listRemoteThreadsFromState({ CODEX_HOME: home })).toEqual([]); + }); +}); + +describe('chatgpt-desktop CDP HTTP status probe', () => { + it('reports reachable when /json/version answers on loopback', async () => { + const server: Server = createServer((req, res) => { + if (req.url === '/json/version') { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end( + JSON.stringify({ + Browser: 'Test/1', + 'Protocol-Version': '1.3', + webSocketDebuggerUrl: 'ws://127.0.0.1:9/devtools/browser/test', + }), + ); + return; + } + if (req.url === '/json/list') { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify([{ id: '1', type: 'page', url: 'app://-/index.html' }])); + return; + } + res.writeHead(404); + res.end(); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address() as AddressInfo; + try { + const { CdpChatGptDesktopAdapter } = await import( + '../../src/core/chatgpt-desktop/cdp-adapter.js' + ); + const adapter = new CdpChatGptDesktopAdapter(); + // connect sets the port then fails on WebSocket; status still uses /json/version. + await expect(adapter.connect({ port })).rejects.toThrow(/CDP|WebSocket|ECONNREFUSED|failed/i); + const status = await adapter.status(); + expect(status).toMatchObject({ + reachable: true, + port, + host: '127.0.0.1', + browser: 'Test/1', + targetCount: 1, + exitCode: 0, + }); + } finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } + }); +}); + +describe('chatgpt-desktop production-risk guards', () => { + it('never treats local:client-new-thread as a durable send/wait id', async () => { + const { resolveDurableThreadId, TEMP_THREAD_ID_PREFIX } = await import( + '../../src/core/chatgpt-desktop/cdp-dom.js' + ); + expect(resolveDurableThreadId(null, `${TEMP_THREAD_ID_PREFIX}abc`)).toBeNull(); + expect(resolveDurableThreadId(`${TEMP_THREAD_ID_PREFIX}abc`)).toBeNull(); + expect(resolveDurableThreadId('conv-real')).toBe('local:conv-real'); + expect(resolveDurableThreadId('local:conv-real', `${TEMP_THREAD_ID_PREFIX}x`)).toBe( + 'local:conv-real', + ); + }); + + it('loads history with mouseWheel (column-reverse), not scrollTop writes', async () => { + const { + FULL_READ_WHEEL_DELTA_Y, + readTurnsFromDom, + } = await import('../../src/core/chatgpt-desktop/cdp-dom.js'); + expect(FULL_READ_WHEEL_DELTA_Y).toBeLessThan(0); + + const sends: Array<{ method: string; params?: Record }> = []; + let evaluateN = 0; + const session = { + async evaluate(expression: string) { + evaluateN += 1; + if (expression.includes('scrollTop')) { + return { ok: true, x: 10, y: 20, scrollTop: 0, scrollHeight: 2000, clientHeight: 400 }; + } + // First harvest: one turn; later harvests idle so wheel loop stops. + if (evaluateN <= 2) { + return [{ turnKey: 'history-content:turn:1', role: 'assistant', text: 'hi' }]; + } + return [{ turnKey: 'history-content:turn:1', role: 'assistant', text: 'hi' }]; + }, + async send(method: string, params?: Record) { + sends.push({ method, params }); + }, + }; + + const turns = await readTurnsFromDom(session as never, 'sess', { + full: true, + limit: 10, + idleWheels: 2, + maxWheels: 3, + }); + expect(turns.length).toBeGreaterThan(0); + expect(sends.some((s) => s.method === 'Input.dispatchMouseEvent')).toBe(true); + expect( + sends.every( + (s) => + s.method !== 'Input.dispatchMouseEvent' + || (s.params?.type === 'mouseWheel' + && s.params?.deltaY === FULL_READ_WHEEL_DELTA_Y + && !('scrollTop' in (s.params ?? {}))), + ), + ).toBe(true); + }); + + it('times out when Loading task… never clears', async () => { + const { waitForLoadingTaskGone, LOADING_TASK_TEXT } = await import( + '../../src/core/chatgpt-desktop/cdp-dom.js' + ); + const session = { + async evaluate() { + return false; + }, + }; + await expect( + waitForLoadingTaskGone(session as never, 'sess', { timeoutMs: 50 }), + ).rejects.toThrow(LOADING_TASK_TEXT); + }); + + it('opens sidebar rows for both local: and bare thread ids', async () => { + const { openThreadExpression } = await import('../../src/core/chatgpt-desktop/cdp-dom.js'); + const expr = openThreadExpression('local:abc-123'); + expect(expr).toContain('local:abc-123'); + expect(expr).toContain('abc-123'); + }); +}); diff --git a/tests/route-unit/tools.test.ts b/tests/route-unit/tools.test.ts index b3f9a89..e7b6d10 100644 --- a/tests/route-unit/tools.test.ts +++ b/tests/route-unit/tools.test.ts @@ -133,7 +133,7 @@ beforeEach(() => { describe('grok-bot MCP server', () => { it('registers messaging, conversation and managed bridge tools', async () => { const surface = await listMcpSurface({ server: 'grok-bot' }); - expect([...surface.tools].sort()).toEqual(['claude_send', 'codex_new', 'codex_send', 'codex_threads', 'codex_wait', 'codex_watch', 'gbot_bridge_start', 'gbot_bridge_status', 'gbot_bridge_stop', 'gbot_codex_respond', 'gbot_grok_approvals', 'gbot_grok_respond', 'gbot_send', 'gbot_thread']); + expect([...surface.tools].sort()).toEqual(['chatgpt_desktop_list_hosts', 'chatgpt_desktop_list_threads', 'chatgpt_desktop_open_thread', 'chatgpt_desktop_read_thread', 'chatgpt_desktop_search_threads', 'chatgpt_desktop_send', 'chatgpt_desktop_status', 'chatgpt_desktop_wait_reply', 'claude_send', 'codex_new', 'codex_send', 'codex_threads', 'codex_wait', 'codex_watch', 'gbot_bridge_start', 'gbot_bridge_status', 'gbot_bridge_stop', 'gbot_codex_respond', 'gbot_grok_approvals', 'gbot_grok_respond', 'gbot_send', 'gbot_thread']); }); it('lists and responds to an exact current Grok approval through the native API', async () => {