claude-slim 2.7.3 → 2.8.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.
- package/README.md +38 -19
- package/dist/cli.js +4 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +11 -0
- package/dist/report.js +30 -2
- package/dist/scanner/index.js +31 -7
- package/dist/scanner/local-skills.js +2 -0
- package/dist/scanner/plugin-cost.d.ts +1 -1
- package/dist/scanner/plugin-cost.js +2 -2
- package/dist/scanner/plugin-skills.js +2 -0
- package/dist/scanner/plugin-surfaces.d.ts +1 -0
- package/dist/scanner/plugin-surfaces.js +18 -5
- package/dist/scanner/skill-listing.d.ts +16 -0
- package/dist/scanner/skill-listing.js +66 -0
- package/dist/scanner/user-surfaces.d.ts +16 -0
- package/dist/scanner/user-surfaces.js +59 -0
- package/dist/tokenizer.js +39 -8
- package/dist/types.d.ts +19 -0
- package/package.json +15 -8
package/README.md
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
# claude-slim
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-slim)
|
|
6
|
+
[](https://www.npmjs.com/package/claude-slim)
|
|
6
7
|
[](https://github.com/iops-leo/claude-slim/actions/workflows/ci.yml)
|
|
8
|
+
[](https://nodejs.org)
|
|
7
9
|
[](./LICENSE)
|
|
8
10
|
|
|
9
|
-
**
|
|
11
|
+
**Your Claude Code session burns thousands of tokens before you even say "hello."**
|
|
10
12
|
|
|
11
|
-
Every
|
|
13
|
+
Every session auto-loads every skill, agent, slash command, memory file, and plugin instruction into the system prompt — even the ones you never use. If you run OMC, marketplace plugins, or a custom skill stack, you're paying for context you'll never touch on every single turn. claude-slim measures that startup overhead and removes what you don't need.
|
|
14
|
+
|
|
15
|
+
No proxy, no compression, no changes to how Claude Code talks to the API — it reads `~/.claude/`, tells you what each skill and plugin actually costs, and moves the dead weight aside reversibly.
|
|
12
16
|
|
|
13
17
|
```
|
|
14
18
|
/claude-slim
|
|
@@ -26,17 +30,20 @@ Every Claude Code session auto-loads every skill, memory file, and plugin instru
|
|
|
26
30
|
<img src="docs/demo.gif" alt="claude-slim cleanup: 11,442 tokens of overhead reduced to ~5,800 in 45 seconds" width="900" />
|
|
27
31
|
</p>
|
|
28
32
|
|
|
29
|
-
Where the bloat hides:
|
|
33
|
+
Where the bloat hides — measured on one real install:
|
|
34
|
+
|
|
35
|
+
| Source | What it costs | |
|
|
36
|
+
|--------|:---:|---|
|
|
37
|
+
| Skill listings | ~10,100 tokens | 256 skills × their `name: description` line |
|
|
38
|
+
| Agent catalog | ~2,250 tokens | `~/.claude/agents/`, 12 agents |
|
|
39
|
+
| CLAUDE.md | ~2,000 tokens | plugin instructions |
|
|
40
|
+
| Deferred tools list | ~1,500 tokens | MCP tool schemas |
|
|
41
|
+
| Slash commands | ~80 tokens | `~/.claude/commands/` |
|
|
42
|
+
| Memory files | **0 – 63,500 tokens** | current project only — varies wildly per project |
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|--------|:---:|
|
|
33
|
-
| 60+ registered skills | ~3,000 tokens |
|
|
34
|
-
| CLAUDE.md (plugin instructions) | ~5,000 tokens |
|
|
35
|
-
| Memory files | ~2,500 tokens |
|
|
36
|
-
| Deferred tools list | ~1,500 tokens |
|
|
37
|
-
| **Total** | **~12,000 tokens** |
|
|
44
|
+
Skill listings are the part people underestimate: each installed skill contributes one `- name: description` line to the system prompt, and those run anywhere from **30 to 509 tokens each**. Sixty terse skills and sixty verbose ones are not the same bill.
|
|
38
45
|
|
|
39
|
-
That's slower responses. Hitting your usage cap faster. Paying for context you're not using.
|
|
46
|
+
That's slower responses. Hitting your usage cap faster. Paying for context you're not using — on every turn, of every session.
|
|
40
47
|
|
|
41
48
|
---
|
|
42
49
|
|
|
@@ -67,6 +74,7 @@ That's slower responses. Hitting your usage cap faster. Paying for context you'r
|
|
|
67
74
|
| Empty templates | Placeholder skills with no content |
|
|
68
75
|
| Oversized files | SKILL.md over 10KB |
|
|
69
76
|
| **Unused skills** | **Local skills never invoked in your last N days of sessions (default 60d)** |
|
|
77
|
+
| Agents & commands | `~/.claude/agents/` and `~/.claude/commands/` — measured and reported, never modified |
|
|
70
78
|
| **Unused plugins** | **Plugins whose skill/mcp/cmd were never invoked in your last N days of sessions (default 60d). Tier 3, never auto-selected.** |
|
|
71
79
|
| Stale memory | Large memory files loaded every session |
|
|
72
80
|
| Disabled plugins | Installed but disabled plugins still in cache |
|
|
@@ -173,6 +181,7 @@ npx claude-slim report # Show savings from last clean
|
|
|
173
181
|
|
|
174
182
|
- **`~/.claude/CLAUDE.md`** — your system instructions, read-only.
|
|
175
183
|
- **`~/.claude/settings.json`** — MCP server config, hooks, and any other settings. Read-only.
|
|
184
|
+
- **`~/.claude/agents/` and `~/.claude/commands/`** — measured and reported since v2.8, never moved or deleted. There is no restore path for them yet, and a destructive action without its undo isn't worth shipping.
|
|
176
185
|
- **Plugin internals** (`~/.claude/plugins/config.json`, individual `plugin.json` files) — left alone; use `claude plugin` to manage plugins.
|
|
177
186
|
- **Git / project sources** — claude-slim only looks inside `~/.claude/`, never at your code.
|
|
178
187
|
- **Anything outside `~/.claude/`** — a path-containment guard refuses destructive ops anywhere else, even if a tampered manifest asked it to.
|
|
@@ -188,9 +197,11 @@ claude-slim scans these locations. No plugin-specific logic — pure filesystem
|
|
|
188
197
|
```
|
|
189
198
|
~/.claude/
|
|
190
199
|
├── skills/ ← user-installed skills
|
|
191
|
-
├── plugins/cache/ ← plugin skills
|
|
200
|
+
├── plugins/cache/ ← plugin skills, agents, commands, MCP servers
|
|
201
|
+
├── agents/ ← user agents (measured, read-only)
|
|
202
|
+
├── commands/ ← user slash commands (measured, read-only)
|
|
192
203
|
├── CLAUDE.md ← system instructions (read-only)
|
|
193
|
-
├── projects/*/memory/ ← auto-memory files
|
|
204
|
+
├── projects/*/memory/ ← auto-memory files (current project counts toward startup)
|
|
194
205
|
└── settings.json ← MCP server count (read-only)
|
|
195
206
|
```
|
|
196
207
|
|
|
@@ -209,17 +220,25 @@ From a real cleanup session:
|
|
|
209
220
|
| Memory files | 15KB | 2KB | **-87%** |
|
|
210
221
|
| **Est. token savings** | | **~4,300/session** | |
|
|
211
222
|
|
|
223
|
+
### A note on the numbers
|
|
224
|
+
|
|
225
|
+
claude-slim reports what a session in **this** directory pays. Memory is per-project — Claude Code loads `~/.claude/projects/<slug>/memory/` for the project you're in, not every project on disk — so running `scan` from two different repos will legitimately give you two different totals.
|
|
226
|
+
|
|
227
|
+
Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktoken) against the actual file contents. The only estimates left are marked with `~`: MCP tool schemas (~8 tokens/tool) and skills whose frontmatter can't be parsed (~30 tokens). Everything else is measured.
|
|
228
|
+
|
|
212
229
|
---
|
|
213
230
|
|
|
214
|
-
## v2.
|
|
231
|
+
## v2.8.0 — What's new
|
|
215
232
|
|
|
216
|
-
|
|
233
|
+
Accuracy release. Three reported numbers were wrong; the largest was wrong by an order of magnitude. **If your startup estimate drops sharply after upgrading, the old number was the inaccurate one.**
|
|
217
234
|
|
|
218
|
-
- **
|
|
219
|
-
- **
|
|
220
|
-
-
|
|
235
|
+
- **Startup estimate no longer sums memory across every project on disk.** Claude Code loads `~/.claude/projects/<slug>/memory/` for the project you're in — not the other 40 project directories in your `~/.claude`. The old total scaled with how many projects you'd ever opened: on the dev machine it reported **116,259 tokens where the real per-session cost was 14,399**. Now scoped to the current project, with the cross-project total still shown and labelled as not a per-session cost.
|
|
236
|
+
- **Skill listing cost is measured, not assumed.** Each skill adds a `- <name>: <description>` line to the system prompt. The flat 30-tokens-per-skill estimate stood in for all of them; measured across 68 installed skills the real spread is **30 → 509 tokens (mean 51)**. The per-plugin cost gradient can now tell five terse skills apart from five verbose ones.
|
|
237
|
+
- **`~/.claude/agents/` and `~/.claude/commands/` are now scanned.** Previously invisible despite loading into every session — 12 agents worth ~2,254 tokens on the dev machine. Reported only; never moved or deleted, because there's no restore path for them yet.
|
|
238
|
+
- **Fixed: plugin manifests were stuck at 2.7.0 for three releases**, so `claude plugin install` advertised a stale version. CI now fails on version drift.
|
|
239
|
+
- **Fixed: the token cache grew without bound** — 355 of 776 entries (46%) pointed at deleted files. `flushCache()` now prunes them.
|
|
221
240
|
|
|
222
|
-
Tests:
|
|
241
|
+
Tests: 206 → 241 (+35).
|
|
223
242
|
|
|
224
243
|
For older release notes, see [CHANGELOG.md](CHANGELOG.md).
|
|
225
244
|
|
package/dist/cli.js
CHANGED
|
@@ -201,6 +201,10 @@ program
|
|
|
201
201
|
path: e.from,
|
|
202
202
|
sizeBytes: 0,
|
|
203
203
|
tokens: e.tokenCount || 0,
|
|
204
|
+
// The file is gone, so its real listing cost is unrecoverable. Fall
|
|
205
|
+
// back to the flat estimate — the same one `totalBefore` above uses,
|
|
206
|
+
// keeping the reconstructed before-state internally consistent.
|
|
207
|
+
listingTokens: SKILL_PROMPT_OVERHEAD_TOKENS,
|
|
204
208
|
source: 'local',
|
|
205
209
|
})),
|
|
206
210
|
],
|
package/dist/paths.d.ts
CHANGED
|
@@ -3,6 +3,15 @@ export declare function getSkillsDir(): string;
|
|
|
3
3
|
export declare function getPluginsDir(): string;
|
|
4
4
|
export declare function getProjectsDir(): string;
|
|
5
5
|
export declare function getDisabledDir(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Claude Code stores per-project state under `~/.claude/projects/<slug>/`,
|
|
8
|
+
* where the slug is the absolute project path with every `/` replaced by `-`
|
|
9
|
+
* (e.g. `/Users/me/app` → `-Users-me-app`).
|
|
10
|
+
*
|
|
11
|
+
* Only the current project's `memory/` is loaded into a session — which is why
|
|
12
|
+
* the startup estimate must not sum memory across every project on disk.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getCurrentProjectSlug(cwd?: string): string;
|
|
6
15
|
export declare function getManifestPath(): string;
|
|
7
16
|
export declare function getLegacyManifestPath(): string;
|
|
8
17
|
export declare function assertInsideClaudeDir(p: string): void;
|
package/dist/paths.js
CHANGED
|
@@ -15,6 +15,17 @@ export function getProjectsDir() {
|
|
|
15
15
|
export function getDisabledDir() {
|
|
16
16
|
return join(getClaudeDir(), 'skills.disabled');
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Claude Code stores per-project state under `~/.claude/projects/<slug>/`,
|
|
20
|
+
* where the slug is the absolute project path with every `/` replaced by `-`
|
|
21
|
+
* (e.g. `/Users/me/app` → `-Users-me-app`).
|
|
22
|
+
*
|
|
23
|
+
* Only the current project's `memory/` is loaded into a session — which is why
|
|
24
|
+
* the startup estimate must not sum memory across every project on disk.
|
|
25
|
+
*/
|
|
26
|
+
export function getCurrentProjectSlug(cwd = process.cwd()) {
|
|
27
|
+
return resolve(cwd).replace(/\//g, '-');
|
|
28
|
+
}
|
|
18
29
|
export function getManifestPath() {
|
|
19
30
|
return join(getDisabledDir(), 'manifest.json');
|
|
20
31
|
}
|
package/dist/report.js
CHANGED
|
@@ -181,7 +181,6 @@ export function formatScanSummary(result) {
|
|
|
181
181
|
// --- MEMORY FILES ---
|
|
182
182
|
lines.push('');
|
|
183
183
|
const memTotal = result.memoryFiles.reduce((s, m) => s + m.sizeBytes, 0);
|
|
184
|
-
const memTokTotal = result.memoryFiles.reduce((s, m) => s + m.tokens, 0);
|
|
185
184
|
lines.push(`\x1b[1m MEMORY FILES\x1b[0m (${result.memoryFiles.length} files, ${(memTotal / 1024).toFixed(1)}KB)`);
|
|
186
185
|
const sortedMem = [...result.memoryFiles].sort((a, b) => b.sizeBytes - a.sizeBytes);
|
|
187
186
|
for (const mem of sortedMem) {
|
|
@@ -194,7 +193,16 @@ export function formatScanSummary(result) {
|
|
|
194
193
|
project = rest ? '~' + rest : '~';
|
|
195
194
|
}
|
|
196
195
|
const label = `${project}/${mem.name}`;
|
|
197
|
-
|
|
196
|
+
// Mark the only project whose memory this session would actually load.
|
|
197
|
+
const active = mem.project === result.currentProjectSlug ? ' \x1b[32m←\x1b[0m' : '';
|
|
198
|
+
lines.push(` ${label.padEnd(52)} ${kb.padStart(6)}KB ${tok.padStart(7)} tok${active}`);
|
|
199
|
+
}
|
|
200
|
+
if (result.memoryFiles.length > 0) {
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push(` \x1b[32m←\x1b[0m loaded in this project: ` +
|
|
203
|
+
`${result.currentProjectMemoryTokens.toLocaleString()} tok ` +
|
|
204
|
+
`\x1b[90m(${result.allProjectsMemoryTokens.toLocaleString()} tok across all projects, ` +
|
|
205
|
+
`not a per-session cost)\x1b[0m`);
|
|
198
206
|
}
|
|
199
207
|
// --- MCP SERVERS ---
|
|
200
208
|
lines.push('');
|
|
@@ -207,6 +215,26 @@ export function formatScanSummary(result) {
|
|
|
207
215
|
else {
|
|
208
216
|
lines.push(`\x1b[1m MCP SERVERS\x1b[0m: ${result.mcpServers}`);
|
|
209
217
|
}
|
|
218
|
+
// --- USER AGENTS & COMMANDS ---
|
|
219
|
+
// Read-only in v2.8: these are measured and reported, but never moved or
|
|
220
|
+
// deleted. There is no restore path for ~/.claude/agents yet, and shipping a
|
|
221
|
+
// destructive action without its undo would break the tool's core promise.
|
|
222
|
+
const agentTokens = result.userAgents.reduce((s, a) => s + a.listingTokens, 0);
|
|
223
|
+
const commandTokens = result.userCommands.reduce((s, c) => s + c.listingTokens, 0);
|
|
224
|
+
if (result.userAgents.length > 0 || result.userCommands.length > 0) {
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push(`\x1b[1m AGENTS & COMMANDS\x1b[0m ` +
|
|
227
|
+
`(${result.userAgents.length} agents, ${result.userCommands.length} commands)`);
|
|
228
|
+
if (result.userAgents.length > 0) {
|
|
229
|
+
lines.push(` ~/.claude/agents/`.padEnd(56) +
|
|
230
|
+
`${agentTokens.toLocaleString().padStart(7)} tok`);
|
|
231
|
+
}
|
|
232
|
+
if (result.userCommands.length > 0) {
|
|
233
|
+
lines.push(` ~/.claude/commands/`.padEnd(56) +
|
|
234
|
+
`${commandTokens.toLocaleString().padStart(7)} tok`);
|
|
235
|
+
}
|
|
236
|
+
lines.push(` \x1b[90mreported only — not touched by clean\x1b[0m`);
|
|
237
|
+
}
|
|
210
238
|
// --- SUMMARY ---
|
|
211
239
|
lines.push('');
|
|
212
240
|
lines.push(`\x1b[1m ESTIMATED OVERHEAD\x1b[0m: ~${result.totalTokensBefore.toLocaleString()} tokens at session start`);
|
package/dist/scanner/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// src/__tests__/scan-stdout-invariant.test.ts.
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { countTokensCached } from '../tokenizer.js';
|
|
8
|
-
import { getClaudeDir } from '../paths.js';
|
|
8
|
+
import { getClaudeDir, getCurrentProjectSlug } from '../paths.js';
|
|
9
9
|
import { safeReadFile } from './fs-walk.js';
|
|
10
10
|
import { scanLocalSkills } from './local-skills.js';
|
|
11
11
|
import { scanPluginSkills } from './plugin-skills.js';
|
|
@@ -18,17 +18,18 @@ import { classifyIssues } from './detectors.js';
|
|
|
18
18
|
import { scanPluginSurfaces } from './plugin-surfaces.js';
|
|
19
19
|
import { computePluginBreakdown } from './plugin-breakdown.js';
|
|
20
20
|
import { computePluginCosts } from './plugin-cost.js';
|
|
21
|
-
import {
|
|
21
|
+
import { scanUserSurfaces } from './user-surfaces.js';
|
|
22
22
|
const DEFAULT_LOOKBACK_DAYS = 60;
|
|
23
23
|
export async function scan(opts = {}) {
|
|
24
24
|
const lookbackDays = opts.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
|
|
25
|
-
const [{ skills: localSkills, brokenSymlinks, contents }, { skills: pluginSkills, plugins, tempCaches }, { memoryFiles, staleProjects }, mcp, disabledPlugins, sessionUsage,] = await Promise.all([
|
|
25
|
+
const [{ skills: localSkills, brokenSymlinks, contents }, { skills: pluginSkills, plugins, tempCaches }, { memoryFiles, staleProjects }, mcp, disabledPlugins, sessionUsage, userSurfaces,] = await Promise.all([
|
|
26
26
|
scanLocalSkills(),
|
|
27
27
|
scanPluginSkills(),
|
|
28
28
|
scanMemoryFiles(),
|
|
29
29
|
scanMcpServers(),
|
|
30
30
|
getDisabledPlugins(),
|
|
31
31
|
scanSessionUsage(lookbackDays),
|
|
32
|
+
scanUserSurfaces(),
|
|
32
33
|
]);
|
|
33
34
|
const pluginSurfaces = scanPluginSurfaces();
|
|
34
35
|
// Annotate plugin status
|
|
@@ -84,10 +85,28 @@ export async function scan(opts = {}) {
|
|
|
84
85
|
sessionsInWindow: sessionUsage.sessionsInWindow,
|
|
85
86
|
claudeMdSections,
|
|
86
87
|
});
|
|
87
|
-
// Estimate total tokens at startup
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
88
|
+
// Estimate total tokens at startup. Skill/agent/command listing costs are
|
|
89
|
+
// measured from each file's frontmatter description rather than assumed
|
|
90
|
+
// (see scanner/skill-listing.ts) — the real spread is 30–500+ tokens apiece.
|
|
91
|
+
const sumListing = (entries) => entries.reduce((sum, e) => sum + e.listingTokens, 0);
|
|
92
|
+
const skillListingTokens = sumListing(localSkills) + sumListing(pluginSkills);
|
|
93
|
+
const agentListingTokens = sumListing(userSurfaces.agents);
|
|
94
|
+
const commandListingTokens = sumListing(userSurfaces.commands);
|
|
95
|
+
// Memory is per-project: a session loads ~/.claude/projects/<slug>/memory/
|
|
96
|
+
// for the project it is running in, and nothing from the other projects on
|
|
97
|
+
// disk. Summing all of them (pre-2.8 behaviour) inflated the startup estimate
|
|
98
|
+
// by a factor of however many projects the user had — 100k+ tokens on a busy
|
|
99
|
+
// machine, for a number labelled "tokens at session start".
|
|
100
|
+
const currentProjectSlug = getCurrentProjectSlug();
|
|
101
|
+
const currentProjectMemoryTokens = memoryFiles
|
|
102
|
+
.filter((m) => m.project === currentProjectSlug)
|
|
103
|
+
.reduce((sum, m) => sum + m.tokens, 0);
|
|
104
|
+
const allProjectsMemoryTokens = memoryFiles.reduce((sum, m) => sum + m.tokens, 0);
|
|
105
|
+
const totalTokensBefore = skillListingTokens +
|
|
106
|
+
agentListingTokens +
|
|
107
|
+
commandListingTokens +
|
|
108
|
+
claudeMdTokens +
|
|
109
|
+
currentProjectMemoryTokens;
|
|
91
110
|
return {
|
|
92
111
|
localSkills,
|
|
93
112
|
pluginSkills,
|
|
@@ -102,5 +121,10 @@ export async function scan(opts = {}) {
|
|
|
102
121
|
issues,
|
|
103
122
|
totalTokensBefore,
|
|
104
123
|
pluginBreakdown,
|
|
124
|
+
userAgents: userSurfaces.agents,
|
|
125
|
+
userCommands: userSurfaces.commands,
|
|
126
|
+
currentProjectSlug,
|
|
127
|
+
currentProjectMemoryTokens,
|
|
128
|
+
allProjectsMemoryTokens,
|
|
105
129
|
};
|
|
106
130
|
}
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { countTokensCached } from '../tokenizer.js';
|
|
4
4
|
import { getSkillsDir } from '../paths.js';
|
|
5
5
|
import { safeReadFile, safeReaddir, isDirectory, isBrokenSymlink, resolveRealPath, } from './fs-walk.js';
|
|
6
|
+
import { listingTokensFromContent } from './skill-listing.js';
|
|
6
7
|
export function dedupeBySymlink(candidates) {
|
|
7
8
|
const seen = new Map();
|
|
8
9
|
for (const { skill, realMdPath } of candidates) {
|
|
@@ -57,6 +58,7 @@ export async function scanLocalSkills() {
|
|
|
57
58
|
path: dirPath,
|
|
58
59
|
sizeBytes: Buffer.byteLength(content),
|
|
59
60
|
tokens,
|
|
61
|
+
listingTokens: listingTokensFromContent(displayName, content),
|
|
60
62
|
source: 'local',
|
|
61
63
|
},
|
|
62
64
|
realMdPath,
|
|
@@ -4,7 +4,7 @@ export interface PluginCostBreakdown {
|
|
|
4
4
|
marketplace: string;
|
|
5
5
|
/** Tokens from the matching CLAUDE.md section (0 if no match). */
|
|
6
6
|
claudeMdTokens: number;
|
|
7
|
-
/**
|
|
7
|
+
/** Measured sum of each skill's `- <name>: <description>` listing line. */
|
|
8
8
|
skillTokens: number;
|
|
9
9
|
/** DEFERRED_TOOL_OVERHEAD_TOKENS × MCP_SERVER_TOOLS_AVG × mcpServerKeys.length */
|
|
10
10
|
mcpToolTokens: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFERRED_TOOL_OVERHEAD_TOKENS, COMMAND_OVERHEAD_TOKENS, MCP_SERVER_TOOLS_AVG, } from './constants.js';
|
|
2
2
|
/**
|
|
3
3
|
* Find a CLAUDE.md section whose name contains the plugin name as a substring
|
|
4
4
|
* (case-insensitive). Returns the first match or null.
|
|
@@ -18,7 +18,7 @@ export function computePluginCosts(surfaces, claudeMdSections) {
|
|
|
18
18
|
return surfaces.map((s) => {
|
|
19
19
|
const matched = matchSection(s.pluginName, claudeMdSections);
|
|
20
20
|
const claudeMdTokens = matched?.tokens ?? 0;
|
|
21
|
-
const skillTokens =
|
|
21
|
+
const skillTokens = s.skillListingTokens;
|
|
22
22
|
const mcpToolTokens = DEFERRED_TOOL_OVERHEAD_TOKENS * MCP_SERVER_TOOLS_AVG * s.mcpServerKeys.length;
|
|
23
23
|
const commandTokens = COMMAND_OVERHEAD_TOKENS * s.commands.length;
|
|
24
24
|
const totalEstimatedTokens = claudeMdTokens + skillTokens + mcpToolTokens + commandTokens;
|
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { countTokensCached } from '../tokenizer.js';
|
|
3
3
|
import { getPluginsDir } from '../paths.js';
|
|
4
4
|
import { safeReadFile, safeReaddir, isDirectory, getDirSize } from './fs-walk.js';
|
|
5
|
+
import { listingTokensFromContent } from './skill-listing.js';
|
|
5
6
|
export async function scanPluginSkills() {
|
|
6
7
|
const skills = [];
|
|
7
8
|
const plugins = [];
|
|
@@ -40,6 +41,7 @@ export async function scanPluginSkills() {
|
|
|
40
41
|
path: skillPath,
|
|
41
42
|
sizeBytes: Buffer.byteLength(content),
|
|
42
43
|
tokens: countTokensCached(content, skillMd),
|
|
44
|
+
listingTokens: listingTokensFromContent(skillDir, content),
|
|
43
45
|
source: 'plugin',
|
|
44
46
|
pluginName,
|
|
45
47
|
});
|
|
@@ -2,6 +2,8 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { getPluginsDir } from '../paths.js';
|
|
5
|
+
import { listingTokensFromContent } from './skill-listing.js';
|
|
6
|
+
import { SKILL_PROMPT_OVERHEAD_TOKENS } from './constants.js';
|
|
5
7
|
function safeReaddir(p) {
|
|
6
8
|
try {
|
|
7
9
|
return readdirSync(p);
|
|
@@ -49,19 +51,29 @@ function parseMcpServerKeys(installDir) {
|
|
|
49
51
|
function scanSkills(installDir) {
|
|
50
52
|
const skillsDir = join(installDir, 'skills');
|
|
51
53
|
if (!isDir(skillsDir))
|
|
52
|
-
return [];
|
|
54
|
+
return { names: [], listingTokens: 0 };
|
|
53
55
|
const names = [];
|
|
56
|
+
let listingTokens = 0;
|
|
54
57
|
for (const entry of safeReaddir(skillsDir)) {
|
|
55
58
|
const skillDir = join(skillsDir, entry);
|
|
56
59
|
if (!isDir(skillDir))
|
|
57
60
|
continue;
|
|
58
61
|
const upper = join(skillDir, 'SKILL.md');
|
|
59
62
|
const lower = join(skillDir, 'skill.md');
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
const present = isFile(upper) ? upper : isFile(lower) ? lower : null;
|
|
64
|
+
if (present === null)
|
|
65
|
+
continue;
|
|
66
|
+
names.push(entry);
|
|
67
|
+
let content = '';
|
|
68
|
+
try {
|
|
69
|
+
content = readFileSync(present, 'utf-8');
|
|
62
70
|
}
|
|
71
|
+
catch { /* unreadable */ }
|
|
72
|
+
listingTokens += content
|
|
73
|
+
? listingTokensFromContent(entry, content)
|
|
74
|
+
: SKILL_PROMPT_OVERHEAD_TOKENS;
|
|
63
75
|
}
|
|
64
|
-
return names;
|
|
76
|
+
return { names, listingTokens };
|
|
65
77
|
}
|
|
66
78
|
function scanCommands(installDir) {
|
|
67
79
|
const commandsDir = join(installDir, 'commands');
|
|
@@ -103,7 +115,7 @@ export function scanPluginSurfaces() {
|
|
|
103
115
|
continue;
|
|
104
116
|
const dirStat = safeStat(installDir);
|
|
105
117
|
const installedAt = dirStat ? Number(dirStat.mtimeMs) : 0;
|
|
106
|
-
const skills = scanSkills(installDir);
|
|
118
|
+
const { names: skills, listingTokens: skillListingTokens } = scanSkills(installDir);
|
|
107
119
|
const mcpServerKeys = parseMcpServerKeys(installDir);
|
|
108
120
|
const mcpToolPrefixes = mcpServerKeys.map((key) => `plugin_${pluginName}_${key}`);
|
|
109
121
|
const commands = scanCommands(installDir);
|
|
@@ -116,6 +128,7 @@ export function scanPluginSurfaces() {
|
|
|
116
128
|
installDir,
|
|
117
129
|
installedAt,
|
|
118
130
|
skills,
|
|
131
|
+
skillListingTokens,
|
|
119
132
|
mcpServerKeys,
|
|
120
133
|
mcpToolPrefixes,
|
|
121
134
|
commands,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the `description` value from a SKILL.md (or agent .md) frontmatter
|
|
3
|
+
* block. Returns null when there is no frontmatter or no description key —
|
|
4
|
+
* callers fall back to {@link SKILL_PROMPT_OVERHEAD_TOKENS}.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseFrontmatterDescription(content: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Token cost of one entry in the system prompt's skill listing.
|
|
9
|
+
*
|
|
10
|
+
* Falls back to the flat {@link SKILL_PROMPT_OVERHEAD_TOKENS} estimate when the
|
|
11
|
+
* description is missing or unreadable, so a malformed SKILL.md degrades to the
|
|
12
|
+
* pre-2.8 behaviour rather than reporting zero.
|
|
13
|
+
*/
|
|
14
|
+
export declare function listingTokens(name: string, description: string | null): number;
|
|
15
|
+
/** Convenience wrapper: parse a raw SKILL.md and return its listing cost. */
|
|
16
|
+
export declare function listingTokensFromContent(name: string, content: string): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { countTokens } from '../tokenizer.js';
|
|
2
|
+
import { SKILL_PROMPT_OVERHEAD_TOKENS } from './constants.js';
|
|
3
|
+
// Claude Code renders each available skill into the system prompt as one line:
|
|
4
|
+
// `- <name>: <description>`
|
|
5
|
+
// The description comes from the SKILL.md YAML frontmatter and is reproduced
|
|
6
|
+
// verbatim, so its real length — not a flat per-skill constant — is what the
|
|
7
|
+
// session actually pays for. Measured across 68 installed skills the spread is
|
|
8
|
+
// 30 → 509 tokens (mean 51), which a fixed estimate cannot represent.
|
|
9
|
+
const LISTING_PREFIX = '- ';
|
|
10
|
+
const LISTING_SEPARATOR = ': ';
|
|
11
|
+
// Frontmatter block at the very top of the file: `---\n<yaml>\n---`.
|
|
12
|
+
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
13
|
+
// `description:` up to the next top-level key. YAML folded/literal blocks and
|
|
14
|
+
// plain multi-line values both indent their continuation lines, so "next line
|
|
15
|
+
// that starts at column 0 with `key:`" is the terminator.
|
|
16
|
+
//
|
|
17
|
+
// Deliberately NOT /m: with the multiline flag `$` matches end-of-line, which
|
|
18
|
+
// would terminate the capture at the first newline and silently truncate every
|
|
19
|
+
// wrapped description to its first line.
|
|
20
|
+
const DESCRIPTION_PATTERN = /(?:^|\n)description:[ \t]*([\s\S]*?)(?=\r?\n[A-Za-z_][\w-]*:|$)/;
|
|
21
|
+
/**
|
|
22
|
+
* Extract the `description` value from a SKILL.md (or agent .md) frontmatter
|
|
23
|
+
* block. Returns null when there is no frontmatter or no description key —
|
|
24
|
+
* callers fall back to {@link SKILL_PROMPT_OVERHEAD_TOKENS}.
|
|
25
|
+
*/
|
|
26
|
+
export function parseFrontmatterDescription(content) {
|
|
27
|
+
const fm = FRONTMATTER_PATTERN.exec(content);
|
|
28
|
+
if (!fm)
|
|
29
|
+
return null;
|
|
30
|
+
const match = DESCRIPTION_PATTERN.exec(fm[1]);
|
|
31
|
+
if (!match)
|
|
32
|
+
return null;
|
|
33
|
+
const raw = match[1]
|
|
34
|
+
// Drop a leading YAML block scalar indicator (`|`, `>`, `|-`, `>-`, …).
|
|
35
|
+
.replace(/^[|>][+-]?\d*[ \t]*\r?\n/, '')
|
|
36
|
+
// Collapse the indentation + wrapping that YAML uses for long values;
|
|
37
|
+
// the system prompt renders them as a single line.
|
|
38
|
+
.split(/\r?\n/)
|
|
39
|
+
.map((line) => line.trim())
|
|
40
|
+
.join(' ')
|
|
41
|
+
.trim();
|
|
42
|
+
if (!raw)
|
|
43
|
+
return null;
|
|
44
|
+
// Strip surrounding quotes if the value was quoted.
|
|
45
|
+
const unquoted = (raw.startsWith('"') && raw.endsWith('"')) ||
|
|
46
|
+
(raw.startsWith("'") && raw.endsWith("'"))
|
|
47
|
+
? raw.slice(1, -1)
|
|
48
|
+
: raw;
|
|
49
|
+
return unquoted.trim() || null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Token cost of one entry in the system prompt's skill listing.
|
|
53
|
+
*
|
|
54
|
+
* Falls back to the flat {@link SKILL_PROMPT_OVERHEAD_TOKENS} estimate when the
|
|
55
|
+
* description is missing or unreadable, so a malformed SKILL.md degrades to the
|
|
56
|
+
* pre-2.8 behaviour rather than reporting zero.
|
|
57
|
+
*/
|
|
58
|
+
export function listingTokens(name, description) {
|
|
59
|
+
if (description === null)
|
|
60
|
+
return SKILL_PROMPT_OVERHEAD_TOKENS;
|
|
61
|
+
return countTokens(LISTING_PREFIX + name + LISTING_SEPARATOR + description);
|
|
62
|
+
}
|
|
63
|
+
/** Convenience wrapper: parse a raw SKILL.md and return its listing cost. */
|
|
64
|
+
export function listingTokensFromContent(name, content) {
|
|
65
|
+
return listingTokens(name, parseFrontmatterDescription(content));
|
|
66
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface UserSurfaceEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
sizeBytes: number;
|
|
5
|
+
/** Full file body — what the agent/command costs once invoked. */
|
|
6
|
+
tokens: number;
|
|
7
|
+
/** What it costs at startup by merely existing. */
|
|
8
|
+
listingTokens: number;
|
|
9
|
+
}
|
|
10
|
+
export interface UserSurfacesResult {
|
|
11
|
+
agents: UserSurfaceEntry[];
|
|
12
|
+
commands: UserSurfaceEntry[];
|
|
13
|
+
}
|
|
14
|
+
export declare function getUserAgentsDir(): string;
|
|
15
|
+
export declare function getUserCommandsDir(): string;
|
|
16
|
+
export declare function scanUserSurfaces(): Promise<UserSurfacesResult>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { getClaudeDir } from '../paths.js';
|
|
3
|
+
import { countTokensCached } from '../tokenizer.js';
|
|
4
|
+
import { safeReadFile, safeReaddir, isBrokenSymlink } from './fs-walk.js';
|
|
5
|
+
import { listingTokens, parseFrontmatterDescription } from './skill-listing.js';
|
|
6
|
+
import { COMMAND_OVERHEAD_TOKENS } from './constants.js';
|
|
7
|
+
export function getUserAgentsDir() {
|
|
8
|
+
return join(getClaudeDir(), 'agents');
|
|
9
|
+
}
|
|
10
|
+
export function getUserCommandsDir() {
|
|
11
|
+
return join(getClaudeDir(), 'commands');
|
|
12
|
+
}
|
|
13
|
+
/** Agents render exactly like skills: `- <name>: <description>`. */
|
|
14
|
+
function agentListingCost(name, content) {
|
|
15
|
+
return listingTokens(name, parseFrontmatterDescription(content));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Commands without frontmatter appear as a bare name in the listing, which is
|
|
19
|
+
* what COMMAND_OVERHEAD_TOKENS already estimates for plugin commands. With a
|
|
20
|
+
* description they cost the same as a skill line.
|
|
21
|
+
*/
|
|
22
|
+
function commandListingCost(name, content) {
|
|
23
|
+
const description = parseFrontmatterDescription(content);
|
|
24
|
+
if (description === null)
|
|
25
|
+
return COMMAND_OVERHEAD_TOKENS;
|
|
26
|
+
return listingTokens(name, description);
|
|
27
|
+
}
|
|
28
|
+
async function scanMarkdownDir(dir, listingCost) {
|
|
29
|
+
const entries = await safeReaddir(dir);
|
|
30
|
+
const results = await Promise.all(entries.map(async (entry) => {
|
|
31
|
+
if (!entry.endsWith('.md'))
|
|
32
|
+
return null;
|
|
33
|
+
const path = join(dir, entry);
|
|
34
|
+
// Symlinked agents are common (vendored packs link into ~/.claude/agents).
|
|
35
|
+
// A dangling link contributes nothing to the prompt — skip it rather than
|
|
36
|
+
// counting a phantom entry.
|
|
37
|
+
if (await isBrokenSymlink(path))
|
|
38
|
+
return null;
|
|
39
|
+
const content = await safeReadFile(path);
|
|
40
|
+
if (content === null)
|
|
41
|
+
return null;
|
|
42
|
+
const name = entry.slice(0, -3);
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
path,
|
|
46
|
+
sizeBytes: Buffer.byteLength(content),
|
|
47
|
+
tokens: countTokensCached(content, path),
|
|
48
|
+
listingTokens: listingCost(name, content),
|
|
49
|
+
};
|
|
50
|
+
}));
|
|
51
|
+
return results.filter((r) => r !== null);
|
|
52
|
+
}
|
|
53
|
+
export async function scanUserSurfaces() {
|
|
54
|
+
const [agents, commands] = await Promise.all([
|
|
55
|
+
scanMarkdownDir(getUserAgentsDir(), agentListingCost),
|
|
56
|
+
scanMarkdownDir(getUserCommandsDir(), commandListingCost),
|
|
57
|
+
]);
|
|
58
|
+
return { agents, commands };
|
|
59
|
+
}
|
package/dist/tokenizer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
2
|
+
import { readFile, writeFile, mkdir, rename, access } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { getClaudeDir } from './paths.js';
|
|
5
5
|
let encoder = null;
|
|
@@ -11,13 +11,18 @@ function getCachePath() {
|
|
|
11
11
|
let cache = { version: 1, entries: {} };
|
|
12
12
|
let cacheDirty = false;
|
|
13
13
|
export async function initTokenizer() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
// Building the cl100k_base encoder parses a large rank table — hundreds of
|
|
15
|
+
// milliseconds, and far more under CPU contention. It is immutable once
|
|
16
|
+
// built, so reuse it across calls. Only the cache is per-init state.
|
|
17
|
+
if (encoder === null && !useFallback) {
|
|
18
|
+
try {
|
|
19
|
+
// cl100k_base is closest to Claude's tokenizer (gpt-4o uses o200k_base which undercounts by ~15%)
|
|
20
|
+
const { getEncoding } = await import('js-tiktoken');
|
|
21
|
+
encoder = getEncoding('cl100k_base');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
useFallback = true;
|
|
25
|
+
}
|
|
21
26
|
}
|
|
22
27
|
// Reset in-memory state so repeated initTokenizer() calls (e.g. across
|
|
23
28
|
// test cases) don't bleed cache entries from a prior invocation.
|
|
@@ -51,9 +56,35 @@ export function countTokensCached(text, filePath) {
|
|
|
51
56
|
cacheDirty = true;
|
|
52
57
|
return tokens;
|
|
53
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Drop cache entries whose source file no longer exists.
|
|
61
|
+
*
|
|
62
|
+
* Skills get uninstalled, plugins get removed, sessions get rotated — but the
|
|
63
|
+
* entry keyed by that path stayed forever, so the cache grew without bound. A
|
|
64
|
+
* path that is gone can never produce a hit again, which makes existence the
|
|
65
|
+
* safe pruning predicate: it cannot evict an entry that is still reachable.
|
|
66
|
+
* (Measured on a real install before this fix: 355 of 776 entries were dead.)
|
|
67
|
+
*/
|
|
68
|
+
async function pruneMissingEntries() {
|
|
69
|
+
const paths = Object.keys(cache.entries);
|
|
70
|
+
const alive = await Promise.all(paths.map(async (p) => {
|
|
71
|
+
try {
|
|
72
|
+
await access(p);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}));
|
|
79
|
+
for (let i = 0; i < paths.length; i++) {
|
|
80
|
+
if (!alive[i])
|
|
81
|
+
delete cache.entries[paths[i]];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
54
84
|
export async function flushCache() {
|
|
55
85
|
if (!cacheDirty)
|
|
56
86
|
return;
|
|
87
|
+
await pruneMissingEntries();
|
|
57
88
|
const target = getCachePath();
|
|
58
89
|
const tmp = target + '.tmp';
|
|
59
90
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import type { UserSurfaceEntry } from './scanner/user-surfaces.js';
|
|
2
|
+
export type { UserSurfaceEntry };
|
|
1
3
|
export interface SkillInfo {
|
|
2
4
|
name: string;
|
|
3
5
|
path: string;
|
|
4
6
|
sizeBytes: number;
|
|
7
|
+
/** Full SKILL.md body tokens — what the skill costs once invoked. */
|
|
5
8
|
tokens: number;
|
|
9
|
+
/**
|
|
10
|
+
* Tokens this skill adds to the system prompt just by being installed
|
|
11
|
+
* (its `- <name>: <description>` listing line). Measured from the
|
|
12
|
+
* frontmatter description; see scanner/skill-listing.ts.
|
|
13
|
+
*/
|
|
14
|
+
listingTokens: number;
|
|
6
15
|
source: 'local' | 'plugin';
|
|
7
16
|
pluginName?: string;
|
|
8
17
|
}
|
|
@@ -63,6 +72,16 @@ export interface ScanResult {
|
|
|
63
72
|
issues: Issue[];
|
|
64
73
|
totalTokensBefore: number;
|
|
65
74
|
pluginBreakdown: PluginBreakdown[];
|
|
75
|
+
/** ~/.claude/agents/*.md — loaded into the system prompt as the agent catalog. */
|
|
76
|
+
userAgents: UserSurfaceEntry[];
|
|
77
|
+
/** ~/.claude/commands/*.md — loaded as the slash-command listing. */
|
|
78
|
+
userCommands: UserSurfaceEntry[];
|
|
79
|
+
/** Project slug (cwd with `/` → `-`) whose memory a session here would load. */
|
|
80
|
+
currentProjectSlug: string;
|
|
81
|
+
/** Memory tokens actually loaded at startup — current project only. */
|
|
82
|
+
currentProjectMemoryTokens: number;
|
|
83
|
+
/** Memory tokens across every project on disk. Not a per-session cost. */
|
|
84
|
+
allProjectsMemoryTokens: number;
|
|
66
85
|
}
|
|
67
86
|
export interface ManifestEntry {
|
|
68
87
|
date: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slim",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.8.1",
|
|
4
|
+
"description": "Audit and shrink your Claude Code startup context. Measures what every skill, plugin, agent, command, and memory file costs in the system prompt, then reversibly disables the dead weight. Non-destructive scan, tiered proposals, one-command restore — no proxy, no compression.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-slim": "./dist/cli.js"
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"dev": "tsc --watch",
|
|
18
18
|
"test": "vitest run",
|
|
19
|
+
"check:versions": "node scripts/check-version-sync.mjs",
|
|
19
20
|
"test:watch": "vitest",
|
|
20
21
|
"demo": "npm run build && bash scripts/demo/fixture.sh && vhs scripts/demo/demo.tape",
|
|
21
|
-
"prepublishOnly": "npm run build"
|
|
22
|
+
"prepublishOnly": "npm run check:versions && npm run build"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
24
25
|
"claude",
|
|
@@ -35,7 +36,13 @@
|
|
|
35
36
|
"plugin",
|
|
36
37
|
"plugin-cleanup",
|
|
37
38
|
"cli",
|
|
38
|
-
"developer-experience"
|
|
39
|
+
"developer-experience",
|
|
40
|
+
"context-engineering",
|
|
41
|
+
"context-window",
|
|
42
|
+
"agent-skills",
|
|
43
|
+
"claude-code-plugin",
|
|
44
|
+
"usage-limits",
|
|
45
|
+
"llm-tools"
|
|
39
46
|
],
|
|
40
47
|
"author": "iops-leo",
|
|
41
48
|
"license": "MIT",
|
|
@@ -51,13 +58,13 @@
|
|
|
51
58
|
"node": ">=20"
|
|
52
59
|
},
|
|
53
60
|
"dependencies": {
|
|
54
|
-
"commander": "^
|
|
61
|
+
"commander": "^14.0.3",
|
|
55
62
|
"js-tiktoken": "^1.0.18"
|
|
56
63
|
},
|
|
57
64
|
"devDependencies": {
|
|
58
|
-
"@types/node": "^22.
|
|
65
|
+
"@types/node": "^22.20.1",
|
|
59
66
|
"tmp-promise": "^3.0.3",
|
|
60
|
-
"typescript": "^5.
|
|
61
|
-
"vitest": "^4.1.
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"vitest": "^4.1.10"
|
|
62
69
|
}
|
|
63
70
|
}
|