@sporesec/arcana 3.0.2 → 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/clean.js
CHANGED
|
@@ -28,17 +28,80 @@ function isAgentLog(filename) {
|
|
|
28
28
|
return filename.startsWith("agent-") && filename.endsWith(".jsonl");
|
|
29
29
|
}
|
|
30
30
|
export async function cleanCommand(opts) {
|
|
31
|
+
// --trim: session bloat trimming (large tool results, base64)
|
|
32
|
+
if (opts.trim) {
|
|
33
|
+
const { findLatestSession } = await import("../utils/sessions.js");
|
|
34
|
+
const { analyzeSession, trimSession } = await import("../session/trim.js");
|
|
35
|
+
const sessionPath = findLatestSession(process.cwd());
|
|
36
|
+
if (!sessionPath) {
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
console.log(JSON.stringify({ error: "No session found" }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
/* v8 ignore start */
|
|
42
|
+
console.log(`${ui.warn("[!!]")} No session found for current project.`);
|
|
43
|
+
return;
|
|
44
|
+
/* v8 ignore stop */
|
|
45
|
+
}
|
|
46
|
+
if (opts.dryRun) {
|
|
47
|
+
const analysis = analyzeSession(sessionPath);
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify(analysis));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
/* v8 ignore start */
|
|
53
|
+
banner();
|
|
54
|
+
console.log(ui.bold(" Trim Analysis (dry run)\n"));
|
|
55
|
+
console.log(` Original: ${(analysis.originalBytes / 1024).toFixed(0)} KB (${analysis.originalLines} lines)`);
|
|
56
|
+
console.log(` Would trim: ${(analysis.savedBytes / 1024).toFixed(0)} KB (${analysis.savedPct}%)`);
|
|
57
|
+
console.log(` Tool results > 500 chars: ${analysis.toolResultsTrimmed}`);
|
|
58
|
+
console.log(` Base64 images: ${analysis.base64Removed}`);
|
|
59
|
+
console.log();
|
|
60
|
+
return;
|
|
61
|
+
/* v8 ignore stop */
|
|
62
|
+
}
|
|
63
|
+
const trimmed = trimSession(sessionPath);
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
if (opts.json) {
|
|
66
|
+
console.log(JSON.stringify({ error: "Trim failed" }));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
/* v8 ignore start */
|
|
70
|
+
console.log(`${ui.warn("[!!]")} Trim failed.`);
|
|
71
|
+
return;
|
|
72
|
+
/* v8 ignore stop */
|
|
73
|
+
}
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
console.log(JSON.stringify({ ...trimmed.result, destPath: trimmed.destPath }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
/* v8 ignore start */
|
|
79
|
+
banner();
|
|
80
|
+
console.log(ui.bold(" Session Trim\n"));
|
|
81
|
+
console.log(` ${ui.success("[OK]")} Saved ${(trimmed.result.savedBytes / 1024).toFixed(0)} KB (${trimmed.result.savedPct}%)`);
|
|
82
|
+
console.log(` Tool results trimmed: ${trimmed.result.toolResultsTrimmed}`);
|
|
83
|
+
console.log(` Base64 removed: ${trimmed.result.base64Removed}`);
|
|
84
|
+
console.log(ui.dim(` Trimmed copy: ${trimmed.destPath}`));
|
|
85
|
+
console.log(ui.dim(" Original session unchanged."));
|
|
86
|
+
console.log();
|
|
87
|
+
return;
|
|
88
|
+
/* v8 ignore stop */
|
|
89
|
+
}
|
|
90
|
+
/* v8 ignore start */
|
|
31
91
|
if (!opts.json)
|
|
32
92
|
banner();
|
|
93
|
+
/* v8 ignore stop */
|
|
33
94
|
const dryRun = opts.dryRun ?? false;
|
|
34
95
|
const aggressive = opts.aggressive ?? false;
|
|
35
96
|
const keepDays = opts.keepDays ?? MAIN_LOG_MAX_AGE_DAYS;
|
|
36
97
|
const agentKeepDays = aggressive ? 0 : AGENT_LOG_MAX_AGE_DAYS;
|
|
37
98
|
const mainKeepDays = aggressive ? 0 : keepDays;
|
|
99
|
+
/* v8 ignore start */
|
|
38
100
|
if (dryRun && !opts.json)
|
|
39
101
|
console.log(ui.warn(" DRY RUN - no files will be deleted\n"));
|
|
40
102
|
if (aggressive && !opts.json)
|
|
41
103
|
console.log(ui.warn(" AGGRESSIVE MODE - all session logs will be targeted\n"));
|
|
104
|
+
/* v8 ignore stop */
|
|
42
105
|
const result = {
|
|
43
106
|
dryRun,
|
|
44
107
|
actions: 0,
|
|
@@ -51,6 +114,7 @@ export async function cleanCommand(opts) {
|
|
|
51
114
|
};
|
|
52
115
|
// 1. Clean broken symlinks
|
|
53
116
|
const failedSymlinks = [];
|
|
117
|
+
/* v8 ignore next */
|
|
54
118
|
if (!opts.json)
|
|
55
119
|
console.log(ui.bold(" Symlinks"));
|
|
56
120
|
for (const link of listSymlinks().filter((s) => s.broken)) {
|
|
@@ -59,20 +123,24 @@ export async function cleanCommand(opts) {
|
|
|
59
123
|
rmSync(link.fullPath);
|
|
60
124
|
}
|
|
61
125
|
catch (err) {
|
|
126
|
+
/* v8 ignore next 4 */
|
|
62
127
|
if (!opts.json)
|
|
63
128
|
console.log(` ${ui.warn(" Could not remove:")} ${link.name} ${ui.dim(`(${err instanceof Error ? err.message : "unknown"})`)}`);
|
|
64
129
|
failedSymlinks.push(link.name);
|
|
65
130
|
continue;
|
|
66
131
|
}
|
|
67
132
|
}
|
|
133
|
+
/* v8 ignore next */
|
|
68
134
|
if (!opts.json)
|
|
69
135
|
console.log(` ${ui.dim(" Remove broken:")} ${link.name}`);
|
|
70
136
|
result.removedSymlinks.push(link.name);
|
|
71
137
|
result.actions++;
|
|
72
138
|
}
|
|
139
|
+
/* v8 ignore next */
|
|
73
140
|
if (result.removedSymlinks.length === 0 && !opts.json)
|
|
74
141
|
console.log(ui.dim(" No broken symlinks"));
|
|
75
142
|
// 2. Clean orphaned + stale project dirs
|
|
143
|
+
/* v8 ignore next */
|
|
76
144
|
if (!opts.json)
|
|
77
145
|
console.log(ui.bold("\n Project Data"));
|
|
78
146
|
const projectsDir = join(homedir(), ".claude", "projects");
|
|
@@ -104,6 +172,7 @@ export async function cleanCommand(opts) {
|
|
|
104
172
|
const reason = orphaned ? "orphaned (source deleted)" : `stale (${Math.floor(daysOld)}d old)`;
|
|
105
173
|
if (!dryRun)
|
|
106
174
|
rmSync(projDir, { recursive: true, force: true });
|
|
175
|
+
/* v8 ignore next */
|
|
107
176
|
if (!opts.json)
|
|
108
177
|
console.log(` ${ui.dim("Remove:")} ${entry} ${ui.dim(`(${mb} MB, ${reason})`)}`);
|
|
109
178
|
result.removedProjects.push({ name: entry, sizeMB: mb, reason });
|
|
@@ -111,18 +180,21 @@ export async function cleanCommand(opts) {
|
|
|
111
180
|
}
|
|
112
181
|
}
|
|
113
182
|
}
|
|
183
|
+
/* v8 ignore next */
|
|
114
184
|
if (result.removedProjects.length === 0 && !opts.json)
|
|
115
185
|
console.log(ui.dim(" No orphaned or stale projects"));
|
|
116
186
|
// 3. Tiered session log cleanup
|
|
117
187
|
// - Agent logs (agent-*.jsonl): delete after 7 days (never resumed, bulk of bloat)
|
|
118
188
|
// - Main sessions (UUID.jsonl): delete after 30 days (configurable with --keep-days)
|
|
119
189
|
// - Aggressive mode: delete everything regardless of age
|
|
190
|
+
/* v8 ignore start */
|
|
120
191
|
if (!opts.json) {
|
|
121
192
|
console.log(ui.bold("\n Session Logs"));
|
|
122
193
|
if (!aggressive) {
|
|
123
194
|
console.log(ui.dim(` Agent logs: remove >${agentKeepDays}d | Main sessions: remove >${mainKeepDays}d`));
|
|
124
195
|
}
|
|
125
196
|
}
|
|
197
|
+
/* v8 ignore stop */
|
|
126
198
|
let logCount = 0;
|
|
127
199
|
if (existsSync(projectsDir)) {
|
|
128
200
|
const now = Date.now();
|
|
@@ -159,6 +231,7 @@ export async function cleanCommand(opts) {
|
|
|
159
231
|
}
|
|
160
232
|
}
|
|
161
233
|
const reason = isAgent ? "agent log" : "main session";
|
|
234
|
+
/* v8 ignore next 4 */
|
|
162
235
|
if (!opts.json)
|
|
163
236
|
console.log(` ${ui.dim("Remove:")} ${entry}/${file} ${ui.dim(`(${sizeMB.toFixed(1)} MB, ${Math.floor(daysOld)}d, ${reason})`)}`);
|
|
164
237
|
result.removedSessionLogs.push({
|
|
@@ -191,6 +264,7 @@ export async function cleanCommand(opts) {
|
|
|
191
264
|
if (!dryRun)
|
|
192
265
|
rmSync(subPath, { recursive: true, force: true });
|
|
193
266
|
const mb = (size / (1024 * 1024)).toFixed(1);
|
|
267
|
+
/* v8 ignore next 4 */
|
|
194
268
|
if (!opts.json)
|
|
195
269
|
console.log(` ${ui.dim("Remove:")} ${entry}/${sub}/ ${ui.dim(`(${mb} MB, ${Math.floor(daysOld)}d, session dir)`)}`);
|
|
196
270
|
result.removedSessionLogs.push({
|
|
@@ -210,9 +284,11 @@ export async function cleanCommand(opts) {
|
|
|
210
284
|
}
|
|
211
285
|
}
|
|
212
286
|
}
|
|
287
|
+
/* v8 ignore next */
|
|
213
288
|
if (logCount === 0 && !opts.json)
|
|
214
289
|
console.log(ui.dim(" No session logs to trim"));
|
|
215
290
|
// 4. Purge auxiliary directories
|
|
291
|
+
/* v8 ignore next */
|
|
216
292
|
if (!opts.json)
|
|
217
293
|
console.log(ui.bold("\n Auxiliary Data"));
|
|
218
294
|
const claudeDir = join(homedir(), ".claude");
|
|
@@ -226,11 +302,13 @@ export async function cleanCommand(opts) {
|
|
|
226
302
|
const mb = (size / (1024 * 1024)).toFixed(1);
|
|
227
303
|
const reclaimed = dryRun ? size : purgeDir(dir, false);
|
|
228
304
|
result.reclaimedBytes += reclaimed;
|
|
305
|
+
/* v8 ignore next */
|
|
229
306
|
if (!opts.json)
|
|
230
307
|
console.log(` ${ui.dim("Purge:")} ${dirName}/ ${ui.dim(`(${mb} MB)`)}`);
|
|
231
308
|
result.purgedDirs.push({ name: dirName, sizeMB: mb });
|
|
232
309
|
result.actions++;
|
|
233
310
|
}
|
|
311
|
+
/* v8 ignore next */
|
|
234
312
|
if (result.purgedDirs.length === 0 && !opts.json)
|
|
235
313
|
console.log(ui.dim(" All clean"));
|
|
236
314
|
// 5. Clear action history
|
|
@@ -263,6 +341,7 @@ export async function cleanCommand(opts) {
|
|
|
263
341
|
}, null, 2));
|
|
264
342
|
return;
|
|
265
343
|
}
|
|
344
|
+
/* v8 ignore start */
|
|
266
345
|
console.log();
|
|
267
346
|
const mb = (result.reclaimedBytes / (1024 * 1024)).toFixed(1);
|
|
268
347
|
const verb = dryRun ? "Would reclaim" : "Reclaimed";
|
|
@@ -273,4 +352,5 @@ export async function cleanCommand(opts) {
|
|
|
273
352
|
console.log(ui.success(" Nothing to clean."));
|
|
274
353
|
}
|
|
275
354
|
console.log();
|
|
355
|
+
/* v8 ignore stop */
|
|
276
356
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { compress, compressionStats, recordCompression } from "../compress/index.js";
|
|
2
|
+
export async function compressCommand(command, opts) {
|
|
3
|
+
let input;
|
|
4
|
+
if (opts.stdin) {
|
|
5
|
+
const chunks = [];
|
|
6
|
+
for await (const chunk of process.stdin)
|
|
7
|
+
chunks.push(chunk);
|
|
8
|
+
input = Buffer.concat(chunks).toString("utf-8");
|
|
9
|
+
}
|
|
10
|
+
else if (command.length > 0) {
|
|
11
|
+
const { execSync } = await import("node:child_process");
|
|
12
|
+
try {
|
|
13
|
+
input = execSync(command.join(" "), {
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
16
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
input = err.stdout ?? "" + (err.stderr ?? "");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.error("Usage: arcana compress <command> or echo ... | arcana compress --stdin --tool git");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const tool = opts.tool ?? command[0] ?? "unknown";
|
|
29
|
+
const compressed = compress(input, tool);
|
|
30
|
+
const stats = compressionStats(input, compressed);
|
|
31
|
+
recordCompression(tool, stats.originalTokens, stats.compressedTokens);
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
console.log(JSON.stringify(stats));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
process.stdout.write(compressed);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/commands/config.js
CHANGED
|
@@ -6,9 +6,11 @@ import { ui, banner, table } from "../utils/ui.js";
|
|
|
6
6
|
import { clearProviderCache } from "../registry.js";
|
|
7
7
|
const VALID_KEYS = ["defaultProvider", "installDir"];
|
|
8
8
|
export async function configCommand(action, value, opts) {
|
|
9
|
+
/* v8 ignore start */
|
|
9
10
|
if (!opts?.json) {
|
|
10
11
|
banner();
|
|
11
12
|
}
|
|
13
|
+
/* v8 ignore stop */
|
|
12
14
|
// arcana config list (or no args)
|
|
13
15
|
if (!action || action === "list") {
|
|
14
16
|
const config = loadConfig();
|
|
@@ -16,6 +18,7 @@ export async function configCommand(action, value, opts) {
|
|
|
16
18
|
console.log(JSON.stringify({ config }));
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
21
|
+
/* v8 ignore start */
|
|
19
22
|
console.log(ui.bold(" Configuration\n"));
|
|
20
23
|
const envInstallDir = process.env.ARCANA_INSTALL_DIR;
|
|
21
24
|
const envProvider = process.env.ARCANA_DEFAULT_PROVIDER;
|
|
@@ -37,22 +40,26 @@ export async function configCommand(action, value, opts) {
|
|
|
37
40
|
console.log(ui.dim(` ${existsSync(configPath) ? "Custom config" : "Using defaults"}`));
|
|
38
41
|
console.log();
|
|
39
42
|
return;
|
|
43
|
+
/* v8 ignore stop */
|
|
40
44
|
}
|
|
41
45
|
// arcana config path
|
|
42
46
|
if (action === "path") {
|
|
47
|
+
/* v8 ignore start */
|
|
43
48
|
if (value && !opts?.json) {
|
|
44
49
|
console.log(ui.warn(` 'path' does not take a value (ignoring "${value}")`));
|
|
45
50
|
}
|
|
51
|
+
/* v8 ignore stop */
|
|
46
52
|
const configPath = join(homedir(), ".arcana", "config.json");
|
|
47
53
|
if (opts?.json) {
|
|
48
54
|
console.log(JSON.stringify({ path: configPath, exists: existsSync(configPath) }));
|
|
55
|
+
return;
|
|
49
56
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
57
|
+
/* v8 ignore start */
|
|
58
|
+
console.log(` ${configPath}`);
|
|
59
|
+
console.log(ui.dim(` ${existsSync(configPath) ? "Custom config" : "Using defaults (file does not exist)"}`));
|
|
60
|
+
console.log();
|
|
55
61
|
return;
|
|
62
|
+
/* v8 ignore stop */
|
|
56
63
|
}
|
|
57
64
|
// arcana config reset
|
|
58
65
|
if (action === "reset") {
|
|
@@ -69,35 +76,38 @@ export async function configCommand(action, value, opts) {
|
|
|
69
76
|
success: false,
|
|
70
77
|
error: err instanceof Error ? err.message : "Failed to remove config",
|
|
71
78
|
}));
|
|
79
|
+
process.exit(1);
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
81
|
+
/* v8 ignore start */
|
|
82
|
+
console.log(ui.error(` Failed to reset config: ${err instanceof Error ? err.message : "unknown error"}`));
|
|
83
|
+
console.log();
|
|
77
84
|
process.exit(1);
|
|
85
|
+
/* v8 ignore stop */
|
|
78
86
|
}
|
|
79
87
|
clearProviderCache();
|
|
80
88
|
}
|
|
81
89
|
if (opts?.json) {
|
|
82
90
|
console.log(JSON.stringify({ action: "reset", success: true, existed }));
|
|
91
|
+
return;
|
|
83
92
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
93
|
+
/* v8 ignore start */
|
|
94
|
+
console.log(existed ? ui.success(" Config reset to defaults") : ui.dim(" Already using defaults"));
|
|
95
|
+
console.log();
|
|
88
96
|
return;
|
|
97
|
+
/* v8 ignore stop */
|
|
89
98
|
}
|
|
90
99
|
// arcana config <key> [value]
|
|
91
100
|
if (!VALID_KEYS.includes(action)) {
|
|
92
101
|
if (opts?.json) {
|
|
93
102
|
console.log(JSON.stringify({ error: `Unknown config key: ${action}`, validKeys: [...VALID_KEYS] }));
|
|
103
|
+
process.exit(1);
|
|
94
104
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
105
|
+
/* v8 ignore start */
|
|
106
|
+
console.log(ui.error(` Unknown config key: ${action}`));
|
|
107
|
+
console.log(ui.dim(` Valid keys: ${VALID_KEYS.join(", ")}`));
|
|
108
|
+
console.log();
|
|
100
109
|
process.exit(1);
|
|
110
|
+
/* v8 ignore stop */
|
|
101
111
|
}
|
|
102
112
|
const config = loadConfig();
|
|
103
113
|
const key = action;
|
|
@@ -105,14 +115,16 @@ export async function configCommand(action, value, opts) {
|
|
|
105
115
|
// Get value
|
|
106
116
|
if (opts?.json) {
|
|
107
117
|
console.log(JSON.stringify({ action: "get", key, value: config[key] }));
|
|
118
|
+
return;
|
|
108
119
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
120
|
+
/* v8 ignore start */
|
|
121
|
+
console.log(` ${key} = ${config[key]}`);
|
|
122
|
+
console.log();
|
|
113
123
|
return;
|
|
124
|
+
/* v8 ignore stop */
|
|
114
125
|
}
|
|
115
126
|
// Set value
|
|
127
|
+
/* v8 ignore start */
|
|
116
128
|
if (key === "installDir") {
|
|
117
129
|
if (value === "") {
|
|
118
130
|
console.log(ui.error(" installDir cannot be empty"));
|
|
@@ -132,13 +144,15 @@ export async function configCommand(action, value, opts) {
|
|
|
132
144
|
process.exit(1);
|
|
133
145
|
}
|
|
134
146
|
}
|
|
147
|
+
/* v8 ignore stop */
|
|
135
148
|
config[key] = value;
|
|
136
149
|
saveConfig(config);
|
|
137
150
|
if (opts?.json) {
|
|
138
151
|
console.log(JSON.stringify({ action: "set", key, value }));
|
|
152
|
+
return;
|
|
139
153
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
/* v8 ignore start */
|
|
155
|
+
console.log(ui.success(` Set ${key} = ${value}`));
|
|
156
|
+
console.log();
|
|
157
|
+
/* v8 ignore stop */
|
|
144
158
|
}
|
package/dist/commands/create.js
CHANGED
|
@@ -43,6 +43,7 @@ const good = "example";
|
|
|
43
43
|
See \`references/\` for detailed documentation.
|
|
44
44
|
`;
|
|
45
45
|
}
|
|
46
|
+
/* v8 ignore start */
|
|
46
47
|
export async function createCommand(name) {
|
|
47
48
|
console.log(renderBanner());
|
|
48
49
|
console.log();
|
|
@@ -98,3 +99,4 @@ export async function createCommand(name) {
|
|
|
98
99
|
p.log.info("Edit SKILL.md to add your skill instructions.");
|
|
99
100
|
p.outro(`Next: ${chalk.cyan("arcana validate " + name)}`);
|
|
100
101
|
}
|
|
102
|
+
/* v8 ignore stop */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface CuratedSkill {
|
|
2
|
+
name: string;
|
|
3
|
+
score: number;
|
|
4
|
+
tokens: number;
|
|
5
|
+
reasons: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface CurationResult {
|
|
8
|
+
selected: CuratedSkill[];
|
|
9
|
+
skipped: {
|
|
10
|
+
name: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}[];
|
|
13
|
+
totalTokens: number;
|
|
14
|
+
budgetTokens: number;
|
|
15
|
+
budgetPct: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Curate skills for context: rank by relevance, greedily fill token budget.
|
|
19
|
+
* Uses existing rankSkills() from scoring.ts and readSkillContent() from load.ts.
|
|
20
|
+
*/
|
|
21
|
+
export declare function curateForContext(cwd: string, opts: {
|
|
22
|
+
budgetPct?: number;
|
|
23
|
+
model?: string;
|
|
24
|
+
forceInclude?: string[];
|
|
25
|
+
}): CurationResult;
|
|
26
|
+
/**
|
|
27
|
+
* Generate _active.md with curated skill content.
|
|
28
|
+
* Writes to ~/.agents/skills/_active.md.
|
|
29
|
+
*/
|
|
30
|
+
export declare function regenerateActive(opts?: {
|
|
31
|
+
budgetPct?: number;
|
|
32
|
+
model?: string;
|
|
33
|
+
}): CurationResult;
|
|
34
|
+
export declare function curateCommand(opts: {
|
|
35
|
+
json?: boolean;
|
|
36
|
+
budget?: number;
|
|
37
|
+
model?: string;
|
|
38
|
+
include?: string[];
|
|
39
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getInstallDir } from "../utils/fs.js";
|
|
4
|
+
import { atomicWriteSync } from "../utils/atomic.js";
|
|
5
|
+
import { detectProjectContext } from "../utils/project-context.js";
|
|
6
|
+
import { rankSkills } from "../utils/scoring.js";
|
|
7
|
+
import { readSkillContent } from "./load.js";
|
|
8
|
+
import { ACTIVE_FILENAME, CONTEXT_BUDGET_PCT, MODEL_CONTEXTS, TOKENS_PER_KB } from "../constants.js";
|
|
9
|
+
import { recordCuration } from "../utils/usage.js";
|
|
10
|
+
import { ui, banner } from "../utils/ui.js";
|
|
11
|
+
import { getInstalledNames } from "../interactive/helpers.js";
|
|
12
|
+
/** Resolve model context window size. */
|
|
13
|
+
function resolveModelContext(model) {
|
|
14
|
+
if (!model)
|
|
15
|
+
return MODEL_CONTEXTS["default"];
|
|
16
|
+
// Try exact match first, then prefix match
|
|
17
|
+
if (MODEL_CONTEXTS[model] !== undefined)
|
|
18
|
+
return MODEL_CONTEXTS[model];
|
|
19
|
+
const key = Object.keys(MODEL_CONTEXTS).find((k) => model.startsWith(k));
|
|
20
|
+
return key ? MODEL_CONTEXTS[key] : MODEL_CONTEXTS["default"];
|
|
21
|
+
}
|
|
22
|
+
/** Estimate token count from byte size. */
|
|
23
|
+
function estimateTokens(bytes) {
|
|
24
|
+
return Math.round((bytes / 1024) * TOKENS_PER_KB);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Curate skills for context: rank by relevance, greedily fill token budget.
|
|
28
|
+
* Uses existing rankSkills() from scoring.ts and readSkillContent() from load.ts.
|
|
29
|
+
*/
|
|
30
|
+
export function curateForContext(cwd, opts) {
|
|
31
|
+
const context = detectProjectContext(cwd);
|
|
32
|
+
const installedNames = getInstalledNames();
|
|
33
|
+
if (installedNames.length === 0) {
|
|
34
|
+
return { selected: [], skipped: [], totalTokens: 0, budgetTokens: 0, budgetPct: 0 };
|
|
35
|
+
}
|
|
36
|
+
// Build SkillInfo stubs for ranking (only installed skills)
|
|
37
|
+
const skillInfos = installedNames.map((name) => ({
|
|
38
|
+
name,
|
|
39
|
+
description: "",
|
|
40
|
+
version: "0.0.0",
|
|
41
|
+
source: "local",
|
|
42
|
+
tags: context.tags.length > 0 ? [...context.tags] : undefined,
|
|
43
|
+
}));
|
|
44
|
+
// Get marketplace data for better scoring if available
|
|
45
|
+
const ranked = rankSkills(skillInfos, context);
|
|
46
|
+
const modelContext = resolveModelContext(opts.model);
|
|
47
|
+
const budgetPct = opts.budgetPct ?? CONTEXT_BUDGET_PCT;
|
|
48
|
+
const budgetTokens = Math.floor((modelContext * budgetPct) / 100);
|
|
49
|
+
const selected = [];
|
|
50
|
+
const skipped = [];
|
|
51
|
+
let totalTokens = 0;
|
|
52
|
+
// Force-included skills go first
|
|
53
|
+
const forceInclude = opts.forceInclude ?? [];
|
|
54
|
+
for (const name of forceInclude) {
|
|
55
|
+
if (!installedNames.includes(name))
|
|
56
|
+
continue;
|
|
57
|
+
const content = readSkillContent(name);
|
|
58
|
+
if (!content)
|
|
59
|
+
continue;
|
|
60
|
+
const tokens = estimateTokens(content.bytes);
|
|
61
|
+
if (totalTokens + tokens > budgetTokens) {
|
|
62
|
+
skipped.push({ name, reason: `Force-included but exceeds budget (${tokens} tokens)` });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
totalTokens += tokens;
|
|
66
|
+
selected.push({ name, score: 999, tokens, reasons: ["Force-included"] });
|
|
67
|
+
}
|
|
68
|
+
// Fill remaining budget with ranked skills
|
|
69
|
+
for (const verdict of ranked) {
|
|
70
|
+
if (verdict.verdict === "skip" || verdict.verdict === "conflict")
|
|
71
|
+
continue;
|
|
72
|
+
if (selected.some((s) => s.name === verdict.skill))
|
|
73
|
+
continue; // already force-included
|
|
74
|
+
if (!installedNames.includes(verdict.skill))
|
|
75
|
+
continue;
|
|
76
|
+
const content = readSkillContent(verdict.skill);
|
|
77
|
+
if (!content) {
|
|
78
|
+
skipped.push({ name: verdict.skill, reason: "Content unreadable" });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const tokens = estimateTokens(content.bytes);
|
|
82
|
+
if (totalTokens + tokens > budgetTokens) {
|
|
83
|
+
skipped.push({ name: verdict.skill, reason: `Over budget (+${tokens} tokens)` });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
totalTokens += tokens;
|
|
87
|
+
selected.push({
|
|
88
|
+
name: verdict.skill,
|
|
89
|
+
score: verdict.score,
|
|
90
|
+
tokens,
|
|
91
|
+
reasons: verdict.reasons,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return { selected, skipped, totalTokens, budgetTokens, budgetPct };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Generate _active.md with curated skill content.
|
|
98
|
+
* Writes to ~/.agents/skills/_active.md.
|
|
99
|
+
*/
|
|
100
|
+
export function regenerateActive(opts) {
|
|
101
|
+
const result = curateForContext(process.cwd(), opts ?? {});
|
|
102
|
+
const installDir = getInstallDir();
|
|
103
|
+
if (!existsSync(installDir))
|
|
104
|
+
mkdirSync(installDir, { recursive: true });
|
|
105
|
+
const parts = [];
|
|
106
|
+
parts.push(`# Active Skills (${result.selected.length})`);
|
|
107
|
+
parts.push("");
|
|
108
|
+
parts.push(`Budget: ${result.totalTokens.toLocaleString()} / ${result.budgetTokens.toLocaleString()} tokens (${result.budgetPct}% of context)`);
|
|
109
|
+
parts.push(`Curated for project at: ${process.cwd()}`);
|
|
110
|
+
parts.push(`Generated: ${new Date().toISOString()}`);
|
|
111
|
+
parts.push("");
|
|
112
|
+
parts.push("---");
|
|
113
|
+
parts.push("");
|
|
114
|
+
for (const skill of result.selected) {
|
|
115
|
+
const content = readSkillContent(skill.name);
|
|
116
|
+
if (!content)
|
|
117
|
+
continue;
|
|
118
|
+
parts.push(content.content);
|
|
119
|
+
parts.push("");
|
|
120
|
+
parts.push("---");
|
|
121
|
+
parts.push("");
|
|
122
|
+
}
|
|
123
|
+
const activePath = join(installDir, ACTIVE_FILENAME);
|
|
124
|
+
atomicWriteSync(activePath, parts.join("\n"), 0o644);
|
|
125
|
+
// Record usage for each curated skill
|
|
126
|
+
for (const skill of result.selected) {
|
|
127
|
+
try {
|
|
128
|
+
recordCuration(skill.name);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* best-effort */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
export async function curateCommand(opts) {
|
|
137
|
+
/* v8 ignore start */
|
|
138
|
+
if (!opts.json) {
|
|
139
|
+
banner();
|
|
140
|
+
console.log(ui.bold(" Context Curation\n"));
|
|
141
|
+
}
|
|
142
|
+
/* v8 ignore stop */
|
|
143
|
+
const result = curateForContext(process.cwd(), {
|
|
144
|
+
budgetPct: opts.budget,
|
|
145
|
+
model: opts.model,
|
|
146
|
+
forceInclude: opts.include,
|
|
147
|
+
});
|
|
148
|
+
if (result.selected.length === 0 && getInstalledNames().length === 0) {
|
|
149
|
+
if (opts.json) {
|
|
150
|
+
console.log(JSON.stringify({ error: "No skills installed" }));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
/* v8 ignore next */
|
|
154
|
+
console.log(ui.dim(" No skills installed. Run: arcana install --all"));
|
|
155
|
+
}
|
|
156
|
+
/* v8 ignore next */
|
|
157
|
+
console.log();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Write _active.md
|
|
161
|
+
const installDir = getInstallDir();
|
|
162
|
+
if (!existsSync(installDir))
|
|
163
|
+
mkdirSync(installDir, { recursive: true });
|
|
164
|
+
const parts = [];
|
|
165
|
+
parts.push(`# Active Skills (${result.selected.length})`);
|
|
166
|
+
parts.push("");
|
|
167
|
+
parts.push(`Budget: ${result.totalTokens.toLocaleString()} / ${result.budgetTokens.toLocaleString()} tokens (${result.budgetPct}% of context)`);
|
|
168
|
+
parts.push(`Curated for project at: ${process.cwd()}`);
|
|
169
|
+
parts.push(`Generated: ${new Date().toISOString()}`);
|
|
170
|
+
parts.push("");
|
|
171
|
+
parts.push("---");
|
|
172
|
+
parts.push("");
|
|
173
|
+
for (const skill of result.selected) {
|
|
174
|
+
const content = readSkillContent(skill.name);
|
|
175
|
+
if (!content)
|
|
176
|
+
continue;
|
|
177
|
+
parts.push(content.content);
|
|
178
|
+
parts.push("");
|
|
179
|
+
parts.push("---");
|
|
180
|
+
parts.push("");
|
|
181
|
+
}
|
|
182
|
+
const activePath = join(installDir, ACTIVE_FILENAME);
|
|
183
|
+
atomicWriteSync(activePath, parts.join("\n"), 0o644);
|
|
184
|
+
if (opts.json) {
|
|
185
|
+
console.log(JSON.stringify({
|
|
186
|
+
selected: result.selected,
|
|
187
|
+
skipped: result.skipped,
|
|
188
|
+
totalTokens: result.totalTokens,
|
|
189
|
+
budgetTokens: result.budgetTokens,
|
|
190
|
+
path: activePath,
|
|
191
|
+
}));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
/* v8 ignore start */
|
|
195
|
+
// Display results
|
|
196
|
+
const budgetBar = Math.round((result.totalTokens / result.budgetTokens) * 20);
|
|
197
|
+
const bar = ui.success("█".repeat(budgetBar)) + ui.dim("░".repeat(20 - budgetBar));
|
|
198
|
+
console.log(` ${bar} ${result.totalTokens.toLocaleString()} / ${result.budgetTokens.toLocaleString()} tokens`);
|
|
199
|
+
console.log();
|
|
200
|
+
for (const skill of result.selected) {
|
|
201
|
+
const pct = Math.round((skill.tokens / result.budgetTokens) * 100);
|
|
202
|
+
console.log(` ${ui.success("[OK]")} ${skill.name} (${skill.tokens.toLocaleString()} tokens, ${pct}%)`);
|
|
203
|
+
if (skill.reasons.length > 0) {
|
|
204
|
+
console.log(ui.dim(` ${skill.reasons.join(", ")}`));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (result.skipped.length > 0) {
|
|
208
|
+
console.log();
|
|
209
|
+
console.log(ui.dim(" Skipped:"));
|
|
210
|
+
for (const skip of result.skipped.slice(0, 5)) {
|
|
211
|
+
console.log(ui.dim(` ${skip.name}: ${skip.reason}`));
|
|
212
|
+
}
|
|
213
|
+
if (result.skipped.length > 5) {
|
|
214
|
+
console.log(ui.dim(` ...and ${result.skipped.length - 5} more`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
console.log();
|
|
218
|
+
console.log(ui.dim(` Written to: ${activePath}`));
|
|
219
|
+
console.log(ui.dim(" Agents read this file automatically for project-relevant skills."));
|
|
220
|
+
console.log();
|
|
221
|
+
/* v8 ignore stop */
|
|
222
|
+
}
|
package/dist/commands/diff.js
CHANGED
|
@@ -136,6 +136,7 @@ export async function diffCommand(skill, opts) {
|
|
|
136
136
|
console.log(JSON.stringify(result, null, 2));
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
|
+
/* v8 ignore start */
|
|
139
140
|
// Console output
|
|
140
141
|
console.log(`Diff: ${skill}`);
|
|
141
142
|
console.log(` Local version: ${localVersion}`);
|
|
@@ -163,4 +164,5 @@ export async function diffCommand(skill, opts) {
|
|
|
163
164
|
console.log(` ~ ${entry.path} (+${entry.linesAdded} / -${entry.linesRemoved})`);
|
|
164
165
|
}
|
|
165
166
|
}
|
|
167
|
+
/* v8 ignore stop */
|
|
166
168
|
}
|