@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.
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
@@ -270,20 +270,81 @@ const PATTERNS = [
270
270
  },
271
271
  ];
272
272
  // ---------------------------------------------------------------------------
273
- // Scanner
273
+ // Scope detection: skip BAD/DON'T example blocks to reduce false positives
274
274
  // ---------------------------------------------------------------------------
275
+ /** Detect if a line enters or exits a "BAD example" scope. */
276
+ function isBadScopeStart(line) {
277
+ const trimmed = line.trim();
278
+ // Markdown headings: ### BAD, ### DON'T, ### Anti-pattern
279
+ if (/^#{1,4}\s+(?:BAD|DON'T|DONT|Anti-?pattern)/i.test(trimmed))
280
+ return true;
281
+ // Bold markers: **BAD**, **DON'T**
282
+ if (/^\*{2}(?:BAD|DON'T|DONT)\*{2}/i.test(trimmed))
283
+ return true;
284
+ // Code fence with bad label: ```bad, ```BAD
285
+ if (/^```\s*bad\b/i.test(trimmed))
286
+ return true;
287
+ // Inline label: BAD: or DON'T:
288
+ if (/^(?:BAD|DON'T|DONT)\s*:/i.test(trimmed))
289
+ return true;
290
+ return false;
291
+ }
292
+ function isBadScopeEnd(line, inCodeFence) {
293
+ const trimmed = line.trim();
294
+ // End of bad-labeled code fence
295
+ if (inCodeFence && trimmed === "```")
296
+ return true;
297
+ // New heading that isn't BAD
298
+ if (/^#{1,4}\s+/.test(trimmed) && !isBadScopeStart(trimmed))
299
+ return true;
300
+ // GOOD marker ends a BAD section
301
+ if (/^(?:\*{2}GOOD\*{2}|#{1,4}\s+GOOD)/i.test(trimmed))
302
+ return true;
303
+ return false;
304
+ }
275
305
  /**
276
- * Scan SKILL.md content for security threats.
277
- * Returns an array of issues sorted by severity (critical first).
306
+ * Build a set of line indices that are inside BAD/DON'T example blocks.
307
+ * These lines should have their findings suppressed (not scanned) in default mode.
278
308
  */
279
- export function scanSkillContent(content) {
309
+ function buildBadScopeSet(lines) {
310
+ const badLines = new Set();
311
+ let inBadScope = false;
312
+ let inBadCodeFence = false;
313
+ for (let i = 0; i < lines.length; i++) {
314
+ const line = lines[i];
315
+ const trimmed = line.trim();
316
+ if (!inBadScope) {
317
+ if (isBadScopeStart(line)) {
318
+ inBadScope = true;
319
+ inBadCodeFence = /^```\s*bad\b/i.test(trimmed);
320
+ badLines.add(i);
321
+ }
322
+ }
323
+ else {
324
+ badLines.add(i);
325
+ if (isBadScopeEnd(line, inBadCodeFence)) {
326
+ inBadScope = false;
327
+ inBadCodeFence = false;
328
+ }
329
+ }
330
+ }
331
+ return badLines;
332
+ }
333
+ /**
334
+ * Scan with full result including suppressed findings.
335
+ * Used by scan command when --verbose is needed.
336
+ */
337
+ export function scanSkillContentFull(content, options) {
280
338
  const issues = [];
339
+ const suppressed = [];
281
340
  const lines = content.split("\n");
341
+ const badScope = options?.strict ? new Set() : buildBadScopeSet(lines);
282
342
  for (let i = 0; i < lines.length; i++) {
283
343
  const line = lines[i];
344
+ const target = badScope.has(i) && !options?.strict ? suppressed : issues;
284
345
  for (const pattern of PATTERNS) {
285
346
  if (pattern.regex.test(line)) {
286
- issues.push({
347
+ target.push({
287
348
  level: pattern.level,
288
349
  category: pattern.category,
289
350
  detail: pattern.detail,
@@ -298,11 +359,13 @@ export function scanSkillContent(content) {
298
359
  const line = lines[i];
299
360
  if (line.endsWith("\\")) {
300
361
  const joined = line.slice(0, -1) + " " + (lines[i + 1] ?? "").trim();
362
+ const target = badScope.has(i) && !options?.strict ? suppressed : issues;
301
363
  for (const pattern of PATTERNS) {
302
364
  if (pattern.regex.test(joined)) {
303
365
  const alreadyFound = issues.some((iss) => iss.line === i + 1 && iss.category === pattern.category);
304
- if (!alreadyFound) {
305
- issues.push({
366
+ const alreadySuppressed = suppressed.some((iss) => iss.line === i + 1 && iss.category === pattern.category);
367
+ if (!alreadyFound && !alreadySuppressed) {
368
+ target.push({
306
369
  level: pattern.level,
307
370
  category: pattern.category,
308
371
  detail: pattern.detail,
@@ -314,10 +377,19 @@ export function scanSkillContent(content) {
314
377
  }
315
378
  }
316
379
  }
317
- // Sort: critical first, then high, then medium
318
380
  const order = { critical: 0, high: 1, medium: 2 };
319
381
  issues.sort((a, b) => order[a.level] - order[b.level]);
320
- return issues;
382
+ suppressed.sort((a, b) => order[a.level] - order[b.level]);
383
+ return { issues, suppressed };
384
+ }
385
+ /**
386
+ * Scan SKILL.md content for security threats.
387
+ * Returns an array of issues sorted by severity (critical first).
388
+ * By default, findings inside BAD/DON'T example blocks are suppressed.
389
+ * Use strict mode to scan everything.
390
+ */
391
+ export function scanSkillContent(content, options) {
392
+ return scanSkillContentFull(content, options).issues;
321
393
  }
322
394
  /**
323
395
  * Quick check: does this content have any critical issues?
@@ -0,0 +1,2 @@
1
+ /** Find the most recent session JSONL for the current project. */
2
+ export declare function findLatestSession(cwd: string): string | null;
@@ -0,0 +1,36 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ /** Find the most recent session JSONL for the current project. */
5
+ export function findLatestSession(cwd) {
6
+ const projectsDir = join(homedir(), ".claude", "projects");
7
+ if (!existsSync(projectsDir))
8
+ return null;
9
+ // Encode the project path the way Claude Code does
10
+ const encoded = cwd.replace(/[:/\\]/g, "-").replace(/^-+/, "");
11
+ const variants = [encoded, encoded.toLowerCase()];
12
+ for (const variant of variants) {
13
+ const projDir = join(projectsDir, variant);
14
+ if (!existsSync(projDir))
15
+ continue;
16
+ // Find newest .jsonl file
17
+ let newest = null;
18
+ try {
19
+ for (const file of readdirSync(projDir)) {
20
+ if (!file.endsWith(".jsonl"))
21
+ continue;
22
+ const fullPath = join(projDir, file);
23
+ const stat = statSync(fullPath);
24
+ if (!newest || stat.mtimeMs > newest.mtime) {
25
+ newest = { path: fullPath, mtime: stat.mtimeMs };
26
+ }
27
+ }
28
+ }
29
+ catch {
30
+ continue;
31
+ }
32
+ if (newest)
33
+ return newest.path;
34
+ }
35
+ return null;
36
+ }
package/dist/utils/ui.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import ora from "ora";
3
+ /* v8 ignore next 3 -- runtime color detection */
3
4
  if (process.env.NO_COLOR || process.env.TERM === "dumb") {
4
5
  chalk.level = 0;
5
6
  }
@@ -13,6 +14,7 @@ export const ui = {
13
14
  bold: (text) => chalk.bold(text),
14
15
  cyan: (text) => chalk.cyan(text),
15
16
  };
17
+ /* v8 ignore start -- display-only functions */
16
18
  export function banner() {
17
19
  console.log();
18
20
  console.log(ui.brand(" arcana") + ui.dim(" - universal agent skill manager"));
@@ -21,6 +23,7 @@ export function banner() {
21
23
  export function spinner(text) {
22
24
  return ora({ text, color: "yellow" });
23
25
  }
26
+ /* v8 ignore stop */
24
27
  export function noopSpinner() {
25
28
  return {
26
29
  start: () => { },
@@ -86,12 +89,14 @@ export function printErrorWithHint(err, showMessage = false) {
86
89
  }
87
90
  }
88
91
  }
92
+ /* v8 ignore start -- display-only suggestion */
89
93
  export function suggest(text) {
90
94
  if (!process.stdout.isTTY)
91
95
  return;
92
96
  console.log(ui.dim(" Next: ") + text);
93
97
  console.log();
94
98
  }
99
+ /* v8 ignore stop */
95
100
  export function errorAndExit(message, hint) {
96
101
  console.error();
97
102
  console.error(ui.error(" Error: ") + message);
@@ -0,0 +1,17 @@
1
+ export interface SkillUsage {
2
+ loads: number;
3
+ curations: number;
4
+ lastUsed: string;
5
+ firstUsed: string;
6
+ projects: string[];
7
+ }
8
+ /** Record a skill load event. */
9
+ export declare function recordLoad(skillName: string, project?: string): void;
10
+ /** Record a skill curation event. */
11
+ export declare function recordCuration(skillName: string): void;
12
+ /** Get usage data for all skills. */
13
+ export declare function getAllUsage(): Record<string, SkillUsage>;
14
+ /** Get skills not used in the last N days. */
15
+ export declare function getUnusedSkills(days: number): string[];
16
+ /** Get usage boost score for a skill (for curation ranking). */
17
+ export declare function getUsageBoost(skillName: string): number;
@@ -0,0 +1,83 @@
1
+ import { existsSync, readFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { atomicWriteSync } from "./atomic.js";
5
+ function usagePath() {
6
+ return join(homedir(), ".arcana", "usage.json");
7
+ }
8
+ function readUsage() {
9
+ const p = usagePath();
10
+ if (!existsSync(p))
11
+ return {};
12
+ try {
13
+ return JSON.parse(readFileSync(p, "utf-8"));
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ function writeUsage(data) {
20
+ const dir = join(homedir(), ".arcana");
21
+ if (!existsSync(dir))
22
+ mkdirSync(dir, { recursive: true });
23
+ atomicWriteSync(usagePath(), JSON.stringify(data, null, 2));
24
+ }
25
+ function ensureEntry(data, skillName) {
26
+ if (!data[skillName]) {
27
+ data[skillName] = {
28
+ loads: 0,
29
+ curations: 0,
30
+ lastUsed: new Date().toISOString(),
31
+ firstUsed: new Date().toISOString(),
32
+ projects: [],
33
+ };
34
+ }
35
+ return data[skillName];
36
+ }
37
+ /** Record a skill load event. */
38
+ export function recordLoad(skillName, project) {
39
+ const data = readUsage();
40
+ const entry = ensureEntry(data, skillName);
41
+ entry.loads++;
42
+ entry.lastUsed = new Date().toISOString();
43
+ if (project && !entry.projects.includes(project)) {
44
+ entry.projects.push(project);
45
+ }
46
+ writeUsage(data);
47
+ }
48
+ /** Record a skill curation event. */
49
+ export function recordCuration(skillName) {
50
+ const data = readUsage();
51
+ const entry = ensureEntry(data, skillName);
52
+ entry.curations++;
53
+ entry.lastUsed = new Date().toISOString();
54
+ writeUsage(data);
55
+ }
56
+ /** Get usage data for all skills. */
57
+ export function getAllUsage() {
58
+ return readUsage();
59
+ }
60
+ /** Get skills not used in the last N days. */
61
+ export function getUnusedSkills(days) {
62
+ const data = readUsage();
63
+ const threshold = Date.now() - days * 24 * 60 * 60 * 1000;
64
+ return Object.entries(data)
65
+ .filter(([, u]) => new Date(u.lastUsed).getTime() < threshold)
66
+ .map(([name]) => name);
67
+ }
68
+ /** Get usage boost score for a skill (for curation ranking). */
69
+ export function getUsageBoost(skillName) {
70
+ const data = readUsage();
71
+ const entry = data[skillName];
72
+ if (!entry)
73
+ return 0;
74
+ const daysSinceUse = (Date.now() - new Date(entry.lastUsed).getTime()) / (24 * 60 * 60 * 1000);
75
+ // Boost recently used skills, decay over 14 days
76
+ if (daysSinceUse < 1)
77
+ return 15;
78
+ if (daysSinceUse < 7)
79
+ return 10;
80
+ if (daysSinceUse < 14)
81
+ return 5;
82
+ return 0;
83
+ }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@sporesec/arcana",
3
- "version": "3.0.2",
4
- "description": "Universal AI development CLI. Skills, scaffolding, diagnostics, and analytics for every agent.",
3
+ "version": "4.0.0",
4
+ "description": "Context intelligence for AI coding agents. 74 skills, budget-aware curation, output compression, cross-session memory, MCP management. 7 platforms.",
5
5
  "bin": {
6
6
  "arcana": "dist/index.js"
7
7
  },
8
8
  "scripts": {
9
9
  "build": "tsc",
10
+ "build:types": "tsc --emitDeclarationOnly",
11
+ "build:check": "tsc --noEmit",
10
12
  "dev": "tsc --watch",
11
13
  "start": "node dist/index.js",
12
14
  "test": "vitest run",
@@ -21,17 +23,49 @@
21
23
  "keywords": [
22
24
  "agent-skills",
23
25
  "claude",
26
+ "claude-code",
24
27
  "cursor",
25
28
  "codex",
29
+ "gemini",
30
+ "windsurf",
31
+ "aider",
32
+ "antigravity",
26
33
  "ai",
34
+ "ai-agents",
27
35
  "skills",
28
36
  "cli",
29
- "diagnostics",
30
- "scaffolding",
31
- "analytics"
37
+ "context-management",
38
+ "token-optimization",
39
+ "mcp",
40
+ "developer-tools",
41
+ "automation",
42
+ "typescript",
43
+ "golang",
44
+ "python",
45
+ "react",
46
+ "nextjs",
47
+ "devops",
48
+ "security",
49
+ "testing",
50
+ "compression",
51
+ "curation"
52
+ ],
53
+ "license": "Apache-2.0",
54
+ "author": {
55
+ "name": "Medy Gribkov",
56
+ "email": "medy@sporesec.com",
57
+ "url": "https://sporesec.com"
58
+ },
59
+ "funding": [
60
+ {
61
+ "type": "github",
62
+ "url": "https://github.com/sponsors/medy-gribkov"
63
+ },
64
+ {
65
+ "type": "individual",
66
+ "url": "https://buymeacoffee.com/medygribkov"
67
+ }
32
68
  ],
33
- "license": "MIT",
34
- "author": "Mahdy Gribkov",
35
69
  "repository": {
36
70
  "type": "git",
37
71
  "url": "git+https://github.com/medy-gribkov/arcana.git",
@@ -65,6 +99,7 @@
65
99
  "@types/node": "^25.3.0",
66
100
  "@types/semver": "^7.7.1",
67
101
  "@vitest/coverage-v8": "^4.0.18",
102
+ "esbuild": "^0.27.3",
68
103
  "eslint": "^10.0.2",
69
104
  "prettier": "^3.8.1",
70
105
  "typescript": "^5.7.3",
@@ -1,10 +0,0 @@
1
- export interface CommandEntry {
2
- name: string;
3
- usage: string;
4
- description: string;
5
- group: string;
6
- }
7
- export declare function getCommandNames(): string[];
8
- export declare function getGroupedCommands(): Record<string, CommandEntry[]>;
9
- export declare function findClosestCommand(input: string): string | undefined;
10
- export declare function getCliReference(): string;
@@ -1,65 +0,0 @@
1
- const COMMANDS = [
2
- // Getting Started
3
- { name: "init", usage: "init", description: "Initialize arcana in current project", group: "GETTING STARTED" },
4
- { name: "doctor", usage: "doctor", description: "Check environment and diagnose issues", group: "GETTING STARTED" },
5
- // Skills
6
- { name: "list", usage: "list", description: "List available skills", group: "SKILLS" },
7
- { name: "search", usage: "search <query>", description: "Search across providers", group: "SKILLS" },
8
- { name: "info", usage: "info <skill>", description: "Show skill details", group: "SKILLS" },
9
- { name: "install", usage: "install [skills...]", description: "Install one or more skills", group: "SKILLS" },
10
- { name: "update", usage: "update [skills...]", description: "Update installed skills", group: "SKILLS" },
11
- { name: "uninstall", usage: "uninstall [skills...]", description: "Remove one or more skills", group: "SKILLS" },
12
- { name: "recommend", usage: "recommend", description: "Smart skill recommendations", group: "SKILLS" },
13
- // Development
14
- { name: "create", usage: "create <name>", description: "Create a new skill from template", group: "DEVELOPMENT" },
15
- { name: "validate", usage: "validate [skill]", description: "Validate skill structure", group: "DEVELOPMENT" },
16
- { name: "audit", usage: "audit [skill]", description: "Audit skill quality", group: "DEVELOPMENT" },
17
- // Security
18
- { name: "scan", usage: "scan [skill]", description: "Scan skills for security threats", group: "SECURITY" },
19
- { name: "verify", usage: "verify [skill]", description: "Verify skill integrity", group: "SECURITY" },
20
- { name: "lock", usage: "lock", description: "Generate or validate lockfile", group: "SECURITY" },
21
- // Inspection
22
- { name: "benchmark", usage: "benchmark [skill]", description: "Measure token cost", group: "INSPECTION" },
23
- { name: "diff", usage: "diff <skill>", description: "Show installed vs remote changes", group: "INSPECTION" },
24
- { name: "outdated", usage: "outdated", description: "List skills with newer versions", group: "INSPECTION" },
25
- // Configuration
26
- {
27
- name: "config",
28
- usage: "config [key] [val]",
29
- description: "View or modify configuration",
30
- group: "CONFIGURATION",
31
- },
32
- { name: "providers", usage: "providers", description: "Manage skill providers", group: "CONFIGURATION" },
33
- { name: "clean", usage: "clean", description: "Remove orphaned data", group: "CONFIGURATION" },
34
- { name: "compact", usage: "compact", description: "Remove agent logs", group: "CONFIGURATION" },
35
- { name: "stats", usage: "stats", description: "Show session analytics", group: "CONFIGURATION" },
36
- {
37
- name: "optimize",
38
- usage: "optimize",
39
- description: "Suggest token/performance improvements",
40
- group: "CONFIGURATION",
41
- },
42
- // Team & Workflow
43
- { name: "profile", usage: "profile [action]", description: "Manage skill profiles", group: "WORKFLOW" },
44
- { name: "team", usage: "team [action]", description: "Shared team skill config", group: "WORKFLOW" },
45
- { name: "export", usage: "export", description: "Export installed skills manifest", group: "WORKFLOW" },
46
- { name: "import", usage: "import <file>", description: "Import skills from manifest", group: "WORKFLOW" },
47
- { name: "completions", usage: "completions <shell>", description: "Generate shell completions", group: "WORKFLOW" },
48
- ];
49
- export function getCommandNames() {
50
- return COMMANDS.map((c) => c.name);
51
- }
52
- export function getGroupedCommands() {
53
- const groups = {};
54
- for (const cmd of COMMANDS) {
55
- (groups[cmd.group] ??= []).push(cmd);
56
- }
57
- return groups;
58
- }
59
- export function findClosestCommand(input) {
60
- const prefix = input.slice(0, 3).toLowerCase();
61
- return COMMANDS.find((c) => c.name.startsWith(prefix))?.name;
62
- }
63
- export function getCliReference() {
64
- return COMMANDS.map((c) => `arcana ${c.usage}`).join("\n");
65
- }
@@ -1,4 +0,0 @@
1
- export declare function benchmarkCommand(skill: string | undefined, opts: {
2
- all?: boolean;
3
- json?: boolean;
4
- }): Promise<void>;
@@ -1,178 +0,0 @@
1
- import { readdirSync, statSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { getInstallDir, readSkillMeta, getDirSize } from "../utils/fs.js";
4
- import { CONTEXT_WINDOW_TOKENS } from "../constants.js";
5
- function collectFiles(dir, prefix) {
6
- const entries = [];
7
- for (const entry of readdirSync(dir)) {
8
- const fullPath = join(dir, entry);
9
- const stat = statSync(fullPath);
10
- if (stat.isDirectory()) {
11
- entries.push(...collectFiles(fullPath, prefix ? `${prefix}/${entry}` : entry));
12
- }
13
- else {
14
- entries.push({ path: prefix ? `${prefix}/${entry}` : entry, sizeBytes: stat.size });
15
- }
16
- }
17
- return entries;
18
- }
19
- function benchmarkSkill(skillName) {
20
- const installDir = getInstallDir();
21
- const skillDir = join(installDir, skillName);
22
- try {
23
- statSync(skillDir);
24
- }
25
- catch {
26
- return null;
27
- }
28
- const files = collectFiles(skillDir, "");
29
- const totalBytes = getDirSize(skillDir);
30
- const estimatedTokens = Math.round(totalBytes / 4);
31
- const contextPercent = (estimatedTokens / CONTEXT_WINDOW_TOKENS) * 100;
32
- return {
33
- name: skillName,
34
- fileCount: files.length,
35
- totalBytes,
36
- estimatedTokens,
37
- contextPercent,
38
- files,
39
- };
40
- }
41
- function formatKB(bytes) {
42
- return (bytes / 1024).toFixed(1) + " KB";
43
- }
44
- function formatTokens(tokens) {
45
- if (tokens >= 1_000_000)
46
- return (tokens / 1_000_000).toFixed(1) + "M";
47
- if (tokens >= 1_000)
48
- return (tokens / 1_000).toFixed(1) + "k";
49
- return String(tokens);
50
- }
51
- export async function benchmarkCommand(skill, opts) {
52
- const installDir = getInstallDir();
53
- if (skill) {
54
- return benchmarkSingle(skill, opts.json);
55
- }
56
- if (opts.all) {
57
- return benchmarkAll(installDir, opts.json);
58
- }
59
- console.error("Specify a skill name or use --all to benchmark all installed skills.");
60
- console.error("Usage: arcana benchmark <skill-name>");
61
- console.error(" arcana benchmark --all");
62
- process.exit(1);
63
- }
64
- function benchmarkSingle(skillName, json) {
65
- const result = benchmarkSkill(skillName);
66
- if (!result) {
67
- if (json) {
68
- console.log(JSON.stringify({ error: `Skill "${skillName}" not found` }));
69
- }
70
- else {
71
- console.error(`Skill "${skillName}" is not installed.`);
72
- }
73
- process.exit(1);
74
- }
75
- const meta = readSkillMeta(skillName);
76
- if (json) {
77
- console.log(JSON.stringify({
78
- name: result.name,
79
- version: meta?.version ?? "unknown",
80
- fileCount: result.fileCount,
81
- totalBytes: result.totalBytes,
82
- estimatedTokens: result.estimatedTokens,
83
- contextPercent: Math.round(result.contextPercent * 100) / 100,
84
- files: result.files.map((f) => ({
85
- path: f.path,
86
- sizeBytes: f.sizeBytes,
87
- estimatedTokens: Math.round(f.sizeBytes / 4),
88
- })),
89
- }));
90
- return;
91
- }
92
- console.log();
93
- console.log(` Benchmark: ${skillName}${meta?.version ? ` v${meta.version}` : ""}`);
94
- console.log();
95
- console.log(` Files: ${result.fileCount}`);
96
- console.log(` Total size: ${formatKB(result.totalBytes)}`);
97
- console.log(` Est. tokens: ${formatTokens(result.estimatedTokens)}`);
98
- console.log(` Context usage: ${result.contextPercent.toFixed(2)}% of ${(CONTEXT_WINDOW_TOKENS / 1000).toFixed(0)}k window`);
99
- console.log();
100
- console.log(" File breakdown:");
101
- console.log();
102
- const sorted = [...result.files].sort((a, b) => b.sizeBytes - a.sizeBytes);
103
- const maxPathLen = Math.min(Math.max(...sorted.map((f) => f.path.length)), 50);
104
- for (const file of sorted) {
105
- const displayPath = file.path.length > 50 ? file.path.slice(0, 47) + "..." : file.path;
106
- const tokens = Math.round(file.sizeBytes / 4);
107
- console.log(` ${displayPath.padEnd(maxPathLen + 2)} ${formatKB(file.sizeBytes).padStart(10)} ~${formatTokens(tokens).padStart(6)} tokens`);
108
- }
109
- console.log();
110
- }
111
- function benchmarkAll(installDir, json) {
112
- let dirs;
113
- try {
114
- dirs = readdirSync(installDir).filter((d) => {
115
- try {
116
- return statSync(join(installDir, d)).isDirectory();
117
- }
118
- catch {
119
- return false;
120
- }
121
- });
122
- }
123
- catch {
124
- dirs = [];
125
- }
126
- if (dirs.length === 0) {
127
- if (json) {
128
- console.log(JSON.stringify({ skills: [], totalTokens: 0, totalBytes: 0 }));
129
- }
130
- else {
131
- console.log("No skills installed.");
132
- }
133
- return;
134
- }
135
- const results = [];
136
- for (const dir of dirs) {
137
- const result = benchmarkSkill(dir);
138
- if (result)
139
- results.push(result);
140
- }
141
- results.sort((a, b) => b.estimatedTokens - a.estimatedTokens);
142
- const totalBytes = results.reduce((sum, r) => sum + r.totalBytes, 0);
143
- const totalTokens = results.reduce((sum, r) => sum + r.estimatedTokens, 0);
144
- const totalContextPercent = (totalTokens / CONTEXT_WINDOW_TOKENS) * 100;
145
- if (json) {
146
- console.log(JSON.stringify({
147
- skills: results.map((r) => ({
148
- name: r.name,
149
- fileCount: r.fileCount,
150
- totalBytes: r.totalBytes,
151
- estimatedTokens: r.estimatedTokens,
152
- contextPercent: Math.round(r.contextPercent * 100) / 100,
153
- })),
154
- totalBytes,
155
- totalTokens,
156
- totalContextPercent: Math.round(totalContextPercent * 100) / 100,
157
- }));
158
- return;
159
- }
160
- console.log();
161
- console.log(` Benchmark: ${results.length} installed skill(s)`);
162
- console.log();
163
- const maxNameLen = Math.min(Math.max(...results.map((r) => r.name.length)), 30);
164
- console.log(` ${"Skill".padEnd(maxNameLen + 2)} ${"Files".padStart(5)} ${"Size".padStart(10)} ${"Tokens".padStart(8)} ${"Context %".padStart(9)}`);
165
- console.log(` ${"-".repeat(maxNameLen + 2)} ${"-".repeat(5)} ${"-".repeat(10)} ${"-".repeat(8)} ${"-".repeat(9)}`);
166
- for (const r of results) {
167
- const displayName = r.name.length > 30 ? r.name.slice(0, 27) + "..." : r.name;
168
- console.log(` ${displayName.padEnd(maxNameLen + 2)} ${String(r.fileCount).padStart(5)} ${formatKB(r.totalBytes).padStart(10)} ${formatTokens(r.estimatedTokens).padStart(8)} ${r.contextPercent.toFixed(2).padStart(8)}%`);
169
- }
170
- console.log(` ${"-".repeat(maxNameLen + 2)} ${"-".repeat(5)} ${"-".repeat(10)} ${"-".repeat(8)} ${"-".repeat(9)}`);
171
- console.log(` ${"TOTAL".padEnd(maxNameLen + 2)} ${String(results.reduce((s, r) => s + r.fileCount, 0)).padStart(5)} ${formatKB(totalBytes).padStart(10)} ${formatTokens(totalTokens).padStart(8)} ${totalContextPercent.toFixed(2).padStart(8)}%`);
172
- console.log();
173
- if (totalContextPercent > 50) {
174
- console.log(` Warning: installed skills consume ${totalContextPercent.toFixed(1)}% of context window.`);
175
- console.log(" Consider removing unused skills with: arcana uninstall <skill>");
176
- console.log();
177
- }
178
- }
@@ -1,6 +0,0 @@
1
- export declare function compactCommand(opts: {
2
- dryRun?: boolean;
3
- json?: boolean;
4
- prune?: boolean;
5
- pruneDays?: number;
6
- }): Promise<void>;