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 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
- await rm(issue.path, { recursive: true, force: true });
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: `${localAfter - localBefore}`,
31
+ saved: `${localBefore - localAfter}`,
32
32
  },
33
33
  {
34
34
  label: 'System prompt',
35
35
  before: `~${promptBefore}`,
36
36
  after: `~${promptAfter}`,
37
- saved: `${promptAfter - promptBefore}`,
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: `${((memAfter - memBefore) / 1024).toFixed(1)}KB`,
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: `~${(after - before).toLocaleString()}`,
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
- async function runCommand(cmd) {
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 { exec } = await import('node:child_process');
54
+ const { execFile } = await import('node:child_process');
53
55
  return new Promise((resolve) => {
54
- exec(cmd, { timeout: 10000 }, (_err, stdout) => {
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 { homedir } from 'node:os';
4
+ import { getClaudeDir } from './paths.js';
5
5
  let encoder = null;
6
6
  let useFallback = false;
7
- const CACHE_PATH = join(homedir(), '.claude', '.token-cache.json');
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(CACHE_PATH, 'utf-8');
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(CACHE_PATH), { recursive: true });
52
- await writeFile(CACHE_PATH, JSON.stringify(cache, null, 2));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slim",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "Analyze and reduce Claude Code token overhead",
5
5
  "type": "module",
6
6
  "bin": {