ccsini 0.1.59 → 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.
Files changed (2) hide show
  1. package/dist/index.js +239 -102
  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"],
@@ -28005,6 +28005,42 @@ var init_auth = __esm(() => {
28005
28005
  init_src();
28006
28006
  });
28007
28007
 
28008
+ // src/core/schema.ts
28009
+ var exports_schema = {};
28010
+ __export(exports_schema, {
28011
+ saveDefaultSchema: () => saveDefaultSchema,
28012
+ loadSchema: () => loadSchema,
28013
+ getEnabledCategories: () => getEnabledCategories
28014
+ });
28015
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
28016
+ import { join as join2 } from "path";
28017
+ async function saveDefaultSchema(configDir) {
28018
+ const schema = buildDefaultSchema();
28019
+ await writeFile2(join2(configDir, "schema.yaml"), JSON.stringify(schema, null, 2));
28020
+ }
28021
+ async function loadSchema(configDir) {
28022
+ const raw = await readFile2(join2(configDir, "schema.yaml"), "utf-8");
28023
+ return JSON.parse(raw);
28024
+ }
28025
+ function getEnabledCategories(schema) {
28026
+ return Object.entries(schema.categories).filter(([, config]) => config.enabled).map(([name]) => name);
28027
+ }
28028
+ function buildDefaultSchema() {
28029
+ const categories = {};
28030
+ for (const [cat, patterns] of Object.entries(DEFAULT_CATEGORY_PATTERNS)) {
28031
+ const category = cat;
28032
+ categories[category] = {
28033
+ enabled: true,
28034
+ patterns,
28035
+ merge: MERGE_STRATEGIES[category]
28036
+ };
28037
+ }
28038
+ return { version: 1, categories };
28039
+ }
28040
+ var init_schema = __esm(() => {
28041
+ init_src();
28042
+ });
28043
+
28008
28044
  // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
28009
28045
  var import__ = __toESM(require_commander(), 1);
28010
28046
  var {
@@ -28022,7 +28058,7 @@ var {
28022
28058
  } = import__.default;
28023
28059
 
28024
28060
  // src/version.ts
28025
- var VERSION = "0.1.59";
28061
+ var VERSION = "0.1.61";
28026
28062
 
28027
28063
  // src/commands/init.ts
28028
28064
  init_source();
@@ -28765,26 +28801,8 @@ async function saveSessionConfig(configDir, sessionConfig) {
28765
28801
  await writeFile(configPath, JSON.stringify(config, null, 2));
28766
28802
  }
28767
28803
 
28768
- // src/core/schema.ts
28769
- init_src();
28770
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
28771
- import { join as join2 } from "path";
28772
- async function saveDefaultSchema(configDir) {
28773
- const schema = buildDefaultSchema();
28774
- await writeFile2(join2(configDir, "schema.yaml"), JSON.stringify(schema, null, 2));
28775
- }
28776
- function buildDefaultSchema() {
28777
- const categories = {};
28778
- for (const [cat, patterns] of Object.entries(DEFAULT_CATEGORY_PATTERNS)) {
28779
- const category = cat;
28780
- categories[category] = {
28781
- enabled: true,
28782
- patterns,
28783
- merge: MERGE_STRATEGIES[category]
28784
- };
28785
- }
28786
- return { version: 1, categories };
28787
- }
28804
+ // src/commands/init.ts
28805
+ init_schema();
28788
28806
 
28789
28807
  // src/hooks/installer.ts
28790
28808
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
@@ -28949,6 +28967,23 @@ class CcsiniClient {
28949
28967
  this.apiUrl = apiUrl.replace(/\/$/, "");
28950
28968
  this.token = token;
28951
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
+ }
28952
28987
  getHeaders() {
28953
28988
  return {
28954
28989
  Authorization: `Bearer ${this.token}`,
@@ -28977,7 +29012,7 @@ class CcsiniClient {
28977
29012
  throw new Error("Failed to store salt");
28978
29013
  }
28979
29014
  async getManifest() {
28980
- const res = await fetch(`${this.apiUrl}/api/sync/manifest`, {
29015
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/manifest`, {
28981
29016
  headers: this.getHeaders()
28982
29017
  });
28983
29018
  if (!res.ok)
@@ -28988,7 +29023,7 @@ class CcsiniClient {
28988
29023
  return new Uint8Array(Buffer.from(data.manifest, "base64"));
28989
29024
  }
28990
29025
  async putManifest(encryptedManifest) {
28991
- const res = await fetch(`${this.apiUrl}/api/sync/manifest`, {
29026
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/manifest`, {
28992
29027
  method: "PUT",
28993
29028
  headers: {
28994
29029
  Authorization: `Bearer ${this.token}`,
@@ -29000,7 +29035,7 @@ class CcsiniClient {
29000
29035
  throw new Error("Failed to upload manifest");
29001
29036
  }
29002
29037
  async uploadBlob(hash, encryptedData) {
29003
- const res = await fetch(`${this.apiUrl}/api/sync/blob/${hash}`, {
29038
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blob/${hash}`, {
29004
29039
  method: "PUT",
29005
29040
  headers: {
29006
29041
  Authorization: `Bearer ${this.token}`,
@@ -29012,7 +29047,7 @@ class CcsiniClient {
29012
29047
  throw new Error(`Failed to upload blob ${hash}`);
29013
29048
  }
29014
29049
  async downloadBlob(hash) {
29015
- const res = await fetch(`${this.apiUrl}/api/sync/blob/${hash}`, {
29050
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blob/${hash}`, {
29016
29051
  headers: this.getHeaders()
29017
29052
  });
29018
29053
  if (!res.ok)
@@ -29021,7 +29056,7 @@ class CcsiniClient {
29021
29056
  return new Uint8Array(buffer);
29022
29057
  }
29023
29058
  async batchDownload(hashes) {
29024
- const res = await fetch(`${this.apiUrl}/api/sync/batch-download`, {
29059
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/batch-download`, {
29025
29060
  method: "POST",
29026
29061
  headers: this.getHeaders(),
29027
29062
  body: JSON.stringify({ hashes })
@@ -29036,7 +29071,7 @@ class CcsiniClient {
29036
29071
  return result;
29037
29072
  }
29038
29073
  async listBlobs() {
29039
- const res = await fetch(`${this.apiUrl}/api/sync/blobs`, {
29074
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blobs`, {
29040
29075
  headers: this.getHeaders()
29041
29076
  });
29042
29077
  if (!res.ok)
@@ -29044,7 +29079,7 @@ class CcsiniClient {
29044
29079
  return res.json();
29045
29080
  }
29046
29081
  async deleteBlobs(hashes) {
29047
- const res = await fetch(`${this.apiUrl}/api/sync/blobs/delete`, {
29082
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blobs/delete`, {
29048
29083
  method: "POST",
29049
29084
  headers: this.getHeaders(),
29050
29085
  body: JSON.stringify({ hashes })
@@ -29063,7 +29098,7 @@ class CcsiniClient {
29063
29098
  throw new Error("Failed to wipe account data");
29064
29099
  }
29065
29100
  async putProjects(projects, device) {
29066
- const res = await fetch(`${this.apiUrl}/api/sync/projects`, {
29101
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/projects`, {
29067
29102
  method: "PUT",
29068
29103
  headers: this.getHeaders(),
29069
29104
  body: JSON.stringify({ projects, device, timestamp: Date.now() })
@@ -29073,6 +29108,17 @@ class CcsiniClient {
29073
29108
  throw new Error(`Failed to send projects (${res.status}): ${text}`);
29074
29109
  }
29075
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
+ }
29076
29122
  async resetAll() {
29077
29123
  const res = await fetch(`${this.apiUrl}/api/sync/reset`, {
29078
29124
  method: "DELETE",
@@ -29083,7 +29129,7 @@ class CcsiniClient {
29083
29129
  return res.json();
29084
29130
  }
29085
29131
  async heartbeat(mode, stats) {
29086
- await fetch(`${this.apiUrl}/api/sync/heartbeat`, {
29132
+ const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/heartbeat`, {
29087
29133
  method: "POST",
29088
29134
  headers: this.getHeaders(),
29089
29135
  body: JSON.stringify({
@@ -29092,13 +29138,20 @@ class CcsiniClient {
29092
29138
  pushCount: stats.pushCount,
29093
29139
  pullCount: stats.pullCount,
29094
29140
  lastError: stats.lastError,
29095
- conflicts: stats.conflicts
29141
+ conflicts: stats.conflicts,
29142
+ version: stats.version,
29143
+ categories: stats.categories,
29144
+ conflictFiles: stats.conflictFiles
29096
29145
  }
29097
29146
  })
29098
29147
  });
29148
+ if (res.ok) {
29149
+ return res.json();
29150
+ }
29151
+ return {};
29099
29152
  }
29100
29153
  async logSyncEvent(event) {
29101
- await fetch(`${this.apiUrl}/api/sync/log`, {
29154
+ await this.fetchWithRetry(`${this.apiUrl}/api/sync/log`, {
29102
29155
  method: "POST",
29103
29156
  headers: this.getHeaders(),
29104
29157
  body: JSON.stringify(event)
@@ -29111,6 +29164,7 @@ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir2 } from
29111
29164
  import { join as join6, dirname } from "path";
29112
29165
  import { homedir as homedir3 } from "os";
29113
29166
  import { createHash as createHash2 } from "crypto";
29167
+ import { gzipSync, gunzipSync } from "zlib";
29114
29168
 
29115
29169
  // src/core/manifest.ts
29116
29170
  import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
@@ -29377,6 +29431,30 @@ init_src();
29377
29431
  function blobKey(filePath, contentHash) {
29378
29432
  return createHash2("sha256").update(`${filePath}:${contentHash}`).digest("hex");
29379
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
+ }
29380
29458
  function getClaudeDir() {
29381
29459
  return join6(homedir3(), ".claude");
29382
29460
  }
@@ -29441,7 +29519,8 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
29441
29519
  } catch {
29442
29520
  encrypted = await client.downloadBlob(diff.remoteHash);
29443
29521
  }
29444
- const decrypted = decryptFile(masterKey, diff.path, encrypted);
29522
+ const decryptedRaw = decryptFile(masterKey, diff.path, encrypted);
29523
+ const decrypted = decompressFromDownload(decryptedRaw);
29445
29524
  const conflictPath = join6(claudeDir, `${diff.path}.conflict`);
29446
29525
  await mkdir2(dirname(conflictPath), { recursive: true });
29447
29526
  await writeFile5(conflictPath, decrypted);
@@ -29450,30 +29529,32 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
29450
29529
  } catch {}
29451
29530
  }
29452
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)`);
29453
29541
  let uploaded = 0;
29454
29542
  const failedPaths = new Set;
29455
- const chunks = chunkArray(toPush, MAX_CONCURRENT_TRANSFERS);
29543
+ const chunks = chunkArray(toUpload, MAX_CONCURRENT_TRANSFERS);
29456
29544
  for (const chunk of chunks) {
29457
29545
  await Promise.all(chunk.map(async (diff) => {
29458
- const MAX_RETRIES = 3;
29459
- for (let attempt = 1;attempt <= MAX_RETRIES; attempt++) {
29460
- try {
29461
- const filePath = join6(claudeDir, diff.path);
29462
- const content = await readFile6(filePath);
29463
- const encrypted = encryptFile(masterKey, diff.path, content);
29464
- await client.uploadBlob(blobKey(diff.path, diff.localHash), encrypted);
29465
- bytesTransferred += encrypted.length;
29466
- uploaded++;
29467
- progress(`Uploading ${uploaded}/${toPush.length}: ${diff.path}`);
29468
- return;
29469
- } catch (e) {
29470
- if (attempt < MAX_RETRIES) {
29471
- await new Promise((r) => setTimeout(r, 500 * attempt));
29472
- } else {
29473
- failedPaths.add(diff.path);
29474
- errors2.push(`Push ${diff.path}: ${e.message}`);
29475
- }
29476
- }
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}`);
29477
29558
  }
29478
29559
  }));
29479
29560
  }
@@ -29554,56 +29635,84 @@ async function pullSync(client, masterKey, deviceName, configDir, onProgress, se
29554
29635
  const toPull = diffs.filter((d) => (d.action === "pull" || d.action === "merge") && !isExcluded(d.path) && (sessionsEnabled || !isSessionFile(d.path)));
29555
29636
  progress(`${toPull.length} files to pull`);
29556
29637
  let downloaded = 0;
29557
- const chunks = chunkArray(toPull, MAX_CONCURRENT_TRANSFERS);
29558
- for (const chunk of chunks) {
29559
- await Promise.all(chunk.map(async (diff) => {
29560
- try {
29561
- let encrypted;
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) {
29562
29669
  try {
29563
- encrypted = await client.downloadBlob(blobKey(diff.path, diff.remoteHash));
29564
- } catch {
29565
29670
  encrypted = await client.downloadBlob(diff.remoteHash);
29671
+ } catch {
29672
+ errors2.push(`Pull ${diff.path}: blob not found`);
29673
+ continue;
29566
29674
  }
29567
- bytesTransferred += encrypted.length;
29568
- const decrypted = decryptFile(masterKey, diff.path, encrypted);
29569
- if (diff.action === "merge" && diff.localHash) {
29570
- const localPath2 = join6(claudeDir, diff.path);
29571
- const localContent = await readFile6(localPath2, "utf-8");
29572
- const remoteContent = new TextDecoder().decode(decrypted);
29573
- if (diff.category === "session") {
29574
- const merged = mergeSessionFiles(localContent, remoteContent);
29575
- await writeFile5(localPath2, merged);
29576
- downloaded++;
29577
- progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29578
- return;
29579
- }
29580
- const entry = remoteManifest.files[diff.path];
29581
- const localEntry = localManifest?.files[diff.path];
29582
- if (localEntry && entry) {
29583
- const result = mergeLastWriteWins(localContent, remoteContent, localEntry.modified, entry.modified);
29584
- await writeFile5(localPath2, result.content);
29585
- await writeFile5(`${localPath2}.conflict`, result.backupContent);
29586
- conflicts.push(diff.path);
29587
- downloaded++;
29588
- progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29589
- return;
29590
- }
29591
- await writeFile5(`${localPath2}.conflict`, localContent);
29592
- 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);
29593
29696
  conflicts.push(diff.path);
29594
29697
  downloaded++;
29595
- progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path} (conflict saved)`);
29596
- return;
29698
+ progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29699
+ continue;
29597
29700
  }
29598
- const localPath = join6(claudeDir, diff.path);
29599
- await mkdir2(dirname(localPath), { recursive: true });
29600
- await writeFile5(localPath, decrypted);
29701
+ await writeFile5(`${localPath2}.conflict`, localContent);
29702
+ await writeFile5(localPath2, remoteContent);
29703
+ conflicts.push(diff.path);
29601
29704
  downloaded++;
29602
- progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
29603
- } catch (e) {
29604
- errors2.push(`Pull ${diff.path}: ${e.message}`);
29705
+ progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path} (conflict saved)`);
29706
+ continue;
29605
29707
  }
29606
- }));
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
+ }
29607
29716
  }
29608
29717
  progress("Saving manifest...");
29609
29718
  await saveManifest(configDir, remoteManifest);
@@ -31766,25 +31875,53 @@ async function runDaemon() {
31766
31875
  log(configDir, `Pull error: ${e.message}`);
31767
31876
  }
31768
31877
  }
31769
- async function countConflicts() {
31878
+ async function listConflictFiles() {
31770
31879
  try {
31771
31880
  const { readdir: readdir4 } = await import("fs/promises");
31772
31881
  const files = await readdir4(configDir);
31773
- return files.filter((f) => f.endsWith(".conflict")).length;
31882
+ return files.filter((f) => f.endsWith(".conflict"));
31774
31883
  } catch {
31775
- return 0;
31884
+ return [];
31776
31885
  }
31777
31886
  }
31887
+ async function getCategories() {
31888
+ try {
31889
+ const { loadSchema: loadSchema2, getEnabledCategories: getEnabledCategories2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
31890
+ const schema = await loadSchema2(configDir);
31891
+ return getEnabledCategories2(schema);
31892
+ } catch {
31893
+ return [];
31894
+ }
31895
+ }
31896
+ async function deleteConflictFiles() {
31897
+ try {
31898
+ const { unlink: unlink2 } = await import("fs/promises");
31899
+ const files = await listConflictFiles();
31900
+ for (const f of files) {
31901
+ await unlink2(join11(configDir, f)).catch(() => {});
31902
+ }
31903
+ if (files.length > 0) {
31904
+ log(configDir, `Resolved ${files.length} conflict file(s) via dashboard`);
31905
+ }
31906
+ } catch {}
31907
+ }
31778
31908
  async function doHeartbeat() {
31779
31909
  try {
31780
31910
  const client = await getAuthenticatedClient(configDir);
31781
- const conflicts = await countConflicts();
31782
- await client.heartbeat("daemon", {
31911
+ const conflictFiles = await listConflictFiles();
31912
+ const categories = await getCategories();
31913
+ const result = await client.heartbeat("daemon", {
31783
31914
  pushCount: status.pushCount,
31784
31915
  pullCount: status.pullCount,
31785
31916
  lastError: status.lastError,
31786
- conflicts
31917
+ conflicts: conflictFiles.length,
31918
+ version: VERSION,
31919
+ categories,
31920
+ conflictFiles
31787
31921
  });
31922
+ if (result.resolveConflicts) {
31923
+ await deleteConflictFiles();
31924
+ }
31788
31925
  } catch (e) {
31789
31926
  log(configDir, `Heartbeat error: ${e.message}`);
31790
31927
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsini",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "description": "Claude Code seamless sync across devices",
5
5
  "type": "module",
6
6
  "bin": {