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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Merlijn de Groot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # ctxdiet
2
+
3
+ [![npm version](https://img.shields.io/npm/v/ctxdiet.svg)](https://www.npmjs.com/package/ctxdiet)
4
+ [![CI](https://github.com/Merlijnos/ctxdiet/actions/workflows/ci.yml/badge.svg)](https://github.com/Merlijnos/ctxdiet/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
6
+
7
+ Your AI coding agents reload the same context every session. ctxdiet finds the
8
+ waste, fixes it with diffs you approve, and shows what you saved. Local, no account.
9
+
10
+ ```
11
+ npx ctxdiet # scan, read-only
12
+ npx ctxdiet fix # show diffs, confirm, apply, measure
13
+ ```
14
+
15
+ ## What one fix looks like
16
+
17
+ ```
18
+ Before vs after
19
+ ┌────────────────────────┬────────┬───┬───────┬────────┐
20
+ │ │ Before │ │ After │ Saved │
21
+ ├────────────────────────┼────────┼───┼───────┼────────┤
22
+ │ Context tokens/session │ 21,346 │ → │ 1,227 │ 20,119 │
23
+ │ $/month │ $6.40 │ → │ $0.37 │ $6.04 │
24
+ │ Grade │ F │ → │ A │ │
25
+ └────────────────────────┴────────┴───┴───────┴────────┘
26
+ ```
27
+
28
+ Real run on a repo using Claude Code + Cursor: trimmed duplicate memory lines,
29
+ created `.claudeignore` and `.cursorignore`, archived dead `~/.claude` files.
30
+
31
+ ## Agents
32
+
33
+ Auto-detected; only the ones you use are scanned.
34
+
35
+ | Agent | Memory | Ignore |
36
+ | -------------- | --------------------------------------- | ---------------- |
37
+ | Claude Code | `CLAUDE.md`, `~/.claude/CLAUDE.md` | `.claudeignore` |
38
+ | Codex | `AGENTS.md` | — |
39
+ | Cursor | `.cursorrules`, `.cursor/rules/*.mdc` | `.cursorignore` |
40
+ | Gemini CLI | `GEMINI.md` | `.geminiignore` |
41
+ | Windsurf | `.windsurfrules` | `.codeiumignore` |
42
+ | GitHub Copilot | `.github/copilot-instructions.md` | — |
43
+
44
+ ## What it does
45
+
46
+ - **Finds:** duplicate memory lines, missing ignore files, MCP tool schemas, and
47
+ dead `~/.claude` files (empty, `.bak`, broken skills).
48
+ - **Fixes** each with a diff you confirm. Never deletes (archives instead), always
49
+ writes a `.bak`, and `--yes` only touches provably-dead waste.
50
+ - **Leaves alone** anything whose usage it can't verify (MCP servers, real skills).
51
+ It doesn't read your history, so those are listed for review, never auto-removed.
52
+
53
+ Token numbers are a `chars / 4` estimate, not a tokenizer — close enough to rank waste.
54
+
55
+ ## Flags
56
+
57
+ ```
58
+ --path <dir> directory to scan (default: current)
59
+ --model <opus|sonnet|haiku> pricing for the $ estimate (default: sonnet)
60
+ --sessions-per-month <n> default 100
61
+ --dry-run show diffs, write nothing
62
+ --yes apply without prompting
63
+ --json machine-readable output
64
+ ```
65
+
66
+ Node 20+. MIT. Sponsor: https://github.com/sponsors/Merlijnos
@@ -0,0 +1,81 @@
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.AGENTS = void 0;
7
+ exports.detectAgents = detectAgents;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const sources_1 = require("./sources");
10
+ const P = (o, ...parts) => node_path_1.default.join(o.path, ...parts);
11
+ const H = (o, ...parts) => node_path_1.default.join(o.home, ...parts);
12
+ exports.AGENTS = [
13
+ {
14
+ id: "claude",
15
+ label: "Claude Code",
16
+ ownsDefinitions: true,
17
+ memoryFiles: (o) => (0, sources_1.uniq)([P(o, "CLAUDE.md"), P(o, ".claude", "CLAUDE.md"), H(o, ".claude", "CLAUDE.md")]),
18
+ ignoreFile: ".claudeignore",
19
+ mcpFiles: (o) => (0, sources_1.uniq)([
20
+ P(o, ".mcp.json"),
21
+ P(o, ".claude", "settings.json"),
22
+ H(o, ".claude.json"),
23
+ H(o, ".claude", "settings.json"),
24
+ ]),
25
+ detectSignals: (o) => [
26
+ P(o, "CLAUDE.md"),
27
+ P(o, ".claude"),
28
+ P(o, ".claudeignore"),
29
+ P(o, ".mcp.json"),
30
+ H(o, ".claude"),
31
+ ],
32
+ },
33
+ {
34
+ id: "codex",
35
+ label: "Codex / AGENTS.md",
36
+ memoryFiles: (o) => (0, sources_1.uniq)([P(o, "AGENTS.md"), H(o, ".codex", "AGENTS.md")]),
37
+ mcpFiles: () => [],
38
+ detectSignals: (o) => [P(o, "AGENTS.md"), H(o, ".codex")],
39
+ },
40
+ {
41
+ id: "cursor",
42
+ label: "Cursor",
43
+ memoryFiles: (o) => (0, sources_1.uniq)([P(o, ".cursorrules"), ...(0, sources_1.walkFiles)(P(o, ".cursor", "rules"), [".mdc", ".md"])]),
44
+ ignoreFile: ".cursorignore",
45
+ mcpFiles: (o) => (0, sources_1.uniq)([P(o, ".cursor", "mcp.json"), H(o, ".cursor", "mcp.json")]),
46
+ detectSignals: (o) => [P(o, ".cursorrules"), P(o, ".cursor"), P(o, ".cursorignore")],
47
+ },
48
+ {
49
+ id: "gemini",
50
+ label: "Gemini CLI",
51
+ memoryFiles: (o) => (0, sources_1.uniq)([P(o, "GEMINI.md"), H(o, ".gemini", "GEMINI.md")]),
52
+ ignoreFile: ".geminiignore",
53
+ mcpFiles: (o) => (0, sources_1.uniq)([H(o, ".gemini", "settings.json"), P(o, ".gemini", "settings.json")]),
54
+ detectSignals: (o) => [P(o, "GEMINI.md"), P(o, ".gemini"), H(o, ".gemini")],
55
+ },
56
+ {
57
+ id: "windsurf",
58
+ label: "Windsurf",
59
+ memoryFiles: (o) => (0, sources_1.uniq)([P(o, ".windsurfrules"), ...(0, sources_1.walkFiles)(P(o, ".windsurf", "rules"), [".md"])]),
60
+ ignoreFile: ".codeiumignore",
61
+ mcpFiles: () => [],
62
+ detectSignals: (o) => [P(o, ".windsurfrules"), P(o, ".windsurf")],
63
+ },
64
+ {
65
+ id: "copilot",
66
+ label: "GitHub Copilot",
67
+ memoryFiles: (o) => (0, sources_1.uniq)([
68
+ P(o, ".github", "copilot-instructions.md"),
69
+ ...(0, sources_1.walkFiles)(P(o, ".github", "instructions"), [".instructions.md", ".md"]),
70
+ ]),
71
+ mcpFiles: (o) => [P(o, ".vscode", "mcp.json")],
72
+ detectSignals: (o) => [
73
+ P(o, ".github", "copilot-instructions.md"),
74
+ P(o, ".github", "instructions"),
75
+ ],
76
+ },
77
+ ];
78
+ /** Agents with at least one existing signal (project- or home-level). */
79
+ function detectAgents(o) {
80
+ return exports.AGENTS.filter((a) => a.detectSignals(o).some((sig) => (0, sources_1.exists)(sig) && ((0, sources_1.isFile)(sig) || (0, sources_1.isDir)(sig))));
81
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ // All numbers here are deliberate, documented heuristics — see README "How it
3
+ // estimates tokens". ctxdiet never claims exactness.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.LARGE_CLAUDEMD_TOKENS = exports.HEAVY_WALK_MAX_BYTES = exports.HEAVY_WALK_MAX_FILES = exports.HEAVY_PATH_TOKEN_CAP = exports.MCP_SERVER_TOKEN_EST = exports.CHARS_PER_TOKEN = void 0;
6
+ exports.grade = grade;
7
+ /** The one and only token heuristic: ~4 chars per token. */
8
+ exports.CHARS_PER_TOKEN = 4;
9
+ /** Rough average token cost of one MCP server's injected tool schemas. */
10
+ exports.MCP_SERVER_TOKEN_EST = 550;
11
+ /** A session reads only a fraction of a heavy dir; cap the per-path estimate. */
12
+ exports.HEAVY_PATH_TOKEN_CAP = 5000;
13
+ /** Bound the directory walk so scanning stays fast on huge trees. */
14
+ exports.HEAVY_WALK_MAX_FILES = 2000;
15
+ exports.HEAVY_WALK_MAX_BYTES = 8 * 1024 * 1024;
16
+ /** A CLAUDE.md above this with no trimmable redundancy is flagged for manual review. */
17
+ exports.LARGE_CLAUDEMD_TOKENS = 3000;
18
+ /** Grade thresholds on HIGH-confidence waste tokens/session. */
19
+ const GRADE_THRESHOLDS = [
20
+ [500, "A"],
21
+ [2000, "B"],
22
+ [5000, "C"],
23
+ [10000, "D"],
24
+ ];
25
+ function grade(wasteTokens) {
26
+ for (const [limit, letter] of GRADE_THRESHOLDS) {
27
+ if (wasteTokens < limit)
28
+ return letter;
29
+ }
30
+ return "F";
31
+ }
@@ -0,0 +1,270 @@
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.runFix = runFix;
7
+ const diff_1 = require("diff");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_readline_1 = __importDefault(require("node:readline"));
12
+ const report_1 = require("./report");
13
+ const scan_1 = require("./scan");
14
+ const sources_1 = require("./sources");
15
+ const trim_1 = require("./trim");
16
+ /** Build the concrete change from fresh on-disk state (reflects prior edits). */
17
+ function buildChange(action) {
18
+ switch (action.type) {
19
+ case "trim": {
20
+ const before = (0, sources_1.readFileSafe)(action.path);
21
+ const after = (0, trim_1.trimMarkdown)(before);
22
+ if (before === after)
23
+ return null;
24
+ return { kind: "write", path: action.path, before, after, isNew: false };
25
+ }
26
+ case "ignore-create": {
27
+ return {
28
+ kind: "write",
29
+ path: action.path,
30
+ before: "",
31
+ after: action.content,
32
+ isNew: true,
33
+ };
34
+ }
35
+ case "ignore-augment": {
36
+ const before = (0, sources_1.readFileSafe)(action.path);
37
+ const body = before.replace(/\n*$/, "\n");
38
+ const after = body + "\n# added by ctxdiet\n" + action.added.join("\n") + "\n";
39
+ return { kind: "write", path: action.path, before, after, isNew: false };
40
+ }
41
+ case "mcp-disable": {
42
+ const before = (0, sources_1.readFileSafe)(action.path);
43
+ const after = disableMcpServer(before, action.server);
44
+ if (after === null || after === before)
45
+ return null;
46
+ return { kind: "mcp", path: action.path, before, after, server: action.server };
47
+ }
48
+ case "archive": {
49
+ return { kind: "move", path: action.path, to: action.archiveTo };
50
+ }
51
+ }
52
+ }
53
+ function disableMcpServer(content, server) {
54
+ let json;
55
+ try {
56
+ json = JSON.parse(content);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ const servers = json.mcpServers;
62
+ if (!servers || !(server in servers))
63
+ return null;
64
+ const disabled = json.mcpServers_disabledByCtxdiet ?? {};
65
+ disabled[server] = servers[server];
66
+ delete servers[server];
67
+ json.mcpServers_disabledByCtxdiet = disabled;
68
+ return JSON.stringify(json, null, 2) + "\n";
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // rendering + applying
72
+ // ---------------------------------------------------------------------------
73
+ function colorizeDiff(patch) {
74
+ return patch
75
+ .split("\n")
76
+ .map((line) => {
77
+ if (line.startsWith("+++") || line.startsWith("---"))
78
+ return chalk_1.default.dim(line);
79
+ if (line.startsWith("@@"))
80
+ return chalk_1.default.cyan(line);
81
+ if (line.startsWith("+"))
82
+ return chalk_1.default.green(line);
83
+ if (line.startsWith("-"))
84
+ return chalk_1.default.red(line);
85
+ return line;
86
+ })
87
+ .join("\n");
88
+ }
89
+ function printChange(change, o) {
90
+ const rel = (0, sources_1.displayPath)(change.path, o.path, o.home);
91
+ if (change.kind === "move") {
92
+ console.log(" " +
93
+ chalk_1.default.red("archive ") +
94
+ rel +
95
+ chalk_1.default.dim(" → ") +
96
+ (0, sources_1.displayPath)(change.to, o.path, o.home));
97
+ return;
98
+ }
99
+ if (change.kind === "mcp") {
100
+ console.log(" " + chalk_1.default.bold(rel));
101
+ console.log(" " +
102
+ chalk_1.default.red(`- mcpServers.${change.server}`) +
103
+ chalk_1.default.dim(" (kept under ") +
104
+ chalk_1.default.green(`mcpServers_disabledByCtxdiet.${change.server}`) +
105
+ chalk_1.default.dim(", reversible — JSON is reserialized, .bak saved)"));
106
+ return;
107
+ }
108
+ const label = change.isNew ? `${rel} (new file)` : rel;
109
+ const patch = (0, diff_1.createTwoFilesPatch)(label, label, change.before, change.after, "", "", {
110
+ context: 2,
111
+ });
112
+ // Drop the noisy "Index:"/"===" header lines and trailing tabs the diff lib adds.
113
+ const cleaned = patch
114
+ .split("\n")
115
+ .filter((l) => !l.startsWith("Index: ") && !/^=+$/.test(l))
116
+ .map((l) => (l.startsWith("--- ") || l.startsWith("+++ ") ? l.replace(/\s+$/, "") : l))
117
+ .join("\n");
118
+ console.log(colorizeDiff(cleaned));
119
+ }
120
+ function backup(p) {
121
+ if (!node_fs_1.default.existsSync(p))
122
+ return;
123
+ let bak = p + ".bak";
124
+ if (node_fs_1.default.existsSync(bak))
125
+ bak = `${p}.bak.${Date.now()}`;
126
+ node_fs_1.default.copyFileSync(p, bak);
127
+ }
128
+ function applyChange(change) {
129
+ if (change.kind === "move") {
130
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(change.to), { recursive: true });
131
+ try {
132
+ node_fs_1.default.renameSync(change.path, change.to);
133
+ }
134
+ catch {
135
+ // cross-device or dir move fallback: copy then remove.
136
+ node_fs_1.default.cpSync(change.path, change.to, { recursive: true });
137
+ node_fs_1.default.rmSync(change.path, { recursive: true, force: true });
138
+ }
139
+ return;
140
+ }
141
+ // write / mcp — back up any existing file before overwriting.
142
+ const isNewFile = change.kind === "write" && change.isNew;
143
+ if (!isNewFile && node_fs_1.default.existsSync(change.path))
144
+ backup(change.path);
145
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(change.path), { recursive: true });
146
+ node_fs_1.default.writeFileSync(change.path, change.after, "utf8");
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // prompt
150
+ // ---------------------------------------------------------------------------
151
+ function confirm(question) {
152
+ if (!process.stdin.isTTY)
153
+ return Promise.resolve(false);
154
+ const rl = node_readline_1.default.createInterface({
155
+ input: process.stdin,
156
+ output: process.stdout,
157
+ });
158
+ return new Promise((resolve) => {
159
+ rl.question(`${question} [y/N] `, (answer) => {
160
+ rl.close();
161
+ resolve(/^y(es)?$/i.test(answer.trim()));
162
+ });
163
+ });
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // runFix
167
+ // ---------------------------------------------------------------------------
168
+ async function runFix(o) {
169
+ const before = (0, scan_1.scan)(o);
170
+ const high = before.findings.filter((f) => f.confidence === "high" && f.fixable && f.action);
171
+ const low = before.findings.filter((f) => f.confidence === "low" && f.fixable && f.action);
172
+ if (high.length === 0 && low.length === 0) {
173
+ if (o.json) {
174
+ console.log(JSON.stringify({ message: "nothing to fix", before: (0, report_1.toJson)(before) }, null, 2));
175
+ }
176
+ else {
177
+ console.log(chalk_1.default.green("\nNothing to fix — your setup is already lean.\n"));
178
+ }
179
+ return;
180
+ }
181
+ let lowApplied = 0;
182
+ // ---- HIGH-confidence: normal [y/N] / --yes / --dry-run flow ----
183
+ if (high.length > 0 && !o.json) {
184
+ console.log(chalk_1.default.bold("\nHigh-confidence fixes\n"));
185
+ }
186
+ for (const f of high) {
187
+ const change = buildChange(f.action);
188
+ if (!change)
189
+ continue;
190
+ if (!o.json) {
191
+ console.log(chalk_1.default.bold(`• ${f.agent} · ${f.category} — ${f.title}`));
192
+ printChange(change, o);
193
+ }
194
+ const go = decideHigh(o, () => confirm("Apply this change?"));
195
+ await applyIf(go, change, o);
196
+ }
197
+ // ---- LOW-confidence: explicit interactive prompt only, never --yes ----
198
+ if (low.length > 0) {
199
+ if (o.yes) {
200
+ if (!o.json) {
201
+ console.log(chalk_1.default.yellow(`\nSkipped ${low.length} usage-unconfirmed item(s) (MCP servers / definitions). ` +
202
+ `--yes never touches these — re-run \`ctxdiet fix\` without --yes to review them.`));
203
+ }
204
+ }
205
+ else if (o.json) {
206
+ // non-interactive: cannot prompt, leave for interactive review.
207
+ }
208
+ else {
209
+ console.log(chalk_1.default.yellow.bold("\nReview items — usage not confirmed\n"));
210
+ console.log(chalk_1.default.dim("ctxdiet can't see your history. Only disable what you know you don't use.\n"));
211
+ for (const f of low) {
212
+ const change = buildChange(f.action);
213
+ if (!change)
214
+ continue;
215
+ console.log(chalk_1.default.bold(`• ${f.agent} · ${f.category} — ${f.title}`));
216
+ printChange(change, o);
217
+ const verb = f.action.type === "mcp-disable" ? "Disable" : "Archive";
218
+ const go = o.dryRun
219
+ ? false
220
+ : await confirm(`${verb} this? Only do this if you know it's unused.`);
221
+ if (go && !o.dryRun) {
222
+ applyChange(change);
223
+ lowApplied += f.tokensPerSession;
224
+ console.log(chalk_1.default.green(" applied\n"));
225
+ }
226
+ else {
227
+ console.log(chalk_1.default.dim(" skipped\n"));
228
+ }
229
+ }
230
+ }
231
+ }
232
+ // ---- measure ----
233
+ const after = (0, scan_1.scan)(o);
234
+ if (o.json) {
235
+ console.log(JSON.stringify({
236
+ dryRun: o.dryRun,
237
+ before: (0, report_1.toJson)(before),
238
+ after: (0, report_1.toJson)(after),
239
+ savedTokens: before.baselineTokens - after.baselineTokens,
240
+ }, null, 2));
241
+ return;
242
+ }
243
+ if (o.dryRun) {
244
+ console.log(chalk_1.default.yellow("\nDry run — no files were written."));
245
+ }
246
+ (0, report_1.printBeforeAfter)(before, after, o, lowApplied);
247
+ }
248
+ function decideHigh(o, ask) {
249
+ if (o.dryRun)
250
+ return Promise.resolve(false);
251
+ if (o.yes)
252
+ return Promise.resolve(true);
253
+ if (o.json)
254
+ return Promise.resolve(false); // json non-interactive
255
+ return ask();
256
+ }
257
+ async function applyIf(go, change, o) {
258
+ const ok = await go;
259
+ if (ok && !o.dryRun) {
260
+ applyChange(change);
261
+ if (!o.json)
262
+ console.log(chalk_1.default.green(" applied\n"));
263
+ }
264
+ else if (!o.json && !o.dryRun) {
265
+ console.log(chalk_1.default.dim(" skipped\n"));
266
+ }
267
+ else if (!o.json) {
268
+ console.log();
269
+ }
270
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const fix_1 = require("./fix");
11
+ const report_1 = require("./report");
12
+ const scan_1 = require("./scan");
13
+ const sources_1 = require("./sources");
14
+ function resolveOptions(raw, modelFromCli) {
15
+ const home = node_os_1.default.homedir();
16
+ let model;
17
+ let modelDetected = false;
18
+ if (modelFromCli) {
19
+ const m = (raw.model ?? "sonnet").toLowerCase();
20
+ if (m !== "opus" && m !== "sonnet" && m !== "haiku") {
21
+ console.error(`Invalid --model "${raw.model}". Use opus, sonnet, or haiku.`);
22
+ process.exit(1);
23
+ }
24
+ model = m;
25
+ }
26
+ else {
27
+ const detected = (0, sources_1.detectModel)(home);
28
+ model = detected ?? "sonnet";
29
+ modelDetected = detected !== null;
30
+ }
31
+ const parsed = Number.parseInt(raw.sessionsPerMonth ?? "100", 10);
32
+ const sessionsPerMonth = Number.isFinite(parsed) && parsed > 0 ? parsed : 100;
33
+ return {
34
+ path: node_path_1.default.resolve(raw.path ?? process.cwd()),
35
+ home,
36
+ sessionsPerMonth,
37
+ model,
38
+ modelDetected,
39
+ json: Boolean(raw.json),
40
+ dryRun: Boolean(raw.dryRun),
41
+ yes: Boolean(raw.yes),
42
+ };
43
+ }
44
+ function addCommonOptions(cmd) {
45
+ return cmd
46
+ .option("--path <dir>", "project directory to scan", process.cwd())
47
+ .option("--sessions-per-month <n>", "sessions/month for cost estimate", "100")
48
+ .option("--model <model>", "pricing model: opus|sonnet|haiku", "sonnet")
49
+ .option("--json", "machine-readable JSON output")
50
+ .option("--dry-run", "show changes but write nothing")
51
+ .option("--yes", "apply all high-confidence fixes without prompting");
52
+ }
53
+ const program = new commander_1.Command();
54
+ program
55
+ .name("ctxdiet")
56
+ .description("Detect, fix, and measure AI agent context-token waste.")
57
+ .version("0.1.0");
58
+ addCommonOptions(program);
59
+ program.action(() => {
60
+ const fromCli = program.getOptionValueSource("model") === "cli";
61
+ const o = resolveOptions(program.opts(), fromCli);
62
+ (0, report_1.printScanResult)((0, scan_1.scan)(o), o);
63
+ });
64
+ const fix = program
65
+ .command("fix")
66
+ .description("Generate fixes, show diffs, apply on confirmation, then show before/after.");
67
+ addCommonOptions(fix);
68
+ fix.action(async () => {
69
+ const fromCli = fix.getOptionValueSource("model") === "cli" ||
70
+ program.getOptionValueSource("model") === "cli";
71
+ await (0, fix_1.runFix)(resolveOptions(fix.optsWithGlobals(), fromCli));
72
+ });
73
+ program.parseAsync(process.argv).catch((err) => {
74
+ console.error(err instanceof Error ? err.message : String(err));
75
+ process.exit(1);
76
+ });
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PRICE_PER_MTOK = void 0;
4
+ exports.monthlyCost = monthlyCost;
5
+ /** Input price in USD per million tokens. Estimates for cost projection only. */
6
+ exports.PRICE_PER_MTOK = {
7
+ opus: 15,
8
+ sonnet: 3,
9
+ haiku: 0.8,
10
+ };
11
+ function monthlyCost(tokensPerSession, sessionsPerMonth, model) {
12
+ return (tokensPerSession * sessionsPerMonth) / 1_000_000 * exports.PRICE_PER_MTOK[model];
13
+ }