bot-celly
v0.2.0
Published
Drive OpenCode coding agents from Discord, with one isolated microVM per project.
Maintainers
Readme
Celly turns Discord into a control plane for sandboxed OpenCode agents: start a project from any device, watch it stream, approve a command, and pick the session back up any time.
It is for people who want an always-on coding agent without exposing their whole machine: each project runs in its own disposable Docker sandbox (microVM), and the only thing the agent can reach is that project's directory. For a deeper tour, see the docs.
Why Celly
- Drive agents from anywhere. No terminal required — Discord on desktop or mobile is the whole UI.
- Isolation by default. Every project gets its own microVM, not a shared shell on your host.
- Nothing to babysit. Start a session, close the app, come back to the transcript and the running sandbox.
- Built on OpenCode. Use the same sessions from Discord and a terminal attached to the sandbox.
How it works
The whole model fits in three lines:
- Channel = project. One sandbox and one host directory per Discord channel.
- Thread = session. One OpenCode conversation per thread.
- The bot runs on the sandbox host. It supervises one
sbx exec ... opencode servechild per project and talks to it over the sandbox's loopback port. Only the host can run thesbx(Docker Sandboxes) CLI, which is why Celly is host-only.
Contents
- Why Celly
- How it works
- Features
- Architecture
- Prerequisites
- Quick start
- Commands
- Security
- Operations
- FAQ and troubleshooting
- Development
- Status and roadmap
- Limitations
- Known issues
- Contributing
- License
- Credits
Features
- Per-project sandbox isolation. Every project owns a microVM; the host filesystem outside the mounted project directory is unreachable by the agent.
- Streaming replies. Assistant text and tool activity stream into the thread, throttled into a single live message.
- Session resume.
/resumereopens a past OpenCode session in a new thread. - Per-thread worktrees.
/worktreecreates, merges, and removes git worktrees under<project>/.celly/worktrees. SetWORKTREE_DEFAULT=true(or/worktree default state:on) to start every new session in its own worktree; non-git projects fall back to the project root./forkinherits the source worktree unless you passnew_worktree:true. - Model, agent, and thinking depth.
/model,/agent, and/thinkingpick per-thread settings. - Abort.
/abortstops the current run, or every active run in the channel. - Approvals.
/modepicksauto,buttons, orplan;buttonsposts permission requests as Discord buttons. Agent questions render inline in the streamed reply, in the order they were asked, with buttons/selects/modals on that message so the answer is not split around them. Decisions are written to a best-effort audit log, and requests dropped before a decision (run ended, server-resolved, timed out) are logged with their cause so a stale click can be traced. - Shell. A message starting with
!runsbash -lc <command>inside the project's sandbox. - Text attachments. Size-capped, written to a validated inbox, referenced in the prompt.
- Terminal coexistence. The same sessions are reachable from
opencode attachinside the sandbox. - Access control. Guild owner,
Manage Guild/Administrator, an access role, and a block role. - Hardened permission policy. A bot-enforced deny list blocks publish, push, and env-file inspection. It is verified after every wake and re-written only when the running server has been weakened.
- Cost tracking and budgets.
/costreports per-thread and per-channel usage, and a session budget (env or/budget) stops a run that exceeds it. - Local admin page. A loopback-only JSON API on
127.0.0.1:4560(ADMIN_PORT,0disables) for a loopback ops console: live project cards with start/stop/restart, project create/remove, usage and cost, the audit trail, and per-project logs and sessions. No authentication by design — loopback only./dashboard(owner-only) posts the URL to Discord.
Architecture
Discord (channel = project, thread = session)
│
▼
┌──────────────────── host (Node 24, Windows 11) ─────────────────────┐
│ Celly bot │
│ • gateway + slash commands │
│ • Runner + Renderer: run state and streaming edits │
│ • EventRouter: SSE /global/event -> thread routing │
│ • ProjectService: create / wake / stop one sbx per project │
│ • SQLite: projects, threads │
│ │
│ 127.0.0.1:HOSTPORT (Basic auth) │
└──────────────────────────────────┬──────────────────────────────────┘
▼
┌───────────────────┐
│ sbx microVM │ one per project
│ opencode serve │ sandbox port 4096
│ mounted project │
└───────────────────┘See the architecture reference for the module map, create saga, and boot recovery.
Prerequisites
Gather these before you start — the setup steps assume they are ready.
- [ ] A host where
sbxruns. Windows 11, or macOS/Linux where Docker Sandboxes runs. The bot cannot run inside a Linux container: only the host can executesbx. - [ ] Node 24.x exactly (pinned by
enginesand.nvmrc). - [ ] Docker Sandboxes
sbx>= 0.45, installed and logged in. - [ ] A Docker login and an initialized network policy
(
sbx policy init balanced). - [ ] A Discord application with a bot token, the Message Content intent enabled, and one or more guild IDs.
- [ ] An OpenCode provider configured through
sbx secret.
Quick start
Bootstrap the host (once, interactive, as the logged-in user):
winget install -h Docker.sbx sbx setup sbx login sbx policy init balanced sbx secret set <provider>Install and run:
npx bot-celly@latestThe first run asks for your Discord bot token and guild IDs, saves them to
~/.bot-celly/.env(%USERPROFILE%\.bot-celly\.envon Windows, override withCELLY_HOME), checks thatsbxis installed and its network policy is initialized, then starts the bot.PROJECTS_ROOTdefaults to~/Celly/projects. OptionalCELLY_ASCII=1forces ASCII output symbols (off by default). Re-run the same command to start;npx bot-celly doctordiagnoses the host without starting;npx bot-celly setupreconfigures. Prefer a source checkout? See Development.In Discord, run
/project add name:<name> path:<path>and send a message in the new channel.
You'll know it worked when: the bot comes online, your
/project addcommand creates a channel under the Forge category, and a plain message in that channel opens a thread and streams the agent's reply.
The full walkthrough is in the Quickstart. Every environment variable is documented in Configuration.
Commands
The everyday handful:
| Command | What it does |
|---|---|
| /project add <name> <path> | Register a directory and spin up its sandbox. |
| /new [prompt] | Start a session in the project channel. |
| /resume | Reopen a past session in a new thread. |
| /abort | Stop the current run, or all runs in the channel. |
| /model · /agent · /thinking | Switch the model, agent, or thinking depth for a thread. |
| /mode auto\|buttons\|plan | Choose how approvals are requested. |
| /cost | Show accumulated cost, tokens, and the session budget. |
| Command | Where | Description |
|---|---|---|
| /project add <name> <path> | guild (owner) | Register a directory under PROJECTS_ROOT. |
| /project create <name> [clone] [branch] | guild (owner) | Create a project directory; optionally git clone an https repository. |
| /project list | guild | List projects with status and health. |
| /project status <name> | guild | Status, port, and session count. |
| /project start <name> | guild (owner) | Wake the sandbox (recreates it if missing). |
| /project stop <name> | guild (owner) | Stop the sandbox. |
| /project restart <name> | guild (owner) | Restart the supervised server without stopping the sandbox. |
| /project remove <name> <confirm> | guild (owner) | Remove the sandbox, project, and channel. |
| /new [prompt] | project channel | Start a new session. |
| /resume | project channel | Resume a past session in a new thread. |
| /abort | channel or thread | Abort the current run (or all runs). |
| /model | thread or project channel | Choose the model for this thread or the channel default. |
| /agent | thread or project channel | Choose the agent for this thread or the channel default. |
| /thinking [depth] | thread or project channel | Choose the current model's thinking depth (variant) for this thread or the channel default. |
| /attach | thread | Show the terminal attach command for this thread. |
| /session-id | thread | Show this thread's session id and attach command. |
| /queue | thread | Show and manage queued prompts for this thread. |
| /undo | thread | Revert the session to its last user message. |
| /redo | thread | Restore messages reverted by /undo. |
| /diff | thread | List changed files with +adds/-dels and totals. |
| /share | thread | Share the session and post the URL. |
| /unshare | thread | Stop sharing the session. |
| /compact | thread | Summarize the session with the thread's model. |
| /context-usage | thread | Token use against the model's context limit. |
| /worktree status\|new\|merge\|remove | thread | Manage this thread's git worktree. |
| /worktree default <inherit\|on\|off> | project channel (owner) | Set whether new sessions start in a worktree. |
| /fork [prompt] [new_worktree] / /btw <prompt> | thread | Fork this session into a new thread; new_worktree gives the fork its own worktree. |
| /last-sessions [count] | channel or thread | List recent threads (ephemeral, max 10). |
| /cost | thread or channel | Show accumulated cost, tokens, and the session budget. |
| /budget show\|set <usd> | channel (owner) | Show or set the per-channel session budget. |
| /mode <auto\|buttons\|plan> | project channel or thread (owner) | Set the channel approval mode. |
| /dashboard | guild (owner) | Post the loopback admin console URL (ADMIN_PORT; disabled when 0). |
| /task add <channel> <prompt> <every_minutes> | guild (owner) | Schedule a recurring prompt in a project channel. |
| /task list | guild | List scheduled tasks. |
| /task remove <id> | guild (owner) | Remove a scheduled task. |
| !<command> | channel or thread | Run a shell command in the sandbox. |
Full details and the deferred list are in the commands reference.
Security
At a glance:
- argv-only
sbxcalls (shell: false) — no shell interpolation. - One microVM per project — the host filesystem outside the mount is unreachable by the agent.
- Contained paths under
PROJECTS_ROOT, checked against a sensitive-path denylist. - Loopback-only server with a generated password; provider credentials live
in
sbx secretand never touch argv or Discord.
The deny list is defense-in-depth, not a hard boundary — the sandbox is. Read the security reference for the full model.
Operations
The host is designed to run unattended:
- Logs. Console output plus
DATA_DIR/bot.log(JSONL) and per-projectDATA_DIR/logs/<project>.log, with token/password redaction. The CLI setsDATA_DIRto~/.bot-celly/datawhenDATA_DIRis unset, so logs default to~/.bot-celly/data/bot.log; a source checkout uses./data. - Admin page. A loopback-only status page and JSON API on
127.0.0.1:4560(ADMIN_PORT), unauthenticated by design and never network-exposed. - Backups. Scheduled SQLite backups under
DATA_DIR/backupswith pruning. - Rotation.
bot.logand project server logs rotate by size.
The full operational guide is in Operations.
FAQ and troubleshooting
The bot is online but ignores plain messages. Enable the Message Content intent in the Discord Developer Portal, then restart the bot.
sbx: command not found, or the bot exits during preflight.
Celly must run on the host that owns sbx (Windows 11), not inside a Linux
container. Install and log in first (see Prerequisites).
Startup complains the network policy is missing.
Run sbx policy init balanced.
The agent fails with a provider or auth error.
Register the provider with sbx secret set <provider> on the host and confirm
you are logged in with sbx login.
A project shows unhealthy, or the serve child will not start.
Check ~/.bot-celly/data/bot.log, then run /project start <name> (or
/project restart <name>). If the log shows
failed to start runtime with 500 Internal Server Error, that is a known
upstream sbx issue — restart the host. See Known issues.
Role configuration is rejected. Use role IDs, not role names.
Where do I look when something is off?
The loopback-only admin page on 127.0.0.1:4560 (projects, health, logs,
audit) and ~/.bot-celly/data/bot.log. See
Operations.
Development
npm run dev # tsx watch src/index.ts
npm test # full vitest suite
npm run typecheck # tsc --noEmit
npm run build # tsc -p tsconfig.jsonThe published CLI is the same code: npx bot-celly@latest runs dist/cli.js,
which sets up ~/.bot-celly and then calls dist/index.js. To exercise it from
a checkout, run npm run build && npx bot-celly (requires Node 24).
Two host-only scripts exercise the real chain and are not run by the Linux test suite:
node scripts/spike-full-chain.mjs # sbx + serve + health spike
node scripts/smoke.mjs C:\path\to\project # create → prompt → abort → removeDocs live in docs-site/ (npm run docs:dev, npm run docs:validate,
npm run docs:links).
Status and roadmap
The command set above is the current surface. Planned, not committed:
- React dashboard. Replace the server-rendered loopback admin page with a React bot dashboard for project cards, streaming logs, cost and budgets, the audit trail, and approvals, loopback-only by default.
- Thread title updater and live thread stats. Keep each Discord thread's title in sync with its session, and surface live per-thread stats (run state, model and agent, queued prompts, tokens, cost) in the thread or channel.
- Image output. Post images the agent produces (screenshots, diagrams) into the thread, alongside streamed text.
- Cloud sandboxes and hosted deployment. Run projects in cloud sandboxes and deploy Celly as a hosted service, so no local host is required.
- Image and voice input. Send images and voice messages as prompts.
- Single OpenCode API surface. Move all OpenCode calls onto its v2 SDK surface and remove the v1 client, so the v1/v2 split exists only in upstream event names and not in Celly's code.
The canonical roadmap is at Roadmap.
Limitations
- The message queue is lost on restart. Queued-but-unsent prompts are dropped; active runs re-attach from session history.
- Role names are not accepted for role configuration; use role IDs.
- The finalization footer shows cost and in/out tokens. Cache reads/writes
are tracked in
/costbut not rendered in the footer. - Access control is global across guilds. The same role IDs apply to every configured guild; per-guild roles are not supported. A configured guild the bot cannot see is skipped at startup with a warning.
- Per-user command rate limiting, sandbox disk-usage warnings, and
DATA_DIRcloud-sync detection are backlog. KeepDATA_DIRout of synced folders. - Host-only items (the spike,
sbx policy lssemantics, Windows path mapping, live Discord behavior) are exercised on the host, not in the Linux test environment. - The one-line install uses npm.
npx bot-celly@lateststill requires Node 24 and a host withsbxinstalled, logged in, and policy-initialized. - The bash deny list is defense-in-depth, not the sandbox boundary. Celly statically analyzes shell commands and fails closed on what it cannot prove, but arbitrary wrapper binaries, encoded payloads, and unmodelled shell features can still reach the sandbox. The sandbox is the boundary.
- Discord only renders code fences made of exactly three backticks. Celly normalizes agent output so long replies never emit a longer fence, but it cannot represent a nested fence the way a plain Markdown file can.
The canonical list, including what is deferred, lives in Limitations.
Known issues
Confirmed bugs with workarounds, including upstream sbx and OpenCode problems
that affect Celly:
- Project fails to start at boot with
failed to start runtime. Thesbxruntime returns500 Internal Server Errorwhen the sandbox is woken, so the project is skipped with aproject not ready at bootwarning and stays unhealthy. This is an upstreamsbxbug (docker/sbx-releases#350); the only reliable recovery is to restart the host.
The canonical list, with symptoms and workarounds, lives in Known issues.
Contributing
Issues and pull requests are welcome. Before opening a PR, run npm test,
npm run typecheck, and npm run build. Keep the argv-only invariant, add
tests for behavior changes, add a changeset for behavior changes, update the
docs and this README, and never commit secrets. This project follows the
Code of Conduct; report vulnerabilities via
SECURITY.md, not a public issue. See
Contributing for details and AGENTS.md
for the definition of done.
License
MIT. See LICENSE.
Credits
- Inspired by remorses/kimaki (MIT).
- Built on Docker Sandboxes
(
sbx) and OpenCode. - The full stack and credits are on Tech stack.
