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/README.md +132 -1
- package/dist/index.js +130 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +130 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2225,12 +2225,12 @@ async function handleLogStdin() {
|
|
|
2225
2225
|
}
|
|
2226
2226
|
}
|
|
2227
2227
|
function readStdin() {
|
|
2228
|
-
return new Promise((
|
|
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", () =>
|
|
2233
|
-
setTimeout(() =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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() +
|
|
3242
|
-
|
|
3243
|
-
|
|
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(
|
|
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
|
|
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
|
|
3378
|
-
if (line.includes(
|
|
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 "${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|