@alfe.ai/openclaw-sync 0.1.10 → 0.2.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"plugin2.js","names":["resolveAlfeConfig","alfeConfigExists"],"sources":["../src/watcher.ts","../src/shared-sync.ts","../src/delete-brake.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 * Shared file sync — mirrors org/team/project files to a `shared/`\n * directory in the agent's workspace, organised by scope:\n *\n * shared/org/<files…>\n * shared/teams/<scopeId>/<files…>\n * shared/projects/<scopeId>/<files…>\n *\n * Backed by the agent self-service API (`AgentApiClient.sharedListFiles`,\n * `AgentApiClient.sharedDownloadUrl`).\n */\n\nimport { join, dirname, normalize, sep } from \"node:path\";\nimport { mkdir, writeFile, unlink, rm } from \"node:fs/promises\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nconst MAX_SHARED_FILE_SIZE = 100 * 1024 * 1024; // 100 MB\n\n/** Throw if `resolvedPath` would escape `baseDir`. */\nfunction assertContained(baseDir: string, resolvedPath: string): void {\n const normalizedBase = normalize(baseDir) + sep;\n const normalizedPath = normalize(resolvedPath);\n if (\n !normalizedPath.startsWith(normalizedBase) &&\n normalizedPath !== normalize(baseDir)\n ) {\n throw new Error(`Path traversal blocked: ${resolvedPath} escapes ${baseDir}`);\n }\n}\n\nexport interface SharedScope {\n scopeType: \"team\" | \"project\" | \"org\";\n scopeId: string;\n name: string;\n}\n\nexport interface SharedSyncConfig {\n workspacePath: string;\n client: AgentApiClient;\n}\n\ninterface PluginLogger {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n debug(msg: string): void;\n}\n\nexport interface SharedSyncEngine {\n initialize(scopes: SharedScope[]): Promise<void>;\n updateScopes(scopes: SharedScope[]): Promise<void>;\n handleNotification(filePath: string, eventType: \"created\" | \"deleted\"): Promise<void>;\n fullSync(): Promise<void>;\n getScopes(): SharedScope[];\n}\n\nexport function createSharedSyncEngine(\n config: SharedSyncConfig,\n log: PluginLogger,\n): SharedSyncEngine {\n let activeScopes: SharedScope[] = [];\n const sharedDir = join(config.workspacePath, \"shared\");\n\n function scopeDir(scope: SharedScope): string {\n if (scope.scopeType === \"org\") return join(sharedDir, \"org\");\n const plural = scope.scopeType === \"team\" ? \"teams\" : \"projects\";\n return join(sharedDir, plural, scope.scopeId);\n }\n\n async function downloadFile(\n scope: SharedScope,\n filePath: string,\n localPath: string,\n ): Promise<void> {\n assertContained(scopeDir(scope), localPath);\n\n const { downloadUrl } = await config.client.sharedDownloadUrl({\n scope: scope.scopeType,\n scopeId: scope.scopeId,\n filePath,\n });\n\n // Presigned URL → S3 directly (raw fetch is the right tool here).\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`Download failed: HTTP ${String(response.status)}`);\n }\n\n const contentLength = parseInt(\n response.headers.get(\"content-length\") ?? \"0\",\n 10,\n );\n if (contentLength > MAX_SHARED_FILE_SIZE) {\n throw new Error(\n `File too large: ${String(contentLength)} bytes exceeds ${String(MAX_SHARED_FILE_SIZE)} limit`,\n );\n }\n\n const buffer = Buffer.from(await response.arrayBuffer());\n if (buffer.length > MAX_SHARED_FILE_SIZE) {\n throw new Error(`Downloaded file exceeds size limit: ${String(buffer.length)} bytes`);\n }\n\n await mkdir(dirname(localPath), { recursive: true });\n await writeFile(localPath, buffer);\n }\n\n async function syncScope(scope: SharedScope): Promise<void> {\n const dir = scopeDir(scope);\n await mkdir(dir, { recursive: true });\n\n try {\n const { files } = await config.client.sharedListFiles({\n scope: scope.scopeType,\n scopeId: scope.scopeId,\n });\n\n for (const file of files) {\n const localPath = join(dir, file.filePath);\n try {\n await downloadFile(scope, file.filePath, localPath);\n log.debug(\n `Shared sync: downloaded ${scope.scopeType}/${scope.scopeId}/${file.filePath}`,\n );\n } catch (err: unknown) {\n log.error(\n `Shared sync: failed to download ${file.filePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n } catch (err: unknown) {\n log.error(\n `Shared sync: failed to list files for ${scope.scopeType}/${scope.scopeId}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n function parseScopedPath(\n filePath: string,\n ): { scope: SharedScope; relativePath: string } | null {\n if (filePath.includes(\"..\")) return null;\n\n const orgMatch = /^shared\\/org\\/(.+)$/.exec(filePath);\n if (orgMatch) {\n const scope = activeScopes.find((s) => s.scopeType === \"org\");\n if (scope) return { scope, relativePath: orgMatch[1] };\n }\n\n const teamMatch = /^shared\\/teams\\/([^/]+)\\/(.+)$/.exec(filePath);\n if (teamMatch) {\n const scope = activeScopes.find(\n (s) => s.scopeType === \"team\" && s.scopeId === teamMatch[1],\n );\n if (scope) return { scope, relativePath: teamMatch[2] };\n }\n\n const projectMatch = /^shared\\/projects\\/([^/]+)\\/(.+)$/.exec(filePath);\n if (projectMatch) {\n const scope = activeScopes.find(\n (s) => s.scopeType === \"project\" && s.scopeId === projectMatch[1],\n );\n if (scope) return { scope, relativePath: projectMatch[2] };\n }\n\n return null;\n }\n\n return {\n async initialize(scopes: SharedScope[]): Promise<void> {\n activeScopes = [...scopes];\n log.info(`Shared sync: initializing with ${String(scopes.length)} scope(s)`);\n await mkdir(sharedDir, { recursive: true });\n for (const scope of scopes) {\n await syncScope(scope);\n }\n log.info(\"Shared sync: initialization complete\");\n },\n\n async updateScopes(newScopes: SharedScope[]): Promise<void> {\n const oldIds = new Set(activeScopes.map((s) => `${s.scopeType}:${s.scopeId}`));\n const newIds = new Set(newScopes.map((s) => `${s.scopeType}:${s.scopeId}`));\n\n for (const scope of activeScopes) {\n const key = `${scope.scopeType}:${scope.scopeId}`;\n if (!newIds.has(key)) {\n const dir = scopeDir(scope);\n try {\n await rm(dir, { recursive: true, force: true });\n log.info(`Shared sync: removed scope directory ${dir}`);\n } catch (err: unknown) {\n log.warn(\n `Shared sync: failed to remove ${dir}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n for (const scope of newScopes) {\n const key = `${scope.scopeType}:${scope.scopeId}`;\n if (!oldIds.has(key)) {\n log.info(\n `Shared sync: new scope ${scope.scopeType}/${scope.scopeId} — syncing files`,\n );\n await syncScope(scope);\n }\n }\n\n activeScopes = [...newScopes];\n },\n\n async handleNotification(\n filePath: string,\n eventType: \"created\" | \"deleted\",\n ): Promise<void> {\n const parsed = parseScopedPath(filePath);\n if (!parsed) {\n log.debug(`Shared sync: ignoring notification for unknown path: ${filePath}`);\n return;\n }\n\n const dir = scopeDir(parsed.scope);\n const localPath = join(dir, parsed.relativePath);\n\n try {\n assertContained(dir, localPath);\n } catch {\n log.warn(`Shared sync: path traversal blocked for ${filePath}`);\n return;\n }\n\n if (eventType === \"deleted\") {\n try {\n await unlink(localPath);\n log.debug(`Shared sync: deleted ${filePath}`);\n } catch {\n // idempotent — file may not exist locally on out-of-order events\n }\n return;\n }\n\n try {\n await downloadFile(parsed.scope, parsed.relativePath, localPath);\n log.debug(`Shared sync: pulled ${filePath}`);\n } catch (err: unknown) {\n log.error(\n `Shared sync: failed to pull ${filePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n },\n\n async fullSync(): Promise<void> {\n log.info(`Shared sync: full sync across ${String(activeScopes.length)} scope(s)`);\n for (const scope of activeScopes) {\n await syncScope(scope);\n }\n },\n\n getScopes(): SharedScope[] {\n return [...activeScopes];\n },\n };\n}\n","// Rolling-window rate brake for cloud delete propagation.\n//\n// The watcher's onChanges callback emits batches every ~2 s. A per-batch\n// brake (`if batch / manifestSize > 30%`) is trivial to slip on a real\n// `rm -rf` because chokidar dribbles deletes out 5-10 at a time. Instead\n// we sum deletes across the last 60 s and trip when the cumulative count\n// exceeds 30% of the manifest. Legitimate single-file deletes via the\n// agent stay nowhere near the threshold; a workspace unmount or\n// catastrophic delete rapidly accumulates and is stopped.\n//\n// Manifest size of 0 means the agent has nothing in the cloud yet —\n// can't be a disaster, so always allow.\n//\n// In-memory only. A plugin restart resets the window, which is acceptable:\n// a restart already implies operator awareness.\n\nconst DEFAULT_WINDOW_MS = 60_000;\nconst DEFAULT_THRESHOLD_RATIO = 0.3;\n\nexport interface DeleteBrakeOptions {\n windowMs?: number;\n thresholdRatio?: number;\n now?: () => number;\n}\n\nexport interface DeleteBrake {\n // Returns true if the proposed delete count is safe to proceed (and\n // records it). Returns false if combining it with the existing rolling\n // window would exceed the threshold — caller must not perform the\n // delete in that case.\n check(deleteCount: number, manifestSize: number): boolean;\n // Current sum of deletes within the active window (after expiry).\n windowSum(): number;\n}\n\nexport function createDeleteBrake(opts: DeleteBrakeOptions = {}): DeleteBrake {\n const windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;\n const thresholdRatio = opts.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO;\n const now = opts.now ?? (() => Date.now());\n\n const records: { ts: number; count: number }[] = [];\n\n function expire(): void {\n const cutoff = now() - windowMs;\n while (records.length > 0 && records[0].ts < cutoff) records.shift();\n }\n\n function sum(): number {\n return records.reduce((acc, r) => acc + r.count, 0);\n }\n\n return {\n check(deleteCount, manifestSize) {\n if (deleteCount <= 0) return true;\n expire();\n // Fresh agent (no manifest entries) — nothing to lose, always allow.\n if (manifestSize === 0) {\n records.push({ ts: now(), count: deleteCount });\n return true;\n }\n const projected = sum() + deleteCount;\n if (projected / manifestSize > thresholdRatio) return false;\n records.push({ ts: now(), count: deleteCount });\n return true;\n },\n windowSum() {\n expire();\n return sum();\n },\n };\n}\n","/**\n * @alfe.ai/openclaw-sync — OpenClaw Sync plugin.\n *\n * Wraps the sync engine as a lifecycle-managed integration. Same shape as\n * @alfe.ai/openclaw-memory-cloud / -secrets / -google: one AgentApiClient\n * is constructed in `activate()` and reused for every API call. The agent's\n * identity is resolved server-side from the API key — the plugin never\n * touches `/auth/validate`, never plumbs an `agentId` around, never writes\n * a `.alfesync/` directory.\n *\n * Lifecycle:\n * - activate(api): construct client, start sync engine + watcher\n * - deactivate(api): stop watcher, drop relay connections, clean up\n * - configure(api, config): swap schedule/scope at runtime\n *\n * Registers gateway RPC methods: `sync.now`, `sync.status`.\n */\n\nimport { createRequire } from 'node:module';\nimport {\n resolveConfig as resolveAlfeConfig,\n configExists as alfeConfigExists,\n DEFAULT_SOCKET_PATH,\n DEFAULT_WORKSPACE_PATH,\n type ResolvedConfig as AlfeResolvedConfig,\n} from '@alfe.ai/config';\nimport { AgentApiClient, type SyncAgentInfo } from '@alfe.ai/agent-api-client';\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { createSyncEngine, type SyncEngine, type SyncResult } from './sync-engine.js';\nimport { startWatcher } from './watcher.js';\nimport { createSharedSyncEngine, type SharedSyncEngine, type SharedScope } from './shared-sync.js';\nimport { createDeleteBrake, type DeleteBrake } from './delete-brake.js';\nimport { readManifest } from './manifest.js';\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\n// ── Constants ───────────────────────────────────────────────\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\ninterface PluginLogger {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n debug(msg: string): void;\n}\n\ninterface PluginServiceContext {\n config: Record<string, unknown>;\n workspaceDir?: string;\n stateDir: string;\n logger: PluginLogger;\n}\n\ninterface PluginApi {\n logger: PluginLogger;\n registrationMode?: 'full' | 'setup-only' | 'setup-runtime' | 'cli-metadata';\n config?: SyncPluginConfig;\n registerGatewayMethod?: (name: string, handler: () => Promise<unknown>) => void;\n registerService(service: {\n id: string;\n start: (ctx: PluginServiceContext) => void | Promise<void>;\n stop?: (ctx: PluginServiceContext) => void | Promise<void>;\n }): void;\n}\n\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\ninterface SyncRelaySocket {\n on(event: string, handler: (...args: never[]) => void): void;\n send(data: string): void;\n close(code?: number, reason?: string): void;\n}\n\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 the resolved Alfe workspace. */\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: derived from apiUrl). */\n syncRelayUrl?: string;\n /** Enable shared file sync for org/team/project scopes. Default: true. */\n sharedSync?: boolean;\n}\n\n// ── Plugin State ────────────────────────────────────────────\n\nlet client: AgentApiClient | null = null;\nlet agentId: string | null = null;\nlet syncEngine: SyncEngine | null = null;\nlet sharedSyncEngine: SharedSyncEngine | null = null;\nlet stopWatcher: (() => Promise<void>) | null = null;\nlet deleteBrake: DeleteBrake | 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 = new Map<\n string,\n { etag?: string; eventType: 'created' | 'deleted' }\n>();\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 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 scheduledInterval.unref();\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 ipc = new mod.IPCClient(socketPath, log);\n\n ipc.on('connected', () => {\n void (async () => {\n log.info('Connected to Alfe daemon — registering sync capabilities...');\n const response = await ipc.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 ipc.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 ipc.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 if (msg?.type === 'SHARED_SCOPES') {\n const scopes = msg.scopes as SharedScope[] | undefined;\n const engine = sharedSyncEngine;\n if (scopes && engine) {\n log.info(`Received SHARED_SCOPES update: ${String(scopes.length)} scope(s)`);\n void (async () => {\n try {\n await engine.updateScopes(scopes);\n } catch (err: unknown) {\n log.error(\n `SHARED_SCOPES update failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n })();\n }\n }\n });\n\n ipc.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 ipc.start();\n return ipc;\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 (syncRelayPendingPaths.size === 0) return;\n\n const entries = new Map(syncRelayPendingPaths);\n syncRelayPendingPaths.clear();\n\n const sharedEntries = new Map<string, { etag?: string; eventType: 'created' | 'deleted' }>();\n const privateEntries = new Map<string, { etag?: string; eventType: 'created' | 'deleted' }>();\n\n for (const [filePath, info] of entries) {\n if (filePath.startsWith('shared/')) {\n sharedEntries.set(filePath, info);\n } else {\n privateEntries.set(filePath, info);\n }\n }\n\n if (sharedEntries.size > 0 && sharedSyncEngine) {\n for (const [filePath, info] of sharedEntries) {\n try {\n await sharedSyncEngine.handleNotification(filePath, info.eventType);\n } catch (err: unknown) {\n log.error(\n `Shared sync relay: failed ${info.eventType} ${filePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n if (privateEntries.size > 0 && syncEngine) {\n const engine = syncEngine;\n const toPull: string[] = [];\n const toDelete: string[] = [];\n\n for (const [filePath, info] of privateEntries) {\n if (info.eventType === 'deleted') {\n toDelete.push(filePath);\n } else {\n toPull.push(filePath);\n }\n }\n\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(\n `Sync relay: failed to delete ${filePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n if (toPull.length > 0) {\n try {\n const result = await engine.pullFiles(toPull, { quiet: true });\n if (result.pulled > 0) log.info(`Sync relay: pulled ${String(result.pulled)} file(s)`);\n if (result.errors > 0) log.warn(`Sync relay: ${String(result.errors)} pull error(s)`);\n } catch (err: unknown) {\n log.error(`Sync relay: pull failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n }\n}\n\nasync function connectToSyncRelay(\n relayUrl: string,\n token: string,\n agentIdForSubscribe: 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 ws.send(JSON.stringify({ type: 'SUBSCRIBE', agentId: agentIdForSubscribe }));\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 ?? agentIdForSubscribe}`);\n const sharedEngine = sharedSyncEngine;\n if (sharedEngine) {\n void (async () => {\n try {\n await sharedEngine.fullSync();\n log.info('Shared sync: reconnect full sync complete');\n } catch (err: unknown) {\n log.error(\n `Shared sync: reconnect full sync failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n })();\n }\n } else {\n log.warn(`Sync relay subscribe failed: ${message.message ?? 'unknown'}`);\n }\n break;\n\n case 'FILE_CHANGED':\n if (message.filePath) {\n syncRelayPendingPaths.set(message.filePath, {\n etag: message.etag,\n eventType: message.eventType === 'deleted' ? 'deleted' : 'created',\n });\n }\n clearSyncRelayDebounce();\n syncRelayDebounceTimer = setTimeout(\n () => void processPendingNotifications(log),\n SYNC_RELAY_DEBOUNCE_MS,\n );\n syncRelayDebounceTimer.unref();\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, agentIdForSubscribe, log);\n });\n\n ws.on('error', (err: Error) => {\n log.debug(`Sync Relay error: ${err.message}`);\n });\n\n return ws 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, agentIdForSubscribe, log);\n return null;\n }\n}\n\nfunction scheduleSyncRelayReconnect(\n relayUrl: string,\n token: string,\n agentIdForSubscribe: string,\n log: PluginLogger,\n) {\n clearSyncRelayReconnect();\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 log.debug(\n `Reconnecting to Sync Relay in ${String(delay)}ms (attempt ${String(syncRelayReconnectAttempt)})`,\n );\n\n syncRelayReconnectTimer = setTimeout(() => {\n void (async () => {\n syncRelayWs = await connectToSyncRelay(relayUrl, token, agentIdForSubscribe, log);\n })();\n }, delay);\n syncRelayReconnectTimer.unref();\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\nfunction deriveRelayUrl(apiUrl: string): string {\n if (apiUrl.includes('dev.alfe.ai')) return 'wss://sync.dev.alfe.ai/ws';\n if (apiUrl.includes('demo.alfe.ai')) return 'wss://sync.demo.alfe.ai/ws';\n if (apiUrl.includes('test.alfe.ai')) return 'wss://sync.test.alfe.ai/ws';\n return 'wss://sync.alfe.ai/ws';\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: pkg.version,\n\n activate(api: PluginApi) {\n const log = api.logger;\n\n const pluginConfig: SyncPluginConfig = api.config ?? {};\n currentConfig = pluginConfig;\n\n let alfeConfig: { workspacePath: string; socketPath: string } | null = null;\n try {\n alfeConfig = resolveAlfeConfig();\n } catch {\n // alfe login hasn't run; logged below\n }\n\n const workspacePath =\n pluginConfig.workspacePath ?? alfeConfig?.workspacePath ?? DEFAULT_WORKSPACE_PATH;\n\n const syncScope = pluginConfig.syncScope ?? ['config', 'conversations', 'memory'];\n // Default to 'realtime' when no schedule is configured. The integration's\n // user-config schema marks sync_schedule as required, so an unset value\n // here means the framework didn't deliver the user's choice (manifest\n // template missing, applyConfig errored, etc.). 'daily' would leave a\n // managed agent silent for 24 hours after activation; 'realtime' fails\n // open into the safest mode — initial push runs immediately and the\n // watcher streams ongoing changes.\n const syncSchedule = pluginConfig.syncSchedule ?? 'realtime';\n const socketPath =\n pluginConfig.socketPath ?? alfeConfig?.socketPath ?? DEFAULT_SOCKET_PATH;\n\n const startSyncService = async () => {\n if ((globalThis as Record<string, unknown>).__alfeSyncPluginActivated === true) {\n log.debug('Alfe Sync plugin already activated — skipping duplicate');\n return;\n }\n (globalThis as Record<string, unknown>).__alfeSyncPluginActivated = true;\n log.info('Alfe Sync plugin activating...');\n log.info(`Sync scope: ${syncScope.join(', ')}`);\n log.info(`Sync schedule: ${syncSchedule}`);\n log.info(`Workspace: ${workspacePath}`);\n\n // Sync needs `~/.alfe/config.toml` (the existing `alfe login` config).\n // Identity (agentId / tenantId) is resolved server-side from the API\n // key on every AgentApiClient call.\n if (!alfeConfigExists()) {\n log.info('Sync skipped — no Alfe config found. Run `alfe login` to enable.');\n return;\n }\n\n let syncCfg: AlfeResolvedConfig;\n try {\n syncCfg = resolveAlfeConfig();\n } catch (err: unknown) {\n log.warn(\n `Sync skipped — failed to resolve credentials from ~/.alfe/config.toml: ${err instanceof Error ? err.message : String(err)}`,\n );\n return;\n }\n\n client = new AgentApiClient({ apiKey: syncCfg.apiKey, apiUrl: syncCfg.apiUrl });\n syncEngine = createSyncEngine({ workspacePath, client });\n log.info('Sync engine initialized');\n\n // Realtime mode → initial push-only seed of pre-existing files\n // (chokidar ignoreInitial means the watcher only emits for\n // post-activation changes; without this the workspace tree never\n // reaches the cloud unless a file is touched). Push-only — not\n // fullSync — so we don't drag down stale cloud files into a fresh\n // local workspace or write .conflict-<ts> markers on first run.\n if (syncSchedule === 'realtime') {\n try {\n lastSyncResult = await syncEngine.push(undefined, { quiet: true });\n log.info('Initial workspace push complete');\n } catch (err: unknown) {\n log.warn(`Initial workspace push failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n deleteBrake = createDeleteBrake();\n try {\n stopWatcher = await startWatcher({\n workspacePath,\n debounceMs: 2000,\n onChanges: async (paths) => {\n if (!syncEngine) return;\n const existing: string[] = [];\n const missing: string[] = [];\n for (const p of paths) {\n if (existsSync(join(workspacePath, p))) existing.push(p);\n else missing.push(p);\n }\n log.debug(\n `Realtime sync: ${String(existing.length)} upload(s), ${String(missing.length)} delete(s)`,\n );\n if (existing.length > 0) {\n try {\n lastSyncResult = await syncEngine.push(existing, { quiet: true });\n } catch (err: unknown) {\n log.error(`Realtime push failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n if (missing.length > 0) {\n // Rolling-window rate brake: refuse the batch if cumulative\n // deletes in the last 60 s would exceed 30% of the manifest.\n // See packages/openclaw-sync/src/delete-brake.ts.\n const manifest = await readManifest(workspacePath);\n const manifestSize = Object.keys(manifest.files).length;\n if (deleteBrake && !deleteBrake.check(missing.length, manifestSize)) {\n log.error(\n `Sync delete brake tripped: ${String(deleteBrake.windowSum() + missing.length)} ` +\n `deletes in last 60s vs manifest size ${String(manifestSize)} ` +\n `(threshold 30%). Refusing batch — investigate the workspace state.`,\n );\n } else {\n try {\n lastSyncResult = await syncEngine.pushDeletes(missing, { quiet: true });\n } catch (err: unknown) {\n log.error(`Realtime delete failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\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 } else {\n setupSchedule(syncSchedule, log);\n }\n\n // Daemon IPC (for SYNC_NOW + SHARED_SCOPES messages)\n daemonIpcClient = await connectToDaemon(socketPath, log);\n\n // Register with the sync service to materialize the SyncAgent record\n // and learn our own agentId for relay subscription.\n let registered: SyncAgentInfo | null = null;\n try {\n const result = await client.syncRegister();\n registered = result.agent;\n agentId = registered.agentId;\n } catch (err: unknown) {\n log.warn(`Sync register failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n // Sync relay (real-time file change notifications)\n if (registered) {\n try {\n const relayUrl = pluginConfig.syncRelayUrl ?? deriveRelayUrl(syncCfg.apiUrl);\n syncRelayWs = await connectToSyncRelay(\n relayUrl,\n syncCfg.apiKey,\n registered.agentId,\n log,\n );\n } catch (err: unknown) {\n log.debug(\n `Sync Relay connection skipped: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Shared sync (org/team/project scopes pushed by gateway)\n if (pluginConfig.sharedSync !== false) {\n try {\n sharedSyncEngine = createSharedSyncEngine({ workspacePath, client }, log);\n log.info('Shared sync engine created — waiting for SHARED_SCOPES from gateway');\n } catch (err: unknown) {\n log.debug(\n `Shared sync engine skipped: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n };\n\n const stopSyncService = async () => {\n (globalThis as Record<string, unknown>).__alfeSyncPluginActivated = false;\n clearSchedule();\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 deleteBrake = null;\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 client = null;\n agentId = null;\n syncEngine = null;\n sharedSyncEngine = null;\n lastSyncResult = null;\n currentConfig = {};\n\n log.info('Alfe Sync plugin deactivated');\n };\n\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 `alfe login`' };\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 Promise.resolve({\n ok: true,\n initialized: !!syncEngine,\n agentId,\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 api.registerService({\n id: 'alfe-sync-engine',\n start: () => startSyncService(),\n stop: () => stopSyncService(),\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 const log = api.logger;\n log.info('Alfe Sync plugin deactivating...');\n\n clearSchedule();\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 client = null;\n agentId = null;\n syncEngine = null;\n sharedSyncEngine = null;\n lastSyncResult = null;\n currentConfig = {};\n\n log.info('Alfe Sync plugin deactivated');\n },\n\n async configure(api: PluginApi, config: SyncPluginConfig) {\n const log = api.logger;\n log.info('Reconfiguring Alfe Sync plugin...');\n currentConfig = { ...currentConfig, ...config };\n\n if (config.syncSchedule) {\n if (config.syncSchedule === 'realtime' && syncEngine && !stopWatcher) {\n const workspacePath = currentConfig.workspacePath ?? syncEngine.workspacePath;\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":"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"}
1
+ {"version":3,"file":"plugin2.js","names":["resolveAlfeConfig","alfeConfigExists"],"sources":["../src/watcher.ts","../src/shared-sync.ts","../src/delete-brake.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, shouldIgnoreDir } from \"./ignore.js\";\n\nexport interface WatcherOptions {\n workspacePath: string;\n /**\n * Active agent runtime — selects the per-runtime default ignore set so the\n * watcher never descends into runtime-internal dirs (openclaw `npm/`,\n * hermes `.venv/`, …). Default: \"openclaw\".\n */\n runtime?: 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, runtime = \"openclaw\", debounceMs = 2000, onChanges } = options;\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\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 // Posix-normalise so the globs (which are posix-style) match on Windows\n // self-hosted agents too — parity with the `ignored` predicate below.\n const relativePath = relative(workspacePath, absolutePath).replace(/\\\\/g, \"/\");\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 // chokidar 4 dropped glob support in `ignored` — a string-glob array is\n // treated as literal paths and matches nothing, so the watcher would\n // create an inotify watch on every dir (incl. node_modules / the runtime's\n // npm cache) and exhaust the OS watch limit → ENOSPC crash-loop. Use a\n // predicate driven by the SAME ignore engine as event filtering; returning\n // true for a directory stops chokidar descending into it.\n // chokidar invokes this for files AND directories, twice per entry: first\n // with no stats (path check), then with stats before it actually watches.\n // Match ignored files directly; prune ignored directories (whose contents\n // are ignored) via the sentinel-child probe. When stats say it's a FILE,\n // skip the dir probe so a file literally named like an ignored dir (e.g. an\n // extensionless `dist`) isn't wrongly ignored — keeps parity with the walk,\n // which only dir-prunes real directories.\n ignored: (p: string, stats?: { isDirectory(): boolean }) => {\n const rel = relative(workspacePath, p).replace(/\\\\/g, \"/\");\n if (rel === \"\") return false; // never ignore the workspace root\n if (shouldIgnore(rel, ignorePatterns)) return true;\n if (stats && !stats.isDirectory()) return false;\n return shouldIgnoreDir(rel, ignorePatterns);\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 * Shared file sync — mirrors org/team/project files to a `shared/`\n * directory in the agent's workspace, organised by scope:\n *\n * shared/org/<files…>\n * shared/teams/<scopeId>/<files…>\n * shared/projects/<scopeId>/<files…>\n *\n * Backed by the agent self-service API (`AgentApiClient.sharedListFiles`,\n * `AgentApiClient.sharedDownloadUrl`).\n */\n\nimport { join, dirname, normalize, sep } from \"node:path\";\nimport { mkdir, writeFile, unlink, rm } from \"node:fs/promises\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nconst MAX_SHARED_FILE_SIZE = 100 * 1024 * 1024; // 100 MB\n\n/** Throw if `resolvedPath` would escape `baseDir`. */\nfunction assertContained(baseDir: string, resolvedPath: string): void {\n const normalizedBase = normalize(baseDir) + sep;\n const normalizedPath = normalize(resolvedPath);\n if (\n !normalizedPath.startsWith(normalizedBase) &&\n normalizedPath !== normalize(baseDir)\n ) {\n throw new Error(`Path traversal blocked: ${resolvedPath} escapes ${baseDir}`);\n }\n}\n\nexport interface SharedScope {\n scopeType: \"team\" | \"project\" | \"org\";\n scopeId: string;\n name: string;\n}\n\nexport interface SharedSyncConfig {\n workspacePath: string;\n client: AgentApiClient;\n}\n\ninterface PluginLogger {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n debug(msg: string): void;\n}\n\nexport interface SharedSyncEngine {\n initialize(scopes: SharedScope[]): Promise<void>;\n updateScopes(scopes: SharedScope[]): Promise<void>;\n handleNotification(filePath: string, eventType: \"created\" | \"deleted\"): Promise<void>;\n fullSync(): Promise<void>;\n getScopes(): SharedScope[];\n}\n\nexport function createSharedSyncEngine(\n config: SharedSyncConfig,\n log: PluginLogger,\n): SharedSyncEngine {\n let activeScopes: SharedScope[] = [];\n const sharedDir = join(config.workspacePath, \"shared\");\n\n function scopeDir(scope: SharedScope): string {\n if (scope.scopeType === \"org\") return join(sharedDir, \"org\");\n const plural = scope.scopeType === \"team\" ? \"teams\" : \"projects\";\n return join(sharedDir, plural, scope.scopeId);\n }\n\n async function downloadFile(\n scope: SharedScope,\n filePath: string,\n localPath: string,\n ): Promise<void> {\n assertContained(scopeDir(scope), localPath);\n\n const { downloadUrl } = await config.client.sharedDownloadUrl({\n scope: scope.scopeType,\n scopeId: scope.scopeId,\n filePath,\n });\n\n // Presigned URL → S3 directly (raw fetch is the right tool here).\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`Download failed: HTTP ${String(response.status)}`);\n }\n\n const contentLength = parseInt(\n response.headers.get(\"content-length\") ?? \"0\",\n 10,\n );\n if (contentLength > MAX_SHARED_FILE_SIZE) {\n throw new Error(\n `File too large: ${String(contentLength)} bytes exceeds ${String(MAX_SHARED_FILE_SIZE)} limit`,\n );\n }\n\n const buffer = Buffer.from(await response.arrayBuffer());\n if (buffer.length > MAX_SHARED_FILE_SIZE) {\n throw new Error(`Downloaded file exceeds size limit: ${String(buffer.length)} bytes`);\n }\n\n await mkdir(dirname(localPath), { recursive: true });\n await writeFile(localPath, buffer);\n }\n\n async function syncScope(scope: SharedScope): Promise<void> {\n const dir = scopeDir(scope);\n await mkdir(dir, { recursive: true });\n\n try {\n const { files } = await config.client.sharedListFiles({\n scope: scope.scopeType,\n scopeId: scope.scopeId,\n });\n\n for (const file of files) {\n const localPath = join(dir, file.filePath);\n try {\n await downloadFile(scope, file.filePath, localPath);\n log.debug(\n `Shared sync: downloaded ${scope.scopeType}/${scope.scopeId}/${file.filePath}`,\n );\n } catch (err: unknown) {\n log.error(\n `Shared sync: failed to download ${file.filePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n } catch (err: unknown) {\n log.error(\n `Shared sync: failed to list files for ${scope.scopeType}/${scope.scopeId}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n function parseScopedPath(\n filePath: string,\n ): { scope: SharedScope; relativePath: string } | null {\n if (filePath.includes(\"..\")) return null;\n\n const orgMatch = /^shared\\/org\\/(.+)$/.exec(filePath);\n if (orgMatch) {\n const scope = activeScopes.find((s) => s.scopeType === \"org\");\n if (scope) return { scope, relativePath: orgMatch[1] };\n }\n\n const teamMatch = /^shared\\/teams\\/([^/]+)\\/(.+)$/.exec(filePath);\n if (teamMatch) {\n const scope = activeScopes.find(\n (s) => s.scopeType === \"team\" && s.scopeId === teamMatch[1],\n );\n if (scope) return { scope, relativePath: teamMatch[2] };\n }\n\n const projectMatch = /^shared\\/projects\\/([^/]+)\\/(.+)$/.exec(filePath);\n if (projectMatch) {\n const scope = activeScopes.find(\n (s) => s.scopeType === \"project\" && s.scopeId === projectMatch[1],\n );\n if (scope) return { scope, relativePath: projectMatch[2] };\n }\n\n return null;\n }\n\n return {\n async initialize(scopes: SharedScope[]): Promise<void> {\n activeScopes = [...scopes];\n log.info(`Shared sync: initializing with ${String(scopes.length)} scope(s)`);\n await mkdir(sharedDir, { recursive: true });\n for (const scope of scopes) {\n await syncScope(scope);\n }\n log.info(\"Shared sync: initialization complete\");\n },\n\n async updateScopes(newScopes: SharedScope[]): Promise<void> {\n const oldIds = new Set(activeScopes.map((s) => `${s.scopeType}:${s.scopeId}`));\n const newIds = new Set(newScopes.map((s) => `${s.scopeType}:${s.scopeId}`));\n\n for (const scope of activeScopes) {\n const key = `${scope.scopeType}:${scope.scopeId}`;\n if (!newIds.has(key)) {\n const dir = scopeDir(scope);\n try {\n await rm(dir, { recursive: true, force: true });\n log.info(`Shared sync: removed scope directory ${dir}`);\n } catch (err: unknown) {\n log.warn(\n `Shared sync: failed to remove ${dir}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n for (const scope of newScopes) {\n const key = `${scope.scopeType}:${scope.scopeId}`;\n if (!oldIds.has(key)) {\n log.info(\n `Shared sync: new scope ${scope.scopeType}/${scope.scopeId} — syncing files`,\n );\n await syncScope(scope);\n }\n }\n\n activeScopes = [...newScopes];\n },\n\n async handleNotification(\n filePath: string,\n eventType: \"created\" | \"deleted\",\n ): Promise<void> {\n const parsed = parseScopedPath(filePath);\n if (!parsed) {\n log.debug(`Shared sync: ignoring notification for unknown path: ${filePath}`);\n return;\n }\n\n const dir = scopeDir(parsed.scope);\n const localPath = join(dir, parsed.relativePath);\n\n try {\n assertContained(dir, localPath);\n } catch {\n log.warn(`Shared sync: path traversal blocked for ${filePath}`);\n return;\n }\n\n if (eventType === \"deleted\") {\n try {\n await unlink(localPath);\n log.debug(`Shared sync: deleted ${filePath}`);\n } catch {\n // idempotent — file may not exist locally on out-of-order events\n }\n return;\n }\n\n try {\n await downloadFile(parsed.scope, parsed.relativePath, localPath);\n log.debug(`Shared sync: pulled ${filePath}`);\n } catch (err: unknown) {\n log.error(\n `Shared sync: failed to pull ${filePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n },\n\n async fullSync(): Promise<void> {\n log.info(`Shared sync: full sync across ${String(activeScopes.length)} scope(s)`);\n for (const scope of activeScopes) {\n await syncScope(scope);\n }\n },\n\n getScopes(): SharedScope[] {\n return [...activeScopes];\n },\n };\n}\n","// Rolling-window rate brake for cloud delete propagation.\n//\n// The watcher's onChanges callback emits batches every ~2 s. A per-batch\n// brake (`if batch / manifestSize > 30%`) is trivial to slip on a real\n// `rm -rf` because chokidar dribbles deletes out 5-10 at a time. Instead\n// we sum deletes across the last 60 s and trip when the cumulative count\n// exceeds 30% of the manifest. Legitimate single-file deletes via the\n// agent stay nowhere near the threshold; a workspace unmount or\n// catastrophic delete rapidly accumulates and is stopped.\n//\n// Manifest size of 0 means the agent has nothing in the cloud yet —\n// can't be a disaster, so always allow.\n//\n// In-memory only. A plugin restart resets the window, which is acceptable:\n// a restart already implies operator awareness.\n\nconst DEFAULT_WINDOW_MS = 60_000;\nconst DEFAULT_THRESHOLD_RATIO = 0.3;\n\nexport interface DeleteBrakeOptions {\n windowMs?: number;\n thresholdRatio?: number;\n now?: () => number;\n}\n\nexport interface DeleteBrake {\n // Returns true if the proposed delete count is safe to proceed (and\n // records it). Returns false if combining it with the existing rolling\n // window would exceed the threshold — caller must not perform the\n // delete in that case.\n check(deleteCount: number, manifestSize: number): boolean;\n // Current sum of deletes within the active window (after expiry).\n windowSum(): number;\n}\n\nexport function createDeleteBrake(opts: DeleteBrakeOptions = {}): DeleteBrake {\n const windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;\n const thresholdRatio = opts.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO;\n const now = opts.now ?? (() => Date.now());\n\n const records: { ts: number; count: number }[] = [];\n\n function expire(): void {\n const cutoff = now() - windowMs;\n while (records.length > 0 && records[0].ts < cutoff) records.shift();\n }\n\n function sum(): number {\n return records.reduce((acc, r) => acc + r.count, 0);\n }\n\n return {\n check(deleteCount, manifestSize) {\n if (deleteCount <= 0) return true;\n expire();\n // Fresh agent (no manifest entries) — nothing to lose, always allow.\n if (manifestSize === 0) {\n records.push({ ts: now(), count: deleteCount });\n return true;\n }\n const projected = sum() + deleteCount;\n if (projected / manifestSize > thresholdRatio) return false;\n records.push({ ts: now(), count: deleteCount });\n return true;\n },\n windowSum() {\n expire();\n return sum();\n },\n };\n}\n","/**\n * @alfe.ai/openclaw-sync — OpenClaw Sync plugin.\n *\n * Wraps the sync engine as a lifecycle-managed integration. Same shape as\n * @alfe.ai/openclaw-memory-cloud / -secrets / -google: one AgentApiClient\n * is constructed in `activate()` and reused for every API call. The agent's\n * identity is resolved server-side from the API key — the plugin never\n * touches `/auth/validate`, never plumbs an `agentId` around, never writes\n * a `.alfesync/` directory.\n *\n * Lifecycle:\n * - activate(api): construct client, start sync engine + watcher\n * - deactivate(api): stop watcher, drop relay connections, clean up\n * - configure(api, config): swap schedule/scope at runtime\n *\n * Registers gateway RPC methods: `sync.now`, `sync.status`.\n */\n\nimport { createRequire } from 'node:module';\nimport {\n resolveConfig as resolveAlfeConfig,\n configExists as alfeConfigExists,\n DEFAULT_SOCKET_PATH,\n DEFAULT_WORKSPACE_PATH,\n type ResolvedConfig as AlfeResolvedConfig,\n} from '@alfe.ai/config';\nimport { AgentApiClient, type SyncAgentInfo } from '@alfe.ai/agent-api-client';\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { createSyncEngine, type SyncEngine, type SyncResult } from './sync-engine.js';\nimport { startWatcher } from './watcher.js';\nimport { createSharedSyncEngine, type SharedSyncEngine, type SharedScope } from './shared-sync.js';\nimport { createDeleteBrake, type DeleteBrake } from './delete-brake.js';\nimport { readManifest } from './manifest.js';\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\n// ── Constants ───────────────────────────────────────────────\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\ninterface PluginLogger {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n debug(msg: string): void;\n}\n\ninterface PluginServiceContext {\n config: Record<string, unknown>;\n workspaceDir?: string;\n stateDir: string;\n logger: PluginLogger;\n}\n\ninterface PluginApi {\n logger: PluginLogger;\n registrationMode?: 'full' | 'setup-only' | 'setup-runtime' | 'cli-metadata';\n config?: SyncPluginConfig;\n registerGatewayMethod?: (name: string, handler: () => Promise<unknown>) => void;\n registerService(service: {\n id: string;\n start: (ctx: PluginServiceContext) => void | Promise<void>;\n stop?: (ctx: PluginServiceContext) => void | Promise<void>;\n }): void;\n}\n\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\ninterface SyncRelaySocket {\n on(event: string, handler: (...args: never[]) => void): void;\n send(data: string): void;\n close(code?: number, reason?: string): void;\n}\n\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 the resolved Alfe workspace. */\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: derived from apiUrl). */\n syncRelayUrl?: string;\n /** Enable shared file sync for org/team/project scopes. Default: true. */\n sharedSync?: boolean;\n}\n\n// ── Plugin State ────────────────────────────────────────────\n\nlet client: AgentApiClient | null = null;\nlet agentId: string | null = null;\nlet syncEngine: SyncEngine | null = null;\nlet sharedSyncEngine: SharedSyncEngine | null = null;\nlet stopWatcher: (() => Promise<void>) | null = null;\nlet deleteBrake: DeleteBrake | 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 = new Map<\n string,\n { etag?: string; eventType: 'created' | 'deleted' }\n>();\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 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 scheduledInterval.unref();\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 ipc = new mod.IPCClient(socketPath, log);\n\n ipc.on('connected', () => {\n void (async () => {\n log.info('Connected to Alfe daemon — registering sync capabilities...');\n const response = await ipc.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 ipc.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 ipc.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 if (msg?.type === 'SHARED_SCOPES') {\n const scopes = msg.scopes as SharedScope[] | undefined;\n const engine = sharedSyncEngine;\n if (scopes && engine) {\n log.info(`Received SHARED_SCOPES update: ${String(scopes.length)} scope(s)`);\n void (async () => {\n try {\n await engine.updateScopes(scopes);\n } catch (err: unknown) {\n log.error(\n `SHARED_SCOPES update failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n })();\n }\n }\n });\n\n ipc.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 ipc.start();\n return ipc;\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 (syncRelayPendingPaths.size === 0) return;\n\n const entries = new Map(syncRelayPendingPaths);\n syncRelayPendingPaths.clear();\n\n const sharedEntries = new Map<string, { etag?: string; eventType: 'created' | 'deleted' }>();\n const privateEntries = new Map<string, { etag?: string; eventType: 'created' | 'deleted' }>();\n\n for (const [filePath, info] of entries) {\n if (filePath.startsWith('shared/')) {\n sharedEntries.set(filePath, info);\n } else {\n privateEntries.set(filePath, info);\n }\n }\n\n if (sharedEntries.size > 0 && sharedSyncEngine) {\n for (const [filePath, info] of sharedEntries) {\n try {\n await sharedSyncEngine.handleNotification(filePath, info.eventType);\n } catch (err: unknown) {\n log.error(\n `Shared sync relay: failed ${info.eventType} ${filePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n if (privateEntries.size > 0 && syncEngine) {\n const engine = syncEngine;\n const toPull: string[] = [];\n const toDelete: string[] = [];\n\n for (const [filePath, info] of privateEntries) {\n if (info.eventType === 'deleted') {\n toDelete.push(filePath);\n } else {\n toPull.push(filePath);\n }\n }\n\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(\n `Sync relay: failed to delete ${filePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n if (toPull.length > 0) {\n try {\n const result = await engine.pullFiles(toPull, { quiet: true });\n if (result.pulled > 0) log.info(`Sync relay: pulled ${String(result.pulled)} file(s)`);\n if (result.errors > 0) log.warn(`Sync relay: ${String(result.errors)} pull error(s)`);\n } catch (err: unknown) {\n log.error(`Sync relay: pull failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n }\n}\n\nasync function connectToSyncRelay(\n relayUrl: string,\n token: string,\n agentIdForSubscribe: 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 ws.send(JSON.stringify({ type: 'SUBSCRIBE', agentId: agentIdForSubscribe }));\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 ?? agentIdForSubscribe}`);\n const sharedEngine = sharedSyncEngine;\n if (sharedEngine) {\n void (async () => {\n try {\n await sharedEngine.fullSync();\n log.info('Shared sync: reconnect full sync complete');\n } catch (err: unknown) {\n log.error(\n `Shared sync: reconnect full sync failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n })();\n }\n } else {\n log.warn(`Sync relay subscribe failed: ${message.message ?? 'unknown'}`);\n }\n break;\n\n case 'FILE_CHANGED':\n if (message.filePath) {\n syncRelayPendingPaths.set(message.filePath, {\n etag: message.etag,\n eventType: message.eventType === 'deleted' ? 'deleted' : 'created',\n });\n }\n clearSyncRelayDebounce();\n syncRelayDebounceTimer = setTimeout(\n () => void processPendingNotifications(log),\n SYNC_RELAY_DEBOUNCE_MS,\n );\n syncRelayDebounceTimer.unref();\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, agentIdForSubscribe, log);\n });\n\n ws.on('error', (err: Error) => {\n log.debug(`Sync Relay error: ${err.message}`);\n });\n\n return ws 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, agentIdForSubscribe, log);\n return null;\n }\n}\n\nfunction scheduleSyncRelayReconnect(\n relayUrl: string,\n token: string,\n agentIdForSubscribe: string,\n log: PluginLogger,\n) {\n clearSyncRelayReconnect();\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 log.debug(\n `Reconnecting to Sync Relay in ${String(delay)}ms (attempt ${String(syncRelayReconnectAttempt)})`,\n );\n\n syncRelayReconnectTimer = setTimeout(() => {\n void (async () => {\n syncRelayWs = await connectToSyncRelay(relayUrl, token, agentIdForSubscribe, log);\n })();\n }, delay);\n syncRelayReconnectTimer.unref();\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\nfunction deriveRelayUrl(apiUrl: string): string {\n if (apiUrl.includes('dev.alfe.ai')) return 'wss://sync.dev.alfe.ai/ws';\n if (apiUrl.includes('demo.alfe.ai')) return 'wss://sync.demo.alfe.ai/ws';\n if (apiUrl.includes('test.alfe.ai')) return 'wss://sync.test.alfe.ai/ws';\n return 'wss://sync.alfe.ai/ws';\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: pkg.version,\n\n activate(api: PluginApi) {\n const log = api.logger;\n\n const pluginConfig: SyncPluginConfig = api.config ?? {};\n currentConfig = pluginConfig;\n\n let alfeConfig: { workspacePath: string; socketPath: string; runtime: string } | null = null;\n try {\n alfeConfig = resolveAlfeConfig();\n } catch {\n // alfe login hasn't run; logged below\n }\n\n const workspacePath =\n pluginConfig.workspacePath ?? alfeConfig?.workspacePath ?? DEFAULT_WORKSPACE_PATH;\n // Active runtime selects the per-runtime default ignore set (openclaw npm/\n // extensions/ plugins/, hermes .venv/ hermes-agent/) so the watcher never\n // descends into runtime-internal dirs. Falls back to openclaw.\n const runtime = alfeConfig?.runtime ?? 'openclaw';\n\n const syncScope = pluginConfig.syncScope ?? ['config', 'conversations', 'memory'];\n // Default to 'realtime' when no schedule is configured. The integration's\n // user-config schema marks sync_schedule as required, so an unset value\n // here means the framework didn't deliver the user's choice (manifest\n // template missing, applyConfig errored, etc.). 'daily' would leave a\n // managed agent silent for 24 hours after activation; 'realtime' fails\n // open into the safest mode — initial push runs immediately and the\n // watcher streams ongoing changes.\n const syncSchedule = pluginConfig.syncSchedule ?? 'realtime';\n const socketPath =\n pluginConfig.socketPath ?? alfeConfig?.socketPath ?? DEFAULT_SOCKET_PATH;\n\n const startSyncService = async () => {\n if ((globalThis as Record<string, unknown>).__alfeSyncPluginActivated === true) {\n log.debug('Alfe Sync plugin already activated — skipping duplicate');\n return;\n }\n (globalThis as Record<string, unknown>).__alfeSyncPluginActivated = true;\n log.info('Alfe Sync plugin activating...');\n log.info(`Sync scope: ${syncScope.join(', ')}`);\n log.info(`Sync schedule: ${syncSchedule}`);\n log.info(`Workspace: ${workspacePath}`);\n\n // Sync needs `~/.alfe/config.toml` (the existing `alfe login` config).\n // Identity (agentId / tenantId) is resolved server-side from the API\n // key on every AgentApiClient call.\n if (!alfeConfigExists()) {\n log.info('Sync skipped — no Alfe config found. Run `alfe login` to enable.');\n return;\n }\n\n let syncCfg: AlfeResolvedConfig;\n try {\n syncCfg = resolveAlfeConfig();\n } catch (err: unknown) {\n log.warn(\n `Sync skipped — failed to resolve credentials from ~/.alfe/config.toml: ${err instanceof Error ? err.message : String(err)}`,\n );\n return;\n }\n\n client = new AgentApiClient({ apiKey: syncCfg.apiKey, apiUrl: syncCfg.apiUrl });\n syncEngine = createSyncEngine({ workspacePath, client, runtime });\n log.info('Sync engine initialized');\n\n // Realtime mode → first-run reconcile of pre-existing files (chokidar\n // ignoreInitial means the watcher only emits for post-activation\n // changes; without this the workspace tree never reaches the cloud\n // unless a file is touched). This RECONCILES with cloud state rather\n // than blindly pushing: on a from-scratch VM rebuild the local\n // manifest is gone and `alfe setup` has just re-seeded the persona\n // templates, so a blind push(undefined) would upload those fresh seeds\n // OVER the agent's edited cloud copy and lose its edits. firstRunReconcile\n // restores cloud → local (cloud wins) when the cloud already holds\n // state, and only push-seeds files that are genuinely new locally. A\n // brand-new agent (empty cloud) still gets the push-only seed. See\n // sync-engine.ts firstRunReconcile for the fresh-vs-rebuild rationale.\n if (syncSchedule === 'realtime') {\n try {\n lastSyncResult = await syncEngine.firstRunReconcile({ quiet: true });\n log.info('Initial workspace reconcile complete');\n } catch (err: unknown) {\n log.warn(`Initial workspace reconcile failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n // Self-heal: purge cloud files that a pre-fix ignore engine uploaded\n // but the current per-runtime ignore set excludes (node_modules, the\n // openclaw npm cache, …). Bounded to ignored paths (capped per pass),\n // so legit files are never touched. Fire-and-forget so a large drain\n // can't stall watcher startup — the watcher already ignores these\n // paths, so they won't reappear, and the remainder clears next start.\n const engineForPrune = syncEngine;\n void engineForPrune\n .pruneIgnored({ quiet: true })\n .then((pruned) => {\n if (pruned.pushed > 0) {\n log.info(`Pruned ${String(pruned.pushed)} ignored file(s) from cloud`);\n }\n })\n .catch((err: unknown) => {\n log.warn(`Ignored-file prune failed: ${err instanceof Error ? err.message : String(err)}`);\n });\n deleteBrake = createDeleteBrake();\n try {\n stopWatcher = await startWatcher({\n workspacePath,\n runtime,\n debounceMs: 2000,\n onChanges: async (paths) => {\n if (!syncEngine) return;\n const existing: string[] = [];\n const missing: string[] = [];\n for (const p of paths) {\n if (existsSync(join(workspacePath, p))) existing.push(p);\n else missing.push(p);\n }\n log.debug(\n `Realtime sync: ${String(existing.length)} upload(s), ${String(missing.length)} delete(s)`,\n );\n if (existing.length > 0) {\n try {\n lastSyncResult = await syncEngine.push(existing, { quiet: true });\n } catch (err: unknown) {\n log.error(`Realtime push failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n if (missing.length > 0) {\n // Rolling-window rate brake: refuse the batch if cumulative\n // deletes in the last 60 s would exceed 30% of the manifest.\n // See packages/openclaw-sync/src/delete-brake.ts.\n const manifest = await readManifest(workspacePath);\n const manifestSize = Object.keys(manifest.files).length;\n if (deleteBrake && !deleteBrake.check(missing.length, manifestSize)) {\n log.error(\n `Sync delete brake tripped: ${String(deleteBrake.windowSum() + missing.length)} ` +\n `deletes in last 60s vs manifest size ${String(manifestSize)} ` +\n `(threshold 30%). Refusing batch — investigate the workspace state.`,\n );\n } else {\n try {\n lastSyncResult = await syncEngine.pushDeletes(missing, { quiet: true });\n } catch (err: unknown) {\n log.error(`Realtime delete failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\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 } else {\n setupSchedule(syncSchedule, log);\n }\n\n // Daemon IPC (for SYNC_NOW + SHARED_SCOPES messages)\n daemonIpcClient = await connectToDaemon(socketPath, log);\n\n // Register with the sync service to materialize the SyncAgent record\n // and learn our own agentId for relay subscription.\n let registered: SyncAgentInfo | null = null;\n try {\n const result = await client.syncRegister();\n registered = result.agent;\n agentId = registered.agentId;\n } catch (err: unknown) {\n log.warn(`Sync register failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n // Sync relay (real-time file change notifications)\n if (registered) {\n try {\n const relayUrl = pluginConfig.syncRelayUrl ?? deriveRelayUrl(syncCfg.apiUrl);\n syncRelayWs = await connectToSyncRelay(\n relayUrl,\n syncCfg.apiKey,\n registered.agentId,\n log,\n );\n } catch (err: unknown) {\n log.debug(\n `Sync Relay connection skipped: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Shared sync (org/team/project scopes pushed by gateway)\n if (pluginConfig.sharedSync !== false) {\n try {\n sharedSyncEngine = createSharedSyncEngine({ workspacePath, client }, log);\n log.info('Shared sync engine created — waiting for SHARED_SCOPES from gateway');\n } catch (err: unknown) {\n log.debug(\n `Shared sync engine skipped: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n };\n\n const stopSyncService = async () => {\n (globalThis as Record<string, unknown>).__alfeSyncPluginActivated = false;\n clearSchedule();\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 deleteBrake = null;\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 client = null;\n agentId = null;\n syncEngine = null;\n sharedSyncEngine = null;\n lastSyncResult = null;\n currentConfig = {};\n\n log.info('Alfe Sync plugin deactivated');\n };\n\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 `alfe login`' };\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 Promise.resolve({\n ok: true,\n initialized: !!syncEngine,\n agentId,\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 api.registerService({\n id: 'alfe-sync-engine',\n start: () => startSyncService(),\n stop: () => stopSyncService(),\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 const log = api.logger;\n log.info('Alfe Sync plugin deactivating...');\n\n clearSchedule();\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 client = null;\n agentId = null;\n syncEngine = null;\n sharedSyncEngine = null;\n lastSyncResult = null;\n currentConfig = {};\n\n log.info('Alfe Sync plugin deactivated');\n },\n\n async configure(api: PluginApi, config: SyncPluginConfig) {\n const log = api.logger;\n log.info('Reconfiguring Alfe Sync plugin...');\n currentConfig = { ...currentConfig, ...config };\n\n if (config.syncSchedule) {\n if (config.syncSchedule === 'realtime' && syncEngine && !stopWatcher) {\n const workspacePath = currentConfig.workspacePath ?? syncEngine.workspacePath;\n stopWatcher = await startWatcher({\n workspacePath,\n runtime: syncEngine.runtime,\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":";;;;;;;;;;;;;;;;;;;;AA8BA,eAAsB,aACpB,SAC8B;CAC9B,MAAM,EAAE,eAAe,UAAU,YAAY,aAAa,KAAM,cAAc;CAC9E,MAAM,iBAAiB,MAAM,mBAAmB,eAAe,QAAQ;CAGvE,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;EAG1C,MAAM,eAAe,SAAS,eAAe,aAAa,CAAC,QAAQ,OAAO,IAAI;AAG9E,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,UAAU,GAAW,UAAuC;GAC1D,MAAM,MAAM,SAAS,eAAe,EAAE,CAAC,QAAQ,OAAO,IAAI;AAC1D,OAAI,QAAQ,GAAI,QAAO;AACvB,OAAI,aAAa,KAAK,eAAe,CAAE,QAAO;AAC9C,OAAI,SAAS,CAAC,MAAM,aAAa,CAAE,QAAO;AAC1C,UAAO,gBAAgB,KAAK,eAAe;;EAE9C,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;;;;;;;;;;;;;;;;ACxGzB,MAAM,uBAAuB,MAAM,OAAO;;AAG1C,SAAS,gBAAgB,SAAiB,cAA4B;CACpE,MAAM,iBAAiB,UAAU,QAAQ,GAAG;CAC5C,MAAM,iBAAiB,UAAU,aAAa;AAC9C,KACE,CAAC,eAAe,WAAW,eAAe,IAC1C,mBAAmB,UAAU,QAAQ,CAErC,OAAM,IAAI,MAAM,2BAA2B,aAAa,WAAW,UAAU;;AA8BjF,SAAgB,uBACd,QACA,KACkB;CAClB,IAAI,eAA8B,EAAE;CACpC,MAAM,YAAY,KAAK,OAAO,eAAe,SAAS;CAEtD,SAAS,SAAS,OAA4B;AAC5C,MAAI,MAAM,cAAc,MAAO,QAAO,KAAK,WAAW,MAAM;AAE5D,SAAO,KAAK,WADG,MAAM,cAAc,SAAS,UAAU,YACvB,MAAM,QAAQ;;CAG/C,eAAe,aACb,OACA,UACA,WACe;AACf,kBAAgB,SAAS,MAAM,EAAE,UAAU;EAE3C,MAAM,EAAE,gBAAgB,MAAM,OAAO,OAAO,kBAAkB;GAC5D,OAAO,MAAM;GACb,SAAS,MAAM;GACf;GACD,CAAC;EAGF,MAAM,WAAW,MAAM,MAAM,YAAY;AACzC,MAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,yBAAyB,OAAO,SAAS,OAAO,GAAG;EAGrE,MAAM,gBAAgB,SACpB,SAAS,QAAQ,IAAI,iBAAiB,IAAI,KAC1C,GACD;AACD,MAAI,gBAAgB,qBAClB,OAAM,IAAI,MACR,mBAAmB,OAAO,cAAc,CAAC,iBAAiB,OAAO,qBAAqB,CAAC,QACxF;EAGH,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,aAAa,CAAC;AACxD,MAAI,OAAO,SAAS,qBAClB,OAAM,IAAI,MAAM,uCAAuC,OAAO,OAAO,OAAO,CAAC,QAAQ;AAGvF,QAAM,MAAM,QAAQ,UAAU,EAAE,EAAE,WAAW,MAAM,CAAC;AACpD,QAAM,UAAU,WAAW,OAAO;;CAGpC,eAAe,UAAU,OAAmC;EAC1D,MAAM,MAAM,SAAS,MAAM;AAC3B,QAAM,MAAM,KAAK,EAAE,WAAW,MAAM,CAAC;AAErC,MAAI;GACF,MAAM,EAAE,UAAU,MAAM,OAAO,OAAO,gBAAgB;IACpD,OAAO,MAAM;IACb,SAAS,MAAM;IAChB,CAAC;AAEF,QAAK,MAAM,QAAQ,OAAO;IACxB,MAAM,YAAY,KAAK,KAAK,KAAK,SAAS;AAC1C,QAAI;AACF,WAAM,aAAa,OAAO,KAAK,UAAU,UAAU;AACnD,SAAI,MACF,2BAA2B,MAAM,UAAU,GAAG,MAAM,QAAQ,GAAG,KAAK,WACrE;aACM,KAAc;AACrB,SAAI,MACF,mCAAmC,KAAK,SAAS,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACtG;;;WAGE,KAAc;AACrB,OAAI,MACF,yCAAyC,MAAM,UAAU,GAAG,MAAM,QAAQ,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC/H;;;CAIL,SAAS,gBACP,UACqD;AACrD,MAAI,SAAS,SAAS,KAAK,CAAE,QAAO;EAEpC,MAAM,WAAW,sBAAsB,KAAK,SAAS;AACrD,MAAI,UAAU;GACZ,MAAM,QAAQ,aAAa,MAAM,MAAM,EAAE,cAAc,MAAM;AAC7D,OAAI,MAAO,QAAO;IAAE;IAAO,cAAc,SAAS;IAAI;;EAGxD,MAAM,YAAY,iCAAiC,KAAK,SAAS;AACjE,MAAI,WAAW;GACb,MAAM,QAAQ,aAAa,MACxB,MAAM,EAAE,cAAc,UAAU,EAAE,YAAY,UAAU,GAC1D;AACD,OAAI,MAAO,QAAO;IAAE;IAAO,cAAc,UAAU;IAAI;;EAGzD,MAAM,eAAe,oCAAoC,KAAK,SAAS;AACvE,MAAI,cAAc;GAChB,MAAM,QAAQ,aAAa,MACxB,MAAM,EAAE,cAAc,aAAa,EAAE,YAAY,aAAa,GAChE;AACD,OAAI,MAAO,QAAO;IAAE;IAAO,cAAc,aAAa;IAAI;;AAG5D,SAAO;;AAGT,QAAO;EACL,MAAM,WAAW,QAAsC;AACrD,kBAAe,CAAC,GAAG,OAAO;AAC1B,OAAI,KAAK,kCAAkC,OAAO,OAAO,OAAO,CAAC,WAAW;AAC5E,SAAM,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;AAC3C,QAAK,MAAM,SAAS,OAClB,OAAM,UAAU,MAAM;AAExB,OAAI,KAAK,uCAAuC;;EAGlD,MAAM,aAAa,WAAyC;GAC1D,MAAM,SAAS,IAAI,IAAI,aAAa,KAAK,MAAM,GAAG,EAAE,UAAU,GAAG,EAAE,UAAU,CAAC;GAC9E,MAAM,SAAS,IAAI,IAAI,UAAU,KAAK,MAAM,GAAG,EAAE,UAAU,GAAG,EAAE,UAAU,CAAC;AAE3E,QAAK,MAAM,SAAS,cAAc;IAChC,MAAM,MAAM,GAAG,MAAM,UAAU,GAAG,MAAM;AACxC,QAAI,CAAC,OAAO,IAAI,IAAI,EAAE;KACpB,MAAM,MAAM,SAAS,MAAM;AAC3B,SAAI;AACF,YAAM,GAAG,KAAK;OAAE,WAAW;OAAM,OAAO;OAAM,CAAC;AAC/C,UAAI,KAAK,wCAAwC,MAAM;cAChD,KAAc;AACrB,UAAI,KACF,iCAAiC,IAAI,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC1F;;;;AAKP,QAAK,MAAM,SAAS,WAAW;IAC7B,MAAM,MAAM,GAAG,MAAM,UAAU,GAAG,MAAM;AACxC,QAAI,CAAC,OAAO,IAAI,IAAI,EAAE;AACpB,SAAI,KACF,0BAA0B,MAAM,UAAU,GAAG,MAAM,QAAQ,kBAC5D;AACD,WAAM,UAAU,MAAM;;;AAI1B,kBAAe,CAAC,GAAG,UAAU;;EAG/B,MAAM,mBACJ,UACA,WACe;GACf,MAAM,SAAS,gBAAgB,SAAS;AACxC,OAAI,CAAC,QAAQ;AACX,QAAI,MAAM,wDAAwD,WAAW;AAC7E;;GAGF,MAAM,MAAM,SAAS,OAAO,MAAM;GAClC,MAAM,YAAY,KAAK,KAAK,OAAO,aAAa;AAEhD,OAAI;AACF,oBAAgB,KAAK,UAAU;WACzB;AACN,QAAI,KAAK,2CAA2C,WAAW;AAC/D;;AAGF,OAAI,cAAc,WAAW;AAC3B,QAAI;AACF,WAAM,OAAO,UAAU;AACvB,SAAI,MAAM,wBAAwB,WAAW;YACvC;AAGR;;AAGF,OAAI;AACF,UAAM,aAAa,OAAO,OAAO,OAAO,cAAc,UAAU;AAChE,QAAI,MAAM,uBAAuB,WAAW;YACrC,KAAc;AACrB,QAAI,MACF,+BAA+B,SAAS,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC7F;;;EAIL,MAAM,WAA0B;AAC9B,OAAI,KAAK,iCAAiC,OAAO,aAAa,OAAO,CAAC,WAAW;AACjF,QAAK,MAAM,SAAS,aAClB,OAAM,UAAU,MAAM;;EAI1B,YAA2B;AACzB,UAAO,CAAC,GAAG,aAAa;;EAE3B;;;;ACpPH,MAAM,oBAAoB;AAC1B,MAAM,0BAA0B;AAkBhC,SAAgB,kBAAkB,OAA2B,EAAE,EAAe;CAC5E,MAAM,WAAW,KAAK,YAAY;CAClC,MAAM,iBAAiB,KAAK,kBAAkB;CAC9C,MAAM,MAAM,KAAK,cAAc,KAAK,KAAK;CAEzC,MAAM,UAA2C,EAAE;CAEnD,SAAS,SAAe;EACtB,MAAM,SAAS,KAAK,GAAG;AACvB,SAAO,QAAQ,SAAS,KAAK,QAAQ,GAAG,KAAK,OAAQ,SAAQ,OAAO;;CAGtE,SAAS,MAAc;AACrB,SAAO,QAAQ,QAAQ,KAAK,MAAM,MAAM,EAAE,OAAO,EAAE;;AAGrD,QAAO;EACL,MAAM,aAAa,cAAc;AAC/B,OAAI,eAAe,EAAG,QAAO;AAC7B,WAAQ;AAER,OAAI,iBAAiB,GAAG;AACtB,YAAQ,KAAK;KAAE,IAAI,KAAK;KAAE,OAAO;KAAa,CAAC;AAC/C,WAAO;;AAGT,QADkB,KAAK,GAAG,eACV,eAAe,eAAgB,QAAO;AACtD,WAAQ,KAAK;IAAE,IAAI,KAAK;IAAE,OAAO;IAAa,CAAC;AAC/C,UAAO;;EAET,YAAY;AACV,WAAQ;AACR,UAAO,KAAK;;EAEf;;;;;;;;;;;;;;;;;;;;;AC/BH,MAAM,MADU,cAAc,OAAO,KAAK,IAAI,CAC1B,kBAAkB;AAGtC,MAAM,oBAAoB;CAAC;CAAa;CAAa;CAAgB;AACrE,MAAM,+BAA+B;AACrC,MAAM,8BAA8B;AACpC,MAAM,yBAAyB;AAsE/B,IAAI,SAAgC;AACpC,IAAI,UAAyB;AAC7B,IAAI,aAAgC;AACpC,IAAI,mBAA4C;AAChD,IAAI,cAA4C;AAChD,IAAI,cAAkC;AACtC,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,wCAAwB,IAAI,KAG/B;AAIH,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;AACf,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;AACd,mBAAkB,OAAO;;AAK3B,eAAe,gBAAgB,YAAoB,KAA8C;AAC/F,KAAI;EAKF,MAAM,MAAM,KAHC,OAAM,OADH,uBAII,UAAU,YAAY,IAAI;AAE9C,MAAI,GAAG,mBAAmB;AACxB,IAAM,YAAY;AAChB,QAAI,KAAK,8DAA8D;IACvE,MAAM,WAAW,MAAM,IAAI,QAAQ,uBAAuB;KACxD,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,MAAI,GAAG,iBAAiB,GAAG,SAAoB;GAC7C,MAAM,SAAS,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK,OAAO,KAAK,GAAG;AACtE,OAAI,KAAK,kCAAkC,SAAS;IACpD;AAEF,MAAI,GAAG,YAAY,GAAG,SAAoB;GACxC,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;;;AAIR,OAAI,KAAK,SAAS,iBAAiB;IACjC,MAAM,SAAS,IAAI;IACnB,MAAM,SAAS;AACf,QAAI,UAAU,QAAQ;AACpB,SAAI,KAAK,kCAAkC,OAAO,OAAO,OAAO,CAAC,WAAW;AAC5E,MAAM,YAAY;AAChB,UAAI;AACF,aAAM,OAAO,aAAa,OAAO;eAC1B,KAAc;AACrB,WAAI,MACF,gCAAgC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACjF;;SAED;;;IAGR;AAEF,MAAI,GAAG,UAAU,GAAG,SAAoB;GACtC,MAAM,MAAM,KAAK;AACjB,OAAI,MAAM,qBAAqB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;IAClF;AAEF,MAAI,OAAO;AACX,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,sBAAsB,SAAS,EAAG;CAEtC,MAAM,UAAU,IAAI,IAAI,sBAAsB;AAC9C,uBAAsB,OAAO;CAE7B,MAAM,gCAAgB,IAAI,KAAkE;CAC5F,MAAM,iCAAiB,IAAI,KAAkE;AAE7F,MAAK,MAAM,CAAC,UAAU,SAAS,QAC7B,KAAI,SAAS,WAAW,UAAU,CAChC,eAAc,IAAI,UAAU,KAAK;KAEjC,gBAAe,IAAI,UAAU,KAAK;AAItC,KAAI,cAAc,OAAO,KAAK,iBAC5B,MAAK,MAAM,CAAC,UAAU,SAAS,cAC7B,KAAI;AACF,QAAM,iBAAiB,mBAAmB,UAAU,KAAK,UAAU;UAC5D,KAAc;AACrB,MAAI,MACF,6BAA6B,KAAK,UAAU,GAAG,SAAS,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC7G;;AAKP,KAAI,eAAe,OAAO,KAAK,YAAY;EACzC,MAAM,SAAS;EACf,MAAM,SAAmB,EAAE;EAC3B,MAAM,WAAqB,EAAE;AAE7B,OAAK,MAAM,CAAC,UAAU,SAAS,eAC7B,KAAI,KAAK,cAAc,UACrB,UAAS,KAAK,SAAS;MAEvB,QAAO,KAAK,SAAS;AAIzB,OAAK,MAAM,YAAY,SACrB,KAAI;AACF,SAAM,OAAO,gBAAgB,UAAU,EAAE,OAAO,MAAM,CAAC;AACvD,OAAI,MAAM,uBAAuB,WAAW;WACrC,KAAc;AACrB,OAAI,MACF,gCAAgC,SAAS,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC9F;;AAIL,MAAI,OAAO,SAAS,EAClB,KAAI;GACF,MAAM,SAAS,MAAM,OAAO,UAAU,QAAQ,EAAE,OAAO,MAAM,CAAC;AAC9D,OAAI,OAAO,SAAS,EAAG,KAAI,KAAK,sBAAsB,OAAO,OAAO,OAAO,CAAC,UAAU;AACtF,OAAI,OAAO,SAAS,EAAG,KAAI,KAAK,eAAe,OAAO,OAAO,OAAO,CAAC,gBAAgB;WAC9E,KAAc;AACrB,OAAI,MAAM,4BAA4B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;;;AAMjG,eAAe,mBACb,UACA,OACA,qBACA,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;AAC5B,MAAG,KAAK,KAAK,UAAU;IAAE,MAAM;IAAa,SAAS;IAAqB,CAAC,CAAC;IAC5E;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,MAAM;AAC3B,UAAI,KAAK,8CAA8C,QAAQ,WAAW,sBAAsB;MAChG,MAAM,eAAe;AACrB,UAAI,aACF,EAAM,YAAY;AAChB,WAAI;AACF,cAAM,aAAa,UAAU;AAC7B,YAAI,KAAK,4CAA4C;gBAC9C,KAAc;AACrB,YAAI,MACF,4CAA4C,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC7F;;UAED;WAGN,KAAI,KAAK,gCAAgC,QAAQ,WAAW,YAAY;AAE1E;IAEF,KAAK;AACH,SAAI,QAAQ,SACV,uBAAsB,IAAI,QAAQ,UAAU;MAC1C,MAAM,QAAQ;MACd,WAAW,QAAQ,cAAc,YAAY,YAAY;MAC1D,CAAC;AAEJ,6BAAwB;AACxB,8BAAyB,iBACjB,KAAK,4BAA4B,IAAI,EAC3C,uBACD;AACD,4BAAuB,OAAO;AAC9B;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,qBAAqB,IAAI;IACrE;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,qBAAqB,IAAI;AACrE,SAAO;;;AAIX,SAAS,2BACP,UACA,OACA,qBACA,KACA;AACA,0BAAyB;CACzB,MAAM,QAAQ,KAAK,IACjB,+BAA+B,KAAK,IAAI,GAAG,0BAA0B,EACrE,4BACD;AACD;AACA,KAAI,MACF,iCAAiC,OAAO,MAAM,CAAC,cAAc,OAAO,0BAA0B,CAAC,GAChG;AAED,2BAA0B,iBAAiB;AACzC,GAAM,YAAY;AAChB,iBAAc,MAAM,mBAAmB,UAAU,OAAO,qBAAqB,IAAI;MAC/E;IACH,MAAM;AACT,yBAAwB,OAAO;;AAGjC,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;;;AAIlB,SAAS,eAAe,QAAwB;AAC9C,KAAI,OAAO,SAAS,cAAc,CAAE,QAAO;AAC3C,KAAI,OAAO,SAAS,eAAe,CAAE,QAAO;AAC5C,KAAI,OAAO,SAAS,eAAe,CAAE,QAAO;AAC5C,QAAO;;AAKT,MAAM,SAAS;CACb,IAAI;CACJ,MAAM;CACN,aACE;CACF,SAAS,IAAI;CAEb,SAAS,KAAgB;EACvB,MAAM,MAAM,IAAI;EAEhB,MAAM,eAAiC,IAAI,UAAU,EAAE;AACvD,kBAAgB;EAEhB,IAAI,aAAoF;AACxF,MAAI;AACF,gBAAaA,eAAmB;UAC1B;EAIR,MAAM,gBACJ,aAAa,iBAAiB,YAAY,iBAAiB;EAI7D,MAAM,UAAU,YAAY,WAAW;EAEvC,MAAM,YAAY,aAAa,aAAa;GAAC;GAAU;GAAiB;GAAS;EAQjF,MAAM,eAAe,aAAa,gBAAgB;EAClD,MAAM,aACJ,aAAa,cAAc,YAAY,cAAc;EAEvD,MAAM,mBAAmB,YAAY;AACnC,OAAK,WAAuC,8BAA8B,MAAM;AAC9E,QAAI,MAAM,0DAA0D;AACpE;;AAED,cAAuC,4BAA4B;AACpE,OAAI,KAAK,iCAAiC;AAC1C,OAAI,KAAK,eAAe,UAAU,KAAK,KAAK,GAAG;AAC/C,OAAI,KAAK,kBAAkB,eAAe;AAC1C,OAAI,KAAK,cAAc,gBAAgB;AAKvC,OAAI,CAACC,cAAkB,EAAE;AACvB,QAAI,KAAK,mEAAmE;AAC5E;;GAGF,IAAI;AACJ,OAAI;AACF,cAAUD,eAAmB;YACtB,KAAc;AACrB,QAAI,KACF,0EAA0E,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC3H;AACD;;AAGF,YAAS,IAAI,eAAe;IAAE,QAAQ,QAAQ;IAAQ,QAAQ,QAAQ;IAAQ,CAAC;AAC/E,gBAAa,iBAAiB;IAAE;IAAe;IAAQ;IAAS,CAAC;AACjE,OAAI,KAAK,0BAA0B;AAcnC,OAAI,iBAAiB,YAAY;AAC/B,QAAI;AACF,sBAAiB,MAAM,WAAW,kBAAkB,EAAE,OAAO,MAAM,CAAC;AACpE,SAAI,KAAK,uCAAuC;aACzC,KAAc;AACrB,SAAI,KAAK,uCAAuC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAQ9E,eAEpB,aAAa,EAAE,OAAO,MAAM,CAAC,CAC7B,MAAM,WAAW;AAChB,SAAI,OAAO,SAAS,EAClB,KAAI,KAAK,UAAU,OAAO,OAAO,OAAO,CAAC,6BAA6B;MAExE,CACD,OAAO,QAAiB;AACvB,SAAI,KAAK,8BAA8B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;MAC1F;AACJ,kBAAc,mBAAmB;AACjC,QAAI;AACF,mBAAc,MAAM,aAAa;MAC/B;MACA;MACA,YAAY;MACZ,WAAW,OAAO,UAAU;AAC1B,WAAI,CAAC,WAAY;OACjB,MAAM,WAAqB,EAAE;OAC7B,MAAM,UAAoB,EAAE;AAC5B,YAAK,MAAM,KAAK,MACd,KAAI,WAAW,KAAK,eAAe,EAAE,CAAC,CAAE,UAAS,KAAK,EAAE;WACnD,SAAQ,KAAK,EAAE;AAEtB,WAAI,MACF,kBAAkB,OAAO,SAAS,OAAO,CAAC,cAAc,OAAO,QAAQ,OAAO,CAAC,YAChF;AACD,WAAI,SAAS,SAAS,EACpB,KAAI;AACF,yBAAiB,MAAM,WAAW,KAAK,UAAU,EAAE,OAAO,MAAM,CAAC;gBAC1D,KAAc;AACrB,YAAI,MAAM,yBAAyB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAG1F,WAAI,QAAQ,SAAS,GAAG;QAItB,MAAM,WAAW,MAAM,aAAa,cAAc;QAClD,MAAM,eAAe,OAAO,KAAK,SAAS,MAAM,CAAC;AACjD,YAAI,eAAe,CAAC,YAAY,MAAM,QAAQ,QAAQ,aAAa,CACjE,KAAI,MACF,8BAA8B,OAAO,YAAY,WAAW,GAAG,QAAQ,OAAO,CAAC,wCACrC,OAAO,aAAa,CAAC,qEAEhE;YAED,KAAI;AACF,0BAAiB,MAAM,WAAW,YAAY,SAAS,EAAE,OAAO,MAAM,CAAC;iBAChE,KAAc;AACrB,aAAI,MAAM,2BAA2B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;;;MAKjG,CAAC;AACF,SAAI,KAAK,yCAAyC;aAC3C,KAAc;AACrB,SAAI,KAAK,iCAAiC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;SAG/F,eAAc,cAAc,IAAI;AAIlC,qBAAkB,MAAM,gBAAgB,YAAY,IAAI;GAIxD,IAAI,aAAmC;AACvC,OAAI;AAEF,kBADe,MAAM,OAAO,cAAc,EACtB;AACpB,cAAU,WAAW;YACd,KAAc;AACrB,QAAI,KAAK,yBAAyB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAIvF,OAAI,YAAY;AACd,QAAI;AAEF,mBAAc,MAAM,mBADH,aAAa,gBAAgB,eAAe,QAAQ,OAAO,EAG1E,QAAQ,QACR,WAAW,SACX,IACD;aACM,KAAc;AACrB,SAAI,MACF,kCAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACnF;;AAIH,QAAI,aAAa,eAAe,MAC9B,KAAI;AACF,wBAAmB,uBAAuB;MAAE;MAAe;MAAQ,EAAE,IAAI;AACzE,SAAI,KAAK,sEAAsE;aACxE,KAAc;AACrB,SAAI,MACF,+BAA+B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAChF;;;;EAMT,MAAM,kBAAkB,YAAY;AACjC,cAAuC,4BAA4B;AACpE,kBAAe;AACf,wBAAqB;AAErB,OAAI,aAAa;AACf,QAAI;AACF,WAAM,aAAa;AACnB,SAAI,KAAK,uBAAuB;aACzB,KAAc;AACrB,SAAI,MAAM,2BAA2B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAE1F,kBAAc;;AAEhB,iBAAc;AAEd,OAAI,iBAAiB;AACnB,QAAI;AACF,qBAAgB,MAAM;AACtB,SAAI,KAAK,gCAAgC;aAClC,KAAc;AACrB,SAAI,MAAM,oCAAoC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;AAEnG,sBAAkB;;AAGpB,YAAS;AACT,aAAU;AACV,gBAAa;AACb,sBAAmB;AACnB,oBAAiB;AACjB,mBAAgB,EAAE;AAElB,OAAI,KAAK,+BAA+B;;AAG1C,MAAI,OAAO,IAAI,0BAA0B,YAAY;AACnD,OAAI,sBAAsB,YAAY,YAAY;AAChD,QAAI,CAAC,WACH,QAAO;KAAE,IAAI;KAAO,OAAO;KAAkD;AAE/E,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,qBACxB,QAAQ,QAAQ;IACd,IAAI;IACJ,aAAa,CAAC,CAAC;IACf;IACA,UAAU,cAAc,gBAAgB;IACxC,OAAO,cAAc,aAAa;KAAC;KAAU;KAAiB;KAAS;IACvE,YAAY;IACZ,eAAe,CAAC,CAAC;IAClB,CAAC,CACH;AACD,OAAI,KAAK,6CAA6C;;AAGxD,MAAI,gBAAgB;GAClB,IAAI;GACJ,aAAa,kBAAkB;GAC/B,YAAY,iBAAiB;GAC9B,CAAC;AAEF,MAAI,KAAK,6BAA6B;;CAGxC,MAAM,WAAW,KAAgB;AAC9B,aAAuC,4BAA4B;EACpE,MAAM,MAAM,IAAI;AAChB,MAAI,KAAK,mCAAmC;AAE5C,iBAAe;AACf,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,WAAS;AACT,YAAU;AACV,eAAa;AACb,qBAAmB;AACnB,mBAAiB;AACjB,kBAAgB,EAAE;AAElB,MAAI,KAAK,+BAA+B;;CAG1C,MAAM,UAAU,KAAgB,QAA0B;EACxD,MAAM,MAAM,IAAI;AAChB,MAAI,KAAK,oCAAoC;AAC7C,kBAAgB;GAAE,GAAG;GAAe,GAAG;GAAQ;AAE/C,MAAI,OAAO;OACL,OAAO,iBAAiB,cAAc,cAAc,CAAC,aAAa;AAEpE,kBAAc,MAAM,aAAa;KAC/B,eAFoB,cAAc,iBAAiB,WAAW;KAG9D,SAAS,WAAW;KACpB,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"}
@@ -25,6 +25,7 @@ let node_fs = require("node:fs");
25
25
  let node_crypto = require("node:crypto");
26
26
  let node_path = require("node:path");
27
27
  let node_os = require("node:os");
28
+ let node_url = require("node:url");
28
29
  let micromatch = require("micromatch");
29
30
  micromatch = __toESM(micromatch);
30
31
  let _auriclabs_logger = require("@auriclabs/logger");
@@ -144,33 +145,39 @@ function diffManifests(local, remote) {
144
145
  }
145
146
  //#endregion
146
147
  //#region src/ignore.ts
147
- const DEFAULT_IGNORES = [
148
- "**/.alfesync/**",
148
+ const MODULE_DIR = (0, node_url.fileURLToPath)(new URL(".", require("url").pathToFileURL(__filename).href));
149
+ const DEFAULTS_DIR = [(0, node_path.join)(MODULE_DIR, "defaults"), (0, node_path.join)(MODULE_DIR, "..", "defaults")].find(node_fs.existsSync) ?? (0, node_path.join)(MODULE_DIR, "defaults");
150
+ function parseDefaultGlobs(text) {
151
+ return text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
152
+ }
153
+ function readDefaultGlobs(name) {
154
+ const file = (0, node_path.join)(DEFAULTS_DIR, `${name}.alfesyncignore`);
155
+ try {
156
+ return parseDefaultGlobs((0, node_fs.readFileSync)(file, "utf-8"));
157
+ } catch {
158
+ return [];
159
+ }
160
+ }
161
+ const CRITICAL_FALLBACK_IGNORES = [
149
162
  "**/node_modules/**",
150
- "**/*.tmp",
151
- "**/.DS_Store",
152
163
  "**/.git/**",
153
- "**/.sst/**",
154
- "**/.build/**",
155
- "**/dist/**",
156
164
  "**/.env",
157
- "**/.env.*",
158
- "**/openclaw.json.bak*",
159
- "**/openclaw.json.clobbered.*",
160
- "**/openclaw.json.preserve.*",
161
- "**/Cache/**",
162
- "**/Code Cache/**",
163
- "**/GPUCache/**",
164
- "**/CacheStorage/**",
165
- "**/Service Worker/CacheStorage/**",
166
- "**/blob_storage/**",
167
- "**/Crashpad/**",
168
- "**/ShaderCache/**",
169
- "**/component_crx_cache/**",
170
165
  "**/__pycache__/**",
171
- "**/*.pyc",
172
- "**/*.log"
166
+ "npm/**",
167
+ "extensions/**",
168
+ "plugins/**",
169
+ "**/.venv/**"
173
170
  ];
171
+ const commonGlobs = readDefaultGlobs("common");
172
+ const DEFAULT_IGNORES = commonGlobs.length > 0 ? commonGlobs : CRITICAL_FALLBACK_IGNORES;
173
+ const runtimeDefaultsCache = /* @__PURE__ */ new Map();
174
+ function runtimeDefaults(runtime) {
175
+ const cached = runtimeDefaultsCache.get(runtime);
176
+ if (cached) return cached;
177
+ const patterns = [...DEFAULT_IGNORES, ...readDefaultGlobs(runtime)];
178
+ runtimeDefaultsCache.set(runtime, patterns);
179
+ return patterns;
180
+ }
174
181
  function normaliseIgnorePattern(raw) {
175
182
  const trimmed = raw.trim();
176
183
  if (!trimmed) return trimmed;
@@ -184,9 +191,9 @@ function normaliseIgnorePattern(raw) {
184
191
  if (trimmed.endsWith("/")) return isAnchored ? `${stripped}/**` : `**/${stripped}/**`;
185
192
  return isAnchored ? trimmed : `**/${trimmed}`;
186
193
  }
187
- async function loadIgnorePatterns(workspacePath) {
194
+ async function loadIgnorePatterns(workspacePath, runtime = "openclaw") {
188
195
  const ignoreFile = (0, node_path.join)(workspacePath, ".alfesyncignore");
189
- const patterns = [...DEFAULT_IGNORES];
196
+ const patterns = [...runtimeDefaults(runtime)];
190
197
  if ((0, node_fs.existsSync)(ignoreFile)) try {
191
198
  const lines = (await (0, node_fs_promises.readFile)(ignoreFile, "utf-8")).split("\n");
192
199
  for (const line of lines) {
@@ -207,6 +214,11 @@ function shouldIgnore(relativePath, patterns) {
207
214
  if (negative.length === 0) return true;
208
215
  return !micromatch.default.isMatch(relativePath, negative, { dot: true });
209
216
  }
217
+ const IGNORE_DIR_PROBE = "__alfe_ignore_probe__";
218
+ function shouldIgnoreDir(relativeDir, patterns) {
219
+ if (relativeDir === "") return false;
220
+ return shouldIgnore(`${relativeDir}/${IGNORE_DIR_PROBE}`, patterns);
221
+ }
210
222
  function filterIgnored(paths, patterns) {
211
223
  return paths.filter((p) => !shouldIgnore(p, patterns));
212
224
  }
@@ -419,13 +431,14 @@ const log = (0, _auriclabs_logger.createLogger)("SyncEngine");
419
431
  * The client is constructed once in plugin.ts (or the CLI) and passed in,
420
432
  * so credentials never leak into multiple places.
421
433
  */
422
- function createSyncEngine({ workspacePath, client }) {
434
+ function createSyncEngine({ workspacePath, client, runtime = "openclaw" }) {
423
435
  return {
424
436
  workspacePath,
425
437
  client,
438
+ runtime,
426
439
  async push(paths, options = {}) {
427
440
  const { quiet = false, filter } = options;
428
- const ignorePatterns = await loadIgnorePatterns(workspacePath);
441
+ const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);
429
442
  let filesToPush = paths && paths.length > 0 ? filterIgnored(paths, ignorePatterns) : await detectLocalChanges(workspacePath, ignorePatterns);
430
443
  if (filter) filesToPush = filesToPush.filter((p) => p.startsWith(filter));
431
444
  if (filesToPush.length === 0) {
@@ -505,7 +518,7 @@ function createSyncEngine({ workspacePath, client }) {
505
518
  async fullSync(options = {}) {
506
519
  const { quiet = false } = options;
507
520
  const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
508
- const ignorePatterns = await loadIgnorePatterns(workspacePath);
521
+ const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);
509
522
  const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);
510
523
  const diff = diffManifests(localManifest, remoteManifest);
511
524
  const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));
@@ -557,6 +570,33 @@ function createSyncEngine({ workspacePath, client }) {
557
570
  errors: totalErrors
558
571
  };
559
572
  },
573
+ async firstRunReconcile(options = {}) {
574
+ const { quiet = false } = options;
575
+ const remoteManifest = await client.syncGetManifest();
576
+ const remotePaths = Object.keys(remoteManifest.files);
577
+ if (remotePaths.length === 0) {
578
+ if (!quiet) log.info("First-run sync: no cloud state — seeding workspace to cloud");
579
+ return this.push(void 0, options);
580
+ }
581
+ if (!quiet) log.info(`First-run sync: ${String(remotePaths.length)} file(s) in cloud — restoring cloud state before seeding`);
582
+ const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);
583
+ const restorePaths = filterIgnored(remotePaths, ignorePatterns);
584
+ const restore = await this.pullFiles(restorePaths, options);
585
+ const remoteSet = new Set(remotePaths);
586
+ const toSeed = (await detectLocalChanges(workspacePath, ignorePatterns)).filter((p) => !remoteSet.has(p));
587
+ const seed = toSeed.length > 0 ? await this.push(toSeed, options) : {
588
+ pushed: 0,
589
+ pulled: 0,
590
+ conflicts: 0,
591
+ errors: 0
592
+ };
593
+ return {
594
+ pushed: seed.pushed,
595
+ pulled: restore.pulled,
596
+ conflicts: restore.conflicts + seed.conflicts,
597
+ errors: restore.errors + seed.errors
598
+ };
599
+ },
560
600
  async pullFiles(paths, options = {}) {
561
601
  const { quiet = false } = options;
562
602
  if (paths.length === 0) return {
@@ -593,6 +633,24 @@ function createSyncEngine({ workspacePath, client }) {
593
633
  errors
594
634
  };
595
635
  },
636
+ async pruneIgnored(options = {}) {
637
+ const { quiet = false, limit = 1e3 } = options;
638
+ const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);
639
+ const remoteManifest = await client.syncGetManifest();
640
+ const ignoredPaths = Object.keys(remoteManifest.files).filter((p) => shouldIgnore(p, ignorePatterns));
641
+ if (ignoredPaths.length === 0) return {
642
+ pushed: 0,
643
+ pulled: 0,
644
+ conflicts: 0,
645
+ errors: 0
646
+ };
647
+ const batch = ignoredPaths.slice(0, limit);
648
+ if (!quiet) {
649
+ const capped = ignoredPaths.length > batch.length ? ` (capped at ${String(limit)}; ${String(ignoredPaths.length - batch.length)} remain for next pass)` : "";
650
+ log.info(`Pruning ${String(batch.length)} ignored file(s) from cloud${capped}`);
651
+ }
652
+ return this.pushDeletes(batch, { quiet });
653
+ },
596
654
  async removeLocalFile(filePath, options = {}) {
597
655
  const { quiet = false } = options;
598
656
  const absolutePath = (0, node_path.join)(workspacePath, filePath);
@@ -622,9 +680,8 @@ async function detectLocalChanges(workspacePath, ignorePatterns) {
622
680
  for (const entry of entries) {
623
681
  const fullPath = (0, node_path.join)(dir, entry.name);
624
682
  const relativePath = fullPath.slice(workspacePath.length + 1).replace(/\\/g, "/");
625
- if (shouldSkipDir(entry.name)) continue;
626
683
  if (entry.isDirectory()) {
627
- if (!ignorePatterns.some((p) => p.endsWith("/**") && relativePath.startsWith(p.slice(0, -3)))) await walk(fullPath);
684
+ if (!shouldIgnoreDir(relativePath, ignorePatterns)) await walk(fullPath);
628
685
  } else if (entry.isFile()) {
629
686
  if (shouldIgnore(relativePath, ignorePatterns)) continue;
630
687
  if (!(relativePath in manifest.files)) {
@@ -640,11 +697,13 @@ async function detectLocalChanges(workspacePath, ignorePatterns) {
640
697
  await walk(workspacePath);
641
698
  return changed;
642
699
  }
643
- /** Directories the engine never descends into. */
644
- function shouldSkipDir(name) {
645
- return name === "node_modules" || name === ".git" || name === ".sst" || name === ".build" || name === "dist";
646
- }
647
700
  //#endregion
701
+ Object.defineProperty(exports, "DEFAULT_IGNORES", {
702
+ enumerable: true,
703
+ get: function() {
704
+ return DEFAULT_IGNORES;
705
+ }
706
+ });
648
707
  Object.defineProperty(exports, "__toESM", {
649
708
  enumerable: true,
650
709
  get: function() {
@@ -705,6 +764,12 @@ Object.defineProperty(exports, "shouldIgnore", {
705
764
  return shouldIgnore;
706
765
  }
707
766
  });
767
+ Object.defineProperty(exports, "shouldIgnoreDir", {
768
+ enumerable: true,
769
+ get: function() {
770
+ return shouldIgnoreDir;
771
+ }
772
+ });
708
773
  Object.defineProperty(exports, "updateManifestEntry", {
709
774
  enumerable: true,
710
775
  get: function() {