@alfe.ai/openclaw-sync 0.0.13 → 0.0.15

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.
@@ -0,0 +1,749 @@
1
+ import { i as shouldIgnore, r as loadIgnorePatterns } from "./ignore.js";
2
+ import { m as readConfig, p as isInitialized, t as createSyncEngine } from "./sync-engine.js";
3
+ import { createRequire } from "node:module";
4
+ import { mkdir, rm, unlink, writeFile } from "node:fs/promises";
5
+ import { dirname, join, normalize, relative, sep } from "node:path";
6
+ import { watch } from "chokidar";
7
+ import { DEFAULT_SOCKET_PATH, DEFAULT_WORKSPACE_PATH, resolveConfig } from "@alfe.ai/config";
8
+ //#region src/watcher.ts
9
+ /**
10
+ * AlfeSync watcher — recursive file watcher with debounce and ignore support.
11
+ *
12
+ * Uses chokidar to watch the workspace root, debounces per-file changes
13
+ * by 2 seconds, and emits batches of changed paths.
14
+ */
15
+ /**
16
+ * Start watching a workspace for file changes.
17
+ *
18
+ * Returns a cleanup function to stop watching.
19
+ */
20
+ async function startWatcher(options) {
21
+ const { workspacePath, debounceMs = 2e3, onChanges } = options;
22
+ const ignorePatterns = await loadIgnorePatterns(workspacePath);
23
+ const pending = /* @__PURE__ */ new Map();
24
+ let batchPaths = /* @__PURE__ */ new Set();
25
+ let flushTimer = null;
26
+ function scheduleBatch() {
27
+ if (flushTimer) return;
28
+ flushTimer = setTimeout(() => {
29
+ flushTimer = null;
30
+ if (batchPaths.size === 0) return;
31
+ const paths = [...batchPaths];
32
+ batchPaths = /* @__PURE__ */ new Set();
33
+ onChanges(paths);
34
+ }, debounceMs);
35
+ }
36
+ function handleChange(absolutePath) {
37
+ const relativePath = relative(workspacePath, absolutePath);
38
+ if (shouldIgnore(relativePath, ignorePatterns)) return;
39
+ const existingTimer = pending.get(relativePath);
40
+ if (existingTimer) clearTimeout(existingTimer);
41
+ const timer = setTimeout(() => {
42
+ pending.delete(relativePath);
43
+ batchPaths.add(relativePath);
44
+ scheduleBatch();
45
+ }, debounceMs);
46
+ pending.set(relativePath, timer);
47
+ }
48
+ const watcher = watch(workspacePath, {
49
+ persistent: true,
50
+ ignoreInitial: true,
51
+ followSymlinks: false,
52
+ depth: void 0,
53
+ ignored: [
54
+ "**/node_modules/**",
55
+ "**/.alfesync/**",
56
+ "**/.git/**",
57
+ "**/.sst/**"
58
+ ]
59
+ });
60
+ watcher.on("add", handleChange);
61
+ watcher.on("change", handleChange);
62
+ watcher.on("unlink", handleChange);
63
+ return async () => {
64
+ for (const timer of pending.values()) clearTimeout(timer);
65
+ pending.clear();
66
+ if (flushTimer) {
67
+ clearTimeout(flushTimer);
68
+ flushTimer = null;
69
+ }
70
+ await watcher.close();
71
+ };
72
+ }
73
+ //#endregion
74
+ //#region src/shared-sync.ts
75
+ /**
76
+ * Shared file sync engine for org/team/project scoped files.
77
+ *
78
+ * Syncs shared files to a `shared/` directory in the agent's workspace,
79
+ * organized by scope: shared/org/, shared/teams/{id}/, shared/projects/{id}/
80
+ *
81
+ * Uses the agent self-service API (/agents/org/...) with the agent's API key.
82
+ */
83
+ const MAX_SHARED_FILE_SIZE = 100 * 1024 * 1024;
84
+ /** Verify that resolvedPath stays within baseDir. Prevents path traversal. */
85
+ function assertContained(baseDir, resolvedPath) {
86
+ const normalizedBase = normalize(baseDir) + sep;
87
+ const normalizedPath = normalize(resolvedPath);
88
+ if (!normalizedPath.startsWith(normalizedBase) && normalizedPath !== normalize(baseDir)) throw new Error(`Path traversal blocked: ${resolvedPath} escapes ${baseDir}`);
89
+ }
90
+ async function fetchJson(url, token) {
91
+ const response = await fetch(url, { headers: {
92
+ "Authorization": `Bearer ${token}`,
93
+ "Content-Type": "application/json"
94
+ } });
95
+ if (!response.ok) {
96
+ let errorBody = "";
97
+ try {
98
+ errorBody = await response.text();
99
+ } catch {
100
+ errorBody = "(unable to read error body)";
101
+ }
102
+ throw new Error(`HTTP ${String(response.status)}: ${errorBody}`);
103
+ }
104
+ return response.json();
105
+ }
106
+ function createSharedSyncEngine(config, log) {
107
+ let activeScopes = [];
108
+ const sharedDir = join(config.workspacePath, "shared");
109
+ function scopeDir(scope) {
110
+ if (scope.scopeType === "org") return join(sharedDir, "org");
111
+ return join(sharedDir, scope.scopeType === "team" ? "teams" : "projects", scope.scopeId);
112
+ }
113
+ function apiBase() {
114
+ return `${config.apiUrl}/agents/org`;
115
+ }
116
+ async function listRemoteFiles(scope) {
117
+ return (await fetchJson(`${apiBase()}/files/${scope.scopeType}/${scope.scopeId}`, config.token)).files;
118
+ }
119
+ async function downloadFile(scope, filePath, localPath) {
120
+ assertContained(scopeDir(scope), localPath);
121
+ const result = await fetchJson(`${apiBase()}/files/${scope.scopeType}/${scope.scopeId}/${encodeURIComponent(filePath)}/download`, config.token);
122
+ const response = await fetch(result.downloadUrl);
123
+ if (!response.ok) throw new Error(`Download failed: HTTP ${String(response.status)}`);
124
+ const contentLength = parseInt(response.headers.get("content-length") ?? "0", 10);
125
+ if (contentLength > MAX_SHARED_FILE_SIZE) throw new Error(`File too large: ${String(contentLength)} bytes exceeds ${String(MAX_SHARED_FILE_SIZE)} limit`);
126
+ const buffer = Buffer.from(await response.arrayBuffer());
127
+ if (buffer.length > MAX_SHARED_FILE_SIZE) throw new Error(`Downloaded file exceeds size limit: ${String(buffer.length)} bytes`);
128
+ await mkdir(dirname(localPath), { recursive: true });
129
+ await writeFile(localPath, buffer);
130
+ }
131
+ async function syncScope(scope) {
132
+ const dir = scopeDir(scope);
133
+ await mkdir(dir, { recursive: true });
134
+ try {
135
+ const remoteFiles = await listRemoteFiles(scope);
136
+ for (const file of remoteFiles) {
137
+ const localPath = join(dir, file.filePath);
138
+ try {
139
+ await downloadFile(scope, file.filePath, localPath);
140
+ log.debug(`Shared sync: downloaded ${scope.scopeType}/${scope.scopeId}/${file.filePath}`);
141
+ } catch (err) {
142
+ log.error(`Shared sync: failed to download ${file.filePath}: ${err instanceof Error ? err.message : String(err)}`);
143
+ }
144
+ }
145
+ } catch (err) {
146
+ log.error(`Shared sync: failed to list files for ${scope.scopeType}/${scope.scopeId}: ${err instanceof Error ? err.message : String(err)}`);
147
+ }
148
+ }
149
+ function parseScopedPath(filePath) {
150
+ if (filePath.includes("..")) return null;
151
+ const orgMatch = /^shared\/org\/(.+)$/.exec(filePath);
152
+ if (orgMatch) {
153
+ const scope = activeScopes.find((s) => s.scopeType === "org");
154
+ if (scope) return {
155
+ scope,
156
+ relativePath: orgMatch[1]
157
+ };
158
+ }
159
+ const teamMatch = /^shared\/teams\/([^/]+)\/(.+)$/.exec(filePath);
160
+ if (teamMatch) {
161
+ const scope = activeScopes.find((s) => s.scopeType === "team" && s.scopeId === teamMatch[1]);
162
+ if (scope) return {
163
+ scope,
164
+ relativePath: teamMatch[2]
165
+ };
166
+ }
167
+ const projectMatch = /^shared\/projects\/([^/]+)\/(.+)$/.exec(filePath);
168
+ if (projectMatch) {
169
+ const scope = activeScopes.find((s) => s.scopeType === "project" && s.scopeId === projectMatch[1]);
170
+ if (scope) return {
171
+ scope,
172
+ relativePath: projectMatch[2]
173
+ };
174
+ }
175
+ return null;
176
+ }
177
+ return {
178
+ async initialize(scopes) {
179
+ activeScopes = [...scopes];
180
+ log.info(`Shared sync: initializing with ${String(scopes.length)} scope(s)`);
181
+ await mkdir(sharedDir, { recursive: true });
182
+ for (const scope of scopes) await syncScope(scope);
183
+ log.info("Shared sync: initialization complete");
184
+ },
185
+ async updateScopes(newScopes) {
186
+ const oldIds = new Set(activeScopes.map((s) => `${s.scopeType}:${s.scopeId}`));
187
+ const newIds = new Set(newScopes.map((s) => `${s.scopeType}:${s.scopeId}`));
188
+ for (const scope of activeScopes) {
189
+ const key = `${scope.scopeType}:${scope.scopeId}`;
190
+ if (!newIds.has(key)) {
191
+ const dir = scopeDir(scope);
192
+ try {
193
+ await rm(dir, {
194
+ recursive: true,
195
+ force: true
196
+ });
197
+ log.info(`Shared sync: removed scope directory ${dir}`);
198
+ } catch (err) {
199
+ log.warn(`Shared sync: failed to remove ${dir}: ${err instanceof Error ? err.message : String(err)}`);
200
+ }
201
+ }
202
+ }
203
+ for (const scope of newScopes) {
204
+ const key = `${scope.scopeType}:${scope.scopeId}`;
205
+ if (!oldIds.has(key)) {
206
+ log.info(`Shared sync: new scope ${scope.scopeType}/${scope.scopeId} — syncing files`);
207
+ await syncScope(scope);
208
+ }
209
+ }
210
+ activeScopes = [...newScopes];
211
+ },
212
+ async handleNotification(filePath, eventType) {
213
+ const parsed = parseScopedPath(filePath);
214
+ if (!parsed) {
215
+ log.debug(`Shared sync: ignoring notification for unknown path: ${filePath}`);
216
+ return;
217
+ }
218
+ const dir = scopeDir(parsed.scope);
219
+ const localPath = join(dir, parsed.relativePath);
220
+ try {
221
+ assertContained(dir, localPath);
222
+ } catch {
223
+ log.warn(`Shared sync: path traversal blocked for ${filePath}`);
224
+ return;
225
+ }
226
+ if (eventType === "deleted") {
227
+ try {
228
+ await unlink(localPath);
229
+ log.debug(`Shared sync: deleted ${filePath}`);
230
+ } catch {}
231
+ return;
232
+ }
233
+ try {
234
+ await downloadFile(parsed.scope, parsed.relativePath, localPath);
235
+ log.debug(`Shared sync: pulled ${filePath}`);
236
+ } catch (err) {
237
+ log.error(`Shared sync: failed to pull ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
238
+ }
239
+ },
240
+ async fullSync() {
241
+ log.info(`Shared sync: full sync across ${String(activeScopes.length)} scope(s)`);
242
+ for (const scope of activeScopes) await syncScope(scope);
243
+ },
244
+ getScopes() {
245
+ return [...activeScopes];
246
+ }
247
+ };
248
+ }
249
+ //#endregion
250
+ //#region src/plugin.ts
251
+ /**
252
+ * @alfe.ai/openclaw-sync — OpenClaw Sync plugin.
253
+ *
254
+ * Wraps the existing sync engine as a lifecycle-managed integration,
255
+ * following the same plugin pattern as @alfe.ai/openclaw-mobile and
256
+ * @alfe.ai/openclaw-discord.
257
+ *
258
+ * Lifecycle:
259
+ * - activate(api): start the sync engine + watcher based on config
260
+ * - deactivate(api): stop the watcher, clean up resources
261
+ * - configure(api, config): update scope/schedule at runtime
262
+ *
263
+ * Registers 'sync.now' and 'sync.status' gateway RPC methods.
264
+ */
265
+ const pkg = createRequire(import.meta.url)("../package.json");
266
+ const SYNC_CAPABILITIES = [
267
+ "sync.push",
268
+ "sync.pull",
269
+ "sync.fullSync"
270
+ ];
271
+ const SYNC_RELAY_RECONNECT_BASE_MS = 1e3;
272
+ const SYNC_RELAY_RECONNECT_MAX_MS = 3e4;
273
+ const SYNC_RELAY_DEBOUNCE_MS = 500;
274
+ let syncEngine = null;
275
+ let sharedSyncEngine = null;
276
+ let stopWatcher = null;
277
+ let daemonIpcClient = null;
278
+ let scheduledInterval = null;
279
+ let currentConfig = {};
280
+ let lastSyncResult = null;
281
+ let syncRelayWs = null;
282
+ let syncRelayReconnectTimer = null;
283
+ let syncRelayReconnectAttempt = 0;
284
+ let syncRelayDebounceTimer = null;
285
+ const syncRelayPendingPaths = /* @__PURE__ */ new Map();
286
+ const SCHEDULE_INTERVALS_MS = {
287
+ hourly: 3600 * 1e3,
288
+ daily: 1440 * 60 * 1e3,
289
+ weekly: 10080 * 60 * 1e3
290
+ };
291
+ function clearSchedule() {
292
+ if (scheduledInterval) {
293
+ clearInterval(scheduledInterval);
294
+ scheduledInterval = null;
295
+ }
296
+ }
297
+ function setupSchedule(schedule, log) {
298
+ clearSchedule();
299
+ if (schedule === "realtime") {
300
+ log.info("Sync schedule: realtime (file watcher active)");
301
+ return;
302
+ }
303
+ const intervalMs = SCHEDULE_INTERVALS_MS[schedule];
304
+ if (!intervalMs) {
305
+ log.warn(`Unknown sync schedule: ${schedule}, defaulting to hourly`);
306
+ setupSchedule("hourly", log);
307
+ return;
308
+ }
309
+ log.info(`Sync schedule: ${schedule} (every ${String(intervalMs / 1e3)}s)`);
310
+ scheduledInterval = setInterval(() => {
311
+ if (!syncEngine) return;
312
+ const engine = syncEngine;
313
+ (async () => {
314
+ try {
315
+ log.info(`Scheduled sync (${schedule}) starting...`);
316
+ lastSyncResult = await engine.fullSync({ quiet: true });
317
+ log.info(`Scheduled sync complete: ${String(lastSyncResult.pushed)} pushed, ${String(lastSyncResult.pulled)} pulled`);
318
+ } catch (err) {
319
+ log.error(`Scheduled sync failed: ${err instanceof Error ? err.message : String(err)}`);
320
+ }
321
+ })();
322
+ }, intervalMs);
323
+ }
324
+ async function connectToDaemon(socketPath, log) {
325
+ try {
326
+ const IPCClient = (await import("@alfe.ai/openclaw")).IPCClient;
327
+ const client = new IPCClient(socketPath, log);
328
+ client.on("connected", () => {
329
+ (async () => {
330
+ log.info("Connected to Alfe daemon — registering sync capabilities...");
331
+ const response = await client.request("capability.register", {
332
+ plugin: "@alfe.ai/openclaw-sync",
333
+ capabilities: [...SYNC_CAPABILITIES]
334
+ });
335
+ if (response.ok) log.info("Sync capabilities registered with daemon");
336
+ else log.warn(`Failed to register sync capabilities: ${response.error?.message ?? "unknown"}`);
337
+ })();
338
+ });
339
+ client.on("disconnected", (...args) => {
340
+ const reason = typeof args[0] === "string" ? args[0] : String(args[0]);
341
+ log.warn(`Disconnected from Alfe daemon: ${reason}`);
342
+ });
343
+ client.on("message", (...args) => {
344
+ const msg = args[0];
345
+ if (msg?.type === "SYNC_NOW" || msg?.command === "SYNC_NOW") {
346
+ log.info("Received SYNC_NOW command — triggering immediate sync...");
347
+ if (syncEngine) {
348
+ const engine = syncEngine;
349
+ (async () => {
350
+ try {
351
+ lastSyncResult = await engine.fullSync({ quiet: true });
352
+ log.info(`SYNC_NOW complete: ${String(lastSyncResult.pushed)} pushed, ${String(lastSyncResult.pulled)} pulled`);
353
+ } catch (err) {
354
+ log.error(`SYNC_NOW failed: ${err instanceof Error ? err.message : String(err)}`);
355
+ }
356
+ })();
357
+ }
358
+ }
359
+ if (msg?.type === "SHARED_SCOPES") {
360
+ const scopes = msg.scopes;
361
+ const engine = sharedSyncEngine;
362
+ if (scopes && engine) {
363
+ log.info(`Received SHARED_SCOPES update: ${String(scopes.length)} scope(s)`);
364
+ (async () => {
365
+ try {
366
+ await engine.updateScopes(scopes);
367
+ } catch (err) {
368
+ log.error(`SHARED_SCOPES update failed: ${err instanceof Error ? err.message : String(err)}`);
369
+ }
370
+ })();
371
+ }
372
+ }
373
+ });
374
+ client.on("error", (...args) => {
375
+ const err = args[0];
376
+ log.debug(`Daemon IPC error: ${err instanceof Error ? err.message : String(err)}`);
377
+ });
378
+ client.start();
379
+ return client;
380
+ } catch {
381
+ log.info("Alfe daemon not available — Sync plugin running standalone");
382
+ return null;
383
+ }
384
+ }
385
+ function clearSyncRelayReconnect() {
386
+ if (syncRelayReconnectTimer) {
387
+ clearTimeout(syncRelayReconnectTimer);
388
+ syncRelayReconnectTimer = null;
389
+ }
390
+ }
391
+ function clearSyncRelayDebounce() {
392
+ if (syncRelayDebounceTimer) {
393
+ clearTimeout(syncRelayDebounceTimer);
394
+ syncRelayDebounceTimer = null;
395
+ }
396
+ }
397
+ async function processPendingNotifications(log) {
398
+ if (syncRelayPendingPaths.size === 0) return;
399
+ const entries = new Map(syncRelayPendingPaths);
400
+ syncRelayPendingPaths.clear();
401
+ const sharedEntries = /* @__PURE__ */ new Map();
402
+ const privateEntries = /* @__PURE__ */ new Map();
403
+ for (const [filePath, info] of entries) if (filePath.startsWith("shared/")) sharedEntries.set(filePath, info);
404
+ else privateEntries.set(filePath, info);
405
+ if (sharedEntries.size > 0 && sharedSyncEngine) for (const [filePath, info] of sharedEntries) try {
406
+ await sharedSyncEngine.handleNotification(filePath, info.eventType);
407
+ } catch (err) {
408
+ log.error(`Shared sync relay: failed ${info.eventType} ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
409
+ }
410
+ if (privateEntries.size > 0 && syncEngine) {
411
+ const engine = syncEngine;
412
+ const toPull = [];
413
+ const toDelete = [];
414
+ for (const [filePath, info] of privateEntries) if (info.eventType === "deleted") toDelete.push(filePath);
415
+ else toPull.push(filePath);
416
+ for (const filePath of toDelete) try {
417
+ await engine.removeLocalFile(filePath, { quiet: true });
418
+ log.debug(`Sync relay: deleted ${filePath}`);
419
+ } catch (err) {
420
+ log.error(`Sync relay: failed to delete ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
421
+ }
422
+ if (toPull.length > 0) try {
423
+ const result = await engine.pullFiles(toPull, { quiet: true });
424
+ if (result.pulled > 0) log.info(`Sync relay: pulled ${String(result.pulled)} file(s)`);
425
+ if (result.errors > 0) log.warn(`Sync relay: ${String(result.errors)} pull error(s)`);
426
+ } catch (err) {
427
+ log.error(`Sync relay: pull failed: ${err instanceof Error ? err.message : String(err)}`);
428
+ }
429
+ }
430
+ }
431
+ async function connectToSyncRelay(relayUrl, token, agentId, log) {
432
+ try {
433
+ const { default: WebSocket } = await import("ws");
434
+ const ws = new WebSocket(`${relayUrl}?token=${encodeURIComponent(token)}`);
435
+ ws.on("open", () => {
436
+ log.info("Connected to Sync Relay");
437
+ syncRelayReconnectAttempt = 0;
438
+ ws.send(JSON.stringify({
439
+ type: "SUBSCRIBE",
440
+ agentId
441
+ }));
442
+ });
443
+ ws.on("message", (data) => {
444
+ let message;
445
+ try {
446
+ message = JSON.parse(data.toString());
447
+ } catch {
448
+ return;
449
+ }
450
+ switch (message.type) {
451
+ case "SUBSCRIBE_ACK":
452
+ if (message.status === "ok") {
453
+ log.info(`Subscribed to sync notifications for agent ${message.agentId ?? agentId}`);
454
+ const sharedEngine = sharedSyncEngine;
455
+ if (sharedEngine) (async () => {
456
+ try {
457
+ await sharedEngine.fullSync();
458
+ log.info("Shared sync: reconnect full sync complete");
459
+ } catch (err) {
460
+ log.error(`Shared sync: reconnect full sync failed: ${err instanceof Error ? err.message : String(err)}`);
461
+ }
462
+ })();
463
+ } else log.warn(`Sync relay subscribe failed: ${message.message ?? "unknown"}`);
464
+ break;
465
+ case "FILE_CHANGED":
466
+ if (message.filePath) syncRelayPendingPaths.set(message.filePath, {
467
+ etag: message.etag,
468
+ eventType: message.eventType === "deleted" ? "deleted" : "created"
469
+ });
470
+ clearSyncRelayDebounce();
471
+ syncRelayDebounceTimer = setTimeout(() => {
472
+ processPendingNotifications(log);
473
+ }, SYNC_RELAY_DEBOUNCE_MS);
474
+ break;
475
+ case "PING":
476
+ try {
477
+ ws.send(JSON.stringify({ type: "PONG" }));
478
+ } catch {}
479
+ break;
480
+ }
481
+ });
482
+ ws.on("close", (code) => {
483
+ log.info(`Sync Relay disconnected (code=${String(code)})`);
484
+ syncRelayWs = null;
485
+ scheduleSyncRelayReconnect(relayUrl, token, agentId, log);
486
+ });
487
+ ws.on("error", (err) => {
488
+ log.debug(`Sync Relay error: ${err.message}`);
489
+ });
490
+ return ws;
491
+ } catch (err) {
492
+ log.debug(`Failed to connect to Sync Relay: ${err instanceof Error ? err.message : String(err)}`);
493
+ scheduleSyncRelayReconnect(relayUrl, token, agentId, log);
494
+ return null;
495
+ }
496
+ }
497
+ function scheduleSyncRelayReconnect(relayUrl, token, agentId, log) {
498
+ clearSyncRelayReconnect();
499
+ const delay = Math.min(SYNC_RELAY_RECONNECT_BASE_MS * Math.pow(2, syncRelayReconnectAttempt), SYNC_RELAY_RECONNECT_MAX_MS);
500
+ syncRelayReconnectAttempt++;
501
+ log.debug(`Reconnecting to Sync Relay in ${String(delay)}ms (attempt ${String(syncRelayReconnectAttempt)})`);
502
+ syncRelayReconnectTimer = setTimeout(() => {
503
+ (async () => {
504
+ syncRelayWs = await connectToSyncRelay(relayUrl, token, agentId, log);
505
+ })();
506
+ }, delay);
507
+ }
508
+ function disconnectSyncRelay() {
509
+ clearSyncRelayReconnect();
510
+ clearSyncRelayDebounce();
511
+ syncRelayPendingPaths.clear();
512
+ if (syncRelayWs) {
513
+ try {
514
+ syncRelayWs.send(JSON.stringify({ type: "UNSUBSCRIBE" }));
515
+ syncRelayWs.close(1e3, "Plugin deactivating");
516
+ } catch {}
517
+ syncRelayWs = null;
518
+ }
519
+ }
520
+ const plugin = {
521
+ id: "@alfe.ai/openclaw-sync",
522
+ name: "Alfe Sync Plugin",
523
+ description: "Back up agent configuration, conversations, and memory to the cloud with scheduled or real-time sync.",
524
+ version: pkg.version,
525
+ activate(api) {
526
+ const log = api.logger;
527
+ const pluginConfig = api.config ?? {};
528
+ currentConfig = pluginConfig;
529
+ let alfeConfig = null;
530
+ try {
531
+ alfeConfig = resolveConfig();
532
+ } catch {}
533
+ const workspacePath = pluginConfig.workspacePath ?? alfeConfig?.workspacePath ?? DEFAULT_WORKSPACE_PATH;
534
+ const syncScope = pluginConfig.syncScope ?? [
535
+ "config",
536
+ "conversations",
537
+ "memory"
538
+ ];
539
+ const syncSchedule = pluginConfig.syncSchedule ?? "daily";
540
+ const socketPath = pluginConfig.socketPath ?? alfeConfig?.socketPath ?? DEFAULT_SOCKET_PATH;
541
+ const startSyncService = async () => {
542
+ if (globalThis.__alfeSyncPluginActivated === true) {
543
+ log.debug("Alfe Sync plugin already activated — skipping duplicate");
544
+ return;
545
+ }
546
+ globalThis.__alfeSyncPluginActivated = true;
547
+ log.info("Alfe Sync plugin activating...");
548
+ log.info(`Sync scope: ${syncScope.join(", ")}`);
549
+ log.info(`Sync schedule: ${syncSchedule}`);
550
+ log.info(`Workspace: ${workspacePath}`);
551
+ if (isInitialized(workspacePath)) try {
552
+ syncEngine = await createSyncEngine(workspacePath);
553
+ log.info("Sync engine initialized");
554
+ } catch (err) {
555
+ log.warn(`Failed to initialize sync engine: ${err instanceof Error ? err.message : String(err)}`);
556
+ log.warn("Sync will be available once the workspace is configured (alfesync init)");
557
+ }
558
+ else log.info("Workspace not initialized for sync — run alfesync init to enable");
559
+ if (syncSchedule === "realtime" && syncEngine) try {
560
+ stopWatcher = await startWatcher({
561
+ workspacePath,
562
+ debounceMs: 2e3,
563
+ onChanges: async (paths) => {
564
+ if (!syncEngine) return;
565
+ log.debug(`Realtime sync: ${String(paths.length)} file(s) changed`);
566
+ try {
567
+ lastSyncResult = await syncEngine.push(paths, { quiet: true });
568
+ } catch (err) {
569
+ log.error(`Realtime push failed: ${err instanceof Error ? err.message : String(err)}`);
570
+ }
571
+ }
572
+ });
573
+ log.info("File watcher started for realtime sync");
574
+ } catch (err) {
575
+ log.warn(`Failed to start file watcher: ${err instanceof Error ? err.message : String(err)}`);
576
+ }
577
+ if (syncSchedule !== "realtime" && syncEngine) setupSchedule(syncSchedule, log);
578
+ daemonIpcClient = await connectToDaemon(socketPath, log);
579
+ if (syncEngine) try {
580
+ const config = await readConfig(workspacePath);
581
+ if (config) {
582
+ const defaultRelayUrl = config.apiUrl.includes("dev.alfe.ai") ? "wss://sync.dev.alfe.ai/ws" : "wss://sync.alfe.ai/ws";
583
+ syncRelayWs = await connectToSyncRelay(pluginConfig.syncRelayUrl ?? defaultRelayUrl, config.token, config.agentId, log);
584
+ }
585
+ } catch (err) {
586
+ log.debug(`Sync Relay connection skipped: ${err instanceof Error ? err.message : String(err)}`);
587
+ }
588
+ if (syncEngine && pluginConfig.sharedSync !== false) try {
589
+ const sharedConfig = await readConfig(workspacePath);
590
+ if (sharedConfig) {
591
+ sharedSyncEngine = createSharedSyncEngine({
592
+ workspacePath,
593
+ apiUrl: sharedConfig.apiUrl,
594
+ token: sharedConfig.token,
595
+ agentId: sharedConfig.agentId
596
+ }, log);
597
+ log.info("Shared sync engine created — waiting for SHARED_SCOPES from gateway");
598
+ }
599
+ } catch (err) {
600
+ log.debug(`Shared sync engine skipped: ${err instanceof Error ? err.message : String(err)}`);
601
+ }
602
+ };
603
+ const stopSyncService = async () => {
604
+ globalThis.__alfeSyncPluginActivated = false;
605
+ clearSchedule();
606
+ disconnectSyncRelay();
607
+ if (stopWatcher) {
608
+ try {
609
+ await stopWatcher();
610
+ log.info("File watcher stopped");
611
+ } catch (err) {
612
+ log.debug(`Error stopping watcher: ${err instanceof Error ? err.message : String(err)}`);
613
+ }
614
+ stopWatcher = null;
615
+ }
616
+ if (daemonIpcClient) {
617
+ try {
618
+ daemonIpcClient.stop();
619
+ log.info("Disconnected from Alfe daemon");
620
+ } catch (err) {
621
+ log.debug(`Error disconnecting from daemon: ${err instanceof Error ? err.message : String(err)}`);
622
+ }
623
+ daemonIpcClient = null;
624
+ }
625
+ syncEngine = null;
626
+ sharedSyncEngine = null;
627
+ lastSyncResult = null;
628
+ currentConfig = {};
629
+ log.info("Alfe Sync plugin deactivated");
630
+ };
631
+ if (typeof api.registerGatewayMethod === "function") {
632
+ api.registerGatewayMethod("sync.now", async () => {
633
+ if (!syncEngine) return {
634
+ ok: false,
635
+ error: "Sync engine not initialized — run alfesync init"
636
+ };
637
+ try {
638
+ lastSyncResult = await syncEngine.fullSync({ quiet: true });
639
+ return {
640
+ ok: true,
641
+ result: lastSyncResult
642
+ };
643
+ } catch (err) {
644
+ return {
645
+ ok: false,
646
+ error: err instanceof Error ? err.message : String(err)
647
+ };
648
+ }
649
+ });
650
+ log.info("Registered gateway RPC method: sync.now");
651
+ api.registerGatewayMethod("sync.status", () => {
652
+ return Promise.resolve({
653
+ ok: true,
654
+ initialized: !!syncEngine,
655
+ schedule: currentConfig.syncSchedule ?? "daily",
656
+ scope: currentConfig.syncScope ?? [
657
+ "config",
658
+ "conversations",
659
+ "memory"
660
+ ],
661
+ lastResult: lastSyncResult,
662
+ watcherActive: !!stopWatcher
663
+ });
664
+ });
665
+ log.info("Registered gateway RPC method: sync.status");
666
+ }
667
+ if (api.registerService) api.registerService({
668
+ id: "alfe-sync-engine",
669
+ start: () => startSyncService(),
670
+ stop: () => stopSyncService()
671
+ });
672
+ else startSyncService().catch((err) => {
673
+ log.error(`Sync plugin async init failed: ${err instanceof Error ? err.message : String(err)}`);
674
+ });
675
+ log.info("Alfe Sync plugin activated");
676
+ },
677
+ async deactivate(api) {
678
+ globalThis.__alfeSyncPluginActivated = false;
679
+ const log = api.logger;
680
+ log.info("Alfe Sync plugin deactivating...");
681
+ clearSchedule();
682
+ disconnectSyncRelay();
683
+ if (stopWatcher) {
684
+ try {
685
+ await stopWatcher();
686
+ log.info("File watcher stopped");
687
+ } catch (err) {
688
+ log.debug(`Error stopping watcher: ${err instanceof Error ? err.message : String(err)}`);
689
+ }
690
+ stopWatcher = null;
691
+ }
692
+ if (daemonIpcClient) {
693
+ try {
694
+ daemonIpcClient.stop();
695
+ log.info("Disconnected from Alfe daemon");
696
+ } catch (err) {
697
+ log.debug(`Error disconnecting from daemon: ${err instanceof Error ? err.message : String(err)}`);
698
+ }
699
+ daemonIpcClient = null;
700
+ }
701
+ syncEngine = null;
702
+ sharedSyncEngine = null;
703
+ lastSyncResult = null;
704
+ currentConfig = {};
705
+ log.info("Alfe Sync plugin deactivated");
706
+ },
707
+ async configure(api, config) {
708
+ const log = api.logger;
709
+ log.info("Reconfiguring Alfe Sync plugin...");
710
+ currentConfig = {
711
+ ...currentConfig,
712
+ ...config
713
+ };
714
+ if (config.syncSchedule) {
715
+ if (config.syncSchedule === "realtime" && syncEngine && !stopWatcher) {
716
+ let cfgForWorkspace = null;
717
+ try {
718
+ cfgForWorkspace = resolveConfig();
719
+ } catch {}
720
+ stopWatcher = await startWatcher({
721
+ workspacePath: currentConfig.workspacePath ?? cfgForWorkspace?.workspacePath ?? "",
722
+ debounceMs: 2e3,
723
+ onChanges: async (paths) => {
724
+ if (!syncEngine) return;
725
+ try {
726
+ lastSyncResult = await syncEngine.push(paths, { quiet: true });
727
+ } catch (err) {
728
+ log.error(`Realtime push failed: ${err instanceof Error ? err.message : String(err)}`);
729
+ }
730
+ }
731
+ });
732
+ clearSchedule();
733
+ log.info("Switched to realtime sync");
734
+ } else if (config.syncSchedule !== "realtime") {
735
+ if (stopWatcher) {
736
+ await stopWatcher();
737
+ stopWatcher = null;
738
+ }
739
+ setupSchedule(config.syncSchedule, log);
740
+ }
741
+ }
742
+ if (config.syncScope) log.info(`Updated sync scope: ${config.syncScope.join(", ")}`);
743
+ log.info("Alfe Sync plugin reconfigured");
744
+ }
745
+ };
746
+ //#endregion
747
+ export { createSharedSyncEngine as n, startWatcher as r, plugin as t };
748
+
749
+ //# sourceMappingURL=plugin2.js.map