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.
- package/dist/{AutomodeManager-TJSW2SQY.cjs → AutomodeManager-HV6M7EAX.cjs} +61 -22
- package/dist/{AutomodeManager-WIMHLG4W.js → AutomodeManager-YVCJXOMQ.js} +61 -22
- package/dist/CommunitySkillsCache-2BITCEAA.cjs +7 -0
- package/dist/{CommunitySkillsCache-XPDVYU3K.js → CommunitySkillsCache-Q22FUAR5.js} +2 -2
- package/dist/HookManager-X47HCM5G.cjs +6 -0
- package/dist/{HookManager-VIX56KFU.js → HookManager-ZXKHCD7U.js} +1 -1
- package/dist/MemoryManager-2ATHG7BH.js +7 -0
- package/dist/MemoryManager-AENCGCEW.cjs +7 -0
- package/dist/PermissionManager-6HZGTK2N.cjs +10 -0
- package/dist/{PermissionManager-YFZI4ZZ6.js → PermissionManager-HATZKTRC.js} +3 -3
- package/dist/SessionManager-AG4WT3DP.cjs +9 -0
- package/dist/{SessionManager-XDBEQUPG.js → SessionManager-S5R6O3NU.js} +2 -2
- package/dist/{SkillsRegistry-7NICF6FY.js → SkillsRegistry-R5WDM6T3.js} +2 -2
- package/dist/SkillsRegistry-ZXU6YDRP.cjs +8 -0
- package/dist/SyncApiClient-FAOMIZAP.js +10 -0
- package/dist/SyncApiClient-UOA4VLLD.cjs +10 -0
- package/dist/add-dir-OMK3Y4DM.cjs +8 -0
- package/dist/add-dir-PNU7AGKO.js +8 -0
- package/dist/{agents-B33IAATH.js → agents-ICAC3KD3.js} +2 -2
- package/dist/agents-YONWPKFS.cjs +9 -0
- package/dist/agents-new-NV557UVG.cjs +10 -0
- package/dist/{agents-new-KTXJFC5E.js → agents-new-QHM3CO4B.js} +2 -2
- package/dist/{chunk-MFLRXVKU.js → chunk-23JQSCTO.js} +1 -1
- package/dist/chunk-2JPUEN44.cjs +299 -0
- package/dist/{chunk-5PD2L6WI.js → chunk-3YEDXG6S.js} +1 -1
- package/dist/{chunk-3ZUWWML7.cjs → chunk-4M2GX7RH.cjs} +2 -2
- package/dist/{chunk-R5KNHJ27.js → chunk-4RWTUT2Z.js} +1 -1
- package/dist/{chunk-CHQMK2ZG.js → chunk-52MLYK5P.js} +1 -1
- package/dist/{chunk-CVYEUA3D.cjs → chunk-53BR4MUW.cjs} +3 -3
- package/dist/{chunk-JYTXG6OV.cjs → chunk-54GVL2SE.cjs} +4 -2
- package/dist/{chunk-5WKR4HIB.js → chunk-5DN5KNXU.js} +1 -1
- package/dist/{chunk-K6NBYSME.cjs → chunk-5MDDOGTD.cjs} +6 -6
- package/dist/{chunk-5MCDN53U.js → chunk-6KMAJTU3.js} +4 -2
- package/dist/{chunk-3CO5R6M2.cjs → chunk-7TOHYAUF.cjs} +2 -2
- package/dist/{chunk-67NJKV5A.cjs → chunk-7VW3A7DO.cjs} +2 -2
- package/dist/{chunk-2W3QTBNG.cjs → chunk-A552JHUJ.cjs} +2 -2
- package/dist/{chunk-NGSLABLS.js → chunk-A6QBABQ7.js} +1 -1
- package/dist/chunk-ARVFUZOB.js +736 -0
- package/dist/{chunk-VO3JKFUH.js → chunk-AVL4DKQO.js} +1 -1
- package/dist/{chunk-CT2VTDPQ.cjs → chunk-B4ZPNXZE.cjs} +1 -1
- package/dist/{chunk-OKMYLMCR.cjs → chunk-B7EUETGY.cjs} +4 -4
- package/dist/chunk-C26EN22G.cjs +328 -0
- package/dist/chunk-DOTAX65F.js +328 -0
- package/dist/{chunk-4KZCGK7D.js → chunk-DPJ3IIBB.js} +1 -1
- package/dist/{chunk-SKT2CRNY.cjs → chunk-DSKVMFRM.cjs} +56 -8
- package/dist/{chunk-723DZKBU.js → chunk-EDGV5BNH.js} +2 -2
- package/dist/{chunk-FUEL6BK7.js → chunk-EKY5PKQI.js} +15 -0
- package/dist/{chunk-YMP7AGNT.js → chunk-G77ZY4QG.js} +1 -1
- package/dist/{chunk-KN5C4TR4.cjs → chunk-GDTZQSJ6.cjs} +2 -2
- package/dist/{chunk-536VWSZK.cjs → chunk-GFJ6AETU.cjs} +4 -4
- package/dist/{chunk-PVM5I5WI.js → chunk-GWIAMKKF.js} +1 -1
- package/dist/{chunk-XAM7SFVB.cjs → chunk-GWXXFQ3F.cjs} +2 -2
- package/dist/{chunk-REPKBECD.cjs → chunk-JHFH3N4U.cjs} +2 -2
- package/dist/{chunk-4L5WYXHN.js → chunk-KH7BCZJN.js} +1 -1
- package/dist/{chunk-JXOXZTMA.js → chunk-L5ZFPWHY.js} +54 -6
- package/dist/chunk-MDWULS57.js +288 -0
- package/dist/{chunk-6LP2GO5C.js → chunk-MJFBVQHB.js} +2 -2
- package/dist/{chunk-MWLAHCU7.js → chunk-NI3BQXKU.js} +1 -1
- package/dist/{chunk-SKU4M27Z.js → chunk-OBV3UUIL.js} +1 -1
- package/dist/{chunk-27ISZOFA.js → chunk-P2Z6GDEN.js} +1 -1
- package/dist/{chunk-XTHHDIBG.cjs → chunk-PMMSDR44.cjs} +16 -1
- package/dist/{chunk-URY4AS4L.cjs → chunk-PU534KPO.cjs} +4 -4
- package/dist/chunk-SFGJQPGC.cjs +288 -0
- package/dist/{chunk-53YDUYNS.cjs → chunk-SLISYSP4.cjs} +2 -2
- package/dist/{chunk-2E2COWKB.cjs → chunk-SYJLMBLP.cjs} +66 -10
- package/dist/chunk-U5WIP4HS.js +674 -0
- package/dist/{chunk-7HB7GSQF.js → chunk-UL7YPRCU.js} +1 -1
- package/dist/{chunk-LUKMRIKJ.cjs → chunk-VEDIYPWY.cjs} +2 -2
- package/dist/{chunk-C2NFLFHH.js → chunk-VPAN5H7Q.js} +1 -1
- package/dist/chunk-WH3D42BQ.js +299 -0
- package/dist/{chunk-2FSQPRPJ.js → chunk-WIUGUR5T.js} +59 -3
- package/dist/{chunk-QMVTT55Y.cjs → chunk-WQSWU2QA.cjs} +4 -4
- package/dist/chunk-XFPITUFJ.cjs +674 -0
- package/dist/chunk-XFQS2VGT.cjs +736 -0
- package/dist/{chunk-HYTYXN2G.cjs → chunk-YAGD43KA.cjs} +10 -10
- package/dist/{chunk-KJ67C72C.cjs → chunk-YDOR2OCA.cjs} +2 -2
- package/dist/constants-G2PLP5HH.cjs +20 -0
- package/dist/{constants-QYBEF3DB.js → constants-ZLG6M5SI.js} +3 -1
- package/dist/{defaultHooks-3G3DVF6I.js → defaultHooks-R56VYG7I.js} +315 -1
- package/dist/{defaultHooks-Z4KA6U5C.cjs → defaultHooks-WLMRQUXG.cjs} +315 -1
- package/dist/{feedback-PZ2PINDU.js → feedback-HZBCTSFG.js} +2 -2
- package/dist/feedback-JBQ3UPGZ.cjs +10 -0
- package/dist/index.cjs +897 -600
- package/dist/index.js +1438 -1141
- package/dist/language-KODBDE5R.js +12 -0
- package/dist/language-SJT475NW.cjs +12 -0
- package/dist/localProjectPermissions-AYQYGTOE.cjs +17 -0
- package/dist/{localProjectPermissions-DURCNDZG.js → localProjectPermissions-YFFAKLUZ.js} +2 -2
- package/dist/login-TC2KROQI.js +14 -0
- package/dist/login-TYMR2ZD3.cjs +14 -0
- package/dist/logout-2ECV365P.js +12 -0
- package/dist/logout-CO3CPYZJ.cjs +12 -0
- package/dist/resume-EPOEF3WV.cjs +9 -0
- package/dist/{resume-CWYAK6XR.js → resume-LOYD5MMP.js} +2 -2
- package/dist/share-APR5S2CS.cjs +10 -0
- package/dist/share-VLJFDZKR.js +10 -0
- package/dist/{skills-CRFOVWEQ.js → skills-3YEEODHK.js} +1 -1
- package/dist/skills-CRM55MKM.cjs +12 -0
- package/dist/{skills-install-Z27KPEGF.cjs → skills-install-FTGOHOZ4.cjs} +5 -5
- package/dist/{skills-install-RMPXN6RK.js → skills-install-KAXAQSN6.js} +2 -2
- package/dist/skills-new-JF4FKNUT.cjs +11 -0
- package/dist/{skills-new-S2YPO635.js → skills-new-JYX2GBKM.js} +2 -2
- package/dist/status-DAEFE7ZC.cjs +9 -0
- package/dist/{status-GPAZ67ZZ.js → status-PBFFUC4Q.js} +2 -2
- package/dist/sync-3B7SNBYC.js +14 -0
- package/dist/sync-4RARBQIH.cjs +39 -0
- package/dist/sync-H4UHHLKU.js +39 -0
- package/dist/sync-YZ6YZ42H.cjs +14 -0
- package/dist/theme-3XV5BWUB.js +12 -0
- package/dist/theme-Z2WS5XWZ.cjs +12 -0
- package/package.json +4 -2
- package/dist/CommunitySkillsCache-X3X237QQ.cjs +0 -7
- package/dist/HookManager-EOMUXKJ4.cjs +0 -6
- package/dist/MemoryManager-UVHILGV5.js +0 -7
- package/dist/MemoryManager-WO3KUZVA.cjs +0 -7
- package/dist/PermissionManager-PMTQN263.cjs +0 -10
- package/dist/SessionManager-M5ZLCLCW.cjs +0 -9
- package/dist/SkillsRegistry-OINIPILA.cjs +0 -8
- package/dist/agents-GRAFXZY3.cjs +0 -9
- package/dist/agents-new-67NJJSDA.cjs +0 -10
- package/dist/constants-PE5DLI7Q.cjs +0 -18
- package/dist/feedback-R66B3B3C.cjs +0 -10
- package/dist/localProjectPermissions-75X3ZGKH.cjs +0 -17
- package/dist/login-NYWZRZO5.js +0 -12
- package/dist/login-QNJ5C42G.cjs +0 -12
- package/dist/logout-MBS7L3ZW.js +0 -12
- package/dist/logout-MVUP7GPU.cjs +0 -12
- package/dist/resume-ANISKRWL.cjs +0 -9
- package/dist/share-3PSV53CQ.js +0 -10
- package/dist/share-4ACH6626.cjs +0 -10
- package/dist/skills-6PIGHOWS.cjs +0 -12
- package/dist/skills-new-3QJUST7P.cjs +0 -11
- package/dist/status-4U5CPUVT.cjs +0 -9
- package/dist/theme-CVY6MVEK.cjs +0 -12
- 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
|
+
*/
|