@alfe.ai/openclaw-sync 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-engine.js","names":["log","MAX_RETRIES","BASE_DELAY_MS","formatBytes","log"],"sources":["../src/config.ts","../src/manifest.ts","../src/api-client.ts","../src/uploader.ts","../src/downloader.ts","../src/sync-engine.ts"],"sourcesContent":["/**\n * AlfeSync configuration — read/write `.alfesync/config.json` in workspace root.\n */\n\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport interface SyncConfig {\n agentId: string;\n orgId: string;\n token: string;\n workspacePath: string;\n apiUrl: string;\n}\n\nconst CONFIG_DIR = \".alfesync\";\nconst CONFIG_FILE = \"config.json\";\n\n/**\n * Resolve the .alfesync directory path for a given workspace root.\n */\nexport function configDir(workspacePath: string): string {\n return join(workspacePath, CONFIG_DIR);\n}\n\n/**\n * Resolve the config file path for a given workspace root.\n */\nexport function configPath(workspacePath: string): string {\n return join(workspacePath, CONFIG_DIR, CONFIG_FILE);\n}\n\n/**\n * Check if a workspace has been initialized with AlfeSync.\n */\nexport function isInitialized(workspacePath: string): boolean {\n return existsSync(configPath(workspacePath));\n}\n\n/**\n * Read the AlfeSync config from a workspace.\n * Returns null if not initialized.\n */\nexport async function readConfig(\n workspacePath: string,\n): Promise<SyncConfig | null> {\n const path = configPath(workspacePath);\n if (!existsSync(path)) return null;\n\n try {\n const raw = await readFile(path, \"utf-8\");\n const parsed = JSON.parse(raw) as SyncConfig;\n\n // Validate required fields\n if (!parsed.agentId || !parsed.token || !parsed.apiUrl) {\n return null;\n }\n\n return { ...parsed, workspacePath };\n } catch {\n return null;\n }\n}\n\n/**\n * Write the AlfeSync config to a workspace.\n * Creates the .alfesync directory if it doesn't exist.\n */\nexport async function writeConfig(config: SyncConfig): Promise<void> {\n const dir = configDir(config.workspacePath);\n await mkdir(dir, { recursive: true });\n\n const path = configPath(config.workspacePath);\n const data = {\n agentId: config.agentId,\n orgId: config.orgId,\n token: config.token,\n workspacePath: config.workspacePath,\n apiUrl: config.apiUrl,\n };\n\n await writeFile(path, JSON.stringify(data, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/**\n * Load config from workspace, throwing if not initialized.\n */\nexport async function requireConfig(\n workspacePath: string,\n): Promise<SyncConfig> {\n const config = await readConfig(workspacePath);\n if (!config) {\n throw new Error(\n `AlfeSync not initialized in ${workspacePath}. Run: alfesync init`,\n );\n }\n return config;\n}\n","/**\n * AlfeSync manifest — local file manifest at `.alfesync/manifest.json`.\n *\n * Tracks file hashes, sizes, sync timestamps, and storage classes\n * to enable efficient diff-based syncing.\n */\n\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync, createReadStream } from \"node:fs\";\nimport { createHash } from \"node:crypto\";\nimport { join } from \"node:path\";\nimport { configDir } from \"./config.js\";\n\nexport interface ManifestEntry {\n hash: string;\n size: number;\n lastSynced: string;\n storageClass: \"STANDARD\" | \"GLACIER_IR\";\n}\n\nexport interface LocalManifest {\n files: Record<string, ManifestEntry>;\n}\n\nexport interface RemoteManifest {\n version: 1;\n agentId: string;\n lastSync: string;\n files: Record<\n string,\n {\n hash: string;\n size: number;\n modified: string;\n etag?: string;\n storageClass?: string;\n compressed?: boolean;\n }\n >;\n}\n\nexport interface ManifestDiff {\n /** Files that exist locally but not on remote, or have changed locally */\n toPush: string[];\n /** Files that exist on remote but not locally, or are newer on remote */\n toPull: string[];\n /** Files that exist in both and have diverged (conflict) */\n conflicts: string[];\n /** Files that exist locally but were deleted on remote */\n remoteDeleted: string[];\n}\n\nconst MANIFEST_FILE = \"manifest.json\";\n\n/**\n * Resolve the manifest file path for a given workspace root.\n */\nfunction manifestPath(workspacePath: string): string {\n return join(configDir(workspacePath), MANIFEST_FILE);\n}\n\n/**\n * Read the local manifest. Returns empty manifest if not found.\n */\nexport async function readManifest(\n workspacePath: string,\n): Promise<LocalManifest> {\n const path = manifestPath(workspacePath);\n if (!existsSync(path)) {\n return { files: {} };\n }\n\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as LocalManifest;\n } catch {\n return { files: {} };\n }\n}\n\n/**\n * Write the local manifest.\n */\nexport async function writeManifest(\n workspacePath: string,\n manifest: LocalManifest,\n): Promise<void> {\n const dir = configDir(workspacePath);\n await mkdir(dir, { recursive: true });\n\n const path = manifestPath(workspacePath);\n await writeFile(path, JSON.stringify(manifest, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/**\n * Update a single file entry in the local manifest.\n */\nexport async function updateManifestEntry(\n workspacePath: string,\n relativePath: string,\n entry: ManifestEntry,\n): Promise<void> {\n const manifest = await readManifest(workspacePath);\n manifest.files[relativePath] = entry;\n await writeManifest(workspacePath, manifest);\n}\n\n/**\n * Remove a file entry from the local manifest.\n */\nexport async function removeManifestEntry(\n workspacePath: string,\n relativePath: string,\n): Promise<void> {\n const manifest = await readManifest(workspacePath);\n const remainingFiles = Object.fromEntries(\n Object.entries(manifest.files).filter(([key]) => key !== relativePath),\n );\n manifest.files = remainingFiles;\n await writeManifest(workspacePath, manifest);\n}\n\n/**\n * Compute SHA-256 hash of a file using streaming (memory-efficient).\n * Returns `sha256:<hex>` format.\n */\nexport async function computeFileHash(filePath: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const hash = createHash(\"sha256\");\n const stream = createReadStream(filePath);\n\n stream.on(\"data\", (chunk) => hash.update(chunk));\n stream.on(\"end\", () => { resolve(`sha256:${hash.digest(\"hex\")}`); });\n stream.on(\"error\", reject);\n });\n}\n\n/**\n * Diff the local manifest against the remote manifest.\n *\n * Returns lists of files to push, pull, and conflicts.\n */\nexport function diffManifests(\n local: LocalManifest,\n remote: RemoteManifest,\n): ManifestDiff {\n const toPush: string[] = [];\n const toPull: string[] = [];\n const conflicts: string[] = [];\n const remoteDeleted: string[] = [];\n\n const localPaths = new Set(Object.keys(local.files));\n const remotePaths = new Set(Object.keys(remote.files));\n\n // Files only in local → push\n for (const path of localPaths) {\n if (!remotePaths.has(path)) {\n toPush.push(path);\n }\n }\n\n // Files only in remote → pull\n for (const path of remotePaths) {\n if (!localPaths.has(path)) {\n toPull.push(path);\n }\n }\n\n // Files in both → compare hashes\n for (const path of localPaths) {\n if (!remotePaths.has(path)) continue;\n\n const localEntry = local.files[path];\n const remoteEntry = remote.files[path];\n\n if (localEntry.hash === remoteEntry.hash) {\n // In sync — nothing to do\n continue;\n }\n\n // Hashes differ — determine direction\n const localSyncTime = new Date(localEntry.lastSynced).getTime();\n const remoteModTime = new Date(remoteEntry.modified).getTime();\n\n if (remoteModTime > localSyncTime) {\n // Remote is newer than our last sync — could be a conflict\n // If local file was also modified (hash differs from what we synced),\n // it's a conflict\n conflicts.push(path);\n } else {\n // Local is newer — push\n toPush.push(path);\n }\n }\n\n // Files that were in our manifest but deleted from remote\n for (const path of localPaths) {\n if (\n local.files[path].lastSynced !== \"\" &&\n !remotePaths.has(path)\n ) {\n remoteDeleted.push(path);\n }\n }\n\n return { toPush, toPull, conflicts, remoteDeleted };\n}\n","/**\n * AlfeSync API client — typed HTTP client for the AlfeSync backend.\n */\n\nimport type { RemoteManifest } from \"./manifest.js\";\n\nexport interface ApiClientConfig {\n apiUrl: string;\n token: string;\n agentId: string;\n}\n\nexport interface PresignResult {\n path: string;\n url: string;\n expiresAt: string;\n}\n\nexport interface AgentStats {\n agentId: string;\n standardBytes: number;\n glacierBytes: number;\n fileCount: number;\n lastSyncAt: string | null;\n}\n\nexport interface RegisterAgentResult {\n agent: {\n agentId: string;\n orgId: string;\n displayName: string;\n status: string;\n createdAt: string;\n };\n}\n\nexport interface ConfirmResult {\n filePath: string;\n hash: string;\n size: number;\n storageClass: string;\n syncedAt: string;\n}\n\nexport interface ReconstructResult {\n agentId: string;\n mode: \"full\" | \"active\" | \"memory\";\n fileCount: number;\n totalSize: number;\n files: {\n path: string;\n url: string;\n size: number;\n compressed: boolean;\n expiresAt: string;\n }[];\n generatedAt: string;\n expiresAt: string;\n}\n\nexport interface FileHistoryEntry {\n versionId: string;\n isLatest: boolean;\n lastModified: string;\n size: number;\n etag?: string;\n}\n\n/**\n * Create an AlfeSync API client.\n */\nexport function createApiClient(config: ApiClientConfig) {\n const { apiUrl, token, agentId } = config;\n\n // Normalize URL: strip trailing slash\n const baseUrl = apiUrl.replace(/\\/$/, \"\");\n\n async function request<T>(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<T> {\n const url = `${baseUrl}${path}`;\n const headers: Record<string, string> = {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n };\n\n const response = await fetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (!response.ok) {\n const text = await response.text();\n let errorMsg: string;\n try {\n const parsed = JSON.parse(text) as Record<string, unknown>;\n errorMsg = (typeof parsed.error === 'string' ? parsed.error : undefined)\n ?? (typeof parsed.message === 'string' ? parsed.message : undefined)\n ?? text;\n } catch {\n errorMsg = text;\n }\n throw new Error(\n `AlfeSync API error (${String(response.status)}): ${errorMsg}`,\n );\n }\n\n const json = (await response.json()) as { success: boolean; data: T };\n if (!json.success) {\n throw new Error(\"AlfeSync API returned unsuccessful response\");\n }\n return json.data;\n }\n\n return {\n /**\n * Get the remote manifest for this agent.\n */\n async getManifest(): Promise<RemoteManifest> {\n return request<RemoteManifest>(\n \"GET\",\n `/sync/agents/${agentId}/manifest`,\n );\n },\n\n /**\n * Request a presigned PUT URL for uploading a file.\n */\n async presignPut(\n filePath: string,\n contentType = \"application/octet-stream\",\n ): Promise<PresignResult> {\n const result = await request<{ urls: PresignResult[] }>(\n \"POST\",\n `/sync/agents/${agentId}/presign`,\n {\n files: [{ path: filePath, operation: \"put\", contentType }],\n },\n );\n return result.urls[0];\n },\n\n /**\n * Request presigned PUT URLs for multiple files.\n */\n async presignPutBatch(\n files: { path: string; contentType?: string }[],\n ): Promise<PresignResult[]> {\n const result = await request<{ urls: PresignResult[] }>(\n \"POST\",\n `/sync/agents/${agentId}/presign`,\n {\n files: files.map((f) => ({\n path: f.path,\n operation: \"put\" as const,\n contentType: f.contentType ?? \"application/octet-stream\",\n })),\n },\n );\n return result.urls;\n },\n\n /**\n * Confirm a presigned upload completed — updates DynamoDB.\n */\n async confirmUpload(\n filePath: string,\n hash: string,\n size: number,\n storageClass: \"STANDARD\" | \"GLACIER_IR\" = \"STANDARD\",\n ): Promise<ConfirmResult> {\n return request<ConfirmResult>(\n \"POST\",\n `/sync/agents/${agentId}/files/${filePath}/confirm`,\n { hash, size, storageClass },\n );\n },\n\n /**\n * Request a presigned GET URL for downloading a file.\n */\n async presignGet(filePath: string): Promise<PresignResult> {\n const result = await request<{ urls: PresignResult[] }>(\n \"POST\",\n `/sync/agents/${agentId}/presign`,\n {\n files: [{ path: filePath, operation: \"get\" }],\n },\n );\n return result.urls[0];\n },\n\n /**\n * Request presigned GET URLs for multiple files.\n */\n async presignGetBatch(paths: string[]): Promise<PresignResult[]> {\n const result = await request<{ urls: PresignResult[] }>(\n \"POST\",\n `/sync/agents/${agentId}/presign`,\n {\n files: paths.map((p) => ({ path: p, operation: \"get\" as const })),\n },\n );\n return result.urls;\n },\n\n /**\n * Get agent storage stats.\n */\n async getStats(): Promise<AgentStats> {\n return request<AgentStats>(\n \"GET\",\n `/sync/agents/${agentId}/stats`,\n );\n },\n\n /**\n * Register this agent with the sync service.\n */\n async registerAgent(\n displayName?: string,\n ): Promise<RegisterAgentResult> {\n return request<RegisterAgentResult>(\n \"POST\",\n \"/sync/agents\",\n { agentId, displayName },\n );\n },\n\n /**\n * Get file version history.\n */\n async getFileHistory(filePath: string): Promise<FileHistoryEntry[]> {\n const result = await request<{\n filePath: string;\n versions: FileHistoryEntry[];\n }>(\"GET\", `/sync/agents/${agentId}/files/${filePath}/versions`);\n return result.versions;\n },\n\n /**\n * Generate a reconstruction bundle.\n */\n async reconstruct(\n mode: \"full\" | \"active\" | \"memory\" = \"full\",\n ): Promise<ReconstructResult> {\n return request<ReconstructResult>(\n \"POST\",\n `/sync/agents/${agentId}/reconstruct`,\n { mode },\n );\n },\n };\n}\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\n","/**\n * AlfeSync uploader — upload changed files to S3 via presigned URLs.\n *\n * Flow per file:\n * 1. Request presigned PUT URL from API\n * 2. PUT file content directly to S3\n * 3. Notify API of completion (confirm endpoint)\n * 4. Update local manifest\n *\n * Retries 3x with exponential backoff on transient failures.\n */\n\nimport { readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { ApiClient } from \"./api-client.js\";\n\nconst log = createLogger(\"SyncUploader\");\nimport { computeFileHash, updateManifestEntry } from \"./manifest.js\";\nimport type { ManifestEntry } from \"./manifest.js\";\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\n\nexport interface UploadResult {\n path: string;\n success: boolean;\n hash?: string;\n size?: number;\n error?: string;\n}\n\n/**\n * Determine the storage class based on file path.\n */\nfunction getStorageClass(\n relativePath: string,\n): \"STANDARD\" | \"GLACIER_IR\" {\n return relativePath.startsWith(\"sessions/archive/\") ||\n relativePath.startsWith(\"context/archive/\")\n ? \"GLACIER_IR\"\n : \"STANDARD\";\n}\n\n/**\n * Determine MIME type from file path.\n */\nfunction getContentType(relativePath: string): string {\n if (relativePath.endsWith(\".json\")) return \"application/json\";\n if (relativePath.endsWith(\".md\")) return \"text/markdown\";\n if (relativePath.endsWith(\".txt\")) return \"text/plain\";\n if (relativePath.endsWith(\".gz\")) return \"application/gzip\";\n if (relativePath.endsWith(\".yaml\") || relativePath.endsWith(\".yml\"))\n return \"text/yaml\";\n return \"application/octet-stream\";\n}\n\n/**\n * Upload a single file with retry logic.\n */\nasync function uploadFileWithRetry(\n workspacePath: string,\n relativePath: string,\n client: ApiClient,\n): Promise<UploadResult> {\n const absolutePath = join(workspacePath, relativePath);\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n // Compute hash and get file size\n const [hash, fileStat] = await Promise.all([\n computeFileHash(absolutePath),\n stat(absolutePath),\n ]);\n const size = fileStat.size;\n const storageClass = getStorageClass(relativePath);\n const contentType = getContentType(relativePath);\n\n // 1. Get presigned PUT URL\n const presigned = await client.presignPut(relativePath, contentType);\n\n // 2. PUT to S3 directly\n const fileContent = await readFile(absolutePath);\n const putResponse = await fetch(presigned.url, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": contentType,\n },\n body: fileContent,\n });\n\n if (!putResponse.ok) {\n throw new Error(\n `S3 PUT failed (${String(putResponse.status)}): ${await putResponse.text()}`,\n );\n }\n\n // 3. Confirm upload with API\n await client.confirmUpload(relativePath, hash, size, storageClass);\n\n // 4. Update local manifest\n const entry: ManifestEntry = {\n hash,\n size,\n lastSynced: new Date().toISOString(),\n storageClass,\n };\n await updateManifestEntry(workspacePath, relativePath, entry);\n\n return { path: relativePath, success: true, hash, size };\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n\n if (attempt < MAX_RETRIES) {\n // Exponential backoff: 1s, 2s, 4s\n const delay = BASE_DELAY_MS * Math.pow(2, attempt);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n return {\n path: relativePath,\n success: false,\n error: lastError?.message ?? \"Unknown error\",\n };\n}\n\n/**\n * Upload multiple files to S3.\n *\n * Uploads are performed in parallel with a concurrency limit.\n */\nexport async function uploadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: ApiClient,\n options: { concurrency?: number; quiet?: boolean } = {},\n): Promise<UploadResult[]> {\n const { concurrency = 5, quiet = false } = options;\n const results: UploadResult[] = [];\n\n // Process in batches for controlled concurrency\n for (let i = 0; i < relativePaths.length; i += concurrency) {\n const batch = relativePaths.slice(i, i + concurrency);\n const batchResults = await Promise.all(\n batch.map((path) => uploadFileWithRetry(workspacePath, path, client)),\n );\n\n for (const result of batchResults) {\n results.push(result);\n if (!quiet) {\n if (result.success) {\n log.info(`Uploaded ${result.path} (${formatBytes(result.size ?? 0)})`);\n } else {\n log.error(`Failed to upload ${result.path}: ${result.error ?? \"Unknown error\"}`);\n }\n }\n }\n }\n\n return results;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${String(bytes)} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n","/**\n * AlfeSync downloader — download files from S3 via presigned URLs.\n *\n * Flow per file:\n * 1. Request presigned GET URL from API\n * 2. GET file content from S3\n * 3. Write to local disk\n * 4. Update local manifest\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { ApiClient } from \"./api-client.js\";\n\nconst log = createLogger(\"SyncDownloader\");\nimport { updateManifestEntry } from \"./manifest.js\";\nimport type { ManifestEntry, RemoteManifest } from \"./manifest.js\";\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\n\nexport interface DownloadResult {\n path: string;\n success: boolean;\n size?: number;\n error?: string;\n}\n\n/**\n * Download a single file with retry logic.\n */\nasync function downloadFileWithRetry(\n workspacePath: string,\n relativePath: string,\n client: ApiClient,\n remoteEntry?: RemoteManifest[\"files\"][string],\n): Promise<DownloadResult> {\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n // 1. Get presigned GET URL\n const presigned = await client.presignGet(relativePath);\n\n // 2. GET from S3\n const response = await fetch(presigned.url);\n if (!response.ok) {\n throw new Error(\n `S3 GET failed (${String(response.status)}): ${await response.text()}`,\n );\n }\n\n const buffer = Buffer.from(await response.arrayBuffer());\n\n // 3. Write to disk\n const absolutePath = join(workspacePath, relativePath);\n await mkdir(dirname(absolutePath), { recursive: true });\n await writeFile(absolutePath, buffer);\n\n // 4. Update local manifest\n const entry: ManifestEntry = {\n hash: remoteEntry?.hash ?? \"\",\n size: buffer.length,\n lastSynced: new Date().toISOString(),\n storageClass:\n (remoteEntry?.storageClass ?? \"STANDARD\") as \"STANDARD\" | \"GLACIER_IR\",\n };\n await updateManifestEntry(workspacePath, relativePath, entry);\n\n return { path: relativePath, success: true, size: buffer.length };\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n\n if (attempt < MAX_RETRIES) {\n const delay = BASE_DELAY_MS * Math.pow(2, attempt);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n return {\n path: relativePath,\n success: false,\n error: lastError?.message ?? \"Unknown error\",\n };\n}\n\n/**\n * Download multiple files from S3.\n *\n * Downloads are performed in parallel with a concurrency limit.\n */\nexport async function downloadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: ApiClient,\n remoteManifest?: RemoteManifest,\n options: { concurrency?: number; quiet?: boolean } = {},\n): Promise<DownloadResult[]> {\n const { concurrency = 5, quiet = false } = options;\n const results: DownloadResult[] = [];\n\n for (let i = 0; i < relativePaths.length; i += concurrency) {\n const batch = relativePaths.slice(i, i + concurrency);\n const batchResults = await Promise.all(\n batch.map((path) =>\n downloadFileWithRetry(\n workspacePath,\n path,\n client,\n remoteManifest?.files[path],\n ),\n ),\n );\n\n for (const result of batchResults) {\n results.push(result);\n if (!quiet) {\n if (result.success) {\n log.info(`Downloaded ${result.path} (${formatBytes(result.size ?? 0)})`);\n } else {\n log.error(`Failed to download ${result.path}: ${result.error ?? \"Unknown error\"}`);\n }\n }\n }\n }\n\n return results;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${String(bytes)} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n","/**\n * AlfeSync engine — orchestrates push, pull, and full sync operations.\n *\n * Handles:\n * - push(paths[]): upload changed files to S3\n * - pull(): download files newer on remote\n * - fullSync(): bidirectional sync with conflict detection\n *\n * Conflict resolution: if remote file is newer than local manifest entry\n * AND local file has changed → write `.conflict-{timestamp}` alongside original.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join, dirname, basename, extname } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\n\nconst log = createLogger(\"SyncEngine\");\nimport { createApiClient } from \"./api-client.js\";\nimport { requireConfig } from \"./config.js\";\nimport {\n readManifest,\n computeFileHash,\n diffManifests,\n removeManifestEntry,\n} from \"./manifest.js\";\nimport { loadIgnorePatterns, filterIgnored } from \"./ignore.js\";\nimport { uploadFiles } from \"./uploader.js\";\nimport { downloadFiles } from \"./downloader.js\";\n\nexport interface SyncResult {\n pushed: number;\n pulled: number;\n conflicts: number;\n errors: number;\n}\n\nexport interface SyncEngineOptions {\n workspacePath: string;\n quiet?: boolean;\n filter?: string;\n}\n\n/**\n * Create a sync engine for a workspace.\n */\nexport async function createSyncEngine(workspacePath: string) {\n const config = await requireConfig(workspacePath);\n const client = createApiClient({\n apiUrl: config.apiUrl,\n token: config.token,\n agentId: config.agentId,\n });\n\n return {\n config,\n client,\n\n /**\n * Push changed files to remote.\n *\n * If paths are provided, only push those files.\n * If no paths, detect all changed files and push them.\n */\n async push(\n paths?: string[],\n options: { quiet?: boolean; filter?: string } = {},\n ): Promise<SyncResult> {\n const { quiet = false, filter } = options;\n const ignorePatterns = await loadIgnorePatterns(workspacePath);\n\n let filesToPush: string[];\n\n if (paths && paths.length > 0) {\n filesToPush = filterIgnored(paths, ignorePatterns);\n } else {\n // Detect changed files by comparing local files to manifest\n filesToPush = await detectLocalChanges(\n workspacePath,\n ignorePatterns,\n );\n }\n\n // Apply filter if provided\n if (filter) {\n filesToPush = filesToPush.filter((p) => p.startsWith(filter));\n }\n\n if (filesToPush.length === 0) {\n if (!quiet) log.info(\"Nothing to push\");\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n\n if (!quiet) {\n log.info(`Pushing ${String(filesToPush.length)} file(s)`);\n }\n\n const results = await uploadFiles(\n workspacePath,\n filesToPush,\n client,\n { quiet },\n );\n\n const pushed = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Push complete: ${String(pushed)} uploaded, ${String(errors)} failed`);\n }\n\n return { pushed, pulled: 0, conflicts: 0, errors };\n },\n\n /**\n * Pull files from remote that are newer than local.\n */\n async pull(\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.getManifest(),\n ]);\n\n const diff = diffManifests(localManifest, remoteManifest);\n const filesToPull = [...diff.toPull];\n\n if (filesToPull.length === 0) {\n if (!quiet) log.info(\"Nothing to pull\");\n return {\n pushed: 0,\n pulled: 0,\n conflicts: diff.conflicts.length,\n errors: 0,\n };\n }\n\n if (!quiet) {\n log.info(`Pulling ${String(filesToPull.length)} file(s)`);\n }\n\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\n );\n\n const pulled = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);\n }\n\n return {\n pushed: 0,\n pulled,\n conflicts: diff.conflicts.length,\n errors,\n };\n },\n\n /**\n * Full bidirectional sync with conflict detection.\n *\n * For conflicts: if remote file is newer than local manifest entry AND\n * local file has changed → write `.conflict-{timestamp}` file alongside\n * the original, then pull the remote version.\n */\n async fullSync(\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.getManifest(),\n ]);\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath);\n const localChanges = await detectLocalChanges(\n workspacePath,\n ignorePatterns,\n );\n\n const diff = diffManifests(localManifest, remoteManifest);\n\n // Classify conflicts: only files that changed both locally AND remotely\n // are true conflicts. Files in diff.conflicts where only the remote\n // changed (local untouched since last sync) are clean pulls.\n const trueConflicts = diff.conflicts.filter((p) =>\n localChanges.includes(p),\n );\n const remoteOnlyChanges = diff.conflicts.filter(\n (p) => !localChanges.includes(p),\n );\n\n // Handle true conflicts: save local version as .conflict-{timestamp}\n let conflictCount = 0;\n for (const conflictPath of trueConflicts) {\n const absolutePath = join(workspacePath, conflictPath);\n if (existsSync(absolutePath)) {\n const ext = extname(conflictPath);\n const base = basename(conflictPath, ext);\n const dir = dirname(conflictPath);\n const timestamp = new Date()\n .toISOString()\n .replace(/[:.]/g, \"-\");\n const conflictName = `${base}.conflict-${timestamp}${ext}`;\n const conflictAbsolute = join(\n workspacePath,\n dir,\n conflictName,\n );\n\n const content = await readFile(absolutePath);\n await writeFile(conflictAbsolute, content);\n\n if (!quiet) {\n log.warn(`Conflict: ${conflictPath} — saved as ${conflictName}`);\n }\n conflictCount++;\n }\n }\n\n // Push local changes (excluding true conflicts — remote wins, local saved)\n const filesToPush = filterIgnored(\n [...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))],\n ignorePatterns,\n );\n\n // Pull remote changes + remote-only changes + true conflicts (remote wins)\n const filesToPull = [\n ...diff.toPull,\n ...remoteOnlyChanges,\n ...trueConflicts,\n ];\n\n const pushResult = { pushed: 0, errors: 0 };\n const pullResult = { pulled: 0, errors: 0 };\n\n if (filesToPush.length > 0) {\n if (!quiet) {\n log.info(`Pushing ${String(filesToPush.length)} file(s)`);\n }\n const results = await uploadFiles(\n workspacePath,\n [...new Set(filesToPush)],\n client,\n { quiet },\n );\n pushResult.pushed = results.filter((r) => r.success).length;\n pushResult.errors = results.filter((r) => !r.success).length;\n }\n\n if (filesToPull.length > 0) {\n if (!quiet) {\n log.info(`Pulling ${String(filesToPull.length)} file(s)`);\n }\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\n );\n pullResult.pulled = results.filter((r) => r.success).length;\n pullResult.errors = results.filter((r) => !r.success).length;\n }\n\n const totalErrors = pushResult.errors + pullResult.errors;\n\n if (!quiet) {\n log.info(`Sync complete: ${String(pushResult.pushed)} pushed, ${String(pullResult.pulled)} pulled, ${String(conflictCount)} conflicts, ${String(totalErrors)} errors`);\n }\n\n return {\n pushed: pushResult.pushed,\n pulled: pullResult.pulled,\n conflicts: conflictCount,\n errors: totalErrors,\n };\n },\n\n /**\n * Pull specific files by path without a full manifest diff.\n *\n * Used for notification-driven pulls where we already know which\n * files changed on the remote.\n */\n async pullFiles(\n paths: string[],\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n\n if (paths.length === 0) {\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n\n // Fetch remote manifest to get hashes for manifest update\n const remoteManifest = await client.getManifest();\n\n // Filter to only files that differ from our local manifest\n const localManifest = await readManifest(workspacePath);\n const filesToPull = paths.filter((p) => {\n if (!(p in remoteManifest.files)) return false; // File doesn't exist on remote\n const remoteEntry = remoteManifest.files[p];\n if (!(p in localManifest.files)) return true; // New file\n const localEntry = localManifest.files[p];\n return localEntry.hash !== remoteEntry.hash;\n });\n\n if (filesToPull.length === 0) {\n if (!quiet) log.info(\"All notified files already in sync\");\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n\n if (!quiet) {\n log.info(`Pulling ${String(filesToPull.length)} changed file(s)`);\n }\n\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\n );\n\n const pulled = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);\n }\n\n return { pushed: 0, pulled, conflicts: 0, errors };\n },\n\n /**\n * Remove a file locally and from the manifest.\n *\n * Used for notification-driven deletes when a remote file is removed.\n */\n async removeLocalFile(\n filePath: string,\n options: { quiet?: boolean } = {},\n ): Promise<void> {\n const { quiet = false } = options;\n const absolutePath = join(workspacePath, filePath);\n\n try {\n const { unlink } = await import(\"node:fs/promises\");\n if (existsSync(absolutePath)) {\n await unlink(absolutePath);\n if (!quiet) log.info(`Deleted: ${filePath}`);\n }\n } catch (err) {\n if (!quiet) log.error({ err }, `Failed to delete ${filePath}`);\n }\n\n await removeManifestEntry(workspacePath, filePath);\n },\n };\n}\n\n/**\n * Detect files that have changed locally since last sync.\n *\n * Compares current file hashes against the local manifest.\n * Returns list of relative paths that need pushing.\n */\nasync function detectLocalChanges(\n workspacePath: string,\n ignorePatterns: string[],\n): Promise<string[]> {\n const manifest = await readManifest(workspacePath);\n const changed: string[] = [];\n\n // Walk the workspace directory\n const { readdir } = await import(\"node:fs/promises\");\n\n async function walk(dir: string): Promise<void> {\n const entries = await readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n const relativePath = fullPath\n .slice(workspacePath.length + 1)\n .replace(/\\\\/g, \"/\");\n\n if (shouldSkipDir(entry.name)) continue;\n\n if (entry.isDirectory()) {\n // Check if directory is ignored before recursing\n const isIgnored = ignorePatterns.some((p) => {\n if (p.endsWith(\"/**\")) {\n return relativePath.startsWith(p.slice(0, -3));\n }\n return false;\n });\n if (!isIgnored) {\n await walk(fullPath);\n }\n } else if (entry.isFile()) {\n // Check if file is ignored\n const { shouldIgnore } = await import(\"./ignore.js\");\n if (shouldIgnore(relativePath, ignorePatterns)) continue;\n\n if (!(relativePath in manifest.files)) {\n // New file — needs push\n changed.push(relativePath);\n } else {\n const manifestEntry = manifest.files[relativePath];\n // Existing file — check if hash changed\n try {\n const currentHash = await computeFileHash(fullPath);\n if (currentHash !== manifestEntry.hash) {\n changed.push(relativePath);\n }\n } catch {\n // File might have been deleted between listing and hashing\n }\n }\n }\n }\n }\n\n await walk(workspacePath);\n return changed;\n}\n\n/**\n * Directories to always skip during walks.\n */\nfunction shouldSkipDir(name: string): boolean {\n return (\n name === \"node_modules\" ||\n name === \".git\" ||\n name === \".sst\" ||\n name === \".alfesync\" ||\n name === \".build\" ||\n name === \"dist\"\n );\n}\n\nexport type SyncEngine = Awaited<ReturnType<typeof createSyncEngine>>;\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@alfe.ai/openclaw-sync",
3
+ "version": "0.0.1",
4
+ "description": "AlfeSync — agent workspace backup and sync skill for OpenClaw",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "alfesync": "dist/cli/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "dependencies": {
18
+ "@auriclabs/logger": "^0.1.1",
19
+ "chokidar": "^4.0.0",
20
+ "commander": "^13.0.0",
21
+ "micromatch": "^4.0.8",
22
+ "ws": "^8.18.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/micromatch": "^4.0.9",
26
+ "@types/ws": "^8.18.1"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "license": "UNLICENSED",
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "dev": "tsdown --watch",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "test:coverage": "vitest run --coverage",
39
+ "lint": "eslint ."
40
+ }
41
+ }