@alaarab/cortex 1.14.0 → 1.15.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/mcp/dist/cli-config.js +0 -1
- package/mcp/dist/cli-hooks-citations.js +1 -1
- package/mcp/dist/cli-hooks-retrieval.js +42 -9
- package/mcp/dist/cli-hooks.js +2 -2
- package/mcp/dist/cli.js +146 -3
- package/mcp/dist/index.js +8 -0
- package/mcp/dist/link.js +14 -1
- package/mcp/dist/utils.js +49 -5
- package/package.json +10 -10
package/mcp/dist/cli-config.js
CHANGED
|
@@ -8,7 +8,6 @@ function getCortexPath() {
|
|
|
8
8
|
_cortexPath = ensureCortexPath();
|
|
9
9
|
return _cortexPath;
|
|
10
10
|
}
|
|
11
|
-
const profile = process.env.CORTEX_PROFILE || "";
|
|
12
11
|
// ── Config router ────────────────────────────────────────────────────────────
|
|
13
12
|
export async function handleConfig(args) {
|
|
14
13
|
const sub = args[0];
|
|
@@ -20,7 +20,7 @@ export function clearCitationValidCache() {
|
|
|
20
20
|
citationValidCache.clear();
|
|
21
21
|
}
|
|
22
22
|
/** @deprecated Legacy citation formats. Use `<!-- cortex:cite {...} -->` instead. */
|
|
23
|
-
const CITATION_PATTERN = /<!-- source: ([^:]+):(\d+) -->|\[file:([^:]+):(\d+)\]/g;
|
|
23
|
+
const CITATION_PATTERN = /<!-- source: ((?:[a-zA-Z]:[\\\/])?[^:]+):(\d+) -->|\[file:((?:[a-zA-Z]:[\\\/])?[^:]+):(\d+)\]/g;
|
|
24
24
|
export function parseCitations(text) {
|
|
25
25
|
const results = [];
|
|
26
26
|
let m;
|
|
@@ -122,7 +122,7 @@ function semanticFallbackDocs(db, prompt, project) {
|
|
|
122
122
|
const queryTokens = tokenizeForOverlap(prompt);
|
|
123
123
|
if (!queryTokens.length)
|
|
124
124
|
return [];
|
|
125
|
-
const sampleLimit =
|
|
125
|
+
const sampleLimit = 100;
|
|
126
126
|
const docs = project
|
|
127
127
|
? queryDocRows(db, "SELECT project, filename, type, content, path FROM docs WHERE project = ? LIMIT ?", [project, sampleLimit]) || []
|
|
128
128
|
: queryDocRows(db, "SELECT project, filename, type, content, path FROM docs LIMIT ?", [sampleLimit]) || [];
|
|
@@ -132,7 +132,7 @@ function semanticFallbackDocs(db, prompt, project) {
|
|
|
132
132
|
const score = overlapScore(queryTokens, corpus);
|
|
133
133
|
return { doc, score };
|
|
134
134
|
})
|
|
135
|
-
.filter((x) => x.score >= 0.
|
|
135
|
+
.filter((x) => x.score >= 0.25)
|
|
136
136
|
.sort((a, b) => b.score - a.score)
|
|
137
137
|
.slice(0, 8)
|
|
138
138
|
.map((x) => x.doc);
|
|
@@ -219,6 +219,22 @@ function mostRecentDate(content) {
|
|
|
219
219
|
return "0000-00-00";
|
|
220
220
|
return matches.map((m) => m.slice(3)).sort().reverse()[0];
|
|
221
221
|
}
|
|
222
|
+
function crossProjectAgeMultiplier(doc, detectedProject) {
|
|
223
|
+
if (doc.type !== "findings" || !detectedProject || doc.project === detectedProject)
|
|
224
|
+
return 1;
|
|
225
|
+
const decayDaysRaw = Number.parseInt(process.env.CORTEX_CROSS_PROJECT_DECAY_DAYS ?? "30", 10);
|
|
226
|
+
const decayDays = Number.isFinite(decayDaysRaw) && decayDaysRaw > 0 ? decayDaysRaw : 30;
|
|
227
|
+
const latest = mostRecentDate(doc.content);
|
|
228
|
+
const todayUtc = Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate());
|
|
229
|
+
let ageInDays = 90;
|
|
230
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(latest) && latest !== "0000-00-00") {
|
|
231
|
+
const entryUtc = Date.parse(`${latest}T00:00:00Z`);
|
|
232
|
+
if (!Number.isNaN(entryUtc)) {
|
|
233
|
+
ageInDays = Math.max(0, Math.floor((todayUtc - entryUtc) / 86_400_000));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return Math.max(0.1, 1 - (ageInDays / decayDays));
|
|
237
|
+
}
|
|
222
238
|
export function rankResults(rows, intent, gitCtx, detectedProject, cortexPathLocal, db, cwd, query) {
|
|
223
239
|
let ranked = [...rows];
|
|
224
240
|
if (detectedProject) {
|
|
@@ -260,31 +276,48 @@ export function rankResults(rows, intent, gitCtx, detectedProject, cortexPathLoc
|
|
|
260
276
|
if (byDate !== 0)
|
|
261
277
|
return byDate;
|
|
262
278
|
}
|
|
279
|
+
const changedFiles = gitCtx?.changedFiles || new Set();
|
|
280
|
+
const globBoostA = getProjectGlobBoost(cortexPathLocal, a.project, cwd, gitCtx?.changedFiles);
|
|
281
|
+
const globBoostB = getProjectGlobBoost(cortexPathLocal, b.project, cwd, gitCtx?.changedFiles);
|
|
282
|
+
const keyA = entryScoreKey(a.project, a.filename, a.content);
|
|
283
|
+
const keyB = entryScoreKey(b.project, b.filename, b.content);
|
|
284
|
+
const entityA = entityBoostPaths.has(a.path) ? 1.3 : 1;
|
|
285
|
+
const entityB = entityBoostPaths.has(b.path) ? 1.3 : 1;
|
|
286
|
+
const scoreA = (intentBoost(intent, a.type) +
|
|
287
|
+
fileRelevanceBoost(a.path, changedFiles) +
|
|
288
|
+
branchMatchBoost(a.content, gitCtx?.branch) +
|
|
289
|
+
globBoostA +
|
|
290
|
+
getQualityMultiplier(cortexPathLocal, keyA) +
|
|
291
|
+
entityA -
|
|
292
|
+
lowValuePenalty(a.content, a.type)) * crossProjectAgeMultiplier(a, detectedProject);
|
|
293
|
+
const scoreB = (intentBoost(intent, b.type) +
|
|
294
|
+
fileRelevanceBoost(b.path, changedFiles) +
|
|
295
|
+
branchMatchBoost(b.content, gitCtx?.branch) +
|
|
296
|
+
globBoostB +
|
|
297
|
+
getQualityMultiplier(cortexPathLocal, keyB) +
|
|
298
|
+
entityB -
|
|
299
|
+
lowValuePenalty(b.content, b.type)) * crossProjectAgeMultiplier(b, detectedProject);
|
|
300
|
+
const scoreDelta = scoreB - scoreA;
|
|
301
|
+
if (Math.abs(scoreDelta) > 0.01)
|
|
302
|
+
return scoreDelta;
|
|
263
303
|
const intentDelta = intentBoost(intent, b.type) - intentBoost(intent, a.type);
|
|
264
304
|
if (intentDelta !== 0)
|
|
265
305
|
return intentDelta;
|
|
266
|
-
const changedFiles = gitCtx?.changedFiles || new Set();
|
|
267
306
|
const fileDelta = fileRelevanceBoost(b.path, changedFiles) - fileRelevanceBoost(a.path, changedFiles);
|
|
268
307
|
if (fileDelta !== 0)
|
|
269
308
|
return fileDelta;
|
|
270
309
|
const branchDelta = branchMatchBoost(b.content, gitCtx?.branch) - branchMatchBoost(a.content, gitCtx?.branch);
|
|
271
310
|
if (branchDelta !== 0)
|
|
272
311
|
return branchDelta;
|
|
273
|
-
const globBoostA = getProjectGlobBoost(cortexPathLocal, a.project, cwd, gitCtx?.changedFiles);
|
|
274
|
-
const globBoostB = getProjectGlobBoost(cortexPathLocal, b.project, cwd, gitCtx?.changedFiles);
|
|
275
312
|
const globDelta = globBoostB - globBoostA;
|
|
276
313
|
if (Math.abs(globDelta) > 0.01)
|
|
277
314
|
return globDelta;
|
|
278
|
-
const keyA = entryScoreKey(a.project, a.filename, a.content);
|
|
279
|
-
const keyB = entryScoreKey(b.project, b.filename, b.content);
|
|
280
315
|
const qualityDelta = getQualityMultiplier(cortexPathLocal, keyB) - getQualityMultiplier(cortexPathLocal, keyA);
|
|
281
316
|
if (qualityDelta !== 0)
|
|
282
317
|
return qualityDelta;
|
|
283
318
|
const penaltyDelta = lowValuePenalty(a.content, a.type) - lowValuePenalty(b.content, b.type);
|
|
284
319
|
if (penaltyDelta !== 0)
|
|
285
320
|
return penaltyDelta;
|
|
286
|
-
const entityA = entityBoostPaths.has(a.path) ? 1.3 : 1;
|
|
287
|
-
const entityB = entityBoostPaths.has(b.path) ? 1.3 : 1;
|
|
288
321
|
if (entityB !== entityA)
|
|
289
322
|
return entityB - entityA;
|
|
290
323
|
return 0;
|
package/mcp/dist/cli-hooks.js
CHANGED
|
@@ -40,7 +40,7 @@ const profile = process.env.CORTEX_PROFILE || "";
|
|
|
40
40
|
async function readStdin() {
|
|
41
41
|
return new Promise((resolve, reject) => {
|
|
42
42
|
const chunks = [];
|
|
43
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
43
|
+
process.stdin.on("data", (chunk) => chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
|
|
44
44
|
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
45
45
|
process.stdin.on("error", reject);
|
|
46
46
|
});
|
|
@@ -89,7 +89,7 @@ export async function handleHookPrompt() {
|
|
|
89
89
|
const detectedProject = cwd ? detectProject(getCortexPath(), cwd, profile) : null;
|
|
90
90
|
if (detectedProject)
|
|
91
91
|
debugLog(`Detected project: ${detectedProject}`);
|
|
92
|
-
const safeQuery = buildRobustFtsQuery(keywords);
|
|
92
|
+
const safeQuery = buildRobustFtsQuery(keywords, detectedProject);
|
|
93
93
|
if (!safeQuery)
|
|
94
94
|
process.exit(0);
|
|
95
95
|
try {
|
package/mcp/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { handleHookPrompt, handleHookSessionStart, handleHookStop, handleHookCon
|
|
|
21
21
|
import { handleExtractMemories } from "./cli-extract.js";
|
|
22
22
|
import { handleGovernMemories, handlePruneMemories, handleConsolidateMemories, handleMigrateFindings, handleMaintain, handleBackgroundMaintenance, } from "./cli-govern.js";
|
|
23
23
|
import { handleConfig, handleIndexPolicy, handleRetentionPolicy, handleWorkflowPolicy, handleAccessControl, } from "./cli-config.js";
|
|
24
|
+
import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
|
|
24
25
|
let _cortexPath;
|
|
25
26
|
function getCortexPath() {
|
|
26
27
|
if (!_cortexPath)
|
|
@@ -275,6 +276,10 @@ export async function runCliCommand(command, args) {
|
|
|
275
276
|
return handleMaintain(args);
|
|
276
277
|
case "skill-list":
|
|
277
278
|
return handleSkillList();
|
|
279
|
+
case "skills":
|
|
280
|
+
return handleSkillsNamespace(args);
|
|
281
|
+
case "hooks":
|
|
282
|
+
return handleHooksNamespace(args);
|
|
278
283
|
case "backlog":
|
|
279
284
|
return handleBacklogView();
|
|
280
285
|
case "quickstart":
|
|
@@ -305,7 +310,7 @@ async function handleSearch(opts) {
|
|
|
305
310
|
const where = [];
|
|
306
311
|
const params = [];
|
|
307
312
|
if (opts.query) {
|
|
308
|
-
const safeQuery = buildRobustFtsQuery(opts.query);
|
|
313
|
+
const safeQuery = buildRobustFtsQuery(opts.query, opts.project);
|
|
309
314
|
if (!safeQuery) {
|
|
310
315
|
console.error("Query empty after sanitization.");
|
|
311
316
|
process.exit(1);
|
|
@@ -487,14 +492,149 @@ async function handleUpdate(args) {
|
|
|
487
492
|
const result = await runCortexUpdate();
|
|
488
493
|
console.log(result);
|
|
489
494
|
}
|
|
495
|
+
const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
|
|
496
|
+
function printSkillsUsage() {
|
|
497
|
+
console.log("Usage:");
|
|
498
|
+
console.log(" cortex skills list");
|
|
499
|
+
console.log(" cortex skills add <project> <path>");
|
|
500
|
+
console.log(" cortex skills remove <project> <name>");
|
|
501
|
+
}
|
|
502
|
+
function printHooksUsage() {
|
|
503
|
+
console.log("Usage:");
|
|
504
|
+
console.log(" cortex hooks list");
|
|
505
|
+
console.log(" cortex hooks enable <tool>");
|
|
506
|
+
console.log(" cortex hooks disable <tool>");
|
|
507
|
+
console.log(" tools: claude|copilot|cursor|codex");
|
|
508
|
+
}
|
|
509
|
+
function normalizeHookTool(raw) {
|
|
510
|
+
if (!raw)
|
|
511
|
+
return null;
|
|
512
|
+
const tool = raw.toLowerCase();
|
|
513
|
+
return HOOK_TOOLS.includes(tool) ? tool : null;
|
|
514
|
+
}
|
|
515
|
+
function handleSkillsNamespace(args) {
|
|
516
|
+
const subcommand = args[0];
|
|
517
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
518
|
+
printSkillsUsage();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (subcommand === "list") {
|
|
522
|
+
handleSkillList();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (subcommand === "add") {
|
|
526
|
+
const project = args[1];
|
|
527
|
+
const skillPath = args[2];
|
|
528
|
+
if (!project || !skillPath) {
|
|
529
|
+
printSkillsUsage();
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
if (!isValidProjectName(project)) {
|
|
533
|
+
console.error(`Invalid project name: "${project}"`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
const source = path.resolve(skillPath.replace(/^~/, os.homedir()));
|
|
537
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
|
|
538
|
+
console.error(`Skill file not found: ${source}`);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
const baseName = path.basename(source);
|
|
542
|
+
const fileName = baseName.toLowerCase().endsWith(".md") ? baseName : `${baseName}.md`;
|
|
543
|
+
const destDir = path.join(getCortexPath(), project, ".claude", "skills");
|
|
544
|
+
const dest = path.join(destDir, fileName);
|
|
545
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
546
|
+
if (fs.existsSync(dest)) {
|
|
547
|
+
console.error(`Skill already exists: ${dest}`);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
fs.symlinkSync(source, dest);
|
|
552
|
+
console.log(`Linked skill ${fileName} into ${project}.`);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
fs.copyFileSync(source, dest);
|
|
556
|
+
console.log(`Copied skill ${fileName} into ${project}.`);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (subcommand === "remove") {
|
|
561
|
+
const project = args[1];
|
|
562
|
+
const name = args[2];
|
|
563
|
+
if (!project || !name) {
|
|
564
|
+
printSkillsUsage();
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
if (!isValidProjectName(project)) {
|
|
568
|
+
console.error(`Invalid project name: "${project}"`);
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
const dest = path.join(getCortexPath(), project, ".claude", "skills", `${name.replace(/\.md$/i, "")}.md`);
|
|
572
|
+
if (!fs.existsSync(dest)) {
|
|
573
|
+
console.error(`Skill not found: ${dest}`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
fs.unlinkSync(dest);
|
|
577
|
+
console.log(`Removed skill ${name.replace(/\.md$/i, "")}.md from ${project}.`);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
console.error(`Unknown skills subcommand: ${subcommand}`);
|
|
581
|
+
printSkillsUsage();
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
function handleHooksNamespace(args) {
|
|
585
|
+
const subcommand = args[0];
|
|
586
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
587
|
+
printHooksUsage();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (subcommand === "list") {
|
|
591
|
+
const prefs = readInstallPreferences(getCortexPath());
|
|
592
|
+
const hooksEnabled = prefs.hooksEnabled !== false;
|
|
593
|
+
const toolPrefs = prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {};
|
|
594
|
+
const rows = HOOK_TOOLS.map((tool) => ({
|
|
595
|
+
tool,
|
|
596
|
+
hookType: "lifecycle",
|
|
597
|
+
status: hooksEnabled && toolPrefs[tool] !== false ? "enabled" : "disabled",
|
|
598
|
+
}));
|
|
599
|
+
console.log(`Tool Hook Type Status`);
|
|
600
|
+
console.log(`-------- --------- --------`);
|
|
601
|
+
for (const row of rows) {
|
|
602
|
+
console.log(`${row.tool.padEnd(8)} ${row.hookType.padEnd(9)} ${row.status}`);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (subcommand === "enable" || subcommand === "disable") {
|
|
607
|
+
const tool = normalizeHookTool(args[1]);
|
|
608
|
+
if (!tool) {
|
|
609
|
+
printHooksUsage();
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
const prefs = readInstallPreferences(getCortexPath());
|
|
613
|
+
writeInstallPreferences(getCortexPath(), {
|
|
614
|
+
hookTools: {
|
|
615
|
+
...(prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {}),
|
|
616
|
+
[tool]: subcommand === "enable",
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
console.log(`${subcommand === "enable" ? "Enabled" : "Disabled"} hooks for ${tool}.`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
console.error(`Unknown hooks subcommand: ${subcommand}`);
|
|
623
|
+
printHooksUsage();
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
490
626
|
function handleSkillList() {
|
|
491
627
|
const sources = [];
|
|
628
|
+
const seenPaths = new Set();
|
|
492
629
|
function collectSkills(root, sourceLabel) {
|
|
493
630
|
if (!fs.existsSync(root))
|
|
494
631
|
return;
|
|
495
632
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
496
633
|
const entryPath = path.join(root, entry.name);
|
|
497
634
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
635
|
+
if (seenPaths.has(entryPath))
|
|
636
|
+
continue;
|
|
637
|
+
seenPaths.add(entryPath);
|
|
498
638
|
sources.push({
|
|
499
639
|
name: entry.name.replace(/\.md$/, ""),
|
|
500
640
|
source: sourceLabel,
|
|
@@ -507,6 +647,9 @@ function handleSkillList() {
|
|
|
507
647
|
const skillFile = path.join(entryPath, "SKILL.md");
|
|
508
648
|
if (!fs.existsSync(skillFile))
|
|
509
649
|
continue;
|
|
650
|
+
if (seenPaths.has(skillFile))
|
|
651
|
+
continue;
|
|
652
|
+
seenPaths.add(skillFile);
|
|
510
653
|
sources.push({
|
|
511
654
|
name: entry.name,
|
|
512
655
|
source: sourceLabel,
|
|
@@ -523,8 +666,8 @@ function handleSkillList() {
|
|
|
523
666
|
const projectName = path.basename(dir);
|
|
524
667
|
if (projectName === "global")
|
|
525
668
|
continue;
|
|
526
|
-
|
|
527
|
-
collectSkills(
|
|
669
|
+
collectSkills(path.join(dir, "skills"), projectName);
|
|
670
|
+
collectSkills(path.join(dir, ".claude", "skills"), projectName);
|
|
528
671
|
}
|
|
529
672
|
if (!sources.length) {
|
|
530
673
|
console.log("No skills found.");
|
package/mcp/dist/index.js
CHANGED
|
@@ -25,6 +25,12 @@ Usage:
|
|
|
25
25
|
cortex init [--machine <n>] [--profile <n>] [--mcp on|off] [--template <t>] [--from-existing <path>] [--dry-run] [-y]
|
|
26
26
|
Set up cortex (templates: python-project, monorepo, library, frontend)
|
|
27
27
|
cortex detect-skills [--import] Find untracked skills in ~/.claude/skills/
|
|
28
|
+
cortex skills list List installed skills
|
|
29
|
+
cortex skills add <project> <path> Link or copy a skill file into one project
|
|
30
|
+
cortex skills remove <project> <name> Remove a project skill by name
|
|
31
|
+
cortex hooks list Show hook tool preferences
|
|
32
|
+
cortex hooks enable <tool> Enable hooks for one tool
|
|
33
|
+
cortex hooks disable <tool> Disable hooks for one tool
|
|
28
34
|
cortex status Health, active project, stats
|
|
29
35
|
cortex search <query> [--project <n>] [--type <t>] [--limit <n>]
|
|
30
36
|
Search your cortex
|
|
@@ -206,6 +212,8 @@ const CLI_COMMANDS = [
|
|
|
206
212
|
"review-ui",
|
|
207
213
|
"quality-feedback",
|
|
208
214
|
"skill-list",
|
|
215
|
+
"skills",
|
|
216
|
+
"hooks",
|
|
209
217
|
"detect-skills",
|
|
210
218
|
"backlog",
|
|
211
219
|
"quickstart",
|
package/mcp/dist/link.js
CHANGED
|
@@ -124,6 +124,18 @@ function currentPackageVersion() {
|
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
function readProjectConfig(cortexPath, project) {
|
|
128
|
+
const configPath = path.join(cortexPath, project, "cortex.project.yaml");
|
|
129
|
+
if (!fs.existsSync(configPath))
|
|
130
|
+
return {};
|
|
131
|
+
try {
|
|
132
|
+
const parsed = yaml.load(fs.readFileSync(configPath, "utf8"), { schema: yaml.CORE_SCHEMA });
|
|
133
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
127
139
|
function maybeOfferStarterTemplateUpdate(cortexPath) {
|
|
128
140
|
const current = currentPackageVersion();
|
|
129
141
|
if (!current)
|
|
@@ -318,7 +330,8 @@ function linkProject(cortexPath, project, tools) {
|
|
|
318
330
|
}
|
|
319
331
|
// Project-level skills
|
|
320
332
|
const projectSkills = path.join(cortexPath, project, ".claude", "skills");
|
|
321
|
-
|
|
333
|
+
const config = readProjectConfig(cortexPath, project);
|
|
334
|
+
if (config.skills !== false && fs.existsSync(projectSkills)) {
|
|
322
335
|
const targetSkills = path.join(target, ".claude", "skills");
|
|
323
336
|
linkSkillsDir(projectSkills, targetSkills, cortexPath, symlinkFile);
|
|
324
337
|
}
|
package/mcp/dist/utils.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
1
2
|
import * as path from "path";
|
|
2
3
|
import { execFileSync } from "child_process";
|
|
4
|
+
import * as yaml from "js-yaml";
|
|
5
|
+
import { findCortexPath } from "./shared.js";
|
|
3
6
|
// ── Shared Git helper ────────────────────────────────────────────────────────
|
|
4
7
|
export function runGit(cwd, args, timeoutMs, debugLogFn) {
|
|
5
8
|
try {
|
|
@@ -266,17 +269,59 @@ export function sanitizeFts5Query(raw) {
|
|
|
266
269
|
q = q.replace(/\s+/g, " ");
|
|
267
270
|
return q.trim();
|
|
268
271
|
}
|
|
272
|
+
function parseSynonymsYaml(filePath) {
|
|
273
|
+
if (!fs.existsSync(filePath))
|
|
274
|
+
return {};
|
|
275
|
+
try {
|
|
276
|
+
const parsed = yaml.load(fs.readFileSync(filePath, "utf8"), { schema: yaml.CORE_SCHEMA });
|
|
277
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
278
|
+
return {};
|
|
279
|
+
const loaded = {};
|
|
280
|
+
for (const [rawKey, value] of Object.entries(parsed)) {
|
|
281
|
+
const key = String(rawKey).trim().toLowerCase();
|
|
282
|
+
if (!key || !Array.isArray(value))
|
|
283
|
+
continue;
|
|
284
|
+
const synonyms = value
|
|
285
|
+
.filter((item) => typeof item === "string")
|
|
286
|
+
.map((item) => item.replace(/"/g, "").trim())
|
|
287
|
+
.filter((item) => item.length > 1);
|
|
288
|
+
if (synonyms.length > 0)
|
|
289
|
+
loaded[key] = synonyms;
|
|
290
|
+
}
|
|
291
|
+
return loaded;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return {};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function loadUserSynonyms(project) {
|
|
298
|
+
const cortexPath = findCortexPath();
|
|
299
|
+
if (!cortexPath)
|
|
300
|
+
return {};
|
|
301
|
+
const globalSynonyms = parseSynonymsYaml(path.join(cortexPath, "global", "synonyms.yaml"));
|
|
302
|
+
if (!project || !isValidProjectName(project))
|
|
303
|
+
return globalSynonyms;
|
|
304
|
+
const projectSynonyms = parseSynonymsYaml(path.join(cortexPath, project, "synonyms.yaml"));
|
|
305
|
+
return {
|
|
306
|
+
...globalSynonyms,
|
|
307
|
+
...projectSynonyms,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
269
310
|
// Build a defensive FTS5 MATCH query:
|
|
270
311
|
// - sanitizes user input
|
|
271
312
|
// - extracts bigrams and treats them as quoted phrases
|
|
272
313
|
// - expands known synonyms (capped at 10 total terms)
|
|
273
314
|
// - applies AND between core terms, with synonyms as OR alternatives
|
|
274
|
-
export function buildRobustFtsQuery(raw) {
|
|
315
|
+
export function buildRobustFtsQuery(raw, project) {
|
|
275
316
|
const MAX_TOTAL_TERMS = 10;
|
|
276
317
|
const MAX_SYNONYM_GROUPS = 3;
|
|
277
318
|
const safe = sanitizeFts5Query(raw);
|
|
278
319
|
if (!safe)
|
|
279
320
|
return "";
|
|
321
|
+
const synonymsMap = {
|
|
322
|
+
...SYNONYMS,
|
|
323
|
+
...loadUserSynonyms(project),
|
|
324
|
+
};
|
|
280
325
|
const baseWords = safe.split(/\s+/).filter((t) => t.length > 1);
|
|
281
326
|
if (baseWords.length === 0)
|
|
282
327
|
return "";
|
|
@@ -286,12 +331,11 @@ export function buildRobustFtsQuery(raw) {
|
|
|
286
331
|
bigrams.push(`${baseWords[i]} ${baseWords[i + 1]}`);
|
|
287
332
|
}
|
|
288
333
|
// Determine which words are consumed by bigrams that match synonym keys
|
|
289
|
-
const lowered = safe.toLowerCase();
|
|
290
334
|
const consumedIndices = new Set();
|
|
291
335
|
const matchedBigrams = [];
|
|
292
336
|
for (let i = 0; i < bigrams.length; i++) {
|
|
293
337
|
const bg = bigrams[i].toLowerCase();
|
|
294
|
-
if (
|
|
338
|
+
if (synonymsMap[bg]) {
|
|
295
339
|
consumedIndices.add(i);
|
|
296
340
|
consumedIndices.add(i + 1);
|
|
297
341
|
matchedBigrams.push(bigrams[i]);
|
|
@@ -326,8 +370,8 @@ export function buildRobustFtsQuery(raw) {
|
|
|
326
370
|
for (const coreTerm of coreTerms) {
|
|
327
371
|
const termText = coreTerm.slice(1, -1).toLowerCase(); // strip quotes
|
|
328
372
|
const synonyms = [];
|
|
329
|
-
if (groupsExpanded < MAX_SYNONYM_GROUPS &&
|
|
330
|
-
for (const syn of
|
|
373
|
+
if (groupsExpanded < MAX_SYNONYM_GROUPS && synonymsMap[termText]) {
|
|
374
|
+
for (const syn of synonymsMap[termText]) {
|
|
331
375
|
if (totalTermCount >= MAX_TOTAL_TERMS)
|
|
332
376
|
break;
|
|
333
377
|
const cleanSyn = syn.replace(/"/g, "").trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/cortex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Long-term memory for AI agents. Stored as markdown in a git repo you own.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,21 +12,21 @@
|
|
|
12
12
|
"skills"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
16
16
|
"@xenova/transformers": "^2.17.2",
|
|
17
|
-
"glob": "^
|
|
18
|
-
"js-yaml": "^4.1.
|
|
17
|
+
"glob": "^13.0.6",
|
|
18
|
+
"js-yaml": "^4.1.1",
|
|
19
19
|
"sql.js-fts5": "^1.4.0",
|
|
20
|
-
"zod": "^4.
|
|
20
|
+
"zod": "^4.3.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/js-yaml": "^4.0.
|
|
24
|
-
"@types/node": "^
|
|
23
|
+
"@types/js-yaml": "^4.0.9",
|
|
24
|
+
"@types/node": "^25.3.5",
|
|
25
25
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
26
26
|
"@typescript-eslint/parser": "^8.56.1",
|
|
27
|
-
"eslint": "^10.0.
|
|
28
|
-
"tsx": "^4.
|
|
29
|
-
"typescript": "^5.
|
|
27
|
+
"eslint": "^10.0.3",
|
|
28
|
+
"tsx": "^4.21.0",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
30
|
"vitest": "^4.0.18"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|