claude-slim 2.2.1 → 2.2.3
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/dist/cleaner.js +22 -3
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +10 -1
- package/dist/report.js +4 -4
- package/dist/scanner.js +6 -4
- package/dist/selection.js +3 -1
- package/dist/tokenizer.js +19 -6
- package/package.json +1 -1
- package/skills/claude-slim/SKILL.md +18 -16
package/dist/cleaner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { rename, readdir, rmdir, rm, unlink, lstat, mkdir } from 'node:fs/promises';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { appendManifest, ensureDisabledDir, getDisabledDir, removeEntry } from './manifest.js';
|
|
4
|
-
import { getSkillsDir } from './paths.js';
|
|
4
|
+
import { assertInsideClaudeDir, getSkillsDir } from './paths.js';
|
|
5
5
|
async function pathExists(p) {
|
|
6
6
|
try {
|
|
7
7
|
await lstat(p);
|
|
@@ -33,6 +33,7 @@ export async function cleanIssues(issues) {
|
|
|
33
33
|
const errors = [];
|
|
34
34
|
for (const issue of issues) {
|
|
35
35
|
try {
|
|
36
|
+
assertInsideClaudeDir(issue.path);
|
|
36
37
|
if (issue.type === 'broken_symlink') {
|
|
37
38
|
await unlink(issue.path);
|
|
38
39
|
const entry = {
|
|
@@ -67,8 +68,17 @@ export async function cleanIssues(issues) {
|
|
|
67
68
|
moved.push(entry);
|
|
68
69
|
}
|
|
69
70
|
else if (issue.type === 'temp_cache') {
|
|
70
|
-
// Delete temp directories (failed plugin installs, not restorable)
|
|
71
|
-
|
|
71
|
+
// Delete temp directories (failed plugin installs, not restorable).
|
|
72
|
+
// If the path is itself a symlink, only remove the link — never
|
|
73
|
+
// follow it into whatever it points at. fs.rm on Node >=18 already
|
|
74
|
+
// behaves this way, but we encode the invariant explicitly.
|
|
75
|
+
const st = await lstat(issue.path);
|
|
76
|
+
if (st.isSymbolicLink()) {
|
|
77
|
+
await unlink(issue.path);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
await rm(issue.path, { recursive: true, force: true });
|
|
81
|
+
}
|
|
72
82
|
const entry = {
|
|
73
83
|
date: new Date().toISOString(),
|
|
74
84
|
name: issue.name,
|
|
@@ -138,6 +148,7 @@ async function cleanEmptyDirs(dir) {
|
|
|
138
148
|
catch { /* skip */ }
|
|
139
149
|
}
|
|
140
150
|
export async function restoreItem(entry) {
|
|
151
|
+
assertInsideClaudeDir(entry.from);
|
|
141
152
|
if (entry.type === 'broken_symlink') {
|
|
142
153
|
throw new Error(`Broken symlinks cannot be restored (${entry.name})`);
|
|
143
154
|
}
|
|
@@ -163,6 +174,14 @@ export async function restoreItem(entry) {
|
|
|
163
174
|
// Restore skill directory using the same naming as cleanIssues
|
|
164
175
|
const safeName = entry.name.replace(/\//g, '--');
|
|
165
176
|
const src = join(disabledDir, safeName);
|
|
177
|
+
if (!(await pathExists(src))) {
|
|
178
|
+
throw new Error(`Backup not found for "${entry.name}" at ${src}. ` +
|
|
179
|
+
`It may have been manually removed.`);
|
|
180
|
+
}
|
|
181
|
+
if (await pathExists(entry.from)) {
|
|
182
|
+
throw new Error(`Cannot restore: ${entry.from} already exists. ` +
|
|
183
|
+
`Remove or rename it first.`);
|
|
184
|
+
}
|
|
166
185
|
await mkdir(dirname(entry.from), { recursive: true });
|
|
167
186
|
await rename(src, entry.from);
|
|
168
187
|
}
|
package/dist/paths.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export declare function getProjectsDir(): string;
|
|
|
5
5
|
export declare function getDisabledDir(): string;
|
|
6
6
|
export declare function getManifestPath(): string;
|
|
7
7
|
export declare function getLegacyManifestPath(): string;
|
|
8
|
+
export declare function assertInsideClaudeDir(p: string): void;
|
package/dist/paths.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, resolve, sep } from 'node:path';
|
|
3
3
|
export function getClaudeDir() {
|
|
4
4
|
return join(homedir(), '.claude');
|
|
5
5
|
}
|
|
@@ -21,3 +21,12 @@ export function getManifestPath() {
|
|
|
21
21
|
export function getLegacyManifestPath() {
|
|
22
22
|
return join(getDisabledDir(), '.claude-slim-manifest.jsonl');
|
|
23
23
|
}
|
|
24
|
+
// Refuse to operate on a path outside ~/.claude/. Guards destructive operations
|
|
25
|
+
// (rename/rm/unlink) from acting on attacker-tampered manifests or scanner bugs.
|
|
26
|
+
export function assertInsideClaudeDir(p) {
|
|
27
|
+
const resolved = resolve(p);
|
|
28
|
+
const root = resolve(getClaudeDir());
|
|
29
|
+
if (resolved !== root && !resolved.startsWith(root + sep)) {
|
|
30
|
+
throw new Error(`Refusing to operate on path outside ~/.claude/: ${p}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/report.js
CHANGED
|
@@ -28,25 +28,25 @@ export function calculateReport(scanBefore, scanAfter, movedEntries, sessionsPer
|
|
|
28
28
|
label: 'Local skills',
|
|
29
29
|
before: String(localBefore),
|
|
30
30
|
after: String(localAfter),
|
|
31
|
-
saved: `${
|
|
31
|
+
saved: `${localBefore - localAfter}`,
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
label: 'System prompt',
|
|
35
35
|
before: `~${promptBefore}`,
|
|
36
36
|
after: `~${promptAfter}`,
|
|
37
|
-
saved: `${
|
|
37
|
+
saved: `${promptBefore - promptAfter}`,
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
label: 'Memory files',
|
|
41
41
|
before: `${(memBefore / 1024).toFixed(1)}KB`,
|
|
42
42
|
after: `${(memAfter / 1024).toFixed(1)}KB`,
|
|
43
|
-
saved: `${((
|
|
43
|
+
saved: `${((memBefore - memAfter) / 1024).toFixed(1)}KB`,
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
label: 'Est. tokens',
|
|
47
47
|
before: `~${before.toLocaleString()}`,
|
|
48
48
|
after: `~${after.toLocaleString()}`,
|
|
49
|
-
saved: `~${(
|
|
49
|
+
saved: `~${(before - after).toLocaleString()}`,
|
|
50
50
|
},
|
|
51
51
|
];
|
|
52
52
|
return {
|
package/dist/scanner.js
CHANGED
|
@@ -47,11 +47,13 @@ async function isBrokenSymlink(p) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
// execFile (not exec) — never routes through a shell, so command arguments
|
|
51
|
+
// cannot be interpreted as shell metacharacters regardless of caller inputs.
|
|
52
|
+
async function runCommand(file, args) {
|
|
51
53
|
try {
|
|
52
|
-
const {
|
|
54
|
+
const { execFile } = await import('node:child_process');
|
|
53
55
|
return new Promise((resolve) => {
|
|
54
|
-
|
|
56
|
+
execFile(file, args, { timeout: 10000 }, (_err, stdout) => {
|
|
55
57
|
resolve(stdout || '');
|
|
56
58
|
});
|
|
57
59
|
});
|
|
@@ -307,7 +309,7 @@ export function parseDisabledPlugins(output) {
|
|
|
307
309
|
return disabled;
|
|
308
310
|
}
|
|
309
311
|
async function getDisabledPlugins() {
|
|
310
|
-
return parseDisabledPlugins(await runCommand('claude plugin list'));
|
|
312
|
+
return parseDisabledPlugins(await runCommand('claude', ['plugin', 'list']));
|
|
311
313
|
}
|
|
312
314
|
export function parseClaudeMdSections(content) {
|
|
313
315
|
const sections = [];
|
package/dist/selection.js
CHANGED
|
@@ -28,9 +28,11 @@ export function resolveRestoreSelection(input, count) {
|
|
|
28
28
|
return Array.from({ length: count }, (_, i) => i);
|
|
29
29
|
}
|
|
30
30
|
const indices = [];
|
|
31
|
+
const seen = new Set();
|
|
31
32
|
for (const part of trimmed.split(',')) {
|
|
32
33
|
const num = parseInt(part.trim(), 10);
|
|
33
|
-
if (!isNaN(num) && num >= 1 && num <= count) {
|
|
34
|
+
if (!isNaN(num) && num >= 1 && num <= count && !seen.has(num)) {
|
|
35
|
+
seen.add(num);
|
|
34
36
|
indices.push(num - 1);
|
|
35
37
|
}
|
|
36
38
|
}
|
package/dist/tokenizer.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { getClaudeDir } from './paths.js';
|
|
5
5
|
let encoder = null;
|
|
6
6
|
let useFallback = false;
|
|
7
|
-
|
|
7
|
+
// Resolved lazily so the HOME env stub used in tests is honored.
|
|
8
|
+
function getCachePath() {
|
|
9
|
+
return join(getClaudeDir(), '.token-cache.json');
|
|
10
|
+
}
|
|
8
11
|
let cache = { version: 1, entries: {} };
|
|
9
12
|
let cacheDirty = false;
|
|
10
13
|
export async function initTokenizer() {
|
|
@@ -16,8 +19,12 @@ export async function initTokenizer() {
|
|
|
16
19
|
catch {
|
|
17
20
|
useFallback = true;
|
|
18
21
|
}
|
|
22
|
+
// Reset in-memory state so repeated initTokenizer() calls (e.g. across
|
|
23
|
+
// test cases) don't bleed cache entries from a prior invocation.
|
|
24
|
+
cache = { version: 1, entries: {} };
|
|
25
|
+
cacheDirty = false;
|
|
19
26
|
try {
|
|
20
|
-
const raw = await readFile(
|
|
27
|
+
const raw = await readFile(getCachePath(), 'utf-8');
|
|
21
28
|
cache = JSON.parse(raw);
|
|
22
29
|
}
|
|
23
30
|
catch {
|
|
@@ -47,9 +54,15 @@ export function countTokensCached(text, filePath) {
|
|
|
47
54
|
export async function flushCache() {
|
|
48
55
|
if (!cacheDirty)
|
|
49
56
|
return;
|
|
57
|
+
const target = getCachePath();
|
|
58
|
+
const tmp = target + '.tmp';
|
|
50
59
|
try {
|
|
51
|
-
await mkdir(dirname(
|
|
52
|
-
|
|
60
|
+
await mkdir(dirname(target), { recursive: true });
|
|
61
|
+
// Atomic: write to a sibling tmp file first, then rename. A crash mid-write
|
|
62
|
+
// leaves the prior cache (or nothing) — never a torn JSON file.
|
|
63
|
+
await writeFile(tmp, JSON.stringify(cache, null, 2));
|
|
64
|
+
await rename(tmp, target);
|
|
65
|
+
cacheDirty = false;
|
|
53
66
|
}
|
|
54
67
|
catch {
|
|
55
68
|
// Non-critical
|
package/package.json
CHANGED
|
@@ -41,26 +41,28 @@ bash "${CLAUDE_PLUGIN_ROOT}/skills/claude-slim/scripts/scan.sh"
|
|
|
41
41
|
|
|
42
42
|
After getting the scan JSON, YOU must interpret and present results to the user. Do NOT just dump raw CLI output. Present a full diagnostic report in the user's language.
|
|
43
43
|
|
|
44
|
+
> **Templates below are shown in English for readability. Always translate headers, labels, and prompts into the user's detected language when rendering.**
|
|
45
|
+
|
|
44
46
|
### 2-1. Environment Snapshot Table
|
|
45
47
|
|
|
46
48
|
Show a summary table:
|
|
47
49
|
|
|
48
|
-
|
|
|
49
|
-
|
|
50
|
-
|
|
|
51
|
-
|
|
|
50
|
+
| Item | Count | Tokens |
|
|
51
|
+
|------|-------|--------|
|
|
52
|
+
| Local skills | N (XKB) | X tok |
|
|
53
|
+
| Plugins | N (M skills) | ~X tok |
|
|
52
54
|
| CLAUDE.md | XKB | X tok |
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
+
| Memory files | N (XKB) | ~X tok |
|
|
56
|
+
| **Session startup overhead** | | **~X tok** |
|
|
55
57
|
|
|
56
58
|
### 2-2. Plugin Detail Table
|
|
57
59
|
|
|
58
60
|
List each plugin with skill count and a judgment:
|
|
59
61
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
| omc | 36 |
|
|
63
|
-
| temp_local_... | 1 |
|
|
62
|
+
| Plugin | Skills | Notes |
|
|
63
|
+
|--------|:------:|-------|
|
|
64
|
+
| omc | 36 | Core plugin. Keep. |
|
|
65
|
+
| temp_local_... | 1 | **Failed install remnant. Cleanup target.** |
|
|
64
66
|
|
|
65
67
|
Annotate each with status: actively used, possibly unused, or cleanup target. Flag `temp_local_*` entries as failed install remnants.
|
|
66
68
|
|
|
@@ -68,18 +70,18 @@ Annotate each with status: actively used, possibly unused, or cleanup target. Fl
|
|
|
68
70
|
|
|
69
71
|
Group issues by tier and explain EACH one with context and recommendation:
|
|
70
72
|
|
|
71
|
-
**Tier 1 —
|
|
73
|
+
**Tier 1 — Immediate cleanup (zero risk):**
|
|
72
74
|
These are safe to remove with zero risk: broken symlinks, empty templates, .skill/ duplicates, temp_local_* cache. Pre-selected. Explain why each is safe.
|
|
73
75
|
|
|
74
|
-
**Tier 2 —
|
|
76
|
+
**Tier 2 — Recommended cleanup:**
|
|
75
77
|
These are recommended but need user judgment. For each issue, explain:
|
|
76
78
|
- What is it and why it's flagged
|
|
77
79
|
- What happens if you remove it (safe? any side effects?)
|
|
78
80
|
- How many tokens it saves
|
|
79
81
|
|
|
80
|
-
Example: "frontend-design
|
|
82
|
+
Example: "frontend-design exists both locally and in a plugin. Removing the local copy is safe because the plugin version remains. Saves ~823 tok."
|
|
81
83
|
|
|
82
|
-
**Tier 3 —
|
|
84
|
+
**Tier 3 — Optional (user judgment):**
|
|
83
85
|
These are large skills that cost tokens but might be in active use. For each:
|
|
84
86
|
- Show size and token cost
|
|
85
87
|
- Judge whether the user likely uses it (based on what it does)
|
|
@@ -94,7 +96,7 @@ End with a numbered action list, ordered by impact:
|
|
|
94
96
|
|
|
95
97
|
Show estimated total token savings if all recommended actions are taken.
|
|
96
98
|
|
|
97
|
-
If subcommand is `scan`, stop here. Ask "
|
|
99
|
+
If subcommand is `scan`, stop here. Ask a localized equivalent of "Proceed with cleanup?" only for the full pipeline.
|
|
98
100
|
|
|
99
101
|
---
|
|
100
102
|
|
|
@@ -133,7 +135,7 @@ cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js restore
|
|
|
133
135
|
|
|
134
136
|
## Language
|
|
135
137
|
|
|
136
|
-
Detect the user's language from their most recent message. Present all reports, analysis, and explanations in that language. The CLI output is machine-readable
|
|
138
|
+
Detect the user's language from their most recent message. Present all reports, analysis, and explanations in that language — including table headers, tier labels, prompts, and every user-facing string. The CLI output is machine-readable (always English) and must not be echoed verbatim; translate its content into the user's language when you interpret it. The example tables above are written in English only for authoring clarity — do not treat them as a required output format.
|
|
137
139
|
|
|
138
140
|
## Rules
|
|
139
141
|
|