Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codex-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"grok-bot-cli": minor
---

Add `gbot codex status`, `gbot codex list-threads [--limit N]`, and `gbot codex send <threadId> <message...>`: attach to the local Codex app-server daemon socket (`$CODEX_HOME/app-server-control/app-server-control.sock`) with a built-in WebSocket client, list threads, and start a turn with documented JSON-RPC (`thread/resume` + `turn/start`). Reports an absent socket (no daemon or ChatGPT Desktop private mode), unknown threads, threads owned by another client, and refuses server approval requests instead of approving them. Method names are pinned to Codex 0.154.0.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@ By default `gbot` only sends credentials to expected hosts:

All gateway / `EnsureSandBox` fetches use `redirect: "error"` so credentials are not followed across redirects.

## Messaging Codex threads from Grok Bot

`gbot codex` attaches to a local [Codex app-server](https://learn.chatgpt.com/docs/app-server) daemon and injects messages into its threads with the documented JSON-RPC methods (`initialize`, `thread/list`, `thread/resume`, `turn/start`).

```sh
codex app-server daemon start # once per machine session
gbot codex status # socket, daemon version, reachability
gbot codex list-threads --limit 10 # id, status, cwd, preview
gbot codex send <threadId> "Grok here: the build is green, please continue."
```

`send` resumes the thread, starts a turn with your text, prints the turn id, and returns; Codex keeps working after `gbot` disconnects. Every command accepts `--json`.

**Which Codex you reach.** `gbot` connects to `$CODEX_HOME/app-server-control/app-server-control.sock` (default `~/.codex/...`) with a built-in WebSocket client. The daemon must be started by `codex app-server daemon start`. `list-threads` shows the threads recorded under `CODEX_HOME` (CLI, TUI, VS Code); `send` works on any of them that no other client currently holds open. Method and parameter names are pinned to the Codex release recorded in `src/codex-bridge.js` (`codex app-server generate-json-schema`); `status` prints the daemon and CLI versions so a stale daemon is visible, and `codex app-server daemon restart` picks up the installed CLI.

**ChatGPT Desktop limitation.** Desktop runs its own private stdio app-server and does not publish the shared control socket, so external clients cannot reach live Desktop tasks. When the socket is absent, `gbot codex status` exits 1 and says so, naming the upstream issues: [openai/codex#41014](https://github.com/openai/codex/issues/41014) and [openai/codex#41112](https://github.com/openai/codex/issues/41112). `gbot` never reads Desktop's temporary `CODEX_APP_TOOLS_PIPE_PATH` sockets under `/tmp/codex-browser-use/`; that channel is private to Desktop.

**Failure modes.**

- Socket absent: no daemon, or Desktop-private mode. Start the daemon or wait for the upstream fixes.
- Unknown thread: `send` fails with "Unknown Codex thread"; use `list-threads`.
- Thread open elsewhere: a thread with an active writer (VS Code, TUI) fails with "open in another client"; close it there first.
- Approvals: `gbot` never approves commands or file changes on your behalf. If Codex asks while `gbot` is still connected, `send` refuses the request, exits 1, and tells you the turn id. `send` disconnects as soon as the turn starts, so later approval requests stay with the daemon for a Codex client to answer; for unattended sends set `approval_policy = "never"` in the daemon's `config.toml`.

## License

MIT
54 changes: 54 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { hasGatewayAuth } from "./gateway.js";
import { openBackend } from "./commands.js";
import { inspectGrokBotGatewaySession } from "./app-session.js";
import { redactSecrets } from "./url-policy.js";
import { codexStatus, listCodexThreads, sendToCodexThread } from "./codex-bridge.js";

function print(value) {
if (typeof value === "string") process.stdout.write(value + "\n");
Expand Down Expand Up @@ -48,6 +49,9 @@ function usage() {
" send <bot-or-group> <message...>",
" thread <bot-or-group> [--limit N] [--root MESSAGE_ID]",
" chat <bot-or-group> alias for thread",
" codex status",
" codex list-threads [--limit N]",
" codex send <threadId> <message...>",
"",
"Max group members: " + MAX_GROUP_MEMBERS,
"--description / --instructions is the UI Instructions field (same key).",
Expand All @@ -56,6 +60,7 @@ function usage() {
"Flags: --gateway --files --dir DIR --json",
"Auth: GROK_BOT_GATEWAY_URL + GROK_BOT_GATEWAY_TOKEN, or the Grok Bot app session, or CURSOR_ACCESS_TOKEN",
"File fallback: GROK_BOT_AGENTS_DIR",
"Codex: talks to the local app-server daemon socket under CODEX_HOME (default ~/.codex)",
].join("\n");
}

Expand Down Expand Up @@ -220,6 +225,50 @@ function formatTranscript(out) {
return lines.join("\n");
}

function formatCodexStatus(s) {
const lines = ["socket: " + s.socketPath];
if (!s.reachable) return lines.concat("reachable: no", s.message).join("\n");
lines.push("reachable: yes (daemon)");
lines.push("daemon version: " + (s.daemonVersion ?? "unknown") + " cli version: " + (s.cliVersion ?? "unknown") + " pinned schema: " + s.pinnedVersion);
if (s.versionMismatch) lines.push("warning: daemon and CLI versions differ; `codex app-server daemon restart` picks up the installed CLI");
return lines.join("\n");
}

function formatCodexThread(t) {
const title = t.name ? " - " + t.name : "";
const preview = t.preview ? "\n " + String(t.preview).replace(/\s+/g, " ").slice(0, 200) : "";
return t.id + " " + t.status + title + "\n " + (t.cwd ?? "") + preview;
}

async function runCodex(sub, rest, json) {
if (sub === "status") {
const status = await codexStatus();
print(json ? status : formatCodexStatus(status));
if (!status.reachable) process.exitCode = 1;
return;
}
if (sub === "list-threads") {
const limitRaw = takeFlag(rest, "--limit");
const limit = limitRaw ? Number(limitRaw) : 20;
if (!Number.isInteger(limit) || limit < 1) throw new StoreError("--limit must be a positive integer");
const out = await listCodexThreads({ limit });
if (json) print(out);
else if (out.threads.length === 0) print("No Codex threads.");
else print(out.threads.map(formatCodexThread).join("\n\n"));
return;
}
if (sub === "send") {
const threadId = rest.shift();
const message = rest.join(" ").trim();
if (!threadId || threadId.startsWith("-") || !message) throw new StoreError("gbot codex send <threadId> <message...>");
const out = await sendToCodexThread(threadId, message);
if (json) print(out);
else print("Started turn " + out.turnId + " (" + out.turnStatus + ") on Codex thread " + out.threadId);
return;
}
throw new StoreError("gbot codex status | list-threads [--limit N] | send <threadId> <message...>");
}

async function main(argv) {
const args = argv.slice(2);
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
Expand Down Expand Up @@ -268,6 +317,11 @@ async function main(argv) {
return;
}

if (cmd === "codex") {
await runCodex(sub, rest, json);
return;
}

const backend = await openBackend({ root: rootFlag, gateway, files: filesMode });

if (cmd === "bots" && sub === "list") {
Expand Down
Loading