Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CodeMonkey

中文 | English

A background runtime and observability console for multiple CLI coding agents. Run opencode, codex, kiro-cli, qwen, cursor-agent and others in the background, switch between them like ghostty tabs, record every execution in full, and talk to them — including approving permissions remotely — from your terminal or from Feishu / DingTalk / QQ bots.


Contents


The problem

CLI coding agents are designed for a human sitting in front of a terminal. Real work needs them to run unattended for long stretches while remaining interruptible from anywhere:

  • Running three agents in parallel means three terminal windows, and remembering which is doing what
  • Close the terminal and the task dies; come back and all you have is scrollback
  • The agent asks "allow rm -rf build?" while you're away from the keyboard, and the task blocks indefinitely
  • You want to know which files an agent touched and which commands it ran — scroll past it and it's gone
  • Multiple agents editing the same repository step on each other

CodeMonkey turns these agents into session objects that can be hosted in the background and observed or driven remotely.


Features

1. Run any CLI agent in the background

Agents start inside a real PTY, detached from the terminal that launched them. Close the terminal, shut the laptop — the session keeps running under the daemon.

A real PTY rather than a pipe is mandatory: nearly every CLI agent checks isatty. Behind a pipe they downgrade to non-interactive mode, disable colour, or refuse to start their TUI at all.

Supported agents:

Agent Modes Events & tools Interactive approval Terminal screen
opencode hybrid / native / pty structured (HTTP + SSE) yes yes
codex native / hybrid / pty structured (stdio JSON-RPC) yes yes
kiro-cli pty screen inference yes (dialog keystrokes) yes
qwen pty screen inference no yes
cursor-agent pty screen inference no yes

Adding another terminal-only agent usually means appending one profile to src/adapters/generic-pty/profiles.ts (launch command plus screen-feature regexes) — no code required.

Supported IM channels:

Channel Transport Approval card Voice input Status
Feishu / Lark long connection (no public domain needed) real buttons, one tap decides yes (ASR + confirmation) ✅ supported
DingTalk webhook (public URL required) markdown, buttons degrade to commands no 🔜 planned
QQ webhook (public URL required) plain text, buttons degrade to commands no 🔜 planned

2. Concurrent agents that don't interfere

Sessions are independent objects, not bound to any frontend. Run as many agents of as many kinds as you like: hand a refactor to opencode, test coverage to codex, docs to qwen — each with its own workspace, recording and state.

3. Dual-channel observation

Each agent can expose two channels. They complement rather than replace each other:

Mode Native channel PTY channel Use for
hybrid events & approvals screen preferred for opencode
native everything not started structured observation only
pty none everything agents without an API

The native channel is semantically precise (tool calls, file changes and approval requests arrive as structured data). The PTY channel is universal (it works for any CLI agent, and the screen matches ghostty exactly).

The native channel supports two transports, declared by the adapter: http (opencode's serve + SSE) and stdio (codex's app-server JSON-RPC).

4. Interactive approval

This is the biggest difference from comparable tools. Approval requests are broadcast to every frontend with full context, and whichever frontend answers first wins:

agent requests approval
  → denylist match (rm -rf / git push -f / writes to ~/.ssh / sudo …)
      hit → human confirmation required, and cannot be overridden by "always allow"
  → allowlist match → auto-approved, audited
  → neither → queued as pending, broadcast to CLI / TUI / bots
      any frontend answers → CAS claim → the others are told "already handled by X"
      timeout → hold (default, wait forever) / reject / allow (opt-in, warns)
  → audit appended to ~/.codemonkey/audit.jsonl

kiro-cli deserves a note: it has no API whatsoever, but its approval dialog has a fixed structure (three options, cursor always on the first). That's enough for CodeMonkey to recognise the request, push it to you, and answer with keystrokes. The full loop — capture, remote approve, file written — has been verified against the real binary.

Other PTY-only agents have not had their dialogs verified, so CodeMonkey does not claim approval support for them. Claiming it and then letting something through unchecked is worse than admitting it isn't supported.

5. Progress tracking and summaries

summary answers four questions: what has been done, what is happening now, what is blocked, what's next. It defaults to a rule-based summary computed directly from structured events — zero latency, zero cost, always available:

Done      7 tool calls (read×3, write×2, bash×2); touched src/auth/index.ts and 3 more
Doing     waiting for approval: run command: npm publish
Blocked   high risk, matched rule "npm publish", awaiting human decision
Next      run `codemonkey allow cm_a1b2c3:per_xxx` to approve, or `deny` to refuse

LLM summarisation is an optional enhancement that must be configured explicitly. If the model is unavailable it falls back to the rule-based summary rather than failing.

6. Full execution records and replay

Two layers, each with a purpose:

  • stream.cast — raw terminal stream in asciicast v2, replayable with codemonkey replay or asciinema play
  • events.jsonl — structured events for search, summarisation and bot notifications

7. Workspace isolation

The worktree sandbox gives each session its own git worktree, so several agents can edit the same repository without collisions and your main branch stays untouched. When the session ends the worktree is removed but the branch is kept — that branch is the agent's output.

8. IM bots

A bot is just another frontend, on equal footing with the CLI and TUI. Feishu, DingTalk and QQ are supported.

Feishu is the most complete: long-connection transport needs no public domain, approval cards use real buttons (one tap decides), and voice input works (transcribed, echoed back for confirmation, then executed).


Quick start

Requirements: Node.js 22+, macOS (Linux best-effort).

git clone <repo> && cd codemonkey
npm install

Start your first session:

# launch opencode in the background, workspace = the given directory
npm run cm -- start opencode --dir ~/proj/api

# output
# 已启动 cm_a1b2c3d4e5f6  opencode (hybrid)
#   工作区 ~/proj/api
#   版本   1.18.6

You can close the terminal now — the session keeps running under the daemon:

npm run cm -- list        # still there
npm run cm -- tui         # open the tab-switching console

Drive it:

npm run cm -- say cm_a1b2c3d4e5f6 "split the auth module into its own package"

When the agent asks for approval:

npm run cm -- perms                              # list pending requests
npm run cm -- allow cm_a1b2c3d4e5f6:per_xxx      # approve once

The CLI currently prints Chinese messages. English output is not yet implemented — see Known limitations.

Install as a global command (optional)

Prefixing everything with npm run cm -- gets old. After building you can call codemonkey directly:

npm run build
npm link
codemonkey list

The rest of this document uses codemonkey; substitute npm run cm -- if you haven't built.


Usage: command line

Session management

# start a session
codemonkey start <agent> [--dir <path>] [--mode <mode>] [--sandbox <policy>]

codemonkey start opencode                          # current directory, hybrid mode
codemonkey start codex --dir ~/proj --mode native  # explicit directory and mode
codemonkey start opencode --sandbox worktree       # isolate with git worktree
codemonkey start kiro-cli --dir ~/proj             # PTY-only agent

# list running sessions
codemonkey list

#    SESSION          AGENT     MODE    STATE     PENDING  WORKSPACE     STARTED
# ●  cm_a1b2c3d4e5f6  opencode  hybrid  busy      -        ~/proj/api    3m12s ago
# !  cm_f6e5d4c3b2a1  codex     native  awaiting  1        ~/proj/web    1m05s ago

# session detail
codemonkey status cm_a1b2c3d4e5f6

# past sessions (finished or orphaned)
codemonkey history

# stop a session (terminates the agent process)
codemonkey kill cm_a1b2c3d4e5f6

State badges: busy  idle ! awaiting approval ? awaiting input × error  orphaned · exited

Modes:

Mode Meaning
hybrid native channel for events and approvals, PTY channel for the screen. opencode default
native structured channel only — precise events and approvals, no terminal screen
pty real terminal only — perfect fidelity, but approvals cannot be captured reliably

One constraint of hybrid is worth knowing: the PTY and native channels are two independent sessions on the agent's side. Conversation and approvals go through the native channel; the PTY screen is for observation only — so typing in the TUI will not change what the PTY screen shows.

Sandbox policies:

Policy Behaviour
none the agent works directly in the given directory (default)
worktree git worktree creates an isolated tree on branch codemonkey/<session-id>. On exit the tree is removed and the branch is kept

If the directory isn't a git repository, or the repository has no commits, worktree degrades to none and says so explicitly — it will never let you believe you have isolation when you don't.

Interaction

# send a message
codemonkey say cm_a1b2c3 "refactor the login flow"

# forward the agent's own slash command verbatim
codemonkey keys cm_a1b2c3 /model
codemonkey keys cm_a1b2c3 /status

# send terminal keystrokes
codemonkey keys cm_a1b2c3 Up Enter
codemonkey keys cm_a1b2c3 CtrlC

# interrupt the current turn
codemonkey abort cm_a1b2c3

/keys: operate the agent terminal verbatim

/keys is the unified pass-through command for an agent terminal. It does not interpret or rewrite the agent command after it, and it is not limited to /model or /status. The content after /keys is handled by the current coding agent, and the resulting agent terminal content is returned verbatim without a delivery prefix, Markdown fence, or application-level clipping.

Where Syntax
Bot with a bound session /keys <agent-command-or-keys...>
Bot with an explicit session /keys <session-id> <agent-command-or-keys...>
Local CLI codemonkey keys <session-id> <agent-command-or-keys...>

The available slash commands are defined by the agent itself. Common examples follow.

View the agent's own status:

/keys /status

Open the model menu and make a selection:

/keys /model
/keys Down
/keys Enter

Multiple consecutive keys can also be sent together:

/keys Down Down Enter

Open the agent's own help or settings:

/keys /help
/keys /settings

Close a menu or interrupt the current terminal operation:

/keys Escape
/keys CtrlC

If no current session is bound, specify one explicitly:

/keys cm_a1b2c3 /status

A slash command is forwarded as one complete command. If the command opens an interactive menu, continue with arrow keys, Enter, or Escape; each operation returns the updated agent terminal content. In the local CLI, quote the complete agent command when consecutive spaces or shell-special characters must be preserved:

codemonkey keys cm_a1b2c3 '/command  value'

Key names: Enter Escape Tab ShiftTab Backspace Up Down Left Right Home End PageUp PageDown CtrlC CtrlD CtrlL CtrlP CtrlR. Anything else is sent verbatim.

Observation

# progress summary (done / doing / blocked / next)
codemonkey summary cm_a1b2c3
codemonkey summary cm_a1b2c3 --minutes 30    # last half hour only
codemonkey summary cm_a1b2c3 --full          # read all events from disk

# structured progress: tool timeline and change surface
codemonkey progress cm_a1b2c3

# current terminal screen
codemonkey screen cm_a1b2c3

# structured execution record
codemonkey logs cm_a1b2c3
codemonkey logs cm_a1b2c3 --follow                  # tail in real time
codemonkey logs cm_a1b2c3 --type tool.started       # filter by event type

# replay the terminal session
codemonkey replay cm_a1b2c3
codemonkey replay cm_a1b2c3 --speed 4

Event types accepted by --type: session.created session.state session.exited session.error screen.updated message.user message.assistant.delta message.assistant.done tool.started tool.finished file.changed permission.asked permission.resolved input.needed

Records are plain files, so standard tools work too:

asciinema play ~/.codemonkey/sessions/cm_a1b2c3/stream.cast
jq 'select(.type=="tool.finished")' ~/.codemonkey/sessions/cm_a1b2c3/events.jsonl

Approvals

codemonkey perms                          # list all pending requests (with diff preview)
codemonkey allow <request-id>             # approve once
codemonkey allow <request-id> --always    # auto-approve similar requests from now on
codemonkey deny <request-id>              # refuse

Request IDs look like cm_a1b2c3:per_xxxxx. "Already handled" means another frontend (the TUI or a bot) answered first — that's the race resolving normally, not an error.

Daemon and diagnostics

codemonkey daemon          # run the daemon in the foreground with visible logs (debugging)
codemonkey stop-daemon     # stop the daemon (drains sessions first)
codemonkey adapters        # list registered agents and their per-mode capabilities
codemonkey bots            # check bot configuration and surface security warnings

The first start launches the daemon automatically, detached from your terminal. You never need to start it by hand.


Usage: TUI console

codemonkey tui

Three regions: a tab bar at the top (one tab per session with a state badge), that session's live terminal screen in the middle, and a status bar plus approval area at the bottom.

Key Action
or [ ] switch tabs
19 jump to the Nth session
i input mode — send a message to the current session (Esc cancels, Enter sends)
y / a / n when awaiting approval: allow once / always allow / deny
b interrupt the current turn
r refresh now
q quit (sessions keep running in the background)

When a request is pending, the bottom pane shows the risk level, any matched dangerous rule, and a diff preview; answer with y/a/n.


Usage: Feishu bot

Feishu connects over a long-lived connection: no public domain, no encryption setup, and no listening port at all (zero attack surface). Full walkthrough in docs/03-feishu.md; the essentials follow.

Setup

  1. Create an internal app at open.feishu.cn
  2. From "Credentials & Basic Info", note the App ID and App Secret
  3. Under "Events & Callbacks", choose receive events over long connection, note the Verification Token, and subscribe to im.message.receive_v1
  4. Under "Permissions", grant im:message, im:message:send_as_bot, im:chat — plus speech_to_text:speech for voice
  5. Publish the app and add the bot to a group (or message it directly)

Write ~/.codemonkey/config.json:

{
  "bots": {
    "enabled": true,
    "feishu": {
      "enabled": true,
      "mode": "longconn",
      "appId": "cli_xxxxxxxx",
      "appSecret": "xxxxxxxx",
      "secret": "Verification Token",
      "allowedUsers": [],
      "subscribers": []
    }
  }
}
chmod 600 ~/.codemonkey/config.json   # it holds credentials
codemonkey stop-daemon && codemonkey daemon
codemonkey bots                       # self-check; reports anything missing

Finally, message the bot with /whoami. It replies with two lines you can paste straight into allowedUsers and subscribers; restart the daemon afterwards.

Session stickiness: just talk to it

Once a session is selected, send messages directly — no need to repeat the session ID:

you:  /new opencode /Users/me/proj
bot:  已启动 cm_a1b2c3  opencode (hybrid)
      已设为当前会话,直接发消息即可与它对话。

you:  analyse the architecture of this project
bot:  任务已收到,马不停蹄的开展工作ing......
bot:  [approval card] edit file: docs/arch.md  [Allow once][Always allow][Deny]
you:  (tap "Allow once"; the card immediately becomes "handled")
bot:  ✅ 回合完成 · 工具 read×8、write×1 · 改动 docs/arch.md
      详情:/summary cm_a1b2c3

The rule: once a session is bound, only messages starting with / are treated as commands; everything else is forwarded to the agent. This matters because you might reasonably say "list the files you changed" — under a "slash optional" rule that would be executed as /list. When no session is bound, the slash is optional.

Commands

session   /new <agent> <dir>     create and select a session
          /use <id>              switch current session (aliases /switch /attach)
          /close                 end the conversation binding; session keeps running (alias /detach)
          /exit [id]             stop the session and terminate the agent (aliases /stop /kill /quit)
          /resume <id>           resume a stopped session, restoring context where possible
          /list                  session list, ▶ marks the current one

query     /status <id>           session detail
          /summary <id> [mins]   progress summary
          /progress <id>         tool timeline and change surface
          /screen <id>           current terminal screen
          /perms                 pending approvals (returns a card with buttons)

action    /say <id> <message>    send a message to a specific session
          /keys [id] <command-or-keys...>  operate the agent terminal verbatim and return its content
          /abort <id>            interrupt the current turn

approval  /allow <request-id>    approve once
          /always <request-id>   auto-approve similar requests from now on
          /deny <request-id>     refuse

other     /whoami                show your open_id and the group chat_id
          /ok                    confirm transcribed voice input (aliases /yes /y)
          /help                  usage

Most commands also accept Chinese aliases (/列表, /状态, /进展, /批准, /拒绝, /切换, /断开, /退出, /恢复, …).

Voice input

Send a voice message and CodeMonkey downloads the audio and transcribes it with Feishu's ASR. The transcript is not executed directly — it's echoed back for confirmation first:

you:  [voice] split the auth module into its own package
bot:  🎤 听到:split the auth module into its own package
      确认发送给 cm_a1b2c3 请回复 /ok,重说请直接再发一条语音。
you:  /ok

The extra step exists because transcription has an error rate, and the resulting text drives real operations against your machine. The cost of a misrecognition is asymmetric. Recording again overwrites the pending transcript.

Limit: 60 seconds per message (longer is rejected explicitly).

Notification policy

Only four kinds of event are pushed: pending approvals, fatal errors, requests for human input, and end-of-turn progress.

Deliberately not pushed: the agent's prose replies (often thousands of words), token-level streaming, and individual tool calls (dozens per minute). Pushing those turns the chat into a log tail and gets the bot muted. Pull the detail with /summary or /progress instead.

DingTalk and QQ

Both use webhook mode, which needs a publicly reachable address (use cloudflared or ngrok locally). Capability differences are imposed by the platforms, not by CodeMonkey:

Platform Inbound verification Approval card
Feishu Verification Token (supports encrypt) real buttons, one tap decides
DingTalk HMAC-SHA256 signature + timestamp window markdown; buttons degrade to tappable commands
QQ Ed25519 signature + callback verification plain text; buttons degrade to copyable commands

Start with Feishu.

Security boundary

The daemon itself listens only on a Unix domain socket and 127.0.0.1. In Feishu long-connection mode it listens on no port at all.

Webhook mode has three gates, all of which must pass before a command runs:

  1. Path token — the request must carry ?token=, compared in constant time to avoid timing probes
  2. Platform signature — proves the request really came from that platform. Missing credentials means reject, not allow
  3. User allowlist — an empty allowedUsers rejects everything, including read-only commands (progress data is itself sensitive)

The allowlist applies identically to text, card buttons and voice. Buttons and voice are not back doors; the integration suite asserts this.


Configuration

~/.codemonkey/config.json. Every field is optional; without the file, defaults apply.

{
  "cols": 120,                    // virtual terminal size
  "rows": 40,
  "sandbox": "none",              // default sandbox policy: none | worktree
  "httpPort": 0,                  // >0 also listens on 127.0.0.1 over HTTP

  "permission": {
    "allow": ["git status", "npm test"],   // auto-approve (never overrides the built-in denylist)
    "deny": [],                            // extra dangerous rules; the 17 built-ins always apply
    "timeoutPolicy": "hold",               // hold (default) | reject | allow
    "timeoutMs": 600000
  },

  "summarizer": {                 // LLM summaries, off by default; rule-based always available
    "enabled": false,
    "baseUrl": "https://api.example.com/v1",   // any OpenAI-compatible endpoint
    "apiKey": "sk-...",
    "model": "qwen-plus"
  },

  "bots": {
    "enabled": false,
    "port": 45800,                // webhook mode only
    "host": "127.0.0.1",
    "feishu": {
      "enabled": true,
      "mode": "longconn",         // longconn (recommended) | webhook
      "appId": "cli_xxx",
      "appSecret": "xxx",
      "secret": "Verification Token",
      "encryptKey": "",           // only if encryption is enabled in the Feishu console
      "allowedUsers": ["ou_xxx"], // empty array = reject everything
      "subscribers": ["oc_xxx"]   // groups that receive pushed notifications
    },
    "dingtalk": {
      "enabled": false,
      "mode": "webhook",
      "token": "your own path token",
      "secret": "DingTalk signing secret",
      "outgoingUrl": "https://oapi.dingtalk.com/robot/send?access_token=xxx",
      "allowedUsers": [],
      "subscribers": []
    }
  }
}

The file holds credentials, so chmod 600 it. codemonkey bots checks the permission bits and warns, and never prints credential values.

Approval rules

The built-in denylist has 17 entries covering rm -rf, sudo, git push --force, git reset --hard, chmod 777, curl … | sh, writes to ~/.ssh and /etc, dd, mkfs and similar. They always apply and cannot be overridden by allow or by choosing "always allow" — one careless tap shouldn't create standing risk.

Pattern matching supports only *, and matches the whole string. That's deliberate: an ls rule will not approve rm -rf /; ls.

Storage layout

~/.codemonkey/
├── config.json              global configuration
├── audit.jsonl              approval audit trail (append-only)
├── daemon.sock              UDS listener
├── daemon.pid
└── sessions/<session-id>/
    ├── meta.json            session metadata (agent, mode, workspace, branch, native session id)
    ├── stream.cast          raw terminal stream, asciicast v2
    ├── events.jsonl         structured events
    ├── screen.txt           latest screen snapshot
    └── worktree/            the worktree sandbox tree (removed when the session ends)

Architecture

frontends/{cli,tui,http,bots}   frontends with equal capabilities, all over one core interface
daemon/{server,client,main}     the daemon; sessions outlive the terminal that started them
core/{events,session,manager,   core, standard library only
      permission,recorder,progress,summarizer,registry,config}
runtime/{pty,screen,net,        PTY, terminal emulation, HTTP, JSON-RPC, sandbox
         jsonrpc,sandbox}
adapters/{types,opencode,codex, adapters — the single point where agents differ
          generic-pty,mock}

Dependencies flow one way: core/ must not import adapters/ or frontends/, and adapters never import each other. core/ is forbidden from containing checks like if (adapter === 'opencode') — differences between agents may only be expressed through the Adapter interface and its capability declarations.

Every observability feature is built on one event contract (core/events.ts), the single source of truth: recording, TUI rendering, progress summaries and bot notifications all consume the same events.

Only translate() on the Adapter interface is allowed to contain agent-specific logic, which is why adding an agent requires no changes to the core.


Development

npm test                  # unit tests (492 cases)
npm run test:integration  # integration tests (the opencode suite needs the real binary)
npm run typecheck
npm run build

Integration tests come in two flavours. tests/integration/bots.test.ts and daemon.test.ts use test doubles and need no external services. opencode.test.ts actually launches opencode, sends a real prompt and completes a real approval; set CM_SKIP_INTEGRATION=1 to skip it.

When debugging bots, run the daemon in the foreground so logs are visible:

codemonkey daemon

Every hop is logged inbound and outbound — connection established, message received, forwarded to session, replied — and outbound failures carry the platform error code plus an actionable hint.


Known limitations

  • The CLI and bot messages are Chinese-only. There is no i18n layer yet; this English document describes an interface that still speaks Chinese at runtime.
  • qwen's native channel is not wired up. qwen serve --http-bridge is still marked Stage 1 experimental upstream; the protocol may change at any time, so the maintenance cost outweighs the benefit. It uses the PTY channel today.
  • cursor-agent's post-login screen features are unverified. The not-logged-in prompt is verified and reported correctly (you get "log in first" rather than a silent hang), but logging in requires a browser and can't be done unattended.
  • DingTalk and QQ have not been tested against real apps. Signature algorithms, message parsing and card structures are all covered by tests, but no full round trip has run against a live application. Feishu has been verified end to end.
  • Feishu ASR quality is unmeasured. Feishu's speech recognition targets general use; how well it handles mixed Chinese/English technical phrasing needs real verification. transcribeAudio takes its client through an interface, so swapping in local whisper is straightforward.
  • seatbelt / docker sandboxes are not implemented. Only none and worktree exist. Apple has deprecated macOS sandbox-exec, so whether to invest there is undecided.
  • Existing sessions cannot survive a daemon restart. They are drained and marked orphaned in history. /resume can restore the agent-side conversation context (supported by opencode and codex), but under a new session ID.
  • macOS only, as a guarantee. Linux is best-effort (the PTY layer itself is portable); Windows is untested.

Documentation

These design documents are written in Chinese.

About

Background runtime & observability console for CLI coding agents — run opencode, codex, kiro-cli in PTYs, approve permissions remotely via Feishu/DingTalk/QQ bots

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages