@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/utils/scanner.js
CHANGED
|
@@ -270,20 +270,81 @@ const PATTERNS = [
|
|
|
270
270
|
},
|
|
271
271
|
];
|
|
272
272
|
// ---------------------------------------------------------------------------
|
|
273
|
-
//
|
|
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
|
-
*
|
|
277
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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,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": "
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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;
|
package/dist/command-registry.js
DELETED
|
@@ -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,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
|
-
}
|