@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
@@ -0,0 +1,113 @@
1
+ import { registerRule } from "../engine.js";
2
+ registerRule({
3
+ name: "git-status",
4
+ tools: ["git"],
5
+ compress(lines) {
6
+ // Detect git status output and compact it
7
+ const isStatus = lines.some((l) => /^(On branch|Changes|Untracked|modified:|new file:|deleted:)/.test(l.trim()));
8
+ if (!isStatus)
9
+ return lines;
10
+ const result = [];
11
+ let branch = "";
12
+ const staged = [];
13
+ const unstaged = [];
14
+ const untracked = [];
15
+ let section = "none";
16
+ for (const line of lines) {
17
+ const trimmed = line.trim();
18
+ if (trimmed.startsWith("On branch ")) {
19
+ branch = trimmed.replace("On branch ", "");
20
+ }
21
+ else if (trimmed.startsWith("Changes to be committed")) {
22
+ section = "staged";
23
+ }
24
+ else if (trimmed.startsWith("Changes not staged")) {
25
+ section = "unstaged";
26
+ }
27
+ else if (trimmed.startsWith("Untracked files")) {
28
+ section = "untracked";
29
+ }
30
+ else if (/^\s*(modified|new file|deleted|renamed|copied):/.test(line)) {
31
+ const file = trimmed.replace(/^(modified|new file|deleted|renamed|copied):\s*/, "");
32
+ if (section === "staged")
33
+ staged.push(file);
34
+ else
35
+ unstaged.push(file);
36
+ }
37
+ else if (section === "untracked" && trimmed && !trimmed.startsWith("(") && !trimmed.startsWith("no changes")) {
38
+ untracked.push(trimmed);
39
+ }
40
+ }
41
+ if (branch)
42
+ result.push(`branch: ${branch}`);
43
+ if (staged.length > 0)
44
+ result.push(`staged (${staged.length}): ${staged.slice(0, 5).join(", ")}${staged.length > 5 ? ` +${staged.length - 5}` : ""}`);
45
+ if (unstaged.length > 0)
46
+ result.push(`modified (${unstaged.length}): ${unstaged.slice(0, 5).join(", ")}${unstaged.length > 5 ? ` +${unstaged.length - 5}` : ""}`);
47
+ if (untracked.length > 0)
48
+ result.push(`untracked (${untracked.length}): ${untracked.slice(0, 3).join(", ")}${untracked.length > 3 ? ` +${untracked.length - 3}` : ""}`);
49
+ if (result.length === 0)
50
+ result.push("clean");
51
+ return result.length > 0 ? result : lines;
52
+ },
53
+ });
54
+ registerRule({
55
+ name: "git-log",
56
+ tools: ["git"],
57
+ compress(lines) {
58
+ // Detect git log output (lines starting with "commit ")
59
+ const isLog = lines.some((l) => /^commit [0-9a-f]{40}$/.test(l.trim()));
60
+ if (!isLog)
61
+ return lines;
62
+ const result = [];
63
+ let currentHash = "";
64
+ let currentMsg = "";
65
+ for (const line of lines) {
66
+ const trimmed = line.trim();
67
+ if (/^commit [0-9a-f]{40}$/.test(trimmed)) {
68
+ if (currentHash && currentMsg) {
69
+ result.push(`${currentHash.slice(0, 7)} ${currentMsg}`);
70
+ }
71
+ currentHash = trimmed.replace("commit ", "");
72
+ currentMsg = "";
73
+ }
74
+ else if (trimmed &&
75
+ !trimmed.startsWith("Author:") &&
76
+ !trimmed.startsWith("Date:") &&
77
+ !trimmed.startsWith("Merge:")) {
78
+ if (!currentMsg)
79
+ currentMsg = trimmed;
80
+ }
81
+ }
82
+ if (currentHash && currentMsg) {
83
+ result.push(`${currentHash.slice(0, 7)} ${currentMsg}`);
84
+ }
85
+ return result.length > 0 ? result : lines;
86
+ },
87
+ });
88
+ registerRule({
89
+ name: "git-diff-stat",
90
+ tools: ["git"],
91
+ compress(lines) {
92
+ // Compact large diffs: keep stat summary, trim hunks
93
+ const statLine = lines.findIndex((l) => /^\s*\d+ files? changed/.test(l));
94
+ if (statLine === -1)
95
+ return lines;
96
+ // Keep everything up to and including the stat summary
97
+ const result = lines.slice(0, statLine + 1);
98
+ // After stat, only keep hunk headers and first 3 lines of each hunk
99
+ let hunkLines = 0;
100
+ for (let i = statLine + 1; i < lines.length; i++) {
101
+ const line = lines[i];
102
+ if (line.startsWith("diff --git") || line.startsWith("@@")) {
103
+ result.push(line);
104
+ hunkLines = 0;
105
+ }
106
+ else if (hunkLines < 3) {
107
+ result.push(line);
108
+ hunkLines++;
109
+ }
110
+ }
111
+ return result;
112
+ },
113
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,99 @@
1
+ import { registerRule } from "../engine.js";
2
+ registerRule({
3
+ name: "npm-install",
4
+ tools: ["npm", "pnpm", "yarn"],
5
+ compress(lines) {
6
+ // Compact npm/pnpm install output
7
+ const isInstall = lines.some((l) => /^(added|removed|up to date|Packages:|Progress:)/.test(l.trim()));
8
+ if (!isInstall)
9
+ return lines;
10
+ const result = [];
11
+ const warnings = [];
12
+ let summary = "";
13
+ for (const line of lines) {
14
+ const trimmed = line.trim();
15
+ // Keep summary lines
16
+ if (/^(added|removed|up to date|Done in)/.test(trimmed)) {
17
+ summary = trimmed;
18
+ }
19
+ // Keep dependency sections
20
+ else if (/^(dependencies|devDependencies|peerDependencies):/.test(trimmed)) {
21
+ result.push(trimmed);
22
+ }
23
+ // Keep actual package additions (+ package@version)
24
+ else if (trimmed.startsWith("+ ")) {
25
+ result.push(trimmed);
26
+ }
27
+ // Collect warnings
28
+ else if (/^(WARN|warn|npm warn)/.test(trimmed)) {
29
+ if (warnings.length < 3)
30
+ warnings.push(trimmed);
31
+ }
32
+ // Skip progress bars, http lines, timing
33
+ }
34
+ if (warnings.length > 0) {
35
+ result.push(`warnings (${warnings.length}): ${warnings[0]}`);
36
+ }
37
+ if (summary)
38
+ result.push(summary);
39
+ return result.length > 0 ? result : lines;
40
+ },
41
+ });
42
+ registerRule({
43
+ name: "npm-test",
44
+ tools: ["npm", "pnpm"],
45
+ compress(lines) {
46
+ // Compact test runner output: keep failures and summary
47
+ const hasSummary = lines.some((l) => /Tests?\s+\d+/.test(l) || /\d+ (passed|failed|skipped)/.test(l));
48
+ if (!hasSummary)
49
+ return lines;
50
+ const result = [];
51
+ const failures = [];
52
+ for (const line of lines) {
53
+ const trimmed = line.trim();
54
+ // Keep summary lines
55
+ if (/Tests?\s+\d+/.test(trimmed) || /\d+ (passed|failed|skipped)/.test(trimmed)) {
56
+ result.push(line);
57
+ }
58
+ // Keep test file results
59
+ else if (/^[✓✗×]|PASS|FAIL/.test(trimmed)) {
60
+ if (/FAIL|✗|×/.test(trimmed)) {
61
+ failures.push(line);
62
+ }
63
+ // Skip individual passing tests
64
+ }
65
+ // Keep error details
66
+ else if (/^(Error|AssertionError|Expected|Received|at )/.test(trimmed)) {
67
+ result.push(line);
68
+ }
69
+ // Keep duration
70
+ else if (/Duration|Time/.test(trimmed)) {
71
+ result.push(line);
72
+ }
73
+ }
74
+ // Show failures first, then summary
75
+ return [...failures, ...result].length > 0 ? [...failures, ...result] : lines;
76
+ },
77
+ });
78
+ registerRule({
79
+ name: "npm-audit",
80
+ tools: ["npm", "pnpm"],
81
+ compress(lines) {
82
+ const isAudit = lines.some((l) => /vulnerabilit/.test(l));
83
+ if (!isAudit)
84
+ return lines;
85
+ const result = [];
86
+ for (const line of lines) {
87
+ const trimmed = line.trim();
88
+ // Keep vulnerability summary
89
+ if (/\d+ vulnerabilit/.test(trimmed) || /found 0/.test(trimmed)) {
90
+ result.push(trimmed);
91
+ }
92
+ // Keep severity breakdown
93
+ else if (/^(critical|high|moderate|low)\s*\|?\s*\d+/.test(trimmed)) {
94
+ result.push(trimmed);
95
+ }
96
+ }
97
+ return result.length > 0 ? result : lines;
98
+ },
99
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,103 @@
1
+ import { registerRule } from "../engine.js";
2
+ registerRule({
3
+ name: "vitest",
4
+ tools: ["vitest"],
5
+ compress(lines) {
6
+ const isVitest = lines.some((l) => /Test Files|Tests\s+\d+/.test(l));
7
+ if (!isVitest)
8
+ return lines;
9
+ const result = [];
10
+ const failures = [];
11
+ for (const line of lines) {
12
+ const trimmed = line.trim();
13
+ // Keep file-level results
14
+ if (/^[✓✗×].*\.test\./.test(trimmed) || /FAIL/.test(trimmed)) {
15
+ if (/✗|×|FAIL/.test(trimmed))
16
+ failures.push(line);
17
+ }
18
+ // Keep summary
19
+ else if (/Test Files|Tests\s+\d+|Duration|Start at/.test(trimmed)) {
20
+ result.push(line);
21
+ }
22
+ // Keep assertion errors
23
+ else if (/^(AssertionError|Error|Expected|Received|expect\()/.test(trimmed)) {
24
+ result.push(line);
25
+ }
26
+ }
27
+ return [...failures, ...result].length > 0 ? [...failures, ...result] : lines;
28
+ },
29
+ });
30
+ registerRule({
31
+ name: "jest",
32
+ tools: ["jest"],
33
+ compress(lines) {
34
+ const isJest = lines.some((l) => /Test Suites:|Tests:/.test(l));
35
+ if (!isJest)
36
+ return lines;
37
+ const result = [];
38
+ const failures = [];
39
+ for (const line of lines) {
40
+ const trimmed = line.trim();
41
+ if (/^(FAIL|PASS)/.test(trimmed)) {
42
+ if (trimmed.startsWith("FAIL"))
43
+ failures.push(line);
44
+ }
45
+ else if (/Test Suites:|Tests:|Snapshots:|Time:/.test(trimmed)) {
46
+ result.push(line);
47
+ }
48
+ else if (/●/.test(line)) {
49
+ // Jest failure markers
50
+ failures.push(line);
51
+ }
52
+ }
53
+ return [...failures, ...result].length > 0 ? [...failures, ...result] : lines;
54
+ },
55
+ });
56
+ registerRule({
57
+ name: "pytest",
58
+ tools: ["pytest", "python"],
59
+ compress(lines) {
60
+ const isPytest = lines.some((l) => /passed|failed|error/.test(l) && /=+/.test(l));
61
+ if (!isPytest)
62
+ return lines;
63
+ const result = [];
64
+ for (const line of lines) {
65
+ const trimmed = line.trim();
66
+ // Keep summary line
67
+ if (/^=+.*=+$/.test(trimmed) && /(passed|failed|error|warning)/.test(trimmed)) {
68
+ result.push(line);
69
+ }
70
+ // Keep FAILED markers
71
+ else if (/^FAILED/.test(trimmed)) {
72
+ result.push(line);
73
+ }
74
+ // Keep short test results section
75
+ else if (/^(ERRORS|FAILURES|SHORT TEST SUMMARY)/.test(trimmed)) {
76
+ result.push(line);
77
+ }
78
+ }
79
+ return result.length > 0 ? result : lines;
80
+ },
81
+ });
82
+ registerRule({
83
+ name: "go-test",
84
+ tools: ["go"],
85
+ compress(lines) {
86
+ const isGoTest = lines.some((l) => /^(ok|FAIL|---)\s/.test(l.trim()));
87
+ if (!isGoTest)
88
+ return lines;
89
+ const result = [];
90
+ for (const line of lines) {
91
+ const trimmed = line.trim();
92
+ // Keep package results
93
+ if (/^(ok|FAIL)\s/.test(trimmed)) {
94
+ result.push(line);
95
+ }
96
+ // Keep individual test failures
97
+ else if (/^--- FAIL/.test(trimmed)) {
98
+ result.push(line);
99
+ }
100
+ }
101
+ return result.length > 0 ? result : lines;
102
+ },
103
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { registerRule } from "../engine.js";
2
+ registerRule({
3
+ name: "tsc-errors",
4
+ tools: ["tsc", "typescript"],
5
+ compress(lines) {
6
+ // Compact TypeScript compiler errors: group by file, show first error per file
7
+ const isTs = lines.some((l) => /\.tsx?:\d+:\d+/.test(l) || /error TS\d+/.test(l));
8
+ if (!isTs)
9
+ return lines;
10
+ const byFile = new Map();
11
+ let summary = "";
12
+ for (const line of lines) {
13
+ const match = line.match(/^(.+\.tsx?)[:(\s]+(\d+)/);
14
+ if (match) {
15
+ const file = match[1];
16
+ if (!byFile.has(file))
17
+ byFile.set(file, []);
18
+ byFile.get(file).push(line.trim());
19
+ }
20
+ if (/Found \d+ error/.test(line)) {
21
+ summary = line.trim();
22
+ }
23
+ }
24
+ const result = [];
25
+ for (const [file, errors] of byFile) {
26
+ result.push(`${file} (${errors.length} error${errors.length > 1 ? "s" : ""}):`);
27
+ // Show first 2 errors per file
28
+ for (const err of errors.slice(0, 2)) {
29
+ result.push(` ${err}`);
30
+ }
31
+ if (errors.length > 2) {
32
+ result.push(` ...+${errors.length - 2} more`);
33
+ }
34
+ }
35
+ if (summary)
36
+ result.push(summary);
37
+ return result.length > 0 ? result : lines;
38
+ },
39
+ });
@@ -0,0 +1,16 @@
1
+ interface ToolStats {
2
+ calls: number;
3
+ savedTokens: number;
4
+ }
5
+ interface CompressionStats {
6
+ totalInputTokens: number;
7
+ totalOutputTokens: number;
8
+ totalSaved: number;
9
+ byTool: Record<string, ToolStats>;
10
+ }
11
+ export declare function recordCompression(tool: string, inputTokens: number, outputTokens: number): void;
12
+ export declare function getCompressionStats(): CompressionStats & {
13
+ savingsPct: number;
14
+ };
15
+ export declare function resetCompressionStats(): void;
16
+ export {};
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { atomicWriteSync } from "../utils/atomic.js";
5
+ function statsPath() {
6
+ return join(homedir(), ".arcana", "compression-stats.json");
7
+ }
8
+ function readStats() {
9
+ const p = statsPath();
10
+ if (!existsSync(p)) {
11
+ return { totalInputTokens: 0, totalOutputTokens: 0, totalSaved: 0, byTool: {} };
12
+ }
13
+ try {
14
+ return JSON.parse(readFileSync(p, "utf-8"));
15
+ }
16
+ catch {
17
+ return { totalInputTokens: 0, totalOutputTokens: 0, totalSaved: 0, byTool: {} };
18
+ }
19
+ }
20
+ function writeStats(stats) {
21
+ const dir = join(homedir(), ".arcana");
22
+ if (!existsSync(dir))
23
+ mkdirSync(dir, { recursive: true });
24
+ atomicWriteSync(statsPath(), JSON.stringify(stats, null, 2));
25
+ }
26
+ export function recordCompression(tool, inputTokens, outputTokens) {
27
+ const stats = readStats();
28
+ stats.totalInputTokens += inputTokens;
29
+ stats.totalOutputTokens += outputTokens;
30
+ stats.totalSaved += inputTokens - outputTokens;
31
+ if (!stats.byTool[tool]) {
32
+ stats.byTool[tool] = { calls: 0, savedTokens: 0 };
33
+ }
34
+ stats.byTool[tool].calls++;
35
+ stats.byTool[tool].savedTokens += inputTokens - outputTokens;
36
+ writeStats(stats);
37
+ }
38
+ export function getCompressionStats() {
39
+ const stats = readStats();
40
+ const savingsPct = stats.totalInputTokens > 0 ? Math.round((stats.totalSaved / stats.totalInputTokens) * 100) : 0;
41
+ return { ...stats, savingsPct };
42
+ }
43
+ export function resetCompressionStats() {
44
+ writeStats({ totalInputTokens: 0, totalOutputTokens: 0, totalSaved: 0, byTool: {} });
45
+ }
@@ -7,4 +7,16 @@ export declare const PRUNE_KEEP_NEWEST = 3;
7
7
  export declare const LARGE_SKILL_KB_THRESHOLD = 50;
8
8
  export declare const TOKENS_PER_KB = 256;
9
9
  export declare const CONTEXT_WINDOW_TOKENS = 200000;
10
+ export declare const INDEX_FILENAME = "_index.md";
11
+ export declare const LOADED_FILENAME = "_loaded.md";
12
+ export declare const ACTIVE_FILENAME = "_active.md";
13
+ export declare const CONTEXT_BUDGET_PCT = 30;
14
+ export declare const MODEL_CONTEXTS: Record<string, number>;
15
+ export declare const SKILL_NAME_REGEX: RegExp;
16
+ export declare const SKILL_MAX_LINES = 300;
17
+ export declare const JACCARD_THRESHOLD = 0.5;
18
+ export declare const CACHE_MAX_AGE_MS: number;
19
+ export declare const MEMORY_MAX_LINES = 200;
20
+ export declare const AGENT_BLOAT_PERCENT = 70;
21
+ export declare const DISK_WARN_BYTES: number;
10
22
  export declare const DESCRIPTION_TRUNCATION = 50;
package/dist/constants.js CHANGED
@@ -9,5 +9,34 @@ export const PRUNE_KEEP_NEWEST = 3;
9
9
  export const LARGE_SKILL_KB_THRESHOLD = 50;
10
10
  export const TOKENS_PER_KB = 256;
11
11
  export const CONTEXT_WINDOW_TOKENS = 200_000;
12
+ // Progressive disclosure
13
+ export const INDEX_FILENAME = "_index.md";
14
+ export const LOADED_FILENAME = "_loaded.md";
15
+ export const ACTIVE_FILENAME = "_active.md";
16
+ // Context curation budget
17
+ export const CONTEXT_BUDGET_PCT = 30; // Max % of model context for skills
18
+ export const MODEL_CONTEXTS = {
19
+ // Claude (March 2026): 200K standard, 1M beta for Opus/Sonnet
20
+ "claude-opus-4.6": 200_000,
21
+ "claude-sonnet-4.6": 200_000,
22
+ "claude-haiku-4.5": 200_000,
23
+ // OpenAI (March 2026)
24
+ "gpt-5.4": 1_000_000,
25
+ // Google Gemini (March 2026)
26
+ "gemini-3.1-pro": 1_000_000,
27
+ "gemini-3.1-flash": 1_000_000,
28
+ "gemini-3.1-thinking": 1_000_000,
29
+ default: 200_000,
30
+ };
31
+ // Validation
32
+ export const SKILL_NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/;
33
+ export const SKILL_MAX_LINES = 300;
34
+ export const JACCARD_THRESHOLD = 0.5;
35
+ // Cache
36
+ export const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
37
+ // Diagnostics thresholds
38
+ export const MEMORY_MAX_LINES = 200;
39
+ export const AGENT_BLOAT_PERCENT = 70;
40
+ export const DISK_WARN_BYTES = 1 * 1024 * 1024 * 1024; // 1 GB
12
41
  // Display
13
42
  export const DESCRIPTION_TRUNCATION = 50;
@@ -85,6 +85,7 @@ export function buildMenuOptions(installedCount, _availableCount) {
85
85
  if (!isNew) {
86
86
  options.push({ value: "setup", label: "Get Started", hint: "detect project, add more skills" });
87
87
  }
88
+ options.push({ value: "curate", label: "Curate context", hint: "auto-select skills for token budget" });
88
89
  options.push({ value: "health", label: "Health check" });
89
90
  options.push({ value: "optimize", label: "Token budget" });
90
91
  options.push({ value: "ref", label: "CLI reference" });
@@ -3,7 +3,7 @@ import chalk from "chalk";
3
3
  import { renderBanner } from "../utils/help.js";
4
4
  import { loadConfig } from "../utils/config.js";
5
5
  import { getProviders, clearProviderCache } from "../registry.js";
6
- import { getCliReference } from "../command-registry.js";
6
+ import { getCliReference } from "../command-defs.js";
7
7
  import { AMBER, countInstalled, buildMenuOptions } from "./helpers.js";
8
8
  import { SKILL_CATEGORIES } from "./categories.js";
9
9
  import { browseByCategory } from "./browse.js";
@@ -97,6 +97,11 @@ export async function showInteractiveMenu(version) {
97
97
  case "optimize":
98
98
  await optimizeInteractive();
99
99
  break;
100
+ case "curate": {
101
+ const { curateCommand } = await import("../commands/curate.js");
102
+ await curateCommand({});
103
+ break;
104
+ }
100
105
  case "ref":
101
106
  p.note(getCliReference(), "CLI Reference");
102
107
  break;
@@ -24,13 +24,13 @@ export async function optimizeInteractive() {
24
24
  const action = await p.select({
25
25
  message: "What next?",
26
26
  options: [
27
- { value: "full", label: "Run full optimization report" },
27
+ { value: "doctor", label: "Run doctor report" },
28
28
  { value: "__back", label: "Back" },
29
29
  ],
30
30
  });
31
31
  handleCancel(action);
32
- if (action === "full") {
33
- const { optimizeCommand } = await import("../commands/optimize.js");
34
- await optimizeCommand({ json: false });
32
+ if (action === "doctor") {
33
+ const { doctorCommand } = await import("../commands/doctor.js");
34
+ await doctorCommand({ json: false });
35
35
  }
36
36
  }
@@ -0,0 +1,10 @@
1
+ /** Install an MCP server into the appropriate tool config. */
2
+ export declare function installMcpServer(name: string, tool: "claude" | "cursor", cwd: string): {
3
+ installed: boolean;
4
+ path: string;
5
+ error?: string;
6
+ };
7
+ /** List MCP servers configured in a tool's config. */
8
+ export declare function listConfiguredServers(tool: "claude" | "cursor", cwd: string): string[];
9
+ /** Remove an MCP server from tool config. */
10
+ export declare function removeMcpServer(name: string, tool: "claude" | "cursor", cwd: string): boolean;
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { atomicWriteSync } from "../utils/atomic.js";
5
+ import { getServerDef } from "./registry.js";
6
+ /** Get the path to Claude Code's MCP config file. */
7
+ function getClaudeMcpPath() {
8
+ return join(homedir(), ".claude.json");
9
+ }
10
+ /** Get the path to Cursor's MCP config file. */
11
+ function getCursorMcpPath(cwd) {
12
+ return join(cwd, ".cursor", "mcp.json");
13
+ }
14
+ /** Read existing MCP config from a JSON file. */
15
+ function readMcpConfig(filePath) {
16
+ if (!existsSync(filePath))
17
+ return { mcpServers: {} };
18
+ try {
19
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
20
+ return {
21
+ mcpServers: data.mcpServers ?? {},
22
+ };
23
+ }
24
+ catch {
25
+ return { mcpServers: {} };
26
+ }
27
+ }
28
+ /** Write MCP server config to a tool's config file. */
29
+ function writeMcpServer(filePath, name, def) {
30
+ // Read existing config, preserving all other keys
31
+ let fullConfig = {};
32
+ if (existsSync(filePath)) {
33
+ try {
34
+ fullConfig = JSON.parse(readFileSync(filePath, "utf-8"));
35
+ }
36
+ catch {
37
+ fullConfig = {};
38
+ }
39
+ }
40
+ if (!fullConfig.mcpServers)
41
+ fullConfig.mcpServers = {};
42
+ const servers = fullConfig.mcpServers;
43
+ const entry = {
44
+ command: def.command,
45
+ args: def.args,
46
+ };
47
+ // Add env placeholders for keys that need configuration
48
+ if (def.envKeys && def.envKeys.length > 0) {
49
+ const env = {};
50
+ for (const key of def.envKeys) {
51
+ env[key] = process.env[key] ?? `<your-${key.toLowerCase().replace(/_/g, "-")}>`;
52
+ }
53
+ entry.env = env;
54
+ }
55
+ servers[name] = entry;
56
+ const dir = join(filePath, "..");
57
+ if (!existsSync(dir))
58
+ mkdirSync(dir, { recursive: true });
59
+ atomicWriteSync(filePath, JSON.stringify(fullConfig, null, 2));
60
+ }
61
+ /** Install an MCP server into the appropriate tool config. */
62
+ export function installMcpServer(name, tool, cwd) {
63
+ const def = getServerDef(name);
64
+ if (!def) {
65
+ return {
66
+ installed: false,
67
+ path: "",
68
+ error: `Unknown MCP server: ${name}. Use 'arcana mcp list' to see available servers.`,
69
+ };
70
+ }
71
+ const filePath = tool === "claude" ? getClaudeMcpPath() : getCursorMcpPath(cwd);
72
+ // Check if already installed
73
+ const existing = readMcpConfig(filePath);
74
+ if (existing.mcpServers[name]) {
75
+ return { installed: true, path: filePath, error: `${name} already configured in ${filePath}` };
76
+ }
77
+ try {
78
+ writeMcpServer(filePath, name, def);
79
+ return { installed: true, path: filePath };
80
+ }
81
+ catch (err) {
82
+ return { installed: false, path: filePath, error: err instanceof Error ? err.message : "Write failed" };
83
+ }
84
+ }
85
+ /** List MCP servers configured in a tool's config. */
86
+ export function listConfiguredServers(tool, cwd) {
87
+ const filePath = tool === "claude" ? getClaudeMcpPath() : getCursorMcpPath(cwd);
88
+ const config = readMcpConfig(filePath);
89
+ return Object.keys(config.mcpServers);
90
+ }
91
+ /** Remove an MCP server from tool config. */
92
+ export function removeMcpServer(name, tool, cwd) {
93
+ const filePath = tool === "claude" ? getClaudeMcpPath() : getCursorMcpPath(cwd);
94
+ if (!existsSync(filePath))
95
+ return false;
96
+ let fullConfig;
97
+ try {
98
+ fullConfig = JSON.parse(readFileSync(filePath, "utf-8"));
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ const servers = fullConfig.mcpServers;
104
+ if (!servers || !servers[name])
105
+ return false;
106
+ delete servers[name];
107
+ atomicWriteSync(filePath, JSON.stringify(fullConfig, null, 2));
108
+ return true;
109
+ }
@@ -0,0 +1,11 @@
1
+ /** Built-in registry of recommended MCP servers. */
2
+ export interface McpServerDef {
3
+ name: string;
4
+ description: string;
5
+ command: string;
6
+ args: string[];
7
+ envKeys?: string[];
8
+ }
9
+ export declare const MCP_REGISTRY: McpServerDef[];
10
+ export declare function getServerDef(name: string): McpServerDef | undefined;
11
+ export declare function listRegistry(): McpServerDef[];