@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.
Files changed (102) hide show
  1. package/dist/cli.js +25 -298
  2. package/dist/command-defs.d.ts +28 -0
  3. package/dist/command-defs.js +414 -0
  4. package/dist/commands/audit.js +18 -4
  5. package/dist/commands/clean.d.ts +1 -0
  6. package/dist/commands/clean.js +80 -0
  7. package/dist/commands/compress.d.ts +5 -0
  8. package/dist/commands/compress.js +38 -0
  9. package/dist/commands/config.js +40 -26
  10. package/dist/commands/create.js +2 -0
  11. package/dist/commands/curate.d.ts +39 -0
  12. package/dist/commands/curate.js +222 -0
  13. package/dist/commands/diff.js +2 -0
  14. package/dist/commands/doctor.d.ts +1 -0
  15. package/dist/commands/doctor.js +61 -2
  16. package/dist/commands/import-cmd.js +5 -0
  17. package/dist/commands/index.d.ts +5 -0
  18. package/dist/commands/index.js +107 -0
  19. package/dist/commands/info.js +19 -8
  20. package/dist/commands/init.d.ts +3 -0
  21. package/dist/commands/init.js +71 -0
  22. package/dist/commands/install.js +2 -0
  23. package/dist/commands/list.js +8 -0
  24. package/dist/commands/load.d.ts +10 -0
  25. package/dist/commands/load.js +130 -0
  26. package/dist/commands/lock.js +35 -24
  27. package/dist/commands/mcp.d.ts +4 -0
  28. package/dist/commands/mcp.js +87 -0
  29. package/dist/commands/outdated.js +8 -6
  30. package/dist/commands/providers.js +29 -21
  31. package/dist/commands/recommend.js +11 -3
  32. package/dist/commands/remember.d.ts +12 -0
  33. package/dist/commands/remember.js +111 -0
  34. package/dist/commands/scan.d.ts +2 -0
  35. package/dist/commands/scan.js +46 -8
  36. package/dist/commands/search.js +6 -0
  37. package/dist/commands/uninstall.js +36 -0
  38. package/dist/commands/update.js +27 -0
  39. package/dist/commands/validate.js +8 -0
  40. package/dist/commands/verify.js +2 -0
  41. package/dist/compress/engine.d.ts +21 -0
  42. package/dist/compress/engine.js +106 -0
  43. package/dist/compress/index.d.ts +7 -0
  44. package/dist/compress/index.js +10 -0
  45. package/dist/compress/rules/generic.d.ts +1 -0
  46. package/dist/compress/rules/generic.js +9 -0
  47. package/dist/compress/rules/git.d.ts +1 -0
  48. package/dist/compress/rules/git.js +113 -0
  49. package/dist/compress/rules/npm.d.ts +1 -0
  50. package/dist/compress/rules/npm.js +99 -0
  51. package/dist/compress/rules/test-runner.d.ts +1 -0
  52. package/dist/compress/rules/test-runner.js +103 -0
  53. package/dist/compress/rules/tsc.d.ts +1 -0
  54. package/dist/compress/rules/tsc.js +39 -0
  55. package/dist/compress/tracker.d.ts +16 -0
  56. package/dist/compress/tracker.js +45 -0
  57. package/dist/constants.d.ts +12 -0
  58. package/dist/constants.js +29 -0
  59. package/dist/interactive/helpers.js +1 -0
  60. package/dist/interactive/menu.js +6 -1
  61. package/dist/interactive/optimize-flow.js +4 -4
  62. package/dist/mcp/install.d.ts +10 -0
  63. package/dist/mcp/install.js +109 -0
  64. package/dist/mcp/registry.d.ts +11 -0
  65. package/dist/mcp/registry.js +27 -0
  66. package/dist/providers/anthropics.d.ts +4 -0
  67. package/dist/providers/anthropics.js +10 -0
  68. package/dist/registry.js +4 -0
  69. package/dist/session/trim.d.ts +23 -0
  70. package/dist/session/trim.js +132 -0
  71. package/dist/utils/cache.js +2 -2
  72. package/dist/utils/config.d.ts +2 -0
  73. package/dist/utils/config.js +33 -14
  74. package/dist/utils/help.js +16 -8
  75. package/dist/utils/install-core.js +23 -1
  76. package/dist/utils/memory.d.ts +25 -0
  77. package/dist/utils/memory.js +103 -0
  78. package/dist/utils/project-context.js +4 -0
  79. package/dist/utils/scanner.d.ts +22 -1
  80. package/dist/utils/scanner.js +81 -9
  81. package/dist/utils/sessions.d.ts +2 -0
  82. package/dist/utils/sessions.js +36 -0
  83. package/dist/utils/ui.js +5 -0
  84. package/dist/utils/usage.d.ts +17 -0
  85. package/dist/utils/usage.js +83 -0
  86. package/package.json +42 -7
  87. package/dist/command-registry.d.ts +0 -10
  88. package/dist/command-registry.js +0 -65
  89. package/dist/commands/benchmark.d.ts +0 -4
  90. package/dist/commands/benchmark.js +0 -178
  91. package/dist/commands/compact.d.ts +0 -6
  92. package/dist/commands/compact.js +0 -239
  93. package/dist/commands/optimize.d.ts +0 -3
  94. package/dist/commands/optimize.js +0 -356
  95. package/dist/commands/profile.d.ts +0 -3
  96. package/dist/commands/profile.js +0 -274
  97. package/dist/commands/stats.d.ts +0 -3
  98. package/dist/commands/stats.js +0 -210
  99. package/dist/commands/team.d.ts +0 -3
  100. package/dist/commands/team.js +0 -291
  101. package/dist/interactive.d.ts +0 -1
  102. package/dist/interactive.js +0 -841
@@ -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,5 @@
1
+ export declare function compressCommand(command: string[], opts: {
2
+ stdin?: boolean;
3
+ tool?: string;
4
+ json?: boolean;
5
+ }): Promise<void>;
@@ -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
+ }
@@ -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
- else {
51
- console.log(` ${configPath}`);
52
- console.log(ui.dim(` ${existsSync(configPath) ? "Custom config" : "Using defaults (file does not exist)"}`));
53
- console.log();
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
- else {
74
- console.log(ui.error(` Failed to reset config: ${err instanceof Error ? err.message : "unknown error"}`));
75
- console.log();
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
- else {
85
- console.log(existed ? ui.success(" Config reset to defaults") : ui.dim(" Already using defaults"));
86
- console.log();
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
- else {
96
- console.log(ui.error(` Unknown config key: ${action}`));
97
- console.log(ui.dim(` Valid keys: ${VALID_KEYS.join(", ")}`));
98
- console.log();
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
- else {
110
- console.log(` ${key} = ${config[key]}`);
111
- console.log();
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
- else {
141
- console.log(ui.success(` Set ${key} = ${value}`));
142
- console.log();
143
- }
154
+ /* v8 ignore start */
155
+ console.log(ui.success(` Set ${key} = ${value}`));
156
+ console.log();
157
+ /* v8 ignore stop */
144
158
  }
@@ -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
+ }
@@ -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
  }
@@ -2,4 +2,5 @@ import type { DoctorCheck } from "../types.js";
2
2
  export declare function runDoctorChecks(): DoctorCheck[];
3
3
  export declare function doctorCommand(opts?: {
4
4
  json?: boolean;
5
+ fix?: boolean;
5
6
  }): Promise<void>;