claude-slim 2.10.0 → 2.12.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 +6 -11
- package/dist/cleaner.js +18 -8
- package/dist/cli.js +88 -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/dist/update-run.d.ts +40 -0
- package/dist/update-run.js +101 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,6 +165,7 @@ npx claude-slim scan --no-codex # Skip the ~/.codex scan
|
|
|
165
165
|
npx claude-slim doctor # Diagnose Node/Claude/session-log readiness
|
|
166
166
|
npx claude-slim doctor --offline # Same, without the version check (no network)
|
|
167
167
|
npx claude-slim check-update # Report-only version check
|
|
168
|
+
npx claude-slim update # Run the upgrade for this install method
|
|
168
169
|
npx claude-slim restore # Undo
|
|
169
170
|
npx claude-slim report # Show savings from last clean
|
|
170
171
|
```
|
|
@@ -202,7 +203,7 @@ claude-slim never updates itself — that's your package manager's job, and writ
|
|
|
202
203
|
- **`~/.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
204
|
- **Plugin internals** (`~/.claude/plugins/config.json`, individual `plugin.json` files) — left alone; use `claude plugin` to manage plugins.
|
|
204
205
|
- **Git / project sources** — claude-slim only looks inside `~/.claude/`, never at your code.
|
|
205
|
-
- **`~/.codex/`** — scanned and
|
|
206
|
+
- **`~/.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
207
|
- **Anything outside `~/.claude/`** — a path-containment guard refuses destructive ops anywhere else, even if a tampered manifest asked it to.
|
|
207
208
|
|
|
208
209
|
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 +248,12 @@ Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktok
|
|
|
247
248
|
|
|
248
249
|
---
|
|
249
250
|
|
|
250
|
-
## v2.
|
|
251
|
+
## v2.12.0 — What's new
|
|
251
252
|
|
|
252
|
-
|
|
253
|
+
- **`claude-slim update`** — runs the upgrade command for however this copy was installed, after showing it and asking. `--dry-run` to preview, `--yes` to skip the prompt. Plugin installs get the marketplace refresh first, then the qualified `claude-slim@claude-slim` id, and are reminded that a restart is needed. npx and source checkouts run nothing and say why — npx already resolves the latest every invocation, and pulling your own repository is not claude-slim's to do.
|
|
254
|
+
- **This corrects v2.9.0's reasoning.** That release stopped at detection, arguing updating belonged to the package manager. Half held: claude-slim must not write into a directory `claude plugin` owns. The other half did not — invoking the package manager is something this tool already does during cleanup (`claude plugin disable`), so refusing to here was inconsistent. It still writes nothing itself.
|
|
253
255
|
|
|
254
|
-
|
|
255
|
-
- Codex `SKILL.md` frontmatter matches Claude Code's exactly, so the existing listing parser is reused unchanged. Agents differ (`<name>.toml` with `description = "…"`) and get a small parser, verified against all 18 installed agents.
|
|
256
|
-
- **Unused-skill detection is not offered for Codex, and `scan` says so.** Codex session logs record the skill *catalog* injected into each prompt, not invocations — every skill shows up in nearly every session, so using them as a usage signal would mark everything "used". Checked against 408 session files, a 56,724-row log database, and the tool-registry table before concluding.
|
|
257
|
-
- **`~/.codex/` is read-only.** Nothing is moved or deleted there, same as `~/.claude/agents/`.
|
|
258
|
-
|
|
259
|
-
- **Backup-artifact detection, on both agents.** `foo.bak.20260711`, `foo (1)`, `foo~` and similar are flagged — Tier 2 on Claude Code (movable, restorable), report-only on Codex. Matching is limited to artifact *shapes*, so `backup-manager` and `test-engineer` are never touched.
|
|
260
|
-
|
|
261
|
-
Tests: 279 → 347 (+68).
|
|
256
|
+
Tests: 374 → 392 (+18), pinning the safety properties — every argv a fixed literal, no shell metacharacters, executable only ever `claude` or `npm`, and a refusal to run unattended with no TTY and no `--yes`.
|
|
262
257
|
|
|
263
258
|
For older release notes, see [CHANGELOG.md](CHANGELOG.md).
|
|
264
259
|
|
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
|
@@ -11,8 +11,10 @@ import { readManifest } from './manifest.js';
|
|
|
11
11
|
import { formatScanSummary, formatReportBox, calculateReport, } from './report.js';
|
|
12
12
|
import { collectDoctorReport, formatDoctorReport } from './doctor.js';
|
|
13
13
|
import { checkForUpdate, formatUpdateNotice } from './update-check.js';
|
|
14
|
+
import { confirmDecision, planUpdate, renderStep, runUpdate } from './update-run.js';
|
|
14
15
|
import { scanCodex } from './codex/index.js';
|
|
15
16
|
import { formatCodexSummary } from './codex/report.js';
|
|
17
|
+
import { classifyCodexIssues } from './codex/detectors.js';
|
|
16
18
|
import { resolveSelection, resolveRestoreSelection } from './selection.js';
|
|
17
19
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
18
20
|
const { version: PKG_VERSION } = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
@@ -100,6 +102,81 @@ program
|
|
|
100
102
|
console.log(`\n \x1b[32m✓\x1b[0m claude-slim ${result.installed} is up to date.\n`);
|
|
101
103
|
}
|
|
102
104
|
});
|
|
105
|
+
// --- update ---
|
|
106
|
+
// Detection lives in `check-update`; this runs the command that detection
|
|
107
|
+
// identified. claude-slim still never writes into a package manager's
|
|
108
|
+
// directories itself — it invokes the manager, the same way cleanup already
|
|
109
|
+
// invokes `claude plugin disable`.
|
|
110
|
+
program
|
|
111
|
+
.command('update')
|
|
112
|
+
.description('Run the upgrade command for however this copy was installed')
|
|
113
|
+
.option('--yes', 'Skip the confirmation prompt')
|
|
114
|
+
.option('--dry-run', 'Show the commands without running them')
|
|
115
|
+
.action(async (opts) => {
|
|
116
|
+
const result = await checkForUpdate({ force: true });
|
|
117
|
+
const plan = planUpdate(result.installMethod);
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(` Installed: ${result.installed} Latest: ${result.latest ?? 'unknown'}`);
|
|
120
|
+
console.log(` Install method: ${result.installMethod}`);
|
|
121
|
+
console.log('');
|
|
122
|
+
if (result.latest === null) {
|
|
123
|
+
console.log(' Could not reach the npm registry, so there is nothing to compare against.');
|
|
124
|
+
console.log('');
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!result.outdated) {
|
|
129
|
+
console.log(' \x1b[32m✓\x1b[0m Already up to date.\n');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!plan.runnable) {
|
|
133
|
+
console.log(` ${plan.guidance}\n`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log(' Will run:');
|
|
137
|
+
for (const step of plan.steps)
|
|
138
|
+
console.log(` ${renderStep(step)}`);
|
|
139
|
+
console.log('');
|
|
140
|
+
if (opts.dryRun) {
|
|
141
|
+
console.log(' \x1b[90mDry run — nothing was executed.\x1b[0m\n');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const gate = confirmDecision(opts, Boolean(process.stdin.isTTY));
|
|
145
|
+
if (gate === 'refuse') {
|
|
146
|
+
console.error(' Refusing to run unattended. Re-run with --yes to confirm.\n');
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (gate === 'prompt') {
|
|
151
|
+
const answer = await askUser(' Proceed? [y/N] ');
|
|
152
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
153
|
+
console.log(' Cancelled.\n');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
console.log('');
|
|
157
|
+
}
|
|
158
|
+
const results = await runUpdate(plan);
|
|
159
|
+
for (const r of results) {
|
|
160
|
+
const mark = r.ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
161
|
+
console.log(` ${mark} ${renderStep(r.step)}`);
|
|
162
|
+
if (r.output)
|
|
163
|
+
console.log(r.output.split('\n').map((l) => ` ${l}`).join('\n'));
|
|
164
|
+
}
|
|
165
|
+
const failed = results.find((r) => !r.ok);
|
|
166
|
+
console.log('');
|
|
167
|
+
if (failed) {
|
|
168
|
+
console.log(' Update did not complete. Run the commands above manually to see the full error.\n');
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
}
|
|
171
|
+
else if (result.installMethod === 'plugin') {
|
|
172
|
+
// The new version is on disk but the running session still holds the old
|
|
173
|
+
// one — without saying so, users update, re-run, and see identical output.
|
|
174
|
+
console.log(' \x1b[33m!\x1b[0m Restart Claude Code for the update to take effect.\n');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(' \x1b[32m✓\x1b[0m Updated.\n');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
103
180
|
// --- clean ---
|
|
104
181
|
program
|
|
105
182
|
.command('clean')
|
|
@@ -108,12 +185,14 @@ program
|
|
|
108
185
|
.option('--auto', 'Non-interactive: auto-select Tier 1 items only')
|
|
109
186
|
.option('--sessions-per-day <n>', 'Sessions per day for savings estimate', '2')
|
|
110
187
|
.option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
|
|
188
|
+
.option('--no-codex', 'Skip ~/.codex entirely')
|
|
111
189
|
.action(async (opts) => {
|
|
112
190
|
await runCleanPipeline({
|
|
113
191
|
dryRun: !!opts.dryRun,
|
|
114
192
|
auto: !!opts.auto,
|
|
115
193
|
sessionsPerDay: parseNonNegativeInt(opts.sessionsPerDay, 2),
|
|
116
194
|
lookbackDays: parseNonNegativeInt(opts.lookbackDays, 60),
|
|
195
|
+
codex: opts.codex !== false,
|
|
117
196
|
});
|
|
118
197
|
});
|
|
119
198
|
// --- restore ---
|
|
@@ -269,12 +348,20 @@ program.action(async () => {
|
|
|
269
348
|
process.exitCode = 1;
|
|
270
349
|
return;
|
|
271
350
|
}
|
|
272
|
-
await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60 });
|
|
351
|
+
await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60, codex: true });
|
|
273
352
|
});
|
|
274
353
|
// --- shared clean pipeline ---
|
|
275
354
|
async function runCleanPipeline(opts) {
|
|
276
355
|
await initTokenizer();
|
|
277
356
|
const result = await scan({ lookbackDays: opts.lookbackDays });
|
|
357
|
+
// Codex issues join the same tiered list. They carry `agent: 'codex'`, which
|
|
358
|
+
// is what keeps the cleaner's path guard pointed at ~/.codex/.
|
|
359
|
+
const codexContents = new Map();
|
|
360
|
+
const codexScan = opts.codex ? await scanCodex(codexContents) : null;
|
|
361
|
+
if (codexScan) {
|
|
362
|
+
result.issues.push(...(await classifyCodexIssues({ scan: codexScan, contents: codexContents })));
|
|
363
|
+
result.issues.sort((a, b) => a.tier - b.tier || b.tokens - a.tokens);
|
|
364
|
+
}
|
|
278
365
|
if (result.issues.length === 0) {
|
|
279
366
|
console.log('\n \x1b[32mAlready slim!\x1b[0m No issues found.\n');
|
|
280
367
|
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;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { InstallMethod } from './update-check.js';
|
|
2
|
+
export interface UpdateStep {
|
|
3
|
+
file: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface UpdatePlan {
|
|
7
|
+
/** false when there is nothing to execute — see `guidance`. */
|
|
8
|
+
runnable: boolean;
|
|
9
|
+
steps: UpdateStep[];
|
|
10
|
+
/** Why nothing runs, for the methods where that is the correct answer. */
|
|
11
|
+
guidance?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function planUpdate(method: InstallMethod): UpdatePlan;
|
|
14
|
+
/** Human-readable rendering of a step, matching what the user would type. */
|
|
15
|
+
export declare function renderStep(step: UpdateStep): string;
|
|
16
|
+
export interface StepResult {
|
|
17
|
+
step: UpdateStep;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
output: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run the plan's steps in order, stopping at the first failure.
|
|
23
|
+
*
|
|
24
|
+
* Sequential rather than parallel, and halting on error, because step 2 depends
|
|
25
|
+
* on step 1 having refreshed the marketplace — running it against a stale
|
|
26
|
+
* manifest would report success while changing nothing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runUpdate(plan: UpdatePlan): Promise<StepResult[]>;
|
|
29
|
+
export type ConfirmDecision = 'run' | 'prompt' | 'refuse';
|
|
30
|
+
/**
|
|
31
|
+
* Decide how to gate an update that will modify the user's install.
|
|
32
|
+
*
|
|
33
|
+
* Refusing when there is no TTY is the important case: piped into a script or a
|
|
34
|
+
* CI job, there is nobody to answer the prompt, and silently proceeding would
|
|
35
|
+
* mean a tool that changes an installation without anyone agreeing to it.
|
|
36
|
+
* `--yes` is how you say so deliberately.
|
|
37
|
+
*/
|
|
38
|
+
export declare function confirmDecision(opts: {
|
|
39
|
+
yes?: boolean;
|
|
40
|
+
}, isTTY: boolean): ConfirmDecision;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
// Running the upgrade.
|
|
3
|
+
//
|
|
4
|
+
// v2.9.0 shipped detection only, on the reasoning that updating is the package
|
|
5
|
+
// manager's job. Half of that was right and half was not. Writing into the
|
|
6
|
+
// plugin directory ourselves would indeed corrupt an install — but *invoking*
|
|
7
|
+
// the package manager is something this tool already does elsewhere
|
|
8
|
+
// (`claude plugin disable` during unused-plugin cleanup). Refusing to invoke it
|
|
9
|
+
// here was inconsistent, not principled.
|
|
10
|
+
//
|
|
11
|
+
// So: still never write to those directories directly. Just run the command the
|
|
12
|
+
// user would have typed, after showing it to them.
|
|
13
|
+
//
|
|
14
|
+
// Every argv below is a fixed literal. Nothing from the environment, the
|
|
15
|
+
// manifest, or user input is interpolated, and execFile never routes through a
|
|
16
|
+
// shell — so there is no argument-injection surface to guard.
|
|
17
|
+
const STEP_TIMEOUT_MS = 180_000;
|
|
18
|
+
export function planUpdate(method) {
|
|
19
|
+
switch (method) {
|
|
20
|
+
case 'plugin':
|
|
21
|
+
return {
|
|
22
|
+
runnable: true,
|
|
23
|
+
steps: [
|
|
24
|
+
// Refresh the marketplace first; otherwise `plugin update` compares
|
|
25
|
+
// against a stale manifest and reports "already up to date".
|
|
26
|
+
{ file: 'claude', args: ['plugin', 'marketplace', 'update', 'claude-slim'] },
|
|
27
|
+
// The qualified id is required: the bare name fails with
|
|
28
|
+
// `Plugin "claude-slim" not found` when a marketplace shares its name.
|
|
29
|
+
{ file: 'claude', args: ['plugin', 'update', 'claude-slim@claude-slim'] },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
case 'global':
|
|
33
|
+
return {
|
|
34
|
+
runnable: true,
|
|
35
|
+
steps: [{ file: 'npm', args: ['install', '-g', 'claude-slim@latest'] }],
|
|
36
|
+
};
|
|
37
|
+
case 'npx':
|
|
38
|
+
return {
|
|
39
|
+
runnable: false,
|
|
40
|
+
steps: [],
|
|
41
|
+
guidance: 'npx resolves the latest version on every invocation, so there is nothing to update. ' +
|
|
42
|
+
'If a stale copy is cached, run `npx claude-slim@latest` once to refresh it.',
|
|
43
|
+
};
|
|
44
|
+
case 'source':
|
|
45
|
+
return {
|
|
46
|
+
runnable: false,
|
|
47
|
+
steps: [],
|
|
48
|
+
guidance: 'This is a source checkout. Updating it means pulling your own repository, ' +
|
|
49
|
+
'which claude-slim will not do on your behalf — run `git pull && npm install` yourself.',
|
|
50
|
+
};
|
|
51
|
+
default:
|
|
52
|
+
return {
|
|
53
|
+
runnable: false,
|
|
54
|
+
steps: [],
|
|
55
|
+
guidance: 'Could not tell how this copy was installed, so no upgrade command can be chosen safely. ' +
|
|
56
|
+
'Update it the same way you installed it.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Human-readable rendering of a step, matching what the user would type. */
|
|
61
|
+
export function renderStep(step) {
|
|
62
|
+
return [step.file, ...step.args].join(' ');
|
|
63
|
+
}
|
|
64
|
+
function runStep(step) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
execFile(step.file, step.args, { timeout: STEP_TIMEOUT_MS }, (err, stdout, stderr) => {
|
|
67
|
+
const output = [stdout, stderr].map((s) => s?.trim()).filter(Boolean).join('\n');
|
|
68
|
+
resolve({ step, ok: !err, output: err ? `${err.message}${output ? `\n${output}` : ''}` : output });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Run the plan's steps in order, stopping at the first failure.
|
|
74
|
+
*
|
|
75
|
+
* Sequential rather than parallel, and halting on error, because step 2 depends
|
|
76
|
+
* on step 1 having refreshed the marketplace — running it against a stale
|
|
77
|
+
* manifest would report success while changing nothing.
|
|
78
|
+
*/
|
|
79
|
+
export async function runUpdate(plan) {
|
|
80
|
+
const results = [];
|
|
81
|
+
for (const step of plan.steps) {
|
|
82
|
+
const result = await runStep(step);
|
|
83
|
+
results.push(result);
|
|
84
|
+
if (!result.ok)
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Decide how to gate an update that will modify the user's install.
|
|
91
|
+
*
|
|
92
|
+
* Refusing when there is no TTY is the important case: piped into a script or a
|
|
93
|
+
* CI job, there is nobody to answer the prompt, and silently proceeding would
|
|
94
|
+
* mean a tool that changes an installation without anyone agreeing to it.
|
|
95
|
+
* `--yes` is how you say so deliberately.
|
|
96
|
+
*/
|
|
97
|
+
export function confirmDecision(opts, isTTY) {
|
|
98
|
+
if (opts.yes)
|
|
99
|
+
return 'run';
|
|
100
|
+
return isTTY ? 'prompt' : 'refuse';
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slim",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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": {
|