claude-slim 2.2.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.
@@ -0,0 +1,125 @@
1
+ import { readFile, writeFile, mkdir, rename, access } from 'node:fs/promises';
2
+ import { getDisabledDir as getDir, getManifestPath, getLegacyManifestPath, } from './paths.js';
3
+ export function getDisabledDir() {
4
+ return getDir();
5
+ }
6
+ export async function ensureDisabledDir() {
7
+ await mkdir(getDisabledDir(), { recursive: true });
8
+ }
9
+ async function pathExists(p) {
10
+ try {
11
+ await access(p);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function parseJsonl(content) {
19
+ const entries = [];
20
+ for (const line of content.split('\n')) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed)
23
+ continue;
24
+ try {
25
+ entries.push(JSON.parse(trimmed));
26
+ }
27
+ catch {
28
+ // Skip corrupted lines
29
+ }
30
+ }
31
+ return entries;
32
+ }
33
+ function collapseLegacy(entries) {
34
+ // Group by name. If the latest entry for a name has action='restored',
35
+ // the item was restored and is excluded. Otherwise keep the first
36
+ // (earliest clean record) as the canonical entry.
37
+ const byName = new Map();
38
+ for (const e of entries) {
39
+ const list = byName.get(e.name) ?? [];
40
+ list.push(e);
41
+ byName.set(e.name, list);
42
+ }
43
+ const active = [];
44
+ for (const [, list] of byName) {
45
+ const latest = list[list.length - 1];
46
+ if (latest.action === 'restored')
47
+ continue;
48
+ const clean = list.find((e) => e.action !== 'restored');
49
+ if (clean) {
50
+ // Strip the legacy `action` field before persisting to v2
51
+ const { action: _discarded, ...v2Entry } = clean;
52
+ active.push(v2Entry);
53
+ }
54
+ }
55
+ return active;
56
+ }
57
+ export async function migrateLegacyIfNeeded() {
58
+ const legacyPath = getLegacyManifestPath();
59
+ const newPath = getManifestPath();
60
+ if (!(await pathExists(legacyPath)))
61
+ return;
62
+ if (await pathExists(newPath))
63
+ return; // already migrated
64
+ const content = await readFile(legacyPath, 'utf-8');
65
+ const legacyEntries = parseJsonl(content);
66
+ const activeEntries = collapseLegacy(legacyEntries);
67
+ await ensureDisabledDir();
68
+ const manifest = { version: 2, entries: activeEntries };
69
+ const tmpPath = newPath + '.tmp';
70
+ await writeFile(tmpPath, JSON.stringify(manifest, null, 2));
71
+ await rename(tmpPath, newPath);
72
+ try {
73
+ await rename(legacyPath, legacyPath + '.bak');
74
+ }
75
+ catch (err) {
76
+ const code = err?.code;
77
+ if (code !== 'ENOENT')
78
+ throw err;
79
+ // Already renamed by a concurrent migration — safe to ignore
80
+ }
81
+ }
82
+ export async function readManifestV2() {
83
+ await migrateLegacyIfNeeded();
84
+ const newPath = getManifestPath();
85
+ try {
86
+ const content = await readFile(newPath, 'utf-8');
87
+ const parsed = JSON.parse(content);
88
+ if (parsed && parsed.version === 2 && Array.isArray(parsed.entries)) {
89
+ return parsed;
90
+ }
91
+ }
92
+ catch {
93
+ // fall through to empty manifest
94
+ }
95
+ return { version: 2, entries: [] };
96
+ }
97
+ export async function writeManifestV2(manifest) {
98
+ await ensureDisabledDir();
99
+ const target = getManifestPath();
100
+ const tmp = target + '.tmp';
101
+ await writeFile(tmp, JSON.stringify(manifest, null, 2));
102
+ await rename(tmp, target);
103
+ }
104
+ export async function addEntry(entry) {
105
+ const m = await readManifestV2();
106
+ m.entries.push(entry);
107
+ await writeManifestV2(m);
108
+ }
109
+ export async function removeEntry(name) {
110
+ const m = await readManifestV2();
111
+ const idx = m.entries.findIndex((e) => e.name === name);
112
+ if (idx === -1)
113
+ return null;
114
+ const [removed] = m.entries.splice(idx, 1);
115
+ await writeManifestV2(m);
116
+ return removed;
117
+ }
118
+ // --- Legacy-compatible API (still used by cleaner/cli pending Task 8) ---
119
+ export async function readManifest() {
120
+ const m = await readManifestV2();
121
+ return m.entries;
122
+ }
123
+ export async function appendManifest(entry) {
124
+ await addEntry(entry);
125
+ }
@@ -0,0 +1,7 @@
1
+ export declare function getClaudeDir(): string;
2
+ export declare function getSkillsDir(): string;
3
+ export declare function getPluginsDir(): string;
4
+ export declare function getProjectsDir(): string;
5
+ export declare function getDisabledDir(): string;
6
+ export declare function getManifestPath(): string;
7
+ export declare function getLegacyManifestPath(): string;
package/dist/paths.js ADDED
@@ -0,0 +1,23 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export function getClaudeDir() {
4
+ return join(homedir(), '.claude');
5
+ }
6
+ export function getSkillsDir() {
7
+ return join(getClaudeDir(), 'skills');
8
+ }
9
+ export function getPluginsDir() {
10
+ return join(getClaudeDir(), 'plugins', 'cache');
11
+ }
12
+ export function getProjectsDir() {
13
+ return join(getClaudeDir(), 'projects');
14
+ }
15
+ export function getDisabledDir() {
16
+ return join(getClaudeDir(), 'skills.disabled');
17
+ }
18
+ export function getManifestPath() {
19
+ return join(getDisabledDir(), 'manifest.json');
20
+ }
21
+ export function getLegacyManifestPath() {
22
+ return join(getDisabledDir(), '.claude-slim-manifest.jsonl');
23
+ }
@@ -0,0 +1,23 @@
1
+ import type { ScanResult, ManifestEntry } from './types.js';
2
+ export interface BreakdownRow {
3
+ label: string;
4
+ before: string;
5
+ after: string;
6
+ saved: string;
7
+ }
8
+ export interface ReportData {
9
+ before: number;
10
+ after: number;
11
+ saved: number;
12
+ percent: number;
13
+ topOffenders: Array<{
14
+ name: string;
15
+ tokens: number;
16
+ }>;
17
+ monthlySavings: number;
18
+ sessionsPerDay: number;
19
+ breakdown: BreakdownRow[];
20
+ }
21
+ export declare function calculateReport(scanBefore: ScanResult, scanAfter: ScanResult | null, movedEntries: ManifestEntry[], sessionsPerDay?: number): ReportData;
22
+ export declare function formatReportBox(data: ReportData): string;
23
+ export declare function formatScanSummary(result: ScanResult): string;
package/dist/report.js ADDED
@@ -0,0 +1,222 @@
1
+ import { homedir } from 'node:os';
2
+ import { isUsingFallback } from './tokenizer.js';
3
+ // Claude Code encodes /Users/leo.new/foo as -Users-leo-new-foo
4
+ const HOME_PREFIX = homedir().replace(/\//g, '-').replace(/\./g, '-');
5
+ const SESSIONS_PER_DAY_DEFAULT = 2;
6
+ const PRICE_PER_1K_TOKENS = 0.003; // Claude Sonnet input price
7
+ export function calculateReport(scanBefore, scanAfter, movedEntries, sessionsPerDay = SESSIONS_PER_DAY_DEFAULT) {
8
+ const before = scanBefore.totalTokensBefore;
9
+ const after = scanAfter ? scanAfter.totalTokensBefore : before;
10
+ // Use actual scan difference for accurate savings, not SKILL.md file sizes
11
+ const saved = before - after;
12
+ const percent = before > 0 ? (saved / before) * 100 : 0;
13
+ const topOffenders = movedEntries
14
+ .filter((e) => (e.tokenCount || 0) > 0)
15
+ .sort((a, b) => (b.tokenCount || 0) - (a.tokenCount || 0))
16
+ .slice(0, 5)
17
+ .map((e) => ({ name: e.name, tokens: e.tokenCount || 0 }));
18
+ const monthlySavings = (saved / 1000) * PRICE_PER_1K_TOKENS * sessionsPerDay * 30;
19
+ // Breakdown rows
20
+ const localBefore = scanBefore.localSkills.length;
21
+ const localAfter = scanAfter ? scanAfter.localSkills.length : localBefore - movedEntries.filter((e) => e.type !== 'oversized_memory').length;
22
+ const promptBefore = scanBefore.localSkills.length + scanBefore.pluginSkills.length;
23
+ const promptAfter = scanAfter
24
+ ? scanAfter.localSkills.length + scanAfter.pluginSkills.length
25
+ : promptBefore - movedEntries.filter((e) => e.type !== 'oversized_memory').length;
26
+ const memBefore = scanBefore.memoryFiles.reduce((s, m) => s + m.sizeBytes, 0);
27
+ const memAfter = scanAfter
28
+ ? scanAfter.memoryFiles.reduce((s, m) => s + m.sizeBytes, 0)
29
+ : memBefore;
30
+ const breakdown = [
31
+ {
32
+ label: 'Local skills',
33
+ before: String(localBefore),
34
+ after: String(localAfter),
35
+ saved: `${localAfter - localBefore}`,
36
+ },
37
+ {
38
+ label: 'System prompt',
39
+ before: `~${promptBefore}`,
40
+ after: `~${promptAfter}`,
41
+ saved: `${promptAfter - promptBefore}`,
42
+ },
43
+ {
44
+ label: 'Memory files',
45
+ before: `${(memBefore / 1024).toFixed(1)}KB`,
46
+ after: `${(memAfter / 1024).toFixed(1)}KB`,
47
+ saved: `${((memAfter - memBefore) / 1024).toFixed(1)}KB`,
48
+ },
49
+ {
50
+ label: 'Est. tokens',
51
+ before: `~${before.toLocaleString()}`,
52
+ after: `~${after.toLocaleString()}`,
53
+ saved: `~${(after - before).toLocaleString()}`,
54
+ },
55
+ ];
56
+ return {
57
+ before,
58
+ after,
59
+ saved,
60
+ percent,
61
+ topOffenders,
62
+ monthlySavings,
63
+ sessionsPerDay,
64
+ breakdown,
65
+ };
66
+ }
67
+ export function formatReportBox(data) {
68
+ const lines = [];
69
+ const W = 42;
70
+ const pad = (s) => {
71
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
72
+ return s + ' '.repeat(Math.max(0, W - 2 - visible.length));
73
+ };
74
+ const top = '\u256d' + '\u2500'.repeat(W) + '\u256e';
75
+ const bot = '\u2570' + '\u2500'.repeat(W) + '\u256f';
76
+ const blank = '\u2502' + ' '.repeat(W) + '\u2502';
77
+ lines.push(top);
78
+ lines.push(`\u2502${pad(' claude-slim report')}\u2502`);
79
+ lines.push(blank);
80
+ lines.push(`\u2502${pad(` Before: ${data.before.toLocaleString()} tokens at startup`)}\u2502`);
81
+ lines.push(`\u2502${pad(` After: ${data.after.toLocaleString()} tokens at startup`)}\u2502`);
82
+ lines.push(`\u2502${pad(` Saved: ${data.saved.toLocaleString()} tokens (${data.percent.toFixed(1)}%)`)}\u2502`);
83
+ lines.push(blank);
84
+ if (data.topOffenders.length > 0) {
85
+ lines.push(`\u2502${pad(' Top offenders removed:')}\u2502`);
86
+ for (const item of data.topOffenders) {
87
+ const tokStr = item.tokens.toLocaleString() + ' tok';
88
+ const nameStr = ` \u2022 ${item.name}`;
89
+ const gap = Math.max(1, W - 2 - nameStr.length - tokStr.length);
90
+ lines.push(`\u2502${nameStr}${' '.repeat(gap)}${tokStr}\u2502`);
91
+ }
92
+ lines.push(blank);
93
+ }
94
+ const savingsStr = `$${data.monthlySavings.toFixed(2)}`;
95
+ lines.push(`\u2502${pad(` Est. monthly savings: ~${savingsStr}`)}\u2502`);
96
+ lines.push(`\u2502${pad(` (${data.sessionsPerDay} sessions/day \u00d7 $${PRICE_PER_1K_TOKENS}/1K tok)`)}\u2502`);
97
+ if (isUsingFallback()) {
98
+ lines.push(blank);
99
+ lines.push(`\u2502${pad(' \u26a0 Token counts are approximations (bytes/4)')}\u2502`);
100
+ }
101
+ lines.push(bot);
102
+ // Breakdown table
103
+ if (data.breakdown.length > 0) {
104
+ lines.push('');
105
+ lines.push(formatBreakdownTable(data.breakdown));
106
+ }
107
+ return lines.join('\n');
108
+ }
109
+ function formatBreakdownTable(rows) {
110
+ const cols = [18, 10, 10, 12];
111
+ const hr = '\u2500';
112
+ const lines = [];
113
+ const cell = (s, w, align = 'center') => {
114
+ const pad = Math.max(0, w - s.length);
115
+ if (align === 'center') {
116
+ const l = Math.floor(pad / 2);
117
+ return ' '.repeat(l) + s + ' '.repeat(pad - l);
118
+ }
119
+ return ' ' + s + ' '.repeat(pad - 1);
120
+ };
121
+ lines.push(` \u250c${hr.repeat(cols[0])}\u252c${hr.repeat(cols[1])}\u252c${hr.repeat(cols[2])}\u252c${hr.repeat(cols[3])}\u2510`);
122
+ lines.push(` \u2502${cell('', cols[0])}\u2502${cell('Before', cols[1])}\u2502${cell('After', cols[2])}\u2502${cell('Saved', cols[3])}\u2502`);
123
+ lines.push(` \u251c${hr.repeat(cols[0])}\u253c${hr.repeat(cols[1])}\u253c${hr.repeat(cols[2])}\u253c${hr.repeat(cols[3])}\u2524`);
124
+ for (const row of rows) {
125
+ lines.push(` \u2502${cell(row.label, cols[0], 'left')}\u2502${cell(row.before, cols[1])}\u2502${cell(row.after, cols[2])}\u2502${cell(row.saved, cols[3])}\u2502`);
126
+ }
127
+ lines.push(` \u2514${hr.repeat(cols[0])}\u2534${hr.repeat(cols[1])}\u2534${hr.repeat(cols[2])}\u2534${hr.repeat(cols[3])}\u2518`);
128
+ return lines.join('\n');
129
+ }
130
+ export function formatScanSummary(result) {
131
+ const lines = [];
132
+ lines.push('');
133
+ lines.push('\x1b[1m=== claude-slim scan ===\x1b[0m');
134
+ // --- LOCAL SKILLS ---
135
+ lines.push('');
136
+ lines.push(`\x1b[1m LOCAL SKILLS\x1b[0m (${result.localSkills.length})`);
137
+ const sortedLocal = [...result.localSkills].sort((a, b) => b.sizeBytes - a.sizeBytes);
138
+ for (const skill of sortedLocal) {
139
+ const kb = (skill.sizeBytes / 1024).toFixed(1);
140
+ const tok = skill.tokens.toLocaleString();
141
+ lines.push(` ${skill.name.padEnd(28)} ${kb.padStart(6)}KB ${tok.padStart(7)} tok`);
142
+ }
143
+ const localTotal = result.localSkills.reduce((s, sk) => s + sk.sizeBytes, 0);
144
+ const localTokTotal = result.localSkills.reduce((s, sk) => s + sk.tokens, 0);
145
+ lines.push(` ${'─'.repeat(50)}`);
146
+ lines.push(` ${'Total'.padEnd(28)} ${(localTotal / 1024).toFixed(1).padStart(6)}KB ${localTokTotal.toLocaleString().padStart(7)} tok`);
147
+ // --- PLUGINS ---
148
+ lines.push('');
149
+ lines.push(`\x1b[1m PLUGINS\x1b[0m (${result.plugins.length} plugins, ${result.pluginSkills.length} skills)`);
150
+ const sortedPlugins = [...result.plugins].sort((a, b) => b.skillCount - a.skillCount);
151
+ for (const plugin of sortedPlugins) {
152
+ const status = plugin.status === 'disabled' ? ' \x1b[33m(disabled)\x1b[0m' : '';
153
+ lines.push(` ${plugin.name.padEnd(28)} ${String(plugin.skillCount).padStart(3)} skills${status}`);
154
+ }
155
+ // --- CLAUDE.MD ---
156
+ lines.push('');
157
+ lines.push(`\x1b[1m CLAUDE.MD\x1b[0m (${(result.claudeMdBytes / 1024).toFixed(1)}KB, ${result.claudeMdTokens.toLocaleString()} tok)`);
158
+ if (result.claudeMdSections && result.claudeMdSections.length > 0) {
159
+ for (const section of result.claudeMdSections) {
160
+ const kb = (section.sizeBytes / 1024).toFixed(1);
161
+ const tok = section.tokens.toLocaleString();
162
+ lines.push(` ${section.name.padEnd(44)} ${kb.padStart(6)}KB ${tok.padStart(7)} tok`);
163
+ }
164
+ }
165
+ // --- MEMORY FILES ---
166
+ lines.push('');
167
+ const memTotal = result.memoryFiles.reduce((s, m) => s + m.sizeBytes, 0);
168
+ const memTokTotal = result.memoryFiles.reduce((s, m) => s + m.tokens, 0);
169
+ lines.push(`\x1b[1m MEMORY FILES\x1b[0m (${result.memoryFiles.length} files, ${(memTotal / 1024).toFixed(1)}KB)`);
170
+ const sortedMem = [...result.memoryFiles].sort((a, b) => b.sizeBytes - a.sizeBytes);
171
+ for (const mem of sortedMem) {
172
+ const kb = (mem.sizeBytes / 1024).toFixed(1);
173
+ const tok = mem.tokens.toLocaleString();
174
+ // Strip home directory prefix, keep rest as project identifier
175
+ let project = mem.project;
176
+ if (project.startsWith(HOME_PREFIX)) {
177
+ const rest = project.slice(HOME_PREFIX.length).replace(/^-/, '/');
178
+ project = rest ? '~' + rest : '~';
179
+ }
180
+ const label = `${project}/${mem.name}`;
181
+ lines.push(` ${label.padEnd(52)} ${kb.padStart(6)}KB ${tok.padStart(7)} tok`);
182
+ }
183
+ // --- MCP SERVERS ---
184
+ lines.push('');
185
+ if (result.mcpServerNames && result.mcpServerNames.length > 0) {
186
+ lines.push(`\x1b[1m MCP SERVERS\x1b[0m (${result.mcpServers})`);
187
+ for (const name of result.mcpServerNames) {
188
+ lines.push(` ${name}`);
189
+ }
190
+ }
191
+ else {
192
+ lines.push(`\x1b[1m MCP SERVERS\x1b[0m: ${result.mcpServers}`);
193
+ }
194
+ // --- SUMMARY ---
195
+ lines.push('');
196
+ lines.push(`\x1b[1m ESTIMATED OVERHEAD\x1b[0m: ~${result.totalTokensBefore.toLocaleString()} tokens at session start`);
197
+ if (isUsingFallback()) {
198
+ lines.push(` \x1b[33m\u26a0 Using bytes/4 approximation (js-tiktoken unavailable)\x1b[0m`);
199
+ }
200
+ // --- ISSUES ---
201
+ lines.push('');
202
+ if (result.issues.length === 0) {
203
+ lines.push(' \x1b[32mAlready slim!\x1b[0m No issues found.');
204
+ }
205
+ else {
206
+ lines.push(`\x1b[1m ISSUES\x1b[0m (${result.issues.length} found)`);
207
+ lines.push('');
208
+ const tierLabels = { 1: 'Auto', 2: 'Recommended', 3: 'Optional' };
209
+ const tierColors = { 1: '31', 2: '33', 3: '37' };
210
+ for (let i = 0; i < result.issues.length; i++) {
211
+ const issue = result.issues[i];
212
+ const selected = issue.tier === 1 ? '\u2713' : '\u25cb';
213
+ const tierLabel = tierLabels[issue.tier] || 'Unknown';
214
+ const color = tierColors[issue.tier] || '37';
215
+ const detail = issue.detail ? ` (${issue.detail})` : '';
216
+ const tokStr = issue.tokens > 0 ? ` ~${issue.tokens.toLocaleString()} tok` : '';
217
+ lines.push(` ${selected} ${i + 1}. \x1b[${color}m[${tierLabel}]\x1b[0m ${issue.type}: ${issue.name}${detail}${tokStr}`);
218
+ }
219
+ }
220
+ lines.push('');
221
+ return lines.join('\n');
222
+ }
@@ -0,0 +1,13 @@
1
+ import type { ScanResult, SkillInfo } from './types.js';
2
+ interface SkillCandidate {
3
+ skill: SkillInfo;
4
+ realMdPath: string;
5
+ }
6
+ export declare function dedupeBySymlink(candidates: SkillCandidate[]): SkillInfo[];
7
+ export declare function parseClaudeMdSections(content: string): Array<{
8
+ name: string;
9
+ sizeBytes: number;
10
+ tokens: number;
11
+ }>;
12
+ export declare function scan(): Promise<ScanResult>;
13
+ export {};