@alfe.ai/openclaw-sync 0.1.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.cjs +12 -8
- package/dist/cli/index.js +13 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/defaults/common.alfesyncignore +43 -0
- package/dist/defaults/hermes.alfesyncignore +14 -0
- package/dist/defaults/openclaw.alfesyncignore +17 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +32 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +32 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin2.cjs +20 -10
- package/dist/plugin2.js +21 -11
- package/dist/plugin2.js.map +1 -1
- package/dist/sync-engine.cjs +72 -34
- package/dist/sync-engine.js +62 -36
- package/dist/sync-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/sync-engine.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { createReadStream, existsSync } from "node:fs";
|
|
2
|
+
import { createReadStream, existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { basename, dirname, extname, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import micromatch from "micromatch";
|
|
7
8
|
import { createLogger } from "@auriclabs/logger";
|
|
8
9
|
//#region src/manifest.ts
|
|
@@ -121,33 +122,39 @@ function diffManifests(local, remote) {
|
|
|
121
122
|
}
|
|
122
123
|
//#endregion
|
|
123
124
|
//#region src/ignore.ts
|
|
124
|
-
const
|
|
125
|
-
|
|
125
|
+
const MODULE_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
126
|
+
const DEFAULTS_DIR = [join(MODULE_DIR, "defaults"), join(MODULE_DIR, "..", "defaults")].find(existsSync) ?? join(MODULE_DIR, "defaults");
|
|
127
|
+
function parseDefaultGlobs(text) {
|
|
128
|
+
return text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
129
|
+
}
|
|
130
|
+
function readDefaultGlobs(name) {
|
|
131
|
+
const file = join(DEFAULTS_DIR, `${name}.alfesyncignore`);
|
|
132
|
+
try {
|
|
133
|
+
return parseDefaultGlobs(readFileSync(file, "utf-8"));
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const CRITICAL_FALLBACK_IGNORES = [
|
|
126
139
|
"**/node_modules/**",
|
|
127
|
-
"**/*.tmp",
|
|
128
|
-
"**/.DS_Store",
|
|
129
140
|
"**/.git/**",
|
|
130
|
-
"**/.sst/**",
|
|
131
|
-
"**/.build/**",
|
|
132
|
-
"**/dist/**",
|
|
133
141
|
"**/.env",
|
|
134
|
-
"**/.env.*",
|
|
135
|
-
"**/openclaw.json.bak*",
|
|
136
|
-
"**/openclaw.json.clobbered.*",
|
|
137
|
-
"**/openclaw.json.preserve.*",
|
|
138
|
-
"**/Cache/**",
|
|
139
|
-
"**/Code Cache/**",
|
|
140
|
-
"**/GPUCache/**",
|
|
141
|
-
"**/CacheStorage/**",
|
|
142
|
-
"**/Service Worker/CacheStorage/**",
|
|
143
|
-
"**/blob_storage/**",
|
|
144
|
-
"**/Crashpad/**",
|
|
145
|
-
"**/ShaderCache/**",
|
|
146
|
-
"**/component_crx_cache/**",
|
|
147
142
|
"**/__pycache__/**",
|
|
148
|
-
"
|
|
149
|
-
"
|
|
143
|
+
"npm/**",
|
|
144
|
+
"extensions/**",
|
|
145
|
+
"plugins/**",
|
|
146
|
+
"**/.venv/**"
|
|
150
147
|
];
|
|
148
|
+
const commonGlobs = readDefaultGlobs("common");
|
|
149
|
+
const DEFAULT_IGNORES = commonGlobs.length > 0 ? commonGlobs : CRITICAL_FALLBACK_IGNORES;
|
|
150
|
+
const runtimeDefaultsCache = /* @__PURE__ */ new Map();
|
|
151
|
+
function runtimeDefaults(runtime) {
|
|
152
|
+
const cached = runtimeDefaultsCache.get(runtime);
|
|
153
|
+
if (cached) return cached;
|
|
154
|
+
const patterns = [...DEFAULT_IGNORES, ...readDefaultGlobs(runtime)];
|
|
155
|
+
runtimeDefaultsCache.set(runtime, patterns);
|
|
156
|
+
return patterns;
|
|
157
|
+
}
|
|
151
158
|
function normaliseIgnorePattern(raw) {
|
|
152
159
|
const trimmed = raw.trim();
|
|
153
160
|
if (!trimmed) return trimmed;
|
|
@@ -161,9 +168,9 @@ function normaliseIgnorePattern(raw) {
|
|
|
161
168
|
if (trimmed.endsWith("/")) return isAnchored ? `${stripped}/**` : `**/${stripped}/**`;
|
|
162
169
|
return isAnchored ? trimmed : `**/${trimmed}`;
|
|
163
170
|
}
|
|
164
|
-
async function loadIgnorePatterns(workspacePath) {
|
|
171
|
+
async function loadIgnorePatterns(workspacePath, runtime = "openclaw") {
|
|
165
172
|
const ignoreFile = join(workspacePath, ".alfesyncignore");
|
|
166
|
-
const patterns = [...
|
|
173
|
+
const patterns = [...runtimeDefaults(runtime)];
|
|
167
174
|
if (existsSync(ignoreFile)) try {
|
|
168
175
|
const lines = (await readFile(ignoreFile, "utf-8")).split("\n");
|
|
169
176
|
for (const line of lines) {
|
|
@@ -184,6 +191,11 @@ function shouldIgnore(relativePath, patterns) {
|
|
|
184
191
|
if (negative.length === 0) return true;
|
|
185
192
|
return !micromatch.isMatch(relativePath, negative, { dot: true });
|
|
186
193
|
}
|
|
194
|
+
const IGNORE_DIR_PROBE = "__alfe_ignore_probe__";
|
|
195
|
+
function shouldIgnoreDir(relativeDir, patterns) {
|
|
196
|
+
if (relativeDir === "") return false;
|
|
197
|
+
return shouldIgnore(`${relativeDir}/${IGNORE_DIR_PROBE}`, patterns);
|
|
198
|
+
}
|
|
187
199
|
function filterIgnored(paths, patterns) {
|
|
188
200
|
return paths.filter((p) => !shouldIgnore(p, patterns));
|
|
189
201
|
}
|
|
@@ -396,13 +408,14 @@ const log = createLogger("SyncEngine");
|
|
|
396
408
|
* The client is constructed once in plugin.ts (or the CLI) and passed in,
|
|
397
409
|
* so credentials never leak into multiple places.
|
|
398
410
|
*/
|
|
399
|
-
function createSyncEngine({ workspacePath, client }) {
|
|
411
|
+
function createSyncEngine({ workspacePath, client, runtime = "openclaw" }) {
|
|
400
412
|
return {
|
|
401
413
|
workspacePath,
|
|
402
414
|
client,
|
|
415
|
+
runtime,
|
|
403
416
|
async push(paths, options = {}) {
|
|
404
417
|
const { quiet = false, filter } = options;
|
|
405
|
-
const ignorePatterns = await loadIgnorePatterns(workspacePath);
|
|
418
|
+
const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);
|
|
406
419
|
let filesToPush = paths && paths.length > 0 ? filterIgnored(paths, ignorePatterns) : await detectLocalChanges(workspacePath, ignorePatterns);
|
|
407
420
|
if (filter) filesToPush = filesToPush.filter((p) => p.startsWith(filter));
|
|
408
421
|
if (filesToPush.length === 0) {
|
|
@@ -482,7 +495,7 @@ function createSyncEngine({ workspacePath, client }) {
|
|
|
482
495
|
async fullSync(options = {}) {
|
|
483
496
|
const { quiet = false } = options;
|
|
484
497
|
const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
|
|
485
|
-
const ignorePatterns = await loadIgnorePatterns(workspacePath);
|
|
498
|
+
const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);
|
|
486
499
|
const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);
|
|
487
500
|
const diff = diffManifests(localManifest, remoteManifest);
|
|
488
501
|
const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));
|
|
@@ -543,7 +556,7 @@ function createSyncEngine({ workspacePath, client }) {
|
|
|
543
556
|
return this.push(void 0, options);
|
|
544
557
|
}
|
|
545
558
|
if (!quiet) log.info(`First-run sync: ${String(remotePaths.length)} file(s) in cloud — restoring cloud state before seeding`);
|
|
546
|
-
const ignorePatterns = await loadIgnorePatterns(workspacePath);
|
|
559
|
+
const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);
|
|
547
560
|
const restorePaths = filterIgnored(remotePaths, ignorePatterns);
|
|
548
561
|
const restore = await this.pullFiles(restorePaths, options);
|
|
549
562
|
const remoteSet = new Set(remotePaths);
|
|
@@ -597,6 +610,24 @@ function createSyncEngine({ workspacePath, client }) {
|
|
|
597
610
|
errors
|
|
598
611
|
};
|
|
599
612
|
},
|
|
613
|
+
async pruneIgnored(options = {}) {
|
|
614
|
+
const { quiet = false, limit = 1e3 } = options;
|
|
615
|
+
const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);
|
|
616
|
+
const remoteManifest = await client.syncGetManifest();
|
|
617
|
+
const ignoredPaths = Object.keys(remoteManifest.files).filter((p) => shouldIgnore(p, ignorePatterns));
|
|
618
|
+
if (ignoredPaths.length === 0) return {
|
|
619
|
+
pushed: 0,
|
|
620
|
+
pulled: 0,
|
|
621
|
+
conflicts: 0,
|
|
622
|
+
errors: 0
|
|
623
|
+
};
|
|
624
|
+
const batch = ignoredPaths.slice(0, limit);
|
|
625
|
+
if (!quiet) {
|
|
626
|
+
const capped = ignoredPaths.length > batch.length ? ` (capped at ${String(limit)}; ${String(ignoredPaths.length - batch.length)} remain for next pass)` : "";
|
|
627
|
+
log.info(`Pruning ${String(batch.length)} ignored file(s) from cloud${capped}`);
|
|
628
|
+
}
|
|
629
|
+
return this.pushDeletes(batch, { quiet });
|
|
630
|
+
},
|
|
600
631
|
async removeLocalFile(filePath, options = {}) {
|
|
601
632
|
const { quiet = false } = options;
|
|
602
633
|
const absolutePath = join(workspacePath, filePath);
|
|
@@ -626,9 +657,8 @@ async function detectLocalChanges(workspacePath, ignorePatterns) {
|
|
|
626
657
|
for (const entry of entries) {
|
|
627
658
|
const fullPath = join(dir, entry.name);
|
|
628
659
|
const relativePath = fullPath.slice(workspacePath.length + 1).replace(/\\/g, "/");
|
|
629
|
-
if (shouldSkipDir(entry.name)) continue;
|
|
630
660
|
if (entry.isDirectory()) {
|
|
631
|
-
if (!
|
|
661
|
+
if (!shouldIgnoreDir(relativePath, ignorePatterns)) await walk(fullPath);
|
|
632
662
|
} else if (entry.isFile()) {
|
|
633
663
|
if (shouldIgnore(relativePath, ignorePatterns)) continue;
|
|
634
664
|
if (!(relativePath in manifest.files)) {
|
|
@@ -644,11 +674,7 @@ async function detectLocalChanges(workspacePath, ignorePatterns) {
|
|
|
644
674
|
await walk(workspacePath);
|
|
645
675
|
return changed;
|
|
646
676
|
}
|
|
647
|
-
/** Directories the engine never descends into. */
|
|
648
|
-
function shouldSkipDir(name) {
|
|
649
|
-
return name === "node_modules" || name === ".git" || name === ".sst" || name === ".build" || name === "dist";
|
|
650
|
-
}
|
|
651
677
|
//#endregion
|
|
652
|
-
export {
|
|
678
|
+
export { DEFAULT_IGNORES as a, shouldIgnore as c, diffManifests as d, readManifest as f, writeManifest as h, withRetry as i, shouldIgnoreDir as l, updateManifestEntry as m, downloadFiles as n, filterIgnored as o, removeManifestEntry as p, uploadFiles as r, loadIgnorePatterns as s, createSyncEngine as t, computeFileHash as u };
|
|
653
679
|
|
|
654
680
|
//# sourceMappingURL=sync-engine.js.map
|
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","// 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 * First-run reconcile for realtime activation.\n *\n * Distinguishes a genuinely NEW agent (empty cloud state → push-only\n * seed, today's behaviour) from a REBUILD / reconnect (cloud already\n * holds this agent's last-synced state → restore cloud → local, cloud\n * wins, THEN seed only the files that are new locally).\n *\n * Why not `push(undefined)`? On a from-scratch VM rebuild the local\n * manifest is gone and `alfe setup` has just re-seeded the persona\n * templates (SOUL.md, IDENTITY.md, …) onto a fresh disk. With no\n * manifest, `detectLocalChanges()` classifies every seeded template as\n * \"new\", so a blind push uploads those fresh seeds OVER the agent's\n * edited cloud copy — the agent's edits are lost on every rebuild. This\n * is the persona-durability gap this method closes.\n *\n * Why not `fullSync()`? With an empty local manifest it double-counts\n * each on-disk seed as BOTH a push candidate (`detectLocalChanges`) and\n * a pull candidate (remote-only in `diffManifests`, since the local\n * manifest has no entry for it). fullSync pushes before it pulls, so it\n * would still clobber the cloud edit before restoring it locally.\n *\n * Conflict direction: for a rebuild the CLOUD copy is authoritative for\n * anything it holds. We do not write `.conflict-<ts>` markers here — a\n * fresh setup seed losing to the agent's own prior edit is the intended\n * outcome, not a conflict.\n */\n async firstRunReconcile(\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const remoteManifest = await client.syncGetManifest();\n const remotePaths = Object.keys(remoteManifest.files);\n\n // Fresh agent: nothing in the cloud → seed the workspace up (today's\n // push-only behaviour). There is no prior state to lose, so pushing\n // the setup-seeded templates is correct.\n if (remotePaths.length === 0) {\n if (!quiet) {\n log.info(\"First-run sync: no cloud state — seeding workspace to cloud\");\n }\n return this.push(undefined, options);\n }\n\n // Rebuild / reconnect: the cloud is the source of truth for anything\n // it holds. Restore it over the local setup-seeded templates so the\n // agent's edits survive.\n if (!quiet) {\n log.info(\n `First-run sync: ${String(remotePaths.length)} file(s) in cloud — restoring cloud state before seeding`,\n );\n }\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath);\n\n // Restore cloud → local (cloud wins). Ignore-filter the pull list so a\n // pre-fix workspace's stale ignored objects (e.g. `**/Cache/**`) that\n // an older buggy ignore engine pushed into the cloud aren't dragged\n // back onto a fresh disk — the exact regression that made initial sync\n // push-only. `pullFiles` only downloads paths whose local copy is\n // missing or hash-divergent, so identical seeds aren't re-fetched.\n const restorePaths = filterIgnored(remotePaths, ignorePatterns);\n const restore = await this.pullFiles(restorePaths, options);\n\n // Seed up only files that are genuinely new locally — present on disk\n // but absent from the cloud. Files the cloud already holds were just\n // restored above (cloud wins), so they're intentionally excluded here.\n const remoteSet = new Set(remotePaths);\n const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);\n const toSeed = localChanges.filter((p) => !remoteSet.has(p));\n const seed =\n toSeed.length > 0\n ? await this.push(toSeed, options)\n : { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n return {\n pushed: seed.pushed,\n pulled: restore.pulled,\n conflicts: restore.conflicts + seed.conflicts,\n errors: restore.errors + seed.errors,\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, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport micromatch from \"micromatch\";\n\n// Committed default ignore files live next to this module (copied to\n// dist/defaults at build). `common` applies to every runtime; each runtime\n// (openclaw, hermes) adds its own runtime-internal dirs. These are the source\n// of truth for the baseline set — a hardcoded array used to live here.\n//\n// tsdown bundles this module into three entries at different depths\n// (dist/index.js, dist/plugin.js, dist/cli/index.js), so `./defaults` resolves\n// to dist/defaults for the top-level entries but dist/cli/defaults for the CLI.\n// The copy step only writes dist/defaults, so probe both `./defaults` and\n// `../defaults` and use whichever exists. In src (vitest) `./defaults` wins.\nconst MODULE_DIR = fileURLToPath(new URL(\".\", import.meta.url));\nconst DEFAULTS_DIR =\n [join(MODULE_DIR, \"defaults\"), join(MODULE_DIR, \"..\", \"defaults\")].find(existsSync) ??\n join(MODULE_DIR, \"defaults\");\n\n// Parse a committed default ignore file: lines are FULL micromatch globs,\n// used verbatim (no gitignore normalisation). Blank lines and `#` comments\n// are skipped.\nfunction parseDefaultGlobs(text: string): string[] {\n return text\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0 && !l.startsWith(\"#\"));\n}\n\n// Read a committed default file, tolerating a missing file (unknown runtime →\n// no extra patterns). Returns [] on any read error.\nfunction readDefaultGlobs(name: string): string[] {\n const file = join(DEFAULTS_DIR, `${name}.alfesyncignore`);\n try {\n return parseDefaultGlobs(readFileSync(file, \"utf-8\"));\n } catch {\n return [];\n }\n}\n\n// Load-bearing backstop if the committed common file can't be read (a bundler\n// stripped it, or the tsdown copy step regressed). Failing OPEN to \"ignore\n// nothing\" would re-watch node_modules + the runtime caches and resurrect the\n// exact ENOSPC crash-loop this fix exists to prevent — so the crash-prevention\n// entries are duplicated here. Kept intentionally minimal; the committed files\n// remain the source of truth. The `DEFAULT_IGNORES loaded from committed file`\n// test asserts the real file loaded (not this fallback) so a copy/bundle\n// regression fails CI rather than silently degrading in prod.\nconst CRITICAL_FALLBACK_IGNORES = [\n \"**/node_modules/**\",\n \"**/.git/**\",\n \"**/.env\",\n \"**/__pycache__/**\",\n \"npm/**\",\n \"extensions/**\",\n \"plugins/**\",\n \"**/.venv/**\",\n];\n\n// Default ignore patterns — always applied to every runtime. Exported so tests\n// and the docs generator can introspect the list without duplicating it.\n// Sourced from the committed `defaults/common.alfesyncignore`, with a hardcoded\n// fallback so an unreadable file never fails open.\n//\n// Patterns are full micromatch globs. To match a name at any depth use a\n// globstar prefix (two asterisks plus slash). The `matchBase` option is NOT\n// used because it breaks deep globstar/X/globstar matches in micromatch 4.x.\nconst commonGlobs = readDefaultGlobs(\"common\");\nexport const DEFAULT_IGNORES =\n commonGlobs.length > 0 ? commonGlobs : CRITICAL_FALLBACK_IGNORES;\n\n// Cache the per-runtime default set (common + runtime) so repeated\n// loadIgnorePatterns calls don't re-read the committed files.\nconst runtimeDefaultsCache = new Map<string, string[]>();\n\nfunction runtimeDefaults(runtime: string): string[] {\n const cached = runtimeDefaultsCache.get(runtime);\n if (cached) return cached;\n // common first, then runtime-specific; unknown runtime contributes nothing.\n const patterns = [...DEFAULT_IGNORES, ...readDefaultGlobs(runtime)];\n runtimeDefaultsCache.set(runtime, patterns);\n return patterns;\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 the committed per-runtime defaults + the\n// workspace `.alfesyncignore`. Defaults (common + runtime-internal dirs) come\n// first; user patterns are appended last (so a `!negation` can un-ignore a\n// default) and run through normaliseIgnorePattern so a gitignore-style `*.log`\n// matches at any depth. `runtime` selects the runtime-specific default file\n// (openclaw / hermes); an unknown runtime falls back to the common set.\nexport async function loadIgnorePatterns(\n workspacePath: string,\n runtime = \"openclaw\",\n): Promise<string[]> {\n const ignoreFile = join(workspacePath, \".alfesyncignore\");\n const patterns = [...runtimeDefaults(runtime)];\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// Check whether a DIRECTORY's contents are ignored — i.e. whether the watcher\n// / scanner may prune it and never descend. A directory `npm` isn't itself\n// matched by `npm/**` (that glob needs a child segment), so we probe a\n// sentinel child: if `npm/<probe>` is ignored, everything under `npm` is too.\n// Used by the chokidar `ignored` predicate and the workspace walk so a single\n// ignore engine governs both file matching and directory pruning.\nconst IGNORE_DIR_PROBE = \"__alfe_ignore_probe__\";\nexport function shouldIgnoreDir(\n relativeDir: string,\n patterns: string[],\n): boolean {\n if (relativeDir === \"\") return false; // never prune the workspace root\n // Forward-slash join — micromatch globs are posix-style regardless of OS.\n return shouldIgnore(`${relativeDir}/${IGNORE_DIR_PROBE}`, patterns);\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, shouldIgnoreDir } 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 * Active agent runtime — selects the per-runtime default ignore set used by\n * every scan (push / fullSync / firstRunReconcile). Default: \"openclaw\".\n */\n runtime?: string;\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, runtime = \"openclaw\" }: SyncEngineOptions) {\n return {\n workspacePath,\n client,\n runtime,\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, runtime);\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, runtime);\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 * First-run reconcile for realtime activation.\n *\n * Distinguishes a genuinely NEW agent (empty cloud state → push-only\n * seed, today's behaviour) from a REBUILD / reconnect (cloud already\n * holds this agent's last-synced state → restore cloud → local, cloud\n * wins, THEN seed only the files that are new locally).\n *\n * Why not `push(undefined)`? On a from-scratch VM rebuild the local\n * manifest is gone and `alfe setup` has just re-seeded the persona\n * templates (SOUL.md, IDENTITY.md, …) onto a fresh disk. With no\n * manifest, `detectLocalChanges()` classifies every seeded template as\n * \"new\", so a blind push uploads those fresh seeds OVER the agent's\n * edited cloud copy — the agent's edits are lost on every rebuild. This\n * is the persona-durability gap this method closes.\n *\n * Why not `fullSync()`? With an empty local manifest it double-counts\n * each on-disk seed as BOTH a push candidate (`detectLocalChanges`) and\n * a pull candidate (remote-only in `diffManifests`, since the local\n * manifest has no entry for it). fullSync pushes before it pulls, so it\n * would still clobber the cloud edit before restoring it locally.\n *\n * Conflict direction: for a rebuild the CLOUD copy is authoritative for\n * anything it holds. We do not write `.conflict-<ts>` markers here — a\n * fresh setup seed losing to the agent's own prior edit is the intended\n * outcome, not a conflict.\n */\n async firstRunReconcile(\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const remoteManifest = await client.syncGetManifest();\n const remotePaths = Object.keys(remoteManifest.files);\n\n // Fresh agent: nothing in the cloud → seed the workspace up (today's\n // push-only behaviour). There is no prior state to lose, so pushing\n // the setup-seeded templates is correct.\n if (remotePaths.length === 0) {\n if (!quiet) {\n log.info(\"First-run sync: no cloud state — seeding workspace to cloud\");\n }\n return this.push(undefined, options);\n }\n\n // Rebuild / reconnect: the cloud is the source of truth for anything\n // it holds. Restore it over the local setup-seeded templates so the\n // agent's edits survive.\n if (!quiet) {\n log.info(\n `First-run sync: ${String(remotePaths.length)} file(s) in cloud — restoring cloud state before seeding`,\n );\n }\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n\n // Restore cloud → local (cloud wins). Ignore-filter the pull list so a\n // pre-fix workspace's stale ignored objects (e.g. `**/Cache/**`) that\n // an older buggy ignore engine pushed into the cloud aren't dragged\n // back onto a fresh disk — the exact regression that made initial sync\n // push-only. `pullFiles` only downloads paths whose local copy is\n // missing or hash-divergent, so identical seeds aren't re-fetched.\n const restorePaths = filterIgnored(remotePaths, ignorePatterns);\n const restore = await this.pullFiles(restorePaths, options);\n\n // Seed up only files that are genuinely new locally — present on disk\n // but absent from the cloud. Files the cloud already holds were just\n // restored above (cloud wins), so they're intentionally excluded here.\n const remoteSet = new Set(remotePaths);\n const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);\n const toSeed = localChanges.filter((p) => !remoteSet.has(p));\n const seed =\n toSeed.length > 0\n ? await this.push(toSeed, options)\n : { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n return {\n pushed: seed.pushed,\n pulled: restore.pulled,\n conflicts: restore.conflicts + seed.conflicts,\n errors: restore.errors + seed.errors,\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 * Purge cloud files that match the current (per-runtime) ignore set.\n *\n * Self-heals workspaces whose cloud copy still holds runtime-internal junk\n * (node_modules, the openclaw npm cache, …) that a pre-fix ignore engine\n * uploaded before it was correctly ignored. Bounded to ignored paths only,\n * so it can never delete a legitimately-synced file, and it deletes the\n * cloud copy directly (not via the watcher) so the rolling-window delete\n * brake — which guards against a buggy `rm -rf` — does not apply.\n */\n async pruneIgnored(\n options: { quiet?: boolean; limit?: number } = {},\n ): Promise<SyncResult> {\n // Cap deletes per pass. pushDeletes is serial (one HTTP call per path), so\n // an agent with thousands of pre-fix leaked node_modules objects would\n // otherwise hammer the sync service in one burst. Draining over several\n // activations is fine — the watcher already ignores these paths, so they\n // never come back and the remainder is cleared on the next start.\n const { quiet = false, limit = 1000 } = options;\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n const remoteManifest = await client.syncGetManifest();\n const ignoredPaths = Object.keys(remoteManifest.files).filter((p) =>\n shouldIgnore(p, ignorePatterns),\n );\n if (ignoredPaths.length === 0) {\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n const batch = ignoredPaths.slice(0, limit);\n if (!quiet) {\n const capped = ignoredPaths.length > batch.length\n ? ` (capped at ${String(limit)}; ${String(ignoredPaths.length - batch.length)} remain for next pass)`\n : \"\";\n log.info(`Pruning ${String(batch.length)} ignored file(s) from cloud${capped}`);\n }\n return this.pushDeletes(batch, { quiet });\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 (entry.isDirectory()) {\n // Prune ignored directories (incl. node_modules and the runtime's\n // internal dirs) via the same ignore engine the watcher uses — one\n // source of truth, no hardcoded name list to drift.\n if (!shouldIgnoreDir(relativePath, ignorePatterns)) 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"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AlfeSync — agent workspace backup and sync skill for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"micromatch": "^4.0.8",
|
|
23
23
|
"ws": "^8.18.0",
|
|
24
24
|
"@alfe.ai/agent-api-client": "0.4.0",
|
|
25
|
-
"@alfe.ai/config": "0.
|
|
25
|
+
"@alfe.ai/config": "0.3.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"openclaw": ">=2026.3.0"
|