claw / sync-external-storage.mjs
jkes6203's picture
Enhance OpenClaw Gateway setup with Supabase sync and Telegram support
15e4cfe
#!/usr/bin/env node
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const home = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
const stateDir = path.join(home, ".openclaw");
const supabaseUrl = process.env.SUPABASE_URL?.trim();
const supabaseKey = process.env.SUPABASE_KEY?.trim();
const tableName = process.env.OPENCLAW_SUPABASE_TABLE?.trim() || "openclaw_state";
const intervalMs = Number(process.env.OPENCLAW_SYNC_INTERVAL_MS || 300000);
const maxFileBytes = Number(process.env.OPENCLAW_SYNC_MAX_FILE_BYTES || 5 * 1024 * 1024);
const includeGlobs = (process.env.OPENCLAW_SYNC_INCLUDE_EXTENSIONS || ".json,.jsonl,.md,.txt")
.split(",")
.map((value) => value.trim().toLowerCase())
.filter(Boolean);
if (!supabaseUrl || !supabaseKey) {
console.error("[openclaw-sync] SUPABASE_URL or SUPABASE_KEY is missing");
process.exit(1);
}
const seenHashes = new Map();
function shouldSyncFile(filePath, stats) {
if (!stats.isFile()) return false;
if (stats.size > maxFileBytes) return false;
const normalized = filePath.toLowerCase();
return includeGlobs.some((ext) => normalized.endsWith(ext));
}
function listFilesRecursive(rootDir) {
if (!fs.existsSync(rootDir)) return [];
const results = [];
const stack = [rootDir];
while (stack.length > 0) {
const currentDir = stack.pop();
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
continue;
}
const stats = fs.statSync(fullPath);
if (shouldSyncFile(fullPath, stats)) {
results.push({ fullPath, stats });
}
}
}
return results.sort((a, b) => a.fullPath.localeCompare(b.fullPath));
}
function sha256(content) {
return crypto.createHash("sha256").update(content).digest("hex");
}
function classifyPath(relativePath) {
if (relativePath === "openclaw.json") return "config";
if (relativePath.includes("sessions")) return "session";
if (relativePath.includes("memory")) return "memory";
if (relativePath.includes("logs")) return "log";
return "state";
}
async function upsertRows(rows) {
const endpoint = `${supabaseUrl.replace(/\/$/, "")}/rest/v1/${tableName}?on_conflict=path`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
apikey: supabaseKey,
Authorization: `Bearer ${supabaseKey}`,
"Content-Type": "application/json",
Prefer: "resolution=merge-duplicates",
},
body: JSON.stringify(rows),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Supabase upsert failed (${response.status}): ${body}`);
}
}
async function syncOnce() {
const files = listFilesRecursive(stateDir);
const changedRows = [];
for (const { fullPath, stats } of files) {
const relativePath = path.relative(stateDir, fullPath).replaceAll("\\", "/");
const content = fs.readFileSync(fullPath, "utf-8");
const hash = sha256(content);
if (seenHashes.get(relativePath) === hash) continue;
seenHashes.set(relativePath, hash);
changedRows.push({
path: relativePath,
kind: classifyPath(relativePath),
content,
sha256: hash,
size_bytes: stats.size,
updated_at: new Date(stats.mtimeMs).toISOString(),
synced_at: new Date().toISOString(),
});
}
if (changedRows.length === 0) {
console.log("[openclaw-sync] no changes");
return;
}
for (let index = 0; index < changedRows.length; index += 25) {
const batch = changedRows.slice(index, index + 25);
await upsertRows(batch);
}
console.log(`[openclaw-sync] synced ${changedRows.length} file(s) from ${stateDir}`);
}
async function main() {
console.log(
`[openclaw-sync] provider=supabase table=${tableName} interval_ms=${intervalMs} state_dir=${stateDir}`,
);
while (true) {
try {
await syncOnce();
} catch (error) {
console.error(`[openclaw-sync] ${error instanceof Error ? error.message : String(error)}`);
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
}
main().catch((error) => {
console.error(`[openclaw-sync] fatal ${error instanceof Error ? error.stack : String(error)}`);
process.exit(1);
});