claudectx 1.1.0 → 1.1.2
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 +140 -1
- package/dist/index.js +194 -70
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +194 -70
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -239,7 +239,7 @@ function findSessionFile(sessionId) {
|
|
|
239
239
|
}
|
|
240
240
|
return files[0]?.filePath ?? null;
|
|
241
241
|
}
|
|
242
|
-
function readSessionUsage(sessionFilePath) {
|
|
242
|
+
async function readSessionUsage(sessionFilePath) {
|
|
243
243
|
const result = {
|
|
244
244
|
inputTokens: 0,
|
|
245
245
|
outputTokens: 0,
|
|
@@ -248,28 +248,32 @@ function readSessionUsage(sessionFilePath) {
|
|
|
248
248
|
requestCount: 0
|
|
249
249
|
};
|
|
250
250
|
if (!fs9.existsSync(sessionFilePath)) return result;
|
|
251
|
-
|
|
251
|
+
const { createReadStream } = await import("fs");
|
|
252
|
+
const { createInterface } = await import("readline");
|
|
252
253
|
try {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
254
|
+
const rl = createInterface({
|
|
255
|
+
input: createReadStream(sessionFilePath, { encoding: "utf-8" }),
|
|
256
|
+
crlfDelay: Infinity
|
|
257
|
+
});
|
|
258
|
+
for await (const line of rl) {
|
|
259
|
+
if (!line.trim()) continue;
|
|
260
|
+
try {
|
|
261
|
+
const entry = JSON.parse(line);
|
|
262
|
+
const usage = entry.usage ?? entry.message?.usage;
|
|
263
|
+
if (!usage) continue;
|
|
264
|
+
const isAssistant = entry.type === "assistant" || entry.message?.role === "assistant";
|
|
265
|
+
if (isAssistant) {
|
|
266
|
+
result.inputTokens += usage.input_tokens ?? 0;
|
|
267
|
+
result.outputTokens += usage.output_tokens ?? 0;
|
|
268
|
+
result.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
269
|
+
result.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
270
|
+
result.requestCount++;
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
270
273
|
}
|
|
271
|
-
} catch {
|
|
272
274
|
}
|
|
275
|
+
} catch {
|
|
276
|
+
return result;
|
|
273
277
|
}
|
|
274
278
|
return result;
|
|
275
279
|
}
|
|
@@ -397,20 +401,23 @@ function Dashboard({
|
|
|
397
401
|
const events = readAllEvents();
|
|
398
402
|
const fileStats2 = aggregateStats(events);
|
|
399
403
|
const sessionFile2 = sessionId ? findSessionFile(sessionId) : findSessionFile();
|
|
400
|
-
const
|
|
404
|
+
const usagePromise = sessionFile2 ? readSessionUsage(sessionFile2) : Promise.resolve({
|
|
401
405
|
inputTokens: 0,
|
|
402
406
|
outputTokens: 0,
|
|
403
407
|
cacheCreationTokens: 0,
|
|
404
408
|
cacheReadTokens: 0,
|
|
405
409
|
requestCount: 0
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
});
|
|
411
|
+
usagePromise.then((usage2) => {
|
|
412
|
+
setState((prev) => ({
|
|
413
|
+
fileStats: fileStats2,
|
|
414
|
+
usage: usage2,
|
|
415
|
+
sessionFile: sessionFile2,
|
|
416
|
+
lastUpdated: /* @__PURE__ */ new Date(),
|
|
417
|
+
tickCount: prev.tickCount + 1
|
|
418
|
+
}));
|
|
419
|
+
}).catch(() => {
|
|
420
|
+
});
|
|
414
421
|
}, [sessionId]);
|
|
415
422
|
(0, import_react.useEffect)(() => {
|
|
416
423
|
refresh();
|
|
@@ -2243,12 +2250,12 @@ async function handleLogStdin() {
|
|
|
2243
2250
|
}
|
|
2244
2251
|
}
|
|
2245
2252
|
function readStdin() {
|
|
2246
|
-
return new Promise((
|
|
2253
|
+
return new Promise((resolve12) => {
|
|
2247
2254
|
let data = "";
|
|
2248
2255
|
process.stdin.setEncoding("utf-8");
|
|
2249
2256
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
2250
|
-
process.stdin.on("end", () =>
|
|
2251
|
-
setTimeout(() =>
|
|
2257
|
+
process.stdin.on("end", () => resolve12(data));
|
|
2258
|
+
setTimeout(() => resolve12(data), 500);
|
|
2252
2259
|
});
|
|
2253
2260
|
}
|
|
2254
2261
|
|
|
@@ -2767,9 +2774,9 @@ init_models();
|
|
|
2767
2774
|
function isoDate(d) {
|
|
2768
2775
|
return d.toISOString().slice(0, 10);
|
|
2769
2776
|
}
|
|
2770
|
-
function calcCost2(inputTokens, outputTokens, model) {
|
|
2777
|
+
function calcCost2(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
|
|
2771
2778
|
const p = MODEL_PRICING[model];
|
|
2772
|
-
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion;
|
|
2779
|
+
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion + cacheCreationTokens / 1e6 * p.cacheWritePerMillion + cacheReadTokens / 1e6 * p.cacheReadPerMillion;
|
|
2773
2780
|
}
|
|
2774
2781
|
async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
2775
2782
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -2788,6 +2795,7 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2788
2795
|
inputTokens: 0,
|
|
2789
2796
|
outputTokens: 0,
|
|
2790
2797
|
cacheReadTokens: 0,
|
|
2798
|
+
cacheCreationTokens: 0,
|
|
2791
2799
|
requests: 0,
|
|
2792
2800
|
costUsd: 0
|
|
2793
2801
|
});
|
|
@@ -2796,20 +2804,23 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2796
2804
|
let totalInput = 0;
|
|
2797
2805
|
let totalOutput = 0;
|
|
2798
2806
|
let totalCacheRead = 0;
|
|
2807
|
+
let totalCacheCreation = 0;
|
|
2799
2808
|
for (const sf of sessionFiles) {
|
|
2800
2809
|
const dateStr = isoDate(new Date(sf.mtimeMs));
|
|
2801
2810
|
const bucket = bucketMap.get(dateStr);
|
|
2802
2811
|
if (!bucket) continue;
|
|
2803
|
-
const usage = readSessionUsage(sf.filePath);
|
|
2812
|
+
const usage = await readSessionUsage(sf.filePath);
|
|
2804
2813
|
bucket.sessions++;
|
|
2805
2814
|
bucket.inputTokens += usage.inputTokens;
|
|
2806
2815
|
bucket.outputTokens += usage.outputTokens;
|
|
2807
2816
|
bucket.cacheReadTokens += usage.cacheReadTokens;
|
|
2817
|
+
bucket.cacheCreationTokens += usage.cacheCreationTokens;
|
|
2808
2818
|
bucket.requests += usage.requestCount;
|
|
2809
|
-
bucket.costUsd += calcCost2(usage.inputTokens, usage.outputTokens, model);
|
|
2819
|
+
bucket.costUsd += calcCost2(usage.inputTokens, usage.outputTokens, usage.cacheCreationTokens, usage.cacheReadTokens, model);
|
|
2810
2820
|
totalInput += usage.inputTokens;
|
|
2811
2821
|
totalOutput += usage.outputTokens;
|
|
2812
2822
|
totalCacheRead += usage.cacheReadTokens;
|
|
2823
|
+
totalCacheCreation += usage.cacheCreationTokens;
|
|
2813
2824
|
totalRequests += usage.requestCount;
|
|
2814
2825
|
}
|
|
2815
2826
|
const fileEvents = readAllEvents().filter(
|
|
@@ -2820,10 +2831,12 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2820
2831
|
filePath: s.filePath,
|
|
2821
2832
|
readCount: s.readCount
|
|
2822
2833
|
}));
|
|
2823
|
-
const totalCost = calcCost2(totalInput, totalOutput, model);
|
|
2834
|
+
const totalCost = calcCost2(totalInput, totalOutput, totalCacheCreation, totalCacheRead, model);
|
|
2824
2835
|
const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
|
|
2825
2836
|
const byDay = [...bucketMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
2826
2837
|
const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
|
|
2838
|
+
const dailyAvgCostUsd = days > 0 ? totalCost / days : 0;
|
|
2839
|
+
const projectedMonthlyUsd = dailyAvgCostUsd * 30;
|
|
2827
2840
|
return {
|
|
2828
2841
|
periodDays: days,
|
|
2829
2842
|
startDate: isoDate(cutoff),
|
|
@@ -2833,10 +2846,13 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2833
2846
|
totalInputTokens: totalInput,
|
|
2834
2847
|
totalOutputTokens: totalOutput,
|
|
2835
2848
|
totalCacheReadTokens: totalCacheRead,
|
|
2849
|
+
totalCacheCreationTokens: totalCacheCreation,
|
|
2836
2850
|
cacheHitRate,
|
|
2837
2851
|
totalCostUsd: totalCost,
|
|
2838
2852
|
avgCostPerSession: uniqueSessions > 0 ? totalCost / uniqueSessions : 0,
|
|
2839
2853
|
avgTokensPerRequest: totalRequests > 0 ? Math.round(totalInput / totalRequests) : 0,
|
|
2854
|
+
dailyAvgCostUsd,
|
|
2855
|
+
projectedMonthlyUsd,
|
|
2840
2856
|
byDay,
|
|
2841
2857
|
topFiles,
|
|
2842
2858
|
model,
|
|
@@ -2879,9 +2895,12 @@ function formatText(data) {
|
|
|
2879
2895
|
lines.push(` Input tokens: ${fmtNum2(data.totalInputTokens)}`);
|
|
2880
2896
|
lines.push(` Output tokens: ${fmtNum2(data.totalOutputTokens)}`);
|
|
2881
2897
|
lines.push(` Cache reads: ${fmtNum2(data.totalCacheReadTokens)} (${data.cacheHitRate}% hit rate)`);
|
|
2898
|
+
lines.push(` Cache writes: ${fmtNum2(data.totalCacheCreationTokens)}`);
|
|
2882
2899
|
lines.push(` Total cost (est.): ${fmtCost2(data.totalCostUsd)}`);
|
|
2883
2900
|
lines.push(` Avg cost/session: ${fmtCost2(data.avgCostPerSession)}`);
|
|
2884
2901
|
lines.push(` Avg tokens/request: ${fmtNum2(data.avgTokensPerRequest)}`);
|
|
2902
|
+
lines.push(` Daily avg cost: ${fmtCost2(data.dailyAvgCostUsd)}`);
|
|
2903
|
+
lines.push(` Projected (30-day): ${fmtCost2(data.projectedMonthlyUsd)}`);
|
|
2885
2904
|
lines.push(` Model: ${data.model}`);
|
|
2886
2905
|
lines.push("");
|
|
2887
2906
|
const activeDays = data.byDay.filter((d) => d.sessions > 0);
|
|
@@ -2950,9 +2969,12 @@ function formatMarkdown(data) {
|
|
|
2950
2969
|
lines.push(`| Input tokens | ${fmtNum2(data.totalInputTokens)} |`);
|
|
2951
2970
|
lines.push(`| Output tokens | ${fmtNum2(data.totalOutputTokens)} |`);
|
|
2952
2971
|
lines.push(`| Cache hit rate | ${data.cacheHitRate}% |`);
|
|
2972
|
+
lines.push(`| Cache writes | ${fmtNum2(data.totalCacheCreationTokens)} tokens |`);
|
|
2953
2973
|
lines.push(`| Total cost (est.) | ${fmtCost2(data.totalCostUsd)} |`);
|
|
2954
2974
|
lines.push(`| Avg cost/session | ${fmtCost2(data.avgCostPerSession)} |`);
|
|
2955
2975
|
lines.push(`| Avg tokens/request | ${fmtNum2(data.avgTokensPerRequest)} |`);
|
|
2976
|
+
lines.push(`| Daily avg cost | ${fmtCost2(data.dailyAvgCostUsd)} |`);
|
|
2977
|
+
lines.push(`| Projected (30-day) | ${fmtCost2(data.projectedMonthlyUsd)} |`);
|
|
2956
2978
|
lines.push(`| Model | \`${data.model}\` |`);
|
|
2957
2979
|
lines.push("");
|
|
2958
2980
|
const activeDays = data.byDay.filter((d) => d.sessions > 0);
|
|
@@ -3241,29 +3263,65 @@ async function executeWarmup(claudeMdContent, model, ttl, client) {
|
|
|
3241
3263
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3242
3264
|
};
|
|
3243
3265
|
}
|
|
3244
|
-
|
|
3266
|
+
function isValidCronExpr(expr) {
|
|
3267
|
+
const parts = expr.trim().split(/\s+/);
|
|
3268
|
+
if (parts.length < 5 || parts.length > 6) return false;
|
|
3269
|
+
return parts.every((p) => /^[0-9*,/\-]+$/.test(p));
|
|
3270
|
+
}
|
|
3271
|
+
async function installCron(cronExpr) {
|
|
3272
|
+
if (!isValidCronExpr(cronExpr)) {
|
|
3273
|
+
process.stderr.write(
|
|
3274
|
+
`Error: invalid cron expression "${cronExpr}".
|
|
3275
|
+
Example: "0 9 * * 1-5" (weekdays at 9am)
|
|
3276
|
+
`
|
|
3277
|
+
);
|
|
3278
|
+
process.exit(1);
|
|
3279
|
+
}
|
|
3245
3280
|
const { execSync: execSync3 } = await import("child_process");
|
|
3246
|
-
const command = `claudectx warmup
|
|
3281
|
+
const command = `claudectx warmup`;
|
|
3247
3282
|
const cronLine = `${cronExpr} ${command}`;
|
|
3283
|
+
const marker = "# claudectx warmup";
|
|
3248
3284
|
try {
|
|
3249
3285
|
let existing = "";
|
|
3250
3286
|
try {
|
|
3251
|
-
existing = execSync3("crontab -l
|
|
3287
|
+
existing = execSync3("crontab -l", {
|
|
3288
|
+
encoding: "utf-8",
|
|
3289
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3290
|
+
});
|
|
3252
3291
|
} catch {
|
|
3253
3292
|
existing = "";
|
|
3254
3293
|
}
|
|
3255
|
-
if (existing.includes(
|
|
3256
|
-
process.stdout.write("Cron job already installed for claudectx warmup.\n");
|
|
3294
|
+
if (existing.includes(marker)) {
|
|
3295
|
+
process.stdout.write(" Cron job already installed for claudectx warmup.\n");
|
|
3257
3296
|
return;
|
|
3258
3297
|
}
|
|
3259
|
-
const newCrontab = existing.trimEnd() +
|
|
3260
|
-
|
|
3261
|
-
|
|
3298
|
+
const newCrontab = existing.trimEnd() + `
|
|
3299
|
+
${marker}
|
|
3300
|
+
${cronLine}
|
|
3301
|
+
`;
|
|
3302
|
+
const { writeFileSync: writeFileSync11, unlinkSync: unlinkSync2 } = await import("fs");
|
|
3303
|
+
const { tmpdir } = await import("os");
|
|
3304
|
+
const { join: join17 } = await import("path");
|
|
3305
|
+
const tmpFile = join17(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
|
|
3306
|
+
try {
|
|
3307
|
+
writeFileSync11(tmpFile, newCrontab, "utf-8");
|
|
3308
|
+
execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3309
|
+
} finally {
|
|
3310
|
+
try {
|
|
3311
|
+
unlinkSync2(tmpFile);
|
|
3312
|
+
} catch {
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
process.stdout.write(` \u2713 Cron job installed: ${cronLine}
|
|
3262
3316
|
`);
|
|
3317
|
+
process.stdout.write(" Note: Set ANTHROPIC_API_KEY in your cron environment (e.g. via ~/.profile).\n");
|
|
3263
3318
|
} catch {
|
|
3264
|
-
process.stdout.write(
|
|
3319
|
+
process.stdout.write(
|
|
3320
|
+
` Could not install cron automatically. Add this line manually with "crontab -e":
|
|
3321
|
+
ANTHROPIC_API_KEY=<your-key>
|
|
3265
3322
|
${cronLine}
|
|
3266
|
-
`
|
|
3323
|
+
`
|
|
3324
|
+
);
|
|
3267
3325
|
}
|
|
3268
3326
|
}
|
|
3269
3327
|
async function warmupCommand(options) {
|
|
@@ -3333,7 +3391,7 @@ async function warmupCommand(options) {
|
|
|
3333
3391
|
}
|
|
3334
3392
|
process.stdout.write("\n");
|
|
3335
3393
|
if (options.cron) {
|
|
3336
|
-
await installCron(options.cron
|
|
3394
|
+
await installCron(options.cron);
|
|
3337
3395
|
}
|
|
3338
3396
|
}
|
|
3339
3397
|
|
|
@@ -3392,15 +3450,15 @@ async function findGitDeletedMentions(content, projectRoot) {
|
|
|
3392
3450
|
for (let i = 0; i < lines.length; i++) {
|
|
3393
3451
|
const line = lines[i];
|
|
3394
3452
|
for (const deleted of deletedFiles) {
|
|
3395
|
-
const
|
|
3396
|
-
if (line.includes(
|
|
3453
|
+
const basename8 = path22.basename(deleted);
|
|
3454
|
+
if (line.includes(basename8) || line.includes(deleted)) {
|
|
3397
3455
|
issues.push({
|
|
3398
3456
|
type: "git-deleted",
|
|
3399
3457
|
line: i + 1,
|
|
3400
3458
|
text: line.trim(),
|
|
3401
3459
|
severity: "warning",
|
|
3402
3460
|
estimatedTokenWaste: countTokens(line),
|
|
3403
|
-
suggestion: `References "${
|
|
3461
|
+
suggestion: `References "${basename8}" which was deleted from git. Consider removing this mention.`
|
|
3404
3462
|
});
|
|
3405
3463
|
break;
|
|
3406
3464
|
}
|
|
@@ -3607,11 +3665,34 @@ async function applyFix(claudeMdPath, issues) {
|
|
|
3607
3665
|
const lines = content.split("\n");
|
|
3608
3666
|
const lineSet = new Set(selectedLines.map((l) => l - 1));
|
|
3609
3667
|
const newLines = lines.filter((_, i) => !lineSet.has(i));
|
|
3610
|
-
|
|
3668
|
+
const newContent = newLines.join("\n");
|
|
3669
|
+
const backupPath = `${claudeMdPath}.bak`;
|
|
3670
|
+
fs20.writeFileSync(backupPath, content, "utf-8");
|
|
3671
|
+
const os5 = await import("os");
|
|
3672
|
+
const tmpPath = `${claudeMdPath}.tmp-${Date.now()}`;
|
|
3673
|
+
try {
|
|
3674
|
+
fs20.writeFileSync(tmpPath, newContent, "utf-8");
|
|
3675
|
+
fs20.renameSync(tmpPath, claudeMdPath);
|
|
3676
|
+
} catch (err) {
|
|
3677
|
+
try {
|
|
3678
|
+
fs20.copyFileSync(backupPath, claudeMdPath);
|
|
3679
|
+
} catch {
|
|
3680
|
+
}
|
|
3681
|
+
try {
|
|
3682
|
+
fs20.unlinkSync(tmpPath);
|
|
3683
|
+
} catch {
|
|
3684
|
+
}
|
|
3685
|
+
process.stderr.write(`Error writing CLAUDE.md: ${err instanceof Error ? err.message : String(err)}
|
|
3686
|
+
`);
|
|
3687
|
+
process.exit(1);
|
|
3688
|
+
}
|
|
3611
3689
|
process.stdout.write(`
|
|
3612
|
-
\u2713 Removed ${selectedLines.length} line(s) from ${claudeMdPath}
|
|
3690
|
+
\u2713 Removed ${selectedLines.length} line(s) from ${path23.basename(claudeMdPath)}
|
|
3691
|
+
`);
|
|
3692
|
+
process.stdout.write(` \u2713 Backup saved to ${backupPath}
|
|
3613
3693
|
|
|
3614
3694
|
`);
|
|
3695
|
+
void os5;
|
|
3615
3696
|
}
|
|
3616
3697
|
|
|
3617
3698
|
// src/commands/hooks.ts
|
|
@@ -3627,10 +3708,10 @@ var HOOK_REGISTRY = [
|
|
|
3627
3708
|
description: "Compress session when token count exceeds threshold",
|
|
3628
3709
|
triggerEvent: "PostToolUse",
|
|
3629
3710
|
matcher: "Read",
|
|
3630
|
-
|
|
3711
|
+
// API key is read from ANTHROPIC_API_KEY env var — never stored in settings.json
|
|
3712
|
+
commandTemplate: "claudectx compress --auto",
|
|
3631
3713
|
configSchema: {
|
|
3632
|
-
threshold: { type: "number", default: 5e4, description: "Token threshold to trigger compression" }
|
|
3633
|
-
apiKey: { type: "string", description: "Anthropic API key for AI summarization", required: true }
|
|
3714
|
+
threshold: { type: "number", default: 5e4, description: "Token threshold to trigger compression" }
|
|
3634
3715
|
},
|
|
3635
3716
|
category: "compression"
|
|
3636
3717
|
},
|
|
@@ -3663,10 +3744,9 @@ var HOOK_REGISTRY = [
|
|
|
3663
3744
|
description: "Pre-warm the Anthropic prompt cache on each session start",
|
|
3664
3745
|
triggerEvent: "PostToolUse",
|
|
3665
3746
|
matcher: "Read",
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
},
|
|
3747
|
+
// API key is read from ANTHROPIC_API_KEY env var — never stored in settings.json
|
|
3748
|
+
commandTemplate: "claudectx warmup",
|
|
3749
|
+
configSchema: {},
|
|
3670
3750
|
category: "warmup"
|
|
3671
3751
|
}
|
|
3672
3752
|
];
|
|
@@ -3695,9 +3775,18 @@ function buildHookEntry(def, config) {
|
|
|
3695
3775
|
// src/commands/hooks.ts
|
|
3696
3776
|
function readInstalledHooks(projectRoot) {
|
|
3697
3777
|
const settingsPath = path24.join(projectRoot, ".claude", "settings.local.json");
|
|
3778
|
+
if (!fs21.existsSync(settingsPath)) return {};
|
|
3698
3779
|
try {
|
|
3699
3780
|
return JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
|
|
3700
3781
|
} catch {
|
|
3782
|
+
process.stderr.write(
|
|
3783
|
+
`Warning: ${settingsPath} exists but contains invalid JSON. Existing settings will be preserved as a backup.
|
|
3784
|
+
`
|
|
3785
|
+
);
|
|
3786
|
+
try {
|
|
3787
|
+
fs21.copyFileSync(settingsPath, `${settingsPath}.bak`);
|
|
3788
|
+
} catch {
|
|
3789
|
+
}
|
|
3701
3790
|
return {};
|
|
3702
3791
|
}
|
|
3703
3792
|
}
|
|
@@ -3784,6 +3873,16 @@ async function hooksAdd(name, projectRoot, configPairs) {
|
|
|
3784
3873
|
`);
|
|
3785
3874
|
process.exit(1);
|
|
3786
3875
|
}
|
|
3876
|
+
const sensitiveKeys = Object.keys(config).filter(
|
|
3877
|
+
(k) => /key|token|secret|password|webhook/i.test(k)
|
|
3878
|
+
);
|
|
3879
|
+
if (sensitiveKeys.length > 0) {
|
|
3880
|
+
process.stderr.write(
|
|
3881
|
+
`Warning: config field(s) [${sensitiveKeys.join(", ")}] will be stored in plain text in
|
|
3882
|
+
.claude/settings.local.json \u2014 ensure this file is in your .gitignore.
|
|
3883
|
+
`
|
|
3884
|
+
);
|
|
3885
|
+
}
|
|
3787
3886
|
const entry = { ...buildHookEntry(def, config), name };
|
|
3788
3887
|
const settings = readInstalledHooks(projectRoot);
|
|
3789
3888
|
const hooksObj = settings.hooks ?? {};
|
|
@@ -3948,7 +4047,9 @@ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
|
|
|
3948
4047
|
`);
|
|
3949
4048
|
for (const file of files) {
|
|
3950
4049
|
const filePath = path25.join(targetDir, file.filename);
|
|
3951
|
-
|
|
4050
|
+
const exists = fs22.existsSync(filePath);
|
|
4051
|
+
const prefix = options.dryRun ? "[dry-run] " : exists ? "[overwrite] " : "";
|
|
4052
|
+
process.stdout.write(` ${prefix}\u2192 .cursor/rules/${file.filename}
|
|
3952
4053
|
`);
|
|
3953
4054
|
if (!options.dryRun) {
|
|
3954
4055
|
fs22.mkdirSync(targetDir, { recursive: true });
|
|
@@ -3959,8 +4060,9 @@ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
|
|
|
3959
4060
|
} else if (to === "copilot") {
|
|
3960
4061
|
const converted = claudeMdToCopilot(content);
|
|
3961
4062
|
const targetPath = path25.join(projectRoot, ".github", "copilot-instructions.md");
|
|
4063
|
+
const exists = fs22.existsSync(targetPath);
|
|
3962
4064
|
process.stdout.write(`
|
|
3963
|
-
Converting CLAUDE.md \u2192 .github/copilot-instructions.md
|
|
4065
|
+
Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwrite]" : ""}
|
|
3964
4066
|
`);
|
|
3965
4067
|
if (!options.dryRun) {
|
|
3966
4068
|
fs22.mkdirSync(path25.dirname(targetPath), { recursive: true });
|
|
@@ -3976,8 +4078,9 @@ Converting CLAUDE.md \u2192 .github/copilot-instructions.md
|
|
|
3976
4078
|
} else if (to === "windsurf") {
|
|
3977
4079
|
const converted = claudeMdToWindsurf(content);
|
|
3978
4080
|
const targetPath = path25.join(projectRoot, ".windsurfrules");
|
|
4081
|
+
const exists = fs22.existsSync(targetPath);
|
|
3979
4082
|
process.stdout.write(`
|
|
3980
|
-
Converting CLAUDE.md \u2192 .windsurfrules
|
|
4083
|
+
Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
|
|
3981
4084
|
`);
|
|
3982
4085
|
if (!options.dryRun) {
|
|
3983
4086
|
fs22.writeFileSync(targetPath, converted, "utf-8");
|
|
@@ -4019,9 +4122,9 @@ function getDeveloperIdentity() {
|
|
|
4019
4122
|
}
|
|
4020
4123
|
return os4.hostname();
|
|
4021
4124
|
}
|
|
4022
|
-
function calcCost3(inputTokens, outputTokens, model) {
|
|
4125
|
+
function calcCost3(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
|
|
4023
4126
|
const p = MODEL_PRICING[model];
|
|
4024
|
-
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion;
|
|
4127
|
+
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion + cacheCreationTokens / 1e6 * p.cacheWritePerMillion + cacheReadTokens / 1e6 * p.cacheReadPerMillion;
|
|
4025
4128
|
}
|
|
4026
4129
|
function isoDate2(d) {
|
|
4027
4130
|
return d.toISOString().slice(0, 10);
|
|
@@ -4052,6 +4155,7 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4052
4155
|
inputTokens: 0,
|
|
4053
4156
|
outputTokens: 0,
|
|
4054
4157
|
cacheReadTokens: 0,
|
|
4158
|
+
cacheCreationTokens: 0,
|
|
4055
4159
|
requests: 0,
|
|
4056
4160
|
costUsd: 0
|
|
4057
4161
|
});
|
|
@@ -4063,14 +4167,15 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4063
4167
|
for (const sf of sessionFiles) {
|
|
4064
4168
|
const dateStr = isoDate2(new Date(sf.mtimeMs));
|
|
4065
4169
|
const bucket = bucketMap.get(dateStr);
|
|
4066
|
-
const usage = readSessionUsage(sf.filePath);
|
|
4170
|
+
const usage = await readSessionUsage(sf.filePath);
|
|
4067
4171
|
if (bucket) {
|
|
4068
4172
|
bucket.sessions++;
|
|
4069
4173
|
bucket.inputTokens += usage.inputTokens;
|
|
4070
4174
|
bucket.outputTokens += usage.outputTokens;
|
|
4071
4175
|
bucket.cacheReadTokens += usage.cacheReadTokens;
|
|
4176
|
+
bucket.cacheCreationTokens += usage.cacheCreationTokens;
|
|
4072
4177
|
bucket.requests += usage.requestCount;
|
|
4073
|
-
bucket.costUsd += calcCost3(usage.inputTokens, usage.outputTokens, model);
|
|
4178
|
+
bucket.costUsd += calcCost3(usage.inputTokens, usage.outputTokens, usage.cacheCreationTokens, usage.cacheReadTokens, model);
|
|
4074
4179
|
}
|
|
4075
4180
|
totalInput += usage.inputTokens;
|
|
4076
4181
|
totalOutput += usage.outputTokens;
|
|
@@ -4081,7 +4186,7 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4081
4186
|
(e) => new Date(e.timestamp).getTime() >= cutoffMs
|
|
4082
4187
|
);
|
|
4083
4188
|
const topWasteFiles = aggregateStats(fileEvents).slice(0, 10).map((s) => ({ filePath: s.filePath, readCount: s.readCount }));
|
|
4084
|
-
const totalCostUsd = calcCost3(totalInput, totalOutput, model);
|
|
4189
|
+
const totalCostUsd = calcCost3(totalInput, totalOutput, 0, totalCacheRead, model);
|
|
4085
4190
|
const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
|
|
4086
4191
|
const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
|
|
4087
4192
|
const developer = {
|
|
@@ -4251,7 +4356,26 @@ async function teamsShare(options) {
|
|
|
4251
4356
|
}
|
|
4252
4357
|
const latest = exportFiles[0];
|
|
4253
4358
|
const src = path27.join(storeDir, latest);
|
|
4254
|
-
|
|
4359
|
+
let destPath;
|
|
4360
|
+
try {
|
|
4361
|
+
const stat = fs24.statSync(dest);
|
|
4362
|
+
destPath = stat.isDirectory() ? path27.join(dest, latest) : dest;
|
|
4363
|
+
} catch {
|
|
4364
|
+
destPath = dest;
|
|
4365
|
+
}
|
|
4366
|
+
const destDir = path27.dirname(path27.resolve(destPath));
|
|
4367
|
+
let resolvedDir;
|
|
4368
|
+
try {
|
|
4369
|
+
resolvedDir = fs24.realpathSync(destDir);
|
|
4370
|
+
} catch {
|
|
4371
|
+
resolvedDir = destDir;
|
|
4372
|
+
}
|
|
4373
|
+
const systemDirs = ["/etc", "/bin", "/sbin", "/usr", "/var", "/proc", "/sys"];
|
|
4374
|
+
if (systemDirs.some((d) => resolvedDir === d || resolvedDir.startsWith(d + "/"))) {
|
|
4375
|
+
process.stderr.write(`Error: destination path resolves to a system directory (${resolvedDir}). Aborting.
|
|
4376
|
+
`);
|
|
4377
|
+
process.exit(1);
|
|
4378
|
+
}
|
|
4255
4379
|
fs24.copyFileSync(src, destPath);
|
|
4256
4380
|
process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
|
|
4257
4381
|
|
|
@@ -4278,7 +4402,7 @@ async function teamsCommand(subcommand, options) {
|
|
|
4278
4402
|
}
|
|
4279
4403
|
|
|
4280
4404
|
// src/index.ts
|
|
4281
|
-
var VERSION = "1.
|
|
4405
|
+
var VERSION = "1.1.2";
|
|
4282
4406
|
var DESCRIPTION = "Reduce Claude Code token usage by up to 80%. Context analyzer, auto-optimizer, live dashboard, and smart MCP tools.";
|
|
4283
4407
|
var program = new import_commander.Command();
|
|
4284
4408
|
program.name("claudectx").description(DESCRIPTION).version(VERSION);
|