@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
@@ -1,21 +1,25 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getInstallDir } from "../utils/fs.js";
4
- import { scanSkillContent, formatScanResults } from "../utils/scanner.js";
4
+ import { scanSkillContentFull, formatScanResults } from "../utils/scanner.js";
5
5
  import { ui, banner } from "../utils/ui.js";
6
6
  export async function scanCommand(skill, opts) {
7
+ /* v8 ignore start */
7
8
  if (!opts.json) {
8
9
  banner();
9
- console.log(ui.bold(" Security Scan\n"));
10
+ console.log(ui.bold(" Security Scan") + (opts.strict ? ui.dim(" (strict mode)") : "") + "\n");
10
11
  }
12
+ /* v8 ignore stop */
11
13
  const installDir = getInstallDir();
12
14
  if (!existsSync(installDir)) {
13
15
  if (opts.json) {
14
16
  console.log(JSON.stringify({ results: [] }));
15
17
  }
16
18
  else {
19
+ /* v8 ignore start */
17
20
  console.log(ui.dim(" No skills installed."));
18
21
  console.log();
22
+ /* v8 ignore stop */
19
23
  }
20
24
  return;
21
25
  }
@@ -31,15 +35,18 @@ export async function scanCommand(skill, opts) {
31
35
  console.log(JSON.stringify({ error: "Specify a skill name or use --all" }));
32
36
  }
33
37
  else {
38
+ /* v8 ignore start */
34
39
  console.log(ui.error(" Specify a skill name or use --all"));
35
40
  console.log(ui.dim(" Usage: arcana scan <skill>"));
36
41
  console.log(ui.dim(" arcana scan --all [--json]"));
37
42
  console.log();
43
+ /* v8 ignore stop */
38
44
  }
39
45
  process.exit(1);
40
46
  }
41
47
  const results = [];
42
48
  let totalIssues = 0;
49
+ let totalSuppressed = 0;
43
50
  let criticalCount = 0;
44
51
  let highCount = 0;
45
52
  let mediumCount = 0;
@@ -47,13 +54,13 @@ export async function scanCommand(skill, opts) {
47
54
  for (const name of skills) {
48
55
  const skillMd = join(installDir, name, "SKILL.md");
49
56
  if (!existsSync(skillMd)) {
50
- results.push({ skill: name, issues: [], error: "Missing SKILL.md" });
57
+ results.push({ skill: name, issues: [], suppressed: [], error: "Missing SKILL.md" });
51
58
  continue;
52
59
  }
53
60
  try {
54
61
  const content = readFileSync(skillMd, "utf-8");
55
- const issues = scanSkillContent(content);
56
- results.push({ skill: name, issues });
62
+ const { issues, suppressed } = scanSkillContentFull(content, { strict: opts.strict });
63
+ results.push({ skill: name, issues, suppressed });
57
64
  if (issues.length === 0) {
58
65
  cleanCount++;
59
66
  }
@@ -63,13 +70,19 @@ export async function scanCommand(skill, opts) {
63
70
  highCount += issues.filter((i) => i.level === "high").length;
64
71
  mediumCount += issues.filter((i) => i.level === "medium").length;
65
72
  }
73
+ totalSuppressed += suppressed.length;
66
74
  }
67
75
  catch (err) {
68
- results.push({ skill: name, issues: [], error: err instanceof Error ? err.message : "Read failed" });
76
+ results.push({
77
+ skill: name,
78
+ issues: [],
79
+ suppressed: [],
80
+ error: err instanceof Error ? err.message : "Read failed",
81
+ });
69
82
  }
70
83
  }
71
84
  if (opts.json) {
72
- console.log(JSON.stringify({
85
+ const jsonOutput = {
73
86
  summary: {
74
87
  total: skills.length,
75
88
  clean: cleanCount,
@@ -77,17 +90,30 @@ export async function scanCommand(skill, opts) {
77
90
  critical: criticalCount,
78
91
  high: highCount,
79
92
  medium: mediumCount,
93
+ ...(opts.verbose ? { suppressed: totalSuppressed } : {}),
80
94
  },
81
95
  results: results.map((r) => ({
82
96
  skill: r.skill,
83
97
  ...(r.error ? { error: r.error } : {}),
84
98
  issues: r.issues.map((i) => ({ level: i.level, category: i.category, detail: i.detail, line: i.line })),
99
+ ...(opts.verbose
100
+ ? {
101
+ suppressed: r.suppressed.map((i) => ({
102
+ level: i.level,
103
+ category: i.category,
104
+ detail: i.detail,
105
+ line: i.line,
106
+ })),
107
+ }
108
+ : {}),
85
109
  })),
86
- }, null, 2));
110
+ };
111
+ console.log(JSON.stringify(jsonOutput, null, 2));
87
112
  if (criticalCount > 0)
88
113
  process.exit(1);
89
114
  return;
90
115
  }
116
+ /* v8 ignore start */
91
117
  // Display results
92
118
  for (const r of results) {
93
119
  if (r.error) {
@@ -95,6 +121,12 @@ export async function scanCommand(skill, opts) {
95
121
  continue;
96
122
  }
97
123
  console.log(formatScanResults(r.skill, r.issues));
124
+ if (opts.verbose && !opts.strict && r.suppressed.length > 0) {
125
+ for (const s of r.suppressed) {
126
+ const icon = s.level === "critical" ? "CRIT" : s.level === "high" ? "HIGH" : "MED";
127
+ console.log(` ${ui.dim(`[SKIP] [${icon}] ${s.category}: ${s.detail} (line ${s.line})`)}`);
128
+ }
129
+ }
98
130
  }
99
131
  console.log();
100
132
  const parts = [];
@@ -106,11 +138,17 @@ export async function scanCommand(skill, opts) {
106
138
  parts.push(ui.warn(`${highCount} high`));
107
139
  if (mediumCount > 0)
108
140
  parts.push(ui.dim(`${mediumCount} medium`));
141
+ if (totalSuppressed > 0 && !opts.strict)
142
+ parts.push(ui.dim(`${totalSuppressed} suppressed`));
109
143
  console.log(` ${parts.join(ui.dim(" | "))}`);
110
144
  if (totalIssues === 0) {
111
145
  console.log(ui.success(" No security issues detected."));
112
146
  }
147
+ if (!opts.strict) {
148
+ console.log(ui.dim(" Note: BAD/DON'T example blocks are skipped. Use --strict to scan everything."));
149
+ }
113
150
  console.log();
114
151
  if (criticalCount > 0)
115
152
  process.exit(1);
153
+ /* v8 ignore stop */
116
154
  }
@@ -3,6 +3,7 @@ import { isSkillInstalled } from "../utils/fs.js";
3
3
  import { getProviders } from "../registry.js";
4
4
  import { detectProjectContext } from "../utils/project-context.js";
5
5
  export async function searchCommand(query, opts) {
6
+ /* v8 ignore next */
6
7
  if (!opts.json)
7
8
  banner();
8
9
  const providers = getProviders(opts.provider);
@@ -10,6 +11,7 @@ export async function searchCommand(query, opts) {
10
11
  for (const provider of providers)
11
12
  provider.clearCache();
12
13
  }
14
+ /* v8 ignore next */
13
15
  const s = opts.json ? noopSpinner() : spinner(`Searching for "${query}"...`);
14
16
  s.start();
15
17
  let results = [];
@@ -29,9 +31,11 @@ export async function searchCommand(query, opts) {
29
31
  console.log(JSON.stringify({ error: err instanceof Error ? err.message : "Search failed" }));
30
32
  process.exit(1);
31
33
  }
34
+ /* v8 ignore start */
32
35
  s.fail("Search failed due to a network or provider error.");
33
36
  printErrorWithHint(err, true);
34
37
  process.exit(1);
38
+ /* v8 ignore stop */
35
39
  }
36
40
  // Filter by tag
37
41
  if (opts.tag) {
@@ -62,6 +66,7 @@ export async function searchCommand(query, opts) {
62
66
  }, null, 2));
63
67
  return;
64
68
  }
69
+ /* v8 ignore start */
65
70
  if (results.length === 0) {
66
71
  console.log(ui.dim(` No skills matching "${query}"${opts.tag ? ` with tag "${opts.tag}"` : ""}`));
67
72
  }
@@ -78,4 +83,5 @@ export async function searchCommand(query, opts) {
78
83
  table(rows);
79
84
  }
80
85
  console.log();
86
+ /* v8 ignore stop */
81
87
  }
@@ -10,6 +10,7 @@ export async function uninstallCommand(skillNames, opts = {}) {
10
10
  if (opts.json) {
11
11
  return uninstallJson(skillNames);
12
12
  }
13
+ /* v8 ignore start */
13
14
  console.log(renderBanner());
14
15
  console.log();
15
16
  if (skillNames.length === 0) {
@@ -32,7 +33,9 @@ export async function uninstallCommand(skillNames, opts = {}) {
32
33
  else {
33
34
  await uninstallMultipleInteractive(skillNames, opts.yes);
34
35
  }
36
+ /* v8 ignore stop */
35
37
  }
38
+ /* v8 ignore start */
36
39
  async function uninstallOneInteractive(skillName, skipConfirm) {
37
40
  p.intro(chalk.bold("Uninstall skill"));
38
41
  const skillDir = getSkillDir(skillName);
@@ -54,12 +57,29 @@ async function uninstallOneInteractive(skillName, skipConfirm) {
54
57
  backupSkill(skillName);
55
58
  rmSync(skillDir, { recursive: true, force: true });
56
59
  const symlinksRemoved = removeSymlinksFor(skillName);
60
+ // Regenerate skill index and active curation
61
+ try {
62
+ const { regenerateIndex } = await import("./index.js");
63
+ regenerateIndex();
64
+ }
65
+ catch {
66
+ /* best-effort */
67
+ }
68
+ try {
69
+ const { regenerateActive } = await import("./curate.js");
70
+ regenerateActive();
71
+ }
72
+ catch {
73
+ /* best-effort */
74
+ }
57
75
  spin.stop(`Removed ${chalk.bold(skillName)}`);
58
76
  if (symlinksRemoved > 0) {
59
77
  p.log.info(`Removed ${symlinksRemoved} symlink${symlinksRemoved > 1 ? "s" : ""}`);
60
78
  }
61
79
  p.outro(`Next: ${chalk.cyan("arcana list --installed")}`);
62
80
  }
81
+ /* v8 ignore stop */
82
+ /* v8 ignore start */
63
83
  async function uninstallMultipleInteractive(skillNames, skipConfirm) {
64
84
  p.intro(chalk.bold(`Uninstall ${skillNames.length} skills`));
65
85
  const missing = [];
@@ -94,12 +114,28 @@ async function uninstallMultipleInteractive(skillNames, skipConfirm) {
94
114
  rmSync(getSkillDir(skillName), { recursive: true, force: true });
95
115
  totalSymlinks += removeSymlinksFor(skillName);
96
116
  }
117
+ // Regenerate skill index and active curation
118
+ try {
119
+ const { regenerateIndex } = await import("./index.js");
120
+ regenerateIndex();
121
+ }
122
+ catch {
123
+ /* best-effort */
124
+ }
125
+ try {
126
+ const { regenerateActive } = await import("./curate.js");
127
+ regenerateActive();
128
+ }
129
+ catch {
130
+ /* best-effort */
131
+ }
97
132
  spin.stop(`Removed ${toRemove.length} skills`);
98
133
  if (totalSymlinks > 0) {
99
134
  p.log.info(`Removed ${totalSymlinks} symlink${totalSymlinks > 1 ? "s" : ""}`);
100
135
  }
101
136
  p.outro(`Next: ${chalk.cyan("arcana list --installed")}`);
102
137
  }
138
+ /* v8 ignore stop */
103
139
  export function removeSymlinksFor(skillName) {
104
140
  let removed = 0;
105
141
  const expectedTarget = resolve(getSkillDir(skillName));
@@ -27,6 +27,7 @@ async function applyUpdate(skillName, remote, provider) {
27
27
  return files;
28
28
  }
29
29
  export async function updateCommand(skills, opts) {
30
+ /* v8 ignore next */
30
31
  if (!opts.json)
31
32
  banner();
32
33
  if (skills.length === 0 && !opts.all) {
@@ -34,10 +35,12 @@ export async function updateCommand(skills, opts) {
34
35
  console.log(JSON.stringify({ error: "Specify a skill name or use --all" }));
35
36
  }
36
37
  else {
38
+ /* v8 ignore start */
37
39
  console.log(ui.error(" Specify a skill name or use --all"));
38
40
  console.log(ui.dim(" Usage: arcana update <skill> [skill2 ...]"));
39
41
  console.log(ui.dim(" arcana update --all"));
40
42
  console.log();
43
+ /* v8 ignore stop */
41
44
  }
42
45
  process.exit(1);
43
46
  }
@@ -47,8 +50,10 @@ export async function updateCommand(skills, opts) {
47
50
  console.log(JSON.stringify({ updated: [], upToDate: [], failed: [] }));
48
51
  }
49
52
  else {
53
+ /* v8 ignore start */
50
54
  console.log(ui.dim(" No skills installed."));
51
55
  console.log();
56
+ /* v8 ignore stop */
52
57
  }
53
58
  return;
54
59
  }
@@ -77,8 +82,10 @@ async function updateOne(skillName, installDir, providerName, json, dryRun) {
77
82
  }));
78
83
  }
79
84
  else {
85
+ /* v8 ignore start */
80
86
  console.log(ui.error(` ${err instanceof Error ? err.message : "Invalid skill name"}`));
81
87
  console.log();
88
+ /* v8 ignore stop */
82
89
  }
83
90
  process.exit(1);
84
91
  }
@@ -88,8 +95,10 @@ async function updateOne(skillName, installDir, providerName, json, dryRun) {
88
95
  console.log(JSON.stringify({ updated: [], upToDate: [], failed: [skillName], error: "Not installed" }));
89
96
  }
90
97
  else {
98
+ /* v8 ignore start */
91
99
  console.log(ui.error(` Skill "${skillName}" is not installed.`));
92
100
  console.log();
101
+ /* v8 ignore stop */
93
102
  }
94
103
  process.exit(1);
95
104
  }
@@ -103,8 +112,10 @@ async function updateOne(skillName, installDir, providerName, json, dryRun) {
103
112
  console.log(JSON.stringify({ updated: [], upToDate: [], failed: [skillName], error: `Not found on ${providerName}` }));
104
113
  }
105
114
  else {
115
+ /* v8 ignore start */
106
116
  s.fail(`Skill "${skillName}" not found on ${providerName}`);
107
117
  console.log();
118
+ /* v8 ignore stop */
108
119
  }
109
120
  process.exit(1);
110
121
  }
@@ -114,8 +125,10 @@ async function updateOne(skillName, installDir, providerName, json, dryRun) {
114
125
  console.log(JSON.stringify({ updated: [], upToDate: [skillName], failed: [] }));
115
126
  }
116
127
  else {
128
+ /* v8 ignore start */
117
129
  s.info(`${ui.bold(skillName)} is already up to date (v${remote.version})`);
118
130
  console.log();
131
+ /* v8 ignore stop */
119
132
  }
120
133
  return;
121
134
  }
@@ -127,8 +140,10 @@ async function updateOne(skillName, installDir, providerName, json, dryRun) {
127
140
  }));
128
141
  }
129
142
  else {
143
+ /* v8 ignore start */
130
144
  s.info(`${ui.bold(skillName)} would be updated: v${meta?.version ?? "unknown"} -> v${remote.version}`);
131
145
  console.log();
146
+ /* v8 ignore stop */
132
147
  }
133
148
  return;
134
149
  }
@@ -138,8 +153,10 @@ async function updateOne(skillName, installDir, providerName, json, dryRun) {
138
153
  console.log(JSON.stringify({ updated: [skillName], upToDate: [], failed: [] }));
139
154
  }
140
155
  else {
156
+ /* v8 ignore start */
141
157
  s.succeed(`Updated ${ui.bold(skillName)} to v${remote.version} (${files.length} files)`);
142
158
  console.log();
159
+ /* v8 ignore stop */
143
160
  }
144
161
  }
145
162
  catch (err) {
@@ -152,10 +169,12 @@ async function updateOne(skillName, installDir, providerName, json, dryRun) {
152
169
  }));
153
170
  }
154
171
  else {
172
+ /* v8 ignore start */
155
173
  s.fail(`Failed to update ${skillName}`);
156
174
  if (err instanceof Error)
157
175
  console.error(ui.dim(` ${err.message}`));
158
176
  console.log();
177
+ /* v8 ignore stop */
159
178
  }
160
179
  process.exit(1);
161
180
  }
@@ -181,8 +200,10 @@ async function updateBatch(skillNames, installDir, providerName, json, dryRun) {
181
200
  }));
182
201
  }
183
202
  else {
203
+ /* v8 ignore start */
184
204
  console.log(ui.error(` ${err instanceof Error ? err.message : "Invalid skill name"}`));
185
205
  console.log();
206
+ /* v8 ignore stop */
186
207
  }
187
208
  process.exit(1);
188
209
  }
@@ -194,8 +215,10 @@ async function updateBatch(skillNames, installDir, providerName, json, dryRun) {
194
215
  console.log(JSON.stringify({ updated: [], upToDate: [], failed: [] }));
195
216
  }
196
217
  else {
218
+ /* v8 ignore start */
197
219
  console.log(ui.dim(" No skills installed."));
198
220
  console.log();
221
+ /* v8 ignore stop */
199
222
  }
200
223
  return;
201
224
  }
@@ -308,6 +331,7 @@ async function updateBatch(skillNames, installDir, providerName, json, dryRun) {
308
331
  }));
309
332
  }
310
333
  else {
334
+ /* v8 ignore start */
311
335
  s.stop();
312
336
  if (dryRunUpdates.length === 0) {
313
337
  console.log(ui.dim(" All skills are up to date."));
@@ -322,6 +346,7 @@ async function updateBatch(skillNames, installDir, providerName, json, dryRun) {
322
346
  }
323
347
  }
324
348
  console.log();
349
+ /* v8 ignore stop */
325
350
  }
326
351
  return;
327
352
  }
@@ -334,6 +359,7 @@ async function updateBatch(skillNames, installDir, providerName, json, dryRun) {
334
359
  }));
335
360
  }
336
361
  else {
362
+ /* v8 ignore start */
337
363
  s.succeed(`Update complete`);
338
364
  const parts = [`${updatedList.length} updated`, `${upToDateList.length} up to date`];
339
365
  if (skippedList.length > 0)
@@ -342,6 +368,7 @@ async function updateBatch(skillNames, installDir, providerName, json, dryRun) {
342
368
  parts.push(`${failedList.length} failed`);
343
369
  console.log(ui.dim(` ${parts.join(", ")}`));
344
370
  console.log();
371
+ /* v8 ignore stop */
345
372
  }
346
373
  if (failedList.length > 0)
347
374
  process.exit(1);
@@ -6,6 +6,7 @@ import { atomicWriteSync } from "../utils/atomic.js";
6
6
  import { ui, banner } from "../utils/ui.js";
7
7
  import { scanSkillContent } from "../utils/scanner.js";
8
8
  export async function validateCommand(skill, opts) {
9
+ /* v8 ignore next */
9
10
  if (!opts.json)
10
11
  banner();
11
12
  const baseDir = opts.source ? resolve(opts.source) : getInstallDir();
@@ -14,8 +15,10 @@ export async function validateCommand(skill, opts) {
14
15
  console.log(JSON.stringify({ results: [] }));
15
16
  }
16
17
  else {
18
+ /* v8 ignore start */
17
19
  console.log(ui.dim(" No skills installed."));
18
20
  console.log();
21
+ /* v8 ignore stop */
19
22
  }
20
23
  return;
21
24
  }
@@ -38,10 +41,12 @@ export async function validateCommand(skill, opts) {
38
41
  console.log(JSON.stringify({ error: "Specify a skill name or use --all" }));
39
42
  }
40
43
  else {
44
+ /* v8 ignore start */
41
45
  console.log(ui.error(" Specify a skill name or use --all"));
42
46
  console.log(ui.dim(" Usage: arcana validate <skill>"));
43
47
  console.log(ui.dim(" arcana validate --all [--fix]"));
44
48
  console.log();
49
+ /* v8 ignore stop */
45
50
  }
46
51
  process.exit(1);
47
52
  }
@@ -121,6 +126,7 @@ export async function validateCommand(skill, opts) {
121
126
  crossIssues = crossValidate(baseDir, marketplacePath);
122
127
  }
123
128
  else if (!opts.json) {
129
+ /* v8 ignore next */
124
130
  console.log(ui.warn(" Could not find marketplace.json for cross-validation"));
125
131
  }
126
132
  }
@@ -156,6 +162,7 @@ export async function validateCommand(skill, opts) {
156
162
  process.exit(1);
157
163
  return;
158
164
  }
165
+ /* v8 ignore start */
159
166
  // Human-readable output
160
167
  let passed = 0;
161
168
  let warned = 0;
@@ -212,4 +219,5 @@ export async function validateCommand(skill, opts) {
212
219
  console.log();
213
220
  if (failed > 0 || hasCrossErrors)
214
221
  process.exit(1);
222
+ /* v8 ignore stop */
215
223
  }
@@ -10,6 +10,7 @@ export async function verifyCommand(skillNames, opts) {
10
10
  if (opts.json) {
11
11
  return verifyJson(skillNames, opts);
12
12
  }
13
+ /* v8 ignore start */
13
14
  console.log(renderBanner());
14
15
  console.log();
15
16
  const installDir = getInstallDir();
@@ -77,6 +78,7 @@ export async function verifyCommand(skillNames, opts) {
77
78
  if (modifiedCount > 0) {
78
79
  process.exit(1);
79
80
  }
81
+ /* v8 ignore stop */
80
82
  }
81
83
  async function verifyJson(skillNames, opts) {
82
84
  const installDir = getInstallDir();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Output compression engine (RTK concept).
3
+ * 4-stage pipeline: filter -> group -> truncate -> dedup
4
+ */
5
+ export interface CompressRule {
6
+ name: string;
7
+ /** Tool names this rule applies to (e.g. "git", "npm") */
8
+ tools: string[];
9
+ /** Apply compression to lines of output */
10
+ compress(lines: string[]): string[];
11
+ }
12
+ export declare function registerRule(rule: CompressRule): void;
13
+ /** Run the full 4-stage compression pipeline. */
14
+ export declare function compress(input: string, tool?: string, maxLines?: number): string;
15
+ /** Calculate compression stats. */
16
+ export declare function compressionStats(original: string, compressed: string): {
17
+ originalTokens: number;
18
+ compressedTokens: number;
19
+ savedTokens: number;
20
+ savedPct: number;
21
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Output compression engine (RTK concept).
3
+ * 4-stage pipeline: filter -> group -> truncate -> dedup
4
+ */
5
+ const rules = [];
6
+ export function registerRule(rule) {
7
+ rules.push(rule);
8
+ }
9
+ /** Strip ANSI escape codes */
10
+ function stripAnsi(str) {
11
+ // eslint-disable-next-line no-control-regex
12
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
13
+ }
14
+ /** Stage 1: Filter - remove noise lines */
15
+ function filterLines(lines) {
16
+ return lines.filter((line) => {
17
+ const clean = stripAnsi(line).trim();
18
+ // Remove empty lines in sequences of 2+
19
+ if (clean === "")
20
+ return true; // keep single blanks, dedup handles runs
21
+ // Remove progress bars and spinners
22
+ if (/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏|\\/-]/.test(clean))
23
+ return false;
24
+ // Remove timing-only lines
25
+ if (/^\s*\d+(\.\d+)?\s*(ms|s|sec|seconds)\s*$/.test(clean))
26
+ return false;
27
+ return true;
28
+ });
29
+ }
30
+ /** Stage 2: Group - collapse similar consecutive lines */
31
+ function groupLines(lines) {
32
+ const result = [];
33
+ let lastPattern = "";
34
+ let count = 0;
35
+ for (const line of lines) {
36
+ // Normalize for grouping: strip numbers, hashes, timestamps
37
+ const pattern = stripAnsi(line)
38
+ .replace(/\b[0-9a-f]{7,40}\b/g, "<hash>")
39
+ .replace(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/g, "<time>")
40
+ .replace(/\d+/g, "<n>")
41
+ .trim();
42
+ if (pattern === lastPattern && count < 100) {
43
+ count++;
44
+ }
45
+ else {
46
+ if (count > 1) {
47
+ result.push(` ... (${count}x similar)`);
48
+ }
49
+ result.push(line);
50
+ lastPattern = pattern;
51
+ count = 1;
52
+ }
53
+ }
54
+ if (count > 1) {
55
+ result.push(` ... (${count}x similar)`);
56
+ }
57
+ return result;
58
+ }
59
+ /** Stage 3: Truncate - cap output length */
60
+ function truncateLines(lines, maxLines) {
61
+ if (lines.length <= maxLines)
62
+ return lines;
63
+ const head = lines.slice(0, Math.floor(maxLines * 0.6));
64
+ const tail = lines.slice(-Math.floor(maxLines * 0.3));
65
+ const omitted = lines.length - head.length - tail.length;
66
+ return [...head, `\n... ${omitted} lines omitted ...\n`, ...tail];
67
+ }
68
+ /** Stage 4: Dedup - collapse consecutive blank lines */
69
+ function dedupBlanks(lines) {
70
+ const result = [];
71
+ let lastBlank = false;
72
+ for (const line of lines) {
73
+ const isBlank = stripAnsi(line).trim() === "";
74
+ if (isBlank && lastBlank)
75
+ continue;
76
+ result.push(line);
77
+ lastBlank = isBlank;
78
+ }
79
+ return result;
80
+ }
81
+ /** Run the full 4-stage compression pipeline. */
82
+ export function compress(input, tool, maxLines = 80) {
83
+ let lines = input.split("\n");
84
+ // Apply tool-specific rules first
85
+ if (tool) {
86
+ const matching = rules.filter((r) => r.tools.includes(tool));
87
+ for (const rule of matching) {
88
+ lines = rule.compress(lines);
89
+ }
90
+ }
91
+ // Generic pipeline
92
+ lines = filterLines(lines);
93
+ lines = groupLines(lines);
94
+ lines = truncateLines(lines, maxLines);
95
+ lines = dedupBlanks(lines);
96
+ return lines.join("\n");
97
+ }
98
+ /** Calculate compression stats. */
99
+ export function compressionStats(original, compressed) {
100
+ // ~4 chars per token approximation
101
+ const originalTokens = Math.round(original.length / 4);
102
+ const compressedTokens = Math.round(compressed.length / 4);
103
+ const savedTokens = originalTokens - compressedTokens;
104
+ const savedPct = originalTokens > 0 ? Math.round((savedTokens / originalTokens) * 100) : 0;
105
+ return { originalTokens, compressedTokens, savedTokens, savedPct };
106
+ }
@@ -0,0 +1,7 @@
1
+ export { compress, compressionStats, registerRule } from "./engine.js";
2
+ export { recordCompression, getCompressionStats, resetCompressionStats } from "./tracker.js";
3
+ import "./rules/git.js";
4
+ import "./rules/npm.js";
5
+ import "./rules/tsc.js";
6
+ import "./rules/test-runner.js";
7
+ import "./rules/generic.js";
@@ -0,0 +1,10 @@
1
+ // Re-export compression engine and load all rules
2
+ export { compress, compressionStats, registerRule } from "./engine.js";
3
+ export { recordCompression, getCompressionStats, resetCompressionStats } from "./tracker.js";
4
+ // Load built-in rules (side-effect imports).
5
+ // Static imports are required for esbuild compatibility.
6
+ import "./rules/git.js";
7
+ import "./rules/npm.js";
8
+ import "./rules/tsc.js";
9
+ import "./rules/test-runner.js";
10
+ import "./rules/generic.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { registerRule } from "../engine.js";
2
+ registerRule({
3
+ name: "generic-cleanup",
4
+ tools: [], // Empty = applies to all tools as fallback via engine
5
+ /* v8 ignore next 3 -- intentional no-op placeholder */
6
+ compress(lines) {
7
+ return lines;
8
+ },
9
+ });
@@ -0,0 +1 @@
1
+ export {};