claude-slim 2.10.0 → 2.11.0
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 +8 -10
- package/dist/cleaner.js +18 -8
- package/dist/cli.js +12 -1
- package/dist/codex/detectors.d.ts +26 -0
- package/dist/codex/detectors.js +110 -0
- package/dist/codex/index.d.ts +6 -1
- package/dist/codex/index.js +9 -3
- package/dist/manifest.d.ts +3 -1
- package/dist/manifest.js +6 -3
- package/dist/paths.d.ts +19 -0
- package/dist/paths.js +39 -7
- package/dist/report.js +4 -1
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ claude-slim never updates itself — that's your package manager's job, and writ
|
|
|
202
202
|
- **`~/.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.
|
|
203
203
|
- **Plugin internals** (`~/.claude/plugins/config.json`, individual `plugin.json` files) — left alone; use `claude plugin` to manage plugins.
|
|
204
204
|
- **Git / project sources** — claude-slim only looks inside `~/.claude/`, never at your code.
|
|
205
|
-
- **`~/.codex/`** — scanned and
|
|
205
|
+
- **`~/.codex/`** — scanned and, since v2.11, cleanable under the same tiers. Moves go to `~/.codex/skills.disabled/` and reverse with `restore`. The path guard is per-agent, so a Codex item can never resolve into `~/.claude/`. Unused-skill detection is still not offered there: Codex session logs record the skill catalog, not invocations, so there is no honest usage signal to act on.
|
|
206
206
|
- **Anything outside `~/.claude/`** — a path-containment guard refuses destructive ops anywhere else, even if a tampered manifest asked it to.
|
|
207
207
|
|
|
208
208
|
Only touched: entries under `~/.claude/skills/`, `~/.claude/plugins/cache/temp_local_*`, and `~/.claude/projects/*/memory/`. Skill and memory entries are moved to `skills.disabled/`; broken symlink files are unlinked and `temp_local_*` failed-install caches are removed outright.
|
|
@@ -247,18 +247,16 @@ Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktok
|
|
|
247
247
|
|
|
248
248
|
---
|
|
249
249
|
|
|
250
|
-
## v2.
|
|
250
|
+
## v2.11.0 — What's new
|
|
251
251
|
|
|
252
|
-
Codex
|
|
252
|
+
Codex gets the same tiered propose-and-choose cleanup Claude Code has, minus the two categories Codex cannot honestly support.
|
|
253
253
|
|
|
254
|
-
-
|
|
255
|
-
-
|
|
256
|
-
- **
|
|
257
|
-
-
|
|
254
|
+
- **Codex cleanup, tiered.** Five of seven categories carry over because they are filesystem facts, not usage inferences: broken symlinks and unfilled templates (Tier 1), install leftovers in `~/.codex/.tmp` (Tier 1 — 146MB on the dev machine), backup copies and plugin-shadowed duplicates (Tier 2), oversized skills (Tier 3). Codex items join the same numbered list tagged `[codex]`. `--no-codex` opts out.
|
|
255
|
+
- **`~/.codex/skills.disabled/`** — moves are reversible through the same `restore`, and manifest entries record which agent they came from.
|
|
256
|
+
- **The path guard is now per-agent.** Not "inside any known root" — a Codex issue must not resolve into `~/.claude/` and vice versa, so a tampered manifest cannot cross between agents.
|
|
257
|
+
- `unused_skill` and `oversized_memory` remain unavailable for Codex: no invocation record, and no `~/.codex/projects/*/memory/`.
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Tests: 279 → 347 (+68).
|
|
259
|
+
Tests: 347 → 374 (+27), the guard suite being the point — cross-agent isolation both directions, traversal escapes, and a `~/.claude-backup` sibling a naive `startsWith` would have allowed.
|
|
262
260
|
|
|
263
261
|
For older release notes, see [CHANGELOG.md](CHANGELOG.md).
|
|
264
262
|
|
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, resolve, sep } from 'node:path';
|
|
3
|
-
import { appendManifest, ensureDisabledDir,
|
|
4
|
-
import {
|
|
3
|
+
import { appendManifest, ensureDisabledDir, removeEntry, recordDisabledPlugin, removeDisabledPlugin } from './manifest.js';
|
|
4
|
+
import { assertInsideAgentRoot, getAgentRoot, getAgentDisabledDir, getSkillsDir, getProjectsDir } from './paths.js';
|
|
5
5
|
import { disablePlugin, enablePlugin, isClaudeCliAvailable, ClaudeCliMissingError } from './plugin-runtime.js';
|
|
6
6
|
// Restrict a restore target to a specific subtree of ~/.claude/. Complements
|
|
7
7
|
// assertInsideClaudeDir: a tampered manifest could still name a legal
|
|
@@ -40,8 +40,6 @@ async function recordOrRollback(entry, rollback) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
export async function cleanIssues(issues) {
|
|
43
|
-
await ensureDisabledDir();
|
|
44
|
-
const disabledDir = getDisabledDir();
|
|
45
43
|
const moved = [];
|
|
46
44
|
const skipped = [];
|
|
47
45
|
const errors = [];
|
|
@@ -54,15 +52,20 @@ export async function cleanIssues(issues) {
|
|
|
54
52
|
claudeCliMissing = true;
|
|
55
53
|
}
|
|
56
54
|
for (const issue of issues) {
|
|
55
|
+
// Scope every path decision to the issue's own agent, so a Codex issue can
|
|
56
|
+
// never resolve into ~/.claude/ (or the reverse).
|
|
57
|
+
const agent = issue.agent ?? 'claude';
|
|
58
|
+
const disabledDir = await ensureDisabledDir(agent);
|
|
57
59
|
try {
|
|
58
60
|
// unused_plugin and report-only types don't touch filesystem paths directly
|
|
59
61
|
if (issue.type !== 'unused_plugin' && issue.type !== 'oversized_memory' && issue.type !== 'disabled_plugin') {
|
|
60
|
-
|
|
62
|
+
assertInsideAgentRoot(issue.path, agent);
|
|
61
63
|
}
|
|
62
64
|
if (issue.type === 'broken_symlink') {
|
|
63
65
|
await unlink(issue.path);
|
|
64
66
|
const entry = {
|
|
65
67
|
date: new Date().toISOString(),
|
|
68
|
+
agent,
|
|
66
69
|
name: issue.name,
|
|
67
70
|
from: issue.path,
|
|
68
71
|
type: issue.type,
|
|
@@ -85,6 +88,7 @@ export async function cleanIssues(issues) {
|
|
|
85
88
|
await rename(issue.path, dest);
|
|
86
89
|
const entry = {
|
|
87
90
|
date: new Date().toISOString(),
|
|
91
|
+
agent,
|
|
88
92
|
name: issue.name,
|
|
89
93
|
from: issue.path,
|
|
90
94
|
type: issue.type,
|
|
@@ -108,6 +112,7 @@ export async function cleanIssues(issues) {
|
|
|
108
112
|
}
|
|
109
113
|
const entry = {
|
|
110
114
|
date: new Date().toISOString(),
|
|
115
|
+
agent,
|
|
111
116
|
name: issue.name,
|
|
112
117
|
from: issue.path,
|
|
113
118
|
type: issue.type,
|
|
@@ -131,6 +136,7 @@ export async function cleanIssues(issues) {
|
|
|
131
136
|
await rename(issue.path, backupDir);
|
|
132
137
|
const entry = {
|
|
133
138
|
date: new Date().toISOString(),
|
|
139
|
+
agent,
|
|
134
140
|
name: issue.name,
|
|
135
141
|
from: issue.path,
|
|
136
142
|
type: issue.type,
|
|
@@ -210,7 +216,7 @@ export async function restoreItem(entry) {
|
|
|
210
216
|
return;
|
|
211
217
|
}
|
|
212
218
|
const legacyEntry = entry;
|
|
213
|
-
|
|
219
|
+
assertInsideAgentRoot(legacyEntry.from, legacyEntry.agent ?? 'claude');
|
|
214
220
|
if (legacyEntry.type === 'broken_symlink') {
|
|
215
221
|
throw new Error(`Broken symlinks cannot be restored (${legacyEntry.name})`);
|
|
216
222
|
}
|
|
@@ -220,7 +226,9 @@ export async function restoreItem(entry) {
|
|
|
220
226
|
if (legacyEntry.type === 'temp_cache') {
|
|
221
227
|
throw new Error(`Temp caches were deleted and cannot be restored (${legacyEntry.name})`);
|
|
222
228
|
}
|
|
223
|
-
|
|
229
|
+
// Resolve the store from the entry's own agent — a Codex skill was parked
|
|
230
|
+
// under ~/.codex/skills.disabled and must be looked for there.
|
|
231
|
+
const disabledDir = getAgentDisabledDir(('agent' in entry ? entry.agent : undefined) ?? 'claude');
|
|
224
232
|
if (legacyEntry.type === 'stale_project') {
|
|
225
233
|
// Type-scoped path guard: stale-project backups must restore under
|
|
226
234
|
// ~/.claude/projects/. Prevents a tampered manifest from redirecting a
|
|
@@ -239,7 +247,9 @@ export async function restoreItem(entry) {
|
|
|
239
247
|
}
|
|
240
248
|
else {
|
|
241
249
|
// Type-scoped path guard: skill restores must land under ~/.claude/skills/.
|
|
242
|
-
|
|
250
|
+
// Per-agent skills/ subtree: a Codex entry restores under ~/.codex/skills,
|
|
251
|
+
// never ~/.claude/skills.
|
|
252
|
+
assertInsideSubtree(legacyEntry.from, join(getAgentRoot(legacyEntry.agent ?? 'claude'), 'skills'), 'skill');
|
|
243
253
|
// Restore skill directory using the same naming as cleanIssues
|
|
244
254
|
const safeName = legacyEntry.name.replace(/\//g, '--');
|
|
245
255
|
const src = join(disabledDir, safeName);
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { collectDoctorReport, formatDoctorReport } from './doctor.js';
|
|
|
13
13
|
import { checkForUpdate, formatUpdateNotice } from './update-check.js';
|
|
14
14
|
import { scanCodex } from './codex/index.js';
|
|
15
15
|
import { formatCodexSummary } from './codex/report.js';
|
|
16
|
+
import { classifyCodexIssues } from './codex/detectors.js';
|
|
16
17
|
import { resolveSelection, resolveRestoreSelection } from './selection.js';
|
|
17
18
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
18
19
|
const { version: PKG_VERSION } = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
@@ -108,12 +109,14 @@ program
|
|
|
108
109
|
.option('--auto', 'Non-interactive: auto-select Tier 1 items only')
|
|
109
110
|
.option('--sessions-per-day <n>', 'Sessions per day for savings estimate', '2')
|
|
110
111
|
.option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
|
|
112
|
+
.option('--no-codex', 'Skip ~/.codex entirely')
|
|
111
113
|
.action(async (opts) => {
|
|
112
114
|
await runCleanPipeline({
|
|
113
115
|
dryRun: !!opts.dryRun,
|
|
114
116
|
auto: !!opts.auto,
|
|
115
117
|
sessionsPerDay: parseNonNegativeInt(opts.sessionsPerDay, 2),
|
|
116
118
|
lookbackDays: parseNonNegativeInt(opts.lookbackDays, 60),
|
|
119
|
+
codex: opts.codex !== false,
|
|
117
120
|
});
|
|
118
121
|
});
|
|
119
122
|
// --- restore ---
|
|
@@ -269,12 +272,20 @@ program.action(async () => {
|
|
|
269
272
|
process.exitCode = 1;
|
|
270
273
|
return;
|
|
271
274
|
}
|
|
272
|
-
await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60 });
|
|
275
|
+
await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60, codex: true });
|
|
273
276
|
});
|
|
274
277
|
// --- shared clean pipeline ---
|
|
275
278
|
async function runCleanPipeline(opts) {
|
|
276
279
|
await initTokenizer();
|
|
277
280
|
const result = await scan({ lookbackDays: opts.lookbackDays });
|
|
281
|
+
// Codex issues join the same tiered list. They carry `agent: 'codex'`, which
|
|
282
|
+
// is what keeps the cleaner's path guard pointed at ~/.codex/.
|
|
283
|
+
const codexContents = new Map();
|
|
284
|
+
const codexScan = opts.codex ? await scanCodex(codexContents) : null;
|
|
285
|
+
if (codexScan) {
|
|
286
|
+
result.issues.push(...(await classifyCodexIssues({ scan: codexScan, contents: codexContents })));
|
|
287
|
+
result.issues.sort((a, b) => a.tier - b.tier || b.tokens - a.tokens);
|
|
288
|
+
}
|
|
278
289
|
if (result.issues.length === 0) {
|
|
279
290
|
console.log('\n \x1b[32mAlready slim!\x1b[0m No issues found.\n');
|
|
280
291
|
await flushCache();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Issue } from '../types.js';
|
|
2
|
+
import type { CodexScanResult } from './index.js';
|
|
3
|
+
export interface CodexDetectorContext {
|
|
4
|
+
scan: CodexScanResult;
|
|
5
|
+
/** SKILL.md contents keyed by absolute path, so detectors need not re-read. */
|
|
6
|
+
contents: Map<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/** Tier 1 — a SKILL.md symlink whose target is gone. Contributes nothing. */
|
|
9
|
+
export declare function detectBrokenSymlinks(ctx: CodexDetectorContext): Promise<Issue[]>;
|
|
10
|
+
/** Tier 1 — a scaffold nobody filled in. */
|
|
11
|
+
export declare function detectTemplates(ctx: CodexDetectorContext): Issue[];
|
|
12
|
+
/**
|
|
13
|
+
* Tier 1 — `~/.codex/.tmp`, where interrupted plugin installs accumulate.
|
|
14
|
+
* Sized rather than token-counted: this is disk, not context.
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectInstallLeftovers(): Promise<Issue[]>;
|
|
17
|
+
/** Tier 2 — a leftover copy, recognised by name shape alone. */
|
|
18
|
+
export declare function detectBackups(ctx: CodexDetectorContext): Issue[];
|
|
19
|
+
/**
|
|
20
|
+
* Tier 2 — a local skill shadowed by a plugin-provided one of the same name.
|
|
21
|
+
* Removing the local copy leaves the plugin version in place.
|
|
22
|
+
*/
|
|
23
|
+
export declare function detectDuplicates(ctx: CodexDetectorContext): Issue[];
|
|
24
|
+
/** Tier 3 — large enough to be worth a look, but possibly still in use. */
|
|
25
|
+
export declare function detectOversized(ctx: CodexDetectorContext): Issue[];
|
|
26
|
+
export declare function classifyCodexIssues(ctx: CodexDetectorContext): Promise<Issue[]>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { getCodexDir } from '../paths.js';
|
|
3
|
+
import { OVERSIZED_SKILL_BYTES } from '../scanner/constants.js';
|
|
4
|
+
import { isDirectory, safeReaddir, getDirSize, isBrokenSymlink } from '../scanner/fs-walk.js';
|
|
5
|
+
// Codex cleanup candidates, in the same three tiers the Claude path uses.
|
|
6
|
+
//
|
|
7
|
+
// Five of Claude's seven categories carry over unchanged, because they are
|
|
8
|
+
// filesystem facts rather than usage inferences:
|
|
9
|
+
//
|
|
10
|
+
// broken symlinks · empty templates · duplicates · oversized · install leftovers
|
|
11
|
+
//
|
|
12
|
+
// The two that do not:
|
|
13
|
+
// unused_skill — no invocation record exists in Codex session logs
|
|
14
|
+
// oversized_memory— Codex has no ~/.codex/projects/*/memory/ equivalent
|
|
15
|
+
//
|
|
16
|
+
// Every issue produced here carries `agent: 'codex'`, which is what keeps the
|
|
17
|
+
// cleaner's path guard scoped to ~/.codex/ and unable to reach ~/.claude/.
|
|
18
|
+
const TEMPLATE_MARKER = 'Replace with description';
|
|
19
|
+
/** MB once it is worth calling MB; KB below that, so a 3KB leftover is not "0MB". */
|
|
20
|
+
function formatBytes(bytes) {
|
|
21
|
+
const mb = bytes / 1024 / 1024;
|
|
22
|
+
return mb >= 1 ? `${mb.toFixed(0)}MB` : `${Math.max(1, Math.round(bytes / 1024))}KB`;
|
|
23
|
+
}
|
|
24
|
+
function issue(type, tier, skill, detail) {
|
|
25
|
+
return { type, tier, agent: 'codex', name: skill.name, path: skill.path, tokens: skill.tokens, detail };
|
|
26
|
+
}
|
|
27
|
+
/** Tier 1 — a SKILL.md symlink whose target is gone. Contributes nothing. */
|
|
28
|
+
export async function detectBrokenSymlinks(ctx) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const skillsDir = join(getCodexDir(), 'skills');
|
|
31
|
+
for (const entry of await safeReaddir(skillsDir)) {
|
|
32
|
+
if (entry.startsWith('.'))
|
|
33
|
+
continue;
|
|
34
|
+
const dir = join(skillsDir, entry);
|
|
35
|
+
if (!(await isDirectory(dir)))
|
|
36
|
+
continue;
|
|
37
|
+
const md = join(dir, 'SKILL.md');
|
|
38
|
+
if (await isBrokenSymlink(md)) {
|
|
39
|
+
out.push({
|
|
40
|
+
type: 'broken_symlink', tier: 1, agent: 'codex',
|
|
41
|
+
name: entry, path: md, tokens: 0, detail: 'dead symlink',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Tier 1 — a scaffold nobody filled in. */
|
|
48
|
+
export function detectTemplates(ctx) {
|
|
49
|
+
return ctx.scan.skills
|
|
50
|
+
.filter((s) => s.source === 'local')
|
|
51
|
+
.filter((s) => ctx.contents.get(join(s.path, 'SKILL.md'))?.includes(TEMPLATE_MARKER))
|
|
52
|
+
.map((s) => issue('template', 1, s, 'unfilled template'));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Tier 1 — `~/.codex/.tmp`, where interrupted plugin installs accumulate.
|
|
56
|
+
* Sized rather than token-counted: this is disk, not context.
|
|
57
|
+
*/
|
|
58
|
+
export async function detectInstallLeftovers() {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const name of ['.tmp', '.remote-plugin-install-staging']) {
|
|
61
|
+
const path = join(getCodexDir(), name);
|
|
62
|
+
if (!(await isDirectory(path)))
|
|
63
|
+
continue;
|
|
64
|
+
const bytes = await getDirSize(path);
|
|
65
|
+
if (bytes === 0)
|
|
66
|
+
continue;
|
|
67
|
+
out.push({
|
|
68
|
+
type: 'temp_cache', tier: 1, agent: 'codex',
|
|
69
|
+
name, path, tokens: 0,
|
|
70
|
+
detail: `${formatBytes(bytes)} of install leftovers`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Tier 2 — a leftover copy, recognised by name shape alone. */
|
|
76
|
+
export function detectBackups(ctx) {
|
|
77
|
+
return ctx.scan.skills
|
|
78
|
+
.filter((s) => s.source === 'local' && s.backupArtifact)
|
|
79
|
+
.map((s) => issue('backup_artifact', 2, s, `looks like a backup copy (${s.backupArtifact})`));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Tier 2 — a local skill shadowed by a plugin-provided one of the same name.
|
|
83
|
+
* Removing the local copy leaves the plugin version in place.
|
|
84
|
+
*/
|
|
85
|
+
export function detectDuplicates(ctx) {
|
|
86
|
+
const fromPlugins = new Set(ctx.scan.skills.filter((s) => s.source === 'plugin').map((s) => s.name));
|
|
87
|
+
return ctx.scan.skills
|
|
88
|
+
.filter((s) => s.source === 'local' && fromPlugins.has(s.name))
|
|
89
|
+
.map((s) => issue('duplicate', 2, s, 'also provided by a plugin'));
|
|
90
|
+
}
|
|
91
|
+
/** Tier 3 — large enough to be worth a look, but possibly still in use. */
|
|
92
|
+
export function detectOversized(ctx) {
|
|
93
|
+
return ctx.scan.skills
|
|
94
|
+
.filter((s) => s.source === 'local' && s.sizeBytes > OVERSIZED_SKILL_BYTES)
|
|
95
|
+
.map((s) => issue('oversized_skill', 3, s, `${Math.round(s.sizeBytes / 1024)}KB`));
|
|
96
|
+
}
|
|
97
|
+
export async function classifyCodexIssues(ctx) {
|
|
98
|
+
const [symlinks, leftovers] = await Promise.all([
|
|
99
|
+
detectBrokenSymlinks(ctx),
|
|
100
|
+
detectInstallLeftovers(),
|
|
101
|
+
]);
|
|
102
|
+
return [
|
|
103
|
+
...symlinks,
|
|
104
|
+
...detectTemplates(ctx),
|
|
105
|
+
...leftovers,
|
|
106
|
+
...detectBackups(ctx),
|
|
107
|
+
...detectDuplicates(ctx),
|
|
108
|
+
...detectOversized(ctx),
|
|
109
|
+
].sort((a, b) => a.tier - b.tier || b.tokens - a.tokens);
|
|
110
|
+
}
|
package/dist/codex/index.d.ts
CHANGED
|
@@ -41,6 +41,11 @@ export declare function isCodexInstalled(): Promise<boolean>;
|
|
|
41
41
|
* back to the flat estimate.
|
|
42
42
|
*/
|
|
43
43
|
export declare function parseTomlDescription(content: string): string | null;
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* @param contents optional sink for SKILL.md bodies, so detectors can inspect
|
|
46
|
+
* them without a second pass over the filesystem. Deliberately an out-param
|
|
47
|
+
* rather than part of the result: it must not land in `scan --json`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function scanCodex(contents?: Map<string, string>): Promise<CodexScanResult | null>;
|
|
45
50
|
/** Re-exported so callers can reuse the frontmatter parser without a deep import. */
|
|
46
51
|
export { parseFrontmatterDescription };
|
package/dist/codex/index.js
CHANGED
|
@@ -60,7 +60,7 @@ export function parseTomlDescription(content) {
|
|
|
60
60
|
const unescaped = single[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\').trim();
|
|
61
61
|
return unescaped || null;
|
|
62
62
|
}
|
|
63
|
-
async function scanLocalSkills() {
|
|
63
|
+
async function scanLocalSkills(contents) {
|
|
64
64
|
const dir = join(getCodexDir(), 'skills');
|
|
65
65
|
const entries = await safeReaddir(dir);
|
|
66
66
|
const results = [];
|
|
@@ -77,6 +77,7 @@ async function scanLocalSkills() {
|
|
|
77
77
|
const content = await safeReadFile(md);
|
|
78
78
|
if (content === null)
|
|
79
79
|
continue;
|
|
80
|
+
contents?.set(md, content);
|
|
80
81
|
results.push({
|
|
81
82
|
name: entry,
|
|
82
83
|
path: skillDir,
|
|
@@ -161,11 +162,16 @@ async function scanAgents() {
|
|
|
161
162
|
}
|
|
162
163
|
return results;
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
+
/**
|
|
166
|
+
* @param contents optional sink for SKILL.md bodies, so detectors can inspect
|
|
167
|
+
* them without a second pass over the filesystem. Deliberately an out-param
|
|
168
|
+
* rather than part of the result: it must not land in `scan --json`.
|
|
169
|
+
*/
|
|
170
|
+
export async function scanCodex(contents) {
|
|
165
171
|
if (!(await isCodexInstalled()))
|
|
166
172
|
return null;
|
|
167
173
|
const [local, plugin, agents] = await Promise.all([
|
|
168
|
-
scanLocalSkills(),
|
|
174
|
+
scanLocalSkills(contents),
|
|
169
175
|
scanPluginSkills(),
|
|
170
176
|
scanAgents(),
|
|
171
177
|
]);
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Manifest, ManifestEntry, AnyManifestEntry, DisabledPluginEntry } from './types.js';
|
|
2
|
+
import type { AgentId } from './paths.js';
|
|
2
3
|
export declare function getDisabledDir(): string;
|
|
3
|
-
|
|
4
|
+
/** Create (if needed) and return the disabled-skill store for one agent. */
|
|
5
|
+
export declare function ensureDisabledDir(agent?: AgentId): Promise<string>;
|
|
4
6
|
export declare function migrateLegacyIfNeeded(): Promise<void>;
|
|
5
7
|
export declare function readManifestV2(): Promise<Manifest>;
|
|
6
8
|
export declare function writeManifestV2(manifest: Manifest): Promise<void>;
|
package/dist/manifest.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, rename, access } from 'node:fs/promises';
|
|
2
|
-
import { getDisabledDir as getDir, getManifestPath, getLegacyManifestPath, } from './paths.js';
|
|
2
|
+
import { getDisabledDir as getDir, getAgentDisabledDir, getManifestPath, getLegacyManifestPath, } from './paths.js';
|
|
3
3
|
export function getDisabledDir() {
|
|
4
4
|
return getDir();
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/** Create (if needed) and return the disabled-skill store for one agent. */
|
|
7
|
+
export async function ensureDisabledDir(agent = 'claude') {
|
|
8
|
+
const dir = getAgentDisabledDir(agent);
|
|
9
|
+
await mkdir(dir, { recursive: true });
|
|
10
|
+
return dir;
|
|
8
11
|
}
|
|
9
12
|
async function pathExists(p) {
|
|
10
13
|
try {
|
package/dist/paths.d.ts
CHANGED
|
@@ -14,4 +14,23 @@ export declare function getDisabledDir(): string;
|
|
|
14
14
|
export declare function getCurrentProjectSlug(cwd?: string): string;
|
|
15
15
|
export declare function getManifestPath(): string;
|
|
16
16
|
export declare function getLegacyManifestPath(): string;
|
|
17
|
+
/** The agents claude-slim is allowed to touch. Adding one widens what every
|
|
18
|
+
* destructive operation may reach, so this list is the security boundary. */
|
|
19
|
+
export type AgentId = 'claude' | 'codex';
|
|
20
|
+
export declare function getCodexDir(): string;
|
|
21
|
+
export declare function getAgentRoot(agent: AgentId): string;
|
|
22
|
+
export declare function getAgentDisabledDir(agent: AgentId): string;
|
|
23
|
+
/**
|
|
24
|
+
* Refuse to operate on a path outside the given agent's root. Guards destructive
|
|
25
|
+
* operations (rename/rm/unlink) against tampered manifests and scanner bugs.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately per-agent rather than "inside any known root": a Codex issue must
|
|
28
|
+
* not be able to reach into ~/.claude/ and vice versa. Widening this to a single
|
|
29
|
+
* combined check would let one bad manifest entry cross between agents, which is
|
|
30
|
+
* exactly the failure this exists to prevent.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertInsideAgentRoot(p: string, agent: AgentId): void;
|
|
33
|
+
/** Back-compat wrapper — the Claude path is by far the most common caller. */
|
|
17
34
|
export declare function assertInsideClaudeDir(p: string): void;
|
|
35
|
+
/** Which agent owns this path, or null if it belongs to neither. */
|
|
36
|
+
export declare function agentForPath(p: string): AgentId | null;
|
package/dist/paths.js
CHANGED
|
@@ -32,12 +32,44 @@ export function getManifestPath() {
|
|
|
32
32
|
export function getLegacyManifestPath() {
|
|
33
33
|
return join(getDisabledDir(), '.claude-slim-manifest.jsonl');
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
export function getCodexDir() {
|
|
36
|
+
return join(homedir(), '.codex');
|
|
37
|
+
}
|
|
38
|
+
export function getAgentRoot(agent) {
|
|
39
|
+
return agent === 'claude' ? getClaudeDir() : getCodexDir();
|
|
40
|
+
}
|
|
41
|
+
export function getAgentDisabledDir(agent) {
|
|
42
|
+
return join(getAgentRoot(agent), 'skills.disabled');
|
|
43
|
+
}
|
|
44
|
+
function isInside(child, root) {
|
|
45
|
+
const c = resolve(child);
|
|
46
|
+
const r = resolve(root);
|
|
47
|
+
return c === r || c.startsWith(r + sep);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Refuse to operate on a path outside the given agent's root. Guards destructive
|
|
51
|
+
* operations (rename/rm/unlink) against tampered manifests and scanner bugs.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately per-agent rather than "inside any known root": a Codex issue must
|
|
54
|
+
* not be able to reach into ~/.claude/ and vice versa. Widening this to a single
|
|
55
|
+
* combined check would let one bad manifest entry cross between agents, which is
|
|
56
|
+
* exactly the failure this exists to prevent.
|
|
57
|
+
*/
|
|
58
|
+
export function assertInsideAgentRoot(p, agent) {
|
|
59
|
+
if (!isInside(p, getAgentRoot(agent))) {
|
|
60
|
+
const label = agent === 'claude' ? '~/.claude/' : '~/.codex/';
|
|
61
|
+
throw new Error(`Refusing to operate on path outside ${label}: ${p}`);
|
|
42
62
|
}
|
|
43
63
|
}
|
|
64
|
+
/** Back-compat wrapper — the Claude path is by far the most common caller. */
|
|
65
|
+
export function assertInsideClaudeDir(p) {
|
|
66
|
+
assertInsideAgentRoot(p, 'claude');
|
|
67
|
+
}
|
|
68
|
+
/** Which agent owns this path, or null if it belongs to neither. */
|
|
69
|
+
export function agentForPath(p) {
|
|
70
|
+
if (isInside(p, getClaudeDir()))
|
|
71
|
+
return 'claude';
|
|
72
|
+
if (isInside(p, getCodexDir()))
|
|
73
|
+
return 'codex';
|
|
74
|
+
return null;
|
|
75
|
+
}
|
package/dist/report.js
CHANGED
|
@@ -261,7 +261,10 @@ export function formatScanSummary(result) {
|
|
|
261
261
|
const detail = issue.detail ? ` (${issue.detail})` : '';
|
|
262
262
|
const tokStr = issue.tokens > 0 ? ` ~${issue.tokens.toLocaleString()} tok` : '';
|
|
263
263
|
const permanent = permanentTypes.has(issue.type) ? ' \x1b[31m(permanent)\x1b[0m' : '';
|
|
264
|
-
|
|
264
|
+
// Tag the agent when it is not Claude Code, so a ~/.codex item is never
|
|
265
|
+
// mistaken for one under ~/.claude.
|
|
266
|
+
const agentTag = issue.agent && issue.agent !== 'claude' ? ` \x1b[36m[${issue.agent}]\x1b[0m` : '';
|
|
267
|
+
lines.push(` ${selected} ${i + 1}. \x1b[${color}m[${tierLabel}]\x1b[0m${agentTag} ${issue.type}: ${issue.name}${detail}${tokStr}${permanent}`);
|
|
265
268
|
}
|
|
266
269
|
}
|
|
267
270
|
// --- PLUGINS BREAKDOWN ---
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { UserSurfaceEntry } from './scanner/user-surfaces.js';
|
|
2
|
-
|
|
2
|
+
import type { AgentId } from './paths.js';
|
|
3
|
+
export type { UserSurfaceEntry, AgentId };
|
|
3
4
|
export interface SkillInfo {
|
|
4
5
|
name: string;
|
|
5
6
|
path: string;
|
|
@@ -37,6 +38,8 @@ export type IssueTier = 1 | 2 | 3;
|
|
|
37
38
|
export type IssueType = 'broken_symlink' | 'template' | 'skill_dup' | 'duplicate' | 'oversized_memory' | 'oversized_skill' | 'unused_skill' | 'unused_plugin' | 'disabled_plugin' | 'stale_project' | 'temp_cache' | 'backup_artifact';
|
|
38
39
|
export interface Issue {
|
|
39
40
|
type: IssueType;
|
|
41
|
+
/** Which agent root this issue lives under. Absent means Claude Code. */
|
|
42
|
+
agent?: AgentId;
|
|
40
43
|
tier: IssueTier;
|
|
41
44
|
name: string;
|
|
42
45
|
detail?: string;
|
|
@@ -85,6 +88,8 @@ export interface ScanResult {
|
|
|
85
88
|
}
|
|
86
89
|
export interface ManifestEntry {
|
|
87
90
|
date: string;
|
|
91
|
+
/** Which agent root `from` belongs to. Absent means Claude Code (pre-2.11 entries). */
|
|
92
|
+
agent?: AgentId;
|
|
88
93
|
name: string;
|
|
89
94
|
from: string;
|
|
90
95
|
type: IssueType;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slim",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
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": {
|