@alfe.ai/openclaw-sync 0.0.16 → 0.0.18

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
- let node_path = require("node:path");
4
- let _alfe_ai_config = require("@alfe.ai/config");
5
2
  let node_fs_promises = require("node:fs/promises");
3
+ let node_path = require("node:path");
6
4
  let chokidar = require("chokidar");
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,14 +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()) 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)}`);
555
+ log.warn(`Sync skipped — failed to resolve credentials from ~/.alfe/config.toml: ${err instanceof Error ? err.message : String(err)}`);
556
+ return;
556
557
  }
557
- else log.info("Sync skipped — no Alfe config found. Run `alfe login` to enable.");
558
- 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 {
559
568
  stopWatcher = await startWatcher({
560
569
  workspacePath,
561
570
  debounceMs: 2e3,
@@ -573,28 +582,29 @@ const plugin = {
573
582
  } catch (err) {
574
583
  log.warn(`Failed to start file watcher: ${err instanceof Error ? err.message : String(err)}`);
575
584
  }
576
- if (syncSchedule !== "realtime" && syncEngine) setupSchedule(syncSchedule, log);
585
+ else setupSchedule(syncSchedule, log);
577
586
  daemonIpcClient = await connectToDaemon(socketPath, log);
578
- if (syncEngine) {
579
- const config = await require_sync_engine.resolveSyncConfig();
580
- if (config) {
581
- try {
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
- } catch (err) {
585
- log.debug(`Sync Relay connection skipped: ${err instanceof Error ? err.message : String(err)}`);
586
- }
587
- if (pluginConfig.sharedSync !== false) try {
588
- sharedSyncEngine = createSharedSyncEngine({
589
- workspacePath,
590
- apiUrl: config.apiUrl,
591
- token: config.token,
592
- agentId: config.agentId
593
- }, log);
594
- log.info("Shared sync engine created — waiting for SHARED_SCOPES from gateway");
595
- } catch (err) {
596
- log.debug(`Shared sync engine skipped: ${err instanceof Error ? err.message : String(err)}`);
597
- }
587
+ let registered = null;
588
+ try {
589
+ registered = (await client.syncRegister()).agent;
590
+ agentId = registered.agentId;
591
+ } catch (err) {
592
+ log.warn(`Sync register failed: ${err instanceof Error ? err.message : String(err)}`);
593
+ }
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 {
601
+ sharedSyncEngine = createSharedSyncEngine({
602
+ workspacePath,
603
+ client
604
+ }, log);
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
609
  }
600
610
  };
@@ -620,6 +630,8 @@ const plugin = {
620
630
  }
621
631
  daemonIpcClient = null;
622
632
  }
633
+ client = null;
634
+ agentId = null;
623
635
  syncEngine = null;
624
636
  sharedSyncEngine = null;
625
637
  lastSyncResult = null;
@@ -630,7 +642,7 @@ const plugin = {
630
642
  api.registerGatewayMethod("sync.now", async () => {
631
643
  if (!syncEngine) return {
632
644
  ok: false,
633
- error: "Sync engine not initialized — run alfesync init"
645
+ error: "Sync engine not initialized — run `alfe login`"
634
646
  };
635
647
  try {
636
648
  lastSyncResult = await syncEngine.fullSync({ quiet: true });
@@ -646,20 +658,19 @@ const plugin = {
646
658
  }
647
659
  });
648
660
  log.info("Registered gateway RPC method: sync.now");
649
- api.registerGatewayMethod("sync.status", () => {
650
- return Promise.resolve({
651
- ok: true,
652
- initialized: !!syncEngine,
653
- schedule: currentConfig.syncSchedule ?? "daily",
654
- scope: currentConfig.syncScope ?? [
655
- "config",
656
- "conversations",
657
- "memory"
658
- ],
659
- lastResult: lastSyncResult,
660
- watcherActive: !!stopWatcher
661
- });
662
- });
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
+ }));
663
674
  log.info("Registered gateway RPC method: sync.status");
664
675
  }
665
676
  if (api.registerService) api.registerService({
@@ -696,6 +707,8 @@ const plugin = {
696
707
  }
697
708
  daemonIpcClient = null;
698
709
  }
710
+ client = null;
711
+ agentId = null;
699
712
  syncEngine = null;
700
713
  sharedSyncEngine = null;
701
714
  lastSyncResult = null;
@@ -711,12 +724,8 @@ const plugin = {
711
724
  };
712
725
  if (config.syncSchedule) {
713
726
  if (config.syncSchedule === "realtime" && syncEngine && !stopWatcher) {
714
- let cfgForWorkspace = null;
715
- try {
716
- cfgForWorkspace = (0, _alfe_ai_config.resolveConfig)();
717
- } catch {}
718
727
  stopWatcher = await startWatcher({
719
- workspacePath: currentConfig.workspacePath ?? cfgForWorkspace?.workspacePath ?? "",
728
+ workspacePath: currentConfig.workspacePath ?? syncEngine.workspacePath,
720
729
  debounceMs: 2e3,
721
730
  onChanges: async (paths) => {
722
731
  if (!syncEngine) return;