@alfe.ai/openclaw-sync 0.0.15 → 0.0.17

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/dist/plugin2.cjs CHANGED
@@ -1,10 +1,10 @@
1
- const require_ignore = require("./ignore.cjs");
2
1
  const require_sync_engine = require("./sync-engine.cjs");
3
2
  let node_fs_promises = require("node:fs/promises");
4
3
  let node_path = require("node:path");
5
4
  let chokidar = require("chokidar");
6
- let _alfe_ai_config = require("@alfe.ai/config");
7
5
  let node_module = require("node:module");
6
+ let _alfe_ai_config = require("@alfe.ai/config");
7
+ let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
8
8
  //#region src/watcher.ts
9
9
  /**
10
10
  * AlfeSync watcher — recursive file watcher with debounce and ignore support.
@@ -19,7 +19,7 @@ let node_module = require("node:module");
19
19
  */
20
20
  async function startWatcher(options) {
21
21
  const { workspacePath, debounceMs = 2e3, onChanges } = options;
22
- const ignorePatterns = await require_ignore.loadIgnorePatterns(workspacePath);
22
+ const ignorePatterns = await require_sync_engine.loadIgnorePatterns(workspacePath);
23
23
  const pending = /* @__PURE__ */ new Map();
24
24
  let batchPaths = /* @__PURE__ */ new Set();
25
25
  let flushTimer = null;
@@ -35,7 +35,7 @@ async function startWatcher(options) {
35
35
  }
36
36
  function handleChange(absolutePath) {
37
37
  const relativePath = (0, node_path.relative)(workspacePath, absolutePath);
38
- if (require_ignore.shouldIgnore(relativePath, ignorePatterns)) return;
38
+ if (require_sync_engine.shouldIgnore(relativePath, ignorePatterns)) return;
39
39
  const existingTimer = pending.get(relativePath);
40
40
  if (existingTimer) clearTimeout(existingTimer);
41
41
  const timer = setTimeout(() => {
@@ -73,36 +73,23 @@ async function startWatcher(options) {
73
73
  //#endregion
74
74
  //#region src/shared-sync.ts
75
75
  /**
76
- * Shared file sync engine for org/team/project scoped files.
76
+ * Shared file sync mirrors org/team/project files to a `shared/`
77
+ * directory in the agent's workspace, organised by scope:
77
78
  *
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}/
79
+ * shared/org/<files…>
80
+ * shared/teams/<scopeId>/<files…>
81
+ * shared/projects/<scopeId>/<files…>
80
82
  *
81
- * Uses the agent self-service API (/agents/org/...) with the agent's API key.
83
+ * Backed by the agent self-service API (`AgentApiClient.sharedListFiles`,
84
+ * `AgentApiClient.sharedDownloadUrl`).
82
85
  */
83
86
  const MAX_SHARED_FILE_SIZE = 100 * 1024 * 1024;
84
- /** Verify that resolvedPath stays within baseDir. Prevents path traversal. */
87
+ /** Throw if `resolvedPath` would escape `baseDir`. */
85
88
  function assertContained(baseDir, resolvedPath) {
86
89
  const normalizedBase = (0, node_path.normalize)(baseDir) + node_path.sep;
87
90
  const normalizedPath = (0, node_path.normalize)(resolvedPath);
88
91
  if (!normalizedPath.startsWith(normalizedBase) && normalizedPath !== (0, node_path.normalize)(baseDir)) throw new Error(`Path traversal blocked: ${resolvedPath} escapes ${baseDir}`);
89
92
  }
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
93
  function createSharedSyncEngine(config, log) {
107
94
  let activeScopes = [];
108
95
  const sharedDir = (0, node_path.join)(config.workspacePath, "shared");
@@ -110,16 +97,14 @@ function createSharedSyncEngine(config, log) {
110
97
  if (scope.scopeType === "org") return (0, node_path.join)(sharedDir, "org");
111
98
  return (0, node_path.join)(sharedDir, scope.scopeType === "team" ? "teams" : "projects", scope.scopeId);
112
99
  }
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
100
  async function downloadFile(scope, filePath, localPath) {
120
101
  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);
102
+ const { downloadUrl } = await config.client.sharedDownloadUrl({
103
+ scope: scope.scopeType,
104
+ scopeId: scope.scopeId,
105
+ filePath
106
+ });
107
+ const response = await fetch(downloadUrl);
123
108
  if (!response.ok) throw new Error(`Download failed: HTTP ${String(response.status)}`);
124
109
  const contentLength = parseInt(response.headers.get("content-length") ?? "0", 10);
125
110
  if (contentLength > MAX_SHARED_FILE_SIZE) throw new Error(`File too large: ${String(contentLength)} bytes exceeds ${String(MAX_SHARED_FILE_SIZE)} limit`);
@@ -132,8 +117,11 @@ function createSharedSyncEngine(config, log) {
132
117
  const dir = scopeDir(scope);
133
118
  await (0, node_fs_promises.mkdir)(dir, { recursive: true });
134
119
  try {
135
- const remoteFiles = await listRemoteFiles(scope);
136
- for (const file of remoteFiles) {
120
+ const { files } = await config.client.sharedListFiles({
121
+ scope: scope.scopeType,
122
+ scopeId: scope.scopeId
123
+ });
124
+ for (const file of files) {
137
125
  const localPath = (0, node_path.join)(dir, file.filePath);
138
126
  try {
139
127
  await downloadFile(scope, file.filePath, localPath);
@@ -251,16 +239,19 @@ function createSharedSyncEngine(config, log) {
251
239
  /**
252
240
  * @alfe.ai/openclaw-sync — OpenClaw Sync plugin.
253
241
  *
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.
242
+ * Wraps the sync engine as a lifecycle-managed integration. Same shape as
243
+ * @alfe.ai/openclaw-memory-cloud / -secrets / -google: one AgentApiClient
244
+ * is constructed in `activate()` and reused for every API call. The agent's
245
+ * identity is resolved server-side from the API key — the plugin never
246
+ * touches `/auth/validate`, never plumbs an `agentId` around, never writes
247
+ * a `.alfesync/` directory.
257
248
  *
258
249
  * 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
250
+ * - activate(api): construct client, start sync engine + watcher
251
+ * - deactivate(api): stop watcher, drop relay connections, clean up
252
+ * - configure(api, config): swap schedule/scope at runtime
262
253
  *
263
- * Registers 'sync.now' and 'sync.status' gateway RPC methods.
254
+ * Registers gateway RPC methods: `sync.now`, `sync.status`.
264
255
  */
265
256
  const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
266
257
  const SYNC_CAPABILITIES = [
@@ -271,6 +262,8 @@ const SYNC_CAPABILITIES = [
271
262
  const SYNC_RELAY_RECONNECT_BASE_MS = 1e3;
272
263
  const SYNC_RELAY_RECONNECT_MAX_MS = 3e4;
273
264
  const SYNC_RELAY_DEBOUNCE_MS = 500;
265
+ let client = null;
266
+ let agentId = null;
274
267
  let syncEngine = null;
275
268
  let sharedSyncEngine = null;
276
269
  let stopWatcher = null;
@@ -323,12 +316,11 @@ function setupSchedule(schedule, log) {
323
316
  }
324
317
  async function connectToDaemon(socketPath, log) {
325
318
  try {
326
- const IPCClient = (await import("@alfe.ai/openclaw")).IPCClient;
327
- const client = new IPCClient(socketPath, log);
328
- client.on("connected", () => {
319
+ const ipc = new (await (import("@alfe.ai/openclaw"))).IPCClient(socketPath, log);
320
+ ipc.on("connected", () => {
329
321
  (async () => {
330
322
  log.info("Connected to Alfe daemon — registering sync capabilities...");
331
- const response = await client.request("capability.register", {
323
+ const response = await ipc.request("capability.register", {
332
324
  plugin: "@alfe.ai/openclaw-sync",
333
325
  capabilities: [...SYNC_CAPABILITIES]
334
326
  });
@@ -336,11 +328,11 @@ async function connectToDaemon(socketPath, log) {
336
328
  else log.warn(`Failed to register sync capabilities: ${response.error?.message ?? "unknown"}`);
337
329
  })();
338
330
  });
339
- client.on("disconnected", (...args) => {
331
+ ipc.on("disconnected", (...args) => {
340
332
  const reason = typeof args[0] === "string" ? args[0] : String(args[0]);
341
333
  log.warn(`Disconnected from Alfe daemon: ${reason}`);
342
334
  });
343
- client.on("message", (...args) => {
335
+ ipc.on("message", (...args) => {
344
336
  const msg = args[0];
345
337
  if (msg?.type === "SYNC_NOW" || msg?.command === "SYNC_NOW") {
346
338
  log.info("Received SYNC_NOW command — triggering immediate sync...");
@@ -371,12 +363,12 @@ async function connectToDaemon(socketPath, log) {
371
363
  }
372
364
  }
373
365
  });
374
- client.on("error", (...args) => {
366
+ ipc.on("error", (...args) => {
375
367
  const err = args[0];
376
368
  log.debug(`Daemon IPC error: ${err instanceof Error ? err.message : String(err)}`);
377
369
  });
378
- client.start();
379
- return client;
370
+ ipc.start();
371
+ return ipc;
380
372
  } catch {
381
373
  log.info("Alfe daemon not available — Sync plugin running standalone");
382
374
  return null;
@@ -428,7 +420,7 @@ async function processPendingNotifications(log) {
428
420
  }
429
421
  }
430
422
  }
431
- async function connectToSyncRelay(relayUrl, token, agentId, log) {
423
+ async function connectToSyncRelay(relayUrl, token, agentIdForSubscribe, log) {
432
424
  try {
433
425
  const { default: WebSocket } = await import("ws");
434
426
  const ws = new WebSocket(`${relayUrl}?token=${encodeURIComponent(token)}`);
@@ -437,7 +429,7 @@ async function connectToSyncRelay(relayUrl, token, agentId, log) {
437
429
  syncRelayReconnectAttempt = 0;
438
430
  ws.send(JSON.stringify({
439
431
  type: "SUBSCRIBE",
440
- agentId
432
+ agentId: agentIdForSubscribe
441
433
  }));
442
434
  });
443
435
  ws.on("message", (data) => {
@@ -450,7 +442,7 @@ async function connectToSyncRelay(relayUrl, token, agentId, log) {
450
442
  switch (message.type) {
451
443
  case "SUBSCRIBE_ACK":
452
444
  if (message.status === "ok") {
453
- log.info(`Subscribed to sync notifications for agent ${message.agentId ?? agentId}`);
445
+ log.info(`Subscribed to sync notifications for agent ${message.agentId ?? agentIdForSubscribe}`);
454
446
  const sharedEngine = sharedSyncEngine;
455
447
  if (sharedEngine) (async () => {
456
448
  try {
@@ -468,9 +460,7 @@ async function connectToSyncRelay(relayUrl, token, agentId, log) {
468
460
  eventType: message.eventType === "deleted" ? "deleted" : "created"
469
461
  });
470
462
  clearSyncRelayDebounce();
471
- syncRelayDebounceTimer = setTimeout(() => {
472
- processPendingNotifications(log);
473
- }, SYNC_RELAY_DEBOUNCE_MS);
463
+ syncRelayDebounceTimer = setTimeout(() => void processPendingNotifications(log), SYNC_RELAY_DEBOUNCE_MS);
474
464
  break;
475
465
  case "PING":
476
466
  try {
@@ -482,7 +472,7 @@ async function connectToSyncRelay(relayUrl, token, agentId, log) {
482
472
  ws.on("close", (code) => {
483
473
  log.info(`Sync Relay disconnected (code=${String(code)})`);
484
474
  syncRelayWs = null;
485
- scheduleSyncRelayReconnect(relayUrl, token, agentId, log);
475
+ scheduleSyncRelayReconnect(relayUrl, token, agentIdForSubscribe, log);
486
476
  });
487
477
  ws.on("error", (err) => {
488
478
  log.debug(`Sync Relay error: ${err.message}`);
@@ -490,18 +480,18 @@ async function connectToSyncRelay(relayUrl, token, agentId, log) {
490
480
  return ws;
491
481
  } catch (err) {
492
482
  log.debug(`Failed to connect to Sync Relay: ${err instanceof Error ? err.message : String(err)}`);
493
- scheduleSyncRelayReconnect(relayUrl, token, agentId, log);
483
+ scheduleSyncRelayReconnect(relayUrl, token, agentIdForSubscribe, log);
494
484
  return null;
495
485
  }
496
486
  }
497
- function scheduleSyncRelayReconnect(relayUrl, token, agentId, log) {
487
+ function scheduleSyncRelayReconnect(relayUrl, token, agentIdForSubscribe, log) {
498
488
  clearSyncRelayReconnect();
499
489
  const delay = Math.min(SYNC_RELAY_RECONNECT_BASE_MS * Math.pow(2, syncRelayReconnectAttempt), SYNC_RELAY_RECONNECT_MAX_MS);
500
490
  syncRelayReconnectAttempt++;
501
491
  log.debug(`Reconnecting to Sync Relay in ${String(delay)}ms (attempt ${String(syncRelayReconnectAttempt)})`);
502
492
  syncRelayReconnectTimer = setTimeout(() => {
503
493
  (async () => {
504
- syncRelayWs = await connectToSyncRelay(relayUrl, token, agentId, log);
494
+ syncRelayWs = await connectToSyncRelay(relayUrl, token, agentIdForSubscribe, log);
505
495
  })();
506
496
  }, delay);
507
497
  }
@@ -517,6 +507,12 @@ function disconnectSyncRelay() {
517
507
  syncRelayWs = null;
518
508
  }
519
509
  }
510
+ function deriveRelayUrl(apiUrl) {
511
+ if (apiUrl.includes("dev.alfe.ai")) return "wss://sync.dev.alfe.ai/ws";
512
+ if (apiUrl.includes("demo.alfe.ai")) return "wss://sync.demo.alfe.ai/ws";
513
+ if (apiUrl.includes("test.alfe.ai")) return "wss://sync.test.alfe.ai/ws";
514
+ return "wss://sync.alfe.ai/ws";
515
+ }
520
516
  const plugin = {
521
517
  id: "@alfe.ai/openclaw-sync",
522
518
  name: "Alfe Sync Plugin",
@@ -548,15 +544,27 @@ const plugin = {
548
544
  log.info(`Sync scope: ${syncScope.join(", ")}`);
549
545
  log.info(`Sync schedule: ${syncSchedule}`);
550
546
  log.info(`Workspace: ${workspacePath}`);
551
- if (require_sync_engine.isInitialized(workspacePath)) try {
552
- syncEngine = await require_sync_engine.createSyncEngine(workspacePath);
553
- log.info("Sync engine initialized");
547
+ if (!(0, _alfe_ai_config.configExists)()) {
548
+ log.info("Sync skipped no Alfe config found. Run `alfe login` to enable.");
549
+ return;
550
+ }
551
+ let syncCfg;
552
+ try {
553
+ syncCfg = (0, _alfe_ai_config.resolveConfig)();
554
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)");
555
+ log.warn(`Sync skipped — failed to resolve credentials from ~/.alfe/config.toml: ${err instanceof Error ? err.message : String(err)}`);
556
+ return;
557
557
  }
558
- else log.info("Workspace not initialized for sync — run alfesync init to enable");
559
- if (syncSchedule === "realtime" && syncEngine) try {
558
+ client = new _alfe_ai_agent_api_client.AgentApiClient({
559
+ apiKey: syncCfg.apiKey,
560
+ apiUrl: syncCfg.apiUrl
561
+ });
562
+ syncEngine = require_sync_engine.createSyncEngine({
563
+ workspacePath,
564
+ client
565
+ });
566
+ log.info("Sync engine initialized");
567
+ if (syncSchedule === "realtime") try {
560
568
  stopWatcher = await startWatcher({
561
569
  workspacePath,
562
570
  debounceMs: 2e3,
@@ -574,30 +582,30 @@ const plugin = {
574
582
  } catch (err) {
575
583
  log.warn(`Failed to start file watcher: ${err instanceof Error ? err.message : String(err)}`);
576
584
  }
577
- if (syncSchedule !== "realtime" && syncEngine) setupSchedule(syncSchedule, log);
585
+ else setupSchedule(syncSchedule, log);
578
586
  daemonIpcClient = await connectToDaemon(socketPath, log);
579
- if (syncEngine) try {
580
- const config = await require_sync_engine.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
- }
587
+ let registered = null;
588
+ try {
589
+ registered = (await client.syncRegister()).agent;
590
+ agentId = registered.agentId;
585
591
  } catch (err) {
586
- log.debug(`Sync Relay connection skipped: ${err instanceof Error ? err.message : String(err)}`);
592
+ log.warn(`Sync register failed: ${err instanceof Error ? err.message : String(err)}`);
587
593
  }
588
- if (syncEngine && pluginConfig.sharedSync !== false) try {
589
- const sharedConfig = await require_sync_engine.readConfig(workspacePath);
590
- if (sharedConfig) {
594
+ if (registered) {
595
+ try {
596
+ syncRelayWs = await connectToSyncRelay(pluginConfig.syncRelayUrl ?? deriveRelayUrl(syncCfg.apiUrl), syncCfg.apiKey, registered.agentId, log);
597
+ } catch (err) {
598
+ log.debug(`Sync Relay connection skipped: ${err instanceof Error ? err.message : String(err)}`);
599
+ }
600
+ if (pluginConfig.sharedSync !== false) try {
591
601
  sharedSyncEngine = createSharedSyncEngine({
592
602
  workspacePath,
593
- apiUrl: sharedConfig.apiUrl,
594
- token: sharedConfig.token,
595
- agentId: sharedConfig.agentId
603
+ client
596
604
  }, log);
597
605
  log.info("Shared sync engine created — waiting for SHARED_SCOPES from gateway");
606
+ } catch (err) {
607
+ log.debug(`Shared sync engine skipped: ${err instanceof Error ? err.message : String(err)}`);
598
608
  }
599
- } catch (err) {
600
- log.debug(`Shared sync engine skipped: ${err instanceof Error ? err.message : String(err)}`);
601
609
  }
602
610
  };
603
611
  const stopSyncService = async () => {
@@ -622,6 +630,8 @@ const plugin = {
622
630
  }
623
631
  daemonIpcClient = null;
624
632
  }
633
+ client = null;
634
+ agentId = null;
625
635
  syncEngine = null;
626
636
  sharedSyncEngine = null;
627
637
  lastSyncResult = null;
@@ -632,7 +642,7 @@ const plugin = {
632
642
  api.registerGatewayMethod("sync.now", async () => {
633
643
  if (!syncEngine) return {
634
644
  ok: false,
635
- error: "Sync engine not initialized — run alfesync init"
645
+ error: "Sync engine not initialized — run `alfe login`"
636
646
  };
637
647
  try {
638
648
  lastSyncResult = await syncEngine.fullSync({ quiet: true });
@@ -648,20 +658,19 @@ const plugin = {
648
658
  }
649
659
  });
650
660
  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
- });
661
+ api.registerGatewayMethod("sync.status", () => Promise.resolve({
662
+ ok: true,
663
+ initialized: !!syncEngine,
664
+ agentId,
665
+ schedule: currentConfig.syncSchedule ?? "daily",
666
+ scope: currentConfig.syncScope ?? [
667
+ "config",
668
+ "conversations",
669
+ "memory"
670
+ ],
671
+ lastResult: lastSyncResult,
672
+ watcherActive: !!stopWatcher
673
+ }));
665
674
  log.info("Registered gateway RPC method: sync.status");
666
675
  }
667
676
  if (api.registerService) api.registerService({
@@ -698,6 +707,8 @@ const plugin = {
698
707
  }
699
708
  daemonIpcClient = null;
700
709
  }
710
+ client = null;
711
+ agentId = null;
701
712
  syncEngine = null;
702
713
  sharedSyncEngine = null;
703
714
  lastSyncResult = null;
@@ -713,12 +724,8 @@ const plugin = {
713
724
  };
714
725
  if (config.syncSchedule) {
715
726
  if (config.syncSchedule === "realtime" && syncEngine && !stopWatcher) {
716
- let cfgForWorkspace = null;
717
- try {
718
- cfgForWorkspace = (0, _alfe_ai_config.resolveConfig)();
719
- } catch {}
720
727
  stopWatcher = await startWatcher({
721
- workspacePath: currentConfig.workspacePath ?? cfgForWorkspace?.workspacePath ?? "",
728
+ workspacePath: currentConfig.workspacePath ?? syncEngine.workspacePath,
722
729
  debounceMs: 2e3,
723
730
  onChanges: async (paths) => {
724
731
  if (!syncEngine) return;