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/README.md +73 -2
- package/dist/index.js +565 -230
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +560 -225
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
108
|
-
import * as
|
|
109
|
-
import * as
|
|
107
|
+
import * as fs9 from "fs";
|
|
108
|
+
import * as os3 from "os";
|
|
109
|
+
import * as path10 from "path";
|
|
110
110
|
function getStoreDirPath() {
|
|
111
|
-
return
|
|
111
|
+
return path10.join(os3.homedir(), ".claudectx");
|
|
112
112
|
}
|
|
113
113
|
function getReadsFilePath_() {
|
|
114
|
-
return
|
|
114
|
+
return path10.join(getStoreDirPath(), "reads.jsonl");
|
|
115
115
|
}
|
|
116
116
|
function ensureStoreDir() {
|
|
117
117
|
const dir = getStoreDirPath();
|
|
118
|
-
if (!
|
|
119
|
-
|
|
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
|
-
|
|
129
|
+
fs9.appendFileSync(getReadsFilePath_(), JSON.stringify(event) + "\n", "utf-8");
|
|
130
130
|
}
|
|
131
131
|
function readAllEvents() {
|
|
132
132
|
const readsFile = getReadsFilePath_();
|
|
133
|
-
if (!
|
|
134
|
-
const lines =
|
|
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 (
|
|
164
|
-
|
|
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
|
|
182
|
-
import * as
|
|
183
|
-
import * as
|
|
181
|
+
import * as fs10 from "fs";
|
|
182
|
+
import * as os4 from "os";
|
|
183
|
+
import * as path11 from "path";
|
|
184
184
|
function listSessionFiles() {
|
|
185
|
-
if (!
|
|
185
|
+
if (!fs10.existsSync(CLAUDE_PROJECTS_DIR)) return [];
|
|
186
186
|
const results = [];
|
|
187
187
|
try {
|
|
188
|
-
const projectDirs =
|
|
188
|
+
const projectDirs = fs10.readdirSync(CLAUDE_PROJECTS_DIR);
|
|
189
189
|
for (const projectDir of projectDirs) {
|
|
190
|
-
const projectPath =
|
|
190
|
+
const projectPath = path11.join(CLAUDE_PROJECTS_DIR, projectDir);
|
|
191
191
|
try {
|
|
192
|
-
const stat =
|
|
192
|
+
const stat = fs10.statSync(projectPath);
|
|
193
193
|
if (!stat.isDirectory()) continue;
|
|
194
|
-
const files =
|
|
194
|
+
const files = fs10.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
195
195
|
for (const file of files) {
|
|
196
|
-
const filePath =
|
|
196
|
+
const filePath = path11.join(projectPath, file);
|
|
197
197
|
try {
|
|
198
|
-
const fstat =
|
|
198
|
+
const fstat = fs10.statSync(filePath);
|
|
199
199
|
results.push({
|
|
200
200
|
filePath,
|
|
201
201
|
mtimeMs: fstat.mtimeMs,
|
|
202
|
-
sessionId:
|
|
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 (!
|
|
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 =
|
|
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
|
|
280
|
-
import * as
|
|
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(
|
|
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 (
|
|
413
|
+
if (fs11.existsSync(readsFile)) {
|
|
414
414
|
try {
|
|
415
|
-
watcher =
|
|
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
|
-
|
|
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
|
|
481
|
-
import * as
|
|
480
|
+
import * as fs13 from "fs";
|
|
481
|
+
import * as path15 from "path";
|
|
482
482
|
function detectLanguage(filePath) {
|
|
483
|
-
const ext =
|
|
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 (!
|
|
501
|
-
const content =
|
|
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 (!
|
|
563
|
-
const allLines =
|
|
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 (!
|
|
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:
|
|
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 =
|
|
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
|
|
677
|
-
import * as
|
|
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 =
|
|
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) =>
|
|
770
|
-
ignore: IGNORE_DIRS.map((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
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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
|
|
1567
|
-
import * as
|
|
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 =
|
|
1890
|
+
const content = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
1768
1891
|
const sections = parseSections(content);
|
|
1769
|
-
const claudeDir =
|
|
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 =
|
|
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
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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
|
-
|
|
1939
|
+
fs5.writeFileSync(file.filePath, file.content, "utf-8");
|
|
1814
1940
|
}
|
|
1815
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1861
|
-
import * as
|
|
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 =
|
|
1880
|
-
const settingsPath =
|
|
1881
|
-
const existed =
|
|
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(
|
|
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 =
|
|
1907
|
-
if (!
|
|
1908
|
-
|
|
2035
|
+
const dir = path8.dirname(result.settingsPath);
|
|
2036
|
+
if (!fs7.existsSync(dir)) {
|
|
2037
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1909
2038
|
}
|
|
1910
|
-
|
|
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 =
|
|
1914
|
-
const dir =
|
|
1915
|
-
if (!
|
|
1916
|
-
|
|
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
|
-
|
|
2047
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n", "utf-8");
|
|
1919
2048
|
}
|
|
1920
2049
|
function isAlreadyInstalled(projectRoot) {
|
|
1921
|
-
const settingsPath =
|
|
1922
|
-
if (!
|
|
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(
|
|
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 ?
|
|
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 =
|
|
2060
|
-
if (!
|
|
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 =
|
|
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(
|
|
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 =
|
|
2131
|
-
if (!
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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((
|
|
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", () =>
|
|
2240
|
-
setTimeout(() =>
|
|
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
|
|
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
|
|
2252
|
-
import * as
|
|
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 =
|
|
2261
|
-
const settingsPath =
|
|
2262
|
-
const existed =
|
|
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(
|
|
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 =
|
|
2284
|
-
if (!
|
|
2285
|
-
|
|
2412
|
+
const dir = path14.dirname(result.settingsPath);
|
|
2413
|
+
if (!fs12.existsSync(dir)) {
|
|
2414
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
2286
2415
|
}
|
|
2287
|
-
|
|
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 =
|
|
2295
|
-
if (!
|
|
2423
|
+
const settingsPath = path14.join(projectRoot, ".claude", "settings.json");
|
|
2424
|
+
if (!fs12.existsSync(settingsPath)) return false;
|
|
2296
2425
|
try {
|
|
2297
|
-
const settings = JSON.parse(
|
|
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 ?
|
|
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
|
|
2356
|
-
import * as
|
|
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
|
|
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 (!
|
|
2500
|
+
if (!fs15.existsSync(sessionFilePath)) return null;
|
|
2372
2501
|
let content;
|
|
2373
2502
|
try {
|
|
2374
|
-
content =
|
|
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
|
|
2575
|
-
import * as
|
|
2703
|
+
import * as fs16 from "fs";
|
|
2704
|
+
import * as path19 from "path";
|
|
2576
2705
|
function parseMemoryFile(filePath) {
|
|
2577
|
-
if (!
|
|
2706
|
+
if (!fs16.existsSync(filePath)) {
|
|
2578
2707
|
return { preamble: "", entries: [] };
|
|
2579
2708
|
}
|
|
2580
|
-
const content =
|
|
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 =
|
|
2632
|
-
if (!
|
|
2633
|
-
|
|
2760
|
+
const dir = path19.dirname(memoryFilePath);
|
|
2761
|
+
if (!fs16.existsSync(dir)) {
|
|
2762
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
2634
2763
|
}
|
|
2635
|
-
|
|
2764
|
+
fs16.writeFileSync(memoryFilePath, newContent, "utf-8");
|
|
2636
2765
|
return newContent;
|
|
2637
2766
|
}
|
|
2638
2767
|
function pruneOldEntries(memoryFilePath, days) {
|
|
2639
|
-
if (!
|
|
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
|
-
|
|
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 ?
|
|
2664
|
-
const memoryFilePath =
|
|
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 =
|
|
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 (!
|
|
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
|
|
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
|
|
3025
|
-
import * as
|
|
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 =
|
|
3195
|
+
const ignorePath = path21.join(projectRoot, ".claudeignore");
|
|
3051
3196
|
let ignorePatterns = [];
|
|
3052
3197
|
try {
|
|
3053
|
-
const content =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 ?
|
|
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
|
|
3333
|
+
import * as path23 from "path";
|
|
3189
3334
|
import Anthropic from "@anthropic-ai/sdk";
|
|
3190
3335
|
init_models();
|
|
3191
|
-
import
|
|
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:
|
|
3442
|
+
const { writeFileSync: writeFileSync12, unlinkSync: unlinkSync3 } = await import("fs");
|
|
3285
3443
|
const { tmpdir } = await import("os");
|
|
3286
|
-
const { join:
|
|
3287
|
-
const tmpFile =
|
|
3444
|
+
const { join: join18 } = await import("path");
|
|
3445
|
+
const tmpFile = join18(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
|
|
3288
3446
|
try {
|
|
3289
|
-
|
|
3447
|
+
writeFileSync12(tmpFile, newCrontab, "utf-8");
|
|
3290
3448
|
execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3291
3449
|
} finally {
|
|
3292
3450
|
try {
|
|
3293
|
-
|
|
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 ?
|
|
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 =
|
|
3480
|
+
const claudeMdPath = path23.join(projectRoot, "CLAUDE.md");
|
|
3323
3481
|
try {
|
|
3324
|
-
claudeMdContent =
|
|
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
|
|
3383
|
-
import * as
|
|
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
|
|
3390
|
-
import * as
|
|
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 =
|
|
3402
|
-
if (!
|
|
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
|
|
3436
|
-
if (line.includes(
|
|
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 "${
|
|
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 =
|
|
3509
|
-
if (!
|
|
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 =
|
|
3683
|
+
const claudeMdPath = path24.join(projectRoot, "CLAUDE.md");
|
|
3526
3684
|
let content = "";
|
|
3527
3685
|
try {
|
|
3528
|
-
content =
|
|
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 ?
|
|
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 =
|
|
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
|
-
|
|
3653
|
-
const
|
|
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
|
-
|
|
3657
|
-
|
|
3814
|
+
fs21.writeFileSync(tmpPath, newContent, "utf-8");
|
|
3815
|
+
fs21.renameSync(tmpPath, claudeMdPath);
|
|
3658
3816
|
} catch (err) {
|
|
3659
3817
|
try {
|
|
3660
|
-
|
|
3818
|
+
fs21.copyFileSync(backupPath, claudeMdPath);
|
|
3661
3819
|
} catch {
|
|
3662
3820
|
}
|
|
3663
3821
|
try {
|
|
3664
|
-
|
|
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 ${
|
|
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
|
|
3835
|
+
void os6;
|
|
3678
3836
|
}
|
|
3679
3837
|
|
|
3680
3838
|
// src/commands/hooks.ts
|
|
3681
3839
|
init_esm_shims();
|
|
3682
|
-
import * as
|
|
3683
|
-
import * as
|
|
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 =
|
|
3760
|
-
if (!
|
|
3917
|
+
const settingsPath = path26.join(projectRoot, ".claude", "settings.local.json");
|
|
3918
|
+
if (!fs22.existsSync(settingsPath)) return {};
|
|
3761
3919
|
try {
|
|
3762
|
-
return JSON.parse(
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
3956
|
-
import * as
|
|
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 ?
|
|
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 =
|
|
4189
|
+
const claudeMdPath = path27.join(projectRoot, "CLAUDE.md");
|
|
4015
4190
|
let content = "";
|
|
4016
4191
|
try {
|
|
4017
|
-
content =
|
|
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 =
|
|
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 =
|
|
4032
|
-
const exists =
|
|
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
|
-
|
|
4038
|
-
|
|
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 =
|
|
4045
|
-
const exists =
|
|
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
|
-
|
|
4051
|
-
|
|
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 =
|
|
4063
|
-
const exists =
|
|
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
|
-
|
|
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
|
|
4088
|
-
import * as
|
|
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
|
|
4096
|
-
import * as
|
|
4097
|
-
import * as
|
|
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
|
|
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 (!
|
|
4414
|
+
if (!fs24.existsSync(storeDir)) fs24.mkdirSync(storeDir, { recursive: true });
|
|
4223
4415
|
const date = isoDate2(/* @__PURE__ */ new Date());
|
|
4224
|
-
const filePath =
|
|
4225
|
-
|
|
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 (!
|
|
4231
|
-
const files =
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
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 =
|
|
4532
|
+
const src = path29.join(storeDir, latest);
|
|
4341
4533
|
let destPath;
|
|
4342
4534
|
try {
|
|
4343
|
-
const stat =
|
|
4344
|
-
destPath = stat.isDirectory() ?
|
|
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 =
|
|
4540
|
+
const destDir = path29.dirname(path29.resolve(destPath));
|
|
4349
4541
|
let resolvedDir;
|
|
4350
4542
|
try {
|
|
4351
|
-
resolvedDir =
|
|
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
|
-
|
|
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.
|
|
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
|