You are building a TypeScript CLI tool that migrates an OpenClaw multi-agent deployment to run natively on Claude Code. The tool analyzes the existing OpenClaw installation, converts its file formats and configuration, and produces a fully functional Claude Code project with a web chat frontend.
This document contains everything you need to build the tool from scratch. Read it fully before starting.
OpenClaw is a Node.js daemon (port 18789) that manages multiple AI agents. Each agent has a workspace directory containing markdown identity and behavior files. OpenClaw handles conversation sessions, cron-scheduled tasks, model routing, tool restrictions, and channel bindings (typically Discord). Configuration lives in a central openclaw.json.
Claude Code is Anthropic's stateless CLI. The headless mode (claude -p) takes a prompt, runs to completion, and exits. There is no daemon. Identity is defined by a CLAUDE.md file (max 200 lines, auto-discovered from the working directory) plus .claude/rules/*.md files for additional context. MCP servers provide tool extensions. Scheduling must use OS primitives (launchd on macOS).
OpenClaw is stateful and daemon-driven. Claude Code is stateless and invocation-driven. The migration tool bridges this gap by:
- Converting OpenClaw's identity files into Claude Code's format
- Abstracting all agent invocations through a single runner function
- Converting cron schedules to OS-level schedulers
- Building a web chat frontend to replace Discord channel bindings
- Setting up a memory system (files + optional pgvector) for persistence across stateless calls
<project>/
├── src/ # Migration CLI
│ ├── cli.ts # Entry point (commander): analyze, generate, verify, install, chat, db
│ ├── types.ts # All shared TypeScript interfaces
│ ├── constants.ts # Tool mappings, model mappings, limits
│ ├── analyze/ # Phase 1: Parse OpenClaw deployment
│ │ ├── index.ts # Orchestrator
│ │ ├── workspace.ts # Scan workspace directories, read .md files
│ │ └── cron.ts # Parse cron/jobs.json, filter, categorize
│ ├── generate/ # Phase 2: Produce Claude Code project
│ │ ├── index.ts # Orchestrator: iterate agents, generate shared infra (renders env.sh from Handlebars template)
│ │ ├── agent-project.ts # Generate one agent's full directory
│ │ └── converters/ # Per-concern transformation modules
│ │ ├── identity.ts # SOUL.md + USER.md + IDENTITY.md → CLAUDE.md
│ │ ├── rules.ts # AGENTS.md → .claude/rules/*.md
│ │ ├── tools.ts # Tool deny list → --disallowed-tools mapping
│ │ ├── cron.ts # Cron jobs → wrapper scripts + launchd plists
│ │ ├── memory.ts # Copy memory files, generate SQL seed
│ │ └── sentinel.ts # Sentinel scripts → adapted scripts + registry
│ ├── verify/ # Phase 3: Validate output
│ │ └── index.ts
│ └── util/
│ ├── markdown.ts # Section parser, line counter, heading cleaner
│ ├── cron-parser.ts # Cron expr → launchd StartCalendarInterval/StartInterval
│ └── openclaw-paths.ts # Resolve OC_HOME, workspace paths
├── lib/ # Runtime libraries (also deployed to output)
│ ├── agent-runner.ts # THE single abstraction wrapping `claude -p`
│ ├── agent-runner-cli.ts # Shell-callable CLI wrapper for agent-runner
│ ├── scheduler/
│ │ ├── types.ts # Platform-agnostic Scheduler interface
│ │ └── launchd.ts # macOS launchd implementation
│ └── memory/
│ ├── schema.sql # pgvector table definition
│ ├── embed.ts # Embedding provider abstraction (OpenAI, Ollama)
│ ├── mcp-server.ts # MCP server exposing memory_search, memory_store, memory_list
│ └── sync.ts # File watcher syncing .md memory files to database
├── scripts/
│ ├── env.sh # Standalone environment config (for manual use)
│ ├── env.sh.hbs # Handlebars template for generating env.sh into output
│ ├── wrapper.sh # Generic headless Claude Code wrapper for gate scripts
│ ├── dream-gate.sh # Nightly dream orchestrator (checks activity, triggers dreams)
│ ├── reflection-gate.sh # Weekly reflection orchestrator
│ ├── dream-preprocessor.py # Condenses chat-history.jsonl into lightweight digests
│ ├── extract-session-dialogue.py # Read chat-history.jsonl for dream pipeline
│ ├── session-health-report.sh # Checks agent activity in chat-history.jsonl
│ └── jobs/ # Per-cron-job wrappers (generated)
├── chat/ # Web frontend replacing Discord
│ ├── server.ts # Express + WebSocket, session resumption
│ └── public/
│ └── index.html # Single-page dark-themed chat UI
├── test/ # Vitest test suite
│ ├── converters/ # Tests for each converter
│ │ ├── identity.test.ts
│ │ ├── rules.test.ts
│ │ ├── tools.test.ts
│ │ ├── cron.test.ts
│ │ └── sentinel.test.ts
│ ├── util/
│ │ └── cron-parser.test.ts
│ ├── analyze/
│ │ └── cron.test.ts
│ ├── agent-runner.test.ts
│ └── fixtures/ # Sample .md, .json files for tests
│ ├── SOUL.md
│ ├── USER.md
│ ├── IDENTITY.md
│ ├── AGENTS.md
│ ├── jobs.json
│ ├── registry.json
│ └── openclaw.json
├── package.json
└── tsconfig.json
output/
├── agents/
│ └── <agent-id>/
│ ├── CLAUDE.md # Merged identity (<200 lines)
│ ├── .claude/
│ │ ├── rules/
│ │ │ ├── workflow.md # Session startup, responsibilities
│ │ │ ├── memory.md # Memory management rules
│ │ │ ├── style.md # Communication rules
│ │ │ ├── safety.md # Boundaries, red lines
│ │ │ ├── domain.md # Domain-specific rules (if any)
│ │ │ ├── tool-restrictions.md # Migration notes for unmapped tools
│ │ │ └── identity-overflow.md # Overflow if CLAUDE.md > 200 lines
│ │ └── mcp_config.json # Points to memory MCP server
│ ├── memory/ # Copied from OpenClaw workspace
│ └── shared-memory → ../../shared-memory
├── shared-memory/ # Cross-agent knowledge base
├── lib/memory/seed.sql # Database seed from memory files
├── scripts/
│ ├── env.sh # Environment config
│ ├── wrapper.sh # Generic agent invocation wrapper
│ ├── dream-gate.sh # Nightly dream orchestrator
│ ├── reflection-gate.sh # Weekly reflection orchestrator
│ ├── dream-preprocessor.py # Session digest condenser
│ ├── extract-session-dialogue.py # chat-history.jsonl extractor
│ ├── session-health-report.sh # Activity checker
│ ├── jobs/ # Per-cron-job wrappers
│ │ ├── <slug>.sh
│ │ ├── <slug>.plist
│ │ └── messages/<slug>.md
│ └── sentinel/ # Adapted sentinel system
│ ├── scripts/
│ │ ├── run_sentinel.sh # Adapted (openclaw → agent-runner)
│ │ ├── dispatch_*.sh # Adapted dispatch scripts
│ │ └── check_*.sh # Copied unchanged
│ ├── registry.json # Path-updated registry
│ ├── state/ # Copied state files
│ └── <sentinel-name>.plist # One plist per enabled sentinel
├── logs/
├── sessions.json # Active Claude Code session IDs per agent
└── config.json # Master config for the deployment
The migration tool reads from a standard OpenClaw installation (default ~/.openclaw/):
interface OpenClawConfig {
agents: {
defaults: {
model: { primary: string }; // e.g. "anthropic/claude-sonnet-4-6"
workspace: string; // default workspace path
memorySearch: MemorySearchConfig;
};
list: AgentDefinition[]; // array of agent configs
};
bindings: ChannelBinding[]; // Discord channel → agent mappings
}
interface AgentDefinition {
id: string; // e.g. "main", "forge", "health"
workspace: string; // workspace directory name
model?: { primary: string }; // override model
subagents?: { model: string }; // sub-agent model
tools?: { deny?: string[] }; // denied tool names
}Each agent has a workspace. The default workspace applies to agents that don't specify their own. Per-agent workspaces override or extend the default.
Standard files in each workspace:
| File | Purpose | Migration target |
|---|---|---|
SOUL.md |
Core personality, boundaries, epistemic stance | Merged into CLAUDE.md |
USER.md |
User context (who the user is, their needs) | Merged into CLAUDE.md |
IDENTITY.md |
Agent-specific name, role, emoji | Merged into CLAUDE.md |
AGENTS.md |
Operating manual: startup, memory rules, red lines, communication | Sharded into .claude/rules/*.md |
TOOLS.md |
Local tool/environment notes | Copied to .claude/rules/tools-reference.md |
MEMORY.md |
Curated long-term memory index | Copied to memory/MEMORY.md |
HEARTBEAT.md |
Proactive task list (if any) | Converted to scheduling rules |
memory/ |
Daily memory files, observations | Copied to memory/ |
interface CronJob {
id: string;
agentId: string;
name: string;
enabled: boolean;
schedule: { kind: 'cron'; expr: string; tz: string } | { kind: 'at'; at: string };
payload: { message: string; model?: string; thinking?: string; timeoutSeconds?: number };
delivery: { mode: string; channel?: string; to?: string };
}Directory of from-<agent>.md files. Each agent can read these. Deployed as a shared directory with symlinks into each agent's workspace.
Parse IDENTITY.md for structured fields (name, role, emoji) using the pattern:
- **Name**: Forge
- **Role**: Agent Builder & Infrastructure Admin
- **Emoji**: ...
Parse SOUL.md into sections by heading. Extract and order:
- Preamble (text before first heading — often the most important identity statement)
- Core truths / identity section
- Boundaries / constraints
- Epistemic standards
- Vibe / personality
- Any remaining sections
Parse USER.md into sections. Include all.
Assemble into a single markdown document. If over 200 lines, move the detailed user context sections to .claude/rules/identity-overflow.md and leave a pointer in CLAUDE.md.
Parse AGENTS.md by headings. Route each section to a target file based on heading text:
| Heading pattern | Target file |
|---|---|
| Session Startup, Bootstrap, First Run | workflow.md |
| Memory, Remember | memory.md |
| Red Line, Boundary, Safety | safety.md |
| Communication, Group Chat, Platform, Discord | style.md |
| Heartbeat, Cron, Schedule, Proactive | scheduling.md |
| Tool, Skill, Building | workflow.md |
| Responsibility, Purpose, Role | workflow.md |
| (domain-specific, no match) | domain.md |
Rewrite session startup instructions: remove "Read SOUL.md" / "Read USER.md" (these are now in CLAUDE.md which is auto-discovered). Renumber remaining steps.
Map OpenClaw tool names to Claude Code --disallowed-tools values:
| OpenClaw tool | Claude Code equivalent |
|---|---|
browser |
WebFetch, WebSearch |
subagents, sessions_spawn |
Bash(claude*) (prevents spawning sub-agents) |
cron |
No equivalent (cron is OS-level, not a Claude Code tool) |
sessions_send/list/history/yield |
No equivalent |
image_generate, image |
No equivalent |
process |
No equivalent |
Generate a tool-restrictions.md rule file listing unmapped denials as documentation.
For each recurring cron job (schedule.kind === 'cron'):
Wrapper script (scripts/jobs/<slug>.sh):
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../env.sh"
MESSAGE_FILE="${SCRIPT_DIR}/messages/<slug>.md"
OUTPUT=$(node "${CODECLAWED_HOME}/lib/agent-runner-cli.js" \
--agent "<agentId>" \
--model "<model>" \
--message-file "$MESSAGE_FILE" \
--working-directory "${CODECLAWED_HOME}/agents/<agentId>" \
--timeout "<timeout>" \
--skip-permissions)
echo "$OUTPUT"
# Optional: deliver to chat server
curl -s -X POST "http://localhost:${CHAT_PORT:-3456}/api/deliver" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg agent "<agentId>" --arg content "$OUTPUT" '{agentId: $agent, content: $content}')"Message file (scripts/jobs/messages/<slug>.md): the raw payload.message from the cron job.
Launchd plist: Convert cron expression to launchd format:
*/N * * * *→<key>StartInterval</key><integer>N*60</integer>- Fixed times →
<key>StartCalendarInterval</key><dict>with Minute, Hour, Day, Month, Weekday keys - Ranges (e.g.,
1-5for weekdays) → expand to multiple CalendarInterval dicts - Lists (e.g.,
3,15) → expand to multiple CalendarInterval dicts - If the source job has
enabled: false, set<key>Disabled</key><true/>in the plist
- Copy each agent's
memory/directory contents tooutput/agents/<id>/memory/ - Copy
MEMORY.mdinto the memory directory - Copy
shared-memory/tooutput/shared-memory/ - Create symlinks from each agent's directory to shared-memory
- Generate
seed.sqlwith INSERT statements for all memory file contents (for pgvector database)
Sentinels are zero-token monitoring scripts: a check script runs on a cron schedule, and only if the check triggers (exit code 1) does the LLM get invoked via a dispatch script. The generate phase wires sentinels into the output.
Source: ~/.openclaw/workspace/skills/sentinel/ contains registry.json, a scripts/ directory (run_sentinel.sh, dispatch scripts, check scripts), and a state/ directory.
What gets adapted:
run_sentinel.sh—openclaw agent --messagecalls replaced withnode agent-runner-cli.js --skip-permissions --message.$OC_HOMEreplaced with$CODECLAWED_HOME.$OPENCLAWreplaced withclaude.- Dispatch scripts (
dispatch_youtube_feeds.sh,dispatch_freshrss_feeds.sh) — same replacements, plusopenclaw message sendcalls replaced withcurlPOST to the chat server's/api/deliverendpoint. registry.json— all absolute paths rewritten from the OpenClaw sentinel directory tooutput/scripts/sentinel/.
What gets copied unchanged: Check scripts (e.g., check_youtube_feeds.sh), utility scripts (e.g., sentinel_ctl.py), state files.
Launchd plists: One plist per enabled sentinel, labeled com.codeclawed.sentinel.<name>. The plist runs /bin/bash run_sentinel.sh <sentinel-name> at the sentinel's schedule interval. Uses cronToLaunchd() from cron-parser.ts to convert the cron expression.
Output directory: output/scripts/sentinel/ with scripts/, state/, registry.json, and *.plist files.
The sentinelScripts field in GenerationResult is populated with all generated/adapted file contents (keyed by filename).
Every component in the system invokes agents through a single function. This is the most important architectural decision.
// lib/agent-runner.ts
interface AgentRunOptions {
agentId: string;
model: string; // "opus" | "sonnet" | "haiku"
message: string;
workingDirectory: string; // agents/<id>/ — Claude Code reads CLAUDE.md from here
systemPrompt?: string; // --append-system-prompt
disallowedTools?: string[]; // --disallowed-tools
timeout?: number;
skipPermissions?: boolean; // --dangerously-skip-permissions
outputFormat?: 'text' | 'stream-json';
resumeSessionId?: string; // --resume <session-id>
}The function builds CLI arguments and spawns:
claude -p --model <model> [--resume <id>] [--output-format stream-json --verbose] [--dangerously-skip-permissions] [--append-system-prompt "..."]
With cwd set to the agent's directory. Claude Code auto-discovers CLAUDE.md from cwd. The message is written to stdin and stdin is closed.
Key details:
--output-format stream-jsonrequires--verbose— without it, the command silently fails- Stream-json output format: each line is a JSON object. Text content is at
message.content[].textfor assistant messages, andresultfield for the final result - The
session_idappears in the first line (type: "system", subtype: "init")
Three exported functions:
runAgent(options)— returns a Promise with full stdout/stderr after completionrunAgentStreaming(options, onChunk)— returns{ process, result }and calls onChunk for each stdout data eventbuildArgs(options)— builds the claude CLI argument array from options (also used for testing)
Also provide lib/agent-runner-cli.ts — a CLI wrapper so shell scripts can call:
node agent-runner-cli.js --agent main --model opus --message "..." --skip-permissions --source cronThe CLI wrapper always appends both the user message and agent response to chat-history.jsonl in the agent's working directory. This makes chat-history.jsonl the single source of truth for all agent interactions, regardless of whether they originated from the chat server, cron jobs, or headless scripts. The --source flag (default: cron) tags entries with their origin (chat, cron, or sentinel) so downstream consumers like the dream pipeline can filter by source.
The chat server replaces Discord as the user-facing interface.
Claude Code sessions persist across claude -p calls via --resume <session-id>. The chat server:
- First message to an agent: Runs
claude -pnormally. Capturessession_idfrom the stream-json init event. Stores it insessions.json(agentId → sessionId). - Subsequent messages: Passes
--resume <session_id>. Claude Code loads the full prior conversation — no need to inject history via system prompt. - Resume failure: If the session expired or errored, automatically falls back to a fresh session.
- "New Session" button: Client sends
{type: "new_session", agentId}. Server clears the stored session ID.
- Client sends:
{type: "message", agentId: "main", text: "hello"} - Server sends:
{type: "stream_start", agentId, resuming: bool} - Server sends:
{type: "stream_chunk", chunk: "<raw stream-json line>"}(repeated) - Server sends:
{type: "stream_end", agentId, exitCode, durationMs, sessionId} - Server sends:
{type: "error", error: "..."} - Cron deliveries:
{type: "delivery", message: ChatMessage}
All exchanges persisted to output/agents/<id>/chat-history.jsonl:
{"id":"uuid","agentId":"main","role":"user","content":"...","timestamp":"ISO","source":"chat"}
{"id":"uuid","agentId":"main","role":"assistant","content":"...","timestamp":"ISO","source":"chat"}The assistant content is extracted as plain text from the stream-json result (not raw stream-json).
GET /api/agents— list agents from config.jsonGET /api/history/:agentId— last 50 messages from chat-history.jsonlPOST /api/deliver— webhook for cron/script output delivery
Single-page HTML with inline CSS and JavaScript. Dark theme. Left sidebar with agent list (name + model badge). Main area with streaming message display. Enter to send, Shift+Enter for newline. Status bar shows session state.
Abstract interface so launchd (macOS) can be swapped for systemd (Linux) later:
interface Scheduler {
install(job: ScheduledJob): Promise<void>;
uninstall(jobId: string): Promise<void>;
list(): Promise<ScheduledJob[]>;
status(jobId: string): Promise<JobStatus>;
enable(jobId: string): Promise<void>;
disable(jobId: string): Promise<void>;
}macOS implementation writes plists to ~/Library/LaunchAgents/com.<tool-name>.<job-id>.plist and uses launchctl load/unload.
Agent memory files at output/agents/<id>/memory/*.md are human-readable and the source of truth. Agents can read and write these directly using Claude Code's file tools.
MCP server exposes memory tools to agents:
memory_search(query, agent_id?, limit?)— hybrid vector + text search when embedding provider is configured, falls back to text-only search otherwisememory_store(content, file_path?)— insert/upsert with embeddingmemory_list(agent_id?)— list entries (includeshas_embeddingfield)
Sync daemon watches memory files, computes embeddings via the configured provider, and upserts changes to the database.
Abstraction over embedding APIs. Two implementations:
- OpenAI (default): Uses
text-embedding-3-small(1536 dimensions). RequiresOPENAI_API_KEY. Supports batch embedding via the/v1/embeddingsAPI. - Ollama (local alternative): Uses
nomic-embed-text(768 dimensions) by default. Requires Ollama running atOLLAMA_HOST(defaulthttp://localhost:11434). No batch support (calls sequentially).
interface EmbeddingProvider {
embed(text: string): Promise<number[]>;
embedBatch(texts: string[]): Promise<number[][]>;
readonly dimensions: number;
}Factory function createEmbeddingProviderFromEnv() auto-detects from environment:
- If
EMBEDDING_PROVIDERis set (openaiorollama), uses that provider with optionalEMBEDDING_MODELandEMBEDDING_DIMENSIONSoverrides - If
OPENAI_API_KEYis set, uses OpenAI - Otherwise returns
null— embeddings are skipped, search falls back to text-only
When embeddings are available, memory_search uses a weighted hybrid query:
(0.7 * (1 - (embedding <=> query_vector))) + (0.3 * ts_rank(tsvector, tsquery)) AS scoreWeights (0.7 vector, 0.3 text) match OpenClaw's hybrid search config. Rows with either an embedding or a text match are included, so entries without embeddings still surface via text search.
Schema:
CREATE TABLE memory_entries (
id SERIAL PRIMARY KEY,
agent_id TEXT NOT NULL,
file_path TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
metadata JSONB DEFAULT '{}'
);
CREATE UNIQUE INDEX idx_memory_agent_file ON memory_entries(agent_id, file_path);Each agent's .claude/mcp_config.json points to the MCP server with AGENT_ID scoped:
{
"mcpServers": {
"memory": {
"command": "node",
"args": ["<output>/lib/memory/mcp-server.js"],
"env": { "AGENT_ID": "<id>", "DATABASE_URL": "postgresql://localhost:5432/<dbname>", "OPENAI_API_KEY": "..." }
}
}
}The dream system is the nightly/weekly memory consolidation pipeline. Five scripts work together:
dream-gate.sh — Nightly orchestrator (launchd, 3 AM). For each agent:
- Calls
session-health-report.shto check for activity in the last 24 hours - Calls
extract-session-dialogue.pyto pull recent dialogue intodream-extracts/nightly-extract.jsonl - Calls
dream-preprocessor.pyto condense into lightweight digests - Invokes the agent via
wrapper.shwith the dream prompt
reflection-gate.sh — Weekly orchestrator (launchd, Sundays at noon). Same pattern but 7-day window, outputs to dream-extracts/weekly-extract.jsonl.
session-health-report.sh — Checks chat-history.jsonl for entries within a time window. Takes <agent_dir> and --hours N. Exits 0 (active) or 1 (inactive). Prints entry count to stdout.
extract-session-dialogue.py — Reads chat-history.jsonl, filters by --since/--until date range and --sources (default: chat,cron), outputs JSONL in the format dream-preprocessor.py expects: {type: "message", message: {role, content: [{type: "text", text: "..."}]}}.
dream-preprocessor.py — Two-tier condensation of chat-history.jsonl:
- Line-by-line truncation: truncates tool results/details to 500 chars, drops results for
Bash/Edit/Write/NotebookEdittools (assistant text captures the outcome) - Chain summarization: finds sequential tool chains (6+ entries), optionally summarizes via Haiku API (requires
ANTHROPIC_API_KEYenv var)
Output: agents/<id>/dream-extracts/chat-history-digest.jsonl. Skips processing if digest is newer than source.
The generate phase copies all dream scripts from scripts/ to output/scripts/. The verify phase warns if any dream script is missing from the output.
| OpenClaw | CodeClawed |
|---|---|
$OPENCLAW cron run <id> |
wrapper.sh --agent <id> --model sonnet --message-file <path> |
$OC_HOME/agents/<id>/sessions/*.jsonl |
agents/<id>/chat-history.jsonl (single file) |
$OC_HOME |
$CODECLAWED_HOME |
API key from secrets.json |
ANTHROPIC_API_KEY environment variable |
Per-session .jsonl files in sessions/ dir |
Unified chat-history.jsonl written by agent-runner-cli |
The verify command checks the generated output:
config.jsonexists and is valid JSON- Each agent has
CLAUDE.mdunder 200 lines - Each agent has
.claude/mcp_config.json(valid JSON) - Each agent has at least one rule file in
.claude/rules/ - Each cron wrapper script has a matching message file
scripts/env.shexists- Dream system scripts present (
dream-gate.sh,reflection-gate.sh,dream-preprocessor.py,extract-session-dialogue.py,session-health-report.sh,wrapper.sh) — warns if missing - Working directories referenced in config.json exist
- Models are recognized Claude Code values
<tool> analyze [--oc-home ~/.openclaw] [--format text|json]
<tool> generate [--oc-home ~/.openclaw] [--output ./output] [--dry-run]
<tool> verify [--output ./output]
<tool> install [--output ./output] [--jobs <categories>] [--dry-run] [--uninstall]
<tool> chat [--port 3456] [--output ./output]
<tool> db init [--database-url <url>]
<tool> db status [--database-url <url>]
<tool> db seed [--output ./output] [--database-url <url>]
<tool> db reset [--database-url <url>] [--yes]Copies generated .plist files from output/scripts/jobs/ to ~/Library/LaunchAgents/ and loads them via launchctl load. Reads output/config.json to discover the list of cron jobs and their slugs.
--jobs <categories>: Comma-separated list of categories to filter by (dreams,reflections,sentinels,market,content,research). Jobs are categorized by matching their name againstCRON_CATEGORY_PATTERNSfromconstants.ts. Unmatched jobs are categorized asother.--dry-run: Lists what would be installed/uninstalled without writing or loading plists.--uninstall: Scans~/Library/LaunchAgents/for files prefixed withLAUNCHD_LABEL_PREFIX(com.codeclawed), unloads them vialaunchctl unload, and deletes the plist files. Combinable with--dry-run.
Plists are named com.codeclawed.<slug>.plist in ~/Library/LaunchAgents/. Jobs without a generated plist (e.g., one-time at schedule jobs) are skipped with a warning.
Manages the PostgreSQL database used by the memory system (pgvector).
db init: Checks thatpsqlis available, runscreatedb codeclawed(ignores "already exists" errors), then applieslib/memory/schema.sqlviapsql. Extracts the database name from--database-url(defaults topostgresql://localhost:5432/codeclawed).db status: Connects to the database viapg.Pool, checks whether thevectorextension is installed, whether thememory_entriestable exists, and reports the row count.db seed: Reads<output>/lib/memory/seed.sql(generated duringclawcode generate) and executes it against the database.db reset: Drops and recreates the database. Prompts for confirmation unless--yesis passed. Runsdropdb,createdb, then applies the schema.
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"chalk": "^5.4.1",
"chokidar": "^4.0.3",
"commander": "^13.1.0",
"cron-parser": "^5.0.6",
"express": "^5.1.0",
"handlebars": "^4.7.8",
"pg": "^8.14.1",
"pgvector": "^0.2.0",
"ws": "^8.18.1",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/express": "^5.0.2",
"@types/node": "^22.15.2",
"@types/pg": "^8.11.11",
"@types/ws": "^8.18.1",
"tsx": "^4.19.4",
"typescript": "^5.8.3",
"vitest": "^3.1.2"
}
}TypeScript config: ES2022, NodeNext, strict, outDir dist/.
| Step | What | Depends on |
|---|---|---|
| 1 | types.ts, constants.ts, util/* | — |
| 2 | analyze/* | Step 1 |
| 3 | agent-runner.ts + CLI wrapper | — |
| 4 | Identity converter | Step 2 |
| 5 | Rules converter | Step 2 |
| 6 | Tools converter | Step 1 |
| 7 | Memory converter | Step 2 |
| 8 | Cron converter | Step 1 |
| 8b | Sentinel converter | Steps 1-2 |
| 9 | generate/index.ts + agent-project.ts | Steps 4-8b |
| 10 | verify/* | Step 9 |
| 11 | cli.ts | Steps 2, 9, 10 |
| 12 | scheduler/launchd.ts | — |
| 13 | memory/embed.ts + mcp-server.ts + sync.ts + schema.sql | — |
| 14 | chat/* | Step 3 |
| 15 | test/* | Steps 1-14 |
Steps 3, 12, 13 can run in parallel with steps 4-11.
-
--output-format stream-jsonrequires--verbosein Claude Code CLI. Without it, the command fails silently to stderr. -
Claude Code's CLAUDE.md limit is 200 lines. Exceeding this doesn't error but degrades performance. Always check and overflow to rules files.
-
claude -pis stateless. Each invocation starts fresh. Use--resume <session-id>for conversation continuity. The session ID comes from the stream-json init event. -
Tool deny names differ. OpenClaw uses names like
browser,sessions_spawn,cron. Claude Code usesWebFetch,WebSearch,Bash(pattern). Many OpenClaw tools have no Claude Code equivalent. -
Cron expressions don't map 1:1 to launchd.
*/Npatterns becomeStartInterval(seconds). Ranges and lists must be expanded into multipleStartCalendarIntervaldicts. launchd has no native step-pattern support for non-minute fields. -
The working directory IS the agent. When
claude -pruns withcwdset to an agent's directory, it auto-discovers that agent's CLAUDE.md, rules, and MCP config. There's no agent registry or instantiation — the filesystem defines identity. -
All cron jobs in an existing deployment may be disabled. The migration tool always migrates all recurring cron jobs regardless of their
enabledstate. Jobs that haveenabled: falseget<key>Disabled</key><true/>in their launchd plist, preventing them from auto-starting. They can be selectively enabled later viaclawcode install.
The project uses vitest (npm test). Tests are in test/ (excluded from tsconfig). Fixtures are in test/fixtures/.
What is tested (86 tests across 8 files):
-
Converters (all pure functions, tested with fixture data):
identity.test.ts— CLAUDE.md generation, line limit enforcement, overflow, IDENTITY.md parsing, fallback to agent IDrules.test.ts— AGENTS.md section routing to correct rule files, session startup rewriting, heartbeat rewritingtools.test.ts— tool deny list mapping (browser→WebFetch/WebSearch, subagents→Bash(claude*), unknown tools, deduplication)cron.test.ts— wrapper script generation (env.sh sourcing, agent-runner-cli flags, delivery section), plist XML validity, slugificationsentinel.test.ts— openclaw→agent-runner replacement, $OC_HOME→$CODECLAWED_HOME, dispatch script adaptation, registry path updates
-
Utilities:
cron-parser.test.ts— cron-to-launchd conversion (fixed times, */N intervals, weekday ranges, hour lists, monthly), plist XML generation, XML escaping, error handling
-
Analysis:
cron.test.ts— filterMigratableJobs (excludes at-schedule and already-run deleteAfterRun jobs, keeps disabled cron jobs), categorizeJobs (dream/reflection/research/market/content/other/skipped)
-
Agent runner:
agent-runner.test.ts— buildArgs flag construction (--resume, --verbose with stream-json, --dangerously-skip-permissions, --disallowed-tools, --effort, --max-budget-usd, --append-system-prompt, --mcp-config)
Test fixtures in test/fixtures/:
SOUL.md,USER.md,IDENTITY.md,AGENTS.md— sample workspace files with representative sectionsjobs.json— 6 jobs: recurring cron (various categories), disabled cron, one-shot at (already run)registry.json— 2 sentinel entries with paths to updateopenclaw.json— minimal config with 2 agents, tool deny lists
This prompt describes the standard migration framework. The following are deployment-specific extensions that may or may not apply: