@alfe.ai/openclaw-sync 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +263 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/ignore.js +73 -0
- package/dist/ignore.js.map +1 -0
- package/dist/index.d.ts +433 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +469 -0
- package/dist/index.js.map +1 -0
- package/dist/sync-engine.js +672 -0
- package/dist/sync-engine.js.map +1 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { i as shouldIgnore, r as loadIgnorePatterns, t as filterIgnored } from "./ignore.js";
|
|
2
|
+
import { a as computeFileHash, c as removeManifestEntry, d as configDir, f as configPath, g as writeConfig, h as requireConfig, i as createApiClient, l as updateManifestEntry, m as readConfig, n as downloadFiles, o as diffManifests, p as isInitialized, r as uploadFiles, s as readManifest, t as createSyncEngine, u as writeManifest } from "./sync-engine.js";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { watch } from "chokidar";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
//#region src/watcher.ts
|
|
7
|
+
/**
|
|
8
|
+
* AlfeSync watcher — recursive file watcher with debounce and ignore support.
|
|
9
|
+
*
|
|
10
|
+
* Uses chokidar to watch the workspace root, debounces per-file changes
|
|
11
|
+
* by 2 seconds, and emits batches of changed paths.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Start watching a workspace for file changes.
|
|
15
|
+
*
|
|
16
|
+
* Returns a cleanup function to stop watching.
|
|
17
|
+
*/
|
|
18
|
+
async function startWatcher(options) {
|
|
19
|
+
const { workspacePath, debounceMs = 2e3, onChanges } = options;
|
|
20
|
+
const ignorePatterns = await loadIgnorePatterns(workspacePath);
|
|
21
|
+
const pending = /* @__PURE__ */ new Map();
|
|
22
|
+
let batchPaths = /* @__PURE__ */ new Set();
|
|
23
|
+
let flushTimer = null;
|
|
24
|
+
function scheduleBatch() {
|
|
25
|
+
if (flushTimer) return;
|
|
26
|
+
flushTimer = setTimeout(() => {
|
|
27
|
+
flushTimer = null;
|
|
28
|
+
if (batchPaths.size === 0) return;
|
|
29
|
+
const paths = [...batchPaths];
|
|
30
|
+
batchPaths = /* @__PURE__ */ new Set();
|
|
31
|
+
onChanges(paths);
|
|
32
|
+
}, debounceMs);
|
|
33
|
+
}
|
|
34
|
+
function handleChange(absolutePath) {
|
|
35
|
+
const relativePath = relative(workspacePath, absolutePath);
|
|
36
|
+
if (shouldIgnore(relativePath, ignorePatterns)) return;
|
|
37
|
+
const existingTimer = pending.get(relativePath);
|
|
38
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
pending.delete(relativePath);
|
|
41
|
+
batchPaths.add(relativePath);
|
|
42
|
+
scheduleBatch();
|
|
43
|
+
}, debounceMs);
|
|
44
|
+
pending.set(relativePath, timer);
|
|
45
|
+
}
|
|
46
|
+
const watcher = watch(workspacePath, {
|
|
47
|
+
persistent: true,
|
|
48
|
+
ignoreInitial: true,
|
|
49
|
+
followSymlinks: false,
|
|
50
|
+
depth: void 0,
|
|
51
|
+
ignored: [
|
|
52
|
+
"**/node_modules/**",
|
|
53
|
+
"**/.alfesync/**",
|
|
54
|
+
"**/.git/**",
|
|
55
|
+
"**/.sst/**"
|
|
56
|
+
]
|
|
57
|
+
});
|
|
58
|
+
watcher.on("add", handleChange);
|
|
59
|
+
watcher.on("change", handleChange);
|
|
60
|
+
watcher.on("unlink", handleChange);
|
|
61
|
+
return async () => {
|
|
62
|
+
for (const timer of pending.values()) clearTimeout(timer);
|
|
63
|
+
pending.clear();
|
|
64
|
+
if (flushTimer) {
|
|
65
|
+
clearTimeout(flushTimer);
|
|
66
|
+
flushTimer = null;
|
|
67
|
+
}
|
|
68
|
+
await watcher.close();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/plugin.ts
|
|
73
|
+
/**
|
|
74
|
+
* @alfe.ai/openclaw-sync — OpenClaw Sync plugin.
|
|
75
|
+
*
|
|
76
|
+
* Wraps the existing sync engine as a lifecycle-managed integration,
|
|
77
|
+
* following the same plugin pattern as @alfe.ai/openclaw-mobile and
|
|
78
|
+
* @alfe.ai/openclaw-discord.
|
|
79
|
+
*
|
|
80
|
+
* Lifecycle:
|
|
81
|
+
* - activate(api): start the sync engine + watcher based on config
|
|
82
|
+
* - deactivate(api): stop the watcher, clean up resources
|
|
83
|
+
* - configure(api, config): update scope/schedule at runtime
|
|
84
|
+
*
|
|
85
|
+
* Registers 'sync.now' and 'sync.status' gateway RPC methods.
|
|
86
|
+
*/
|
|
87
|
+
const DEFAULT_SOCKET_PATH = join(homedir(), ".alfe", "gateway.sock");
|
|
88
|
+
const SYNC_CAPABILITIES = [
|
|
89
|
+
"sync.push",
|
|
90
|
+
"sync.pull",
|
|
91
|
+
"sync.fullSync"
|
|
92
|
+
];
|
|
93
|
+
const SYNC_RELAY_RECONNECT_BASE_MS = 1e3;
|
|
94
|
+
const SYNC_RELAY_RECONNECT_MAX_MS = 3e4;
|
|
95
|
+
const SYNC_RELAY_DEBOUNCE_MS = 500;
|
|
96
|
+
let syncEngine = null;
|
|
97
|
+
let stopWatcher = null;
|
|
98
|
+
let daemonIpcClient = null;
|
|
99
|
+
let scheduledInterval = null;
|
|
100
|
+
let currentConfig = {};
|
|
101
|
+
let lastSyncResult = null;
|
|
102
|
+
let syncRelayWs = null;
|
|
103
|
+
let syncRelayReconnectTimer = null;
|
|
104
|
+
let syncRelayReconnectAttempt = 0;
|
|
105
|
+
let syncRelayDebounceTimer = null;
|
|
106
|
+
const syncRelayPendingPaths = /* @__PURE__ */ new Map();
|
|
107
|
+
const SCHEDULE_INTERVALS_MS = {
|
|
108
|
+
hourly: 3600 * 1e3,
|
|
109
|
+
daily: 1440 * 60 * 1e3,
|
|
110
|
+
weekly: 10080 * 60 * 1e3
|
|
111
|
+
};
|
|
112
|
+
function clearSchedule() {
|
|
113
|
+
if (scheduledInterval) {
|
|
114
|
+
clearInterval(scheduledInterval);
|
|
115
|
+
scheduledInterval = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function setupSchedule(schedule, log) {
|
|
119
|
+
clearSchedule();
|
|
120
|
+
if (schedule === "realtime") {
|
|
121
|
+
log.info("Sync schedule: realtime (file watcher active)");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const intervalMs = SCHEDULE_INTERVALS_MS[schedule];
|
|
125
|
+
if (!intervalMs) {
|
|
126
|
+
log.warn(`Unknown sync schedule: ${schedule}, defaulting to hourly`);
|
|
127
|
+
setupSchedule("hourly", log);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
log.info(`Sync schedule: ${schedule} (every ${String(intervalMs / 1e3)}s)`);
|
|
131
|
+
scheduledInterval = setInterval(() => {
|
|
132
|
+
if (!syncEngine) return;
|
|
133
|
+
const engine = syncEngine;
|
|
134
|
+
(async () => {
|
|
135
|
+
try {
|
|
136
|
+
log.info(`Scheduled sync (${schedule}) starting...`);
|
|
137
|
+
lastSyncResult = await engine.fullSync({ quiet: true });
|
|
138
|
+
log.info(`Scheduled sync complete: ${String(lastSyncResult.pushed)} pushed, ${String(lastSyncResult.pulled)} pulled`);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
log.error(`Scheduled sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
}, intervalMs);
|
|
144
|
+
}
|
|
145
|
+
async function connectToDaemon(socketPath, log) {
|
|
146
|
+
try {
|
|
147
|
+
const IPCClient = (await import("@alfe.ai/openclaw")).IPCClient;
|
|
148
|
+
const client = new IPCClient(socketPath, log);
|
|
149
|
+
client.on("connected", () => {
|
|
150
|
+
(async () => {
|
|
151
|
+
log.info("Connected to Alfe daemon — registering sync capabilities...");
|
|
152
|
+
const response = await client.request("capability.register", {
|
|
153
|
+
plugin: "@alfe.ai/openclaw-sync",
|
|
154
|
+
capabilities: [...SYNC_CAPABILITIES]
|
|
155
|
+
});
|
|
156
|
+
if (response.ok) log.info("Sync capabilities registered with daemon");
|
|
157
|
+
else log.warn(`Failed to register sync capabilities: ${response.error?.message ?? "unknown"}`);
|
|
158
|
+
})();
|
|
159
|
+
});
|
|
160
|
+
client.on("disconnected", (...args) => {
|
|
161
|
+
const reason = typeof args[0] === "string" ? args[0] : String(args[0]);
|
|
162
|
+
log.warn(`Disconnected from Alfe daemon: ${reason}`);
|
|
163
|
+
});
|
|
164
|
+
client.on("message", (...args) => {
|
|
165
|
+
const msg = args[0];
|
|
166
|
+
if (msg?.type === "SYNC_NOW" || msg?.command === "SYNC_NOW") {
|
|
167
|
+
log.info("Received SYNC_NOW command — triggering immediate sync...");
|
|
168
|
+
if (syncEngine) {
|
|
169
|
+
const engine = syncEngine;
|
|
170
|
+
(async () => {
|
|
171
|
+
try {
|
|
172
|
+
lastSyncResult = await engine.fullSync({ quiet: true });
|
|
173
|
+
log.info(`SYNC_NOW complete: ${String(lastSyncResult.pushed)} pushed, ${String(lastSyncResult.pulled)} pulled`);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
log.error(`SYNC_NOW failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
client.on("error", (...args) => {
|
|
182
|
+
const err = args[0];
|
|
183
|
+
log.debug(`Daemon IPC error: ${err instanceof Error ? err.message : String(err)}`);
|
|
184
|
+
});
|
|
185
|
+
client.start();
|
|
186
|
+
return client;
|
|
187
|
+
} catch {
|
|
188
|
+
log.info("Alfe daemon not available — Sync plugin running standalone");
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function clearSyncRelayReconnect() {
|
|
193
|
+
if (syncRelayReconnectTimer) {
|
|
194
|
+
clearTimeout(syncRelayReconnectTimer);
|
|
195
|
+
syncRelayReconnectTimer = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function clearSyncRelayDebounce() {
|
|
199
|
+
if (syncRelayDebounceTimer) {
|
|
200
|
+
clearTimeout(syncRelayDebounceTimer);
|
|
201
|
+
syncRelayDebounceTimer = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function processPendingNotifications(log) {
|
|
205
|
+
if (!syncEngine || syncRelayPendingPaths.size === 0) return;
|
|
206
|
+
const engine = syncEngine;
|
|
207
|
+
const entries = new Map(syncRelayPendingPaths);
|
|
208
|
+
syncRelayPendingPaths.clear();
|
|
209
|
+
const toPull = [];
|
|
210
|
+
const toDelete = [];
|
|
211
|
+
for (const [filePath, info] of entries) if (info.eventType === "deleted") toDelete.push(filePath);
|
|
212
|
+
else toPull.push(filePath);
|
|
213
|
+
for (const filePath of toDelete) try {
|
|
214
|
+
await engine.removeLocalFile(filePath, { quiet: true });
|
|
215
|
+
log.debug(`Sync relay: deleted ${filePath}`);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
log.error(`Sync relay: failed to delete ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
218
|
+
}
|
|
219
|
+
if (toPull.length > 0) try {
|
|
220
|
+
const result = await engine.pullFiles(toPull, { quiet: true });
|
|
221
|
+
if (result.pulled > 0) log.info(`Sync relay: pulled ${String(result.pulled)} file(s)`);
|
|
222
|
+
if (result.errors > 0) log.warn(`Sync relay: ${String(result.errors)} pull error(s)`);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
log.error(`Sync relay: pull failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function connectToSyncRelay(relayUrl, token, agentId, log) {
|
|
228
|
+
try {
|
|
229
|
+
const { default: WebSocket } = await import("ws");
|
|
230
|
+
const ws = new WebSocket(`${relayUrl}?token=${encodeURIComponent(token)}`);
|
|
231
|
+
ws.on("open", () => {
|
|
232
|
+
log.info("Connected to Sync Relay");
|
|
233
|
+
syncRelayReconnectAttempt = 0;
|
|
234
|
+
ws.send(JSON.stringify({
|
|
235
|
+
type: "SUBSCRIBE",
|
|
236
|
+
agentId
|
|
237
|
+
}));
|
|
238
|
+
});
|
|
239
|
+
ws.on("message", (data) => {
|
|
240
|
+
let message;
|
|
241
|
+
try {
|
|
242
|
+
message = JSON.parse(data.toString());
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
switch (message.type) {
|
|
247
|
+
case "SUBSCRIBE_ACK":
|
|
248
|
+
if (message.status === "ok") log.info(`Subscribed to sync notifications for agent ${message.agentId ?? agentId}`);
|
|
249
|
+
else log.warn(`Sync relay subscribe failed: ${message.message ?? "unknown"}`);
|
|
250
|
+
break;
|
|
251
|
+
case "FILE_CHANGED":
|
|
252
|
+
if (message.filePath) syncRelayPendingPaths.set(message.filePath, {
|
|
253
|
+
etag: message.etag,
|
|
254
|
+
eventType: message.eventType === "deleted" ? "deleted" : "created"
|
|
255
|
+
});
|
|
256
|
+
clearSyncRelayDebounce();
|
|
257
|
+
syncRelayDebounceTimer = setTimeout(() => {
|
|
258
|
+
processPendingNotifications(log);
|
|
259
|
+
}, SYNC_RELAY_DEBOUNCE_MS);
|
|
260
|
+
break;
|
|
261
|
+
case "PING":
|
|
262
|
+
try {
|
|
263
|
+
ws.send(JSON.stringify({ type: "PONG" }));
|
|
264
|
+
} catch {}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
ws.on("close", (code) => {
|
|
269
|
+
log.info(`Sync Relay disconnected (code=${String(code)})`);
|
|
270
|
+
syncRelayWs = null;
|
|
271
|
+
scheduleSyncRelayReconnect(relayUrl, token, agentId, log);
|
|
272
|
+
});
|
|
273
|
+
ws.on("error", (err) => {
|
|
274
|
+
log.debug(`Sync Relay error: ${err.message}`);
|
|
275
|
+
});
|
|
276
|
+
return ws;
|
|
277
|
+
} catch (err) {
|
|
278
|
+
log.debug(`Failed to connect to Sync Relay: ${err instanceof Error ? err.message : String(err)}`);
|
|
279
|
+
scheduleSyncRelayReconnect(relayUrl, token, agentId, log);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function scheduleSyncRelayReconnect(relayUrl, token, agentId, log) {
|
|
284
|
+
clearSyncRelayReconnect();
|
|
285
|
+
const delay = Math.min(SYNC_RELAY_RECONNECT_BASE_MS * Math.pow(2, syncRelayReconnectAttempt), SYNC_RELAY_RECONNECT_MAX_MS);
|
|
286
|
+
syncRelayReconnectAttempt++;
|
|
287
|
+
log.debug(`Reconnecting to Sync Relay in ${String(delay)}ms (attempt ${String(syncRelayReconnectAttempt)})`);
|
|
288
|
+
syncRelayReconnectTimer = setTimeout(() => {
|
|
289
|
+
(async () => {
|
|
290
|
+
syncRelayWs = await connectToSyncRelay(relayUrl, token, agentId, log);
|
|
291
|
+
})();
|
|
292
|
+
}, delay);
|
|
293
|
+
}
|
|
294
|
+
function disconnectSyncRelay() {
|
|
295
|
+
clearSyncRelayReconnect();
|
|
296
|
+
clearSyncRelayDebounce();
|
|
297
|
+
syncRelayPendingPaths.clear();
|
|
298
|
+
if (syncRelayWs) {
|
|
299
|
+
try {
|
|
300
|
+
syncRelayWs.send(JSON.stringify({ type: "UNSUBSCRIBE" }));
|
|
301
|
+
syncRelayWs.close(1e3, "Plugin deactivating");
|
|
302
|
+
} catch {}
|
|
303
|
+
syncRelayWs = null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const plugin = {
|
|
307
|
+
id: "@alfe.ai/openclaw-sync",
|
|
308
|
+
name: "Alfe Sync Plugin",
|
|
309
|
+
description: "Back up agent configuration, conversations, and memory to the cloud with scheduled or real-time sync.",
|
|
310
|
+
version: "0.1.0",
|
|
311
|
+
async activate(api) {
|
|
312
|
+
globalThis.__alfeSyncPluginActivated = true;
|
|
313
|
+
const log = api.logger;
|
|
314
|
+
log.info("Alfe Sync plugin activating...");
|
|
315
|
+
const pluginConfig = api.config ?? {};
|
|
316
|
+
currentConfig = pluginConfig;
|
|
317
|
+
const workspacePath = pluginConfig.workspacePath ?? process.env.OPENCLAW_WORKSPACE ?? join(homedir(), ".openclaw");
|
|
318
|
+
const syncScope = pluginConfig.syncScope ?? [
|
|
319
|
+
"config",
|
|
320
|
+
"conversations",
|
|
321
|
+
"memory"
|
|
322
|
+
];
|
|
323
|
+
const syncSchedule = pluginConfig.syncSchedule ?? "daily";
|
|
324
|
+
log.info(`Sync scope: ${syncScope.join(", ")}`);
|
|
325
|
+
log.info(`Sync schedule: ${syncSchedule}`);
|
|
326
|
+
log.info(`Workspace: ${workspacePath}`);
|
|
327
|
+
if (isInitialized(workspacePath)) try {
|
|
328
|
+
syncEngine = await createSyncEngine(workspacePath);
|
|
329
|
+
log.info("Sync engine initialized");
|
|
330
|
+
} catch (err) {
|
|
331
|
+
log.warn(`Failed to initialize sync engine: ${err instanceof Error ? err.message : String(err)}`);
|
|
332
|
+
log.warn("Sync will be available once the workspace is configured (alfesync init)");
|
|
333
|
+
}
|
|
334
|
+
else log.info("Workspace not initialized for sync — run alfesync init to enable");
|
|
335
|
+
if (syncSchedule === "realtime" && syncEngine) try {
|
|
336
|
+
stopWatcher = await startWatcher({
|
|
337
|
+
workspacePath,
|
|
338
|
+
debounceMs: 2e3,
|
|
339
|
+
onChanges: async (paths) => {
|
|
340
|
+
if (!syncEngine) return;
|
|
341
|
+
log.debug(`Realtime sync: ${String(paths.length)} file(s) changed`);
|
|
342
|
+
try {
|
|
343
|
+
lastSyncResult = await syncEngine.push(paths, { quiet: true });
|
|
344
|
+
} catch (err) {
|
|
345
|
+
log.error(`Realtime push failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
log.info("File watcher started for realtime sync");
|
|
350
|
+
} catch (err) {
|
|
351
|
+
log.warn(`Failed to start file watcher: ${err instanceof Error ? err.message : String(err)}`);
|
|
352
|
+
}
|
|
353
|
+
if (syncSchedule !== "realtime" && syncEngine) setupSchedule(syncSchedule, log);
|
|
354
|
+
daemonIpcClient = await connectToDaemon(pluginConfig.socketPath ?? process.env.ALFE_GATEWAY_SOCKET ?? DEFAULT_SOCKET_PATH, log);
|
|
355
|
+
if (syncEngine) try {
|
|
356
|
+
const config = await readConfig(workspacePath);
|
|
357
|
+
if (config) {
|
|
358
|
+
const defaultRelayUrl = config.apiUrl.includes("dev.alfe.ai") ? "wss://sync.dev.alfe.ai/ws" : "wss://sync.alfe.ai/ws";
|
|
359
|
+
syncRelayWs = await connectToSyncRelay(pluginConfig.syncRelayUrl ?? process.env.SYNC_RELAY_URL ?? defaultRelayUrl, config.token, config.agentId, log);
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
log.debug(`Sync Relay connection skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
363
|
+
}
|
|
364
|
+
if (typeof api.registerGatewayMethod === "function") {
|
|
365
|
+
api.registerGatewayMethod("sync.now", async () => {
|
|
366
|
+
if (!syncEngine) return {
|
|
367
|
+
ok: false,
|
|
368
|
+
error: "Sync engine not initialized — run alfesync init"
|
|
369
|
+
};
|
|
370
|
+
try {
|
|
371
|
+
lastSyncResult = await syncEngine.fullSync({ quiet: true });
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
result: lastSyncResult
|
|
375
|
+
};
|
|
376
|
+
} catch (err) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
error: err instanceof Error ? err.message : String(err)
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
log.info("Registered gateway RPC method: sync.now");
|
|
384
|
+
api.registerGatewayMethod("sync.status", () => {
|
|
385
|
+
return Promise.resolve({
|
|
386
|
+
ok: true,
|
|
387
|
+
initialized: !!syncEngine,
|
|
388
|
+
schedule: currentConfig.syncSchedule ?? "daily",
|
|
389
|
+
scope: currentConfig.syncScope ?? [
|
|
390
|
+
"config",
|
|
391
|
+
"conversations",
|
|
392
|
+
"memory"
|
|
393
|
+
],
|
|
394
|
+
lastResult: lastSyncResult,
|
|
395
|
+
watcherActive: !!stopWatcher
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
log.info("Registered gateway RPC method: sync.status");
|
|
399
|
+
}
|
|
400
|
+
log.info("Alfe Sync plugin activated");
|
|
401
|
+
},
|
|
402
|
+
async deactivate(api) {
|
|
403
|
+
globalThis.__alfeSyncPluginActivated = false;
|
|
404
|
+
const log = api.logger;
|
|
405
|
+
log.info("Alfe Sync plugin deactivating...");
|
|
406
|
+
clearSchedule();
|
|
407
|
+
disconnectSyncRelay();
|
|
408
|
+
if (stopWatcher) {
|
|
409
|
+
try {
|
|
410
|
+
await stopWatcher();
|
|
411
|
+
log.info("File watcher stopped");
|
|
412
|
+
} catch (err) {
|
|
413
|
+
log.debug(`Error stopping watcher: ${err instanceof Error ? err.message : String(err)}`);
|
|
414
|
+
}
|
|
415
|
+
stopWatcher = null;
|
|
416
|
+
}
|
|
417
|
+
if (daemonIpcClient) {
|
|
418
|
+
try {
|
|
419
|
+
daemonIpcClient.stop();
|
|
420
|
+
log.info("Disconnected from Alfe daemon");
|
|
421
|
+
} catch (err) {
|
|
422
|
+
log.debug(`Error disconnecting from daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
423
|
+
}
|
|
424
|
+
daemonIpcClient = null;
|
|
425
|
+
}
|
|
426
|
+
syncEngine = null;
|
|
427
|
+
lastSyncResult = null;
|
|
428
|
+
currentConfig = {};
|
|
429
|
+
log.info("Alfe Sync plugin deactivated");
|
|
430
|
+
},
|
|
431
|
+
async configure(api, config) {
|
|
432
|
+
const log = api.logger;
|
|
433
|
+
log.info("Reconfiguring Alfe Sync plugin...");
|
|
434
|
+
currentConfig = {
|
|
435
|
+
...currentConfig,
|
|
436
|
+
...config
|
|
437
|
+
};
|
|
438
|
+
if (config.syncSchedule) {
|
|
439
|
+
if (config.syncSchedule === "realtime" && syncEngine && !stopWatcher) {
|
|
440
|
+
stopWatcher = await startWatcher({
|
|
441
|
+
workspacePath: currentConfig.workspacePath ?? process.env.OPENCLAW_WORKSPACE ?? join(homedir(), ".openclaw"),
|
|
442
|
+
debounceMs: 2e3,
|
|
443
|
+
onChanges: async (paths) => {
|
|
444
|
+
if (!syncEngine) return;
|
|
445
|
+
try {
|
|
446
|
+
lastSyncResult = await syncEngine.push(paths, { quiet: true });
|
|
447
|
+
} catch (err) {
|
|
448
|
+
log.error(`Realtime push failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
clearSchedule();
|
|
453
|
+
log.info("Switched to realtime sync");
|
|
454
|
+
} else if (config.syncSchedule !== "realtime") {
|
|
455
|
+
if (stopWatcher) {
|
|
456
|
+
await stopWatcher();
|
|
457
|
+
stopWatcher = null;
|
|
458
|
+
}
|
|
459
|
+
setupSchedule(config.syncSchedule, log);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (config.syncScope) log.info(`Updated sync scope: ${config.syncScope.join(", ")}`);
|
|
463
|
+
log.info("Alfe Sync plugin reconfigured");
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
//#endregion
|
|
467
|
+
export { computeFileHash, configDir, configPath, createApiClient, createSyncEngine, diffManifests, downloadFiles, filterIgnored, isInitialized, loadIgnorePatterns, plugin, readConfig, readManifest, removeManifestEntry, requireConfig, shouldIgnore, startWatcher, updateManifestEntry, uploadFiles, writeConfig, writeManifest };
|
|
468
|
+
|
|
469
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/watcher.ts","../src/plugin.ts"],"sourcesContent":["/**\n * AlfeSync watcher — recursive file watcher with debounce and ignore support.\n *\n * Uses chokidar to watch the workspace root, debounces per-file changes\n * by 2 seconds, and emits batches of changed paths.\n */\n\nimport { watch } from \"chokidar\";\nimport { relative } from \"node:path\";\nimport { loadIgnorePatterns, shouldIgnore } from \"./ignore.js\";\n\nexport interface WatcherOptions {\n workspacePath: string;\n /** Debounce delay per file in milliseconds. Default: 2000 */\n debounceMs?: number;\n /** Callback invoked with batches of changed relative paths */\n onChanges: (paths: string[]) => void | Promise<void>;\n}\n\n/**\n * Start watching a workspace for file changes.\n *\n * Returns a cleanup function to stop watching.\n */\nexport async function startWatcher(\n options: WatcherOptions,\n): Promise<() => Promise<void>> {\n const { workspacePath, debounceMs = 2000, onChanges } = options;\n const ignorePatterns = await loadIgnorePatterns(workspacePath);\n\n // Pending changes map: relativePath → timeout handle\n const pending = new Map<string, ReturnType<typeof setTimeout>>();\n // Batch accumulator for flushing\n let batchPaths = new Set<string>();\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n function scheduleBatch() {\n if (flushTimer) return;\n flushTimer = setTimeout(() => {\n flushTimer = null;\n if (batchPaths.size === 0) return;\n\n const paths = [...batchPaths];\n batchPaths = new Set();\n void onChanges(paths);\n }, debounceMs);\n }\n\n function handleChange(absolutePath: string) {\n const relativePath = relative(workspacePath, absolutePath);\n\n // Skip ignored files\n if (shouldIgnore(relativePath, ignorePatterns)) return;\n\n // Clear existing timer for this file\n const existingTimer = pending.get(relativePath);\n if (existingTimer) clearTimeout(existingTimer);\n\n // Debounce: wait before adding to batch\n const timer = setTimeout(() => {\n pending.delete(relativePath);\n batchPaths.add(relativePath);\n scheduleBatch();\n }, debounceMs);\n\n pending.set(relativePath, timer);\n }\n\n const watcher = watch(workspacePath, {\n persistent: true,\n ignoreInitial: true,\n followSymlinks: false,\n depth: undefined, // unlimited depth\n ignored: [\n \"**/node_modules/**\",\n \"**/.alfesync/**\",\n \"**/.git/**\",\n \"**/.sst/**\",\n ],\n });\n\n watcher.on(\"add\", handleChange);\n watcher.on(\"change\", handleChange);\n watcher.on(\"unlink\", handleChange);\n\n // Return cleanup function\n return async () => {\n // Clear all pending timers\n for (const timer of pending.values()) {\n clearTimeout(timer);\n }\n pending.clear();\n\n if (flushTimer) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n\n await watcher.close();\n };\n}\n","/**\n * @alfe.ai/openclaw-sync — OpenClaw Sync plugin.\n *\n * Wraps the existing sync engine as a lifecycle-managed integration,\n * following the same plugin pattern as @alfe.ai/openclaw-mobile and\n * @alfe.ai/openclaw-discord.\n *\n * Lifecycle:\n * - activate(api): start the sync engine + watcher based on config\n * - deactivate(api): stop the watcher, clean up resources\n * - configure(api, config): update scope/schedule at runtime\n *\n * Registers 'sync.now' and 'sync.status' gateway RPC methods.\n */\n\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { createSyncEngine } from './sync-engine.js';\nimport { startWatcher } from './watcher.js';\nimport { isInitialized, readConfig } from './config.js';\nimport type { SyncEngine, SyncResult } from './sync-engine.js';\n\n// ── Constants ───────────────────────────────────────────────\n\nconst DEFAULT_SOCKET_PATH = join(homedir(), '.alfe', 'gateway.sock');\nconst SYNC_CAPABILITIES = ['sync.push', 'sync.pull', 'sync.fullSync'] as const;\nconst SYNC_RELAY_RECONNECT_BASE_MS = 1000;\nconst SYNC_RELAY_RECONNECT_MAX_MS = 30000;\nconst SYNC_RELAY_DEBOUNCE_MS = 500;\n\n// ── Types ───────────────────────────────────────────────────\n\n/** Logger interface used by the plugin API */\ninterface PluginLogger {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n debug(msg: string): void;\n}\n\n/** Plugin API passed to activate/deactivate/configure */\ninterface PluginApi {\n logger: PluginLogger;\n config?: SyncPluginConfig;\n registerGatewayMethod?: (name: string, handler: () => Promise<unknown>) => void;\n}\n\n/** IPC client interface for daemon communication */\ninterface IpcClient {\n on(event: string, handler: (...args: unknown[]) => void): void;\n request(method: string, params: unknown): Promise<{ ok: boolean; error?: { message: string } }>;\n start(): void;\n stop(): void;\n}\n\n/** WebSocket-like interface for the sync relay */\ninterface SyncRelaySocket {\n on(event: string, handler: (...args: unknown[]) => void): void;\n send(data: string): void;\n close(code: number, reason: string): void;\n}\n\n/** Parsed relay message */\ninterface RelayMessage {\n type: string;\n status?: string;\n agentId?: string;\n message?: string;\n filePath?: string;\n etag?: string;\n eventType?: string;\n}\n\nexport interface SyncPluginConfig {\n /** Workspace path to sync. Defaults to ~/.openclaw */\n workspacePath?: string;\n /** Sync scope — which data categories to sync */\n syncScope?: ('config' | 'conversations' | 'memory')[];\n /** Sync schedule — how often to auto-sync */\n syncSchedule?: 'realtime' | 'hourly' | 'daily' | 'weekly';\n /** IPC socket path override */\n socketPath?: string;\n /** Sync relay URL override (default: wss://sync.alfe.ai/ws) */\n syncRelayUrl?: string;\n}\n\n// ── Plugin State ────────────────────────────────────────────\n\nlet syncEngine: SyncEngine | null = null;\nlet stopWatcher: (() => Promise<void>) | null = null;\nlet daemonIpcClient: IpcClient | null = null;\nlet scheduledInterval: ReturnType<typeof setInterval> | null = null;\nlet currentConfig: SyncPluginConfig = {};\nlet lastSyncResult: SyncResult | null = null;\nlet syncRelayWs: SyncRelaySocket | null = null;\nlet syncRelayReconnectTimer: ReturnType<typeof setTimeout> | null = null;\nlet syncRelayReconnectAttempt = 0;\nlet syncRelayDebounceTimer: ReturnType<typeof setTimeout> | null = null;\nconst syncRelayPendingPaths =\n new Map<string, { etag?: string; eventType: 'created' | 'deleted' }>();\n\n// ── Schedule Helpers ────────────────────────────────────────\n\nconst SCHEDULE_INTERVALS_MS: Record<string, number> = {\n hourly: 60 * 60 * 1000,\n daily: 24 * 60 * 60 * 1000,\n weekly: 7 * 24 * 60 * 60 * 1000,\n};\n\nfunction clearSchedule() {\n if (scheduledInterval) {\n clearInterval(scheduledInterval);\n scheduledInterval = null;\n }\n}\n\nfunction setupSchedule(schedule: string, log: PluginLogger) {\n clearSchedule();\n\n if (schedule === 'realtime') {\n log.info('Sync schedule: realtime (file watcher active)');\n return;\n }\n\n const intervalMs = SCHEDULE_INTERVALS_MS[schedule];\n if (!intervalMs) {\n log.warn(`Unknown sync schedule: ${schedule}, defaulting to hourly`);\n setupSchedule('hourly', log);\n return;\n }\n\n log.info(`Sync schedule: ${schedule} (every ${String(intervalMs / 1000)}s)`);\n scheduledInterval = setInterval(() => {\n if (!syncEngine) return;\n const engine = syncEngine;\n void (async () => {\n try {\n log.info(`Scheduled sync (${schedule}) starting...`);\n lastSyncResult = await engine.fullSync({ quiet: true });\n log.info(\n `Scheduled sync complete: ${String(lastSyncResult.pushed)} pushed, ${String(lastSyncResult.pulled)} pulled`,\n );\n } catch (err: unknown) {\n log.error(`Scheduled sync failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n })();\n }, intervalMs);\n}\n\n// ── Daemon IPC ──────────────────────────────────────────────\n\nasync function connectToDaemon(socketPath: string, log: PluginLogger): Promise<IpcClient | null> {\n try {\n const modPath = '@alfe.ai/openclaw';\n const mod = await import(/* webpackIgnore: true */ modPath) as {\n IPCClient: new (socketPath: string, log: PluginLogger) => IpcClient;\n };\n const IPCClient = mod.IPCClient;\n const client = new IPCClient(socketPath, log);\n\n client.on('connected', () => {\n void (async () => {\n log.info('Connected to Alfe daemon — registering sync capabilities...');\n const response = await client.request('capability.register', {\n plugin: '@alfe.ai/openclaw-sync',\n capabilities: [...SYNC_CAPABILITIES],\n });\n if (response.ok) {\n log.info('Sync capabilities registered with daemon');\n } else {\n log.warn(`Failed to register sync capabilities: ${response.error?.message ?? 'unknown'}`);\n }\n })();\n });\n\n client.on('disconnected', (...args: unknown[]) => {\n const reason = typeof args[0] === 'string' ? args[0] : String(args[0]);\n log.warn(`Disconnected from Alfe daemon: ${reason}`);\n });\n\n client.on('message', (...args: unknown[]) => {\n const msg = args[0] as Record<string, unknown> | undefined;\n if (msg?.type === 'SYNC_NOW' || msg?.command === 'SYNC_NOW') {\n log.info('Received SYNC_NOW command — triggering immediate sync...');\n if (syncEngine) {\n const engine = syncEngine;\n void (async () => {\n try {\n lastSyncResult = await engine.fullSync({ quiet: true });\n log.info(\n `SYNC_NOW complete: ${String(lastSyncResult.pushed)} pushed, ${String(lastSyncResult.pulled)} pulled`,\n );\n } catch (err: unknown) {\n log.error(`SYNC_NOW failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n })();\n }\n }\n });\n\n client.on('error', (...args: unknown[]) => {\n const err = args[0];\n log.debug(`Daemon IPC error: ${err instanceof Error ? err.message : String(err)}`);\n });\n\n client.start();\n return client;\n } catch {\n log.info('Alfe daemon not available — Sync plugin running standalone');\n return null;\n }\n}\n\n// ── Sync Relay Connection ───────────────────────────────────\n\nfunction clearSyncRelayReconnect() {\n if (syncRelayReconnectTimer) {\n clearTimeout(syncRelayReconnectTimer);\n syncRelayReconnectTimer = null;\n }\n}\n\nfunction clearSyncRelayDebounce() {\n if (syncRelayDebounceTimer) {\n clearTimeout(syncRelayDebounceTimer);\n syncRelayDebounceTimer = null;\n }\n}\n\nasync function processPendingNotifications(log: PluginLogger) {\n if (!syncEngine || syncRelayPendingPaths.size === 0) return;\n\n const engine = syncEngine;\n const entries = new Map(syncRelayPendingPaths);\n syncRelayPendingPaths.clear();\n\n const toPull: string[] = [];\n const toDelete: string[] = [];\n\n for (const [filePath, info] of entries) {\n if (info.eventType === 'deleted') {\n toDelete.push(filePath);\n } else {\n toPull.push(filePath);\n }\n }\n\n // Handle deletes\n for (const filePath of toDelete) {\n try {\n await engine.removeLocalFile(filePath, { quiet: true });\n log.debug(`Sync relay: deleted ${filePath}`);\n } catch (err: unknown) {\n log.error(`Sync relay: failed to delete ${filePath}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // Handle pulls\n if (toPull.length > 0) {\n try {\n const result = await engine.pullFiles(toPull, { quiet: true });\n if (result.pulled > 0) {\n log.info(`Sync relay: pulled ${String(result.pulled)} file(s)`);\n }\n if (result.errors > 0) {\n log.warn(`Sync relay: ${String(result.errors)} pull error(s)`);\n }\n } catch (err: unknown) {\n log.error(`Sync relay: pull failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n}\n\nasync function connectToSyncRelay(\n relayUrl: string,\n token: string,\n agentId: string,\n log: PluginLogger,\n): Promise<SyncRelaySocket | null> {\n try {\n const { default: WebSocket } = await import('ws');\n\n const wsUrl = `${relayUrl}?token=${encodeURIComponent(token)}`;\n const ws = new WebSocket(wsUrl);\n\n ws.on('open', () => {\n log.info('Connected to Sync Relay');\n syncRelayReconnectAttempt = 0;\n\n // Subscribe to file changes for our agent\n ws.send(JSON.stringify({ type: 'SUBSCRIBE', agentId }));\n });\n\n ws.on('message', (data: Buffer | string) => {\n let message: RelayMessage;\n try {\n message = JSON.parse(data.toString()) as RelayMessage;\n } catch {\n return;\n }\n\n switch (message.type) {\n case 'SUBSCRIBE_ACK':\n if (message.status === 'ok') {\n log.info(`Subscribed to sync notifications for agent ${message.agentId ?? agentId}`);\n } else {\n log.warn(`Sync relay subscribe failed: ${message.message ?? 'unknown'}`);\n }\n break;\n\n case 'FILE_CHANGED':\n // Accumulate file paths, debounce before acting\n if (message.filePath) {\n syncRelayPendingPaths.set(message.filePath, {\n etag: message.etag,\n eventType: (message.eventType === 'deleted' ? 'deleted' : 'created'),\n });\n }\n\n clearSyncRelayDebounce();\n syncRelayDebounceTimer = setTimeout(() => {\n void processPendingNotifications(log);\n }, SYNC_RELAY_DEBOUNCE_MS);\n break;\n\n case 'PING':\n try {\n ws.send(JSON.stringify({ type: 'PONG' }));\n } catch {\n // Ignore\n }\n break;\n }\n });\n\n ws.on('close', (code: number) => {\n log.info(`Sync Relay disconnected (code=${String(code)})`);\n syncRelayWs = null;\n scheduleSyncRelayReconnect(relayUrl, token, agentId, log);\n });\n\n ws.on('error', (err: Error) => {\n log.debug(`Sync Relay error: ${err.message}`);\n });\n\n return ws as unknown as SyncRelaySocket;\n } catch (err: unknown) {\n log.debug(`Failed to connect to Sync Relay: ${err instanceof Error ? err.message : String(err)}`);\n scheduleSyncRelayReconnect(relayUrl, token, agentId, log);\n return null;\n }\n}\n\nfunction scheduleSyncRelayReconnect(\n relayUrl: string,\n token: string,\n agentId: string,\n log: PluginLogger,\n) {\n clearSyncRelayReconnect();\n\n const delay = Math.min(\n SYNC_RELAY_RECONNECT_BASE_MS * Math.pow(2, syncRelayReconnectAttempt),\n SYNC_RELAY_RECONNECT_MAX_MS,\n );\n syncRelayReconnectAttempt++;\n\n log.debug(`Reconnecting to Sync Relay in ${String(delay)}ms (attempt ${String(syncRelayReconnectAttempt)})`);\n\n syncRelayReconnectTimer = setTimeout(() => {\n void (async () => {\n syncRelayWs = await connectToSyncRelay(relayUrl, token, agentId, log);\n })();\n }, delay);\n}\n\nfunction disconnectSyncRelay() {\n clearSyncRelayReconnect();\n clearSyncRelayDebounce();\n syncRelayPendingPaths.clear();\n\n if (syncRelayWs) {\n try {\n syncRelayWs.send(JSON.stringify({ type: 'UNSUBSCRIBE' }));\n syncRelayWs.close(1000, 'Plugin deactivating');\n } catch {\n // Ignore\n }\n syncRelayWs = null;\n }\n}\n\n// ── Plugin Definition ───────────────────────────────────────\n\nconst plugin = {\n id: '@alfe.ai/openclaw-sync',\n name: 'Alfe Sync Plugin',\n description:\n 'Back up agent configuration, conversations, and memory to the cloud with scheduled or real-time sync.',\n version: '0.1.0',\n\n async activate(api: PluginApi) {\n (globalThis as Record<string, unknown>).__alfeSyncPluginActivated = true;\n\n const log = api.logger;\n log.info('Alfe Sync plugin activating...');\n\n // ── Parse config ────────────────────────────────────────\n const pluginConfig: SyncPluginConfig = api.config ?? {};\n currentConfig = pluginConfig;\n\n const workspacePath =\n pluginConfig.workspacePath ??\n process.env.OPENCLAW_WORKSPACE ??\n join(homedir(), '.openclaw');\n\n const syncScope = pluginConfig.syncScope ?? ['config', 'conversations', 'memory'];\n const syncSchedule = pluginConfig.syncSchedule ?? 'daily';\n\n log.info(`Sync scope: ${syncScope.join(', ')}`);\n log.info(`Sync schedule: ${syncSchedule}`);\n log.info(`Workspace: ${workspacePath}`);\n\n // ── Initialize sync engine (if workspace is configured) ─\n if (isInitialized(workspacePath)) {\n try {\n syncEngine = await createSyncEngine(workspacePath);\n log.info('Sync engine initialized');\n } catch (err: unknown) {\n log.warn(`Failed to initialize sync engine: ${err instanceof Error ? err.message : String(err)}`);\n log.warn('Sync will be available once the workspace is configured (alfesync init)');\n }\n } else {\n log.info('Workspace not initialized for sync — run alfesync init to enable');\n }\n\n // ── Start file watcher for realtime mode ────────────────\n if (syncSchedule === 'realtime' && syncEngine) {\n try {\n stopWatcher = await startWatcher({\n workspacePath,\n debounceMs: 2000,\n onChanges: async (paths) => {\n if (!syncEngine) return;\n log.debug(`Realtime sync: ${String(paths.length)} file(s) changed`);\n try {\n lastSyncResult = await syncEngine.push(paths, { quiet: true });\n } catch (err: unknown) {\n log.error(`Realtime push failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n },\n });\n log.info('File watcher started for realtime sync');\n } catch (err: unknown) {\n log.warn(`Failed to start file watcher: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // ── Setup scheduled sync ────────────────────────────────\n if (syncSchedule !== 'realtime' && syncEngine) {\n setupSchedule(syncSchedule, log);\n }\n\n // ── Connect to Alfe daemon IPC (optional) ───────────────\n const socketPath =\n pluginConfig.socketPath ??\n process.env.ALFE_GATEWAY_SOCKET ??\n DEFAULT_SOCKET_PATH;\n\n daemonIpcClient = await connectToDaemon(socketPath, log);\n\n // ── Connect to Sync Relay for realtime notifications ────\n if (syncEngine) {\n try {\n const config = await readConfig(workspacePath);\n if (config) {\n const defaultRelayUrl =\n config.apiUrl.includes('dev.alfe.ai')\n ? 'wss://sync.dev.alfe.ai/ws'\n : 'wss://sync.alfe.ai/ws';\n const relayUrl =\n pluginConfig.syncRelayUrl ??\n process.env.SYNC_RELAY_URL ??\n defaultRelayUrl;\n\n syncRelayWs = await connectToSyncRelay(\n relayUrl,\n config.token,\n config.agentId,\n log,\n );\n }\n } catch (err: unknown) {\n log.debug(`Sync Relay connection skipped: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // ── Register gateway RPCs ───────────────────────────────\n if (typeof api.registerGatewayMethod === 'function') {\n api.registerGatewayMethod('sync.now', async () => {\n if (!syncEngine) {\n return { ok: false, error: 'Sync engine not initialized — run alfesync init' };\n }\n try {\n lastSyncResult = await syncEngine.fullSync({ quiet: true });\n return { ok: true, result: lastSyncResult };\n } catch (err: unknown) {\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n });\n log.info('Registered gateway RPC method: sync.now');\n\n api.registerGatewayMethod('sync.status', () => {\n return Promise.resolve({\n ok: true,\n initialized: !!syncEngine,\n schedule: currentConfig.syncSchedule ?? 'daily',\n scope: currentConfig.syncScope ?? ['config', 'conversations', 'memory'],\n lastResult: lastSyncResult,\n watcherActive: !!stopWatcher,\n });\n });\n log.info('Registered gateway RPC method: sync.status');\n }\n\n log.info('Alfe Sync plugin activated');\n },\n\n async deactivate(api: PluginApi) {\n (globalThis as Record<string, unknown>).__alfeSyncPluginActivated = false;\n\n const log = api.logger;\n log.info('Alfe Sync plugin deactivating...');\n\n clearSchedule();\n\n // Disconnect from Sync Relay\n disconnectSyncRelay();\n\n if (stopWatcher) {\n try {\n await stopWatcher();\n log.info('File watcher stopped');\n } catch (err: unknown) {\n log.debug(`Error stopping watcher: ${err instanceof Error ? err.message : String(err)}`);\n }\n stopWatcher = null;\n }\n\n if (daemonIpcClient) {\n try {\n daemonIpcClient.stop();\n log.info('Disconnected from Alfe daemon');\n } catch (err: unknown) {\n log.debug(`Error disconnecting from daemon: ${err instanceof Error ? err.message : String(err)}`);\n }\n daemonIpcClient = null;\n }\n\n syncEngine = null;\n lastSyncResult = null;\n currentConfig = {};\n\n log.info('Alfe Sync plugin deactivated');\n },\n\n /**\n * Runtime config update — apply new scope/schedule without full restart.\n */\n async configure(api: PluginApi, config: SyncPluginConfig) {\n const log = api.logger;\n log.info('Reconfiguring Alfe Sync plugin...');\n\n currentConfig = { ...currentConfig, ...config };\n\n if (config.syncSchedule) {\n if (config.syncSchedule === 'realtime' && syncEngine && !stopWatcher) {\n const workspacePath =\n currentConfig.workspacePath ??\n process.env.OPENCLAW_WORKSPACE ??\n join(homedir(), '.openclaw');\n\n stopWatcher = await startWatcher({\n workspacePath,\n debounceMs: 2000,\n onChanges: async (paths) => {\n if (!syncEngine) return;\n try {\n lastSyncResult = await syncEngine.push(paths, { quiet: true });\n } catch (err: unknown) {\n log.error(`Realtime push failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n },\n });\n clearSchedule();\n log.info('Switched to realtime sync');\n } else if (config.syncSchedule !== 'realtime') {\n if (stopWatcher) {\n await stopWatcher();\n stopWatcher = null;\n }\n setupSchedule(config.syncSchedule, log);\n }\n }\n\n if (config.syncScope) {\n log.info(`Updated sync scope: ${config.syncScope.join(', ')}`);\n }\n\n log.info('Alfe Sync plugin reconfigured');\n },\n};\n\nexport default plugin;\n"],"mappings":";;;;;;;;;;;;;;;;;AAwBA,eAAsB,aACpB,SAC8B;CAC9B,MAAM,EAAE,eAAe,aAAa,KAAM,cAAc;CACxD,MAAM,iBAAiB,MAAM,mBAAmB,cAAc;CAG9D,MAAM,0BAAU,IAAI,KAA4C;CAEhE,IAAI,6BAAa,IAAI,KAAa;CAClC,IAAI,aAAmD;CAEvD,SAAS,gBAAgB;AACvB,MAAI,WAAY;AAChB,eAAa,iBAAiB;AAC5B,gBAAa;AACb,OAAI,WAAW,SAAS,EAAG;GAE3B,MAAM,QAAQ,CAAC,GAAG,WAAW;AAC7B,gCAAa,IAAI,KAAK;AACjB,aAAU,MAAM;KACpB,WAAW;;CAGhB,SAAS,aAAa,cAAsB;EAC1C,MAAM,eAAe,SAAS,eAAe,aAAa;AAG1D,MAAI,aAAa,cAAc,eAAe,CAAE;EAGhD,MAAM,gBAAgB,QAAQ,IAAI,aAAa;AAC/C,MAAI,cAAe,cAAa,cAAc;EAG9C,MAAM,QAAQ,iBAAiB;AAC7B,WAAQ,OAAO,aAAa;AAC5B,cAAW,IAAI,aAAa;AAC5B,kBAAe;KACd,WAAW;AAEd,UAAQ,IAAI,cAAc,MAAM;;CAGlC,MAAM,UAAU,MAAM,eAAe;EACnC,YAAY;EACZ,eAAe;EACf,gBAAgB;EAChB,OAAO,KAAA;EACP,SAAS;GACP;GACA;GACA;GACA;GACD;EACF,CAAC;AAEF,SAAQ,GAAG,OAAO,aAAa;AAC/B,SAAQ,GAAG,UAAU,aAAa;AAClC,SAAQ,GAAG,UAAU,aAAa;AAGlC,QAAO,YAAY;AAEjB,OAAK,MAAM,SAAS,QAAQ,QAAQ,CAClC,cAAa,MAAM;AAErB,UAAQ,OAAO;AAEf,MAAI,YAAY;AACd,gBAAa,WAAW;AACxB,gBAAa;;AAGf,QAAM,QAAQ,OAAO;;;;;;;;;;;;;;;;;;;AC1EzB,MAAM,sBAAsB,KAAK,SAAS,EAAE,SAAS,eAAe;AACpE,MAAM,oBAAoB;CAAC;CAAa;CAAa;CAAgB;AACrE,MAAM,+BAA+B;AACrC,MAAM,8BAA8B;AACpC,MAAM,yBAAyB;AA4D/B,IAAI,aAAgC;AACpC,IAAI,cAA4C;AAChD,IAAI,kBAAoC;AACxC,IAAI,oBAA2D;AAC/D,IAAI,gBAAkC,EAAE;AACxC,IAAI,iBAAoC;AACxC,IAAI,cAAsC;AAC1C,IAAI,0BAAgE;AACpE,IAAI,4BAA4B;AAChC,IAAI,yBAA+D;AACnE,MAAM,wCACJ,IAAI,KAAkE;AAIxE,MAAM,wBAAgD;CACpD,QAAQ,OAAU;CAClB,OAAO,OAAU,KAAK;CACtB,QAAQ,QAAc,KAAK;CAC5B;AAED,SAAS,gBAAgB;AACvB,KAAI,mBAAmB;AACrB,gBAAc,kBAAkB;AAChC,sBAAoB;;;AAIxB,SAAS,cAAc,UAAkB,KAAmB;AAC1D,gBAAe;AAEf,KAAI,aAAa,YAAY;AAC3B,MAAI,KAAK,gDAAgD;AACzD;;CAGF,MAAM,aAAa,sBAAsB;AACzC,KAAI,CAAC,YAAY;AACf,MAAI,KAAK,0BAA0B,SAAS,wBAAwB;AACpE,gBAAc,UAAU,IAAI;AAC5B;;AAGF,KAAI,KAAK,kBAAkB,SAAS,UAAU,OAAO,aAAa,IAAK,CAAC,IAAI;AAC5E,qBAAoB,kBAAkB;AACpC,MAAI,CAAC,WAAY;EACjB,MAAM,SAAS;AACf,GAAM,YAAY;AAChB,OAAI;AACF,QAAI,KAAK,mBAAmB,SAAS,eAAe;AACpD,qBAAiB,MAAM,OAAO,SAAS,EAAE,OAAO,MAAM,CAAC;AACvD,QAAI,KACF,4BAA4B,OAAO,eAAe,OAAO,CAAC,WAAW,OAAO,eAAe,OAAO,CAAC,SACpG;YACM,KAAc;AACrB,QAAI,MAAM,0BAA0B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;MAEvF;IACH,WAAW;;AAKhB,eAAe,gBAAgB,YAAoB,KAA8C;AAC/F,KAAI;EAKF,MAAM,aAHM,MAAM,OADF,sBAIM;EACtB,MAAM,SAAS,IAAI,UAAU,YAAY,IAAI;AAE7C,SAAO,GAAG,mBAAmB;AAC3B,IAAM,YAAY;AAChB,QAAI,KAAK,8DAA8D;IACvE,MAAM,WAAW,MAAM,OAAO,QAAQ,uBAAuB;KAC3D,QAAQ;KACR,cAAc,CAAC,GAAG,kBAAkB;KACrC,CAAC;AACF,QAAI,SAAS,GACX,KAAI,KAAK,2CAA2C;QAEpD,KAAI,KAAK,yCAAyC,SAAS,OAAO,WAAW,YAAY;OAEzF;IACJ;AAEF,SAAO,GAAG,iBAAiB,GAAG,SAAoB;GAChD,MAAM,SAAS,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK,OAAO,KAAK,GAAG;AACtE,OAAI,KAAK,kCAAkC,SAAS;IACpD;AAEF,SAAO,GAAG,YAAY,GAAG,SAAoB;GAC3C,MAAM,MAAM,KAAK;AACjB,OAAI,KAAK,SAAS,cAAc,KAAK,YAAY,YAAY;AAC3D,QAAI,KAAK,2DAA2D;AACpE,QAAI,YAAY;KACd,MAAM,SAAS;AACf,MAAM,YAAY;AAChB,UAAI;AACF,wBAAiB,MAAM,OAAO,SAAS,EAAE,OAAO,MAAM,CAAC;AACvD,WAAI,KACF,sBAAsB,OAAO,eAAe,OAAO,CAAC,WAAW,OAAO,eAAe,OAAO,CAAC,SAC9F;eACM,KAAc;AACrB,WAAI,MAAM,oBAAoB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;SAEjF;;;IAGR;AAEF,SAAO,GAAG,UAAU,GAAG,SAAoB;GACzC,MAAM,MAAM,KAAK;AACjB,OAAI,MAAM,qBAAqB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;IAClF;AAEF,SAAO,OAAO;AACd,SAAO;SACD;AACN,MAAI,KAAK,6DAA6D;AACtE,SAAO;;;AAMX,SAAS,0BAA0B;AACjC,KAAI,yBAAyB;AAC3B,eAAa,wBAAwB;AACrC,4BAA0B;;;AAI9B,SAAS,yBAAyB;AAChC,KAAI,wBAAwB;AAC1B,eAAa,uBAAuB;AACpC,2BAAyB;;;AAI7B,eAAe,4BAA4B,KAAmB;AAC5D,KAAI,CAAC,cAAc,sBAAsB,SAAS,EAAG;CAErD,MAAM,SAAS;CACf,MAAM,UAAU,IAAI,IAAI,sBAAsB;AAC9C,uBAAsB,OAAO;CAE7B,MAAM,SAAmB,EAAE;CAC3B,MAAM,WAAqB,EAAE;AAE7B,MAAK,MAAM,CAAC,UAAU,SAAS,QAC7B,KAAI,KAAK,cAAc,UACrB,UAAS,KAAK,SAAS;KAEvB,QAAO,KAAK,SAAS;AAKzB,MAAK,MAAM,YAAY,SACrB,KAAI;AACF,QAAM,OAAO,gBAAgB,UAAU,EAAE,OAAO,MAAM,CAAC;AACvD,MAAI,MAAM,uBAAuB,WAAW;UACrC,KAAc;AACrB,MAAI,MAAM,gCAAgC,SAAS,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAK9G,KAAI,OAAO,SAAS,EAClB,KAAI;EACF,MAAM,SAAS,MAAM,OAAO,UAAU,QAAQ,EAAE,OAAO,MAAM,CAAC;AAC9D,MAAI,OAAO,SAAS,EAClB,KAAI,KAAK,sBAAsB,OAAO,OAAO,OAAO,CAAC,UAAU;AAEjE,MAAI,OAAO,SAAS,EAClB,KAAI,KAAK,eAAe,OAAO,OAAO,OAAO,CAAC,gBAAgB;UAEzD,KAAc;AACrB,MAAI,MAAM,4BAA4B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;;AAK/F,eAAe,mBACb,UACA,OACA,SACA,KACiC;AACjC,KAAI;EACF,MAAM,EAAE,SAAS,cAAc,MAAM,OAAO;EAG5C,MAAM,KAAK,IAAI,UADD,GAAG,SAAS,SAAS,mBAAmB,MAAM,GAC7B;AAE/B,KAAG,GAAG,cAAc;AAClB,OAAI,KAAK,0BAA0B;AACnC,+BAA4B;AAG5B,MAAG,KAAK,KAAK,UAAU;IAAE,MAAM;IAAa;IAAS,CAAC,CAAC;IACvD;AAEF,KAAG,GAAG,YAAY,SAA0B;GAC1C,IAAI;AACJ,OAAI;AACF,cAAU,KAAK,MAAM,KAAK,UAAU,CAAC;WAC/B;AACN;;AAGF,WAAQ,QAAQ,MAAhB;IACE,KAAK;AACH,SAAI,QAAQ,WAAW,KACrB,KAAI,KAAK,8CAA8C,QAAQ,WAAW,UAAU;SAEpF,KAAI,KAAK,gCAAgC,QAAQ,WAAW,YAAY;AAE1E;IAEF,KAAK;AAEH,SAAI,QAAQ,SACV,uBAAsB,IAAI,QAAQ,UAAU;MAC1C,MAAM,QAAQ;MACd,WAAY,QAAQ,cAAc,YAAY,YAAY;MAC3D,CAAC;AAGJ,6BAAwB;AACxB,8BAAyB,iBAAiB;AACnC,kCAA4B,IAAI;QACpC,uBAAuB;AAC1B;IAEF,KAAK;AACH,SAAI;AACF,SAAG,KAAK,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC,CAAC;aACnC;AAGR;;IAEJ;AAEF,KAAG,GAAG,UAAU,SAAiB;AAC/B,OAAI,KAAK,iCAAiC,OAAO,KAAK,CAAC,GAAG;AAC1D,iBAAc;AACd,8BAA2B,UAAU,OAAO,SAAS,IAAI;IACzD;AAEF,KAAG,GAAG,UAAU,QAAe;AAC7B,OAAI,MAAM,qBAAqB,IAAI,UAAU;IAC7C;AAEF,SAAO;UACA,KAAc;AACrB,MAAI,MAAM,oCAAoC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;AACjG,6BAA2B,UAAU,OAAO,SAAS,IAAI;AACzD,SAAO;;;AAIX,SAAS,2BACP,UACA,OACA,SACA,KACA;AACA,0BAAyB;CAEzB,MAAM,QAAQ,KAAK,IACjB,+BAA+B,KAAK,IAAI,GAAG,0BAA0B,EACrE,4BACD;AACD;AAEA,KAAI,MAAM,iCAAiC,OAAO,MAAM,CAAC,cAAc,OAAO,0BAA0B,CAAC,GAAG;AAE5G,2BAA0B,iBAAiB;AACzC,GAAM,YAAY;AAChB,iBAAc,MAAM,mBAAmB,UAAU,OAAO,SAAS,IAAI;MACnE;IACH,MAAM;;AAGX,SAAS,sBAAsB;AAC7B,0BAAyB;AACzB,yBAAwB;AACxB,uBAAsB,OAAO;AAE7B,KAAI,aAAa;AACf,MAAI;AACF,eAAY,KAAK,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC,CAAC;AACzD,eAAY,MAAM,KAAM,sBAAsB;UACxC;AAGR,gBAAc;;;AAMlB,MAAM,SAAS;CACb,IAAI;CACJ,MAAM;CACN,aACE;CACF,SAAS;CAET,MAAM,SAAS,KAAgB;AAC5B,aAAuC,4BAA4B;EAEpE,MAAM,MAAM,IAAI;AAChB,MAAI,KAAK,iCAAiC;EAG1C,MAAM,eAAiC,IAAI,UAAU,EAAE;AACvD,kBAAgB;EAEhB,MAAM,gBACJ,aAAa,iBACb,QAAQ,IAAI,sBACZ,KAAK,SAAS,EAAE,YAAY;EAE9B,MAAM,YAAY,aAAa,aAAa;GAAC;GAAU;GAAiB;GAAS;EACjF,MAAM,eAAe,aAAa,gBAAgB;AAElD,MAAI,KAAK,eAAe,UAAU,KAAK,KAAK,GAAG;AAC/C,MAAI,KAAK,kBAAkB,eAAe;AAC1C,MAAI,KAAK,cAAc,gBAAgB;AAGvC,MAAI,cAAc,cAAc,CAC9B,KAAI;AACF,gBAAa,MAAM,iBAAiB,cAAc;AAClD,OAAI,KAAK,0BAA0B;WAC5B,KAAc;AACrB,OAAI,KAAK,qCAAqC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;AACjG,OAAI,KAAK,0EAA0E;;MAGrF,KAAI,KAAK,mEAAmE;AAI9E,MAAI,iBAAiB,cAAc,WACjC,KAAI;AACF,iBAAc,MAAM,aAAa;IAC/B;IACA,YAAY;IACZ,WAAW,OAAO,UAAU;AAC1B,SAAI,CAAC,WAAY;AACjB,SAAI,MAAM,kBAAkB,OAAO,MAAM,OAAO,CAAC,kBAAkB;AACnE,SAAI;AACF,uBAAiB,MAAM,WAAW,KAAK,OAAO,EAAE,OAAO,MAAM,CAAC;cACvD,KAAc;AACrB,UAAI,MAAM,yBAAyB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;;IAG3F,CAAC;AACF,OAAI,KAAK,yCAAyC;WAC3C,KAAc;AACrB,OAAI,KAAK,iCAAiC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAKjG,MAAI,iBAAiB,cAAc,WACjC,eAAc,cAAc,IAAI;AASlC,oBAAkB,MAAM,gBAJtB,aAAa,cACb,QAAQ,IAAI,uBACZ,qBAEkD,IAAI;AAGxD,MAAI,WACF,KAAI;GACF,MAAM,SAAS,MAAM,WAAW,cAAc;AAC9C,OAAI,QAAQ;IACV,MAAM,kBACJ,OAAO,OAAO,SAAS,cAAc,GACjC,8BACA;AAMN,kBAAc,MAAM,mBAJlB,aAAa,gBACb,QAAQ,IAAI,kBACZ,iBAIA,OAAO,OACP,OAAO,SACP,IACD;;WAEI,KAAc;AACrB,OAAI,MAAM,kCAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAKnG,MAAI,OAAO,IAAI,0BAA0B,YAAY;AACnD,OAAI,sBAAsB,YAAY,YAAY;AAChD,QAAI,CAAC,WACH,QAAO;KAAE,IAAI;KAAO,OAAO;KAAmD;AAEhF,QAAI;AACF,sBAAiB,MAAM,WAAW,SAAS,EAAE,OAAO,MAAM,CAAC;AAC3D,YAAO;MAAE,IAAI;MAAM,QAAQ;MAAgB;aACpC,KAAc;AACrB,YAAO;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;MAAE;;KAE/E;AACF,OAAI,KAAK,0CAA0C;AAEnD,OAAI,sBAAsB,qBAAqB;AAC7C,WAAO,QAAQ,QAAQ;KACrB,IAAI;KACJ,aAAa,CAAC,CAAC;KACf,UAAU,cAAc,gBAAgB;KACxC,OAAO,cAAc,aAAa;MAAC;MAAU;MAAiB;MAAS;KACvE,YAAY;KACZ,eAAe,CAAC,CAAC;KAClB,CAAC;KACF;AACF,OAAI,KAAK,6CAA6C;;AAGxD,MAAI,KAAK,6BAA6B;;CAGxC,MAAM,WAAW,KAAgB;AAC9B,aAAuC,4BAA4B;EAEpE,MAAM,MAAM,IAAI;AAChB,MAAI,KAAK,mCAAmC;AAE5C,iBAAe;AAGf,uBAAqB;AAErB,MAAI,aAAa;AACf,OAAI;AACF,UAAM,aAAa;AACnB,QAAI,KAAK,uBAAuB;YACzB,KAAc;AACrB,QAAI,MAAM,2BAA2B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAE1F,iBAAc;;AAGhB,MAAI,iBAAiB;AACnB,OAAI;AACF,oBAAgB,MAAM;AACtB,QAAI,KAAK,gCAAgC;YAClC,KAAc;AACrB,QAAI,MAAM,oCAAoC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAEnG,qBAAkB;;AAGpB,eAAa;AACb,mBAAiB;AACjB,kBAAgB,EAAE;AAElB,MAAI,KAAK,+BAA+B;;CAM1C,MAAM,UAAU,KAAgB,QAA0B;EACxD,MAAM,MAAM,IAAI;AAChB,MAAI,KAAK,oCAAoC;AAE7C,kBAAgB;GAAE,GAAG;GAAe,GAAG;GAAQ;AAE/C,MAAI,OAAO;OACL,OAAO,iBAAiB,cAAc,cAAc,CAAC,aAAa;AAMpE,kBAAc,MAAM,aAAa;KAC/B,eALA,cAAc,iBACd,QAAQ,IAAI,sBACZ,KAAK,SAAS,EAAE,YAAY;KAI5B,YAAY;KACZ,WAAW,OAAO,UAAU;AAC1B,UAAI,CAAC,WAAY;AACjB,UAAI;AACF,wBAAiB,MAAM,WAAW,KAAK,OAAO,EAAE,OAAO,MAAM,CAAC;eACvD,KAAc;AACrB,WAAI,MAAM,yBAAyB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;;KAG3F,CAAC;AACF,mBAAe;AACf,QAAI,KAAK,4BAA4B;cAC5B,OAAO,iBAAiB,YAAY;AAC7C,QAAI,aAAa;AACf,WAAM,aAAa;AACnB,mBAAc;;AAEhB,kBAAc,OAAO,cAAc,IAAI;;;AAI3C,MAAI,OAAO,UACT,KAAI,KAAK,uBAAuB,OAAO,UAAU,KAAK,KAAK,GAAG;AAGhE,MAAI,KAAK,gCAAgC;;CAE5C"}
|