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/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
- let content;
234
+ const { createReadStream } = await import("fs");
235
+ const { createInterface } = await import("readline");
235
236
  try {
236
- content = fs9.readFileSync(sessionFilePath, "utf-8");
237
- } catch {
238
- return result;
239
- }
240
- const lines = content.trim().split("\n").filter(Boolean);
241
- for (const line of lines) {
242
- try {
243
- const entry = JSON.parse(line);
244
- const usage = entry.usage ?? entry.message?.usage;
245
- if (!usage) continue;
246
- const isAssistant = entry.type === "assistant" || entry.message?.role === "assistant";
247
- if (isAssistant) {
248
- result.inputTokens += usage.input_tokens ?? 0;
249
- result.outputTokens += usage.output_tokens ?? 0;
250
- result.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
251
- result.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
252
- result.requestCount++;
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 usage2 = sessionFile2 ? readSessionUsage(sessionFile2) : {
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
- setState((prev) => ({
393
- fileStats: fileStats2,
394
- usage: usage2,
395
- sessionFile: sessionFile2,
396
- lastUpdated: /* @__PURE__ */ new Date(),
397
- tickCount: prev.tickCount + 1
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((resolve11) => {
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", () => resolve11(data));
2233
- setTimeout(() => resolve11(data), 500);
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
- async function installCron(cronExpr, apiKey) {
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 --api-key ${apiKey}`;
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 2>/dev/null", { encoding: "utf-8" });
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("claudectx warmup")) {
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() + "\n" + cronLine + "\n";
3242
- execSync3(`echo ${JSON.stringify(newCrontab)} | crontab -`);
3243
- process.stdout.write(`\u2713 Cron job installed: ${cronLine}
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(`Could not install cron automatically. Add manually:
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, apiKey);
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 basename7 = path23.basename(deleted);
3378
- if (line.includes(basename7) || line.includes(deleted)) {
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 "${basename7}" which was deleted from git. Consider removing this mention.`
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
- fs20.writeFileSync(claudeMdPath, newLines.join("\n"), "utf-8");
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
- commandTemplate: "claudectx compress --auto --api-key {{config.apiKey}}",
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
- commandTemplate: "claudectx warmup --api-key {{config.apiKey}}",
3649
- configSchema: {
3650
- apiKey: { type: "string", description: "Anthropic API key", required: true }
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
- process.stdout.write(` ${options.dryRun ? "[dry-run] " : ""}\u2192 .cursor/rules/${file.filename}
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
- const destPath = fs24.statSync(dest).isDirectory() ? path28.join(dest, latest) : dest;
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.0.0";
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);