ccsini 0.1.60 → 0.1.61
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/index.js +159 -75
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27876,7 +27876,7 @@ function decodeProjectPath(encoded) {
|
|
|
27876
27876
|
}
|
|
27877
27877
|
|
|
27878
27878
|
// ../shared/src/constants.ts
|
|
27879
|
-
var DEFAULT_CATEGORY_PATTERNS, MERGE_STRATEGIES, NEVER_SYNC_PATTERNS, SESSION_SYNC_DEFAULTS, JWT_EXPIRY_SECONDS = 900, DAEMON_DEBOUNCE_MS = 5000, DAEMON_INTERVAL_MS = 30000, MAX_CONCURRENT_TRANSFERS = 50;
|
|
27879
|
+
var DEFAULT_CATEGORY_PATTERNS, MERGE_STRATEGIES, NEVER_SYNC_PATTERNS, SESSION_SYNC_DEFAULTS, JWT_EXPIRY_SECONDS = 900, DAEMON_DEBOUNCE_MS = 5000, DAEMON_INTERVAL_MS = 30000, MAX_CONCURRENT_TRANSFERS = 50, BATCH_DOWNLOAD_SIZE = 20;
|
|
27880
27880
|
var init_constants = __esm(() => {
|
|
27881
27881
|
DEFAULT_CATEGORY_PATTERNS = {
|
|
27882
27882
|
session: ["projects/**/*.jsonl"],
|
|
@@ -28058,7 +28058,7 @@ var {
|
|
|
28058
28058
|
} = import__.default;
|
|
28059
28059
|
|
|
28060
28060
|
// src/version.ts
|
|
28061
|
-
var VERSION = "0.1.
|
|
28061
|
+
var VERSION = "0.1.61";
|
|
28062
28062
|
|
|
28063
28063
|
// src/commands/init.ts
|
|
28064
28064
|
init_source();
|
|
@@ -28967,6 +28967,23 @@ class CcsiniClient {
|
|
|
28967
28967
|
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
28968
28968
|
this.token = token;
|
|
28969
28969
|
}
|
|
28970
|
+
async fetchWithRetry(input, init, maxRetries = 3) {
|
|
28971
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
28972
|
+
try {
|
|
28973
|
+
const res = await fetch(input, init);
|
|
28974
|
+
if (res.status >= 500 && attempt < maxRetries) {
|
|
28975
|
+
await new Promise((r) => setTimeout(r, 500 * Math.pow(2, attempt - 1)));
|
|
28976
|
+
continue;
|
|
28977
|
+
}
|
|
28978
|
+
return res;
|
|
28979
|
+
} catch (e) {
|
|
28980
|
+
if (attempt >= maxRetries)
|
|
28981
|
+
throw e;
|
|
28982
|
+
await new Promise((r) => setTimeout(r, 500 * Math.pow(2, attempt - 1)));
|
|
28983
|
+
}
|
|
28984
|
+
}
|
|
28985
|
+
throw new Error("fetchWithRetry exhausted");
|
|
28986
|
+
}
|
|
28970
28987
|
getHeaders() {
|
|
28971
28988
|
return {
|
|
28972
28989
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -28995,7 +29012,7 @@ class CcsiniClient {
|
|
|
28995
29012
|
throw new Error("Failed to store salt");
|
|
28996
29013
|
}
|
|
28997
29014
|
async getManifest() {
|
|
28998
|
-
const res = await
|
|
29015
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/manifest`, {
|
|
28999
29016
|
headers: this.getHeaders()
|
|
29000
29017
|
});
|
|
29001
29018
|
if (!res.ok)
|
|
@@ -29006,7 +29023,7 @@ class CcsiniClient {
|
|
|
29006
29023
|
return new Uint8Array(Buffer.from(data.manifest, "base64"));
|
|
29007
29024
|
}
|
|
29008
29025
|
async putManifest(encryptedManifest) {
|
|
29009
|
-
const res = await
|
|
29026
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/manifest`, {
|
|
29010
29027
|
method: "PUT",
|
|
29011
29028
|
headers: {
|
|
29012
29029
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -29018,7 +29035,7 @@ class CcsiniClient {
|
|
|
29018
29035
|
throw new Error("Failed to upload manifest");
|
|
29019
29036
|
}
|
|
29020
29037
|
async uploadBlob(hash, encryptedData) {
|
|
29021
|
-
const res = await
|
|
29038
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blob/${hash}`, {
|
|
29022
29039
|
method: "PUT",
|
|
29023
29040
|
headers: {
|
|
29024
29041
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -29030,7 +29047,7 @@ class CcsiniClient {
|
|
|
29030
29047
|
throw new Error(`Failed to upload blob ${hash}`);
|
|
29031
29048
|
}
|
|
29032
29049
|
async downloadBlob(hash) {
|
|
29033
|
-
const res = await
|
|
29050
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blob/${hash}`, {
|
|
29034
29051
|
headers: this.getHeaders()
|
|
29035
29052
|
});
|
|
29036
29053
|
if (!res.ok)
|
|
@@ -29039,7 +29056,7 @@ class CcsiniClient {
|
|
|
29039
29056
|
return new Uint8Array(buffer);
|
|
29040
29057
|
}
|
|
29041
29058
|
async batchDownload(hashes) {
|
|
29042
|
-
const res = await
|
|
29059
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/batch-download`, {
|
|
29043
29060
|
method: "POST",
|
|
29044
29061
|
headers: this.getHeaders(),
|
|
29045
29062
|
body: JSON.stringify({ hashes })
|
|
@@ -29054,7 +29071,7 @@ class CcsiniClient {
|
|
|
29054
29071
|
return result;
|
|
29055
29072
|
}
|
|
29056
29073
|
async listBlobs() {
|
|
29057
|
-
const res = await
|
|
29074
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blobs`, {
|
|
29058
29075
|
headers: this.getHeaders()
|
|
29059
29076
|
});
|
|
29060
29077
|
if (!res.ok)
|
|
@@ -29062,7 +29079,7 @@ class CcsiniClient {
|
|
|
29062
29079
|
return res.json();
|
|
29063
29080
|
}
|
|
29064
29081
|
async deleteBlobs(hashes) {
|
|
29065
|
-
const res = await
|
|
29082
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blobs/delete`, {
|
|
29066
29083
|
method: "POST",
|
|
29067
29084
|
headers: this.getHeaders(),
|
|
29068
29085
|
body: JSON.stringify({ hashes })
|
|
@@ -29081,7 +29098,7 @@ class CcsiniClient {
|
|
|
29081
29098
|
throw new Error("Failed to wipe account data");
|
|
29082
29099
|
}
|
|
29083
29100
|
async putProjects(projects, device) {
|
|
29084
|
-
const res = await
|
|
29101
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/projects`, {
|
|
29085
29102
|
method: "PUT",
|
|
29086
29103
|
headers: this.getHeaders(),
|
|
29087
29104
|
body: JSON.stringify({ projects, device, timestamp: Date.now() })
|
|
@@ -29091,6 +29108,17 @@ class CcsiniClient {
|
|
|
29091
29108
|
throw new Error(`Failed to send projects (${res.status}): ${text}`);
|
|
29092
29109
|
}
|
|
29093
29110
|
}
|
|
29111
|
+
async checkBlobsExist(hashes) {
|
|
29112
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blobs/exists`, {
|
|
29113
|
+
method: "POST",
|
|
29114
|
+
headers: this.getHeaders(),
|
|
29115
|
+
body: JSON.stringify({ hashes })
|
|
29116
|
+
});
|
|
29117
|
+
if (!res.ok)
|
|
29118
|
+
throw new Error("Failed to check blob existence");
|
|
29119
|
+
const data = await res.json();
|
|
29120
|
+
return data.exists;
|
|
29121
|
+
}
|
|
29094
29122
|
async resetAll() {
|
|
29095
29123
|
const res = await fetch(`${this.apiUrl}/api/sync/reset`, {
|
|
29096
29124
|
method: "DELETE",
|
|
@@ -29101,7 +29129,7 @@ class CcsiniClient {
|
|
|
29101
29129
|
return res.json();
|
|
29102
29130
|
}
|
|
29103
29131
|
async heartbeat(mode, stats) {
|
|
29104
|
-
const res = await
|
|
29132
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/heartbeat`, {
|
|
29105
29133
|
method: "POST",
|
|
29106
29134
|
headers: this.getHeaders(),
|
|
29107
29135
|
body: JSON.stringify({
|
|
@@ -29123,7 +29151,7 @@ class CcsiniClient {
|
|
|
29123
29151
|
return {};
|
|
29124
29152
|
}
|
|
29125
29153
|
async logSyncEvent(event) {
|
|
29126
|
-
await
|
|
29154
|
+
await this.fetchWithRetry(`${this.apiUrl}/api/sync/log`, {
|
|
29127
29155
|
method: "POST",
|
|
29128
29156
|
headers: this.getHeaders(),
|
|
29129
29157
|
body: JSON.stringify(event)
|
|
@@ -29136,6 +29164,7 @@ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir2 } from
|
|
|
29136
29164
|
import { join as join6, dirname } from "path";
|
|
29137
29165
|
import { homedir as homedir3 } from "os";
|
|
29138
29166
|
import { createHash as createHash2 } from "crypto";
|
|
29167
|
+
import { gzipSync, gunzipSync } from "zlib";
|
|
29139
29168
|
|
|
29140
29169
|
// src/core/manifest.ts
|
|
29141
29170
|
import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -29402,6 +29431,30 @@ init_src();
|
|
|
29402
29431
|
function blobKey(filePath, contentHash) {
|
|
29403
29432
|
return createHash2("sha256").update(`${filePath}:${contentHash}`).digest("hex");
|
|
29404
29433
|
}
|
|
29434
|
+
var COMPRESS_FLAG = 1;
|
|
29435
|
+
var RAW_FLAG = 0;
|
|
29436
|
+
function compressForUpload(plaintext) {
|
|
29437
|
+
const compressed = gzipSync(plaintext);
|
|
29438
|
+
if (compressed.length < plaintext.length) {
|
|
29439
|
+
const result2 = new Uint8Array(1 + compressed.length);
|
|
29440
|
+
result2[0] = COMPRESS_FLAG;
|
|
29441
|
+
result2.set(compressed, 1);
|
|
29442
|
+
return result2;
|
|
29443
|
+
}
|
|
29444
|
+
const result = new Uint8Array(1 + plaintext.length);
|
|
29445
|
+
result[0] = RAW_FLAG;
|
|
29446
|
+
result.set(plaintext, 1);
|
|
29447
|
+
return result;
|
|
29448
|
+
}
|
|
29449
|
+
function decompressFromDownload(data) {
|
|
29450
|
+
if (data.length === 0)
|
|
29451
|
+
return data;
|
|
29452
|
+
if (data[0] === COMPRESS_FLAG)
|
|
29453
|
+
return gunzipSync(data.slice(1));
|
|
29454
|
+
if (data[0] === RAW_FLAG)
|
|
29455
|
+
return data.slice(1);
|
|
29456
|
+
return data;
|
|
29457
|
+
}
|
|
29405
29458
|
function getClaudeDir() {
|
|
29406
29459
|
return join6(homedir3(), ".claude");
|
|
29407
29460
|
}
|
|
@@ -29466,7 +29519,8 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29466
29519
|
} catch {
|
|
29467
29520
|
encrypted = await client.downloadBlob(diff.remoteHash);
|
|
29468
29521
|
}
|
|
29469
|
-
const
|
|
29522
|
+
const decryptedRaw = decryptFile(masterKey, diff.path, encrypted);
|
|
29523
|
+
const decrypted = decompressFromDownload(decryptedRaw);
|
|
29470
29524
|
const conflictPath = join6(claudeDir, `${diff.path}.conflict`);
|
|
29471
29525
|
await mkdir2(dirname(conflictPath), { recursive: true });
|
|
29472
29526
|
await writeFile5(conflictPath, decrypted);
|
|
@@ -29475,30 +29529,32 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29475
29529
|
} catch {}
|
|
29476
29530
|
}
|
|
29477
29531
|
}
|
|
29532
|
+
const pushKeys = toPush.map((d) => blobKey(d.path, d.localHash));
|
|
29533
|
+
let existing = {};
|
|
29534
|
+
try {
|
|
29535
|
+
for (const batch of chunkArray(pushKeys, 100)) {
|
|
29536
|
+
Object.assign(existing, await client.checkBlobsExist(batch));
|
|
29537
|
+
}
|
|
29538
|
+
} catch {}
|
|
29539
|
+
const toUpload = toPush.filter((d) => !existing[blobKey(d.path, d.localHash)]);
|
|
29540
|
+
progress(`${toPush.length} changed, ${toUpload.length} to upload (${toPush.length - toUpload.length} skipped)`);
|
|
29478
29541
|
let uploaded = 0;
|
|
29479
29542
|
const failedPaths = new Set;
|
|
29480
|
-
const chunks = chunkArray(
|
|
29543
|
+
const chunks = chunkArray(toUpload, MAX_CONCURRENT_TRANSFERS);
|
|
29481
29544
|
for (const chunk of chunks) {
|
|
29482
29545
|
await Promise.all(chunk.map(async (diff) => {
|
|
29483
|
-
|
|
29484
|
-
|
|
29485
|
-
|
|
29486
|
-
|
|
29487
|
-
|
|
29488
|
-
|
|
29489
|
-
|
|
29490
|
-
|
|
29491
|
-
|
|
29492
|
-
|
|
29493
|
-
|
|
29494
|
-
}
|
|
29495
|
-
if (attempt < MAX_RETRIES) {
|
|
29496
|
-
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
29497
|
-
} else {
|
|
29498
|
-
failedPaths.add(diff.path);
|
|
29499
|
-
errors2.push(`Push ${diff.path}: ${e.message}`);
|
|
29500
|
-
}
|
|
29501
|
-
}
|
|
29546
|
+
try {
|
|
29547
|
+
const filePath = join6(claudeDir, diff.path);
|
|
29548
|
+
const content = await readFile6(filePath);
|
|
29549
|
+
const compressed = compressForUpload(content);
|
|
29550
|
+
const encrypted = encryptFile(masterKey, diff.path, compressed);
|
|
29551
|
+
await client.uploadBlob(blobKey(diff.path, diff.localHash), encrypted);
|
|
29552
|
+
bytesTransferred += encrypted.length;
|
|
29553
|
+
uploaded++;
|
|
29554
|
+
progress(`Uploading ${uploaded}/${toUpload.length}: ${diff.path}`);
|
|
29555
|
+
} catch (e) {
|
|
29556
|
+
failedPaths.add(diff.path);
|
|
29557
|
+
errors2.push(`Push ${diff.path}: ${e.message}`);
|
|
29502
29558
|
}
|
|
29503
29559
|
}));
|
|
29504
29560
|
}
|
|
@@ -29579,56 +29635,84 @@ async function pullSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29579
29635
|
const toPull = diffs.filter((d) => (d.action === "pull" || d.action === "merge") && !isExcluded(d.path) && (sessionsEnabled || !isSessionFile(d.path)));
|
|
29580
29636
|
progress(`${toPull.length} files to pull`);
|
|
29581
29637
|
let downloaded = 0;
|
|
29582
|
-
const
|
|
29583
|
-
for (const
|
|
29584
|
-
|
|
29585
|
-
|
|
29586
|
-
|
|
29638
|
+
const keyToDiffs = new Map;
|
|
29639
|
+
for (const diff of toPull) {
|
|
29640
|
+
const key = blobKey(diff.path, diff.remoteHash);
|
|
29641
|
+
const list = keyToDiffs.get(key) ?? [];
|
|
29642
|
+
list.push(diff);
|
|
29643
|
+
keyToDiffs.set(key, list);
|
|
29644
|
+
}
|
|
29645
|
+
const uniqueKeys = Array.from(keyToDiffs.keys());
|
|
29646
|
+
const batches = chunkArray(uniqueKeys, BATCH_DOWNLOAD_SIZE);
|
|
29647
|
+
const downloadedBlobs = new Map;
|
|
29648
|
+
for (const batch of batches) {
|
|
29649
|
+
try {
|
|
29650
|
+
const results = await client.batchDownload(batch);
|
|
29651
|
+
for (const [key, data] of Object.entries(results)) {
|
|
29652
|
+
if (data)
|
|
29653
|
+
downloadedBlobs.set(key, data);
|
|
29654
|
+
}
|
|
29655
|
+
} catch {
|
|
29656
|
+
for (const key of batch) {
|
|
29657
|
+
try {
|
|
29658
|
+
const data = await client.downloadBlob(key);
|
|
29659
|
+
downloadedBlobs.set(key, data);
|
|
29660
|
+
} catch {}
|
|
29661
|
+
}
|
|
29662
|
+
}
|
|
29663
|
+
}
|
|
29664
|
+
for (const diff of toPull) {
|
|
29665
|
+
try {
|
|
29666
|
+
const key = blobKey(diff.path, diff.remoteHash);
|
|
29667
|
+
let encrypted = downloadedBlobs.get(key);
|
|
29668
|
+
if (!encrypted) {
|
|
29587
29669
|
try {
|
|
29588
|
-
encrypted = await client.downloadBlob(blobKey(diff.path, diff.remoteHash));
|
|
29589
|
-
} catch {
|
|
29590
29670
|
encrypted = await client.downloadBlob(diff.remoteHash);
|
|
29671
|
+
} catch {
|
|
29672
|
+
errors2.push(`Pull ${diff.path}: blob not found`);
|
|
29673
|
+
continue;
|
|
29591
29674
|
}
|
|
29592
|
-
|
|
29593
|
-
|
|
29594
|
-
|
|
29595
|
-
|
|
29596
|
-
|
|
29597
|
-
|
|
29598
|
-
|
|
29599
|
-
|
|
29600
|
-
|
|
29601
|
-
|
|
29602
|
-
|
|
29603
|
-
|
|
29604
|
-
}
|
|
29605
|
-
|
|
29606
|
-
|
|
29607
|
-
|
|
29608
|
-
|
|
29609
|
-
|
|
29610
|
-
|
|
29611
|
-
|
|
29612
|
-
|
|
29613
|
-
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29614
|
-
return;
|
|
29615
|
-
}
|
|
29616
|
-
await writeFile5(`${localPath2}.conflict`, localContent);
|
|
29617
|
-
await writeFile5(localPath2, remoteContent);
|
|
29675
|
+
}
|
|
29676
|
+
bytesTransferred += encrypted.length;
|
|
29677
|
+
const decryptedRaw = decryptFile(masterKey, diff.path, encrypted);
|
|
29678
|
+
const decrypted = decompressFromDownload(decryptedRaw);
|
|
29679
|
+
if (diff.action === "merge" && diff.localHash) {
|
|
29680
|
+
const localPath2 = join6(claudeDir, diff.path);
|
|
29681
|
+
const localContent = await readFile6(localPath2, "utf-8");
|
|
29682
|
+
const remoteContent = new TextDecoder().decode(decrypted);
|
|
29683
|
+
if (diff.category === "session") {
|
|
29684
|
+
const merged = mergeSessionFiles(localContent, remoteContent);
|
|
29685
|
+
await writeFile5(localPath2, merged);
|
|
29686
|
+
downloaded++;
|
|
29687
|
+
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29688
|
+
continue;
|
|
29689
|
+
}
|
|
29690
|
+
const entry = remoteManifest.files[diff.path];
|
|
29691
|
+
const localEntry = localManifest?.files[diff.path];
|
|
29692
|
+
if (localEntry && entry) {
|
|
29693
|
+
const result = mergeLastWriteWins(localContent, remoteContent, localEntry.modified, entry.modified);
|
|
29694
|
+
await writeFile5(localPath2, result.content);
|
|
29695
|
+
await writeFile5(`${localPath2}.conflict`, result.backupContent);
|
|
29618
29696
|
conflicts.push(diff.path);
|
|
29619
29697
|
downloaded++;
|
|
29620
|
-
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}
|
|
29621
|
-
|
|
29698
|
+
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29699
|
+
continue;
|
|
29622
29700
|
}
|
|
29623
|
-
|
|
29624
|
-
await
|
|
29625
|
-
|
|
29701
|
+
await writeFile5(`${localPath2}.conflict`, localContent);
|
|
29702
|
+
await writeFile5(localPath2, remoteContent);
|
|
29703
|
+
conflicts.push(diff.path);
|
|
29626
29704
|
downloaded++;
|
|
29627
|
-
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29628
|
-
|
|
29629
|
-
errors2.push(`Pull ${diff.path}: ${e.message}`);
|
|
29705
|
+
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path} (conflict saved)`);
|
|
29706
|
+
continue;
|
|
29630
29707
|
}
|
|
29631
|
-
|
|
29708
|
+
const localPath = join6(claudeDir, diff.path);
|
|
29709
|
+
await mkdir2(dirname(localPath), { recursive: true });
|
|
29710
|
+
await writeFile5(localPath, decrypted);
|
|
29711
|
+
downloaded++;
|
|
29712
|
+
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29713
|
+
} catch (e) {
|
|
29714
|
+
errors2.push(`Pull ${diff.path}: ${e.message}`);
|
|
29715
|
+
}
|
|
29632
29716
|
}
|
|
29633
29717
|
progress("Saving manifest...");
|
|
29634
29718
|
await saveManifest(configDir, remoteManifest);
|