@sporesec/arcana 3.0.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const MCP_REGISTRY = [
|
|
2
|
+
{
|
|
3
|
+
name: "context7",
|
|
4
|
+
description: "Live version-specific documentation from source repos (Upstash)",
|
|
5
|
+
command: "npx",
|
|
6
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
7
|
+
envKeys: ["CONTEXT7_API_KEY"],
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: "filesystem",
|
|
11
|
+
description: "File system operations for AI agents (Anthropic)",
|
|
12
|
+
command: "npx",
|
|
13
|
+
args: ["-y", "@anthropic/mcp-filesystem"],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "memory",
|
|
17
|
+
description: "Persistent key-value memory for agents (Anthropic)",
|
|
18
|
+
command: "npx",
|
|
19
|
+
args: ["-y", "@anthropic/mcp-memory"],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
export function getServerDef(name) {
|
|
23
|
+
return MCP_REGISTRY.find((s) => s.name === name);
|
|
24
|
+
}
|
|
25
|
+
export function listRegistry() {
|
|
26
|
+
return [...MCP_REGISTRY];
|
|
27
|
+
}
|
package/dist/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ArcanaProvider } from "./providers/arcana.js";
|
|
2
|
+
import { AnthropicsProvider } from "./providers/anthropics.js";
|
|
2
3
|
import { GitHubProvider, validateSlug } from "./providers/github.js";
|
|
3
4
|
import { loadConfig } from "./utils/config.js";
|
|
4
5
|
import { errorAndExit } from "./utils/ui.js";
|
|
@@ -21,6 +22,9 @@ function createProvider(name, type, url) {
|
|
|
21
22
|
if (name === "arcana") {
|
|
22
23
|
provider = new ArcanaProvider();
|
|
23
24
|
}
|
|
25
|
+
else if (name === "anthropics") {
|
|
26
|
+
provider = new AnthropicsProvider();
|
|
27
|
+
}
|
|
24
28
|
else if (type === "github") {
|
|
25
29
|
const { owner, repo } = parseProviderSlug(url);
|
|
26
30
|
try {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface TrimResult {
|
|
2
|
+
originalLines: number;
|
|
3
|
+
trimmedLines: number;
|
|
4
|
+
originalBytes: number;
|
|
5
|
+
trimmedBytes: number;
|
|
6
|
+
savedBytes: number;
|
|
7
|
+
savedPct: number;
|
|
8
|
+
toolResultsTrimmed: number;
|
|
9
|
+
base64Removed: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Analyze a session JSONL for trimmable content.
|
|
13
|
+
* Returns stats without modifying the file.
|
|
14
|
+
*/
|
|
15
|
+
export declare function analyzeSession(filePath: string): TrimResult;
|
|
16
|
+
/**
|
|
17
|
+
* Trim a session JSONL and write trimmed copy.
|
|
18
|
+
* NEVER modifies the original file.
|
|
19
|
+
*/
|
|
20
|
+
export declare function trimSession(filePath: string): {
|
|
21
|
+
destPath: string;
|
|
22
|
+
result: TrimResult;
|
|
23
|
+
} | null;
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
/** Threshold for tool result bodies (chars). Results larger than this get stubbed. */
|
|
6
|
+
const RESULT_THRESHOLD = 500;
|
|
7
|
+
/**
|
|
8
|
+
* Analyze a session JSONL for trimmable content.
|
|
9
|
+
* Returns stats without modifying the file.
|
|
10
|
+
*/
|
|
11
|
+
export function analyzeSession(filePath) {
|
|
12
|
+
if (!existsSync(filePath)) {
|
|
13
|
+
return {
|
|
14
|
+
originalLines: 0,
|
|
15
|
+
trimmedLines: 0,
|
|
16
|
+
originalBytes: 0,
|
|
17
|
+
trimmedBytes: 0,
|
|
18
|
+
savedBytes: 0,
|
|
19
|
+
savedPct: 0,
|
|
20
|
+
toolResultsTrimmed: 0,
|
|
21
|
+
base64Removed: 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const content = readFileSync(filePath, "utf-8");
|
|
25
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
26
|
+
let toolResultsTrimmed = 0;
|
|
27
|
+
let base64Removed = 0;
|
|
28
|
+
let trimmedSize = 0;
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
try {
|
|
31
|
+
const msg = JSON.parse(line);
|
|
32
|
+
const role = msg.role;
|
|
33
|
+
// Tool results: stub if too large
|
|
34
|
+
if (role === "tool" || msg.type === "tool_result") {
|
|
35
|
+
const content = JSON.stringify(msg);
|
|
36
|
+
if (content.length > RESULT_THRESHOLD) {
|
|
37
|
+
toolResultsTrimmed++;
|
|
38
|
+
trimmedSize += 100; // stub size
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Base64 encoded content
|
|
43
|
+
const lineStr = JSON.stringify(msg);
|
|
44
|
+
if (lineStr.includes("base64,") || lineStr.includes('"type":"image"')) {
|
|
45
|
+
base64Removed++;
|
|
46
|
+
trimmedSize += 50; // stub size
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
trimmedSize += line.length;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
trimmedSize += line.length;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const originalBytes = content.length;
|
|
56
|
+
const savedBytes = originalBytes - trimmedSize;
|
|
57
|
+
const savedPct = originalBytes > 0 ? Math.round((savedBytes / originalBytes) * 100) : 0;
|
|
58
|
+
return {
|
|
59
|
+
originalLines: lines.length,
|
|
60
|
+
trimmedLines: lines.length - toolResultsTrimmed - base64Removed,
|
|
61
|
+
originalBytes,
|
|
62
|
+
trimmedBytes: trimmedSize,
|
|
63
|
+
savedBytes,
|
|
64
|
+
savedPct,
|
|
65
|
+
toolResultsTrimmed,
|
|
66
|
+
base64Removed,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Trim a session JSONL and write trimmed copy.
|
|
71
|
+
* NEVER modifies the original file.
|
|
72
|
+
*/
|
|
73
|
+
export function trimSession(filePath) {
|
|
74
|
+
if (!existsSync(filePath))
|
|
75
|
+
return null;
|
|
76
|
+
const content = readFileSync(filePath, "utf-8");
|
|
77
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
78
|
+
const trimmedLines = [];
|
|
79
|
+
let toolResultsTrimmed = 0;
|
|
80
|
+
let base64Removed = 0;
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
try {
|
|
83
|
+
const msg = JSON.parse(line);
|
|
84
|
+
const role = msg.role;
|
|
85
|
+
// Tool results: stub if too large
|
|
86
|
+
if (role === "tool" || msg.type === "tool_result") {
|
|
87
|
+
const msgStr = JSON.stringify(msg);
|
|
88
|
+
if (msgStr.length > RESULT_THRESHOLD) {
|
|
89
|
+
toolResultsTrimmed++;
|
|
90
|
+
// Replace with stub preserving structure
|
|
91
|
+
const stubbed = { ...msg, content: `[trimmed: ${Math.round(msgStr.length / 1024)}KB]` };
|
|
92
|
+
trimmedLines.push(JSON.stringify(stubbed));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Base64 encoded content
|
|
97
|
+
const lineStr = JSON.stringify(msg);
|
|
98
|
+
if (lineStr.includes("base64,") || lineStr.includes('"type":"image"')) {
|
|
99
|
+
base64Removed++;
|
|
100
|
+
const stubbed = { ...msg, content: "[base64 image removed]" };
|
|
101
|
+
trimmedLines.push(JSON.stringify(stubbed));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
trimmedLines.push(line);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
trimmedLines.push(line);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Write to ~/.arcana/trimmed/
|
|
111
|
+
const trimmedDir = join(homedir(), ".arcana", "trimmed");
|
|
112
|
+
if (!existsSync(trimmedDir))
|
|
113
|
+
mkdirSync(trimmedDir, { recursive: true });
|
|
114
|
+
const trimmedContent = trimmedLines.join("\n") + "\n";
|
|
115
|
+
const destPath = join(trimmedDir, `trimmed-${Date.now()}.jsonl`);
|
|
116
|
+
atomicWriteSync(destPath, trimmedContent);
|
|
117
|
+
const originalBytes = content.length;
|
|
118
|
+
const trimmedBytes = trimmedContent.length;
|
|
119
|
+
return {
|
|
120
|
+
destPath,
|
|
121
|
+
result: {
|
|
122
|
+
originalLines: lines.length,
|
|
123
|
+
trimmedLines: trimmedLines.length,
|
|
124
|
+
originalBytes,
|
|
125
|
+
trimmedBytes,
|
|
126
|
+
savedBytes: originalBytes - trimmedBytes,
|
|
127
|
+
savedPct: originalBytes > 0 ? Math.round(((originalBytes - trimmedBytes) / originalBytes) * 100) : 0,
|
|
128
|
+
toolResultsTrimmed,
|
|
129
|
+
base64Removed,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
package/dist/utils/cache.js
CHANGED
|
@@ -2,8 +2,8 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, statSync } from "node:
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { atomicWriteSync } from "./atomic.js";
|
|
5
|
+
import { CACHE_MAX_AGE_MS } from "../constants.js";
|
|
5
6
|
const CACHE_DIR = join(homedir(), ".arcana", "cache");
|
|
6
|
-
const DEFAULT_TTL = 60 * 60 * 1000; // 1 hour
|
|
7
7
|
function ensureCacheDir() {
|
|
8
8
|
if (!existsSync(CACHE_DIR)) {
|
|
9
9
|
mkdirSync(CACHE_DIR, { recursive: true });
|
|
@@ -12,7 +12,7 @@ function ensureCacheDir() {
|
|
|
12
12
|
function cacheFile(key) {
|
|
13
13
|
return join(CACHE_DIR, `${key}.json`);
|
|
14
14
|
}
|
|
15
|
-
export function readCache(key, maxAgeMs =
|
|
15
|
+
export function readCache(key, maxAgeMs = CACHE_MAX_AGE_MS) {
|
|
16
16
|
const file = cacheFile(key);
|
|
17
17
|
if (!existsSync(file))
|
|
18
18
|
return null;
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -3,5 +3,7 @@ import type { ArcanaConfig, ProviderConfig } from "../types.js";
|
|
|
3
3
|
export declare function validateConfig(config: ArcanaConfig): string[];
|
|
4
4
|
export declare function loadConfig(): ArcanaConfig;
|
|
5
5
|
export declare function saveConfig(config: ArcanaConfig): void;
|
|
6
|
+
/** Clear the config cache. Call from tests to ensure isolated state. */
|
|
7
|
+
export declare function clearConfigCache(): void;
|
|
6
8
|
export declare function addProvider(provider: ProviderConfig): void;
|
|
7
9
|
export declare function removeProvider(name: string): boolean;
|
package/dist/utils/config.js
CHANGED
|
@@ -4,6 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { ui } from "./ui.js";
|
|
5
5
|
import { atomicWriteSync } from "./atomic.js";
|
|
6
6
|
const CONFIG_PATH = join(homedir(), ".arcana", "config.json");
|
|
7
|
+
/** Module-level config cache. Avoids repeated disk reads during a single CLI invocation. */
|
|
8
|
+
let _cache = null;
|
|
7
9
|
/** Matches owner/repo slug format (e.g. "medy-gribkov/arcana") */
|
|
8
10
|
const SLUG_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
9
11
|
const DEFAULT_CONFIG = {
|
|
@@ -16,6 +18,12 @@ const DEFAULT_CONFIG = {
|
|
|
16
18
|
url: "medy-gribkov/arcana",
|
|
17
19
|
enabled: true,
|
|
18
20
|
},
|
|
21
|
+
{
|
|
22
|
+
name: "anthropics",
|
|
23
|
+
type: "github",
|
|
24
|
+
url: "anthropics/skills",
|
|
25
|
+
enabled: true,
|
|
26
|
+
},
|
|
19
27
|
],
|
|
20
28
|
};
|
|
21
29
|
function cloneConfig(config) {
|
|
@@ -42,23 +50,29 @@ export function validateConfig(config) {
|
|
|
42
50
|
return warnings;
|
|
43
51
|
}
|
|
44
52
|
export function loadConfig() {
|
|
53
|
+
if (_cache)
|
|
54
|
+
return cloneConfig(_cache);
|
|
55
|
+
let config;
|
|
45
56
|
if (!existsSync(CONFIG_PATH)) {
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
50
|
-
const loaded = JSON.parse(raw);
|
|
51
|
-
const config = {
|
|
52
|
-
...DEFAULT_CONFIG,
|
|
53
|
-
...loaded,
|
|
54
|
-
providers: loaded.providers ?? DEFAULT_CONFIG.providers.map((p) => ({ ...p })),
|
|
55
|
-
};
|
|
56
|
-
return applyEnvOverrides(config);
|
|
57
|
+
config = applyEnvOverrides(cloneConfig(DEFAULT_CONFIG));
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
else {
|
|
60
|
+
try {
|
|
61
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
62
|
+
const loaded = JSON.parse(raw);
|
|
63
|
+
config = applyEnvOverrides({
|
|
64
|
+
...DEFAULT_CONFIG,
|
|
65
|
+
...loaded,
|
|
66
|
+
providers: loaded.providers ?? DEFAULT_CONFIG.providers.map((p) => ({ ...p })),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
console.error(ui.warn(" Warning: Config file is corrupted, using defaults"));
|
|
71
|
+
config = applyEnvOverrides(cloneConfig(DEFAULT_CONFIG));
|
|
72
|
+
}
|
|
61
73
|
}
|
|
74
|
+
_cache = config;
|
|
75
|
+
return cloneConfig(config);
|
|
62
76
|
}
|
|
63
77
|
function applyEnvOverrides(base) {
|
|
64
78
|
const config = { ...base, providers: base.providers };
|
|
@@ -92,6 +106,11 @@ export function saveConfig(config) {
|
|
|
92
106
|
mkdirSync(dir, { recursive: true });
|
|
93
107
|
}
|
|
94
108
|
atomicWriteSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", 0o600);
|
|
109
|
+
_cache = null; // Invalidate cache on write
|
|
110
|
+
}
|
|
111
|
+
/** Clear the config cache. Call from tests to ensure isolated state. */
|
|
112
|
+
export function clearConfigCache() {
|
|
113
|
+
_cache = null;
|
|
95
114
|
}
|
|
96
115
|
export function addProvider(provider) {
|
|
97
116
|
const config = loadConfig();
|
package/dist/utils/help.js
CHANGED
|
@@ -4,7 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { ui } from "./ui.js";
|
|
7
|
-
import { getGroupedCommands } from "../command-
|
|
7
|
+
import { getGroupedCommands } from "../command-defs.js";
|
|
8
|
+
/* v8 ignore start */
|
|
8
9
|
const noColor = !!(process.env.NO_COLOR || process.env.TERM === "dumb");
|
|
9
10
|
function amberShade(hex, text) {
|
|
10
11
|
if (noColor)
|
|
@@ -33,27 +34,31 @@ export function renderBanner() {
|
|
|
33
34
|
}
|
|
34
35
|
return BANNER_LINES.map((line, i) => ` ${amberShade(AMBER_HEXES[i], line)}`).join("\n");
|
|
35
36
|
}
|
|
37
|
+
/* v8 ignore stop */
|
|
36
38
|
// Help groups: subset of registry for --help display (keeps output scannable)
|
|
37
39
|
const HELP_GROUPS = {
|
|
38
40
|
"GETTING STARTED": ["init", "doctor"],
|
|
39
|
-
SKILLS: ["list", "search", "
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
SKILLS: ["list", "search", "install", "update", "uninstall", "recommend"],
|
|
42
|
+
"CONTEXT INTELLIGENCE": ["curate", "compress", "remember", "recall", "mcp"],
|
|
43
|
+
SECURITY: ["scan", "verify", "lock"],
|
|
44
|
+
CONFIGURATION: ["config", "providers", "clean"],
|
|
42
45
|
};
|
|
43
46
|
const EXAMPLES = [
|
|
44
|
-
"$ arcana install
|
|
45
|
-
'$ arcana
|
|
46
|
-
"$ arcana
|
|
47
|
+
"$ arcana install --all && arcana curate",
|
|
48
|
+
'$ arcana remember "always use pnpm"',
|
|
49
|
+
"$ arcana compress git status --json",
|
|
50
|
+
"$ arcana init",
|
|
47
51
|
];
|
|
48
52
|
function padRight(str, width) {
|
|
49
53
|
return str + " ".repeat(Math.max(0, width - str.length));
|
|
50
54
|
}
|
|
55
|
+
/* v8 ignore start */
|
|
51
56
|
export function buildCustomHelp(version) {
|
|
52
57
|
const lines = [];
|
|
53
58
|
lines.push("");
|
|
54
59
|
lines.push(renderBanner());
|
|
55
60
|
lines.push("");
|
|
56
|
-
lines.push(` ${ui.bold("
|
|
61
|
+
lines.push(` ${ui.bold("Context intelligence for AI coding agents.")}${" ".repeat(11)}${ui.dim(`v${version}`)}`);
|
|
57
62
|
lines.push("");
|
|
58
63
|
lines.push(` ${ui.dim("USAGE")}`);
|
|
59
64
|
lines.push(" arcana <command> [options]");
|
|
@@ -81,6 +86,7 @@ export function buildCustomHelp(version) {
|
|
|
81
86
|
lines.push("");
|
|
82
87
|
return lines.join("\n");
|
|
83
88
|
}
|
|
89
|
+
/* v8 ignore stop */
|
|
84
90
|
const FIRST_RUN_FLAG = join(homedir(), ".arcana", ".initialized");
|
|
85
91
|
export function isFirstRun() {
|
|
86
92
|
return !existsSync(FIRST_RUN_FLAG);
|
|
@@ -92,6 +98,7 @@ export function markInitialized() {
|
|
|
92
98
|
}
|
|
93
99
|
writeFileSync(FIRST_RUN_FLAG, new Date().toISOString(), "utf-8");
|
|
94
100
|
}
|
|
101
|
+
/* v8 ignore start */
|
|
95
102
|
export function showWelcome(version) {
|
|
96
103
|
console.log();
|
|
97
104
|
console.log(renderBanner());
|
|
@@ -102,3 +109,4 @@ export function showWelcome(version) {
|
|
|
102
109
|
p.log.info("They install on-demand and only load when relevant, not all at once.");
|
|
103
110
|
console.log();
|
|
104
111
|
}
|
|
112
|
+
/* v8 ignore stop */
|
|
@@ -4,7 +4,7 @@ import { scanSkillContent } from "./scanner.js";
|
|
|
4
4
|
import { updateLockEntry } from "./integrity.js";
|
|
5
5
|
import { checkConflicts } from "./conflict-check.js";
|
|
6
6
|
import { detectProjectContext } from "./project-context.js";
|
|
7
|
-
import { LARGE_SKILL_KB_THRESHOLD, TOKENS_PER_KB } from "../constants.js";
|
|
7
|
+
import { LARGE_SKILL_KB_THRESHOLD, TOKENS_PER_KB, SKILL_NAME_REGEX } from "../constants.js";
|
|
8
8
|
/** Scan fetched files for security threats. Returns true if install should proceed. */
|
|
9
9
|
export function preInstallScan(_skillName, files, force) {
|
|
10
10
|
const skillMd = files.find((f) => f.path.endsWith("SKILL.md"));
|
|
@@ -41,6 +41,13 @@ export function preInstallConflictCheck(skillName, remote, files, force) {
|
|
|
41
41
|
* fetch -> security scan -> conflict check -> write files -> write meta -> update lock
|
|
42
42
|
*/
|
|
43
43
|
export async function installOneCore(skillName, provider, opts) {
|
|
44
|
+
if (!SKILL_NAME_REGEX.test(skillName)) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
skillName,
|
|
48
|
+
error: `Invalid skill name "${skillName}". Must match: lowercase alphanumeric with hyphens, 1-64 chars.`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
44
51
|
const files = await provider.fetch(skillName);
|
|
45
52
|
// Security scan
|
|
46
53
|
const scan = preInstallScan(skillName, files, opts.force);
|
|
@@ -81,6 +88,21 @@ export async function installOneCore(skillName, provider, opts) {
|
|
|
81
88
|
sizeBytes,
|
|
82
89
|
});
|
|
83
90
|
updateLockEntry(skillName, version, provider.name, files);
|
|
91
|
+
// Auto-regenerate skill index and active curation
|
|
92
|
+
try {
|
|
93
|
+
const { regenerateIndex } = await import("../commands/index.js");
|
|
94
|
+
regenerateIndex();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* non-critical, index regeneration is best-effort */
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const { regenerateActive } = await import("../commands/curate.js");
|
|
101
|
+
regenerateActive();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* non-critical, active curation is best-effort */
|
|
105
|
+
}
|
|
84
106
|
const sizeKB = sizeBytes / 1024;
|
|
85
107
|
return { success: true, skillName, files, sizeKB, conflictWarnings };
|
|
86
108
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Memory {
|
|
2
|
+
id: string;
|
|
3
|
+
content: string;
|
|
4
|
+
tags: string[];
|
|
5
|
+
project?: string;
|
|
6
|
+
created: string;
|
|
7
|
+
}
|
|
8
|
+
/** Add a memory. Extracts tags from content if not provided. */
|
|
9
|
+
export declare function addMemory(content: string, opts?: {
|
|
10
|
+
tags?: string[];
|
|
11
|
+
project?: string;
|
|
12
|
+
}): Memory;
|
|
13
|
+
/** Search memories by query (substring + tag match). */
|
|
14
|
+
export declare function searchMemories(query: string, opts?: {
|
|
15
|
+
project?: string;
|
|
16
|
+
}): Memory[];
|
|
17
|
+
/** List all memories, optionally filtered by project. */
|
|
18
|
+
export declare function listMemories(opts?: {
|
|
19
|
+
project?: string;
|
|
20
|
+
limit?: number;
|
|
21
|
+
}): Memory[];
|
|
22
|
+
/** Remove a memory by ID. */
|
|
23
|
+
export declare function removeMemory(id: string): boolean;
|
|
24
|
+
/** Get memories relevant to the current project for injection into _active.md */
|
|
25
|
+
export declare function getProjectMemories(project?: string): Memory[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { atomicWriteSync } from "./atomic.js";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
const MAX_MEMORIES = 200;
|
|
7
|
+
function memoriesPath() {
|
|
8
|
+
return join(homedir(), ".arcana", "memories.json");
|
|
9
|
+
}
|
|
10
|
+
function readMemories() {
|
|
11
|
+
const p = memoriesPath();
|
|
12
|
+
if (!existsSync(p))
|
|
13
|
+
return [];
|
|
14
|
+
try {
|
|
15
|
+
const data = JSON.parse(readFileSync(p, "utf-8"));
|
|
16
|
+
return Array.isArray(data) ? data : [];
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writeMemories(memories) {
|
|
23
|
+
const dir = join(homedir(), ".arcana");
|
|
24
|
+
if (!existsSync(dir))
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
atomicWriteSync(memoriesPath(), JSON.stringify(memories, null, 2));
|
|
27
|
+
}
|
|
28
|
+
function generateId() {
|
|
29
|
+
return randomBytes(4).toString("hex");
|
|
30
|
+
}
|
|
31
|
+
/** Add a memory. Extracts tags from content if not provided. */
|
|
32
|
+
export function addMemory(content, opts) {
|
|
33
|
+
const memories = readMemories();
|
|
34
|
+
const tags = opts?.tags ?? [];
|
|
35
|
+
// Auto-extract simple tags from content if none provided
|
|
36
|
+
if (tags.length === 0) {
|
|
37
|
+
const words = content.toLowerCase().split(/\s+/);
|
|
38
|
+
const keywords = words.filter((w) => w.length > 3 &&
|
|
39
|
+
!["always", "never", "should", "this", "that", "with", "from", "have", "will", "when", "then", "than"].includes(w));
|
|
40
|
+
tags.push(...keywords.slice(0, 3));
|
|
41
|
+
}
|
|
42
|
+
const project = opts?.project ?? basename(process.cwd());
|
|
43
|
+
const memory = {
|
|
44
|
+
id: generateId(),
|
|
45
|
+
content,
|
|
46
|
+
tags,
|
|
47
|
+
project,
|
|
48
|
+
created: new Date().toISOString(),
|
|
49
|
+
};
|
|
50
|
+
memories.push(memory);
|
|
51
|
+
// Cap at max
|
|
52
|
+
while (memories.length > MAX_MEMORIES)
|
|
53
|
+
memories.shift();
|
|
54
|
+
writeMemories(memories);
|
|
55
|
+
return memory;
|
|
56
|
+
}
|
|
57
|
+
/** Search memories by query (substring + tag match). */
|
|
58
|
+
export function searchMemories(query, opts) {
|
|
59
|
+
const memories = readMemories();
|
|
60
|
+
const q = query.toLowerCase();
|
|
61
|
+
return memories
|
|
62
|
+
.filter((m) => {
|
|
63
|
+
// Project filter
|
|
64
|
+
if (opts?.project && m.project !== opts.project)
|
|
65
|
+
return false;
|
|
66
|
+
// Content match
|
|
67
|
+
if (m.content.toLowerCase().includes(q))
|
|
68
|
+
return true;
|
|
69
|
+
// Tag match
|
|
70
|
+
if (m.tags.some((t) => t.toLowerCase().includes(q)))
|
|
71
|
+
return true;
|
|
72
|
+
return false;
|
|
73
|
+
})
|
|
74
|
+
.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
|
|
75
|
+
}
|
|
76
|
+
/** List all memories, optionally filtered by project. */
|
|
77
|
+
export function listMemories(opts) {
|
|
78
|
+
const memories = readMemories();
|
|
79
|
+
let filtered = memories;
|
|
80
|
+
if (opts?.project) {
|
|
81
|
+
filtered = filtered.filter((m) => m.project === opts.project);
|
|
82
|
+
}
|
|
83
|
+
filtered.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
|
|
84
|
+
if (opts?.limit) {
|
|
85
|
+
filtered = filtered.slice(0, opts.limit);
|
|
86
|
+
}
|
|
87
|
+
return filtered;
|
|
88
|
+
}
|
|
89
|
+
/** Remove a memory by ID. */
|
|
90
|
+
export function removeMemory(id) {
|
|
91
|
+
const memories = readMemories();
|
|
92
|
+
const idx = memories.findIndex((m) => m.id === id);
|
|
93
|
+
if (idx === -1)
|
|
94
|
+
return false;
|
|
95
|
+
memories.splice(idx, 1);
|
|
96
|
+
writeMemories(memories);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/** Get memories relevant to the current project for injection into _active.md */
|
|
100
|
+
export function getProjectMemories(project) {
|
|
101
|
+
const proj = project ?? basename(process.cwd());
|
|
102
|
+
return listMemories({ project: proj, limit: 10 });
|
|
103
|
+
}
|
|
@@ -129,6 +129,10 @@ function extractNpmTags(cwd) {
|
|
|
129
129
|
if (existsSync(join(cwd, "tsconfig.json")) || allDeps.typescript) {
|
|
130
130
|
tags.add("typescript");
|
|
131
131
|
}
|
|
132
|
+
// Detect package manager
|
|
133
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) {
|
|
134
|
+
tags.add("pnpm");
|
|
135
|
+
}
|
|
132
136
|
return [...tags];
|
|
133
137
|
}
|
|
134
138
|
function extractGoTags(cwd) {
|
package/dist/utils/scanner.d.ts
CHANGED
|
@@ -11,11 +11,32 @@ export interface ScanIssue {
|
|
|
11
11
|
line: number;
|
|
12
12
|
context: string;
|
|
13
13
|
}
|
|
14
|
+
export interface ScanOptions {
|
|
15
|
+
/** When true, scan all lines including BAD/DON'T blocks (no scope filtering). */
|
|
16
|
+
strict?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Scan SKILL.md content for security threats.
|
|
20
|
+
* Returns an array of issues sorted by severity (critical first).
|
|
21
|
+
* By default, findings inside BAD/DON'T example blocks are suppressed.
|
|
22
|
+
* Use strict mode to scan everything.
|
|
23
|
+
*/
|
|
24
|
+
export interface ScanResult {
|
|
25
|
+
issues: ScanIssue[];
|
|
26
|
+
suppressed: ScanIssue[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Scan with full result including suppressed findings.
|
|
30
|
+
* Used by scan command when --verbose is needed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function scanSkillContentFull(content: string, options?: ScanOptions): ScanResult;
|
|
14
33
|
/**
|
|
15
34
|
* Scan SKILL.md content for security threats.
|
|
16
35
|
* Returns an array of issues sorted by severity (critical first).
|
|
36
|
+
* By default, findings inside BAD/DON'T example blocks are suppressed.
|
|
37
|
+
* Use strict mode to scan everything.
|
|
17
38
|
*/
|
|
18
|
-
export declare function scanSkillContent(content: string): ScanIssue[];
|
|
39
|
+
export declare function scanSkillContent(content: string, options?: ScanOptions): ScanIssue[];
|
|
19
40
|
/**
|
|
20
41
|
* Quick check: does this content have any critical issues?
|
|
21
42
|
*/
|