claudectx 1.1.2 → 1.1.4

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
@@ -124,15 +124,15 @@ __export(session_store_exports, {
124
124
  readAllEvents: () => readAllEvents
125
125
  });
126
126
  function getStoreDirPath() {
127
- return path8.join(os2.homedir(), ".claudectx");
127
+ return path9.join(os3.homedir(), ".claudectx");
128
128
  }
129
129
  function getReadsFilePath_() {
130
- return path8.join(getStoreDirPath(), "reads.jsonl");
130
+ return path9.join(getStoreDirPath(), "reads.jsonl");
131
131
  }
132
132
  function ensureStoreDir() {
133
133
  const dir = getStoreDirPath();
134
- if (!fs8.existsSync(dir)) {
135
- fs8.mkdirSync(dir, { recursive: true });
134
+ if (!fs9.existsSync(dir)) {
135
+ fs9.mkdirSync(dir, { recursive: true });
136
136
  }
137
137
  }
138
138
  function appendFileRead(filePath, sessionId) {
@@ -142,12 +142,12 @@ function appendFileRead(filePath, sessionId) {
142
142
  filePath,
143
143
  sessionId
144
144
  };
145
- fs8.appendFileSync(getReadsFilePath_(), JSON.stringify(event) + "\n", "utf-8");
145
+ fs9.appendFileSync(getReadsFilePath_(), JSON.stringify(event) + "\n", "utf-8");
146
146
  }
147
147
  function readAllEvents() {
148
148
  const readsFile = getReadsFilePath_();
149
- if (!fs8.existsSync(readsFile)) return [];
150
- const lines = fs8.readFileSync(readsFile, "utf-8").trim().split("\n").filter(Boolean);
149
+ if (!fs9.existsSync(readsFile)) return [];
150
+ const lines = fs9.readFileSync(readsFile, "utf-8").trim().split("\n").filter(Boolean);
151
151
  return lines.map((line) => {
152
152
  try {
153
153
  return JSON.parse(line);
@@ -176,8 +176,8 @@ function aggregateStats(events) {
176
176
  }
177
177
  function clearStore() {
178
178
  const readsFile = getReadsFilePath_();
179
- if (fs8.existsSync(readsFile)) {
180
- fs8.writeFileSync(readsFile, "", "utf-8");
179
+ if (fs9.existsSync(readsFile)) {
180
+ fs9.writeFileSync(readsFile, "", "utf-8");
181
181
  }
182
182
  }
183
183
  function getReadsFilePath() {
@@ -186,37 +186,37 @@ function getReadsFilePath() {
186
186
  function getStoreDir() {
187
187
  return getStoreDirPath();
188
188
  }
189
- var fs8, os2, path8;
189
+ var fs9, os3, path9;
190
190
  var init_session_store = __esm({
191
191
  "src/watcher/session-store.ts"() {
192
192
  "use strict";
193
193
  init_cjs_shims();
194
- fs8 = __toESM(require("fs"));
195
- os2 = __toESM(require("os"));
196
- path8 = __toESM(require("path"));
194
+ fs9 = __toESM(require("fs"));
195
+ os3 = __toESM(require("os"));
196
+ path9 = __toESM(require("path"));
197
197
  }
198
198
  });
199
199
 
200
200
  // src/watcher/session-reader.ts
201
201
  function listSessionFiles() {
202
- if (!fs9.existsSync(CLAUDE_PROJECTS_DIR)) return [];
202
+ if (!fs10.existsSync(CLAUDE_PROJECTS_DIR)) return [];
203
203
  const results = [];
204
204
  try {
205
- const projectDirs = fs9.readdirSync(CLAUDE_PROJECTS_DIR);
205
+ const projectDirs = fs10.readdirSync(CLAUDE_PROJECTS_DIR);
206
206
  for (const projectDir of projectDirs) {
207
- const projectPath = path9.join(CLAUDE_PROJECTS_DIR, projectDir);
207
+ const projectPath = path10.join(CLAUDE_PROJECTS_DIR, projectDir);
208
208
  try {
209
- const stat = fs9.statSync(projectPath);
209
+ const stat = fs10.statSync(projectPath);
210
210
  if (!stat.isDirectory()) continue;
211
- const files = fs9.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
211
+ const files = fs10.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
212
212
  for (const file of files) {
213
- const filePath = path9.join(projectPath, file);
213
+ const filePath = path10.join(projectPath, file);
214
214
  try {
215
- const fstat = fs9.statSync(filePath);
215
+ const fstat = fs10.statSync(filePath);
216
216
  results.push({
217
217
  filePath,
218
218
  mtimeMs: fstat.mtimeMs,
219
- sessionId: path9.basename(file, ".jsonl"),
219
+ sessionId: path10.basename(file, ".jsonl"),
220
220
  projectDir
221
221
  });
222
222
  } catch {
@@ -247,7 +247,7 @@ async function readSessionUsage(sessionFilePath) {
247
247
  cacheReadTokens: 0,
248
248
  requestCount: 0
249
249
  };
250
- if (!fs9.existsSync(sessionFilePath)) return result;
250
+ if (!fs10.existsSync(sessionFilePath)) return result;
251
251
  const { createReadStream } = await import("fs");
252
252
  const { createInterface } = await import("readline");
253
253
  try {
@@ -277,15 +277,15 @@ async function readSessionUsage(sessionFilePath) {
277
277
  }
278
278
  return result;
279
279
  }
280
- var fs9, os3, path9, CLAUDE_PROJECTS_DIR;
280
+ var fs10, os4, path10, CLAUDE_PROJECTS_DIR;
281
281
  var init_session_reader = __esm({
282
282
  "src/watcher/session-reader.ts"() {
283
283
  "use strict";
284
284
  init_cjs_shims();
285
- fs9 = __toESM(require("fs"));
286
- os3 = __toESM(require("os"));
287
- path9 = __toESM(require("path"));
288
- CLAUDE_PROJECTS_DIR = path9.join(os3.homedir(), ".claude", "projects");
285
+ fs10 = __toESM(require("fs"));
286
+ os4 = __toESM(require("os"));
287
+ path10 = __toESM(require("path"));
288
+ CLAUDE_PROJECTS_DIR = path10.join(os4.homedir(), ".claude", "projects");
289
289
  }
290
290
  });
291
291
 
@@ -303,7 +303,7 @@ function fmtCost(tokens, model) {
303
303
  return `$${cost.toFixed(4)}`;
304
304
  }
305
305
  function shortPath(filePath) {
306
- const parts = filePath.split(path10.sep);
306
+ const parts = filePath.split(path11.sep);
307
307
  if (parts.length <= 3) return filePath;
308
308
  return "\u2026/" + parts.slice(-3).join("/");
309
309
  }
@@ -425,9 +425,9 @@ function Dashboard({
425
425
  const readsFile = getReadsFilePath();
426
426
  let watcher = null;
427
427
  const tryWatch = () => {
428
- if (fs10.existsSync(readsFile)) {
428
+ if (fs11.existsSync(readsFile)) {
429
429
  try {
430
- watcher = fs10.watch(readsFile, () => refresh());
430
+ watcher = fs11.watch(readsFile, () => refresh());
431
431
  } catch {
432
432
  }
433
433
  }
@@ -465,7 +465,7 @@ function Dashboard({
465
465
  ] }),
466
466
  sessionFile && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink.Text, { dimColor: true, children: [
467
467
  " \u2014 ",
468
- path10.basename(sessionFile, ".jsonl").slice(0, 8),
468
+ path11.basename(sessionFile, ".jsonl").slice(0, 8),
469
469
  "\u2026"
470
470
  ] }),
471
471
  !sessionFile && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink.Text, { dimColor: true, children: " \u2014 no session file found" })
@@ -481,7 +481,7 @@ function Dashboard({
481
481
  ] })
482
482
  ] });
483
483
  }
484
- var import_react, import_ink, fs10, path10, import_jsx_runtime;
484
+ var import_react, import_ink, fs11, path11, import_jsx_runtime;
485
485
  var init_Dashboard = __esm({
486
486
  "src/components/Dashboard.tsx"() {
487
487
  "use strict";
@@ -491,15 +491,15 @@ var init_Dashboard = __esm({
491
491
  init_session_store();
492
492
  init_session_reader();
493
493
  init_models();
494
- fs10 = __toESM(require("fs"));
495
- path10 = __toESM(require("path"));
494
+ fs11 = __toESM(require("fs"));
495
+ path11 = __toESM(require("path"));
496
496
  import_jsx_runtime = require("react/jsx-runtime");
497
497
  }
498
498
  });
499
499
 
500
500
  // src/mcp/smart-reader.ts
501
501
  function detectLanguage(filePath) {
502
- const ext = path13.extname(filePath).toLowerCase();
502
+ const ext = path14.extname(filePath).toLowerCase();
503
503
  switch (ext) {
504
504
  case ".ts":
505
505
  case ".tsx":
@@ -516,8 +516,8 @@ function detectLanguage(filePath) {
516
516
  }
517
517
  }
518
518
  function findSymbol(filePath, symbolName) {
519
- if (!fs12.existsSync(filePath)) return null;
520
- const content = fs12.readFileSync(filePath, "utf-8");
519
+ if (!fs13.existsSync(filePath)) return null;
520
+ const content = fs13.readFileSync(filePath, "utf-8");
521
521
  const lines = content.split("\n");
522
522
  const lang = detectLanguage(filePath);
523
523
  const patterns = lang === "python" ? PYTHON_PATTERNS : TS_JS_PATTERNS;
@@ -578,8 +578,8 @@ function findPythonBlockEnd(lines, startIdx) {
578
578
  return lines.length;
579
579
  }
580
580
  function extractLineRange(filePath, startLine, endLine, contextLines = 0) {
581
- if (!fs12.existsSync(filePath)) return null;
582
- const allLines = fs12.readFileSync(filePath, "utf-8").split("\n");
581
+ if (!fs13.existsSync(filePath)) return null;
582
+ const allLines = fs13.readFileSync(filePath, "utf-8").split("\n");
583
583
  const totalLines = allLines.length;
584
584
  const from = Math.max(0, startLine - 1 - contextLines);
585
585
  const to = Math.min(totalLines, endLine + contextLines);
@@ -594,7 +594,7 @@ function extractLineRange(filePath, startLine, endLine, contextLines = 0) {
594
594
  };
595
595
  }
596
596
  function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
597
- if (!fs12.existsSync(filePath)) {
597
+ if (!fs13.existsSync(filePath)) {
598
598
  throw new Error(`File not found: ${filePath}`);
599
599
  }
600
600
  if (symbol) {
@@ -606,7 +606,7 @@ function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
606
606
  filePath,
607
607
  startLine: extracted.startLine,
608
608
  endLine: extracted.endLine,
609
- totalLines: fs12.readFileSync(filePath, "utf-8").split("\n").length,
609
+ totalLines: fs13.readFileSync(filePath, "utf-8").split("\n").length,
610
610
  truncated: false,
611
611
  symbolName: symbol
612
612
  };
@@ -618,7 +618,7 @@ function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
618
618
  return { ...result, truncated: false };
619
619
  }
620
620
  }
621
- const fullContent = fs12.readFileSync(filePath, "utf-8");
621
+ const fullContent = fs13.readFileSync(filePath, "utf-8");
622
622
  const allLines = fullContent.split("\n");
623
623
  const totalLines = allLines.length;
624
624
  const fullTokens = countTokens(fullContent);
@@ -654,13 +654,13 @@ function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
654
654
  truncated: true
655
655
  };
656
656
  }
657
- var fs12, path13, TS_JS_PATTERNS, PYTHON_PATTERNS, MAX_FULL_FILE_TOKENS;
657
+ var fs13, path14, TS_JS_PATTERNS, PYTHON_PATTERNS, MAX_FULL_FILE_TOKENS;
658
658
  var init_smart_reader = __esm({
659
659
  "src/mcp/smart-reader.ts"() {
660
660
  "use strict";
661
661
  init_cjs_shims();
662
- fs12 = __toESM(require("fs"));
663
- path13 = __toESM(require("path"));
662
+ fs13 = __toESM(require("fs"));
663
+ path14 = __toESM(require("path"));
664
664
  init_tokenizer();
665
665
  TS_JS_PATTERNS = [
666
666
  // export async function name / export function name / function name
@@ -699,7 +699,7 @@ function extractSymbolsFromFile(filePath) {
699
699
  if (lang === "other") return [];
700
700
  let content;
701
701
  try {
702
- content = fs13.readFileSync(filePath, "utf-8");
702
+ content = fs14.readFileSync(filePath, "utf-8");
703
703
  } catch {
704
704
  return [];
705
705
  }
@@ -728,13 +728,13 @@ function extractSymbolsFromFile(filePath) {
728
728
  }
729
729
  return results;
730
730
  }
731
- var fs13, path14, import_glob, TS_JS_EXTRACTORS, PYTHON_EXTRACTORS, SOURCE_GLOBS, IGNORE_DIRS, SymbolIndex, globalIndex;
731
+ var fs14, path15, import_glob, TS_JS_EXTRACTORS, PYTHON_EXTRACTORS, SOURCE_GLOBS, IGNORE_DIRS, SymbolIndex, globalIndex;
732
732
  var init_symbol_index = __esm({
733
733
  "src/mcp/symbol-index.ts"() {
734
734
  "use strict";
735
735
  init_cjs_shims();
736
- fs13 = __toESM(require("fs"));
737
- path14 = __toESM(require("path"));
736
+ fs14 = __toESM(require("fs"));
737
+ path15 = __toESM(require("path"));
738
738
  import_glob = require("glob");
739
739
  init_smart_reader();
740
740
  TS_JS_EXTRACTORS = [
@@ -787,8 +787,8 @@ var init_symbol_index = __esm({
787
787
  this.entries = [];
788
788
  let files = [];
789
789
  try {
790
- files = await (0, import_glob.glob)(SOURCE_GLOBS.map((g) => path14.join(projectRoot, g)), {
791
- ignore: IGNORE_DIRS.map((g) => path14.join(projectRoot, g)),
790
+ files = await (0, import_glob.glob)(SOURCE_GLOBS.map((g) => path15.join(projectRoot, g)), {
791
+ ignore: IGNORE_DIRS.map((g) => path15.join(projectRoot, g)),
792
792
  absolute: true
793
793
  });
794
794
  } catch {
@@ -842,7 +842,7 @@ __export(server_exports, {
842
842
  startMcpServer: () => startMcpServer
843
843
  });
844
844
  function handleSmartRead(args) {
845
- const filePath = path15.resolve(args.file);
845
+ const filePath = path16.resolve(args.file);
846
846
  const result = smartRead(
847
847
  filePath,
848
848
  args.symbol,
@@ -878,7 +878,7 @@ Example: index_project({ "project_root": "${process.cwd()}" })`;
878
878
  ];
879
879
  for (let i = 0; i < results.length; i++) {
880
880
  const r = results[i];
881
- const rel = path15.relative(process.cwd(), r.filePath);
881
+ const rel = path16.relative(process.cwd(), r.filePath);
882
882
  lines.push(`${i + 1}. [${r.type}] ${r.name}`);
883
883
  lines.push(` ${rel}:${r.lineStart}`);
884
884
  lines.push(` ${r.signature.trim()}`);
@@ -890,7 +890,7 @@ Example: index_project({ "project_root": "${process.cwd()}" })`;
890
890
  return lines.join("\n");
891
891
  }
892
892
  async function handleIndexProject(args) {
893
- const projectRoot = args.project_root ? path15.resolve(args.project_root) : process.cwd();
893
+ const projectRoot = args.project_root ? path16.resolve(args.project_root) : process.cwd();
894
894
  const fn = args.rebuild ? () => globalIndex.rebuild(projectRoot) : () => globalIndex.build(projectRoot);
895
895
  const { fileCount, symbolCount } = await fn();
896
896
  if (fileCount === 0 && globalIndex.isReady) {
@@ -938,12 +938,12 @@ async function startMcpServer() {
938
938
  await server.connect(transport);
939
939
  process.stderr.write("[claudectx mcp] Server started (stdio)\n");
940
940
  }
941
- var path15, import_server, import_stdio, import_types, TOOLS;
941
+ var path16, import_server, import_stdio, import_types, TOOLS;
942
942
  var init_server = __esm({
943
943
  "src/mcp/server.ts"() {
944
944
  "use strict";
945
945
  init_cjs_shims();
946
- path15 = __toESM(require("path"));
946
+ path16 = __toESM(require("path"));
947
947
  import_server = require("@modelcontextprotocol/sdk/server/index.js");
948
948
  import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
949
949
  import_types = require("@modelcontextprotocol/sdk/types.js");
@@ -1581,8 +1581,8 @@ async function analyzeCommand(options) {
1581
1581
 
1582
1582
  // src/commands/optimize.ts
1583
1583
  init_cjs_shims();
1584
- var fs7 = __toESM(require("fs"));
1585
- var path7 = __toESM(require("path"));
1584
+ var fs8 = __toESM(require("fs"));
1585
+ var path8 = __toESM(require("path"));
1586
1586
  var import_chalk3 = __toESM(require("chalk"));
1587
1587
  var import_boxen2 = __toESM(require("boxen"));
1588
1588
  var import_prompts = require("@inquirer/prompts");
@@ -1745,9 +1745,132 @@ function writeIgnorefile(result) {
1745
1745
 
1746
1746
  // src/optimizer/claudemd-splitter.ts
1747
1747
  init_cjs_shims();
1748
+ var fs5 = __toESM(require("fs"));
1749
+ var path6 = __toESM(require("path"));
1750
+ init_tokenizer();
1751
+
1752
+ // src/shared/backup-manager.ts
1753
+ init_cjs_shims();
1748
1754
  var fs4 = __toESM(require("fs"));
1755
+ var os2 = __toESM(require("os"));
1749
1756
  var path5 = __toESM(require("path"));
1750
- init_tokenizer();
1757
+ var MAX_BACKUPS = 50;
1758
+ var _backupDirOverride = null;
1759
+ function getBackupDir() {
1760
+ return _backupDirOverride ?? path5.join(os2.homedir(), ".claudectx", "backups");
1761
+ }
1762
+ var BACKUP_DIR = path5.join(os2.homedir(), ".claudectx", "backups");
1763
+ function ensureBackupDir() {
1764
+ const dir = getBackupDir();
1765
+ if (!fs4.existsSync(dir)) {
1766
+ fs4.mkdirSync(dir, { recursive: true });
1767
+ }
1768
+ }
1769
+ function getManifestPath() {
1770
+ return path5.join(getBackupDir(), "manifest.json");
1771
+ }
1772
+ function readManifest() {
1773
+ ensureBackupDir();
1774
+ const manifestPath = getManifestPath();
1775
+ if (!fs4.existsSync(manifestPath)) {
1776
+ return { version: "1", entries: [] };
1777
+ }
1778
+ try {
1779
+ return JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
1780
+ } catch {
1781
+ return { version: "1", entries: [] };
1782
+ }
1783
+ }
1784
+ function writeManifest(manifest) {
1785
+ ensureBackupDir();
1786
+ const manifestPath = getManifestPath();
1787
+ const tmpPath = `${manifestPath}.tmp-${Date.now()}`;
1788
+ try {
1789
+ fs4.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
1790
+ fs4.renameSync(tmpPath, manifestPath);
1791
+ } catch {
1792
+ try {
1793
+ fs4.unlinkSync(tmpPath);
1794
+ } catch {
1795
+ }
1796
+ throw new Error(`Failed to write backup manifest at ${manifestPath}`);
1797
+ }
1798
+ }
1799
+ function generateId(originalPath) {
1800
+ const now = /* @__PURE__ */ new Date();
1801
+ const ts = now.toISOString().replace(/[-:]/g, "").replace("T", "T").replace(".", "m").replace("Z", "");
1802
+ const rand = Math.floor(Math.random() * 9e3 + 1e3);
1803
+ const basename10 = path5.basename(originalPath);
1804
+ return `${ts}-${rand}-${basename10}`;
1805
+ }
1806
+ async function backupFile(filePath, command) {
1807
+ const resolved = path5.resolve(filePath);
1808
+ if (!fs4.existsSync(resolved)) {
1809
+ throw new Error(`Cannot back up "${resolved}": file does not exist.`);
1810
+ }
1811
+ ensureBackupDir();
1812
+ const id = generateId(resolved);
1813
+ const backupPath = path5.join(getBackupDir(), id);
1814
+ fs4.copyFileSync(resolved, backupPath);
1815
+ const stat = fs4.statSync(backupPath);
1816
+ const entry = {
1817
+ id,
1818
+ originalPath: resolved,
1819
+ backupPath,
1820
+ command,
1821
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1822
+ sizeBytes: stat.size
1823
+ };
1824
+ const manifest = readManifest();
1825
+ manifest.entries.unshift(entry);
1826
+ writeManifest(manifest);
1827
+ if (manifest.entries.length > MAX_BACKUPS) {
1828
+ await pruneOldBackups();
1829
+ }
1830
+ return entry;
1831
+ }
1832
+ async function listBackups(filterPath) {
1833
+ const manifest = readManifest();
1834
+ const entries = manifest.entries;
1835
+ if (!filterPath) return entries;
1836
+ const resolved = path5.resolve(filterPath);
1837
+ return entries.filter((e) => e.originalPath === resolved);
1838
+ }
1839
+ async function restoreBackup(backupId) {
1840
+ const manifest = readManifest();
1841
+ const entry = manifest.entries.find((e) => e.id === backupId);
1842
+ if (!entry) {
1843
+ throw new Error(`Backup "${backupId}" not found. Run "claudectx revert --list" to see available backups.`);
1844
+ }
1845
+ if (!fs4.existsSync(entry.backupPath)) {
1846
+ throw new Error(`Backup file missing at "${entry.backupPath}". It may have been deleted manually.`);
1847
+ }
1848
+ let undoEntry = null;
1849
+ if (fs4.existsSync(entry.originalPath)) {
1850
+ undoEntry = await backupFile(entry.originalPath, "revert");
1851
+ }
1852
+ const targetDir = path5.dirname(entry.originalPath);
1853
+ if (!fs4.existsSync(targetDir)) {
1854
+ fs4.mkdirSync(targetDir, { recursive: true });
1855
+ }
1856
+ fs4.copyFileSync(entry.backupPath, entry.originalPath);
1857
+ return { entry, undoEntry };
1858
+ }
1859
+ async function pruneOldBackups() {
1860
+ const manifest = readManifest();
1861
+ if (manifest.entries.length <= MAX_BACKUPS) return 0;
1862
+ const toRemove = manifest.entries.splice(MAX_BACKUPS);
1863
+ for (const entry of toRemove) {
1864
+ try {
1865
+ fs4.unlinkSync(entry.backupPath);
1866
+ } catch {
1867
+ }
1868
+ }
1869
+ writeManifest(manifest);
1870
+ return toRemove.length;
1871
+ }
1872
+
1873
+ // src/optimizer/claudemd-splitter.ts
1751
1874
  var SPLIT_MIN_TOKENS = 300;
1752
1875
  function slugify(title) {
1753
1876
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -1782,9 +1905,9 @@ function parseSections(content) {
1782
1905
  return sections;
1783
1906
  }
1784
1907
  function planSplit(claudeMdPath, sectionsToExtract) {
1785
- const content = fs4.readFileSync(claudeMdPath, "utf-8");
1908
+ const content = fs5.readFileSync(claudeMdPath, "utf-8");
1786
1909
  const sections = parseSections(content);
1787
- const claudeDir = path5.join(path5.dirname(claudeMdPath), ".claude");
1910
+ const claudeDir = path6.join(path6.dirname(claudeMdPath), ".claude");
1788
1911
  const extractedFiles = [];
1789
1912
  let newContent = "";
1790
1913
  let tokensSaved = 0;
@@ -1797,7 +1920,7 @@ function planSplit(claudeMdPath, sectionsToExtract) {
1797
1920
  usedSlugs.set(slug, count + 1);
1798
1921
  const filename = `${slug}.md`;
1799
1922
  const relRefPath = `.claude/${filename}`;
1800
- const filePath = path5.join(claudeDir, filename);
1923
+ const filePath = path6.join(claudeDir, filename);
1801
1924
  const refBlock = `## ${section.title}
1802
1925
 
1803
1926
  @${relRefPath}
@@ -1821,21 +1944,24 @@ function planSplit(claudeMdPath, sectionsToExtract) {
1821
1944
  tokensSaved: Math.max(0, tokensSaved)
1822
1945
  };
1823
1946
  }
1824
- function applySplit(result) {
1947
+ async function applySplit(result) {
1825
1948
  if (result.extractedFiles.length === 0) return;
1826
- const claudeDir = path5.dirname(result.extractedFiles[0].filePath);
1827
- if (!fs4.existsSync(claudeDir)) {
1828
- fs4.mkdirSync(claudeDir, { recursive: true });
1949
+ if (fs5.existsSync(result.claudeMdPath)) {
1950
+ await backupFile(result.claudeMdPath, "optimize");
1951
+ }
1952
+ const claudeDir = path6.dirname(result.extractedFiles[0].filePath);
1953
+ if (!fs5.existsSync(claudeDir)) {
1954
+ fs5.mkdirSync(claudeDir, { recursive: true });
1829
1955
  }
1830
1956
  for (const file of result.extractedFiles) {
1831
- fs4.writeFileSync(file.filePath, file.content, "utf-8");
1957
+ fs5.writeFileSync(file.filePath, file.content, "utf-8");
1832
1958
  }
1833
- fs4.writeFileSync(result.claudeMdPath, result.newClaudeMd, "utf-8");
1959
+ fs5.writeFileSync(result.claudeMdPath, result.newClaudeMd, "utf-8");
1834
1960
  }
1835
1961
 
1836
1962
  // src/optimizer/cache-applier.ts
1837
1963
  init_cjs_shims();
1838
- var fs5 = __toESM(require("fs"));
1964
+ var fs6 = __toESM(require("fs"));
1839
1965
  function findCacheBusters(content) {
1840
1966
  const fixes = [];
1841
1967
  const lines = content.split("\n");
@@ -1865,18 +1991,21 @@ function applyCacheFixes(content, fixes) {
1865
1991
  return lines.join("\n");
1866
1992
  }
1867
1993
  function planCacheFixes(claudeMdPath) {
1868
- const content = fs5.readFileSync(claudeMdPath, "utf-8");
1994
+ const content = fs6.readFileSync(claudeMdPath, "utf-8");
1869
1995
  const fixes = findCacheBusters(content);
1870
1996
  return { fixes, newContent: applyCacheFixes(content, fixes) };
1871
1997
  }
1872
- function applyAndWriteCacheFixes(claudeMdPath, result) {
1873
- fs5.writeFileSync(claudeMdPath, result.newContent, "utf-8");
1998
+ async function applyAndWriteCacheFixes(claudeMdPath, result) {
1999
+ if (fs6.existsSync(claudeMdPath)) {
2000
+ await backupFile(claudeMdPath, "optimize");
2001
+ }
2002
+ fs6.writeFileSync(claudeMdPath, result.newContent, "utf-8");
1874
2003
  }
1875
2004
 
1876
2005
  // src/optimizer/hooks-installer.ts
1877
2006
  init_cjs_shims();
1878
- var fs6 = __toESM(require("fs"));
1879
- var path6 = __toESM(require("path"));
2007
+ var fs7 = __toESM(require("fs"));
2008
+ var path7 = __toESM(require("path"));
1880
2009
  var CLAUDECTX_HOOKS = {
1881
2010
  PostToolUse: [
1882
2011
  {
@@ -1894,13 +2023,13 @@ var CLAUDECTX_HOOKS = {
1894
2023
  ]
1895
2024
  };
1896
2025
  function planHooksInstall(projectRoot) {
1897
- const claudeDir = path6.join(projectRoot, ".claude");
1898
- const settingsPath = path6.join(claudeDir, "settings.local.json");
1899
- const existed = fs6.existsSync(settingsPath);
2026
+ const claudeDir = path7.join(projectRoot, ".claude");
2027
+ const settingsPath = path7.join(claudeDir, "settings.local.json");
2028
+ const existed = fs7.existsSync(settingsPath);
1900
2029
  let existing = {};
1901
2030
  if (existed) {
1902
2031
  try {
1903
- existing = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2032
+ existing = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
1904
2033
  } catch {
1905
2034
  existing = {};
1906
2035
  }
@@ -1921,25 +2050,25 @@ function planHooksInstall(projectRoot) {
1921
2050
  return { settingsPath, existed, mergedSettings };
1922
2051
  }
1923
2052
  function applyHooksInstall(result) {
1924
- const dir = path6.dirname(result.settingsPath);
1925
- if (!fs6.existsSync(dir)) {
1926
- fs6.mkdirSync(dir, { recursive: true });
2053
+ const dir = path7.dirname(result.settingsPath);
2054
+ if (!fs7.existsSync(dir)) {
2055
+ fs7.mkdirSync(dir, { recursive: true });
1927
2056
  }
1928
- fs6.writeFileSync(result.settingsPath, JSON.stringify(result.mergedSettings, null, 2) + "\n", "utf-8");
2057
+ fs7.writeFileSync(result.settingsPath, JSON.stringify(result.mergedSettings, null, 2) + "\n", "utf-8");
1929
2058
  }
1930
2059
  function writeHooksSettings(projectRoot, mergedSettings) {
1931
- const settingsPath = path6.join(projectRoot, ".claude", "settings.local.json");
1932
- const dir = path6.dirname(settingsPath);
1933
- if (!fs6.existsSync(dir)) {
1934
- fs6.mkdirSync(dir, { recursive: true });
2060
+ const settingsPath = path7.join(projectRoot, ".claude", "settings.local.json");
2061
+ const dir = path7.dirname(settingsPath);
2062
+ if (!fs7.existsSync(dir)) {
2063
+ fs7.mkdirSync(dir, { recursive: true });
1935
2064
  }
1936
- fs6.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n", "utf-8");
2065
+ fs7.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n", "utf-8");
1937
2066
  }
1938
2067
  function isAlreadyInstalled(projectRoot) {
1939
- const settingsPath = path6.join(projectRoot, ".claude", "settings.local.json");
1940
- if (!fs6.existsSync(settingsPath)) return false;
2068
+ const settingsPath = path7.join(projectRoot, ".claude", "settings.local.json");
2069
+ if (!fs7.existsSync(settingsPath)) return false;
1941
2070
  try {
1942
- const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2071
+ const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
1943
2072
  const postToolUse = settings?.hooks?.PostToolUse ?? [];
1944
2073
  return postToolUse.some((h) => h.matcher === "Read");
1945
2074
  } catch {
@@ -1949,7 +2078,7 @@ function isAlreadyInstalled(projectRoot) {
1949
2078
 
1950
2079
  // src/commands/optimize.ts
1951
2080
  async function optimizeCommand(options) {
1952
- const projectPath = options.path ? path7.resolve(options.path) : findProjectRoot() ?? process.cwd();
2081
+ const projectPath = options.path ? path8.resolve(options.path) : findProjectRoot() ?? process.cwd();
1953
2082
  const dryRun = options.dryRun ?? false;
1954
2083
  const autoApply = options.apply ?? false;
1955
2084
  const specificMode = options.claudemd || options.ignorefile || options.cache || options.hooks;
@@ -2074,12 +2203,12 @@ async function runIgnorefile(projectRoot, dryRun, autoApply) {
2074
2203
  }
2075
2204
  async function runClaudeMdSplit(projectRoot, report, dryRun, autoApply) {
2076
2205
  printSectionHeader("CLAUDE.md \u2192 @files");
2077
- const claudeMdPath = path7.join(projectRoot, "CLAUDE.md");
2078
- if (!fs7.existsSync(claudeMdPath)) {
2206
+ const claudeMdPath = path8.join(projectRoot, "CLAUDE.md");
2207
+ if (!fs8.existsSync(claudeMdPath)) {
2079
2208
  logger.warn("No CLAUDE.md found \u2014 skipping.");
2080
2209
  return;
2081
2210
  }
2082
- const content = fs7.readFileSync(claudeMdPath, "utf-8");
2211
+ const content = fs8.readFileSync(claudeMdPath, "utf-8");
2083
2212
  const sections = parseSections(content);
2084
2213
  const largeSections = sections.filter(
2085
2214
  (s) => !s.isPreamble && s.tokens >= SPLIT_MIN_TOKENS
@@ -2135,18 +2264,18 @@ Would extract ${splitResult.extractedFiles.length} section(s) to .claude/`
2135
2264
  logger.info("Skipped.");
2136
2265
  return;
2137
2266
  }
2138
- applySplit(splitResult);
2267
+ await applySplit(splitResult);
2139
2268
  logger.success(
2140
2269
  `Extracted ${splitResult.extractedFiles.length} section(s). Saved ~${splitResult.tokensSaved} tokens/request.`
2141
2270
  );
2142
2271
  for (const f of splitResult.extractedFiles) {
2143
- logger.info(` Created: ${import_chalk3.default.cyan(path7.relative(projectRoot, f.filePath))}`);
2272
+ logger.info(` Created: ${import_chalk3.default.cyan(path8.relative(projectRoot, f.filePath))}`);
2144
2273
  }
2145
2274
  }
2146
2275
  async function runCacheOptimization(projectRoot, dryRun, autoApply) {
2147
2276
  printSectionHeader("Prompt cache optimisation");
2148
- const claudeMdPath = path7.join(projectRoot, "CLAUDE.md");
2149
- if (!fs7.existsSync(claudeMdPath)) {
2277
+ const claudeMdPath = path8.join(projectRoot, "CLAUDE.md");
2278
+ if (!fs8.existsSync(claudeMdPath)) {
2150
2279
  logger.warn("No CLAUDE.md found \u2014 skipping.");
2151
2280
  return;
2152
2281
  }
@@ -2174,14 +2303,14 @@ async function runCacheOptimization(projectRoot, dryRun, autoApply) {
2174
2303
  logger.info("Skipped.");
2175
2304
  return;
2176
2305
  }
2177
- applyAndWriteCacheFixes(claudeMdPath, result);
2306
+ await applyAndWriteCacheFixes(claudeMdPath, result);
2178
2307
  logger.success(`Fixed ${result.fixes.length} cache-busting pattern(s) in CLAUDE.md.`);
2179
2308
  }
2180
2309
  async function runHooks(projectRoot, dryRun, autoApply) {
2181
2310
  printSectionHeader("Session hooks");
2182
2311
  const result = planHooksInstall(projectRoot);
2183
2312
  logger.info(
2184
- `Settings file: ${import_chalk3.default.cyan(path7.relative(projectRoot, result.settingsPath))}`
2313
+ `Settings file: ${import_chalk3.default.cyan(path8.relative(projectRoot, result.settingsPath))}`
2185
2314
  );
2186
2315
  logger.info(result.existed ? "Will merge with existing settings." : "Will create new file.");
2187
2316
  console.log(import_chalk3.default.dim("\n Hooks to install:"));
@@ -2196,7 +2325,7 @@ async function runHooks(projectRoot, dryRun, autoApply) {
2196
2325
  }
2197
2326
  applyHooksInstall(result);
2198
2327
  logger.success(
2199
- `Hooks installed \u2192 ${import_chalk3.default.cyan(path7.relative(projectRoot, result.settingsPath))}`
2328
+ `Hooks installed \u2192 ${import_chalk3.default.cyan(path8.relative(projectRoot, result.settingsPath))}`
2200
2329
  );
2201
2330
  }
2202
2331
  function printSectionHeader(title) {
@@ -2206,7 +2335,7 @@ function printSectionHeader(title) {
2206
2335
 
2207
2336
  // src/commands/watch.ts
2208
2337
  init_cjs_shims();
2209
- var path11 = __toESM(require("path"));
2338
+ var path12 = __toESM(require("path"));
2210
2339
  init_session_store();
2211
2340
  init_models();
2212
2341
  async function watchCommand(options) {
@@ -2244,30 +2373,30 @@ async function handleLogStdin() {
2244
2373
  const payload = JSON.parse(raw);
2245
2374
  const filePath = payload.tool_input?.file_path;
2246
2375
  if (filePath) {
2247
- appendFileRead(path11.resolve(filePath), payload.session_id);
2376
+ appendFileRead(path12.resolve(filePath), payload.session_id);
2248
2377
  }
2249
2378
  } catch {
2250
2379
  }
2251
2380
  }
2252
2381
  function readStdin() {
2253
- return new Promise((resolve12) => {
2382
+ return new Promise((resolve13) => {
2254
2383
  let data = "";
2255
2384
  process.stdin.setEncoding("utf-8");
2256
2385
  process.stdin.on("data", (chunk) => data += chunk);
2257
- process.stdin.on("end", () => resolve12(data));
2258
- setTimeout(() => resolve12(data), 500);
2386
+ process.stdin.on("end", () => resolve13(data));
2387
+ setTimeout(() => resolve13(data), 500);
2259
2388
  });
2260
2389
  }
2261
2390
 
2262
2391
  // src/commands/mcp.ts
2263
2392
  init_cjs_shims();
2264
- var path16 = __toESM(require("path"));
2393
+ var path17 = __toESM(require("path"));
2265
2394
  var import_chalk4 = __toESM(require("chalk"));
2266
2395
 
2267
2396
  // src/mcp/installer.ts
2268
2397
  init_cjs_shims();
2269
- var fs11 = __toESM(require("fs"));
2270
- var path12 = __toESM(require("path"));
2398
+ var fs12 = __toESM(require("fs"));
2399
+ var path13 = __toESM(require("path"));
2271
2400
  var SERVER_NAME = "claudectx";
2272
2401
  var SERVER_ENTRY = {
2273
2402
  command: "claudectx",
@@ -2275,13 +2404,13 @@ var SERVER_ENTRY = {
2275
2404
  type: "stdio"
2276
2405
  };
2277
2406
  function planInstall(projectRoot) {
2278
- const claudeDir = path12.join(projectRoot, ".claude");
2279
- const settingsPath = path12.join(claudeDir, "settings.json");
2280
- const existed = fs11.existsSync(settingsPath);
2407
+ const claudeDir = path13.join(projectRoot, ".claude");
2408
+ const settingsPath = path13.join(claudeDir, "settings.json");
2409
+ const existed = fs12.existsSync(settingsPath);
2281
2410
  let existing = {};
2282
2411
  if (existed) {
2283
2412
  try {
2284
- existing = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
2413
+ existing = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
2285
2414
  } catch {
2286
2415
  existing = {};
2287
2416
  }
@@ -2298,21 +2427,21 @@ function planInstall(projectRoot) {
2298
2427
  return { settingsPath, existed, alreadyInstalled, mergedSettings };
2299
2428
  }
2300
2429
  function applyInstall(result) {
2301
- const dir = path12.dirname(result.settingsPath);
2302
- if (!fs11.existsSync(dir)) {
2303
- fs11.mkdirSync(dir, { recursive: true });
2430
+ const dir = path13.dirname(result.settingsPath);
2431
+ if (!fs12.existsSync(dir)) {
2432
+ fs12.mkdirSync(dir, { recursive: true });
2304
2433
  }
2305
- fs11.writeFileSync(
2434
+ fs12.writeFileSync(
2306
2435
  result.settingsPath,
2307
2436
  JSON.stringify(result.mergedSettings, null, 2) + "\n",
2308
2437
  "utf-8"
2309
2438
  );
2310
2439
  }
2311
2440
  function isInstalled(projectRoot) {
2312
- const settingsPath = path12.join(projectRoot, ".claude", "settings.json");
2313
- if (!fs11.existsSync(settingsPath)) return false;
2441
+ const settingsPath = path13.join(projectRoot, ".claude", "settings.json");
2442
+ if (!fs12.existsSync(settingsPath)) return false;
2314
2443
  try {
2315
- const settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
2444
+ const settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
2316
2445
  return SERVER_NAME in (settings.mcpServers ?? {});
2317
2446
  } catch {
2318
2447
  return false;
@@ -2321,7 +2450,7 @@ function isInstalled(projectRoot) {
2321
2450
 
2322
2451
  // src/commands/mcp.ts
2323
2452
  async function mcpCommand(options) {
2324
- const projectRoot = options.path ? path16.resolve(options.path) : process.cwd();
2453
+ const projectRoot = options.path ? path17.resolve(options.path) : process.cwd();
2325
2454
  if (options.install) {
2326
2455
  await runInstall(projectRoot);
2327
2456
  return;
@@ -2369,13 +2498,13 @@ async function runInstall(projectRoot) {
2369
2498
 
2370
2499
  // src/commands/compress.ts
2371
2500
  init_cjs_shims();
2372
- var path18 = __toESM(require("path"));
2373
- var fs16 = __toESM(require("fs"));
2501
+ var path19 = __toESM(require("path"));
2502
+ var fs17 = __toESM(require("fs"));
2374
2503
  init_session_reader();
2375
2504
 
2376
2505
  // src/compressor/session-parser.ts
2377
2506
  init_cjs_shims();
2378
- var fs14 = __toESM(require("fs"));
2507
+ var fs15 = __toESM(require("fs"));
2379
2508
  function extractText(content) {
2380
2509
  if (!content) return "";
2381
2510
  if (typeof content === "string") return content;
@@ -2386,10 +2515,10 @@ function extractToolCalls(content) {
2386
2515
  return content.filter((b) => b.type === "tool_use" && b.name).map((b) => ({ tool: b.name, input: b.input ?? {} }));
2387
2516
  }
2388
2517
  function parseSessionFile(sessionFilePath) {
2389
- if (!fs14.existsSync(sessionFilePath)) return null;
2518
+ if (!fs15.existsSync(sessionFilePath)) return null;
2390
2519
  let content;
2391
2520
  try {
2392
- content = fs14.readFileSync(sessionFilePath, "utf-8");
2521
+ content = fs15.readFileSync(sessionFilePath, "utf-8");
2393
2522
  } catch {
2394
2523
  return null;
2395
2524
  }
@@ -2589,13 +2718,13 @@ function calcCost(inputTokens, outputTokens) {
2589
2718
 
2590
2719
  // src/compressor/memory-writer.ts
2591
2720
  init_cjs_shims();
2592
- var fs15 = __toESM(require("fs"));
2593
- var path17 = __toESM(require("path"));
2721
+ var fs16 = __toESM(require("fs"));
2722
+ var path18 = __toESM(require("path"));
2594
2723
  function parseMemoryFile(filePath) {
2595
- if (!fs15.existsSync(filePath)) {
2724
+ if (!fs16.existsSync(filePath)) {
2596
2725
  return { preamble: "", entries: [] };
2597
2726
  }
2598
- const content = fs15.readFileSync(filePath, "utf-8");
2727
+ const content = fs16.readFileSync(filePath, "utf-8");
2599
2728
  const markerRegex = /<!-- claudectx-entry: (\d{4}-\d{2}-\d{2}) \| session: ([a-z0-9-]+) -->/g;
2600
2729
  const indices = [];
2601
2730
  let match;
@@ -2646,15 +2775,15 @@ function appendEntry(memoryFilePath, sessionId, summaryText, date = /* @__PURE__
2646
2775
  const newBlock = buildEntryBlock(sessionId, summaryText, date);
2647
2776
  const allBlocks = [...entries.map((e) => e.raw), newBlock];
2648
2777
  const newContent = (preamble.trimEnd() ? preamble.trimEnd() + "\n\n" : "") + allBlocks.join("\n\n") + "\n";
2649
- const dir = path17.dirname(memoryFilePath);
2650
- if (!fs15.existsSync(dir)) {
2651
- fs15.mkdirSync(dir, { recursive: true });
2778
+ const dir = path18.dirname(memoryFilePath);
2779
+ if (!fs16.existsSync(dir)) {
2780
+ fs16.mkdirSync(dir, { recursive: true });
2652
2781
  }
2653
- fs15.writeFileSync(memoryFilePath, newContent, "utf-8");
2782
+ fs16.writeFileSync(memoryFilePath, newContent, "utf-8");
2654
2783
  return newContent;
2655
2784
  }
2656
2785
  function pruneOldEntries(memoryFilePath, days) {
2657
- if (!fs15.existsSync(memoryFilePath)) {
2786
+ if (!fs16.existsSync(memoryFilePath)) {
2658
2787
  return { removed: 0, kept: 0, removedEntries: [] };
2659
2788
  }
2660
2789
  const { preamble, entries } = parseMemoryFile(memoryFilePath);
@@ -2667,7 +2796,7 @@ function pruneOldEntries(memoryFilePath, days) {
2667
2796
  return { removed: 0, kept: kept.length, removedEntries: [] };
2668
2797
  }
2669
2798
  const newContent = (preamble.trimEnd() ? preamble.trimEnd() + "\n\n" : "") + kept.map((e) => e.raw).join("\n\n") + (kept.length > 0 ? "\n" : "");
2670
- fs15.writeFileSync(memoryFilePath, newContent, "utf-8");
2799
+ fs16.writeFileSync(memoryFilePath, newContent, "utf-8");
2671
2800
  return { removed: removed.length, kept: kept.length, removedEntries: removed };
2672
2801
  }
2673
2802
  function isAlreadyCompressed(memoryFilePath, sessionId) {
@@ -2678,8 +2807,8 @@ function isAlreadyCompressed(memoryFilePath, sessionId) {
2678
2807
  // src/commands/compress.ts
2679
2808
  async function compressCommand(options) {
2680
2809
  const chalk5 = (await import("chalk")).default;
2681
- const projectRoot = options.path ? path18.resolve(options.path) : process.cwd();
2682
- const memoryFilePath = path18.join(projectRoot, "MEMORY.md");
2810
+ const projectRoot = options.path ? path19.resolve(options.path) : process.cwd();
2811
+ const memoryFilePath = path19.join(projectRoot, "MEMORY.md");
2683
2812
  const sessionFiles = listSessionFiles();
2684
2813
  if (sessionFiles.length === 0) {
2685
2814
  process.stdout.write(chalk5.red("No Claude Code sessions found.\n"));
@@ -2704,7 +2833,7 @@ async function compressCommand(options) {
2704
2833
  } else {
2705
2834
  targetFile = sessionFiles[0].filePath;
2706
2835
  }
2707
- const sessionId = path18.basename(targetFile, ".jsonl");
2836
+ const sessionId = path19.basename(targetFile, ".jsonl");
2708
2837
  if (isAlreadyCompressed(memoryFilePath, sessionId)) {
2709
2838
  if (!options.auto) {
2710
2839
  process.stdout.write(chalk5.yellow(`Session ${sessionId.slice(0, 8)}\u2026 is already in MEMORY.md \u2014 skipping.
@@ -2752,11 +2881,27 @@ async function compressCommand(options) {
2752
2881
  }
2753
2882
  if (options.prune) {
2754
2883
  const days = parseInt(options.days ?? "30", 10);
2755
- if (!fs16.existsSync(memoryFilePath)) return;
2884
+ if (!fs17.existsSync(memoryFilePath)) return;
2885
+ if (!options.auto) {
2886
+ let confirmed = true;
2887
+ try {
2888
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
2889
+ confirmed = await confirm2({
2890
+ message: `Prune MEMORY.md entries older than ${days} days? Run 'claudectx revert' to undo.`,
2891
+ default: false
2892
+ });
2893
+ } catch {
2894
+ }
2895
+ if (!confirmed) {
2896
+ process.stdout.write(chalk5.dim("Prune skipped.\n"));
2897
+ return;
2898
+ }
2899
+ }
2900
+ await backupFile(memoryFilePath, "compress");
2756
2901
  const pruned = pruneOldEntries(memoryFilePath, days);
2757
2902
  if (pruned.removed > 0 && !options.auto) {
2758
2903
  process.stdout.write(
2759
- chalk5.dim(`Pruned ${pruned.removed} entr${pruned.removed === 1 ? "y" : "ies"} older than ${days} days.
2904
+ chalk5.dim(`Pruned ${pruned.removed} entr${pruned.removed === 1 ? "y" : "ies"} older than ${days} days. Run 'claudectx revert' to undo.
2760
2905
  `)
2761
2906
  );
2762
2907
  }
@@ -2832,7 +2977,8 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
2832
2977
  readCount: s.readCount
2833
2978
  }));
2834
2979
  const totalCost = calcCost2(totalInput, totalOutput, totalCacheCreation, totalCacheRead, model);
2835
- const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
2980
+ const totalContext = totalInput + totalCacheRead;
2981
+ const cacheHitRate = totalContext > 0 ? Math.round(totalCacheRead / totalContext * 100) : 0;
2836
2982
  const byDay = [...bucketMap.values()].sort((a, b) => a.date.localeCompare(b.date));
2837
2983
  const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
2838
2984
  const dailyAvgCostUsd = days > 0 ? totalCost / days : 0;
@@ -3034,12 +3180,12 @@ async function reportCommand(options) {
3034
3180
 
3035
3181
  // src/commands/budget.ts
3036
3182
  init_cjs_shims();
3037
- var path20 = __toESM(require("path"));
3183
+ var path21 = __toESM(require("path"));
3038
3184
 
3039
3185
  // src/analyzer/budget-estimator.ts
3040
3186
  init_cjs_shims();
3041
- var fs17 = __toESM(require("fs"));
3042
- var path19 = __toESM(require("path"));
3187
+ var fs18 = __toESM(require("fs"));
3188
+ var path20 = __toESM(require("path"));
3043
3189
  var import_glob2 = require("glob");
3044
3190
  init_tokenizer();
3045
3191
  init_session_store();
@@ -3065,20 +3211,20 @@ function classifyCacheHit(recentReadCount) {
3065
3211
  return "low";
3066
3212
  }
3067
3213
  function suggestClaudeignoreAdditions(files, projectRoot) {
3068
- const ignorePath = path19.join(projectRoot, ".claudeignore");
3214
+ const ignorePath = path20.join(projectRoot, ".claudeignore");
3069
3215
  let ignorePatterns = [];
3070
3216
  try {
3071
- const content = fs17.readFileSync(ignorePath, "utf-8");
3217
+ const content = fs18.readFileSync(ignorePath, "utf-8");
3072
3218
  ignorePatterns = content.split("\n").filter(Boolean);
3073
3219
  } catch {
3074
3220
  }
3075
3221
  const recommendations = [];
3076
3222
  for (const file of files) {
3077
3223
  if (file.tokenCount <= WASTE_THRESHOLDS.MAX_REFERENCE_FILE_TOKENS) continue;
3078
- const rel = path19.relative(projectRoot, file.filePath);
3224
+ const rel = path20.relative(projectRoot, file.filePath);
3079
3225
  const alreadyIgnored = ignorePatterns.some((pattern) => {
3080
3226
  const cleanPattern = pattern.replace(/^!/, "");
3081
- return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g, path19.sep));
3227
+ return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g, path20.sep));
3082
3228
  });
3083
3229
  if (!alreadyIgnored) {
3084
3230
  recommendations.push(rel);
@@ -3098,7 +3244,7 @@ async function estimateBudget(globs, projectRoot, model, thresholdTokens) {
3098
3244
  for (const filePath of filePaths) {
3099
3245
  let content = "";
3100
3246
  try {
3101
- content = fs17.readFileSync(filePath, "utf-8");
3247
+ content = fs18.readFileSync(filePath, "utf-8");
3102
3248
  } catch {
3103
3249
  continue;
3104
3250
  }
@@ -3147,7 +3293,7 @@ function formatBudgetReport(report) {
3147
3293
  }
3148
3294
  const LIKELIHOOD_ICON = { high: "\u{1F7E2}", medium: "\u{1F7E1}", low: "\u{1F534}" };
3149
3295
  const maxPathLen = Math.min(
3150
- Math.max(...report.files.map((f) => path19.basename(f.filePath).length)),
3296
+ Math.max(...report.files.map((f) => path20.basename(f.filePath).length)),
3151
3297
  40
3152
3298
  );
3153
3299
  lines.push(
@@ -3155,7 +3301,7 @@ function formatBudgetReport(report) {
3155
3301
  );
3156
3302
  lines.push("\u2500".repeat(50));
3157
3303
  for (const file of report.files.slice(0, 20)) {
3158
- const name = path19.basename(file.filePath).slice(0, maxPathLen).padEnd(maxPathLen);
3304
+ const name = path20.basename(file.filePath).slice(0, maxPathLen).padEnd(maxPathLen);
3159
3305
  const tokens = file.tokenCount.toLocaleString().padStart(7);
3160
3306
  const cache = `${LIKELIHOOD_ICON[file.cacheHitLikelihood]} ${file.cacheHitLikelihood.padEnd(6)}`;
3161
3307
  const cost = formatCost(file.estimatedCostUsd).padStart(7);
@@ -3184,7 +3330,7 @@ function formatBudgetReport(report) {
3184
3330
  // src/commands/budget.ts
3185
3331
  init_models();
3186
3332
  async function budgetCommand(globs, options) {
3187
- const projectPath = options.path ? path20.resolve(options.path) : process.cwd();
3333
+ const projectPath = options.path ? path21.resolve(options.path) : process.cwd();
3188
3334
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3189
3335
  const model = resolveModel(options.model ?? "sonnet");
3190
3336
  const thresholdTokens = parseInt(options.threshold ?? "10000", 10);
@@ -3203,7 +3349,7 @@ async function budgetCommand(globs, options) {
3203
3349
 
3204
3350
  // src/commands/warmup.ts
3205
3351
  init_cjs_shims();
3206
- var path21 = __toESM(require("path"));
3352
+ var path22 = __toESM(require("path"));
3207
3353
  var import_sdk = __toESM(require("@anthropic-ai/sdk"));
3208
3354
  init_models();
3209
3355
  var import_fs3 = __toESM(require("fs"));
@@ -3277,6 +3423,19 @@ async function installCron(cronExpr) {
3277
3423
  );
3278
3424
  process.exit(1);
3279
3425
  }
3426
+ let confirmed = true;
3427
+ try {
3428
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
3429
+ confirmed = await confirm2({
3430
+ message: `Install cron job "${cronExpr} claudectx warmup" in your system crontab?`,
3431
+ default: false
3432
+ });
3433
+ } catch {
3434
+ }
3435
+ if (!confirmed) {
3436
+ process.stdout.write(" Cron install cancelled.\n");
3437
+ return;
3438
+ }
3280
3439
  const { execSync: execSync3 } = await import("child_process");
3281
3440
  const command = `claudectx warmup`;
3282
3441
  const cronLine = `${cronExpr} ${command}`;
@@ -3299,16 +3458,16 @@ async function installCron(cronExpr) {
3299
3458
  ${marker}
3300
3459
  ${cronLine}
3301
3460
  `;
3302
- const { writeFileSync: writeFileSync11, unlinkSync: unlinkSync2 } = await import("fs");
3461
+ const { writeFileSync: writeFileSync12, unlinkSync: unlinkSync3 } = await import("fs");
3303
3462
  const { tmpdir } = await import("os");
3304
- const { join: join17 } = await import("path");
3305
- const tmpFile = join17(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
3463
+ const { join: join18 } = await import("path");
3464
+ const tmpFile = join18(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
3306
3465
  try {
3307
- writeFileSync11(tmpFile, newCrontab, "utf-8");
3466
+ writeFileSync12(tmpFile, newCrontab, "utf-8");
3308
3467
  execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
3309
3468
  } finally {
3310
3469
  try {
3311
- unlinkSync2(tmpFile);
3470
+ unlinkSync3(tmpFile);
3312
3471
  } catch {
3313
3472
  }
3314
3473
  }
@@ -3325,7 +3484,7 @@ ${cronLine}
3325
3484
  }
3326
3485
  }
3327
3486
  async function warmupCommand(options) {
3328
- const projectPath = options.path ? path21.resolve(options.path) : process.cwd();
3487
+ const projectPath = options.path ? path22.resolve(options.path) : process.cwd();
3329
3488
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3330
3489
  const model = resolveModel(options.model ?? "haiku");
3331
3490
  const ttl = options.ttl === "60" ? 60 : 5;
@@ -3337,7 +3496,7 @@ async function warmupCommand(options) {
3337
3496
  process.exit(1);
3338
3497
  }
3339
3498
  let claudeMdContent = "";
3340
- const claudeMdPath = path21.join(projectRoot, "CLAUDE.md");
3499
+ const claudeMdPath = path22.join(projectRoot, "CLAUDE.md");
3341
3500
  try {
3342
3501
  claudeMdContent = import_fs3.default.readFileSync(claudeMdPath, "utf-8");
3343
3502
  } catch {
@@ -3397,13 +3556,13 @@ async function warmupCommand(options) {
3397
3556
 
3398
3557
  // src/commands/drift.ts
3399
3558
  init_cjs_shims();
3400
- var path23 = __toESM(require("path"));
3401
- var fs20 = __toESM(require("fs"));
3559
+ var path24 = __toESM(require("path"));
3560
+ var fs21 = __toESM(require("fs"));
3402
3561
 
3403
3562
  // src/analyzer/drift-detector.ts
3404
3563
  init_cjs_shims();
3405
- var fs19 = __toESM(require("fs"));
3406
- var path22 = __toESM(require("path"));
3564
+ var fs20 = __toESM(require("fs"));
3565
+ var path23 = __toESM(require("path"));
3407
3566
  var childProcess = __toESM(require("child_process"));
3408
3567
  init_tokenizer();
3409
3568
  init_session_store();
@@ -3416,8 +3575,8 @@ function findDeadAtReferences(content, projectRoot) {
3416
3575
  const match = lines[i].match(AT_REF_RE);
3417
3576
  if (!match) continue;
3418
3577
  const ref = match[1].trim();
3419
- const absPath = path22.isAbsolute(ref) ? ref : path22.join(projectRoot, ref);
3420
- if (!fs19.existsSync(absPath)) {
3578
+ const absPath = path23.isAbsolute(ref) ? ref : path23.join(projectRoot, ref);
3579
+ if (!fs20.existsSync(absPath)) {
3421
3580
  const lineText = lines[i];
3422
3581
  issues.push({
3423
3582
  type: "dead-ref",
@@ -3446,19 +3605,33 @@ async function findGitDeletedMentions(content, projectRoot) {
3446
3605
  return [];
3447
3606
  }
3448
3607
  if (deletedFiles.size === 0) return [];
3608
+ const deletedTerms = [];
3609
+ for (const deleted of deletedFiles) {
3610
+ const stem = path23.basename(deleted, path23.extname(deleted));
3611
+ const basenameWithExt = path23.basename(deleted);
3612
+ if (stem.length < 4) continue;
3613
+ deletedTerms.push({ basename: basenameWithExt, fullPath: deleted });
3614
+ }
3615
+ if (deletedTerms.length === 0) return [];
3449
3616
  const lines = content.split("\n");
3450
3617
  for (let i = 0; i < lines.length; i++) {
3451
3618
  const line = lines[i];
3452
- for (const deleted of deletedFiles) {
3453
- const basename8 = path22.basename(deleted);
3454
- if (line.includes(basename8) || line.includes(deleted)) {
3619
+ const lineLower = line.toLowerCase();
3620
+ for (const { basename: basename10, fullPath } of deletedTerms) {
3621
+ const matchesFullPath = lineLower.includes(fullPath.toLowerCase());
3622
+ const basenameLower = basename10.toLowerCase();
3623
+ const idx = lineLower.indexOf(basenameLower);
3624
+ const matchesBasename = idx !== -1 && // Check left boundary: start of string, space, slash, quote, backtick, or `@`
3625
+ (idx === 0 || /[\s/`'"@(]/.test(line[idx - 1])) && // Check right boundary: end of string, space, punctuation, or extension dot
3626
+ (idx + basenameLower.length >= line.length || /[\s/`'",.):@]/.test(line[idx + basenameLower.length]));
3627
+ if (matchesFullPath || matchesBasename) {
3455
3628
  issues.push({
3456
3629
  type: "git-deleted",
3457
3630
  line: i + 1,
3458
3631
  text: line.trim(),
3459
3632
  severity: "warning",
3460
3633
  estimatedTokenWaste: countTokens(line),
3461
- suggestion: `References "${basename8}" which was deleted from git. Consider removing this mention.`
3634
+ suggestion: `References "${basename10}" which was deleted from git. Consider removing this mention.`
3462
3635
  });
3463
3636
  break;
3464
3637
  }
@@ -3523,8 +3696,8 @@ function findDeadInlinePaths(content, projectRoot) {
3523
3696
  const rawPath = match[1].trim();
3524
3697
  if (seen.has(rawPath)) continue;
3525
3698
  seen.add(rawPath);
3526
- const absPath = path22.isAbsolute(rawPath) ? rawPath : path22.join(projectRoot, rawPath);
3527
- if (!fs19.existsSync(absPath)) {
3699
+ const absPath = path23.isAbsolute(rawPath) ? rawPath : path23.join(projectRoot, rawPath);
3700
+ if (!fs20.existsSync(absPath)) {
3528
3701
  issues.push({
3529
3702
  type: "dead-inline-path",
3530
3703
  line: i + 1,
@@ -3540,10 +3713,10 @@ function findDeadInlinePaths(content, projectRoot) {
3540
3713
  return issues;
3541
3714
  }
3542
3715
  async function detectDrift(projectRoot, dayWindow) {
3543
- const claudeMdPath = path22.join(projectRoot, "CLAUDE.md");
3716
+ const claudeMdPath = path23.join(projectRoot, "CLAUDE.md");
3544
3717
  let content = "";
3545
3718
  try {
3546
- content = fs19.readFileSync(claudeMdPath, "utf-8");
3719
+ content = fs20.readFileSync(claudeMdPath, "utf-8");
3547
3720
  } catch {
3548
3721
  return {
3549
3722
  claudeMdPath,
@@ -3585,7 +3758,7 @@ var TYPE_LABEL = {
3585
3758
  "dead-inline-path": "Dead path"
3586
3759
  };
3587
3760
  async function driftCommand(options) {
3588
- const projectPath = options.path ? path23.resolve(options.path) : process.cwd();
3761
+ const projectPath = options.path ? path24.resolve(options.path) : process.cwd();
3589
3762
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3590
3763
  const dayWindow = parseInt(options.days ?? "30", 10);
3591
3764
  const report = await detectDrift(projectRoot, dayWindow);
@@ -3661,25 +3834,25 @@ async function applyFix(claudeMdPath, issues) {
3661
3834
  process.stdout.write("No lines selected. Nothing changed.\n");
3662
3835
  return;
3663
3836
  }
3664
- const content = fs20.readFileSync(claudeMdPath, "utf-8");
3837
+ const content = fs21.readFileSync(claudeMdPath, "utf-8");
3665
3838
  const lines = content.split("\n");
3666
3839
  const lineSet = new Set(selectedLines.map((l) => l - 1));
3667
3840
  const newLines = lines.filter((_, i) => !lineSet.has(i));
3668
3841
  const newContent = newLines.join("\n");
3669
3842
  const backupPath = `${claudeMdPath}.bak`;
3670
- fs20.writeFileSync(backupPath, content, "utf-8");
3671
- const os5 = await import("os");
3843
+ fs21.writeFileSync(backupPath, content, "utf-8");
3844
+ const os6 = await import("os");
3672
3845
  const tmpPath = `${claudeMdPath}.tmp-${Date.now()}`;
3673
3846
  try {
3674
- fs20.writeFileSync(tmpPath, newContent, "utf-8");
3675
- fs20.renameSync(tmpPath, claudeMdPath);
3847
+ fs21.writeFileSync(tmpPath, newContent, "utf-8");
3848
+ fs21.renameSync(tmpPath, claudeMdPath);
3676
3849
  } catch (err) {
3677
3850
  try {
3678
- fs20.copyFileSync(backupPath, claudeMdPath);
3851
+ fs21.copyFileSync(backupPath, claudeMdPath);
3679
3852
  } catch {
3680
3853
  }
3681
3854
  try {
3682
- fs20.unlinkSync(tmpPath);
3855
+ fs21.unlinkSync(tmpPath);
3683
3856
  } catch {
3684
3857
  }
3685
3858
  process.stderr.write(`Error writing CLAUDE.md: ${err instanceof Error ? err.message : String(err)}
@@ -3687,18 +3860,18 @@ async function applyFix(claudeMdPath, issues) {
3687
3860
  process.exit(1);
3688
3861
  }
3689
3862
  process.stdout.write(`
3690
- \u2713 Removed ${selectedLines.length} line(s) from ${path23.basename(claudeMdPath)}
3863
+ \u2713 Removed ${selectedLines.length} line(s) from ${path24.basename(claudeMdPath)}
3691
3864
  `);
3692
3865
  process.stdout.write(` \u2713 Backup saved to ${backupPath}
3693
3866
 
3694
3867
  `);
3695
- void os5;
3868
+ void os6;
3696
3869
  }
3697
3870
 
3698
3871
  // src/commands/hooks.ts
3699
3872
  init_cjs_shims();
3700
- var fs21 = __toESM(require("fs"));
3701
- var path24 = __toESM(require("path"));
3873
+ var fs22 = __toESM(require("fs"));
3874
+ var path25 = __toESM(require("path"));
3702
3875
 
3703
3876
  // src/hooks/registry.ts
3704
3877
  init_cjs_shims();
@@ -3774,17 +3947,17 @@ function buildHookEntry(def, config) {
3774
3947
 
3775
3948
  // src/commands/hooks.ts
3776
3949
  function readInstalledHooks(projectRoot) {
3777
- const settingsPath = path24.join(projectRoot, ".claude", "settings.local.json");
3778
- if (!fs21.existsSync(settingsPath)) return {};
3950
+ const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
3951
+ if (!fs22.existsSync(settingsPath)) return {};
3779
3952
  try {
3780
- return JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
3953
+ return JSON.parse(fs22.readFileSync(settingsPath, "utf-8"));
3781
3954
  } catch {
3782
3955
  process.stderr.write(
3783
3956
  `Warning: ${settingsPath} exists but contains invalid JSON. Existing settings will be preserved as a backup.
3784
3957
  `
3785
3958
  );
3786
3959
  try {
3787
- fs21.copyFileSync(settingsPath, `${settingsPath}.bak`);
3960
+ fs22.copyFileSync(settingsPath, `${settingsPath}.bak`);
3788
3961
  } catch {
3789
3962
  }
3790
3963
  return {};
@@ -3904,9 +4077,26 @@ async function hooksAdd(name, projectRoot, configPairs) {
3904
4077
  }
3905
4078
  async function hooksRemove(name, projectRoot) {
3906
4079
  const settings = readInstalledHooks(projectRoot);
4080
+ let confirmed = true;
4081
+ try {
4082
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
4083
+ confirmed = await confirm2({
4084
+ message: `Remove hook "${name}"? Run 'claudectx revert' to undo.`,
4085
+ default: false
4086
+ });
4087
+ } catch {
4088
+ }
4089
+ if (!confirmed) {
4090
+ process.stdout.write(" Cancelled.\n\n");
4091
+ return;
4092
+ }
4093
+ const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
4094
+ if (fs22.existsSync(settingsPath)) {
4095
+ await backupFile(settingsPath, "hooks");
4096
+ }
3907
4097
  const updated = removeHookByName(settings, name);
3908
4098
  writeHooksSettings(projectRoot, updated);
3909
- process.stdout.write(` \u2713 Hook "${name}" removed.
4099
+ process.stdout.write(` \u2713 Hook "${name}" removed. Run 'claudectx revert --list' to undo.
3910
4100
 
3911
4101
  `);
3912
4102
  }
@@ -3931,7 +4121,7 @@ async function hooksStatus(projectRoot) {
3931
4121
  process.stdout.write("\n");
3932
4122
  }
3933
4123
  async function hooksCommand(subcommand, options) {
3934
- const projectPath = options.path ? path24.resolve(options.path) : process.cwd();
4124
+ const projectPath = options.path ? path25.resolve(options.path) : process.cwd();
3935
4125
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3936
4126
  const sub = subcommand ?? "list";
3937
4127
  switch (sub) {
@@ -3970,8 +4160,8 @@ async function hooksCommand(subcommand, options) {
3970
4160
 
3971
4161
  // src/commands/convert.ts
3972
4162
  init_cjs_shims();
3973
- var fs22 = __toESM(require("fs"));
3974
- var path25 = __toESM(require("path"));
4163
+ var fs23 = __toESM(require("fs"));
4164
+ var path26 = __toESM(require("path"));
3975
4165
  function slugify2(text) {
3976
4166
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3977
4167
  }
@@ -4020,7 +4210,7 @@ function claudeMdToWindsurf(content) {
4020
4210
  return content.split("\n").filter((line) => !line.match(/^@.+$/)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
4021
4211
  }
4022
4212
  async function convertCommand(options) {
4023
- const projectPath = options.path ? path25.resolve(options.path) : process.cwd();
4213
+ const projectPath = options.path ? path26.resolve(options.path) : process.cwd();
4024
4214
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
4025
4215
  const from = options.from ?? "claude";
4026
4216
  const to = options.to;
@@ -4029,10 +4219,10 @@ async function convertCommand(options) {
4029
4219
  `);
4030
4220
  process.exit(1);
4031
4221
  }
4032
- const claudeMdPath = path25.join(projectRoot, "CLAUDE.md");
4222
+ const claudeMdPath = path26.join(projectRoot, "CLAUDE.md");
4033
4223
  let content = "";
4034
4224
  try {
4035
- content = fs22.readFileSync(claudeMdPath, "utf-8");
4225
+ content = fs23.readFileSync(claudeMdPath, "utf-8");
4036
4226
  } catch {
4037
4227
  process.stderr.write(`Error: CLAUDE.md not found at ${claudeMdPath}
4038
4228
  `);
@@ -4040,33 +4230,45 @@ async function convertCommand(options) {
4040
4230
  }
4041
4231
  if (to === "cursor") {
4042
4232
  const files = claudeMdToCursorRules(content);
4043
- const targetDir = path25.join(projectRoot, ".cursor", "rules");
4233
+ const targetDir = path26.join(projectRoot, ".cursor", "rules");
4044
4234
  process.stdout.write(`
4045
4235
  Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
4046
-
4047
4236
  `);
4237
+ if (!options.dryRun) {
4238
+ process.stdout.write(` \u26A0 Existing .mdc files will be overwritten. A backup is saved automatically.
4239
+ `);
4240
+ process.stdout.write(` Run 'claudectx revert --list' to see backups.
4241
+ `);
4242
+ }
4243
+ process.stdout.write("\n");
4048
4244
  for (const file of files) {
4049
- const filePath = path25.join(targetDir, file.filename);
4050
- const exists = fs22.existsSync(filePath);
4245
+ const filePath = path26.join(targetDir, file.filename);
4246
+ const exists = fs23.existsSync(filePath);
4051
4247
  const prefix = options.dryRun ? "[dry-run] " : exists ? "[overwrite] " : "";
4052
4248
  process.stdout.write(` ${prefix}\u2192 .cursor/rules/${file.filename}
4053
4249
  `);
4054
4250
  if (!options.dryRun) {
4055
- fs22.mkdirSync(targetDir, { recursive: true });
4056
- fs22.writeFileSync(filePath, file.content, "utf-8");
4251
+ fs23.mkdirSync(targetDir, { recursive: true });
4252
+ if (exists) await backupFile(filePath, "convert");
4253
+ fs23.writeFileSync(filePath, file.content, "utf-8");
4057
4254
  }
4058
4255
  }
4059
4256
  process.stdout.write("\n");
4060
4257
  } else if (to === "copilot") {
4061
4258
  const converted = claudeMdToCopilot(content);
4062
- const targetPath = path25.join(projectRoot, ".github", "copilot-instructions.md");
4063
- const exists = fs22.existsSync(targetPath);
4259
+ const targetPath = path26.join(projectRoot, ".github", "copilot-instructions.md");
4260
+ const exists = fs23.existsSync(targetPath);
4064
4261
  process.stdout.write(`
4065
4262
  Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwrite]" : ""}
4066
4263
  `);
4264
+ if (!options.dryRun && exists) {
4265
+ process.stdout.write(` \u26A0 Existing file will be overwritten. Run 'claudectx revert --list' to undo.
4266
+ `);
4267
+ }
4067
4268
  if (!options.dryRun) {
4068
- fs22.mkdirSync(path25.dirname(targetPath), { recursive: true });
4069
- fs22.writeFileSync(targetPath, converted, "utf-8");
4269
+ fs23.mkdirSync(path26.dirname(targetPath), { recursive: true });
4270
+ if (exists) await backupFile(targetPath, "convert");
4271
+ fs23.writeFileSync(targetPath, converted, "utf-8");
4070
4272
  process.stdout.write(` \u2713 Written to ${targetPath}
4071
4273
 
4072
4274
  `);
@@ -4077,13 +4279,18 @@ Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwri
4077
4279
  }
4078
4280
  } else if (to === "windsurf") {
4079
4281
  const converted = claudeMdToWindsurf(content);
4080
- const targetPath = path25.join(projectRoot, ".windsurfrules");
4081
- const exists = fs22.existsSync(targetPath);
4282
+ const targetPath = path26.join(projectRoot, ".windsurfrules");
4283
+ const exists = fs23.existsSync(targetPath);
4082
4284
  process.stdout.write(`
4083
4285
  Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
4084
4286
  `);
4287
+ if (!options.dryRun && exists) {
4288
+ process.stdout.write(` \u26A0 Existing file will be overwritten. Run 'claudectx revert --list' to undo.
4289
+ `);
4290
+ }
4085
4291
  if (!options.dryRun) {
4086
- fs22.writeFileSync(targetPath, converted, "utf-8");
4292
+ if (exists) await backupFile(targetPath, "convert");
4293
+ fs23.writeFileSync(targetPath, converted, "utf-8");
4087
4294
  process.stdout.write(` \u2713 Written to ${targetPath}
4088
4295
 
4089
4296
  `);
@@ -4101,15 +4308,15 @@ Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
4101
4308
 
4102
4309
  // src/commands/teams.ts
4103
4310
  init_cjs_shims();
4104
- var path27 = __toESM(require("path"));
4105
- var fs24 = __toESM(require("fs"));
4311
+ var path28 = __toESM(require("path"));
4312
+ var fs25 = __toESM(require("fs"));
4106
4313
  init_models();
4107
4314
 
4108
4315
  // src/reporter/team-aggregator.ts
4109
4316
  init_cjs_shims();
4110
- var fs23 = __toESM(require("fs"));
4111
- var path26 = __toESM(require("path"));
4112
- var os4 = __toESM(require("os"));
4317
+ var fs24 = __toESM(require("fs"));
4318
+ var path27 = __toESM(require("path"));
4319
+ var os5 = __toESM(require("os"));
4113
4320
  var childProcess2 = __toESM(require("child_process"));
4114
4321
  init_session_reader();
4115
4322
  init_session_store();
@@ -4120,7 +4327,7 @@ function getDeveloperIdentity() {
4120
4327
  if (email) return email;
4121
4328
  } catch {
4122
4329
  }
4123
- return os4.hostname();
4330
+ return os5.hostname();
4124
4331
  }
4125
4332
  function calcCost3(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
4126
4333
  const p = MODEL_PRICING[model];
@@ -4187,7 +4394,8 @@ async function buildTeamExport(days, model, anonymize) {
4187
4394
  );
4188
4395
  const topWasteFiles = aggregateStats(fileEvents).slice(0, 10).map((s) => ({ filePath: s.filePath, readCount: s.readCount }));
4189
4396
  const totalCostUsd = calcCost3(totalInput, totalOutput, 0, totalCacheRead, model);
4190
- const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
4397
+ const totalContext = totalInput + totalCacheRead;
4398
+ const cacheHitRate = totalContext > 0 ? Math.round(totalCacheRead / totalContext * 100) : 0;
4191
4399
  const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
4192
4400
  const developer = {
4193
4401
  identity: getDeveloperIdentity(),
@@ -4237,19 +4445,19 @@ function aggregateTeamReports(exports2) {
4237
4445
  }
4238
4446
  function writeTeamExport(exportData) {
4239
4447
  const storeDir = getStoreDir();
4240
- if (!fs23.existsSync(storeDir)) fs23.mkdirSync(storeDir, { recursive: true });
4448
+ if (!fs24.existsSync(storeDir)) fs24.mkdirSync(storeDir, { recursive: true });
4241
4449
  const date = isoDate2(/* @__PURE__ */ new Date());
4242
- const filePath = path26.join(storeDir, `team-export-${date}.json`);
4243
- fs23.writeFileSync(filePath, JSON.stringify(exportData, null, 2), "utf-8");
4450
+ const filePath = path27.join(storeDir, `team-export-${date}.json`);
4451
+ fs24.writeFileSync(filePath, JSON.stringify(exportData, null, 2), "utf-8");
4244
4452
  return filePath;
4245
4453
  }
4246
4454
  function readTeamExports(dir) {
4247
4455
  const exports2 = [];
4248
- if (!fs23.existsSync(dir)) return exports2;
4249
- const files = fs23.readdirSync(dir).filter((f) => f.match(/^team-export-.*\.json$/));
4456
+ if (!fs24.existsSync(dir)) return exports2;
4457
+ const files = fs24.readdirSync(dir).filter((f) => f.match(/^team-export-.*\.json$/));
4250
4458
  for (const file of files) {
4251
4459
  try {
4252
- const raw = fs23.readFileSync(path26.join(dir, file), "utf-8");
4460
+ const raw = fs24.readFileSync(path27.join(dir, file), "utf-8");
4253
4461
  exports2.push(JSON.parse(raw));
4254
4462
  } catch {
4255
4463
  }
@@ -4336,7 +4544,7 @@ Run "claudectx teams export" on each developer machine first.
4336
4544
  process.stdout.write(" Top shared files (by read count across team):\n");
4337
4545
  for (const f of report.topWasteFiles.slice(0, 5)) {
4338
4546
  const devList = f.developers.slice(0, 3).join(", ");
4339
- process.stdout.write(` ${f.readCount}x ${path27.basename(f.filePath)} (${devList})
4547
+ process.stdout.write(` ${f.readCount}x ${path28.basename(f.filePath)} (${devList})
4340
4548
  `);
4341
4549
  }
4342
4550
  process.stdout.write("\n");
@@ -4349,24 +4557,24 @@ async function teamsShare(options) {
4349
4557
  process.exit(1);
4350
4558
  }
4351
4559
  const storeDir = getStoreDir();
4352
- const exportFiles = fs24.readdirSync(storeDir).filter((f) => f.match(/^team-export-.*\.json$/)).sort().reverse();
4560
+ const exportFiles = fs25.readdirSync(storeDir).filter((f) => f.match(/^team-export-.*\.json$/)).sort().reverse();
4353
4561
  if (exportFiles.length === 0) {
4354
4562
  process.stderr.write('No team export files found. Run "claudectx teams export" first.\n');
4355
4563
  process.exit(1);
4356
4564
  }
4357
4565
  const latest = exportFiles[0];
4358
- const src = path27.join(storeDir, latest);
4566
+ const src = path28.join(storeDir, latest);
4359
4567
  let destPath;
4360
4568
  try {
4361
- const stat = fs24.statSync(dest);
4362
- destPath = stat.isDirectory() ? path27.join(dest, latest) : dest;
4569
+ const stat = fs25.statSync(dest);
4570
+ destPath = stat.isDirectory() ? path28.join(dest, latest) : dest;
4363
4571
  } catch {
4364
4572
  destPath = dest;
4365
4573
  }
4366
- const destDir = path27.dirname(path27.resolve(destPath));
4574
+ const destDir = path28.dirname(path28.resolve(destPath));
4367
4575
  let resolvedDir;
4368
4576
  try {
4369
- resolvedDir = fs24.realpathSync(destDir);
4577
+ resolvedDir = fs25.realpathSync(destDir);
4370
4578
  } catch {
4371
4579
  resolvedDir = destDir;
4372
4580
  }
@@ -4376,7 +4584,7 @@ async function teamsShare(options) {
4376
4584
  `);
4377
4585
  process.exit(1);
4378
4586
  }
4379
- fs24.copyFileSync(src, destPath);
4587
+ fs25.copyFileSync(src, destPath);
4380
4588
  process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
4381
4589
 
4382
4590
  `);
@@ -4401,8 +4609,148 @@ async function teamsCommand(subcommand, options) {
4401
4609
  }
4402
4610
  }
4403
4611
 
4612
+ // src/commands/revert.ts
4613
+ init_cjs_shims();
4614
+ var path29 = __toESM(require("path"));
4615
+ function timeAgo(isoString) {
4616
+ const diffMs = Date.now() - new Date(isoString).getTime();
4617
+ const minutes = Math.floor(diffMs / 6e4);
4618
+ if (minutes < 1) return "just now";
4619
+ if (minutes < 60) return `${minutes} min ago`;
4620
+ const hours = Math.floor(minutes / 60);
4621
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
4622
+ const days = Math.floor(hours / 24);
4623
+ return `${days} day${days === 1 ? "" : "s"} ago`;
4624
+ }
4625
+ function formatBytes(bytes) {
4626
+ if (bytes < 1024) return `${bytes} B`;
4627
+ return `${Math.round(bytes / 1024)} KB`;
4628
+ }
4629
+ function printBackupTable(entries) {
4630
+ if (entries.length === 0) {
4631
+ process.stdout.write(" No backups found.\n");
4632
+ process.stdout.write(` Backups are created automatically when claudectx modifies your files.
4633
+ `);
4634
+ process.stdout.write(` Backup directory: ${BACKUP_DIR}
4635
+
4636
+ `);
4637
+ return;
4638
+ }
4639
+ const idWidth = 26;
4640
+ const fileWidth = 20;
4641
+ const cmdWidth = 10;
4642
+ const timeWidth = 14;
4643
+ const sizeWidth = 7;
4644
+ const hr = "\u2550".repeat(idWidth + fileWidth + cmdWidth + timeWidth + sizeWidth + 16);
4645
+ process.stdout.write("\n");
4646
+ process.stdout.write("claudectx \u2014 Backup History\n");
4647
+ process.stdout.write(hr + "\n");
4648
+ process.stdout.write(
4649
+ ` ${"ID".padEnd(idWidth)} ${"File".padEnd(fileWidth)} ${"Command".padEnd(cmdWidth)} ${"When".padEnd(timeWidth)} ${"Size".padEnd(sizeWidth)}
4650
+ `
4651
+ );
4652
+ process.stdout.write("\u2500".repeat(idWidth + fileWidth + cmdWidth + timeWidth + sizeWidth + 16) + "\n");
4653
+ for (const entry of entries) {
4654
+ const id = entry.id.slice(0, idWidth).padEnd(idWidth);
4655
+ const file = path29.basename(entry.originalPath).slice(0, fileWidth).padEnd(fileWidth);
4656
+ const cmd = entry.command.slice(0, cmdWidth).padEnd(cmdWidth);
4657
+ const when = timeAgo(entry.createdAt).slice(0, timeWidth).padEnd(timeWidth);
4658
+ const size = formatBytes(entry.sizeBytes).padEnd(sizeWidth);
4659
+ process.stdout.write(` ${id} ${file} ${cmd} ${when} ${size}
4660
+ `);
4661
+ }
4662
+ process.stdout.write("\n");
4663
+ process.stdout.write(` Backup directory: ${BACKUP_DIR}
4664
+ `);
4665
+ process.stdout.write(" To restore: claudectx revert --id <ID>\n\n");
4666
+ }
4667
+ async function interactivePick(entries) {
4668
+ try {
4669
+ const { select } = await import("@inquirer/prompts");
4670
+ const choices = entries.map((e) => ({
4671
+ name: `${timeAgo(e.createdAt).padEnd(14)} ${path29.basename(e.originalPath).padEnd(16)} [${e.command}] ${e.id}`,
4672
+ value: e.id
4673
+ }));
4674
+ choices.push({ name: "Cancel", value: "" });
4675
+ return await select({ message: "Choose a backup to restore:", choices });
4676
+ } catch {
4677
+ process.stderr.write("Interactive mode unavailable. Use --id <id> to restore a specific backup.\n");
4678
+ return null;
4679
+ }
4680
+ }
4681
+ async function doRestore(id) {
4682
+ const chalk5 = (await import("chalk")).default;
4683
+ process.stdout.write("\n");
4684
+ try {
4685
+ const entries = await listBackups();
4686
+ const entry = entries.find((e) => e.id === id);
4687
+ if (!entry) {
4688
+ process.stderr.write(chalk5.red(`Backup "${id}" not found.
4689
+ `));
4690
+ process.stderr.write('Run "claudectx revert --list" to see available backups.\n');
4691
+ process.exitCode = 1;
4692
+ return;
4693
+ }
4694
+ process.stdout.write(chalk5.yellow(`\u26A0 This will overwrite: ${entry.originalPath}
4695
+ `));
4696
+ process.stdout.write(` Backup from: ${timeAgo(entry.createdAt)} (${entry.command})
4697
+ `);
4698
+ process.stdout.write(` Your current file will be backed up first (so you can undo this).
4699
+
4700
+ `);
4701
+ let confirmed = true;
4702
+ try {
4703
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
4704
+ confirmed = await confirm2({ message: "Restore this backup?", default: false });
4705
+ } catch {
4706
+ }
4707
+ if (!confirmed) {
4708
+ process.stdout.write(" Cancelled.\n\n");
4709
+ return;
4710
+ }
4711
+ const { undoEntry } = await restoreBackup(id);
4712
+ process.stdout.write(chalk5.green(" \u2713 ") + `Restored to ${entry.originalPath}
4713
+ `);
4714
+ if (undoEntry) {
4715
+ process.stdout.write(
4716
+ chalk5.dim(` Your previous version was saved as backup "${undoEntry.id}" \u2014 run 'claudectx revert --id ${undoEntry.id}' to undo.
4717
+ `)
4718
+ );
4719
+ }
4720
+ process.stdout.write("\n");
4721
+ } catch (err) {
4722
+ process.stderr.write(chalk5.red(`Error: ${err instanceof Error ? err.message : String(err)}
4723
+ `));
4724
+ process.exitCode = 1;
4725
+ }
4726
+ }
4727
+ async function revertCommand(options) {
4728
+ const entries = await listBackups(options.file);
4729
+ if (options.json) {
4730
+ process.stdout.write(JSON.stringify({ backups: entries }, null, 2) + "\n");
4731
+ return;
4732
+ }
4733
+ if (options.list) {
4734
+ printBackupTable(entries);
4735
+ return;
4736
+ }
4737
+ if (options.id) {
4738
+ await doRestore(options.id);
4739
+ return;
4740
+ }
4741
+ if (entries.length === 0) {
4742
+ process.stdout.write("\n No backups found. Backups are created automatically when claudectx modifies your files.\n\n");
4743
+ return;
4744
+ }
4745
+ printBackupTable(entries);
4746
+ const picked = await interactivePick(entries);
4747
+ if (picked) {
4748
+ await doRestore(picked);
4749
+ }
4750
+ }
4751
+
4404
4752
  // src/index.ts
4405
- var VERSION = "1.1.2";
4753
+ var VERSION = "1.1.4";
4406
4754
  var DESCRIPTION = "Reduce Claude Code token usage by up to 80%. Context analyzer, auto-optimizer, live dashboard, and smart MCP tools.";
4407
4755
  var program = new import_commander.Command();
4408
4756
  program.name("claudectx").description(DESCRIPTION).version(VERSION);
@@ -4442,5 +4790,8 @@ program.command("convert").description("Convert CLAUDE.md to another AI assistan
4442
4790
  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) => {
4443
4791
  await teamsCommand(subcommand ?? "export", options);
4444
4792
  });
4793
+ program.command("revert").description("List and restore backups created automatically by claudectx commands").option("--list", "Show all backups").option("--id <id>", "Restore a specific backup by ID").option("--file <path>", "Filter backups by original file path").option("--json", "JSON output").action(async (options) => {
4794
+ await revertCommand(options);
4795
+ });
4445
4796
  program.parse();
4446
4797
  //# sourceMappingURL=index.js.map