@alfe.ai/openclaw-sync 0.0.20 → 0.1.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.
- package/dist/cli/index.cjs +36 -0
- package/dist/cli/index.js +37 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.cts +4 -14
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +4 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin2.cjs +83 -18
- package/dist/plugin2.js +84 -19
- package/dist/plugin2.js.map +1 -1
- package/dist/sync-engine.cjs +75 -28
- package/dist/sync-engine.js +75 -28
- package/dist/sync-engine.js.map +1 -1
- package/package.json +1 -1
package/dist/sync-engine.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-engine.js","names":["log","formatBytes","log"],"sources":["../src/manifest.ts","../src/ignore.ts","../src/retry.ts","../src/uploader.ts","../src/downloader.ts","../src/sync-engine.ts"],"sourcesContent":["/**\n * AlfeSync manifest — local file manifest at `~/.alfe/sync/manifest.json`.\n *\n * Tracks file hashes, sizes, sync timestamps, and storage classes\n * to enable efficient diff-based syncing. Lives under `~/.alfe/sync/`\n * so workspace directories stay clean.\n *\n * `workspacePath` is accepted by every function for consistency with the\n * rest of the package, but the manifest itself is workspace-independent\n * (one agent, one workspace, one manifest).\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 { homedir } from \"node:os\";\n\n/**\n * Local state directory for sync — manifest, etc. Lives under `~/.alfe/sync/`\n * so it stays alongside the rest of Alfe's agent state instead of polluting\n * the workspace.\n */\nconst SYNC_STATE_DIR = join(homedir(), \".alfe\", \"sync\");\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. Lives under `~/.alfe/sync/`, independent\n * of the workspace path — one agent has one manifest.\n */\nfunction manifestPath(): string {\n return join(SYNC_STATE_DIR, MANIFEST_FILE);\n}\n\n/**\n * Read the local manifest. Returns empty manifest if not found.\n *\n * `workspacePath` is accepted for call-site symmetry with the rest of the\n * package but is not used — the manifest path is resolved from\n * `~/.alfe/sync/` regardless of which workspace the call comes from.\n */\nexport async function readManifest(\n workspacePath: string,\n): Promise<LocalManifest> {\n void workspacePath;\n const path = manifestPath();\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. `workspacePath` is accepted for call-site\n * symmetry but unused (see `readManifest`).\n */\nexport async function writeManifest(\n workspacePath: string,\n manifest: LocalManifest,\n): Promise<void> {\n void workspacePath;\n const path = manifestPath();\n await mkdir(SYNC_STATE_DIR, { recursive: true });\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 ignore — parse `.alfesyncignore` (gitignore-style glob matching).\n *\n * Uses micromatch for glob pattern matching, compatible with .gitignore syntax.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport micromatch from \"micromatch\";\n\n/** Default ignore patterns — always applied */\nconst DEFAULT_IGNORES = [\n \".alfesync/**\",\n \"node_modules/**\",\n \"*.tmp\",\n \".DS_Store\",\n \".git/**\",\n \".sst/**\",\n \".build/**\",\n \"dist/**\",\n];\n\n/**\n * Load ignore patterns from `.alfesyncignore` file + defaults.\n */\nexport async function loadIgnorePatterns(\n workspacePath: string,\n): Promise<string[]> {\n const ignoreFile = join(workspacePath, \".alfesyncignore\");\n const patterns = [...DEFAULT_IGNORES];\n\n if (existsSync(ignoreFile)) {\n try {\n const content = await readFile(ignoreFile, \"utf-8\");\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n // Skip empty lines and comments\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n patterns.push(trimmed);\n }\n } catch {\n // Ignore read errors — use defaults only\n }\n }\n\n return patterns;\n}\n\n/**\n * Check if a relative path should be ignored.\n */\nexport function shouldIgnore(\n relativePath: string,\n patterns: string[],\n): boolean {\n return micromatch.isMatch(relativePath, patterns, {\n dot: true,\n matchBase: true,\n });\n}\n\n/**\n * Filter a list of relative paths, removing ignored ones.\n */\nexport function filterIgnored(\n paths: string[],\n patterns: string[],\n): string[] {\n return paths.filter((p) => !shouldIgnore(p, patterns));\n}\n","/**\n * Tiny retry helper used by uploader/downloader. Extracted so both\n * use the same timing and surface the same final error shape.\n */\n\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_BASE_DELAY_MS = 1000;\n\nexport interface RetryOptions {\n maxRetries?: number;\n baseDelayMs?: number;\n}\n\n/**\n * Run `fn` up to `maxRetries + 1` times with exponential backoff\n * (`base * 2^attempt`). Returns the first successful value, or rethrows\n * the last error.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n if (attempt < maxRetries) {\n const delay = baseDelayMs * Math.pow(2, attempt);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(String(lastError));\n}\n","/**\n * AlfeSync uploader — push changed files to S3.\n *\n * Per file:\n * 1. Ask the agent API for a presigned PUT URL\n * 2. PUT bytes directly to S3 (raw fetch — S3 isn't an Alfe API)\n * 3. Tell the agent API the upload completed (`syncConfirmUpload`)\n * 4. Update the local manifest\n *\n * Each step retries with exponential backoff via `withRetry`.\n */\n\nimport { readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nimport { computeFileHash, updateManifestEntry } from \"./manifest.js\";\nimport type { ManifestEntry } from \"./manifest.js\";\nimport { withRetry } from \"./retry.js\";\n\nconst log = createLogger(\"SyncUploader\");\n\nexport interface UploadResult {\n path: string;\n success: boolean;\n hash?: string;\n size?: number;\n error?: string;\n}\n\nconst ARCHIVE_PREFIXES = [\"sessions/archive/\", \"context/archive/\"] as const;\n\nfunction getStorageClass(relativePath: string): \"STANDARD\" | \"GLACIER_IR\" {\n return ARCHIVE_PREFIXES.some((p) => relativePath.startsWith(p))\n ? \"GLACIER_IR\"\n : \"STANDARD\";\n}\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\")) return \"text/yaml\";\n return \"application/octet-stream\";\n}\n\nasync function uploadOne(\n workspacePath: string,\n relativePath: string,\n client: AgentApiClient,\n): Promise<UploadResult> {\n const absolutePath = join(workspacePath, relativePath);\n try {\n return await withRetry(async () => {\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. Presigned PUT URL\n const presigned = await client.syncPresign({\n files: [{ path: relativePath, operation: \"put\", contentType }],\n });\n const url = presigned.urls[0]?.url;\n if (!url) throw new Error(\"No presigned URL returned\");\n\n // 2. Upload bytes directly to S3 (not an Alfe API)\n const fileContent = await readFile(absolutePath);\n const putResponse = await fetch(url, {\n method: \"PUT\",\n headers: { \"Content-Type\": contentType },\n body: fileContent,\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 via agent API\n await client.syncConfirmUpload({\n filePath: relativePath,\n hash,\n size,\n storageClass,\n });\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 });\n } catch (err) {\n return {\n path: relativePath,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Upload many files, batched by `concurrency`.\n */\nexport async function uploadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: AgentApiClient,\n options: { concurrency?: number; quiet?: boolean } = {},\n): Promise<UploadResult[]> {\n const { concurrency = 5, quiet = false } = options;\n const results: UploadResult[] = [];\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) => uploadOne(workspacePath, path, client)),\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 — pull files from S3 to disk.\n *\n * Per file:\n * 1. Ask the agent API for a presigned GET URL\n * 2. GET bytes directly from S3 (raw fetch — S3 isn't an Alfe API)\n * 3. Write to disk\n * 4. Update the local manifest\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nimport { updateManifestEntry } from \"./manifest.js\";\nimport type { ManifestEntry, RemoteManifest } from \"./manifest.js\";\nimport { withRetry } from \"./retry.js\";\n\nconst log = createLogger(\"SyncDownloader\");\n\nexport interface DownloadResult {\n path: string;\n success: boolean;\n size?: number;\n error?: string;\n}\n\nasync function downloadOne(\n workspacePath: string,\n relativePath: string,\n client: AgentApiClient,\n remoteEntry?: RemoteManifest[\"files\"][string],\n): Promise<DownloadResult> {\n try {\n return await withRetry(async () => {\n const presigned = await client.syncPresign({\n files: [{ path: relativePath, operation: \"get\" }],\n });\n const url = presigned.urls[0]?.url;\n if (!url) throw new Error(\"No presigned URL returned\");\n\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(\n `S3 GET failed (${String(response.status)}): ${await response.text()}`,\n );\n }\n const buffer = Buffer.from(await response.arrayBuffer());\n\n const absolutePath = join(workspacePath, relativePath);\n await mkdir(dirname(absolutePath), { recursive: true });\n await writeFile(absolutePath, buffer);\n\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 });\n } catch (err) {\n return {\n path: relativePath,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\nexport async function downloadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: AgentApiClient,\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 downloadOne(workspacePath, path, client, remoteManifest?.files[path]),\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.\n *\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: when a remote file is newer than the local manifest\n * entry AND the local file has also changed, the local copy is renamed to\n * `<name>.conflict-<timestamp>.<ext>` and the remote version wins.\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\";\nimport type { AgentApiClient, SyncManifest } from \"@alfe.ai/agent-api-client\";\n\nimport {\n readManifest,\n computeFileHash,\n diffManifests,\n removeManifestEntry,\n} from \"./manifest.js\";\nimport { loadIgnorePatterns, filterIgnored, shouldIgnore } from \"./ignore.js\";\nimport { uploadFiles } from \"./uploader.js\";\nimport { downloadFiles } from \"./downloader.js\";\n\nconst log = createLogger(\"SyncEngine\");\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 client: AgentApiClient;\n}\n\n/**\n * Construct a sync engine bound to a workspace + agent API client.\n *\n * The client is constructed once in plugin.ts (or the CLI) and passed in,\n * so credentials never leak into multiple places.\n */\nexport function createSyncEngine({ workspacePath, client }: SyncEngineOptions) {\n return {\n workspacePath,\n client,\n\n /**\n * Upload changed files. Pass `paths` for an explicit list, or omit\n * to scan the workspace for anything that drifted from the manifest.\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 paths && paths.length > 0\n ? filterIgnored(paths, ignorePatterns)\n : await detectLocalChanges(workspacePath, ignorePatterns);\n\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) log.info(`Pushing ${String(filesToPush.length)} file(s)`);\n\n const results = await uploadFiles(workspacePath, filesToPush, client, { quiet });\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 /** Download files newer on remote. */\n async pull(options: { quiet?: boolean } = {}): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.syncGetManifest(),\n ]);\n\n const diff = diffManifests(localManifest, remoteManifest);\n if (diff.toPull.length === 0) {\n if (!quiet) log.info(\"Nothing to pull\");\n return { pushed: 0, pulled: 0, conflicts: diff.conflicts.length, errors: 0 };\n }\n\n if (!quiet) log.info(`Pulling ${String(diff.toPull.length)} file(s)`);\n\n const results = await downloadFiles(\n workspacePath,\n [...diff.toPull],\n client,\n remoteManifest,\n { quiet },\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: diff.conflicts.length, errors };\n },\n\n /**\n * Bidirectional sync. True conflicts (changed on both sides) are saved\n * locally as `.conflict-<ts>` files and the remote version wins.\n */\n async fullSync(options: { quiet?: boolean } = {}): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.syncGetManifest(),\n ]);\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath);\n const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);\n\n const diff = diffManifests(localManifest, remoteManifest);\n const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));\n const remoteOnlyChanges = diff.conflicts.filter((p) => !localChanges.includes(p));\n\n let conflictCount = 0;\n for (const conflictPath of trueConflicts) {\n const absolutePath = join(workspacePath, conflictPath);\n if (!existsSync(absolutePath)) continue;\n const ext = extname(conflictPath);\n const base = basename(conflictPath, ext);\n const dir = dirname(conflictPath);\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const conflictName = `${base}.conflict-${timestamp}${ext}`;\n const conflictAbsolute = join(workspacePath, dir, conflictName);\n\n const content = await readFile(absolutePath);\n await writeFile(conflictAbsolute, content);\n\n if (!quiet) log.warn(`Conflict: ${conflictPath} — saved as ${conflictName}`);\n conflictCount++;\n }\n\n const filesToPush = filterIgnored(\n [...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))],\n ignorePatterns,\n );\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) log.info(`Pushing ${String(filesToPush.length)} file(s)`);\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) log.info(`Pulling ${String(filesToPull.length)} file(s)`);\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 if (!quiet) {\n log.info(\n `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 a known list of files (used for relay-driven notifications,\n * where we already know which paths changed remotely).\n */\n async pullFiles(\n paths: string[],\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n if (paths.length === 0) return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n const remoteManifest: SyncManifest = await client.syncGetManifest();\n const localManifest = await readManifest(workspacePath);\n\n const filesToPull = paths.filter((p) => {\n if (!(p in remoteManifest.files)) return false;\n if (!(p in localManifest.files)) return true;\n return localManifest.files[p].hash !== remoteManifest.files[p].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) log.info(`Pulling ${String(filesToPull.length)} changed file(s)`);\n\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\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 return { pushed: 0, pulled, conflicts: 0, errors };\n },\n\n /**\n * Delete a file locally and from the manifest. Used when the sync relay\n * tells us a file was removed remotely.\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\nexport type SyncEngine = ReturnType<typeof createSyncEngine>;\n\n/**\n * Walk the workspace and return paths whose hash differs from the manifest\n * (or that are missing from it entirely).\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 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 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 const isIgnored = ignorePatterns.some(\n (p) => p.endsWith(\"/**\") && relativePath.startsWith(p.slice(0, -3)),\n );\n if (!isIgnored) await walk(fullPath);\n } else if (entry.isFile()) {\n if (shouldIgnore(relativePath, ignorePatterns)) continue;\n if (!(relativePath in manifest.files)) {\n changed.push(relativePath);\n continue;\n }\n try {\n const currentHash = await computeFileHash(fullPath);\n if (currentHash !== manifest.files[relativePath].hash) {\n changed.push(relativePath);\n }\n } catch {\n // file may have been deleted between listing and hashing\n }\n }\n }\n }\n\n await walk(workspacePath);\n return changed;\n}\n\n/** Directories the engine never descends into. */\nfunction shouldSkipDir(name: string): boolean {\n return (\n name === \"node_modules\" ||\n name === \".git\" ||\n name === \".sst\" ||\n name === \".build\" ||\n name === \"dist\"\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sync-engine.js","names":["log","formatBytes","log"],"sources":["../src/manifest.ts","../src/ignore.ts","../src/retry.ts","../src/uploader.ts","../src/downloader.ts","../src/sync-engine.ts"],"sourcesContent":["/**\n * AlfeSync manifest — local file manifest at `~/.alfe/sync/manifest.json`.\n *\n * Tracks file hashes, sizes, sync timestamps, and storage classes\n * to enable efficient diff-based syncing. Lives under `~/.alfe/sync/`\n * so workspace directories stay clean.\n *\n * `workspacePath` is accepted by every function for consistency with the\n * rest of the package, but the manifest itself is workspace-independent\n * (one agent, one workspace, one manifest).\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 { homedir } from \"node:os\";\n\n/**\n * Local state directory for sync — manifest, etc. Lives under `~/.alfe/sync/`\n * so it stays alongside the rest of Alfe's agent state instead of polluting\n * the workspace.\n */\nconst SYNC_STATE_DIR = join(homedir(), \".alfe\", \"sync\");\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. Lives under `~/.alfe/sync/`, independent\n * of the workspace path — one agent has one manifest.\n */\nfunction manifestPath(): string {\n return join(SYNC_STATE_DIR, MANIFEST_FILE);\n}\n\n/**\n * Read the local manifest. Returns empty manifest if not found.\n *\n * `workspacePath` is accepted for call-site symmetry with the rest of the\n * package but is not used — the manifest path is resolved from\n * `~/.alfe/sync/` regardless of which workspace the call comes from.\n */\nexport async function readManifest(\n workspacePath: string,\n): Promise<LocalManifest> {\n void workspacePath;\n const path = manifestPath();\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. `workspacePath` is accepted for call-site\n * symmetry but unused (see `readManifest`).\n */\nexport async function writeManifest(\n workspacePath: string,\n manifest: LocalManifest,\n): Promise<void> {\n void workspacePath;\n const path = manifestPath();\n await mkdir(SYNC_STATE_DIR, { recursive: true });\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","// AlfeSync ignore — parse .alfesyncignore (gitignore-style glob matching).\n// Uses micromatch for pattern matching, compatible with .gitignore syntax.\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport micromatch from \"micromatch\";\n\n// Default ignore patterns — always applied. Exported so tests and the docs\n// generator can introspect the list without duplicating it.\n//\n// Patterns are full micromatch globs. To match a directory or file at any\n// depth use a globstar prefix (two asterisks plus slash). The `matchBase`\n// option is NOT used because it breaks deep globstar/X/globstar matches in\n// micromatch 4.x.\n//\n// Categories:\n// - VCS / build / package metadata: .git, .sst, .build, dist, node_modules\n// - OS / editor noise: .DS_Store, *.tmp\n// - Sync-internal: .alfesync (legacy directory; current runtime uses\n// ~/.alfe/sync/, this entry is a backstop)\n// - Secrets (security — never sync): .env, .env.*\n// - OpenClaw config-rotation backups (high churn, no restore value):\n// openclaw.json.bak*, .clobbered.*, .preserve.*\n// - Chromium / Electron browser caches (the headless-browser plugin\n// dumps tens of MB of HTTP / GPU / shader cache that has zero restore\n// value — 125 MB on QA Tester before this broaden): Cache, Code Cache,\n// GPUCache, CacheStorage, Service Worker/CacheStorage, blob_storage,\n// Crashpad, ShaderCache, component_crx_cache\n// - Language churn: __pycache__, *.pyc, *.log\nexport const DEFAULT_IGNORES = [\n \"**/.alfesync/**\",\n \"**/node_modules/**\",\n \"**/*.tmp\",\n \"**/.DS_Store\",\n \"**/.git/**\",\n \"**/.sst/**\",\n \"**/.build/**\",\n \"**/dist/**\",\n \"**/.env\",\n \"**/.env.*\",\n \"**/openclaw.json.bak*\",\n \"**/openclaw.json.clobbered.*\",\n \"**/openclaw.json.preserve.*\",\n \"**/Cache/**\",\n \"**/Code Cache/**\",\n \"**/GPUCache/**\",\n \"**/CacheStorage/**\",\n \"**/Service Worker/CacheStorage/**\",\n \"**/blob_storage/**\",\n \"**/Crashpad/**\",\n \"**/ShaderCache/**\",\n \"**/component_crx_cache/**\",\n \"**/__pycache__/**\",\n \"**/*.pyc\",\n \"**/*.log\",\n];\n\n// Normalise a user-supplied gitignore-style pattern into a micromatch glob.\n// Mapping (input -> output):\n// foo -> **\\/foo (no slash: match anywhere)\n// /foo -> foo (leading slash: root-anchored)\n// foo/ -> **\\/foo/** (trailing slash: dir + contents anywhere)\n// path/to/foo -> path/to/foo (internal slash: leave anchored)\n// !pattern -> !<recurse on body> (negation; applied to body)\n// This keeps the historical .alfesyncignore UX (gitignore semantics) while\n// the engine matches on full globs without micromatch's matchBase option.\nexport function normaliseIgnorePattern(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return trimmed;\n if (trimmed.startsWith(\"!\")) return `!${normaliseIgnorePattern(trimmed.slice(1))}`;\n if (trimmed.startsWith(\"/\")) {\n const body = trimmed.slice(1);\n return body.endsWith(\"/\") ? `${body.slice(0, -1)}/**` : body;\n }\n // Decide based on whether the unsuffixed pattern has any internal slashes:\n // foo, *.log, scratch.* → basename-style, prepend **/\n // foo/ → basename-style dir, prepend **/ + suffix /**\n // path/to/foo, path/foo/ → workspace-anchored as written\n const stripped = trimmed.endsWith(\"/\") ? trimmed.slice(0, -1) : trimmed;\n const isAnchored = stripped.includes(\"/\");\n if (trimmed.endsWith(\"/\")) return isAnchored ? `${stripped}/**` : `**/${stripped}/**`;\n return isAnchored ? trimmed : `**/${trimmed}`;\n}\n\n// Load ignore patterns from .alfesyncignore + defaults. User patterns\n// are run through normaliseIgnorePattern so a gitignore-style `*.log`\n// matches at any depth (rather than only at the workspace root).\nexport async function loadIgnorePatterns(\n workspacePath: string,\n): Promise<string[]> {\n const ignoreFile = join(workspacePath, \".alfesyncignore\");\n const patterns = [...DEFAULT_IGNORES];\n\n if (existsSync(ignoreFile)) {\n try {\n const content = await readFile(ignoreFile, \"utf-8\");\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n // Skip empty lines and comments\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n patterns.push(normaliseIgnorePattern(trimmed));\n }\n } catch {\n // Ignore read errors — use defaults only\n }\n }\n\n return patterns;\n}\n\n// Check if a relative path should be ignored. Negation patterns (!foo)\n// un-ignore a path matched by an earlier positive pattern — gitignore\n// semantics. micromatch.isMatch with an array does NOT honour negation\n// (it OR's all patterns), so we split positive/negative and combine\n// manually: ignored iff any positive matches AND no negative matches.\nexport function shouldIgnore(\n relativePath: string,\n patterns: string[],\n): boolean {\n const positive: string[] = [];\n const negative: string[] = [];\n for (const p of patterns) {\n if (p.startsWith(\"!\")) negative.push(p.slice(1));\n else positive.push(p);\n }\n if (positive.length === 0) return false;\n if (!micromatch.isMatch(relativePath, positive, { dot: true })) return false;\n if (negative.length === 0) return true;\n return !micromatch.isMatch(relativePath, negative, { dot: true });\n}\n\n// Filter a list of relative paths, removing ignored ones.\nexport function filterIgnored(\n paths: string[],\n patterns: string[],\n): string[] {\n return paths.filter((p) => !shouldIgnore(p, patterns));\n}\n","/**\n * Tiny retry helper used by uploader/downloader. Extracted so both\n * use the same timing and surface the same final error shape.\n */\n\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_BASE_DELAY_MS = 1000;\n\nexport interface RetryOptions {\n maxRetries?: number;\n baseDelayMs?: number;\n}\n\n/**\n * Run `fn` up to `maxRetries + 1` times with exponential backoff\n * (`base * 2^attempt`). Returns the first successful value, or rethrows\n * the last error.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n if (attempt < maxRetries) {\n const delay = baseDelayMs * Math.pow(2, attempt);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(String(lastError));\n}\n","/**\n * AlfeSync uploader — push changed files to S3.\n *\n * Per file:\n * 1. Ask the agent API for a presigned PUT URL\n * 2. PUT bytes directly to S3 (raw fetch — S3 isn't an Alfe API)\n * 3. Tell the agent API the upload completed (`syncConfirmUpload`)\n * 4. Update the local manifest\n *\n * Each step retries with exponential backoff via `withRetry`.\n */\n\nimport { readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nimport { computeFileHash, updateManifestEntry } from \"./manifest.js\";\nimport type { ManifestEntry } from \"./manifest.js\";\nimport { withRetry } from \"./retry.js\";\n\nconst log = createLogger(\"SyncUploader\");\n\nexport interface UploadResult {\n path: string;\n success: boolean;\n hash?: string;\n size?: number;\n error?: string;\n}\n\nconst ARCHIVE_PREFIXES = [\"sessions/archive/\", \"context/archive/\"] as const;\n\nfunction getStorageClass(relativePath: string): \"STANDARD\" | \"GLACIER_IR\" {\n return ARCHIVE_PREFIXES.some((p) => relativePath.startsWith(p))\n ? \"GLACIER_IR\"\n : \"STANDARD\";\n}\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\")) return \"text/yaml\";\n return \"application/octet-stream\";\n}\n\nasync function uploadOne(\n workspacePath: string,\n relativePath: string,\n client: AgentApiClient,\n): Promise<UploadResult> {\n const absolutePath = join(workspacePath, relativePath);\n try {\n return await withRetry(async () => {\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. Presigned PUT URL\n const presigned = await client.syncPresign({\n files: [{ path: relativePath, operation: \"put\", contentType }],\n });\n const url = presigned.urls[0]?.url;\n if (!url) throw new Error(\"No presigned URL returned\");\n\n // 2. Upload bytes directly to S3 (not an Alfe API)\n const fileContent = await readFile(absolutePath);\n const putResponse = await fetch(url, {\n method: \"PUT\",\n headers: { \"Content-Type\": contentType },\n body: fileContent,\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 via agent API\n await client.syncConfirmUpload({\n filePath: relativePath,\n hash,\n size,\n storageClass,\n });\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 });\n } catch (err) {\n return {\n path: relativePath,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Upload many files, batched by `concurrency`.\n */\nexport async function uploadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: AgentApiClient,\n options: { concurrency?: number; quiet?: boolean } = {},\n): Promise<UploadResult[]> {\n const { concurrency = 5, quiet = false } = options;\n const results: UploadResult[] = [];\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) => uploadOne(workspacePath, path, client)),\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 — pull files from S3 to disk.\n *\n * Per file:\n * 1. Ask the agent API for a presigned GET URL\n * 2. GET bytes directly from S3 (raw fetch — S3 isn't an Alfe API)\n * 3. Write to disk\n * 4. Update the local manifest\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nimport { updateManifestEntry } from \"./manifest.js\";\nimport type { ManifestEntry, RemoteManifest } from \"./manifest.js\";\nimport { withRetry } from \"./retry.js\";\n\nconst log = createLogger(\"SyncDownloader\");\n\nexport interface DownloadResult {\n path: string;\n success: boolean;\n size?: number;\n error?: string;\n}\n\nasync function downloadOne(\n workspacePath: string,\n relativePath: string,\n client: AgentApiClient,\n remoteEntry?: RemoteManifest[\"files\"][string],\n): Promise<DownloadResult> {\n try {\n return await withRetry(async () => {\n const presigned = await client.syncPresign({\n files: [{ path: relativePath, operation: \"get\" }],\n });\n const url = presigned.urls[0]?.url;\n if (!url) throw new Error(\"No presigned URL returned\");\n\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(\n `S3 GET failed (${String(response.status)}): ${await response.text()}`,\n );\n }\n const buffer = Buffer.from(await response.arrayBuffer());\n\n const absolutePath = join(workspacePath, relativePath);\n await mkdir(dirname(absolutePath), { recursive: true });\n await writeFile(absolutePath, buffer);\n\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 });\n } catch (err) {\n return {\n path: relativePath,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\nexport async function downloadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: AgentApiClient,\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 downloadOne(workspacePath, path, client, remoteManifest?.files[path]),\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.\n *\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: when a remote file is newer than the local manifest\n * entry AND the local file has also changed, the local copy is renamed to\n * `<name>.conflict-<timestamp>.<ext>` and the remote version wins.\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\";\nimport type { AgentApiClient, SyncManifest } from \"@alfe.ai/agent-api-client\";\n\nimport {\n readManifest,\n computeFileHash,\n diffManifests,\n removeManifestEntry,\n} from \"./manifest.js\";\nimport { loadIgnorePatterns, filterIgnored, shouldIgnore } from \"./ignore.js\";\nimport { uploadFiles } from \"./uploader.js\";\nimport { downloadFiles } from \"./downloader.js\";\n\nconst log = createLogger(\"SyncEngine\");\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 client: AgentApiClient;\n}\n\n/**\n * Construct a sync engine bound to a workspace + agent API client.\n *\n * The client is constructed once in plugin.ts (or the CLI) and passed in,\n * so credentials never leak into multiple places.\n */\nexport function createSyncEngine({ workspacePath, client }: SyncEngineOptions) {\n return {\n workspacePath,\n client,\n\n /**\n * Upload changed files. Pass `paths` for an explicit list, or omit\n * to scan the workspace for anything that drifted from the manifest.\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 paths && paths.length > 0\n ? filterIgnored(paths, ignorePatterns)\n : await detectLocalChanges(workspacePath, ignorePatterns);\n\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) log.info(`Pushing ${String(filesToPush.length)} file(s)`);\n\n const results = await uploadFiles(workspacePath, filesToPush, client, { quiet });\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 // Propagate local deletes to the cloud. Called by the watcher's onChanges\n // when chokidar reports `unlink` events (paths that no longer exist on\n // disk). Each path is removed via `client.syncDeleteFile()` then dropped\n // from the local manifest. Wrapped by the caller with a rolling-window\n // rate brake — see makeDeleteBrake() — so a buggy watcher event or a\n // mistaken `rm -rf` can't nuke the cloud copy in one go.\n async pushDeletes(\n paths: string[],\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n if (paths.length === 0) return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n if (!quiet) log.info(`Deleting ${String(paths.length)} file(s) from cloud`);\n\n let deleted = 0;\n let errors = 0;\n for (const path of paths) {\n try {\n await client.syncDeleteFile(path);\n await removeManifestEntry(workspacePath, path);\n deleted++;\n if (!quiet) log.info(`Deleted from cloud: ${path}`);\n } catch (err) {\n errors++;\n if (!quiet) log.error({ err }, `Failed to delete ${path} from cloud`);\n }\n }\n\n if (!quiet) {\n log.info(`Delete complete: ${String(deleted)} deleted, ${String(errors)} failed`);\n }\n // Reuse `pushed` to count successful deletes — same shape, distinct meaning.\n return { pushed: deleted, pulled: 0, conflicts: 0, errors };\n },\n\n /** Download files newer on remote. */\n async pull(options: { quiet?: boolean } = {}): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.syncGetManifest(),\n ]);\n\n const diff = diffManifests(localManifest, remoteManifest);\n if (diff.toPull.length === 0) {\n if (!quiet) log.info(\"Nothing to pull\");\n return { pushed: 0, pulled: 0, conflicts: diff.conflicts.length, errors: 0 };\n }\n\n if (!quiet) log.info(`Pulling ${String(diff.toPull.length)} file(s)`);\n\n const results = await downloadFiles(\n workspacePath,\n [...diff.toPull],\n client,\n remoteManifest,\n { quiet },\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: diff.conflicts.length, errors };\n },\n\n /**\n * Bidirectional sync. True conflicts (changed on both sides) are saved\n * locally as `.conflict-<ts>` files and the remote version wins.\n */\n async fullSync(options: { quiet?: boolean } = {}): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.syncGetManifest(),\n ]);\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath);\n const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);\n\n const diff = diffManifests(localManifest, remoteManifest);\n const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));\n const remoteOnlyChanges = diff.conflicts.filter((p) => !localChanges.includes(p));\n\n let conflictCount = 0;\n for (const conflictPath of trueConflicts) {\n const absolutePath = join(workspacePath, conflictPath);\n if (!existsSync(absolutePath)) continue;\n const ext = extname(conflictPath);\n const base = basename(conflictPath, ext);\n const dir = dirname(conflictPath);\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const conflictName = `${base}.conflict-${timestamp}${ext}`;\n const conflictAbsolute = join(workspacePath, dir, conflictName);\n\n const content = await readFile(absolutePath);\n await writeFile(conflictAbsolute, content);\n\n if (!quiet) log.warn(`Conflict: ${conflictPath} — saved as ${conflictName}`);\n conflictCount++;\n }\n\n const filesToPush = filterIgnored(\n [...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))],\n ignorePatterns,\n );\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) log.info(`Pushing ${String(filesToPush.length)} file(s)`);\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) log.info(`Pulling ${String(filesToPull.length)} file(s)`);\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 if (!quiet) {\n log.info(\n `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 a known list of files (used for relay-driven notifications,\n * where we already know which paths changed remotely).\n */\n async pullFiles(\n paths: string[],\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n if (paths.length === 0) return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n const remoteManifest: SyncManifest = await client.syncGetManifest();\n const localManifest = await readManifest(workspacePath);\n\n const filesToPull = paths.filter((p) => {\n if (!(p in remoteManifest.files)) return false;\n if (!(p in localManifest.files)) return true;\n return localManifest.files[p].hash !== remoteManifest.files[p].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) log.info(`Pulling ${String(filesToPull.length)} changed file(s)`);\n\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\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 return { pushed: 0, pulled, conflicts: 0, errors };\n },\n\n /**\n * Delete a file locally and from the manifest. Used when the sync relay\n * tells us a file was removed remotely.\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\nexport type SyncEngine = ReturnType<typeof createSyncEngine>;\n\n/**\n * Walk the workspace and return paths whose hash differs from the manifest\n * (or that are missing from it entirely).\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 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 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 const isIgnored = ignorePatterns.some(\n (p) => p.endsWith(\"/**\") && relativePath.startsWith(p.slice(0, -3)),\n );\n if (!isIgnored) await walk(fullPath);\n } else if (entry.isFile()) {\n if (shouldIgnore(relativePath, ignorePatterns)) continue;\n if (!(relativePath in manifest.files)) {\n changed.push(relativePath);\n continue;\n }\n try {\n const currentHash = await computeFileHash(fullPath);\n if (currentHash !== manifest.files[relativePath].hash) {\n changed.push(relativePath);\n }\n } catch {\n // file may have been deleted between listing and hashing\n }\n }\n }\n }\n\n await walk(workspacePath);\n return changed;\n}\n\n/** Directories the engine never descends into. */\nfunction shouldSkipDir(name: string): boolean {\n return (\n name === \"node_modules\" ||\n name === \".git\" ||\n name === \".sst\" ||\n name === \".build\" ||\n name === \"dist\"\n );\n}\n"],"mappings":"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"}
|