Skill: arakichanxd/claw-sync (version 2.0.2)

--- name: claw-sync description: Secure sync for OpenClaw memory and workspace. Use /sync to push, /restore to pull, /sync-status to check. Supports versioned backups and disaster recovery. commands: - name: sync description: Push memory and skills to remote repository usage: /sync [--dry-run] run: node skills/claw-sync/index.js sync - name: restore description: Restore memory and skills from remote usage: /restore [latest|<version>] [--force] run: node skills/claw-sync/index.js restore - name: sync-status description: Show sync configuration and local backups usage: /sync-status run: node skills/claw-sync/index.js status - name: sync-list description: List all available backup versions usage: /sync-list run: node skills/claw-sync/index.js list --- # Claw Sync Secure, versioned sync for OpenClaw memory and workspace. ## Commands ### /sync Push your memory and skills to the remote repository. ``` /sync → Push and create versioned backup /sync --dry-run → Preview what would be synced ``` ### /restore Restore memory and skills from the remote repository. ``` /restore → Restore latest version /restore latest → Same as above /restore backup-20260202-1430 → Restore specific version /restore latest --force → Skip confirmation ``` ### /sync-status Show sync configuration and local backup info. ``` /sync-status ``` ### /sync-list List all available backup versions. ``` /sync-list ``` --- ## What Gets Synced | File | Description | |------|-------------| | `MEMORY.md` | Long-term memory | | `USER.md` | User profile | | `SOUL.md` | Agent persona | | `IDENTITY.md` | Agent identity | | `TOOLS.md` | Tool configs | | `AGENTS.md` | Workspace rules | | `memory/*.md` | Daily logs | | `skills/*` | Custom skills | ## NOT Synced (security) - `openclaw.json` - Contains API keys - `.env` - Contains secrets ## Setup Required Create `~/.openclaw/.backup.env`: ``` BACKUP_REPO=https://github.com/username/your-repo BACKUP_TOKEN=ghp_your_token ``` ## Features - 🏷️ **Versioned** - Each sync creates a restorable version - 💾 **Disaster Recovery** - Local backup before every restore - 🔒 **Secure** - No config files synced, token sanitization - 🖥️ **Cross-platform** - Windows, Mac, Linux

path: claw-sync/.gitignore
content: # Dependencies
node_modules/

# Logs
*.log
logs/

# Runtime
.backup-staging/
.backup-restore/

# OS
.DS_Store
Thumbs.db

# Editor
.vscode/
.idea/
*.swp
*.swo


path: config.example.env
content: # OpenClaw Backup Configuration
# Copy this file to ~/.openclaw/.backup.env and fill in your values

# Your private GitHub repository URL
BACKUP_REPO=https://github.com/username/openclaw-backup

# GitHub Fine-Grained Personal Access Token
# Required permissions: Contents (Read and Write)
# Generate at: https://github.com/settings/tokens?type=beta
BACKUP_TOKEN=[REDACTED_SECRET]


path: index.js
content: #!/usr/bin/env node
/**
 * Claw Sync - Main Entry Point
 * Handles all slash commands: /sync, /restore, /sync-status, /sync-list
 * 
 * Usage:
 *   node index.js sync [--dry-run]
 *   node index.js restore [latest|<version>] [--force]
 *   node index.js status
 *   node index.js list
 */

const { spawn } = require('child_process');
const path = require('path');

const SCRIPTS_DIR = path.join(__dirname, 'scripts');

// Parse command and args
const args = process.argv.slice(2);
const command = args[0]?.toLowerCase();
const commandArgs = args.slice(1);

// Help text
function showHelp() {
    console.log(`
🔄 Claw Sync - Commands

  /sync              Push memory and skills to remote
  /sync --dry-run    Preview what would be synced

  /restore           Restore latest version
  /restore <version> Restore specific version (e.g., backup-20260202-1430)
  /restore --force   Skip confirmation prompt

  /sync-status       Show sync configuration and local backups
  /sync-list         List all available backup versions

Examples:
  node index.js sync
  node index.js sync --dry-run
  node index.js restore
  node index.js restore backup-20260202-1430
  node index.js restore latest --force
  node index.js status
  node index.js list
`);
}

// Run a script
function runScript(scriptName, scriptArgs = []) {
    const scriptPath = path.join(SCRIPTS_DIR, scriptName);

    const child = spawn('node', [scriptPath, ...scriptArgs], {
        stdio: 'inherit',
        cwd: __dirname
    });

    child.on('error', (err) => {
        console.error(`❌ Failed to run ${scriptName}:`, err.message);
        process.exit(1);
    });

    child.on('exit', (code) => {
        process.exit(code || 0);
    });
}

// Main command handler
switch (command) {
    case 'sync':
    case 'push':
    case 'backup':
        // /sync or /backup
        runScript('push.js', commandArgs);
        break;

    case 'restore':
    case 'pull':
        // /restore [version] [--force]
        const restoreArgs = [];

        for (let i = 0; i < commandArgs.length; i++) {
            const arg = commandArgs[i];

            if (arg === '--force' || arg === '-f') {
                restoreArgs.push('--force');
            } else if (arg === 'latest') {
                // 'latest' means no version specified (default behavior)
                continue;
            } else if (arg.startsWith('backup-')) {
                // Specific version
                restoreArgs.push('--version', arg);
            } else if (!arg.startsWith('-')) {
                // Assume it's a version tag
                restoreArgs.push('--version', arg);
            }
        }

        runScript('pull.js', restoreArgs);
        break;

    case 'status':
    case 'sync-status':
        // /sync-status
        runScript('status.js');
        break;

    case 'list':
    case 'sync-list':
    case 'versions':
        // /sync-list
        runScript('pull.js', ['--list']);
        break;

    case 'setup':
    case 'cron':
        // Setup auto-sync
        runScript('setup-cron.js');
        break;

    case 'help':
    case '--help':
    case '-h':
    case undefined:
        showHelp();
        break;

    default:
        console.error(`❌ Unknown command: ${command}`);
        console.log('Run with --help to see available commands.');
        process.exit(1);
}


path: package.json
content: {
  "name": "claw-sync",
  "version": "2.1.0",
  "description": "Secure versioned sync for OpenClaw memory and workspace",
  "main": "index.js",
  "bin": {
    "claw-sync": "./index.js"
  },
  "scripts": {
    "sync": "node index.js sync",
    "restore": "node index.js restore",
    "status": "node index.js status",
    "list": "node index.js list",
    "push": "node scripts/push.js",
    "pull": "node scripts/pull.js",
    "setup": "node scripts/setup-cron.js"
  },
  "keywords": [
    "openclaw",
    "sync",
    "backup",
    "github"
  ],
  "author": "",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/username/claw-sync"
  }
}

path: README.md
content: # Claw Sync

Secure, versioned sync for OpenClaw memory files and custom skills.

## Features

- 🏷️ **Versioned**: Each sync creates a tagged version you can restore
- 💾 **Disaster Recovery**: Local backup created before every restore
- 🔒 **Secure**: Config files NOT synced, URL validation, path protection
- 🖥️ **Cross-platform**: Works on Windows, Mac, Linux

## Quick Start

```bash
/sync              # Push to remote
/restore           # Restore latest
/sync-list         # List versions
/sync-status       # Check status
```

## What Gets Synced

| File | Description |
|------|-------------|
| `MEMORY.md` | Long-term memory |
| `USER.md` | User profile |
| `SOUL.md` | Agent persona |
| `IDENTITY.md` | Agent identity |
| `TOOLS.md` | Tool configurations |
| `AGENTS.md` | Workspace conventions |
| `memory/*.md` | Daily logs |
| `skills/*` | Custom skills |

## NOT Synced (security)

- `openclaw.json` - Contains API keys/tokens
- `.env` - Contains secrets

## Setup

Create `~/.openclaw/.backup.env`:

```bash
BACKUP_REPO=https://github.com/yourusername/your-sync-repo
BACKUP_TOKEN=[REDACTED_SECRET]
```

## All Commands

| Command | Description |
|---------|-------------|
| `/sync` | Push memory and skills to remote |
| `/sync --dry-run` | Preview what would be synced |
| `/restore` | Restore latest version |
| `/restore latest` | Same as above |
| `/restore backup-20260202-1430` | Restore specific version |
| `/restore --force` | Skip confirmation |
| `/sync-list` | List all available versions |
| `/sync-status` | Show config and local backups |

## CLI Usage

You can also run commands directly:

```bash
node index.js sync              # Push
node index.js sync --dry-run    # Preview
node index.js restore           # Restore latest
node index.js restore backup-20260202-1430   # Specific version
node index.js list              # List versions
node index.js status            # Check status
```

## Disaster Recovery

Local backup automatically created before every restore at:
```
~/.openclaw/.local-backup/<timestamp>/
```

## Restore on New Machine

1. Install OpenClaw
2. Clone this skill to `skills/claw-sync/`
3. Create `.backup.env` with repo and token
4. Run `/sync-list` to see versions
5. Run `/restore` to get latest
6. Configure API keys in `openclaw.json` and `.env`
7. Restart OpenClaw

## Security Features

- No config sync (secrets never leave your machine)
- URL validation (only GitHub/GitLab/Bitbucket)
- Path protection (blocks directory traversal)
- Token sanitization (never in error messages)
- Version validation (strict format checking)

## License

MIT


path: scripts/pull.js
content: #!/usr/bin/env node
/**
 * Claw Sync - Pull Script
 * Restores OpenClaw memory/workspace from GitHub
 * 
 * Features:
 * - Restore specific version or latest
 * - Local backup before restore (disaster recovery)
 * - Path traversal protection
 * - Cross-platform
 * 
 * Usage:
 *   node pull.js                     # Restore latest
 *   node pull.js --list              # List available versions
 *   node pull.js --version backup-20260202-1430   # Restore specific version
 *   node pull.js --force             # Skip confirmation
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const os = require('os');
const readline = require('readline');

// Config
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
const RESTORE_DIR = path.join(OPENCLAW_DIR, '.sync-restore');
const LOCAL_BACKUP_DIR = path.join(OPENCLAW_DIR, '.local-backup');

// Security: Allowed git hosts
const ALLOWED_HOSTS = ['github.com', 'gitlab.com', 'bitbucket.org'];

// Parse args
const args = process.argv.slice(2);
const LIST_MODE = args.includes('--list');
const FORCE_MODE = args.includes('--force');
const VERSION_INDEX = args.indexOf('--version');
const RAW_VERSION = VERSION_INDEX !== -1 ? args[VERSION_INDEX + 1] : null;

// Security: Validate version tag format (prevent command injection)
function isValidTagName(tag) {
  if (!tag) return true; // null is ok (means latest)
  // Only allow: backup-YYYYMMDD-HHMM format
  return /^backup-\d{8}-\d{4}$/.test(tag);
}

const TARGET_VERSION = RAW_VERSION;
if (RAW_VERSION && !isValidTagName(RAW_VERSION)) {
  console.error('❌ Invalid version format. Expected: backup-YYYYMMDD-HHMM');
  console.error('   Example: backup-20260202-1430');
  process.exit(1);
}

// Load env
function loadEnv() {
  const envFile = path.join(OPENCLAW_DIR, '.backup.env');
  if (fs.existsSync(envFile)) {
    const content = fs.readFileSync(envFile, 'utf8');
    content.split('\n').forEach(line => {
      const match = line.match(/^([A-Z_]+)=(.+)$/);
      if (match && !process.env[match[1]]) {
        process.env[match[1]] = match[2].trim();
      }
    });
  }
}

// Security: Validate repository URL
function validateRepoUrl(url) {
  try {
    const parsed = new URL(url);
    if (parsed.protocol !== 'https:') {
      throw new Error('Repository URL must use HTTPS');
    }
    if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
      throw new Error(`Repository must be on: ${ALLOWED_HOSTS.join(', ')}`);
    }
    return true;
  } catch (err) {
    console.error(`❌ Invalid repository URL: ${err.message}`);
    process.exit(1);
  }
}

// Check configuration
function checkConfig() {
  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];

  if (!repo || !token) {
    console.error('❌ Sync not configured!');
    console.error('');
    console.error('Create ~/.openclaw/.backup.env with:');
    console.error('  BACKUP_REPO=https://github.com/username/repo');
    console.error('  BACKUP_TOKEN=ghp_your_token_here');
    process.exit(1);
  }

  if (token.includes('xxxx') || token.includes('YOUR_')) {
    console.error('❌ Please replace the placeholder token with your real token');
    process.exit(1);
  }

  validateRepoUrl(repo);
}

// Security: Execute git command without exposing token in errors
function safeExec(cmd, options = {}) {
  try {
    return execSync(cmd, { ...options, encoding: 'utf8' });
  } catch (err) {
    let msg = err.message || 'Unknown error';
    const [REDACTED_SECRET];
    if (token) {
      msg = msg.replace(new RegExp(token, 'g'), '***TOKEN***');
    }
    throw new Error(msg);
  }
}

// Security: Validate filename (prevent path traversal)
function isValidFilename(filename) {
  // Reject if contains path traversal or absolute paths
  if (filename.includes('..') ||
    filename.startsWith('/') ||
    filename.startsWith('\\') ||
    /^[a-zA-Z]:/.test(filename)) {
    return false;
  }
  // Only allow alphanumeric, dash, underscore, dot
  return /^[\w\-\.]+$/.test(filename);
}

// Create local backup before restore (disaster recovery)
function createLocalBackup() {
  console.log('💾 Creating local backup before restore...');

  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const backupPath = path.join(LOCAL_BACKUP_DIR, timestamp);

  fs.mkdirSync(backupPath, { recursive: true });

  // Backup current workspace
  if (fs.existsSync(WORKSPACE_DIR)) {
    const files = ['MEMORY.md', 'USER.md', 'SOUL.md', 'IDENTITY.md', 'TOOLS.md', 'AGENTS.md'];
    for (const file of files) {
      const src = path.join(WORKSPACE_DIR, file);
      if (fs.existsSync(src)) {
        fs.copyFileSync(src, path.join(backupPath, file));
      }
    }

    // Backup memory dir
    const memoryDir = path.join(WORKSPACE_DIR, 'memory');
    if (fs.existsSync(memoryDir)) {
      const destMemory = path.join(backupPath, 'memory');
      fs.mkdirSync(destMemory, { recursive: true });
      for (const f of fs.readdirSync(memoryDir)) {
        if (f.endsWith('.md')) {
          fs.copyFileSync(path.join(memoryDir, f), path.join(destMemory, f));
        }
      }
    }
  }

  console.log(`✅ Local backup saved: ${backupPath}`);
  return backupPath;
}

// List available versions
function listVersions() {
  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];
  const repoUrl = repo.replace('https://', `https://${token}@`);

  console.log('📋 Fetching available versions...\n');

  try {
    // Clone with all tags
    if (fs.existsSync(RESTORE_DIR)) {
      fs.rmSync(RESTORE_DIR, { recursive: true });
    }

    safeExec(`git clone --bare ${repoUrl} ${RESTORE_DIR}`, { stdio: 'pipe' });
    process.chdir(RESTORE_DIR);

    const tagsOutput = safeExec('git tag -l --sort=-creatordate', { stdio: 'pipe' });
    const tags = tagsOutput.trim().split('\n').filter(t => t.startsWith('backup-'));

    if (tags.length === 0) {
      console.log('No backup versions found.');
      console.log('Run `node push.js` to create your first backup.');
    } else {
      console.log('Available versions:\n');
      tags.forEach((tag, i) => {
        const marker = i === 0 ? ' (latest)' : '';
        console.log(`  🏷️  ${tag}${marker}`);
      });
      console.log('');
      console.log('To restore a specific version:');
      console.log(`  node pull.js --version ${tags[0]}`);
    }

  } catch (err) {
    console.error('❌ Failed to list versions:', err.message);
  } finally {
    process.chdir(os.homedir());
    if (fs.existsSync(RESTORE_DIR)) {
      fs.rmSync(RESTORE_DIR, { recursive: true });
    }
  }
}

// Prompt for confirmation
async function confirm(message) {
  if (FORCE_MODE) return true;

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  return new Promise(resolve => {
    rl.question(`${message} (y/N): `, answer => {
      rl.close();
      resolve(answer.toLowerCase() === 'y');
    });
  });
}

// Safely copy file with path validation
function safeCopyFile(src, dest) {
  const filename = path.basename(src);
  if (!isValidFilename(filename)) {
    console.log(`⚠️  Skipping unsafe filename: ${filename}`);
    return false;
  }

  if (!fs.existsSync(src)) return false;
  fs.mkdirSync(path.dirname(dest), { recursive: true });
  fs.copyFileSync(src, dest);
  console.log(`✅ Restored: ${path.basename(dest)}`);
  return true;
}

// Main
async function main() {
  loadEnv();
  checkConfig();

  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];

  // List mode
  if (LIST_MODE) {
    listVersions();
    return;
  }

  console.log('📥 Claw Sync - Restore\n');
  console.log(`Repository: ${repo}`);
  if (TARGET_VERSION) {
    console.log(`Version: ${TARGET_VERSION}`);
  } else {
    console.log('Version: latest');
  }
  console.log('');

  // Confirm restore
  const confirmed = await confirm('⚠️  This will overwrite local files. Continue?');
  if (!confirmed) {
    console.log('Restore cancelled.');
    return;
  }

  // Create local backup first (disaster recovery)
  const localBackup = createLocalBackup();

  // Clean restore area
  if (fs.existsSync(RESTORE_DIR)) {
    fs.rmSync(RESTORE_DIR, { recursive: true });
  }

  // Clone repo
  const repoUrl = repo.replace('https://', `https://${token}@`);

  try {
    console.log('\n📡 Cloning repository...');

    if (TARGET_VERSION) {
      // Clone specific tag
      safeExec(`git clone --depth 1 --branch ${TARGET_VERSION} ${repoUrl} ${RESTORE_DIR}`, { stdio: 'pipe' });
    } else {
      // Clone latest
      safeExec(`git clone --depth 1 ${repoUrl} ${RESTORE_DIR}`, { stdio: 'pipe' });
    }
  } catch (err) {
    console.error('❌ Clone failed:', err.message);
    console.log(`\n💡 Your local backup is safe at: ${localBackup}`);
    process.exit(1);
  }

  // Show metadata
  const metadataPath = path.join(RESTORE_DIR, 'SYNC_METADATA.json');
  if (fs.existsSync(metadataPath)) {
    const meta = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
    console.log(`\n📅 Backup from: ${meta.timestamp}`);
    console.log(`🏷️  Version: ${meta.tag || 'unknown'}`);
    console.log(`🖥️  Source: ${meta.hostname}`);
  }

  // Restore workspace files
  const backupWorkspace = path.join(RESTORE_DIR, 'workspace');
  if (fs.existsSync(backupWorkspace)) {
    console.log('\n📁 Restoring workspace...');

    const files = ['MEMORY.md', 'USER.md', 'SOUL.md', 'IDENTITY.md', 'TOOLS.md', 'AGENTS.md'];
    for (const file of files) {
      safeCopyFile(
        path.join(backupWorkspace, file),
        path.join(WORKSPACE_DIR, file)
      );
    }

    // Restore memory logs
    const backupMemory = path.join(backupWorkspace, 'memory');
    if (fs.existsSync(backupMemory)) {
      console.log('\n📅 Restoring daily logs...');
      fs.mkdirSync(path.join(WORKSPACE_DIR, 'memory'), { recursive: true });
      for (const file of fs.readdirSync(backupMemory)) {
        if (isValidFilename(file)) {
          safeCopyFile(
            path.join(backupMemory, file),
            path.join(WORKSPACE_DIR, 'memory', file)
          );
        }
      }
    }

    // Restore skills
    const backupSkills = path.join(backupWorkspace, 'skills');
    if (fs.existsSync(backupSkills)) {
      console.log('\n🔧 Restoring skills...');
      fs.mkdirSync(path.join(WORKSPACE_DIR, 'skills'), { recursive: true });
      for (const skill of fs.readdirSync(backupSkills)) {
        if (!isValidFilename(skill)) {
          console.log(`⚠️  Skipping unsafe skill name: ${skill}`);
          continue;
        }
        const src = path.join(backupSkills, skill);
        const dest = path.join(WORKSPACE_DIR, 'skills', skill);
        if (fs.statSync(src).isDirectory()) {
          if (fs.existsSync(dest)) {
            fs.rmSync(dest, { recursive: true });
          }
          fs.cpSync(src, dest, { recursive: true });
          console.log(`✅ Restored: skills/${skill}`);
        }
      }
    }
  }

  // Cleanup
  fs.rmSync(RESTORE_DIR, { recursive: true });

  console.log('\n✅ Restore complete!');
  console.log('');
  console.log('📝 Notes:');
  console.log(`   - Local backup saved at: ${localBackup}`);
  console.log('   - Config files (openclaw.json, .env) were NOT restored');
  console.log('   - Restart OpenClaw if running');
}

main().catch(err => {
  console.error('❌ Error:', err.message);
  process.exit(1);
});


path: scripts/push.js
content: #!/usr/bin/env node
/**
 * Claw Sync - Push Script
 * Pushes OpenClaw memory/workspace to GitHub with versioned backups
 * 
 * Features:
 * - Versioned backups with git tags (can restore any version)
 * - Security: No config files, validated URLs, sanitized errors
 * - Cross-platform: Works on Windows, Mac, Linux
 * 
 * Usage:
 *   node push.js              # Normal push
 *   node push.js --dry-run    # Show what would be pushed
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const os = require('os');

// Config
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
const STAGING_DIR = path.join(OPENCLAW_DIR, '.sync-staging');

// Security: Allowed git hosts
const ALLOWED_HOSTS = ['github.com', 'gitlab.com', 'bitbucket.org'];

// Parse args
const args = process.argv.slice(2);
const DRY_RUN = args.includes('--dry-run');

// Load env from .backup.env
function loadEnv() {
  const envFile = path.join(OPENCLAW_DIR, '.backup.env');
  if (fs.existsSync(envFile)) {
    const content = fs.readFileSync(envFile, 'utf8');
    content.split('\n').forEach(line => {
      const match = line.match(/^([A-Z_]+)=(.+)$/);
      if (match && !process.env[match[1]]) {
        process.env[match[1]] = match[2].trim();
      }
    });
  }
}

// Security: Validate repository URL
function validateRepoUrl(url) {
  try {
    const parsed = new URL(url);
    if (parsed.protocol !== 'https:') {
      throw new Error('Repository URL must use HTTPS');
    }
    if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
      throw new Error(`Repository must be on: ${ALLOWED_HOSTS.join(', ')}`);
    }
    return true;
  } catch (err) {
    console.error(`❌ Invalid repository URL: ${err.message}`);
    process.exit(1);
  }
}

// Security: Validate token format
function validateToken(token) {
  // GitHub tokens start with ghp_, gho_, ghu_, ghs_, ghr_
  // GitLab tokens are typically glpat-
  const validPrefixes = ['ghp_', 'gho_', 'ghu_', 'ghs_', 'ghr_', 'glpat-'];
  if (!validPrefixes.some(p => token.startsWith(p))) {
    console.warn('⚠️  Token format not recognized. Proceeding anyway...');
  }
  if (token.length < 20) {
    console.error('❌ Token appears too short');
    process.exit(1);
  }
}

// Check configuration
function checkConfig() {
  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];

  if (!repo || !token) {
    console.error('❌ Sync not configured!');
    console.error('');
    console.error('Create ~/.openclaw/.backup.env with:');
    console.error('  BACKUP_REPO=https://github.com/username/repo');
    console.error('  BACKUP_TOKEN=ghp_your_token_here');
    process.exit(1);
  }

  if (token.includes('xxxx') || token.includes('YOUR_')) {
    console.error('❌ Please replace the placeholder token with your real token');
    process.exit(1);
  }

  validateRepoUrl(repo);
  validateToken(token);
}

// Security: Sanitize string for git commit message
function sanitizeForGit(str) {
  return str.replace(/[`$"\\]/g, '');
}

// Copy file with optional transform
function copyFile(src, dest, transform = null) {
  if (!fs.existsSync(src)) {
    if (!DRY_RUN) console.log(`⏭️  Skipping (not found): ${path.basename(src)}`);
    return false;
  }

  if (DRY_RUN) {
    console.log(`  📄 ${path.basename(src)}`);
    return true;
  }

  fs.mkdirSync(path.dirname(dest), { recursive: true });
  let content = fs.readFileSync(src, 'utf8');
  if (transform) content = transform(content);
  fs.writeFileSync(dest, content);
  console.log(`✅ ${path.basename(src)}`);
  return true;
}

// Generate backup tag name
function generateTagName() {
  const now = new Date();
  const y = now.getFullYear();
  const m = String(now.getMonth() + 1).padStart(2, '0');
  const d = String(now.getDate()).padStart(2, '0');
  const h = String(now.getHours()).padStart(2, '0');
  const min = String(now.getMinutes()).padStart(2, '0');
  return `backup-${y}${m}${d}-${h}${min}`;
}

// Security: Execute git command without exposing token in errors
function safeExec(cmd, options = {}) {
  try {
    return execSync(cmd, { ...options, encoding: 'utf8' });
  } catch (err) {
    // Sanitize error message to remove any tokens
    let msg = err.message || 'Unknown error';
    const [REDACTED_SECRET];
    if (token) {
      msg = msg.replace(new RegExp(token, 'g'), '***TOKEN***');
    }
    throw new Error(msg);
  }
}

// Main
async function main() {
  loadEnv();
  checkConfig();

  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];
  const timestamp = new Date().toISOString();
  const tagName = generateTagName();

  if (DRY_RUN) {
    console.log('🔍 DRY RUN - showing what would be synced:\n');
  } else {
    console.log('🔧 Preparing sync...\n');
  }

  // Clean staging area
  if (!DRY_RUN) {
    if (fs.existsSync(STAGING_DIR)) {
      fs.rmSync(STAGING_DIR, { recursive: true });
    }
    fs.mkdirSync(STAGING_DIR, { recursive: true });
  }

  // Memory files
  console.log('📝 Memory files:');
  const memoryFiles = ['MEMORY.md', 'USER.md', 'SOUL.md', 'IDENTITY.md', 'TOOLS.md', 'AGENTS.md'];
  for (const file of memoryFiles) {
    copyFile(
      path.join(WORKSPACE_DIR, file),
      path.join(STAGING_DIR, 'workspace', file)
    );
  }

  // Daily logs
  const memoryDir = path.join(WORKSPACE_DIR, 'memory');
  if (fs.existsSync(memoryDir)) {
    console.log('\n📅 Daily logs:');
    if (!DRY_RUN) fs.mkdirSync(path.join(STAGING_DIR, 'workspace/memory'), { recursive: true });
    const logs = fs.readdirSync(memoryDir).filter(f => f.endsWith('.md'));
    for (const file of logs) {
      copyFile(
        path.join(memoryDir, file),
        path.join(STAGING_DIR, 'workspace/memory', file)
      );
    }
  }

  // Custom skills
  const skillsDir = path.join(WORKSPACE_DIR, 'skills');
  if (fs.existsSync(skillsDir)) {
    console.log('\n🔧 Skills:');
    if (!DRY_RUN) fs.mkdirSync(path.join(STAGING_DIR, 'workspace/skills'), { recursive: true });
    for (const skill of fs.readdirSync(skillsDir)) {
      const skillPath = path.join(skillsDir, skill);
      const destPath = path.join(STAGING_DIR, 'workspace/skills', skill);
      if (fs.statSync(skillPath).isDirectory()) {
        if (DRY_RUN) {
          console.log(`  📦 skills/${skill}`);
        } else {
          fs.cpSync(skillPath, destPath, { recursive: true });
          console.log(`✅ skills/${skill}`);
        }
      }
    }
  }

  if (DRY_RUN) {
    console.log('\n✅ Dry run complete. No changes made.');
    return;
  }

  // Write metadata
  fs.writeFileSync(
    path.join(STAGING_DIR, 'SYNC_METADATA.json'),
    JSON.stringify({
      timestamp,
      tag: tagName,
      hostname: os.hostname(),
      platform: os.platform(),
      version: '2.1.0'
    }, null, 2)
  );

  // Git operations
  console.log('\n📤 Pushing to remote...');

  const repoUrl = repo.replace('https://', `https://${token}@`);

  try {
    process.chdir(STAGING_DIR);

    safeExec('git init', { stdio: 'ignore' });
    safeExec('git config user.email "[REDACTED_SECRET]"', { stdio: 'ignore' });
    safeExec('git config user.name "Claw Sync"', { stdio: 'ignore' });
    safeExec('git add -A', { stdio: 'ignore' });

    const commitMsg = sanitizeForGit(`Sync: ${timestamp}`);
    safeExec(`git commit -m "${commitMsg}"`, { stdio: 'ignore' });

    // Push to main
    safeExec(`git push ${repoUrl} HEAD:main --force`, { stdio: 'pipe' });

    // Create version tag
    safeExec(`git tag ${tagName}`, { stdio: 'ignore' });
    safeExec(`git push ${repoUrl} ${tagName}`, { stdio: 'pipe' });

    console.log('\n✅ Sync complete!');
    console.log(`🏷️  Version: ${tagName}`);
    console.log(`🕐 ${timestamp}`);
    console.log(`📁 ${repo}`);

  } catch (err) {
    console.error('\n❌ Sync failed:', err.message);
    process.exit(1);
  } finally {
    // Cleanup
    process.chdir(os.homedir());
    if (fs.existsSync(STAGING_DIR)) {
      fs.rmSync(STAGING_DIR, { recursive: true });
    }
  }
}

main().catch(err => {
  console.error('❌ Error:', err.message);
  process.exit(1);
});


path: scripts/setup-cron.js
content: #!/usr/bin/env node
/**
 * Setup auto-backup cron job
 * Runs backup every 12 hours
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const OPENCLAW_DIR = path.join(require('os').homedir(), '.openclaw');
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
const SKILL_DIR = path.join(WORKSPACE_DIR, 'skills/claw-sync');

async function main() {
  console.log('🔧 Setting up auto-backup cron job...\n');

  // Check if skill exists
  if (!fs.existsSync(SKILL_DIR)) {
    console.error('❌ Backup skill not found at:', SKILL_DIR);
    process.exit(1);
  }

  // Create the cron job payload
  const job = {
    name: "claw-sync",
    schedule: {
      kind: "every",
      everyMs: 12 * 60 * 60 * 1000  // 12 hours
    },
    payload: {
      kind: "agentTurn",
      message: "exec: node skills/claw-sync/scripts/push.js",
      timeoutSeconds: 300
    },
    sessionTarget: "isolated",
    enabled: true
  };

  console.log('Creating cron job...');
  console.log('  Name: claw-sync');
  console.log('  Schedule: Every 12 hours');
  console.log('  Command: node skills/claw-sync/scripts/push.js');
  console.log('');

  // Use the cron tool via gateway
  try {
    // We'll output the config for manual addition or use a direct approach
    const cronConfigPath = path.join(OPENCLAW_DIR, 'cron', 'backup.json');

    // Ensure cron directory exists
    fs.mkdirSync(path.join(OPENCLAW_DIR, 'cron'), { recursive: true });

    // Write job config
    fs.writeFileSync(cronConfigPath, JSON.stringify(job, null, 2));

    console.log('✅ Cron config saved to:', cronConfigPath);
    console.log('');
    console.log('To activate via OpenClaw CLI:');
    console.log(`  openclaw cron add ${cronConfigPath}`);
    console.log('');
    console.log('Or add manually to your crontab:');
    console.log(`  0 */12 * * * cd ${WORKSPACE_DIR} && node skills/claw-sync/scripts/push.js >> /tmp/claw-sync.log 2>&1`);

  } catch (err) {
    console.error('❌ Setup failed:', err.message);
    process.exit(1);
  }
}

main().catch(console.error);


path: scripts/status.js
content: #!/usr/bin/env node
/**
 * Claw Sync - Status Script
 * Shows sync status and local backup info
 */

const fs = require('fs');
const path = require('path');
const os = require('os');

const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
const LOCAL_BACKUP_DIR = path.join(OPENCLAW_DIR, '.local-backup');

function loadEnv() {
  const envFile = path.join(OPENCLAW_DIR, '.backup.env');
  if (fs.existsSync(envFile)) {
    const content = fs.readFileSync(envFile, 'utf8');
    content.split('\n').forEach(line => {
      const match = line.match(/^([A-Z_]+)=(.+)$/);
      if (match && !process.env[match[1]]) {
        process.env[match[1]] = match[2].trim();
      }
    });
  }
}

function isConfigured() {
  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];
  return repo && token &&
    !token.includes('xxxx') &&
    !token.includes('YOUR_');
}

function main() {
  loadEnv();

  const repo = process.env.BACKUP_REPO;
  const configured = isConfigured();

  console.log('📊 Claw Sync Status\n');

  // Configuration status
  if (!configured) {
    console.log('⚠️  Not configured');
    console.log('');
    console.log('Create ~/.openclaw/.backup.env with:');
    console.log('  BACKUP_REPO=https://github.com/username/repo');
    console.log('  BACKUP_TOKEN=ghp_your_token');
    console.log('');
    return;
  }

  console.log(`✅ Configured`);
  console.log(`📁 Repository: ${repo}`);
  console.log('');

  // Files to sync
  console.log('📝 Files to sync:');

  const checks = [
    ['MEMORY.md', path.join(WORKSPACE_DIR, 'MEMORY.md')],
    ['USER.md', path.join(WORKSPACE_DIR, 'USER.md')],
    ['SOUL.md', path.join(WORKSPACE_DIR, 'SOUL.md')],
    ['IDENTITY.md', path.join(WORKSPACE_DIR, 'IDENTITY.md')],
    ['TOOLS.md', path.join(WORKSPACE_DIR, 'TOOLS.md')],
    ['AGENTS.md', path.join(WORKSPACE_DIR, 'AGENTS.md')],
  ];

  for (const [name, filePath] of checks) {
    const exists = fs.existsSync(filePath);
    const size = exists ? fs.statSync(filePath).size : 0;
    const sizeStr = exists ? `${(size / 1024).toFixed(1)}KB` : 'missing';
    console.log(`  ${exists ? '✅' : '⏭️ '} ${name} (${sizeStr})`);
  }

  // Count memory files
  const memoryDir = path.join(WORKSPACE_DIR, 'memory');
  if (fs.existsSync(memoryDir)) {
    const files = fs.readdirSync(memoryDir).filter(f => f.endsWith('.md'));
    console.log(`  ✅ memory/*.md (${files.length} files)`);
  }

  // Count skills
  const skillsDir = path.join(WORKSPACE_DIR, 'skills');
  if (fs.existsSync(skillsDir)) {
    const skills = fs.readdirSync(skillsDir).filter(f =>
      fs.statSync(path.join(skillsDir, f)).isDirectory()
    );
    console.log(`  ✅ skills/ (${skills.length} skills)`);
  }

  console.log('');
  console.log('🔒 NOT synced (security): openclaw.json, .env');

  // Local backups
  console.log('\n💾 Local backups (disaster recovery):');
  if (fs.existsSync(LOCAL_BACKUP_DIR)) {
    const backups = fs.readdirSync(LOCAL_BACKUP_DIR)
      .filter(f => fs.statSync(path.join(LOCAL_BACKUP_DIR, f)).isDirectory())
      .sort()
      .reverse()
      .slice(0, 5);

    if (backups.length > 0) {
      backups.forEach((b, i) => {
        const marker = i === 0 ? ' (latest)' : '';
        console.log(`  📦 ${b}${marker}`);
      });
    } else {
      console.log('  No local backups yet');
    }
  } else {
    console.log('  No local backups yet');
  }

  // Commands
  console.log('\n💡 Commands:');
  console.log('  node skills/claw-sync/scripts/push.js           # Push to remote');
  console.log('  node skills/claw-sync/scripts/push.js --dry-run # Preview');
  console.log('  node skills/claw-sync/scripts/pull.js --list    # List versions');
  console.log('  node skills/claw-sync/scripts/pull.js           # Restore latest');
}

main();


path: claw-sync/config.example.env
content: # OpenClaw Backup Configuration
# Copy this file to ~/.openclaw/.backup.env and fill in your values

# Your private GitHub repository URL
BACKUP_REPO=https://github.com/username/openclaw-backup

# GitHub Fine-Grained Personal Access Token
# Required permissions: Contents (Read and Write)
# Generate at: https://github.com/settings/tokens?type=beta
BACKUP_TOKEN=[REDACTED_SECRET]


path: claw-sync/index.js
content: #!/usr/bin/env node
/**
 * Claw Sync - Main Entry Point
 * Handles all slash commands: /sync, /restore, /sync-status, /sync-list
 * 
 * Usage:
 *   node index.js sync [--dry-run]
 *   node index.js restore [latest|<version>] [--force]
 *   node index.js status
 *   node index.js list
 */

const { spawn } = require('child_process');
const path = require('path');

const SCRIPTS_DIR = path.join(__dirname, 'scripts');

// Parse command and args
const args = process.argv.slice(2);
const command = args[0]?.toLowerCase();
const commandArgs = args.slice(1);

// Help text
function showHelp() {
    console.log(`
🔄 Claw Sync - Commands

  /sync              Push memory and skills to remote
  /sync --dry-run    Preview what would be synced

  /restore           Restore latest version
  /restore <version> Restore specific version (e.g., backup-20260202-1430)
  /restore --force   Skip confirmation prompt

  /sync-status       Show sync configuration and local backups
  /sync-list         List all available backup versions

Examples:
  node index.js sync
  node index.js sync --dry-run
  node index.js restore
  node index.js restore backup-20260202-1430
  node index.js restore latest --force
  node index.js status
  node index.js list
`);
}

// Run a script
function runScript(scriptName, scriptArgs = []) {
    const scriptPath = path.join(SCRIPTS_DIR, scriptName);

    const child = spawn('node', [scriptPath, ...scriptArgs], {
        stdio: 'inherit',
        cwd: __dirname
    });

    child.on('error', (err) => {
        console.error(`❌ Failed to run ${scriptName}:`, err.message);
        process.exit(1);
    });

    child.on('exit', (code) => {
        process.exit(code || 0);
    });
}

// Main command handler
switch (command) {
    case 'sync':
    case 'push':
    case 'backup':
        // /sync or /backup
        runScript('push.js', commandArgs);
        break;

    case 'restore':
    case 'pull':
        // /restore [version] [--force]
        const restoreArgs = [];

        for (let i = 0; i < commandArgs.length; i++) {
            const arg = commandArgs[i];

            if (arg === '--force' || arg === '-f') {
                restoreArgs.push('--force');
            } else if (arg === 'latest') {
                // 'latest' means no version specified (default behavior)
                continue;
            } else if (arg.startsWith('backup-')) {
                // Specific version
                restoreArgs.push('--version', arg);
            } else if (!arg.startsWith('-')) {
                // Assume it's a version tag
                restoreArgs.push('--version', arg);
            }
        }

        runScript('pull.js', restoreArgs);
        break;

    case 'status':
    case 'sync-status':
        // /sync-status
        runScript('status.js');
        break;

    case 'list':
    case 'sync-list':
    case 'versions':
        // /sync-list
        runScript('pull.js', ['--list']);
        break;

    case 'setup':
    case 'cron':
        // Setup auto-sync
        runScript('setup-cron.js');
        break;

    case 'help':
    case '--help':
    case '-h':
    case undefined:
        showHelp();
        break;

    default:
        console.error(`❌ Unknown command: ${command}`);
        console.log('Run with --help to see available commands.');
        process.exit(1);
}


path: claw-sync/package.json
content: {
  "name": "claw-sync",
  "version": "2.0.2",
  "description": "Secure versioned sync for OpenClaw memory and workspace",
  "main": "index.js",
  "bin": {
    "claw-sync": "./index.js"
  },
  "scripts": {
    "sync": "node index.js sync",
    "restore": "node index.js restore",
    "status": "node index.js status",
    "list": "node index.js list",
    "push": "node scripts/push.js",
    "pull": "node scripts/pull.js",
    "setup": "node scripts/setup-cron.js"
  },
  "keywords": [
    "openclaw",
    "sync",
    "backup",
    "github"
  ],
  "author": "",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/arakichanxd/Claw-Sync"
  }
}

path: claw-sync/README.md
content: # Claw Sync

[![GitHub](https://img.shields.io/badge/GitHub-arakichanxd%2FClaw--Sync-blue?logo=github)](https://github.com/arakichanxd/Claw-Sync)
[![Version](https://img.shields.io/badge/version-2.0.2-green)](https://github.com/arakichanxd/Claw-Sync)
[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

Secure, versioned sync for OpenClaw memory files and custom skills.

## Features

- 🏷️ **Versioned**: Each sync creates a tagged version you can restore
- 💾 **Disaster Recovery**: Local backup created before every restore
- 🔒 **Secure**: Config files NOT synced, URL validation, path protection
- 🖥️ **Cross-platform**: Works on Windows, Mac, Linux

## File References

| File | Description | Link |
|------|-------------|------|
| `SKILL.md` | AI agent instructions | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/SKILL.md) |
| `README.md` | User documentation | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/README.md) |
| `index.js` | Command router | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/index.js) |
| `package.json` | NPM config | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/package.json) |
| `config.example.env` | Example config | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/config.example.env) |
| `scripts/push.js` | Sync to remote | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/scripts/push.js) |
| `scripts/pull.js` | Restore from remote | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/scripts/pull.js) |
| `scripts/status.js` | Show status | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/scripts/status.js) |
| `scripts/setup-cron.js` | Auto-sync setup | [View](https://github.com/arakichanxd/Claw-Sync/blob/main/scripts/setup-cron.js) |

## Installation

### From GitHub
```bash
cd ~/.openclaw/workspace/skills
git clone https://github.com/arakichanxd/Claw-Sync.git claw-sync
```

### From ClawHub
Search for "claw-sync" in ClawHub and install.

## Quick Start

```bash
/sync              # Push to remote
/restore           # Restore latest
/sync-list         # List versions
/sync-status       # Check status
```

## What Gets Synced

| File | Description |
|------|-------------|
| `MEMORY.md` | Long-term memory |
| `USER.md` | User profile |
| `SOUL.md` | Agent persona |
| `IDENTITY.md` | Agent identity |
| `TOOLS.md` | Tool configurations |
| `AGENTS.md` | Workspace conventions |
| `memory/*.md` | Daily logs |
| `skills/*` | Custom skills |

## NOT Synced (security)

- `openclaw.json` - Contains API keys/tokens
- `.env` - Contains secrets

## Setup

Create `~/.openclaw/.backup.env`:

```bash
BACKUP_REPO=https://github.com/yourusername/your-sync-repo
BACKUP_TOKEN=[REDACTED_SECRET]
```

## All Commands

| Command | Description |
|---------|-------------|
| `/sync` | Push memory and skills to remote |
| `/sync --dry-run` | Preview what would be synced |
| `/restore` | Restore latest version |
| `/restore latest` | Same as above |
| `/restore backup-20260202-1430` | Restore specific version |
| `/restore --force` | Skip confirmation |
| `/sync-list` | List all available versions |
| `/sync-status` | Show config and local backups |

## CLI Usage

```bash
node index.js sync              # Push
node index.js sync --dry-run    # Preview
node index.js restore           # Restore latest
node index.js restore backup-20260202-1430   # Specific version
node index.js list              # List versions
node index.js status            # Check status
```

## Disaster Recovery

Local backup automatically created before every restore at:
```
~/.openclaw/.local-backup/<timestamp>/
```

## Security Features

- No config sync (secrets never leave your machine)
- URL validation (only GitHub/GitLab/Bitbucket)
- Path protection (blocks directory traversal)
- Token sanitization (never in error messages)
- Version validation (strict format checking)

## Contributing

GitHub: https://github.com/arakichanxd/Claw-Sync

## License

MIT


path: claw-sync/scripts/pull.js
content: #!/usr/bin/env node
/**
 * Claw Sync - Pull Script
 * Restores OpenClaw memory/workspace from GitHub
 * 
 * Features:
 * - Restore specific version or latest
 * - Local backup before restore (disaster recovery)
 * - Path traversal protection
 * - Cross-platform
 * 
 * Usage:
 *   node pull.js                     # Restore latest
 *   node pull.js --list              # List available versions
 *   node pull.js --version backup-20260202-1430   # Restore specific version
 *   node pull.js --force             # Skip confirmation
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const os = require('os');
const readline = require('readline');

// Config
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
const RESTORE_DIR = path.join(OPENCLAW_DIR, '.sync-restore');
const LOCAL_BACKUP_DIR = path.join(OPENCLAW_DIR, '.local-backup');

// Security: Allowed git hosts
const ALLOWED_HOSTS = ['github.com', 'gitlab.com', 'bitbucket.org'];

// Parse args
const args = process.argv.slice(2);
const LIST_MODE = args.includes('--list');
const FORCE_MODE = args.includes('--force');
const VERSION_INDEX = args.indexOf('--version');
const RAW_VERSION = VERSION_INDEX !== -1 ? args[VERSION_INDEX + 1] : null;

// Security: Validate version tag format (prevent command injection)
function isValidTagName(tag) {
  if (!tag) return true; // null is ok (means latest)
  // Only allow: backup-YYYYMMDD-HHMM format
  return /^backup-\d{8}-\d{4}$/.test(tag);
}

const TARGET_VERSION = RAW_VERSION;
if (RAW_VERSION && !isValidTagName(RAW_VERSION)) {
  console.error('❌ Invalid version format. Expected: backup-YYYYMMDD-HHMM');
  console.error('   Example: backup-20260202-1430');
  process.exit(1);
}

// Load env
function loadEnv() {
  const envFile = path.join(OPENCLAW_DIR, '.backup.env');
  if (fs.existsSync(envFile)) {
    const content = fs.readFileSync(envFile, 'utf8');
    content.split('\n').forEach(line => {
      const match = line.match(/^([A-Z_]+)=(.+)$/);
      if (match && !process.env[match[1]]) {
        process.env[match[1]] = match[2].trim();
      }
    });
  }
}

// Security: Validate repository URL
function validateRepoUrl(url) {
  try {
    const parsed = new URL(url);
    if (parsed.protocol !== 'https:') {
      throw new Error('Repository URL must use HTTPS');
    }
    if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
      throw new Error(`Repository must be on: ${ALLOWED_HOSTS.join(', ')}`);
    }
    return true;
  } catch (err) {
    console.error(`❌ Invalid repository URL: ${err.message}`);
    process.exit(1);
  }
}

// Check configuration
function checkConfig() {
  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];

  if (!repo || !token) {
    console.error('❌ Sync not configured!');
    console.error('');
    console.error('Create ~/.openclaw/.backup.env with:');
    console.error('  BACKUP_REPO=https://github.com/username/repo');
    console.error('  BACKUP_TOKEN=ghp_your_token_here');
    process.exit(1);
  }

  if (token.includes('xxxx') || token.includes('YOUR_')) {
    console.error('❌ Please replace the placeholder token with your real token');
    process.exit(1);
  }

  validateRepoUrl(repo);
}

// Security: Execute git command without exposing token in errors
function safeExec(cmd, options = {}) {
  try {
    return execSync(cmd, { ...options, encoding: 'utf8' });
  } catch (err) {
    let msg = err.message || 'Unknown error';
    const [REDACTED_SECRET];
    if (token) {
      msg = msg.replace(new RegExp(token, 'g'), '***TOKEN***');
    }
    throw new Error(msg);
  }
}

// Security: Validate filename (prevent path traversal)
function isValidFilename(filename) {
  // Reject if contains path traversal or absolute paths
  if (filename.includes('..') ||
    filename.startsWith('/') ||
    filename.startsWith('\\') ||
    /^[a-zA-Z]:/.test(filename)) {
    return false;
  }
  // Only allow alphanumeric, dash, underscore, dot
  return /^[\w\-\.]+$/.test(filename);
}

// Create local backup before restore (disaster recovery)
function createLocalBackup() {
  console.log('💾 Creating local backup before restore...');

  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const backupPath = path.join(LOCAL_BACKUP_DIR, timestamp);

  fs.mkdirSync(backupPath, { recursive: true });

  // Backup current workspace
  if (fs.existsSync(WORKSPACE_DIR)) {
    const files = ['MEMORY.md', 'USER.md', 'SOUL.md', 'IDENTITY.md', 'TOOLS.md', 'AGENTS.md'];
    for (const file of files) {
      const src = path.join(WORKSPACE_DIR, file);
      if (fs.existsSync(src)) {
        fs.copyFileSync(src, path.join(backupPath, file));
      }
    }

    // Backup memory dir
    const memoryDir = path.join(WORKSPACE_DIR, 'memory');
    if (fs.existsSync(memoryDir)) {
      const destMemory = path.join(backupPath, 'memory');
      fs.mkdirSync(destMemory, { recursive: true });
      for (const f of fs.readdirSync(memoryDir)) {
        if (f.endsWith('.md')) {
          fs.copyFileSync(path.join(memoryDir, f), path.join(destMemory, f));
        }
      }
    }
  }

  console.log(`✅ Local backup saved: ${backupPath}`);
  return backupPath;
}

// List available versions
function listVersions() {
  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];
  const repoUrl = repo.replace('https://', `https://${token}@`);

  console.log('📋 Fetching available versions...\n');

  try {
    // Clone with all tags
    if (fs.existsSync(RESTORE_DIR)) {
      fs.rmSync(RESTORE_DIR, { recursive: true });
    }

    safeExec(`git clone --bare ${repoUrl} ${RESTORE_DIR}`, { stdio: 'pipe' });
    process.chdir(RESTORE_DIR);

    const tagsOutput = safeExec('git tag -l --sort=-creatordate', { stdio: 'pipe' });
    const tags = tagsOutput.trim().split('\n').filter(t => t.startsWith('backup-'));

    if (tags.length === 0) {
      console.log('No backup versions found.');
      console.log('Run `node push.js` to create your first backup.');
    } else {
      console.log('Available versions:\n');
      tags.forEach((tag, i) => {
        const marker = i === 0 ? ' (latest)' : '';
        console.log(`  🏷️  ${tag}${marker}`);
      });
      console.log('');
      console.log('To restore a specific version:');
      console.log(`  node pull.js --version ${tags[0]}`);
    }

  } catch (err) {
    console.error('❌ Failed to list versions:', err.message);
  } finally {
    process.chdir(os.homedir());
    if (fs.existsSync(RESTORE_DIR)) {
      fs.rmSync(RESTORE_DIR, { recursive: true });
    }
  }
}

// Prompt for confirmation
async function confirm(message) {
  if (FORCE_MODE) return true;

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  return new Promise(resolve => {
    rl.question(`${message} (y/N): `, answer => {
      rl.close();
      resolve(answer.toLowerCase() === 'y');
    });
  });
}

// Safely copy file with path validation
function safeCopyFile(src, dest) {
  const filename = path.basename(src);
  if (!isValidFilename(filename)) {
    console.log(`⚠️  Skipping unsafe filename: ${filename}`);
    return false;
  }

  if (!fs.existsSync(src)) return false;
  fs.mkdirSync(path.dirname(dest), { recursive: true });
  fs.copyFileSync(src, dest);
  console.log(`✅ Restored: ${path.basename(dest)}`);
  return true;
}

// Main
async function main() {
  loadEnv();
  checkConfig();

  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];

  // List mode
  if (LIST_MODE) {
    listVersions();
    return;
  }

  console.log('📥 Claw Sync - Restore\n');
  console.log(`Repository: ${repo}`);
  if (TARGET_VERSION) {
    console.log(`Version: ${TARGET_VERSION}`);
  } else {
    console.log('Version: latest');
  }
  console.log('');

  // Confirm restore
  const confirmed = await confirm('⚠️  This will overwrite local files. Continue?');
  if (!confirmed) {
    console.log('Restore cancelled.');
    return;
  }

  // Create local backup first (disaster recovery)
  const localBackup = createLocalBackup();

  // Clean restore area
  if (fs.existsSync(RESTORE_DIR)) {
    fs.rmSync(RESTORE_DIR, { recursive: true });
  }

  // Clone repo
  const repoUrl = repo.replace('https://', `https://${token}@`);

  try {
    console.log('\n📡 Cloning repository...');

    if (TARGET_VERSION) {
      // Clone specific tag
      safeExec(`git clone --depth 1 --branch ${TARGET_VERSION} ${repoUrl} ${RESTORE_DIR}`, { stdio: 'pipe' });
    } else {
      // Clone latest
      safeExec(`git clone --depth 1 ${repoUrl} ${RESTORE_DIR}`, { stdio: 'pipe' });
    }
  } catch (err) {
    console.error('❌ Clone failed:', err.message);
    console.log(`\n💡 Your local backup is safe at: ${localBackup}`);
    process.exit(1);
  }

  // Show metadata
  const metadataPath = path.join(RESTORE_DIR, 'SYNC_METADATA.json');
  if (fs.existsSync(metadataPath)) {
    const meta = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
    console.log(`\n📅 Backup from: ${meta.timestamp}`);
    console.log(`🏷️  Version: ${meta.tag || 'unknown'}`);
    console.log(`🖥️  Source: ${meta.hostname}`);
  }

  // Restore workspace files
  const backupWorkspace = path.join(RESTORE_DIR, 'workspace');
  if (fs.existsSync(backupWorkspace)) {
    console.log('\n📁 Restoring workspace...');

    const files = ['MEMORY.md', 'USER.md', 'SOUL.md', 'IDENTITY.md', 'TOOLS.md', 'AGENTS.md'];
    for (const file of files) {
      safeCopyFile(
        path.join(backupWorkspace, file),
        path.join(WORKSPACE_DIR, file)
      );
    }

    // Restore memory logs
    const backupMemory = path.join(backupWorkspace, 'memory');
    if (fs.existsSync(backupMemory)) {
      console.log('\n📅 Restoring daily logs...');
      fs.mkdirSync(path.join(WORKSPACE_DIR, 'memory'), { recursive: true });
      for (const file of fs.readdirSync(backupMemory)) {
        if (isValidFilename(file)) {
          safeCopyFile(
            path.join(backupMemory, file),
            path.join(WORKSPACE_DIR, 'memory', file)
          );
        }
      }
    }

    // Restore skills
    const backupSkills = path.join(backupWorkspace, 'skills');
    if (fs.existsSync(backupSkills)) {
      console.log('\n🔧 Restoring skills...');
      fs.mkdirSync(path.join(WORKSPACE_DIR, 'skills'), { recursive: true });
      for (const skill of fs.readdirSync(backupSkills)) {
        if (!isValidFilename(skill)) {
          console.log(`⚠️  Skipping unsafe skill name: ${skill}`);
          continue;
        }
        const src = path.join(backupSkills, skill);
        const dest = path.join(WORKSPACE_DIR, 'skills', skill);
        if (fs.statSync(src).isDirectory()) {
          if (fs.existsSync(dest)) {
            fs.rmSync(dest, { recursive: true });
          }
          fs.cpSync(src, dest, { recursive: true });
          console.log(`✅ Restored: skills/${skill}`);
        }
      }
    }
  }

  // Cleanup
  fs.rmSync(RESTORE_DIR, { recursive: true });

  console.log('\n✅ Restore complete!');
  console.log('');
  console.log('📝 Notes:');
  console.log(`   - Local backup saved at: ${localBackup}`);
  console.log('   - Config files (openclaw.json, .env) were NOT restored');
  console.log('   - Restart OpenClaw if running');
}

main().catch(err => {
  console.error('❌ Error:', err.message);
  process.exit(1);
});


path: claw-sync/scripts/push.js
content: #!/usr/bin/env node
/**
 * Claw Sync - Push Script
 * Pushes OpenClaw memory/workspace to GitHub with versioned backups
 * 
 * Features:
 * - Versioned backups with git tags (can restore any version)
 * - Security: No config files, validated URLs, sanitized errors
 * - Cross-platform: Works on Windows, Mac, Linux
 * 
 * Usage:
 *   node push.js              # Normal push
 *   node push.js --dry-run    # Show what would be pushed
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const os = require('os');

// Config
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
const STAGING_DIR = path.join(OPENCLAW_DIR, '.sync-staging');

// Security: Allowed git hosts
const ALLOWED_HOSTS = ['github.com', 'gitlab.com', 'bitbucket.org'];

// Parse args
const args = process.argv.slice(2);
const DRY_RUN = args.includes('--dry-run');

// Load env from .backup.env
function loadEnv() {
  const envFile = path.join(OPENCLAW_DIR, '.backup.env');
  if (fs.existsSync(envFile)) {
    const content = fs.readFileSync(envFile, 'utf8');
    content.split('\n').forEach(line => {
      const match = line.match(/^([A-Z_]+)=(.+)$/);
      if (match && !process.env[match[1]]) {
        process.env[match[1]] = match[2].trim();
      }
    });
  }
}

// Security: Validate repository URL
function validateRepoUrl(url) {
  try {
    const parsed = new URL(url);
    if (parsed.protocol !== 'https:') {
      throw new Error('Repository URL must use HTTPS');
    }
    if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
      throw new Error(`Repository must be on: ${ALLOWED_HOSTS.join(', ')}`);
    }
    return true;
  } catch (err) {
    console.error(`❌ Invalid repository URL: ${err.message}`);
    process.exit(1);
  }
}

// Security: Validate token format
function validateToken(token) {
  // GitHub tokens start with ghp_, gho_, ghu_, ghs_, ghr_
  // GitLab tokens are typically glpat-
  const validPrefixes = ['ghp_', 'gho_', 'ghu_', 'ghs_', 'ghr_', 'glpat-'];
  if (!validPrefixes.some(p => token.startsWith(p))) {
    console.warn('⚠️  Token format not recognized. Proceeding anyway...');
  }
  if (token.length < 20) {
    console.error('❌ Token appears too short');
    process.exit(1);
  }
}

// Check configuration
function checkConfig() {
  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];

  if (!repo || !token) {
    console.error('❌ Sync not configured!');
    console.error('');
    console.error('Create ~/.openclaw/.backup.env with:');
    console.error('  BACKUP_REPO=https://github.com/username/repo');
    console.error('  BACKUP_TOKEN=ghp_your_token_here');
    process.exit(1);
  }

  if (token.includes('xxxx') || token.includes('YOUR_')) {
    console.error('❌ Please replace the placeholder token with your real token');
    process.exit(1);
  }

  validateRepoUrl(repo);
  validateToken(token);
}

// Security: Sanitize string for git commit message
function sanitizeForGit(str) {
  return str.replace(/[`$"\\]/g, '');
}

// Copy file with optional transform
function copyFile(src, dest, transform = null) {
  if (!fs.existsSync(src)) {
    if (!DRY_RUN) console.log(`⏭️  Skipping (not found): ${path.basename(src)}`);
    return false;
  }

  if (DRY_RUN) {
    console.log(`  📄 ${path.basename(src)}`);
    return true;
  }

  fs.mkdirSync(path.dirname(dest), { recursive: true });
  let content = fs.readFileSync(src, 'utf8');
  if (transform) content = transform(content);
  fs.writeFileSync(dest, content);
  console.log(`✅ ${path.basename(src)}`);
  return true;
}

// Generate backup tag name
function generateTagName() {
  const now = new Date();
  const y = now.getFullYear();
  const m = String(now.getMonth() + 1).padStart(2, '0');
  const d = String(now.getDate()).padStart(2, '0');
  const h = String(now.getHours()).padStart(2, '0');
  const min = String(now.getMinutes()).padStart(2, '0');
  return `backup-${y}${m}${d}-${h}${min}`;
}

// Security: Execute git command without exposing token in errors
function safeExec(cmd, options = {}) {
  try {
    return execSync(cmd, { ...options, encoding: 'utf8' });
  } catch (err) {
    // Sanitize error message to remove any tokens
    let msg = err.message || 'Unknown error';
    const [REDACTED_SECRET];
    if (token) {
      msg = msg.replace(new RegExp(token, 'g'), '***TOKEN***');
    }
    throw new Error(msg);
  }
}

// Main
async function main() {
  loadEnv();
  checkConfig();

  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];
  const timestamp = new Date().toISOString();
  const tagName = generateTagName();

  if (DRY_RUN) {
    console.log('🔍 DRY RUN - showing what would be synced:\n');
  } else {
    console.log('🔧 Preparing sync...\n');
  }

  // Clean staging area
  if (!DRY_RUN) {
    if (fs.existsSync(STAGING_DIR)) {
      fs.rmSync(STAGING_DIR, { recursive: true });
    }
    fs.mkdirSync(STAGING_DIR, { recursive: true });
  }

  // Memory files
  console.log('📝 Memory files:');
  const memoryFiles = ['MEMORY.md', 'USER.md', 'SOUL.md', 'IDENTITY.md', 'TOOLS.md', 'AGENTS.md'];
  for (const file of memoryFiles) {
    copyFile(
      path.join(WORKSPACE_DIR, file),
      path.join(STAGING_DIR, 'workspace', file)
    );
  }

  // Daily logs
  const memoryDir = path.join(WORKSPACE_DIR, 'memory');
  if (fs.existsSync(memoryDir)) {
    console.log('\n📅 Daily logs:');
    if (!DRY_RUN) fs.mkdirSync(path.join(STAGING_DIR, 'workspace/memory'), { recursive: true });
    const logs = fs.readdirSync(memoryDir).filter(f => f.endsWith('.md'));
    for (const file of logs) {
      copyFile(
        path.join(memoryDir, file),
        path.join(STAGING_DIR, 'workspace/memory', file)
      );
    }
  }

  // Custom skills
  const skillsDir = path.join(WORKSPACE_DIR, 'skills');
  if (fs.existsSync(skillsDir)) {
    console.log('\n🔧 Skills:');
    if (!DRY_RUN) fs.mkdirSync(path.join(STAGING_DIR, 'workspace/skills'), { recursive: true });
    for (const skill of fs.readdirSync(skillsDir)) {
      const skillPath = path.join(skillsDir, skill);
      const destPath = path.join(STAGING_DIR, 'workspace/skills', skill);
      if (fs.statSync(skillPath).isDirectory()) {
        if (DRY_RUN) {
          console.log(`  📦 skills/${skill}`);
        } else {
          fs.cpSync(skillPath, destPath, { recursive: true });
          console.log(`✅ skills/${skill}`);
        }
      }
    }
  }

  if (DRY_RUN) {
    console.log('\n✅ Dry run complete. No changes made.');
    return;
  }

  // Write metadata
  fs.writeFileSync(
    path.join(STAGING_DIR, 'SYNC_METADATA.json'),
    JSON.stringify({
      timestamp,
      tag: tagName,
      hostname: os.hostname(),
      platform: os.platform(),
      version: '2.0.2'
    }, null, 2)
  );

  // Git operations
  console.log('\n📤 Pushing to remote...');

  const repoUrl = repo.replace('https://', `https://${token}@`);

  try {
    process.chdir(STAGING_DIR);

    safeExec('git init', { stdio: 'ignore' });
    safeExec('git config user.email "[REDACTED_SECRET]"', { stdio: 'ignore' });
    safeExec('git config user.name "Claw Sync"', { stdio: 'ignore' });
    safeExec('git add -A', { stdio: 'ignore' });

    const commitMsg = sanitizeForGit(`Sync: ${timestamp}`);
    safeExec(`git commit -m "${commitMsg}"`, { stdio: 'ignore' });

    // Push to main
    safeExec(`git push ${repoUrl} HEAD:main --force`, { stdio: 'pipe' });

    // Create version tag
    safeExec(`git tag ${tagName}`, { stdio: 'ignore' });
    safeExec(`git push ${repoUrl} ${tagName}`, { stdio: 'pipe' });

    console.log('\n✅ Sync complete!');
    console.log(`🏷️  Version: ${tagName}`);
    console.log(`🕐 ${timestamp}`);
    console.log(`📁 ${repo}`);

  } catch (err) {
    console.error('\n❌ Sync failed:', err.message);
    process.exit(1);
  } finally {
    // Cleanup
    process.chdir(os.homedir());
    if (fs.existsSync(STAGING_DIR)) {
      fs.rmSync(STAGING_DIR, { recursive: true });
    }
  }
}

main().catch(err => {
  console.error('❌ Error:', err.message);
  process.exit(1);
});


path: claw-sync/scripts/setup-cron.js
content: #!/usr/bin/env node
/**
 * Setup auto-sync cron job
 * Runs sync every 12 hours
 */

const fs = require('fs');
const path = require('path');

const OPENCLAW_DIR = path.join(require('os').homedir(), '.openclaw');
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
const SKILL_DIR = path.join(WORKSPACE_DIR, 'skills/claw-sync');

async function main() {
  console.log('🔧 Setting up auto-sync cron job...\n');

  // Check if skill exists
  if (!fs.existsSync(SKILL_DIR)) {
    console.error('❌ Claw Sync skill not found at:', SKILL_DIR);
    process.exit(1);
  }

  // Create the cron job payload
  const job = {
    name: "claw-sync",
    schedule: {
      kind: "every",
      everyMs: 12 * 60 * 60 * 1000  // 12 hours
    },
    payload: {
      kind: "agentTurn",
      message: "exec: node skills/claw-sync/index.js sync",
      timeoutSeconds: 300
    },
    sessionTarget: "isolated",
    enabled: true
  };

  console.log('Creating cron job...');
  console.log('  Name: claw-sync');
  console.log('  Schedule: Every 12 hours');
  console.log('  Command: node skills/claw-sync/index.js sync');
  console.log('');

  try {
    const cronConfigPath = path.join(OPENCLAW_DIR, 'cron', 'claw-sync.json');

    // Ensure cron directory exists
    fs.mkdirSync(path.join(OPENCLAW_DIR, 'cron'), { recursive: true });

    // Write job config
    fs.writeFileSync(cronConfigPath, JSON.stringify(job, null, 2));

    console.log('✅ Cron config saved to:', cronConfigPath);
    console.log('');
    console.log('To activate via OpenClaw CLI:');
    console.log(`  openclaw cron add ${cronConfigPath}`);
    console.log('');
    console.log('Or add manually to your crontab:');
    console.log(`  0 */12 * * * cd ${WORKSPACE_DIR} && node skills/claw-sync/index.js sync >> /tmp/claw-sync.log 2>&1`);

  } catch (err) {
    console.error('❌ Setup failed:', err.message);
    process.exit(1);
  }
}

main().catch(console.error);


path: claw-sync/scripts/status.js
content: #!/usr/bin/env node
/**
 * Claw Sync - Status Script
 * Shows sync status and local backup info
 */

const fs = require('fs');
const path = require('path');
const os = require('os');

const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
const LOCAL_BACKUP_DIR = path.join(OPENCLAW_DIR, '.local-backup');

function loadEnv() {
  const envFile = path.join(OPENCLAW_DIR, '.backup.env');
  if (fs.existsSync(envFile)) {
    const content = fs.readFileSync(envFile, 'utf8');
    content.split('\n').forEach(line => {
      const match = line.match(/^([A-Z_]+)=(.+)$/);
      if (match && !process.env[match[1]]) {
        process.env[match[1]] = match[2].trim();
      }
    });
  }
}

function isConfigured() {
  const repo = process.env.BACKUP_REPO;
  const [REDACTED_SECRET];
  return repo && token &&
    !token.includes('xxxx') &&
    !token.includes('YOUR_');
}

function main() {
  loadEnv();

  const repo = process.env.BACKUP_REPO;
  const configured = isConfigured();

  console.log('📊 Claw Sync Status\n');

  // Configuration status
  if (!configured) {
    console.log('⚠️  Not configured');
    console.log('');
    console.log('Create ~/.openclaw/.backup.env with:');
    console.log('  BACKUP_REPO=https://github.com/username/repo');
    console.log('  BACKUP_TOKEN=ghp_your_token');
    console.log('');
    return;
  }

  console.log(`✅ Configured`);
  console.log(`📁 Repository: ${repo}`);
  console.log('');

  // Files to sync
  console.log('📝 Files to sync:');

  const checks = [
    ['MEMORY.md', path.join(WORKSPACE_DIR, 'MEMORY.md')],
    ['USER.md', path.join(WORKSPACE_DIR, 'USER.md')],
    ['SOUL.md', path.join(WORKSPACE_DIR, 'SOUL.md')],
    ['IDENTITY.md', path.join(WORKSPACE_DIR, 'IDENTITY.md')],
    ['TOOLS.md', path.join(WORKSPACE_DIR, 'TOOLS.md')],
    ['AGENTS.md', path.join(WORKSPACE_DIR, 'AGENTS.md')],
  ];

  for (const [name, filePath] of checks) {
    const exists = fs.existsSync(filePath);
    const size = exists ? fs.statSync(filePath).size : 0;
    const sizeStr = exists ? `${(size / 1024).toFixed(1)}KB` : 'missing';
    console.log(`  ${exists ? '✅' : '⏭️ '} ${name} (${sizeStr})`);
  }

  // Count memory files
  const memoryDir = path.join(WORKSPACE_DIR, 'memory');
  if (fs.existsSync(memoryDir)) {
    const files = fs.readdirSync(memoryDir).filter(f => f.endsWith('.md'));
    console.log(`  ✅ memory/*.md (${files.length} files)`);
  }

  // Count skills
  const skillsDir = path.join(WORKSPACE_DIR, 'skills');
  if (fs.existsSync(skillsDir)) {
    const skills = fs.readdirSync(skillsDir).filter(f =>
      fs.statSync(path.join(skillsDir, f)).isDirectory()
    );
    console.log(`  ✅ skills/ (${skills.length} skills)`);
  }

  console.log('');
  console.log('🔒 NOT synced (security): openclaw.json, .env');

  // Local backups
  console.log('\n💾 Local backups (disaster recovery):');
  if (fs.existsSync(LOCAL_BACKUP_DIR)) {
    const backups = fs.readdirSync(LOCAL_BACKUP_DIR)
      .filter(f => fs.statSync(path.join(LOCAL_BACKUP_DIR, f)).isDirectory())
      .sort()
      .reverse()
      .slice(0, 5);

    if (backups.length > 0) {
      backups.forEach((b, i) => {
        const marker = i === 0 ? ' (latest)' : '';
        console.log(`  📦 ${b}${marker}`);
      });
    } else {
      console.log('  No local backups yet');
    }
  } else {
    console.log('  No local backups yet');
  }

  // Commands
  console.log('\n💡 Commands:');
  console.log('  node skills/claw-sync/scripts/push.js           # Push to remote');
  console.log('  node skills/claw-sync/scripts/push.js --dry-run # Preview');
  console.log('  node skills/claw-sync/scripts/pull.js --list    # List versions');
  console.log('  node skills/claw-sync/scripts/pull.js           # Restore latest');
}

main();


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)