claudectx 1.1.0 → 1.1.1

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.mjs CHANGED
@@ -2225,12 +2225,12 @@ async function handleLogStdin() {
2225
2225
  }
2226
2226
  }
2227
2227
  function readStdin() {
2228
- return new Promise((resolve11) => {
2228
+ return new Promise((resolve12) => {
2229
2229
  let data = "";
2230
2230
  process.stdin.setEncoding("utf-8");
2231
2231
  process.stdin.on("data", (chunk) => data += chunk);
2232
- process.stdin.on("end", () => resolve11(data));
2233
- setTimeout(() => resolve11(data), 500);
2232
+ process.stdin.on("end", () => resolve12(data));
2233
+ setTimeout(() => resolve12(data), 500);
2234
2234
  });
2235
2235
  }
2236
2236
 
@@ -3223,29 +3223,65 @@ async function executeWarmup(claudeMdContent, model, ttl, client) {
3223
3223
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3224
3224
  };
3225
3225
  }
3226
- async function installCron(cronExpr, apiKey) {
3226
+ function isValidCronExpr(expr) {
3227
+ const parts = expr.trim().split(/\s+/);
3228
+ if (parts.length < 5 || parts.length > 6) return false;
3229
+ return parts.every((p) => /^[0-9*,/\-]+$/.test(p));
3230
+ }
3231
+ async function installCron(cronExpr) {
3232
+ if (!isValidCronExpr(cronExpr)) {
3233
+ process.stderr.write(
3234
+ `Error: invalid cron expression "${cronExpr}".
3235
+ Example: "0 9 * * 1-5" (weekdays at 9am)
3236
+ `
3237
+ );
3238
+ process.exit(1);
3239
+ }
3227
3240
  const { execSync: execSync3 } = await import("child_process");
3228
- const command = `claudectx warmup --api-key ${apiKey}`;
3241
+ const command = `claudectx warmup`;
3229
3242
  const cronLine = `${cronExpr} ${command}`;
3243
+ const marker = "# claudectx warmup";
3230
3244
  try {
3231
3245
  let existing = "";
3232
3246
  try {
3233
- existing = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
3247
+ existing = execSync3("crontab -l", {
3248
+ encoding: "utf-8",
3249
+ stdio: ["pipe", "pipe", "pipe"]
3250
+ });
3234
3251
  } catch {
3235
3252
  existing = "";
3236
3253
  }
3237
- if (existing.includes("claudectx warmup")) {
3238
- process.stdout.write("Cron job already installed for claudectx warmup.\n");
3254
+ if (existing.includes(marker)) {
3255
+ process.stdout.write(" Cron job already installed for claudectx warmup.\n");
3239
3256
  return;
3240
3257
  }
3241
- const newCrontab = existing.trimEnd() + "\n" + cronLine + "\n";
3242
- execSync3(`echo ${JSON.stringify(newCrontab)} | crontab -`);
3243
- process.stdout.write(`\u2713 Cron job installed: ${cronLine}
3258
+ const newCrontab = existing.trimEnd() + `
3259
+ ${marker}
3260
+ ${cronLine}
3261
+ `;
3262
+ const { writeFileSync: writeFileSync11, unlinkSync: unlinkSync2 } = await import("fs");
3263
+ const { tmpdir } = await import("os");
3264
+ const { join: join17 } = await import("path");
3265
+ const tmpFile = join17(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
3266
+ try {
3267
+ writeFileSync11(tmpFile, newCrontab, "utf-8");
3268
+ execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
3269
+ } finally {
3270
+ try {
3271
+ unlinkSync2(tmpFile);
3272
+ } catch {
3273
+ }
3274
+ }
3275
+ process.stdout.write(` \u2713 Cron job installed: ${cronLine}
3244
3276
  `);
3277
+ process.stdout.write(" Note: Set ANTHROPIC_API_KEY in your cron environment (e.g. via ~/.profile).\n");
3245
3278
  } catch {
3246
- process.stdout.write(`Could not install cron automatically. Add manually:
3279
+ process.stdout.write(
3280
+ ` Could not install cron automatically. Add this line manually with "crontab -e":
3281
+ ANTHROPIC_API_KEY=<your-key>
3247
3282
  ${cronLine}
3248
- `);
3283
+ `
3284
+ );
3249
3285
  }
3250
3286
  }
3251
3287
  async function warmupCommand(options) {
@@ -3315,7 +3351,7 @@ async function warmupCommand(options) {
3315
3351
  }
3316
3352
  process.stdout.write("\n");
3317
3353
  if (options.cron) {
3318
- await installCron(options.cron, apiKey);
3354
+ await installCron(options.cron);
3319
3355
  }
3320
3356
  }
3321
3357
 
@@ -3374,15 +3410,15 @@ async function findGitDeletedMentions(content, projectRoot) {
3374
3410
  for (let i = 0; i < lines.length; i++) {
3375
3411
  const line = lines[i];
3376
3412
  for (const deleted of deletedFiles) {
3377
- const basename7 = path23.basename(deleted);
3378
- if (line.includes(basename7) || line.includes(deleted)) {
3413
+ const basename8 = path23.basename(deleted);
3414
+ if (line.includes(basename8) || line.includes(deleted)) {
3379
3415
  issues.push({
3380
3416
  type: "git-deleted",
3381
3417
  line: i + 1,
3382
3418
  text: line.trim(),
3383
3419
  severity: "warning",
3384
3420
  estimatedTokenWaste: countTokens(line),
3385
- suggestion: `References "${basename7}" which was deleted from git. Consider removing this mention.`
3421
+ suggestion: `References "${basename8}" which was deleted from git. Consider removing this mention.`
3386
3422
  });
3387
3423
  break;
3388
3424
  }
@@ -3589,11 +3625,34 @@ async function applyFix(claudeMdPath, issues) {
3589
3625
  const lines = content.split("\n");
3590
3626
  const lineSet = new Set(selectedLines.map((l) => l - 1));
3591
3627
  const newLines = lines.filter((_, i) => !lineSet.has(i));
3592
- fs20.writeFileSync(claudeMdPath, newLines.join("\n"), "utf-8");
3628
+ const newContent = newLines.join("\n");
3629
+ const backupPath = `${claudeMdPath}.bak`;
3630
+ fs20.writeFileSync(backupPath, content, "utf-8");
3631
+ const os5 = await import("os");
3632
+ const tmpPath = `${claudeMdPath}.tmp-${Date.now()}`;
3633
+ try {
3634
+ fs20.writeFileSync(tmpPath, newContent, "utf-8");
3635
+ fs20.renameSync(tmpPath, claudeMdPath);
3636
+ } catch (err) {
3637
+ try {
3638
+ fs20.copyFileSync(backupPath, claudeMdPath);
3639
+ } catch {
3640
+ }
3641
+ try {
3642
+ fs20.unlinkSync(tmpPath);
3643
+ } catch {
3644
+ }
3645
+ process.stderr.write(`Error writing CLAUDE.md: ${err instanceof Error ? err.message : String(err)}
3646
+ `);
3647
+ process.exit(1);
3648
+ }
3593
3649
  process.stdout.write(`
3594
- \u2713 Removed ${selectedLines.length} line(s) from ${claudeMdPath}
3650
+ \u2713 Removed ${selectedLines.length} line(s) from ${path24.basename(claudeMdPath)}
3651
+ `);
3652
+ process.stdout.write(` \u2713 Backup saved to ${backupPath}
3595
3653
 
3596
3654
  `);
3655
+ void os5;
3597
3656
  }
3598
3657
 
3599
3658
  // src/commands/hooks.ts
@@ -3609,10 +3668,10 @@ var HOOK_REGISTRY = [
3609
3668
  description: "Compress session when token count exceeds threshold",
3610
3669
  triggerEvent: "PostToolUse",
3611
3670
  matcher: "Read",
3612
- commandTemplate: "claudectx compress --auto --api-key {{config.apiKey}}",
3671
+ // API key is read from ANTHROPIC_API_KEY env var — never stored in settings.json
3672
+ commandTemplate: "claudectx compress --auto",
3613
3673
  configSchema: {
3614
- threshold: { type: "number", default: 5e4, description: "Token threshold to trigger compression" },
3615
- apiKey: { type: "string", description: "Anthropic API key for AI summarization", required: true }
3674
+ threshold: { type: "number", default: 5e4, description: "Token threshold to trigger compression" }
3616
3675
  },
3617
3676
  category: "compression"
3618
3677
  },
@@ -3645,10 +3704,9 @@ var HOOK_REGISTRY = [
3645
3704
  description: "Pre-warm the Anthropic prompt cache on each session start",
3646
3705
  triggerEvent: "PostToolUse",
3647
3706
  matcher: "Read",
3648
- commandTemplate: "claudectx warmup --api-key {{config.apiKey}}",
3649
- configSchema: {
3650
- apiKey: { type: "string", description: "Anthropic API key", required: true }
3651
- },
3707
+ // API key is read from ANTHROPIC_API_KEY env var — never stored in settings.json
3708
+ commandTemplate: "claudectx warmup",
3709
+ configSchema: {},
3652
3710
  category: "warmup"
3653
3711
  }
3654
3712
  ];
@@ -3677,9 +3735,18 @@ function buildHookEntry(def, config) {
3677
3735
  // src/commands/hooks.ts
3678
3736
  function readInstalledHooks(projectRoot) {
3679
3737
  const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
3738
+ if (!fs21.existsSync(settingsPath)) return {};
3680
3739
  try {
3681
3740
  return JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
3682
3741
  } catch {
3742
+ process.stderr.write(
3743
+ `Warning: ${settingsPath} exists but contains invalid JSON. Existing settings will be preserved as a backup.
3744
+ `
3745
+ );
3746
+ try {
3747
+ fs21.copyFileSync(settingsPath, `${settingsPath}.bak`);
3748
+ } catch {
3749
+ }
3683
3750
  return {};
3684
3751
  }
3685
3752
  }
@@ -3766,6 +3833,16 @@ async function hooksAdd(name, projectRoot, configPairs) {
3766
3833
  `);
3767
3834
  process.exit(1);
3768
3835
  }
3836
+ const sensitiveKeys = Object.keys(config).filter(
3837
+ (k) => /key|token|secret|password|webhook/i.test(k)
3838
+ );
3839
+ if (sensitiveKeys.length > 0) {
3840
+ process.stderr.write(
3841
+ `Warning: config field(s) [${sensitiveKeys.join(", ")}] will be stored in plain text in
3842
+ .claude/settings.local.json \u2014 ensure this file is in your .gitignore.
3843
+ `
3844
+ );
3845
+ }
3769
3846
  const entry = { ...buildHookEntry(def, config), name };
3770
3847
  const settings = readInstalledHooks(projectRoot);
3771
3848
  const hooksObj = settings.hooks ?? {};
@@ -3930,7 +4007,9 @@ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
3930
4007
  `);
3931
4008
  for (const file of files) {
3932
4009
  const filePath = path26.join(targetDir, file.filename);
3933
- process.stdout.write(` ${options.dryRun ? "[dry-run] " : ""}\u2192 .cursor/rules/${file.filename}
4010
+ const exists = fs22.existsSync(filePath);
4011
+ const prefix = options.dryRun ? "[dry-run] " : exists ? "[overwrite] " : "";
4012
+ process.stdout.write(` ${prefix}\u2192 .cursor/rules/${file.filename}
3934
4013
  `);
3935
4014
  if (!options.dryRun) {
3936
4015
  fs22.mkdirSync(targetDir, { recursive: true });
@@ -3941,8 +4020,9 @@ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
3941
4020
  } else if (to === "copilot") {
3942
4021
  const converted = claudeMdToCopilot(content);
3943
4022
  const targetPath = path26.join(projectRoot, ".github", "copilot-instructions.md");
4023
+ const exists = fs22.existsSync(targetPath);
3944
4024
  process.stdout.write(`
3945
- Converting CLAUDE.md \u2192 .github/copilot-instructions.md
4025
+ Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwrite]" : ""}
3946
4026
  `);
3947
4027
  if (!options.dryRun) {
3948
4028
  fs22.mkdirSync(path26.dirname(targetPath), { recursive: true });
@@ -3958,8 +4038,9 @@ Converting CLAUDE.md \u2192 .github/copilot-instructions.md
3958
4038
  } else if (to === "windsurf") {
3959
4039
  const converted = claudeMdToWindsurf(content);
3960
4040
  const targetPath = path26.join(projectRoot, ".windsurfrules");
4041
+ const exists = fs22.existsSync(targetPath);
3961
4042
  process.stdout.write(`
3962
- Converting CLAUDE.md \u2192 .windsurfrules
4043
+ Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
3963
4044
  `);
3964
4045
  if (!options.dryRun) {
3965
4046
  fs22.writeFileSync(targetPath, converted, "utf-8");
@@ -4233,7 +4314,26 @@ async function teamsShare(options) {
4233
4314
  }
4234
4315
  const latest = exportFiles[0];
4235
4316
  const src = path28.join(storeDir, latest);
4236
- const destPath = fs24.statSync(dest).isDirectory() ? path28.join(dest, latest) : dest;
4317
+ let destPath;
4318
+ try {
4319
+ const stat = fs24.statSync(dest);
4320
+ destPath = stat.isDirectory() ? path28.join(dest, latest) : dest;
4321
+ } catch {
4322
+ destPath = dest;
4323
+ }
4324
+ const destDir = path28.dirname(path28.resolve(destPath));
4325
+ let resolvedDir;
4326
+ try {
4327
+ resolvedDir = fs24.realpathSync(destDir);
4328
+ } catch {
4329
+ resolvedDir = destDir;
4330
+ }
4331
+ const systemDirs = ["/etc", "/bin", "/sbin", "/usr", "/var", "/proc", "/sys"];
4332
+ if (systemDirs.some((d) => resolvedDir === d || resolvedDir.startsWith(d + "/"))) {
4333
+ process.stderr.write(`Error: destination path resolves to a system directory (${resolvedDir}). Aborting.
4334
+ `);
4335
+ process.exit(1);
4336
+ }
4237
4337
  fs24.copyFileSync(src, destPath);
4238
4338
  process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
4239
4339