claude-slim 2.2.2 → 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/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
|