claudectx 1.1.2 → 1.1.3

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
  }
@@ -3034,12 +3179,12 @@ async function reportCommand(options) {
3034
3179
 
3035
3180
  // src/commands/budget.ts
3036
3181
  init_cjs_shims();
3037
- var path20 = __toESM(require("path"));
3182
+ var path21 = __toESM(require("path"));
3038
3183
 
3039
3184
  // src/analyzer/budget-estimator.ts
3040
3185
  init_cjs_shims();
3041
- var fs17 = __toESM(require("fs"));
3042
- var path19 = __toESM(require("path"));
3186
+ var fs18 = __toESM(require("fs"));
3187
+ var path20 = __toESM(require("path"));
3043
3188
  var import_glob2 = require("glob");
3044
3189
  init_tokenizer();
3045
3190
  init_session_store();
@@ -3065,20 +3210,20 @@ function classifyCacheHit(recentReadCount) {
3065
3210
  return "low";
3066
3211
  }
3067
3212
  function suggestClaudeignoreAdditions(files, projectRoot) {
3068
- const ignorePath = path19.join(projectRoot, ".claudeignore");
3213
+ const ignorePath = path20.join(projectRoot, ".claudeignore");
3069
3214
  let ignorePatterns = [];
3070
3215
  try {
3071
- const content = fs17.readFileSync(ignorePath, "utf-8");
3216
+ const content = fs18.readFileSync(ignorePath, "utf-8");
3072
3217
  ignorePatterns = content.split("\n").filter(Boolean);
3073
3218
  } catch {
3074
3219
  }
3075
3220
  const recommendations = [];
3076
3221
  for (const file of files) {
3077
3222
  if (file.tokenCount <= WASTE_THRESHOLDS.MAX_REFERENCE_FILE_TOKENS) continue;
3078
- const rel = path19.relative(projectRoot, file.filePath);
3223
+ const rel = path20.relative(projectRoot, file.filePath);
3079
3224
  const alreadyIgnored = ignorePatterns.some((pattern) => {
3080
3225
  const cleanPattern = pattern.replace(/^!/, "");
3081
- return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g, path19.sep));
3226
+ return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g, path20.sep));
3082
3227
  });
3083
3228
  if (!alreadyIgnored) {
3084
3229
  recommendations.push(rel);
@@ -3098,7 +3243,7 @@ async function estimateBudget(globs, projectRoot, model, thresholdTokens) {
3098
3243
  for (const filePath of filePaths) {
3099
3244
  let content = "";
3100
3245
  try {
3101
- content = fs17.readFileSync(filePath, "utf-8");
3246
+ content = fs18.readFileSync(filePath, "utf-8");
3102
3247
  } catch {
3103
3248
  continue;
3104
3249
  }
@@ -3147,7 +3292,7 @@ function formatBudgetReport(report) {
3147
3292
  }
3148
3293
  const LIKELIHOOD_ICON = { high: "\u{1F7E2}", medium: "\u{1F7E1}", low: "\u{1F534}" };
3149
3294
  const maxPathLen = Math.min(
3150
- Math.max(...report.files.map((f) => path19.basename(f.filePath).length)),
3295
+ Math.max(...report.files.map((f) => path20.basename(f.filePath).length)),
3151
3296
  40
3152
3297
  );
3153
3298
  lines.push(
@@ -3155,7 +3300,7 @@ function formatBudgetReport(report) {
3155
3300
  );
3156
3301
  lines.push("\u2500".repeat(50));
3157
3302
  for (const file of report.files.slice(0, 20)) {
3158
- const name = path19.basename(file.filePath).slice(0, maxPathLen).padEnd(maxPathLen);
3303
+ const name = path20.basename(file.filePath).slice(0, maxPathLen).padEnd(maxPathLen);
3159
3304
  const tokens = file.tokenCount.toLocaleString().padStart(7);
3160
3305
  const cache = `${LIKELIHOOD_ICON[file.cacheHitLikelihood]} ${file.cacheHitLikelihood.padEnd(6)}`;
3161
3306
  const cost = formatCost(file.estimatedCostUsd).padStart(7);
@@ -3184,7 +3329,7 @@ function formatBudgetReport(report) {
3184
3329
  // src/commands/budget.ts
3185
3330
  init_models();
3186
3331
  async function budgetCommand(globs, options) {
3187
- const projectPath = options.path ? path20.resolve(options.path) : process.cwd();
3332
+ const projectPath = options.path ? path21.resolve(options.path) : process.cwd();
3188
3333
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3189
3334
  const model = resolveModel(options.model ?? "sonnet");
3190
3335
  const thresholdTokens = parseInt(options.threshold ?? "10000", 10);
@@ -3203,7 +3348,7 @@ async function budgetCommand(globs, options) {
3203
3348
 
3204
3349
  // src/commands/warmup.ts
3205
3350
  init_cjs_shims();
3206
- var path21 = __toESM(require("path"));
3351
+ var path22 = __toESM(require("path"));
3207
3352
  var import_sdk = __toESM(require("@anthropic-ai/sdk"));
3208
3353
  init_models();
3209
3354
  var import_fs3 = __toESM(require("fs"));
@@ -3277,6 +3422,19 @@ async function installCron(cronExpr) {
3277
3422
  );
3278
3423
  process.exit(1);
3279
3424
  }
3425
+ let confirmed = true;
3426
+ try {
3427
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
3428
+ confirmed = await confirm2({
3429
+ message: `Install cron job "${cronExpr} claudectx warmup" in your system crontab?`,
3430
+ default: false
3431
+ });
3432
+ } catch {
3433
+ }
3434
+ if (!confirmed) {
3435
+ process.stdout.write(" Cron install cancelled.\n");
3436
+ return;
3437
+ }
3280
3438
  const { execSync: execSync3 } = await import("child_process");
3281
3439
  const command = `claudectx warmup`;
3282
3440
  const cronLine = `${cronExpr} ${command}`;
@@ -3299,16 +3457,16 @@ async function installCron(cronExpr) {
3299
3457
  ${marker}
3300
3458
  ${cronLine}
3301
3459
  `;
3302
- const { writeFileSync: writeFileSync11, unlinkSync: unlinkSync2 } = await import("fs");
3460
+ const { writeFileSync: writeFileSync12, unlinkSync: unlinkSync3 } = await import("fs");
3303
3461
  const { tmpdir } = await import("os");
3304
- const { join: join17 } = await import("path");
3305
- const tmpFile = join17(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
3462
+ const { join: join18 } = await import("path");
3463
+ const tmpFile = join18(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
3306
3464
  try {
3307
- writeFileSync11(tmpFile, newCrontab, "utf-8");
3465
+ writeFileSync12(tmpFile, newCrontab, "utf-8");
3308
3466
  execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
3309
3467
  } finally {
3310
3468
  try {
3311
- unlinkSync2(tmpFile);
3469
+ unlinkSync3(tmpFile);
3312
3470
  } catch {
3313
3471
  }
3314
3472
  }
@@ -3325,7 +3483,7 @@ ${cronLine}
3325
3483
  }
3326
3484
  }
3327
3485
  async function warmupCommand(options) {
3328
- const projectPath = options.path ? path21.resolve(options.path) : process.cwd();
3486
+ const projectPath = options.path ? path22.resolve(options.path) : process.cwd();
3329
3487
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3330
3488
  const model = resolveModel(options.model ?? "haiku");
3331
3489
  const ttl = options.ttl === "60" ? 60 : 5;
@@ -3337,7 +3495,7 @@ async function warmupCommand(options) {
3337
3495
  process.exit(1);
3338
3496
  }
3339
3497
  let claudeMdContent = "";
3340
- const claudeMdPath = path21.join(projectRoot, "CLAUDE.md");
3498
+ const claudeMdPath = path22.join(projectRoot, "CLAUDE.md");
3341
3499
  try {
3342
3500
  claudeMdContent = import_fs3.default.readFileSync(claudeMdPath, "utf-8");
3343
3501
  } catch {
@@ -3397,13 +3555,13 @@ async function warmupCommand(options) {
3397
3555
 
3398
3556
  // src/commands/drift.ts
3399
3557
  init_cjs_shims();
3400
- var path23 = __toESM(require("path"));
3401
- var fs20 = __toESM(require("fs"));
3558
+ var path24 = __toESM(require("path"));
3559
+ var fs21 = __toESM(require("fs"));
3402
3560
 
3403
3561
  // src/analyzer/drift-detector.ts
3404
3562
  init_cjs_shims();
3405
- var fs19 = __toESM(require("fs"));
3406
- var path22 = __toESM(require("path"));
3563
+ var fs20 = __toESM(require("fs"));
3564
+ var path23 = __toESM(require("path"));
3407
3565
  var childProcess = __toESM(require("child_process"));
3408
3566
  init_tokenizer();
3409
3567
  init_session_store();
@@ -3416,8 +3574,8 @@ function findDeadAtReferences(content, projectRoot) {
3416
3574
  const match = lines[i].match(AT_REF_RE);
3417
3575
  if (!match) continue;
3418
3576
  const ref = match[1].trim();
3419
- const absPath = path22.isAbsolute(ref) ? ref : path22.join(projectRoot, ref);
3420
- if (!fs19.existsSync(absPath)) {
3577
+ const absPath = path23.isAbsolute(ref) ? ref : path23.join(projectRoot, ref);
3578
+ if (!fs20.existsSync(absPath)) {
3421
3579
  const lineText = lines[i];
3422
3580
  issues.push({
3423
3581
  type: "dead-ref",
@@ -3450,15 +3608,15 @@ async function findGitDeletedMentions(content, projectRoot) {
3450
3608
  for (let i = 0; i < lines.length; i++) {
3451
3609
  const line = lines[i];
3452
3610
  for (const deleted of deletedFiles) {
3453
- const basename8 = path22.basename(deleted);
3454
- if (line.includes(basename8) || line.includes(deleted)) {
3611
+ const basename10 = path23.basename(deleted);
3612
+ if (line.includes(basename10) || line.includes(deleted)) {
3455
3613
  issues.push({
3456
3614
  type: "git-deleted",
3457
3615
  line: i + 1,
3458
3616
  text: line.trim(),
3459
3617
  severity: "warning",
3460
3618
  estimatedTokenWaste: countTokens(line),
3461
- suggestion: `References "${basename8}" which was deleted from git. Consider removing this mention.`
3619
+ suggestion: `References "${basename10}" which was deleted from git. Consider removing this mention.`
3462
3620
  });
3463
3621
  break;
3464
3622
  }
@@ -3523,8 +3681,8 @@ function findDeadInlinePaths(content, projectRoot) {
3523
3681
  const rawPath = match[1].trim();
3524
3682
  if (seen.has(rawPath)) continue;
3525
3683
  seen.add(rawPath);
3526
- const absPath = path22.isAbsolute(rawPath) ? rawPath : path22.join(projectRoot, rawPath);
3527
- if (!fs19.existsSync(absPath)) {
3684
+ const absPath = path23.isAbsolute(rawPath) ? rawPath : path23.join(projectRoot, rawPath);
3685
+ if (!fs20.existsSync(absPath)) {
3528
3686
  issues.push({
3529
3687
  type: "dead-inline-path",
3530
3688
  line: i + 1,
@@ -3540,10 +3698,10 @@ function findDeadInlinePaths(content, projectRoot) {
3540
3698
  return issues;
3541
3699
  }
3542
3700
  async function detectDrift(projectRoot, dayWindow) {
3543
- const claudeMdPath = path22.join(projectRoot, "CLAUDE.md");
3701
+ const claudeMdPath = path23.join(projectRoot, "CLAUDE.md");
3544
3702
  let content = "";
3545
3703
  try {
3546
- content = fs19.readFileSync(claudeMdPath, "utf-8");
3704
+ content = fs20.readFileSync(claudeMdPath, "utf-8");
3547
3705
  } catch {
3548
3706
  return {
3549
3707
  claudeMdPath,
@@ -3585,7 +3743,7 @@ var TYPE_LABEL = {
3585
3743
  "dead-inline-path": "Dead path"
3586
3744
  };
3587
3745
  async function driftCommand(options) {
3588
- const projectPath = options.path ? path23.resolve(options.path) : process.cwd();
3746
+ const projectPath = options.path ? path24.resolve(options.path) : process.cwd();
3589
3747
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3590
3748
  const dayWindow = parseInt(options.days ?? "30", 10);
3591
3749
  const report = await detectDrift(projectRoot, dayWindow);
@@ -3661,25 +3819,25 @@ async function applyFix(claudeMdPath, issues) {
3661
3819
  process.stdout.write("No lines selected. Nothing changed.\n");
3662
3820
  return;
3663
3821
  }
3664
- const content = fs20.readFileSync(claudeMdPath, "utf-8");
3822
+ const content = fs21.readFileSync(claudeMdPath, "utf-8");
3665
3823
  const lines = content.split("\n");
3666
3824
  const lineSet = new Set(selectedLines.map((l) => l - 1));
3667
3825
  const newLines = lines.filter((_, i) => !lineSet.has(i));
3668
3826
  const newContent = newLines.join("\n");
3669
3827
  const backupPath = `${claudeMdPath}.bak`;
3670
- fs20.writeFileSync(backupPath, content, "utf-8");
3671
- const os5 = await import("os");
3828
+ fs21.writeFileSync(backupPath, content, "utf-8");
3829
+ const os6 = await import("os");
3672
3830
  const tmpPath = `${claudeMdPath}.tmp-${Date.now()}`;
3673
3831
  try {
3674
- fs20.writeFileSync(tmpPath, newContent, "utf-8");
3675
- fs20.renameSync(tmpPath, claudeMdPath);
3832
+ fs21.writeFileSync(tmpPath, newContent, "utf-8");
3833
+ fs21.renameSync(tmpPath, claudeMdPath);
3676
3834
  } catch (err) {
3677
3835
  try {
3678
- fs20.copyFileSync(backupPath, claudeMdPath);
3836
+ fs21.copyFileSync(backupPath, claudeMdPath);
3679
3837
  } catch {
3680
3838
  }
3681
3839
  try {
3682
- fs20.unlinkSync(tmpPath);
3840
+ fs21.unlinkSync(tmpPath);
3683
3841
  } catch {
3684
3842
  }
3685
3843
  process.stderr.write(`Error writing CLAUDE.md: ${err instanceof Error ? err.message : String(err)}
@@ -3687,18 +3845,18 @@ async function applyFix(claudeMdPath, issues) {
3687
3845
  process.exit(1);
3688
3846
  }
3689
3847
  process.stdout.write(`
3690
- \u2713 Removed ${selectedLines.length} line(s) from ${path23.basename(claudeMdPath)}
3848
+ \u2713 Removed ${selectedLines.length} line(s) from ${path24.basename(claudeMdPath)}
3691
3849
  `);
3692
3850
  process.stdout.write(` \u2713 Backup saved to ${backupPath}
3693
3851
 
3694
3852
  `);
3695
- void os5;
3853
+ void os6;
3696
3854
  }
3697
3855
 
3698
3856
  // src/commands/hooks.ts
3699
3857
  init_cjs_shims();
3700
- var fs21 = __toESM(require("fs"));
3701
- var path24 = __toESM(require("path"));
3858
+ var fs22 = __toESM(require("fs"));
3859
+ var path25 = __toESM(require("path"));
3702
3860
 
3703
3861
  // src/hooks/registry.ts
3704
3862
  init_cjs_shims();
@@ -3774,17 +3932,17 @@ function buildHookEntry(def, config) {
3774
3932
 
3775
3933
  // src/commands/hooks.ts
3776
3934
  function readInstalledHooks(projectRoot) {
3777
- const settingsPath = path24.join(projectRoot, ".claude", "settings.local.json");
3778
- if (!fs21.existsSync(settingsPath)) return {};
3935
+ const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
3936
+ if (!fs22.existsSync(settingsPath)) return {};
3779
3937
  try {
3780
- return JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
3938
+ return JSON.parse(fs22.readFileSync(settingsPath, "utf-8"));
3781
3939
  } catch {
3782
3940
  process.stderr.write(
3783
3941
  `Warning: ${settingsPath} exists but contains invalid JSON. Existing settings will be preserved as a backup.
3784
3942
  `
3785
3943
  );
3786
3944
  try {
3787
- fs21.copyFileSync(settingsPath, `${settingsPath}.bak`);
3945
+ fs22.copyFileSync(settingsPath, `${settingsPath}.bak`);
3788
3946
  } catch {
3789
3947
  }
3790
3948
  return {};
@@ -3904,9 +4062,26 @@ async function hooksAdd(name, projectRoot, configPairs) {
3904
4062
  }
3905
4063
  async function hooksRemove(name, projectRoot) {
3906
4064
  const settings = readInstalledHooks(projectRoot);
4065
+ let confirmed = true;
4066
+ try {
4067
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
4068
+ confirmed = await confirm2({
4069
+ message: `Remove hook "${name}"? Run 'claudectx revert' to undo.`,
4070
+ default: false
4071
+ });
4072
+ } catch {
4073
+ }
4074
+ if (!confirmed) {
4075
+ process.stdout.write(" Cancelled.\n\n");
4076
+ return;
4077
+ }
4078
+ const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
4079
+ if (fs22.existsSync(settingsPath)) {
4080
+ await backupFile(settingsPath, "hooks");
4081
+ }
3907
4082
  const updated = removeHookByName(settings, name);
3908
4083
  writeHooksSettings(projectRoot, updated);
3909
- process.stdout.write(` \u2713 Hook "${name}" removed.
4084
+ process.stdout.write(` \u2713 Hook "${name}" removed. Run 'claudectx revert --list' to undo.
3910
4085
 
3911
4086
  `);
3912
4087
  }
@@ -3931,7 +4106,7 @@ async function hooksStatus(projectRoot) {
3931
4106
  process.stdout.write("\n");
3932
4107
  }
3933
4108
  async function hooksCommand(subcommand, options) {
3934
- const projectPath = options.path ? path24.resolve(options.path) : process.cwd();
4109
+ const projectPath = options.path ? path25.resolve(options.path) : process.cwd();
3935
4110
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3936
4111
  const sub = subcommand ?? "list";
3937
4112
  switch (sub) {
@@ -3970,8 +4145,8 @@ async function hooksCommand(subcommand, options) {
3970
4145
 
3971
4146
  // src/commands/convert.ts
3972
4147
  init_cjs_shims();
3973
- var fs22 = __toESM(require("fs"));
3974
- var path25 = __toESM(require("path"));
4148
+ var fs23 = __toESM(require("fs"));
4149
+ var path26 = __toESM(require("path"));
3975
4150
  function slugify2(text) {
3976
4151
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3977
4152
  }
@@ -4020,7 +4195,7 @@ function claudeMdToWindsurf(content) {
4020
4195
  return content.split("\n").filter((line) => !line.match(/^@.+$/)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
4021
4196
  }
4022
4197
  async function convertCommand(options) {
4023
- const projectPath = options.path ? path25.resolve(options.path) : process.cwd();
4198
+ const projectPath = options.path ? path26.resolve(options.path) : process.cwd();
4024
4199
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
4025
4200
  const from = options.from ?? "claude";
4026
4201
  const to = options.to;
@@ -4029,10 +4204,10 @@ async function convertCommand(options) {
4029
4204
  `);
4030
4205
  process.exit(1);
4031
4206
  }
4032
- const claudeMdPath = path25.join(projectRoot, "CLAUDE.md");
4207
+ const claudeMdPath = path26.join(projectRoot, "CLAUDE.md");
4033
4208
  let content = "";
4034
4209
  try {
4035
- content = fs22.readFileSync(claudeMdPath, "utf-8");
4210
+ content = fs23.readFileSync(claudeMdPath, "utf-8");
4036
4211
  } catch {
4037
4212
  process.stderr.write(`Error: CLAUDE.md not found at ${claudeMdPath}
4038
4213
  `);
@@ -4040,33 +4215,45 @@ async function convertCommand(options) {
4040
4215
  }
4041
4216
  if (to === "cursor") {
4042
4217
  const files = claudeMdToCursorRules(content);
4043
- const targetDir = path25.join(projectRoot, ".cursor", "rules");
4218
+ const targetDir = path26.join(projectRoot, ".cursor", "rules");
4044
4219
  process.stdout.write(`
4045
4220
  Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
4046
-
4047
4221
  `);
4222
+ if (!options.dryRun) {
4223
+ process.stdout.write(` \u26A0 Existing .mdc files will be overwritten. A backup is saved automatically.
4224
+ `);
4225
+ process.stdout.write(` Run 'claudectx revert --list' to see backups.
4226
+ `);
4227
+ }
4228
+ process.stdout.write("\n");
4048
4229
  for (const file of files) {
4049
- const filePath = path25.join(targetDir, file.filename);
4050
- const exists = fs22.existsSync(filePath);
4230
+ const filePath = path26.join(targetDir, file.filename);
4231
+ const exists = fs23.existsSync(filePath);
4051
4232
  const prefix = options.dryRun ? "[dry-run] " : exists ? "[overwrite] " : "";
4052
4233
  process.stdout.write(` ${prefix}\u2192 .cursor/rules/${file.filename}
4053
4234
  `);
4054
4235
  if (!options.dryRun) {
4055
- fs22.mkdirSync(targetDir, { recursive: true });
4056
- fs22.writeFileSync(filePath, file.content, "utf-8");
4236
+ fs23.mkdirSync(targetDir, { recursive: true });
4237
+ if (exists) await backupFile(filePath, "convert");
4238
+ fs23.writeFileSync(filePath, file.content, "utf-8");
4057
4239
  }
4058
4240
  }
4059
4241
  process.stdout.write("\n");
4060
4242
  } else if (to === "copilot") {
4061
4243
  const converted = claudeMdToCopilot(content);
4062
- const targetPath = path25.join(projectRoot, ".github", "copilot-instructions.md");
4063
- const exists = fs22.existsSync(targetPath);
4244
+ const targetPath = path26.join(projectRoot, ".github", "copilot-instructions.md");
4245
+ const exists = fs23.existsSync(targetPath);
4064
4246
  process.stdout.write(`
4065
4247
  Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwrite]" : ""}
4066
4248
  `);
4249
+ if (!options.dryRun && exists) {
4250
+ process.stdout.write(` \u26A0 Existing file will be overwritten. Run 'claudectx revert --list' to undo.
4251
+ `);
4252
+ }
4067
4253
  if (!options.dryRun) {
4068
- fs22.mkdirSync(path25.dirname(targetPath), { recursive: true });
4069
- fs22.writeFileSync(targetPath, converted, "utf-8");
4254
+ fs23.mkdirSync(path26.dirname(targetPath), { recursive: true });
4255
+ if (exists) await backupFile(targetPath, "convert");
4256
+ fs23.writeFileSync(targetPath, converted, "utf-8");
4070
4257
  process.stdout.write(` \u2713 Written to ${targetPath}
4071
4258
 
4072
4259
  `);
@@ -4077,13 +4264,18 @@ Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwri
4077
4264
  }
4078
4265
  } else if (to === "windsurf") {
4079
4266
  const converted = claudeMdToWindsurf(content);
4080
- const targetPath = path25.join(projectRoot, ".windsurfrules");
4081
- const exists = fs22.existsSync(targetPath);
4267
+ const targetPath = path26.join(projectRoot, ".windsurfrules");
4268
+ const exists = fs23.existsSync(targetPath);
4082
4269
  process.stdout.write(`
4083
4270
  Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
4084
4271
  `);
4272
+ if (!options.dryRun && exists) {
4273
+ process.stdout.write(` \u26A0 Existing file will be overwritten. Run 'claudectx revert --list' to undo.
4274
+ `);
4275
+ }
4085
4276
  if (!options.dryRun) {
4086
- fs22.writeFileSync(targetPath, converted, "utf-8");
4277
+ if (exists) await backupFile(targetPath, "convert");
4278
+ fs23.writeFileSync(targetPath, converted, "utf-8");
4087
4279
  process.stdout.write(` \u2713 Written to ${targetPath}
4088
4280
 
4089
4281
  `);
@@ -4101,15 +4293,15 @@ Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
4101
4293
 
4102
4294
  // src/commands/teams.ts
4103
4295
  init_cjs_shims();
4104
- var path27 = __toESM(require("path"));
4105
- var fs24 = __toESM(require("fs"));
4296
+ var path28 = __toESM(require("path"));
4297
+ var fs25 = __toESM(require("fs"));
4106
4298
  init_models();
4107
4299
 
4108
4300
  // src/reporter/team-aggregator.ts
4109
4301
  init_cjs_shims();
4110
- var fs23 = __toESM(require("fs"));
4111
- var path26 = __toESM(require("path"));
4112
- var os4 = __toESM(require("os"));
4302
+ var fs24 = __toESM(require("fs"));
4303
+ var path27 = __toESM(require("path"));
4304
+ var os5 = __toESM(require("os"));
4113
4305
  var childProcess2 = __toESM(require("child_process"));
4114
4306
  init_session_reader();
4115
4307
  init_session_store();
@@ -4120,7 +4312,7 @@ function getDeveloperIdentity() {
4120
4312
  if (email) return email;
4121
4313
  } catch {
4122
4314
  }
4123
- return os4.hostname();
4315
+ return os5.hostname();
4124
4316
  }
4125
4317
  function calcCost3(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
4126
4318
  const p = MODEL_PRICING[model];
@@ -4237,19 +4429,19 @@ function aggregateTeamReports(exports2) {
4237
4429
  }
4238
4430
  function writeTeamExport(exportData) {
4239
4431
  const storeDir = getStoreDir();
4240
- if (!fs23.existsSync(storeDir)) fs23.mkdirSync(storeDir, { recursive: true });
4432
+ if (!fs24.existsSync(storeDir)) fs24.mkdirSync(storeDir, { recursive: true });
4241
4433
  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");
4434
+ const filePath = path27.join(storeDir, `team-export-${date}.json`);
4435
+ fs24.writeFileSync(filePath, JSON.stringify(exportData, null, 2), "utf-8");
4244
4436
  return filePath;
4245
4437
  }
4246
4438
  function readTeamExports(dir) {
4247
4439
  const exports2 = [];
4248
- if (!fs23.existsSync(dir)) return exports2;
4249
- const files = fs23.readdirSync(dir).filter((f) => f.match(/^team-export-.*\.json$/));
4440
+ if (!fs24.existsSync(dir)) return exports2;
4441
+ const files = fs24.readdirSync(dir).filter((f) => f.match(/^team-export-.*\.json$/));
4250
4442
  for (const file of files) {
4251
4443
  try {
4252
- const raw = fs23.readFileSync(path26.join(dir, file), "utf-8");
4444
+ const raw = fs24.readFileSync(path27.join(dir, file), "utf-8");
4253
4445
  exports2.push(JSON.parse(raw));
4254
4446
  } catch {
4255
4447
  }
@@ -4336,7 +4528,7 @@ Run "claudectx teams export" on each developer machine first.
4336
4528
  process.stdout.write(" Top shared files (by read count across team):\n");
4337
4529
  for (const f of report.topWasteFiles.slice(0, 5)) {
4338
4530
  const devList = f.developers.slice(0, 3).join(", ");
4339
- process.stdout.write(` ${f.readCount}x ${path27.basename(f.filePath)} (${devList})
4531
+ process.stdout.write(` ${f.readCount}x ${path28.basename(f.filePath)} (${devList})
4340
4532
  `);
4341
4533
  }
4342
4534
  process.stdout.write("\n");
@@ -4349,24 +4541,24 @@ async function teamsShare(options) {
4349
4541
  process.exit(1);
4350
4542
  }
4351
4543
  const storeDir = getStoreDir();
4352
- const exportFiles = fs24.readdirSync(storeDir).filter((f) => f.match(/^team-export-.*\.json$/)).sort().reverse();
4544
+ const exportFiles = fs25.readdirSync(storeDir).filter((f) => f.match(/^team-export-.*\.json$/)).sort().reverse();
4353
4545
  if (exportFiles.length === 0) {
4354
4546
  process.stderr.write('No team export files found. Run "claudectx teams export" first.\n');
4355
4547
  process.exit(1);
4356
4548
  }
4357
4549
  const latest = exportFiles[0];
4358
- const src = path27.join(storeDir, latest);
4550
+ const src = path28.join(storeDir, latest);
4359
4551
  let destPath;
4360
4552
  try {
4361
- const stat = fs24.statSync(dest);
4362
- destPath = stat.isDirectory() ? path27.join(dest, latest) : dest;
4553
+ const stat = fs25.statSync(dest);
4554
+ destPath = stat.isDirectory() ? path28.join(dest, latest) : dest;
4363
4555
  } catch {
4364
4556
  destPath = dest;
4365
4557
  }
4366
- const destDir = path27.dirname(path27.resolve(destPath));
4558
+ const destDir = path28.dirname(path28.resolve(destPath));
4367
4559
  let resolvedDir;
4368
4560
  try {
4369
- resolvedDir = fs24.realpathSync(destDir);
4561
+ resolvedDir = fs25.realpathSync(destDir);
4370
4562
  } catch {
4371
4563
  resolvedDir = destDir;
4372
4564
  }
@@ -4376,7 +4568,7 @@ async function teamsShare(options) {
4376
4568
  `);
4377
4569
  process.exit(1);
4378
4570
  }
4379
- fs24.copyFileSync(src, destPath);
4571
+ fs25.copyFileSync(src, destPath);
4380
4572
  process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
4381
4573
 
4382
4574
  `);
@@ -4401,8 +4593,148 @@ async function teamsCommand(subcommand, options) {
4401
4593
  }
4402
4594
  }
4403
4595
 
4596
+ // src/commands/revert.ts
4597
+ init_cjs_shims();
4598
+ var path29 = __toESM(require("path"));
4599
+ function timeAgo(isoString) {
4600
+ const diffMs = Date.now() - new Date(isoString).getTime();
4601
+ const minutes = Math.floor(diffMs / 6e4);
4602
+ if (minutes < 1) return "just now";
4603
+ if (minutes < 60) return `${minutes} min ago`;
4604
+ const hours = Math.floor(minutes / 60);
4605
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
4606
+ const days = Math.floor(hours / 24);
4607
+ return `${days} day${days === 1 ? "" : "s"} ago`;
4608
+ }
4609
+ function formatBytes(bytes) {
4610
+ if (bytes < 1024) return `${bytes} B`;
4611
+ return `${Math.round(bytes / 1024)} KB`;
4612
+ }
4613
+ function printBackupTable(entries) {
4614
+ if (entries.length === 0) {
4615
+ process.stdout.write(" No backups found.\n");
4616
+ process.stdout.write(` Backups are created automatically when claudectx modifies your files.
4617
+ `);
4618
+ process.stdout.write(` Backup directory: ${BACKUP_DIR}
4619
+
4620
+ `);
4621
+ return;
4622
+ }
4623
+ const idWidth = 26;
4624
+ const fileWidth = 20;
4625
+ const cmdWidth = 10;
4626
+ const timeWidth = 14;
4627
+ const sizeWidth = 7;
4628
+ const hr = "\u2550".repeat(idWidth + fileWidth + cmdWidth + timeWidth + sizeWidth + 16);
4629
+ process.stdout.write("\n");
4630
+ process.stdout.write("claudectx \u2014 Backup History\n");
4631
+ process.stdout.write(hr + "\n");
4632
+ process.stdout.write(
4633
+ ` ${"ID".padEnd(idWidth)} ${"File".padEnd(fileWidth)} ${"Command".padEnd(cmdWidth)} ${"When".padEnd(timeWidth)} ${"Size".padEnd(sizeWidth)}
4634
+ `
4635
+ );
4636
+ process.stdout.write("\u2500".repeat(idWidth + fileWidth + cmdWidth + timeWidth + sizeWidth + 16) + "\n");
4637
+ for (const entry of entries) {
4638
+ const id = entry.id.slice(0, idWidth).padEnd(idWidth);
4639
+ const file = path29.basename(entry.originalPath).slice(0, fileWidth).padEnd(fileWidth);
4640
+ const cmd = entry.command.slice(0, cmdWidth).padEnd(cmdWidth);
4641
+ const when = timeAgo(entry.createdAt).slice(0, timeWidth).padEnd(timeWidth);
4642
+ const size = formatBytes(entry.sizeBytes).padEnd(sizeWidth);
4643
+ process.stdout.write(` ${id} ${file} ${cmd} ${when} ${size}
4644
+ `);
4645
+ }
4646
+ process.stdout.write("\n");
4647
+ process.stdout.write(` Backup directory: ${BACKUP_DIR}
4648
+ `);
4649
+ process.stdout.write(" To restore: claudectx revert --id <ID>\n\n");
4650
+ }
4651
+ async function interactivePick(entries) {
4652
+ try {
4653
+ const { select } = await import("@inquirer/prompts");
4654
+ const choices = entries.map((e) => ({
4655
+ name: `${timeAgo(e.createdAt).padEnd(14)} ${path29.basename(e.originalPath).padEnd(16)} [${e.command}] ${e.id}`,
4656
+ value: e.id
4657
+ }));
4658
+ choices.push({ name: "Cancel", value: "" });
4659
+ return await select({ message: "Choose a backup to restore:", choices });
4660
+ } catch {
4661
+ process.stderr.write("Interactive mode unavailable. Use --id <id> to restore a specific backup.\n");
4662
+ return null;
4663
+ }
4664
+ }
4665
+ async function doRestore(id) {
4666
+ const chalk5 = (await import("chalk")).default;
4667
+ process.stdout.write("\n");
4668
+ try {
4669
+ const entries = await listBackups();
4670
+ const entry = entries.find((e) => e.id === id);
4671
+ if (!entry) {
4672
+ process.stderr.write(chalk5.red(`Backup "${id}" not found.
4673
+ `));
4674
+ process.stderr.write('Run "claudectx revert --list" to see available backups.\n');
4675
+ process.exitCode = 1;
4676
+ return;
4677
+ }
4678
+ process.stdout.write(chalk5.yellow(`\u26A0 This will overwrite: ${entry.originalPath}
4679
+ `));
4680
+ process.stdout.write(` Backup from: ${timeAgo(entry.createdAt)} (${entry.command})
4681
+ `);
4682
+ process.stdout.write(` Your current file will be backed up first (so you can undo this).
4683
+
4684
+ `);
4685
+ let confirmed = true;
4686
+ try {
4687
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
4688
+ confirmed = await confirm2({ message: "Restore this backup?", default: false });
4689
+ } catch {
4690
+ }
4691
+ if (!confirmed) {
4692
+ process.stdout.write(" Cancelled.\n\n");
4693
+ return;
4694
+ }
4695
+ const { undoEntry } = await restoreBackup(id);
4696
+ process.stdout.write(chalk5.green(" \u2713 ") + `Restored to ${entry.originalPath}
4697
+ `);
4698
+ if (undoEntry) {
4699
+ process.stdout.write(
4700
+ chalk5.dim(` Your previous version was saved as backup "${undoEntry.id}" \u2014 run 'claudectx revert --id ${undoEntry.id}' to undo.
4701
+ `)
4702
+ );
4703
+ }
4704
+ process.stdout.write("\n");
4705
+ } catch (err) {
4706
+ process.stderr.write(chalk5.red(`Error: ${err instanceof Error ? err.message : String(err)}
4707
+ `));
4708
+ process.exitCode = 1;
4709
+ }
4710
+ }
4711
+ async function revertCommand(options) {
4712
+ const entries = await listBackups(options.file);
4713
+ if (options.json) {
4714
+ process.stdout.write(JSON.stringify({ backups: entries }, null, 2) + "\n");
4715
+ return;
4716
+ }
4717
+ if (options.list) {
4718
+ printBackupTable(entries);
4719
+ return;
4720
+ }
4721
+ if (options.id) {
4722
+ await doRestore(options.id);
4723
+ return;
4724
+ }
4725
+ if (entries.length === 0) {
4726
+ process.stdout.write("\n No backups found. Backups are created automatically when claudectx modifies your files.\n\n");
4727
+ return;
4728
+ }
4729
+ printBackupTable(entries);
4730
+ const picked = await interactivePick(entries);
4731
+ if (picked) {
4732
+ await doRestore(picked);
4733
+ }
4734
+ }
4735
+
4404
4736
  // src/index.ts
4405
- var VERSION = "1.1.2";
4737
+ var VERSION = "1.1.3";
4406
4738
  var DESCRIPTION = "Reduce Claude Code token usage by up to 80%. Context analyzer, auto-optimizer, live dashboard, and smart MCP tools.";
4407
4739
  var program = new import_commander.Command();
4408
4740
  program.name("claudectx").description(DESCRIPTION).version(VERSION);
@@ -4442,5 +4774,8 @@ program.command("convert").description("Convert CLAUDE.md to another AI assistan
4442
4774
  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
4775
  await teamsCommand(subcommand ?? "export", options);
4444
4776
  });
4777
+ 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) => {
4778
+ await revertCommand(options);
4779
+ });
4445
4780
  program.parse();
4446
4781
  //# sourceMappingURL=index.js.map