@alfe.ai/openclaw-sync 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,112 +1,33 @@
1
- const require_ignore = require("./ignore.cjs");
2
- let node_path = require("node:path");
3
- let node_os = require("node:os");
4
- let node_fs = require("node:fs");
5
- let _alfe_ai_config = require("@alfe.ai/config");
6
- let node_fs_promises = require("node:fs/promises");
7
- let node_crypto = require("node:crypto");
8
- let _auriclabs_logger = require("@auriclabs/logger");
9
- //#region src/config.ts
10
- /**
11
- * AlfeSync configuration.
12
- *
13
- * Derives credentials from `@alfe.ai/config` (~/.alfe/config.toml) so the
14
- * sync plugin works on any agent that's already logged in — no separate
15
- * `.alfesync/config.json` or manual `alfesync init` required.
16
- *
17
- * The only field not directly in @alfe.ai/config is `agentId`, which is
18
- * resolved once at startup by calling POST /auth/validate (same pattern
19
- * the gateway daemon uses to bootstrap its own identity).
20
- */
21
- const SYNC_STATE_DIR = (0, node_path.join)((0, node_path.join)((0, node_os.homedir)(), ".alfe"), "sync");
22
- /**
23
- * In-memory cache — agentId is stable for process lifetime.
24
- *
25
- * We cache the *Promise* (not just the resolved value) so that concurrent
26
- * callers during plugin activation share a single `/auth/validate` round-trip
27
- * instead of each firing their own.
28
- */
29
- let cachedSyncConfigPromise = null;
30
- /**
31
- * Local on-disk state directory (manifest, etc.) — separate from credentials.
32
- * Lives under ~/.alfe/sync/ so sync state stays alongside the rest of Alfe's
33
- * agent state, not scattered across the workspace.
34
- */
35
- function syncStateDir() {
36
- return SYNC_STATE_DIR;
37
- }
38
- /**
39
- * True if the agent has logged in (~/.alfe/config.toml exists).
40
- * Sync needs no separate initialization — login is enough.
41
- */
42
- function isInitialized() {
43
- return (0, _alfe_ai_config.configExists)();
44
- }
45
- /**
46
- * Invalidate the in-memory config cache. Call after login changes.
47
- */
48
- function invalidateSyncConfigCache() {
49
- cachedSyncConfigPromise = null;
50
- }
51
- /**
52
- * Resolve the full sync config — apiKey + apiUrl from @alfe.ai/config,
53
- * agentId/orgId fetched once from /auth/validate.
54
- *
55
- * Returns null if the agent isn't logged in or validation fails.
56
- * Callers that require config should use requireConfig().
57
- *
58
- * Concurrent callers share a single in-flight promise; failures are
59
- * not cached so a transient API blip doesn't poison the process.
60
- */
61
- async function resolveSyncConfig() {
62
- if (cachedSyncConfigPromise) return cachedSyncConfigPromise;
63
- const promise = doResolveSyncConfig();
64
- cachedSyncConfigPromise = promise;
65
- const result = await promise;
66
- if (result === null) cachedSyncConfigPromise = null;
67
- return result;
68
- }
69
- async function doResolveSyncConfig() {
70
- if (!(0, _alfe_ai_config.configExists)()) return null;
71
- let alfe;
72
- try {
73
- alfe = (0, _alfe_ai_config.resolveConfig)();
74
- } catch {
75
- return null;
76
- }
77
- const validated = await validateToken(alfe.apiUrl, alfe.apiKey);
78
- if (!validated?.agentId || !validated.tenantId) return null;
79
- return {
80
- agentId: validated.agentId,
81
- orgId: validated.tenantId,
82
- token: alfe.apiKey,
83
- workspacePath: alfe.workspacePath,
84
- apiUrl: alfe.apiUrl
85
- };
86
- }
87
- /**
88
- * Resolve config or throw with a clear message. Use in CLI entry points.
89
- */
90
- async function requireConfig() {
91
- if (!(0, _alfe_ai_config.configExists)()) throw new Error("Alfe not configured — run `alfe login` first.");
92
- const config = await resolveSyncConfig();
93
- if (!config) throw new Error("Failed to resolve sync config — token may be invalid or the API is unreachable.");
94
- return config;
95
- }
96
- async function validateToken(apiUrl, token) {
97
- try {
98
- const res = await fetch(`${apiUrl}/auth/validate`, {
99
- method: "POST",
100
- headers: { "Content-Type": "application/json" },
101
- body: JSON.stringify({ token })
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
102
14
  });
103
- if (!res.ok) return null;
104
- return await res.json();
105
- } catch {
106
- return null;
107
15
  }
108
- }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
109
22
  //#endregion
23
+ let node_fs_promises = require("node:fs/promises");
24
+ let node_fs = require("node:fs");
25
+ let node_crypto = require("node:crypto");
26
+ let node_path = require("node:path");
27
+ let node_os = require("node:os");
28
+ let micromatch = require("micromatch");
29
+ micromatch = __toESM(micromatch);
30
+ let _auriclabs_logger = require("@auriclabs/logger");
110
31
  //#region src/manifest.ts
111
32
  /**
112
33
  * AlfeSync manifest — local file manifest at `~/.alfe/sync/manifest.json`.
@@ -119,13 +40,19 @@ async function validateToken(apiUrl, token) {
119
40
  * rest of the package, but the manifest itself is workspace-independent
120
41
  * (one agent, one workspace, one manifest).
121
42
  */
43
+ /**
44
+ * Local state directory for sync — manifest, etc. Lives under `~/.alfe/sync/`
45
+ * so it stays alongside the rest of Alfe's agent state instead of polluting
46
+ * the workspace.
47
+ */
48
+ const SYNC_STATE_DIR = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "sync");
122
49
  const MANIFEST_FILE = "manifest.json";
123
50
  /**
124
51
  * Resolve the manifest file path. Lives under `~/.alfe/sync/`, independent
125
52
  * of the workspace path — one agent has one manifest.
126
53
  */
127
54
  function manifestPath() {
128
- return (0, node_path.join)(syncStateDir(), MANIFEST_FILE);
55
+ return (0, node_path.join)(SYNC_STATE_DIR, MANIFEST_FILE);
129
56
  }
130
57
  /**
131
58
  * Read the local manifest. Returns empty manifest if not found.
@@ -150,7 +77,7 @@ async function readManifest(workspacePath) {
150
77
  */
151
78
  async function writeManifest(workspacePath, manifest) {
152
79
  const path = manifestPath();
153
- await (0, node_fs_promises.mkdir)(syncStateDir(), { recursive: true });
80
+ await (0, node_fs_promises.mkdir)(SYNC_STATE_DIR, { recursive: true });
154
81
  await (0, node_fs_promises.writeFile)(path, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
155
82
  }
156
83
  /**
@@ -216,118 +143,100 @@ function diffManifests(local, remote) {
216
143
  };
217
144
  }
218
145
  //#endregion
219
- //#region src/api-client.ts
146
+ //#region src/ignore.ts
220
147
  /**
221
- * Create an AlfeSync API client.
148
+ * AlfeSync ignore parse `.alfesyncignore` (gitignore-style glob matching).
149
+ *
150
+ * Uses micromatch for glob pattern matching, compatible with .gitignore syntax.
222
151
  */
223
- function createApiClient(config) {
224
- const { apiUrl, token, agentId } = config;
225
- const baseUrl = apiUrl.replace(/\/$/, "");
226
- async function request(method, path, body) {
227
- const url = `${baseUrl}${path}`;
228
- const headers = {
229
- Authorization: `Bearer ${token}`,
230
- "Content-Type": "application/json"
231
- };
232
- const response = await fetch(url, {
233
- method,
234
- headers,
235
- body: body ? JSON.stringify(body) : void 0
236
- });
237
- if (!response.ok) {
238
- const text = await response.text();
239
- let errorMsg;
240
- try {
241
- const parsed = JSON.parse(text);
242
- errorMsg = (typeof parsed.error === "string" ? parsed.error : void 0) ?? (typeof parsed.message === "string" ? parsed.message : void 0) ?? text;
243
- } catch {
244
- errorMsg = text;
245
- }
246
- throw new Error(`AlfeSync API error (${String(response.status)}): ${errorMsg}`);
152
+ /** Default ignore patterns — always applied */
153
+ const DEFAULT_IGNORES = [
154
+ ".alfesync/**",
155
+ "node_modules/**",
156
+ "*.tmp",
157
+ ".DS_Store",
158
+ ".git/**",
159
+ ".sst/**",
160
+ ".build/**",
161
+ "dist/**"
162
+ ];
163
+ /**
164
+ * Load ignore patterns from `.alfesyncignore` file + defaults.
165
+ */
166
+ async function loadIgnorePatterns(workspacePath) {
167
+ const ignoreFile = (0, node_path.join)(workspacePath, ".alfesyncignore");
168
+ const patterns = [...DEFAULT_IGNORES];
169
+ if ((0, node_fs.existsSync)(ignoreFile)) try {
170
+ const lines = (await (0, node_fs_promises.readFile)(ignoreFile, "utf-8")).split("\n");
171
+ for (const line of lines) {
172
+ const trimmed = line.trim();
173
+ if (!trimmed || trimmed.startsWith("#")) continue;
174
+ patterns.push(trimmed);
247
175
  }
248
- const json = await response.json();
249
- if (!json.success) throw new Error("AlfeSync API returned unsuccessful response");
250
- return json.data;
251
- }
252
- return {
253
- async getManifest() {
254
- return request("GET", `/sync/agents/${agentId}/manifest`);
255
- },
256
- async presignPut(filePath, contentType = "application/octet-stream") {
257
- return (await request("POST", `/sync/agents/${agentId}/presign`, { files: [{
258
- path: filePath,
259
- operation: "put",
260
- contentType
261
- }] })).urls[0];
262
- },
263
- async presignPutBatch(files) {
264
- return (await request("POST", `/sync/agents/${agentId}/presign`, { files: files.map((f) => ({
265
- path: f.path,
266
- operation: "put",
267
- contentType: f.contentType ?? "application/octet-stream"
268
- })) })).urls;
269
- },
270
- async confirmUpload(filePath, hash, size, storageClass = "STANDARD") {
271
- return request("POST", `/sync/agents/${agentId}/files/${filePath}/confirm`, {
272
- hash,
273
- size,
274
- storageClass
275
- });
276
- },
277
- async presignGet(filePath) {
278
- return (await request("POST", `/sync/agents/${agentId}/presign`, { files: [{
279
- path: filePath,
280
- operation: "get"
281
- }] })).urls[0];
282
- },
283
- async presignGetBatch(paths) {
284
- return (await request("POST", `/sync/agents/${agentId}/presign`, { files: paths.map((p) => ({
285
- path: p,
286
- operation: "get"
287
- })) })).urls;
288
- },
289
- async getStats() {
290
- return request("GET", `/sync/agents/${agentId}/stats`);
291
- },
292
- async registerAgent(displayName) {
293
- return request("POST", "/sync/agents", {
294
- agentId,
295
- displayName
296
- });
297
- },
298
- async getFileHistory(filePath) {
299
- return (await request("GET", `/sync/agents/${agentId}/files/${filePath}/versions`)).versions;
300
- },
301
- async reconstruct(mode = "full") {
302
- return request("POST", `/sync/agents/${agentId}/reconstruct`, { mode });
176
+ } catch {}
177
+ return patterns;
178
+ }
179
+ /**
180
+ * Check if a relative path should be ignored.
181
+ */
182
+ function shouldIgnore(relativePath, patterns) {
183
+ return micromatch.default.isMatch(relativePath, patterns, {
184
+ dot: true,
185
+ matchBase: true
186
+ });
187
+ }
188
+ /**
189
+ * Filter a list of relative paths, removing ignored ones.
190
+ */
191
+ function filterIgnored(paths, patterns) {
192
+ return paths.filter((p) => !shouldIgnore(p, patterns));
193
+ }
194
+ //#endregion
195
+ //#region src/retry.ts
196
+ /**
197
+ * Tiny retry helper used by uploader/downloader. Extracted so both
198
+ * use the same timing and surface the same final error shape.
199
+ */
200
+ const DEFAULT_MAX_RETRIES = 3;
201
+ const DEFAULT_BASE_DELAY_MS = 1e3;
202
+ /**
203
+ * Run `fn` up to `maxRetries + 1` times with exponential backoff
204
+ * (`base * 2^attempt`). Returns the first successful value, or rethrows
205
+ * the last error.
206
+ */
207
+ async function withRetry(fn, options = {}) {
208
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
209
+ const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
210
+ let lastError;
211
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
212
+ return await fn();
213
+ } catch (err) {
214
+ lastError = err;
215
+ if (attempt < maxRetries) {
216
+ const delay = baseDelayMs * Math.pow(2, attempt);
217
+ await new Promise((resolve) => setTimeout(resolve, delay));
303
218
  }
304
- };
219
+ }
220
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
305
221
  }
306
222
  //#endregion
307
223
  //#region src/uploader.ts
308
224
  /**
309
- * AlfeSync uploader — upload changed files to S3 via presigned URLs.
225
+ * AlfeSync uploader — push changed files to S3.
310
226
  *
311
- * Flow per file:
312
- * 1. Request presigned PUT URL from API
313
- * 2. PUT file content directly to S3
314
- * 3. Notify API of completion (confirm endpoint)
315
- * 4. Update local manifest
227
+ * Per file:
228
+ * 1. Ask the agent API for a presigned PUT URL
229
+ * 2. PUT bytes directly to S3 (raw fetch — S3 isn't an Alfe API)
230
+ * 3. Tell the agent API the upload completed (`syncConfirmUpload`)
231
+ * 4. Update the local manifest
316
232
  *
317
- * Retries 3x with exponential backoff on transient failures.
233
+ * Each step retries with exponential backoff via `withRetry`.
318
234
  */
319
235
  const log$2 = (0, _auriclabs_logger.createLogger)("SyncUploader");
320
- const MAX_RETRIES$1 = 3;
321
- const BASE_DELAY_MS$1 = 1e3;
322
- /**
323
- * Determine the storage class based on file path.
324
- */
236
+ const ARCHIVE_PREFIXES = ["sessions/archive/", "context/archive/"];
325
237
  function getStorageClass(relativePath) {
326
- return relativePath.startsWith("sessions/archive/") || relativePath.startsWith("context/archive/") ? "GLACIER_IR" : "STANDARD";
238
+ return ARCHIVE_PREFIXES.some((p) => relativePath.startsWith(p)) ? "GLACIER_IR" : "STANDARD";
327
239
  }
328
- /**
329
- * Determine MIME type from file path.
330
- */
331
240
  function getContentType(relativePath) {
332
241
  if (relativePath.endsWith(".json")) return "application/json";
333
242
  if (relativePath.endsWith(".md")) return "text/markdown";
@@ -336,62 +245,63 @@ function getContentType(relativePath) {
336
245
  if (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml")) return "text/yaml";
337
246
  return "application/octet-stream";
338
247
  }
339
- /**
340
- * Upload a single file with retry logic.
341
- */
342
- async function uploadFileWithRetry(workspacePath, relativePath, client) {
248
+ async function uploadOne(workspacePath, relativePath, client) {
343
249
  const absolutePath = (0, node_path.join)(workspacePath, relativePath);
344
- let lastError;
345
- for (let attempt = 0; attempt <= MAX_RETRIES$1; attempt++) try {
346
- const [hash, fileStat] = await Promise.all([computeFileHash(absolutePath), (0, node_fs_promises.stat)(absolutePath)]);
347
- const size = fileStat.size;
348
- const storageClass = getStorageClass(relativePath);
349
- const contentType = getContentType(relativePath);
350
- const presigned = await client.presignPut(relativePath, contentType);
351
- const fileContent = await (0, node_fs_promises.readFile)(absolutePath);
352
- const putResponse = await fetch(presigned.url, {
353
- method: "PUT",
354
- headers: { "Content-Type": contentType },
355
- body: fileContent
356
- });
357
- if (!putResponse.ok) throw new Error(`S3 PUT failed (${String(putResponse.status)}): ${await putResponse.text()}`);
358
- await client.confirmUpload(relativePath, hash, size, storageClass);
359
- await updateManifestEntry(workspacePath, relativePath, {
360
- hash,
361
- size,
362
- lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
363
- storageClass
250
+ try {
251
+ return await withRetry(async () => {
252
+ const [hash, fileStat] = await Promise.all([computeFileHash(absolutePath), (0, node_fs_promises.stat)(absolutePath)]);
253
+ const size = fileStat.size;
254
+ const storageClass = getStorageClass(relativePath);
255
+ const contentType = getContentType(relativePath);
256
+ const url = (await client.syncPresign({ files: [{
257
+ path: relativePath,
258
+ operation: "put",
259
+ contentType
260
+ }] })).urls[0]?.url;
261
+ if (!url) throw new Error("No presigned URL returned");
262
+ const fileContent = await (0, node_fs_promises.readFile)(absolutePath);
263
+ const putResponse = await fetch(url, {
264
+ method: "PUT",
265
+ headers: { "Content-Type": contentType },
266
+ body: fileContent
267
+ });
268
+ if (!putResponse.ok) throw new Error(`S3 PUT failed (${String(putResponse.status)}): ${await putResponse.text()}`);
269
+ await client.syncConfirmUpload({
270
+ filePath: relativePath,
271
+ hash,
272
+ size,
273
+ storageClass
274
+ });
275
+ await updateManifestEntry(workspacePath, relativePath, {
276
+ hash,
277
+ size,
278
+ lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
279
+ storageClass
280
+ });
281
+ return {
282
+ path: relativePath,
283
+ success: true,
284
+ hash,
285
+ size
286
+ };
364
287
  });
288
+ } catch (err) {
365
289
  return {
366
290
  path: relativePath,
367
- success: true,
368
- hash,
369
- size
291
+ success: false,
292
+ error: err instanceof Error ? err.message : String(err)
370
293
  };
371
- } catch (err) {
372
- lastError = err instanceof Error ? err : new Error(String(err));
373
- if (attempt < MAX_RETRIES$1) {
374
- const delay = BASE_DELAY_MS$1 * Math.pow(2, attempt);
375
- await new Promise((resolve) => setTimeout(resolve, delay));
376
- }
377
294
  }
378
- return {
379
- path: relativePath,
380
- success: false,
381
- error: lastError?.message ?? "Unknown error"
382
- };
383
295
  }
384
296
  /**
385
- * Upload multiple files to S3.
386
- *
387
- * Uploads are performed in parallel with a concurrency limit.
297
+ * Upload many files, batched by `concurrency`.
388
298
  */
389
299
  async function uploadFiles(workspacePath, relativePaths, client, options = {}) {
390
300
  const { concurrency = 5, quiet = false } = options;
391
301
  const results = [];
392
302
  for (let i = 0; i < relativePaths.length; i += concurrency) {
393
303
  const batch = relativePaths.slice(i, i + concurrency);
394
- const batchResults = await Promise.all(batch.map((path) => uploadFileWithRetry(workspacePath, path, client)));
304
+ const batchResults = await Promise.all(batch.map((path) => uploadOne(workspacePath, path, client)));
395
305
  for (const result of batchResults) {
396
306
  results.push(result);
397
307
  if (!quiet) if (result.success) log$2.info(`Uploaded ${result.path} (${formatBytes$1(result.size ?? 0)})`);
@@ -408,65 +318,55 @@ function formatBytes$1(bytes) {
408
318
  //#endregion
409
319
  //#region src/downloader.ts
410
320
  /**
411
- * AlfeSync downloader — download files from S3 via presigned URLs.
321
+ * AlfeSync downloader — pull files from S3 to disk.
412
322
  *
413
- * Flow per file:
414
- * 1. Request presigned GET URL from API
415
- * 2. GET file content from S3
416
- * 3. Write to local disk
417
- * 4. Update local manifest
323
+ * Per file:
324
+ * 1. Ask the agent API for a presigned GET URL
325
+ * 2. GET bytes directly from S3 (raw fetch — S3 isn't an Alfe API)
326
+ * 3. Write to disk
327
+ * 4. Update the local manifest
418
328
  */
419
329
  const log$1 = (0, _auriclabs_logger.createLogger)("SyncDownloader");
420
- const MAX_RETRIES = 3;
421
- const BASE_DELAY_MS = 1e3;
422
- /**
423
- * Download a single file with retry logic.
424
- */
425
- async function downloadFileWithRetry(workspacePath, relativePath, client, remoteEntry) {
426
- let lastError;
427
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) try {
428
- const presigned = await client.presignGet(relativePath);
429
- const response = await fetch(presigned.url);
430
- if (!response.ok) throw new Error(`S3 GET failed (${String(response.status)}): ${await response.text()}`);
431
- const buffer = Buffer.from(await response.arrayBuffer());
432
- const absolutePath = (0, node_path.join)(workspacePath, relativePath);
433
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(absolutePath), { recursive: true });
434
- await (0, node_fs_promises.writeFile)(absolutePath, buffer);
435
- await updateManifestEntry(workspacePath, relativePath, {
436
- hash: remoteEntry?.hash ?? "",
437
- size: buffer.length,
438
- lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
439
- storageClass: remoteEntry?.storageClass ?? "STANDARD"
330
+ async function downloadOne(workspacePath, relativePath, client, remoteEntry) {
331
+ try {
332
+ return await withRetry(async () => {
333
+ const url = (await client.syncPresign({ files: [{
334
+ path: relativePath,
335
+ operation: "get"
336
+ }] })).urls[0]?.url;
337
+ if (!url) throw new Error("No presigned URL returned");
338
+ const response = await fetch(url);
339
+ if (!response.ok) throw new Error(`S3 GET failed (${String(response.status)}): ${await response.text()}`);
340
+ const buffer = Buffer.from(await response.arrayBuffer());
341
+ const absolutePath = (0, node_path.join)(workspacePath, relativePath);
342
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(absolutePath), { recursive: true });
343
+ await (0, node_fs_promises.writeFile)(absolutePath, buffer);
344
+ await updateManifestEntry(workspacePath, relativePath, {
345
+ hash: remoteEntry?.hash ?? "",
346
+ size: buffer.length,
347
+ lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
348
+ storageClass: remoteEntry?.storageClass ?? "STANDARD"
349
+ });
350
+ return {
351
+ path: relativePath,
352
+ success: true,
353
+ size: buffer.length
354
+ };
440
355
  });
356
+ } catch (err) {
441
357
  return {
442
358
  path: relativePath,
443
- success: true,
444
- size: buffer.length
359
+ success: false,
360
+ error: err instanceof Error ? err.message : String(err)
445
361
  };
446
- } catch (err) {
447
- lastError = err instanceof Error ? err : new Error(String(err));
448
- if (attempt < MAX_RETRIES) {
449
- const delay = BASE_DELAY_MS * Math.pow(2, attempt);
450
- await new Promise((resolve) => setTimeout(resolve, delay));
451
- }
452
362
  }
453
- return {
454
- path: relativePath,
455
- success: false,
456
- error: lastError?.message ?? "Unknown error"
457
- };
458
363
  }
459
- /**
460
- * Download multiple files from S3.
461
- *
462
- * Downloads are performed in parallel with a concurrency limit.
463
- */
464
364
  async function downloadFiles(workspacePath, relativePaths, client, remoteManifest, options = {}) {
465
365
  const { concurrency = 5, quiet = false } = options;
466
366
  const results = [];
467
367
  for (let i = 0; i < relativePaths.length; i += concurrency) {
468
368
  const batch = relativePaths.slice(i, i + concurrency);
469
- const batchResults = await Promise.all(batch.map((path) => downloadFileWithRetry(workspacePath, path, client, remoteManifest?.files[path])));
369
+ const batchResults = await Promise.all(batch.map((path) => downloadOne(workspacePath, path, client, remoteManifest?.files[path])));
470
370
  for (const result of batchResults) {
471
371
  results.push(result);
472
372
  if (!quiet) if (result.success) log$1.info(`Downloaded ${result.path} (${formatBytes(result.size ?? 0)})`);
@@ -483,38 +383,31 @@ function formatBytes(bytes) {
483
383
  //#endregion
484
384
  //#region src/sync-engine.ts
485
385
  /**
486
- * AlfeSync engine — orchestrates push, pull, and full sync operations.
386
+ * AlfeSync engine — orchestrates push, pull, and full sync.
487
387
  *
488
- * Handles:
489
388
  * - push(paths[]): upload changed files to S3
490
389
  * - pull(): download files newer on remote
491
390
  * - fullSync(): bidirectional sync with conflict detection
492
391
  *
493
- * Conflict resolution: if remote file is newer than local manifest entry
494
- * AND local file has changed write `.conflict-{timestamp}` alongside original.
392
+ * Conflict resolution: when a remote file is newer than the local manifest
393
+ * entry AND the local file has also changed, the local copy is renamed to
394
+ * `<name>.conflict-<timestamp>.<ext>` and the remote version wins.
495
395
  */
496
396
  const log = (0, _auriclabs_logger.createLogger)("SyncEngine");
497
397
  /**
498
- * Create a sync engine. Workspace path defaults to `config.workspacePath`
499
- * from the resolved Alfe config, but can be overridden (e.g. for tests).
398
+ * Construct a sync engine bound to a workspace + agent API client.
399
+ *
400
+ * The client is constructed once in plugin.ts (or the CLI) and passed in,
401
+ * so credentials never leak into multiple places.
500
402
  */
501
- async function createSyncEngine(workspacePathOverride) {
502
- const config = await requireConfig();
503
- const workspacePath = workspacePathOverride ?? config.workspacePath;
504
- const client = createApiClient({
505
- apiUrl: config.apiUrl,
506
- token: config.token,
507
- agentId: config.agentId
508
- });
403
+ function createSyncEngine({ workspacePath, client }) {
509
404
  return {
510
- config,
405
+ workspacePath,
511
406
  client,
512
407
  async push(paths, options = {}) {
513
408
  const { quiet = false, filter } = options;
514
- const ignorePatterns = await require_ignore.loadIgnorePatterns(workspacePath);
515
- let filesToPush;
516
- if (paths && paths.length > 0) filesToPush = require_ignore.filterIgnored(paths, ignorePatterns);
517
- else filesToPush = await detectLocalChanges(workspacePath, ignorePatterns);
409
+ const ignorePatterns = await loadIgnorePatterns(workspacePath);
410
+ let filesToPush = paths && paths.length > 0 ? filterIgnored(paths, ignorePatterns) : await detectLocalChanges(workspacePath, ignorePatterns);
518
411
  if (filter) filesToPush = filesToPush.filter((p) => p.startsWith(filter));
519
412
  if (filesToPush.length === 0) {
520
413
  if (!quiet) log.info("Nothing to push");
@@ -539,10 +432,9 @@ async function createSyncEngine(workspacePathOverride) {
539
432
  },
540
433
  async pull(options = {}) {
541
434
  const { quiet = false } = options;
542
- const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.getManifest()]);
435
+ const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
543
436
  const diff = diffManifests(localManifest, remoteManifest);
544
- const filesToPull = [...diff.toPull];
545
- if (filesToPull.length === 0) {
437
+ if (diff.toPull.length === 0) {
546
438
  if (!quiet) log.info("Nothing to pull");
547
439
  return {
548
440
  pushed: 0,
@@ -551,8 +443,8 @@ async function createSyncEngine(workspacePathOverride) {
551
443
  errors: 0
552
444
  };
553
445
  }
554
- if (!quiet) log.info(`Pulling ${String(filesToPull.length)} file(s)`);
555
- const results = await downloadFiles(workspacePath, filesToPull, client, remoteManifest, { quiet });
446
+ if (!quiet) log.info(`Pulling ${String(diff.toPull.length)} file(s)`);
447
+ const results = await downloadFiles(workspacePath, [...diff.toPull], client, remoteManifest, { quiet });
556
448
  const pulled = results.filter((r) => r.success).length;
557
449
  const errors = results.filter((r) => !r.success).length;
558
450
  if (!quiet) log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);
@@ -565,8 +457,8 @@ async function createSyncEngine(workspacePathOverride) {
565
457
  },
566
458
  async fullSync(options = {}) {
567
459
  const { quiet = false } = options;
568
- const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.getManifest()]);
569
- const ignorePatterns = await require_ignore.loadIgnorePatterns(workspacePath);
460
+ const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
461
+ const ignorePatterns = await loadIgnorePatterns(workspacePath);
570
462
  const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);
571
463
  const diff = diffManifests(localManifest, remoteManifest);
572
464
  const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));
@@ -574,17 +466,16 @@ async function createSyncEngine(workspacePathOverride) {
574
466
  let conflictCount = 0;
575
467
  for (const conflictPath of trueConflicts) {
576
468
  const absolutePath = (0, node_path.join)(workspacePath, conflictPath);
577
- if ((0, node_fs.existsSync)(absolutePath)) {
578
- const ext = (0, node_path.extname)(conflictPath);
579
- const base = (0, node_path.basename)(conflictPath, ext);
580
- const dir = (0, node_path.dirname)(conflictPath);
581
- const conflictName = `${base}.conflict-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}${ext}`;
582
- await (0, node_fs_promises.writeFile)((0, node_path.join)(workspacePath, dir, conflictName), await (0, node_fs_promises.readFile)(absolutePath));
583
- if (!quiet) log.warn(`Conflict: ${conflictPath} — saved as ${conflictName}`);
584
- conflictCount++;
585
- }
469
+ if (!(0, node_fs.existsSync)(absolutePath)) continue;
470
+ const ext = (0, node_path.extname)(conflictPath);
471
+ const base = (0, node_path.basename)(conflictPath, ext);
472
+ const dir = (0, node_path.dirname)(conflictPath);
473
+ const conflictName = `${base}.conflict-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}${ext}`;
474
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(workspacePath, dir, conflictName), await (0, node_fs_promises.readFile)(absolutePath));
475
+ if (!quiet) log.warn(`Conflict: ${conflictPath} — saved as ${conflictName}`);
476
+ conflictCount++;
586
477
  }
587
- const filesToPush = require_ignore.filterIgnored([...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))], ignorePatterns);
478
+ const filesToPush = filterIgnored([...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))], ignorePatterns);
588
479
  const filesToPull = [
589
480
  ...diff.toPull,
590
481
  ...remoteOnlyChanges,
@@ -627,13 +518,12 @@ async function createSyncEngine(workspacePathOverride) {
627
518
  conflicts: 0,
628
519
  errors: 0
629
520
  };
630
- const remoteManifest = await client.getManifest();
521
+ const remoteManifest = await client.syncGetManifest();
631
522
  const localManifest = await readManifest(workspacePath);
632
523
  const filesToPull = paths.filter((p) => {
633
524
  if (!(p in remoteManifest.files)) return false;
634
- const remoteEntry = remoteManifest.files[p];
635
525
  if (!(p in localManifest.files)) return true;
636
- return localManifest.files[p].hash !== remoteEntry.hash;
526
+ return localManifest.files[p].hash !== remoteManifest.files[p].hash;
637
527
  });
638
528
  if (filesToPull.length === 0) {
639
529
  if (!quiet) log.info("All notified files already in sync");
@@ -673,10 +563,8 @@ async function createSyncEngine(workspacePathOverride) {
673
563
  };
674
564
  }
675
565
  /**
676
- * Detect files that have changed locally since last sync.
677
- *
678
- * Compares current file hashes against the local manifest.
679
- * Returns list of relative paths that need pushing.
566
+ * Walk the workspace and return paths whose hash differs from the manifest
567
+ * (or that are missing from it entirely).
680
568
  */
681
569
  async function detectLocalChanges(workspacePath, ignorePatterns) {
682
570
  const manifest = await readManifest(workspacePath);
@@ -689,43 +577,37 @@ async function detectLocalChanges(workspacePath, ignorePatterns) {
689
577
  const relativePath = fullPath.slice(workspacePath.length + 1).replace(/\\/g, "/");
690
578
  if (shouldSkipDir(entry.name)) continue;
691
579
  if (entry.isDirectory()) {
692
- if (!ignorePatterns.some((p) => {
693
- if (p.endsWith("/**")) return relativePath.startsWith(p.slice(0, -3));
694
- return false;
695
- })) await walk(fullPath);
580
+ if (!ignorePatterns.some((p) => p.endsWith("/**") && relativePath.startsWith(p.slice(0, -3)))) await walk(fullPath);
696
581
  } else if (entry.isFile()) {
697
- const { shouldIgnore } = await Promise.resolve().then(() => require("./ignore.cjs")).then((n) => n.ignore_exports);
698
582
  if (shouldIgnore(relativePath, ignorePatterns)) continue;
699
- if (!(relativePath in manifest.files)) changed.push(relativePath);
700
- else {
701
- const manifestEntry = manifest.files[relativePath];
702
- try {
703
- if (await computeFileHash(fullPath) !== manifestEntry.hash) changed.push(relativePath);
704
- } catch {}
583
+ if (!(relativePath in manifest.files)) {
584
+ changed.push(relativePath);
585
+ continue;
705
586
  }
587
+ try {
588
+ if (await computeFileHash(fullPath) !== manifest.files[relativePath].hash) changed.push(relativePath);
589
+ } catch {}
706
590
  }
707
591
  }
708
592
  }
709
593
  await walk(workspacePath);
710
594
  return changed;
711
595
  }
712
- /**
713
- * Directories to always skip during walks.
714
- */
596
+ /** Directories the engine never descends into. */
715
597
  function shouldSkipDir(name) {
716
598
  return name === "node_modules" || name === ".git" || name === ".sst" || name === ".build" || name === "dist";
717
599
  }
718
600
  //#endregion
719
- Object.defineProperty(exports, "computeFileHash", {
601
+ Object.defineProperty(exports, "__toESM", {
720
602
  enumerable: true,
721
603
  get: function() {
722
- return computeFileHash;
604
+ return __toESM;
723
605
  }
724
606
  });
725
- Object.defineProperty(exports, "createApiClient", {
607
+ Object.defineProperty(exports, "computeFileHash", {
726
608
  enumerable: true,
727
609
  get: function() {
728
- return createApiClient;
610
+ return computeFileHash;
729
611
  }
730
612
  });
731
613
  Object.defineProperty(exports, "createSyncEngine", {
@@ -746,16 +628,16 @@ Object.defineProperty(exports, "downloadFiles", {
746
628
  return downloadFiles;
747
629
  }
748
630
  });
749
- Object.defineProperty(exports, "invalidateSyncConfigCache", {
631
+ Object.defineProperty(exports, "filterIgnored", {
750
632
  enumerable: true,
751
633
  get: function() {
752
- return invalidateSyncConfigCache;
634
+ return filterIgnored;
753
635
  }
754
636
  });
755
- Object.defineProperty(exports, "isInitialized", {
637
+ Object.defineProperty(exports, "loadIgnorePatterns", {
756
638
  enumerable: true,
757
639
  get: function() {
758
- return isInitialized;
640
+ return loadIgnorePatterns;
759
641
  }
760
642
  });
761
643
  Object.defineProperty(exports, "readManifest", {
@@ -770,22 +652,10 @@ Object.defineProperty(exports, "removeManifestEntry", {
770
652
  return removeManifestEntry;
771
653
  }
772
654
  });
773
- Object.defineProperty(exports, "requireConfig", {
774
- enumerable: true,
775
- get: function() {
776
- return requireConfig;
777
- }
778
- });
779
- Object.defineProperty(exports, "resolveSyncConfig", {
780
- enumerable: true,
781
- get: function() {
782
- return resolveSyncConfig;
783
- }
784
- });
785
- Object.defineProperty(exports, "syncStateDir", {
655
+ Object.defineProperty(exports, "shouldIgnore", {
786
656
  enumerable: true,
787
657
  get: function() {
788
- return syncStateDir;
658
+ return shouldIgnore;
789
659
  }
790
660
  });
791
661
  Object.defineProperty(exports, "updateManifestEntry", {
@@ -800,6 +670,12 @@ Object.defineProperty(exports, "uploadFiles", {
800
670
  return uploadFiles;
801
671
  }
802
672
  });
673
+ Object.defineProperty(exports, "withRetry", {
674
+ enumerable: true,
675
+ get: function() {
676
+ return withRetry;
677
+ }
678
+ });
803
679
  Object.defineProperty(exports, "writeManifest", {
804
680
  enumerable: true,
805
681
  get: function() {