claude-slim 2.6.1 → 2.7.1

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.
@@ -79,6 +79,117 @@ export function extractSkillsFromTranscript(content) {
79
79
  }
80
80
  return skills;
81
81
  }
82
+ // Extract MCP server prefixes from tool_use events whose name matches
83
+ // `mcp__<prefix>__<tool>`. Returns the set of unique prefixes seen.
84
+ //
85
+ // Example: `mcp__plugin_oh-my-claudecode_t__lsp_diagnostics` → `plugin_oh-my-claudecode_t`
86
+ export function extractMcpPrefixesFromTranscript(content) {
87
+ const prefixes = new Set();
88
+ const lines = content.split('\n');
89
+ for (const line of lines) {
90
+ if (!line)
91
+ continue;
92
+ let obj;
93
+ try {
94
+ obj = JSON.parse(line);
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ if (typeof obj !== 'object' || obj === null)
100
+ continue;
101
+ const message = obj.message;
102
+ if (typeof message !== 'object' || message === null)
103
+ continue;
104
+ const msgContent = message.content;
105
+ if (!Array.isArray(msgContent))
106
+ continue;
107
+ for (const c of msgContent) {
108
+ if (typeof c !== 'object' || c === null)
109
+ continue;
110
+ const rec = c;
111
+ if (rec.type !== 'tool_use')
112
+ continue;
113
+ const name = rec.name;
114
+ if (typeof name !== 'string' || !name.startsWith('mcp__'))
115
+ continue;
116
+ // Must have at least two __ separators: mcp__<prefix>__<tool>
117
+ const after = name.slice('mcp__'.length);
118
+ const sep = after.indexOf('__');
119
+ if (sep === -1)
120
+ continue;
121
+ prefixes.add(after.slice(0, sep));
122
+ }
123
+ }
124
+ return prefixes;
125
+ }
126
+ // Extract slash command names from user messages containing the
127
+ // `<command-name>/foo</command-name>` tag that Claude Code injects when a
128
+ // user runs a slash command. The leading slash is stripped so callers receive
129
+ // plain names like "clear" or "grill-me".
130
+ //
131
+ // Only `type === "user"` / `role === "user"` messages are examined to avoid
132
+ // false positives from assistant text that may reference command names.
133
+ // Regex pattern held as a plain string — each call site constructs a fresh
134
+ // RegExp so there is no shared `lastIndex` state to reset. `String.matchAll`
135
+ // then wraps that RegExp in its own iterator, further insulating the loop.
136
+ const COMMAND_TAG_PATTERN = /<command-name>([^<]+)<\/command-name>/g;
137
+ export function extractCommandsFromTranscript(content) {
138
+ const commands = new Set();
139
+ const lines = content.split('\n');
140
+ for (const line of lines) {
141
+ if (!line)
142
+ continue;
143
+ let obj;
144
+ try {
145
+ obj = JSON.parse(line);
146
+ }
147
+ catch {
148
+ continue;
149
+ }
150
+ if (typeof obj !== 'object' || obj === null)
151
+ continue;
152
+ const rec = obj;
153
+ // Only inspect user-role messages
154
+ const message = rec.message;
155
+ if (typeof message !== 'object' || message === null)
156
+ continue;
157
+ const msgRec = message;
158
+ if (msgRec.role !== 'user')
159
+ continue;
160
+ const msgContent = msgRec.content;
161
+ // User content may be a plain string or an array of content blocks.
162
+ const texts = [];
163
+ if (typeof msgContent === 'string') {
164
+ texts.push(msgContent);
165
+ }
166
+ else if (Array.isArray(msgContent)) {
167
+ for (const c of msgContent) {
168
+ if (typeof c !== 'object' || c === null)
169
+ continue;
170
+ const text = c.text;
171
+ if (typeof text === 'string')
172
+ texts.push(text);
173
+ }
174
+ }
175
+ for (const text of texts) {
176
+ for (const match of text.matchAll(COMMAND_TAG_PATTERN)) {
177
+ // Strip leading slash from the command value (e.g. "/clear" → "clear")
178
+ const raw = match[1].trim();
179
+ commands.add(raw.startsWith('/') ? raw.slice(1) : raw);
180
+ }
181
+ }
182
+ }
183
+ return commands;
184
+ }
185
+ // Parse all signals from a single JSONL transcript.
186
+ function parseTranscript(content) {
187
+ const skills = extractSkillsFromTranscript(content);
188
+ const mcpPrefixes = Array.from(extractMcpPrefixesFromTranscript(content));
189
+ const commands = Array.from(extractCommandsFromTranscript(content));
190
+ const invocationCount = skills.length + mcpPrefixes.length + commands.length;
191
+ return { skills, mcpPrefixes, commands, invocationCount };
192
+ }
82
193
  // Walk every `~/.claude/projects/<slug>/*.jsonl` whose mtime falls inside the
83
194
  // lookback window. Per-file results are cached by mtime, so warm scans only
84
195
  // re-read files that have changed.
@@ -90,6 +201,9 @@ export async function scanSessionUsage(lookbackDays) {
90
201
  // across many scans even as old session logs get rotated/deleted.
91
202
  const newEntries = {};
92
203
  const invokedSkills = new Set();
204
+ const mcpPrefixesInvoked = new Set();
205
+ const commandsInvoked = new Set();
206
+ let totalUserCallableInvocations = 0;
93
207
  let sessionsScanned = 0;
94
208
  let sessionsInWindow = 0;
95
209
  let projectDirs = [];
@@ -97,7 +211,15 @@ export async function scanSessionUsage(lookbackDays) {
97
211
  projectDirs = await readdir(projectsDir);
98
212
  }
99
213
  catch {
100
- return { invokedSkills, dataAvailable: false, sessionsScanned: 0, sessionsInWindow: 0 };
214
+ return {
215
+ invokedSkills,
216
+ dataAvailable: false,
217
+ sessionsScanned: 0,
218
+ sessionsInWindow: 0,
219
+ mcpPrefixesInvoked,
220
+ commandsInvoked,
221
+ totalUserCallableInvocations: 0,
222
+ };
101
223
  }
102
224
  for (const projectName of projectDirs) {
103
225
  const projectPath = join(projectsDir, projectName);
@@ -123,11 +245,21 @@ export async function scanSessionUsage(lookbackDays) {
123
245
  if (mtimeMs < cutoffMs)
124
246
  continue;
125
247
  sessionsInWindow++;
126
- // Cache hit: reuse the parsed skill list, no I/O on the file body.
248
+ // Cache hit: reuse the parsed result only if mtime matches AND the entry
249
+ // has the v2.6 extension fields (mcpPrefixes / commands). Entries from
250
+ // older cache files lack these fields → force a re-parse.
127
251
  const cached = cache.entries[filePath];
128
- if (cached && cached.mtimeMs === mtimeMs) {
252
+ if (cached &&
253
+ cached.mtimeMs === mtimeMs &&
254
+ Array.isArray(cached.mcpPrefixes) &&
255
+ Array.isArray(cached.commands)) {
129
256
  for (const s of cached.skills)
130
257
  invokedSkills.add(s);
258
+ for (const p of cached.mcpPrefixes)
259
+ mcpPrefixesInvoked.add(p);
260
+ for (const cmd of cached.commands)
261
+ commandsInvoked.add(cmd);
262
+ totalUserCallableInvocations += cached.invocationCount ?? 0;
131
263
  newEntries[filePath] = cached;
132
264
  continue;
133
265
  }
@@ -138,13 +270,32 @@ export async function scanSessionUsage(lookbackDays) {
138
270
  catch {
139
271
  continue;
140
272
  }
141
- const skills = extractSkillsFromTranscript(content);
142
- for (const s of skills)
273
+ const parsed = parseTranscript(content);
274
+ for (const s of parsed.skills)
143
275
  invokedSkills.add(s);
144
- newEntries[filePath] = { mtimeMs, skills: Array.from(new Set(skills)) };
276
+ for (const p of parsed.mcpPrefixes)
277
+ mcpPrefixesInvoked.add(p);
278
+ for (const cmd of parsed.commands)
279
+ commandsInvoked.add(cmd);
280
+ totalUserCallableInvocations += parsed.invocationCount;
281
+ newEntries[filePath] = {
282
+ mtimeMs,
283
+ skills: Array.from(new Set(parsed.skills)),
284
+ mcpPrefixes: Array.from(new Set(parsed.mcpPrefixes)),
285
+ commands: Array.from(new Set(parsed.commands)),
286
+ invocationCount: parsed.invocationCount,
287
+ };
145
288
  }
146
289
  }
147
290
  const dataAvailable = sessionsInWindow >= MIN_SESSIONS_FOR_DATA_AVAILABLE && invokedSkills.size > 0;
148
291
  await saveCache({ version: CACHE_VERSION, entries: newEntries });
149
- return { invokedSkills, dataAvailable, sessionsScanned, sessionsInWindow };
292
+ return {
293
+ invokedSkills,
294
+ dataAvailable,
295
+ sessionsScanned,
296
+ sessionsInWindow,
297
+ mcpPrefixesInvoked,
298
+ commandsInvoked,
299
+ totalUserCallableInvocations,
300
+ };
150
301
  }
package/dist/types.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface PluginInfo {
25
25
  status?: 'enabled' | 'disabled';
26
26
  }
27
27
  export type IssueTier = 1 | 2 | 3;
28
- export type IssueType = 'broken_symlink' | 'template' | 'skill_dup' | 'duplicate' | 'oversized_memory' | 'oversized_skill' | 'unused_skill' | 'disabled_plugin' | 'stale_project' | 'temp_cache';
28
+ export type IssueType = 'broken_symlink' | 'template' | 'skill_dup' | 'duplicate' | 'oversized_memory' | 'oversized_skill' | 'unused_skill' | 'unused_plugin' | 'disabled_plugin' | 'stale_project' | 'temp_cache';
29
29
  export interface Issue {
30
30
  type: IssueType;
31
31
  tier: IssueTier;
@@ -33,6 +33,17 @@ export interface Issue {
33
33
  detail?: string;
34
34
  tokens: number;
35
35
  path: string;
36
+ marketplace?: string;
37
+ }
38
+ export interface PluginBreakdown {
39
+ name: string;
40
+ marketplace: string;
41
+ tokens: number;
42
+ skills: number;
43
+ mcp: number;
44
+ commands: number;
45
+ lastUsed: 'used' | 'never';
46
+ status: 'used' | 'unused' | 'agent-only' | 'insufficient data' | 'disabled';
36
47
  }
37
48
  export interface ScanResult {
38
49
  localSkills: SkillInfo[];
@@ -51,6 +62,7 @@ export interface ScanResult {
51
62
  mcpServerNames: string[];
52
63
  issues: Issue[];
53
64
  totalTokensBefore: number;
65
+ pluginBreakdown: PluginBreakdown[];
54
66
  }
55
67
  export interface ManifestEntry {
56
68
  date: string;
@@ -60,9 +72,17 @@ export interface ManifestEntry {
60
72
  tokenCount?: number;
61
73
  tier?: IssueTier;
62
74
  }
75
+ export type LegacyManifestEntry = ManifestEntry;
76
+ export interface DisabledPluginEntry {
77
+ type: 'disabled_plugin';
78
+ plugin: string;
79
+ marketplace: string;
80
+ disabledAt: string;
81
+ }
82
+ export type AnyManifestEntry = ManifestEntry | DisabledPluginEntry;
63
83
  export interface Manifest {
64
84
  version: 2;
65
- entries: ManifestEntry[];
85
+ entries: AnyManifestEntry[];
66
86
  }
67
87
  export interface TokenCache {
68
88
  version: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slim",
3
- "version": "2.6.1",
3
+ "version": "2.7.1",
4
4
  "description": "Analyze and reduce Claude Code token overhead",
5
5
  "type": "module",
6
6
  "bin": {