autohand-cli 0.7.6 → 0.7.8

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.
Files changed (135) hide show
  1. package/dist/{AutomodeManager-TJSW2SQY.cjs → AutomodeManager-HV6M7EAX.cjs} +61 -22
  2. package/dist/{AutomodeManager-WIMHLG4W.js → AutomodeManager-YVCJXOMQ.js} +61 -22
  3. package/dist/CommunitySkillsCache-2BITCEAA.cjs +7 -0
  4. package/dist/{CommunitySkillsCache-XPDVYU3K.js → CommunitySkillsCache-Q22FUAR5.js} +2 -2
  5. package/dist/HookManager-X47HCM5G.cjs +6 -0
  6. package/dist/{HookManager-VIX56KFU.js → HookManager-ZXKHCD7U.js} +1 -1
  7. package/dist/MemoryManager-2ATHG7BH.js +7 -0
  8. package/dist/MemoryManager-AENCGCEW.cjs +7 -0
  9. package/dist/PermissionManager-6HZGTK2N.cjs +10 -0
  10. package/dist/{PermissionManager-YFZI4ZZ6.js → PermissionManager-HATZKTRC.js} +3 -3
  11. package/dist/SessionManager-AG4WT3DP.cjs +9 -0
  12. package/dist/{SessionManager-XDBEQUPG.js → SessionManager-S5R6O3NU.js} +2 -2
  13. package/dist/{SkillsRegistry-7NICF6FY.js → SkillsRegistry-R5WDM6T3.js} +2 -2
  14. package/dist/SkillsRegistry-ZXU6YDRP.cjs +8 -0
  15. package/dist/SyncApiClient-FAOMIZAP.js +10 -0
  16. package/dist/SyncApiClient-UOA4VLLD.cjs +10 -0
  17. package/dist/add-dir-OMK3Y4DM.cjs +8 -0
  18. package/dist/add-dir-PNU7AGKO.js +8 -0
  19. package/dist/{agents-B33IAATH.js → agents-ICAC3KD3.js} +2 -2
  20. package/dist/agents-YONWPKFS.cjs +9 -0
  21. package/dist/agents-new-NV557UVG.cjs +10 -0
  22. package/dist/{agents-new-KTXJFC5E.js → agents-new-QHM3CO4B.js} +2 -2
  23. package/dist/{chunk-MFLRXVKU.js → chunk-23JQSCTO.js} +1 -1
  24. package/dist/chunk-2JPUEN44.cjs +299 -0
  25. package/dist/{chunk-5PD2L6WI.js → chunk-3YEDXG6S.js} +1 -1
  26. package/dist/{chunk-3ZUWWML7.cjs → chunk-4M2GX7RH.cjs} +2 -2
  27. package/dist/{chunk-R5KNHJ27.js → chunk-4RWTUT2Z.js} +1 -1
  28. package/dist/{chunk-CHQMK2ZG.js → chunk-52MLYK5P.js} +1 -1
  29. package/dist/{chunk-CVYEUA3D.cjs → chunk-53BR4MUW.cjs} +3 -3
  30. package/dist/{chunk-JYTXG6OV.cjs → chunk-54GVL2SE.cjs} +4 -2
  31. package/dist/{chunk-5WKR4HIB.js → chunk-5DN5KNXU.js} +1 -1
  32. package/dist/{chunk-K6NBYSME.cjs → chunk-5MDDOGTD.cjs} +6 -6
  33. package/dist/{chunk-5MCDN53U.js → chunk-6KMAJTU3.js} +4 -2
  34. package/dist/{chunk-3CO5R6M2.cjs → chunk-7TOHYAUF.cjs} +2 -2
  35. package/dist/{chunk-67NJKV5A.cjs → chunk-7VW3A7DO.cjs} +2 -2
  36. package/dist/{chunk-2W3QTBNG.cjs → chunk-A552JHUJ.cjs} +2 -2
  37. package/dist/{chunk-NGSLABLS.js → chunk-A6QBABQ7.js} +1 -1
  38. package/dist/chunk-ARVFUZOB.js +736 -0
  39. package/dist/{chunk-VO3JKFUH.js → chunk-AVL4DKQO.js} +1 -1
  40. package/dist/{chunk-CT2VTDPQ.cjs → chunk-B4ZPNXZE.cjs} +1 -1
  41. package/dist/{chunk-OKMYLMCR.cjs → chunk-B7EUETGY.cjs} +4 -4
  42. package/dist/chunk-C26EN22G.cjs +328 -0
  43. package/dist/chunk-DOTAX65F.js +328 -0
  44. package/dist/{chunk-4KZCGK7D.js → chunk-DPJ3IIBB.js} +1 -1
  45. package/dist/{chunk-SKT2CRNY.cjs → chunk-DSKVMFRM.cjs} +56 -8
  46. package/dist/{chunk-723DZKBU.js → chunk-EDGV5BNH.js} +2 -2
  47. package/dist/{chunk-FUEL6BK7.js → chunk-EKY5PKQI.js} +15 -0
  48. package/dist/{chunk-YMP7AGNT.js → chunk-G77ZY4QG.js} +1 -1
  49. package/dist/{chunk-KN5C4TR4.cjs → chunk-GDTZQSJ6.cjs} +2 -2
  50. package/dist/{chunk-536VWSZK.cjs → chunk-GFJ6AETU.cjs} +4 -4
  51. package/dist/{chunk-PVM5I5WI.js → chunk-GWIAMKKF.js} +1 -1
  52. package/dist/{chunk-XAM7SFVB.cjs → chunk-GWXXFQ3F.cjs} +2 -2
  53. package/dist/{chunk-REPKBECD.cjs → chunk-JHFH3N4U.cjs} +2 -2
  54. package/dist/{chunk-4L5WYXHN.js → chunk-KH7BCZJN.js} +1 -1
  55. package/dist/{chunk-JXOXZTMA.js → chunk-L5ZFPWHY.js} +54 -6
  56. package/dist/chunk-MDWULS57.js +288 -0
  57. package/dist/{chunk-6LP2GO5C.js → chunk-MJFBVQHB.js} +2 -2
  58. package/dist/{chunk-MWLAHCU7.js → chunk-NI3BQXKU.js} +1 -1
  59. package/dist/{chunk-SKU4M27Z.js → chunk-OBV3UUIL.js} +1 -1
  60. package/dist/{chunk-27ISZOFA.js → chunk-P2Z6GDEN.js} +1 -1
  61. package/dist/{chunk-XTHHDIBG.cjs → chunk-PMMSDR44.cjs} +16 -1
  62. package/dist/{chunk-URY4AS4L.cjs → chunk-PU534KPO.cjs} +4 -4
  63. package/dist/chunk-SFGJQPGC.cjs +288 -0
  64. package/dist/{chunk-53YDUYNS.cjs → chunk-SLISYSP4.cjs} +2 -2
  65. package/dist/{chunk-2E2COWKB.cjs → chunk-SYJLMBLP.cjs} +66 -10
  66. package/dist/chunk-U5WIP4HS.js +674 -0
  67. package/dist/{chunk-7HB7GSQF.js → chunk-UL7YPRCU.js} +1 -1
  68. package/dist/{chunk-LUKMRIKJ.cjs → chunk-VEDIYPWY.cjs} +2 -2
  69. package/dist/{chunk-C2NFLFHH.js → chunk-VPAN5H7Q.js} +1 -1
  70. package/dist/chunk-WH3D42BQ.js +299 -0
  71. package/dist/{chunk-2FSQPRPJ.js → chunk-WIUGUR5T.js} +59 -3
  72. package/dist/{chunk-QMVTT55Y.cjs → chunk-WQSWU2QA.cjs} +4 -4
  73. package/dist/chunk-XFPITUFJ.cjs +674 -0
  74. package/dist/chunk-XFQS2VGT.cjs +736 -0
  75. package/dist/{chunk-HYTYXN2G.cjs → chunk-YAGD43KA.cjs} +10 -10
  76. package/dist/{chunk-KJ67C72C.cjs → chunk-YDOR2OCA.cjs} +2 -2
  77. package/dist/constants-G2PLP5HH.cjs +20 -0
  78. package/dist/{constants-QYBEF3DB.js → constants-ZLG6M5SI.js} +3 -1
  79. package/dist/{defaultHooks-3G3DVF6I.js → defaultHooks-R56VYG7I.js} +315 -1
  80. package/dist/{defaultHooks-Z4KA6U5C.cjs → defaultHooks-WLMRQUXG.cjs} +315 -1
  81. package/dist/{feedback-PZ2PINDU.js → feedback-HZBCTSFG.js} +2 -2
  82. package/dist/feedback-JBQ3UPGZ.cjs +10 -0
  83. package/dist/index.cjs +897 -600
  84. package/dist/index.js +1438 -1141
  85. package/dist/language-KODBDE5R.js +12 -0
  86. package/dist/language-SJT475NW.cjs +12 -0
  87. package/dist/localProjectPermissions-AYQYGTOE.cjs +17 -0
  88. package/dist/{localProjectPermissions-DURCNDZG.js → localProjectPermissions-YFFAKLUZ.js} +2 -2
  89. package/dist/login-TC2KROQI.js +14 -0
  90. package/dist/login-TYMR2ZD3.cjs +14 -0
  91. package/dist/logout-2ECV365P.js +12 -0
  92. package/dist/logout-CO3CPYZJ.cjs +12 -0
  93. package/dist/resume-EPOEF3WV.cjs +9 -0
  94. package/dist/{resume-CWYAK6XR.js → resume-LOYD5MMP.js} +2 -2
  95. package/dist/share-APR5S2CS.cjs +10 -0
  96. package/dist/share-VLJFDZKR.js +10 -0
  97. package/dist/{skills-CRFOVWEQ.js → skills-3YEEODHK.js} +1 -1
  98. package/dist/skills-CRM55MKM.cjs +12 -0
  99. package/dist/{skills-install-Z27KPEGF.cjs → skills-install-FTGOHOZ4.cjs} +5 -5
  100. package/dist/{skills-install-RMPXN6RK.js → skills-install-KAXAQSN6.js} +2 -2
  101. package/dist/skills-new-JF4FKNUT.cjs +11 -0
  102. package/dist/{skills-new-S2YPO635.js → skills-new-JYX2GBKM.js} +2 -2
  103. package/dist/status-DAEFE7ZC.cjs +9 -0
  104. package/dist/{status-GPAZ67ZZ.js → status-PBFFUC4Q.js} +2 -2
  105. package/dist/sync-3B7SNBYC.js +14 -0
  106. package/dist/sync-4RARBQIH.cjs +39 -0
  107. package/dist/sync-H4UHHLKU.js +39 -0
  108. package/dist/sync-YZ6YZ42H.cjs +14 -0
  109. package/dist/theme-3XV5BWUB.js +12 -0
  110. package/dist/theme-Z2WS5XWZ.cjs +12 -0
  111. package/package.json +4 -2
  112. package/dist/CommunitySkillsCache-X3X237QQ.cjs +0 -7
  113. package/dist/HookManager-EOMUXKJ4.cjs +0 -6
  114. package/dist/MemoryManager-UVHILGV5.js +0 -7
  115. package/dist/MemoryManager-WO3KUZVA.cjs +0 -7
  116. package/dist/PermissionManager-PMTQN263.cjs +0 -10
  117. package/dist/SessionManager-M5ZLCLCW.cjs +0 -9
  118. package/dist/SkillsRegistry-OINIPILA.cjs +0 -8
  119. package/dist/agents-GRAFXZY3.cjs +0 -9
  120. package/dist/agents-new-67NJJSDA.cjs +0 -10
  121. package/dist/constants-PE5DLI7Q.cjs +0 -18
  122. package/dist/feedback-R66B3B3C.cjs +0 -10
  123. package/dist/localProjectPermissions-75X3ZGKH.cjs +0 -17
  124. package/dist/login-NYWZRZO5.js +0 -12
  125. package/dist/login-QNJ5C42G.cjs +0 -12
  126. package/dist/logout-MBS7L3ZW.js +0 -12
  127. package/dist/logout-MVUP7GPU.cjs +0 -12
  128. package/dist/resume-ANISKRWL.cjs +0 -9
  129. package/dist/share-3PSV53CQ.js +0 -10
  130. package/dist/share-4ACH6626.cjs +0 -10
  131. package/dist/skills-6PIGHOWS.cjs +0 -12
  132. package/dist/skills-new-3QJUST7P.cjs +0 -11
  133. package/dist/status-4U5CPUVT.cjs +0 -9
  134. package/dist/theme-CVY6MVEK.cjs +0 -12
  135. package/dist/theme-CY7WF4M6.js +0 -12
@@ -0,0 +1,674 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
2
+
3
+ var _chunkSFGJQPGCcjs = require('./chunk-SFGJQPGC.cjs');
4
+
5
+
6
+ var _chunkPMMSDR44cjs = require('./chunk-PMMSDR44.cjs');
7
+
8
+ // src/sync/types.ts
9
+ var DEFAULT_SYNC_CONFIG = {
10
+ enabled: true,
11
+ interval: 5 * 60 * 1e3,
12
+ // 5 minutes
13
+ includeTelemetry: false,
14
+ includeFeedback: false
15
+ };
16
+ var SYNC_EXCLUDE_ALWAYS = [
17
+ "device-id",
18
+ "error.log",
19
+ "feedback.log",
20
+ "version-*.json",
21
+ ".sync-lock",
22
+ ".sync-state.json"
23
+ ];
24
+ var SYNC_CONSENT_REQUIRED = {
25
+ telemetry: "telemetry/",
26
+ feedback: "feedback/"
27
+ };
28
+ var SYNC_INCLUDE_DEFAULT = [
29
+ "config.json",
30
+ "agents/",
31
+ "community-skills/",
32
+ "hooks/",
33
+ "memory/",
34
+ "projects/",
35
+ "sessions/",
36
+ "share/",
37
+ "skills/"
38
+ ];
39
+
40
+ // src/sync/encryption.ts
41
+ var _crypto = require('crypto'); var _crypto2 = _interopRequireDefault(_crypto);
42
+ var ALGORITHM = "aes-256-gcm";
43
+ var KEY_LENGTH = 32;
44
+ var IV_LENGTH = 16;
45
+ var AUTH_TAG_LENGTH = 16;
46
+ var SALT = "autohand-sync-v1";
47
+ var ITERATIONS = 1e5;
48
+ function deriveKey(authToken) {
49
+ if (!authToken || authToken.length < 10) {
50
+ throw new Error("Invalid auth token for key derivation");
51
+ }
52
+ return _crypto2.default.pbkdf2Sync(authToken, SALT, ITERATIONS, KEY_LENGTH, "sha256");
53
+ }
54
+ function encrypt(plaintext, authToken) {
55
+ if (!plaintext) {
56
+ return plaintext;
57
+ }
58
+ const key = deriveKey(authToken);
59
+ const iv = _crypto2.default.randomBytes(IV_LENGTH);
60
+ const cipher = _crypto2.default.createCipheriv(ALGORITHM, key, iv);
61
+ let ciphertext = cipher.update(plaintext, "utf8", "base64");
62
+ ciphertext += cipher.final("base64");
63
+ const authTag = cipher.getAuthTag();
64
+ return `${iv.toString("base64")}:${authTag.toString("base64")}:${ciphertext}`;
65
+ }
66
+ function decrypt(encrypted, authToken) {
67
+ if (!encrypted || !encrypted.includes(":")) {
68
+ throw new Error("Invalid encrypted format");
69
+ }
70
+ const parts = encrypted.split(":");
71
+ if (parts.length !== 3) {
72
+ throw new Error("Invalid encrypted format: expected iv:authTag:ciphertext");
73
+ }
74
+ const [ivB64, authTagB64, ciphertext] = parts;
75
+ const key = deriveKey(authToken);
76
+ const iv = Buffer.from(ivB64, "base64");
77
+ const authTag = Buffer.from(authTagB64, "base64");
78
+ if (iv.length !== IV_LENGTH) {
79
+ throw new Error("Invalid IV length");
80
+ }
81
+ if (authTag.length !== AUTH_TAG_LENGTH) {
82
+ throw new Error("Invalid auth tag length");
83
+ }
84
+ const decipher = _crypto2.default.createDecipheriv(ALGORITHM, key, iv);
85
+ decipher.setAuthTag(authTag);
86
+ let plaintext = decipher.update(ciphertext, "base64", "utf8");
87
+ plaintext += decipher.final("utf8");
88
+ return plaintext;
89
+ }
90
+ function isEncrypted(value) {
91
+ if (!value || typeof value !== "string") {
92
+ return false;
93
+ }
94
+ const parts = value.split(":");
95
+ if (parts.length !== 3) {
96
+ return false;
97
+ }
98
+ try {
99
+ const iv = Buffer.from(parts[0], "base64");
100
+ const authTag = Buffer.from(parts[1], "base64");
101
+ return iv.length === IV_LENGTH && authTag.length === AUTH_TAG_LENGTH;
102
+ } catch (e) {
103
+ return false;
104
+ }
105
+ }
106
+ function encryptConfig(config, authToken) {
107
+ const result = {};
108
+ for (const [key, value] of Object.entries(config)) {
109
+ if (value === null || value === void 0) {
110
+ result[key] = value;
111
+ continue;
112
+ }
113
+ if (typeof value === "object" && !Array.isArray(value)) {
114
+ result[key] = encryptConfig(value, authToken);
115
+ continue;
116
+ }
117
+ if (typeof value === "string" && isSensitiveKey(key) && value.length > 0 && !isEncrypted(value)) {
118
+ result[key] = encrypt(value, authToken);
119
+ } else {
120
+ result[key] = value;
121
+ }
122
+ }
123
+ return result;
124
+ }
125
+ function decryptConfig(config, authToken) {
126
+ const result = {};
127
+ for (const [key, value] of Object.entries(config)) {
128
+ if (value === null || value === void 0) {
129
+ result[key] = value;
130
+ continue;
131
+ }
132
+ if (typeof value === "object" && !Array.isArray(value)) {
133
+ result[key] = decryptConfig(value, authToken);
134
+ continue;
135
+ }
136
+ if (typeof value === "string" && isSensitiveKey(key) && isEncrypted(value)) {
137
+ try {
138
+ result[key] = decrypt(value, authToken);
139
+ } catch (e2) {
140
+ result[key] = value;
141
+ }
142
+ } else {
143
+ result[key] = value;
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+ function isSensitiveKey(key) {
149
+ const lowerKey = key.toLowerCase();
150
+ return lowerKey === "apikey" || lowerKey.endsWith("key") || lowerKey.endsWith("token") || lowerKey.endsWith("secret") || lowerKey === "password";
151
+ }
152
+ function computeHash(data) {
153
+ return _crypto2.default.createHash("sha256").update(data).digest("hex");
154
+ }
155
+
156
+ // src/sync/SyncService.ts
157
+ var _fsextra = require('fs-extra'); var _fsextra2 = _interopRequireDefault(_fsextra);
158
+ var _path = require('path'); var _path2 = _interopRequireDefault(_path);
159
+ var MANIFEST_VERSION = 1;
160
+ var SYNC_STATE_FILE = ".sync-state.json";
161
+ var SYNC_LOCK_FILE = ".sync-lock";
162
+ var MAX_TOTAL_SIZE = 100 * 1024 * 1024;
163
+ var SyncService = class {
164
+ constructor(options) {
165
+ this.timer = null;
166
+ this.syncing = false;
167
+ this.started = false;
168
+ this.authToken = options.authToken;
169
+ this.userId = options.userId;
170
+ this.config = options.config;
171
+ this.client = options.apiClient || _chunkSFGJQPGCcjs.getSyncApiClient.call(void 0, );
172
+ this.onEvent = options.onEvent || (() => {
173
+ });
174
+ this.basePath = _chunkPMMSDR44cjs.AUTOHAND_HOME;
175
+ }
176
+ /**
177
+ * Start the background sync timer
178
+ */
179
+ start() {
180
+ if (this.started) return;
181
+ this.started = true;
182
+ this.sync().catch(() => {
183
+ });
184
+ this.timer = setInterval(() => {
185
+ this.sync().catch(() => {
186
+ });
187
+ }, this.config.interval);
188
+ }
189
+ /**
190
+ * Stop the background sync timer
191
+ */
192
+ stop() {
193
+ if (this.timer) {
194
+ clearInterval(this.timer);
195
+ this.timer = null;
196
+ }
197
+ this.started = false;
198
+ }
199
+ /**
200
+ * Check if the service is running
201
+ */
202
+ get isRunning() {
203
+ return this.started;
204
+ }
205
+ /**
206
+ * Perform a sync operation
207
+ */
208
+ async sync() {
209
+ if (this.syncing) {
210
+ return {
211
+ success: false,
212
+ uploaded: 0,
213
+ downloaded: 0,
214
+ conflicts: 0,
215
+ error: "Sync already in progress"
216
+ };
217
+ }
218
+ const lockPath = _path2.default.join(this.basePath, SYNC_LOCK_FILE);
219
+ if (await _fsextra2.default.pathExists(lockPath)) {
220
+ const lockContent = await _fsextra2.default.readFile(lockPath, "utf8").catch(() => "");
221
+ const lockAge = Date.now() - parseInt(lockContent, 10);
222
+ if (lockAge < 5 * 60 * 1e3) {
223
+ return {
224
+ success: false,
225
+ uploaded: 0,
226
+ downloaded: 0,
227
+ conflicts: 0,
228
+ error: "Sync locked by another process"
229
+ };
230
+ }
231
+ await _fsextra2.default.remove(lockPath);
232
+ }
233
+ this.syncing = true;
234
+ const startTime = Date.now();
235
+ await _fsextra2.default.writeFile(lockPath, Date.now().toString());
236
+ this.onEvent({ type: "sync_started" });
237
+ try {
238
+ const localManifest = await this.buildLocalManifest();
239
+ const totalSize = localManifest.files.reduce((sum, f) => sum + f.size, 0);
240
+ if (totalSize > MAX_TOTAL_SIZE) {
241
+ return {
242
+ success: false,
243
+ uploaded: 0,
244
+ downloaded: 0,
245
+ conflicts: 0,
246
+ error: `Total sync size (${Math.round(totalSize / 1024 / 1024)}MB) exceeds limit (${Math.round(MAX_TOTAL_SIZE / 1024 / 1024)}MB)`
247
+ };
248
+ }
249
+ const remoteManifest = await this.client.getRemoteManifest(this.authToken);
250
+ const actions = this.compareManifests(localManifest, remoteManifest);
251
+ let downloaded = 0;
252
+ let uploaded = 0;
253
+ let conflicts = 0;
254
+ if (actions.downloads.length > 0 || actions.conflicts.length > 0) {
255
+ const toDownload = [...actions.downloads, ...actions.conflicts];
256
+ conflicts = actions.conflicts.length;
257
+ const { downloadUrls } = await this.client.initiateDownload(
258
+ this.authToken,
259
+ toDownload.map((f) => f.path)
260
+ );
261
+ for (const file of toDownload) {
262
+ const url = downloadUrls[file.path];
263
+ if (!url) continue;
264
+ try {
265
+ const content = await this.client.downloadFile(url);
266
+ const localPath = _path2.default.join(this.basePath, file.path);
267
+ if (file.path === "config.json") {
268
+ const config = JSON.parse(content.toString("utf8"));
269
+ const decrypted = decryptConfig(config, this.authToken);
270
+ await _fsextra2.default.ensureDir(_path2.default.dirname(localPath));
271
+ await _fsextra2.default.writeJson(localPath, decrypted, { spaces: 2 });
272
+ } else {
273
+ await _fsextra2.default.ensureDir(_path2.default.dirname(localPath));
274
+ await _fsextra2.default.writeFile(localPath, content);
275
+ }
276
+ downloaded++;
277
+ this.onEvent({ type: "file_downloaded", path: file.path, size: content.length });
278
+ if (actions.conflicts.includes(file)) {
279
+ this.onEvent({ type: "conflict_resolved", path: file.path, strategy: "cloud_wins" });
280
+ }
281
+ } catch (error) {
282
+ console.error(`Failed to download ${file.path}:`, error);
283
+ }
284
+ }
285
+ for (const filePath of actions.localDeletes) {
286
+ const localPath = _path2.default.join(this.basePath, filePath);
287
+ await _fsextra2.default.remove(localPath).catch(() => {
288
+ });
289
+ }
290
+ }
291
+ if (actions.uploads.length > 0) {
292
+ const { uploadUrls } = await this.client.initiateUpload(
293
+ this.authToken,
294
+ localManifest,
295
+ actions.uploads.map((f) => f.path)
296
+ );
297
+ for (const file of actions.uploads) {
298
+ const url = uploadUrls[file.path];
299
+ if (!url) continue;
300
+ try {
301
+ const localPath = _path2.default.join(this.basePath, file.path);
302
+ let content;
303
+ if (file.path === "config.json") {
304
+ const config = await _fsextra2.default.readJson(localPath);
305
+ const encrypted = encryptConfig(config, this.authToken);
306
+ content = Buffer.from(JSON.stringify(encrypted, null, 2), "utf8");
307
+ } else {
308
+ content = await _fsextra2.default.readFile(localPath);
309
+ }
310
+ await this.client.uploadFile(url, content);
311
+ uploaded++;
312
+ this.onEvent({ type: "file_uploaded", path: file.path, size: content.length });
313
+ } catch (error) {
314
+ console.error(`Failed to upload ${file.path}:`, error);
315
+ }
316
+ }
317
+ await this.client.completeUpload(this.authToken, localManifest);
318
+ }
319
+ const stateFile = _path2.default.join(this.basePath, SYNC_STATE_FILE);
320
+ const state = {
321
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
322
+ lastManifestHash: computeHash(JSON.stringify(localManifest))
323
+ };
324
+ await _fsextra2.default.writeJson(stateFile, state, { spaces: 2 });
325
+ const result = {
326
+ success: true,
327
+ uploaded,
328
+ downloaded,
329
+ conflicts,
330
+ duration: Date.now() - startTime
331
+ };
332
+ this.onEvent({ type: "sync_completed", result });
333
+ return result;
334
+ } catch (error) {
335
+ const errorMessage = error.message;
336
+ this.onEvent({ type: "sync_failed", error: errorMessage });
337
+ return {
338
+ success: false,
339
+ uploaded: 0,
340
+ downloaded: 0,
341
+ conflicts: 0,
342
+ error: errorMessage,
343
+ duration: Date.now() - startTime
344
+ };
345
+ } finally {
346
+ this.syncing = false;
347
+ await _fsextra2.default.remove(lockPath).catch(() => {
348
+ });
349
+ }
350
+ }
351
+ /**
352
+ * Build manifest from local ~/.autohand/ files
353
+ */
354
+ async buildLocalManifest() {
355
+ const files = [];
356
+ const includePaths = await this.getIncludePaths();
357
+ for (const relativePath of includePaths) {
358
+ const fullPath = _path2.default.join(this.basePath, relativePath);
359
+ if (await _fsextra2.default.pathExists(fullPath)) {
360
+ const stat = await _fsextra2.default.stat(fullPath);
361
+ if (stat.isFile()) {
362
+ const content = await _fsextra2.default.readFile(fullPath);
363
+ files.push({
364
+ path: relativePath,
365
+ hash: computeHash(content),
366
+ size: stat.size,
367
+ modifiedAt: stat.mtime.toISOString(),
368
+ encrypted: relativePath === "config.json"
369
+ });
370
+ } else if (stat.isDirectory()) {
371
+ const dirFiles = await this.getFilesInDirectory(relativePath);
372
+ files.push(...dirFiles);
373
+ }
374
+ }
375
+ }
376
+ const manifest = {
377
+ version: MANIFEST_VERSION,
378
+ userId: this.userId,
379
+ lastModified: (/* @__PURE__ */ new Date()).toISOString(),
380
+ files,
381
+ checksum: ""
382
+ // Will be set below
383
+ };
384
+ manifest.checksum = computeHash(JSON.stringify({ ...manifest, checksum: "" }));
385
+ return manifest;
386
+ }
387
+ /**
388
+ * Get list of paths to include in sync
389
+ */
390
+ async getIncludePaths() {
391
+ const paths = [...SYNC_INCLUDE_DEFAULT];
392
+ if (this.config.includeTelemetry) {
393
+ paths.push(SYNC_CONSENT_REQUIRED.telemetry);
394
+ }
395
+ if (this.config.includeFeedback) {
396
+ paths.push(SYNC_CONSENT_REQUIRED.feedback);
397
+ }
398
+ const excludePatterns = [...SYNC_EXCLUDE_ALWAYS, ...this.config.exclude || []];
399
+ return paths.filter((p) => !this.isExcluded(p, excludePatterns));
400
+ }
401
+ /**
402
+ * Get all files in a directory recursively
403
+ * Skips symlinks to prevent security issues and infinite loops
404
+ */
405
+ async getFilesInDirectory(dirPath) {
406
+ const files = [];
407
+ const fullDirPath = _path2.default.join(this.basePath, dirPath);
408
+ if (!await _fsextra2.default.pathExists(fullDirPath)) {
409
+ return files;
410
+ }
411
+ const entries = await _fsextra2.default.readdir(fullDirPath, { withFileTypes: true });
412
+ const excludePatterns = [...SYNC_EXCLUDE_ALWAYS, ...this.config.exclude || []];
413
+ for (const entry of entries) {
414
+ const relativePath = _path2.default.join(dirPath, entry.name);
415
+ const fullPath = _path2.default.join(this.basePath, relativePath);
416
+ if (this.isExcluded(relativePath, excludePatterns)) {
417
+ continue;
418
+ }
419
+ if (entry.isSymbolicLink()) {
420
+ continue;
421
+ }
422
+ if (entry.isFile()) {
423
+ try {
424
+ const stat = await _fsextra2.default.stat(fullPath);
425
+ const content = await _fsextra2.default.readFile(fullPath);
426
+ files.push({
427
+ path: relativePath,
428
+ hash: computeHash(content),
429
+ size: stat.size,
430
+ modifiedAt: stat.mtime.toISOString()
431
+ });
432
+ } catch (e3) {
433
+ continue;
434
+ }
435
+ } else if (entry.isDirectory()) {
436
+ const subFiles = await this.getFilesInDirectory(relativePath);
437
+ files.push(...subFiles);
438
+ }
439
+ }
440
+ return files;
441
+ }
442
+ /**
443
+ * Check if a path matches any exclude pattern
444
+ */
445
+ isExcluded(filePath, patterns) {
446
+ for (const pattern of patterns) {
447
+ if (pattern.includes("*")) {
448
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
449
+ if (regex.test(filePath) || regex.test(_path2.default.basename(filePath))) {
450
+ return true;
451
+ }
452
+ } else if (filePath === pattern || filePath.startsWith(pattern)) {
453
+ return true;
454
+ }
455
+ }
456
+ return false;
457
+ }
458
+ /**
459
+ * Compare local and remote manifests to determine sync actions
460
+ */
461
+ compareManifests(local, remote) {
462
+ const actions = {
463
+ uploads: [],
464
+ downloads: [],
465
+ conflicts: [],
466
+ localDeletes: [],
467
+ remoteDeletes: []
468
+ };
469
+ if (!remote) {
470
+ actions.uploads = [...local.files];
471
+ return actions;
472
+ }
473
+ const localFiles = new Map(local.files.map((f) => [f.path, f]));
474
+ const remoteFiles = new Map(remote.files.map((f) => [f.path, f]));
475
+ for (const [filePath, localFile] of localFiles) {
476
+ const remoteFile = remoteFiles.get(filePath);
477
+ if (!remoteFile) {
478
+ actions.uploads.push(localFile);
479
+ } else if (localFile.hash !== remoteFile.hash) {
480
+ actions.conflicts.push(remoteFile);
481
+ }
482
+ }
483
+ for (const [filePath, remoteFile] of remoteFiles) {
484
+ if (!localFiles.has(filePath)) {
485
+ actions.downloads.push(remoteFile);
486
+ }
487
+ }
488
+ return actions;
489
+ }
490
+ /**
491
+ * Force a full sync (re-download everything from cloud)
492
+ */
493
+ async forceDownload() {
494
+ const remoteManifest = await this.client.getRemoteManifest(this.authToken);
495
+ if (!remoteManifest) {
496
+ return {
497
+ success: false,
498
+ uploaded: 0,
499
+ downloaded: 0,
500
+ conflicts: 0,
501
+ error: "No remote data to download"
502
+ };
503
+ }
504
+ const actions = {
505
+ uploads: [],
506
+ downloads: remoteManifest.files,
507
+ conflicts: [],
508
+ localDeletes: [],
509
+ remoteDeletes: []
510
+ };
511
+ return this.performSyncActions(actions, remoteManifest);
512
+ }
513
+ /**
514
+ * Force a full upload (overwrite cloud with local)
515
+ */
516
+ async forceUpload() {
517
+ const localManifest = await this.buildLocalManifest();
518
+ const actions = {
519
+ uploads: localManifest.files,
520
+ downloads: [],
521
+ conflicts: [],
522
+ localDeletes: [],
523
+ remoteDeletes: []
524
+ };
525
+ return this.performSyncActions(actions, localManifest);
526
+ }
527
+ /**
528
+ * Helper to perform sync actions
529
+ */
530
+ async performSyncActions(actions, manifest) {
531
+ const startTime = Date.now();
532
+ let uploaded = 0;
533
+ let downloaded = 0;
534
+ try {
535
+ if (actions.downloads.length > 0) {
536
+ const { downloadUrls } = await this.client.initiateDownload(
537
+ this.authToken,
538
+ actions.downloads.map((f) => f.path)
539
+ );
540
+ for (const file of actions.downloads) {
541
+ const url = downloadUrls[file.path];
542
+ if (!url) continue;
543
+ try {
544
+ const content = await this.client.downloadFile(url);
545
+ const localPath = _path2.default.join(this.basePath, file.path);
546
+ if (file.path === "config.json") {
547
+ const config = JSON.parse(content.toString("utf8"));
548
+ const decrypted = decryptConfig(config, this.authToken);
549
+ await _fsextra2.default.ensureDir(_path2.default.dirname(localPath));
550
+ await _fsextra2.default.writeJson(localPath, decrypted, { spaces: 2 });
551
+ } else {
552
+ await _fsextra2.default.ensureDir(_path2.default.dirname(localPath));
553
+ await _fsextra2.default.writeFile(localPath, content);
554
+ }
555
+ downloaded++;
556
+ } catch (e4) {
557
+ }
558
+ }
559
+ }
560
+ if (actions.uploads.length > 0) {
561
+ const { uploadUrls } = await this.client.initiateUpload(
562
+ this.authToken,
563
+ manifest,
564
+ actions.uploads.map((f) => f.path)
565
+ );
566
+ for (const file of actions.uploads) {
567
+ const url = uploadUrls[file.path];
568
+ if (!url) continue;
569
+ try {
570
+ const localPath = _path2.default.join(this.basePath, file.path);
571
+ let content;
572
+ if (file.path === "config.json") {
573
+ const config = await _fsextra2.default.readJson(localPath);
574
+ const encrypted = encryptConfig(config, this.authToken);
575
+ content = Buffer.from(JSON.stringify(encrypted, null, 2), "utf8");
576
+ } else {
577
+ content = await _fsextra2.default.readFile(localPath);
578
+ }
579
+ await this.client.uploadFile(url, content);
580
+ uploaded++;
581
+ } catch (e5) {
582
+ }
583
+ }
584
+ await this.client.completeUpload(this.authToken, manifest);
585
+ }
586
+ return {
587
+ success: true,
588
+ uploaded,
589
+ downloaded,
590
+ conflicts: 0,
591
+ duration: Date.now() - startTime
592
+ };
593
+ } catch (error) {
594
+ return {
595
+ success: false,
596
+ uploaded,
597
+ downloaded,
598
+ conflicts: 0,
599
+ error: error.message,
600
+ duration: Date.now() - startTime
601
+ };
602
+ }
603
+ }
604
+ /**
605
+ * Get current sync status
606
+ */
607
+ async getStatus() {
608
+ const stateFile = _path2.default.join(this.basePath, SYNC_STATE_FILE);
609
+ let lastSync = null;
610
+ if (await _fsextra2.default.pathExists(stateFile)) {
611
+ const state = await _fsextra2.default.readJson(stateFile).catch(() => ({}));
612
+ lastSync = state.lastSync || null;
613
+ }
614
+ const manifest = await this.buildLocalManifest();
615
+ const totalSize = manifest.files.reduce((sum, f) => sum + f.size, 0);
616
+ return {
617
+ enabled: this.config.enabled,
618
+ lastSync,
619
+ syncing: this.syncing,
620
+ fileCount: manifest.files.length,
621
+ totalSize
622
+ };
623
+ }
624
+ };
625
+ function createSyncService(options) {
626
+ return new SyncService(options);
627
+ }
628
+
629
+
630
+
631
+
632
+
633
+
634
+
635
+
636
+
637
+
638
+
639
+
640
+
641
+
642
+
643
+ exports.DEFAULT_SYNC_CONFIG = DEFAULT_SYNC_CONFIG; exports.SYNC_EXCLUDE_ALWAYS = SYNC_EXCLUDE_ALWAYS; exports.SYNC_CONSENT_REQUIRED = SYNC_CONSENT_REQUIRED; exports.SYNC_INCLUDE_DEFAULT = SYNC_INCLUDE_DEFAULT; exports.deriveKey = deriveKey; exports.encrypt = encrypt; exports.decrypt = decrypt; exports.isEncrypted = isEncrypted; exports.encryptConfig = encryptConfig; exports.decryptConfig = decryptConfig; exports.computeHash = computeHash; exports.SyncService = SyncService; exports.createSyncService = createSyncService;
644
+ /**
645
+ * @license
646
+ * Copyright 2025 Autohand AI LLC
647
+ * SPDX-License-Identifier: Apache-2.0
648
+ *
649
+ * Types for settings sync service
650
+ */
651
+ /**
652
+ * @license
653
+ * Copyright 2025 Autohand AI LLC
654
+ * SPDX-License-Identifier: Apache-2.0
655
+ *
656
+ * Encryption utilities for settings sync
657
+ * Uses AES-256-GCM for authenticated encryption of sensitive data
658
+ */
659
+ /**
660
+ * @license
661
+ * Copyright 2025 Autohand AI LLC
662
+ * SPDX-License-Identifier: Apache-2.0
663
+ *
664
+ * Settings Sync Service
665
+ * Manages background synchronization of ~/.autohand/ to cloud storage
666
+ */
667
+ /**
668
+ * @license
669
+ * Copyright 2025 Autohand AI LLC
670
+ * SPDX-License-Identifier: Apache-2.0
671
+ *
672
+ * Settings Sync Module
673
+ * Synchronizes ~/.autohand/ configuration to cloud storage for logged-in users
674
+ */