CoolFace
Apppublic

davisc1/claw

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
sync-external-storage.mjs141 linesDownload Raw Back to root
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