@sporesec/arcana 3.0.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +25 -298
- package/dist/command-defs.d.ts +28 -0
- package/dist/command-defs.js +414 -0
- package/dist/commands/audit.js +18 -4
- package/dist/commands/clean.d.ts +1 -0
- package/dist/commands/clean.js +80 -0
- package/dist/commands/compress.d.ts +5 -0
- package/dist/commands/compress.js +38 -0
- package/dist/commands/config.js +40 -26
- package/dist/commands/create.js +2 -0
- package/dist/commands/curate.d.ts +39 -0
- package/dist/commands/curate.js +222 -0
- package/dist/commands/diff.js +2 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +61 -2
- package/dist/commands/import-cmd.js +5 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.js +107 -0
- package/dist/commands/info.js +19 -8
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +71 -0
- package/dist/commands/install.js +2 -0
- package/dist/commands/list.js +8 -0
- package/dist/commands/load.d.ts +10 -0
- package/dist/commands/load.js +130 -0
- package/dist/commands/lock.js +35 -24
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +87 -0
- package/dist/commands/outdated.js +8 -6
- package/dist/commands/providers.js +29 -21
- package/dist/commands/recommend.js +11 -3
- package/dist/commands/remember.d.ts +12 -0
- package/dist/commands/remember.js +111 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +46 -8
- package/dist/commands/search.js +6 -0
- package/dist/commands/uninstall.js +36 -0
- package/dist/commands/update.js +27 -0
- package/dist/commands/validate.js +8 -0
- package/dist/commands/verify.js +2 -0
- package/dist/compress/engine.d.ts +21 -0
- package/dist/compress/engine.js +106 -0
- package/dist/compress/index.d.ts +7 -0
- package/dist/compress/index.js +10 -0
- package/dist/compress/rules/generic.d.ts +1 -0
- package/dist/compress/rules/generic.js +9 -0
- package/dist/compress/rules/git.d.ts +1 -0
- package/dist/compress/rules/git.js +113 -0
- package/dist/compress/rules/npm.d.ts +1 -0
- package/dist/compress/rules/npm.js +99 -0
- package/dist/compress/rules/test-runner.d.ts +1 -0
- package/dist/compress/rules/test-runner.js +103 -0
- package/dist/compress/rules/tsc.d.ts +1 -0
- package/dist/compress/rules/tsc.js +39 -0
- package/dist/compress/tracker.d.ts +16 -0
- package/dist/compress/tracker.js +45 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +29 -0
- package/dist/interactive/helpers.js +1 -0
- package/dist/interactive/menu.js +6 -1
- package/dist/interactive/optimize-flow.js +4 -4
- package/dist/mcp/install.d.ts +10 -0
- package/dist/mcp/install.js +109 -0
- package/dist/mcp/registry.d.ts +11 -0
- package/dist/mcp/registry.js +27 -0
- package/dist/providers/anthropics.d.ts +4 -0
- package/dist/providers/anthropics.js +10 -0
- package/dist/registry.js +4 -0
- package/dist/session/trim.d.ts +23 -0
- package/dist/session/trim.js +132 -0
- package/dist/utils/cache.js +2 -2
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +33 -14
- package/dist/utils/help.js +16 -8
- package/dist/utils/install-core.js +23 -1
- package/dist/utils/memory.d.ts +25 -0
- package/dist/utils/memory.js +103 -0
- package/dist/utils/project-context.js +4 -0
- package/dist/utils/scanner.d.ts +22 -1
- package/dist/utils/scanner.js +81 -9
- package/dist/utils/sessions.d.ts +2 -0
- package/dist/utils/sessions.js +36 -0
- package/dist/utils/ui.js +5 -0
- package/dist/utils/usage.d.ts +17 -0
- package/dist/utils/usage.js +83 -0
- package/package.json +42 -7
- package/dist/command-registry.d.ts +0 -10
- package/dist/command-registry.js +0 -65
- package/dist/commands/benchmark.d.ts +0 -4
- package/dist/commands/benchmark.js +0 -178
- package/dist/commands/compact.d.ts +0 -6
- package/dist/commands/compact.js +0 -239
- package/dist/commands/optimize.d.ts +0 -3
- package/dist/commands/optimize.js +0 -356
- package/dist/commands/profile.d.ts +0 -3
- package/dist/commands/profile.js +0 -274
- package/dist/commands/stats.d.ts +0 -3
- package/dist/commands/stats.js +0 -210
- package/dist/commands/team.d.ts +0 -3
- package/dist/commands/team.js +0 -291
- package/dist/interactive.d.ts +0 -1
- package/dist/interactive.js +0 -841
package/dist/commands/doctor.js
CHANGED
|
@@ -1,9 +1,49 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync, openSync, readSync, closeSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, openSync, readSync, closeSync, mkdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { ui, banner, suggest } from "../utils/ui.js";
|
|
6
6
|
import { getInstallDir, getDirSize, listSymlinks, isOrphanedProject } from "../utils/fs.js";
|
|
7
|
+
/** Auto-fix a doctor check. Returns true if fixed. */
|
|
8
|
+
function autoFix(check) {
|
|
9
|
+
try {
|
|
10
|
+
switch (check.name) {
|
|
11
|
+
case "Skills directory": {
|
|
12
|
+
const dir = getInstallDir();
|
|
13
|
+
if (!existsSync(dir)) {
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
case "Symlinks": {
|
|
20
|
+
const broken = listSymlinks().filter((s) => s.broken);
|
|
21
|
+
for (const s of broken) {
|
|
22
|
+
try {
|
|
23
|
+
rmSync(s.fullPath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* skip */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return broken.length > 0;
|
|
30
|
+
}
|
|
31
|
+
case "Arcana config": {
|
|
32
|
+
const configPath = join(homedir(), ".arcana", "config.json");
|
|
33
|
+
const dir = join(homedir(), ".arcana");
|
|
34
|
+
if (!existsSync(dir))
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
writeFileSync(configPath, JSON.stringify({ defaultProvider: "arcana", providers: [] }, null, 2));
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
default:
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
7
47
|
function checkNodeVersion() {
|
|
8
48
|
const major = parseInt(process.version.slice(1));
|
|
9
49
|
if (major >= 18) {
|
|
@@ -300,10 +340,12 @@ export function runDoctorChecks() {
|
|
|
300
340
|
];
|
|
301
341
|
}
|
|
302
342
|
export async function doctorCommand(opts = {}) {
|
|
343
|
+
/* v8 ignore start */
|
|
303
344
|
if (!opts.json) {
|
|
304
345
|
banner();
|
|
305
346
|
console.log(ui.bold(" Environment Health Check\n"));
|
|
306
347
|
}
|
|
348
|
+
/* v8 ignore stop */
|
|
307
349
|
const checks = runDoctorChecks();
|
|
308
350
|
if (opts.json) {
|
|
309
351
|
console.log(JSON.stringify({
|
|
@@ -316,16 +358,32 @@ export async function doctorCommand(opts = {}) {
|
|
|
316
358
|
}, null, 2));
|
|
317
359
|
return;
|
|
318
360
|
}
|
|
361
|
+
/* v8 ignore start */
|
|
362
|
+
let fixed = 0;
|
|
319
363
|
for (const check of checks) {
|
|
320
364
|
const icon = check.status === "pass" ? ui.success("[OK]") : check.status === "warn" ? ui.warn("[!!]") : ui.error("[XX]");
|
|
321
365
|
console.log(` ${icon} ${ui.bold(check.name)}: ${check.message}`);
|
|
322
|
-
if
|
|
366
|
+
// Auto-fix if --fix flag and check has issues
|
|
367
|
+
if (opts.fix && check.status !== "pass" && check.fix) {
|
|
368
|
+
const wasFixed = autoFix(check);
|
|
369
|
+
if (wasFixed) {
|
|
370
|
+
console.log(ui.success(` Fixed automatically`));
|
|
371
|
+
fixed++;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
console.log(ui.dim(` Manual fix: ${check.fix}`));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (check.fix && !opts.fix) {
|
|
323
378
|
console.log(ui.dim(` Fix: ${check.fix}`));
|
|
324
379
|
}
|
|
325
380
|
}
|
|
326
381
|
const fails = checks.filter((c) => c.status === "fail").length;
|
|
327
382
|
const warns = checks.filter((c) => c.status === "warn").length;
|
|
328
383
|
console.log();
|
|
384
|
+
if (opts.fix && fixed > 0) {
|
|
385
|
+
console.log(ui.success(` Auto-fixed ${fixed} issue${fixed > 1 ? "s" : ""}`));
|
|
386
|
+
}
|
|
329
387
|
if (fails > 0) {
|
|
330
388
|
console.log(ui.error(` ${fails} issue${fails > 1 ? "s" : ""} found`));
|
|
331
389
|
}
|
|
@@ -339,4 +397,5 @@ export async function doctorCommand(opts = {}) {
|
|
|
339
397
|
if (fails === 0 && warns === 0) {
|
|
340
398
|
suggest("arcana list");
|
|
341
399
|
}
|
|
400
|
+
/* v8 ignore stop */
|
|
342
401
|
}
|
|
@@ -33,6 +33,7 @@ export async function importCommand(file, opts) {
|
|
|
33
33
|
console.log(JSON.stringify({ error: `File not found: ${file}` }));
|
|
34
34
|
}
|
|
35
35
|
else {
|
|
36
|
+
/* v8 ignore next */
|
|
36
37
|
console.error(`Error: File not found: ${file}`);
|
|
37
38
|
}
|
|
38
39
|
process.exit(1);
|
|
@@ -76,6 +77,7 @@ export async function importCommand(file, opts) {
|
|
|
76
77
|
failed.push(entry.name);
|
|
77
78
|
errors[entry.name] = msg;
|
|
78
79
|
if (!opts.json) {
|
|
80
|
+
/* v8 ignore next */
|
|
79
81
|
console.error(`Skipping ${entry.name}: ${msg}`);
|
|
80
82
|
}
|
|
81
83
|
continue;
|
|
@@ -105,6 +107,7 @@ export async function importCommand(file, opts) {
|
|
|
105
107
|
updateLockEntry(entry.name, version, provider.name, files);
|
|
106
108
|
installed.push(entry.name);
|
|
107
109
|
if (!opts.json) {
|
|
110
|
+
/* v8 ignore next */
|
|
108
111
|
console.log(`Installed ${entry.name}`);
|
|
109
112
|
}
|
|
110
113
|
}
|
|
@@ -113,6 +116,7 @@ export async function importCommand(file, opts) {
|
|
|
113
116
|
failed.push(entry.name);
|
|
114
117
|
errors[entry.name] = msg;
|
|
115
118
|
if (!opts.json) {
|
|
119
|
+
/* v8 ignore next */
|
|
116
120
|
console.error(`Failed to install ${entry.name}: ${msg}`);
|
|
117
121
|
}
|
|
118
122
|
}
|
|
@@ -124,6 +128,7 @@ export async function importCommand(file, opts) {
|
|
|
124
128
|
console.log(JSON.stringify(result));
|
|
125
129
|
}
|
|
126
130
|
else {
|
|
131
|
+
/* v8 ignore next */
|
|
127
132
|
console.log(`Import complete: ${installed.length} installed, ${skipped.length} skipped, ${failed.length} failed`);
|
|
128
133
|
}
|
|
129
134
|
if (failed.length > 0)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } 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 { INDEX_FILENAME } from "../constants.js";
|
|
6
|
+
import { ui, banner } from "../utils/ui.js";
|
|
7
|
+
/** Parse frontmatter description from a SKILL.md file. */
|
|
8
|
+
function extractDescription(skillDir) {
|
|
9
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
10
|
+
if (!existsSync(skillMd))
|
|
11
|
+
return "";
|
|
12
|
+
try {
|
|
13
|
+
const content = readFileSync(skillMd, "utf-8");
|
|
14
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
15
|
+
if (!match)
|
|
16
|
+
return "";
|
|
17
|
+
const fmLine = match[1].split("\n").find((l) => l.startsWith("description:"));
|
|
18
|
+
if (!fmLine)
|
|
19
|
+
return "";
|
|
20
|
+
// Strip quotes and trim
|
|
21
|
+
return fmLine
|
|
22
|
+
.replace(/^description:\s*/, "")
|
|
23
|
+
.replace(/^["']|["']$/g, "")
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Collect all installed skills with their descriptions. */
|
|
31
|
+
function collectSkillEntries() {
|
|
32
|
+
const installDir = getInstallDir();
|
|
33
|
+
if (!existsSync(installDir))
|
|
34
|
+
return [];
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const name of readdirSync(installDir).sort()) {
|
|
37
|
+
const skillDir = join(installDir, name);
|
|
38
|
+
try {
|
|
39
|
+
if (!statSync(skillDir).isDirectory())
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Skip index and loaded files
|
|
46
|
+
if (name.startsWith("_"))
|
|
47
|
+
continue;
|
|
48
|
+
const description = extractDescription(skillDir);
|
|
49
|
+
entries.push({ name, description });
|
|
50
|
+
}
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
/** Generate the index markdown content. */
|
|
54
|
+
function generateIndexContent(entries) {
|
|
55
|
+
const lines = [];
|
|
56
|
+
lines.push(`# Installed Skills (${entries.length})`);
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push("| Skill | Description |");
|
|
59
|
+
lines.push("|-------|-------------|");
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
// Truncate description to keep index compact
|
|
62
|
+
const desc = entry.description.length > 120 ? entry.description.slice(0, 117) + "..." : entry.description;
|
|
63
|
+
lines.push(`| ${entry.name} | ${desc} |`);
|
|
64
|
+
}
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push("To load a skill into context: `arcana load <skill-name>`");
|
|
67
|
+
lines.push("");
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
/** Regenerate the skill index file. Returns the number of skills indexed. */
|
|
71
|
+
export function regenerateIndex() {
|
|
72
|
+
const entries = collectSkillEntries();
|
|
73
|
+
const installDir = getInstallDir();
|
|
74
|
+
if (!existsSync(installDir))
|
|
75
|
+
return 0;
|
|
76
|
+
const indexPath = join(installDir, INDEX_FILENAME);
|
|
77
|
+
const content = generateIndexContent(entries);
|
|
78
|
+
atomicWriteSync(indexPath, content, 0o644);
|
|
79
|
+
return entries.length;
|
|
80
|
+
}
|
|
81
|
+
export async function indexCommand(opts) {
|
|
82
|
+
if (!opts.json) {
|
|
83
|
+
banner();
|
|
84
|
+
console.log(ui.bold(" Skill Index\n"));
|
|
85
|
+
}
|
|
86
|
+
const count = regenerateIndex();
|
|
87
|
+
const installDir = getInstallDir();
|
|
88
|
+
const indexPath = join(installDir, INDEX_FILENAME);
|
|
89
|
+
if (opts.json) {
|
|
90
|
+
console.log(JSON.stringify({
|
|
91
|
+
indexed: count,
|
|
92
|
+
path: indexPath,
|
|
93
|
+
}));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (count === 0) {
|
|
97
|
+
console.log(ui.dim(" No skills installed."));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(ui.success(` [OK]`) + ` Indexed ${count} skills`);
|
|
101
|
+
console.log(ui.dim(` Path: ${indexPath}`));
|
|
102
|
+
console.log();
|
|
103
|
+
console.log(ui.dim(" Agents load this index instead of all skills at once."));
|
|
104
|
+
console.log(ui.dim(" Use arcana load <skill> to load full skill content on demand."));
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
package/dist/commands/info.js
CHANGED
|
@@ -3,23 +3,27 @@ import { isSkillInstalled, readSkillMeta } from "../utils/fs.js";
|
|
|
3
3
|
import { getProviders } from "../registry.js";
|
|
4
4
|
import { validateSlug } from "../utils/validate.js";
|
|
5
5
|
export async function infoCommand(skillName, opts) {
|
|
6
|
+
/* v8 ignore start */
|
|
6
7
|
if (!opts.json) {
|
|
7
8
|
banner();
|
|
8
9
|
}
|
|
10
|
+
/* v8 ignore stop */
|
|
9
11
|
try {
|
|
10
12
|
validateSlug(skillName, "skill name");
|
|
11
13
|
}
|
|
12
14
|
catch (err) {
|
|
13
15
|
if (opts.json) {
|
|
14
16
|
console.log(JSON.stringify({ error: err instanceof Error ? err.message : "Invalid skill name" }));
|
|
17
|
+
process.exit(1);
|
|
15
18
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
19
|
+
/* v8 ignore start */
|
|
20
|
+
console.log(ui.error(` ${err instanceof Error ? err.message : "Invalid skill name"}`));
|
|
21
|
+
console.log();
|
|
20
22
|
process.exit(1);
|
|
23
|
+
/* v8 ignore stop */
|
|
21
24
|
}
|
|
22
25
|
const providers = getProviders(opts.provider);
|
|
26
|
+
/* v8 ignore next */
|
|
23
27
|
const s = opts.json ? noopSpinner() : spinner(`Looking up ${ui.bold(skillName)}...`);
|
|
24
28
|
s.start();
|
|
25
29
|
try {
|
|
@@ -48,6 +52,7 @@ export async function infoCommand(skillName, opts) {
|
|
|
48
52
|
}));
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
55
|
+
/* v8 ignore start */
|
|
51
56
|
console.log(ui.bold(` ${skill.name}`) + ui.dim(` v${skill.version}`));
|
|
52
57
|
if (installed) {
|
|
53
58
|
const meta = readSkillMeta(skillName);
|
|
@@ -86,6 +91,7 @@ export async function infoCommand(skillName, opts) {
|
|
|
86
91
|
console.log(ui.dim(` Install: `) + ui.cyan(`arcana install ${skill.name}`));
|
|
87
92
|
console.log();
|
|
88
93
|
return;
|
|
94
|
+
/* v8 ignore stop */
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
}
|
|
@@ -107,6 +113,7 @@ export async function infoCommand(skillName, opts) {
|
|
|
107
113
|
}));
|
|
108
114
|
return;
|
|
109
115
|
}
|
|
116
|
+
/* v8 ignore start */
|
|
110
117
|
console.log(ui.warn(" Showing cached data (offline)"));
|
|
111
118
|
console.log();
|
|
112
119
|
console.log(ui.bold(` ${skillName}`) + ui.dim(` v${meta?.version ?? "unknown"}`));
|
|
@@ -119,21 +126,25 @@ export async function infoCommand(skillName, opts) {
|
|
|
119
126
|
console.log(ui.dim(` Source: ${meta?.source ?? "local"}`));
|
|
120
127
|
console.log();
|
|
121
128
|
return;
|
|
129
|
+
/* v8 ignore stop */
|
|
122
130
|
}
|
|
123
131
|
if (opts.json) {
|
|
124
132
|
console.log(JSON.stringify({ error: err instanceof Error ? err.message : "Lookup failed" }));
|
|
125
133
|
process.exit(1);
|
|
126
134
|
}
|
|
135
|
+
/* v8 ignore start */
|
|
127
136
|
s.fail("Lookup failed due to a network or provider error.");
|
|
128
137
|
printErrorWithHint(err, true);
|
|
129
138
|
process.exit(1);
|
|
139
|
+
/* v8 ignore stop */
|
|
130
140
|
}
|
|
131
141
|
if (opts.json) {
|
|
132
142
|
console.log(JSON.stringify({ error: `Skill "${skillName}" not found` }));
|
|
143
|
+
process.exit(1);
|
|
133
144
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
145
|
+
/* v8 ignore start */
|
|
146
|
+
s.fail(`Skill "${skillName}" not found`);
|
|
147
|
+
console.log();
|
|
138
148
|
process.exit(1);
|
|
149
|
+
/* v8 ignore stop */
|
|
139
150
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
type ToolName = "claude" | "cursor" | "codex" | "gemini" | "antigravity" | "windsurf" | "aider";
|
|
1
2
|
interface ProjectInfo {
|
|
2
3
|
name: string;
|
|
3
4
|
type: string;
|
|
4
5
|
lang: string;
|
|
5
6
|
}
|
|
6
7
|
export declare function detectProject(cwd: string): ProjectInfo;
|
|
8
|
+
/** Detect which AI tools are already configured in this project. */
|
|
9
|
+
export declare function detectInstalledTools(cwd: string): ToolName[];
|
|
7
10
|
export declare const SKILL_SUGGESTIONS: Record<string, string[]>;
|
|
8
11
|
export declare const SKILL_SUGGESTIONS_DEFAULT: string[];
|
|
9
12
|
export declare function initCommand(opts: {
|
package/dist/commands/init.js
CHANGED
|
@@ -8,6 +8,26 @@ export function detectProject(cwd) {
|
|
|
8
8
|
const ctx = detectProjectContext(cwd);
|
|
9
9
|
return { name: ctx.name, type: ctx.type, lang: ctx.lang };
|
|
10
10
|
}
|
|
11
|
+
/** Detect which AI tools are already configured in this project. */
|
|
12
|
+
export function detectInstalledTools(cwd) {
|
|
13
|
+
const tools = [];
|
|
14
|
+
if (existsSync(join(cwd, "CLAUDE.md")))
|
|
15
|
+
tools.push("claude");
|
|
16
|
+
if (existsSync(join(cwd, ".cursor")))
|
|
17
|
+
tools.push("cursor");
|
|
18
|
+
if (existsSync(join(cwd, "AGENTS.md")))
|
|
19
|
+
tools.push("codex");
|
|
20
|
+
if (existsSync(join(cwd, "GEMINI.md")))
|
|
21
|
+
tools.push("gemini");
|
|
22
|
+
if (existsSync(join(cwd, ".windsurfrules")))
|
|
23
|
+
tools.push("windsurf");
|
|
24
|
+
if (existsSync(join(cwd, "AGENT.md")))
|
|
25
|
+
tools.push("antigravity");
|
|
26
|
+
if (existsSync(join(cwd, ".aider.conf.yml")))
|
|
27
|
+
tools.push("aider");
|
|
28
|
+
return tools;
|
|
29
|
+
}
|
|
30
|
+
/* v8 ignore start -- template functions used only in interactive initCommand */
|
|
11
31
|
function claudeTemplate(proj) {
|
|
12
32
|
return `# CLAUDE.md - ${proj.name}
|
|
13
33
|
|
|
@@ -15,6 +35,12 @@ function claudeTemplate(proj) {
|
|
|
15
35
|
- **Type:** ${proj.type}
|
|
16
36
|
- **Language:** ${proj.lang}
|
|
17
37
|
|
|
38
|
+
## Skills
|
|
39
|
+
Active skills curated at ~/.agents/skills/_active.md (budget-aware, project-specific).
|
|
40
|
+
Full index at ~/.agents/skills/_index.md.
|
|
41
|
+
Run \`arcana curate\` to refresh after project changes.
|
|
42
|
+
Run \`arcana load <skill>\` for additional skills on demand.
|
|
43
|
+
|
|
18
44
|
## Coding Preferences
|
|
19
45
|
- Follow existing patterns in the codebase
|
|
20
46
|
- Handle errors explicitly
|
|
@@ -51,6 +77,11 @@ function codexTemplate(proj) {
|
|
|
51
77
|
## Project
|
|
52
78
|
Type: ${proj.type} | Language: ${proj.lang}
|
|
53
79
|
|
|
80
|
+
## Skills
|
|
81
|
+
Active skills curated at ~/.agents/skills/_active.md (budget-aware, project-specific).
|
|
82
|
+
Full index at ~/.agents/skills/_index.md.
|
|
83
|
+
Run \`arcana curate\` to refresh. Run \`arcana load <skill>\` for on-demand loading.
|
|
84
|
+
|
|
54
85
|
## Sandbox
|
|
55
86
|
Codex runs in a sandboxed environment with no network access.
|
|
56
87
|
All dependencies must be pre-installed before the session.
|
|
@@ -132,6 +163,7 @@ const TOOL_FILES = {
|
|
|
132
163
|
windsurf: { path: ".windsurfrules", template: windsurfTemplate, label: "Windsurf" },
|
|
133
164
|
aider: { path: ".aider.conf.yml", template: aiderTemplate, label: "Aider" },
|
|
134
165
|
};
|
|
166
|
+
/* v8 ignore stop */
|
|
135
167
|
export const SKILL_SUGGESTIONS = {
|
|
136
168
|
Go: ["golang-pro", "go-linter-configuration", "testing-strategy", "security-review"],
|
|
137
169
|
Rust: ["rust-best-practices", "testing-strategy", "security-review"],
|
|
@@ -146,6 +178,7 @@ export const SKILL_SUGGESTIONS_DEFAULT = [
|
|
|
146
178
|
"codebase-dissection",
|
|
147
179
|
"testing-strategy",
|
|
148
180
|
];
|
|
181
|
+
/* v8 ignore start */
|
|
149
182
|
export async function initCommand(opts) {
|
|
150
183
|
console.log(renderBanner());
|
|
151
184
|
console.log();
|
|
@@ -251,5 +284,43 @@ export async function initCommand(opts) {
|
|
|
251
284
|
const suggestions = SKILL_SUGGESTIONS[proj.type] || SKILL_SUGGESTIONS_DEFAULT;
|
|
252
285
|
const skillList = suggestions.map((s) => `arcana install ${s}`).join("\n");
|
|
253
286
|
p.note(skillList, "Recommended skills");
|
|
287
|
+
// Offer context curation
|
|
288
|
+
const doCurate = await p.confirm({
|
|
289
|
+
message: "Run context curation? (auto-selects project-relevant skills within token budget)",
|
|
290
|
+
initialValue: true,
|
|
291
|
+
});
|
|
292
|
+
if (!p.isCancel(doCurate) && doCurate) {
|
|
293
|
+
try {
|
|
294
|
+
const { regenerateActive } = await import("./curate.js");
|
|
295
|
+
const result = regenerateActive();
|
|
296
|
+
p.log.success(`Curated ${result.selected.length} skills (${result.totalTokens.toLocaleString()} tokens)`);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
p.log.info("No skills installed yet. Run curate after installing skills.");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Offer MCP server setup
|
|
303
|
+
const doMcp = await p.confirm({
|
|
304
|
+
message: "Set up MCP servers? (Context7 for live docs, etc.)",
|
|
305
|
+
initialValue: false,
|
|
306
|
+
});
|
|
307
|
+
if (!p.isCancel(doMcp) && doMcp) {
|
|
308
|
+
try {
|
|
309
|
+
const { installMcpServer } = await import("../mcp/install.js");
|
|
310
|
+
const result = installMcpServer("context7", "claude", cwd);
|
|
311
|
+
if (result.installed) {
|
|
312
|
+
p.log.success("Context7 MCP configured");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
p.log.warn(`MCP setup: ${err instanceof Error ? err.message : "failed"}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Show detected tools
|
|
320
|
+
const detected = detectInstalledTools(cwd);
|
|
321
|
+
if (detected.length > 0) {
|
|
322
|
+
p.log.info(`Detected AI tools: ${detected.join(", ")}`);
|
|
323
|
+
}
|
|
254
324
|
p.outro(`Next: ${chalk.cyan("arcana install <skill>")} or ${chalk.cyan("arcana install --all")}`);
|
|
255
325
|
}
|
|
326
|
+
/* v8 ignore stop */
|
package/dist/commands/install.js
CHANGED
|
@@ -11,6 +11,7 @@ export async function installCommand(skillNames, opts) {
|
|
|
11
11
|
if (opts.json) {
|
|
12
12
|
return installJson(skillNames, opts);
|
|
13
13
|
}
|
|
14
|
+
/* v8 ignore start */
|
|
14
15
|
console.log(renderBanner());
|
|
15
16
|
console.log();
|
|
16
17
|
if (skillNames.length === 0 && !opts.all) {
|
|
@@ -220,6 +221,7 @@ async function installAllInteractive(providers, dryRun, force, noCheck) {
|
|
|
220
221
|
if (failed.length > 0)
|
|
221
222
|
process.exit(1);
|
|
222
223
|
}
|
|
224
|
+
/* v8 ignore stop */
|
|
223
225
|
async function installJson(skillNames, opts) {
|
|
224
226
|
if (skillNames.length === 0 && !opts.all) {
|
|
225
227
|
console.log(JSON.stringify({ installed: [], skipped: [], failed: [], error: "No skill specified" }));
|
package/dist/commands/list.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isSkillInstalled, getInstallDir, readSkillMeta } from "../utils/fs.js";
|
|
|
5
5
|
import { getProviders } from "../registry.js";
|
|
6
6
|
const DESC_TRUNCATE_LENGTH = 80;
|
|
7
7
|
export async function listCommand(opts) {
|
|
8
|
+
/* v8 ignore next */
|
|
8
9
|
if (!opts.json)
|
|
9
10
|
banner();
|
|
10
11
|
if (opts.installed) {
|
|
@@ -16,6 +17,7 @@ export async function listCommand(opts) {
|
|
|
16
17
|
for (const provider of providers)
|
|
17
18
|
provider.clearCache();
|
|
18
19
|
}
|
|
20
|
+
/* v8 ignore next */
|
|
19
21
|
const s = opts.json ? noopSpinner() : spinner("Fetching skills...");
|
|
20
22
|
s.start();
|
|
21
23
|
try {
|
|
@@ -39,6 +41,7 @@ export async function listCommand(opts) {
|
|
|
39
41
|
console.log(JSON.stringify({ skills }, null, 2));
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
44
|
+
/* v8 ignore start */
|
|
42
45
|
if (skills.length === 0) {
|
|
43
46
|
console.log(ui.dim(" No skills found."));
|
|
44
47
|
}
|
|
@@ -57,15 +60,18 @@ export async function listCommand(opts) {
|
|
|
57
60
|
table(rows);
|
|
58
61
|
}
|
|
59
62
|
console.log();
|
|
63
|
+
/* v8 ignore stop */
|
|
60
64
|
}
|
|
61
65
|
catch (err) {
|
|
62
66
|
if (opts.json) {
|
|
63
67
|
console.log(JSON.stringify({ error: err instanceof Error ? err.message : "Failed to fetch skills" }));
|
|
64
68
|
process.exit(1);
|
|
65
69
|
}
|
|
70
|
+
/* v8 ignore start */
|
|
66
71
|
s.fail("Failed to fetch skills");
|
|
67
72
|
printErrorWithHint(err, true);
|
|
68
73
|
process.exit(1);
|
|
74
|
+
/* v8 ignore stop */
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
function listInstalled(json) {
|
|
@@ -86,6 +92,7 @@ function listInstalled(json) {
|
|
|
86
92
|
console.log(JSON.stringify({ skills }, null, 2));
|
|
87
93
|
return;
|
|
88
94
|
}
|
|
95
|
+
/* v8 ignore start */
|
|
89
96
|
if (dirs.length === 0) {
|
|
90
97
|
console.log(ui.dim(" No skills installed."));
|
|
91
98
|
console.log();
|
|
@@ -105,4 +112,5 @@ function listInstalled(json) {
|
|
|
105
112
|
console.log();
|
|
106
113
|
table(rows);
|
|
107
114
|
console.log();
|
|
115
|
+
/* v8 ignore stop */
|
|
108
116
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Read full content of a skill (SKILL.md + references/ + rules/). */
|
|
2
|
+
export declare function readSkillContent(skillName: string): {
|
|
3
|
+
content: string;
|
|
4
|
+
files: number;
|
|
5
|
+
bytes: number;
|
|
6
|
+
} | null;
|
|
7
|
+
export declare function loadCommand(skillNames: string[], opts: {
|
|
8
|
+
json?: boolean;
|
|
9
|
+
append?: boolean;
|
|
10
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, 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 { LOADED_FILENAME } from "../constants.js";
|
|
6
|
+
import { ui, banner } from "../utils/ui.js";
|
|
7
|
+
import { recordLoad } from "../utils/usage.js";
|
|
8
|
+
/** Read full content of a skill (SKILL.md + references/ + rules/). */
|
|
9
|
+
export function readSkillContent(skillName) {
|
|
10
|
+
const installDir = getInstallDir();
|
|
11
|
+
const skillDir = join(installDir, skillName);
|
|
12
|
+
if (!existsSync(skillDir))
|
|
13
|
+
return null;
|
|
14
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
15
|
+
if (!existsSync(skillMd))
|
|
16
|
+
return null;
|
|
17
|
+
const parts = [];
|
|
18
|
+
let totalBytes = 0;
|
|
19
|
+
let fileCount = 0;
|
|
20
|
+
// Main SKILL.md
|
|
21
|
+
const mainContent = readFileSync(skillMd, "utf-8");
|
|
22
|
+
parts.push(mainContent);
|
|
23
|
+
totalBytes += mainContent.length;
|
|
24
|
+
fileCount++;
|
|
25
|
+
// References directory (if exists)
|
|
26
|
+
const refsDir = join(skillDir, "references");
|
|
27
|
+
if (existsSync(refsDir)) {
|
|
28
|
+
try {
|
|
29
|
+
for (const ref of readdirSync(refsDir).sort()) {
|
|
30
|
+
const refPath = join(refsDir, ref);
|
|
31
|
+
if (!statSync(refPath).isFile())
|
|
32
|
+
continue;
|
|
33
|
+
const refContent = readFileSync(refPath, "utf-8");
|
|
34
|
+
parts.push(`\n---\n\n## Reference: ${ref}\n\n${refContent}`);
|
|
35
|
+
totalBytes += refContent.length;
|
|
36
|
+
fileCount++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* skip unreadable refs */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Rules directory (if exists)
|
|
44
|
+
const rulesDir = join(skillDir, "rules");
|
|
45
|
+
if (existsSync(rulesDir)) {
|
|
46
|
+
try {
|
|
47
|
+
for (const rule of readdirSync(rulesDir).sort()) {
|
|
48
|
+
const rulePath = join(rulesDir, rule);
|
|
49
|
+
if (!statSync(rulePath).isFile())
|
|
50
|
+
continue;
|
|
51
|
+
const ruleContent = readFileSync(rulePath, "utf-8");
|
|
52
|
+
parts.push(`\n---\n\n## Rule: ${rule}\n\n${ruleContent}`);
|
|
53
|
+
totalBytes += ruleContent.length;
|
|
54
|
+
fileCount++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* skip unreadable rules */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { content: parts.join("\n"), files: fileCount, bytes: totalBytes };
|
|
62
|
+
}
|
|
63
|
+
export async function loadCommand(skillNames, opts) {
|
|
64
|
+
if (skillNames.length === 0) {
|
|
65
|
+
if (opts.json) {
|
|
66
|
+
console.log(JSON.stringify({ error: "Specify one or more skill names" }));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.error("Specify one or more skill names.");
|
|
70
|
+
console.error("Usage: arcana load <skill> [skill2 ...]");
|
|
71
|
+
console.error(" arcana load golang-pro typescript --append");
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const results = [];
|
|
76
|
+
const loadedParts = [];
|
|
77
|
+
for (const name of skillNames) {
|
|
78
|
+
const result = readSkillContent(name);
|
|
79
|
+
if (!result) {
|
|
80
|
+
results.push({ name, files: 0, bytes: 0, error: `Skill "${name}" not found` });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
results.push({ name, files: result.files, bytes: result.bytes });
|
|
84
|
+
loadedParts.push(result.content);
|
|
85
|
+
try {
|
|
86
|
+
recordLoad(name);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* best-effort */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (opts.json) {
|
|
93
|
+
console.log(JSON.stringify({
|
|
94
|
+
loaded: results.filter((r) => !r.error).map((r) => ({ name: r.name, files: r.files, bytes: r.bytes })),
|
|
95
|
+
failed: results.filter((r) => r.error).map((r) => ({ name: r.name, error: r.error })),
|
|
96
|
+
}));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (opts.append) {
|
|
100
|
+
// Write aggregated content to _loaded.md
|
|
101
|
+
const installDir = getInstallDir();
|
|
102
|
+
mkdirSync(installDir, { recursive: true });
|
|
103
|
+
const loadedPath = join(installDir, LOADED_FILENAME);
|
|
104
|
+
const content = loadedParts.join("\n\n---\n\n");
|
|
105
|
+
atomicWriteSync(loadedPath, content, 0o644);
|
|
106
|
+
banner();
|
|
107
|
+
console.log(ui.bold(" Load Skills\n"));
|
|
108
|
+
for (const r of results) {
|
|
109
|
+
if (r.error) {
|
|
110
|
+
console.log(` ${ui.warn("[!!]")} ${r.name}: ${r.error}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log(` ${ui.success("[OK]")} ${r.name} (${r.files} files, ${(r.bytes / 1024).toFixed(1)} KB)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
console.log(ui.dim(` Written to: ${loadedPath}`));
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Print to stdout for piping
|
|
122
|
+
for (const r of results) {
|
|
123
|
+
if (r.error) {
|
|
124
|
+
console.error(`Error: ${r.error}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
process.stdout.write(loadedParts.join("\n\n---\n\n"));
|
|
129
|
+
}
|
|
130
|
+
}
|