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.mjs CHANGED
@@ -104,19 +104,19 @@ __export(session_store_exports, {
104
104
  getStoreDir: () => getStoreDir,
105
105
  readAllEvents: () => readAllEvents
106
106
  });
107
- import * as fs8 from "fs";
108
- import * as os2 from "os";
109
- import * as path9 from "path";
107
+ import * as fs9 from "fs";
108
+ import * as os3 from "os";
109
+ import * as path10 from "path";
110
110
  function getStoreDirPath() {
111
- return path9.join(os2.homedir(), ".claudectx");
111
+ return path10.join(os3.homedir(), ".claudectx");
112
112
  }
113
113
  function getReadsFilePath_() {
114
- return path9.join(getStoreDirPath(), "reads.jsonl");
114
+ return path10.join(getStoreDirPath(), "reads.jsonl");
115
115
  }
116
116
  function ensureStoreDir() {
117
117
  const dir = getStoreDirPath();
118
- if (!fs8.existsSync(dir)) {
119
- fs8.mkdirSync(dir, { recursive: true });
118
+ if (!fs9.existsSync(dir)) {
119
+ fs9.mkdirSync(dir, { recursive: true });
120
120
  }
121
121
  }
122
122
  function appendFileRead(filePath, sessionId) {
@@ -126,12 +126,12 @@ function appendFileRead(filePath, sessionId) {
126
126
  filePath,
127
127
  sessionId
128
128
  };
129
- fs8.appendFileSync(getReadsFilePath_(), JSON.stringify(event) + "\n", "utf-8");
129
+ fs9.appendFileSync(getReadsFilePath_(), JSON.stringify(event) + "\n", "utf-8");
130
130
  }
131
131
  function readAllEvents() {
132
132
  const readsFile = getReadsFilePath_();
133
- if (!fs8.existsSync(readsFile)) return [];
134
- const lines = fs8.readFileSync(readsFile, "utf-8").trim().split("\n").filter(Boolean);
133
+ if (!fs9.existsSync(readsFile)) return [];
134
+ const lines = fs9.readFileSync(readsFile, "utf-8").trim().split("\n").filter(Boolean);
135
135
  return lines.map((line) => {
136
136
  try {
137
137
  return JSON.parse(line);
@@ -160,8 +160,8 @@ function aggregateStats(events) {
160
160
  }
161
161
  function clearStore() {
162
162
  const readsFile = getReadsFilePath_();
163
- if (fs8.existsSync(readsFile)) {
164
- fs8.writeFileSync(readsFile, "", "utf-8");
163
+ if (fs9.existsSync(readsFile)) {
164
+ fs9.writeFileSync(readsFile, "", "utf-8");
165
165
  }
166
166
  }
167
167
  function getReadsFilePath() {
@@ -178,28 +178,28 @@ var init_session_store = __esm({
178
178
  });
179
179
 
180
180
  // src/watcher/session-reader.ts
181
- import * as fs9 from "fs";
182
- import * as os3 from "os";
183
- import * as path10 from "path";
181
+ import * as fs10 from "fs";
182
+ import * as os4 from "os";
183
+ import * as path11 from "path";
184
184
  function listSessionFiles() {
185
- if (!fs9.existsSync(CLAUDE_PROJECTS_DIR)) return [];
185
+ if (!fs10.existsSync(CLAUDE_PROJECTS_DIR)) return [];
186
186
  const results = [];
187
187
  try {
188
- const projectDirs = fs9.readdirSync(CLAUDE_PROJECTS_DIR);
188
+ const projectDirs = fs10.readdirSync(CLAUDE_PROJECTS_DIR);
189
189
  for (const projectDir of projectDirs) {
190
- const projectPath = path10.join(CLAUDE_PROJECTS_DIR, projectDir);
190
+ const projectPath = path11.join(CLAUDE_PROJECTS_DIR, projectDir);
191
191
  try {
192
- const stat = fs9.statSync(projectPath);
192
+ const stat = fs10.statSync(projectPath);
193
193
  if (!stat.isDirectory()) continue;
194
- const files = fs9.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
194
+ const files = fs10.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
195
195
  for (const file of files) {
196
- const filePath = path10.join(projectPath, file);
196
+ const filePath = path11.join(projectPath, file);
197
197
  try {
198
- const fstat = fs9.statSync(filePath);
198
+ const fstat = fs10.statSync(filePath);
199
199
  results.push({
200
200
  filePath,
201
201
  mtimeMs: fstat.mtimeMs,
202
- sessionId: path10.basename(file, ".jsonl"),
202
+ sessionId: path11.basename(file, ".jsonl"),
203
203
  projectDir
204
204
  });
205
205
  } catch {
@@ -230,7 +230,7 @@ async function readSessionUsage(sessionFilePath) {
230
230
  cacheReadTokens: 0,
231
231
  requestCount: 0
232
232
  };
233
- if (!fs9.existsSync(sessionFilePath)) return result;
233
+ if (!fs10.existsSync(sessionFilePath)) return result;
234
234
  const { createReadStream } = await import("fs");
235
235
  const { createInterface } = await import("readline");
236
236
  try {
@@ -265,7 +265,7 @@ var init_session_reader = __esm({
265
265
  "src/watcher/session-reader.ts"() {
266
266
  "use strict";
267
267
  init_esm_shims();
268
- CLAUDE_PROJECTS_DIR = path10.join(os3.homedir(), ".claude", "projects");
268
+ CLAUDE_PROJECTS_DIR = path11.join(os4.homedir(), ".claude", "projects");
269
269
  }
270
270
  });
271
271
 
@@ -276,8 +276,8 @@ __export(Dashboard_exports, {
276
276
  });
277
277
  import { useState, useEffect, useCallback } from "react";
278
278
  import { Box, Text, useApp, useInput } from "ink";
279
- import * as fs10 from "fs";
280
- import * as path11 from "path";
279
+ import * as fs11 from "fs";
280
+ import * as path12 from "path";
281
281
  import { jsx, jsxs } from "react/jsx-runtime";
282
282
  function fmtNum(n) {
283
283
  return n.toLocaleString();
@@ -288,7 +288,7 @@ function fmtCost(tokens, model) {
288
288
  return `$${cost.toFixed(4)}`;
289
289
  }
290
290
  function shortPath(filePath) {
291
- const parts = filePath.split(path11.sep);
291
+ const parts = filePath.split(path12.sep);
292
292
  if (parts.length <= 3) return filePath;
293
293
  return "\u2026/" + parts.slice(-3).join("/");
294
294
  }
@@ -410,9 +410,9 @@ function Dashboard({
410
410
  const readsFile = getReadsFilePath();
411
411
  let watcher = null;
412
412
  const tryWatch = () => {
413
- if (fs10.existsSync(readsFile)) {
413
+ if (fs11.existsSync(readsFile)) {
414
414
  try {
415
- watcher = fs10.watch(readsFile, () => refresh());
415
+ watcher = fs11.watch(readsFile, () => refresh());
416
416
  } catch {
417
417
  }
418
418
  }
@@ -450,7 +450,7 @@ function Dashboard({
450
450
  ] }),
451
451
  sessionFile && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
452
452
  " \u2014 ",
453
- path11.basename(sessionFile, ".jsonl").slice(0, 8),
453
+ path12.basename(sessionFile, ".jsonl").slice(0, 8),
454
454
  "\u2026"
455
455
  ] }),
456
456
  !sessionFile && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 no session file found" })
@@ -477,10 +477,10 @@ var init_Dashboard = __esm({
477
477
  });
478
478
 
479
479
  // src/mcp/smart-reader.ts
480
- import * as fs12 from "fs";
481
- import * as path14 from "path";
480
+ import * as fs13 from "fs";
481
+ import * as path15 from "path";
482
482
  function detectLanguage(filePath) {
483
- const ext = path14.extname(filePath).toLowerCase();
483
+ const ext = path15.extname(filePath).toLowerCase();
484
484
  switch (ext) {
485
485
  case ".ts":
486
486
  case ".tsx":
@@ -497,8 +497,8 @@ function detectLanguage(filePath) {
497
497
  }
498
498
  }
499
499
  function findSymbol(filePath, symbolName) {
500
- if (!fs12.existsSync(filePath)) return null;
501
- const content = fs12.readFileSync(filePath, "utf-8");
500
+ if (!fs13.existsSync(filePath)) return null;
501
+ const content = fs13.readFileSync(filePath, "utf-8");
502
502
  const lines = content.split("\n");
503
503
  const lang = detectLanguage(filePath);
504
504
  const patterns = lang === "python" ? PYTHON_PATTERNS : TS_JS_PATTERNS;
@@ -559,8 +559,8 @@ function findPythonBlockEnd(lines, startIdx) {
559
559
  return lines.length;
560
560
  }
561
561
  function extractLineRange(filePath, startLine, endLine, contextLines = 0) {
562
- if (!fs12.existsSync(filePath)) return null;
563
- const allLines = fs12.readFileSync(filePath, "utf-8").split("\n");
562
+ if (!fs13.existsSync(filePath)) return null;
563
+ const allLines = fs13.readFileSync(filePath, "utf-8").split("\n");
564
564
  const totalLines = allLines.length;
565
565
  const from = Math.max(0, startLine - 1 - contextLines);
566
566
  const to = Math.min(totalLines, endLine + contextLines);
@@ -575,7 +575,7 @@ function extractLineRange(filePath, startLine, endLine, contextLines = 0) {
575
575
  };
576
576
  }
577
577
  function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
578
- if (!fs12.existsSync(filePath)) {
578
+ if (!fs13.existsSync(filePath)) {
579
579
  throw new Error(`File not found: ${filePath}`);
580
580
  }
581
581
  if (symbol) {
@@ -587,7 +587,7 @@ function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
587
587
  filePath,
588
588
  startLine: extracted.startLine,
589
589
  endLine: extracted.endLine,
590
- totalLines: fs12.readFileSync(filePath, "utf-8").split("\n").length,
590
+ totalLines: fs13.readFileSync(filePath, "utf-8").split("\n").length,
591
591
  truncated: false,
592
592
  symbolName: symbol
593
593
  };
@@ -599,7 +599,7 @@ function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
599
599
  return { ...result, truncated: false };
600
600
  }
601
601
  }
602
- const fullContent = fs12.readFileSync(filePath, "utf-8");
602
+ const fullContent = fs13.readFileSync(filePath, "utf-8");
603
603
  const allLines = fullContent.split("\n");
604
604
  const totalLines = allLines.length;
605
605
  const fullTokens = countTokens(fullContent);
@@ -673,15 +673,15 @@ var init_smart_reader = __esm({
673
673
  });
674
674
 
675
675
  // src/mcp/symbol-index.ts
676
- import * as fs13 from "fs";
677
- import * as path15 from "path";
676
+ import * as fs14 from "fs";
677
+ import * as path16 from "path";
678
678
  import { glob } from "glob";
679
679
  function extractSymbolsFromFile(filePath) {
680
680
  const lang = detectLanguage(filePath);
681
681
  if (lang === "other") return [];
682
682
  let content;
683
683
  try {
684
- content = fs13.readFileSync(filePath, "utf-8");
684
+ content = fs14.readFileSync(filePath, "utf-8");
685
685
  } catch {
686
686
  return [];
687
687
  }
@@ -766,8 +766,8 @@ var init_symbol_index = __esm({
766
766
  this.entries = [];
767
767
  let files = [];
768
768
  try {
769
- files = await glob(SOURCE_GLOBS.map((g) => path15.join(projectRoot, g)), {
770
- ignore: IGNORE_DIRS.map((g) => path15.join(projectRoot, g)),
769
+ files = await glob(SOURCE_GLOBS.map((g) => path16.join(projectRoot, g)), {
770
+ ignore: IGNORE_DIRS.map((g) => path16.join(projectRoot, g)),
771
771
  absolute: true
772
772
  });
773
773
  } catch {
@@ -820,7 +820,7 @@ var server_exports = {};
820
820
  __export(server_exports, {
821
821
  startMcpServer: () => startMcpServer
822
822
  });
823
- import * as path16 from "path";
823
+ import * as path17 from "path";
824
824
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
825
825
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
826
826
  import {
@@ -828,7 +828,7 @@ import {
828
828
  ListToolsRequestSchema
829
829
  } from "@modelcontextprotocol/sdk/types.js";
830
830
  function handleSmartRead(args) {
831
- const filePath = path16.resolve(args.file);
831
+ const filePath = path17.resolve(args.file);
832
832
  const result = smartRead(
833
833
  filePath,
834
834
  args.symbol,
@@ -864,7 +864,7 @@ Example: index_project({ "project_root": "${process.cwd()}" })`;
864
864
  ];
865
865
  for (let i = 0; i < results.length; i++) {
866
866
  const r = results[i];
867
- const rel = path16.relative(process.cwd(), r.filePath);
867
+ const rel = path17.relative(process.cwd(), r.filePath);
868
868
  lines.push(`${i + 1}. [${r.type}] ${r.name}`);
869
869
  lines.push(` ${rel}:${r.lineStart}`);
870
870
  lines.push(` ${r.signature.trim()}`);
@@ -876,7 +876,7 @@ Example: index_project({ "project_root": "${process.cwd()}" })`;
876
876
  return lines.join("\n");
877
877
  }
878
878
  async function handleIndexProject(args) {
879
- const projectRoot = args.project_root ? path16.resolve(args.project_root) : process.cwd();
879
+ const projectRoot = args.project_root ? path17.resolve(args.project_root) : process.cwd();
880
880
  const fn = args.rebuild ? () => globalIndex.rebuild(projectRoot) : () => globalIndex.build(projectRoot);
881
881
  const { fileCount, symbolCount } = await fn();
882
882
  if (fileCount === 0 && globalIndex.isReady) {
@@ -1563,8 +1563,8 @@ async function analyzeCommand(options) {
1563
1563
 
1564
1564
  // src/commands/optimize.ts
1565
1565
  init_esm_shims();
1566
- import * as fs7 from "fs";
1567
- import * as path8 from "path";
1566
+ import * as fs8 from "fs";
1567
+ import * as path9 from "path";
1568
1568
  import chalk3 from "chalk";
1569
1569
  import boxen2 from "boxen";
1570
1570
  import { checkbox, confirm } from "@inquirer/prompts";
@@ -1728,8 +1728,131 @@ function writeIgnorefile(result) {
1728
1728
  // src/optimizer/claudemd-splitter.ts
1729
1729
  init_esm_shims();
1730
1730
  init_tokenizer();
1731
+ import * as fs5 from "fs";
1732
+ import * as path7 from "path";
1733
+
1734
+ // src/shared/backup-manager.ts
1735
+ init_esm_shims();
1731
1736
  import * as fs4 from "fs";
1737
+ import * as os2 from "os";
1732
1738
  import * as path6 from "path";
1739
+ var MAX_BACKUPS = 50;
1740
+ var _backupDirOverride = null;
1741
+ function getBackupDir() {
1742
+ return _backupDirOverride ?? path6.join(os2.homedir(), ".claudectx", "backups");
1743
+ }
1744
+ var BACKUP_DIR = path6.join(os2.homedir(), ".claudectx", "backups");
1745
+ function ensureBackupDir() {
1746
+ const dir = getBackupDir();
1747
+ if (!fs4.existsSync(dir)) {
1748
+ fs4.mkdirSync(dir, { recursive: true });
1749
+ }
1750
+ }
1751
+ function getManifestPath() {
1752
+ return path6.join(getBackupDir(), "manifest.json");
1753
+ }
1754
+ function readManifest() {
1755
+ ensureBackupDir();
1756
+ const manifestPath = getManifestPath();
1757
+ if (!fs4.existsSync(manifestPath)) {
1758
+ return { version: "1", entries: [] };
1759
+ }
1760
+ try {
1761
+ return JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
1762
+ } catch {
1763
+ return { version: "1", entries: [] };
1764
+ }
1765
+ }
1766
+ function writeManifest(manifest) {
1767
+ ensureBackupDir();
1768
+ const manifestPath = getManifestPath();
1769
+ const tmpPath = `${manifestPath}.tmp-${Date.now()}`;
1770
+ try {
1771
+ fs4.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
1772
+ fs4.renameSync(tmpPath, manifestPath);
1773
+ } catch {
1774
+ try {
1775
+ fs4.unlinkSync(tmpPath);
1776
+ } catch {
1777
+ }
1778
+ throw new Error(`Failed to write backup manifest at ${manifestPath}`);
1779
+ }
1780
+ }
1781
+ function generateId(originalPath) {
1782
+ const now = /* @__PURE__ */ new Date();
1783
+ const ts = now.toISOString().replace(/[-:]/g, "").replace("T", "T").replace(".", "m").replace("Z", "");
1784
+ const rand = Math.floor(Math.random() * 9e3 + 1e3);
1785
+ const basename10 = path6.basename(originalPath);
1786
+ return `${ts}-${rand}-${basename10}`;
1787
+ }
1788
+ async function backupFile(filePath, command) {
1789
+ const resolved = path6.resolve(filePath);
1790
+ if (!fs4.existsSync(resolved)) {
1791
+ throw new Error(`Cannot back up "${resolved}": file does not exist.`);
1792
+ }
1793
+ ensureBackupDir();
1794
+ const id = generateId(resolved);
1795
+ const backupPath = path6.join(getBackupDir(), id);
1796
+ fs4.copyFileSync(resolved, backupPath);
1797
+ const stat = fs4.statSync(backupPath);
1798
+ const entry = {
1799
+ id,
1800
+ originalPath: resolved,
1801
+ backupPath,
1802
+ command,
1803
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1804
+ sizeBytes: stat.size
1805
+ };
1806
+ const manifest = readManifest();
1807
+ manifest.entries.unshift(entry);
1808
+ writeManifest(manifest);
1809
+ if (manifest.entries.length > MAX_BACKUPS) {
1810
+ await pruneOldBackups();
1811
+ }
1812
+ return entry;
1813
+ }
1814
+ async function listBackups(filterPath) {
1815
+ const manifest = readManifest();
1816
+ const entries = manifest.entries;
1817
+ if (!filterPath) return entries;
1818
+ const resolved = path6.resolve(filterPath);
1819
+ return entries.filter((e) => e.originalPath === resolved);
1820
+ }
1821
+ async function restoreBackup(backupId) {
1822
+ const manifest = readManifest();
1823
+ const entry = manifest.entries.find((e) => e.id === backupId);
1824
+ if (!entry) {
1825
+ throw new Error(`Backup "${backupId}" not found. Run "claudectx revert --list" to see available backups.`);
1826
+ }
1827
+ if (!fs4.existsSync(entry.backupPath)) {
1828
+ throw new Error(`Backup file missing at "${entry.backupPath}". It may have been deleted manually.`);
1829
+ }
1830
+ let undoEntry = null;
1831
+ if (fs4.existsSync(entry.originalPath)) {
1832
+ undoEntry = await backupFile(entry.originalPath, "revert");
1833
+ }
1834
+ const targetDir = path6.dirname(entry.originalPath);
1835
+ if (!fs4.existsSync(targetDir)) {
1836
+ fs4.mkdirSync(targetDir, { recursive: true });
1837
+ }
1838
+ fs4.copyFileSync(entry.backupPath, entry.originalPath);
1839
+ return { entry, undoEntry };
1840
+ }
1841
+ async function pruneOldBackups() {
1842
+ const manifest = readManifest();
1843
+ if (manifest.entries.length <= MAX_BACKUPS) return 0;
1844
+ const toRemove = manifest.entries.splice(MAX_BACKUPS);
1845
+ for (const entry of toRemove) {
1846
+ try {
1847
+ fs4.unlinkSync(entry.backupPath);
1848
+ } catch {
1849
+ }
1850
+ }
1851
+ writeManifest(manifest);
1852
+ return toRemove.length;
1853
+ }
1854
+
1855
+ // src/optimizer/claudemd-splitter.ts
1733
1856
  var SPLIT_MIN_TOKENS = 300;
1734
1857
  function slugify(title) {
1735
1858
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -1764,9 +1887,9 @@ function parseSections(content) {
1764
1887
  return sections;
1765
1888
  }
1766
1889
  function planSplit(claudeMdPath, sectionsToExtract) {
1767
- const content = fs4.readFileSync(claudeMdPath, "utf-8");
1890
+ const content = fs5.readFileSync(claudeMdPath, "utf-8");
1768
1891
  const sections = parseSections(content);
1769
- const claudeDir = path6.join(path6.dirname(claudeMdPath), ".claude");
1892
+ const claudeDir = path7.join(path7.dirname(claudeMdPath), ".claude");
1770
1893
  const extractedFiles = [];
1771
1894
  let newContent = "";
1772
1895
  let tokensSaved = 0;
@@ -1779,7 +1902,7 @@ function planSplit(claudeMdPath, sectionsToExtract) {
1779
1902
  usedSlugs.set(slug, count + 1);
1780
1903
  const filename = `${slug}.md`;
1781
1904
  const relRefPath = `.claude/${filename}`;
1782
- const filePath = path6.join(claudeDir, filename);
1905
+ const filePath = path7.join(claudeDir, filename);
1783
1906
  const refBlock = `## ${section.title}
1784
1907
 
1785
1908
  @${relRefPath}
@@ -1803,21 +1926,24 @@ function planSplit(claudeMdPath, sectionsToExtract) {
1803
1926
  tokensSaved: Math.max(0, tokensSaved)
1804
1927
  };
1805
1928
  }
1806
- function applySplit(result) {
1929
+ async function applySplit(result) {
1807
1930
  if (result.extractedFiles.length === 0) return;
1808
- const claudeDir = path6.dirname(result.extractedFiles[0].filePath);
1809
- if (!fs4.existsSync(claudeDir)) {
1810
- fs4.mkdirSync(claudeDir, { recursive: true });
1931
+ if (fs5.existsSync(result.claudeMdPath)) {
1932
+ await backupFile(result.claudeMdPath, "optimize");
1933
+ }
1934
+ const claudeDir = path7.dirname(result.extractedFiles[0].filePath);
1935
+ if (!fs5.existsSync(claudeDir)) {
1936
+ fs5.mkdirSync(claudeDir, { recursive: true });
1811
1937
  }
1812
1938
  for (const file of result.extractedFiles) {
1813
- fs4.writeFileSync(file.filePath, file.content, "utf-8");
1939
+ fs5.writeFileSync(file.filePath, file.content, "utf-8");
1814
1940
  }
1815
- fs4.writeFileSync(result.claudeMdPath, result.newClaudeMd, "utf-8");
1941
+ fs5.writeFileSync(result.claudeMdPath, result.newClaudeMd, "utf-8");
1816
1942
  }
1817
1943
 
1818
1944
  // src/optimizer/cache-applier.ts
1819
1945
  init_esm_shims();
1820
- import * as fs5 from "fs";
1946
+ import * as fs6 from "fs";
1821
1947
  function findCacheBusters(content) {
1822
1948
  const fixes = [];
1823
1949
  const lines = content.split("\n");
@@ -1847,18 +1973,21 @@ function applyCacheFixes(content, fixes) {
1847
1973
  return lines.join("\n");
1848
1974
  }
1849
1975
  function planCacheFixes(claudeMdPath) {
1850
- const content = fs5.readFileSync(claudeMdPath, "utf-8");
1976
+ const content = fs6.readFileSync(claudeMdPath, "utf-8");
1851
1977
  const fixes = findCacheBusters(content);
1852
1978
  return { fixes, newContent: applyCacheFixes(content, fixes) };
1853
1979
  }
1854
- function applyAndWriteCacheFixes(claudeMdPath, result) {
1855
- fs5.writeFileSync(claudeMdPath, result.newContent, "utf-8");
1980
+ async function applyAndWriteCacheFixes(claudeMdPath, result) {
1981
+ if (fs6.existsSync(claudeMdPath)) {
1982
+ await backupFile(claudeMdPath, "optimize");
1983
+ }
1984
+ fs6.writeFileSync(claudeMdPath, result.newContent, "utf-8");
1856
1985
  }
1857
1986
 
1858
1987
  // src/optimizer/hooks-installer.ts
1859
1988
  init_esm_shims();
1860
- import * as fs6 from "fs";
1861
- import * as path7 from "path";
1989
+ import * as fs7 from "fs";
1990
+ import * as path8 from "path";
1862
1991
  var CLAUDECTX_HOOKS = {
1863
1992
  PostToolUse: [
1864
1993
  {
@@ -1876,13 +2005,13 @@ var CLAUDECTX_HOOKS = {
1876
2005
  ]
1877
2006
  };
1878
2007
  function planHooksInstall(projectRoot) {
1879
- const claudeDir = path7.join(projectRoot, ".claude");
1880
- const settingsPath = path7.join(claudeDir, "settings.local.json");
1881
- const existed = fs6.existsSync(settingsPath);
2008
+ const claudeDir = path8.join(projectRoot, ".claude");
2009
+ const settingsPath = path8.join(claudeDir, "settings.local.json");
2010
+ const existed = fs7.existsSync(settingsPath);
1882
2011
  let existing = {};
1883
2012
  if (existed) {
1884
2013
  try {
1885
- existing = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2014
+ existing = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
1886
2015
  } catch {
1887
2016
  existing = {};
1888
2017
  }
@@ -1903,25 +2032,25 @@ function planHooksInstall(projectRoot) {
1903
2032
  return { settingsPath, existed, mergedSettings };
1904
2033
  }
1905
2034
  function applyHooksInstall(result) {
1906
- const dir = path7.dirname(result.settingsPath);
1907
- if (!fs6.existsSync(dir)) {
1908
- fs6.mkdirSync(dir, { recursive: true });
2035
+ const dir = path8.dirname(result.settingsPath);
2036
+ if (!fs7.existsSync(dir)) {
2037
+ fs7.mkdirSync(dir, { recursive: true });
1909
2038
  }
1910
- fs6.writeFileSync(result.settingsPath, JSON.stringify(result.mergedSettings, null, 2) + "\n", "utf-8");
2039
+ fs7.writeFileSync(result.settingsPath, JSON.stringify(result.mergedSettings, null, 2) + "\n", "utf-8");
1911
2040
  }
1912
2041
  function writeHooksSettings(projectRoot, mergedSettings) {
1913
- const settingsPath = path7.join(projectRoot, ".claude", "settings.local.json");
1914
- const dir = path7.dirname(settingsPath);
1915
- if (!fs6.existsSync(dir)) {
1916
- fs6.mkdirSync(dir, { recursive: true });
2042
+ const settingsPath = path8.join(projectRoot, ".claude", "settings.local.json");
2043
+ const dir = path8.dirname(settingsPath);
2044
+ if (!fs7.existsSync(dir)) {
2045
+ fs7.mkdirSync(dir, { recursive: true });
1917
2046
  }
1918
- fs6.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n", "utf-8");
2047
+ fs7.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n", "utf-8");
1919
2048
  }
1920
2049
  function isAlreadyInstalled(projectRoot) {
1921
- const settingsPath = path7.join(projectRoot, ".claude", "settings.local.json");
1922
- if (!fs6.existsSync(settingsPath)) return false;
2050
+ const settingsPath = path8.join(projectRoot, ".claude", "settings.local.json");
2051
+ if (!fs7.existsSync(settingsPath)) return false;
1923
2052
  try {
1924
- const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2053
+ const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
1925
2054
  const postToolUse = settings?.hooks?.PostToolUse ?? [];
1926
2055
  return postToolUse.some((h) => h.matcher === "Read");
1927
2056
  } catch {
@@ -1931,7 +2060,7 @@ function isAlreadyInstalled(projectRoot) {
1931
2060
 
1932
2061
  // src/commands/optimize.ts
1933
2062
  async function optimizeCommand(options) {
1934
- const projectPath = options.path ? path8.resolve(options.path) : findProjectRoot() ?? process.cwd();
2063
+ const projectPath = options.path ? path9.resolve(options.path) : findProjectRoot() ?? process.cwd();
1935
2064
  const dryRun = options.dryRun ?? false;
1936
2065
  const autoApply = options.apply ?? false;
1937
2066
  const specificMode = options.claudemd || options.ignorefile || options.cache || options.hooks;
@@ -2056,12 +2185,12 @@ async function runIgnorefile(projectRoot, dryRun, autoApply) {
2056
2185
  }
2057
2186
  async function runClaudeMdSplit(projectRoot, report, dryRun, autoApply) {
2058
2187
  printSectionHeader("CLAUDE.md \u2192 @files");
2059
- const claudeMdPath = path8.join(projectRoot, "CLAUDE.md");
2060
- if (!fs7.existsSync(claudeMdPath)) {
2188
+ const claudeMdPath = path9.join(projectRoot, "CLAUDE.md");
2189
+ if (!fs8.existsSync(claudeMdPath)) {
2061
2190
  logger.warn("No CLAUDE.md found \u2014 skipping.");
2062
2191
  return;
2063
2192
  }
2064
- const content = fs7.readFileSync(claudeMdPath, "utf-8");
2193
+ const content = fs8.readFileSync(claudeMdPath, "utf-8");
2065
2194
  const sections = parseSections(content);
2066
2195
  const largeSections = sections.filter(
2067
2196
  (s) => !s.isPreamble && s.tokens >= SPLIT_MIN_TOKENS
@@ -2117,18 +2246,18 @@ Would extract ${splitResult.extractedFiles.length} section(s) to .claude/`
2117
2246
  logger.info("Skipped.");
2118
2247
  return;
2119
2248
  }
2120
- applySplit(splitResult);
2249
+ await applySplit(splitResult);
2121
2250
  logger.success(
2122
2251
  `Extracted ${splitResult.extractedFiles.length} section(s). Saved ~${splitResult.tokensSaved} tokens/request.`
2123
2252
  );
2124
2253
  for (const f of splitResult.extractedFiles) {
2125
- logger.info(` Created: ${chalk3.cyan(path8.relative(projectRoot, f.filePath))}`);
2254
+ logger.info(` Created: ${chalk3.cyan(path9.relative(projectRoot, f.filePath))}`);
2126
2255
  }
2127
2256
  }
2128
2257
  async function runCacheOptimization(projectRoot, dryRun, autoApply) {
2129
2258
  printSectionHeader("Prompt cache optimisation");
2130
- const claudeMdPath = path8.join(projectRoot, "CLAUDE.md");
2131
- if (!fs7.existsSync(claudeMdPath)) {
2259
+ const claudeMdPath = path9.join(projectRoot, "CLAUDE.md");
2260
+ if (!fs8.existsSync(claudeMdPath)) {
2132
2261
  logger.warn("No CLAUDE.md found \u2014 skipping.");
2133
2262
  return;
2134
2263
  }
@@ -2156,14 +2285,14 @@ async function runCacheOptimization(projectRoot, dryRun, autoApply) {
2156
2285
  logger.info("Skipped.");
2157
2286
  return;
2158
2287
  }
2159
- applyAndWriteCacheFixes(claudeMdPath, result);
2288
+ await applyAndWriteCacheFixes(claudeMdPath, result);
2160
2289
  logger.success(`Fixed ${result.fixes.length} cache-busting pattern(s) in CLAUDE.md.`);
2161
2290
  }
2162
2291
  async function runHooks(projectRoot, dryRun, autoApply) {
2163
2292
  printSectionHeader("Session hooks");
2164
2293
  const result = planHooksInstall(projectRoot);
2165
2294
  logger.info(
2166
- `Settings file: ${chalk3.cyan(path8.relative(projectRoot, result.settingsPath))}`
2295
+ `Settings file: ${chalk3.cyan(path9.relative(projectRoot, result.settingsPath))}`
2167
2296
  );
2168
2297
  logger.info(result.existed ? "Will merge with existing settings." : "Will create new file.");
2169
2298
  console.log(chalk3.dim("\n Hooks to install:"));
@@ -2178,7 +2307,7 @@ async function runHooks(projectRoot, dryRun, autoApply) {
2178
2307
  }
2179
2308
  applyHooksInstall(result);
2180
2309
  logger.success(
2181
- `Hooks installed \u2192 ${chalk3.cyan(path8.relative(projectRoot, result.settingsPath))}`
2310
+ `Hooks installed \u2192 ${chalk3.cyan(path9.relative(projectRoot, result.settingsPath))}`
2182
2311
  );
2183
2312
  }
2184
2313
  function printSectionHeader(title) {
@@ -2190,7 +2319,7 @@ function printSectionHeader(title) {
2190
2319
  init_esm_shims();
2191
2320
  init_session_store();
2192
2321
  init_models();
2193
- import * as path12 from "path";
2322
+ import * as path13 from "path";
2194
2323
  async function watchCommand(options) {
2195
2324
  if (options.logStdin) {
2196
2325
  await handleLogStdin();
@@ -2226,30 +2355,30 @@ async function handleLogStdin() {
2226
2355
  const payload = JSON.parse(raw);
2227
2356
  const filePath = payload.tool_input?.file_path;
2228
2357
  if (filePath) {
2229
- appendFileRead(path12.resolve(filePath), payload.session_id);
2358
+ appendFileRead(path13.resolve(filePath), payload.session_id);
2230
2359
  }
2231
2360
  } catch {
2232
2361
  }
2233
2362
  }
2234
2363
  function readStdin() {
2235
- return new Promise((resolve12) => {
2364
+ return new Promise((resolve13) => {
2236
2365
  let data = "";
2237
2366
  process.stdin.setEncoding("utf-8");
2238
2367
  process.stdin.on("data", (chunk) => data += chunk);
2239
- process.stdin.on("end", () => resolve12(data));
2240
- setTimeout(() => resolve12(data), 500);
2368
+ process.stdin.on("end", () => resolve13(data));
2369
+ setTimeout(() => resolve13(data), 500);
2241
2370
  });
2242
2371
  }
2243
2372
 
2244
2373
  // src/commands/mcp.ts
2245
2374
  init_esm_shims();
2246
- import * as path17 from "path";
2375
+ import * as path18 from "path";
2247
2376
  import chalk4 from "chalk";
2248
2377
 
2249
2378
  // src/mcp/installer.ts
2250
2379
  init_esm_shims();
2251
- import * as fs11 from "fs";
2252
- import * as path13 from "path";
2380
+ import * as fs12 from "fs";
2381
+ import * as path14 from "path";
2253
2382
  var SERVER_NAME = "claudectx";
2254
2383
  var SERVER_ENTRY = {
2255
2384
  command: "claudectx",
@@ -2257,13 +2386,13 @@ var SERVER_ENTRY = {
2257
2386
  type: "stdio"
2258
2387
  };
2259
2388
  function planInstall(projectRoot) {
2260
- const claudeDir = path13.join(projectRoot, ".claude");
2261
- const settingsPath = path13.join(claudeDir, "settings.json");
2262
- const existed = fs11.existsSync(settingsPath);
2389
+ const claudeDir = path14.join(projectRoot, ".claude");
2390
+ const settingsPath = path14.join(claudeDir, "settings.json");
2391
+ const existed = fs12.existsSync(settingsPath);
2263
2392
  let existing = {};
2264
2393
  if (existed) {
2265
2394
  try {
2266
- existing = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
2395
+ existing = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
2267
2396
  } catch {
2268
2397
  existing = {};
2269
2398
  }
@@ -2280,21 +2409,21 @@ function planInstall(projectRoot) {
2280
2409
  return { settingsPath, existed, alreadyInstalled, mergedSettings };
2281
2410
  }
2282
2411
  function applyInstall(result) {
2283
- const dir = path13.dirname(result.settingsPath);
2284
- if (!fs11.existsSync(dir)) {
2285
- fs11.mkdirSync(dir, { recursive: true });
2412
+ const dir = path14.dirname(result.settingsPath);
2413
+ if (!fs12.existsSync(dir)) {
2414
+ fs12.mkdirSync(dir, { recursive: true });
2286
2415
  }
2287
- fs11.writeFileSync(
2416
+ fs12.writeFileSync(
2288
2417
  result.settingsPath,
2289
2418
  JSON.stringify(result.mergedSettings, null, 2) + "\n",
2290
2419
  "utf-8"
2291
2420
  );
2292
2421
  }
2293
2422
  function isInstalled(projectRoot) {
2294
- const settingsPath = path13.join(projectRoot, ".claude", "settings.json");
2295
- if (!fs11.existsSync(settingsPath)) return false;
2423
+ const settingsPath = path14.join(projectRoot, ".claude", "settings.json");
2424
+ if (!fs12.existsSync(settingsPath)) return false;
2296
2425
  try {
2297
- const settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
2426
+ const settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
2298
2427
  return SERVER_NAME in (settings.mcpServers ?? {});
2299
2428
  } catch {
2300
2429
  return false;
@@ -2303,7 +2432,7 @@ function isInstalled(projectRoot) {
2303
2432
 
2304
2433
  // src/commands/mcp.ts
2305
2434
  async function mcpCommand(options) {
2306
- const projectRoot = options.path ? path17.resolve(options.path) : process.cwd();
2435
+ const projectRoot = options.path ? path18.resolve(options.path) : process.cwd();
2307
2436
  if (options.install) {
2308
2437
  await runInstall(projectRoot);
2309
2438
  return;
@@ -2352,12 +2481,12 @@ async function runInstall(projectRoot) {
2352
2481
  // src/commands/compress.ts
2353
2482
  init_esm_shims();
2354
2483
  init_session_reader();
2355
- import * as path19 from "path";
2356
- import * as fs16 from "fs";
2484
+ import * as path20 from "path";
2485
+ import * as fs17 from "fs";
2357
2486
 
2358
2487
  // src/compressor/session-parser.ts
2359
2488
  init_esm_shims();
2360
- import * as fs14 from "fs";
2489
+ import * as fs15 from "fs";
2361
2490
  function extractText(content) {
2362
2491
  if (!content) return "";
2363
2492
  if (typeof content === "string") return content;
@@ -2368,10 +2497,10 @@ function extractToolCalls(content) {
2368
2497
  return content.filter((b) => b.type === "tool_use" && b.name).map((b) => ({ tool: b.name, input: b.input ?? {} }));
2369
2498
  }
2370
2499
  function parseSessionFile(sessionFilePath) {
2371
- if (!fs14.existsSync(sessionFilePath)) return null;
2500
+ if (!fs15.existsSync(sessionFilePath)) return null;
2372
2501
  let content;
2373
2502
  try {
2374
- content = fs14.readFileSync(sessionFilePath, "utf-8");
2503
+ content = fs15.readFileSync(sessionFilePath, "utf-8");
2375
2504
  } catch {
2376
2505
  return null;
2377
2506
  }
@@ -2571,13 +2700,13 @@ function calcCost(inputTokens, outputTokens) {
2571
2700
 
2572
2701
  // src/compressor/memory-writer.ts
2573
2702
  init_esm_shims();
2574
- import * as fs15 from "fs";
2575
- import * as path18 from "path";
2703
+ import * as fs16 from "fs";
2704
+ import * as path19 from "path";
2576
2705
  function parseMemoryFile(filePath) {
2577
- if (!fs15.existsSync(filePath)) {
2706
+ if (!fs16.existsSync(filePath)) {
2578
2707
  return { preamble: "", entries: [] };
2579
2708
  }
2580
- const content = fs15.readFileSync(filePath, "utf-8");
2709
+ const content = fs16.readFileSync(filePath, "utf-8");
2581
2710
  const markerRegex = /<!-- claudectx-entry: (\d{4}-\d{2}-\d{2}) \| session: ([a-z0-9-]+) -->/g;
2582
2711
  const indices = [];
2583
2712
  let match;
@@ -2628,15 +2757,15 @@ function appendEntry(memoryFilePath, sessionId, summaryText, date = /* @__PURE__
2628
2757
  const newBlock = buildEntryBlock(sessionId, summaryText, date);
2629
2758
  const allBlocks = [...entries.map((e) => e.raw), newBlock];
2630
2759
  const newContent = (preamble.trimEnd() ? preamble.trimEnd() + "\n\n" : "") + allBlocks.join("\n\n") + "\n";
2631
- const dir = path18.dirname(memoryFilePath);
2632
- if (!fs15.existsSync(dir)) {
2633
- fs15.mkdirSync(dir, { recursive: true });
2760
+ const dir = path19.dirname(memoryFilePath);
2761
+ if (!fs16.existsSync(dir)) {
2762
+ fs16.mkdirSync(dir, { recursive: true });
2634
2763
  }
2635
- fs15.writeFileSync(memoryFilePath, newContent, "utf-8");
2764
+ fs16.writeFileSync(memoryFilePath, newContent, "utf-8");
2636
2765
  return newContent;
2637
2766
  }
2638
2767
  function pruneOldEntries(memoryFilePath, days) {
2639
- if (!fs15.existsSync(memoryFilePath)) {
2768
+ if (!fs16.existsSync(memoryFilePath)) {
2640
2769
  return { removed: 0, kept: 0, removedEntries: [] };
2641
2770
  }
2642
2771
  const { preamble, entries } = parseMemoryFile(memoryFilePath);
@@ -2649,7 +2778,7 @@ function pruneOldEntries(memoryFilePath, days) {
2649
2778
  return { removed: 0, kept: kept.length, removedEntries: [] };
2650
2779
  }
2651
2780
  const newContent = (preamble.trimEnd() ? preamble.trimEnd() + "\n\n" : "") + kept.map((e) => e.raw).join("\n\n") + (kept.length > 0 ? "\n" : "");
2652
- fs15.writeFileSync(memoryFilePath, newContent, "utf-8");
2781
+ fs16.writeFileSync(memoryFilePath, newContent, "utf-8");
2653
2782
  return { removed: removed.length, kept: kept.length, removedEntries: removed };
2654
2783
  }
2655
2784
  function isAlreadyCompressed(memoryFilePath, sessionId) {
@@ -2660,8 +2789,8 @@ function isAlreadyCompressed(memoryFilePath, sessionId) {
2660
2789
  // src/commands/compress.ts
2661
2790
  async function compressCommand(options) {
2662
2791
  const chalk5 = (await import("chalk")).default;
2663
- const projectRoot = options.path ? path19.resolve(options.path) : process.cwd();
2664
- const memoryFilePath = path19.join(projectRoot, "MEMORY.md");
2792
+ const projectRoot = options.path ? path20.resolve(options.path) : process.cwd();
2793
+ const memoryFilePath = path20.join(projectRoot, "MEMORY.md");
2665
2794
  const sessionFiles = listSessionFiles();
2666
2795
  if (sessionFiles.length === 0) {
2667
2796
  process.stdout.write(chalk5.red("No Claude Code sessions found.\n"));
@@ -2686,7 +2815,7 @@ async function compressCommand(options) {
2686
2815
  } else {
2687
2816
  targetFile = sessionFiles[0].filePath;
2688
2817
  }
2689
- const sessionId = path19.basename(targetFile, ".jsonl");
2818
+ const sessionId = path20.basename(targetFile, ".jsonl");
2690
2819
  if (isAlreadyCompressed(memoryFilePath, sessionId)) {
2691
2820
  if (!options.auto) {
2692
2821
  process.stdout.write(chalk5.yellow(`Session ${sessionId.slice(0, 8)}\u2026 is already in MEMORY.md \u2014 skipping.
@@ -2734,11 +2863,27 @@ async function compressCommand(options) {
2734
2863
  }
2735
2864
  if (options.prune) {
2736
2865
  const days = parseInt(options.days ?? "30", 10);
2737
- if (!fs16.existsSync(memoryFilePath)) return;
2866
+ if (!fs17.existsSync(memoryFilePath)) return;
2867
+ if (!options.auto) {
2868
+ let confirmed = true;
2869
+ try {
2870
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
2871
+ confirmed = await confirm2({
2872
+ message: `Prune MEMORY.md entries older than ${days} days? Run 'claudectx revert' to undo.`,
2873
+ default: false
2874
+ });
2875
+ } catch {
2876
+ }
2877
+ if (!confirmed) {
2878
+ process.stdout.write(chalk5.dim("Prune skipped.\n"));
2879
+ return;
2880
+ }
2881
+ }
2882
+ await backupFile(memoryFilePath, "compress");
2738
2883
  const pruned = pruneOldEntries(memoryFilePath, days);
2739
2884
  if (pruned.removed > 0 && !options.auto) {
2740
2885
  process.stdout.write(
2741
- chalk5.dim(`Pruned ${pruned.removed} entr${pruned.removed === 1 ? "y" : "ies"} older than ${days} days.
2886
+ chalk5.dim(`Pruned ${pruned.removed} entr${pruned.removed === 1 ? "y" : "ies"} older than ${days} days. Run 'claudectx revert' to undo.
2742
2887
  `)
2743
2888
  );
2744
2889
  }
@@ -3016,13 +3161,13 @@ async function reportCommand(options) {
3016
3161
 
3017
3162
  // src/commands/budget.ts
3018
3163
  init_esm_shims();
3019
- import * as path21 from "path";
3164
+ import * as path22 from "path";
3020
3165
 
3021
3166
  // src/analyzer/budget-estimator.ts
3022
3167
  init_esm_shims();
3023
3168
  init_tokenizer();
3024
- import * as fs17 from "fs";
3025
- import * as path20 from "path";
3169
+ import * as fs18 from "fs";
3170
+ import * as path21 from "path";
3026
3171
  import { glob as glob2 } from "glob";
3027
3172
  init_session_store();
3028
3173
  function resolveGlobs(globs, projectRoot) {
@@ -3047,20 +3192,20 @@ function classifyCacheHit(recentReadCount) {
3047
3192
  return "low";
3048
3193
  }
3049
3194
  function suggestClaudeignoreAdditions(files, projectRoot) {
3050
- const ignorePath = path20.join(projectRoot, ".claudeignore");
3195
+ const ignorePath = path21.join(projectRoot, ".claudeignore");
3051
3196
  let ignorePatterns = [];
3052
3197
  try {
3053
- const content = fs17.readFileSync(ignorePath, "utf-8");
3198
+ const content = fs18.readFileSync(ignorePath, "utf-8");
3054
3199
  ignorePatterns = content.split("\n").filter(Boolean);
3055
3200
  } catch {
3056
3201
  }
3057
3202
  const recommendations = [];
3058
3203
  for (const file of files) {
3059
3204
  if (file.tokenCount <= WASTE_THRESHOLDS.MAX_REFERENCE_FILE_TOKENS) continue;
3060
- const rel = path20.relative(projectRoot, file.filePath);
3205
+ const rel = path21.relative(projectRoot, file.filePath);
3061
3206
  const alreadyIgnored = ignorePatterns.some((pattern) => {
3062
3207
  const cleanPattern = pattern.replace(/^!/, "");
3063
- return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g, path20.sep));
3208
+ return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g, path21.sep));
3064
3209
  });
3065
3210
  if (!alreadyIgnored) {
3066
3211
  recommendations.push(rel);
@@ -3080,7 +3225,7 @@ async function estimateBudget(globs, projectRoot, model, thresholdTokens) {
3080
3225
  for (const filePath of filePaths) {
3081
3226
  let content = "";
3082
3227
  try {
3083
- content = fs17.readFileSync(filePath, "utf-8");
3228
+ content = fs18.readFileSync(filePath, "utf-8");
3084
3229
  } catch {
3085
3230
  continue;
3086
3231
  }
@@ -3129,7 +3274,7 @@ function formatBudgetReport(report) {
3129
3274
  }
3130
3275
  const LIKELIHOOD_ICON = { high: "\u{1F7E2}", medium: "\u{1F7E1}", low: "\u{1F534}" };
3131
3276
  const maxPathLen = Math.min(
3132
- Math.max(...report.files.map((f) => path20.basename(f.filePath).length)),
3277
+ Math.max(...report.files.map((f) => path21.basename(f.filePath).length)),
3133
3278
  40
3134
3279
  );
3135
3280
  lines.push(
@@ -3137,7 +3282,7 @@ function formatBudgetReport(report) {
3137
3282
  );
3138
3283
  lines.push("\u2500".repeat(50));
3139
3284
  for (const file of report.files.slice(0, 20)) {
3140
- const name = path20.basename(file.filePath).slice(0, maxPathLen).padEnd(maxPathLen);
3285
+ const name = path21.basename(file.filePath).slice(0, maxPathLen).padEnd(maxPathLen);
3141
3286
  const tokens = file.tokenCount.toLocaleString().padStart(7);
3142
3287
  const cache = `${LIKELIHOOD_ICON[file.cacheHitLikelihood]} ${file.cacheHitLikelihood.padEnd(6)}`;
3143
3288
  const cost = formatCost(file.estimatedCostUsd).padStart(7);
@@ -3166,7 +3311,7 @@ function formatBudgetReport(report) {
3166
3311
  // src/commands/budget.ts
3167
3312
  init_models();
3168
3313
  async function budgetCommand(globs, options) {
3169
- const projectPath = options.path ? path21.resolve(options.path) : process.cwd();
3314
+ const projectPath = options.path ? path22.resolve(options.path) : process.cwd();
3170
3315
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3171
3316
  const model = resolveModel(options.model ?? "sonnet");
3172
3317
  const thresholdTokens = parseInt(options.threshold ?? "10000", 10);
@@ -3185,10 +3330,10 @@ async function budgetCommand(globs, options) {
3185
3330
 
3186
3331
  // src/commands/warmup.ts
3187
3332
  init_esm_shims();
3188
- import * as path22 from "path";
3333
+ import * as path23 from "path";
3189
3334
  import Anthropic from "@anthropic-ai/sdk";
3190
3335
  init_models();
3191
- import fs18 from "fs";
3336
+ import fs19 from "fs";
3192
3337
  function buildWarmupMessages(claudeMdContent) {
3193
3338
  const systemBlock = {
3194
3339
  type: "text",
@@ -3259,6 +3404,19 @@ async function installCron(cronExpr) {
3259
3404
  );
3260
3405
  process.exit(1);
3261
3406
  }
3407
+ let confirmed = true;
3408
+ try {
3409
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
3410
+ confirmed = await confirm2({
3411
+ message: `Install cron job "${cronExpr} claudectx warmup" in your system crontab?`,
3412
+ default: false
3413
+ });
3414
+ } catch {
3415
+ }
3416
+ if (!confirmed) {
3417
+ process.stdout.write(" Cron install cancelled.\n");
3418
+ return;
3419
+ }
3262
3420
  const { execSync: execSync3 } = await import("child_process");
3263
3421
  const command = `claudectx warmup`;
3264
3422
  const cronLine = `${cronExpr} ${command}`;
@@ -3281,16 +3439,16 @@ async function installCron(cronExpr) {
3281
3439
  ${marker}
3282
3440
  ${cronLine}
3283
3441
  `;
3284
- const { writeFileSync: writeFileSync11, unlinkSync: unlinkSync2 } = await import("fs");
3442
+ const { writeFileSync: writeFileSync12, unlinkSync: unlinkSync3 } = await import("fs");
3285
3443
  const { tmpdir } = await import("os");
3286
- const { join: join17 } = await import("path");
3287
- const tmpFile = join17(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
3444
+ const { join: join18 } = await import("path");
3445
+ const tmpFile = join18(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
3288
3446
  try {
3289
- writeFileSync11(tmpFile, newCrontab, "utf-8");
3447
+ writeFileSync12(tmpFile, newCrontab, "utf-8");
3290
3448
  execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
3291
3449
  } finally {
3292
3450
  try {
3293
- unlinkSync2(tmpFile);
3451
+ unlinkSync3(tmpFile);
3294
3452
  } catch {
3295
3453
  }
3296
3454
  }
@@ -3307,7 +3465,7 @@ ${cronLine}
3307
3465
  }
3308
3466
  }
3309
3467
  async function warmupCommand(options) {
3310
- const projectPath = options.path ? path22.resolve(options.path) : process.cwd();
3468
+ const projectPath = options.path ? path23.resolve(options.path) : process.cwd();
3311
3469
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3312
3470
  const model = resolveModel(options.model ?? "haiku");
3313
3471
  const ttl = options.ttl === "60" ? 60 : 5;
@@ -3319,9 +3477,9 @@ async function warmupCommand(options) {
3319
3477
  process.exit(1);
3320
3478
  }
3321
3479
  let claudeMdContent = "";
3322
- const claudeMdPath = path22.join(projectRoot, "CLAUDE.md");
3480
+ const claudeMdPath = path23.join(projectRoot, "CLAUDE.md");
3323
3481
  try {
3324
- claudeMdContent = fs18.readFileSync(claudeMdPath, "utf-8");
3482
+ claudeMdContent = fs19.readFileSync(claudeMdPath, "utf-8");
3325
3483
  } catch {
3326
3484
  process.stderr.write(`Warning: No CLAUDE.md found at ${claudeMdPath}
3327
3485
  `);
@@ -3379,15 +3537,15 @@ async function warmupCommand(options) {
3379
3537
 
3380
3538
  // src/commands/drift.ts
3381
3539
  init_esm_shims();
3382
- import * as path24 from "path";
3383
- import * as fs20 from "fs";
3540
+ import * as path25 from "path";
3541
+ import * as fs21 from "fs";
3384
3542
 
3385
3543
  // src/analyzer/drift-detector.ts
3386
3544
  init_esm_shims();
3387
3545
  init_tokenizer();
3388
3546
  init_session_store();
3389
- import * as fs19 from "fs";
3390
- import * as path23 from "path";
3547
+ import * as fs20 from "fs";
3548
+ import * as path24 from "path";
3391
3549
  import * as childProcess from "child_process";
3392
3550
  var INLINE_PATH_RE = /(?:^|\s)((?:\.{1,2}\/|src\/|lib\/|docs\/|app\/|tests?\/)\S+\.\w{1,6})/gm;
3393
3551
  var AT_REF_RE = /^@(.+)$/;
@@ -3398,8 +3556,8 @@ function findDeadAtReferences(content, projectRoot) {
3398
3556
  const match = lines[i].match(AT_REF_RE);
3399
3557
  if (!match) continue;
3400
3558
  const ref = match[1].trim();
3401
- const absPath = path23.isAbsolute(ref) ? ref : path23.join(projectRoot, ref);
3402
- if (!fs19.existsSync(absPath)) {
3559
+ const absPath = path24.isAbsolute(ref) ? ref : path24.join(projectRoot, ref);
3560
+ if (!fs20.existsSync(absPath)) {
3403
3561
  const lineText = lines[i];
3404
3562
  issues.push({
3405
3563
  type: "dead-ref",
@@ -3432,15 +3590,15 @@ async function findGitDeletedMentions(content, projectRoot) {
3432
3590
  for (let i = 0; i < lines.length; i++) {
3433
3591
  const line = lines[i];
3434
3592
  for (const deleted of deletedFiles) {
3435
- const basename8 = path23.basename(deleted);
3436
- if (line.includes(basename8) || line.includes(deleted)) {
3593
+ const basename10 = path24.basename(deleted);
3594
+ if (line.includes(basename10) || line.includes(deleted)) {
3437
3595
  issues.push({
3438
3596
  type: "git-deleted",
3439
3597
  line: i + 1,
3440
3598
  text: line.trim(),
3441
3599
  severity: "warning",
3442
3600
  estimatedTokenWaste: countTokens(line),
3443
- suggestion: `References "${basename8}" which was deleted from git. Consider removing this mention.`
3601
+ suggestion: `References "${basename10}" which was deleted from git. Consider removing this mention.`
3444
3602
  });
3445
3603
  break;
3446
3604
  }
@@ -3505,8 +3663,8 @@ function findDeadInlinePaths(content, projectRoot) {
3505
3663
  const rawPath = match[1].trim();
3506
3664
  if (seen.has(rawPath)) continue;
3507
3665
  seen.add(rawPath);
3508
- const absPath = path23.isAbsolute(rawPath) ? rawPath : path23.join(projectRoot, rawPath);
3509
- if (!fs19.existsSync(absPath)) {
3666
+ const absPath = path24.isAbsolute(rawPath) ? rawPath : path24.join(projectRoot, rawPath);
3667
+ if (!fs20.existsSync(absPath)) {
3510
3668
  issues.push({
3511
3669
  type: "dead-inline-path",
3512
3670
  line: i + 1,
@@ -3522,10 +3680,10 @@ function findDeadInlinePaths(content, projectRoot) {
3522
3680
  return issues;
3523
3681
  }
3524
3682
  async function detectDrift(projectRoot, dayWindow) {
3525
- const claudeMdPath = path23.join(projectRoot, "CLAUDE.md");
3683
+ const claudeMdPath = path24.join(projectRoot, "CLAUDE.md");
3526
3684
  let content = "";
3527
3685
  try {
3528
- content = fs19.readFileSync(claudeMdPath, "utf-8");
3686
+ content = fs20.readFileSync(claudeMdPath, "utf-8");
3529
3687
  } catch {
3530
3688
  return {
3531
3689
  claudeMdPath,
@@ -3567,7 +3725,7 @@ var TYPE_LABEL = {
3567
3725
  "dead-inline-path": "Dead path"
3568
3726
  };
3569
3727
  async function driftCommand(options) {
3570
- const projectPath = options.path ? path24.resolve(options.path) : process.cwd();
3728
+ const projectPath = options.path ? path25.resolve(options.path) : process.cwd();
3571
3729
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3572
3730
  const dayWindow = parseInt(options.days ?? "30", 10);
3573
3731
  const report = await detectDrift(projectRoot, dayWindow);
@@ -3643,25 +3801,25 @@ async function applyFix(claudeMdPath, issues) {
3643
3801
  process.stdout.write("No lines selected. Nothing changed.\n");
3644
3802
  return;
3645
3803
  }
3646
- const content = fs20.readFileSync(claudeMdPath, "utf-8");
3804
+ const content = fs21.readFileSync(claudeMdPath, "utf-8");
3647
3805
  const lines = content.split("\n");
3648
3806
  const lineSet = new Set(selectedLines.map((l) => l - 1));
3649
3807
  const newLines = lines.filter((_, i) => !lineSet.has(i));
3650
3808
  const newContent = newLines.join("\n");
3651
3809
  const backupPath = `${claudeMdPath}.bak`;
3652
- fs20.writeFileSync(backupPath, content, "utf-8");
3653
- const os5 = await import("os");
3810
+ fs21.writeFileSync(backupPath, content, "utf-8");
3811
+ const os6 = await import("os");
3654
3812
  const tmpPath = `${claudeMdPath}.tmp-${Date.now()}`;
3655
3813
  try {
3656
- fs20.writeFileSync(tmpPath, newContent, "utf-8");
3657
- fs20.renameSync(tmpPath, claudeMdPath);
3814
+ fs21.writeFileSync(tmpPath, newContent, "utf-8");
3815
+ fs21.renameSync(tmpPath, claudeMdPath);
3658
3816
  } catch (err) {
3659
3817
  try {
3660
- fs20.copyFileSync(backupPath, claudeMdPath);
3818
+ fs21.copyFileSync(backupPath, claudeMdPath);
3661
3819
  } catch {
3662
3820
  }
3663
3821
  try {
3664
- fs20.unlinkSync(tmpPath);
3822
+ fs21.unlinkSync(tmpPath);
3665
3823
  } catch {
3666
3824
  }
3667
3825
  process.stderr.write(`Error writing CLAUDE.md: ${err instanceof Error ? err.message : String(err)}
@@ -3669,18 +3827,18 @@ async function applyFix(claudeMdPath, issues) {
3669
3827
  process.exit(1);
3670
3828
  }
3671
3829
  process.stdout.write(`
3672
- \u2713 Removed ${selectedLines.length} line(s) from ${path24.basename(claudeMdPath)}
3830
+ \u2713 Removed ${selectedLines.length} line(s) from ${path25.basename(claudeMdPath)}
3673
3831
  `);
3674
3832
  process.stdout.write(` \u2713 Backup saved to ${backupPath}
3675
3833
 
3676
3834
  `);
3677
- void os5;
3835
+ void os6;
3678
3836
  }
3679
3837
 
3680
3838
  // src/commands/hooks.ts
3681
3839
  init_esm_shims();
3682
- import * as fs21 from "fs";
3683
- import * as path25 from "path";
3840
+ import * as fs22 from "fs";
3841
+ import * as path26 from "path";
3684
3842
 
3685
3843
  // src/hooks/registry.ts
3686
3844
  init_esm_shims();
@@ -3756,17 +3914,17 @@ function buildHookEntry(def, config) {
3756
3914
 
3757
3915
  // src/commands/hooks.ts
3758
3916
  function readInstalledHooks(projectRoot) {
3759
- const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
3760
- if (!fs21.existsSync(settingsPath)) return {};
3917
+ const settingsPath = path26.join(projectRoot, ".claude", "settings.local.json");
3918
+ if (!fs22.existsSync(settingsPath)) return {};
3761
3919
  try {
3762
- return JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
3920
+ return JSON.parse(fs22.readFileSync(settingsPath, "utf-8"));
3763
3921
  } catch {
3764
3922
  process.stderr.write(
3765
3923
  `Warning: ${settingsPath} exists but contains invalid JSON. Existing settings will be preserved as a backup.
3766
3924
  `
3767
3925
  );
3768
3926
  try {
3769
- fs21.copyFileSync(settingsPath, `${settingsPath}.bak`);
3927
+ fs22.copyFileSync(settingsPath, `${settingsPath}.bak`);
3770
3928
  } catch {
3771
3929
  }
3772
3930
  return {};
@@ -3886,9 +4044,26 @@ async function hooksAdd(name, projectRoot, configPairs) {
3886
4044
  }
3887
4045
  async function hooksRemove(name, projectRoot) {
3888
4046
  const settings = readInstalledHooks(projectRoot);
4047
+ let confirmed = true;
4048
+ try {
4049
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
4050
+ confirmed = await confirm2({
4051
+ message: `Remove hook "${name}"? Run 'claudectx revert' to undo.`,
4052
+ default: false
4053
+ });
4054
+ } catch {
4055
+ }
4056
+ if (!confirmed) {
4057
+ process.stdout.write(" Cancelled.\n\n");
4058
+ return;
4059
+ }
4060
+ const settingsPath = path26.join(projectRoot, ".claude", "settings.local.json");
4061
+ if (fs22.existsSync(settingsPath)) {
4062
+ await backupFile(settingsPath, "hooks");
4063
+ }
3889
4064
  const updated = removeHookByName(settings, name);
3890
4065
  writeHooksSettings(projectRoot, updated);
3891
- process.stdout.write(` \u2713 Hook "${name}" removed.
4066
+ process.stdout.write(` \u2713 Hook "${name}" removed. Run 'claudectx revert --list' to undo.
3892
4067
 
3893
4068
  `);
3894
4069
  }
@@ -3913,7 +4088,7 @@ async function hooksStatus(projectRoot) {
3913
4088
  process.stdout.write("\n");
3914
4089
  }
3915
4090
  async function hooksCommand(subcommand, options) {
3916
- const projectPath = options.path ? path25.resolve(options.path) : process.cwd();
4091
+ const projectPath = options.path ? path26.resolve(options.path) : process.cwd();
3917
4092
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
3918
4093
  const sub = subcommand ?? "list";
3919
4094
  switch (sub) {
@@ -3952,8 +4127,8 @@ async function hooksCommand(subcommand, options) {
3952
4127
 
3953
4128
  // src/commands/convert.ts
3954
4129
  init_esm_shims();
3955
- import * as fs22 from "fs";
3956
- import * as path26 from "path";
4130
+ import * as fs23 from "fs";
4131
+ import * as path27 from "path";
3957
4132
  function slugify2(text) {
3958
4133
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3959
4134
  }
@@ -4002,7 +4177,7 @@ function claudeMdToWindsurf(content) {
4002
4177
  return content.split("\n").filter((line) => !line.match(/^@.+$/)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
4003
4178
  }
4004
4179
  async function convertCommand(options) {
4005
- const projectPath = options.path ? path26.resolve(options.path) : process.cwd();
4180
+ const projectPath = options.path ? path27.resolve(options.path) : process.cwd();
4006
4181
  const projectRoot = findProjectRoot(projectPath) ?? projectPath;
4007
4182
  const from = options.from ?? "claude";
4008
4183
  const to = options.to;
@@ -4011,10 +4186,10 @@ async function convertCommand(options) {
4011
4186
  `);
4012
4187
  process.exit(1);
4013
4188
  }
4014
- const claudeMdPath = path26.join(projectRoot, "CLAUDE.md");
4189
+ const claudeMdPath = path27.join(projectRoot, "CLAUDE.md");
4015
4190
  let content = "";
4016
4191
  try {
4017
- content = fs22.readFileSync(claudeMdPath, "utf-8");
4192
+ content = fs23.readFileSync(claudeMdPath, "utf-8");
4018
4193
  } catch {
4019
4194
  process.stderr.write(`Error: CLAUDE.md not found at ${claudeMdPath}
4020
4195
  `);
@@ -4022,33 +4197,45 @@ async function convertCommand(options) {
4022
4197
  }
4023
4198
  if (to === "cursor") {
4024
4199
  const files = claudeMdToCursorRules(content);
4025
- const targetDir = path26.join(projectRoot, ".cursor", "rules");
4200
+ const targetDir = path27.join(projectRoot, ".cursor", "rules");
4026
4201
  process.stdout.write(`
4027
4202
  Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
4028
-
4029
4203
  `);
4204
+ if (!options.dryRun) {
4205
+ process.stdout.write(` \u26A0 Existing .mdc files will be overwritten. A backup is saved automatically.
4206
+ `);
4207
+ process.stdout.write(` Run 'claudectx revert --list' to see backups.
4208
+ `);
4209
+ }
4210
+ process.stdout.write("\n");
4030
4211
  for (const file of files) {
4031
- const filePath = path26.join(targetDir, file.filename);
4032
- const exists = fs22.existsSync(filePath);
4212
+ const filePath = path27.join(targetDir, file.filename);
4213
+ const exists = fs23.existsSync(filePath);
4033
4214
  const prefix = options.dryRun ? "[dry-run] " : exists ? "[overwrite] " : "";
4034
4215
  process.stdout.write(` ${prefix}\u2192 .cursor/rules/${file.filename}
4035
4216
  `);
4036
4217
  if (!options.dryRun) {
4037
- fs22.mkdirSync(targetDir, { recursive: true });
4038
- fs22.writeFileSync(filePath, file.content, "utf-8");
4218
+ fs23.mkdirSync(targetDir, { recursive: true });
4219
+ if (exists) await backupFile(filePath, "convert");
4220
+ fs23.writeFileSync(filePath, file.content, "utf-8");
4039
4221
  }
4040
4222
  }
4041
4223
  process.stdout.write("\n");
4042
4224
  } else if (to === "copilot") {
4043
4225
  const converted = claudeMdToCopilot(content);
4044
- const targetPath = path26.join(projectRoot, ".github", "copilot-instructions.md");
4045
- const exists = fs22.existsSync(targetPath);
4226
+ const targetPath = path27.join(projectRoot, ".github", "copilot-instructions.md");
4227
+ const exists = fs23.existsSync(targetPath);
4046
4228
  process.stdout.write(`
4047
4229
  Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwrite]" : ""}
4048
4230
  `);
4231
+ if (!options.dryRun && exists) {
4232
+ process.stdout.write(` \u26A0 Existing file will be overwritten. Run 'claudectx revert --list' to undo.
4233
+ `);
4234
+ }
4049
4235
  if (!options.dryRun) {
4050
- fs22.mkdirSync(path26.dirname(targetPath), { recursive: true });
4051
- fs22.writeFileSync(targetPath, converted, "utf-8");
4236
+ fs23.mkdirSync(path27.dirname(targetPath), { recursive: true });
4237
+ if (exists) await backupFile(targetPath, "convert");
4238
+ fs23.writeFileSync(targetPath, converted, "utf-8");
4052
4239
  process.stdout.write(` \u2713 Written to ${targetPath}
4053
4240
 
4054
4241
  `);
@@ -4059,13 +4246,18 @@ Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwri
4059
4246
  }
4060
4247
  } else if (to === "windsurf") {
4061
4248
  const converted = claudeMdToWindsurf(content);
4062
- const targetPath = path26.join(projectRoot, ".windsurfrules");
4063
- const exists = fs22.existsSync(targetPath);
4249
+ const targetPath = path27.join(projectRoot, ".windsurfrules");
4250
+ const exists = fs23.existsSync(targetPath);
4064
4251
  process.stdout.write(`
4065
4252
  Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
4066
4253
  `);
4254
+ if (!options.dryRun && exists) {
4255
+ process.stdout.write(` \u26A0 Existing file will be overwritten. Run 'claudectx revert --list' to undo.
4256
+ `);
4257
+ }
4067
4258
  if (!options.dryRun) {
4068
- fs22.writeFileSync(targetPath, converted, "utf-8");
4259
+ if (exists) await backupFile(targetPath, "convert");
4260
+ fs23.writeFileSync(targetPath, converted, "utf-8");
4069
4261
  process.stdout.write(` \u2713 Written to ${targetPath}
4070
4262
 
4071
4263
  `);
@@ -4084,17 +4276,17 @@ Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
4084
4276
  // src/commands/teams.ts
4085
4277
  init_esm_shims();
4086
4278
  init_models();
4087
- import * as path28 from "path";
4088
- import * as fs24 from "fs";
4279
+ import * as path29 from "path";
4280
+ import * as fs25 from "fs";
4089
4281
 
4090
4282
  // src/reporter/team-aggregator.ts
4091
4283
  init_esm_shims();
4092
4284
  init_session_reader();
4093
4285
  init_session_store();
4094
4286
  init_models();
4095
- import * as fs23 from "fs";
4096
- import * as path27 from "path";
4097
- import * as os4 from "os";
4287
+ import * as fs24 from "fs";
4288
+ import * as path28 from "path";
4289
+ import * as os5 from "os";
4098
4290
  import * as childProcess2 from "child_process";
4099
4291
  function getDeveloperIdentity() {
4100
4292
  try {
@@ -4102,7 +4294,7 @@ function getDeveloperIdentity() {
4102
4294
  if (email) return email;
4103
4295
  } catch {
4104
4296
  }
4105
- return os4.hostname();
4297
+ return os5.hostname();
4106
4298
  }
4107
4299
  function calcCost3(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
4108
4300
  const p = MODEL_PRICING[model];
@@ -4219,19 +4411,19 @@ function aggregateTeamReports(exports) {
4219
4411
  }
4220
4412
  function writeTeamExport(exportData) {
4221
4413
  const storeDir = getStoreDir();
4222
- if (!fs23.existsSync(storeDir)) fs23.mkdirSync(storeDir, { recursive: true });
4414
+ if (!fs24.existsSync(storeDir)) fs24.mkdirSync(storeDir, { recursive: true });
4223
4415
  const date = isoDate2(/* @__PURE__ */ new Date());
4224
- const filePath = path27.join(storeDir, `team-export-${date}.json`);
4225
- fs23.writeFileSync(filePath, JSON.stringify(exportData, null, 2), "utf-8");
4416
+ const filePath = path28.join(storeDir, `team-export-${date}.json`);
4417
+ fs24.writeFileSync(filePath, JSON.stringify(exportData, null, 2), "utf-8");
4226
4418
  return filePath;
4227
4419
  }
4228
4420
  function readTeamExports(dir) {
4229
4421
  const exports = [];
4230
- if (!fs23.existsSync(dir)) return exports;
4231
- const files = fs23.readdirSync(dir).filter((f) => f.match(/^team-export-.*\.json$/));
4422
+ if (!fs24.existsSync(dir)) return exports;
4423
+ const files = fs24.readdirSync(dir).filter((f) => f.match(/^team-export-.*\.json$/));
4232
4424
  for (const file of files) {
4233
4425
  try {
4234
- const raw = fs23.readFileSync(path27.join(dir, file), "utf-8");
4426
+ const raw = fs24.readFileSync(path28.join(dir, file), "utf-8");
4235
4427
  exports.push(JSON.parse(raw));
4236
4428
  } catch {
4237
4429
  }
@@ -4318,7 +4510,7 @@ Run "claudectx teams export" on each developer machine first.
4318
4510
  process.stdout.write(" Top shared files (by read count across team):\n");
4319
4511
  for (const f of report.topWasteFiles.slice(0, 5)) {
4320
4512
  const devList = f.developers.slice(0, 3).join(", ");
4321
- process.stdout.write(` ${f.readCount}x ${path28.basename(f.filePath)} (${devList})
4513
+ process.stdout.write(` ${f.readCount}x ${path29.basename(f.filePath)} (${devList})
4322
4514
  `);
4323
4515
  }
4324
4516
  process.stdout.write("\n");
@@ -4331,24 +4523,24 @@ async function teamsShare(options) {
4331
4523
  process.exit(1);
4332
4524
  }
4333
4525
  const storeDir = getStoreDir();
4334
- const exportFiles = fs24.readdirSync(storeDir).filter((f) => f.match(/^team-export-.*\.json$/)).sort().reverse();
4526
+ const exportFiles = fs25.readdirSync(storeDir).filter((f) => f.match(/^team-export-.*\.json$/)).sort().reverse();
4335
4527
  if (exportFiles.length === 0) {
4336
4528
  process.stderr.write('No team export files found. Run "claudectx teams export" first.\n');
4337
4529
  process.exit(1);
4338
4530
  }
4339
4531
  const latest = exportFiles[0];
4340
- const src = path28.join(storeDir, latest);
4532
+ const src = path29.join(storeDir, latest);
4341
4533
  let destPath;
4342
4534
  try {
4343
- const stat = fs24.statSync(dest);
4344
- destPath = stat.isDirectory() ? path28.join(dest, latest) : dest;
4535
+ const stat = fs25.statSync(dest);
4536
+ destPath = stat.isDirectory() ? path29.join(dest, latest) : dest;
4345
4537
  } catch {
4346
4538
  destPath = dest;
4347
4539
  }
4348
- const destDir = path28.dirname(path28.resolve(destPath));
4540
+ const destDir = path29.dirname(path29.resolve(destPath));
4349
4541
  let resolvedDir;
4350
4542
  try {
4351
- resolvedDir = fs24.realpathSync(destDir);
4543
+ resolvedDir = fs25.realpathSync(destDir);
4352
4544
  } catch {
4353
4545
  resolvedDir = destDir;
4354
4546
  }
@@ -4358,7 +4550,7 @@ async function teamsShare(options) {
4358
4550
  `);
4359
4551
  process.exit(1);
4360
4552
  }
4361
- fs24.copyFileSync(src, destPath);
4553
+ fs25.copyFileSync(src, destPath);
4362
4554
  process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
4363
4555
 
4364
4556
  `);
@@ -4383,8 +4575,148 @@ async function teamsCommand(subcommand, options) {
4383
4575
  }
4384
4576
  }
4385
4577
 
4578
+ // src/commands/revert.ts
4579
+ init_esm_shims();
4580
+ import * as path30 from "path";
4581
+ function timeAgo(isoString) {
4582
+ const diffMs = Date.now() - new Date(isoString).getTime();
4583
+ const minutes = Math.floor(diffMs / 6e4);
4584
+ if (minutes < 1) return "just now";
4585
+ if (minutes < 60) return `${minutes} min ago`;
4586
+ const hours = Math.floor(minutes / 60);
4587
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
4588
+ const days = Math.floor(hours / 24);
4589
+ return `${days} day${days === 1 ? "" : "s"} ago`;
4590
+ }
4591
+ function formatBytes(bytes) {
4592
+ if (bytes < 1024) return `${bytes} B`;
4593
+ return `${Math.round(bytes / 1024)} KB`;
4594
+ }
4595
+ function printBackupTable(entries) {
4596
+ if (entries.length === 0) {
4597
+ process.stdout.write(" No backups found.\n");
4598
+ process.stdout.write(` Backups are created automatically when claudectx modifies your files.
4599
+ `);
4600
+ process.stdout.write(` Backup directory: ${BACKUP_DIR}
4601
+
4602
+ `);
4603
+ return;
4604
+ }
4605
+ const idWidth = 26;
4606
+ const fileWidth = 20;
4607
+ const cmdWidth = 10;
4608
+ const timeWidth = 14;
4609
+ const sizeWidth = 7;
4610
+ const hr = "\u2550".repeat(idWidth + fileWidth + cmdWidth + timeWidth + sizeWidth + 16);
4611
+ process.stdout.write("\n");
4612
+ process.stdout.write("claudectx \u2014 Backup History\n");
4613
+ process.stdout.write(hr + "\n");
4614
+ process.stdout.write(
4615
+ ` ${"ID".padEnd(idWidth)} ${"File".padEnd(fileWidth)} ${"Command".padEnd(cmdWidth)} ${"When".padEnd(timeWidth)} ${"Size".padEnd(sizeWidth)}
4616
+ `
4617
+ );
4618
+ process.stdout.write("\u2500".repeat(idWidth + fileWidth + cmdWidth + timeWidth + sizeWidth + 16) + "\n");
4619
+ for (const entry of entries) {
4620
+ const id = entry.id.slice(0, idWidth).padEnd(idWidth);
4621
+ const file = path30.basename(entry.originalPath).slice(0, fileWidth).padEnd(fileWidth);
4622
+ const cmd = entry.command.slice(0, cmdWidth).padEnd(cmdWidth);
4623
+ const when = timeAgo(entry.createdAt).slice(0, timeWidth).padEnd(timeWidth);
4624
+ const size = formatBytes(entry.sizeBytes).padEnd(sizeWidth);
4625
+ process.stdout.write(` ${id} ${file} ${cmd} ${when} ${size}
4626
+ `);
4627
+ }
4628
+ process.stdout.write("\n");
4629
+ process.stdout.write(` Backup directory: ${BACKUP_DIR}
4630
+ `);
4631
+ process.stdout.write(" To restore: claudectx revert --id <ID>\n\n");
4632
+ }
4633
+ async function interactivePick(entries) {
4634
+ try {
4635
+ const { select } = await import("@inquirer/prompts");
4636
+ const choices = entries.map((e) => ({
4637
+ name: `${timeAgo(e.createdAt).padEnd(14)} ${path30.basename(e.originalPath).padEnd(16)} [${e.command}] ${e.id}`,
4638
+ value: e.id
4639
+ }));
4640
+ choices.push({ name: "Cancel", value: "" });
4641
+ return await select({ message: "Choose a backup to restore:", choices });
4642
+ } catch {
4643
+ process.stderr.write("Interactive mode unavailable. Use --id <id> to restore a specific backup.\n");
4644
+ return null;
4645
+ }
4646
+ }
4647
+ async function doRestore(id) {
4648
+ const chalk5 = (await import("chalk")).default;
4649
+ process.stdout.write("\n");
4650
+ try {
4651
+ const entries = await listBackups();
4652
+ const entry = entries.find((e) => e.id === id);
4653
+ if (!entry) {
4654
+ process.stderr.write(chalk5.red(`Backup "${id}" not found.
4655
+ `));
4656
+ process.stderr.write('Run "claudectx revert --list" to see available backups.\n');
4657
+ process.exitCode = 1;
4658
+ return;
4659
+ }
4660
+ process.stdout.write(chalk5.yellow(`\u26A0 This will overwrite: ${entry.originalPath}
4661
+ `));
4662
+ process.stdout.write(` Backup from: ${timeAgo(entry.createdAt)} (${entry.command})
4663
+ `);
4664
+ process.stdout.write(` Your current file will be backed up first (so you can undo this).
4665
+
4666
+ `);
4667
+ let confirmed = true;
4668
+ try {
4669
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
4670
+ confirmed = await confirm2({ message: "Restore this backup?", default: false });
4671
+ } catch {
4672
+ }
4673
+ if (!confirmed) {
4674
+ process.stdout.write(" Cancelled.\n\n");
4675
+ return;
4676
+ }
4677
+ const { undoEntry } = await restoreBackup(id);
4678
+ process.stdout.write(chalk5.green(" \u2713 ") + `Restored to ${entry.originalPath}
4679
+ `);
4680
+ if (undoEntry) {
4681
+ process.stdout.write(
4682
+ chalk5.dim(` Your previous version was saved as backup "${undoEntry.id}" \u2014 run 'claudectx revert --id ${undoEntry.id}' to undo.
4683
+ `)
4684
+ );
4685
+ }
4686
+ process.stdout.write("\n");
4687
+ } catch (err) {
4688
+ process.stderr.write(chalk5.red(`Error: ${err instanceof Error ? err.message : String(err)}
4689
+ `));
4690
+ process.exitCode = 1;
4691
+ }
4692
+ }
4693
+ async function revertCommand(options) {
4694
+ const entries = await listBackups(options.file);
4695
+ if (options.json) {
4696
+ process.stdout.write(JSON.stringify({ backups: entries }, null, 2) + "\n");
4697
+ return;
4698
+ }
4699
+ if (options.list) {
4700
+ printBackupTable(entries);
4701
+ return;
4702
+ }
4703
+ if (options.id) {
4704
+ await doRestore(options.id);
4705
+ return;
4706
+ }
4707
+ if (entries.length === 0) {
4708
+ process.stdout.write("\n No backups found. Backups are created automatically when claudectx modifies your files.\n\n");
4709
+ return;
4710
+ }
4711
+ printBackupTable(entries);
4712
+ const picked = await interactivePick(entries);
4713
+ if (picked) {
4714
+ await doRestore(picked);
4715
+ }
4716
+ }
4717
+
4386
4718
  // src/index.ts
4387
- var VERSION = "1.1.2";
4719
+ var VERSION = "1.1.3";
4388
4720
  var DESCRIPTION = "Reduce Claude Code token usage by up to 80%. Context analyzer, auto-optimizer, live dashboard, and smart MCP tools.";
4389
4721
  var program = new Command();
4390
4722
  program.name("claudectx").description(DESCRIPTION).version(VERSION);
@@ -4424,5 +4756,8 @@ program.command("convert").description("Convert CLAUDE.md to another AI assistan
4424
4756
  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) => {
4425
4757
  await teamsCommand(subcommand ?? "export", options);
4426
4758
  });
4759
+ 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) => {
4760
+ await revertCommand(options);
4761
+ });
4427
4762
  program.parse();
4428
4763
  //# sourceMappingURL=index.mjs.map