ccsini 0.1.24 → 0.1.26

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 +139 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -27996,7 +27996,7 @@ var {
27996
27996
  } = import__.default;
27997
27997
 
27998
27998
  // src/version.ts
27999
- var VERSION = "0.1.24";
27999
+ var VERSION = "0.1.26";
28000
28000
 
28001
28001
  // src/commands/init.ts
28002
28002
  init_source();
@@ -28737,7 +28737,13 @@ function buildDefaultSchema() {
28737
28737
  // src/hooks/installer.ts
28738
28738
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
28739
28739
  import { join as join3 } from "path";
28740
- var HOOK_MARKER = "ccsini-auto-sync";
28740
+ var HOOK_MARKER = "ccsini auto-";
28741
+ var OLD_HOOK_MARKER = "ccsini-auto-sync";
28742
+ var OLD_COMMAND_SUFFIX = /\s*#\s*ccsini-auto-sync$/;
28743
+ function isCcsiniHook(hookEntry) {
28744
+ const s = JSON.stringify(hookEntry);
28745
+ return s.includes(HOOK_MARKER) || s.includes(OLD_HOOK_MARKER);
28746
+ }
28741
28747
  async function installHooks(claudeDir) {
28742
28748
  const settingsPath = join3(claudeDir, "settings.json");
28743
28749
  let settings = {};
@@ -28747,26 +28753,26 @@ async function installHooks(claudeDir) {
28747
28753
  } catch {}
28748
28754
  settings.hooks = settings.hooks ?? {};
28749
28755
  settings.hooks.PreToolUse = [
28750
- ...(settings.hooks.PreToolUse ?? []).filter((h) => !h.command?.includes(HOOK_MARKER))
28756
+ ...(settings.hooks.PreToolUse ?? []).filter((h) => !isCcsiniHook(h))
28751
28757
  ];
28752
28758
  settings.hooks.Stop = [
28753
- ...(settings.hooks.Stop ?? []).filter((h) => !JSON.stringify(h).includes(HOOK_MARKER)),
28759
+ ...(settings.hooks.Stop ?? []).filter((h) => !isCcsiniHook(h)),
28754
28760
  {
28755
28761
  hooks: [
28756
28762
  {
28757
28763
  type: "command",
28758
- command: `ccsini auto-push # ${HOOK_MARKER}`
28764
+ command: "ccsini auto-push"
28759
28765
  }
28760
28766
  ]
28761
28767
  }
28762
28768
  ];
28763
28769
  settings.hooks.Notification = [
28764
- ...(settings.hooks.Notification ?? []).filter((h) => !JSON.stringify(h).includes(HOOK_MARKER)),
28770
+ ...(settings.hooks.Notification ?? []).filter((h) => !isCcsiniHook(h)),
28765
28771
  {
28766
28772
  hooks: [
28767
28773
  {
28768
28774
  type: "command",
28769
- command: `ccsini auto-pull # ${HOOK_MARKER}`
28775
+ command: "ccsini auto-pull"
28770
28776
  }
28771
28777
  ]
28772
28778
  }
@@ -28778,7 +28784,7 @@ async function hooksInstalled(claudeDir) {
28778
28784
  const raw = await readFile3(join3(claudeDir, "settings.json"), "utf-8");
28779
28785
  const settings = JSON.parse(raw);
28780
28786
  const allHooks = JSON.stringify(settings.hooks ?? {});
28781
- return allHooks.includes(HOOK_MARKER);
28787
+ return allHooks.includes(HOOK_MARKER) || allHooks.includes(OLD_HOOK_MARKER);
28782
28788
  } catch {
28783
28789
  return false;
28784
28790
  }
@@ -28805,13 +28811,18 @@ async function fixHooks(claudeDir) {
28805
28811
  if (!Array.isArray(settings.hooks[hookType]))
28806
28812
  continue;
28807
28813
  for (const hookEntry of settings.hooks[hookType]) {
28808
- if (!JSON.stringify(hookEntry).includes(HOOK_MARKER))
28814
+ if (!isCcsiniHook(hookEntry))
28809
28815
  continue;
28810
28816
  if ("matcher" in hookEntry && typeof hookEntry.matcher !== "string") {
28811
28817
  delete hookEntry.matcher;
28812
28818
  details.push(`Removed invalid matcher from ${hookType} hook`);
28813
28819
  fixed++;
28814
28820
  }
28821
+ if (typeof hookEntry.command === "string" && OLD_COMMAND_SUFFIX.test(hookEntry.command)) {
28822
+ hookEntry.command = hookEntry.command.replace(OLD_COMMAND_SUFFIX, "");
28823
+ details.push(`Stripped old marker suffix from ${hookType} command`);
28824
+ fixed++;
28825
+ }
28815
28826
  if (Array.isArray(hookEntry.hooks)) {
28816
28827
  for (const innerHook of hookEntry.hooks) {
28817
28828
  if ("matcher" in innerHook && typeof innerHook.matcher !== "string") {
@@ -28819,6 +28830,11 @@ async function fixHooks(claudeDir) {
28819
28830
  details.push(`Removed invalid matcher from ${hookType} inner hook`);
28820
28831
  fixed++;
28821
28832
  }
28833
+ if (typeof innerHook.command === "string" && OLD_COMMAND_SUFFIX.test(innerHook.command)) {
28834
+ innerHook.command = innerHook.command.replace(OLD_COMMAND_SUFFIX, "");
28835
+ details.push(`Stripped old marker suffix from ${hookType} inner command`);
28836
+ fixed++;
28837
+ }
28822
28838
  }
28823
28839
  }
28824
28840
  }
@@ -28850,16 +28866,22 @@ async function checkHooksHealth(claudeDir) {
28850
28866
  if (!Array.isArray(settings.hooks[hookType]))
28851
28867
  continue;
28852
28868
  for (const hookEntry of settings.hooks[hookType]) {
28853
- if (!JSON.stringify(hookEntry).includes(HOOK_MARKER))
28869
+ if (!isCcsiniHook(hookEntry))
28854
28870
  continue;
28855
28871
  if ("matcher" in hookEntry && typeof hookEntry.matcher !== "string") {
28856
28872
  issues.push(`${hookType}: matcher is ${typeof hookEntry.matcher}, expected string`);
28857
28873
  }
28874
+ if (typeof hookEntry.command === "string" && OLD_COMMAND_SUFFIX.test(hookEntry.command)) {
28875
+ issues.push(`${hookType}: command has old marker suffix that causes argument errors`);
28876
+ }
28858
28877
  if (Array.isArray(hookEntry.hooks)) {
28859
28878
  for (const innerHook of hookEntry.hooks) {
28860
28879
  if ("matcher" in innerHook && typeof innerHook.matcher !== "string") {
28861
28880
  issues.push(`${hookType} inner hook: matcher is ${typeof innerHook.matcher}, expected string`);
28862
28881
  }
28882
+ if (typeof innerHook.command === "string" && OLD_COMMAND_SUFFIX.test(innerHook.command)) {
28883
+ issues.push(`${hookType} inner hook: command has old marker suffix that causes argument errors`);
28884
+ }
28863
28885
  }
28864
28886
  }
28865
28887
  }
@@ -29338,6 +29360,8 @@ function chunkArray(arr, size) {
29338
29360
  }
29339
29361
 
29340
29362
  // src/commands/init.ts
29363
+ import { writeFile as writeFile6 } from "fs/promises";
29364
+ import { join as join7 } from "path";
29341
29365
  function registerInitCommand(program2) {
29342
29366
  program2.command("init").description("Connect this device to your ccsini account").option("--token <token>", "Setup token from dashboard").action(async (opts) => {
29343
29367
  const configDir = getConfigDir();
@@ -29368,24 +29392,6 @@ function registerInitCommand(program2) {
29368
29392
  ]);
29369
29393
  token = answer.token;
29370
29394
  }
29371
- const { password } = await dist_default12.prompt([
29372
- {
29373
- type: "password",
29374
- name: "password",
29375
- message: "Set your encryption password (encrypts data locally):",
29376
- mask: "*",
29377
- validate: (v) => v.length >= 8 || "Password must be at least 8 characters"
29378
- }
29379
- ]);
29380
- const { confirmPassword } = await dist_default12.prompt([
29381
- {
29382
- type: "password",
29383
- name: "confirmPassword",
29384
- message: "Confirm encryption password:",
29385
- mask: "*",
29386
- validate: (v) => v === password || "Passwords do not match"
29387
- }
29388
- ]);
29389
29395
  const { deviceName } = await dist_default12.prompt([
29390
29396
  {
29391
29397
  type: "input",
@@ -29395,30 +29401,105 @@ function registerInitCommand(program2) {
29395
29401
  }
29396
29402
  ]);
29397
29403
  const spinner = ora("Setting up...").start();
29404
+ let keypair;
29405
+ let publicKeyB64;
29406
+ let privateKeyB64;
29407
+ let deviceId;
29408
+ let existingSalt;
29398
29409
  try {
29399
29410
  spinner.text = "Generating device keypair...";
29400
- const keypair = await generateDeviceKeypair();
29401
- const publicKeyB64 = await exportPublicKey(keypair.publicKey);
29402
- const privateKeyB64 = await exportPrivateKey(keypair.privateKey);
29411
+ keypair = await generateDeviceKeypair();
29412
+ publicKeyB64 = await exportPublicKey(keypair.publicKey);
29413
+ privateKeyB64 = await exportPrivateKey(keypair.privateKey);
29403
29414
  spinner.text = "Registering device...";
29404
29415
  const client = new CcsiniClient("https://ccsini-api.anis-maisara190.workers.dev", "");
29405
- const { deviceId, salt: existingSalt } = await client.registerDevice(token, deviceName, publicKeyB64, detectPlatform());
29406
- spinner.text = "Setting up encryption...";
29407
- let salt;
29408
- if (existingSalt) {
29409
- salt = new Uint8Array(Buffer.from(existingSalt, "hex"));
29410
- } else {
29411
- salt = crypto.getRandomValues(new Uint8Array(32));
29416
+ const result = await client.registerDevice(token, deviceName, publicKeyB64, detectPlatform());
29417
+ deviceId = result.deviceId;
29418
+ existingSalt = result.salt;
29419
+ spinner.stop();
29420
+ } catch (e) {
29421
+ spinner.fail(e.message);
29422
+ process.exit(1);
29423
+ }
29424
+ const isMultiDevice = !!existingSalt;
29425
+ let masterKey;
29426
+ let salt;
29427
+ if (isMultiDevice) {
29428
+ salt = new Uint8Array(Buffer.from(existingSalt, "hex"));
29429
+ const MAX_ATTEMPTS = 3;
29430
+ for (let attempt = 1;attempt <= MAX_ATTEMPTS; attempt++) {
29431
+ const { password } = await dist_default12.prompt([
29432
+ {
29433
+ type: "password",
29434
+ name: "password",
29435
+ message: "Enter your encryption password (same as your first device):",
29436
+ mask: "*",
29437
+ validate: (v) => v.length >= 8 || "Password must be at least 8 characters"
29438
+ }
29439
+ ]);
29440
+ const derivingSpinner = ora("Deriving encryption key...").start();
29441
+ masterKey = await deriveKeyFromPassphrase(password, salt);
29442
+ try {
29443
+ const { importPrivateKey: importPrivateKey2, createDeviceJWT: createDeviceJWT2 } = await Promise.resolve().then(() => (init_auth(), exports_auth));
29444
+ const privateKey = await importPrivateKey2(privateKeyB64);
29445
+ const jwt = await createDeviceJWT2(privateKey, deviceId);
29446
+ const authedClient = new CcsiniClient("https://ccsini-api.anis-maisara190.workers.dev", jwt);
29447
+ derivingSpinner.text = "Validating password...";
29448
+ const manifestEnc = await authedClient.getManifest();
29449
+ if (manifestEnc) {
29450
+ decryptFile(masterKey, "__manifest__", manifestEnc);
29451
+ }
29452
+ derivingSpinner.stop();
29453
+ break;
29454
+ } catch {
29455
+ derivingSpinner.stop();
29456
+ if (attempt < MAX_ATTEMPTS) {
29457
+ console.log(source_default.red(` Password doesn't match your first device. ${MAX_ATTEMPTS - attempt} attempt(s) remaining.`));
29458
+ } else {
29459
+ console.log(source_default.red(`
29460
+ Too many failed attempts. Please verify your password and try again.`));
29461
+ process.exit(1);
29462
+ }
29463
+ }
29412
29464
  }
29413
- await deriveKeyFromPassphrase(password, salt);
29414
- if (!existingSalt) {
29465
+ } else {
29466
+ const { password } = await dist_default12.prompt([
29467
+ {
29468
+ type: "password",
29469
+ name: "password",
29470
+ message: "Set your encryption password (encrypts data locally):",
29471
+ mask: "*",
29472
+ validate: (v) => v.length >= 8 || "Password must be at least 8 characters"
29473
+ }
29474
+ ]);
29475
+ await dist_default12.prompt([
29476
+ {
29477
+ type: "password",
29478
+ name: "confirmPassword",
29479
+ message: "Confirm encryption password:",
29480
+ mask: "*",
29481
+ validate: (v) => v === password || "Passwords do not match"
29482
+ }
29483
+ ]);
29484
+ salt = crypto.getRandomValues(new Uint8Array(32));
29485
+ const derivingSpinner = ora("Deriving encryption key...").start();
29486
+ masterKey = await deriveKeyFromPassphrase(password, salt);
29487
+ derivingSpinner.stop();
29488
+ const uploadSpinner = ora("Uploading encryption salt...").start();
29489
+ try {
29415
29490
  const { importPrivateKey: importPrivateKey2, createDeviceJWT: createDeviceJWT2 } = await Promise.resolve().then(() => (init_auth(), exports_auth));
29416
29491
  const privateKey = await importPrivateKey2(privateKeyB64);
29417
29492
  const jwt = await createDeviceJWT2(privateKey, deviceId);
29418
29493
  const authedClient = new CcsiniClient("https://ccsini-api.anis-maisara190.workers.dev", jwt);
29419
29494
  await authedClient.putSalt(Buffer.from(salt).toString("hex"));
29495
+ uploadSpinner.stop();
29496
+ } catch (e) {
29497
+ uploadSpinner.fail(e.message);
29498
+ process.exit(1);
29420
29499
  }
29421
- spinner.text = "Saving configuration...";
29500
+ }
29501
+ const saveSpinner = ora("Saving configuration...").start();
29502
+ try {
29422
29503
  await saveKeys(configDir, {
29423
29504
  salt,
29424
29505
  devicePrivateKey: privateKeyB64,
@@ -29427,10 +29508,13 @@ function registerInitCommand(program2) {
29427
29508
  deviceName,
29428
29509
  apiUrl: "https://ccsini-api.anis-maisara190.workers.dev"
29429
29510
  });
29511
+ await writeFile6(join7(configDir, ".cached-key"), masterKey, {
29512
+ mode: 384
29513
+ });
29430
29514
  await saveDefaultSchema(configDir);
29431
- spinner.text = "Installing Claude Code hooks...";
29515
+ saveSpinner.text = "Installing Claude Code hooks...";
29432
29516
  await installHooks(getClaudeDir());
29433
- spinner.succeed("Setup complete!");
29517
+ saveSpinner.succeed("Setup complete!");
29434
29518
  console.log(source_default.green(`
29435
29519
  \u2713 Encryption keys generated`));
29436
29520
  console.log(source_default.green(" \u2713 Device registered"));
@@ -29440,7 +29524,7 @@ function registerInitCommand(program2) {
29440
29524
  Just use 'claude' as normal. Sync happens automatically.
29441
29525
  `));
29442
29526
  } catch (e) {
29443
- spinner.fail(e.message);
29527
+ saveSpinner.fail(e.message);
29444
29528
  process.exit(1);
29445
29529
  }
29446
29530
  });
@@ -29457,10 +29541,10 @@ function detectPlatform() {
29457
29541
  // src/commands/auto.ts
29458
29542
  init_auth();
29459
29543
  init_auth();
29460
- import { readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
29461
- import { join as join7 } from "path";
29544
+ import { readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
29545
+ import { join as join8 } from "path";
29462
29546
  async function getMasterKey(configDir) {
29463
- const cachedKeyPath = join7(configDir, ".cached-key");
29547
+ const cachedKeyPath = join8(configDir, ".cached-key");
29464
29548
  try {
29465
29549
  const cached = await readFile7(cachedKeyPath);
29466
29550
  return new Uint8Array(cached);
@@ -29520,7 +29604,7 @@ function registerAutoCommands(program2) {
29520
29604
  ]);
29521
29605
  const config = await loadKeys(configDir);
29522
29606
  const masterKey = await deriveKeyFromPassphrase(password, config.salt);
29523
- await writeFile6(join7(configDir, ".cached-key"), masterKey, {
29607
+ await writeFile7(join8(configDir, ".cached-key"), masterKey, {
29524
29608
  mode: 384
29525
29609
  });
29526
29610
  console.log("Unlocked. Auto-sync is now active.");
@@ -29529,7 +29613,7 @@ function registerAutoCommands(program2) {
29529
29613
  const configDir = getConfigDir();
29530
29614
  const { rm } = await import("fs/promises");
29531
29615
  try {
29532
- await rm(join7(configDir, ".cached-key"));
29616
+ await rm(join8(configDir, ".cached-key"));
29533
29617
  console.log("Locked. Auto-sync paused until next unlock.");
29534
29618
  } catch {
29535
29619
  console.log("Already locked.");
@@ -29540,7 +29624,7 @@ function registerAutoCommands(program2) {
29540
29624
  // src/commands/doctor.ts
29541
29625
  init_source();
29542
29626
  import { access as access2 } from "fs/promises";
29543
- import { join as join8 } from "path";
29627
+ import { join as join9 } from "path";
29544
29628
  function registerDoctorCommand(program2) {
29545
29629
  program2.command("doctor").description("Diagnose ccsini setup and connectivity").action(async () => {
29546
29630
  const configDir = getConfigDir();
@@ -29582,7 +29666,7 @@ function registerDoctorCommand(program2) {
29582
29666
  allGood = false;
29583
29667
  }
29584
29668
  try {
29585
- await access2(join8(configDir, ".cached-key"));
29669
+ await access2(join9(configDir, ".cached-key"));
29586
29670
  console.log(source_default.green(" \u2713 Auto-sync unlocked"));
29587
29671
  } catch {
29588
29672
  console.log(source_default.yellow(" \u26A0 Auto-sync locked (run 'ccsini unlock')"));
@@ -29690,9 +29774,9 @@ Uninstall failed. Try manually:`);
29690
29774
  // src/commands/sync.ts
29691
29775
  init_auth();
29692
29776
  import { readFile as readFile8 } from "fs/promises";
29693
- import { join as join9 } from "path";
29777
+ import { join as join10 } from "path";
29694
29778
  async function getMasterKey2(configDir) {
29695
- const cachedKeyPath = join9(configDir, ".cached-key");
29779
+ const cachedKeyPath = join10(configDir, ".cached-key");
29696
29780
  try {
29697
29781
  const cached = await readFile8(cachedKeyPath);
29698
29782
  return new Uint8Array(cached);
@@ -29708,8 +29792,8 @@ async function getMasterKey2(configDir) {
29708
29792
  ]);
29709
29793
  const config = await loadKeys(configDir);
29710
29794
  const masterKey = await deriveKeyFromPassphrase(password, config.salt);
29711
- const { writeFile: writeFile7 } = await import("fs/promises");
29712
- await writeFile7(cachedKeyPath, masterKey, { mode: 384 });
29795
+ const { writeFile: writeFile8 } = await import("fs/promises");
29796
+ await writeFile8(cachedKeyPath, masterKey, { mode: 384 });
29713
29797
  return masterKey;
29714
29798
  }
29715
29799
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsini",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Claude Code seamless sync across devices",
5
5
  "type": "module",
6
6
  "bin": {