diff --git a/CLAUDE.md b/CLAUDE.md
index 8e6052d..4888f80 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,10 +1,10 @@
-# NanoClaw
+# EJClaw
Dual-agent AI assistant (Claude Code + Codex) over Discord. Based on [qwibitai/nanoclaw](https://github.com/qwibitai/nanoclaw).
## Quick Context
-Two systemd services (`nanoclaw`, `nanoclaw-codex`) share the same codebase but run with separate stores, data, and groups (will be unified — DB supports shared access via WAL mode + service partitioning). Agents run as direct host processes (no containers). Claude Code uses the Agent SDK; Codex uses the Codex SDK (`codex exec`). Auth via `CLAUDE_CODE_OAUTH_TOKEN` in `.env` (1-year token from `claude setup-token`).
+Two systemd services (`ejclaw`, `ejclaw-codex`) share the same codebase but run with separate stores, data, and groups (will be unified — DB supports shared access via WAL mode + service partitioning). Agents run as direct host processes (no containers). Claude Code uses the Agent SDK; Codex uses the Codex SDK (`codex exec`). Auth via `CLAUDE_CODE_OAUTH_TOKEN` in `.env` (1-year token from `claude setup-token`).
## Key Files
@@ -29,7 +29,7 @@ Two systemd services (`nanoclaw`, `nanoclaw-codex`) share the same codebase but
| `/setup` | First-time installation, authentication, service configuration |
| `/customize` | Adding channels, integrations, changing behavior |
| `/debug` | Agent issues, logs, troubleshooting |
-| `/update-nanoclaw` | Bring upstream NanoClaw updates into a customized install |
+| `/update-nanoclaw` | Bring upstream EJClaw updates into a customized install |
| `/qodo-pr-resolver` | Fetch and fix Qodo PR review issues interactively or in batch |
| `/get-qodo-rules` | Load org- and repo-level coding rules from Qodo before code tasks |
@@ -45,17 +45,17 @@ npm run dev # Dev mode with hot reload
Service management (Linux):
```bash
-systemctl --user restart nanoclaw nanoclaw-codex # Restart both
-systemctl --user status nanoclaw # Check status
-journalctl --user -u nanoclaw -f # Follow logs
+systemctl --user restart ejclaw ejclaw-codex # Restart both
+systemctl --user status ejclaw # Check status
+journalctl --user -u ejclaw -f # Follow logs
```
-Deploy to server: `scp dist/*.js clone-ej@100.64.185.108:~/nanoclaw/dist/`
+Deploy to server: `scp dist/*.js clone-ej@100.64.185.108:~/ejclaw/dist/`
## Dual-Service Architecture
-- `nanoclaw.service` — Claude Code bot (`@claude`), `SERVICE_ID=claude`, `SERVICE_AGENT_TYPE=claude-code`
-- `nanoclaw-codex.service` — Codex bot (`@codex`), `SERVICE_ID=codex`, `SERVICE_AGENT_TYPE=codex`
+- `ejclaw.service` — Claude Code bot (`@claude`), `SERVICE_ID=claude`, `SERVICE_AGENT_TYPE=claude-code`
+- `ejclaw-codex.service` — Codex bot (`@codex`), `SERVICE_ID=codex`, `SERVICE_AGENT_TYPE=codex`
- Both share the same codebase (`dist/index.js`), differentiated by env vars
- Unified dirs (`store/`, `groups/`, `data/` shared by both services):
- `router_state`: keys prefixed with `{SERVICE_ID}:` (e.g., `claude:last_timestamp`)
@@ -70,13 +70,14 @@ Unified DB + directories (both services share `store/`, `groups/`, `data/`):
| 항목 | 경로 |
|------|------|
| **DB** | `store/messages.db` (공유, WAL 모드) |
-| 서비스 로그 (Claude) | `journalctl --user -u nanoclaw -f` 또는 `logs/nanoclaw.log` |
-| 서비스 로그 (Codex) | `journalctl --user -u nanoclaw-codex -f` 또는 `logs/nanoclaw-codex.log` |
+| 서비스 로그 (Claude) | `journalctl --user -u ejclaw -f` 또는 `logs/ejclaw.log` |
+| 서비스 로그 (Codex) | `journalctl --user -u ejclaw-codex -f` 또는 `logs/ejclaw-codex.log` |
| 그룹별 로그 | `groups/{name}/logs/` (공유 채널은 양쪽 봇 로그가 같은 폴더) |
| Claude 세션 | `data/sessions/{name}/.claude/` |
| Codex 세션 | `data/sessions/{name}/.codex/` |
-| Claude 글로벌 설정 | `groups/global/CLAUDE.md` |
-| Codex 글로벌 설정 | `~/.codex/AGENTS.md` |
+| Claude 플랫폼 규칙 | `prompts/claude-platform.md` |
+| Codex 플랫폼 규칙 | `prompts/codex-platform.md` |
+| Claude 글로벌 메모리 | `groups/global/CLAUDE.md` |
## Codex SDK
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dd3614d..8a358a5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,7 +8,7 @@
## Skills
-A [skill](https://code.claude.com/docs/en/skills) is a markdown file in `.claude/skills/` that teaches Claude Code how to transform a NanoClaw installation.
+A [skill](https://code.claude.com/docs/en/skills) is a markdown file in `.claude/skills/` that teaches Claude Code how to transform an EJClaw installation.
A PR that contributes a skill should not modify any source files.
diff --git a/README.md b/README.md
index f39fdfe..976bd25 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,11 @@
-
-
-
+EJClaw
Dual-agent AI assistant running Claude Code + Codex as parallel services over Discord.
- Based on qwibitai/nanoclaw
+ Forked from qwibitai/nanoclaw
## Overview
@@ -40,7 +38,7 @@ Discord ──► SQLite ──► GroupQueue ──┬──► Claude Agent SD
### Directory Layout
```
-nanoclaw/
+ejclaw/
├── src/
│ ├── index.ts # Orchestrator: state, message loop, agent invocation
│ ├── agent-runner.ts # Spawns agent processes, manages env/sessions/skills
@@ -119,8 +117,8 @@ Skills are managed from a single source of truth (`~/.claude/skills/` on the ser
### 1. Clone and Install
```bash
-git clone https://github.com/phj1081/nanoclaw.git
-cd nanoclaw
+git clone https://github.com/phj1081/nanoclaw.git ejclaw
+cd ejclaw
npm install
npm run build:runners # installs + builds both agent-runner and codex-runner
npm run build # builds main project
@@ -163,17 +161,17 @@ DISCORD_BOT_TOKEN= # Codex Discord bot token (different from above)
### 4. Systemd Services (Linux)
-Create `~/.config/systemd/user/nanoclaw.service`:
+Create `~/.config/systemd/user/ejclaw.service`:
```ini
[Unit]
-Description=NanoClaw Claude Code
+Description=EJClaw Claude Code
After=network.target
[Service]
Type=simple
-ExecStart=/path/to/node /path/to/nanoclaw/dist/index.js
-WorkingDirectory=/path/to/nanoclaw
+ExecStart=/path/to/node /path/to/ejclaw/dist/index.js
+WorkingDirectory=/path/to/ejclaw
Restart=always
RestartSec=5
Environment=HOME=/home/youruser
@@ -183,26 +181,26 @@ Environment=PATH=/path/to/node/bin:/usr/local/bin:/usr/bin:/bin
WantedBy=default.target
```
-Create `~/.config/systemd/user/nanoclaw-codex.service`:
+Create `~/.config/systemd/user/ejclaw-codex.service`:
```ini
[Unit]
-Description=NanoClaw Codex
+Description=EJClaw Codex
After=network.target
[Service]
-EnvironmentFile=/path/to/nanoclaw/.env.codex
+EnvironmentFile=/path/to/ejclaw/.env.codex
Type=simple
-ExecStart=/path/to/node /path/to/nanoclaw/dist/index.js
-WorkingDirectory=/path/to/nanoclaw
+ExecStart=/path/to/node /path/to/ejclaw/dist/index.js
+WorkingDirectory=/path/to/ejclaw
Restart=always
RestartSec=5
Environment=HOME=/home/youruser
Environment=PATH=/path/to/node/bin:/usr/local/bin:/usr/bin:/bin
Environment=ASSISTANT_NAME=codex
-Environment=NANOCLAW_STORE_DIR=/path/to/nanoclaw/store-codex
-Environment=NANOCLAW_DATA_DIR=/path/to/nanoclaw/data-codex
-Environment=NANOCLAW_GROUPS_DIR=/path/to/nanoclaw/groups-codex
+Environment=EJCLAW_STORE_DIR=/path/to/ejclaw/store-codex
+Environment=EJCLAW_DATA_DIR=/path/to/ejclaw/data-codex
+Environment=EJCLAW_GROUPS_DIR=/path/to/ejclaw/groups-codex
[Install]
WantedBy=default.target
@@ -212,12 +210,12 @@ Then enable and start:
```bash
systemctl --user daemon-reload
-systemctl --user enable nanoclaw nanoclaw-codex
-systemctl --user start nanoclaw nanoclaw-codex
+systemctl --user enable ejclaw ejclaw-codex
+systemctl --user start ejclaw ejclaw-codex
# Logs
-journalctl --user -u nanoclaw -f
-journalctl --user -u nanoclaw-codex -f
+journalctl --user -u ejclaw -f
+journalctl --user -u ejclaw-codex -f
```
### 5. Register Discord Channels
@@ -247,9 +245,9 @@ Fields:
### macOS (launchd)
```bash
-launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
-launchctl load ~/Library/LaunchAgents/com.nanoclaw-codex.plist
-launchctl kickstart -k gui/$(id -u)/com.nanoclaw
+launchctl load ~/Library/LaunchAgents/com.ejclaw.plist
+launchctl load ~/Library/LaunchAgents/com.ejclaw-codex.plist
+launchctl kickstart -k gui/$(id -u)/com.ejclaw
```
## Development
diff --git a/docs/DEBUG_CHECKLIST.md b/docs/DEBUG_CHECKLIST.md
index 5597067..d2b182e 100644
--- a/docs/DEBUG_CHECKLIST.md
+++ b/docs/DEBUG_CHECKLIST.md
@@ -1,4 +1,4 @@
-# NanoClaw Debug Checklist
+# EJClaw Debug Checklist
## Known Issues (2026-02-08)
@@ -16,7 +16,7 @@ Both timers fire at the same time, so containers always exit via hard SIGKILL (c
```bash
# 1. Is the service running?
launchctl list | grep nanoclaw
-# Expected: PID 0 com.nanoclaw (PID = running, "-" = not running, non-zero exit = crashed)
+# Expected: PID 0 com.ejclaw (PID = running, "-" = not running, non-zero exit = crashed)
# 2. Any running containers?
container ls --format '{{.Names}} {{.Status}}' 2>/dev/null | grep nanoclaw
@@ -25,13 +25,13 @@ container ls --format '{{.Names}} {{.Status}}' 2>/dev/null | grep nanoclaw
container ls -a --format '{{.Names}} {{.Status}}' 2>/dev/null | grep nanoclaw
# 4. Recent errors in service log?
-grep -E 'ERROR|WARN' logs/nanoclaw.log | tail -20
+grep -E 'ERROR|WARN' logs/ejclaw.log | tail -20
# 5. Is WhatsApp connected? (look for last connection event)
-grep -E 'Connected to WhatsApp|Connection closed|connection.*close' logs/nanoclaw.log | tail -5
+grep -E 'Connected to WhatsApp|Connection closed|connection.*close' logs/ejclaw.log | tail -5
# 6. Are groups loaded?
-grep 'groupCount' logs/nanoclaw.log | tail -3
+grep 'groupCount' logs/ejclaw.log | tail -3
```
## Session Transcript Branching
@@ -62,7 +62,7 @@ for i, line in enumerate(lines):
```bash
# Check for recent timeouts
-grep -E 'Container timeout|timed out' logs/nanoclaw.log | tail -10
+grep -E 'Container timeout|timed out' logs/ejclaw.log | tail -10
# Check container log files for the timed-out container
ls -lt groups/*/logs/container-*.log | head -10
@@ -71,23 +71,23 @@ ls -lt groups/*/logs/container-*.log | head -10
cat groups//logs/container-.log
# Check if retries were scheduled and what happened
-grep -E 'Scheduling retry|retry|Max retries' logs/nanoclaw.log | tail -10
+grep -E 'Scheduling retry|retry|Max retries' logs/ejclaw.log | tail -10
```
## Agent Not Responding
```bash
# Check if messages are being received from WhatsApp
-grep 'New messages' logs/nanoclaw.log | tail -10
+grep 'New messages' logs/ejclaw.log | tail -10
# Check if messages are being processed (container spawned)
-grep -E 'Processing messages|Spawning container' logs/nanoclaw.log | tail -10
+grep -E 'Processing messages|Spawning container' logs/ejclaw.log | tail -10
# Check if messages are being piped to active container
-grep -E 'Piped messages|sendMessage' logs/nanoclaw.log | tail -10
+grep -E 'Piped messages|sendMessage' logs/ejclaw.log | tail -10
# Check the queue state — any active containers?
-grep -E 'Starting container|Container active|concurrency limit' logs/nanoclaw.log | tail -10
+grep -E 'Starting container|Container active|concurrency limit' logs/ejclaw.log | tail -10
# Check lastAgentTimestamp vs latest message timestamp
sqlite3 store/messages.db "SELECT chat_jid, MAX(timestamp) as latest FROM messages GROUP BY chat_jid ORDER BY latest DESC LIMIT 5;"
@@ -97,10 +97,10 @@ sqlite3 store/messages.db "SELECT chat_jid, MAX(timestamp) as latest FROM messag
```bash
# Check mount validation logs (shows on container spawn)
-grep -E 'Mount validated|Mount.*REJECTED|mount' logs/nanoclaw.log | tail -10
+grep -E 'Mount validated|Mount.*REJECTED|mount' logs/ejclaw.log | tail -10
# Verify the mount allowlist is readable
-cat ~/.config/nanoclaw/mount-allowlist.json
+cat ~/.config/ejclaw/mount-allowlist.json
# Check group's container_config in DB
sqlite3 store/messages.db "SELECT name, container_config FROM registered_groups;"
@@ -114,7 +114,7 @@ container run -i --rm --entrypoint ls nanoclaw-agent:latest /workspace/extra/
```bash
# Check if QR code was requested (means auth expired)
-grep 'QR\|authentication required\|qr' logs/nanoclaw.log | tail -5
+grep 'QR\|authentication required\|qr' logs/ejclaw.log | tail -5
# Check auth files exist
ls -la store/auth/
@@ -127,17 +127,17 @@ npm run auth
```bash
# Restart the service
-launchctl kickstart -k gui/$(id -u)/com.nanoclaw
+launchctl kickstart -k gui/$(id -u)/com.ejclaw
# View live logs
-tail -f logs/nanoclaw.log
+tail -f logs/ejclaw.log
# Stop the service (careful — running containers are detached, not killed)
-launchctl bootout gui/$(id -u)/com.nanoclaw
+launchctl bootout gui/$(id -u)/com.ejclaw
# Start the service
-launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.nanoclaw.plist
+launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ejclaw.plist
# Rebuild after code changes
-npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw
+npm run build && launchctl kickstart -k gui/$(id -u)/com.ejclaw
```
diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md
index 227c9ad..9b21908 100644
--- a/docs/REQUIREMENTS.md
+++ b/docs/REQUIREMENTS.md
@@ -1,4 +1,4 @@
-# NanoClaw Requirements
+# EJClaw Requirements
Original requirements and design decisions from the project creator.
@@ -8,7 +8,7 @@ Original requirements and design decisions from the project creator.
This is a lightweight, secure alternative to OpenClaw (formerly ClawBot). That project became a monstrosity - 4-5 different processes running different gateways, endless configuration files, endless integrations. It's a security nightmare where agents don't run in isolated processes; there's all kinds of leaky workarounds trying to prevent them from accessing parts of the system they shouldn't. It's impossible for anyone to realistically understand the whole codebase. When you run it you're kind of just yoloing it.
-NanoClaw gives you the core functionality without that mess.
+EJClaw gives you the core functionality without that mess.
---
@@ -193,4 +193,4 @@ These are the creator's settings, stored here for reference:
## Project Name
-**NanoClaw** - A reference to Clawdbot (now OpenClaw).
+**EJClaw** - A reference to Clawdbot (now OpenClaw).
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index db6fc18..8318d90 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -1,4 +1,4 @@
-# NanoClaw Security Model
+# EJClaw Security Model
## Trust Model
@@ -23,7 +23,7 @@ This is the primary security boundary. Rather than relying on application-level
### 2. Mount Security
-**External Allowlist** - Mount permissions stored at `~/.config/nanoclaw/mount-allowlist.json`, which is:
+**External Allowlist** - Mount permissions stored at `~/.config/ejclaw/mount-allowlist.json`, which is:
- Outside project root
- Never mounted into containers
- Cannot be modified by agents
diff --git a/docs/SPEC.md b/docs/SPEC.md
index fd6e535..8ae7970 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -1,4 +1,4 @@
-# NanoClaw Specification
+# EJClaw Specification
A personal Claude assistant with multi-channel support, persistent memory per conversation, scheduled tasks, and container-isolated agent execution.
@@ -314,12 +314,12 @@ nanoclaw/
│ └── ipc/ # Container IPC (messages/, tasks/)
│
├── logs/ # Runtime logs (gitignored)
-│ ├── nanoclaw.log # Host stdout
-│ └── nanoclaw.error.log # Host stderr
+│ ├── ejclaw.log # Host stdout
+│ └── ejclaw.error.log # Host stderr
│ # Note: Per-container logs are in groups/{folder}/logs/container-*.log
│
└── launchd/
- └── com.nanoclaw.plist # macOS service configuration
+ └── com.ejclaw.plist # macOS service configuration
```
---
@@ -422,7 +422,7 @@ Files with `{{PLACEHOLDER}}` values need to be configured:
## Memory System
-NanoClaw uses a hierarchical memory system based on CLAUDE.md files.
+EJClaw uses a hierarchical memory system based on CLAUDE.md files.
### Memory Hierarchy
@@ -555,7 +555,7 @@ This allows the agent to understand the conversation context even if it wasn't m
## Scheduled Tasks
-NanoClaw has a built-in scheduler that runs tasks as full agents in their group's context.
+EJClaw has a built-in scheduler that runs tasks as full agents in their group's context.
### How Scheduling Works
@@ -616,7 +616,7 @@ From main channel:
## MCP Servers
-### NanoClaw MCP (built-in)
+### EJClaw MCP (built-in)
The `nanoclaw` MCP server is created dynamically per agent call with the current group's context.
@@ -636,12 +636,12 @@ The `nanoclaw` MCP server is created dynamically per agent call with the current
## Deployment
-NanoClaw runs as a single macOS launchd service.
+EJClaw runs as a single macOS launchd service.
### Startup Sequence
-When NanoClaw starts, it:
-1. **Ensures container runtime is running** - Automatically starts it if needed; kills orphaned NanoClaw containers from previous runs
+When EJClaw starts, it:
+1. **Ensures container runtime is running** - Automatically starts it if needed; kills orphaned EJClaw containers from previous runs
2. Initializes the SQLite database (migrates from JSON files if they exist)
3. Loads state from SQLite (registered groups, sessions, router state)
4. **Connects channels** — loops through registered channels, instantiates those with credentials, calls `connect()` on each
@@ -652,16 +652,16 @@ When NanoClaw starts, it:
- Recovers any unprocessed messages from before shutdown
- Starts the message polling loop
-### Service: com.nanoclaw
+### Service: com.ejclaw
-**launchd/com.nanoclaw.plist:**
+**launchd/com.ejclaw.plist:**
```xml
Label
- com.nanoclaw
+ com.ejclaw
ProgramArguments
{{NODE_PATH}}
@@ -683,9 +683,9 @@ When NanoClaw starts, it:
Andy
StandardOutPath
- {{PROJECT_ROOT}}/logs/nanoclaw.log
+ {{PROJECT_ROOT}}/logs/ejclaw.log
StandardErrorPath
- {{PROJECT_ROOT}}/logs/nanoclaw.error.log
+ {{PROJECT_ROOT}}/logs/ejclaw.error.log
```
@@ -694,19 +694,19 @@ When NanoClaw starts, it:
```bash
# Install service
-cp launchd/com.nanoclaw.plist ~/Library/LaunchAgents/
+cp launchd/com.ejclaw.plist ~/Library/LaunchAgents/
# Start service
-launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
+launchctl load ~/Library/LaunchAgents/com.ejclaw.plist
# Stop service
-launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
+launchctl unload ~/Library/LaunchAgents/com.ejclaw.plist
# Check status
launchctl list | grep nanoclaw
# View logs
-tail -f logs/nanoclaw.log
+tail -f logs/ejclaw.log
```
---
@@ -763,7 +763,7 @@ chmod 700 groups/
| Issue | Cause | Solution |
|-------|-------|----------|
| No response to messages | Service not running | Check `launchctl list | grep nanoclaw` |
-| "Claude Code process exited with code 1" | Container runtime failed to start | Check logs; NanoClaw auto-starts container runtime but may fail |
+| "Claude Code process exited with code 1" | Container runtime failed to start | Check logs; EJClaw auto-starts container runtime but may fail |
| "Claude Code process exited with code 1" | Session mount path wrong | Ensure mount is to `/home/node/.claude/` not `/root/.claude/` |
| Session not continuing | Session ID not saved | Check SQLite: `sqlite3 store/messages.db "SELECT * FROM sessions"` |
| Session not continuing | Mount path mismatch | Container user is `node` with HOME=/home/node; sessions must be at `/home/node/.claude/` |
@@ -772,8 +772,8 @@ chmod 700 groups/
### Log Location
-- `logs/nanoclaw.log` - stdout
-- `logs/nanoclaw.error.log` - stderr
+- `logs/ejclaw.log` - stdout
+- `logs/ejclaw.error.log` - stderr
### Debug Mode
diff --git a/docs/nanoclaw-architecture-final.md b/docs/nanoclaw-architecture-final.md
index 103b38b..71feb4c 100644
--- a/docs/nanoclaw-architecture-final.md
+++ b/docs/nanoclaw-architecture-final.md
@@ -1,4 +1,4 @@
-# NanoClaw Skills Architecture
+# EJClaw Skills Architecture
## Core Principle
@@ -123,7 +123,7 @@ skills/
- Git's three-way merge uses context matching, so it works even if the user has moved code around — unlike line-number-based diffs that break immediately
- Auditable: `diff .nanoclaw/base/src/server.ts skills/add-whatsapp/modify/src/server.ts` shows exactly what the skill changes
- Deterministic: same three inputs always produce the same merge result
-- Size is negligible since NanoClaw's core files are small
+- Size is negligible since EJClaw's core files are small
### Intent Files
@@ -382,11 +382,11 @@ If tests fail and Level 2 can't resolve, restore from `.nanoclaw/backup/` and re
### The Problem
-`git rerere` is local by default. But NanoClaw has thousands of users applying the same skill combinations. Every user hitting the same conflict and waiting for Claude Code to resolve it is wasteful.
+`git rerere` is local by default. But EJClaw has thousands of users applying the same skill combinations. Every user hitting the same conflict and waiting for Claude Code to resolve it is wasteful.
### The Solution
-NanoClaw maintains a verified resolution cache in `.nanoclaw/resolutions/` that ships with the project. This is the shared artifact — **not** `.git/rr-cache/`, which stays local.
+EJClaw maintains a verified resolution cache in `.nanoclaw/resolutions/` that ships with the project. This is the shared artifact — **not** `.git/rr-cache/`, which stays local.
```
.nanoclaw/
@@ -588,7 +588,7 @@ There is no unrecoverable state.
## 10. Core Updates
-Core updates must be as programmatic as possible. The NanoClaw team is responsible for ensuring updates apply cleanly to common skill combinations.
+Core updates must be as programmatic as possible. The EJClaw team is responsible for ensuring updates apply cleanly to common skill combinations.
### Patches and Migrations
@@ -977,7 +977,7 @@ Each passing combination generates a verified resolution entry for the shared ca
### `.gitattributes`
-Ship with NanoClaw to reduce noisy merge conflicts:
+Ship with EJClaw to reduce noisy merge conflicts:
```
* text=auto
diff --git a/docs/nanorepo-architecture.md b/docs/nanorepo-architecture.md
index 1365e9e..e6f1bec 100644
--- a/docs/nanorepo-architecture.md
+++ b/docs/nanorepo-architecture.md
@@ -1,8 +1,8 @@
-# NanoClaw Skills Architecture
+# EJClaw Skills Architecture
## What Skills Are For
-NanoClaw's core is intentionally minimal. Skills are how users extend it: adding channels, integrations, cross-platform support, or replacing internals entirely. Examples: add Telegram alongside WhatsApp, switch from Apple Container to Docker, add Gmail integration, add voice message transcription. Each skill modifies the actual codebase, adding channel handlers, updating the message router, changing container configuration, and adding dependencies, rather than working through a plugin API or runtime hooks.
+EJClaw's core is intentionally minimal. Skills are how users extend it: adding channels, integrations, cross-platform support, or replacing internals entirely. Examples: add Telegram alongside WhatsApp, switch from Apple Container to Docker, add Gmail integration, add voice message transcription. Each skill modifies the actual codebase, adding channel handlers, updating the message router, changing container configuration, and adding dependencies, rather than working through a plugin API or runtime hooks.
## Why This Architecture
diff --git a/groups/global/CLAUDE.md b/groups/global/CLAUDE.md
index 28e97a7..5882f16 100644
--- a/groups/global/CLAUDE.md
+++ b/groups/global/CLAUDE.md
@@ -1,58 +1,7 @@
-# Andy
+# Global Memory
-You are Andy, a personal assistant. You help with tasks, answer questions, and can schedule reminders.
+This file is for mutable memory shared across Claude groups.
-## What You Can Do
+Use it for durable facts, preferences, and shared context that may change over time.
-- Answer questions and have conversations
-- Search the web and fetch content from URLs
-- **Browse the web** with `agent-browser` — open pages, click, fill forms, take screenshots, extract data (run `agent-browser open ` to start, then `agent-browser snapshot -i` to see interactive elements)
-- Read and write files in your workspace
-- Run bash commands in your sandbox
-- Schedule tasks to run later or on a recurring basis
-- Send messages back to the chat
-
-## Communication
-
-Your output is sent to the user or group.
-
-You also have `mcp__nanoclaw__send_message` which sends a message immediately while you're still working. This is useful when you want to acknowledge a request before starting longer work.
-
-### Internal thoughts
-
-If part of your output is internal reasoning rather than something for the user, wrap it in `` tags:
-
-```
-Compiled all three reports, ready to summarize.
-
-Here are the key findings from the research...
-```
-
-Text inside `` tags is logged but not sent to the user. If you've already sent the key information via `send_message`, you can wrap the recap in `` to avoid sending it again.
-
-### Sub-agents and teammates
-
-When working as a sub-agent or teammate, only use `send_message` if instructed to by the main agent.
-
-## Your Workspace
-
-Files you create are saved in `/workspace/group/`. Use this for notes, research, or anything that should persist.
-
-## Memory
-
-The `conversations/` folder contains searchable history of past conversations. Use this to recall context from previous sessions.
-
-When you learn something important:
-- Create files for structured data (e.g., `customers.md`, `preferences.md`)
-- Split files larger than 500 lines into folders
-- Keep an index in your memory for the files you create
-
-## Message Formatting
-
-NEVER use markdown. Only use WhatsApp/Telegram formatting:
-- *single asterisks* for bold (NEVER **double asterisks**)
-- _underscores_ for italic
-- • bullet points
-- ```triple backticks``` for code
-
-No ## headings. No [links](url). No **double stars**.
+Do not store platform-wide operating rules here. Those now live in `prompts/claude-platform.md`.
diff --git a/groups/main/CLAUDE.md b/groups/main/CLAUDE.md
index 11e846b..aea778e 100644
--- a/groups/main/CLAUDE.md
+++ b/groups/main/CLAUDE.md
@@ -1,246 +1,5 @@
-# Andy
+# Main Group Memory
-You are Andy, a personal assistant. You help with tasks, answer questions, and can schedule reminders.
+This file is for mutable memory and admin context specific to the main group.
-## What You Can Do
-
-- Answer questions and have conversations
-- Search the web and fetch content from URLs
-- **Browse the web** with `agent-browser` — open pages, click, fill forms, take screenshots, extract data (run `agent-browser open ` to start, then `agent-browser snapshot -i` to see interactive elements)
-- Read and write files in your workspace
-- Run bash commands in your sandbox
-- Schedule tasks to run later or on a recurring basis
-- Send messages back to the chat
-
-## Communication
-
-Your output is sent to the user or group.
-
-You also have `mcp__nanoclaw__send_message` which sends a message immediately while you're still working. This is useful when you want to acknowledge a request before starting longer work.
-
-### Internal thoughts
-
-If part of your output is internal reasoning rather than something for the user, wrap it in `` tags:
-
-```
-Compiled all three reports, ready to summarize.
-
-Here are the key findings from the research...
-```
-
-Text inside `` tags is logged but not sent to the user. If you've already sent the key information via `send_message`, you can wrap the recap in `` to avoid sending it again.
-
-### Sub-agents and teammates
-
-When working as a sub-agent or teammate, only use `send_message` if instructed to by the main agent.
-
-## Memory
-
-The `conversations/` folder contains searchable history of past conversations. Use this to recall context from previous sessions.
-
-When you learn something important:
-- Create files for structured data (e.g., `customers.md`, `preferences.md`)
-- Split files larger than 500 lines into folders
-- Keep an index in your memory for the files you create
-
-## WhatsApp Formatting (and other messaging apps)
-
-Do NOT use markdown headings (##) in WhatsApp messages. Only use:
-- *Bold* (single asterisks) (NEVER **double asterisks**)
-- _Italic_ (underscores)
-- • Bullets (bullet points)
-- ```Code blocks``` (triple backticks)
-
-Keep messages clean and readable for WhatsApp.
-
----
-
-## Admin Context
-
-This is the **main channel**, which has elevated privileges.
-
-## Container Mounts
-
-Main has read-only access to the project and read-write access to its group folder:
-
-| Container Path | Host Path | Access |
-|----------------|-----------|--------|
-| `/workspace/project` | Project root | read-only |
-| `/workspace/group` | `groups/main/` | read-write |
-
-Key paths inside the container:
-- `/workspace/project/store/messages.db` - SQLite database
-- `/workspace/project/store/messages.db` (registered_groups table) - Group config
-- `/workspace/project/groups/` - All group folders
-
----
-
-## Managing Groups
-
-### Finding Available Groups
-
-Available groups are provided in `/workspace/ipc/available_groups.json`:
-
-```json
-{
- "groups": [
- {
- "jid": "120363336345536173@g.us",
- "name": "Family Chat",
- "lastActivity": "2026-01-31T12:00:00.000Z",
- "isRegistered": false
- }
- ],
- "lastSync": "2026-01-31T12:00:00.000Z"
-}
-```
-
-Groups are ordered by most recent activity. The list is synced from WhatsApp daily.
-
-If a group the user mentions isn't in the list, request a fresh sync:
-
-```bash
-echo '{"type": "refresh_groups"}' > /workspace/ipc/tasks/refresh_$(date +%s).json
-```
-
-Then wait a moment and re-read `available_groups.json`.
-
-**Fallback**: Query the SQLite database directly:
-
-```bash
-sqlite3 /workspace/project/store/messages.db "
- SELECT jid, name, last_message_time
- FROM chats
- WHERE jid LIKE '%@g.us' AND jid != '__group_sync__'
- ORDER BY last_message_time DESC
- LIMIT 10;
-"
-```
-
-### Registered Groups Config
-
-Groups are registered in the SQLite `registered_groups` table:
-
-```json
-{
- "1234567890-1234567890@g.us": {
- "name": "Family Chat",
- "folder": "whatsapp_family-chat",
- "trigger": "@Andy",
- "added_at": "2024-01-31T12:00:00.000Z"
- }
-}
-```
-
-Fields:
-- **Key**: The chat JID (unique identifier — WhatsApp, Telegram, Slack, Discord, etc.)
-- **name**: Display name for the group
-- **folder**: Channel-prefixed folder name under `groups/` for this group's files and memory
-- **trigger**: The trigger word (usually same as global, but could differ)
-- **requiresTrigger**: Whether `@trigger` prefix is needed (default: `true`). Set to `false` for solo/personal chats where all messages should be processed
-- **isMain**: Whether this is the main control group (elevated privileges, no trigger required)
-- **added_at**: ISO timestamp when registered
-
-### Trigger Behavior
-
-- **Main group** (`isMain: true`): No trigger needed — all messages are processed automatically
-- **Groups with `requiresTrigger: false`**: No trigger needed — all messages processed (use for 1-on-1 or solo chats)
-- **Other groups** (default): Messages must start with `@AssistantName` to be processed
-
-### Adding a Group
-
-1. Query the database to find the group's JID
-2. Use the `register_group` MCP tool with the JID, name, folder, and trigger
-3. Optionally include `containerConfig` for additional mounts
-4. The group folder is created automatically: `/workspace/project/groups/{folder-name}/`
-5. Optionally create an initial `CLAUDE.md` for the group
-
-Folder naming convention — channel prefix with underscore separator:
-- WhatsApp "Family Chat" → `whatsapp_family-chat`
-- Telegram "Dev Team" → `telegram_dev-team`
-- Discord "General" → `discord_general`
-- Slack "Engineering" → `slack_engineering`
-- Use lowercase, hyphens for the group name part
-
-#### Adding Additional Directories for a Group
-
-Groups can have extra directories mounted. Add `containerConfig` to their entry:
-
-```json
-{
- "1234567890@g.us": {
- "name": "Dev Team",
- "folder": "dev-team",
- "trigger": "@Andy",
- "added_at": "2026-01-31T12:00:00Z",
- "containerConfig": {
- "additionalMounts": [
- {
- "hostPath": "~/projects/webapp",
- "containerPath": "webapp",
- "readonly": false
- }
- ]
- }
- }
-}
-```
-
-The directory will appear at `/workspace/extra/webapp` in that group's container.
-
-#### Sender Allowlist
-
-After registering a group, explain the sender allowlist feature to the user:
-
-> This group can be configured with a sender allowlist to control who can interact with me. There are two modes:
->
-> - **Trigger mode** (default): Everyone's messages are stored for context, but only allowed senders can trigger me with @{AssistantName}.
-> - **Drop mode**: Messages from non-allowed senders are not stored at all.
->
-> For closed groups with trusted members, I recommend setting up an allow-only list so only specific people can trigger me. Want me to configure that?
-
-If the user wants to set up an allowlist, edit `~/.config/nanoclaw/sender-allowlist.json` on the host:
-
-```json
-{
- "default": { "allow": "*", "mode": "trigger" },
- "chats": {
- "": {
- "allow": ["sender-id-1", "sender-id-2"],
- "mode": "trigger"
- }
- },
- "logDenied": true
-}
-```
-
-Notes:
-- Your own messages (`is_from_me`) explicitly bypass the allowlist in trigger checks. Bot messages are filtered out by the database query before trigger evaluation, so they never reach the allowlist.
-- If the config file doesn't exist or is invalid, all senders are allowed (fail-open)
-- The config file is on the host at `~/.config/nanoclaw/sender-allowlist.json`, not inside the container
-
-### Removing a Group
-
-1. Read `/workspace/project/data/registered_groups.json`
-2. Remove the entry for that group
-3. Write the updated JSON back
-4. The group folder and its files remain (don't delete them)
-
-### Listing Groups
-
-Read `/workspace/project/data/registered_groups.json` and format it nicely.
-
----
-
-## Global Memory
-
-You can read and write to `/workspace/project/groups/global/CLAUDE.md` for facts that should apply to all groups. Only update global memory when explicitly asked to "remember this globally" or similar.
-
----
-
-## Scheduling for Other Groups
-
-When scheduling tasks for other groups, use the `target_group_jid` parameter with the group's JID from `registered_groups.json`:
-- `schedule_task(prompt: "...", schedule_type: "cron", schedule_value: "0 9 * * 1", target_group_jid: "120363336345536173@g.us")`
-
-The task will run in that group's context with access to their files and memory.
+Keep platform-wide operating rules in `prompts/claude-platform.md`.
diff --git a/launchd/com.nanoclaw-codex.plist b/launchd/com.ejclaw-codex.plist
similarity index 87%
rename from launchd/com.nanoclaw-codex.plist
rename to launchd/com.ejclaw-codex.plist
index 8abc9a7..855828e 100644
--- a/launchd/com.nanoclaw-codex.plist
+++ b/launchd/com.ejclaw-codex.plist
@@ -3,7 +3,7 @@
Label
- com.nanoclaw-codex
+ com.ejclaw-codex
ProgramArguments
{{NODE_PATH}}
@@ -31,8 +31,8 @@
{{PROJECT_ROOT}}/data-codex
StandardOutPath
- {{PROJECT_ROOT}}/logs/nanoclaw-codex.log
+ {{PROJECT_ROOT}}/logs/ejclaw-codex.log
StandardErrorPath
- {{PROJECT_ROOT}}/logs/nanoclaw-codex.error.log
+ {{PROJECT_ROOT}}/logs/ejclaw-codex.error.log
diff --git a/launchd/com.nanoclaw.plist b/launchd/com.ejclaw.plist
similarity index 84%
rename from launchd/com.nanoclaw.plist
rename to launchd/com.ejclaw.plist
index 82bef0a..9625b6c 100644
--- a/launchd/com.nanoclaw.plist
+++ b/launchd/com.ejclaw.plist
@@ -3,7 +3,7 @@
Label
- com.nanoclaw
+ com.ejclaw
ProgramArguments
{{NODE_PATH}}
@@ -25,8 +25,8 @@
Andy
StandardOutPath
- {{PROJECT_ROOT}}/logs/nanoclaw.log
+ {{PROJECT_ROOT}}/logs/ejclaw.log
StandardErrorPath
- {{PROJECT_ROOT}}/logs/nanoclaw.error.log
+ {{PROJECT_ROOT}}/logs/ejclaw.error.log
diff --git a/package-lock.json b/package-lock.json
index 603ad85..9bcbe11 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "nanoclaw",
+ "name": "ejclaw",
"version": "1.2.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "nanoclaw",
+ "name": "ejclaw",
"version": "1.2.12",
"dependencies": {
"better-sqlite3": "^11.8.1",
diff --git a/package.json b/package.json
index 8831677..0f46c24 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "nanoclaw",
+ "name": "ejclaw",
"version": "1.2.12",
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
"type": "module",
diff --git a/prompts/claude-platform.md b/prompts/claude-platform.md
index 9ce497f..8a3f180 100644
--- a/prompts/claude-platform.md
+++ b/prompts/claude-platform.md
@@ -6,7 +6,7 @@ You are Andy, a personal assistant.
Your output is sent directly to the user or Discord group.
-You also have `mcp__nanoclaw__send_message`, which sends a message immediately while you are still working. Use it when you want to acknowledge a request before starting longer work.
+You also have a `send_message` tool, which sends a message immediately while you are still working. Use it when you want to acknowledge a request before starting longer work.
### Internal thoughts
diff --git a/prompts/codex-platform.md b/prompts/codex-platform.md
index 1cb9c86..0ecc1ed 100644
--- a/prompts/codex-platform.md
+++ b/prompts/codex-platform.md
@@ -15,7 +15,7 @@ Your output is sent directly to the Discord group.
- Keep answers concise unless more detail is genuinely needed
- Give conclusions and concrete next steps, not hidden reasoning
- Use code blocks for commands or code when helpful
-- Do not claim you will keep watching, monitor later, report back later, or continue tracking unless you actually scheduled a NanoClaw task with `schedule_task`
+- Do not claim you will keep watching, monitor later, report back later, or continue tracking unless you actually scheduled an EJClaw task with `schedule_task`
- If no task was scheduled, do not imply that background tracking is active. If future follow-up is needed, tell the user to ping you again or explicitly ask for scheduling
- When you do schedule background follow-up, mention that it was scheduled. Include the task ID only when it is useful for later reference
diff --git a/runners/agent-runner/package-lock.json b/runners/agent-runner/package-lock.json
index 4232aa2..1fd7e61 100644
--- a/runners/agent-runner/package-lock.json
+++ b/runners/agent-runner/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "nanoclaw-agent-runner",
+ "name": "ejclaw-agent-runner",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "nanoclaw-agent-runner",
+ "name": "ejclaw-agent-runner",
"version": "1.0.0",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.76",
diff --git a/runners/agent-runner/package.json b/runners/agent-runner/package.json
index 42a994e..1154b6e 100644
--- a/runners/agent-runner/package.json
+++ b/runners/agent-runner/package.json
@@ -1,8 +1,8 @@
{
- "name": "nanoclaw-agent-runner",
+ "name": "ejclaw-agent-runner",
"version": "1.0.0",
"type": "module",
- "description": "Container-side agent runner for NanoClaw",
+ "description": "Container-side agent runner for EJClaw",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
diff --git a/runners/agent-runner/src/index.ts b/runners/agent-runner/src/index.ts
index b962a85..c9f99dd 100644
--- a/runners/agent-runner/src/index.ts
+++ b/runners/agent-runner/src/index.ts
@@ -1,5 +1,5 @@
/**
- * NanoClaw Agent Runner
+ * EJClaw Agent Runner
* Runs inside a container, receives config via stdin, outputs result to stdout
*
* Input protocol:
@@ -60,11 +60,19 @@ interface SDKUserMessage {
}
// Paths configurable via env vars (defaults to container paths for backwards compat)
-const GROUP_DIR = process.env.NANOCLAW_GROUP_DIR || '/workspace/group';
-const IPC_DIR = process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
-const EXTRA_BASE = process.env.NANOCLAW_EXTRA_DIR || '/workspace/extra';
+const GROUP_DIR =
+ process.env.EJCLAW_GROUP_DIR ||
+ process.env.NANOCLAW_GROUP_DIR ||
+ '/workspace/group';
+const IPC_DIR =
+ process.env.EJCLAW_IPC_DIR || process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
+const EXTRA_BASE =
+ process.env.EJCLAW_EXTRA_DIR ||
+ process.env.NANOCLAW_EXTRA_DIR ||
+ '/workspace/extra';
// Optional: override cwd (agent works in this directory instead of GROUP_DIR)
-const WORK_DIR = process.env.NANOCLAW_WORK_DIR || '';
+const WORK_DIR =
+ process.env.EJCLAW_WORK_DIR || process.env.NANOCLAW_WORK_DIR || '';
const IPC_INPUT_DIR = path.join(IPC_DIR, 'input');
const MAX_TURNS = 100;
diff --git a/runners/agent-runner/src/ipc-mcp-stdio.ts b/runners/agent-runner/src/ipc-mcp-stdio.ts
index 3af73aa..c7438b0 100644
--- a/runners/agent-runner/src/ipc-mcp-stdio.ts
+++ b/runners/agent-runner/src/ipc-mcp-stdio.ts
@@ -1,5 +1,5 @@
/**
- * Stdio MCP Server for NanoClaw
+ * Stdio MCP Server for EJClaw
* Standalone process that agent teams subagents can inherit.
* Reads context from environment variables, writes IPC files for the host.
*/
@@ -11,14 +11,17 @@ import fs from 'fs';
import path from 'path';
import { CronExpressionParser } from 'cron-parser';
-const IPC_DIR = process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
+const IPC_DIR =
+ process.env.EJCLAW_IPC_DIR || process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
const MESSAGES_DIR = path.join(IPC_DIR, 'messages');
const TASKS_DIR = path.join(IPC_DIR, 'tasks');
// Context from environment variables (set by the agent runner)
-const chatJid = process.env.NANOCLAW_CHAT_JID!;
-const groupFolder = process.env.NANOCLAW_GROUP_FOLDER!;
-const isMain = process.env.NANOCLAW_IS_MAIN === '1';
+const chatJid = process.env.EJCLAW_CHAT_JID || process.env.NANOCLAW_CHAT_JID!;
+const groupFolder =
+ process.env.EJCLAW_GROUP_FOLDER || process.env.NANOCLAW_GROUP_FOLDER!;
+const isMain =
+ (process.env.EJCLAW_IS_MAIN || process.env.NANOCLAW_IS_MAIN) === '1';
function writeIpcFile(dir: string, data: object): string {
fs.mkdirSync(dir, { recursive: true });
diff --git a/runners/codex-runner/package-lock.json b/runners/codex-runner/package-lock.json
index f070aa1..956a0a0 100644
--- a/runners/codex-runner/package-lock.json
+++ b/runners/codex-runner/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "nanoclaw-codex-runner",
+ "name": "ejclaw-codex-runner",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "nanoclaw-codex-runner",
+ "name": "ejclaw-codex-runner",
"version": "1.0.0",
"dependencies": {
"@openai/codex-sdk": "^0.114.0"
diff --git a/runners/codex-runner/package.json b/runners/codex-runner/package.json
index a4c2d1d..e1aebd0 100644
--- a/runners/codex-runner/package.json
+++ b/runners/codex-runner/package.json
@@ -1,8 +1,8 @@
{
- "name": "nanoclaw-codex-runner",
+ "name": "ejclaw-codex-runner",
"version": "1.0.0",
"type": "module",
- "description": "Container-side Codex CLI runner for NanoClaw",
+ "description": "Container-side Codex CLI runner for EJClaw",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
diff --git a/runners/codex-runner/src/app-server-client.ts b/runners/codex-runner/src/app-server-client.ts
index 11693c9..f86edab 100644
--- a/runners/codex-runner/src/app-server-client.ts
+++ b/runners/codex-runner/src/app-server-client.ts
@@ -144,8 +144,8 @@ export class CodexAppServerClient {
await this.request('initialize', {
clientInfo: {
- name: 'nanoclaw_codex_runner',
- title: 'NanoClaw Codex Runner',
+ name: 'ejclaw_codex_runner',
+ title: 'EJClaw Codex Runner',
version: '1.0.0',
},
capabilities: {
@@ -182,7 +182,7 @@ export class CodexAppServerClient {
model: options.model,
approvalPolicy: 'never',
sandbox: 'danger-full-access',
- serviceName: 'nanoclaw',
+ serviceName: 'ejclaw',
};
const result = sessionId
@@ -362,7 +362,7 @@ export class CodexAppServerClient {
this.respondError(
message.id,
-32601,
- `NanoClaw does not handle server request ${message.method}`,
+ `EJClaw does not handle server request ${message.method}`,
);
}
diff --git a/runners/codex-runner/src/index.ts b/runners/codex-runner/src/index.ts
index d161e6b..bfb565d 100644
--- a/runners/codex-runner/src/index.ts
+++ b/runners/codex-runner/src/index.ts
@@ -1,12 +1,12 @@
/**
- * NanoClaw Codex Runner
+ * EJClaw Codex Runner
*
* Default runtime is Codex app-server, with SDK fallback available via
* CODEX_RUNTIME=sdk or automatic fallback when app-server startup fails.
*
* Input protocol:
* Stdin: Full ContainerInput JSON (read until EOF)
- * IPC: Follow-up messages as JSON files in $NANOCLAW_IPC_DIR/input/
+ * IPC: Follow-up messages as JSON files in $EJCLAW_IPC_DIR/input/
* Sentinel: _close — signals session end
*
* Stdout protocol:
@@ -45,9 +45,14 @@ interface ContainerOutput {
// ── Constants ──────────────────────────────────────────────────────
-const GROUP_DIR = process.env.NANOCLAW_GROUP_DIR || '/workspace/group';
-const IPC_DIR = process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
-const WORK_DIR = process.env.NANOCLAW_WORK_DIR || '';
+const GROUP_DIR =
+ process.env.EJCLAW_GROUP_DIR ||
+ process.env.NANOCLAW_GROUP_DIR ||
+ '/workspace/group';
+const IPC_DIR =
+ process.env.EJCLAW_IPC_DIR || process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
+const WORK_DIR =
+ process.env.EJCLAW_WORK_DIR || process.env.NANOCLAW_WORK_DIR || '';
const IPC_INPUT_DIR = path.join(IPC_DIR, 'input');
const IPC_INPUT_CLOSE_SENTINEL = path.join(IPC_INPUT_DIR, '_close');
const IPC_POLL_MS = 500;
diff --git a/setup/mounts.ts b/setup/mounts.ts
index eb2a5f6..7a028c1 100644
--- a/setup/mounts.ts
+++ b/setup/mounts.ts
@@ -26,7 +26,7 @@ function parseArgs(args: string[]): { empty: boolean; json: string } {
export async function run(args: string[]): Promise {
const { empty, json } = parseArgs(args);
const homeDir = os.homedir();
- const configDir = path.join(homeDir, '.config', 'nanoclaw');
+ const configDir = path.join(homeDir, '.config', 'ejclaw');
const configFile = path.join(configDir, 'mount-allowlist.json');
if (isRoot()) {
diff --git a/setup/platform.ts b/setup/platform.ts
index 5544eac..9f84ed9 100644
--- a/setup/platform.ts
+++ b/setup/platform.ts
@@ -1,5 +1,5 @@
/**
- * Cross-platform detection utilities for NanoClaw setup.
+ * Cross-platform detection utilities for EJClaw setup.
*/
import { execSync } from 'child_process';
import fs from 'fs';
diff --git a/setup/service.test.ts b/setup/service.test.ts
index eb15db8..3458be0 100644
--- a/setup/service.test.ts
+++ b/setup/service.test.ts
@@ -19,7 +19,7 @@ function generatePlist(
Label
- com.nanoclaw
+ com.ejclaw
ProgramArguments
${nodePath}
@@ -39,9 +39,9 @@ function generatePlist(
${homeDir}
StandardOutPath
- ${projectRoot}/logs/nanoclaw.log
+ ${projectRoot}/logs/ejclaw.log
StandardErrorPath
- ${projectRoot}/logs/nanoclaw.error.log
+ ${projectRoot}/logs/ejclaw.error.log
`;
}
@@ -53,7 +53,7 @@ function generateSystemdUnit(
isSystem: boolean,
): string {
return `[Unit]
-Description=NanoClaw Personal Assistant
+Description=EJClaw Personal Assistant
After=network.target
[Service]
@@ -64,8 +64,8 @@ Restart=always
RestartSec=5
Environment=HOME=${homeDir}
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
-StandardOutput=append:${projectRoot}/logs/nanoclaw.log
-StandardError=append:${projectRoot}/logs/nanoclaw.error.log
+StandardOutput=append:${projectRoot}/logs/ejclaw.log
+StandardError=append:${projectRoot}/logs/ejclaw.error.log
[Install]
WantedBy=${isSystem ? 'multi-user.target' : 'default.target'}`;
@@ -75,16 +75,16 @@ describe('plist generation', () => {
it('contains the correct label', () => {
const plist = generatePlist(
'/usr/local/bin/node',
- '/home/user/nanoclaw',
+ '/home/user/ejclaw',
'/home/user',
);
- expect(plist).toContain('com.nanoclaw');
+ expect(plist).toContain('com.ejclaw');
});
it('uses the correct node path', () => {
const plist = generatePlist(
'/opt/node/bin/node',
- '/home/user/nanoclaw',
+ '/home/user/ejclaw',
'/home/user',
);
expect(plist).toContain('/opt/node/bin/node');
@@ -93,20 +93,20 @@ describe('plist generation', () => {
it('points to dist/index.js', () => {
const plist = generatePlist(
'/usr/local/bin/node',
- '/home/user/nanoclaw',
+ '/home/user/ejclaw',
'/home/user',
);
- expect(plist).toContain('/home/user/nanoclaw/dist/index.js');
+ expect(plist).toContain('/home/user/ejclaw/dist/index.js');
});
it('sets log paths', () => {
const plist = generatePlist(
'/usr/local/bin/node',
- '/home/user/nanoclaw',
+ '/home/user/ejclaw',
'/home/user',
);
- expect(plist).toContain('nanoclaw.log');
- expect(plist).toContain('nanoclaw.error.log');
+ expect(plist).toContain('ejclaw.log');
+ expect(plist).toContain('ejclaw.error.log');
});
});
@@ -114,7 +114,7 @@ describe('systemd unit generation', () => {
it('user unit uses default.target', () => {
const unit = generateSystemdUnit(
'/usr/bin/node',
- '/home/user/nanoclaw',
+ '/home/user/ejclaw',
'/home/user',
false,
);
@@ -124,7 +124,7 @@ describe('systemd unit generation', () => {
it('system unit uses multi-user.target', () => {
const unit = generateSystemdUnit(
'/usr/bin/node',
- '/home/user/nanoclaw',
+ '/home/user/ejclaw',
'/home/user',
true,
);
@@ -134,7 +134,7 @@ describe('systemd unit generation', () => {
it('contains restart policy', () => {
const unit = generateSystemdUnit(
'/usr/bin/node',
- '/home/user/nanoclaw',
+ '/home/user/ejclaw',
'/home/user',
false,
);
@@ -145,32 +145,32 @@ describe('systemd unit generation', () => {
it('sets correct ExecStart', () => {
const unit = generateSystemdUnit(
'/usr/bin/node',
- '/srv/nanoclaw',
+ '/srv/ejclaw',
'/home/user',
false,
);
expect(unit).toContain(
- 'ExecStart=/usr/bin/node /srv/nanoclaw/dist/index.js',
+ 'ExecStart=/usr/bin/node /srv/ejclaw/dist/index.js',
);
});
});
describe('WSL nohup fallback', () => {
it('generates a valid wrapper script', () => {
- const projectRoot = '/home/user/nanoclaw';
+ const projectRoot = '/home/user/ejclaw';
const nodePath = '/usr/bin/node';
- const pidFile = path.join(projectRoot, 'nanoclaw.pid');
+ const pidFile = path.join(projectRoot, 'ejclaw.pid');
// Simulate what service.ts generates
const wrapper = `#!/bin/bash
set -euo pipefail
cd ${JSON.stringify(projectRoot)}
-nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot)}/dist/index.js >> ${JSON.stringify(projectRoot)}/logs/nanoclaw.log 2>> ${JSON.stringify(projectRoot)}/logs/nanoclaw.error.log &
+nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot)}/dist/index.js >> ${JSON.stringify(projectRoot)}/logs/ejclaw.log 2>> ${JSON.stringify(projectRoot)}/logs/ejclaw.error.log &
echo $! > ${JSON.stringify(pidFile)}`;
expect(wrapper).toContain('#!/bin/bash');
expect(wrapper).toContain('nohup');
expect(wrapper).toContain(nodePath);
- expect(wrapper).toContain('nanoclaw.pid');
+ expect(wrapper).toContain('ejclaw.pid');
});
});
diff --git a/setup/service.ts b/setup/service.ts
index ffdeb9f..48cd862 100644
--- a/setup/service.ts
+++ b/setup/service.ts
@@ -20,6 +20,9 @@ import {
} from './platform.js';
import { emitStatus } from './status.js';
+const EJCLAW_SERVICE_NAME = 'ejclaw';
+const EJCLAW_LAUNCHD_LABEL = 'com.ejclaw';
+
export async function run(_args: string[]): Promise {
const projectRoot = process.cwd();
const platform = getPlatform();
@@ -77,7 +80,7 @@ function setupLaunchd(
homeDir,
'Library',
'LaunchAgents',
- 'com.nanoclaw.plist',
+ `${EJCLAW_LAUNCHD_LABEL}.plist`,
);
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
@@ -86,7 +89,7 @@ function setupLaunchd(
Label
- com.nanoclaw
+ ${EJCLAW_LAUNCHD_LABEL}
ProgramArguments
${nodePath}
@@ -106,9 +109,9 @@ function setupLaunchd(
${homeDir}
StandardOutPath
- ${projectRoot}/logs/nanoclaw.log
+ ${projectRoot}/logs/ejclaw.log
StandardErrorPath
- ${projectRoot}/logs/nanoclaw.error.log
+ ${projectRoot}/logs/ejclaw.error.log
`;
@@ -128,7 +131,7 @@ function setupLaunchd(
let serviceLoaded = false;
try {
const output = execSync('launchctl list', { encoding: 'utf-8' });
- serviceLoaded = output.includes('com.nanoclaw');
+ serviceLoaded = output.includes(EJCLAW_LAUNCHD_LABEL);
} catch {
// launchctl list failed
}
@@ -160,7 +163,7 @@ function setupLinux(
}
/**
- * Kill any orphaned nanoclaw node processes left from previous runs or debugging.
+ * Kill any orphaned ejclaw node processes left from previous runs or debugging.
* Prevents connection conflicts when two instances connect to the same channel simultaneously.
*/
function killOrphanedProcesses(projectRoot: string): void {
@@ -168,7 +171,7 @@ function killOrphanedProcesses(projectRoot: string): void {
execSync(`pkill -f '${projectRoot}/dist/index\\.js' || true`, {
stdio: 'ignore',
});
- logger.info('Stopped any orphaned nanoclaw processes');
+ logger.info('Stopped any orphaned ejclaw processes');
} catch {
// pkill not available or no orphans
}
@@ -186,7 +189,7 @@ function setupSystemd(
let systemctlPrefix: string;
if (runningAsRoot) {
- unitPath = '/etc/systemd/system/nanoclaw.service';
+ unitPath = `/etc/systemd/system/${EJCLAW_SERVICE_NAME}.service`;
systemctlPrefix = 'systemctl';
logger.info('Running as root — installing system-level systemd unit');
} else {
@@ -202,12 +205,12 @@ function setupSystemd(
}
const unitDir = path.join(homeDir, '.config', 'systemd', 'user');
fs.mkdirSync(unitDir, { recursive: true });
- unitPath = path.join(unitDir, 'nanoclaw.service');
+ unitPath = path.join(unitDir, `${EJCLAW_SERVICE_NAME}.service`);
systemctlPrefix = 'systemctl --user';
}
const unit = `[Unit]
-Description=NanoClaw Personal Assistant
+Description=EJClaw Personal Assistant
After=network.target
[Service]
@@ -218,8 +221,8 @@ Restart=always
RestartSec=5
Environment=HOME=${homeDir}
Environment=PATH=${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin:${homeDir}/.npm-global/bin
-StandardOutput=append:${projectRoot}/logs/nanoclaw.log
-StandardError=append:${projectRoot}/logs/nanoclaw.error.log
+StandardOutput=append:${projectRoot}/logs/ejclaw.log
+StandardError=append:${projectRoot}/logs/ejclaw.error.log
[Install]
WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
@@ -227,7 +230,7 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
fs.writeFileSync(unitPath, unit);
logger.info({ unitPath }, 'Wrote systemd unit');
- // Kill orphaned nanoclaw processes to avoid channel connection conflicts
+ // Kill orphaned ejclaw processes to avoid channel connection conflicts
killOrphanedProcesses(projectRoot);
// Enable and start
@@ -238,13 +241,17 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
}
try {
- execSync(`${systemctlPrefix} enable nanoclaw`, { stdio: 'ignore' });
+ execSync(`${systemctlPrefix} enable ${EJCLAW_SERVICE_NAME}`, {
+ stdio: 'ignore',
+ });
} catch (err) {
logger.error({ err }, 'systemctl enable failed');
}
try {
- execSync(`${systemctlPrefix} start nanoclaw`, { stdio: 'ignore' });
+ execSync(`${systemctlPrefix} start ${EJCLAW_SERVICE_NAME}`, {
+ stdio: 'ignore',
+ });
} catch (err) {
logger.error({ err }, 'systemctl start failed');
}
@@ -252,7 +259,9 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
// Verify
let serviceLoaded = false;
try {
- execSync(`${systemctlPrefix} is-active nanoclaw`, { stdio: 'ignore' });
+ execSync(`${systemctlPrefix} is-active ${EJCLAW_SERVICE_NAME}`, {
+ stdio: 'ignore',
+ });
serviceLoaded = true;
} catch {
// Not active
@@ -276,12 +285,12 @@ function setupNohupFallback(
): void {
logger.warn('No systemd detected — generating nohup wrapper script');
- const wrapperPath = path.join(projectRoot, 'start-nanoclaw.sh');
- const pidFile = path.join(projectRoot, 'nanoclaw.pid');
+ const wrapperPath = path.join(projectRoot, 'start-ejclaw.sh');
+ const pidFile = path.join(projectRoot, 'ejclaw.pid');
const lines = [
'#!/bin/bash',
- '# start-nanoclaw.sh — Start NanoClaw without systemd',
+ '# start-ejclaw.sh — Start EJClaw without systemd',
`# To stop: kill \\$(cat ${pidFile})`,
'',
'set -euo pipefail',
@@ -292,20 +301,20 @@ function setupNohupFallback(
`if [ -f ${JSON.stringify(pidFile)} ]; then`,
` OLD_PID=$(cat ${JSON.stringify(pidFile)} 2>/dev/null || echo "")`,
' if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then',
- ' echo "Stopping existing NanoClaw (PID $OLD_PID)..."',
+ ' echo "Stopping existing EJClaw (PID $OLD_PID)..."',
' kill "$OLD_PID" 2>/dev/null || true',
' sleep 2',
' fi',
'fi',
'',
- 'echo "Starting NanoClaw..."',
+ 'echo "Starting EJClaw..."',
`nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot + '/dist/index.js')} \\`,
- ` >> ${JSON.stringify(projectRoot + '/logs/nanoclaw.log')} \\`,
- ` 2>> ${JSON.stringify(projectRoot + '/logs/nanoclaw.error.log')} &`,
+ ` >> ${JSON.stringify(projectRoot + '/logs/ejclaw.log')} \\`,
+ ` 2>> ${JSON.stringify(projectRoot + '/logs/ejclaw.error.log')} &`,
'',
`echo $! > ${JSON.stringify(pidFile)}`,
- 'echo "NanoClaw started (PID $!)"',
- `echo "Logs: tail -f ${projectRoot}/logs/nanoclaw.log"`,
+ 'echo "EJClaw started (PID $!)"',
+ `echo "Logs: tail -f ${projectRoot}/logs/ejclaw.log"`,
];
const wrapper = lines.join('\n') + '\n';
diff --git a/setup/verify.ts b/setup/verify.ts
index 5b1eba6..8c7f799 100644
--- a/setup/verify.ts
+++ b/setup/verify.ts
@@ -26,6 +26,9 @@ export async function run(_args: string[]): Promise {
const projectRoot = process.cwd();
const platform = getPlatform();
const homeDir = os.homedir();
+ const launchdLabels = ['com.ejclaw', 'com.nanoclaw'];
+ const systemdUnits = ['ejclaw', 'nanoclaw'];
+ const pidFiles = ['ejclaw.pid', 'nanoclaw.pid'];
logger.info('Starting verification');
@@ -36,9 +39,10 @@ export async function run(_args: string[]): Promise {
if (mgr === 'launchd') {
try {
const output = execSync('launchctl list', { encoding: 'utf-8' });
- if (output.includes('com.nanoclaw')) {
+ const matchedLabel = launchdLabels.find((label) => output.includes(label));
+ if (matchedLabel) {
// Check if it has a PID (actually running)
- const line = output.split('\n').find((l) => l.includes('com.nanoclaw'));
+ const line = output.split('\n').find((l) => l.includes(matchedLabel));
if (line) {
const pidField = line.trim().split(/\s+/)[0];
service = pidField !== '-' && pidField ? 'running' : 'stopped';
@@ -49,15 +53,23 @@ export async function run(_args: string[]): Promise {
}
} else if (mgr === 'systemd') {
const prefix = isRoot() ? 'systemctl' : 'systemctl --user';
- try {
- execSync(`${prefix} is-active nanoclaw`, { stdio: 'ignore' });
+ const activeUnit = systemdUnits.find((unit) => {
+ try {
+ execSync(`${prefix} is-active ${unit}`, { stdio: 'ignore' });
+ return true;
+ } catch {
+ return false;
+ }
+ });
+
+ if (activeUnit) {
service = 'running';
- } catch {
+ } else {
try {
const output = execSync(`${prefix} list-unit-files`, {
encoding: 'utf-8',
});
- if (output.includes('nanoclaw')) {
+ if (systemdUnits.some((unit) => output.includes(unit))) {
service = 'stopped';
}
} catch {
@@ -66,8 +78,10 @@ export async function run(_args: string[]): Promise {
}
} else {
// Check for nohup PID file
- const pidFile = path.join(projectRoot, 'nanoclaw.pid');
- if (fs.existsSync(pidFile)) {
+ const pidFile = pidFiles
+ .map((name) => path.join(projectRoot, name))
+ .find((candidate) => fs.existsSync(candidate));
+ if (pidFile) {
try {
const raw = fs.readFileSync(pidFile, 'utf-8').trim();
const pid = Number(raw);
@@ -144,6 +158,7 @@ export async function run(_args: string[]): Promise {
// 5. Check mount allowlist
let mountAllowlist = 'missing';
if (
+ fs.existsSync(path.join(homeDir, '.config', 'ejclaw', 'mount-allowlist.json')) ||
fs.existsSync(
path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'),
)
diff --git a/src/agent-runner.test.ts b/src/agent-runner.test.ts
index dd14196..622d6b2 100644
--- a/src/agent-runner.test.ts
+++ b/src/agent-runner.test.ts
@@ -11,8 +11,8 @@ const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
vi.mock('./config.js', () => ({
AGENT_MAX_OUTPUT_SIZE: 10485760,
AGENT_TIMEOUT: 1800000, // 30min
- DATA_DIR: '/tmp/nanoclaw-test-data',
- GROUPS_DIR: '/tmp/nanoclaw-test-groups',
+ DATA_DIR: '/tmp/ejclaw-test-data',
+ GROUPS_DIR: '/tmp/ejclaw-test-groups',
IDLE_TIMEOUT: 1800000, // 30min
SERVICE_ID: 'claude',
SERVICE_AGENT_TYPE: 'claude-code',
@@ -223,16 +223,18 @@ describe('agent-runner timeout behavior', () => {
path.endsWith('/global/CLAUDE.md')
);
});
- vi.mocked(fs.readFileSync).mockImplementation((p: fs.PathOrFileDescriptor) => {
- const path = String(p);
- if (path.endsWith('/prompts/claude-platform.md')) {
- return 'Platform Claude Rules';
- }
- if (path.endsWith('/global/CLAUDE.md')) {
- return 'Global Claude Memory';
- }
- return '';
- });
+ vi.mocked(fs.readFileSync).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ const path = String(p);
+ if (path.endsWith('/prompts/claude-platform.md')) {
+ return 'Platform Claude Rules';
+ }
+ if (path.endsWith('/global/CLAUDE.md')) {
+ return 'Global Claude Memory';
+ }
+ return '';
+ },
+ );
const resultPromise = runAgentProcess(
testGroup,
@@ -246,7 +248,7 @@ describe('agent-runner timeout behavior', () => {
await resultPromise;
expect(fs.writeFileSync).toHaveBeenCalledWith(
- '/tmp/nanoclaw-test-data/sessions/test-group/.claude/CLAUDE.md',
+ '/tmp/ejclaw-test-data/sessions/test-group/.claude/CLAUDE.md',
'Platform Claude Rules\n\n---\n\nGlobal Claude Memory\n',
);
});
diff --git a/src/agent-runner.ts b/src/agent-runner.ts
index 163f78e..bb53272 100644
--- a/src/agent-runner.ts
+++ b/src/agent-runner.ts
@@ -1,5 +1,5 @@
/**
- * Agent Process Runner for NanoClaw
+ * Agent Process Runner for EJClaw
* Spawns agent execution as direct host processes and handles IPC
*/
import { ChildProcess, spawn } from 'child_process';
@@ -200,15 +200,27 @@ function prepareGroupEnvironment(
TZ: TIMEZONE,
HOME: os.homedir(),
// Path configuration for the runner
+ EJCLAW_GROUP_DIR: groupDir,
NANOCLAW_GROUP_DIR: groupDir,
+ EJCLAW_IPC_DIR: groupIpcDir,
NANOCLAW_IPC_DIR: groupIpcDir,
+ EJCLAW_GLOBAL_DIR: globalDir,
NANOCLAW_GLOBAL_DIR: globalDir,
+ EJCLAW_EXTRA_DIR: extraDirs.length > 0 ? extraDirs[0] : '',
NANOCLAW_EXTRA_DIR: extraDirs.length > 0 ? extraDirs[0] : '',
// Working directory override (agent uses this as cwd instead of group dir)
- ...(group.workDir ? { NANOCLAW_WORK_DIR: group.workDir } : {}),
+ ...(group.workDir
+ ? {
+ EJCLAW_WORK_DIR: group.workDir,
+ NANOCLAW_WORK_DIR: group.workDir,
+ }
+ : {}),
// MCP server context
+ EJCLAW_CHAT_JID: group.folder,
NANOCLAW_CHAT_JID: group.folder,
+ EJCLAW_GROUP_FOLDER: group.folder,
NANOCLAW_GROUP_FOLDER: group.folder,
+ EJCLAW_IS_MAIN: isMain ? '1' : '0',
NANOCLAW_IS_MAIN: isMain ? '1' : '0',
// Claude sessions directory — set CLAUDE_CONFIG_DIR so SDK uses per-group sessions
CLAUDE_CONFIG_DIR: groupSessionsDir,
@@ -373,12 +385,13 @@ export async function runAgentProcess(
input.chatJid,
);
if (input.runId) {
+ env.EJCLAW_RUN_ID = input.runId;
env.NANOCLAW_RUN_ID = input.runId;
}
const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-');
const processSuffix = input.runId || `${Date.now()}`;
- const processName = `nanoclaw-${safeName}-${processSuffix}`;
+ const processName = `ejclaw-${safeName}-${processSuffix}`;
// Check if runner is built
const distEntry = path.join(runnerDir, 'dist', 'index.js');
diff --git a/src/channels/discord.test.ts b/src/channels/discord.test.ts
index 06334ea..ce14894 100644
--- a/src/channels/discord.test.ts
+++ b/src/channels/discord.test.ts
@@ -12,8 +12,8 @@ vi.mock('../env.js', () => ({ readEnvFile: vi.fn(() => ({})) }));
vi.mock('../config.js', () => ({
ASSISTANT_NAME: 'Andy',
TRIGGER_PATTERN: /^@Andy\b/i,
- DATA_DIR: '/tmp/nanoclaw-test-data',
- CACHE_DIR: '/tmp/nanoclaw-test-cache',
+ DATA_DIR: '/tmp/ejclaw-test-data',
+ CACHE_DIR: '/tmp/ejclaw-test-cache',
}));
// Mock logger
diff --git a/src/claude-usage.test.ts b/src/claude-usage.test.ts
new file mode 100644
index 0000000..e868e2b
--- /dev/null
+++ b/src/claude-usage.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from 'vitest';
+
+import { parseClaudeUsagePanel } from './claude-usage.js';
+
+describe('parseClaudeUsagePanel', () => {
+ it('parses session and weekly usage from Claude CLI panel output', () => {
+ const sample = `
+Settings: Status Config Usage
+Loading usage data...
+
+Current session
+████ 4% used
+Resets in 8m (Asia/Seoul)
+
+Current week (all models)
+███████████████████████████████████████ 78% used
+Resets Mar 17 at 10pm (Asia/Seoul)
+
+Current week (Sonnet only)
+███ 6% used
+Resets Mar 17 at 11pm (Asia/Seoul)
+
+Extra usage
+Extra usage not enabled • /extra-usage to enable
+`;
+
+ expect(parseClaudeUsagePanel(sample)).toEqual({
+ five_hour: {
+ utilization: 4,
+ resets_at: 'Resets in 8m (Asia/Seoul)',
+ },
+ seven_day: {
+ utilization: 78,
+ resets_at: 'Resets Mar 17 at 10pm (Asia/Seoul)',
+ },
+ });
+ });
+
+ it('converts percent left into used percent', () => {
+ const sample = `
+Current session
+60% left
+Resets in 1h
+`;
+
+ expect(parseClaudeUsagePanel(sample)).toEqual({
+ five_hour: {
+ utilization: 40,
+ resets_at: 'Resets in 1h',
+ },
+ });
+ });
+});
diff --git a/src/claude-usage.ts b/src/claude-usage.ts
new file mode 100644
index 0000000..3ed83c1
--- /dev/null
+++ b/src/claude-usage.ts
@@ -0,0 +1,183 @@
+import { spawn } from 'child_process';
+
+import { logger } from './logger.js';
+
+export interface ClaudeUsageData {
+ five_hour?: { utilization: number; resets_at: string };
+ seven_day?: { utilization: number; resets_at: string };
+}
+
+const CLAUDE_EXPECT_TIMEOUT_MS = 25000;
+const ANSI_RE = /\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
+
+const EXPECT_PROGRAM = `
+set timeout 20
+log_user 1
+match_max 200000
+set binary $env(CLAUDE_BINARY)
+spawn -noecho -- $binary --setting-sources user --allowed-tools ""
+expect {
+ -re "Do you trust the files in this folder\\\\?" { send "y\\r"; exp_continue }
+ -re "Quick safety check:" { send "\\r"; exp_continue }
+ -re "Yes, I trust this folder" { send "\\r"; exp_continue }
+ -re "Ready to code here\\\\?" { send "\\r"; exp_continue }
+ -re "Press Enter to continue" { send "\\r"; exp_continue }
+ timeout {}
+}
+send "/usage\\r"
+set deadline [expr {[clock seconds] + 20}]
+while {[clock seconds] < $deadline} {
+ expect {
+ -re "Do you trust the files in this folder\\\\?" { send "y\\r"; exp_continue }
+ -re "Quick safety check:" { send "\\r"; exp_continue }
+ -re "Yes, I trust this folder" { send "\\r"; exp_continue }
+ -re "Ready to code here\\\\?" { send "\\r"; exp_continue }
+ -re "Press Enter to continue" { send "\\r"; exp_continue }
+ -re "Current session" { after 2000; exit 0 }
+ -re "Failed to load usage data" { after 500; exit 2 }
+ eof { exit 3 }
+ timeout { send "\\r" }
+ }
+}
+exit 4
+`;
+
+function normalizeLines(rawText: string): string[] {
+ return rawText
+ .replace(ANSI_RE, '')
+ .replace(/\r/g, '\n')
+ .split('\n')
+ .map((line) => line.replace(/\s+/g, ' ').trim())
+ .filter(Boolean);
+}
+
+function parsePercent(windowText: string): number | null {
+ const match = windowText.match(/(\d{1,3})%\s*(used|left)\b/i);
+ if (!match) return null;
+ const value = parseInt(match[1], 10);
+ if (Number.isNaN(value)) return null;
+ return match[2].toLowerCase() === 'left' ? 100 - value : value;
+}
+
+function parseWindow(
+ lines: string[],
+ labels: string[],
+): { utilization: number; resets_at: string } | null {
+ const normalizedLabels = labels.map((label) => label.toLowerCase());
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].toLowerCase();
+ if (!normalizedLabels.some((label) => line.includes(label))) continue;
+
+ const windowLines = lines.slice(i, i + 6);
+ const windowText = windowLines.join('\n');
+ const utilization = parsePercent(windowText);
+ if (utilization === null) continue;
+
+ const resetLine = windowLines.find((candidate) =>
+ candidate.toLowerCase().startsWith('resets'),
+ );
+
+ return {
+ utilization,
+ resets_at: resetLine || '',
+ };
+ }
+
+ return null;
+}
+
+export function parseClaudeUsagePanel(rawText: string): ClaudeUsageData | null {
+ const lines = normalizeLines(rawText);
+ if (lines.length === 0) return null;
+ if (
+ lines.some((line) =>
+ line.toLowerCase().includes('failed to load usage data'),
+ )
+ ) {
+ return null;
+ }
+
+ const fiveHour = parseWindow(lines, ['Current session']);
+ if (!fiveHour) return null;
+
+ const sevenDay =
+ parseWindow(lines, ['Current week (all models)']) ||
+ parseWindow(lines, [
+ 'Current week (Sonnet only)',
+ 'Current week (Sonnet)',
+ ]) ||
+ parseWindow(lines, ['Current week (Opus)']);
+
+ return {
+ five_hour: fiveHour,
+ ...(sevenDay ? { seven_day: sevenDay } : {}),
+ };
+}
+
+export async function fetchClaudeUsageViaCli(
+ binary = 'claude',
+): Promise {
+ return new Promise((resolve) => {
+ let output = '';
+ let finished = false;
+
+ const finish = (value: ClaudeUsageData | null) => {
+ if (finished) return;
+ finished = true;
+ clearTimeout(timer);
+ resolve(value);
+ };
+
+ let proc: ReturnType | null = null;
+ try {
+ proc = spawn('expect', ['-c', EXPECT_PROGRAM], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: {
+ ...(process.env as Record),
+ CLAUDE_BINARY: binary,
+ },
+ });
+ } catch (err) {
+ logger.debug({ err }, 'Claude CLI PTY probe unavailable');
+ finish(null);
+ return;
+ }
+
+ const timer = setTimeout(() => {
+ try {
+ proc?.kill('SIGTERM');
+ } catch {
+ /* ignore */
+ }
+ finish(null);
+ }, CLAUDE_EXPECT_TIMEOUT_MS);
+
+ if (!proc.stdout || !proc.stderr) {
+ finish(null);
+ return;
+ }
+
+ proc.stdout.setEncoding('utf8');
+ proc.stderr.setEncoding('utf8');
+ proc.stdout.on('data', (chunk: string) => {
+ output += chunk;
+ });
+ proc.stderr.on('data', (chunk: string) => {
+ output += chunk;
+ });
+ proc.on('error', (err) => {
+ logger.debug({ err }, 'Claude CLI PTY probe failed to start');
+ finish(null);
+ });
+ proc.on('close', () => {
+ const parsed = parseClaudeUsagePanel(output);
+ if (!parsed && output.trim()) {
+ logger.debug(
+ { tail: output.slice(-400) },
+ 'Claude CLI PTY probe produced unparsable output',
+ );
+ }
+ finish(parsed);
+ });
+ });
+}
diff --git a/src/config.ts b/src/config.ts
index 8325095..8b70305 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,3 +1,4 @@
+import fs from 'fs';
import os from 'os';
import path from 'path';
@@ -36,21 +37,33 @@ export const SCHEDULER_POLL_INTERVAL = 60000;
const PROJECT_ROOT = process.cwd();
const HOME_DIR = process.env.HOME || os.homedir();
+const CONFIG_ROOT = path.join(HOME_DIR, '.config');
+const EJCLAW_CONFIG_DIR = path.join(CONFIG_ROOT, 'ejclaw');
+const LEGACY_NANOCLAW_CONFIG_DIR = path.join(CONFIG_ROOT, 'nanoclaw');
+const ACTIVE_CONFIG_DIR = fs.existsSync(EJCLAW_CONFIG_DIR)
+ ? EJCLAW_CONFIG_DIR
+ : fs.existsSync(LEGACY_NANOCLAW_CONFIG_DIR)
+ ? LEGACY_NANOCLAW_CONFIG_DIR
+ : EJCLAW_CONFIG_DIR;
export const SENDER_ALLOWLIST_PATH = path.join(
- HOME_DIR,
- '.config',
- 'nanoclaw',
+ ACTIVE_CONFIG_DIR,
'sender-allowlist.json',
);
export const STORE_DIR = path.resolve(
- process.env.NANOCLAW_STORE_DIR || path.join(PROJECT_ROOT, 'store'),
+ process.env.EJCLAW_STORE_DIR ||
+ process.env.NANOCLAW_STORE_DIR ||
+ path.join(PROJECT_ROOT, 'store'),
);
export const GROUPS_DIR = path.resolve(
- process.env.NANOCLAW_GROUPS_DIR || path.join(PROJECT_ROOT, 'groups'),
+ process.env.EJCLAW_GROUPS_DIR ||
+ process.env.NANOCLAW_GROUPS_DIR ||
+ path.join(PROJECT_ROOT, 'groups'),
);
export const DATA_DIR = path.resolve(
- process.env.NANOCLAW_DATA_DIR || path.join(PROJECT_ROOT, 'data'),
+ process.env.EJCLAW_DATA_DIR ||
+ process.env.NANOCLAW_DATA_DIR ||
+ path.join(PROJECT_ROOT, 'data'),
);
// Shared cache directory (same across both services for dedup)
export const CACHE_DIR = path.join(PROJECT_ROOT, 'cache');
diff --git a/src/dashboard.ts b/src/dashboard.ts
index bdca357..7bf756a 100644
--- a/src/dashboard.ts
+++ b/src/dashboard.ts
@@ -3,6 +3,10 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
+import {
+ fetchClaudeUsageViaCli,
+ type ClaudeUsageData,
+} from './claude-usage.js';
import { USAGE_DASHBOARD_ENABLED } from './config.js';
import { readEnvFile } from './env.js';
import { GroupQueue, GroupStatus } from './group-queue.js';
@@ -20,11 +24,6 @@ export interface DashboardOptions {
usageUpdateInterval: number;
}
-interface ClaudeUsageData {
- five_hour?: { utilization: number; resets_at: string };
- seven_day?: { utilization: number; resets_at: string };
-}
-
interface CodexRateLimit {
limitId?: string;
limitName: string | null;
@@ -71,6 +70,7 @@ function formatResetKST(value: string | number): string {
try {
const date =
typeof value === 'number' ? new Date(value * 1000) : new Date(value);
+ if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString('ko-KR', {
timeZone: 'Asia/Seoul',
month: 'short',
@@ -188,6 +188,9 @@ export function buildStatusContent(opts: DashboardOptions): string {
}
async function fetchClaudeUsage(): Promise {
+ const cliUsage = await fetchClaudeUsageViaCli();
+ if (cliUsage) return cliUsage;
+
try {
const envToken = readEnvFile(['CLAUDE_CODE_OAUTH_TOKEN']);
let token =
diff --git a/src/db.ts b/src/db.ts
index b705b1b..ec8c2fd 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -139,7 +139,9 @@ function createSchema(database: Database.Database): void {
const hasAgentType = registeredGroupCols.some(
(col) => col.name === 'agent_type',
);
- const hasWorkDir = registeredGroupCols.some((col) => col.name === 'work_dir');
+ const hasWorkDir = registeredGroupCols.some(
+ (col) => col.name === 'work_dir',
+ );
database.exec(`
CREATE TABLE registered_groups_new (
diff --git a/src/group-queue.test.ts b/src/group-queue.test.ts
index 9d01c6c..b914789 100644
--- a/src/group-queue.test.ts
+++ b/src/group-queue.test.ts
@@ -4,7 +4,7 @@ import { GroupQueue } from './group-queue.js';
// Mock config to control concurrency limit
vi.mock('./config.js', () => ({
- DATA_DIR: '/tmp/nanoclaw-test-data',
+ DATA_DIR: '/tmp/ejclaw-test-data',
MAX_CONCURRENT_AGENTS: 2,
}));
diff --git a/src/index.ts b/src/index.ts
index 312ef53..b2e6585 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -280,7 +280,7 @@ const isDirectRun =
if (isDirectRun) {
main().catch((err) => {
- logger.error({ err }, 'Failed to start NanoClaw');
+ logger.error({ err }, 'Failed to start EJClaw');
process.exit(1);
});
}
diff --git a/src/message-runtime.test.ts b/src/message-runtime.test.ts
index 4acc849..d6efae6 100644
--- a/src/message-runtime.test.ts
+++ b/src/message-runtime.test.ts
@@ -346,7 +346,19 @@ describe('createMessageRuntime', () => {
'progress-1',
'오래 걸리는 작업입니다.\n\n1분 10초',
);
- await vi.advanceTimersByTimeAsync(3_600_000);
+ await vi.advanceTimersByTimeAsync(50_000);
+ expect(channel.editMessage).toHaveBeenLastCalledWith(
+ chatJid,
+ 'progress-1',
+ '오래 걸리는 작업입니다.\n\n2분 0초',
+ );
+ await vi.advanceTimersByTimeAsync(3_480_000);
+ expect(channel.editMessage).toHaveBeenLastCalledWith(
+ chatJid,
+ 'progress-1',
+ '오래 걸리는 작업입니다.\n\n1시간 0분 0초',
+ );
+ await vi.advanceTimersByTimeAsync(70_000);
await onOutput?.({
status: 'success',
result: null,
diff --git a/src/message-runtime.ts b/src/message-runtime.ts
index 0451cd2..79c6728 100644
--- a/src/message-runtime.ts
+++ b/src/message-runtime.ts
@@ -352,15 +352,14 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
const hours = Math.floor(elapsedSeconds / 3600);
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
const seconds = elapsedSeconds % 60;
- const elapsedParts: string[] = [];
+ const elapsedLabel =
+ hours > 0
+ ? `${hours}시간 ${minutes}분 ${seconds}초`
+ : minutes > 0
+ ? `${minutes}분 ${seconds}초`
+ : `${seconds}초`;
- if (hours > 0) elapsedParts.push(`${hours}시간`);
- if (minutes > 0) elapsedParts.push(`${minutes}분`);
- if (seconds > 0 || elapsedParts.length === 0) {
- elapsedParts.push(`${seconds}초`);
- }
-
- return `${text}\n\n${elapsedParts.join(' ')}`;
+ return `${text}\n\n${elapsedLabel}`;
};
const syncTrackedProgressMessage = async () => {
@@ -552,7 +551,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
}
messageLoopRunning = true;
- logger.info(`NanoClaw running (trigger: @${deps.assistantName})`);
+ logger.info(`EJClaw running (trigger: @${deps.assistantName})`);
while (true) {
try {
diff --git a/src/platform-prompts.test.ts b/src/platform-prompts.test.ts
index d4145a6..f0fe6cc 100644
--- a/src/platform-prompts.test.ts
+++ b/src/platform-prompts.test.ts
@@ -15,7 +15,7 @@ describe('platform-prompts', () => {
let tempDir: string;
beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nanoclaw-prompts-'));
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-prompts-'));
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
});
diff --git a/src/session-commands.test.ts b/src/session-commands.test.ts
index abbc7e4..436fa8a 100644
--- a/src/session-commands.test.ts
+++ b/src/session-commands.test.ts
@@ -84,9 +84,11 @@ describe('isSessionCommandControlMessage', () => {
});
it('does not match regular bot conversation', () => {
- expect(isSessionCommandControlMessage('좋네요. 필요해지면 바로 말씀드리겠습니다.')).toBe(
- false,
- );
+ expect(
+ isSessionCommandControlMessage(
+ '좋네요. 필요해지면 바로 말씀드리겠습니다.',
+ ),
+ ).toBe(false);
});
});