feat: add /migrate-from-openclaw skill for guided OpenClaw migration

Conversational migration skill that reads an existing OpenClaw installation
and interactively guides users through importing identity, personality,
channel credentials, groups, scheduled tasks, MCP servers, skills, and
plugins into NanoClaw.

8-phase flow: discovery → groups/architecture → settings → identity/memory
→ channel credentials → scheduled tasks → MCP/webhooks/config → summary.

Includes:
- discover-openclaw.ts: finds OpenClaw state dir, parses JSON5 config,
  detects channels (both channels.* and legacy top-level format), groups
  (handles agent:main: prefixed session keys), workspace files (reads
  custom agent.workspace path), skills, config-registered plugins with
  API keys, cron jobs, MCP servers. Dumps raw config keys for robustness.
- extract-channel-credentials.ts: resolves SecretRef formats (plain,
  env template, object), writes credentials directly to .env via
  --write-env flag (never exposes raw values to stdout)
- MIGRATE_CRONS.md: extracted reference for cron job migration, loaded
  only when cron jobs exist
- migration-state.md: persistent state file for recovery after compaction
- Setup hook: detects ~/.openclaw during /setup and offers migration

Tested against real ~/.clawdbot and remote ~/.openclaw installations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-04 19:27:14 +03:00
parent 3608f05233
commit b2a5a58f8a
5 changed files with 1773 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
# Migrating OpenClaw Cron Jobs to NanoClaw Scheduled Tasks
This file is referenced by SKILL.md Phase 5 when cron jobs are detected.
**Before inserting tasks:** Read `src/db.ts` and search for `scheduled_tasks` to verify the current table schema. The schema below is a reference — if columns have been added, removed, or renamed, use the current schema from the source code.
Also verify the `createTask` function signature in `src/db.ts` — it may be simpler to call it via a script than raw SQL.
## OpenClaw Cron Job Format
Source: `<STATE_DIR>/cron/jobs.json` (from `src/cron/types.ts`). If the file format doesn't match what's described below, read the actual file and adapt — OpenClaw may have changed the schema.
The jobs file is `{ version: 1, jobs: CronJob[] }`. Each job has:
- `id`, `name`, `description`, `enabled`, `deleteAfterRun`
- `schedule`: `{ kind: "cron", expr: string, tz?: string }` | `{ kind: "every", everyMs: number }` | `{ kind: "at", at: string }`
- `payload`: `{ kind: "agentTurn", message: string, model?, thinking?, timeoutSeconds? }` | `{ kind: "systemEvent", text: string }`
- `sessionTarget`: `"main"` | `"isolated"` | `"current"` | `"session:<id>"`
- `wakeMode`: `"next-heartbeat"` | `"now"`
- `delivery`: `{ mode: "none" | "announce" | "webhook", channel?, to?, threadId?, bestEffort? }`
- `failureAlert`: `{ after?: number, channel?, to?, cooldownMs? }` | `false`
- `state`: runtime state (nextRunAtMs, lastRunStatus, consecutiveErrors, etc.)
## NanoClaw `scheduled_tasks` Table
Source: `src/db.ts`
| Column | Type | Notes |
|--------|------|-------|
| `id` | TEXT PK | Unique task ID |
| `group_folder` | TEXT | Target group directory (e.g. `"main"`) |
| `chat_jid` | TEXT | Target chat JID |
| `prompt` | TEXT | Task instructions |
| `script` | TEXT | Optional bash pre-check script |
| `schedule_type` | TEXT | `"cron"`, `"interval"`, or `"once"` |
| `schedule_value` | TEXT | Cron expr, ms interval, or ISO timestamp |
| `context_mode` | TEXT | `"group"` or `"isolated"` (default) |
| `next_run` | TEXT | ISO timestamp — must be computed at insert time |
| `last_run` | TEXT | null initially |
| `last_result` | TEXT | null initially |
| `status` | TEXT | `"active"`, `"paused"`, or `"completed"` |
| `created_at` | TEXT | ISO timestamp |
## Field Mapping
- `schedule.kind:"cron"` + `schedule.expr``schedule_type:"cron"`, `schedule_value:<expr>`
- `schedule.kind:"every"` + `schedule.everyMs``schedule_type:"interval"`, `schedule_value:<ms as string>`
- `schedule.kind:"at"` + `schedule.at``schedule_type:"once"`, `schedule_value:<ISO timestamp>`
- `payload.message` or `payload.text``prompt`
- `sessionTarget:"isolated"``context_mode:"isolated"`, `sessionTarget:"main"` or `"current"``context_mode:"group"`
## What Doesn't Map
- `delivery.mode:"webhook"` — NanoClaw has no webhook delivery. Discuss with the user: this could be implemented as a task `script` that runs `curl` to hit the webhook endpoint.
- `failureAlert` — NanoClaw has no failure alert system. Note this to the user.
- `wakeMode` — NanoClaw tasks always wake the agent immediately.
- `payload.model`, `payload.thinking`, `payload.timeoutSeconds` — NanoClaw doesn't support per-task model/thinking config. These are handled by the SDK.
- `deleteAfterRun` — NanoClaw `"once"` tasks are marked `"completed"` after running, not deleted.
## For Each Enabled Job
1. Show what it does: name, schedule, prompt, delivery mode
2. Explain any differences (no retry config, no webhook delivery, no failure alerts)
3. If `delivery.mode:"webhook"`: discuss with the user — a task `script` with `curl` often suffices
4. Ask if they want to keep this task
## Inserting Tasks
Insert directly into the SQLite database. This requires groups to be registered first (Phase 1). Use the registered group's `folder` and `chat_jid`:
```bash
npx tsx -e "
const Database = require('better-sqlite3');
const { CronExpressionParser } = require('cron-parser');
const db = new Database('store/messages.db');
// Compute next_run for cron tasks:
// const interval = CronExpressionParser.parse('<expr>', { tz: process.env.TZ || 'UTC' });
// const nextRun = interval.next().toISOString();
db.prepare(\`INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, script, schedule_type, schedule_value, context_mode, next_run, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\`).run(
'migrated-<original-id>',
'<group_folder>',
'<chat_jid>',
'<mapped prompt>',
null,
'<mapped schedule_type>',
'<mapped schedule_value>',
'<mapped context_mode>',
'<computed next_run ISO>',
'active',
new Date().toISOString()
);
db.close();
"
```
**Computing `next_run`:**
- `cron` tasks: use `CronExpressionParser.parse(expr, { tz }).next().toISOString()`
- `interval` tasks: `new Date(Date.now() + ms).toISOString()`
- `once` tasks: `next_run` equals `schedule_value`
If groups haven't been registered yet (database doesn't exist), save the task details to `groups/main/openclaw-migration-tasks.md` with the exact SQL payloads, and tell the user: "These tasks will be created after `/setup` registers your groups."