- container-runner.ts → agent-runner.ts - Add missing files: group-queue, group-folder, router, sender-allowlist, session-commands, channels/registry - Add image handling, skill sync, auto-continue, GroupQueue sections - Add 'both' agent type, data/attachments directory - Add npm test to development commands
8.7 KiB
Dual-agent AI assistant running Claude Code + Codex as parallel services over Discord.
Fork of qwibitai/nanoclaw
Overview
This fork runs two independent NanoClaw instances as systemd services:
- nanoclaw (Claude Code) — powered by Claude Agent SDK, trigger
@claude - nanoclaw-codex (Codex) — powered by Codex app-server JSON-RPC, trigger
@codex
Each service has its own store, data, and groups directories. Discord channels can be registered with either agent, or both (both agent type for shared channels).
Key Differences from Upstream
| Area | Upstream NanoClaw | This Fork |
|---|---|---|
| Agent runtime | Container-isolated (Docker/Apple Container) | Direct host processes (no containers) |
| Agent backends | Claude Code only | Claude Code + OpenAI Codex |
| Codex integration | N/A | codex app-server (JSON-RPC, streaming, turn/steer) |
| Session management | Container-based | Per-group CLAUDE_CONFIG_DIR / CODEX_HOME |
| Token management | Manual | Auto-refresh with session sync |
| Channel | Multi-channel (WhatsApp, Telegram, etc.) | Discord-focused |
| Image support | N/A | Bidirectional (receive as multimodal input, send as attachments) |
| Skill sync | Per-container | SSOT from ~/.claude/skills/ to all agent sessions |
| Deployment | Single service | Dual systemd services |
Architecture
Discord ──► SQLite ──► GroupQueue ──┬──► Claude Agent SDK (host process)
└──► Codex App-Server (JSON-RPC stdio)
├── thread/start, thread/resume
├── turn/start (streaming, multimodal)
├── turn/steer (mid-turn injection)
├── Auto-approval (bypass sandbox)
└── Auto-continue (text-only turn retry)
Directory Layout
nanoclaw/
├── src/
│ ├── index.ts # Orchestrator: state, message loop, agent invocation
│ ├── agent-runner.ts # Spawns agent processes, manages env/sessions/skills
│ ├── group-queue.ts # Per-group concurrency, priority queue, idle preemption
│ ├── group-folder.ts # Group directory resolution and management
│ ├── router.ts # Outbound message formatting and routing
│ ├── sender-allowlist.ts # Security: sender-based access control
│ ├── session-commands.ts # Session commands (/compact)
│ ├── token-refresh.ts # OAuth auto-refresh + session directory sync
│ ├── task-scheduler.ts # Scheduled tasks (cron/interval/once)
│ ├── ipc.ts # IPC watcher and task processing
│ ├── db.ts # SQLite operations
│ ├── config.ts # Paths, intervals, trigger patterns
│ └── channels/
│ ├── registry.ts # Channel self-registration system
│ └── discord.ts # Discord: mentions, images, typing, file attachments
├── container/
│ ├── agent-runner/ # Claude Code runner (Agent SDK, multimodal input)
│ ├── codex-runner/ # Codex runner (app-server JSON-RPC, auto-continue)
│ └── skills/ # Shared agent skills (browser, etc.)
├── store/ # Claude Code service DB
├── store-codex/ # Codex service DB
├── data/
│ ├── sessions/ # Per-group Claude sessions (.claude/)
│ └── attachments/ # Downloaded Discord image attachments
├── data-codex/sessions/ # Per-group Codex sessions (.codex/)
├── groups/ # Per-group memory and workspace (Claude Code)
├── groups-codex/ # Per-group memory and workspace (Codex)
└── logs/ # Service logs
Codex App-Server Integration
The Codex runner (container/codex-runner/) communicates with codex app-server via JSON-RPC over stdio:
- Session persistence: Thread IDs stored in DB, sessions saved as JSONL on disk
- Streaming:
item/agentMessage/deltanotifications for real-time text - Mid-turn steering: IPC messages injected via
turn/steerduring execution - Auto-approval:
approvalPolicy: "never"+sandbox: "danger-full-access" - Auto-continue: Detects text-only turns (no tool execution) and automatically retries up to 5 times to nudge the agent into actually executing tasks
- Multimodal input: Image attachments converted to
localImageinput blocks inturn/start - Per-group config: Model, effort, MCP servers configured per channel
Image Handling
Bidirectional image support through Discord:
- Receiving (user → agent): Discord image attachments are downloaded to
data/attachments/, then passed as base64ImageBlockParamcontent blocks (Claude Code) orlocalImageinput blocks (Codex) - Sending (agent → user): Markdown image links
[name.png](/path)in agent responses are automatically parsed and sent as Discord file attachments. Non-image file links are converted to readable filenames (BuildPanel.tsx:320)
Skill Sync
Skills are managed from a single source of truth (~/.claude/skills/ on the server) and automatically synced to all agent session directories at process start:
- Claude Code sessions:
~/.claude/skills/+ projectcontainer/skills/ - Codex sessions: Same sources, synced to per-group
.codex/directories - Skills auto-register as slash commands (
/name) in Claude Code and$namein Codex
OAuth Token Auto-Refresh
src/token-refresh.ts handles Claude Code OAuth token lifecycle:
- Checks every 5 minutes, refreshes 30 minutes before expiry
- Tries
platform.claude.comthen falls back toapi.anthropic.com - Syncs refreshed credentials to all per-group session directories
- Solves the known headless environment token expiry issue
GroupQueue
src/group-queue.ts manages agent execution with:
- Per-group serialization: Only one agent process per group at a time
- Global concurrency limit: Configurable max concurrent agents across all groups
- Task priority: Scheduled tasks drain before message processing
- Idle preemption: Idle agents are terminated when higher-priority tasks arrive
- Exponential backoff: Retries with backoff on processing failure
Setup
Prerequisites
- Linux (Ubuntu 22.04+) or macOS
- Node.js 20+
- Claude Code CLI
- Codex CLI (
npm install -g @openai/codex)
Environment Variables
# .env
DISCORD_BOT_TOKEN= # Claude Code bot token
DISCORD_CODEX_BOT_TOKEN= # Codex bot token (optional, for dual-bot)
ANTHROPIC_API_KEY= # Or use OAuth (CLAUDE_CODE_OAUTH_TOKEN)
OPENAI_API_KEY= # For Codex
CODEX_MODEL= # Default codex model
CODEX_EFFORT= # Default reasoning effort (low/medium/high)
Service Management (Linux)
systemctl --user start nanoclaw # Claude Code service
systemctl --user start nanoclaw-codex # Codex service
systemctl --user restart nanoclaw nanoclaw-codex # Restart both
journalctl --user -u nanoclaw -f # Follow logs
Service Management (macOS)
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
launchctl load ~/Library/LaunchAgents/com.nanoclaw-codex.plist
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
Channel Registration
Channels are registered in each service's SQLite database (registered_groups table). Each entry specifies:
- jid: Discord channel ID (
dc:<channel_id>) - folder: Group folder name (matches Discord channel name)
- trigger_pattern: Regex for bot activation (
@claudeor@codex) - agent_type:
claude-code,codex, orboth(shared channel) - work_dir: Working directory for the agent
- container_config: JSON config (e.g.,
{"codexEffort":"high"})
Development
npm run build # Build main project
cd container/agent-runner && npm run build # Build Claude runner
cd container/codex-runner && npm run build # Build Codex runner
npm run dev # Dev mode with hot reload
npm test # Run tests
License
MIT — Fork of qwibitai/nanoclaw
