@sporesec/arcana 3.0.3 → 4.0.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/dist/cli.js +25 -298
- package/dist/command-defs.d.ts +28 -0
- package/dist/command-defs.js +414 -0
- package/dist/commands/audit.js +18 -4
- package/dist/commands/clean.d.ts +1 -0
- package/dist/commands/clean.js +80 -0
- package/dist/commands/compress.d.ts +5 -0
- package/dist/commands/compress.js +38 -0
- package/dist/commands/config.js +40 -26
- package/dist/commands/create.js +2 -0
- package/dist/commands/curate.d.ts +39 -0
- package/dist/commands/curate.js +222 -0
- package/dist/commands/diff.js +2 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +61 -2
- package/dist/commands/import-cmd.js +5 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.js +107 -0
- package/dist/commands/info.js +19 -8
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +71 -0
- package/dist/commands/install.js +2 -0
- package/dist/commands/list.js +8 -0
- package/dist/commands/load.d.ts +10 -0
- package/dist/commands/load.js +130 -0
- package/dist/commands/lock.js +35 -24
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +87 -0
- package/dist/commands/outdated.js +8 -6
- package/dist/commands/providers.js +29 -21
- package/dist/commands/recommend.js +11 -3
- package/dist/commands/remember.d.ts +12 -0
- package/dist/commands/remember.js +111 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +46 -8
- package/dist/commands/search.js +6 -0
- package/dist/commands/uninstall.js +36 -0
- package/dist/commands/update.js +27 -0
- package/dist/commands/validate.js +8 -0
- package/dist/commands/verify.js +2 -0
- package/dist/compress/engine.d.ts +21 -0
- package/dist/compress/engine.js +106 -0
- package/dist/compress/index.d.ts +7 -0
- package/dist/compress/index.js +10 -0
- package/dist/compress/rules/generic.d.ts +1 -0
- package/dist/compress/rules/generic.js +9 -0
- package/dist/compress/rules/git.d.ts +1 -0
- package/dist/compress/rules/git.js +113 -0
- package/dist/compress/rules/npm.d.ts +1 -0
- package/dist/compress/rules/npm.js +99 -0
- package/dist/compress/rules/test-runner.d.ts +1 -0
- package/dist/compress/rules/test-runner.js +103 -0
- package/dist/compress/rules/tsc.d.ts +1 -0
- package/dist/compress/rules/tsc.js +39 -0
- package/dist/compress/tracker.d.ts +16 -0
- package/dist/compress/tracker.js +45 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +29 -0
- package/dist/interactive/helpers.js +1 -0
- package/dist/interactive/menu.js +6 -1
- package/dist/interactive/optimize-flow.js +4 -4
- package/dist/mcp/install.d.ts +10 -0
- package/dist/mcp/install.js +109 -0
- package/dist/mcp/registry.d.ts +11 -0
- package/dist/mcp/registry.js +27 -0
- package/dist/providers/anthropics.d.ts +4 -0
- package/dist/providers/anthropics.js +10 -0
- package/dist/registry.js +4 -0
- package/dist/session/trim.d.ts +23 -0
- package/dist/session/trim.js +132 -0
- package/dist/utils/cache.js +2 -2
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +33 -14
- package/dist/utils/help.js +16 -8
- package/dist/utils/install-core.js +23 -1
- package/dist/utils/memory.d.ts +25 -0
- package/dist/utils/memory.js +103 -0
- package/dist/utils/project-context.js +4 -0
- package/dist/utils/scanner.d.ts +22 -1
- package/dist/utils/scanner.js +81 -9
- package/dist/utils/sessions.d.ts +2 -0
- package/dist/utils/sessions.js +36 -0
- package/dist/utils/ui.js +5 -0
- package/dist/utils/usage.d.ts +17 -0
- package/dist/utils/usage.js +83 -0
- package/package.json +42 -7
- package/dist/command-registry.d.ts +0 -10
- package/dist/command-registry.js +0 -65
- package/dist/commands/benchmark.d.ts +0 -4
- package/dist/commands/benchmark.js +0 -178
- package/dist/commands/compact.d.ts +0 -6
- package/dist/commands/compact.js +0 -239
- package/dist/commands/optimize.d.ts +0 -3
- package/dist/commands/optimize.js +0 -356
- package/dist/commands/profile.d.ts +0 -3
- package/dist/commands/profile.js +0 -274
- package/dist/commands/stats.d.ts +0 -3
- package/dist/commands/stats.js +0 -210
- package/dist/commands/team.d.ts +0 -3
- package/dist/commands/team.js +0 -291
- package/dist/interactive.d.ts +0 -1
- package/dist/interactive.js +0 -841
package/dist/commands/compact.js
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { ui, banner } from "../utils/ui.js";
|
|
5
|
-
import { getDirSize } from "../utils/fs.js";
|
|
6
|
-
import { PRUNE_DEFAULT_DAYS, PRUNE_SIZE_THRESHOLD_BYTES, PRUNE_KEEP_NEWEST } from "../constants.js";
|
|
7
|
-
function analyzeProject(projDir, projName) {
|
|
8
|
-
const now = Date.now();
|
|
9
|
-
const mainFiles = [];
|
|
10
|
-
const agentFiles = [];
|
|
11
|
-
const sessionDirs = [];
|
|
12
|
-
for (const entry of readdirSync(projDir)) {
|
|
13
|
-
const full = join(projDir, entry);
|
|
14
|
-
try {
|
|
15
|
-
const stat = statSync(full);
|
|
16
|
-
if (stat.isDirectory()) {
|
|
17
|
-
if (entry === "memory")
|
|
18
|
-
continue;
|
|
19
|
-
sessionDirs.push({
|
|
20
|
-
name: entry,
|
|
21
|
-
sizeBytes: getDirSize(full),
|
|
22
|
-
daysOld: Math.floor((now - stat.mtimeMs) / (1000 * 60 * 60 * 24)),
|
|
23
|
-
});
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (!entry.endsWith(".jsonl"))
|
|
27
|
-
continue;
|
|
28
|
-
const info = {
|
|
29
|
-
name: entry,
|
|
30
|
-
sizeBytes: stat.size,
|
|
31
|
-
daysOld: Math.floor((now - stat.mtimeMs) / (1000 * 60 * 60 * 24)),
|
|
32
|
-
};
|
|
33
|
-
if (entry.startsWith("agent-")) {
|
|
34
|
-
agentFiles.push(info);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
mainFiles.push(info);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const totalBytes = mainFiles.reduce((s, f) => s + f.sizeBytes, 0) +
|
|
45
|
-
agentFiles.reduce((s, f) => s + f.sizeBytes, 0) +
|
|
46
|
-
sessionDirs.reduce((s, d) => s + d.sizeBytes, 0);
|
|
47
|
-
// Reclaimable: all agent files + all session dirs
|
|
48
|
-
const reclaimableBytes = agentFiles.reduce((s, f) => s + f.sizeBytes, 0) + sessionDirs.reduce((s, d) => s + d.sizeBytes, 0);
|
|
49
|
-
return { name: projName, mainFiles, agentFiles, sessionDirs, totalBytes, reclaimableBytes };
|
|
50
|
-
}
|
|
51
|
-
function formatBytes(bytes) {
|
|
52
|
-
if (bytes < 1024)
|
|
53
|
-
return `${bytes} B`;
|
|
54
|
-
if (bytes < 1024 * 1024)
|
|
55
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
56
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
57
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
58
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Prune oversized main session files.
|
|
62
|
-
* Targets files older than pruneDays AND larger than 10 MB.
|
|
63
|
-
* Always keeps the 3 newest sessions per project regardless.
|
|
64
|
-
*/
|
|
65
|
-
function pruneMainSessions(analyses, projectsDir, pruneDays, dryRun, json) {
|
|
66
|
-
const pruned = [];
|
|
67
|
-
let reclaimedBytes = 0;
|
|
68
|
-
for (const a of analyses) {
|
|
69
|
-
// Sort main files by age (newest first) to protect the 3 newest
|
|
70
|
-
const sorted = [...a.mainFiles].sort((x, y) => x.daysOld - y.daysOld);
|
|
71
|
-
const candidates = sorted.slice(PRUNE_KEEP_NEWEST); // Skip the 3 newest
|
|
72
|
-
for (const f of candidates) {
|
|
73
|
-
if (f.daysOld > pruneDays && f.sizeBytes > PRUNE_SIZE_THRESHOLD_BYTES) {
|
|
74
|
-
const sizeMB = (f.sizeBytes / (1024 * 1024)).toFixed(1);
|
|
75
|
-
if (!json) {
|
|
76
|
-
console.log(` ${ui.warn("Prune:")} ${f.name} ${ui.dim(`(${sizeMB} MB, ${f.daysOld}d old)`)}`);
|
|
77
|
-
}
|
|
78
|
-
if (!dryRun) {
|
|
79
|
-
try {
|
|
80
|
-
rmSync(join(projectsDir, a.name, f.name), { force: true });
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
/* skip */
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
pruned.push({ project: a.name, file: f.name, sizeMB, daysOld: f.daysOld });
|
|
87
|
-
reclaimedBytes += f.sizeBytes;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return { pruned, reclaimedBytes };
|
|
92
|
-
}
|
|
93
|
-
export async function compactCommand(opts) {
|
|
94
|
-
if (!opts.json)
|
|
95
|
-
banner();
|
|
96
|
-
const dryRun = opts.dryRun ?? false;
|
|
97
|
-
if (dryRun && !opts.json)
|
|
98
|
-
console.log(ui.warn(" DRY RUN - no files will be deleted\n"));
|
|
99
|
-
const projectsDir = join(homedir(), ".claude", "projects");
|
|
100
|
-
if (!existsSync(projectsDir)) {
|
|
101
|
-
if (opts.json) {
|
|
102
|
-
console.log(JSON.stringify({ projects: [], totalReclaimed: 0 }));
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
console.log(ui.dim(" No session data found."));
|
|
106
|
-
console.log();
|
|
107
|
-
}
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const analyses = [];
|
|
111
|
-
for (const entry of readdirSync(projectsDir)) {
|
|
112
|
-
const projDir = join(projectsDir, entry);
|
|
113
|
-
if (!statSync(projDir).isDirectory())
|
|
114
|
-
continue;
|
|
115
|
-
if (entry === "memory" || entry.startsWith("."))
|
|
116
|
-
continue;
|
|
117
|
-
const analysis = analyzeProject(projDir, entry);
|
|
118
|
-
if (analysis.totalBytes > 0) {
|
|
119
|
-
analyses.push(analysis);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
analyses.sort((a, b) => b.totalBytes - a.totalBytes);
|
|
123
|
-
if (opts.json) {
|
|
124
|
-
const jsonResult = {
|
|
125
|
-
projects: analyses.map((a) => ({
|
|
126
|
-
name: a.name,
|
|
127
|
-
totalBytes: a.totalBytes,
|
|
128
|
-
reclaimableBytes: a.reclaimableBytes,
|
|
129
|
-
mainSessions: a.mainFiles.length,
|
|
130
|
-
agentLogs: a.agentFiles.length,
|
|
131
|
-
sessionDirs: a.sessionDirs.length,
|
|
132
|
-
})),
|
|
133
|
-
totalReclaimed: dryRun ? 0 : analyses.reduce((s, a) => s + a.reclaimableBytes, 0),
|
|
134
|
-
};
|
|
135
|
-
if (!dryRun) {
|
|
136
|
-
for (const a of analyses) {
|
|
137
|
-
const projDir = join(projectsDir, a.name);
|
|
138
|
-
for (const f of a.agentFiles) {
|
|
139
|
-
try {
|
|
140
|
-
rmSync(join(projDir, f.name), { force: true });
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
/* skip */
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
for (const d of a.sessionDirs) {
|
|
147
|
-
try {
|
|
148
|
-
rmSync(join(projDir, d.name), { recursive: true, force: true });
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
/* skip */
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (opts.prune) {
|
|
157
|
-
const pruneDays = opts.pruneDays ?? PRUNE_DEFAULT_DAYS;
|
|
158
|
-
const pruneResult = pruneMainSessions(analyses, projectsDir, pruneDays, dryRun, true);
|
|
159
|
-
jsonResult.pruned = pruneResult.pruned;
|
|
160
|
-
jsonResult.prunedBytes = pruneResult.reclaimedBytes;
|
|
161
|
-
if (!dryRun) {
|
|
162
|
-
jsonResult.totalReclaimed = jsonResult.totalReclaimed + pruneResult.reclaimedBytes;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
console.log(JSON.stringify(jsonResult, null, 2));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (!opts.json)
|
|
169
|
-
console.log(ui.bold(" Session Compaction Report\n"));
|
|
170
|
-
let totalReclaimed = 0;
|
|
171
|
-
const hasWork = analyses.some((a) => a.reclaimableBytes > 0);
|
|
172
|
-
for (const a of analyses) {
|
|
173
|
-
if (a.totalBytes < 1024)
|
|
174
|
-
continue; // Skip tiny projects
|
|
175
|
-
const truncName = a.name.length > 50 ? a.name.slice(0, 47) + "..." : a.name;
|
|
176
|
-
console.log(` ${ui.bold(truncName)} ${ui.dim(formatBytes(a.totalBytes))}`);
|
|
177
|
-
// Main sessions
|
|
178
|
-
const mainSize = a.mainFiles.reduce((s, f) => s + f.sizeBytes, 0);
|
|
179
|
-
console.log(` ${ui.success("Keep:")} ${a.mainFiles.length} main session${a.mainFiles.length !== 1 ? "s" : ""} ${ui.dim(`(${formatBytes(mainSize)})`)}`);
|
|
180
|
-
// Agent logs
|
|
181
|
-
if (a.agentFiles.length > 0) {
|
|
182
|
-
const agentSize = a.agentFiles.reduce((s, f) => s + f.sizeBytes, 0);
|
|
183
|
-
console.log(` ${ui.warn("Remove:")} ${a.agentFiles.length} agent log${a.agentFiles.length !== 1 ? "s" : ""} ${ui.dim(`(${formatBytes(agentSize)})`)}`);
|
|
184
|
-
if (!dryRun) {
|
|
185
|
-
const projDir = join(projectsDir, a.name);
|
|
186
|
-
for (const f of a.agentFiles) {
|
|
187
|
-
try {
|
|
188
|
-
rmSync(join(projDir, f.name), { force: true });
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
/* skip */
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
totalReclaimed += agentSize;
|
|
196
|
-
}
|
|
197
|
-
// Session dirs
|
|
198
|
-
if (a.sessionDirs.length > 0) {
|
|
199
|
-
const dirSize = a.sessionDirs.reduce((s, d) => s + d.sizeBytes, 0);
|
|
200
|
-
if (dirSize > 0) {
|
|
201
|
-
console.log(` ${ui.warn("Remove:")} ${a.sessionDirs.length} session dir${a.sessionDirs.length !== 1 ? "s" : ""} ${ui.dim(`(${formatBytes(dirSize)})`)}`);
|
|
202
|
-
if (!dryRun) {
|
|
203
|
-
const projDir = join(projectsDir, a.name);
|
|
204
|
-
for (const d of a.sessionDirs) {
|
|
205
|
-
try {
|
|
206
|
-
rmSync(join(projDir, d.name), { recursive: true, force: true });
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
/* skip */
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
totalReclaimed += dirSize;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
console.log();
|
|
217
|
-
}
|
|
218
|
-
// Prune oversized main sessions if requested
|
|
219
|
-
if (opts.prune) {
|
|
220
|
-
const pruneDays = opts.pruneDays ?? PRUNE_DEFAULT_DAYS;
|
|
221
|
-
console.log(ui.bold(` Pruning main sessions (>${pruneDays}d old AND >10 MB, keeping 3 newest per project)\n`));
|
|
222
|
-
const pruneResult = pruneMainSessions(analyses, projectsDir, pruneDays, dryRun, false);
|
|
223
|
-
totalReclaimed += pruneResult.reclaimedBytes;
|
|
224
|
-
if (pruneResult.pruned.length === 0) {
|
|
225
|
-
console.log(ui.dim(" No oversized sessions to prune."));
|
|
226
|
-
}
|
|
227
|
-
console.log();
|
|
228
|
-
}
|
|
229
|
-
// Summary
|
|
230
|
-
if (!hasWork && !opts.prune) {
|
|
231
|
-
console.log(ui.success(" Already compact. No agent logs to remove."));
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
const verb = dryRun ? "Would reclaim" : "Reclaimed";
|
|
235
|
-
console.log(ui.success(` ${verb} ${formatBytes(totalReclaimed)} by removing agent logs and session dirs.`));
|
|
236
|
-
console.log(ui.dim(" Main session files preserved for history."));
|
|
237
|
-
}
|
|
238
|
-
console.log();
|
|
239
|
-
}
|
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { ui, banner } from "../utils/ui.js";
|
|
5
|
-
import { getInstallDir, getDirSize } from "../utils/fs.js";
|
|
6
|
-
function readSettings() {
|
|
7
|
-
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
8
|
-
if (!existsSync(settingsPath))
|
|
9
|
-
return null;
|
|
10
|
-
try {
|
|
11
|
-
return JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function checkAutocompact() {
|
|
18
|
-
const settings = readSettings();
|
|
19
|
-
if (!settings) {
|
|
20
|
-
return {
|
|
21
|
-
area: "Autocompact",
|
|
22
|
-
status: "suggest",
|
|
23
|
-
message: "No settings.json found",
|
|
24
|
-
action: "Set CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=80 in ~/.claude/settings.json env block",
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
const env = settings.env;
|
|
28
|
-
const val = env?.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE;
|
|
29
|
-
if (!val) {
|
|
30
|
-
return {
|
|
31
|
-
area: "Autocompact",
|
|
32
|
-
status: "suggest",
|
|
33
|
-
message: "Not configured (defaults to high threshold)",
|
|
34
|
-
action: "Set to 80 to compact earlier and save tokens",
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
const pct = parseInt(val);
|
|
38
|
-
if (pct <= 70) {
|
|
39
|
-
return {
|
|
40
|
-
area: "Autocompact",
|
|
41
|
-
status: "warn",
|
|
42
|
-
message: `Set to ${pct}%. Too aggressive, may lose context.`,
|
|
43
|
-
action: "Raise to 75-80% for better balance",
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
if (pct > 85) {
|
|
47
|
-
return {
|
|
48
|
-
area: "Autocompact",
|
|
49
|
-
status: "suggest",
|
|
50
|
-
message: `Set to ${pct}%. Compaction happens late, less room for reasoning.`,
|
|
51
|
-
action: "Lower to 80% for better quality (research-backed)",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
return { area: "Autocompact", status: "good", message: `Set to ${pct}% (optimal range)` };
|
|
55
|
-
}
|
|
56
|
-
function checkEffortLevel() {
|
|
57
|
-
const settings = readSettings();
|
|
58
|
-
const env = (settings?.env ?? {});
|
|
59
|
-
const val = env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
60
|
-
if (!val) {
|
|
61
|
-
return { area: "Effort level", status: "good", message: "Using default (high)" };
|
|
62
|
-
}
|
|
63
|
-
if (val === "low") {
|
|
64
|
-
return {
|
|
65
|
-
area: "Effort level",
|
|
66
|
-
status: "suggest",
|
|
67
|
-
message: "Set to 'low'. Faster but may miss details.",
|
|
68
|
-
action: "Use 'medium' for daily work, 'high' for complex tasks",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
if (val === "medium") {
|
|
72
|
-
return { area: "Effort level", status: "good", message: "Set to 'medium'. Good balance of speed and quality." };
|
|
73
|
-
}
|
|
74
|
-
return { area: "Effort level", status: "good", message: `Set to '${val}'` };
|
|
75
|
-
}
|
|
76
|
-
function checkNonEssentialCalls() {
|
|
77
|
-
const settings = readSettings();
|
|
78
|
-
const env = (settings?.env ?? {});
|
|
79
|
-
const val = env.DISABLE_NON_ESSENTIAL_MODEL_CALLS;
|
|
80
|
-
if (val === "1" || val === "true") {
|
|
81
|
-
return { area: "Non-essential calls", status: "good", message: "Disabled (saves tokens)" };
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
area: "Non-essential calls",
|
|
85
|
-
status: "suggest",
|
|
86
|
-
message: "Not disabled",
|
|
87
|
-
action: "Set DISABLE_NON_ESSENTIAL_MODEL_CALLS=1 in settings.json env to save tokens",
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function checkSkillTokenBudget() {
|
|
91
|
-
const dir = getInstallDir();
|
|
92
|
-
if (!existsSync(dir)) {
|
|
93
|
-
return { area: "Skill token budget", status: "good", message: "No skills installed" };
|
|
94
|
-
}
|
|
95
|
-
let totalKB = 0;
|
|
96
|
-
let skillCount = 0;
|
|
97
|
-
const large = [];
|
|
98
|
-
for (const entry of readdirSync(dir)) {
|
|
99
|
-
const skillDir = join(dir, entry);
|
|
100
|
-
if (!statSync(skillDir).isDirectory())
|
|
101
|
-
continue;
|
|
102
|
-
skillCount++;
|
|
103
|
-
const kb = getDirSize(skillDir) / 1024;
|
|
104
|
-
totalKB += kb;
|
|
105
|
-
if (kb > 50)
|
|
106
|
-
large.push({ name: entry, kb });
|
|
107
|
-
}
|
|
108
|
-
const estTokens = Math.round(totalKB * 256);
|
|
109
|
-
if (totalKB > 500) {
|
|
110
|
-
large.sort((a, b) => b.kb - a.kb);
|
|
111
|
-
const topNames = large
|
|
112
|
-
.slice(0, 3)
|
|
113
|
-
.map((s) => s.name)
|
|
114
|
-
.join(", ");
|
|
115
|
-
return {
|
|
116
|
-
area: "Skill token budget",
|
|
117
|
-
status: "warn",
|
|
118
|
-
message: `${skillCount} skills, ${totalKB.toFixed(0)} KB (~${(estTokens / 1000).toFixed(0)}K tokens). Heavy context load.`,
|
|
119
|
-
action: `Consider uninstalling rarely used skills. Largest: ${topNames}`,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (totalKB > 200) {
|
|
123
|
-
return {
|
|
124
|
-
area: "Skill token budget",
|
|
125
|
-
status: "suggest",
|
|
126
|
-
message: `${skillCount} skills, ${totalKB.toFixed(0)} KB (~${(estTokens / 1000).toFixed(0)}K tokens)`,
|
|
127
|
-
action: "Review installed skills with 'arcana list --installed' and remove unused ones",
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
area: "Skill token budget",
|
|
132
|
-
status: "good",
|
|
133
|
-
message: `${skillCount} skills, ${totalKB.toFixed(0)} KB (~${(estTokens / 1000).toFixed(0)}K tokens)`,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
function checkDiskHealth() {
|
|
137
|
-
const claudeDir = join(homedir(), ".claude");
|
|
138
|
-
if (!existsSync(claudeDir)) {
|
|
139
|
-
return { area: "Disk health", status: "good", message: "No Claude data directory" };
|
|
140
|
-
}
|
|
141
|
-
const totalMB = getDirSize(claudeDir) / (1024 * 1024);
|
|
142
|
-
if (totalMB > 1000) {
|
|
143
|
-
return {
|
|
144
|
-
area: "Disk health",
|
|
145
|
-
status: "warn",
|
|
146
|
-
message: `${totalMB.toFixed(0)} MB total Claude data`,
|
|
147
|
-
action: "Run: arcana compact (removes agent logs, keeps sessions)",
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
if (totalMB > 500) {
|
|
151
|
-
return {
|
|
152
|
-
area: "Disk health",
|
|
153
|
-
status: "suggest",
|
|
154
|
-
message: `${totalMB.toFixed(0)} MB total Claude data`,
|
|
155
|
-
action: "Run: arcana compact",
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
return { area: "Disk health", status: "good", message: `${totalMB.toFixed(0)} MB total Claude data` };
|
|
159
|
-
}
|
|
160
|
-
function checkPreCompactHook() {
|
|
161
|
-
// Check both global and local settings for PreCompact hooks
|
|
162
|
-
const paths = [join(homedir(), ".claude", "settings.json"), join(homedir(), ".claude", "settings.local.json")];
|
|
163
|
-
for (const settingsPath of paths) {
|
|
164
|
-
if (!existsSync(settingsPath))
|
|
165
|
-
continue;
|
|
166
|
-
try {
|
|
167
|
-
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
168
|
-
const hooks = settings?.hooks?.PreCompact;
|
|
169
|
-
if (hooks && Array.isArray(hooks) && hooks.length > 0) {
|
|
170
|
-
return { area: "PreCompact hook", status: "good", message: "Installed. Context preserved before compaction." };
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
area: "PreCompact hook",
|
|
179
|
-
status: "suggest",
|
|
180
|
-
message: "Not installed. Context is lost during auto-compaction.",
|
|
181
|
-
action: "Run: arcana init --tool claude (adds PreCompact hook to preserve context)",
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
function checkMemorySize() {
|
|
185
|
-
const projectsDir = join(homedir(), ".claude", "projects");
|
|
186
|
-
if (!existsSync(projectsDir)) {
|
|
187
|
-
return { area: "MEMORY.md sizes", status: "good", message: "No project memory files" };
|
|
188
|
-
}
|
|
189
|
-
const oversized = [];
|
|
190
|
-
for (const entry of readdirSync(projectsDir)) {
|
|
191
|
-
const memDir = join(projectsDir, entry, "memory");
|
|
192
|
-
const memFile = join(memDir, "MEMORY.md");
|
|
193
|
-
if (!existsSync(memFile))
|
|
194
|
-
continue;
|
|
195
|
-
try {
|
|
196
|
-
const content = readFileSync(memFile, "utf-8");
|
|
197
|
-
const lineCount = content.split("\n").length;
|
|
198
|
-
if (lineCount > 200) {
|
|
199
|
-
oversized.push({ project: entry, lines: lineCount });
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (oversized.length === 0) {
|
|
207
|
-
return { area: "MEMORY.md sizes", status: "good", message: "All under 200 lines (auto-load limit)" };
|
|
208
|
-
}
|
|
209
|
-
oversized.sort((a, b) => b.lines - a.lines);
|
|
210
|
-
const top = oversized[0];
|
|
211
|
-
return {
|
|
212
|
-
area: "MEMORY.md sizes",
|
|
213
|
-
status: "warn",
|
|
214
|
-
message: `${oversized.length} MEMORY.md file${oversized.length > 1 ? "s" : ""} exceed 200 lines. Only first 200 auto-load. Worst: ${top.project} (${top.lines} lines)`,
|
|
215
|
-
action: "Trim to 200 lines. Move detailed notes to separate topic files in memory/",
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
function checkAgentBloat() {
|
|
219
|
-
const projectsDir = join(homedir(), ".claude", "projects");
|
|
220
|
-
if (!existsSync(projectsDir)) {
|
|
221
|
-
return { area: "Agent log bloat", status: "good", message: "No session data" };
|
|
222
|
-
}
|
|
223
|
-
let agentBytes = 0;
|
|
224
|
-
let mainBytes = 0;
|
|
225
|
-
let agentCount = 0;
|
|
226
|
-
let mainCount = 0;
|
|
227
|
-
for (const project of readdirSync(projectsDir)) {
|
|
228
|
-
const projDir = join(projectsDir, project);
|
|
229
|
-
if (!statSync(projDir).isDirectory())
|
|
230
|
-
continue;
|
|
231
|
-
for (const file of readdirSync(projDir)) {
|
|
232
|
-
if (!file.endsWith(".jsonl"))
|
|
233
|
-
continue;
|
|
234
|
-
try {
|
|
235
|
-
const size = statSync(join(projDir, file)).size;
|
|
236
|
-
if (file.startsWith("agent-")) {
|
|
237
|
-
agentBytes += size;
|
|
238
|
-
agentCount++;
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
mainBytes += size;
|
|
242
|
-
mainCount++;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
const totalMB = (agentBytes + mainBytes) / (1024 * 1024);
|
|
251
|
-
const agentMB = agentBytes / (1024 * 1024);
|
|
252
|
-
const agentPct = totalMB > 0 ? Math.round((agentMB / totalMB) * 100) : 0;
|
|
253
|
-
if (agentPct > 70 && agentMB > 50) {
|
|
254
|
-
return {
|
|
255
|
-
area: "Agent log bloat",
|
|
256
|
-
status: "warn",
|
|
257
|
-
message: `${agentCount} agent logs (${agentMB.toFixed(0)} MB, ${agentPct}% of all logs). ${mainCount} main sessions (${(mainBytes / (1024 * 1024)).toFixed(0)} MB).`,
|
|
258
|
-
action: "Run: arcana compact (removes agent logs, keeps main sessions)",
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
if (agentMB > 20) {
|
|
262
|
-
return {
|
|
263
|
-
area: "Agent log bloat",
|
|
264
|
-
status: "suggest",
|
|
265
|
-
message: `${agentCount} agent logs (${agentMB.toFixed(0)} MB, ${agentPct}%). ${mainCount} main sessions.`,
|
|
266
|
-
action: "Run: arcana compact",
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
area: "Agent log bloat",
|
|
271
|
-
status: "good",
|
|
272
|
-
message: `${agentCount} agent logs (${agentMB.toFixed(0)} MB), ${mainCount} main sessions (${(mainBytes / (1024 * 1024)).toFixed(0)} MB)`,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
function checkLargestSkills() {
|
|
276
|
-
const dir = getInstallDir();
|
|
277
|
-
if (!existsSync(dir)) {
|
|
278
|
-
return { area: "Top skills by size", status: "good", message: "No skills installed" };
|
|
279
|
-
}
|
|
280
|
-
const skills = [];
|
|
281
|
-
for (const entry of readdirSync(dir)) {
|
|
282
|
-
const skillDir = join(dir, entry);
|
|
283
|
-
if (!statSync(skillDir).isDirectory())
|
|
284
|
-
continue;
|
|
285
|
-
const kb = getDirSize(skillDir) / 1024;
|
|
286
|
-
skills.push({ name: entry, kb });
|
|
287
|
-
}
|
|
288
|
-
if (skills.length === 0) {
|
|
289
|
-
return { area: "Top skills by size", status: "good", message: "No skills installed" };
|
|
290
|
-
}
|
|
291
|
-
skills.sort((a, b) => b.kb - a.kb);
|
|
292
|
-
const totalKB = skills.reduce((s, sk) => s + sk.kb, 0);
|
|
293
|
-
const totalMB = totalKB / 1024;
|
|
294
|
-
const top5 = skills
|
|
295
|
-
.slice(0, 5)
|
|
296
|
-
.map((s) => `${s.name} (${s.kb.toFixed(0)} KB)`)
|
|
297
|
-
.join(", ");
|
|
298
|
-
if (totalMB > 3) {
|
|
299
|
-
return {
|
|
300
|
-
area: "Top skills by size",
|
|
301
|
-
status: "warn",
|
|
302
|
-
message: `${skills.length} skills, ${totalMB.toFixed(1)} MB total. Top 5: ${top5}`,
|
|
303
|
-
action: "Review large skills with 'arcana list --installed'. Uninstall unused ones.",
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
if (totalMB > 1.5) {
|
|
307
|
-
return {
|
|
308
|
-
area: "Top skills by size",
|
|
309
|
-
status: "suggest",
|
|
310
|
-
message: `${skills.length} skills, ${totalMB.toFixed(1)} MB total. Top 5: ${top5}`,
|
|
311
|
-
action: "Consider removing rarely used skills to save context tokens",
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
area: "Top skills by size",
|
|
316
|
-
status: "good",
|
|
317
|
-
message: `${skills.length} skills, ${totalMB.toFixed(1)} MB total. Top 5: ${top5}`,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
export async function optimizeCommand(opts) {
|
|
321
|
-
if (!opts.json) {
|
|
322
|
-
banner();
|
|
323
|
-
console.log(ui.bold(" Claude Code Optimization Report\n"));
|
|
324
|
-
}
|
|
325
|
-
const recommendations = [
|
|
326
|
-
checkAutocompact(),
|
|
327
|
-
checkEffortLevel(),
|
|
328
|
-
checkNonEssentialCalls(),
|
|
329
|
-
checkPreCompactHook(),
|
|
330
|
-
checkSkillTokenBudget(),
|
|
331
|
-
checkMemorySize(),
|
|
332
|
-
checkAgentBloat(),
|
|
333
|
-
checkDiskHealth(),
|
|
334
|
-
checkLargestSkills(),
|
|
335
|
-
];
|
|
336
|
-
if (opts.json) {
|
|
337
|
-
console.log(JSON.stringify({ recommendations }, null, 2));
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
for (const rec of recommendations) {
|
|
341
|
-
const icon = rec.status === "good" ? ui.success("[OK]") : rec.status === "suggest" ? ui.cyan("[>>]") : ui.warn("[!!]");
|
|
342
|
-
console.log(` ${icon} ${ui.bold(rec.area)}: ${rec.message}`);
|
|
343
|
-
if (rec.action) {
|
|
344
|
-
console.log(ui.dim(` ${rec.action}`));
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
const actionable = recommendations.filter((r) => r.status !== "good");
|
|
348
|
-
console.log();
|
|
349
|
-
if (actionable.length === 0) {
|
|
350
|
-
console.log(ui.success(" Your setup is well optimized."));
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
console.log(ui.dim(` ${actionable.length} suggestion${actionable.length > 1 ? "s" : ""} to improve token usage and performance.`));
|
|
354
|
-
}
|
|
355
|
-
console.log();
|
|
356
|
-
}
|