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.mjs
CHANGED
|
@@ -222,7 +222,7 @@ function findSessionFile(sessionId) {
|
|
|
222
222
|
}
|
|
223
223
|
return files[0]?.filePath ?? null;
|
|
224
224
|
}
|
|
225
|
-
function readSessionUsage(sessionFilePath) {
|
|
225
|
+
async function readSessionUsage(sessionFilePath) {
|
|
226
226
|
const result = {
|
|
227
227
|
inputTokens: 0,
|
|
228
228
|
outputTokens: 0,
|
|
@@ -231,28 +231,32 @@ function readSessionUsage(sessionFilePath) {
|
|
|
231
231
|
requestCount: 0
|
|
232
232
|
};
|
|
233
233
|
if (!fs9.existsSync(sessionFilePath)) return result;
|
|
234
|
-
|
|
234
|
+
const { createReadStream } = await import("fs");
|
|
235
|
+
const { createInterface } = await import("readline");
|
|
235
236
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
237
|
+
const rl = createInterface({
|
|
238
|
+
input: createReadStream(sessionFilePath, { encoding: "utf-8" }),
|
|
239
|
+
crlfDelay: Infinity
|
|
240
|
+
});
|
|
241
|
+
for await (const line of rl) {
|
|
242
|
+
if (!line.trim()) continue;
|
|
243
|
+
try {
|
|
244
|
+
const entry = JSON.parse(line);
|
|
245
|
+
const usage = entry.usage ?? entry.message?.usage;
|
|
246
|
+
if (!usage) continue;
|
|
247
|
+
const isAssistant = entry.type === "assistant" || entry.message?.role === "assistant";
|
|
248
|
+
if (isAssistant) {
|
|
249
|
+
result.inputTokens += usage.input_tokens ?? 0;
|
|
250
|
+
result.outputTokens += usage.output_tokens ?? 0;
|
|
251
|
+
result.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
252
|
+
result.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
253
|
+
result.requestCount++;
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
253
256
|
}
|
|
254
|
-
} catch {
|
|
255
257
|
}
|
|
258
|
+
} catch {
|
|
259
|
+
return result;
|
|
256
260
|
}
|
|
257
261
|
return result;
|
|
258
262
|
}
|
|
@@ -382,20 +386,23 @@ function Dashboard({
|
|
|
382
386
|
const events = readAllEvents();
|
|
383
387
|
const fileStats2 = aggregateStats(events);
|
|
384
388
|
const sessionFile2 = sessionId ? findSessionFile(sessionId) : findSessionFile();
|
|
385
|
-
const
|
|
389
|
+
const usagePromise = sessionFile2 ? readSessionUsage(sessionFile2) : Promise.resolve({
|
|
386
390
|
inputTokens: 0,
|
|
387
391
|
outputTokens: 0,
|
|
388
392
|
cacheCreationTokens: 0,
|
|
389
393
|
cacheReadTokens: 0,
|
|
390
394
|
requestCount: 0
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
});
|
|
396
|
+
usagePromise.then((usage2) => {
|
|
397
|
+
setState((prev) => ({
|
|
398
|
+
fileStats: fileStats2,
|
|
399
|
+
usage: usage2,
|
|
400
|
+
sessionFile: sessionFile2,
|
|
401
|
+
lastUpdated: /* @__PURE__ */ new Date(),
|
|
402
|
+
tickCount: prev.tickCount + 1
|
|
403
|
+
}));
|
|
404
|
+
}).catch(() => {
|
|
405
|
+
});
|
|
399
406
|
}, [sessionId]);
|
|
400
407
|
useEffect(() => {
|
|
401
408
|
refresh();
|
|
@@ -2225,12 +2232,12 @@ async function handleLogStdin() {
|
|
|
2225
2232
|
}
|
|
2226
2233
|
}
|
|
2227
2234
|
function readStdin() {
|
|
2228
|
-
return new Promise((
|
|
2235
|
+
return new Promise((resolve12) => {
|
|
2229
2236
|
let data = "";
|
|
2230
2237
|
process.stdin.setEncoding("utf-8");
|
|
2231
2238
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
2232
|
-
process.stdin.on("end", () =>
|
|
2233
|
-
setTimeout(() =>
|
|
2239
|
+
process.stdin.on("end", () => resolve12(data));
|
|
2240
|
+
setTimeout(() => resolve12(data), 500);
|
|
2234
2241
|
});
|
|
2235
2242
|
}
|
|
2236
2243
|
|
|
@@ -2749,9 +2756,9 @@ init_models();
|
|
|
2749
2756
|
function isoDate(d) {
|
|
2750
2757
|
return d.toISOString().slice(0, 10);
|
|
2751
2758
|
}
|
|
2752
|
-
function calcCost2(inputTokens, outputTokens, model) {
|
|
2759
|
+
function calcCost2(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
|
|
2753
2760
|
const p = MODEL_PRICING[model];
|
|
2754
|
-
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion;
|
|
2761
|
+
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion + cacheCreationTokens / 1e6 * p.cacheWritePerMillion + cacheReadTokens / 1e6 * p.cacheReadPerMillion;
|
|
2755
2762
|
}
|
|
2756
2763
|
async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
2757
2764
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -2770,6 +2777,7 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2770
2777
|
inputTokens: 0,
|
|
2771
2778
|
outputTokens: 0,
|
|
2772
2779
|
cacheReadTokens: 0,
|
|
2780
|
+
cacheCreationTokens: 0,
|
|
2773
2781
|
requests: 0,
|
|
2774
2782
|
costUsd: 0
|
|
2775
2783
|
});
|
|
@@ -2778,20 +2786,23 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2778
2786
|
let totalInput = 0;
|
|
2779
2787
|
let totalOutput = 0;
|
|
2780
2788
|
let totalCacheRead = 0;
|
|
2789
|
+
let totalCacheCreation = 0;
|
|
2781
2790
|
for (const sf of sessionFiles) {
|
|
2782
2791
|
const dateStr = isoDate(new Date(sf.mtimeMs));
|
|
2783
2792
|
const bucket = bucketMap.get(dateStr);
|
|
2784
2793
|
if (!bucket) continue;
|
|
2785
|
-
const usage = readSessionUsage(sf.filePath);
|
|
2794
|
+
const usage = await readSessionUsage(sf.filePath);
|
|
2786
2795
|
bucket.sessions++;
|
|
2787
2796
|
bucket.inputTokens += usage.inputTokens;
|
|
2788
2797
|
bucket.outputTokens += usage.outputTokens;
|
|
2789
2798
|
bucket.cacheReadTokens += usage.cacheReadTokens;
|
|
2799
|
+
bucket.cacheCreationTokens += usage.cacheCreationTokens;
|
|
2790
2800
|
bucket.requests += usage.requestCount;
|
|
2791
|
-
bucket.costUsd += calcCost2(usage.inputTokens, usage.outputTokens, model);
|
|
2801
|
+
bucket.costUsd += calcCost2(usage.inputTokens, usage.outputTokens, usage.cacheCreationTokens, usage.cacheReadTokens, model);
|
|
2792
2802
|
totalInput += usage.inputTokens;
|
|
2793
2803
|
totalOutput += usage.outputTokens;
|
|
2794
2804
|
totalCacheRead += usage.cacheReadTokens;
|
|
2805
|
+
totalCacheCreation += usage.cacheCreationTokens;
|
|
2795
2806
|
totalRequests += usage.requestCount;
|
|
2796
2807
|
}
|
|
2797
2808
|
const fileEvents = readAllEvents().filter(
|
|
@@ -2802,10 +2813,12 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2802
2813
|
filePath: s.filePath,
|
|
2803
2814
|
readCount: s.readCount
|
|
2804
2815
|
}));
|
|
2805
|
-
const totalCost = calcCost2(totalInput, totalOutput, model);
|
|
2816
|
+
const totalCost = calcCost2(totalInput, totalOutput, totalCacheCreation, totalCacheRead, model);
|
|
2806
2817
|
const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
|
|
2807
2818
|
const byDay = [...bucketMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
2808
2819
|
const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
|
|
2820
|
+
const dailyAvgCostUsd = days > 0 ? totalCost / days : 0;
|
|
2821
|
+
const projectedMonthlyUsd = dailyAvgCostUsd * 30;
|
|
2809
2822
|
return {
|
|
2810
2823
|
periodDays: days,
|
|
2811
2824
|
startDate: isoDate(cutoff),
|
|
@@ -2815,10 +2828,13 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2815
2828
|
totalInputTokens: totalInput,
|
|
2816
2829
|
totalOutputTokens: totalOutput,
|
|
2817
2830
|
totalCacheReadTokens: totalCacheRead,
|
|
2831
|
+
totalCacheCreationTokens: totalCacheCreation,
|
|
2818
2832
|
cacheHitRate,
|
|
2819
2833
|
totalCostUsd: totalCost,
|
|
2820
2834
|
avgCostPerSession: uniqueSessions > 0 ? totalCost / uniqueSessions : 0,
|
|
2821
2835
|
avgTokensPerRequest: totalRequests > 0 ? Math.round(totalInput / totalRequests) : 0,
|
|
2836
|
+
dailyAvgCostUsd,
|
|
2837
|
+
projectedMonthlyUsd,
|
|
2822
2838
|
byDay,
|
|
2823
2839
|
topFiles,
|
|
2824
2840
|
model,
|
|
@@ -2861,9 +2877,12 @@ function formatText(data) {
|
|
|
2861
2877
|
lines.push(` Input tokens: ${fmtNum2(data.totalInputTokens)}`);
|
|
2862
2878
|
lines.push(` Output tokens: ${fmtNum2(data.totalOutputTokens)}`);
|
|
2863
2879
|
lines.push(` Cache reads: ${fmtNum2(data.totalCacheReadTokens)} (${data.cacheHitRate}% hit rate)`);
|
|
2880
|
+
lines.push(` Cache writes: ${fmtNum2(data.totalCacheCreationTokens)}`);
|
|
2864
2881
|
lines.push(` Total cost (est.): ${fmtCost2(data.totalCostUsd)}`);
|
|
2865
2882
|
lines.push(` Avg cost/session: ${fmtCost2(data.avgCostPerSession)}`);
|
|
2866
2883
|
lines.push(` Avg tokens/request: ${fmtNum2(data.avgTokensPerRequest)}`);
|
|
2884
|
+
lines.push(` Daily avg cost: ${fmtCost2(data.dailyAvgCostUsd)}`);
|
|
2885
|
+
lines.push(` Projected (30-day): ${fmtCost2(data.projectedMonthlyUsd)}`);
|
|
2867
2886
|
lines.push(` Model: ${data.model}`);
|
|
2868
2887
|
lines.push("");
|
|
2869
2888
|
const activeDays = data.byDay.filter((d) => d.sessions > 0);
|
|
@@ -2932,9 +2951,12 @@ function formatMarkdown(data) {
|
|
|
2932
2951
|
lines.push(`| Input tokens | ${fmtNum2(data.totalInputTokens)} |`);
|
|
2933
2952
|
lines.push(`| Output tokens | ${fmtNum2(data.totalOutputTokens)} |`);
|
|
2934
2953
|
lines.push(`| Cache hit rate | ${data.cacheHitRate}% |`);
|
|
2954
|
+
lines.push(`| Cache writes | ${fmtNum2(data.totalCacheCreationTokens)} tokens |`);
|
|
2935
2955
|
lines.push(`| Total cost (est.) | ${fmtCost2(data.totalCostUsd)} |`);
|
|
2936
2956
|
lines.push(`| Avg cost/session | ${fmtCost2(data.avgCostPerSession)} |`);
|
|
2937
2957
|
lines.push(`| Avg tokens/request | ${fmtNum2(data.avgTokensPerRequest)} |`);
|
|
2958
|
+
lines.push(`| Daily avg cost | ${fmtCost2(data.dailyAvgCostUsd)} |`);
|
|
2959
|
+
lines.push(`| Projected (30-day) | ${fmtCost2(data.projectedMonthlyUsd)} |`);
|
|
2938
2960
|
lines.push(`| Model | \`${data.model}\` |`);
|
|
2939
2961
|
lines.push("");
|
|
2940
2962
|
const activeDays = data.byDay.filter((d) => d.sessions > 0);
|
|
@@ -3223,29 +3245,65 @@ async function executeWarmup(claudeMdContent, model, ttl, client) {
|
|
|
3223
3245
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3224
3246
|
};
|
|
3225
3247
|
}
|
|
3226
|
-
|
|
3248
|
+
function isValidCronExpr(expr) {
|
|
3249
|
+
const parts = expr.trim().split(/\s+/);
|
|
3250
|
+
if (parts.length < 5 || parts.length > 6) return false;
|
|
3251
|
+
return parts.every((p) => /^[0-9*,/\-]+$/.test(p));
|
|
3252
|
+
}
|
|
3253
|
+
async function installCron(cronExpr) {
|
|
3254
|
+
if (!isValidCronExpr(cronExpr)) {
|
|
3255
|
+
process.stderr.write(
|
|
3256
|
+
`Error: invalid cron expression "${cronExpr}".
|
|
3257
|
+
Example: "0 9 * * 1-5" (weekdays at 9am)
|
|
3258
|
+
`
|
|
3259
|
+
);
|
|
3260
|
+
process.exit(1);
|
|
3261
|
+
}
|
|
3227
3262
|
const { execSync: execSync3 } = await import("child_process");
|
|
3228
|
-
const command = `claudectx warmup
|
|
3263
|
+
const command = `claudectx warmup`;
|
|
3229
3264
|
const cronLine = `${cronExpr} ${command}`;
|
|
3265
|
+
const marker = "# claudectx warmup";
|
|
3230
3266
|
try {
|
|
3231
3267
|
let existing = "";
|
|
3232
3268
|
try {
|
|
3233
|
-
existing = execSync3("crontab -l
|
|
3269
|
+
existing = execSync3("crontab -l", {
|
|
3270
|
+
encoding: "utf-8",
|
|
3271
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3272
|
+
});
|
|
3234
3273
|
} catch {
|
|
3235
3274
|
existing = "";
|
|
3236
3275
|
}
|
|
3237
|
-
if (existing.includes(
|
|
3238
|
-
process.stdout.write("Cron job already installed for claudectx warmup.\n");
|
|
3276
|
+
if (existing.includes(marker)) {
|
|
3277
|
+
process.stdout.write(" Cron job already installed for claudectx warmup.\n");
|
|
3239
3278
|
return;
|
|
3240
3279
|
}
|
|
3241
|
-
const newCrontab = existing.trimEnd() +
|
|
3242
|
-
|
|
3243
|
-
|
|
3280
|
+
const newCrontab = existing.trimEnd() + `
|
|
3281
|
+
${marker}
|
|
3282
|
+
${cronLine}
|
|
3283
|
+
`;
|
|
3284
|
+
const { writeFileSync: writeFileSync11, unlinkSync: unlinkSync2 } = await import("fs");
|
|
3285
|
+
const { tmpdir } = await import("os");
|
|
3286
|
+
const { join: join17 } = await import("path");
|
|
3287
|
+
const tmpFile = join17(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
|
|
3288
|
+
try {
|
|
3289
|
+
writeFileSync11(tmpFile, newCrontab, "utf-8");
|
|
3290
|
+
execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3291
|
+
} finally {
|
|
3292
|
+
try {
|
|
3293
|
+
unlinkSync2(tmpFile);
|
|
3294
|
+
} catch {
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
process.stdout.write(` \u2713 Cron job installed: ${cronLine}
|
|
3244
3298
|
`);
|
|
3299
|
+
process.stdout.write(" Note: Set ANTHROPIC_API_KEY in your cron environment (e.g. via ~/.profile).\n");
|
|
3245
3300
|
} catch {
|
|
3246
|
-
process.stdout.write(
|
|
3301
|
+
process.stdout.write(
|
|
3302
|
+
` Could not install cron automatically. Add this line manually with "crontab -e":
|
|
3303
|
+
ANTHROPIC_API_KEY=<your-key>
|
|
3247
3304
|
${cronLine}
|
|
3248
|
-
`
|
|
3305
|
+
`
|
|
3306
|
+
);
|
|
3249
3307
|
}
|
|
3250
3308
|
}
|
|
3251
3309
|
async function warmupCommand(options) {
|
|
@@ -3315,7 +3373,7 @@ async function warmupCommand(options) {
|
|
|
3315
3373
|
}
|
|
3316
3374
|
process.stdout.write("\n");
|
|
3317
3375
|
if (options.cron) {
|
|
3318
|
-
await installCron(options.cron
|
|
3376
|
+
await installCron(options.cron);
|
|
3319
3377
|
}
|
|
3320
3378
|
}
|
|
3321
3379
|
|
|
@@ -3374,15 +3432,15 @@ async function findGitDeletedMentions(content, projectRoot) {
|
|
|
3374
3432
|
for (let i = 0; i < lines.length; i++) {
|
|
3375
3433
|
const line = lines[i];
|
|
3376
3434
|
for (const deleted of deletedFiles) {
|
|
3377
|
-
const
|
|
3378
|
-
if (line.includes(
|
|
3435
|
+
const basename8 = path23.basename(deleted);
|
|
3436
|
+
if (line.includes(basename8) || line.includes(deleted)) {
|
|
3379
3437
|
issues.push({
|
|
3380
3438
|
type: "git-deleted",
|
|
3381
3439
|
line: i + 1,
|
|
3382
3440
|
text: line.trim(),
|
|
3383
3441
|
severity: "warning",
|
|
3384
3442
|
estimatedTokenWaste: countTokens(line),
|
|
3385
|
-
suggestion: `References "${
|
|
3443
|
+
suggestion: `References "${basename8}" which was deleted from git. Consider removing this mention.`
|
|
3386
3444
|
});
|
|
3387
3445
|
break;
|
|
3388
3446
|
}
|
|
@@ -3589,11 +3647,34 @@ async function applyFix(claudeMdPath, issues) {
|
|
|
3589
3647
|
const lines = content.split("\n");
|
|
3590
3648
|
const lineSet = new Set(selectedLines.map((l) => l - 1));
|
|
3591
3649
|
const newLines = lines.filter((_, i) => !lineSet.has(i));
|
|
3592
|
-
|
|
3650
|
+
const newContent = newLines.join("\n");
|
|
3651
|
+
const backupPath = `${claudeMdPath}.bak`;
|
|
3652
|
+
fs20.writeFileSync(backupPath, content, "utf-8");
|
|
3653
|
+
const os5 = await import("os");
|
|
3654
|
+
const tmpPath = `${claudeMdPath}.tmp-${Date.now()}`;
|
|
3655
|
+
try {
|
|
3656
|
+
fs20.writeFileSync(tmpPath, newContent, "utf-8");
|
|
3657
|
+
fs20.renameSync(tmpPath, claudeMdPath);
|
|
3658
|
+
} catch (err) {
|
|
3659
|
+
try {
|
|
3660
|
+
fs20.copyFileSync(backupPath, claudeMdPath);
|
|
3661
|
+
} catch {
|
|
3662
|
+
}
|
|
3663
|
+
try {
|
|
3664
|
+
fs20.unlinkSync(tmpPath);
|
|
3665
|
+
} catch {
|
|
3666
|
+
}
|
|
3667
|
+
process.stderr.write(`Error writing CLAUDE.md: ${err instanceof Error ? err.message : String(err)}
|
|
3668
|
+
`);
|
|
3669
|
+
process.exit(1);
|
|
3670
|
+
}
|
|
3593
3671
|
process.stdout.write(`
|
|
3594
|
-
\u2713 Removed ${selectedLines.length} line(s) from ${claudeMdPath}
|
|
3672
|
+
\u2713 Removed ${selectedLines.length} line(s) from ${path24.basename(claudeMdPath)}
|
|
3673
|
+
`);
|
|
3674
|
+
process.stdout.write(` \u2713 Backup saved to ${backupPath}
|
|
3595
3675
|
|
|
3596
3676
|
`);
|
|
3677
|
+
void os5;
|
|
3597
3678
|
}
|
|
3598
3679
|
|
|
3599
3680
|
// src/commands/hooks.ts
|
|
@@ -3609,10 +3690,10 @@ var HOOK_REGISTRY = [
|
|
|
3609
3690
|
description: "Compress session when token count exceeds threshold",
|
|
3610
3691
|
triggerEvent: "PostToolUse",
|
|
3611
3692
|
matcher: "Read",
|
|
3612
|
-
|
|
3693
|
+
// API key is read from ANTHROPIC_API_KEY env var — never stored in settings.json
|
|
3694
|
+
commandTemplate: "claudectx compress --auto",
|
|
3613
3695
|
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 }
|
|
3696
|
+
threshold: { type: "number", default: 5e4, description: "Token threshold to trigger compression" }
|
|
3616
3697
|
},
|
|
3617
3698
|
category: "compression"
|
|
3618
3699
|
},
|
|
@@ -3645,10 +3726,9 @@ var HOOK_REGISTRY = [
|
|
|
3645
3726
|
description: "Pre-warm the Anthropic prompt cache on each session start",
|
|
3646
3727
|
triggerEvent: "PostToolUse",
|
|
3647
3728
|
matcher: "Read",
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
},
|
|
3729
|
+
// API key is read from ANTHROPIC_API_KEY env var — never stored in settings.json
|
|
3730
|
+
commandTemplate: "claudectx warmup",
|
|
3731
|
+
configSchema: {},
|
|
3652
3732
|
category: "warmup"
|
|
3653
3733
|
}
|
|
3654
3734
|
];
|
|
@@ -3677,9 +3757,18 @@ function buildHookEntry(def, config) {
|
|
|
3677
3757
|
// src/commands/hooks.ts
|
|
3678
3758
|
function readInstalledHooks(projectRoot) {
|
|
3679
3759
|
const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
|
|
3760
|
+
if (!fs21.existsSync(settingsPath)) return {};
|
|
3680
3761
|
try {
|
|
3681
3762
|
return JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
|
|
3682
3763
|
} catch {
|
|
3764
|
+
process.stderr.write(
|
|
3765
|
+
`Warning: ${settingsPath} exists but contains invalid JSON. Existing settings will be preserved as a backup.
|
|
3766
|
+
`
|
|
3767
|
+
);
|
|
3768
|
+
try {
|
|
3769
|
+
fs21.copyFileSync(settingsPath, `${settingsPath}.bak`);
|
|
3770
|
+
} catch {
|
|
3771
|
+
}
|
|
3683
3772
|
return {};
|
|
3684
3773
|
}
|
|
3685
3774
|
}
|
|
@@ -3766,6 +3855,16 @@ async function hooksAdd(name, projectRoot, configPairs) {
|
|
|
3766
3855
|
`);
|
|
3767
3856
|
process.exit(1);
|
|
3768
3857
|
}
|
|
3858
|
+
const sensitiveKeys = Object.keys(config).filter(
|
|
3859
|
+
(k) => /key|token|secret|password|webhook/i.test(k)
|
|
3860
|
+
);
|
|
3861
|
+
if (sensitiveKeys.length > 0) {
|
|
3862
|
+
process.stderr.write(
|
|
3863
|
+
`Warning: config field(s) [${sensitiveKeys.join(", ")}] will be stored in plain text in
|
|
3864
|
+
.claude/settings.local.json \u2014 ensure this file is in your .gitignore.
|
|
3865
|
+
`
|
|
3866
|
+
);
|
|
3867
|
+
}
|
|
3769
3868
|
const entry = { ...buildHookEntry(def, config), name };
|
|
3770
3869
|
const settings = readInstalledHooks(projectRoot);
|
|
3771
3870
|
const hooksObj = settings.hooks ?? {};
|
|
@@ -3930,7 +4029,9 @@ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
|
|
|
3930
4029
|
`);
|
|
3931
4030
|
for (const file of files) {
|
|
3932
4031
|
const filePath = path26.join(targetDir, file.filename);
|
|
3933
|
-
|
|
4032
|
+
const exists = fs22.existsSync(filePath);
|
|
4033
|
+
const prefix = options.dryRun ? "[dry-run] " : exists ? "[overwrite] " : "";
|
|
4034
|
+
process.stdout.write(` ${prefix}\u2192 .cursor/rules/${file.filename}
|
|
3934
4035
|
`);
|
|
3935
4036
|
if (!options.dryRun) {
|
|
3936
4037
|
fs22.mkdirSync(targetDir, { recursive: true });
|
|
@@ -3941,8 +4042,9 @@ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
|
|
|
3941
4042
|
} else if (to === "copilot") {
|
|
3942
4043
|
const converted = claudeMdToCopilot(content);
|
|
3943
4044
|
const targetPath = path26.join(projectRoot, ".github", "copilot-instructions.md");
|
|
4045
|
+
const exists = fs22.existsSync(targetPath);
|
|
3944
4046
|
process.stdout.write(`
|
|
3945
|
-
Converting CLAUDE.md \u2192 .github/copilot-instructions.md
|
|
4047
|
+
Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwrite]" : ""}
|
|
3946
4048
|
`);
|
|
3947
4049
|
if (!options.dryRun) {
|
|
3948
4050
|
fs22.mkdirSync(path26.dirname(targetPath), { recursive: true });
|
|
@@ -3958,8 +4060,9 @@ Converting CLAUDE.md \u2192 .github/copilot-instructions.md
|
|
|
3958
4060
|
} else if (to === "windsurf") {
|
|
3959
4061
|
const converted = claudeMdToWindsurf(content);
|
|
3960
4062
|
const targetPath = path26.join(projectRoot, ".windsurfrules");
|
|
4063
|
+
const exists = fs22.existsSync(targetPath);
|
|
3961
4064
|
process.stdout.write(`
|
|
3962
|
-
Converting CLAUDE.md \u2192 .windsurfrules
|
|
4065
|
+
Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
|
|
3963
4066
|
`);
|
|
3964
4067
|
if (!options.dryRun) {
|
|
3965
4068
|
fs22.writeFileSync(targetPath, converted, "utf-8");
|
|
@@ -4001,9 +4104,9 @@ function getDeveloperIdentity() {
|
|
|
4001
4104
|
}
|
|
4002
4105
|
return os4.hostname();
|
|
4003
4106
|
}
|
|
4004
|
-
function calcCost3(inputTokens, outputTokens, model) {
|
|
4107
|
+
function calcCost3(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
|
|
4005
4108
|
const p = MODEL_PRICING[model];
|
|
4006
|
-
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion;
|
|
4109
|
+
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion + cacheCreationTokens / 1e6 * p.cacheWritePerMillion + cacheReadTokens / 1e6 * p.cacheReadPerMillion;
|
|
4007
4110
|
}
|
|
4008
4111
|
function isoDate2(d) {
|
|
4009
4112
|
return d.toISOString().slice(0, 10);
|
|
@@ -4034,6 +4137,7 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4034
4137
|
inputTokens: 0,
|
|
4035
4138
|
outputTokens: 0,
|
|
4036
4139
|
cacheReadTokens: 0,
|
|
4140
|
+
cacheCreationTokens: 0,
|
|
4037
4141
|
requests: 0,
|
|
4038
4142
|
costUsd: 0
|
|
4039
4143
|
});
|
|
@@ -4045,14 +4149,15 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4045
4149
|
for (const sf of sessionFiles) {
|
|
4046
4150
|
const dateStr = isoDate2(new Date(sf.mtimeMs));
|
|
4047
4151
|
const bucket = bucketMap.get(dateStr);
|
|
4048
|
-
const usage = readSessionUsage(sf.filePath);
|
|
4152
|
+
const usage = await readSessionUsage(sf.filePath);
|
|
4049
4153
|
if (bucket) {
|
|
4050
4154
|
bucket.sessions++;
|
|
4051
4155
|
bucket.inputTokens += usage.inputTokens;
|
|
4052
4156
|
bucket.outputTokens += usage.outputTokens;
|
|
4053
4157
|
bucket.cacheReadTokens += usage.cacheReadTokens;
|
|
4158
|
+
bucket.cacheCreationTokens += usage.cacheCreationTokens;
|
|
4054
4159
|
bucket.requests += usage.requestCount;
|
|
4055
|
-
bucket.costUsd += calcCost3(usage.inputTokens, usage.outputTokens, model);
|
|
4160
|
+
bucket.costUsd += calcCost3(usage.inputTokens, usage.outputTokens, usage.cacheCreationTokens, usage.cacheReadTokens, model);
|
|
4056
4161
|
}
|
|
4057
4162
|
totalInput += usage.inputTokens;
|
|
4058
4163
|
totalOutput += usage.outputTokens;
|
|
@@ -4063,7 +4168,7 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4063
4168
|
(e) => new Date(e.timestamp).getTime() >= cutoffMs
|
|
4064
4169
|
);
|
|
4065
4170
|
const topWasteFiles = aggregateStats(fileEvents).slice(0, 10).map((s) => ({ filePath: s.filePath, readCount: s.readCount }));
|
|
4066
|
-
const totalCostUsd = calcCost3(totalInput, totalOutput, model);
|
|
4171
|
+
const totalCostUsd = calcCost3(totalInput, totalOutput, 0, totalCacheRead, model);
|
|
4067
4172
|
const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
|
|
4068
4173
|
const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
|
|
4069
4174
|
const developer = {
|
|
@@ -4233,7 +4338,26 @@ async function teamsShare(options) {
|
|
|
4233
4338
|
}
|
|
4234
4339
|
const latest = exportFiles[0];
|
|
4235
4340
|
const src = path28.join(storeDir, latest);
|
|
4236
|
-
|
|
4341
|
+
let destPath;
|
|
4342
|
+
try {
|
|
4343
|
+
const stat = fs24.statSync(dest);
|
|
4344
|
+
destPath = stat.isDirectory() ? path28.join(dest, latest) : dest;
|
|
4345
|
+
} catch {
|
|
4346
|
+
destPath = dest;
|
|
4347
|
+
}
|
|
4348
|
+
const destDir = path28.dirname(path28.resolve(destPath));
|
|
4349
|
+
let resolvedDir;
|
|
4350
|
+
try {
|
|
4351
|
+
resolvedDir = fs24.realpathSync(destDir);
|
|
4352
|
+
} catch {
|
|
4353
|
+
resolvedDir = destDir;
|
|
4354
|
+
}
|
|
4355
|
+
const systemDirs = ["/etc", "/bin", "/sbin", "/usr", "/var", "/proc", "/sys"];
|
|
4356
|
+
if (systemDirs.some((d) => resolvedDir === d || resolvedDir.startsWith(d + "/"))) {
|
|
4357
|
+
process.stderr.write(`Error: destination path resolves to a system directory (${resolvedDir}). Aborting.
|
|
4358
|
+
`);
|
|
4359
|
+
process.exit(1);
|
|
4360
|
+
}
|
|
4237
4361
|
fs24.copyFileSync(src, destPath);
|
|
4238
4362
|
process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
|
|
4239
4363
|
|
|
@@ -4260,7 +4384,7 @@ async function teamsCommand(subcommand, options) {
|
|
|
4260
4384
|
}
|
|
4261
4385
|
|
|
4262
4386
|
// src/index.ts
|
|
4263
|
-
var VERSION = "1.
|
|
4387
|
+
var VERSION = "1.1.2";
|
|
4264
4388
|
var DESCRIPTION = "Reduce Claude Code token usage by up to 80%. Context analyzer, auto-optimizer, live dashboard, and smart MCP tools.";
|
|
4265
4389
|
var program = new Command();
|
|
4266
4390
|
program.name("claudectx").description(DESCRIPTION).version(VERSION);
|