ctxdiet 0.1.0
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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/src/agents.js +81 -0
- package/dist/src/constants.js +31 -0
- package/dist/src/fix.js +270 -0
- package/dist/src/index.js +76 -0
- package/dist/src/pricing.js +13 -0
- package/dist/src/report.js +181 -0
- package/dist/src/scan.js +211 -0
- package/dist/src/sources.js +347 -0
- package/dist/src/tokens.js +16 -0
- package/dist/src/trim.js +69 -0
- package/dist/src/types.js +2 -0
- package/package.json +63 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_IGNORE_PATTERNS = exports.HEAVY_FILES = exports.HEAVY_DIRS = void 0;
|
|
7
|
+
exports.isFile = isFile;
|
|
8
|
+
exports.isDir = isDir;
|
|
9
|
+
exports.exists = exists;
|
|
10
|
+
exports.readFileSafe = readFileSafe;
|
|
11
|
+
exports.shortenPath = shortenPath;
|
|
12
|
+
exports.displayPath = displayPath;
|
|
13
|
+
exports.uniq = uniq;
|
|
14
|
+
exports.detectModel = detectModel;
|
|
15
|
+
exports.walkFiles = walkFiles;
|
|
16
|
+
exports.parseIgnore = parseIgnore;
|
|
17
|
+
exports.ignoreCovers = ignoreCovers;
|
|
18
|
+
exports.estimatePathTokens = estimatePathTokens;
|
|
19
|
+
exports.readMcpServers = readMcpServers;
|
|
20
|
+
exports.scanDefinitions = scanDefinitions;
|
|
21
|
+
exports.archivePathFor = archivePathFor;
|
|
22
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
23
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
24
|
+
const constants_1 = require("./constants");
|
|
25
|
+
const tokens_1 = require("./tokens");
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// fs helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
function isFile(p) {
|
|
30
|
+
try {
|
|
31
|
+
return node_fs_1.default.statSync(p).isFile();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isDir(p) {
|
|
38
|
+
try {
|
|
39
|
+
return node_fs_1.default.statSync(p).isDirectory();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function exists(p) {
|
|
46
|
+
try {
|
|
47
|
+
node_fs_1.default.statSync(p);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function readFileSafe(p) {
|
|
55
|
+
try {
|
|
56
|
+
return node_fs_1.default.readFileSync(p, "utf8");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function shortenPath(p, home) {
|
|
63
|
+
return p.startsWith(home) ? p.replace(home, "~") : p;
|
|
64
|
+
}
|
|
65
|
+
/** Readable path: project-relative inside the scan dir, ~ inside home, else absolute. */
|
|
66
|
+
function displayPath(p, projectPath, home) {
|
|
67
|
+
if (p === projectPath)
|
|
68
|
+
return ".";
|
|
69
|
+
if (p.startsWith(projectPath + node_path_1.default.sep)) {
|
|
70
|
+
return "./" + node_path_1.default.relative(projectPath, p);
|
|
71
|
+
}
|
|
72
|
+
if (p.startsWith(home))
|
|
73
|
+
return p.replace(home, "~");
|
|
74
|
+
return p;
|
|
75
|
+
}
|
|
76
|
+
function uniq(items) {
|
|
77
|
+
return [...new Set(items)];
|
|
78
|
+
}
|
|
79
|
+
/** Best-effort read of the user's configured Claude model, for the cost estimate. */
|
|
80
|
+
function detectModel(home) {
|
|
81
|
+
for (const file of [
|
|
82
|
+
node_path_1.default.join(home, ".claude", "settings.json"),
|
|
83
|
+
node_path_1.default.join(home, ".claude.json"),
|
|
84
|
+
]) {
|
|
85
|
+
if (!isFile(file))
|
|
86
|
+
continue;
|
|
87
|
+
let json;
|
|
88
|
+
try {
|
|
89
|
+
json = JSON.parse(readFileSafe(file));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const raw = json?.model;
|
|
95
|
+
const value = typeof raw === "string" ? raw.toLowerCase() : "";
|
|
96
|
+
if (value.includes("opus"))
|
|
97
|
+
return "opus";
|
|
98
|
+
if (value.includes("haiku"))
|
|
99
|
+
return "haiku";
|
|
100
|
+
if (value.includes("sonnet"))
|
|
101
|
+
return "sonnet";
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** Recursively list files under `dir` whose name ends with one of `exts`. */
|
|
106
|
+
function walkFiles(dir, exts) {
|
|
107
|
+
if (!isDir(dir))
|
|
108
|
+
return [];
|
|
109
|
+
const out = [];
|
|
110
|
+
const stack = [dir];
|
|
111
|
+
while (stack.length && out.length < constants_1.HEAVY_WALK_MAX_FILES) {
|
|
112
|
+
const cur = stack.pop();
|
|
113
|
+
let entries;
|
|
114
|
+
try {
|
|
115
|
+
entries = node_fs_1.default.readdirSync(cur, { withFileTypes: true });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
for (const e of entries) {
|
|
121
|
+
const full = node_path_1.default.join(cur, e.name);
|
|
122
|
+
if (e.isDirectory()) {
|
|
123
|
+
stack.push(full);
|
|
124
|
+
}
|
|
125
|
+
else if (e.isFile() && exts.some((x) => e.name.endsWith(x))) {
|
|
126
|
+
out.push(full);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out.sort();
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// heavy paths + ignore files
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
exports.HEAVY_DIRS = [
|
|
136
|
+
// JS / web
|
|
137
|
+
"node_modules", "dist", "build", "out", "coverage", "vendor",
|
|
138
|
+
".next", ".nuxt", ".svelte-kit", ".output", ".angular", ".turbo",
|
|
139
|
+
".parcel-cache", ".cache",
|
|
140
|
+
// python
|
|
141
|
+
".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache",
|
|
142
|
+
// rust / java / go / ios / infra
|
|
143
|
+
"target", ".gradle", "Pods", ".terraform",
|
|
144
|
+
// editor / misc
|
|
145
|
+
".idea", "tmp", "logs",
|
|
146
|
+
];
|
|
147
|
+
exports.HEAVY_FILES = [
|
|
148
|
+
"package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "deno.lock",
|
|
149
|
+
"composer.lock", "Cargo.lock", "poetry.lock", "Pipfile.lock", "Gemfile.lock",
|
|
150
|
+
"go.sum",
|
|
151
|
+
];
|
|
152
|
+
exports.DEFAULT_IGNORE_PATTERNS = [
|
|
153
|
+
"node_modules/", "dist/", "build/", "out/", "coverage/", "vendor/",
|
|
154
|
+
".next/", ".nuxt/", ".svelte-kit/", ".output/", ".turbo/", ".parcel-cache/", ".cache/",
|
|
155
|
+
".venv/", "venv/", "__pycache__/", ".pytest_cache/", ".mypy_cache/", ".ruff_cache/",
|
|
156
|
+
"target/", ".gradle/", "Pods/", ".terraform/", ".idea/", "tmp/", "logs/",
|
|
157
|
+
"*.lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "go.sum",
|
|
158
|
+
"*.min.js", "*.min.css", "*.map", "*.log",
|
|
159
|
+
".env", ".env.*", ".DS_Store", ".git/",
|
|
160
|
+
];
|
|
161
|
+
function parseIgnore(content) {
|
|
162
|
+
return content
|
|
163
|
+
.split("\n")
|
|
164
|
+
.map((l) => l.trim())
|
|
165
|
+
.filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
166
|
+
}
|
|
167
|
+
function ignoreCovers(patterns, name) {
|
|
168
|
+
const norm = patterns.map((p) => p.replace(/^\.\//, "").replace(/\/$/, ""));
|
|
169
|
+
return norm.includes(name) || norm.includes("/" + name);
|
|
170
|
+
}
|
|
171
|
+
function walkBytes(dir) {
|
|
172
|
+
let bytes = 0;
|
|
173
|
+
let files = 0;
|
|
174
|
+
const stack = [dir];
|
|
175
|
+
while (stack.length) {
|
|
176
|
+
const cur = stack.pop();
|
|
177
|
+
let entries;
|
|
178
|
+
try {
|
|
179
|
+
entries = node_fs_1.default.readdirSync(cur, { withFileTypes: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (const e of entries) {
|
|
185
|
+
const full = node_path_1.default.join(cur, e.name);
|
|
186
|
+
if (e.isDirectory()) {
|
|
187
|
+
stack.push(full);
|
|
188
|
+
}
|
|
189
|
+
else if (e.isFile()) {
|
|
190
|
+
try {
|
|
191
|
+
bytes += node_fs_1.default.statSync(full).size;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* ignore unreadable file */
|
|
195
|
+
}
|
|
196
|
+
files++;
|
|
197
|
+
}
|
|
198
|
+
if (files >= constants_1.HEAVY_WALK_MAX_FILES || bytes >= constants_1.HEAVY_WALK_MAX_BYTES) {
|
|
199
|
+
return bytes;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return bytes;
|
|
204
|
+
}
|
|
205
|
+
/** Capped, bounded token estimate for a heavy dir or file that could be read. */
|
|
206
|
+
function estimatePathTokens(p) {
|
|
207
|
+
let bytes = 0;
|
|
208
|
+
if (isDir(p)) {
|
|
209
|
+
bytes = walkBytes(p);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
try {
|
|
213
|
+
bytes = node_fs_1.default.statSync(p).size;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
bytes = 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return Math.min((0, tokens_1.estimateTokensFromBytes)(bytes), constants_1.HEAVY_PATH_TOKEN_CAP);
|
|
220
|
+
}
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// MCP servers (any JSON config exposing `mcpServers`)
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
function readMcpServers(file) {
|
|
225
|
+
if (!isFile(file))
|
|
226
|
+
return [];
|
|
227
|
+
let json;
|
|
228
|
+
try {
|
|
229
|
+
json = JSON.parse(readFileSafe(file));
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return []; // malformed JSON — skip rather than guess
|
|
233
|
+
}
|
|
234
|
+
const servers = json?.mcpServers;
|
|
235
|
+
if (servers && typeof servers === "object") {
|
|
236
|
+
return Object.keys(servers);
|
|
237
|
+
}
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Claude-specific definition inventory (agents / skills / commands)
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
const JUNK_RE = /(\.bak|\.orig|\.tmp|\.swp|~)$/i;
|
|
244
|
+
const JUNK_NAMES = new Set([".DS_Store", "Thumbs.db"]);
|
|
245
|
+
function dirTokens(dir) {
|
|
246
|
+
return (0, tokens_1.estimateTokensFromBytes)(walkBytes(dir));
|
|
247
|
+
}
|
|
248
|
+
function scanDefinitions(o) {
|
|
249
|
+
const dead = [];
|
|
250
|
+
const real = [];
|
|
251
|
+
const roots = [
|
|
252
|
+
["agents", node_path_1.default.join(o.home, ".claude", "agents")],
|
|
253
|
+
["commands", node_path_1.default.join(o.home, ".claude", "commands")],
|
|
254
|
+
["skills", node_path_1.default.join(o.home, ".claude", "skills")],
|
|
255
|
+
];
|
|
256
|
+
for (const [kind, root] of roots) {
|
|
257
|
+
if (!isDir(root))
|
|
258
|
+
continue;
|
|
259
|
+
if (kind === "skills") {
|
|
260
|
+
scanSkillRoot(root, dead, real);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
scanFlatRoot(root, dead, real);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { dead, real };
|
|
267
|
+
}
|
|
268
|
+
/** agents/ and commands/ hold .md definition files (possibly nested). */
|
|
269
|
+
function scanFlatRoot(root, dead, real) {
|
|
270
|
+
const stack = [root];
|
|
271
|
+
while (stack.length) {
|
|
272
|
+
const cur = stack.pop();
|
|
273
|
+
let entries;
|
|
274
|
+
try {
|
|
275
|
+
entries = node_fs_1.default.readdirSync(cur, { withFileTypes: true });
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
for (const e of entries) {
|
|
281
|
+
const full = node_path_1.default.join(cur, e.name);
|
|
282
|
+
if (e.isDirectory()) {
|
|
283
|
+
stack.push(full);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!e.isFile())
|
|
287
|
+
continue;
|
|
288
|
+
classifyFile(full, e.name, dead, real);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** skills/ holds one folder per skill, each requiring a SKILL.md. */
|
|
293
|
+
function scanSkillRoot(root, dead, real) {
|
|
294
|
+
let entries;
|
|
295
|
+
try {
|
|
296
|
+
entries = node_fs_1.default.readdirSync(root, { withFileTypes: true });
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
for (const e of entries) {
|
|
302
|
+
const full = node_path_1.default.join(root, e.name);
|
|
303
|
+
if (e.isFile()) {
|
|
304
|
+
classifyFile(full, e.name, dead, real);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (!e.isDirectory())
|
|
308
|
+
continue;
|
|
309
|
+
const skillMd = node_path_1.default.join(full, "SKILL.md");
|
|
310
|
+
if (!isFile(skillMd)) {
|
|
311
|
+
dead.push({
|
|
312
|
+
path: full,
|
|
313
|
+
reason: "skill folder missing SKILL.md (cannot load)",
|
|
314
|
+
tokens: dirTokens(full),
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
real.push({
|
|
319
|
+
path: full,
|
|
320
|
+
reason: "skill",
|
|
321
|
+
tokens: (0, tokens_1.estimateTokens)(readFileSafe(skillMd)),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function classifyFile(full, name, dead, real) {
|
|
326
|
+
if (JUNK_NAMES.has(name) || JUNK_RE.test(name)) {
|
|
327
|
+
dead.push({
|
|
328
|
+
path: full,
|
|
329
|
+
reason: "backup/temp artifact",
|
|
330
|
+
tokens: (0, tokens_1.estimateTokens)(readFileSafe(full)),
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const content = readFileSafe(full);
|
|
335
|
+
if (content.trim() === "") {
|
|
336
|
+
dead.push({ path: full, reason: "empty definition file", tokens: 0 });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (name.toLowerCase().endsWith(".md")) {
|
|
340
|
+
real.push({ path: full, reason: "definition", tokens: (0, tokens_1.estimateTokens)(content) });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function archivePathFor(p, home) {
|
|
344
|
+
const base = node_path_1.default.join(home, ".claude");
|
|
345
|
+
const rel = p.startsWith(base) ? node_path_1.default.relative(base, p) : node_path_1.default.basename(p);
|
|
346
|
+
return node_path_1.default.join(home, ".claude", ".ctxdiet-archive", rel);
|
|
347
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.estimateTokens = estimateTokens;
|
|
4
|
+
exports.estimateTokensFromBytes = estimateTokensFromBytes;
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
/**
|
|
7
|
+
* Estimate tokens with the documented chars/4 heuristic. This is an estimate,
|
|
8
|
+
* not a real tokenizer — see README.
|
|
9
|
+
*/
|
|
10
|
+
function estimateTokens(text) {
|
|
11
|
+
return Math.ceil(text.length / constants_1.CHARS_PER_TOKEN);
|
|
12
|
+
}
|
|
13
|
+
/** Same heuristic from a byte count (bytes ≈ chars for the text we read). */
|
|
14
|
+
function estimateTokensFromBytes(bytes) {
|
|
15
|
+
return Math.ceil(bytes / constants_1.CHARS_PER_TOKEN);
|
|
16
|
+
}
|
package/dist/src/trim.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trimMarkdown = trimMarkdown;
|
|
4
|
+
/**
|
|
5
|
+
* Conservative CLAUDE.md/AGENTS.md trimmer. Only removes *provably* redundant
|
|
6
|
+
* content so a trim is always safe and reversible (callers also keep a .bak):
|
|
7
|
+
* - trailing whitespace on every line
|
|
8
|
+
* - runs of blank lines collapsed to a single blank line
|
|
9
|
+
* - exact-duplicate headers (only the first kept)
|
|
10
|
+
* - exact-duplicate substantial prose lines (>=20 chars, not lists/structural)
|
|
11
|
+
* Anything inside fenced code blocks is left untouched.
|
|
12
|
+
*/
|
|
13
|
+
function trimMarkdown(input) {
|
|
14
|
+
const lines = input.split("\n");
|
|
15
|
+
const out = [];
|
|
16
|
+
const seenLines = new Set();
|
|
17
|
+
const seenHeaders = new Set();
|
|
18
|
+
let inFence = false;
|
|
19
|
+
let blankRun = 0;
|
|
20
|
+
for (const raw of lines) {
|
|
21
|
+
const line = raw.replace(/[ \t]+$/g, "");
|
|
22
|
+
const isFenceToggle = /^\s*```/.test(line);
|
|
23
|
+
if (isFenceToggle) {
|
|
24
|
+
inFence = !inFence;
|
|
25
|
+
blankRun = 0;
|
|
26
|
+
out.push(line);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (inFence) {
|
|
30
|
+
out.push(line);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (line.trim() === "") {
|
|
34
|
+
blankRun++;
|
|
35
|
+
if (blankRun > 1)
|
|
36
|
+
continue; // collapse blank runs to one
|
|
37
|
+
out.push("");
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
blankRun = 0;
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
// Dedupe identical headers (keep first occurrence).
|
|
43
|
+
if (/^#{1,6}\s+\S/.test(trimmed)) {
|
|
44
|
+
const key = trimmed.toLowerCase();
|
|
45
|
+
if (seenHeaders.has(key))
|
|
46
|
+
continue;
|
|
47
|
+
seenHeaders.add(key);
|
|
48
|
+
out.push(line);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Dedupe exact-duplicate substantial lines (prose or list items) — a
|
|
52
|
+
// repeated line of real length is almost always an accidental paste. Skip
|
|
53
|
+
// pure structural markers (rules, bare table borders) where repetition is
|
|
54
|
+
// meaningful, and keep it reversible via the .bak the caller writes.
|
|
55
|
+
const isStructural = /^[-=|>#`]+$/.test(trimmed);
|
|
56
|
+
if (!isStructural && trimmed.length >= 20) {
|
|
57
|
+
if (seenLines.has(trimmed))
|
|
58
|
+
continue;
|
|
59
|
+
seenLines.add(trimmed);
|
|
60
|
+
}
|
|
61
|
+
out.push(line);
|
|
62
|
+
}
|
|
63
|
+
// Drop leading/trailing blank lines, guarantee a single trailing newline.
|
|
64
|
+
while (out.length && out[0] === "")
|
|
65
|
+
out.shift();
|
|
66
|
+
while (out.length && out[out.length - 1] === "")
|
|
67
|
+
out.pop();
|
|
68
|
+
return out.length ? out.join("\n") + "\n" : "";
|
|
69
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ctxdiet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Detect, fix, and measure AI coding-agent context-token waste across Claude Code, Codex, Cursor, Gemini, Windsurf & Copilot. Other tools report waste — ctxdiet fixes it.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"codex",
|
|
9
|
+
"cursor",
|
|
10
|
+
"gemini",
|
|
11
|
+
"copilot",
|
|
12
|
+
"windsurf",
|
|
13
|
+
"agents",
|
|
14
|
+
"agents-md",
|
|
15
|
+
"mcp",
|
|
16
|
+
"tokens",
|
|
17
|
+
"context",
|
|
18
|
+
"cli",
|
|
19
|
+
"ai"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Merlijn de Groot <merlijndegroot498@gmail.com>",
|
|
23
|
+
"homepage": "https://github.com/Merlijnos/ctxdiet#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Merlijnos/ctxdiet.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/Merlijnos/ctxdiet/issues"
|
|
30
|
+
},
|
|
31
|
+
"funding": "https://github.com/sponsors/Merlijnos",
|
|
32
|
+
"type": "commonjs",
|
|
33
|
+
"bin": {
|
|
34
|
+
"ctxdiet": "dist/src/index.js",
|
|
35
|
+
"slimclaude": "dist/src/index.js"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/src",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"prepare": "tsc",
|
|
48
|
+
"test": "tsc && node --test dist/test/*.test.js",
|
|
49
|
+
"prepublishOnly": "npm test",
|
|
50
|
+
"start": "node dist/src/index.js"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"chalk": "^4.1.2",
|
|
54
|
+
"cli-table3": "^0.6.5",
|
|
55
|
+
"commander": "^12.1.0",
|
|
56
|
+
"diff": "^5.2.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/diff": "^5.2.3",
|
|
60
|
+
"@types/node": "^20.14.0",
|
|
61
|
+
"typescript": "^5.5.0"
|
|
62
|
+
}
|
|
63
|
+
}
|