claudectx 1.1.1 → 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 +100 -21
- package/dist/index.js +628 -269
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +623 -264
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -124,15 +124,15 @@ __export(session_store_exports, {
|
|
|
124
124
|
readAllEvents: () => readAllEvents
|
|
125
125
|
});
|
|
126
126
|
function getStoreDirPath() {
|
|
127
|
-
return
|
|
127
|
+
return path9.join(os3.homedir(), ".claudectx");
|
|
128
128
|
}
|
|
129
129
|
function getReadsFilePath_() {
|
|
130
|
-
return
|
|
130
|
+
return path9.join(getStoreDirPath(), "reads.jsonl");
|
|
131
131
|
}
|
|
132
132
|
function ensureStoreDir() {
|
|
133
133
|
const dir = getStoreDirPath();
|
|
134
|
-
if (!
|
|
135
|
-
|
|
134
|
+
if (!fs9.existsSync(dir)) {
|
|
135
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
function appendFileRead(filePath, sessionId) {
|
|
@@ -142,12 +142,12 @@ function appendFileRead(filePath, sessionId) {
|
|
|
142
142
|
filePath,
|
|
143
143
|
sessionId
|
|
144
144
|
};
|
|
145
|
-
|
|
145
|
+
fs9.appendFileSync(getReadsFilePath_(), JSON.stringify(event) + "\n", "utf-8");
|
|
146
146
|
}
|
|
147
147
|
function readAllEvents() {
|
|
148
148
|
const readsFile = getReadsFilePath_();
|
|
149
|
-
if (!
|
|
150
|
-
const lines =
|
|
149
|
+
if (!fs9.existsSync(readsFile)) return [];
|
|
150
|
+
const lines = fs9.readFileSync(readsFile, "utf-8").trim().split("\n").filter(Boolean);
|
|
151
151
|
return lines.map((line) => {
|
|
152
152
|
try {
|
|
153
153
|
return JSON.parse(line);
|
|
@@ -176,8 +176,8 @@ function aggregateStats(events) {
|
|
|
176
176
|
}
|
|
177
177
|
function clearStore() {
|
|
178
178
|
const readsFile = getReadsFilePath_();
|
|
179
|
-
if (
|
|
180
|
-
|
|
179
|
+
if (fs9.existsSync(readsFile)) {
|
|
180
|
+
fs9.writeFileSync(readsFile, "", "utf-8");
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
function getReadsFilePath() {
|
|
@@ -186,37 +186,37 @@ function getReadsFilePath() {
|
|
|
186
186
|
function getStoreDir() {
|
|
187
187
|
return getStoreDirPath();
|
|
188
188
|
}
|
|
189
|
-
var
|
|
189
|
+
var fs9, os3, path9;
|
|
190
190
|
var init_session_store = __esm({
|
|
191
191
|
"src/watcher/session-store.ts"() {
|
|
192
192
|
"use strict";
|
|
193
193
|
init_cjs_shims();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
fs9 = __toESM(require("fs"));
|
|
195
|
+
os3 = __toESM(require("os"));
|
|
196
|
+
path9 = __toESM(require("path"));
|
|
197
197
|
}
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
// src/watcher/session-reader.ts
|
|
201
201
|
function listSessionFiles() {
|
|
202
|
-
if (!
|
|
202
|
+
if (!fs10.existsSync(CLAUDE_PROJECTS_DIR)) return [];
|
|
203
203
|
const results = [];
|
|
204
204
|
try {
|
|
205
|
-
const projectDirs =
|
|
205
|
+
const projectDirs = fs10.readdirSync(CLAUDE_PROJECTS_DIR);
|
|
206
206
|
for (const projectDir of projectDirs) {
|
|
207
|
-
const projectPath =
|
|
207
|
+
const projectPath = path10.join(CLAUDE_PROJECTS_DIR, projectDir);
|
|
208
208
|
try {
|
|
209
|
-
const stat =
|
|
209
|
+
const stat = fs10.statSync(projectPath);
|
|
210
210
|
if (!stat.isDirectory()) continue;
|
|
211
|
-
const files =
|
|
211
|
+
const files = fs10.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
212
212
|
for (const file of files) {
|
|
213
|
-
const filePath =
|
|
213
|
+
const filePath = path10.join(projectPath, file);
|
|
214
214
|
try {
|
|
215
|
-
const fstat =
|
|
215
|
+
const fstat = fs10.statSync(filePath);
|
|
216
216
|
results.push({
|
|
217
217
|
filePath,
|
|
218
218
|
mtimeMs: fstat.mtimeMs,
|
|
219
|
-
sessionId:
|
|
219
|
+
sessionId: path10.basename(file, ".jsonl"),
|
|
220
220
|
projectDir
|
|
221
221
|
});
|
|
222
222
|
} catch {
|
|
@@ -239,7 +239,7 @@ function findSessionFile(sessionId) {
|
|
|
239
239
|
}
|
|
240
240
|
return files[0]?.filePath ?? null;
|
|
241
241
|
}
|
|
242
|
-
function readSessionUsage(sessionFilePath) {
|
|
242
|
+
async function readSessionUsage(sessionFilePath) {
|
|
243
243
|
const result = {
|
|
244
244
|
inputTokens: 0,
|
|
245
245
|
outputTokens: 0,
|
|
@@ -247,41 +247,45 @@ function readSessionUsage(sessionFilePath) {
|
|
|
247
247
|
cacheReadTokens: 0,
|
|
248
248
|
requestCount: 0
|
|
249
249
|
};
|
|
250
|
-
if (!
|
|
251
|
-
|
|
250
|
+
if (!fs10.existsSync(sessionFilePath)) return result;
|
|
251
|
+
const { createReadStream } = await import("fs");
|
|
252
|
+
const { createInterface } = await import("readline");
|
|
252
253
|
try {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
254
|
+
const rl = createInterface({
|
|
255
|
+
input: createReadStream(sessionFilePath, { encoding: "utf-8" }),
|
|
256
|
+
crlfDelay: Infinity
|
|
257
|
+
});
|
|
258
|
+
for await (const line of rl) {
|
|
259
|
+
if (!line.trim()) continue;
|
|
260
|
+
try {
|
|
261
|
+
const entry = JSON.parse(line);
|
|
262
|
+
const usage = entry.usage ?? entry.message?.usage;
|
|
263
|
+
if (!usage) continue;
|
|
264
|
+
const isAssistant = entry.type === "assistant" || entry.message?.role === "assistant";
|
|
265
|
+
if (isAssistant) {
|
|
266
|
+
result.inputTokens += usage.input_tokens ?? 0;
|
|
267
|
+
result.outputTokens += usage.output_tokens ?? 0;
|
|
268
|
+
result.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
269
|
+
result.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
270
|
+
result.requestCount++;
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
270
273
|
}
|
|
271
|
-
} catch {
|
|
272
274
|
}
|
|
275
|
+
} catch {
|
|
276
|
+
return result;
|
|
273
277
|
}
|
|
274
278
|
return result;
|
|
275
279
|
}
|
|
276
|
-
var
|
|
280
|
+
var fs10, os4, path10, CLAUDE_PROJECTS_DIR;
|
|
277
281
|
var init_session_reader = __esm({
|
|
278
282
|
"src/watcher/session-reader.ts"() {
|
|
279
283
|
"use strict";
|
|
280
284
|
init_cjs_shims();
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
CLAUDE_PROJECTS_DIR =
|
|
285
|
+
fs10 = __toESM(require("fs"));
|
|
286
|
+
os4 = __toESM(require("os"));
|
|
287
|
+
path10 = __toESM(require("path"));
|
|
288
|
+
CLAUDE_PROJECTS_DIR = path10.join(os4.homedir(), ".claude", "projects");
|
|
285
289
|
}
|
|
286
290
|
});
|
|
287
291
|
|
|
@@ -299,7 +303,7 @@ function fmtCost(tokens, model) {
|
|
|
299
303
|
return `$${cost.toFixed(4)}`;
|
|
300
304
|
}
|
|
301
305
|
function shortPath(filePath) {
|
|
302
|
-
const parts = filePath.split(
|
|
306
|
+
const parts = filePath.split(path11.sep);
|
|
303
307
|
if (parts.length <= 3) return filePath;
|
|
304
308
|
return "\u2026/" + parts.slice(-3).join("/");
|
|
305
309
|
}
|
|
@@ -397,20 +401,23 @@ function Dashboard({
|
|
|
397
401
|
const events = readAllEvents();
|
|
398
402
|
const fileStats2 = aggregateStats(events);
|
|
399
403
|
const sessionFile2 = sessionId ? findSessionFile(sessionId) : findSessionFile();
|
|
400
|
-
const
|
|
404
|
+
const usagePromise = sessionFile2 ? readSessionUsage(sessionFile2) : Promise.resolve({
|
|
401
405
|
inputTokens: 0,
|
|
402
406
|
outputTokens: 0,
|
|
403
407
|
cacheCreationTokens: 0,
|
|
404
408
|
cacheReadTokens: 0,
|
|
405
409
|
requestCount: 0
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
});
|
|
411
|
+
usagePromise.then((usage2) => {
|
|
412
|
+
setState((prev) => ({
|
|
413
|
+
fileStats: fileStats2,
|
|
414
|
+
usage: usage2,
|
|
415
|
+
sessionFile: sessionFile2,
|
|
416
|
+
lastUpdated: /* @__PURE__ */ new Date(),
|
|
417
|
+
tickCount: prev.tickCount + 1
|
|
418
|
+
}));
|
|
419
|
+
}).catch(() => {
|
|
420
|
+
});
|
|
414
421
|
}, [sessionId]);
|
|
415
422
|
(0, import_react.useEffect)(() => {
|
|
416
423
|
refresh();
|
|
@@ -418,9 +425,9 @@ function Dashboard({
|
|
|
418
425
|
const readsFile = getReadsFilePath();
|
|
419
426
|
let watcher = null;
|
|
420
427
|
const tryWatch = () => {
|
|
421
|
-
if (
|
|
428
|
+
if (fs11.existsSync(readsFile)) {
|
|
422
429
|
try {
|
|
423
|
-
watcher =
|
|
430
|
+
watcher = fs11.watch(readsFile, () => refresh());
|
|
424
431
|
} catch {
|
|
425
432
|
}
|
|
426
433
|
}
|
|
@@ -458,7 +465,7 @@ function Dashboard({
|
|
|
458
465
|
] }),
|
|
459
466
|
sessionFile && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink.Text, { dimColor: true, children: [
|
|
460
467
|
" \u2014 ",
|
|
461
|
-
|
|
468
|
+
path11.basename(sessionFile, ".jsonl").slice(0, 8),
|
|
462
469
|
"\u2026"
|
|
463
470
|
] }),
|
|
464
471
|
!sessionFile && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink.Text, { dimColor: true, children: " \u2014 no session file found" })
|
|
@@ -474,7 +481,7 @@ function Dashboard({
|
|
|
474
481
|
] })
|
|
475
482
|
] });
|
|
476
483
|
}
|
|
477
|
-
var import_react, import_ink,
|
|
484
|
+
var import_react, import_ink, fs11, path11, import_jsx_runtime;
|
|
478
485
|
var init_Dashboard = __esm({
|
|
479
486
|
"src/components/Dashboard.tsx"() {
|
|
480
487
|
"use strict";
|
|
@@ -484,15 +491,15 @@ var init_Dashboard = __esm({
|
|
|
484
491
|
init_session_store();
|
|
485
492
|
init_session_reader();
|
|
486
493
|
init_models();
|
|
487
|
-
|
|
488
|
-
|
|
494
|
+
fs11 = __toESM(require("fs"));
|
|
495
|
+
path11 = __toESM(require("path"));
|
|
489
496
|
import_jsx_runtime = require("react/jsx-runtime");
|
|
490
497
|
}
|
|
491
498
|
});
|
|
492
499
|
|
|
493
500
|
// src/mcp/smart-reader.ts
|
|
494
501
|
function detectLanguage(filePath) {
|
|
495
|
-
const ext =
|
|
502
|
+
const ext = path14.extname(filePath).toLowerCase();
|
|
496
503
|
switch (ext) {
|
|
497
504
|
case ".ts":
|
|
498
505
|
case ".tsx":
|
|
@@ -509,8 +516,8 @@ function detectLanguage(filePath) {
|
|
|
509
516
|
}
|
|
510
517
|
}
|
|
511
518
|
function findSymbol(filePath, symbolName) {
|
|
512
|
-
if (!
|
|
513
|
-
const content =
|
|
519
|
+
if (!fs13.existsSync(filePath)) return null;
|
|
520
|
+
const content = fs13.readFileSync(filePath, "utf-8");
|
|
514
521
|
const lines = content.split("\n");
|
|
515
522
|
const lang = detectLanguage(filePath);
|
|
516
523
|
const patterns = lang === "python" ? PYTHON_PATTERNS : TS_JS_PATTERNS;
|
|
@@ -571,8 +578,8 @@ function findPythonBlockEnd(lines, startIdx) {
|
|
|
571
578
|
return lines.length;
|
|
572
579
|
}
|
|
573
580
|
function extractLineRange(filePath, startLine, endLine, contextLines = 0) {
|
|
574
|
-
if (!
|
|
575
|
-
const allLines =
|
|
581
|
+
if (!fs13.existsSync(filePath)) return null;
|
|
582
|
+
const allLines = fs13.readFileSync(filePath, "utf-8").split("\n");
|
|
576
583
|
const totalLines = allLines.length;
|
|
577
584
|
const from = Math.max(0, startLine - 1 - contextLines);
|
|
578
585
|
const to = Math.min(totalLines, endLine + contextLines);
|
|
@@ -587,7 +594,7 @@ function extractLineRange(filePath, startLine, endLine, contextLines = 0) {
|
|
|
587
594
|
};
|
|
588
595
|
}
|
|
589
596
|
function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
|
|
590
|
-
if (!
|
|
597
|
+
if (!fs13.existsSync(filePath)) {
|
|
591
598
|
throw new Error(`File not found: ${filePath}`);
|
|
592
599
|
}
|
|
593
600
|
if (symbol) {
|
|
@@ -599,7 +606,7 @@ function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
|
|
|
599
606
|
filePath,
|
|
600
607
|
startLine: extracted.startLine,
|
|
601
608
|
endLine: extracted.endLine,
|
|
602
|
-
totalLines:
|
|
609
|
+
totalLines: fs13.readFileSync(filePath, "utf-8").split("\n").length,
|
|
603
610
|
truncated: false,
|
|
604
611
|
symbolName: symbol
|
|
605
612
|
};
|
|
@@ -611,7 +618,7 @@ function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
|
|
|
611
618
|
return { ...result, truncated: false };
|
|
612
619
|
}
|
|
613
620
|
}
|
|
614
|
-
const fullContent =
|
|
621
|
+
const fullContent = fs13.readFileSync(filePath, "utf-8");
|
|
615
622
|
const allLines = fullContent.split("\n");
|
|
616
623
|
const totalLines = allLines.length;
|
|
617
624
|
const fullTokens = countTokens(fullContent);
|
|
@@ -647,13 +654,13 @@ function smartRead(filePath, symbol, startLine, endLine, contextLines = 3) {
|
|
|
647
654
|
truncated: true
|
|
648
655
|
};
|
|
649
656
|
}
|
|
650
|
-
var
|
|
657
|
+
var fs13, path14, TS_JS_PATTERNS, PYTHON_PATTERNS, MAX_FULL_FILE_TOKENS;
|
|
651
658
|
var init_smart_reader = __esm({
|
|
652
659
|
"src/mcp/smart-reader.ts"() {
|
|
653
660
|
"use strict";
|
|
654
661
|
init_cjs_shims();
|
|
655
|
-
|
|
656
|
-
|
|
662
|
+
fs13 = __toESM(require("fs"));
|
|
663
|
+
path14 = __toESM(require("path"));
|
|
657
664
|
init_tokenizer();
|
|
658
665
|
TS_JS_PATTERNS = [
|
|
659
666
|
// export async function name / export function name / function name
|
|
@@ -692,7 +699,7 @@ function extractSymbolsFromFile(filePath) {
|
|
|
692
699
|
if (lang === "other") return [];
|
|
693
700
|
let content;
|
|
694
701
|
try {
|
|
695
|
-
content =
|
|
702
|
+
content = fs14.readFileSync(filePath, "utf-8");
|
|
696
703
|
} catch {
|
|
697
704
|
return [];
|
|
698
705
|
}
|
|
@@ -721,13 +728,13 @@ function extractSymbolsFromFile(filePath) {
|
|
|
721
728
|
}
|
|
722
729
|
return results;
|
|
723
730
|
}
|
|
724
|
-
var
|
|
731
|
+
var fs14, path15, import_glob, TS_JS_EXTRACTORS, PYTHON_EXTRACTORS, SOURCE_GLOBS, IGNORE_DIRS, SymbolIndex, globalIndex;
|
|
725
732
|
var init_symbol_index = __esm({
|
|
726
733
|
"src/mcp/symbol-index.ts"() {
|
|
727
734
|
"use strict";
|
|
728
735
|
init_cjs_shims();
|
|
729
|
-
|
|
730
|
-
|
|
736
|
+
fs14 = __toESM(require("fs"));
|
|
737
|
+
path15 = __toESM(require("path"));
|
|
731
738
|
import_glob = require("glob");
|
|
732
739
|
init_smart_reader();
|
|
733
740
|
TS_JS_EXTRACTORS = [
|
|
@@ -780,8 +787,8 @@ var init_symbol_index = __esm({
|
|
|
780
787
|
this.entries = [];
|
|
781
788
|
let files = [];
|
|
782
789
|
try {
|
|
783
|
-
files = await (0, import_glob.glob)(SOURCE_GLOBS.map((g) =>
|
|
784
|
-
ignore: IGNORE_DIRS.map((g) =>
|
|
790
|
+
files = await (0, import_glob.glob)(SOURCE_GLOBS.map((g) => path15.join(projectRoot, g)), {
|
|
791
|
+
ignore: IGNORE_DIRS.map((g) => path15.join(projectRoot, g)),
|
|
785
792
|
absolute: true
|
|
786
793
|
});
|
|
787
794
|
} catch {
|
|
@@ -835,7 +842,7 @@ __export(server_exports, {
|
|
|
835
842
|
startMcpServer: () => startMcpServer
|
|
836
843
|
});
|
|
837
844
|
function handleSmartRead(args) {
|
|
838
|
-
const filePath =
|
|
845
|
+
const filePath = path16.resolve(args.file);
|
|
839
846
|
const result = smartRead(
|
|
840
847
|
filePath,
|
|
841
848
|
args.symbol,
|
|
@@ -871,7 +878,7 @@ Example: index_project({ "project_root": "${process.cwd()}" })`;
|
|
|
871
878
|
];
|
|
872
879
|
for (let i = 0; i < results.length; i++) {
|
|
873
880
|
const r = results[i];
|
|
874
|
-
const rel =
|
|
881
|
+
const rel = path16.relative(process.cwd(), r.filePath);
|
|
875
882
|
lines.push(`${i + 1}. [${r.type}] ${r.name}`);
|
|
876
883
|
lines.push(` ${rel}:${r.lineStart}`);
|
|
877
884
|
lines.push(` ${r.signature.trim()}`);
|
|
@@ -883,7 +890,7 @@ Example: index_project({ "project_root": "${process.cwd()}" })`;
|
|
|
883
890
|
return lines.join("\n");
|
|
884
891
|
}
|
|
885
892
|
async function handleIndexProject(args) {
|
|
886
|
-
const projectRoot = args.project_root ?
|
|
893
|
+
const projectRoot = args.project_root ? path16.resolve(args.project_root) : process.cwd();
|
|
887
894
|
const fn = args.rebuild ? () => globalIndex.rebuild(projectRoot) : () => globalIndex.build(projectRoot);
|
|
888
895
|
const { fileCount, symbolCount } = await fn();
|
|
889
896
|
if (fileCount === 0 && globalIndex.isReady) {
|
|
@@ -931,12 +938,12 @@ async function startMcpServer() {
|
|
|
931
938
|
await server.connect(transport);
|
|
932
939
|
process.stderr.write("[claudectx mcp] Server started (stdio)\n");
|
|
933
940
|
}
|
|
934
|
-
var
|
|
941
|
+
var path16, import_server, import_stdio, import_types, TOOLS;
|
|
935
942
|
var init_server = __esm({
|
|
936
943
|
"src/mcp/server.ts"() {
|
|
937
944
|
"use strict";
|
|
938
945
|
init_cjs_shims();
|
|
939
|
-
|
|
946
|
+
path16 = __toESM(require("path"));
|
|
940
947
|
import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
941
948
|
import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
942
949
|
import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -1574,8 +1581,8 @@ async function analyzeCommand(options) {
|
|
|
1574
1581
|
|
|
1575
1582
|
// src/commands/optimize.ts
|
|
1576
1583
|
init_cjs_shims();
|
|
1577
|
-
var
|
|
1578
|
-
var
|
|
1584
|
+
var fs8 = __toESM(require("fs"));
|
|
1585
|
+
var path8 = __toESM(require("path"));
|
|
1579
1586
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1580
1587
|
var import_boxen2 = __toESM(require("boxen"));
|
|
1581
1588
|
var import_prompts = require("@inquirer/prompts");
|
|
@@ -1738,9 +1745,132 @@ function writeIgnorefile(result) {
|
|
|
1738
1745
|
|
|
1739
1746
|
// src/optimizer/claudemd-splitter.ts
|
|
1740
1747
|
init_cjs_shims();
|
|
1748
|
+
var fs5 = __toESM(require("fs"));
|
|
1749
|
+
var path6 = __toESM(require("path"));
|
|
1750
|
+
init_tokenizer();
|
|
1751
|
+
|
|
1752
|
+
// src/shared/backup-manager.ts
|
|
1753
|
+
init_cjs_shims();
|
|
1741
1754
|
var fs4 = __toESM(require("fs"));
|
|
1755
|
+
var os2 = __toESM(require("os"));
|
|
1742
1756
|
var path5 = __toESM(require("path"));
|
|
1743
|
-
|
|
1757
|
+
var MAX_BACKUPS = 50;
|
|
1758
|
+
var _backupDirOverride = null;
|
|
1759
|
+
function getBackupDir() {
|
|
1760
|
+
return _backupDirOverride ?? path5.join(os2.homedir(), ".claudectx", "backups");
|
|
1761
|
+
}
|
|
1762
|
+
var BACKUP_DIR = path5.join(os2.homedir(), ".claudectx", "backups");
|
|
1763
|
+
function ensureBackupDir() {
|
|
1764
|
+
const dir = getBackupDir();
|
|
1765
|
+
if (!fs4.existsSync(dir)) {
|
|
1766
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function getManifestPath() {
|
|
1770
|
+
return path5.join(getBackupDir(), "manifest.json");
|
|
1771
|
+
}
|
|
1772
|
+
function readManifest() {
|
|
1773
|
+
ensureBackupDir();
|
|
1774
|
+
const manifestPath = getManifestPath();
|
|
1775
|
+
if (!fs4.existsSync(manifestPath)) {
|
|
1776
|
+
return { version: "1", entries: [] };
|
|
1777
|
+
}
|
|
1778
|
+
try {
|
|
1779
|
+
return JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
|
|
1780
|
+
} catch {
|
|
1781
|
+
return { version: "1", entries: [] };
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
function writeManifest(manifest) {
|
|
1785
|
+
ensureBackupDir();
|
|
1786
|
+
const manifestPath = getManifestPath();
|
|
1787
|
+
const tmpPath = `${manifestPath}.tmp-${Date.now()}`;
|
|
1788
|
+
try {
|
|
1789
|
+
fs4.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
1790
|
+
fs4.renameSync(tmpPath, manifestPath);
|
|
1791
|
+
} catch {
|
|
1792
|
+
try {
|
|
1793
|
+
fs4.unlinkSync(tmpPath);
|
|
1794
|
+
} catch {
|
|
1795
|
+
}
|
|
1796
|
+
throw new Error(`Failed to write backup manifest at ${manifestPath}`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function generateId(originalPath) {
|
|
1800
|
+
const now = /* @__PURE__ */ new Date();
|
|
1801
|
+
const ts = now.toISOString().replace(/[-:]/g, "").replace("T", "T").replace(".", "m").replace("Z", "");
|
|
1802
|
+
const rand = Math.floor(Math.random() * 9e3 + 1e3);
|
|
1803
|
+
const basename10 = path5.basename(originalPath);
|
|
1804
|
+
return `${ts}-${rand}-${basename10}`;
|
|
1805
|
+
}
|
|
1806
|
+
async function backupFile(filePath, command) {
|
|
1807
|
+
const resolved = path5.resolve(filePath);
|
|
1808
|
+
if (!fs4.existsSync(resolved)) {
|
|
1809
|
+
throw new Error(`Cannot back up "${resolved}": file does not exist.`);
|
|
1810
|
+
}
|
|
1811
|
+
ensureBackupDir();
|
|
1812
|
+
const id = generateId(resolved);
|
|
1813
|
+
const backupPath = path5.join(getBackupDir(), id);
|
|
1814
|
+
fs4.copyFileSync(resolved, backupPath);
|
|
1815
|
+
const stat = fs4.statSync(backupPath);
|
|
1816
|
+
const entry = {
|
|
1817
|
+
id,
|
|
1818
|
+
originalPath: resolved,
|
|
1819
|
+
backupPath,
|
|
1820
|
+
command,
|
|
1821
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1822
|
+
sizeBytes: stat.size
|
|
1823
|
+
};
|
|
1824
|
+
const manifest = readManifest();
|
|
1825
|
+
manifest.entries.unshift(entry);
|
|
1826
|
+
writeManifest(manifest);
|
|
1827
|
+
if (manifest.entries.length > MAX_BACKUPS) {
|
|
1828
|
+
await pruneOldBackups();
|
|
1829
|
+
}
|
|
1830
|
+
return entry;
|
|
1831
|
+
}
|
|
1832
|
+
async function listBackups(filterPath) {
|
|
1833
|
+
const manifest = readManifest();
|
|
1834
|
+
const entries = manifest.entries;
|
|
1835
|
+
if (!filterPath) return entries;
|
|
1836
|
+
const resolved = path5.resolve(filterPath);
|
|
1837
|
+
return entries.filter((e) => e.originalPath === resolved);
|
|
1838
|
+
}
|
|
1839
|
+
async function restoreBackup(backupId) {
|
|
1840
|
+
const manifest = readManifest();
|
|
1841
|
+
const entry = manifest.entries.find((e) => e.id === backupId);
|
|
1842
|
+
if (!entry) {
|
|
1843
|
+
throw new Error(`Backup "${backupId}" not found. Run "claudectx revert --list" to see available backups.`);
|
|
1844
|
+
}
|
|
1845
|
+
if (!fs4.existsSync(entry.backupPath)) {
|
|
1846
|
+
throw new Error(`Backup file missing at "${entry.backupPath}". It may have been deleted manually.`);
|
|
1847
|
+
}
|
|
1848
|
+
let undoEntry = null;
|
|
1849
|
+
if (fs4.existsSync(entry.originalPath)) {
|
|
1850
|
+
undoEntry = await backupFile(entry.originalPath, "revert");
|
|
1851
|
+
}
|
|
1852
|
+
const targetDir = path5.dirname(entry.originalPath);
|
|
1853
|
+
if (!fs4.existsSync(targetDir)) {
|
|
1854
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
1855
|
+
}
|
|
1856
|
+
fs4.copyFileSync(entry.backupPath, entry.originalPath);
|
|
1857
|
+
return { entry, undoEntry };
|
|
1858
|
+
}
|
|
1859
|
+
async function pruneOldBackups() {
|
|
1860
|
+
const manifest = readManifest();
|
|
1861
|
+
if (manifest.entries.length <= MAX_BACKUPS) return 0;
|
|
1862
|
+
const toRemove = manifest.entries.splice(MAX_BACKUPS);
|
|
1863
|
+
for (const entry of toRemove) {
|
|
1864
|
+
try {
|
|
1865
|
+
fs4.unlinkSync(entry.backupPath);
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
writeManifest(manifest);
|
|
1870
|
+
return toRemove.length;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/optimizer/claudemd-splitter.ts
|
|
1744
1874
|
var SPLIT_MIN_TOKENS = 300;
|
|
1745
1875
|
function slugify(title) {
|
|
1746
1876
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -1775,9 +1905,9 @@ function parseSections(content) {
|
|
|
1775
1905
|
return sections;
|
|
1776
1906
|
}
|
|
1777
1907
|
function planSplit(claudeMdPath, sectionsToExtract) {
|
|
1778
|
-
const content =
|
|
1908
|
+
const content = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
1779
1909
|
const sections = parseSections(content);
|
|
1780
|
-
const claudeDir =
|
|
1910
|
+
const claudeDir = path6.join(path6.dirname(claudeMdPath), ".claude");
|
|
1781
1911
|
const extractedFiles = [];
|
|
1782
1912
|
let newContent = "";
|
|
1783
1913
|
let tokensSaved = 0;
|
|
@@ -1790,7 +1920,7 @@ function planSplit(claudeMdPath, sectionsToExtract) {
|
|
|
1790
1920
|
usedSlugs.set(slug, count + 1);
|
|
1791
1921
|
const filename = `${slug}.md`;
|
|
1792
1922
|
const relRefPath = `.claude/${filename}`;
|
|
1793
|
-
const filePath =
|
|
1923
|
+
const filePath = path6.join(claudeDir, filename);
|
|
1794
1924
|
const refBlock = `## ${section.title}
|
|
1795
1925
|
|
|
1796
1926
|
@${relRefPath}
|
|
@@ -1814,21 +1944,24 @@ function planSplit(claudeMdPath, sectionsToExtract) {
|
|
|
1814
1944
|
tokensSaved: Math.max(0, tokensSaved)
|
|
1815
1945
|
};
|
|
1816
1946
|
}
|
|
1817
|
-
function applySplit(result) {
|
|
1947
|
+
async function applySplit(result) {
|
|
1818
1948
|
if (result.extractedFiles.length === 0) return;
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1949
|
+
if (fs5.existsSync(result.claudeMdPath)) {
|
|
1950
|
+
await backupFile(result.claudeMdPath, "optimize");
|
|
1951
|
+
}
|
|
1952
|
+
const claudeDir = path6.dirname(result.extractedFiles[0].filePath);
|
|
1953
|
+
if (!fs5.existsSync(claudeDir)) {
|
|
1954
|
+
fs5.mkdirSync(claudeDir, { recursive: true });
|
|
1822
1955
|
}
|
|
1823
1956
|
for (const file of result.extractedFiles) {
|
|
1824
|
-
|
|
1957
|
+
fs5.writeFileSync(file.filePath, file.content, "utf-8");
|
|
1825
1958
|
}
|
|
1826
|
-
|
|
1959
|
+
fs5.writeFileSync(result.claudeMdPath, result.newClaudeMd, "utf-8");
|
|
1827
1960
|
}
|
|
1828
1961
|
|
|
1829
1962
|
// src/optimizer/cache-applier.ts
|
|
1830
1963
|
init_cjs_shims();
|
|
1831
|
-
var
|
|
1964
|
+
var fs6 = __toESM(require("fs"));
|
|
1832
1965
|
function findCacheBusters(content) {
|
|
1833
1966
|
const fixes = [];
|
|
1834
1967
|
const lines = content.split("\n");
|
|
@@ -1858,18 +1991,21 @@ function applyCacheFixes(content, fixes) {
|
|
|
1858
1991
|
return lines.join("\n");
|
|
1859
1992
|
}
|
|
1860
1993
|
function planCacheFixes(claudeMdPath) {
|
|
1861
|
-
const content =
|
|
1994
|
+
const content = fs6.readFileSync(claudeMdPath, "utf-8");
|
|
1862
1995
|
const fixes = findCacheBusters(content);
|
|
1863
1996
|
return { fixes, newContent: applyCacheFixes(content, fixes) };
|
|
1864
1997
|
}
|
|
1865
|
-
function applyAndWriteCacheFixes(claudeMdPath, result) {
|
|
1866
|
-
|
|
1998
|
+
async function applyAndWriteCacheFixes(claudeMdPath, result) {
|
|
1999
|
+
if (fs6.existsSync(claudeMdPath)) {
|
|
2000
|
+
await backupFile(claudeMdPath, "optimize");
|
|
2001
|
+
}
|
|
2002
|
+
fs6.writeFileSync(claudeMdPath, result.newContent, "utf-8");
|
|
1867
2003
|
}
|
|
1868
2004
|
|
|
1869
2005
|
// src/optimizer/hooks-installer.ts
|
|
1870
2006
|
init_cjs_shims();
|
|
1871
|
-
var
|
|
1872
|
-
var
|
|
2007
|
+
var fs7 = __toESM(require("fs"));
|
|
2008
|
+
var path7 = __toESM(require("path"));
|
|
1873
2009
|
var CLAUDECTX_HOOKS = {
|
|
1874
2010
|
PostToolUse: [
|
|
1875
2011
|
{
|
|
@@ -1887,13 +2023,13 @@ var CLAUDECTX_HOOKS = {
|
|
|
1887
2023
|
]
|
|
1888
2024
|
};
|
|
1889
2025
|
function planHooksInstall(projectRoot) {
|
|
1890
|
-
const claudeDir =
|
|
1891
|
-
const settingsPath =
|
|
1892
|
-
const existed =
|
|
2026
|
+
const claudeDir = path7.join(projectRoot, ".claude");
|
|
2027
|
+
const settingsPath = path7.join(claudeDir, "settings.local.json");
|
|
2028
|
+
const existed = fs7.existsSync(settingsPath);
|
|
1893
2029
|
let existing = {};
|
|
1894
2030
|
if (existed) {
|
|
1895
2031
|
try {
|
|
1896
|
-
existing = JSON.parse(
|
|
2032
|
+
existing = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
1897
2033
|
} catch {
|
|
1898
2034
|
existing = {};
|
|
1899
2035
|
}
|
|
@@ -1914,25 +2050,25 @@ function planHooksInstall(projectRoot) {
|
|
|
1914
2050
|
return { settingsPath, existed, mergedSettings };
|
|
1915
2051
|
}
|
|
1916
2052
|
function applyHooksInstall(result) {
|
|
1917
|
-
const dir =
|
|
1918
|
-
if (!
|
|
1919
|
-
|
|
2053
|
+
const dir = path7.dirname(result.settingsPath);
|
|
2054
|
+
if (!fs7.existsSync(dir)) {
|
|
2055
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1920
2056
|
}
|
|
1921
|
-
|
|
2057
|
+
fs7.writeFileSync(result.settingsPath, JSON.stringify(result.mergedSettings, null, 2) + "\n", "utf-8");
|
|
1922
2058
|
}
|
|
1923
2059
|
function writeHooksSettings(projectRoot, mergedSettings) {
|
|
1924
|
-
const settingsPath =
|
|
1925
|
-
const dir =
|
|
1926
|
-
if (!
|
|
1927
|
-
|
|
2060
|
+
const settingsPath = path7.join(projectRoot, ".claude", "settings.local.json");
|
|
2061
|
+
const dir = path7.dirname(settingsPath);
|
|
2062
|
+
if (!fs7.existsSync(dir)) {
|
|
2063
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1928
2064
|
}
|
|
1929
|
-
|
|
2065
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n", "utf-8");
|
|
1930
2066
|
}
|
|
1931
2067
|
function isAlreadyInstalled(projectRoot) {
|
|
1932
|
-
const settingsPath =
|
|
1933
|
-
if (!
|
|
2068
|
+
const settingsPath = path7.join(projectRoot, ".claude", "settings.local.json");
|
|
2069
|
+
if (!fs7.existsSync(settingsPath)) return false;
|
|
1934
2070
|
try {
|
|
1935
|
-
const settings = JSON.parse(
|
|
2071
|
+
const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
1936
2072
|
const postToolUse = settings?.hooks?.PostToolUse ?? [];
|
|
1937
2073
|
return postToolUse.some((h) => h.matcher === "Read");
|
|
1938
2074
|
} catch {
|
|
@@ -1942,7 +2078,7 @@ function isAlreadyInstalled(projectRoot) {
|
|
|
1942
2078
|
|
|
1943
2079
|
// src/commands/optimize.ts
|
|
1944
2080
|
async function optimizeCommand(options) {
|
|
1945
|
-
const projectPath = options.path ?
|
|
2081
|
+
const projectPath = options.path ? path8.resolve(options.path) : findProjectRoot() ?? process.cwd();
|
|
1946
2082
|
const dryRun = options.dryRun ?? false;
|
|
1947
2083
|
const autoApply = options.apply ?? false;
|
|
1948
2084
|
const specificMode = options.claudemd || options.ignorefile || options.cache || options.hooks;
|
|
@@ -2067,12 +2203,12 @@ async function runIgnorefile(projectRoot, dryRun, autoApply) {
|
|
|
2067
2203
|
}
|
|
2068
2204
|
async function runClaudeMdSplit(projectRoot, report, dryRun, autoApply) {
|
|
2069
2205
|
printSectionHeader("CLAUDE.md \u2192 @files");
|
|
2070
|
-
const claudeMdPath =
|
|
2071
|
-
if (!
|
|
2206
|
+
const claudeMdPath = path8.join(projectRoot, "CLAUDE.md");
|
|
2207
|
+
if (!fs8.existsSync(claudeMdPath)) {
|
|
2072
2208
|
logger.warn("No CLAUDE.md found \u2014 skipping.");
|
|
2073
2209
|
return;
|
|
2074
2210
|
}
|
|
2075
|
-
const content =
|
|
2211
|
+
const content = fs8.readFileSync(claudeMdPath, "utf-8");
|
|
2076
2212
|
const sections = parseSections(content);
|
|
2077
2213
|
const largeSections = sections.filter(
|
|
2078
2214
|
(s) => !s.isPreamble && s.tokens >= SPLIT_MIN_TOKENS
|
|
@@ -2128,18 +2264,18 @@ Would extract ${splitResult.extractedFiles.length} section(s) to .claude/`
|
|
|
2128
2264
|
logger.info("Skipped.");
|
|
2129
2265
|
return;
|
|
2130
2266
|
}
|
|
2131
|
-
applySplit(splitResult);
|
|
2267
|
+
await applySplit(splitResult);
|
|
2132
2268
|
logger.success(
|
|
2133
2269
|
`Extracted ${splitResult.extractedFiles.length} section(s). Saved ~${splitResult.tokensSaved} tokens/request.`
|
|
2134
2270
|
);
|
|
2135
2271
|
for (const f of splitResult.extractedFiles) {
|
|
2136
|
-
logger.info(` Created: ${import_chalk3.default.cyan(
|
|
2272
|
+
logger.info(` Created: ${import_chalk3.default.cyan(path8.relative(projectRoot, f.filePath))}`);
|
|
2137
2273
|
}
|
|
2138
2274
|
}
|
|
2139
2275
|
async function runCacheOptimization(projectRoot, dryRun, autoApply) {
|
|
2140
2276
|
printSectionHeader("Prompt cache optimisation");
|
|
2141
|
-
const claudeMdPath =
|
|
2142
|
-
if (!
|
|
2277
|
+
const claudeMdPath = path8.join(projectRoot, "CLAUDE.md");
|
|
2278
|
+
if (!fs8.existsSync(claudeMdPath)) {
|
|
2143
2279
|
logger.warn("No CLAUDE.md found \u2014 skipping.");
|
|
2144
2280
|
return;
|
|
2145
2281
|
}
|
|
@@ -2167,14 +2303,14 @@ async function runCacheOptimization(projectRoot, dryRun, autoApply) {
|
|
|
2167
2303
|
logger.info("Skipped.");
|
|
2168
2304
|
return;
|
|
2169
2305
|
}
|
|
2170
|
-
applyAndWriteCacheFixes(claudeMdPath, result);
|
|
2306
|
+
await applyAndWriteCacheFixes(claudeMdPath, result);
|
|
2171
2307
|
logger.success(`Fixed ${result.fixes.length} cache-busting pattern(s) in CLAUDE.md.`);
|
|
2172
2308
|
}
|
|
2173
2309
|
async function runHooks(projectRoot, dryRun, autoApply) {
|
|
2174
2310
|
printSectionHeader("Session hooks");
|
|
2175
2311
|
const result = planHooksInstall(projectRoot);
|
|
2176
2312
|
logger.info(
|
|
2177
|
-
`Settings file: ${import_chalk3.default.cyan(
|
|
2313
|
+
`Settings file: ${import_chalk3.default.cyan(path8.relative(projectRoot, result.settingsPath))}`
|
|
2178
2314
|
);
|
|
2179
2315
|
logger.info(result.existed ? "Will merge with existing settings." : "Will create new file.");
|
|
2180
2316
|
console.log(import_chalk3.default.dim("\n Hooks to install:"));
|
|
@@ -2189,7 +2325,7 @@ async function runHooks(projectRoot, dryRun, autoApply) {
|
|
|
2189
2325
|
}
|
|
2190
2326
|
applyHooksInstall(result);
|
|
2191
2327
|
logger.success(
|
|
2192
|
-
`Hooks installed \u2192 ${import_chalk3.default.cyan(
|
|
2328
|
+
`Hooks installed \u2192 ${import_chalk3.default.cyan(path8.relative(projectRoot, result.settingsPath))}`
|
|
2193
2329
|
);
|
|
2194
2330
|
}
|
|
2195
2331
|
function printSectionHeader(title) {
|
|
@@ -2199,7 +2335,7 @@ function printSectionHeader(title) {
|
|
|
2199
2335
|
|
|
2200
2336
|
// src/commands/watch.ts
|
|
2201
2337
|
init_cjs_shims();
|
|
2202
|
-
var
|
|
2338
|
+
var path12 = __toESM(require("path"));
|
|
2203
2339
|
init_session_store();
|
|
2204
2340
|
init_models();
|
|
2205
2341
|
async function watchCommand(options) {
|
|
@@ -2237,30 +2373,30 @@ async function handleLogStdin() {
|
|
|
2237
2373
|
const payload = JSON.parse(raw);
|
|
2238
2374
|
const filePath = payload.tool_input?.file_path;
|
|
2239
2375
|
if (filePath) {
|
|
2240
|
-
appendFileRead(
|
|
2376
|
+
appendFileRead(path12.resolve(filePath), payload.session_id);
|
|
2241
2377
|
}
|
|
2242
2378
|
} catch {
|
|
2243
2379
|
}
|
|
2244
2380
|
}
|
|
2245
2381
|
function readStdin() {
|
|
2246
|
-
return new Promise((
|
|
2382
|
+
return new Promise((resolve13) => {
|
|
2247
2383
|
let data = "";
|
|
2248
2384
|
process.stdin.setEncoding("utf-8");
|
|
2249
2385
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
2250
|
-
process.stdin.on("end", () =>
|
|
2251
|
-
setTimeout(() =>
|
|
2386
|
+
process.stdin.on("end", () => resolve13(data));
|
|
2387
|
+
setTimeout(() => resolve13(data), 500);
|
|
2252
2388
|
});
|
|
2253
2389
|
}
|
|
2254
2390
|
|
|
2255
2391
|
// src/commands/mcp.ts
|
|
2256
2392
|
init_cjs_shims();
|
|
2257
|
-
var
|
|
2393
|
+
var path17 = __toESM(require("path"));
|
|
2258
2394
|
var import_chalk4 = __toESM(require("chalk"));
|
|
2259
2395
|
|
|
2260
2396
|
// src/mcp/installer.ts
|
|
2261
2397
|
init_cjs_shims();
|
|
2262
|
-
var
|
|
2263
|
-
var
|
|
2398
|
+
var fs12 = __toESM(require("fs"));
|
|
2399
|
+
var path13 = __toESM(require("path"));
|
|
2264
2400
|
var SERVER_NAME = "claudectx";
|
|
2265
2401
|
var SERVER_ENTRY = {
|
|
2266
2402
|
command: "claudectx",
|
|
@@ -2268,13 +2404,13 @@ var SERVER_ENTRY = {
|
|
|
2268
2404
|
type: "stdio"
|
|
2269
2405
|
};
|
|
2270
2406
|
function planInstall(projectRoot) {
|
|
2271
|
-
const claudeDir =
|
|
2272
|
-
const settingsPath =
|
|
2273
|
-
const existed =
|
|
2407
|
+
const claudeDir = path13.join(projectRoot, ".claude");
|
|
2408
|
+
const settingsPath = path13.join(claudeDir, "settings.json");
|
|
2409
|
+
const existed = fs12.existsSync(settingsPath);
|
|
2274
2410
|
let existing = {};
|
|
2275
2411
|
if (existed) {
|
|
2276
2412
|
try {
|
|
2277
|
-
existing = JSON.parse(
|
|
2413
|
+
existing = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
|
|
2278
2414
|
} catch {
|
|
2279
2415
|
existing = {};
|
|
2280
2416
|
}
|
|
@@ -2291,21 +2427,21 @@ function planInstall(projectRoot) {
|
|
|
2291
2427
|
return { settingsPath, existed, alreadyInstalled, mergedSettings };
|
|
2292
2428
|
}
|
|
2293
2429
|
function applyInstall(result) {
|
|
2294
|
-
const dir =
|
|
2295
|
-
if (!
|
|
2296
|
-
|
|
2430
|
+
const dir = path13.dirname(result.settingsPath);
|
|
2431
|
+
if (!fs12.existsSync(dir)) {
|
|
2432
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
2297
2433
|
}
|
|
2298
|
-
|
|
2434
|
+
fs12.writeFileSync(
|
|
2299
2435
|
result.settingsPath,
|
|
2300
2436
|
JSON.stringify(result.mergedSettings, null, 2) + "\n",
|
|
2301
2437
|
"utf-8"
|
|
2302
2438
|
);
|
|
2303
2439
|
}
|
|
2304
2440
|
function isInstalled(projectRoot) {
|
|
2305
|
-
const settingsPath =
|
|
2306
|
-
if (!
|
|
2441
|
+
const settingsPath = path13.join(projectRoot, ".claude", "settings.json");
|
|
2442
|
+
if (!fs12.existsSync(settingsPath)) return false;
|
|
2307
2443
|
try {
|
|
2308
|
-
const settings = JSON.parse(
|
|
2444
|
+
const settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
|
|
2309
2445
|
return SERVER_NAME in (settings.mcpServers ?? {});
|
|
2310
2446
|
} catch {
|
|
2311
2447
|
return false;
|
|
@@ -2314,7 +2450,7 @@ function isInstalled(projectRoot) {
|
|
|
2314
2450
|
|
|
2315
2451
|
// src/commands/mcp.ts
|
|
2316
2452
|
async function mcpCommand(options) {
|
|
2317
|
-
const projectRoot = options.path ?
|
|
2453
|
+
const projectRoot = options.path ? path17.resolve(options.path) : process.cwd();
|
|
2318
2454
|
if (options.install) {
|
|
2319
2455
|
await runInstall(projectRoot);
|
|
2320
2456
|
return;
|
|
@@ -2362,13 +2498,13 @@ async function runInstall(projectRoot) {
|
|
|
2362
2498
|
|
|
2363
2499
|
// src/commands/compress.ts
|
|
2364
2500
|
init_cjs_shims();
|
|
2365
|
-
var
|
|
2366
|
-
var
|
|
2501
|
+
var path19 = __toESM(require("path"));
|
|
2502
|
+
var fs17 = __toESM(require("fs"));
|
|
2367
2503
|
init_session_reader();
|
|
2368
2504
|
|
|
2369
2505
|
// src/compressor/session-parser.ts
|
|
2370
2506
|
init_cjs_shims();
|
|
2371
|
-
var
|
|
2507
|
+
var fs15 = __toESM(require("fs"));
|
|
2372
2508
|
function extractText(content) {
|
|
2373
2509
|
if (!content) return "";
|
|
2374
2510
|
if (typeof content === "string") return content;
|
|
@@ -2379,10 +2515,10 @@ function extractToolCalls(content) {
|
|
|
2379
2515
|
return content.filter((b) => b.type === "tool_use" && b.name).map((b) => ({ tool: b.name, input: b.input ?? {} }));
|
|
2380
2516
|
}
|
|
2381
2517
|
function parseSessionFile(sessionFilePath) {
|
|
2382
|
-
if (!
|
|
2518
|
+
if (!fs15.existsSync(sessionFilePath)) return null;
|
|
2383
2519
|
let content;
|
|
2384
2520
|
try {
|
|
2385
|
-
content =
|
|
2521
|
+
content = fs15.readFileSync(sessionFilePath, "utf-8");
|
|
2386
2522
|
} catch {
|
|
2387
2523
|
return null;
|
|
2388
2524
|
}
|
|
@@ -2582,13 +2718,13 @@ function calcCost(inputTokens, outputTokens) {
|
|
|
2582
2718
|
|
|
2583
2719
|
// src/compressor/memory-writer.ts
|
|
2584
2720
|
init_cjs_shims();
|
|
2585
|
-
var
|
|
2586
|
-
var
|
|
2721
|
+
var fs16 = __toESM(require("fs"));
|
|
2722
|
+
var path18 = __toESM(require("path"));
|
|
2587
2723
|
function parseMemoryFile(filePath) {
|
|
2588
|
-
if (!
|
|
2724
|
+
if (!fs16.existsSync(filePath)) {
|
|
2589
2725
|
return { preamble: "", entries: [] };
|
|
2590
2726
|
}
|
|
2591
|
-
const content =
|
|
2727
|
+
const content = fs16.readFileSync(filePath, "utf-8");
|
|
2592
2728
|
const markerRegex = /<!-- claudectx-entry: (\d{4}-\d{2}-\d{2}) \| session: ([a-z0-9-]+) -->/g;
|
|
2593
2729
|
const indices = [];
|
|
2594
2730
|
let match;
|
|
@@ -2639,15 +2775,15 @@ function appendEntry(memoryFilePath, sessionId, summaryText, date = /* @__PURE__
|
|
|
2639
2775
|
const newBlock = buildEntryBlock(sessionId, summaryText, date);
|
|
2640
2776
|
const allBlocks = [...entries.map((e) => e.raw), newBlock];
|
|
2641
2777
|
const newContent = (preamble.trimEnd() ? preamble.trimEnd() + "\n\n" : "") + allBlocks.join("\n\n") + "\n";
|
|
2642
|
-
const dir =
|
|
2643
|
-
if (!
|
|
2644
|
-
|
|
2778
|
+
const dir = path18.dirname(memoryFilePath);
|
|
2779
|
+
if (!fs16.existsSync(dir)) {
|
|
2780
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
2645
2781
|
}
|
|
2646
|
-
|
|
2782
|
+
fs16.writeFileSync(memoryFilePath, newContent, "utf-8");
|
|
2647
2783
|
return newContent;
|
|
2648
2784
|
}
|
|
2649
2785
|
function pruneOldEntries(memoryFilePath, days) {
|
|
2650
|
-
if (!
|
|
2786
|
+
if (!fs16.existsSync(memoryFilePath)) {
|
|
2651
2787
|
return { removed: 0, kept: 0, removedEntries: [] };
|
|
2652
2788
|
}
|
|
2653
2789
|
const { preamble, entries } = parseMemoryFile(memoryFilePath);
|
|
@@ -2660,7 +2796,7 @@ function pruneOldEntries(memoryFilePath, days) {
|
|
|
2660
2796
|
return { removed: 0, kept: kept.length, removedEntries: [] };
|
|
2661
2797
|
}
|
|
2662
2798
|
const newContent = (preamble.trimEnd() ? preamble.trimEnd() + "\n\n" : "") + kept.map((e) => e.raw).join("\n\n") + (kept.length > 0 ? "\n" : "");
|
|
2663
|
-
|
|
2799
|
+
fs16.writeFileSync(memoryFilePath, newContent, "utf-8");
|
|
2664
2800
|
return { removed: removed.length, kept: kept.length, removedEntries: removed };
|
|
2665
2801
|
}
|
|
2666
2802
|
function isAlreadyCompressed(memoryFilePath, sessionId) {
|
|
@@ -2671,8 +2807,8 @@ function isAlreadyCompressed(memoryFilePath, sessionId) {
|
|
|
2671
2807
|
// src/commands/compress.ts
|
|
2672
2808
|
async function compressCommand(options) {
|
|
2673
2809
|
const chalk5 = (await import("chalk")).default;
|
|
2674
|
-
const projectRoot = options.path ?
|
|
2675
|
-
const memoryFilePath =
|
|
2810
|
+
const projectRoot = options.path ? path19.resolve(options.path) : process.cwd();
|
|
2811
|
+
const memoryFilePath = path19.join(projectRoot, "MEMORY.md");
|
|
2676
2812
|
const sessionFiles = listSessionFiles();
|
|
2677
2813
|
if (sessionFiles.length === 0) {
|
|
2678
2814
|
process.stdout.write(chalk5.red("No Claude Code sessions found.\n"));
|
|
@@ -2697,7 +2833,7 @@ async function compressCommand(options) {
|
|
|
2697
2833
|
} else {
|
|
2698
2834
|
targetFile = sessionFiles[0].filePath;
|
|
2699
2835
|
}
|
|
2700
|
-
const sessionId =
|
|
2836
|
+
const sessionId = path19.basename(targetFile, ".jsonl");
|
|
2701
2837
|
if (isAlreadyCompressed(memoryFilePath, sessionId)) {
|
|
2702
2838
|
if (!options.auto) {
|
|
2703
2839
|
process.stdout.write(chalk5.yellow(`Session ${sessionId.slice(0, 8)}\u2026 is already in MEMORY.md \u2014 skipping.
|
|
@@ -2745,11 +2881,27 @@ async function compressCommand(options) {
|
|
|
2745
2881
|
}
|
|
2746
2882
|
if (options.prune) {
|
|
2747
2883
|
const days = parseInt(options.days ?? "30", 10);
|
|
2748
|
-
if (!
|
|
2884
|
+
if (!fs17.existsSync(memoryFilePath)) return;
|
|
2885
|
+
if (!options.auto) {
|
|
2886
|
+
let confirmed = true;
|
|
2887
|
+
try {
|
|
2888
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
2889
|
+
confirmed = await confirm2({
|
|
2890
|
+
message: `Prune MEMORY.md entries older than ${days} days? Run 'claudectx revert' to undo.`,
|
|
2891
|
+
default: false
|
|
2892
|
+
});
|
|
2893
|
+
} catch {
|
|
2894
|
+
}
|
|
2895
|
+
if (!confirmed) {
|
|
2896
|
+
process.stdout.write(chalk5.dim("Prune skipped.\n"));
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
await backupFile(memoryFilePath, "compress");
|
|
2749
2901
|
const pruned = pruneOldEntries(memoryFilePath, days);
|
|
2750
2902
|
if (pruned.removed > 0 && !options.auto) {
|
|
2751
2903
|
process.stdout.write(
|
|
2752
|
-
chalk5.dim(`Pruned ${pruned.removed} entr${pruned.removed === 1 ? "y" : "ies"} older than ${days} days.
|
|
2904
|
+
chalk5.dim(`Pruned ${pruned.removed} entr${pruned.removed === 1 ? "y" : "ies"} older than ${days} days. Run 'claudectx revert' to undo.
|
|
2753
2905
|
`)
|
|
2754
2906
|
);
|
|
2755
2907
|
}
|
|
@@ -2767,9 +2919,9 @@ init_models();
|
|
|
2767
2919
|
function isoDate(d) {
|
|
2768
2920
|
return d.toISOString().slice(0, 10);
|
|
2769
2921
|
}
|
|
2770
|
-
function calcCost2(inputTokens, outputTokens, model) {
|
|
2922
|
+
function calcCost2(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
|
|
2771
2923
|
const p = MODEL_PRICING[model];
|
|
2772
|
-
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion;
|
|
2924
|
+
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion + cacheCreationTokens / 1e6 * p.cacheWritePerMillion + cacheReadTokens / 1e6 * p.cacheReadPerMillion;
|
|
2773
2925
|
}
|
|
2774
2926
|
async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
2775
2927
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -2788,6 +2940,7 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2788
2940
|
inputTokens: 0,
|
|
2789
2941
|
outputTokens: 0,
|
|
2790
2942
|
cacheReadTokens: 0,
|
|
2943
|
+
cacheCreationTokens: 0,
|
|
2791
2944
|
requests: 0,
|
|
2792
2945
|
costUsd: 0
|
|
2793
2946
|
});
|
|
@@ -2796,20 +2949,23 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2796
2949
|
let totalInput = 0;
|
|
2797
2950
|
let totalOutput = 0;
|
|
2798
2951
|
let totalCacheRead = 0;
|
|
2952
|
+
let totalCacheCreation = 0;
|
|
2799
2953
|
for (const sf of sessionFiles) {
|
|
2800
2954
|
const dateStr = isoDate(new Date(sf.mtimeMs));
|
|
2801
2955
|
const bucket = bucketMap.get(dateStr);
|
|
2802
2956
|
if (!bucket) continue;
|
|
2803
|
-
const usage = readSessionUsage(sf.filePath);
|
|
2957
|
+
const usage = await readSessionUsage(sf.filePath);
|
|
2804
2958
|
bucket.sessions++;
|
|
2805
2959
|
bucket.inputTokens += usage.inputTokens;
|
|
2806
2960
|
bucket.outputTokens += usage.outputTokens;
|
|
2807
2961
|
bucket.cacheReadTokens += usage.cacheReadTokens;
|
|
2962
|
+
bucket.cacheCreationTokens += usage.cacheCreationTokens;
|
|
2808
2963
|
bucket.requests += usage.requestCount;
|
|
2809
|
-
bucket.costUsd += calcCost2(usage.inputTokens, usage.outputTokens, model);
|
|
2964
|
+
bucket.costUsd += calcCost2(usage.inputTokens, usage.outputTokens, usage.cacheCreationTokens, usage.cacheReadTokens, model);
|
|
2810
2965
|
totalInput += usage.inputTokens;
|
|
2811
2966
|
totalOutput += usage.outputTokens;
|
|
2812
2967
|
totalCacheRead += usage.cacheReadTokens;
|
|
2968
|
+
totalCacheCreation += usage.cacheCreationTokens;
|
|
2813
2969
|
totalRequests += usage.requestCount;
|
|
2814
2970
|
}
|
|
2815
2971
|
const fileEvents = readAllEvents().filter(
|
|
@@ -2820,10 +2976,12 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2820
2976
|
filePath: s.filePath,
|
|
2821
2977
|
readCount: s.readCount
|
|
2822
2978
|
}));
|
|
2823
|
-
const totalCost = calcCost2(totalInput, totalOutput, model);
|
|
2979
|
+
const totalCost = calcCost2(totalInput, totalOutput, totalCacheCreation, totalCacheRead, model);
|
|
2824
2980
|
const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
|
|
2825
2981
|
const byDay = [...bucketMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
2826
2982
|
const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
|
|
2983
|
+
const dailyAvgCostUsd = days > 0 ? totalCost / days : 0;
|
|
2984
|
+
const projectedMonthlyUsd = dailyAvgCostUsd * 30;
|
|
2827
2985
|
return {
|
|
2828
2986
|
periodDays: days,
|
|
2829
2987
|
startDate: isoDate(cutoff),
|
|
@@ -2833,10 +2991,13 @@ async function aggregateUsage(days, model = "claude-sonnet-4-6") {
|
|
|
2833
2991
|
totalInputTokens: totalInput,
|
|
2834
2992
|
totalOutputTokens: totalOutput,
|
|
2835
2993
|
totalCacheReadTokens: totalCacheRead,
|
|
2994
|
+
totalCacheCreationTokens: totalCacheCreation,
|
|
2836
2995
|
cacheHitRate,
|
|
2837
2996
|
totalCostUsd: totalCost,
|
|
2838
2997
|
avgCostPerSession: uniqueSessions > 0 ? totalCost / uniqueSessions : 0,
|
|
2839
2998
|
avgTokensPerRequest: totalRequests > 0 ? Math.round(totalInput / totalRequests) : 0,
|
|
2999
|
+
dailyAvgCostUsd,
|
|
3000
|
+
projectedMonthlyUsd,
|
|
2840
3001
|
byDay,
|
|
2841
3002
|
topFiles,
|
|
2842
3003
|
model,
|
|
@@ -2879,9 +3040,12 @@ function formatText(data) {
|
|
|
2879
3040
|
lines.push(` Input tokens: ${fmtNum2(data.totalInputTokens)}`);
|
|
2880
3041
|
lines.push(` Output tokens: ${fmtNum2(data.totalOutputTokens)}`);
|
|
2881
3042
|
lines.push(` Cache reads: ${fmtNum2(data.totalCacheReadTokens)} (${data.cacheHitRate}% hit rate)`);
|
|
3043
|
+
lines.push(` Cache writes: ${fmtNum2(data.totalCacheCreationTokens)}`);
|
|
2882
3044
|
lines.push(` Total cost (est.): ${fmtCost2(data.totalCostUsd)}`);
|
|
2883
3045
|
lines.push(` Avg cost/session: ${fmtCost2(data.avgCostPerSession)}`);
|
|
2884
3046
|
lines.push(` Avg tokens/request: ${fmtNum2(data.avgTokensPerRequest)}`);
|
|
3047
|
+
lines.push(` Daily avg cost: ${fmtCost2(data.dailyAvgCostUsd)}`);
|
|
3048
|
+
lines.push(` Projected (30-day): ${fmtCost2(data.projectedMonthlyUsd)}`);
|
|
2885
3049
|
lines.push(` Model: ${data.model}`);
|
|
2886
3050
|
lines.push("");
|
|
2887
3051
|
const activeDays = data.byDay.filter((d) => d.sessions > 0);
|
|
@@ -2950,9 +3114,12 @@ function formatMarkdown(data) {
|
|
|
2950
3114
|
lines.push(`| Input tokens | ${fmtNum2(data.totalInputTokens)} |`);
|
|
2951
3115
|
lines.push(`| Output tokens | ${fmtNum2(data.totalOutputTokens)} |`);
|
|
2952
3116
|
lines.push(`| Cache hit rate | ${data.cacheHitRate}% |`);
|
|
3117
|
+
lines.push(`| Cache writes | ${fmtNum2(data.totalCacheCreationTokens)} tokens |`);
|
|
2953
3118
|
lines.push(`| Total cost (est.) | ${fmtCost2(data.totalCostUsd)} |`);
|
|
2954
3119
|
lines.push(`| Avg cost/session | ${fmtCost2(data.avgCostPerSession)} |`);
|
|
2955
3120
|
lines.push(`| Avg tokens/request | ${fmtNum2(data.avgTokensPerRequest)} |`);
|
|
3121
|
+
lines.push(`| Daily avg cost | ${fmtCost2(data.dailyAvgCostUsd)} |`);
|
|
3122
|
+
lines.push(`| Projected (30-day) | ${fmtCost2(data.projectedMonthlyUsd)} |`);
|
|
2956
3123
|
lines.push(`| Model | \`${data.model}\` |`);
|
|
2957
3124
|
lines.push("");
|
|
2958
3125
|
const activeDays = data.byDay.filter((d) => d.sessions > 0);
|
|
@@ -3012,12 +3179,12 @@ async function reportCommand(options) {
|
|
|
3012
3179
|
|
|
3013
3180
|
// src/commands/budget.ts
|
|
3014
3181
|
init_cjs_shims();
|
|
3015
|
-
var
|
|
3182
|
+
var path21 = __toESM(require("path"));
|
|
3016
3183
|
|
|
3017
3184
|
// src/analyzer/budget-estimator.ts
|
|
3018
3185
|
init_cjs_shims();
|
|
3019
|
-
var
|
|
3020
|
-
var
|
|
3186
|
+
var fs18 = __toESM(require("fs"));
|
|
3187
|
+
var path20 = __toESM(require("path"));
|
|
3021
3188
|
var import_glob2 = require("glob");
|
|
3022
3189
|
init_tokenizer();
|
|
3023
3190
|
init_session_store();
|
|
@@ -3043,20 +3210,20 @@ function classifyCacheHit(recentReadCount) {
|
|
|
3043
3210
|
return "low";
|
|
3044
3211
|
}
|
|
3045
3212
|
function suggestClaudeignoreAdditions(files, projectRoot) {
|
|
3046
|
-
const ignorePath =
|
|
3213
|
+
const ignorePath = path20.join(projectRoot, ".claudeignore");
|
|
3047
3214
|
let ignorePatterns = [];
|
|
3048
3215
|
try {
|
|
3049
|
-
const content =
|
|
3216
|
+
const content = fs18.readFileSync(ignorePath, "utf-8");
|
|
3050
3217
|
ignorePatterns = content.split("\n").filter(Boolean);
|
|
3051
3218
|
} catch {
|
|
3052
3219
|
}
|
|
3053
3220
|
const recommendations = [];
|
|
3054
3221
|
for (const file of files) {
|
|
3055
3222
|
if (file.tokenCount <= WASTE_THRESHOLDS.MAX_REFERENCE_FILE_TOKENS) continue;
|
|
3056
|
-
const rel =
|
|
3223
|
+
const rel = path20.relative(projectRoot, file.filePath);
|
|
3057
3224
|
const alreadyIgnored = ignorePatterns.some((pattern) => {
|
|
3058
3225
|
const cleanPattern = pattern.replace(/^!/, "");
|
|
3059
|
-
return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g,
|
|
3226
|
+
return rel.startsWith(cleanPattern.replace(/\*/g, "").replace(/\//g, path20.sep));
|
|
3060
3227
|
});
|
|
3061
3228
|
if (!alreadyIgnored) {
|
|
3062
3229
|
recommendations.push(rel);
|
|
@@ -3076,7 +3243,7 @@ async function estimateBudget(globs, projectRoot, model, thresholdTokens) {
|
|
|
3076
3243
|
for (const filePath of filePaths) {
|
|
3077
3244
|
let content = "";
|
|
3078
3245
|
try {
|
|
3079
|
-
content =
|
|
3246
|
+
content = fs18.readFileSync(filePath, "utf-8");
|
|
3080
3247
|
} catch {
|
|
3081
3248
|
continue;
|
|
3082
3249
|
}
|
|
@@ -3125,7 +3292,7 @@ function formatBudgetReport(report) {
|
|
|
3125
3292
|
}
|
|
3126
3293
|
const LIKELIHOOD_ICON = { high: "\u{1F7E2}", medium: "\u{1F7E1}", low: "\u{1F534}" };
|
|
3127
3294
|
const maxPathLen = Math.min(
|
|
3128
|
-
Math.max(...report.files.map((f) =>
|
|
3295
|
+
Math.max(...report.files.map((f) => path20.basename(f.filePath).length)),
|
|
3129
3296
|
40
|
|
3130
3297
|
);
|
|
3131
3298
|
lines.push(
|
|
@@ -3133,7 +3300,7 @@ function formatBudgetReport(report) {
|
|
|
3133
3300
|
);
|
|
3134
3301
|
lines.push("\u2500".repeat(50));
|
|
3135
3302
|
for (const file of report.files.slice(0, 20)) {
|
|
3136
|
-
const name =
|
|
3303
|
+
const name = path20.basename(file.filePath).slice(0, maxPathLen).padEnd(maxPathLen);
|
|
3137
3304
|
const tokens = file.tokenCount.toLocaleString().padStart(7);
|
|
3138
3305
|
const cache = `${LIKELIHOOD_ICON[file.cacheHitLikelihood]} ${file.cacheHitLikelihood.padEnd(6)}`;
|
|
3139
3306
|
const cost = formatCost(file.estimatedCostUsd).padStart(7);
|
|
@@ -3162,7 +3329,7 @@ function formatBudgetReport(report) {
|
|
|
3162
3329
|
// src/commands/budget.ts
|
|
3163
3330
|
init_models();
|
|
3164
3331
|
async function budgetCommand(globs, options) {
|
|
3165
|
-
const projectPath = options.path ?
|
|
3332
|
+
const projectPath = options.path ? path21.resolve(options.path) : process.cwd();
|
|
3166
3333
|
const projectRoot = findProjectRoot(projectPath) ?? projectPath;
|
|
3167
3334
|
const model = resolveModel(options.model ?? "sonnet");
|
|
3168
3335
|
const thresholdTokens = parseInt(options.threshold ?? "10000", 10);
|
|
@@ -3181,7 +3348,7 @@ async function budgetCommand(globs, options) {
|
|
|
3181
3348
|
|
|
3182
3349
|
// src/commands/warmup.ts
|
|
3183
3350
|
init_cjs_shims();
|
|
3184
|
-
var
|
|
3351
|
+
var path22 = __toESM(require("path"));
|
|
3185
3352
|
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
3186
3353
|
init_models();
|
|
3187
3354
|
var import_fs3 = __toESM(require("fs"));
|
|
@@ -3255,6 +3422,19 @@ async function installCron(cronExpr) {
|
|
|
3255
3422
|
);
|
|
3256
3423
|
process.exit(1);
|
|
3257
3424
|
}
|
|
3425
|
+
let confirmed = true;
|
|
3426
|
+
try {
|
|
3427
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
3428
|
+
confirmed = await confirm2({
|
|
3429
|
+
message: `Install cron job "${cronExpr} claudectx warmup" in your system crontab?`,
|
|
3430
|
+
default: false
|
|
3431
|
+
});
|
|
3432
|
+
} catch {
|
|
3433
|
+
}
|
|
3434
|
+
if (!confirmed) {
|
|
3435
|
+
process.stdout.write(" Cron install cancelled.\n");
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3258
3438
|
const { execSync: execSync3 } = await import("child_process");
|
|
3259
3439
|
const command = `claudectx warmup`;
|
|
3260
3440
|
const cronLine = `${cronExpr} ${command}`;
|
|
@@ -3277,16 +3457,16 @@ async function installCron(cronExpr) {
|
|
|
3277
3457
|
${marker}
|
|
3278
3458
|
${cronLine}
|
|
3279
3459
|
`;
|
|
3280
|
-
const { writeFileSync:
|
|
3460
|
+
const { writeFileSync: writeFileSync12, unlinkSync: unlinkSync3 } = await import("fs");
|
|
3281
3461
|
const { tmpdir } = await import("os");
|
|
3282
|
-
const { join:
|
|
3283
|
-
const tmpFile =
|
|
3462
|
+
const { join: join18 } = await import("path");
|
|
3463
|
+
const tmpFile = join18(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
|
|
3284
3464
|
try {
|
|
3285
|
-
|
|
3465
|
+
writeFileSync12(tmpFile, newCrontab, "utf-8");
|
|
3286
3466
|
execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3287
3467
|
} finally {
|
|
3288
3468
|
try {
|
|
3289
|
-
|
|
3469
|
+
unlinkSync3(tmpFile);
|
|
3290
3470
|
} catch {
|
|
3291
3471
|
}
|
|
3292
3472
|
}
|
|
@@ -3303,7 +3483,7 @@ ${cronLine}
|
|
|
3303
3483
|
}
|
|
3304
3484
|
}
|
|
3305
3485
|
async function warmupCommand(options) {
|
|
3306
|
-
const projectPath = options.path ?
|
|
3486
|
+
const projectPath = options.path ? path22.resolve(options.path) : process.cwd();
|
|
3307
3487
|
const projectRoot = findProjectRoot(projectPath) ?? projectPath;
|
|
3308
3488
|
const model = resolveModel(options.model ?? "haiku");
|
|
3309
3489
|
const ttl = options.ttl === "60" ? 60 : 5;
|
|
@@ -3315,7 +3495,7 @@ async function warmupCommand(options) {
|
|
|
3315
3495
|
process.exit(1);
|
|
3316
3496
|
}
|
|
3317
3497
|
let claudeMdContent = "";
|
|
3318
|
-
const claudeMdPath =
|
|
3498
|
+
const claudeMdPath = path22.join(projectRoot, "CLAUDE.md");
|
|
3319
3499
|
try {
|
|
3320
3500
|
claudeMdContent = import_fs3.default.readFileSync(claudeMdPath, "utf-8");
|
|
3321
3501
|
} catch {
|
|
@@ -3375,13 +3555,13 @@ async function warmupCommand(options) {
|
|
|
3375
3555
|
|
|
3376
3556
|
// src/commands/drift.ts
|
|
3377
3557
|
init_cjs_shims();
|
|
3378
|
-
var
|
|
3379
|
-
var
|
|
3558
|
+
var path24 = __toESM(require("path"));
|
|
3559
|
+
var fs21 = __toESM(require("fs"));
|
|
3380
3560
|
|
|
3381
3561
|
// src/analyzer/drift-detector.ts
|
|
3382
3562
|
init_cjs_shims();
|
|
3383
|
-
var
|
|
3384
|
-
var
|
|
3563
|
+
var fs20 = __toESM(require("fs"));
|
|
3564
|
+
var path23 = __toESM(require("path"));
|
|
3385
3565
|
var childProcess = __toESM(require("child_process"));
|
|
3386
3566
|
init_tokenizer();
|
|
3387
3567
|
init_session_store();
|
|
@@ -3394,8 +3574,8 @@ function findDeadAtReferences(content, projectRoot) {
|
|
|
3394
3574
|
const match = lines[i].match(AT_REF_RE);
|
|
3395
3575
|
if (!match) continue;
|
|
3396
3576
|
const ref = match[1].trim();
|
|
3397
|
-
const absPath =
|
|
3398
|
-
if (!
|
|
3577
|
+
const absPath = path23.isAbsolute(ref) ? ref : path23.join(projectRoot, ref);
|
|
3578
|
+
if (!fs20.existsSync(absPath)) {
|
|
3399
3579
|
const lineText = lines[i];
|
|
3400
3580
|
issues.push({
|
|
3401
3581
|
type: "dead-ref",
|
|
@@ -3428,15 +3608,15 @@ async function findGitDeletedMentions(content, projectRoot) {
|
|
|
3428
3608
|
for (let i = 0; i < lines.length; i++) {
|
|
3429
3609
|
const line = lines[i];
|
|
3430
3610
|
for (const deleted of deletedFiles) {
|
|
3431
|
-
const
|
|
3432
|
-
if (line.includes(
|
|
3611
|
+
const basename10 = path23.basename(deleted);
|
|
3612
|
+
if (line.includes(basename10) || line.includes(deleted)) {
|
|
3433
3613
|
issues.push({
|
|
3434
3614
|
type: "git-deleted",
|
|
3435
3615
|
line: i + 1,
|
|
3436
3616
|
text: line.trim(),
|
|
3437
3617
|
severity: "warning",
|
|
3438
3618
|
estimatedTokenWaste: countTokens(line),
|
|
3439
|
-
suggestion: `References "${
|
|
3619
|
+
suggestion: `References "${basename10}" which was deleted from git. Consider removing this mention.`
|
|
3440
3620
|
});
|
|
3441
3621
|
break;
|
|
3442
3622
|
}
|
|
@@ -3501,8 +3681,8 @@ function findDeadInlinePaths(content, projectRoot) {
|
|
|
3501
3681
|
const rawPath = match[1].trim();
|
|
3502
3682
|
if (seen.has(rawPath)) continue;
|
|
3503
3683
|
seen.add(rawPath);
|
|
3504
|
-
const absPath =
|
|
3505
|
-
if (!
|
|
3684
|
+
const absPath = path23.isAbsolute(rawPath) ? rawPath : path23.join(projectRoot, rawPath);
|
|
3685
|
+
if (!fs20.existsSync(absPath)) {
|
|
3506
3686
|
issues.push({
|
|
3507
3687
|
type: "dead-inline-path",
|
|
3508
3688
|
line: i + 1,
|
|
@@ -3518,10 +3698,10 @@ function findDeadInlinePaths(content, projectRoot) {
|
|
|
3518
3698
|
return issues;
|
|
3519
3699
|
}
|
|
3520
3700
|
async function detectDrift(projectRoot, dayWindow) {
|
|
3521
|
-
const claudeMdPath =
|
|
3701
|
+
const claudeMdPath = path23.join(projectRoot, "CLAUDE.md");
|
|
3522
3702
|
let content = "";
|
|
3523
3703
|
try {
|
|
3524
|
-
content =
|
|
3704
|
+
content = fs20.readFileSync(claudeMdPath, "utf-8");
|
|
3525
3705
|
} catch {
|
|
3526
3706
|
return {
|
|
3527
3707
|
claudeMdPath,
|
|
@@ -3563,7 +3743,7 @@ var TYPE_LABEL = {
|
|
|
3563
3743
|
"dead-inline-path": "Dead path"
|
|
3564
3744
|
};
|
|
3565
3745
|
async function driftCommand(options) {
|
|
3566
|
-
const projectPath = options.path ?
|
|
3746
|
+
const projectPath = options.path ? path24.resolve(options.path) : process.cwd();
|
|
3567
3747
|
const projectRoot = findProjectRoot(projectPath) ?? projectPath;
|
|
3568
3748
|
const dayWindow = parseInt(options.days ?? "30", 10);
|
|
3569
3749
|
const report = await detectDrift(projectRoot, dayWindow);
|
|
@@ -3639,25 +3819,25 @@ async function applyFix(claudeMdPath, issues) {
|
|
|
3639
3819
|
process.stdout.write("No lines selected. Nothing changed.\n");
|
|
3640
3820
|
return;
|
|
3641
3821
|
}
|
|
3642
|
-
const content =
|
|
3822
|
+
const content = fs21.readFileSync(claudeMdPath, "utf-8");
|
|
3643
3823
|
const lines = content.split("\n");
|
|
3644
3824
|
const lineSet = new Set(selectedLines.map((l) => l - 1));
|
|
3645
3825
|
const newLines = lines.filter((_, i) => !lineSet.has(i));
|
|
3646
3826
|
const newContent = newLines.join("\n");
|
|
3647
3827
|
const backupPath = `${claudeMdPath}.bak`;
|
|
3648
|
-
|
|
3649
|
-
const
|
|
3828
|
+
fs21.writeFileSync(backupPath, content, "utf-8");
|
|
3829
|
+
const os6 = await import("os");
|
|
3650
3830
|
const tmpPath = `${claudeMdPath}.tmp-${Date.now()}`;
|
|
3651
3831
|
try {
|
|
3652
|
-
|
|
3653
|
-
|
|
3832
|
+
fs21.writeFileSync(tmpPath, newContent, "utf-8");
|
|
3833
|
+
fs21.renameSync(tmpPath, claudeMdPath);
|
|
3654
3834
|
} catch (err) {
|
|
3655
3835
|
try {
|
|
3656
|
-
|
|
3836
|
+
fs21.copyFileSync(backupPath, claudeMdPath);
|
|
3657
3837
|
} catch {
|
|
3658
3838
|
}
|
|
3659
3839
|
try {
|
|
3660
|
-
|
|
3840
|
+
fs21.unlinkSync(tmpPath);
|
|
3661
3841
|
} catch {
|
|
3662
3842
|
}
|
|
3663
3843
|
process.stderr.write(`Error writing CLAUDE.md: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -3665,18 +3845,18 @@ async function applyFix(claudeMdPath, issues) {
|
|
|
3665
3845
|
process.exit(1);
|
|
3666
3846
|
}
|
|
3667
3847
|
process.stdout.write(`
|
|
3668
|
-
\u2713 Removed ${selectedLines.length} line(s) from ${
|
|
3848
|
+
\u2713 Removed ${selectedLines.length} line(s) from ${path24.basename(claudeMdPath)}
|
|
3669
3849
|
`);
|
|
3670
3850
|
process.stdout.write(` \u2713 Backup saved to ${backupPath}
|
|
3671
3851
|
|
|
3672
3852
|
`);
|
|
3673
|
-
void
|
|
3853
|
+
void os6;
|
|
3674
3854
|
}
|
|
3675
3855
|
|
|
3676
3856
|
// src/commands/hooks.ts
|
|
3677
3857
|
init_cjs_shims();
|
|
3678
|
-
var
|
|
3679
|
-
var
|
|
3858
|
+
var fs22 = __toESM(require("fs"));
|
|
3859
|
+
var path25 = __toESM(require("path"));
|
|
3680
3860
|
|
|
3681
3861
|
// src/hooks/registry.ts
|
|
3682
3862
|
init_cjs_shims();
|
|
@@ -3752,17 +3932,17 @@ function buildHookEntry(def, config) {
|
|
|
3752
3932
|
|
|
3753
3933
|
// src/commands/hooks.ts
|
|
3754
3934
|
function readInstalledHooks(projectRoot) {
|
|
3755
|
-
const settingsPath =
|
|
3756
|
-
if (!
|
|
3935
|
+
const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
|
|
3936
|
+
if (!fs22.existsSync(settingsPath)) return {};
|
|
3757
3937
|
try {
|
|
3758
|
-
return JSON.parse(
|
|
3938
|
+
return JSON.parse(fs22.readFileSync(settingsPath, "utf-8"));
|
|
3759
3939
|
} catch {
|
|
3760
3940
|
process.stderr.write(
|
|
3761
3941
|
`Warning: ${settingsPath} exists but contains invalid JSON. Existing settings will be preserved as a backup.
|
|
3762
3942
|
`
|
|
3763
3943
|
);
|
|
3764
3944
|
try {
|
|
3765
|
-
|
|
3945
|
+
fs22.copyFileSync(settingsPath, `${settingsPath}.bak`);
|
|
3766
3946
|
} catch {
|
|
3767
3947
|
}
|
|
3768
3948
|
return {};
|
|
@@ -3882,9 +4062,26 @@ async function hooksAdd(name, projectRoot, configPairs) {
|
|
|
3882
4062
|
}
|
|
3883
4063
|
async function hooksRemove(name, projectRoot) {
|
|
3884
4064
|
const settings = readInstalledHooks(projectRoot);
|
|
4065
|
+
let confirmed = true;
|
|
4066
|
+
try {
|
|
4067
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
4068
|
+
confirmed = await confirm2({
|
|
4069
|
+
message: `Remove hook "${name}"? Run 'claudectx revert' to undo.`,
|
|
4070
|
+
default: false
|
|
4071
|
+
});
|
|
4072
|
+
} catch {
|
|
4073
|
+
}
|
|
4074
|
+
if (!confirmed) {
|
|
4075
|
+
process.stdout.write(" Cancelled.\n\n");
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
const settingsPath = path25.join(projectRoot, ".claude", "settings.local.json");
|
|
4079
|
+
if (fs22.existsSync(settingsPath)) {
|
|
4080
|
+
await backupFile(settingsPath, "hooks");
|
|
4081
|
+
}
|
|
3885
4082
|
const updated = removeHookByName(settings, name);
|
|
3886
4083
|
writeHooksSettings(projectRoot, updated);
|
|
3887
|
-
process.stdout.write(` \u2713 Hook "${name}" removed.
|
|
4084
|
+
process.stdout.write(` \u2713 Hook "${name}" removed. Run 'claudectx revert --list' to undo.
|
|
3888
4085
|
|
|
3889
4086
|
`);
|
|
3890
4087
|
}
|
|
@@ -3909,7 +4106,7 @@ async function hooksStatus(projectRoot) {
|
|
|
3909
4106
|
process.stdout.write("\n");
|
|
3910
4107
|
}
|
|
3911
4108
|
async function hooksCommand(subcommand, options) {
|
|
3912
|
-
const projectPath = options.path ?
|
|
4109
|
+
const projectPath = options.path ? path25.resolve(options.path) : process.cwd();
|
|
3913
4110
|
const projectRoot = findProjectRoot(projectPath) ?? projectPath;
|
|
3914
4111
|
const sub = subcommand ?? "list";
|
|
3915
4112
|
switch (sub) {
|
|
@@ -3948,8 +4145,8 @@ async function hooksCommand(subcommand, options) {
|
|
|
3948
4145
|
|
|
3949
4146
|
// src/commands/convert.ts
|
|
3950
4147
|
init_cjs_shims();
|
|
3951
|
-
var
|
|
3952
|
-
var
|
|
4148
|
+
var fs23 = __toESM(require("fs"));
|
|
4149
|
+
var path26 = __toESM(require("path"));
|
|
3953
4150
|
function slugify2(text) {
|
|
3954
4151
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3955
4152
|
}
|
|
@@ -3998,7 +4195,7 @@ function claudeMdToWindsurf(content) {
|
|
|
3998
4195
|
return content.split("\n").filter((line) => !line.match(/^@.+$/)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3999
4196
|
}
|
|
4000
4197
|
async function convertCommand(options) {
|
|
4001
|
-
const projectPath = options.path ?
|
|
4198
|
+
const projectPath = options.path ? path26.resolve(options.path) : process.cwd();
|
|
4002
4199
|
const projectRoot = findProjectRoot(projectPath) ?? projectPath;
|
|
4003
4200
|
const from = options.from ?? "claude";
|
|
4004
4201
|
const to = options.to;
|
|
@@ -4007,10 +4204,10 @@ async function convertCommand(options) {
|
|
|
4007
4204
|
`);
|
|
4008
4205
|
process.exit(1);
|
|
4009
4206
|
}
|
|
4010
|
-
const claudeMdPath =
|
|
4207
|
+
const claudeMdPath = path26.join(projectRoot, "CLAUDE.md");
|
|
4011
4208
|
let content = "";
|
|
4012
4209
|
try {
|
|
4013
|
-
content =
|
|
4210
|
+
content = fs23.readFileSync(claudeMdPath, "utf-8");
|
|
4014
4211
|
} catch {
|
|
4015
4212
|
process.stderr.write(`Error: CLAUDE.md not found at ${claudeMdPath}
|
|
4016
4213
|
`);
|
|
@@ -4018,33 +4215,45 @@ async function convertCommand(options) {
|
|
|
4018
4215
|
}
|
|
4019
4216
|
if (to === "cursor") {
|
|
4020
4217
|
const files = claudeMdToCursorRules(content);
|
|
4021
|
-
const targetDir =
|
|
4218
|
+
const targetDir = path26.join(projectRoot, ".cursor", "rules");
|
|
4022
4219
|
process.stdout.write(`
|
|
4023
4220
|
Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
|
|
4024
|
-
|
|
4025
4221
|
`);
|
|
4222
|
+
if (!options.dryRun) {
|
|
4223
|
+
process.stdout.write(` \u26A0 Existing .mdc files will be overwritten. A backup is saved automatically.
|
|
4224
|
+
`);
|
|
4225
|
+
process.stdout.write(` Run 'claudectx revert --list' to see backups.
|
|
4226
|
+
`);
|
|
4227
|
+
}
|
|
4228
|
+
process.stdout.write("\n");
|
|
4026
4229
|
for (const file of files) {
|
|
4027
|
-
const filePath =
|
|
4028
|
-
const exists =
|
|
4230
|
+
const filePath = path26.join(targetDir, file.filename);
|
|
4231
|
+
const exists = fs23.existsSync(filePath);
|
|
4029
4232
|
const prefix = options.dryRun ? "[dry-run] " : exists ? "[overwrite] " : "";
|
|
4030
4233
|
process.stdout.write(` ${prefix}\u2192 .cursor/rules/${file.filename}
|
|
4031
4234
|
`);
|
|
4032
4235
|
if (!options.dryRun) {
|
|
4033
|
-
|
|
4034
|
-
|
|
4236
|
+
fs23.mkdirSync(targetDir, { recursive: true });
|
|
4237
|
+
if (exists) await backupFile(filePath, "convert");
|
|
4238
|
+
fs23.writeFileSync(filePath, file.content, "utf-8");
|
|
4035
4239
|
}
|
|
4036
4240
|
}
|
|
4037
4241
|
process.stdout.write("\n");
|
|
4038
4242
|
} else if (to === "copilot") {
|
|
4039
4243
|
const converted = claudeMdToCopilot(content);
|
|
4040
|
-
const targetPath =
|
|
4041
|
-
const exists =
|
|
4244
|
+
const targetPath = path26.join(projectRoot, ".github", "copilot-instructions.md");
|
|
4245
|
+
const exists = fs23.existsSync(targetPath);
|
|
4042
4246
|
process.stdout.write(`
|
|
4043
4247
|
Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwrite]" : ""}
|
|
4044
4248
|
`);
|
|
4249
|
+
if (!options.dryRun && exists) {
|
|
4250
|
+
process.stdout.write(` \u26A0 Existing file will be overwritten. Run 'claudectx revert --list' to undo.
|
|
4251
|
+
`);
|
|
4252
|
+
}
|
|
4045
4253
|
if (!options.dryRun) {
|
|
4046
|
-
|
|
4047
|
-
|
|
4254
|
+
fs23.mkdirSync(path26.dirname(targetPath), { recursive: true });
|
|
4255
|
+
if (exists) await backupFile(targetPath, "convert");
|
|
4256
|
+
fs23.writeFileSync(targetPath, converted, "utf-8");
|
|
4048
4257
|
process.stdout.write(` \u2713 Written to ${targetPath}
|
|
4049
4258
|
|
|
4050
4259
|
`);
|
|
@@ -4055,13 +4264,18 @@ Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwri
|
|
|
4055
4264
|
}
|
|
4056
4265
|
} else if (to === "windsurf") {
|
|
4057
4266
|
const converted = claudeMdToWindsurf(content);
|
|
4058
|
-
const targetPath =
|
|
4059
|
-
const exists =
|
|
4267
|
+
const targetPath = path26.join(projectRoot, ".windsurfrules");
|
|
4268
|
+
const exists = fs23.existsSync(targetPath);
|
|
4060
4269
|
process.stdout.write(`
|
|
4061
4270
|
Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
|
|
4062
4271
|
`);
|
|
4272
|
+
if (!options.dryRun && exists) {
|
|
4273
|
+
process.stdout.write(` \u26A0 Existing file will be overwritten. Run 'claudectx revert --list' to undo.
|
|
4274
|
+
`);
|
|
4275
|
+
}
|
|
4063
4276
|
if (!options.dryRun) {
|
|
4064
|
-
|
|
4277
|
+
if (exists) await backupFile(targetPath, "convert");
|
|
4278
|
+
fs23.writeFileSync(targetPath, converted, "utf-8");
|
|
4065
4279
|
process.stdout.write(` \u2713 Written to ${targetPath}
|
|
4066
4280
|
|
|
4067
4281
|
`);
|
|
@@ -4079,15 +4293,15 @@ Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
|
|
|
4079
4293
|
|
|
4080
4294
|
// src/commands/teams.ts
|
|
4081
4295
|
init_cjs_shims();
|
|
4082
|
-
var
|
|
4083
|
-
var
|
|
4296
|
+
var path28 = __toESM(require("path"));
|
|
4297
|
+
var fs25 = __toESM(require("fs"));
|
|
4084
4298
|
init_models();
|
|
4085
4299
|
|
|
4086
4300
|
// src/reporter/team-aggregator.ts
|
|
4087
4301
|
init_cjs_shims();
|
|
4088
|
-
var
|
|
4089
|
-
var
|
|
4090
|
-
var
|
|
4302
|
+
var fs24 = __toESM(require("fs"));
|
|
4303
|
+
var path27 = __toESM(require("path"));
|
|
4304
|
+
var os5 = __toESM(require("os"));
|
|
4091
4305
|
var childProcess2 = __toESM(require("child_process"));
|
|
4092
4306
|
init_session_reader();
|
|
4093
4307
|
init_session_store();
|
|
@@ -4098,11 +4312,11 @@ function getDeveloperIdentity() {
|
|
|
4098
4312
|
if (email) return email;
|
|
4099
4313
|
} catch {
|
|
4100
4314
|
}
|
|
4101
|
-
return
|
|
4315
|
+
return os5.hostname();
|
|
4102
4316
|
}
|
|
4103
|
-
function calcCost3(inputTokens, outputTokens, model) {
|
|
4317
|
+
function calcCost3(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, model) {
|
|
4104
4318
|
const p = MODEL_PRICING[model];
|
|
4105
|
-
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion;
|
|
4319
|
+
return inputTokens / 1e6 * p.inputPerMillion + outputTokens / 1e6 * p.outputPerMillion + cacheCreationTokens / 1e6 * p.cacheWritePerMillion + cacheReadTokens / 1e6 * p.cacheReadPerMillion;
|
|
4106
4320
|
}
|
|
4107
4321
|
function isoDate2(d) {
|
|
4108
4322
|
return d.toISOString().slice(0, 10);
|
|
@@ -4133,6 +4347,7 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4133
4347
|
inputTokens: 0,
|
|
4134
4348
|
outputTokens: 0,
|
|
4135
4349
|
cacheReadTokens: 0,
|
|
4350
|
+
cacheCreationTokens: 0,
|
|
4136
4351
|
requests: 0,
|
|
4137
4352
|
costUsd: 0
|
|
4138
4353
|
});
|
|
@@ -4144,14 +4359,15 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4144
4359
|
for (const sf of sessionFiles) {
|
|
4145
4360
|
const dateStr = isoDate2(new Date(sf.mtimeMs));
|
|
4146
4361
|
const bucket = bucketMap.get(dateStr);
|
|
4147
|
-
const usage = readSessionUsage(sf.filePath);
|
|
4362
|
+
const usage = await readSessionUsage(sf.filePath);
|
|
4148
4363
|
if (bucket) {
|
|
4149
4364
|
bucket.sessions++;
|
|
4150
4365
|
bucket.inputTokens += usage.inputTokens;
|
|
4151
4366
|
bucket.outputTokens += usage.outputTokens;
|
|
4152
4367
|
bucket.cacheReadTokens += usage.cacheReadTokens;
|
|
4368
|
+
bucket.cacheCreationTokens += usage.cacheCreationTokens;
|
|
4153
4369
|
bucket.requests += usage.requestCount;
|
|
4154
|
-
bucket.costUsd += calcCost3(usage.inputTokens, usage.outputTokens, model);
|
|
4370
|
+
bucket.costUsd += calcCost3(usage.inputTokens, usage.outputTokens, usage.cacheCreationTokens, usage.cacheReadTokens, model);
|
|
4155
4371
|
}
|
|
4156
4372
|
totalInput += usage.inputTokens;
|
|
4157
4373
|
totalOutput += usage.outputTokens;
|
|
@@ -4162,7 +4378,7 @@ async function buildTeamExport(days, model, anonymize) {
|
|
|
4162
4378
|
(e) => new Date(e.timestamp).getTime() >= cutoffMs
|
|
4163
4379
|
);
|
|
4164
4380
|
const topWasteFiles = aggregateStats(fileEvents).slice(0, 10).map((s) => ({ filePath: s.filePath, readCount: s.readCount }));
|
|
4165
|
-
const totalCostUsd = calcCost3(totalInput, totalOutput, model);
|
|
4381
|
+
const totalCostUsd = calcCost3(totalInput, totalOutput, 0, totalCacheRead, model);
|
|
4166
4382
|
const cacheHitRate = totalInput > 0 ? Math.round(totalCacheRead / totalInput * 100) : 0;
|
|
4167
4383
|
const uniqueSessions = new Set(sessionFiles.map((f) => f.sessionId)).size;
|
|
4168
4384
|
const developer = {
|
|
@@ -4213,19 +4429,19 @@ function aggregateTeamReports(exports2) {
|
|
|
4213
4429
|
}
|
|
4214
4430
|
function writeTeamExport(exportData) {
|
|
4215
4431
|
const storeDir = getStoreDir();
|
|
4216
|
-
if (!
|
|
4432
|
+
if (!fs24.existsSync(storeDir)) fs24.mkdirSync(storeDir, { recursive: true });
|
|
4217
4433
|
const date = isoDate2(/* @__PURE__ */ new Date());
|
|
4218
|
-
const filePath =
|
|
4219
|
-
|
|
4434
|
+
const filePath = path27.join(storeDir, `team-export-${date}.json`);
|
|
4435
|
+
fs24.writeFileSync(filePath, JSON.stringify(exportData, null, 2), "utf-8");
|
|
4220
4436
|
return filePath;
|
|
4221
4437
|
}
|
|
4222
4438
|
function readTeamExports(dir) {
|
|
4223
4439
|
const exports2 = [];
|
|
4224
|
-
if (!
|
|
4225
|
-
const files =
|
|
4440
|
+
if (!fs24.existsSync(dir)) return exports2;
|
|
4441
|
+
const files = fs24.readdirSync(dir).filter((f) => f.match(/^team-export-.*\.json$/));
|
|
4226
4442
|
for (const file of files) {
|
|
4227
4443
|
try {
|
|
4228
|
-
const raw =
|
|
4444
|
+
const raw = fs24.readFileSync(path27.join(dir, file), "utf-8");
|
|
4229
4445
|
exports2.push(JSON.parse(raw));
|
|
4230
4446
|
} catch {
|
|
4231
4447
|
}
|
|
@@ -4312,7 +4528,7 @@ Run "claudectx teams export" on each developer machine first.
|
|
|
4312
4528
|
process.stdout.write(" Top shared files (by read count across team):\n");
|
|
4313
4529
|
for (const f of report.topWasteFiles.slice(0, 5)) {
|
|
4314
4530
|
const devList = f.developers.slice(0, 3).join(", ");
|
|
4315
|
-
process.stdout.write(` ${f.readCount}x ${
|
|
4531
|
+
process.stdout.write(` ${f.readCount}x ${path28.basename(f.filePath)} (${devList})
|
|
4316
4532
|
`);
|
|
4317
4533
|
}
|
|
4318
4534
|
process.stdout.write("\n");
|
|
@@ -4325,24 +4541,24 @@ async function teamsShare(options) {
|
|
|
4325
4541
|
process.exit(1);
|
|
4326
4542
|
}
|
|
4327
4543
|
const storeDir = getStoreDir();
|
|
4328
|
-
const exportFiles =
|
|
4544
|
+
const exportFiles = fs25.readdirSync(storeDir).filter((f) => f.match(/^team-export-.*\.json$/)).sort().reverse();
|
|
4329
4545
|
if (exportFiles.length === 0) {
|
|
4330
4546
|
process.stderr.write('No team export files found. Run "claudectx teams export" first.\n');
|
|
4331
4547
|
process.exit(1);
|
|
4332
4548
|
}
|
|
4333
4549
|
const latest = exportFiles[0];
|
|
4334
|
-
const src =
|
|
4550
|
+
const src = path28.join(storeDir, latest);
|
|
4335
4551
|
let destPath;
|
|
4336
4552
|
try {
|
|
4337
|
-
const stat =
|
|
4338
|
-
destPath = stat.isDirectory() ?
|
|
4553
|
+
const stat = fs25.statSync(dest);
|
|
4554
|
+
destPath = stat.isDirectory() ? path28.join(dest, latest) : dest;
|
|
4339
4555
|
} catch {
|
|
4340
4556
|
destPath = dest;
|
|
4341
4557
|
}
|
|
4342
|
-
const destDir =
|
|
4558
|
+
const destDir = path28.dirname(path28.resolve(destPath));
|
|
4343
4559
|
let resolvedDir;
|
|
4344
4560
|
try {
|
|
4345
|
-
resolvedDir =
|
|
4561
|
+
resolvedDir = fs25.realpathSync(destDir);
|
|
4346
4562
|
} catch {
|
|
4347
4563
|
resolvedDir = destDir;
|
|
4348
4564
|
}
|
|
@@ -4352,7 +4568,7 @@ async function teamsShare(options) {
|
|
|
4352
4568
|
`);
|
|
4353
4569
|
process.exit(1);
|
|
4354
4570
|
}
|
|
4355
|
-
|
|
4571
|
+
fs25.copyFileSync(src, destPath);
|
|
4356
4572
|
process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
|
|
4357
4573
|
|
|
4358
4574
|
`);
|
|
@@ -4377,8 +4593,148 @@ async function teamsCommand(subcommand, options) {
|
|
|
4377
4593
|
}
|
|
4378
4594
|
}
|
|
4379
4595
|
|
|
4596
|
+
// src/commands/revert.ts
|
|
4597
|
+
init_cjs_shims();
|
|
4598
|
+
var path29 = __toESM(require("path"));
|
|
4599
|
+
function timeAgo(isoString) {
|
|
4600
|
+
const diffMs = Date.now() - new Date(isoString).getTime();
|
|
4601
|
+
const minutes = Math.floor(diffMs / 6e4);
|
|
4602
|
+
if (minutes < 1) return "just now";
|
|
4603
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
4604
|
+
const hours = Math.floor(minutes / 60);
|
|
4605
|
+
if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
4606
|
+
const days = Math.floor(hours / 24);
|
|
4607
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
4608
|
+
}
|
|
4609
|
+
function formatBytes(bytes) {
|
|
4610
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
4611
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
4612
|
+
}
|
|
4613
|
+
function printBackupTable(entries) {
|
|
4614
|
+
if (entries.length === 0) {
|
|
4615
|
+
process.stdout.write(" No backups found.\n");
|
|
4616
|
+
process.stdout.write(` Backups are created automatically when claudectx modifies your files.
|
|
4617
|
+
`);
|
|
4618
|
+
process.stdout.write(` Backup directory: ${BACKUP_DIR}
|
|
4619
|
+
|
|
4620
|
+
`);
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4623
|
+
const idWidth = 26;
|
|
4624
|
+
const fileWidth = 20;
|
|
4625
|
+
const cmdWidth = 10;
|
|
4626
|
+
const timeWidth = 14;
|
|
4627
|
+
const sizeWidth = 7;
|
|
4628
|
+
const hr = "\u2550".repeat(idWidth + fileWidth + cmdWidth + timeWidth + sizeWidth + 16);
|
|
4629
|
+
process.stdout.write("\n");
|
|
4630
|
+
process.stdout.write("claudectx \u2014 Backup History\n");
|
|
4631
|
+
process.stdout.write(hr + "\n");
|
|
4632
|
+
process.stdout.write(
|
|
4633
|
+
` ${"ID".padEnd(idWidth)} ${"File".padEnd(fileWidth)} ${"Command".padEnd(cmdWidth)} ${"When".padEnd(timeWidth)} ${"Size".padEnd(sizeWidth)}
|
|
4634
|
+
`
|
|
4635
|
+
);
|
|
4636
|
+
process.stdout.write("\u2500".repeat(idWidth + fileWidth + cmdWidth + timeWidth + sizeWidth + 16) + "\n");
|
|
4637
|
+
for (const entry of entries) {
|
|
4638
|
+
const id = entry.id.slice(0, idWidth).padEnd(idWidth);
|
|
4639
|
+
const file = path29.basename(entry.originalPath).slice(0, fileWidth).padEnd(fileWidth);
|
|
4640
|
+
const cmd = entry.command.slice(0, cmdWidth).padEnd(cmdWidth);
|
|
4641
|
+
const when = timeAgo(entry.createdAt).slice(0, timeWidth).padEnd(timeWidth);
|
|
4642
|
+
const size = formatBytes(entry.sizeBytes).padEnd(sizeWidth);
|
|
4643
|
+
process.stdout.write(` ${id} ${file} ${cmd} ${when} ${size}
|
|
4644
|
+
`);
|
|
4645
|
+
}
|
|
4646
|
+
process.stdout.write("\n");
|
|
4647
|
+
process.stdout.write(` Backup directory: ${BACKUP_DIR}
|
|
4648
|
+
`);
|
|
4649
|
+
process.stdout.write(" To restore: claudectx revert --id <ID>\n\n");
|
|
4650
|
+
}
|
|
4651
|
+
async function interactivePick(entries) {
|
|
4652
|
+
try {
|
|
4653
|
+
const { select } = await import("@inquirer/prompts");
|
|
4654
|
+
const choices = entries.map((e) => ({
|
|
4655
|
+
name: `${timeAgo(e.createdAt).padEnd(14)} ${path29.basename(e.originalPath).padEnd(16)} [${e.command}] ${e.id}`,
|
|
4656
|
+
value: e.id
|
|
4657
|
+
}));
|
|
4658
|
+
choices.push({ name: "Cancel", value: "" });
|
|
4659
|
+
return await select({ message: "Choose a backup to restore:", choices });
|
|
4660
|
+
} catch {
|
|
4661
|
+
process.stderr.write("Interactive mode unavailable. Use --id <id> to restore a specific backup.\n");
|
|
4662
|
+
return null;
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
async function doRestore(id) {
|
|
4666
|
+
const chalk5 = (await import("chalk")).default;
|
|
4667
|
+
process.stdout.write("\n");
|
|
4668
|
+
try {
|
|
4669
|
+
const entries = await listBackups();
|
|
4670
|
+
const entry = entries.find((e) => e.id === id);
|
|
4671
|
+
if (!entry) {
|
|
4672
|
+
process.stderr.write(chalk5.red(`Backup "${id}" not found.
|
|
4673
|
+
`));
|
|
4674
|
+
process.stderr.write('Run "claudectx revert --list" to see available backups.\n');
|
|
4675
|
+
process.exitCode = 1;
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
process.stdout.write(chalk5.yellow(`\u26A0 This will overwrite: ${entry.originalPath}
|
|
4679
|
+
`));
|
|
4680
|
+
process.stdout.write(` Backup from: ${timeAgo(entry.createdAt)} (${entry.command})
|
|
4681
|
+
`);
|
|
4682
|
+
process.stdout.write(` Your current file will be backed up first (so you can undo this).
|
|
4683
|
+
|
|
4684
|
+
`);
|
|
4685
|
+
let confirmed = true;
|
|
4686
|
+
try {
|
|
4687
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
4688
|
+
confirmed = await confirm2({ message: "Restore this backup?", default: false });
|
|
4689
|
+
} catch {
|
|
4690
|
+
}
|
|
4691
|
+
if (!confirmed) {
|
|
4692
|
+
process.stdout.write(" Cancelled.\n\n");
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
const { undoEntry } = await restoreBackup(id);
|
|
4696
|
+
process.stdout.write(chalk5.green(" \u2713 ") + `Restored to ${entry.originalPath}
|
|
4697
|
+
`);
|
|
4698
|
+
if (undoEntry) {
|
|
4699
|
+
process.stdout.write(
|
|
4700
|
+
chalk5.dim(` Your previous version was saved as backup "${undoEntry.id}" \u2014 run 'claudectx revert --id ${undoEntry.id}' to undo.
|
|
4701
|
+
`)
|
|
4702
|
+
);
|
|
4703
|
+
}
|
|
4704
|
+
process.stdout.write("\n");
|
|
4705
|
+
} catch (err) {
|
|
4706
|
+
process.stderr.write(chalk5.red(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4707
|
+
`));
|
|
4708
|
+
process.exitCode = 1;
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
async function revertCommand(options) {
|
|
4712
|
+
const entries = await listBackups(options.file);
|
|
4713
|
+
if (options.json) {
|
|
4714
|
+
process.stdout.write(JSON.stringify({ backups: entries }, null, 2) + "\n");
|
|
4715
|
+
return;
|
|
4716
|
+
}
|
|
4717
|
+
if (options.list) {
|
|
4718
|
+
printBackupTable(entries);
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
if (options.id) {
|
|
4722
|
+
await doRestore(options.id);
|
|
4723
|
+
return;
|
|
4724
|
+
}
|
|
4725
|
+
if (entries.length === 0) {
|
|
4726
|
+
process.stdout.write("\n No backups found. Backups are created automatically when claudectx modifies your files.\n\n");
|
|
4727
|
+
return;
|
|
4728
|
+
}
|
|
4729
|
+
printBackupTable(entries);
|
|
4730
|
+
const picked = await interactivePick(entries);
|
|
4731
|
+
if (picked) {
|
|
4732
|
+
await doRestore(picked);
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4380
4736
|
// src/index.ts
|
|
4381
|
-
var VERSION = "1.
|
|
4737
|
+
var VERSION = "1.1.3";
|
|
4382
4738
|
var DESCRIPTION = "Reduce Claude Code token usage by up to 80%. Context analyzer, auto-optimizer, live dashboard, and smart MCP tools.";
|
|
4383
4739
|
var program = new import_commander.Command();
|
|
4384
4740
|
program.name("claudectx").description(DESCRIPTION).version(VERSION);
|
|
@@ -4418,5 +4774,8 @@ program.command("convert").description("Convert CLAUDE.md to another AI assistan
|
|
|
4418
4774
|
program.command("teams [subcommand]").description("Multi-developer cost attribution (export | aggregate | share)").option("--days <n>", "Days to include", "30").option("-m, --model <model>", "Model", "sonnet").option("--anonymize", "Replace identities with Dev 1, Dev 2...").option("--dir <path>", "Directory with team export JSON files").option("--to <path>", "Destination for share sub-command").option("--json", "JSON output").action(async (subcommand, options) => {
|
|
4419
4775
|
await teamsCommand(subcommand ?? "export", options);
|
|
4420
4776
|
});
|
|
4777
|
+
program.command("revert").description("List and restore backups created automatically by claudectx commands").option("--list", "Show all backups").option("--id <id>", "Restore a specific backup by ID").option("--file <path>", "Filter backups by original file path").option("--json", "JSON output").action(async (options) => {
|
|
4778
|
+
await revertCommand(options);
|
|
4779
|
+
});
|
|
4421
4780
|
program.parse();
|
|
4422
4781
|
//# sourceMappingURL=index.js.map
|