@danielblomma/cortex-mcp 2.0.9 → 2.0.13
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/package.json +1 -1
- package/scaffold/mcp/package.json +2 -1
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +43 -12
- package/scaffold/mcp/src/embed.ts +5 -10
- package/scaffold/mcp/src/loadGraph.ts +1 -7
- package/scaffold/mcp/src/paths.ts +39 -3
- package/scaffold/mcp/tests/paths.test.mjs +38 -0
- package/scaffold/scripts/embed.sh +1 -1
- package/scaffold/scripts/load-ryu.sh +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danielblomma/cortex-mcp",
|
|
3
3
|
"mcpName": "io.github.DanielBlomma/cortex",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.13",
|
|
5
5
|
"description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Daniel Blomma",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"graph:load": "npm run build --silent && node dist/loadGraph.js",
|
|
10
10
|
"dev": "node --loader ts-node/esm src/server.ts",
|
|
11
11
|
"start": "node dist/server.js",
|
|
12
|
-
"test": "npm run build --silent && node --test tests/*.test.mjs"
|
|
12
|
+
"test": "npm run build --silent && node --test tests/*.test.mjs",
|
|
13
|
+
"test:ci": "npm run build --silent && node --test --test-reporter=spec tests/*.test.mjs"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"@huggingface/transformers": "^4.1.0",
|
|
@@ -46,6 +46,7 @@ type ManifestEntry = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
type LocalSkillRecord = {
|
|
49
|
+
cli: SkillCli;
|
|
49
50
|
scope: string;
|
|
50
51
|
updated_at: string;
|
|
51
52
|
path: string;
|
|
@@ -88,7 +89,29 @@ function readState(): LocalSkillsState {
|
|
|
88
89
|
if (!existsSync(path)) return { skills: {} };
|
|
89
90
|
try {
|
|
90
91
|
const parsed = JSON.parse(readFileSync(path, "utf8")) as LocalSkillsState;
|
|
91
|
-
|
|
92
|
+
const normalizedSkills: Record<string, LocalSkillRecord> = {};
|
|
93
|
+
for (const [key, record] of Object.entries(parsed.skills ?? {})) {
|
|
94
|
+
if (!record || typeof record !== "object") continue;
|
|
95
|
+
const inferredCli =
|
|
96
|
+
record.path?.includes("/.codex/skills/")
|
|
97
|
+
? "codex"
|
|
98
|
+
: "claude";
|
|
99
|
+
const cli =
|
|
100
|
+
record.cli === "codex" || record.cli === "claude"
|
|
101
|
+
? record.cli
|
|
102
|
+
: inferredCli;
|
|
103
|
+
const normalizedKey = key.includes(":") ? key : `${cli}:${key}`;
|
|
104
|
+
normalizedSkills[normalizedKey] = {
|
|
105
|
+
cli,
|
|
106
|
+
scope: String(record.scope ?? "global"),
|
|
107
|
+
updated_at: String(record.updated_at ?? ""),
|
|
108
|
+
path: String(record.path ?? ""),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
skills: normalizedSkills,
|
|
113
|
+
last_synced_at: parsed.last_synced_at,
|
|
114
|
+
};
|
|
92
115
|
} catch {
|
|
93
116
|
return { skills: {} };
|
|
94
117
|
}
|
|
@@ -103,19 +126,22 @@ function writeState(state: LocalSkillsState): void {
|
|
|
103
126
|
}
|
|
104
127
|
|
|
105
128
|
/**
|
|
106
|
-
* Resolve the on-disk SKILL.md path for a skill. Global
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* Claude-only and lands in ~/.claude/skills.
|
|
129
|
+
* Resolve the on-disk SKILL.md path for a skill install target. Global
|
|
130
|
+
* skills are installed once per CLI, so the destination root depends on the
|
|
131
|
+
* active sync target rather than just the stored scope.
|
|
110
132
|
*/
|
|
111
|
-
function skillFilePath(
|
|
133
|
+
function skillFilePath(cli: SkillCli, name: string): string {
|
|
112
134
|
const root =
|
|
113
|
-
|
|
135
|
+
cli === "codex"
|
|
114
136
|
? join(homedir(), ".codex", "skills")
|
|
115
137
|
: join(homedir(), ".claude", "skills");
|
|
116
138
|
return join(root, name, "SKILL.md");
|
|
117
139
|
}
|
|
118
140
|
|
|
141
|
+
function stateSkillKey(cli: SkillCli, name: string): string {
|
|
142
|
+
return `${cli}:${name}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
119
145
|
function shouldSyncForCli(scope: string, cli: SkillCli): boolean {
|
|
120
146
|
if (scope === "global") return true;
|
|
121
147
|
return scope === `cli:${cli}`;
|
|
@@ -222,7 +248,8 @@ export async function runSkillSyncForCli(
|
|
|
222
248
|
|
|
223
249
|
// Detect adds + changes
|
|
224
250
|
for (const entry of relevantManifest) {
|
|
225
|
-
const
|
|
251
|
+
const skillKey = stateSkillKey(cli, entry.name);
|
|
252
|
+
const local = state.skills[skillKey];
|
|
226
253
|
const isNew = !local;
|
|
227
254
|
const isChanged =
|
|
228
255
|
Boolean(local) &&
|
|
@@ -243,7 +270,7 @@ export async function runSkillSyncForCli(
|
|
|
243
270
|
};
|
|
244
271
|
}
|
|
245
272
|
|
|
246
|
-
const path = skillFilePath(
|
|
273
|
+
const path = skillFilePath(cli, entry.name);
|
|
247
274
|
try {
|
|
248
275
|
writeSkillFile(path, body);
|
|
249
276
|
} catch (err) {
|
|
@@ -257,7 +284,8 @@ export async function runSkillSyncForCli(
|
|
|
257
284
|
};
|
|
258
285
|
}
|
|
259
286
|
|
|
260
|
-
state.skills[
|
|
287
|
+
state.skills[skillKey] = {
|
|
288
|
+
cli,
|
|
261
289
|
scope: entry.scope,
|
|
262
290
|
updated_at: entry.updated_at,
|
|
263
291
|
path,
|
|
@@ -269,7 +297,10 @@ export async function runSkillSyncForCli(
|
|
|
269
297
|
// dropped (or disabled). We only consider state entries whose scope
|
|
270
298
|
// matches this cli, so we don't accidentally remove the other CLI's
|
|
271
299
|
// skills when running a per-cli tick.
|
|
272
|
-
for (const [
|
|
300
|
+
for (const [skillKey, record] of Object.entries(state.skills)) {
|
|
301
|
+
if (record.cli !== cli) continue;
|
|
302
|
+
const [, name] = skillKey.split(":", 2);
|
|
303
|
+
if (!name) continue;
|
|
273
304
|
if (!shouldSyncForCli(record.scope, cli)) continue;
|
|
274
305
|
if (remoteByName.has(name)) continue;
|
|
275
306
|
try {
|
|
@@ -277,7 +308,7 @@ export async function runSkillSyncForCli(
|
|
|
277
308
|
} catch {
|
|
278
309
|
// best-effort; if unlink fails the next tick will retry
|
|
279
310
|
}
|
|
280
|
-
delete state.skills[
|
|
311
|
+
delete state.skills[skillKey];
|
|
281
312
|
removed.push(name);
|
|
282
313
|
}
|
|
283
314
|
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { env, pipeline } from "@huggingface/transformers";
|
|
6
5
|
import { readJsonl, asString, asNumber, asBoolean } from "./jsonl.js";
|
|
6
|
+
import { CACHE_DIR, PATHS } from "./paths.js";
|
|
7
7
|
import type { JsonObject, JsonValue } from "./types.js";
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const CACHE_DIR = path.join(CONTEXT_DIR, "cache");
|
|
14
|
-
const EMBEDDINGS_DIR = path.join(CONTEXT_DIR, "embeddings");
|
|
15
|
-
const EMBEDDINGS_PATH = path.join(EMBEDDINGS_DIR, "entities.jsonl");
|
|
16
|
-
const EMBEDDINGS_MANIFEST_PATH = path.join(EMBEDDINGS_DIR, "manifest.json");
|
|
17
|
-
const MODEL_CACHE_DIR = path.join(EMBEDDINGS_DIR, "models");
|
|
9
|
+
const EMBEDDINGS_PATH = PATHS.embeddingsEntities;
|
|
10
|
+
const EMBEDDINGS_MANIFEST_PATH = PATHS.embeddingsManifest;
|
|
11
|
+
const MODEL_CACHE_DIR = PATHS.embeddingsModelCache;
|
|
12
|
+
const EMBEDDINGS_DIR = path.dirname(EMBEDDINGS_PATH);
|
|
18
13
|
|
|
19
14
|
const DEFAULT_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
|
|
20
15
|
const DEFAULT_MAX_TEXT_CHARS = 7000;
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import ryugraph, { type Connection, type PreparedStatement, type QueryResult, type RyuValue } from "ryugraph";
|
|
5
4
|
import { readJsonl, asString, asNumber, asBoolean } from "./jsonl.js";
|
|
5
|
+
import { CACHE_DIR, CONTEXT_DIR, DB_PATH } from "./paths.js";
|
|
6
6
|
import type { JsonObject } from "./types.js";
|
|
7
7
|
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
10
|
-
const REPO_ROOT = path.resolve(__dirname, "../..");
|
|
11
|
-
const CONTEXT_DIR = path.join(REPO_ROOT, ".context");
|
|
12
|
-
const CACHE_DIR = path.join(CONTEXT_DIR, "cache");
|
|
13
|
-
const DB_PATH = path.join(CONTEXT_DIR, "db", "graph.ryu");
|
|
14
8
|
const ONTOLOGY_PATH = path.join(CONTEXT_DIR, "ontology.cypher");
|
|
15
9
|
const BATCH_SIZE = 50;
|
|
16
10
|
|
|
@@ -21,9 +21,45 @@ function normalizeForWsl(rawPath: string): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const PROJECT_ROOT_OVERRIDE = process.env.CORTEX_PROJECT_ROOT?.trim();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
|
|
25
|
+
function hasContextConfig(candidate: string): boolean {
|
|
26
|
+
return fs.existsSync(path.join(candidate, ".context", "config.yaml"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveFrom(startDir: string): string | null {
|
|
30
|
+
let current = path.resolve(startDir);
|
|
31
|
+
while (true) {
|
|
32
|
+
if (hasContextConfig(current)) {
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parent = path.dirname(current);
|
|
37
|
+
if (parent === current) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
current = parent;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveRepoRoot(): string {
|
|
45
|
+
const candidates = [
|
|
46
|
+
PROJECT_ROOT_OVERRIDE ? path.resolve(normalizeForWsl(PROJECT_ROOT_OVERRIDE)) : null,
|
|
47
|
+
process.cwd(),
|
|
48
|
+
__dirname,
|
|
49
|
+
process.env.INIT_CWD?.trim() ? path.resolve(normalizeForWsl(process.env.INIT_CWD.trim())) : null
|
|
50
|
+
].filter((value): value is string => Boolean(value));
|
|
51
|
+
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
const resolved = resolveFrom(candidate);
|
|
54
|
+
if (resolved) {
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return path.resolve(__dirname, "../../..");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const REPO_ROOT = resolveRepoRoot();
|
|
27
63
|
export const CONTEXT_DIR = path.join(REPO_ROOT, ".context");
|
|
28
64
|
export const CACHE_DIR = path.join(CONTEXT_DIR, "cache");
|
|
29
65
|
export const DB_PATH = path.join(CONTEXT_DIR, "db", "graph.ryu");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
test("paths resolve the project root from cwd without duplicating .context", () => {
|
|
10
|
+
const projectRoot = mkdtempSync(path.join(tmpdir(), "cortex-paths-"));
|
|
11
|
+
const contextDir = path.join(projectRoot, ".context");
|
|
12
|
+
const mcpDir = path.join(contextDir, "mcp");
|
|
13
|
+
const pathsModuleUrl = pathToFileURL(path.resolve("dist/paths.js")).href;
|
|
14
|
+
|
|
15
|
+
mkdirSync(mcpDir, { recursive: true });
|
|
16
|
+
writeFileSync(path.join(contextDir, "config.yaml"), "source_paths:\n - src\n");
|
|
17
|
+
|
|
18
|
+
const result = spawnSync(
|
|
19
|
+
process.execPath,
|
|
20
|
+
[
|
|
21
|
+
"--input-type=module",
|
|
22
|
+
"-e",
|
|
23
|
+
`import { REPO_ROOT, CONTEXT_DIR, CACHE_DIR } from ${JSON.stringify(pathsModuleUrl)}; console.log(JSON.stringify({ REPO_ROOT, CONTEXT_DIR, CACHE_DIR }));`
|
|
24
|
+
],
|
|
25
|
+
{
|
|
26
|
+
cwd: mcpDir,
|
|
27
|
+
encoding: "utf8"
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
assert.equal(result.status, 0, result.stderr);
|
|
32
|
+
|
|
33
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
34
|
+
const resolvedProjectRoot = realpathSync(projectRoot);
|
|
35
|
+
assert.equal(parsed.REPO_ROOT, resolvedProjectRoot);
|
|
36
|
+
assert.equal(parsed.CONTEXT_DIR, path.join(resolvedProjectRoot, ".context"));
|
|
37
|
+
assert.equal(parsed.CACHE_DIR, path.join(resolvedProjectRoot, ".context", "cache"));
|
|
38
|
+
});
|
|
@@ -12,4 +12,4 @@ fi
|
|
|
12
12
|
mkdir -p "$MCP_DIR/.npm-cache"
|
|
13
13
|
|
|
14
14
|
echo "[embed] generating embeddings via .context/mcp/embed"
|
|
15
|
-
NPM_CONFIG_CACHE="$MCP_DIR/.npm-cache" npm --prefix "$MCP_DIR" run embed --silent -- "$@"
|
|
15
|
+
CORTEX_PROJECT_ROOT="$REPO_ROOT" NPM_CONFIG_CACHE="$MCP_DIR/.npm-cache" npm --prefix "$MCP_DIR" run embed --silent -- "$@"
|
|
@@ -15,4 +15,4 @@ if [[ ! -d "$MCP_DIR/node_modules" ]]; then
|
|
|
15
15
|
exit 1
|
|
16
16
|
fi
|
|
17
17
|
|
|
18
|
-
NPM_CONFIG_CACHE="$MCP_DIR/.npm-cache" npm --prefix "$MCP_DIR" run graph:load -- "$@"
|
|
18
|
+
CORTEX_PROJECT_ROOT="$REPO_ROOT" NPM_CONFIG_CACHE="$MCP_DIR/.npm-cache" npm --prefix "$MCP_DIR" run graph:load -- "$@"
|