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.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
- let content;
251
+ const { createReadStream } = await import("fs");
252
+ const { createInterface } = await import("readline");
252
253
  try {
253
- content = fs9.readFileSync(sessionFilePath, "utf-8");
254
- } catch {
255
- return result;
256
- }
257
- const lines = content.trim().split("\n").filter(Boolean);
258
- for (const line of lines) {
259
- try {
260
- const entry = JSON.parse(line);
261
- const usage = entry.usage ?? entry.message?.usage;
262
- if (!usage) continue;
263
- const isAssistant = entry.type === "assistant" || entry.message?.role === "assistant";
264
- if (isAssistant) {
265
- result.inputTokens += usage.input_tokens ?? 0;
266
- result.outputTokens += usage.output_tokens ?? 0;
267
- result.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
268
- result.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
269
- result.requestCount++;
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 usage2 = sessionFile2 ? readSessionUsage(sessionFile2) : {
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
- setState((prev) => ({
408
- fileStats: fileStats2,
409
- usage: usage2,
410
- sessionFile: sessionFile2,
411
- lastUpdated: /* @__PURE__ */ new Date(),
412
- tickCount: prev.tickCount + 1
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((resolve11) => {
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", () => resolve11(data));
2251
- setTimeout(() => resolve11(data), 500);
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
- async function installCron(cronExpr, apiKey) {
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 --api-key ${apiKey}`;
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 2>/dev/null", { encoding: "utf-8" });
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("claudectx warmup")) {
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() + "\n" + cronLine + "\n";
3260
- execSync3(`echo ${JSON.stringify(newCrontab)} | crontab -`);
3261
- process.stdout.write(`\u2713 Cron job installed: ${cronLine}
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(`Could not install cron automatically. Add manually:
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, apiKey);
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 basename7 = path22.basename(deleted);
3396
- if (line.includes(basename7) || line.includes(deleted)) {
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 "${basename7}" which was deleted from git. Consider removing this mention.`
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
- fs20.writeFileSync(claudeMdPath, newLines.join("\n"), "utf-8");
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
- commandTemplate: "claudectx compress --auto --api-key {{config.apiKey}}",
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
- commandTemplate: "claudectx warmup --api-key {{config.apiKey}}",
3667
- configSchema: {
3668
- apiKey: { type: "string", description: "Anthropic API key", required: true }
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
- process.stdout.write(` ${options.dryRun ? "[dry-run] " : ""}\u2192 .cursor/rules/${file.filename}
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
- const destPath = fs24.statSync(dest).isDirectory() ? path27.join(dest, latest) : dest;
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.0.0";
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);