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/collapse-agent-bundle-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"grok-bot-cli": minor
---

Ship `gbot` as one generated Agent Bundle CLI at the package root and add `gbot-install install|uninstall <cursor|codex|claude>` / `gbot-install doctor` for the bundled `grok-bot` MCP tools and `talk-to-grok-bot` skill; delete the nested `plugin/` project and the hand-written dispatcher. Breaking (pre-1.0): Node.js 22.19.0 or newer is required; options are command-local (`gbot send --history-dir DIR …`, no leading globals); `--json` is reserved anywhere before `--`, so put `--` before flag-like message text; `send` and every `codex` command write one JSON document to stdout with `exitCode` (failures keep `error`, `delivery`, `reason`, `mode` and exit 1), other commands print failures on stderr and exit 1; argument and schema errors exit 2; `--instructions` is gone (use `--description`) and `--notify`/`--hidden` take `on|off` only; `chat` records history rows as `event: "thread"`; the installed bundle is named `gbot` (uninstall an earlier source-built `grok-bot` plugin first). (#50)
37 changes: 12 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,24 @@ jobs:
with:
node-version: "24"

- name: Test
run: npm test
- name: Install dependencies
run: npm ci

- name: Validate, build, typecheck, and test
run: npm run check

- name: Lint the package
run: npx --yes publint@0.3.24

- name: Pack and smoke-test the executable
shell: bash
run: |
package_file=$(npm pack --json | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>process.stdout.write(JSON.parse(s)[0].filename))")
package_root=$(mktemp -d)
install_root=$(mktemp -d)
npm install --global --prefix "$install_root" "./$package_file"
npm pack --pack-destination "$package_root"
package_files=("$package_root"/*.tgz)
test -f "${package_files[0]}"
test "${#package_files[@]}" -eq 1
npm install --global --prefix "$install_root" "${package_files[0]}"
"$install_root/bin/gbot" --help

plugin:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: plugin
steps:
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"

- name: Install the plugin toolchain
run: npm ci

- name: Validate, build, typecheck, and test the plugin
run: npm run check
"$install_root/bin/gbot-install" --help
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ jobs:
- name: Update npm for trusted publishing
run: npm install --global npm@latest

- name: Test
run: npm test
- name: Validate, build, typecheck, and test
run: npm run check

- name: Lint the package
run: npx --yes publint@0.3.24
Expand All @@ -53,6 +53,7 @@ jobs:
test "${#package_files[@]}" -eq 1
npm install --global --prefix "$install_root" "${package_files[0]}"
"$install_root/bin/gbot" --help
"$install_root/bin/gbot-install" --help

- name: Create release pull request or publish
id: changesets
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ artifact/
tmp-agents/
*.log
.DS_Store
*.tsbuildinfo
File renamed without changes.
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Manage [Grok Bot](https://cursor.com/help/grok-bot/plans) agents, groups, and me
npm install --global grok-bot-cli
```

Requires Node.js 18+ and the Grok Bot desktop app on macOS, Linux, or Windows. Open Grok Bot and sign in once; `gbot` automatically uses the app's encrypted session and routing credentials. No token copying is required. On Linux the app keeps its session under `~/.config/Grok Bot` (or `$XDG_CONFIG_HOME`); when it is stored in the system keyring, `gbot` reads the key with `secret-tool` (package `libsecret-tools`). On Windows the session lives under `%APPDATA%\\Grok Bot` and decrypts with the app's DPAPI-wrapped Safe Storage key.
Requires Node.js 22.19.0+ and the Grok Bot desktop app on macOS, Linux, or Windows. Open Grok Bot and sign in once; `gbot` automatically uses the app's encrypted session and routing credentials. No token copying is required. On Linux the app keeps its session under `~/.config/Grok Bot` (or `$XDG_CONFIG_HOME`); when it is stored in the system keyring, `gbot` reads the key with `secret-tool` (package `libsecret-tools`). On Windows the session lives under `%APPDATA%\\Grok Bot` and decrypts with the app's DPAPI-wrapped Safe Storage key.

## Use

Expand All @@ -34,14 +34,19 @@ gbot bots delete Researcher
gbot bots delete Writer
```

`update` fields: `--name` `--description`/`--instructions` `--title` `--avatar-shape` `--avatar-color` `--notify` `--hidden`. `--description` is the UI Instructions field.
`update` fields: `--name` `--description` `--title` `--avatar-shape` `--avatar-color` `--notify on|off` `--hidden on|off`. `--description` is the UI Instructions field.

`gbot thread --after ID` filters the bounded tail locally and returns entries strictly
after that opaque entry ID. Its JSON includes `cursor`, `entryCount`, and `gapReset`.
An unchanged poll has `entryCount: 0`; an unknown or expired ID returns one bounded
snapshot with `gapReset: true`. The gateway request remains limit-only.

Run `gbot --help` for every command.
Options are command-local (for example, `gbot send --history-dir DIR ...`);
the former leading-global form is no longer accepted. `--json` prints the
canonical JSON result; `send` and the `codex` commands also report failures as
a JSON document on stdout with `exitCode` (see below), every other command
prints the failure message on stderr and exits 1.

## Gateway URL policy

Expand All @@ -68,11 +73,11 @@ 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. Native Windows is not supported yet (AF_UNIX control socket); use WSL, Linux, or macOS.
**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/core/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. Native Windows is not supported yet (AF_UNIX control socket); use WSL, Linux, or macOS.

**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.

**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`). Whether ChatGPT Desktop owns a thread is not observable from the socket, so `desktopAttached` is always `"unknown"`. Every failure exits 1; automation reads `mode`/`reason`, not the exit code.
**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`). Whether ChatGPT Desktop owns a thread is not observable from the socket, so `desktopAttached` is always `"unknown"`. The document is always written to stdout and includes `exitCode`; it is `0` only for a usable daemon.

**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"`.

Expand All @@ -83,11 +88,11 @@ gbot codex send <threadId> "Grok here: build is green" # receip
gbot codex send --correlation-id M --reply-to M --hop 1 <threadId> "ack" # the answer, one hop later
```

Sends at `hop >= GROK_BOT_MAX_HOPS` (default 4) are refused with `reason: "hop-limit"` before anything reaches the daemon, so two agents cannot acknowledge each other forever; `gbot` never auto-acknowledges. `--envelope` (implied by any envelope flag) prepends a one-line `[gbot msg=… corr=… reply-to=… hop=… from=user@host]` header so the receiving agent can quote the ids back. That header is caller-authored provenance for the reader, not authentication: the daemon authenticates the local user through the socket, nothing else. The same flags work for `gbot send` to Grok bots and groups (the receipt carries `envelopeId`, `correlationId`, `hop`; Grok's own `messageId` remains the delivery receipt). Private ChatGPT Desktop pipes and arbitrary ChatGPT chats stay out of scope; only Codex threads on a reachable app-server daemon are routes.
Sends at `hop >= GROK_BOT_MAX_HOPS` (default 4) are refused with `reason: "hop-limit"` before anything reaches the daemon, so two agents cannot acknowledge each other forever; `gbot` never auto-acknowledges. `--envelope` (implied by any envelope flag) prepends a one-line `[gbot msg=… corr=… reply-to=… hop=… from=user@host]` header so the receiving agent can quote the ids back. That header is caller-authored provenance for the reader, not authentication: the daemon authenticates the local user through the socket, nothing else. Private ChatGPT Desktop pipes and arbitrary ChatGPT chats stay out of scope; only Codex threads on a reachable app-server daemon are routes.

**Busy threads.** `send` reads the thread status on resume. Only `idle` and `notLoaded` threads start a turn. An `active` thread (a turn in progress, or waiting on approval / user input) is refused with `reason: "busy"`: in app-server 0.154.0 a `turn/start` on an active thread steers that turn rather than queueing behind it, and `gbot` never steers or interrupts work a human may be doing. Either wait for `list-threads` to show `idle` and resend, or pass `--when-busy queue` to hand the message to the daemon's own queue through Codex's experimental `thread/queue/add` — that needs `GROK_BOT_CODEX_EXPERIMENTAL=1`, returns `delivery: "queued"` with `queuedSubmissionId`, and `gbot codex queue <threadId>` shows what is still waiting. `systemError` threads are refused with `reason: "thread-error"`, statuses this version does not know with `reason: "unknown-status"`. Receipts distinguish `delivery: "accepted"` (turn started; `turnId`, `turnStatus`), `"queued"`, `"rejected"` (nothing was sent; see `reason`), and `"unknown"` (the request left but no acknowledgment came back — look for `messageId` in the thread or queue before resending). The decision record, with the schema evidence and a live probe of the queue API, is in [`docs/codex-busy-threads.md`](docs/codex-busy-threads.md).

**Failure modes.** Every `send` / `codex send` failure under `--json` is `{ error, delivery, reason, messageId, correlationId, hop, … }` on stderr with exit 1; argument mistakes are `{ error, reason: "usage" }`; `codex status` never fails this way — it prints the status document on stdout and exits 1 when `mode` is anything but `daemon`. `reason` values are stable:
**Failure modes.** Every `send` and `codex` outcome under `--json` is one document on stdout with `exitCode`; failures include `{ error, delivery, reason, messageId, correlationId, hop, exitCode: 1, … }` and the process exits 1. Framework argument/schema errors remain on stderr and exit 2. `--json` is reserved anywhere before `--`; put `--` before flag-like message text. `reason` values are stable:

- `socket-absent` / `permission-denied` / `not-a-socket` / `connect-failed` / `handshake-failed` / `windows-unsupported`: the route is unavailable. Start the daemon, fix the socket, or wait for the upstream Desktop fixes.
- `unknown-thread`: use `list-threads`.
Expand All @@ -100,29 +105,24 @@ Sends at `hop >= GROK_BOT_MAX_HOPS` (default 4) are refused with `reason: "hop-l

## Talking to Grok Bot from Codex

`plugin/` is an [Agent Bundle](https://scriptedalchemy.github.io/agent-bundle/) plugin
The npm package is also an [Agent Bundle](https://scriptedalchemy.github.io/agent-bundle/) plugin
that gives Codex, Claude Code, and Cursor two MCP tools on a `grok-bot` server,
`gbot_send` and `gbot_thread`, plus a `talk-to-grok-bot` skill that tells the agent
when to ping a bot and how to word the message. The tools bundle this repository's
gateway client, so the installed plugin does not need `gbot` on `PATH`.

The plugin is not part of the npm package. From a clone of this repository, build
the artifact once, then install it into each host you use:
Install the bundled host projections from the same npm package:

```sh
git clone https://github.com/ScriptedAlchemy/grok-bot-cli.git
cd grok-bot-cli/plugin
npm install
npm run build
npx agent-bundle install codex --from artifact
npx agent-bundle install claude --from artifact
npx agent-bundle install cursor --from artifact
npx agent-bundle doctor --from artifact
gbot-install install codex
gbot-install install claude
gbot-install install cursor
gbot-install doctor
```

Add `--replace` to an install command to overwrite an earlier copy. `npm run check`
runs the plugin gates: source validation, build, artifact validation, typecheck, and
the route-unit tests, which drive both tools against a loopback fake gateway.
Add `--replace` to an install command to overwrite an earlier copy. The bundle is
registered as `gbot`; if you installed the pre-0.4 `grok-bot` plugin from a source
checkout, uninstall it first so the two do not both register the `grok-bot` server.

`gbot_thread` returns a small receipt by default: deterministic `summary`, opaque
`cursor`, `entryCount`, and `gapReset`.
Expand Down
28 changes: 28 additions & 0 deletions agent-bundle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineConfig } from 'agent-bundle/config';

/**
* One Agent Bundle project at repo root — same shape as cargo-hauler.
* Nested `plugin/` + hand `src/cli.js` are gone; hosts and npm share this tree.
*
* - `src/mcp/grok-bot/tools/*` → MCP tools
* - `src/cli/**` → npm CLI, including send/thread/history and Codex
* - `src/gbot-install.ts` → `gbot-install` host installer
* - `src/core/*` → domain (gateway, store, codex-bridge, …)
* - `src/skills/*` → installed skills
*/
export default defineConfig({
bin: {
'gbot-install': './src/gbot-install.ts',
},
lib: false,
marketplace: true,
output: { distPath: 'artifact' },
plugin: {
description:
'Message Grok Bot bots and groups and read their threads from Codex, Claude Code, and Cursor.',
// plugin.name is also the routed bin name: `dist/bin/gbot.mjs`.
name: 'gbot',
},
runtime: { node: '22.19.0' },
targets: ['claude', 'codex', 'cursor', 'portable'],
});
2 changes: 1 addition & 1 deletion docs/codex-busy-threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Decision record for [#38](https://github.com/ScriptedAlchemy/grok-bot-cli/issues/38). Evidence is the
app-server schema emitted by `codex app-server generate-json-schema` for codex-cli 0.154.0 — the release
`src/codex-bridge.js` is pinned to — generated both without and with `--experimental`, plus a live probe
`src/core/codex-bridge.js` is pinned to — generated both without and with `--experimental`, plus a live probe
against a 0.154.0 daemon (below).

## What the protocol offers
Expand Down
Loading