ccsini 0.1.60 → 0.1.62

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 (2) hide show
  1. package/dist/index.js +195 -93
  2. 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.60";
28061
+ var VERSION = "0.1.62";
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 fetch(`${this.apiUrl}/api/sync/manifest`, {
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 fetch(`${this.apiUrl}/api/sync/manifest`, {
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 fetch(`${this.apiUrl}/api/sync/blob/${hash}`, {
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 fetch(`${this.apiUrl}/api/sync/blob/${hash}`, {
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 fetch(`${this.apiUrl}/api/sync/batch-download`, {
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 fetch(`${this.apiUrl}/api/sync/blobs`, {
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 fetch(`${this.apiUrl}/api/sync/blobs/delete`, {
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 fetch(`${this.apiUrl}/api/sync/projects`, {
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 fetch(`${this.apiUrl}/api/sync/heartbeat`, {
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 fetch(`${this.apiUrl}/api/sync/log`, {
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
  }
@@ -29438,11 +29491,13 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
29438
29491
  const conflicts = [];
29439
29492
  let bytesTransferred = 0;
29440
29493
  const progress = onProgress ?? (() => {});
29441
- const { manifest: localManifest, cacheStats } = await generateManifest(claudeDir, deviceName, progress, sessionOptions);
29494
+ progress("Scanning local files & fetching remote manifest...");
29495
+ const [{ manifest: localManifest, cacheStats }, remoteManifestEnc] = await Promise.all([
29496
+ generateManifest(claudeDir, deviceName, progress, sessionOptions),
29497
+ client.getManifest()
29498
+ ]);
29442
29499
  const fileCount = Object.keys(localManifest.files).length;
29443
29500
  progress(`Scanned ${fileCount} files`);
29444
- progress("Fetching remote manifest...");
29445
- const remoteManifestEnc = await client.getManifest();
29446
29501
  let remoteManifest = null;
29447
29502
  if (remoteManifestEnc) {
29448
29503
  try {
@@ -29456,6 +29511,17 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
29456
29511
  const diffs = diffManifests(localManifest, remoteManifest);
29457
29512
  const toPush = diffs.filter((d) => d.action === "push" || d.action === "merge");
29458
29513
  progress(`${toPush.length} files to push`);
29514
+ if (toPush.length === 0) {
29515
+ return {
29516
+ action: "push",
29517
+ filesChanged: 0,
29518
+ bytesTransferred: 0,
29519
+ durationMs: Date.now() - start,
29520
+ errors: [],
29521
+ conflicts: [],
29522
+ cacheStats
29523
+ };
29524
+ }
29459
29525
  if (remoteManifest && !pushOptions?.skipConflictBackup) {
29460
29526
  const merges = toPush.filter((d) => d.action === "merge" && d.remoteHash);
29461
29527
  for (const diff of merges) {
@@ -29466,7 +29532,8 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
29466
29532
  } catch {
29467
29533
  encrypted = await client.downloadBlob(diff.remoteHash);
29468
29534
  }
29469
- const decrypted = decryptFile(masterKey, diff.path, encrypted);
29535
+ const decryptedRaw = decryptFile(masterKey, diff.path, encrypted);
29536
+ const decrypted = decompressFromDownload(decryptedRaw);
29470
29537
  const conflictPath = join6(claudeDir, `${diff.path}.conflict`);
29471
29538
  await mkdir2(dirname(conflictPath), { recursive: true });
29472
29539
  await writeFile5(conflictPath, decrypted);
@@ -29475,30 +29542,32 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
29475
29542
  } catch {}
29476
29543
  }
29477
29544
  }
29545
+ const pushKeys = toPush.map((d) => blobKey(d.path, d.localHash));
29546
+ let existing = {};
29547
+ try {
29548
+ const batchResults = await Promise.all(chunkArray(pushKeys, 100).map((batch) => client.checkBlobsExist(batch)));
29549
+ for (const r of batchResults)
29550
+ Object.assign(existing, r);
29551
+ } catch {}
29552
+ const toUpload = toPush.filter((d) => !existing[blobKey(d.path, d.localHash)]);
29553
+ progress(`${toPush.length} changed, ${toUpload.length} to upload (${toPush.length - toUpload.length} skipped)`);
29478
29554
  let uploaded = 0;
29479
29555
  const failedPaths = new Set;
29480
- const chunks = chunkArray(toPush, MAX_CONCURRENT_TRANSFERS);
29556
+ const chunks = chunkArray(toUpload, MAX_CONCURRENT_TRANSFERS);
29481
29557
  for (const chunk of chunks) {
29482
29558
  await Promise.all(chunk.map(async (diff) => {
29483
- const MAX_RETRIES = 3;
29484
- for (let attempt = 1;attempt <= MAX_RETRIES; attempt++) {
29485
- try {
29486
- const filePath = join6(claudeDir, diff.path);
29487
- const content = await readFile6(filePath);
29488
- const encrypted = encryptFile(masterKey, diff.path, content);
29489
- await client.uploadBlob(blobKey(diff.path, diff.localHash), encrypted);
29490
- bytesTransferred += encrypted.length;
29491
- uploaded++;
29492
- progress(`Uploading ${uploaded}/${toPush.length}: ${diff.path}`);
29493
- return;
29494
- } catch (e) {
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
- }
29559
+ try {
29560
+ const filePath = join6(claudeDir, diff.path);
29561
+ const content = await readFile6(filePath);
29562
+ const compressed = compressForUpload(content);
29563
+ const encrypted = encryptFile(masterKey, diff.path, compressed);
29564
+ await client.uploadBlob(blobKey(diff.path, diff.localHash), encrypted);
29565
+ bytesTransferred += encrypted.length;
29566
+ uploaded++;
29567
+ progress(`Uploading ${uploaded}/${toUpload.length}: ${diff.path}`);
29568
+ } catch (e) {
29569
+ failedPaths.add(diff.path);
29570
+ errors2.push(`Push ${diff.path}: ${e.message}`);
29502
29571
  }
29503
29572
  }));
29504
29573
  }
@@ -29510,22 +29579,21 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
29510
29579
  }
29511
29580
  const manifestJson = JSON.stringify(localManifest);
29512
29581
  const manifestEnc = encryptFile(masterKey, "__manifest__", new TextEncoder().encode(manifestJson));
29513
- await client.putManifest(manifestEnc);
29514
- await saveManifest(configDir, localManifest);
29582
+ await Promise.all([
29583
+ client.putManifest(manifestEnc),
29584
+ saveManifest(configDir, localManifest)
29585
+ ]);
29515
29586
  const durationMs = Date.now() - start;
29516
29587
  progress("Logging sync event...");
29517
- await client.logSyncEvent({
29518
- action: "push",
29519
- filesChanged: toPush.length,
29520
- bytesTransferred,
29521
- durationMs
29522
- });
29523
- try {
29524
- const projects = extractProjects(localManifest);
29525
- if (projects.length > 0) {
29526
- await client.putProjects(projects, deviceName);
29527
- }
29528
- } catch {}
29588
+ await Promise.all([
29589
+ client.logSyncEvent({
29590
+ action: "push",
29591
+ filesChanged: toPush.length,
29592
+ bytesTransferred,
29593
+ durationMs
29594
+ }).catch(() => {}),
29595
+ extractAndSendProjects(client, localManifest, deviceName).catch(() => {})
29596
+ ]);
29529
29597
  return {
29530
29598
  action: "push",
29531
29599
  filesChanged: toPush.length,
@@ -29579,56 +29647,84 @@ async function pullSync(client, masterKey, deviceName, configDir, onProgress, se
29579
29647
  const toPull = diffs.filter((d) => (d.action === "pull" || d.action === "merge") && !isExcluded(d.path) && (sessionsEnabled || !isSessionFile(d.path)));
29580
29648
  progress(`${toPull.length} files to pull`);
29581
29649
  let downloaded = 0;
29582
- const chunks = chunkArray(toPull, MAX_CONCURRENT_TRANSFERS);
29583
- for (const chunk of chunks) {
29584
- await Promise.all(chunk.map(async (diff) => {
29585
- try {
29586
- let encrypted;
29650
+ const keyToDiffs = new Map;
29651
+ for (const diff of toPull) {
29652
+ const key = blobKey(diff.path, diff.remoteHash);
29653
+ const list = keyToDiffs.get(key) ?? [];
29654
+ list.push(diff);
29655
+ keyToDiffs.set(key, list);
29656
+ }
29657
+ const uniqueKeys = Array.from(keyToDiffs.keys());
29658
+ const batches = chunkArray(uniqueKeys, BATCH_DOWNLOAD_SIZE);
29659
+ const downloadedBlobs = new Map;
29660
+ for (const batch of batches) {
29661
+ try {
29662
+ const results = await client.batchDownload(batch);
29663
+ for (const [key, data] of Object.entries(results)) {
29664
+ if (data)
29665
+ downloadedBlobs.set(key, data);
29666
+ }
29667
+ } catch {
29668
+ for (const key of batch) {
29669
+ try {
29670
+ const data = await client.downloadBlob(key);
29671
+ downloadedBlobs.set(key, data);
29672
+ } catch {}
29673
+ }
29674
+ }
29675
+ }
29676
+ for (const diff of toPull) {
29677
+ try {
29678
+ const key = blobKey(diff.path, diff.remoteHash);
29679
+ let encrypted = downloadedBlobs.get(key);
29680
+ if (!encrypted) {
29587
29681
  try {
29588
- encrypted = await client.downloadBlob(blobKey(diff.path, diff.remoteHash));
29589
- } catch {
29590
29682
  encrypted = await client.downloadBlob(diff.remoteHash);
29683
+ } catch {
29684
+ errors2.push(`Pull ${diff.path}: blob not found`);
29685
+ continue;
29591
29686
  }
29592
- bytesTransferred += encrypted.length;
29593
- const decrypted = decryptFile(masterKey, diff.path, encrypted);
29594
- if (diff.action === "merge" && diff.localHash) {
29595
- const localPath2 = join6(claudeDir, diff.path);
29596
- const localContent = await readFile6(localPath2, "utf-8");
29597
- const remoteContent = new TextDecoder().decode(decrypted);
29598
- if (diff.category === "session") {
29599
- const merged = mergeSessionFiles(localContent, remoteContent);
29600
- await writeFile5(localPath2, merged);
29601
- downloaded++;
29602
- progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29603
- return;
29604
- }
29605
- const entry = remoteManifest.files[diff.path];
29606
- const localEntry = localManifest?.files[diff.path];
29607
- if (localEntry && entry) {
29608
- const result = mergeLastWriteWins(localContent, remoteContent, localEntry.modified, entry.modified);
29609
- await writeFile5(localPath2, result.content);
29610
- await writeFile5(`${localPath2}.conflict`, result.backupContent);
29611
- conflicts.push(diff.path);
29612
- downloaded++;
29613
- progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29614
- return;
29615
- }
29616
- await writeFile5(`${localPath2}.conflict`, localContent);
29617
- await writeFile5(localPath2, remoteContent);
29687
+ }
29688
+ bytesTransferred += encrypted.length;
29689
+ const decryptedRaw = decryptFile(masterKey, diff.path, encrypted);
29690
+ const decrypted = decompressFromDownload(decryptedRaw);
29691
+ if (diff.action === "merge" && diff.localHash) {
29692
+ const localPath2 = join6(claudeDir, diff.path);
29693
+ const localContent = await readFile6(localPath2, "utf-8");
29694
+ const remoteContent = new TextDecoder().decode(decrypted);
29695
+ if (diff.category === "session") {
29696
+ const merged = mergeSessionFiles(localContent, remoteContent);
29697
+ await writeFile5(localPath2, merged);
29698
+ downloaded++;
29699
+ progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29700
+ continue;
29701
+ }
29702
+ const entry = remoteManifest.files[diff.path];
29703
+ const localEntry = localManifest?.files[diff.path];
29704
+ if (localEntry && entry) {
29705
+ const result = mergeLastWriteWins(localContent, remoteContent, localEntry.modified, entry.modified);
29706
+ await writeFile5(localPath2, result.content);
29707
+ await writeFile5(`${localPath2}.conflict`, result.backupContent);
29618
29708
  conflicts.push(diff.path);
29619
29709
  downloaded++;
29620
- progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path} (conflict saved)`);
29621
- return;
29710
+ progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29711
+ continue;
29622
29712
  }
29623
- const localPath = join6(claudeDir, diff.path);
29624
- await mkdir2(dirname(localPath), { recursive: true });
29625
- await writeFile5(localPath, decrypted);
29713
+ await writeFile5(`${localPath2}.conflict`, localContent);
29714
+ await writeFile5(localPath2, remoteContent);
29715
+ conflicts.push(diff.path);
29626
29716
  downloaded++;
29627
- progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29628
- } catch (e) {
29629
- errors2.push(`Pull ${diff.path}: ${e.message}`);
29717
+ progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path} (conflict saved)`);
29718
+ continue;
29630
29719
  }
29631
- }));
29720
+ const localPath = join6(claudeDir, diff.path);
29721
+ await mkdir2(dirname(localPath), { recursive: true });
29722
+ await writeFile5(localPath, decrypted);
29723
+ downloaded++;
29724
+ progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29725
+ } catch (e) {
29726
+ errors2.push(`Pull ${diff.path}: ${e.message}`);
29727
+ }
29632
29728
  }
29633
29729
  progress("Saving manifest...");
29634
29730
  await saveManifest(configDir, remoteManifest);
@@ -29639,7 +29735,7 @@ async function pullSync(client, masterKey, deviceName, configDir, onProgress, se
29639
29735
  filesChanged: toPull.length,
29640
29736
  bytesTransferred,
29641
29737
  durationMs
29642
- });
29738
+ }).catch(() => {});
29643
29739
  return {
29644
29740
  action: "pull",
29645
29741
  filesChanged: toPull.length,
@@ -29649,6 +29745,12 @@ async function pullSync(client, masterKey, deviceName, configDir, onProgress, se
29649
29745
  conflicts
29650
29746
  };
29651
29747
  }
29748
+ async function extractAndSendProjects(client, manifest, deviceName) {
29749
+ const projects = extractProjects(manifest);
29750
+ if (projects.length > 0) {
29751
+ await client.putProjects(projects, deviceName);
29752
+ }
29753
+ }
29652
29754
  function chunkArray(arr, size) {
29653
29755
  const chunks = [];
29654
29756
  for (let i = 0;i < arr.length; i += size) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsini",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "Claude Code seamless sync across devices",
5
5
  "type": "module",
6
6
  "bin": {