davisc1/claw
0
1#!/usr/bin/env node2import crypto from "node:crypto";3import fs from "node:fs";4import path from "node:path";5 6const home = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";7const stateDir = path.join(home, ".openclaw");8const supabaseUrl = process.env.SUPABASE_URL?.trim();9const supabaseKey = process.env.SUPABASE_KEY?.trim();10const tableName = process.env.OPENCLAW_SUPABASE_TABLE?.trim() || "openclaw_state";11const intervalMs = Number(process.env.OPENCLAW_SYNC_INTERVAL_MS || 300000);12const maxFileBytes = Number(process.env.OPENCLAW_SYNC_MAX_FILE_BYTES || 5 * 1024 * 1024);13const includeGlobs = (process.env.OPENCLAW_SYNC_INCLUDE_EXTENSIONS || ".json,.jsonl,.md,.txt")14 .split(",")15 .map((value) => value.trim().toLowerCase())16 .filter(Boolean);17 18if (!supabaseUrl || !supabaseKey) {19 console.error("[openclaw-sync] SUPABASE_URL or SUPABASE_KEY is missing");20 process.exit(1);21}22 23const seenHashes = new Map();24 25function shouldSyncFile(filePath, stats) {26 if (!stats.isFile()) return false;27 if (stats.size > maxFileBytes) return false;28 const normalized = filePath.toLowerCase();29 return includeGlobs.some((ext) => normalized.endsWith(ext));30}31 32function listFilesRecursive(rootDir) {33 if (!fs.existsSync(rootDir)) return [];34 const results = [];35 const stack = [rootDir];36 37 while (stack.length > 0) {38 const currentDir = stack.pop();39 const entries = fs.readdirSync(currentDir, { withFileTypes: true });40 for (const entry of entries) {41 const fullPath = path.join(currentDir, entry.name);42 if (entry.isDirectory()) {43 stack.push(fullPath);44 continue;45 }46 const stats = fs.statSync(fullPath);47 if (shouldSyncFile(fullPath, stats)) {48 results.push({ fullPath, stats });49 }50 }51 }52 53 return results.sort((a, b) => a.fullPath.localeCompare(b.fullPath));54}55 56function sha256(content) {57 return crypto.createHash("sha256").update(content).digest("hex");58}59 60function classifyPath(relativePath) {61 if (relativePath === "openclaw.json") return "config";62 if (relativePath.includes("sessions")) return "session";63 if (relativePath.includes("memory")) return "memory";64 if (relativePath.includes("logs")) return "log";65 return "state";66}67 68async function upsertRows(rows) {69 const endpoint = `${supabaseUrl.replace(/\/$/, "")}/rest/v1/${tableName}?on_conflict=path`;70 const response = await fetch(endpoint, {71 method: "POST",72 headers: {73 apikey: supabaseKey,74 Authorization: `Bearer ${supabaseKey}`,75 "Content-Type": "application/json",76 Prefer: "resolution=merge-duplicates",77 },78 body: JSON.stringify(rows),79 });80 81 if (!response.ok) {82 const body = await response.text();83 throw new Error(`Supabase upsert failed (${response.status}): ${body}`);84 }85}86 87async function syncOnce() {88 const files = listFilesRecursive(stateDir);89 const changedRows = [];90 91 for (const { fullPath, stats } of files) {92 const relativePath = path.relative(stateDir, fullPath).replaceAll("\\", "/");93 const content = fs.readFileSync(fullPath, "utf-8");94 const hash = sha256(content);95 if (seenHashes.get(relativePath) === hash) continue;96 97 seenHashes.set(relativePath, hash);98 changedRows.push({99 path: relativePath,100 kind: classifyPath(relativePath),101 content,102 sha256: hash,103 size_bytes: stats.size,104 updated_at: new Date(stats.mtimeMs).toISOString(),105 synced_at: new Date().toISOString(),106 });107 }108 109 if (changedRows.length === 0) {110 console.log("[openclaw-sync] no changes");111 return;112 }113 114 for (let index = 0; index < changedRows.length; index += 25) {115 const batch = changedRows.slice(index, index + 25);116 await upsertRows(batch);117 }118 119 console.log(`[openclaw-sync] synced ${changedRows.length} file(s) from ${stateDir}`);120}121 122async function main() {123 console.log(124 `[openclaw-sync] provider=supabase table=${tableName} interval_ms=${intervalMs} state_dir=${stateDir}`,125 );126 127 while (true) {128 try {129 await syncOnce();130 } catch (error) {131 console.error(`[openclaw-sync] ${error instanceof Error ? error.message : String(error)}`);132 }133 await new Promise((resolve) => setTimeout(resolve, intervalMs));134 }135}136 137main().catch((error) => {138 console.error(`[openclaw-sync] fatal ${error instanceof Error ? error.stack : String(error)}`);139 process.exit(1);140});141 