claudectx 1.0.0 → 1.1.0

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
@@ -1277,6 +1277,9 @@ function detectRedundantContent(claudeMdContent, memoryContent) {
1277
1277
  // src/analyzer/cost-calculator.ts
1278
1278
  init_esm_shims();
1279
1279
  init_models();
1280
+ function tokenCost(tokens, model) {
1281
+ return calculateCost(tokens, model);
1282
+ }
1280
1283
  function sessionCost(tokensPerRequest, model) {
1281
1284
  const perRequest = calculateCost(tokensPerRequest, model);
1282
1285
  return {
@@ -1899,6 +1902,14 @@ function applyHooksInstall(result) {
1899
1902
  }
1900
1903
  fs6.writeFileSync(result.settingsPath, JSON.stringify(result.mergedSettings, null, 2) + "\n", "utf-8");
1901
1904
  }
1905
+ function writeHooksSettings(projectRoot, mergedSettings) {
1906
+ const settingsPath = path7.join(projectRoot, ".claude", "settings.local.json");
1907
+ const dir = path7.dirname(settingsPath);
1908
+ if (!fs6.existsSync(dir)) {
1909
+ fs6.mkdirSync(dir, { recursive: true });
1910
+ }
1911
+ fs6.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n", "utf-8");
1912
+ }
1902
1913
  function isAlreadyInstalled(projectRoot) {
1903
1914
  const settingsPath = path7.join(projectRoot, ".claude", "settings.local.json");
1904
1915
  if (!fs6.existsSync(settingsPath)) return false;
@@ -2214,12 +2225,12 @@ async function handleLogStdin() {
2214
2225
  }
2215
2226
  }
2216
2227
  function readStdin() {
2217
- return new Promise((resolve6) => {
2228
+ return new Promise((resolve11) => {
2218
2229
  let data = "";
2219
2230
  process.stdin.setEncoding("utf-8");
2220
2231
  process.stdin.on("data", (chunk) => data += chunk);
2221
- process.stdin.on("end", () => resolve6(data));
2222
- setTimeout(() => resolve6(data), 500);
2232
+ process.stdin.on("end", () => resolve11(data));
2233
+ setTimeout(() => resolve11(data), 500);
2223
2234
  });
2224
2235
  }
2225
2236
 
@@ -2473,8 +2484,8 @@ async function summariseWithAI(conversationText, apiKey) {
2473
2484
  if (!key) {
2474
2485
  throw new Error("No API key available");
2475
2486
  }
2476
- const { default: Anthropic } = await import("@anthropic-ai/sdk");
2477
- const client = new Anthropic({ apiKey: key });
2487
+ const { default: Anthropic2 } = await import("@anthropic-ai/sdk");
2488
+ const client = new Anthropic2({ apiKey: key });
2478
2489
  const response = await client.messages.create({
2479
2490
  model: SUMMARY_MODEL,
2480
2491
  max_tokens: SUMMARY_MAX_TOKENS,
@@ -2981,6 +2992,1273 @@ async function reportCommand(options) {
2981
2992
  process.stdout.write(output + "\n");
2982
2993
  }
2983
2994
 
2995
+ // src/commands/budget.ts
2996
+ init_esm_shims();
2997
+ import * as path21 from "path";
2998
+
2999
+ // src/analyzer/budget-estimator.ts
3000
+ init_esm_shims();
3001
+ init_tokenizer();
3002
+ import * as fs17 from "fs";
3003
+ import * as path20 from "path";
3004
+ import { glob as glob2 } from "glob";
3005
+ init_session_store();
3006
+ function resolveGlobs(globs, projectRoot) {
3007
+ const results = [];
3008
+ for (const pattern of globs) {
3009
+ try {
3010
+ const matches = glob2.sync(pattern, {
3011
+ cwd: projectRoot,
3012
+ absolute: true,
3013
+ nodir: true,
3014
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/*.min.js"]
3015
+ });
3016
+ results.push(...matches);
3017
+ } catch {
3018
+ }
3019
+ }
3020
+ return [...new Set(results)];
3021
+ }
3022
+ function classifyCacheHit(recentReadCount) {
3023
+ if (recentReadCount >= 3) return "high";
3024
+ if (recentReadCount >= 1) return "medium";
3025
+ return "low";
3026
+ }
3027
+ function suggestClaudeignoreAdditions(files, projectRoot) {
3028
+ const ignorePath = path20.join(projectRoot, ".claudeignore");
3029
+ let ignorePatterns = [];
3030
+ try {
3031
+ const content = fs17.readFileSync(ignorePath, "utf-8");
3032
+ ignorePatterns = content.split("\n").filter(Boolean);
3033
+ } catch {
3034
+ }
3035
+ const recommendations = [];
3036
+ for (const file of files) {
3037
+ if (file.tokenCount <= WASTE_THRESHOLDS.MAX_REFERENCE_FILE_TOKENS) continue;
3038
+ const rel = path20.relative(projectRoot, file.filePath);
3039
+ const alreadyIgnored = ignorePatterns.some((pattern) => {
3040
+ const cleanPattern = pattern.replace(/^!/, "");
3041
+ return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g, path20.sep));
3042
+ });
3043
+ if (!alreadyIgnored) {
3044
+ recommendations.push(rel);
3045
+ }
3046
+ }
3047
+ return recommendations;
3048
+ }
3049
+ async function estimateBudget(globs, projectRoot, model, thresholdTokens) {
3050
+ const filePaths = resolveGlobs(globs, projectRoot);
3051
+ const events = readAllEvents();
3052
+ const readCounts = /* @__PURE__ */ new Map();
3053
+ for (const event of events) {
3054
+ const count = readCounts.get(event.filePath) ?? 0;
3055
+ readCounts.set(event.filePath, count + 1);
3056
+ }
3057
+ const files = [];
3058
+ for (const filePath of filePaths) {
3059
+ let content = "";
3060
+ try {
3061
+ content = fs17.readFileSync(filePath, "utf-8");
3062
+ } catch {
3063
+ continue;
3064
+ }
3065
+ const tokenCount = countTokens(content);
3066
+ const recentReadCount = readCounts.get(filePath) ?? 0;
3067
+ const cacheHitLikelihood = classifyCacheHit(recentReadCount);
3068
+ const estimatedCostUsd = tokenCost(tokenCount, model);
3069
+ files.push({
3070
+ filePath,
3071
+ tokenCount,
3072
+ recentReadCount,
3073
+ cacheHitLikelihood,
3074
+ estimatedCostUsd
3075
+ });
3076
+ }
3077
+ files.sort((a, b) => b.tokenCount - a.tokenCount);
3078
+ const totalTokens = files.reduce((sum, f) => sum + f.tokenCount, 0);
3079
+ const totalEstimatedCostUsd = tokenCost(totalTokens, model);
3080
+ const CACHE_WEIGHTS = { high: 0.85, medium: 0.5, low: 0 };
3081
+ let weightedSum = 0;
3082
+ for (const f of files) {
3083
+ weightedSum += f.tokenCount * CACHE_WEIGHTS[f.cacheHitLikelihood];
3084
+ }
3085
+ const cacheHitPotential = totalTokens > 0 ? Math.round(weightedSum / totalTokens * 100) : 0;
3086
+ const claudeignoreRecommendations = suggestClaudeignoreAdditions(files, projectRoot);
3087
+ return {
3088
+ globs,
3089
+ model,
3090
+ files,
3091
+ totalTokens,
3092
+ totalEstimatedCostUsd,
3093
+ thresholdExceeded: totalTokens > thresholdTokens,
3094
+ thresholdTokens,
3095
+ cacheHitPotential,
3096
+ claudeignoreRecommendations
3097
+ };
3098
+ }
3099
+ function formatBudgetReport(report) {
3100
+ const lines = [];
3101
+ lines.push("");
3102
+ lines.push("claudectx budget \u2014 context cost estimate");
3103
+ lines.push("\u2550".repeat(50));
3104
+ if (report.files.length === 0) {
3105
+ lines.push("No files matched the given glob patterns.");
3106
+ return lines.join("\n");
3107
+ }
3108
+ const LIKELIHOOD_ICON = { high: "\u{1F7E2}", medium: "\u{1F7E1}", low: "\u{1F534}" };
3109
+ const maxPathLen = Math.min(
3110
+ Math.max(...report.files.map((f) => path20.basename(f.filePath).length)),
3111
+ 40
3112
+ );
3113
+ lines.push(
3114
+ ` ${"File".padEnd(maxPathLen)} ${"Tokens".padStart(7)} Cache Cost`
3115
+ );
3116
+ lines.push("\u2500".repeat(50));
3117
+ for (const file of report.files.slice(0, 20)) {
3118
+ const name = path20.basename(file.filePath).slice(0, maxPathLen).padEnd(maxPathLen);
3119
+ const tokens = file.tokenCount.toLocaleString().padStart(7);
3120
+ const cache = `${LIKELIHOOD_ICON[file.cacheHitLikelihood]} ${file.cacheHitLikelihood.padEnd(6)}`;
3121
+ const cost = formatCost(file.estimatedCostUsd).padStart(7);
3122
+ lines.push(` ${name} ${tokens} ${cache} ${cost}`);
3123
+ }
3124
+ if (report.files.length > 20) {
3125
+ lines.push(` ... and ${report.files.length - 20} more files`);
3126
+ }
3127
+ lines.push("\u2500".repeat(50));
3128
+ const thresholdStatus = report.thresholdExceeded ? `\u26A0 EXCEEDS threshold (${report.thresholdTokens.toLocaleString()} tokens)` : `\u2713 Within threshold (${report.thresholdTokens.toLocaleString()} tokens)`;
3129
+ lines.push(` Total tokens: ${report.totalTokens.toLocaleString().padStart(10)}`);
3130
+ lines.push(` Estimated cost: ${formatCost(report.totalEstimatedCostUsd).padStart(10)}`);
3131
+ lines.push(` Cache potential: ${`${report.cacheHitPotential}%`.padStart(10)}`);
3132
+ lines.push(` ${thresholdStatus}`);
3133
+ if (report.claudeignoreRecommendations.length > 0) {
3134
+ lines.push("");
3135
+ lines.push(" \u{1F4A1} Add to .claudeignore to exclude large files:");
3136
+ for (const rec of report.claudeignoreRecommendations.slice(0, 5)) {
3137
+ lines.push(` ${rec}`);
3138
+ }
3139
+ }
3140
+ lines.push("");
3141
+ return lines.join("\n");
3142
+ }
3143
+
3144
+ // src/commands/budget.ts
3145
+ init_models();
3146
+ async function budgetCommand(globs, options) {
3147
+ const projectPath = options.path ? path21.resolve(options.path) : process.cwd();
3148
+ const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3149
+ const model = resolveModel(options.model ?? "sonnet");
3150
+ const thresholdTokens = parseInt(options.threshold ?? "10000", 10);
3151
+ if (globs.length === 0) {
3152
+ process.stderr.write("Error: at least one glob pattern is required.\n");
3153
+ process.stderr.write('Example: claudectx budget "src/**/*.ts"\n');
3154
+ process.exit(1);
3155
+ }
3156
+ const report = await estimateBudget(globs, projectRoot, model, thresholdTokens);
3157
+ if (options.json) {
3158
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
3159
+ return;
3160
+ }
3161
+ process.stdout.write(formatBudgetReport(report));
3162
+ }
3163
+
3164
+ // src/commands/warmup.ts
3165
+ init_esm_shims();
3166
+ import * as path22 from "path";
3167
+ import Anthropic from "@anthropic-ai/sdk";
3168
+ init_models();
3169
+ import fs18 from "fs";
3170
+ function buildWarmupMessages(claudeMdContent) {
3171
+ const systemBlock = {
3172
+ type: "text",
3173
+ text: claudeMdContent || "# Project\nNo CLAUDE.md found.",
3174
+ // @ts-expect-error — cache_control is valid but not yet in the TS types for all SDKs
3175
+ cache_control: { type: "ephemeral" }
3176
+ };
3177
+ return {
3178
+ system: [systemBlock],
3179
+ messages: [
3180
+ {
3181
+ role: "user",
3182
+ content: "ping"
3183
+ }
3184
+ ]
3185
+ };
3186
+ }
3187
+ function calculateBreakEven(writeTokens, model, ttlMinutes) {
3188
+ const pricing = MODEL_PRICING[model];
3189
+ const writeMultiplier = ttlMinutes === 60 ? 2 : 1.25;
3190
+ const writeCostPerMillion = pricing.inputPerMillion * writeMultiplier;
3191
+ const writeCostUsd = writeTokens / 1e6 * writeCostPerMillion;
3192
+ const readCostUsd = writeTokens / 1e6 * pricing.cacheReadPerMillion;
3193
+ const inputCostUsd = writeTokens / 1e6 * pricing.inputPerMillion;
3194
+ const savingsPerHit = inputCostUsd - readCostUsd;
3195
+ const breakEvenRequests = savingsPerHit > 0 ? Math.ceil(writeCostUsd / savingsPerHit) : 999;
3196
+ return { breakEvenRequests, savingsPerHit, writeCostUsd };
3197
+ }
3198
+ async function executeWarmup(claudeMdContent, model, ttl, client) {
3199
+ const { system, messages } = buildWarmupMessages(claudeMdContent);
3200
+ const betas = ["prompt-caching-2024-07-31"];
3201
+ if (ttl === 60) betas.push("extended-cache-ttl-2025-02-19");
3202
+ const response = await client.beta.messages.create({
3203
+ model,
3204
+ max_tokens: 8,
3205
+ system,
3206
+ messages,
3207
+ betas
3208
+ });
3209
+ const usage = response.usage;
3210
+ const tokensWarmed = usage.cache_creation_input_tokens ?? 0;
3211
+ const { breakEvenRequests, savingsPerHit, writeCostUsd } = calculateBreakEven(
3212
+ tokensWarmed,
3213
+ model,
3214
+ ttl
3215
+ );
3216
+ return {
3217
+ model,
3218
+ tokensWarmed,
3219
+ cacheWriteCostUsd: writeCostUsd,
3220
+ estimatedSavingsPerHit: savingsPerHit,
3221
+ breakEvenRequests,
3222
+ ttlMinutes: ttl,
3223
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3224
+ };
3225
+ }
3226
+ async function installCron(cronExpr, apiKey) {
3227
+ const { execSync: execSync3 } = await import("child_process");
3228
+ const command = `claudectx warmup --api-key ${apiKey}`;
3229
+ const cronLine = `${cronExpr} ${command}`;
3230
+ try {
3231
+ let existing = "";
3232
+ try {
3233
+ existing = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
3234
+ } catch {
3235
+ existing = "";
3236
+ }
3237
+ if (existing.includes("claudectx warmup")) {
3238
+ process.stdout.write("Cron job already installed for claudectx warmup.\n");
3239
+ return;
3240
+ }
3241
+ const newCrontab = existing.trimEnd() + "\n" + cronLine + "\n";
3242
+ execSync3(`echo ${JSON.stringify(newCrontab)} | crontab -`);
3243
+ process.stdout.write(`\u2713 Cron job installed: ${cronLine}
3244
+ `);
3245
+ } catch {
3246
+ process.stdout.write(`Could not install cron automatically. Add manually:
3247
+ ${cronLine}
3248
+ `);
3249
+ }
3250
+ }
3251
+ async function warmupCommand(options) {
3252
+ const projectPath = options.path ? path22.resolve(options.path) : process.cwd();
3253
+ const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3254
+ const model = resolveModel(options.model ?? "haiku");
3255
+ const ttl = options.ttl === "60" ? 60 : 5;
3256
+ const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
3257
+ if (!apiKey) {
3258
+ process.stderr.write(
3259
+ "Error: Anthropic API key required. Use --api-key or set ANTHROPIC_API_KEY.\n"
3260
+ );
3261
+ process.exit(1);
3262
+ }
3263
+ let claudeMdContent = "";
3264
+ const claudeMdPath = path22.join(projectRoot, "CLAUDE.md");
3265
+ try {
3266
+ claudeMdContent = fs18.readFileSync(claudeMdPath, "utf-8");
3267
+ } catch {
3268
+ process.stderr.write(`Warning: No CLAUDE.md found at ${claudeMdPath}
3269
+ `);
3270
+ }
3271
+ const client = new Anthropic({ apiKey });
3272
+ if (!options.json) {
3273
+ process.stdout.write(`Warming up prompt cache (model: ${model}, TTL: ${ttl}min)...
3274
+ `);
3275
+ }
3276
+ let result;
3277
+ try {
3278
+ result = await executeWarmup(claudeMdContent, model, ttl, client);
3279
+ } catch (err) {
3280
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3281
+ `);
3282
+ process.exit(1);
3283
+ }
3284
+ if (options.json) {
3285
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
3286
+ return;
3287
+ }
3288
+ process.stdout.write("\n");
3289
+ process.stdout.write("claudectx warmup \u2014 prompt cache primed\n");
3290
+ process.stdout.write("\u2550".repeat(45) + "\n");
3291
+ process.stdout.write(` Model: ${result.model}
3292
+ `);
3293
+ process.stdout.write(` Tokens warmed: ${result.tokensWarmed.toLocaleString()}
3294
+ `);
3295
+ process.stdout.write(` Cache write cost: ${formatCost(result.cacheWriteCostUsd)}
3296
+ `);
3297
+ process.stdout.write(` Savings per hit: ${formatCost(result.estimatedSavingsPerHit)}
3298
+ `);
3299
+ process.stdout.write(` Break-even after: ${result.breakEvenRequests} requests
3300
+ `);
3301
+ process.stdout.write(` TTL: ${result.ttlMinutes} minutes
3302
+ `);
3303
+ process.stdout.write("\n");
3304
+ if (result.tokensWarmed === 0) {
3305
+ process.stdout.write(
3306
+ ` \u26A0 No tokens were cached. CLAUDE.md may be below the minimum token threshold
3307
+ (${model === "claude-haiku-4-5" ? "4,096" : "1,024"} tokens for ${model}).
3308
+ `
3309
+ );
3310
+ } else {
3311
+ process.stdout.write(
3312
+ ` \u2713 First ${result.ttlMinutes === 60 ? "60-minute" : "5-minute"} window of requests will benefit from cache hits.
3313
+ `
3314
+ );
3315
+ }
3316
+ process.stdout.write("\n");
3317
+ if (options.cron) {
3318
+ await installCron(options.cron, apiKey);
3319
+ }
3320
+ }
3321
+
3322
+ // src/commands/drift.ts
3323
+ init_esm_shims();
3324
+ import * as path24 from "path";
3325
+ import * as fs20 from "fs";
3326
+
3327
+ // src/analyzer/drift-detector.ts
3328
+ init_esm_shims();
3329
+ init_tokenizer();
3330
+ init_session_store();
3331
+ import * as fs19 from "fs";
3332
+ import * as path23 from "path";
3333
+ import * as childProcess from "child_process";
3334
+ var INLINE_PATH_RE = /(?:^|\s)((?:\.{1,2}\/|src\/|lib\/|docs\/|app\/|tests?\/)\S+\.\w{1,6})/gm;
3335
+ var AT_REF_RE = /^@(.+)$/;
3336
+ function findDeadAtReferences(content, projectRoot) {
3337
+ const issues = [];
3338
+ const lines = content.split("\n");
3339
+ for (let i = 0; i < lines.length; i++) {
3340
+ const match = lines[i].match(AT_REF_RE);
3341
+ if (!match) continue;
3342
+ const ref = match[1].trim();
3343
+ const absPath = path23.isAbsolute(ref) ? ref : path23.join(projectRoot, ref);
3344
+ if (!fs19.existsSync(absPath)) {
3345
+ const lineText = lines[i];
3346
+ issues.push({
3347
+ type: "dead-ref",
3348
+ line: i + 1,
3349
+ text: lineText,
3350
+ severity: "error",
3351
+ estimatedTokenWaste: countTokens(lineText),
3352
+ suggestion: `File "${ref}" does not exist. Remove this @reference or update the path.`
3353
+ });
3354
+ }
3355
+ }
3356
+ return issues;
3357
+ }
3358
+ async function findGitDeletedMentions(content, projectRoot) {
3359
+ const issues = [];
3360
+ let deletedFiles = /* @__PURE__ */ new Set();
3361
+ try {
3362
+ const output = childProcess.execSync(
3363
+ "git log --diff-filter=D --name-only --pretty=format: --",
3364
+ { cwd: projectRoot, encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
3365
+ );
3366
+ deletedFiles = new Set(
3367
+ output.split("\n").map((f) => f.trim()).filter(Boolean)
3368
+ );
3369
+ } catch {
3370
+ return [];
3371
+ }
3372
+ if (deletedFiles.size === 0) return [];
3373
+ const lines = content.split("\n");
3374
+ for (let i = 0; i < lines.length; i++) {
3375
+ const line = lines[i];
3376
+ for (const deleted of deletedFiles) {
3377
+ const basename7 = path23.basename(deleted);
3378
+ if (line.includes(basename7) || line.includes(deleted)) {
3379
+ issues.push({
3380
+ type: "git-deleted",
3381
+ line: i + 1,
3382
+ text: line.trim(),
3383
+ severity: "warning",
3384
+ estimatedTokenWaste: countTokens(line),
3385
+ suggestion: `References "${basename7}" which was deleted from git. Consider removing this mention.`
3386
+ });
3387
+ break;
3388
+ }
3389
+ }
3390
+ }
3391
+ return issues;
3392
+ }
3393
+ function findStaleSections(content, events, dayWindow) {
3394
+ const issues = [];
3395
+ const cutoff = Date.now() - dayWindow * 24 * 60 * 60 * 1e3;
3396
+ const recentEvents = events.filter((e) => new Date(e.timestamp).getTime() >= cutoff);
3397
+ const recentPaths = new Set(
3398
+ recentEvents.map((e) => e.filePath.toLowerCase())
3399
+ );
3400
+ const lines = content.split("\n");
3401
+ const sections = [];
3402
+ let currentSection = null;
3403
+ for (let i = 0; i < lines.length; i++) {
3404
+ const sectionMatch = lines[i].match(/^#{1,3}\s+(.+)$/);
3405
+ if (sectionMatch) {
3406
+ if (currentSection) sections.push(currentSection);
3407
+ currentSection = { line: i + 1, header: sectionMatch[1], bodyLines: [] };
3408
+ } else if (currentSection) {
3409
+ currentSection.bodyLines.push(lines[i]);
3410
+ }
3411
+ }
3412
+ if (currentSection) sections.push(currentSection);
3413
+ for (const section of sections) {
3414
+ const headerWords = section.header.toLowerCase().split(/\W+/).filter((w) => w.length > 3);
3415
+ const matched = headerWords.some(
3416
+ (word) => [...recentPaths].some((p) => {
3417
+ if (p.includes(word)) return true;
3418
+ const segments = p.split(/[/\\.]/).filter((s) => s.length >= 4);
3419
+ return segments.some((seg) => word.startsWith(seg) || seg.startsWith(word));
3420
+ })
3421
+ );
3422
+ if (!matched && recentEvents.length > 0) {
3423
+ const sectionContent = section.bodyLines.join("\n");
3424
+ issues.push({
3425
+ type: "stale-section",
3426
+ line: section.line,
3427
+ text: `## ${section.header}`,
3428
+ severity: "info",
3429
+ estimatedTokenWaste: countTokens(`## ${section.header}
3430
+ ${sectionContent}`),
3431
+ suggestion: `Section "## ${section.header}" had no matching file reads in the last ${dayWindow} days. Consider removing or archiving it.`
3432
+ });
3433
+ }
3434
+ }
3435
+ return issues;
3436
+ }
3437
+ function findDeadInlinePaths(content, projectRoot) {
3438
+ const issues = [];
3439
+ const lines = content.split("\n");
3440
+ for (let i = 0; i < lines.length; i++) {
3441
+ const line = lines[i];
3442
+ if (AT_REF_RE.test(line)) continue;
3443
+ let match;
3444
+ INLINE_PATH_RE.lastIndex = 0;
3445
+ const seen = /* @__PURE__ */ new Set();
3446
+ while ((match = INLINE_PATH_RE.exec(line)) !== null) {
3447
+ const rawPath = match[1].trim();
3448
+ if (seen.has(rawPath)) continue;
3449
+ seen.add(rawPath);
3450
+ const absPath = path23.isAbsolute(rawPath) ? rawPath : path23.join(projectRoot, rawPath);
3451
+ if (!fs19.existsSync(absPath)) {
3452
+ issues.push({
3453
+ type: "dead-inline-path",
3454
+ line: i + 1,
3455
+ text: line.trim(),
3456
+ severity: "warning",
3457
+ estimatedTokenWaste: countTokens(rawPath),
3458
+ suggestion: `Path "${rawPath}" no longer exists. Update or remove this reference.`
3459
+ });
3460
+ break;
3461
+ }
3462
+ }
3463
+ }
3464
+ return issues;
3465
+ }
3466
+ async function detectDrift(projectRoot, dayWindow) {
3467
+ const claudeMdPath = path23.join(projectRoot, "CLAUDE.md");
3468
+ let content = "";
3469
+ try {
3470
+ content = fs19.readFileSync(claudeMdPath, "utf-8");
3471
+ } catch {
3472
+ return {
3473
+ claudeMdPath,
3474
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
3475
+ dayWindow,
3476
+ issues: [],
3477
+ totalWastedTokens: 0
3478
+ };
3479
+ }
3480
+ const events = readAllEvents();
3481
+ const [deadRefs, gitDeleted, staleSections, deadInlinePaths] = await Promise.all([
3482
+ Promise.resolve(findDeadAtReferences(content, projectRoot)),
3483
+ findGitDeletedMentions(content, projectRoot),
3484
+ Promise.resolve(findStaleSections(content, events, dayWindow)),
3485
+ Promise.resolve(findDeadInlinePaths(content, projectRoot))
3486
+ ]);
3487
+ const allIssues = [...deadRefs, ...gitDeleted, ...staleSections, ...deadInlinePaths];
3488
+ allIssues.sort((a, b) => a.line - b.line);
3489
+ const totalWastedTokens = allIssues.reduce((sum, i) => sum + i.estimatedTokenWaste, 0);
3490
+ return {
3491
+ claudeMdPath,
3492
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
3493
+ dayWindow,
3494
+ issues: allIssues,
3495
+ totalWastedTokens
3496
+ };
3497
+ }
3498
+
3499
+ // src/commands/drift.ts
3500
+ var SEVERITY_ICON = {
3501
+ error: "\u2716",
3502
+ warning: "\u26A0",
3503
+ info: "\xB7"
3504
+ };
3505
+ var TYPE_LABEL = {
3506
+ "dead-ref": "Dead @ref",
3507
+ "git-deleted": "Git deleted",
3508
+ "stale-section": "Stale section",
3509
+ "dead-inline-path": "Dead path"
3510
+ };
3511
+ async function driftCommand(options) {
3512
+ const projectPath = options.path ? path24.resolve(options.path) : process.cwd();
3513
+ const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3514
+ const dayWindow = parseInt(options.days ?? "30", 10);
3515
+ const report = await detectDrift(projectRoot, dayWindow);
3516
+ if (options.json) {
3517
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
3518
+ return;
3519
+ }
3520
+ process.stdout.write("\n");
3521
+ process.stdout.write("claudectx drift \u2014 CLAUDE.md staleness check\n");
3522
+ process.stdout.write("\u2550".repeat(55) + "\n");
3523
+ process.stdout.write(` File: ${report.claudeMdPath}
3524
+ `);
3525
+ process.stdout.write(` Window: last ${report.dayWindow} days
3526
+ `);
3527
+ process.stdout.write("\n");
3528
+ if (report.issues.length === 0) {
3529
+ process.stdout.write(" \u2713 No drift detected. CLAUDE.md looks clean.\n\n");
3530
+ return;
3531
+ }
3532
+ process.stdout.write(
3533
+ ` ${"Line".padEnd(5)} ${"Type".padEnd(14)} ${"Waste".padStart(6)} Issue
3534
+ `
3535
+ );
3536
+ process.stdout.write("\u2500".repeat(55) + "\n");
3537
+ for (const issue of report.issues) {
3538
+ const icon = SEVERITY_ICON[issue.severity];
3539
+ const typeLabel = TYPE_LABEL[issue.type].padEnd(14);
3540
+ const waste = `${issue.estimatedTokenWaste}t`.padStart(6);
3541
+ const lineNum = String(issue.line).padEnd(5);
3542
+ const text = issue.text.slice(0, 40);
3543
+ process.stdout.write(` ${lineNum} ${typeLabel} ${waste} ${icon} ${text}
3544
+ `);
3545
+ }
3546
+ process.stdout.write("\u2500".repeat(55) + "\n");
3547
+ process.stdout.write(
3548
+ ` ${report.issues.length} issue(s) found \xB7 ~${report.totalWastedTokens} tokens wasted
3549
+ `
3550
+ );
3551
+ process.stdout.write("\n");
3552
+ if (report.issues.length > 0) {
3553
+ process.stdout.write(" Suggestions:\n");
3554
+ const shown = /* @__PURE__ */ new Set();
3555
+ for (const issue of report.issues.slice(0, 5)) {
3556
+ if (!shown.has(issue.suggestion)) {
3557
+ process.stdout.write(` Line ${issue.line}: ${issue.suggestion}
3558
+ `);
3559
+ shown.add(issue.suggestion);
3560
+ }
3561
+ }
3562
+ process.stdout.write("\n");
3563
+ }
3564
+ if (options.fix && report.issues.length > 0) {
3565
+ await applyFix(report.claudeMdPath, report.issues);
3566
+ } else if (report.issues.length > 0 && !options.fix) {
3567
+ process.stdout.write(" Run with --fix to interactively remove flagged lines.\n\n");
3568
+ }
3569
+ }
3570
+ async function applyFix(claudeMdPath, issues) {
3571
+ const { checkbox: checkbox2 } = await import("@inquirer/prompts").catch(() => {
3572
+ process.stderr.write("Error: @inquirer/prompts is required for --fix mode. Run: npm install -g @inquirer/prompts\n");
3573
+ process.exit(1);
3574
+ });
3575
+ const choices = issues.map((issue) => ({
3576
+ name: `Line ${issue.line}: ${issue.text.slice(0, 60)} (${issue.estimatedTokenWaste}t)`,
3577
+ value: issue.line,
3578
+ checked: issue.severity === "error"
3579
+ }));
3580
+ const selectedLines = await checkbox2({
3581
+ message: "Select lines to remove from CLAUDE.md:",
3582
+ choices
3583
+ });
3584
+ if (selectedLines.length === 0) {
3585
+ process.stdout.write("No lines selected. Nothing changed.\n");
3586
+ return;
3587
+ }
3588
+ const content = fs20.readFileSync(claudeMdPath, "utf-8");
3589
+ const lines = content.split("\n");
3590
+ const lineSet = new Set(selectedLines.map((l) => l - 1));
3591
+ const newLines = lines.filter((_, i) => !lineSet.has(i));
3592
+ fs20.writeFileSync(claudeMdPath, newLines.join("\n"), "utf-8");
3593
+ process.stdout.write(`
3594
+ \u2713 Removed ${selectedLines.length} line(s) from ${claudeMdPath}
3595
+
3596
+ `);
3597
+ }
3598
+
3599
+ // src/commands/hooks.ts
3600
+ init_esm_shims();
3601
+ import * as fs21 from "fs";
3602
+ import * as path25 from "path";
3603
+
3604
+ // src/hooks/registry.ts
3605
+ init_esm_shims();
3606
+ var HOOK_REGISTRY = [
3607
+ {
3608
+ name: "auto-compress",
3609
+ description: "Compress session when token count exceeds threshold",
3610
+ triggerEvent: "PostToolUse",
3611
+ matcher: "Read",
3612
+ commandTemplate: "claudectx compress --auto --api-key {{config.apiKey}}",
3613
+ 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 }
3616
+ },
3617
+ category: "compression"
3618
+ },
3619
+ {
3620
+ name: "daily-budget",
3621
+ description: "Warn when daily spend exceeds a dollar limit",
3622
+ triggerEvent: "PreToolUse",
3623
+ commandTemplate: "claudectx report --days 1 --json",
3624
+ configSchema: {
3625
+ dailyLimit: { type: "number", default: 5, description: "Daily spend limit in USD" }
3626
+ },
3627
+ category: "budget"
3628
+ },
3629
+ {
3630
+ name: "slack-digest",
3631
+ description: "POST daily usage summary to a Slack incoming webhook",
3632
+ triggerEvent: "Stop",
3633
+ commandTemplate: 'claudectx report --json | curl -s -X POST -H "Content-Type: application/json" --data-binary @- {{config.webhookUrl}}',
3634
+ configSchema: {
3635
+ webhookUrl: {
3636
+ type: "string",
3637
+ description: "Slack Incoming Webhook URL",
3638
+ required: true
3639
+ }
3640
+ },
3641
+ category: "notification"
3642
+ },
3643
+ {
3644
+ name: "session-warmup",
3645
+ description: "Pre-warm the Anthropic prompt cache on each session start",
3646
+ triggerEvent: "PostToolUse",
3647
+ matcher: "Read",
3648
+ commandTemplate: "claudectx warmup --api-key {{config.apiKey}}",
3649
+ configSchema: {
3650
+ apiKey: { type: "string", description: "Anthropic API key", required: true }
3651
+ },
3652
+ category: "warmup"
3653
+ }
3654
+ ];
3655
+ function getHook(name) {
3656
+ return HOOK_REGISTRY.find((h) => h.name === name);
3657
+ }
3658
+ function interpolateCommand(template, config) {
3659
+ return template.replace(/\{\{config\.(\w+)\}\}/g, (_, key) => {
3660
+ const val = config[key];
3661
+ return val !== void 0 ? String(val) : `{{config.${key}}}`;
3662
+ });
3663
+ }
3664
+ function buildHookEntry(def, config) {
3665
+ const command = interpolateCommand(def.commandTemplate, config);
3666
+ const hookItem = {
3667
+ type: "command",
3668
+ command
3669
+ };
3670
+ const entry = { hooks: [hookItem] };
3671
+ if (def.matcher) {
3672
+ entry.matcher = def.matcher;
3673
+ }
3674
+ return entry;
3675
+ }
3676
+
3677
+ // src/commands/hooks.ts
3678
+ function readInstalledHooks(projectRoot) {
3679
+ const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
3680
+ try {
3681
+ return JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
3682
+ } catch {
3683
+ return {};
3684
+ }
3685
+ }
3686
+ function removeHookByName(settings, name) {
3687
+ const hooks = settings.hooks ?? {};
3688
+ const updated = {};
3689
+ for (const [event, entries] of Object.entries(hooks)) {
3690
+ updated[event] = entries.filter((entry) => {
3691
+ const hookItems = entry.hooks ?? [];
3692
+ return !hookItems.some((h) => h.command?.includes(`claudectx`) && entry.name === name);
3693
+ });
3694
+ }
3695
+ return { ...settings, hooks: updated };
3696
+ }
3697
+ function parseConfigPairs(pairs) {
3698
+ const result = {};
3699
+ for (const pair of pairs) {
3700
+ const idx = pair.indexOf("=");
3701
+ if (idx === -1) continue;
3702
+ const key = pair.slice(0, idx).trim();
3703
+ const value = pair.slice(idx + 1).trim();
3704
+ if (key) result[key] = value;
3705
+ }
3706
+ return result;
3707
+ }
3708
+ async function hooksList(projectRoot) {
3709
+ const settings = readInstalledHooks(projectRoot);
3710
+ const installedCommands = JSON.stringify(settings.hooks ?? {});
3711
+ process.stdout.write("\n");
3712
+ process.stdout.write("claudectx hooks \u2014 available hooks\n");
3713
+ process.stdout.write("\u2550".repeat(55) + "\n");
3714
+ process.stdout.write(
3715
+ ` ${"Name".padEnd(16)} ${"Category".padEnd(12)} ${"Trigger".padEnd(12)} Status
3716
+ `
3717
+ );
3718
+ process.stdout.write("\u2500".repeat(55) + "\n");
3719
+ for (const hook of HOOK_REGISTRY) {
3720
+ const installed = installedCommands.includes(hook.name) ? "\u2713 installed" : " available";
3721
+ process.stdout.write(
3722
+ ` ${hook.name.padEnd(16)} ${hook.category.padEnd(12)} ${hook.triggerEvent.padEnd(12)} ${installed}
3723
+ `
3724
+ );
3725
+ }
3726
+ process.stdout.write("\n");
3727
+ process.stdout.write(" Use: claudectx hooks add <name> [--config key=value]\n\n");
3728
+ }
3729
+ async function hooksAdd(name, projectRoot, configPairs) {
3730
+ const def = getHook(name);
3731
+ if (!def) {
3732
+ process.stderr.write(
3733
+ `Error: unknown hook "${name}". Run "claudectx hooks list" to see available hooks.
3734
+ `
3735
+ );
3736
+ process.exit(1);
3737
+ }
3738
+ const config = parseConfigPairs(configPairs);
3739
+ const requiredMissing = Object.entries(def.configSchema).filter(
3740
+ ([key, field]) => field.required && config[key] === void 0
3741
+ );
3742
+ if (requiredMissing.length > 0) {
3743
+ try {
3744
+ const { input } = await import("@inquirer/prompts");
3745
+ for (const [key, field] of requiredMissing) {
3746
+ const value = await input({ message: `${field.description} (${key}):` });
3747
+ config[key] = value;
3748
+ }
3749
+ } catch {
3750
+ process.stderr.write(
3751
+ `Error: required config fields missing: ${requiredMissing.map(([k]) => k).join(", ")}
3752
+ Use: claudectx hooks add ${name} --config ${requiredMissing.map(([k]) => `${k}=<value>`).join(" --config ")}
3753
+ `
3754
+ );
3755
+ process.exit(1);
3756
+ }
3757
+ }
3758
+ for (const [key, field] of Object.entries(def.configSchema)) {
3759
+ if (config[key] === void 0 && field.default !== void 0) {
3760
+ config[key] = String(field.default);
3761
+ }
3762
+ }
3763
+ const command = interpolateCommand(def.commandTemplate, config);
3764
+ if (command.includes("{{config.")) {
3765
+ process.stderr.write(`Error: unresolved config placeholders in command: ${command}
3766
+ `);
3767
+ process.exit(1);
3768
+ }
3769
+ const entry = { ...buildHookEntry(def, config), name };
3770
+ const settings = readInstalledHooks(projectRoot);
3771
+ const hooksObj = settings.hooks ?? {};
3772
+ const eventList = hooksObj[def.triggerEvent] ?? [];
3773
+ if (eventList.some((e) => e.name === name)) {
3774
+ process.stdout.write(`Hook "${name}" is already installed.
3775
+ `);
3776
+ return;
3777
+ }
3778
+ const updatedSettings = {
3779
+ ...settings,
3780
+ hooks: { ...hooksObj, [def.triggerEvent]: [...eventList, entry] }
3781
+ };
3782
+ writeHooksSettings(projectRoot, updatedSettings);
3783
+ process.stdout.write(`
3784
+ \u2713 Hook "${name}" installed (${def.triggerEvent}${def.matcher ? ` / ${def.matcher}` : ""}).
3785
+
3786
+ `);
3787
+ }
3788
+ async function hooksRemove(name, projectRoot) {
3789
+ const settings = readInstalledHooks(projectRoot);
3790
+ const updated = removeHookByName(settings, name);
3791
+ writeHooksSettings(projectRoot, updated);
3792
+ process.stdout.write(` \u2713 Hook "${name}" removed.
3793
+
3794
+ `);
3795
+ }
3796
+ async function hooksStatus(projectRoot) {
3797
+ const settings = readInstalledHooks(projectRoot);
3798
+ const hooks = settings.hooks ?? {};
3799
+ const entries = Object.entries(hooks).flatMap(
3800
+ ([event, arr]) => arr.map((e) => ({ event, ...e }))
3801
+ );
3802
+ if (entries.length === 0) {
3803
+ process.stdout.write('\n No hooks installed. Run "claudectx hooks add <name>" to add one.\n\n');
3804
+ return;
3805
+ }
3806
+ process.stdout.write("\n Installed hooks:\n");
3807
+ for (const entry of entries) {
3808
+ const name = entry.name ?? "unnamed";
3809
+ const event = entry.event;
3810
+ const matcher = entry.matcher ? ` / ${entry.matcher}` : "";
3811
+ process.stdout.write(` ${name.padEnd(18)} ${event}${matcher}
3812
+ `);
3813
+ }
3814
+ process.stdout.write("\n");
3815
+ }
3816
+ async function hooksCommand(subcommand, options) {
3817
+ const projectPath = options.path ? path25.resolve(options.path) : process.cwd();
3818
+ const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3819
+ const sub = subcommand ?? "list";
3820
+ switch (sub) {
3821
+ case "list":
3822
+ await hooksList(projectRoot);
3823
+ break;
3824
+ case "add": {
3825
+ const name = options.name;
3826
+ if (!name) {
3827
+ process.stderr.write("Usage: claudectx hooks add <name> [--config key=value]\n");
3828
+ process.exit(1);
3829
+ }
3830
+ await hooksAdd(name, projectRoot, options.config ?? []);
3831
+ break;
3832
+ }
3833
+ case "remove": {
3834
+ const name = options.name;
3835
+ if (!name) {
3836
+ process.stderr.write("Usage: claudectx hooks remove <name>\n");
3837
+ process.exit(1);
3838
+ }
3839
+ await hooksRemove(name, projectRoot);
3840
+ break;
3841
+ }
3842
+ case "status":
3843
+ await hooksStatus(projectRoot);
3844
+ break;
3845
+ default:
3846
+ process.stderr.write(
3847
+ `Unknown sub-command "${sub}". Use: list | add <name> | remove <name> | status
3848
+ `
3849
+ );
3850
+ process.exit(1);
3851
+ }
3852
+ }
3853
+
3854
+ // src/commands/convert.ts
3855
+ init_esm_shims();
3856
+ import * as fs22 from "fs";
3857
+ import * as path26 from "path";
3858
+ function slugify2(text) {
3859
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3860
+ }
3861
+ function claudeMdToCursorRules(content) {
3862
+ const files = [];
3863
+ const lines = content.split("\n");
3864
+ let currentHeader = "";
3865
+ let currentBody = [];
3866
+ function flushSection() {
3867
+ if (!currentHeader) return;
3868
+ const slug = slugify2(currentHeader);
3869
+ if (!slug) return;
3870
+ const mdc = [
3871
+ "---",
3872
+ `description: ${currentHeader}`,
3873
+ "alwaysApply: true",
3874
+ "---",
3875
+ "",
3876
+ ...currentBody
3877
+ ].join("\n");
3878
+ files.push({ filename: `${slug}.mdc`, content: mdc });
3879
+ }
3880
+ for (const line of lines) {
3881
+ const match = line.match(/^##\s+(.+)$/);
3882
+ if (match) {
3883
+ flushSection();
3884
+ currentHeader = match[1];
3885
+ currentBody = [];
3886
+ } else if (currentHeader) {
3887
+ currentBody.push(line);
3888
+ }
3889
+ }
3890
+ flushSection();
3891
+ if (files.length === 0 && content.trim()) {
3892
+ files.push({
3893
+ filename: "project-instructions.mdc",
3894
+ content: ["---", "description: Project Instructions", "alwaysApply: true", "---", "", content].join("\n")
3895
+ });
3896
+ }
3897
+ return files;
3898
+ }
3899
+ function claudeMdToCopilot(content) {
3900
+ return content.split("\n").filter((line) => !line.match(/^@.+$/)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
3901
+ }
3902
+ function claudeMdToWindsurf(content) {
3903
+ return content.split("\n").filter((line) => !line.match(/^@.+$/)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
3904
+ }
3905
+ async function convertCommand(options) {
3906
+ const projectPath = options.path ? path26.resolve(options.path) : process.cwd();
3907
+ const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3908
+ const from = options.from ?? "claude";
3909
+ const to = options.to;
3910
+ if (from !== "claude") {
3911
+ process.stderr.write(`Error: --from "${from}" is not yet supported. Only --from claude is available.
3912
+ `);
3913
+ process.exit(1);
3914
+ }
3915
+ const claudeMdPath = path26.join(projectRoot, "CLAUDE.md");
3916
+ let content = "";
3917
+ try {
3918
+ content = fs22.readFileSync(claudeMdPath, "utf-8");
3919
+ } catch {
3920
+ process.stderr.write(`Error: CLAUDE.md not found at ${claudeMdPath}
3921
+ `);
3922
+ process.exit(1);
3923
+ }
3924
+ if (to === "cursor") {
3925
+ const files = claudeMdToCursorRules(content);
3926
+ const targetDir = path26.join(projectRoot, ".cursor", "rules");
3927
+ process.stdout.write(`
3928
+ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
3929
+
3930
+ `);
3931
+ for (const file of files) {
3932
+ const filePath = path26.join(targetDir, file.filename);
3933
+ process.stdout.write(` ${options.dryRun ? "[dry-run] " : ""}\u2192 .cursor/rules/${file.filename}
3934
+ `);
3935
+ if (!options.dryRun) {
3936
+ fs22.mkdirSync(targetDir, { recursive: true });
3937
+ fs22.writeFileSync(filePath, file.content, "utf-8");
3938
+ }
3939
+ }
3940
+ process.stdout.write("\n");
3941
+ } else if (to === "copilot") {
3942
+ const converted = claudeMdToCopilot(content);
3943
+ const targetPath = path26.join(projectRoot, ".github", "copilot-instructions.md");
3944
+ process.stdout.write(`
3945
+ Converting CLAUDE.md \u2192 .github/copilot-instructions.md
3946
+ `);
3947
+ if (!options.dryRun) {
3948
+ fs22.mkdirSync(path26.dirname(targetPath), { recursive: true });
3949
+ fs22.writeFileSync(targetPath, converted, "utf-8");
3950
+ process.stdout.write(` \u2713 Written to ${targetPath}
3951
+
3952
+ `);
3953
+ } else {
3954
+ process.stdout.write(` [dry-run] Would write ${converted.length} chars to ${targetPath}
3955
+
3956
+ `);
3957
+ }
3958
+ } else if (to === "windsurf") {
3959
+ const converted = claudeMdToWindsurf(content);
3960
+ const targetPath = path26.join(projectRoot, ".windsurfrules");
3961
+ process.stdout.write(`
3962
+ Converting CLAUDE.md \u2192 .windsurfrules
3963
+ `);
3964
+ if (!options.dryRun) {
3965
+ fs22.writeFileSync(targetPath, converted, "utf-8");
3966
+ process.stdout.write(` \u2713 Written to ${targetPath}
3967
+
3968
+ `);
3969
+ } else {
3970
+ process.stdout.write(` [dry-run] Would write ${converted.length} chars to ${targetPath}
3971
+
3972
+ `);
3973
+ }
3974
+ } else {
3975
+ process.stderr.write(`Error: unknown target "${to}". Use: cursor | copilot | windsurf
3976
+ `);
3977
+ process.exit(1);
3978
+ }
3979
+ }
3980
+
3981
+ // src/commands/teams.ts
3982
+ init_esm_shims();
3983
+ init_models();
3984
+ import * as path28 from "path";
3985
+ import * as fs24 from "fs";
3986
+
3987
+ // src/reporter/team-aggregator.ts
3988
+ init_esm_shims();
3989
+ init_session_reader();
3990
+ init_session_store();
3991
+ init_models();
3992
+ import * as fs23 from "fs";
3993
+ import * as path27 from "path";
3994
+ import * as os4 from "os";
3995
+ import * as childProcess2 from "child_process";
3996
+ function getDeveloperIdentity() {
3997
+ try {
3998
+ const email = childProcess2.execSync("git config user.email", { encoding: "utf-8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
3999
+ if (email) return email;
4000
+ } catch {
4001
+ }
4002
+ return os4.hostname();
4003
+ }
4004
+ function calcCost3(inputTokens, outputTokens, model) {
4005
+ const p = MODEL_PRICING[model];
4006
+ return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion;
4007
+ }
4008
+ function isoDate2(d) {
4009
+ return d.toISOString().slice(0, 10);
4010
+ }
4011
+ function anonymizeExport(report, index) {
4012
+ return {
4013
+ ...report,
4014
+ developer: {
4015
+ ...report.developer,
4016
+ identity: `Dev ${index + 1}`
4017
+ }
4018
+ };
4019
+ }
4020
+ async function buildTeamExport(days, model, anonymize) {
4021
+ const now = /* @__PURE__ */ new Date();
4022
+ const cutoff = new Date(now);
4023
+ cutoff.setDate(cutoff.getDate() - days);
4024
+ const cutoffMs = cutoff.getTime();
4025
+ const sessionFiles = listSessionFiles().filter((f) => f.mtimeMs >= cutoffMs);
4026
+ const bucketMap = /* @__PURE__ */ new Map();
4027
+ for (let i = 0; i < days; i++) {
4028
+ const d = new Date(now);
4029
+ d.setDate(d.getDate() - i);
4030
+ const dateStr = isoDate2(d);
4031
+ bucketMap.set(dateStr, {
4032
+ date: dateStr,
4033
+ sessions: 0,
4034
+ inputTokens: 0,
4035
+ outputTokens: 0,
4036
+ cacheReadTokens: 0,
4037
+ requests: 0,
4038
+ costUsd: 0
4039
+ });
4040
+ }
4041
+ let totalInput = 0;
4042
+ let totalOutput = 0;
4043
+ let totalCacheRead = 0;
4044
+ let totalRequests = 0;
4045
+ for (const sf of sessionFiles) {
4046
+ const dateStr = isoDate2(new Date(sf.mtimeMs));
4047
+ const bucket = bucketMap.get(dateStr);
4048
+ const usage = readSessionUsage(sf.filePath);
4049
+ if (bucket) {
4050
+ bucket.sessions++;
4051
+ bucket.inputTokens += usage.inputTokens;
4052
+ bucket.outputTokens += usage.outputTokens;
4053
+ bucket.cacheReadTokens += usage.cacheReadTokens;
4054
+ bucket.requests += usage.requestCount;
4055
+ bucket.costUsd += calcCost3(usage.inputTokens, usage.outputTokens, model);
4056
+ }
4057
+ totalInput += usage.inputTokens;
4058
+ totalOutput += usage.outputTokens;
4059
+ totalCacheRead += usage.cacheReadTokens;
4060
+ totalRequests += usage.requestCount;
4061
+ }
4062
+ const fileEvents = readAllEvents().filter(
4063
+ (e) => new Date(e.timestamp).getTime() >= cutoffMs
4064
+ );
4065
+ const topWasteFiles = aggregateStats(fileEvents).slice(0, 10).map((s) => ({ filePath: s.filePath, readCount: s.readCount }));
4066
+ const totalCostUsd = calcCost3(totalInput, totalOutput, model);
4067
+ const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
4068
+ const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
4069
+ const developer = {
4070
+ identity: getDeveloperIdentity(),
4071
+ exportedAt: now.toISOString(),
4072
+ periodDays: days,
4073
+ totalCostUsd,
4074
+ totalInputTokens: totalInput,
4075
+ totalOutputTokens: totalOutput,
4076
+ cacheHitRate,
4077
+ avgRequestSize: totalRequests > 0 ? Math.round(totalInput / totalRequests) : 0,
4078
+ topWasteFiles,
4079
+ sessionCount: uniqueSessions
4080
+ };
4081
+ const byDay = [...bucketMap.values()].sort((a, b) => a.date.localeCompare(b.date));
4082
+ const exportData = { version: "1", developer, byDay };
4083
+ if (anonymize) return anonymizeExport(exportData, 0);
4084
+ return exportData;
4085
+ }
4086
+ function aggregateTeamReports(exports) {
4087
+ const developers = exports.map((e) => e.developer);
4088
+ developers.sort((a, b) => b.totalCostUsd - a.totalCostUsd);
4089
+ const teamTotalCostUsd = developers.reduce((sum, d) => sum + d.totalCostUsd, 0);
4090
+ const teamCacheHitRate = developers.length > 0 ? Math.round(developers.reduce((sum, d) => sum + d.cacheHitRate, 0) / developers.length) : 0;
4091
+ const fileMap = /* @__PURE__ */ new Map();
4092
+ for (const dev of developers) {
4093
+ for (const wf of dev.topWasteFiles) {
4094
+ const existing = fileMap.get(wf.filePath);
4095
+ if (existing) {
4096
+ existing.readCount += wf.readCount;
4097
+ existing.developers.push(dev.identity);
4098
+ } else {
4099
+ fileMap.set(wf.filePath, { readCount: wf.readCount, developers: [dev.identity] });
4100
+ }
4101
+ }
4102
+ }
4103
+ const topWasteFiles = [...fileMap.entries()].map(([filePath, data]) => ({ filePath, ...data })).sort((a, b) => b.readCount - a.readCount).slice(0, 10);
4104
+ const periodDays = exports[0]?.developer.periodDays ?? 30;
4105
+ return {
4106
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4107
+ totalDevelopers: exports.length,
4108
+ periodDays,
4109
+ developers,
4110
+ teamTotalCostUsd,
4111
+ teamCacheHitRate,
4112
+ topWasteFiles
4113
+ };
4114
+ }
4115
+ function writeTeamExport(exportData) {
4116
+ const storeDir = getStoreDir();
4117
+ if (!fs23.existsSync(storeDir)) fs23.mkdirSync(storeDir, { recursive: true });
4118
+ const date = isoDate2(/* @__PURE__ */ new Date());
4119
+ const filePath = path27.join(storeDir, `team-export-${date}.json`);
4120
+ fs23.writeFileSync(filePath, JSON.stringify(exportData, null, 2), "utf-8");
4121
+ return filePath;
4122
+ }
4123
+ function readTeamExports(dir) {
4124
+ const exports = [];
4125
+ if (!fs23.existsSync(dir)) return exports;
4126
+ const files = fs23.readdirSync(dir).filter((f) => f.match(/^team-export-.*\.json$/));
4127
+ for (const file of files) {
4128
+ try {
4129
+ const raw = fs23.readFileSync(path27.join(dir, file), "utf-8");
4130
+ exports.push(JSON.parse(raw));
4131
+ } catch {
4132
+ }
4133
+ }
4134
+ return exports;
4135
+ }
4136
+
4137
+ // src/commands/teams.ts
4138
+ init_session_store();
4139
+ async function teamsExport(options) {
4140
+ const model = resolveModel(options.model ?? "sonnet");
4141
+ const days = parseInt(options.days ?? "30", 10);
4142
+ const anonymize = options.anonymize ?? false;
4143
+ const exportData = await buildTeamExport(days, model, anonymize);
4144
+ if (options.json) {
4145
+ process.stdout.write(JSON.stringify(exportData, null, 2) + "\n");
4146
+ return;
4147
+ }
4148
+ const filePath = writeTeamExport(exportData);
4149
+ process.stdout.write("\n");
4150
+ process.stdout.write(`claudectx teams export
4151
+ `);
4152
+ process.stdout.write("\u2550".repeat(45) + "\n");
4153
+ process.stdout.write(` Developer: ${exportData.developer.identity}
4154
+ `);
4155
+ process.stdout.write(` Period: Last ${days} days
4156
+ `);
4157
+ process.stdout.write(` Sessions: ${exportData.developer.sessionCount}
4158
+ `);
4159
+ process.stdout.write(` Total cost: ${formatCost(exportData.developer.totalCostUsd)}
4160
+ `);
4161
+ process.stdout.write(` Cache rate: ${exportData.developer.cacheHitRate}%
4162
+ `);
4163
+ process.stdout.write(`
4164
+ \u2713 Saved to: ${filePath}
4165
+ `);
4166
+ process.stdout.write(" Share this file with your team lead for aggregation.\n\n");
4167
+ }
4168
+ async function teamsAggregate(options) {
4169
+ const dir = options.dir ?? getStoreDir();
4170
+ const anonymize = options.anonymize ?? false;
4171
+ const exports = readTeamExports(dir);
4172
+ if (exports.length === 0) {
4173
+ process.stderr.write(
4174
+ `No team export files found in ${dir}.
4175
+ Run "claudectx teams export" on each developer machine first.
4176
+ `
4177
+ );
4178
+ process.exit(1);
4179
+ }
4180
+ const anonymized = anonymize ? exports.map((e, i) => anonymizeExport(e, i)) : exports;
4181
+ const report = aggregateTeamReports(anonymized);
4182
+ if (options.json) {
4183
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
4184
+ return;
4185
+ }
4186
+ process.stdout.write("\n");
4187
+ process.stdout.write("claudectx teams aggregate \u2014 team cost report\n");
4188
+ process.stdout.write("\u2550".repeat(55) + "\n");
4189
+ process.stdout.write(` Developers: ${report.totalDevelopers}
4190
+ `);
4191
+ process.stdout.write(` Period: Last ${report.periodDays} days
4192
+ `);
4193
+ process.stdout.write(` Team total cost: ${formatCost(report.teamTotalCostUsd)}
4194
+ `);
4195
+ process.stdout.write(` Team cache rate: ${report.teamCacheHitRate}%
4196
+ `);
4197
+ process.stdout.write("\n");
4198
+ process.stdout.write(
4199
+ ` ${"Developer".padEnd(30)} ${"Cost".padStart(8)} ${"Cache".padStart(6)} Sessions
4200
+ `
4201
+ );
4202
+ process.stdout.write("\u2500".repeat(55) + "\n");
4203
+ for (const dev of report.developers) {
4204
+ const identity = dev.identity.slice(0, 30).padEnd(30);
4205
+ const cost = formatCost(dev.totalCostUsd).padStart(8);
4206
+ const cache = `${dev.cacheHitRate}%`.padStart(6);
4207
+ const sessions = String(dev.sessionCount).padStart(8);
4208
+ process.stdout.write(` ${identity} ${cost} ${cache} ${sessions}
4209
+ `);
4210
+ }
4211
+ process.stdout.write("\n");
4212
+ if (report.topWasteFiles.length > 0) {
4213
+ process.stdout.write(" Top shared files (by read count across team):\n");
4214
+ for (const f of report.topWasteFiles.slice(0, 5)) {
4215
+ const devList = f.developers.slice(0, 3).join(", ");
4216
+ process.stdout.write(` ${f.readCount}x ${path28.basename(f.filePath)} (${devList})
4217
+ `);
4218
+ }
4219
+ process.stdout.write("\n");
4220
+ }
4221
+ }
4222
+ async function teamsShare(options) {
4223
+ const dest = options.to;
4224
+ if (!dest) {
4225
+ process.stderr.write("Usage: claudectx teams share --to <path>\n");
4226
+ process.exit(1);
4227
+ }
4228
+ const storeDir = getStoreDir();
4229
+ const exportFiles = fs24.readdirSync(storeDir).filter((f) => f.match(/^team-export-.*\.json$/)).sort().reverse();
4230
+ if (exportFiles.length === 0) {
4231
+ process.stderr.write('No team export files found. Run "claudectx teams export" first.\n');
4232
+ process.exit(1);
4233
+ }
4234
+ const latest = exportFiles[0];
4235
+ const src = path28.join(storeDir, latest);
4236
+ const destPath = fs24.statSync(dest).isDirectory() ? path28.join(dest, latest) : dest;
4237
+ fs24.copyFileSync(src, destPath);
4238
+ process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
4239
+
4240
+ `);
4241
+ }
4242
+ async function teamsCommand(subcommand, options) {
4243
+ switch (subcommand) {
4244
+ case "export":
4245
+ await teamsExport(options);
4246
+ break;
4247
+ case "aggregate":
4248
+ await teamsAggregate(options);
4249
+ break;
4250
+ case "share":
4251
+ await teamsShare(options);
4252
+ break;
4253
+ default:
4254
+ process.stderr.write(
4255
+ `Unknown sub-command "${subcommand}". Use: export | aggregate | share
4256
+ `
4257
+ );
4258
+ process.exit(1);
4259
+ }
4260
+ }
4261
+
2984
4262
  // src/index.ts
2985
4263
  var VERSION = "1.0.0";
2986
4264
  var DESCRIPTION = "Reduce Claude Code token usage by up to 80%. Context analyzer, auto-optimizer, live dashboard, and smart MCP tools.";
@@ -3004,5 +4282,23 @@ program.command("compress").alias("c").description("Compress a Claude Code sessi
3004
4282
  program.command("report").alias("r").description("Show token usage analytics for the last N days").option("-p, --path <path>", "Project directory (default: cwd)").option("--days <n>", "Number of days to include", "7").option("--json", "Machine-readable JSON output").option("--markdown", "GitHub-flavoured Markdown output").option("-m, --model <model>", "Claude model for cost estimates (haiku|sonnet|opus)", "sonnet").action(async (options) => {
3005
4283
  await reportCommand(options);
3006
4284
  });
4285
+ program.command("budget <globs...>").description("Estimate token cost before running a task").option("-m, --model <model>", "Model for cost estimates (haiku|sonnet|opus)", "sonnet").option("--threshold <n>", "Warn if total exceeds N tokens", "10000").option("-p, --path <path>", "Project directory").option("--json", "JSON output").action(async (globs, options) => {
4286
+ await budgetCommand(globs, options);
4287
+ });
4288
+ program.command("warmup").description("Pre-warm the Anthropic prompt cache with your CLAUDE.md").option("-m, --model <model>", "Model (haiku|sonnet|opus)", "haiku").option("--ttl <minutes>", "Cache TTL: 5 or 60", "5").option("--cron <expr>", 'Install as cron job (e.g. "0 9 * * 1-5")').option("--api-key <key>", "Anthropic API key").option("-p, --path <path>", "Project directory").option("--json", "JSON output").action(async (options) => {
4289
+ await warmupCommand(options);
4290
+ });
4291
+ program.command("drift").description("Detect stale references and dead sections in CLAUDE.md").option("-p, --path <path>", "Project directory").option("--days <n>", "Days window for section usage", "30").option("--fix", "Interactively remove flagged lines").option("--json", "JSON output").action(async (options) => {
4292
+ await driftCommand(options);
4293
+ });
4294
+ program.command("hooks [subcommand] [name]").description("Hook marketplace: list | add <name> | remove <name> | status").option("-p, --path <path>", "Project directory").option("--config <pair...>", "key=value config pairs for add").action(async (subcommand, name, options) => {
4295
+ await hooksCommand(subcommand, { ...options, name });
4296
+ });
4297
+ program.command("convert").description("Convert CLAUDE.md to another AI assistant format").option("--from <assistant>", "Source format (default: claude)", "claude").requiredOption("--to <assistant>", "Target format: cursor | copilot | windsurf").option("--dry-run", "Preview without writing").option("-p, --path <path>", "Project directory").action(async (options) => {
4298
+ await convertCommand(options);
4299
+ });
4300
+ program.command("teams [subcommand]").description("Multi-developer cost attribution (export | aggregate | share)").option("--days <n>", "Days to include", "30").option("-m, --model <model>", "Model", "sonnet").option("--anonymize", "Replace identities with Dev 1, Dev 2...").option("--dir <path>", "Directory with team export JSON files").option("--to <path>", "Destination for share sub-command").option("--json", "JSON output").action(async (subcommand, options) => {
4301
+ await teamsCommand(subcommand ?? "export", options);
4302
+ });
3007
4303
  program.parse();
3008
4304
  //# sourceMappingURL=index.mjs.map