claude-context-lint 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 +135 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -0
- package/dist/recommendations.d.ts +2 -0
- package/dist/recommendations.js +82 -0
- package/dist/reporter.d.ts +2 -0
- package/dist/reporter.js +107 -0
- package/dist/scanners/claude-md.d.ts +6 -0
- package/dist/scanners/claude-md.js +53 -0
- package/dist/scanners/mcp.d.ts +6 -0
- package/dist/scanners/mcp.js +125 -0
- package/dist/scanners/skills.d.ts +11 -0
- package/dist/scanners/skills.js +83 -0
- package/dist/tokenizer.d.ts +10 -0
- package/dist/tokenizer.js +42 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 claude-context-lint contributors
|
|
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,135 @@
|
|
|
1
|
+
# claude-context-lint
|
|
2
|
+
|
|
3
|
+
> Audit your Claude Code setup and find exactly where tokens are being wasted.
|
|
4
|
+
|
|
5
|
+
[](https://claude.ai/code)
|
|
6
|
+
[](https://www.npmjs.com/package/claude-context-lint)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
Every Claude Code conversation starts with overhead: your CLAUDE.md files, skill descriptions, MCP tool schemas, and the base system prompt all consume context tokens **before you type a single word**. This tool makes that invisible cost visible.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx claude-context-lint
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install globally:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g claude-context-lint
|
|
21
|
+
claude-context-lint
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Example Output
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Claude Code Context Audit
|
|
28
|
+
────────────────────────────────────────────────────
|
|
29
|
+
CLAUDE.md 1,240 tokens █░░░░░░░░░░░░░░░░░░░
|
|
30
|
+
CLAUDE.md 1,240 tokens
|
|
31
|
+
|
|
32
|
+
Skills (32 loaded) 4,800 tokens ████░░░░░░░░░░░░░░░░ MEDIUM
|
|
33
|
+
Listing overhead (per-turn): 4,800 tokens
|
|
34
|
+
Full content (on invocation): 89,200 tokens
|
|
35
|
+
⚠ 3 near-duplicate skills detected (−420 tokens)
|
|
36
|
+
api-helper 120 listing 3,200 full
|
|
37
|
+
db-migrate 105 listing 2,800 full
|
|
38
|
+
test-runner 98 listing 1,950 full
|
|
39
|
+
... and 29 more
|
|
40
|
+
|
|
41
|
+
MCP Servers (3) 14,100 tokens █████████░░░░░░░░░░░ CRITICAL
|
|
42
|
+
postgres 6,600 tokens (22 tools) [always loaded]
|
|
43
|
+
filesystem 1,800 tokens (6 tools) [always loaded]
|
|
44
|
+
memory 1,200 tokens (4 tools) [always loaded]
|
|
45
|
+
|
|
46
|
+
System Prompt 8,500 tokens █████░░░░░░░░░░░░░░░ (base overhead)
|
|
47
|
+
────────────────────────────────────────────────────
|
|
48
|
+
TOTAL OVERHEAD: 28,640 tokens
|
|
49
|
+
Context Limit: 200,000 tokens
|
|
50
|
+
Used Before Input: 14.3% ████░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
51
|
+
|
|
52
|
+
TOP RECOMMENDATIONS
|
|
53
|
+
────────────────────────────────────────────────────
|
|
54
|
+
1. Enable ToolSearch for "postgres" MCP (22 tools)
|
|
55
|
+
−6,270 tokens
|
|
56
|
+
2. Shorten 12 verbose skill descriptions
|
|
57
|
+
−1,840 tokens
|
|
58
|
+
3. Consolidate 3 near-duplicate skills
|
|
59
|
+
−420 tokens
|
|
60
|
+
|
|
61
|
+
Potential savings: 8,530 tokens (29.8% reduction)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## What It Scans
|
|
65
|
+
|
|
66
|
+
| Category | What it finds | How it counts |
|
|
67
|
+
|----------|---------------|---------------|
|
|
68
|
+
| **CLAUDE.md** | Project + parent dirs + `~/.claude/CLAUDE.md` | Exact token estimate of each file |
|
|
69
|
+
| **Skills** | All `.claude/skills/**/SKILL.md` files | **Listing tokens** (per-turn cost of name+description) and **full tokens** (on-invocation cost) |
|
|
70
|
+
| **MCP Servers** | `.mcp.json` + `settings.json` configs | Tool count × estimated tokens per schema |
|
|
71
|
+
| **System Prompt** | Claude Code's base instructions | Fixed estimate (~8,500 tokens) |
|
|
72
|
+
|
|
73
|
+
### Skill Token Accounting
|
|
74
|
+
|
|
75
|
+
Claude Code doesn't inject full skill content every turn. It injects a **listing** (skill name + description one-liner) into the system prompt, and only loads the **full SKILL.md** when invoked. This tool reports both:
|
|
76
|
+
|
|
77
|
+
- **Listing tokens**: Your per-turn cost (what matters for context efficiency)
|
|
78
|
+
- **Full tokens**: What loads when a skill is triggered (matters for complex conversations)
|
|
79
|
+
|
|
80
|
+
### Duplicate Detection
|
|
81
|
+
|
|
82
|
+
Skills with >75% word overlap in their descriptions are flagged as near-duplicates. Uses Jaccard similarity on word sets, filtering stop words under 3 characters.
|
|
83
|
+
|
|
84
|
+
### MCP Tool Estimation
|
|
85
|
+
|
|
86
|
+
MCP tool schemas average ~300 tokens each. Tools deferred via ToolSearch cost only ~15 tokens (just the name). The tool checks your `settings.json` permissions to detect which tools are deferred vs always-loaded, and estimates overhead accordingly.
|
|
87
|
+
|
|
88
|
+
## Options
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Usage: claude-context-lint [options]
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
-p, --path <path> Project path to audit (default: current directory)
|
|
95
|
+
-c, --context <size> Context window size: opus, sonnet, haiku, opus-1m,
|
|
96
|
+
or a number (default: "opus" = 200K)
|
|
97
|
+
--json Output as JSON (structured data on stdout)
|
|
98
|
+
-V, --version Output version number
|
|
99
|
+
-h, --help Display help
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Examples
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Audit current directory
|
|
106
|
+
claude-context-lint
|
|
107
|
+
|
|
108
|
+
# Audit a specific project
|
|
109
|
+
claude-context-lint --path ~/my-project
|
|
110
|
+
|
|
111
|
+
# Check overhead against 1M context window
|
|
112
|
+
claude-context-lint --context opus-1m
|
|
113
|
+
|
|
114
|
+
# Get machine-readable output for scripting
|
|
115
|
+
claude-context-lint --json | jq '.percentUsed'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Why This Matters
|
|
119
|
+
|
|
120
|
+
Research shows that heavy Claude Code users can burn **15-30% of their context window** on setup overhead before any conversation begins. Common culprits:
|
|
121
|
+
|
|
122
|
+
- **MCP servers with all tools always-loaded** instead of deferred via ToolSearch
|
|
123
|
+
- **Verbose skill descriptions** that repeat information
|
|
124
|
+
- **Near-duplicate skills** with overlapping trigger patterns
|
|
125
|
+
- **Large CLAUDE.md files** that could be compressed
|
|
126
|
+
|
|
127
|
+
This overhead is invisible — Claude Code's `/context` command doesn't break it down. This tool does.
|
|
128
|
+
|
|
129
|
+
## Attribution
|
|
130
|
+
|
|
131
|
+
This project was entirely designed, written, and published by [Claude Code](https://claude.ai/code).
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { scanClaudeMd } from './scanners/claude-md.js';
|
|
4
|
+
import { scanSkills } from './scanners/skills.js';
|
|
5
|
+
import { scanMcp } from './scanners/mcp.js';
|
|
6
|
+
import { generateRecommendations } from './recommendations.js';
|
|
7
|
+
import { printReport } from './reporter.js';
|
|
8
|
+
const CONTEXT_LIMITS = {
|
|
9
|
+
'opus': 200_000,
|
|
10
|
+
'sonnet': 200_000,
|
|
11
|
+
'haiku': 200_000,
|
|
12
|
+
'opus-1m': 1_000_000,
|
|
13
|
+
};
|
|
14
|
+
function estimateSystemPrompt() {
|
|
15
|
+
// Claude Code injects: system prompt + built-in tool schemas + env/git info
|
|
16
|
+
// Built-in tools: Bash, Read, Write, Edit, Glob, Grep, Agent, Skill, ToolSearch,
|
|
17
|
+
// TaskCreate, TaskUpdate, WebFetch, WebSearch, NotebookEdit, etc. (~15 tools)
|
|
18
|
+
return {
|
|
19
|
+
estimatedTokens: 8500,
|
|
20
|
+
breakdown: [
|
|
21
|
+
{ label: 'Core instructions', tokens: 2800 },
|
|
22
|
+
{ label: 'Built-in tool schemas (~15)', tokens: 3200 },
|
|
23
|
+
{ label: 'Git/commit/PR rules', tokens: 1200 },
|
|
24
|
+
{ label: 'Tone, style, env info', tokens: 800 },
|
|
25
|
+
{ label: 'Memory system instructions', tokens: 500 },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function runAudit(projectPath, contextLimit) {
|
|
30
|
+
const claudeMd = scanClaudeMd(projectPath);
|
|
31
|
+
const skills = scanSkills(projectPath);
|
|
32
|
+
const mcp = scanMcp(projectPath);
|
|
33
|
+
const systemPrompt = estimateSystemPrompt();
|
|
34
|
+
const totalTokens = claudeMd.totalTokens + skills.totalTokens + mcp.totalTokens + systemPrompt.estimatedTokens;
|
|
35
|
+
return {
|
|
36
|
+
claudeMd,
|
|
37
|
+
skills,
|
|
38
|
+
mcp,
|
|
39
|
+
systemPrompt,
|
|
40
|
+
totalTokens,
|
|
41
|
+
contextLimit,
|
|
42
|
+
percentUsed: (totalTokens / contextLimit) * 100,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const program = new Command();
|
|
46
|
+
program
|
|
47
|
+
.name('claude-context-lint')
|
|
48
|
+
.description('Audit your Claude Code setup and find where tokens are being wasted')
|
|
49
|
+
.version('0.1.0')
|
|
50
|
+
.option('-p, --path <path>', 'Project path to audit', process.cwd())
|
|
51
|
+
.option('-c, --context <size>', 'Context window size (opus, sonnet, haiku, opus-1m, or number)', 'opus')
|
|
52
|
+
.option('--json', 'Output as JSON')
|
|
53
|
+
.action((opts) => {
|
|
54
|
+
try {
|
|
55
|
+
const projectPath = opts.path;
|
|
56
|
+
let contextLimit;
|
|
57
|
+
if (opts.context in CONTEXT_LIMITS) {
|
|
58
|
+
contextLimit = CONTEXT_LIMITS[opts.context];
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const parsed = parseInt(opts.context, 10);
|
|
62
|
+
contextLimit = isNaN(parsed) ? 200_000 : parsed;
|
|
63
|
+
}
|
|
64
|
+
const result = runAudit(projectPath, contextLimit);
|
|
65
|
+
const recommendations = generateRecommendations(result);
|
|
66
|
+
if (opts.json) {
|
|
67
|
+
// JSON mode: only structured data on stdout
|
|
68
|
+
console.log(JSON.stringify({ ...result, recommendations }, null, 2));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
printReport(result, recommendations);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
program.parse();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function generateRecommendations(result) {
|
|
2
|
+
const recs = [];
|
|
3
|
+
let priority = 0;
|
|
4
|
+
// MCP: suggest enabling ToolSearch/deferred loading for large servers
|
|
5
|
+
for (const server of result.mcp.servers) {
|
|
6
|
+
if (!server.isDeferred && server.toolCount > 5) {
|
|
7
|
+
const savings = server.toolCount * (300 - 15);
|
|
8
|
+
recs.push({
|
|
9
|
+
priority: ++priority,
|
|
10
|
+
action: `Enable ToolSearch for "${server.name}" MCP (${server.toolCount} tools)`,
|
|
11
|
+
savings,
|
|
12
|
+
detail: `Add tool permissions to defer loading. Each tool schema is ~300 tokens; deferred tools cost ~15 tokens each.`,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Skills: flag duplicates
|
|
17
|
+
if (result.skills.duplicates.length > 0) {
|
|
18
|
+
recs.push({
|
|
19
|
+
priority: ++priority,
|
|
20
|
+
action: `Consolidate ${result.skills.duplicates.length} near-duplicate skill${result.skills.duplicates.length > 1 ? 's' : ''}`,
|
|
21
|
+
savings: result.skills.duplicateTokenSavings,
|
|
22
|
+
detail: result.skills.duplicates
|
|
23
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
24
|
+
.slice(0, 5)
|
|
25
|
+
.map(d => ` "${d.skill1}" ↔ "${d.skill2}" (${(d.similarity * 100).toFixed(0)}% similar)`)
|
|
26
|
+
.join('\n'),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Skills: flag large listing descriptions (>50 listing tokens = very long description)
|
|
30
|
+
const largeListings = result.skills.skills.filter(s => s.tokens > 50).sort((a, b) => b.tokens - a.tokens);
|
|
31
|
+
if (largeListings.length > 0) {
|
|
32
|
+
const top = largeListings.slice(0, 5);
|
|
33
|
+
const savings = largeListings.reduce((sum, s) => sum + Math.floor(s.tokens * 0.4), 0);
|
|
34
|
+
recs.push({
|
|
35
|
+
priority: ++priority,
|
|
36
|
+
action: `Shorten ${largeListings.length} verbose skill descriptions`,
|
|
37
|
+
savings,
|
|
38
|
+
detail: top
|
|
39
|
+
.map(s => ` ${s.name}: ${s.tokens} listing tokens (${s.fullTokens.toLocaleString()} full)`)
|
|
40
|
+
.join('\n'),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Skills: flag very large full content (>3000 tokens on invocation)
|
|
44
|
+
const heavySkills = result.skills.skills.filter(s => s.fullTokens > 3000).sort((a, b) => b.fullTokens - a.fullTokens);
|
|
45
|
+
if (heavySkills.length > 0) {
|
|
46
|
+
recs.push({
|
|
47
|
+
priority: ++priority,
|
|
48
|
+
action: `${heavySkills.length} skills load >3,000 tokens when invoked`,
|
|
49
|
+
savings: 0,
|
|
50
|
+
detail: heavySkills
|
|
51
|
+
.slice(0, 5)
|
|
52
|
+
.map(s => ` ${s.name}: ${s.fullTokens.toLocaleString()} tokens on invocation`)
|
|
53
|
+
.join('\n') + '\n Consider splitting or compressing these skills.',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// CLAUDE.md: flag large files
|
|
57
|
+
const largeMd = result.claudeMd.files.filter(f => f.tokens > 1000).sort((a, b) => b.tokens - a.tokens);
|
|
58
|
+
if (largeMd.length > 0) {
|
|
59
|
+
const savings = largeMd.reduce((sum, f) => sum + Math.floor(f.tokens * 0.2), 0);
|
|
60
|
+
recs.push({
|
|
61
|
+
priority: ++priority,
|
|
62
|
+
action: `Compress CLAUDE.md (${result.claudeMd.totalTokens.toLocaleString()} tokens)`,
|
|
63
|
+
savings,
|
|
64
|
+
detail: largeMd
|
|
65
|
+
.map(f => ` ${f.filePath}: ${f.tokens.toLocaleString()} tokens`)
|
|
66
|
+
.join('\n'),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Overall: flag if total overhead is >20% of context
|
|
70
|
+
if (result.percentUsed > 20) {
|
|
71
|
+
recs.push({
|
|
72
|
+
priority: ++priority,
|
|
73
|
+
action: `Total overhead is ${result.percentUsed.toFixed(1)}% — target <15%`,
|
|
74
|
+
savings: 0,
|
|
75
|
+
detail: `${result.totalTokens.toLocaleString()} tokens consumed before any conversation begins.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Sort by savings descending
|
|
79
|
+
recs.sort((a, b) => b.savings - a.savings);
|
|
80
|
+
recs.forEach((r, i) => (r.priority = i + 1));
|
|
81
|
+
return recs;
|
|
82
|
+
}
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
const BAR_WIDTH = 20;
|
|
3
|
+
function bar(ratio, width = BAR_WIDTH) {
|
|
4
|
+
const clamped = Math.min(Math.max(ratio, 0), 1);
|
|
5
|
+
const filled = Math.round(clamped * width);
|
|
6
|
+
const empty = width - filled;
|
|
7
|
+
return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
8
|
+
}
|
|
9
|
+
function severityBadge(tokens, total) {
|
|
10
|
+
const pct = total > 0 ? (tokens / total) * 100 : 0;
|
|
11
|
+
let severity;
|
|
12
|
+
if (pct > 40)
|
|
13
|
+
severity = 'CRITICAL';
|
|
14
|
+
else if (pct > 25)
|
|
15
|
+
severity = 'HIGH';
|
|
16
|
+
else if (pct > 10)
|
|
17
|
+
severity = 'MEDIUM';
|
|
18
|
+
else
|
|
19
|
+
severity = 'LOW';
|
|
20
|
+
const colors = {
|
|
21
|
+
CRITICAL: chalk.bgRed.white.bold,
|
|
22
|
+
HIGH: chalk.red.bold,
|
|
23
|
+
MEDIUM: chalk.yellow,
|
|
24
|
+
LOW: chalk.green,
|
|
25
|
+
};
|
|
26
|
+
return colors[severity](` ${severity} `);
|
|
27
|
+
}
|
|
28
|
+
function padRight(str, len) {
|
|
29
|
+
return str.length >= len ? str : str + ' '.repeat(len - str.length);
|
|
30
|
+
}
|
|
31
|
+
function fmt(n) {
|
|
32
|
+
return n.toLocaleString();
|
|
33
|
+
}
|
|
34
|
+
export function printReport(result, recommendations) {
|
|
35
|
+
const { claudeMd, skills, mcp, systemPrompt } = result;
|
|
36
|
+
const total = Math.max(result.totalTokens, 1);
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(chalk.bold.cyan(' Claude Code Context Audit'));
|
|
39
|
+
console.log(chalk.gray(' ' + '─'.repeat(52)));
|
|
40
|
+
// CLAUDE.md
|
|
41
|
+
console.log(` ${padRight('CLAUDE.md', 24)} ${padRight(fmt(claudeMd.totalTokens) + ' tokens', 16)} ${bar(claudeMd.totalTokens / total)}`);
|
|
42
|
+
for (const f of claudeMd.files) {
|
|
43
|
+
const label = f.label || f.filePath;
|
|
44
|
+
console.log(chalk.gray(` ${padRight(label, 34)} ${fmt(f.tokens)} tokens`));
|
|
45
|
+
}
|
|
46
|
+
// Skills — show listing overhead (per-turn cost)
|
|
47
|
+
const fullSkillTokens = skills.skills.reduce((s, sk) => s + sk.fullTokens, 0);
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(` ${padRight(`Skills (${skills.skills.length} loaded)`, 24)} ${padRight(fmt(skills.totalTokens) + ' tokens', 16)} ${bar(skills.totalTokens / total)} ${severityBadge(skills.totalTokens, total)}`);
|
|
50
|
+
console.log(chalk.gray(` Listing overhead (per-turn): ${fmt(skills.totalTokens)} tokens`));
|
|
51
|
+
console.log(chalk.gray(` Full content (on invocation): ${fmt(fullSkillTokens)} tokens`));
|
|
52
|
+
if (skills.duplicates.length > 0) {
|
|
53
|
+
console.log(chalk.yellow(` ⚠ ${skills.duplicates.length} near-duplicate skill${skills.duplicates.length > 1 ? 's' : ''} detected (−${fmt(skills.duplicateTokenSavings)} tokens)`));
|
|
54
|
+
}
|
|
55
|
+
// Top 5 largest by listing tokens
|
|
56
|
+
const topSkills = [...skills.skills].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
57
|
+
for (const s of topSkills) {
|
|
58
|
+
console.log(chalk.gray(` ${padRight(s.name, 28)} ${padRight(fmt(s.tokens) + ' listing', 16)} ${fmt(s.fullTokens)} full`));
|
|
59
|
+
}
|
|
60
|
+
if (skills.skills.length > 5) {
|
|
61
|
+
console.log(chalk.gray(` ... and ${skills.skills.length - 5} more`));
|
|
62
|
+
}
|
|
63
|
+
// MCP Servers
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(` ${padRight(`MCP Servers (${mcp.servers.length})`, 24)} ${padRight(fmt(mcp.totalTokens) + ' tokens', 16)} ${bar(mcp.totalTokens / total)} ${severityBadge(mcp.totalTokens, total)}`);
|
|
66
|
+
for (const s of mcp.servers) {
|
|
67
|
+
const tag = s.isDeferred ? chalk.green(' [deferred]') : chalk.yellow(' [always loaded]');
|
|
68
|
+
console.log(chalk.gray(` ${padRight(s.name, 22)} ${padRight(fmt(s.estimatedTokens) + ' tokens', 14)} (${s.toolCount} tools)${tag}`));
|
|
69
|
+
}
|
|
70
|
+
if (mcp.servers.length === 0) {
|
|
71
|
+
console.log(chalk.gray(' No MCP servers configured'));
|
|
72
|
+
}
|
|
73
|
+
// System prompt
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(` ${padRight('System Prompt', 24)} ${padRight(fmt(systemPrompt.estimatedTokens) + ' tokens', 16)} ${bar(systemPrompt.estimatedTokens / total)} ${chalk.gray('(base overhead)')}`);
|
|
76
|
+
// Total
|
|
77
|
+
console.log(chalk.gray(' ' + '─'.repeat(52)));
|
|
78
|
+
console.log(chalk.bold(` TOTAL OVERHEAD: ${fmt(result.totalTokens)} tokens`));
|
|
79
|
+
console.log(` Context Limit: ${fmt(result.contextLimit)} tokens`);
|
|
80
|
+
const pctBar = bar(result.percentUsed / 100, 30);
|
|
81
|
+
const pctColor = result.percentUsed > 25 ? chalk.red : result.percentUsed > 15 ? chalk.yellow : chalk.green;
|
|
82
|
+
console.log(` Used Before Input: ${pctColor(result.percentUsed.toFixed(1) + '%')} ${pctBar}`);
|
|
83
|
+
// Recommendations
|
|
84
|
+
if (recommendations.length > 0) {
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(chalk.bold.cyan(' TOP RECOMMENDATIONS'));
|
|
87
|
+
console.log(chalk.gray(' ' + '─'.repeat(52)));
|
|
88
|
+
for (const rec of recommendations.slice(0, 8)) {
|
|
89
|
+
const savingsStr = rec.savings > 0 ? chalk.green(`−${fmt(rec.savings)} tokens`) : '';
|
|
90
|
+
console.log(` ${chalk.bold.white(String(rec.priority) + '.')} ${rec.action}`);
|
|
91
|
+
if (savingsStr)
|
|
92
|
+
console.log(` ${savingsStr}`);
|
|
93
|
+
if (rec.detail) {
|
|
94
|
+
for (const line of rec.detail.split('\n')) {
|
|
95
|
+
console.log(chalk.gray(` ${line}`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const totalSavings = recommendations.reduce((sum, r) => sum + r.savings, 0);
|
|
100
|
+
if (totalSavings > 0) {
|
|
101
|
+
const reductionPct = ((totalSavings / result.totalTokens) * 100).toFixed(1);
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(chalk.bold.green(` Potential savings: ${fmt(totalSavings)} tokens (${reductionPct}% reduction)`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ClaudeMdResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Find all CLAUDE.md files in the project and user home directory.
|
|
4
|
+
* Claude Code loads: project CLAUDE.md, parent dirs' CLAUDE.md, ~/.claude/CLAUDE.md
|
|
5
|
+
*/
|
|
6
|
+
export declare function scanClaudeMd(projectPath: string): ClaudeMdResult;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { countTokens } from '../tokenizer.js';
|
|
4
|
+
const CLAUDE_MD_NAMES = ['CLAUDE.md', 'CLAUDE.local.md'];
|
|
5
|
+
/**
|
|
6
|
+
* Find all CLAUDE.md files in the project and user home directory.
|
|
7
|
+
* Claude Code loads: project CLAUDE.md, parent dirs' CLAUDE.md, ~/.claude/CLAUDE.md
|
|
8
|
+
*/
|
|
9
|
+
export function scanClaudeMd(projectPath) {
|
|
10
|
+
const files = [];
|
|
11
|
+
// Project-level CLAUDE.md files (walk up to 3 parent dirs)
|
|
12
|
+
let dir = path.resolve(projectPath);
|
|
13
|
+
const visited = new Set();
|
|
14
|
+
for (let i = 0; i < 4; i++) {
|
|
15
|
+
if (visited.has(dir))
|
|
16
|
+
break;
|
|
17
|
+
visited.add(dir);
|
|
18
|
+
for (const name of CLAUDE_MD_NAMES) {
|
|
19
|
+
const filePath = path.join(dir, name);
|
|
20
|
+
if (fs.existsSync(filePath)) {
|
|
21
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
22
|
+
files.push({
|
|
23
|
+
filePath: filePath,
|
|
24
|
+
tokens: countTokens(content),
|
|
25
|
+
label: name,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const parent = path.dirname(dir);
|
|
30
|
+
if (parent === dir)
|
|
31
|
+
break;
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
// Global ~/.claude/CLAUDE.md
|
|
35
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
36
|
+
if (home) {
|
|
37
|
+
for (const name of CLAUDE_MD_NAMES) {
|
|
38
|
+
const globalPath = path.join(home, '.claude', name);
|
|
39
|
+
if (fs.existsSync(globalPath) && !visited.has(path.dirname(globalPath))) {
|
|
40
|
+
const content = fs.readFileSync(globalPath, 'utf-8');
|
|
41
|
+
files.push({
|
|
42
|
+
filePath: globalPath,
|
|
43
|
+
tokens: countTokens(content),
|
|
44
|
+
label: `~/.claude/${name}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
files,
|
|
51
|
+
totalTokens: files.reduce((sum, f) => sum + f.tokens, 0),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
// Average tokens per MCP tool definition (name + description + parameter schema)
|
|
4
|
+
const TOKENS_PER_TOOL = 300;
|
|
5
|
+
// Tokens for a deferred/ToolSearch tool (just the name, no schema)
|
|
6
|
+
const TOKENS_PER_DEFERRED_TOOL = 15;
|
|
7
|
+
function readJsonSafe(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Known tool counts for common MCP servers
|
|
16
|
+
const KNOWN_SERVERS = {
|
|
17
|
+
'postgres': 22,
|
|
18
|
+
'postgresql': 22,
|
|
19
|
+
'@henkey/postgres': 22,
|
|
20
|
+
'@anthropic/mcp-postgres': 22,
|
|
21
|
+
'serena': 28,
|
|
22
|
+
'gmail': 2,
|
|
23
|
+
'claude_ai_gmail': 2,
|
|
24
|
+
'claude_ai_google_calendar': 3,
|
|
25
|
+
'google-calendar': 3,
|
|
26
|
+
'slack': 8,
|
|
27
|
+
'github': 15,
|
|
28
|
+
'filesystem': 6,
|
|
29
|
+
'brave-search': 2,
|
|
30
|
+
'puppeteer': 8,
|
|
31
|
+
'sqlite': 12,
|
|
32
|
+
'memory': 4,
|
|
33
|
+
'fetch': 2,
|
|
34
|
+
'sequential-thinking': 1,
|
|
35
|
+
'gemini-proxy': 5,
|
|
36
|
+
'gemini': 5,
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Scan MCP server configurations from .mcp.json and settings.json files.
|
|
40
|
+
* Estimates token overhead based on tool count heuristics.
|
|
41
|
+
*/
|
|
42
|
+
export function scanMcp(projectPath) {
|
|
43
|
+
const allMcpServers = {};
|
|
44
|
+
const allowedTools = new Set();
|
|
45
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
46
|
+
// 1. Read .mcp.json files (primary MCP config location)
|
|
47
|
+
const mcpJsonPaths = [
|
|
48
|
+
path.join(projectPath, '.mcp.json'),
|
|
49
|
+
path.join(projectPath, '.mcp.local.json'),
|
|
50
|
+
];
|
|
51
|
+
if (home) {
|
|
52
|
+
mcpJsonPaths.push(path.join(home, '.claude', '.mcp.json'));
|
|
53
|
+
}
|
|
54
|
+
for (const mp of mcpJsonPaths) {
|
|
55
|
+
const config = readJsonSafe(mp);
|
|
56
|
+
if (config?.mcpServers) {
|
|
57
|
+
Object.assign(allMcpServers, config.mcpServers);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 2. Read settings.json for MCP servers defined there + allowed tools
|
|
61
|
+
const settingsPaths = [
|
|
62
|
+
path.join(projectPath, '.claude', 'settings.json'),
|
|
63
|
+
path.join(projectPath, '.claude', 'settings.local.json'),
|
|
64
|
+
];
|
|
65
|
+
if (home) {
|
|
66
|
+
settingsPaths.push(path.join(home, '.claude', 'settings.json'));
|
|
67
|
+
settingsPaths.push(path.join(home, '.claude', 'settings.local.json'));
|
|
68
|
+
}
|
|
69
|
+
for (const sp of settingsPaths) {
|
|
70
|
+
const settings = readJsonSafe(sp);
|
|
71
|
+
if (!settings)
|
|
72
|
+
continue;
|
|
73
|
+
if (settings.mcpServers) {
|
|
74
|
+
Object.assign(allMcpServers, settings.mcpServers);
|
|
75
|
+
}
|
|
76
|
+
if (settings.permissions?.allow) {
|
|
77
|
+
for (const t of settings.permissions.allow)
|
|
78
|
+
allowedTools.add(t);
|
|
79
|
+
}
|
|
80
|
+
if (settings.allowedTools) {
|
|
81
|
+
for (const t of settings.allowedTools)
|
|
82
|
+
allowedTools.add(t);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 3. Build server info
|
|
86
|
+
const servers = [];
|
|
87
|
+
for (const [name, config] of Object.entries(allMcpServers)) {
|
|
88
|
+
let toolCount = 10; // default estimate
|
|
89
|
+
const nameLower = name.toLowerCase();
|
|
90
|
+
// Match against config key name first (most reliable), then command string
|
|
91
|
+
if (nameLower in KNOWN_SERVERS) {
|
|
92
|
+
toolCount = KNOWN_SERVERS[nameLower];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const cmdLower = [config.command || '', ...(config.args || [])].join(' ').toLowerCase();
|
|
96
|
+
for (const [knownName, count] of Object.entries(KNOWN_SERVERS)) {
|
|
97
|
+
if (cmdLower.includes(knownName)) {
|
|
98
|
+
toolCount = count;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Check deferred status from allowed tools
|
|
104
|
+
const mcpPrefix = `mcp__${name}__`;
|
|
105
|
+
const hasWildcard = allowedTools.has(`${mcpPrefix}*`) || allowedTools.has('*:**');
|
|
106
|
+
const specificAllowed = [...allowedTools].filter(t => t.startsWith(mcpPrefix) && !t.includes('*'));
|
|
107
|
+
const alwaysLoaded = hasWildcard ? toolCount : specificAllowed.length;
|
|
108
|
+
const deferred = Math.max(0, toolCount - alwaysLoaded);
|
|
109
|
+
const estimatedTokens = (alwaysLoaded * TOKENS_PER_TOOL) + (deferred * TOKENS_PER_DEFERRED_TOOL);
|
|
110
|
+
servers.push({
|
|
111
|
+
name,
|
|
112
|
+
toolCount,
|
|
113
|
+
estimatedTokens,
|
|
114
|
+
isDeferred: deferred > 0,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const totalTokens = servers.reduce((sum, s) => sum + s.estimatedTokens, 0);
|
|
118
|
+
const potentialSavings = servers.reduce((sum, s) => {
|
|
119
|
+
if (!s.isDeferred) {
|
|
120
|
+
return sum + (s.toolCount * TOKENS_PER_TOOL) - (s.toolCount * TOKENS_PER_DEFERRED_TOOL);
|
|
121
|
+
}
|
|
122
|
+
return sum;
|
|
123
|
+
}, 0);
|
|
124
|
+
return { servers, totalTokens, potentialSavings };
|
|
125
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SkillsResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Scan all skill files in .claude/skills/ (project + global).
|
|
4
|
+
*
|
|
5
|
+
* Token accounting:
|
|
6
|
+
* - "listing tokens": The skill name + description one-liner injected into every system prompt
|
|
7
|
+
* - "full tokens": The complete SKILL.md content, only loaded when the skill is invoked
|
|
8
|
+
*
|
|
9
|
+
* The per-turn overhead is the LISTING tokens, not the full content.
|
|
10
|
+
*/
|
|
11
|
+
export declare function scanSkills(projectPath: string): SkillsResult;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { countTokens, wordSimilarity } from '../tokenizer.js';
|
|
5
|
+
const DUPLICATE_THRESHOLD = 0.75;
|
|
6
|
+
function parseSkillFrontmatter(content) {
|
|
7
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
8
|
+
if (!match)
|
|
9
|
+
return { name: '', description: '' };
|
|
10
|
+
const fm = match[1];
|
|
11
|
+
const name = fm.match(/name:\s*["']?(.+?)["']?\s*$/m)?.[1] || '';
|
|
12
|
+
const description = fm.match(/description:\s*["']?(.+?)["']?\s*$/m)?.[1] || '';
|
|
13
|
+
return { name, description };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Scan all skill files in .claude/skills/ (project + global).
|
|
17
|
+
*
|
|
18
|
+
* Token accounting:
|
|
19
|
+
* - "listing tokens": The skill name + description one-liner injected into every system prompt
|
|
20
|
+
* - "full tokens": The complete SKILL.md content, only loaded when the skill is invoked
|
|
21
|
+
*
|
|
22
|
+
* The per-turn overhead is the LISTING tokens, not the full content.
|
|
23
|
+
*/
|
|
24
|
+
export function scanSkills(projectPath) {
|
|
25
|
+
const skills = [];
|
|
26
|
+
const searchDirs = [
|
|
27
|
+
path.join(projectPath, '.claude', 'skills'),
|
|
28
|
+
];
|
|
29
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
30
|
+
if (home) {
|
|
31
|
+
searchDirs.push(path.join(home, '.claude', 'skills'));
|
|
32
|
+
}
|
|
33
|
+
for (const dir of searchDirs) {
|
|
34
|
+
if (!fs.existsSync(dir))
|
|
35
|
+
continue;
|
|
36
|
+
const files = fg.sync('**/SKILL.md', { cwd: dir, absolute: true, followSymbolicLinks: false });
|
|
37
|
+
for (const filePath of files) {
|
|
38
|
+
let content;
|
|
39
|
+
try {
|
|
40
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue; // skip unreadable files
|
|
44
|
+
}
|
|
45
|
+
const { name, description } = parseSkillFrontmatter(content);
|
|
46
|
+
const skillName = name || path.basename(path.dirname(filePath));
|
|
47
|
+
const desc = description || content.slice(0, 200);
|
|
48
|
+
// The listing line that appears in every system prompt:
|
|
49
|
+
// "- skillName: description text here..."
|
|
50
|
+
const listingLine = `- ${skillName}: ${desc}`;
|
|
51
|
+
const listingTokens = countTokens(listingLine);
|
|
52
|
+
skills.push({
|
|
53
|
+
name: skillName,
|
|
54
|
+
description: desc,
|
|
55
|
+
filePath,
|
|
56
|
+
tokens: listingTokens,
|
|
57
|
+
fullTokens: countTokens(content),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Detect duplicates via description similarity
|
|
62
|
+
const duplicates = [];
|
|
63
|
+
for (let i = 0; i < skills.length; i++) {
|
|
64
|
+
for (let j = i + 1; j < skills.length; j++) {
|
|
65
|
+
const sim = wordSimilarity(skills[i].description, skills[j].description);
|
|
66
|
+
if (sim >= DUPLICATE_THRESHOLD) {
|
|
67
|
+
duplicates.push({
|
|
68
|
+
skill1: skills[i].name,
|
|
69
|
+
skill2: skills[j].name,
|
|
70
|
+
similarity: sim,
|
|
71
|
+
potentialSavings: Math.min(skills[i].tokens, skills[j].tokens),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const duplicateTokenSavings = duplicates.reduce((sum, d) => sum + d.potentialSavings, 0);
|
|
77
|
+
return {
|
|
78
|
+
skills,
|
|
79
|
+
totalTokens: skills.reduce((sum, s) => sum + s.tokens, 0),
|
|
80
|
+
duplicates,
|
|
81
|
+
duplicateTokenSavings,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approximate token count for Claude's tokenizer.
|
|
3
|
+
* Uses word-boundary splitting with length-based sub-word estimation.
|
|
4
|
+
* Accuracy: within ~10% of actual BPE tokenization for English/code.
|
|
5
|
+
*/
|
|
6
|
+
export declare function countTokens(text: string): number;
|
|
7
|
+
/**
|
|
8
|
+
* Jaccard similarity on word sets — used for skill duplicate detection.
|
|
9
|
+
*/
|
|
10
|
+
export declare function wordSimilarity(a: string, b: string): number;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approximate token count for Claude's tokenizer.
|
|
3
|
+
* Uses word-boundary splitting with length-based sub-word estimation.
|
|
4
|
+
* Accuracy: within ~10% of actual BPE tokenization for English/code.
|
|
5
|
+
*/
|
|
6
|
+
export function countTokens(text) {
|
|
7
|
+
if (!text)
|
|
8
|
+
return 0;
|
|
9
|
+
// Split on whitespace first, then sub-split on code-significant characters
|
|
10
|
+
// (BPE tokenizers split on these boundaries too)
|
|
11
|
+
const words = text.split(/\s+/).flatMap(w => w.split(/[-_/@.]+/)).filter(w => w.length > 0);
|
|
12
|
+
let tokens = 0;
|
|
13
|
+
for (const word of words) {
|
|
14
|
+
if (word.length <= 4) {
|
|
15
|
+
tokens += 1;
|
|
16
|
+
}
|
|
17
|
+
else if (word.length <= 8) {
|
|
18
|
+
tokens += 2;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
tokens += Math.ceil(word.length / 4);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return tokens;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Jaccard similarity on word sets — used for skill duplicate detection.
|
|
28
|
+
*/
|
|
29
|
+
export function wordSimilarity(a, b) {
|
|
30
|
+
const setA = new Set(a.toLowerCase().split(/\W+/).filter(w => w.length > 2));
|
|
31
|
+
const setB = new Set(b.toLowerCase().split(/\W+/).filter(w => w.length > 2));
|
|
32
|
+
if (setA.size === 0 && setB.size === 0)
|
|
33
|
+
return 1;
|
|
34
|
+
if (setA.size === 0 || setB.size === 0)
|
|
35
|
+
return 0;
|
|
36
|
+
let intersection = 0;
|
|
37
|
+
for (const word of setA) {
|
|
38
|
+
if (setB.has(word))
|
|
39
|
+
intersection++;
|
|
40
|
+
}
|
|
41
|
+
return intersection / (setA.size + setB.size - intersection);
|
|
42
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface TokenCount {
|
|
2
|
+
tokens: number;
|
|
3
|
+
filePath: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ClaudeMdResult {
|
|
7
|
+
files: TokenCount[];
|
|
8
|
+
totalTokens: number;
|
|
9
|
+
}
|
|
10
|
+
export interface SkillInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
filePath: string;
|
|
14
|
+
tokens: number;
|
|
15
|
+
fullTokens: number;
|
|
16
|
+
}
|
|
17
|
+
export interface DuplicatePair {
|
|
18
|
+
skill1: string;
|
|
19
|
+
skill2: string;
|
|
20
|
+
similarity: number;
|
|
21
|
+
potentialSavings: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SkillsResult {
|
|
24
|
+
skills: SkillInfo[];
|
|
25
|
+
totalTokens: number;
|
|
26
|
+
duplicates: DuplicatePair[];
|
|
27
|
+
duplicateTokenSavings: number;
|
|
28
|
+
}
|
|
29
|
+
export interface McpServerInfo {
|
|
30
|
+
name: string;
|
|
31
|
+
toolCount: number;
|
|
32
|
+
estimatedTokens: number;
|
|
33
|
+
isDeferred: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface McpResult {
|
|
36
|
+
servers: McpServerInfo[];
|
|
37
|
+
totalTokens: number;
|
|
38
|
+
potentialSavings: number;
|
|
39
|
+
}
|
|
40
|
+
export interface SystemPromptResult {
|
|
41
|
+
estimatedTokens: number;
|
|
42
|
+
breakdown: {
|
|
43
|
+
label: string;
|
|
44
|
+
tokens: number;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
export interface AuditResult {
|
|
48
|
+
claudeMd: ClaudeMdResult;
|
|
49
|
+
skills: SkillsResult;
|
|
50
|
+
mcp: McpResult;
|
|
51
|
+
systemPrompt: SystemPromptResult;
|
|
52
|
+
totalTokens: number;
|
|
53
|
+
contextLimit: number;
|
|
54
|
+
percentUsed: number;
|
|
55
|
+
}
|
|
56
|
+
export interface Recommendation {
|
|
57
|
+
priority: number;
|
|
58
|
+
action: string;
|
|
59
|
+
savings: number;
|
|
60
|
+
detail: string;
|
|
61
|
+
}
|
|
62
|
+
export type Severity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-context-lint",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Audit your Claude Code setup and find where tokens are being wasted",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-context-lint": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"prepublishOnly": "tsc"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"context-window",
|
|
18
|
+
"token-optimization",
|
|
19
|
+
"linter",
|
|
20
|
+
"cli",
|
|
21
|
+
"mcp",
|
|
22
|
+
"skills",
|
|
23
|
+
"anthropic",
|
|
24
|
+
"ai-tools"
|
|
25
|
+
],
|
|
26
|
+
"author": "Claude Code <noreply@anthropic.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/skibidiskib/claude-context-lint.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/skibidiskib/claude-context-lint#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/skibidiskib/claude-context-lint/issues"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"commander": "^12.1.0",
|
|
39
|
+
"fast-glob": "^3.3.2"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.14.0",
|
|
43
|
+
"typescript": "^5.5.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist",
|
|
50
|
+
"LICENSE",
|
|
51
|
+
"README.md"
|
|
52
|
+
]
|
|
53
|
+
}
|