@alaarab/cortex 1.13.6 → 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/README.md +16 -15
- package/mcp/dist/cli-config.js +20 -16
- package/mcp/dist/cli-extract.js +15 -10
- package/mcp/dist/cli-govern.js +37 -32
- package/mcp/dist/cli-hooks-citations.js +1 -1
- package/mcp/dist/cli-hooks-retrieval.js +42 -9
- package/mcp/dist/cli-hooks-session.js +46 -41
- package/mcp/dist/cli-hooks.js +26 -21
- package/mcp/dist/cli.js +172 -24
- package/mcp/dist/data-access.js +7 -43
- package/mcp/dist/index.js +23 -15
- package/mcp/dist/link-skills.js +0 -6
- package/mcp/dist/link.js +14 -1
- package/mcp/dist/mcp-finding.js +2 -2
- package/mcp/dist/mcp-search.js +4 -4
- package/mcp/dist/shared-index.js +45 -26
- package/mcp/dist/shared.js +6 -0
- package/mcp/dist/utils.js +49 -5
- package/package.json +11 -11
- package/starter/README.md +2 -2
package/mcp/dist/cli.js
CHANGED
|
@@ -21,7 +21,13 @@ 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
|
-
|
|
24
|
+
import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
|
|
25
|
+
let _cortexPath;
|
|
26
|
+
function getCortexPath() {
|
|
27
|
+
if (!_cortexPath)
|
|
28
|
+
_cortexPath = ensureCortexPath();
|
|
29
|
+
return _cortexPath;
|
|
30
|
+
}
|
|
25
31
|
const profile = process.env.CORTEX_PROFILE || "";
|
|
26
32
|
// ── Search types and parsing ─────────────────────────────────────────────────
|
|
27
33
|
const SEARCH_TYPE_ALIASES = {
|
|
@@ -42,7 +48,7 @@ const SEARCH_TYPES = new Set([
|
|
|
42
48
|
// ── Search history ───────────────────────────────────────────────────────────
|
|
43
49
|
const MAX_HISTORY = 20;
|
|
44
50
|
function historyFile() {
|
|
45
|
-
return runtimeFile(
|
|
51
|
+
return runtimeFile(getCortexPath(), "search-history.jsonl");
|
|
46
52
|
}
|
|
47
53
|
function readSearchHistory() {
|
|
48
54
|
const file = historyFile();
|
|
@@ -270,6 +276,10 @@ export async function runCliCommand(command, args) {
|
|
|
270
276
|
return handleMaintain(args);
|
|
271
277
|
case "skill-list":
|
|
272
278
|
return handleSkillList();
|
|
279
|
+
case "skills":
|
|
280
|
+
return handleSkillsNamespace(args);
|
|
281
|
+
case "hooks":
|
|
282
|
+
return handleHooksNamespace(args);
|
|
273
283
|
case "backlog":
|
|
274
284
|
return handleBacklogView();
|
|
275
285
|
case "quickstart":
|
|
@@ -294,13 +304,13 @@ async function handleSearch(opts) {
|
|
|
294
304
|
return;
|
|
295
305
|
}
|
|
296
306
|
recordSearchQuery(opts);
|
|
297
|
-
const db = await buildIndex(
|
|
307
|
+
const db = await buildIndex(getCortexPath(), profile);
|
|
298
308
|
try {
|
|
299
309
|
let sql = "SELECT project, filename, type, content, path FROM docs";
|
|
300
310
|
const where = [];
|
|
301
311
|
const params = [];
|
|
302
312
|
if (opts.query) {
|
|
303
|
-
const safeQuery = buildRobustFtsQuery(opts.query);
|
|
313
|
+
const safeQuery = buildRobustFtsQuery(opts.query, opts.project);
|
|
304
314
|
if (!safeQuery) {
|
|
305
315
|
console.error("Query empty after sanitization.");
|
|
306
316
|
process.exit(1);
|
|
@@ -334,7 +344,7 @@ async function handleSearch(opts) {
|
|
|
334
344
|
if (opts.query) {
|
|
335
345
|
try {
|
|
336
346
|
const { logSearchMiss } = await import("./mcp-search.js");
|
|
337
|
-
logSearchMiss(
|
|
347
|
+
logSearchMiss(getCortexPath(), opts.query, opts.project);
|
|
338
348
|
}
|
|
339
349
|
catch { /* best-effort */ }
|
|
340
350
|
}
|
|
@@ -371,7 +381,7 @@ async function handleAddFinding(project, learning) {
|
|
|
371
381
|
process.exit(1);
|
|
372
382
|
}
|
|
373
383
|
try {
|
|
374
|
-
const result = addFindingCore(
|
|
384
|
+
const result = addFindingCore(getCortexPath(), project, learning);
|
|
375
385
|
if (!result.ok) {
|
|
376
386
|
console.error(result.message);
|
|
377
387
|
process.exit(1);
|
|
@@ -388,14 +398,14 @@ async function handlePinCanonical(project, memory) {
|
|
|
388
398
|
console.error('Usage: cortex pin <project> "<memory>"');
|
|
389
399
|
process.exit(1);
|
|
390
400
|
}
|
|
391
|
-
const result = upsertCanonical(
|
|
401
|
+
const result = upsertCanonical(getCortexPath(), project, memory);
|
|
392
402
|
console.log(result.ok ? result.data : result.error);
|
|
393
403
|
}
|
|
394
404
|
async function handleDoctor(args) {
|
|
395
405
|
const fix = args.includes("--fix");
|
|
396
406
|
const checkData = args.includes("--check-data");
|
|
397
407
|
const agentsOnly = args.includes("--agents");
|
|
398
|
-
const result = await runDoctor(
|
|
408
|
+
const result = await runDoctor(getCortexPath(), fix, checkData);
|
|
399
409
|
if (agentsOnly) {
|
|
400
410
|
// Filter to only agent-related checks
|
|
401
411
|
const agentChecks = result.checks.filter((c) => c.name.includes("cursor") || c.name.includes("copilot") || c.name.includes("codex") || c.name.includes("windsurf"));
|
|
@@ -418,7 +428,7 @@ async function handleDoctor(args) {
|
|
|
418
428
|
}
|
|
419
429
|
// Q30: Show top search miss patterns
|
|
420
430
|
try {
|
|
421
|
-
const missFile = runtimeFile(
|
|
431
|
+
const missFile = runtimeFile(getCortexPath(), "search-misses.jsonl");
|
|
422
432
|
if (fs.existsSync(missFile)) {
|
|
423
433
|
const lines = fs.readFileSync(missFile, "utf8").split("\n").filter(Boolean);
|
|
424
434
|
if (lines.length > 0) {
|
|
@@ -455,15 +465,15 @@ async function handleQualityFeedback(args) {
|
|
|
455
465
|
console.error("Usage: cortex quality-feedback --key=<entry-key> --type=helpful|reprompt|regression");
|
|
456
466
|
process.exit(1);
|
|
457
467
|
}
|
|
458
|
-
recordFeedback(
|
|
459
|
-
flushEntryScores(
|
|
468
|
+
recordFeedback(getCortexPath(), key, feedback);
|
|
469
|
+
flushEntryScores(getCortexPath());
|
|
460
470
|
console.log(`Recorded feedback: ${feedback} for ${key}`);
|
|
461
471
|
}
|
|
462
472
|
async function handleMemoryUi(args) {
|
|
463
473
|
const portArg = args.find((a) => a.startsWith("--port="));
|
|
464
474
|
const port = portArg ? Number.parseInt(portArg.slice("--port=".length), 10) : 3499;
|
|
465
475
|
const safePort = Number.isNaN(port) ? 3499 : port;
|
|
466
|
-
await startReviewUi(
|
|
476
|
+
await startReviewUi(getCortexPath(), safePort);
|
|
467
477
|
}
|
|
468
478
|
async function handleShell(args) {
|
|
469
479
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -471,7 +481,7 @@ async function handleShell(args) {
|
|
|
471
481
|
console.log("Interactive shell with views for Projects, Backlog, Learnings, Memory Queue, Machines/Profiles, and Health.");
|
|
472
482
|
return;
|
|
473
483
|
}
|
|
474
|
-
await startShell(
|
|
484
|
+
await startShell(getCortexPath(), profile);
|
|
475
485
|
}
|
|
476
486
|
async function handleUpdate(args) {
|
|
477
487
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -482,14 +492,149 @@ async function handleUpdate(args) {
|
|
|
482
492
|
const result = await runCortexUpdate();
|
|
483
493
|
console.log(result);
|
|
484
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
|
+
}
|
|
485
626
|
function handleSkillList() {
|
|
486
627
|
const sources = [];
|
|
628
|
+
const seenPaths = new Set();
|
|
487
629
|
function collectSkills(root, sourceLabel) {
|
|
488
630
|
if (!fs.existsSync(root))
|
|
489
631
|
return;
|
|
490
632
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
491
633
|
const entryPath = path.join(root, entry.name);
|
|
492
634
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
635
|
+
if (seenPaths.has(entryPath))
|
|
636
|
+
continue;
|
|
637
|
+
seenPaths.add(entryPath);
|
|
493
638
|
sources.push({
|
|
494
639
|
name: entry.name.replace(/\.md$/, ""),
|
|
495
640
|
source: sourceLabel,
|
|
@@ -502,6 +647,9 @@ function handleSkillList() {
|
|
|
502
647
|
const skillFile = path.join(entryPath, "SKILL.md");
|
|
503
648
|
if (!fs.existsSync(skillFile))
|
|
504
649
|
continue;
|
|
650
|
+
if (seenPaths.has(skillFile))
|
|
651
|
+
continue;
|
|
652
|
+
seenPaths.add(skillFile);
|
|
505
653
|
sources.push({
|
|
506
654
|
name: entry.name,
|
|
507
655
|
source: sourceLabel,
|
|
@@ -511,15 +659,15 @@ function handleSkillList() {
|
|
|
511
659
|
}
|
|
512
660
|
}
|
|
513
661
|
}
|
|
514
|
-
const globalSkillsDir = path.join(
|
|
662
|
+
const globalSkillsDir = path.join(getCortexPath(), "global", "skills");
|
|
515
663
|
collectSkills(globalSkillsDir, "global");
|
|
516
|
-
const projectDirs = getProjectDirs(
|
|
664
|
+
const projectDirs = getProjectDirs(getCortexPath(), profile);
|
|
517
665
|
for (const dir of projectDirs) {
|
|
518
666
|
const projectName = path.basename(dir);
|
|
519
667
|
if (projectName === "global")
|
|
520
668
|
continue;
|
|
521
|
-
|
|
522
|
-
collectSkills(
|
|
669
|
+
collectSkills(path.join(dir, "skills"), projectName);
|
|
670
|
+
collectSkills(path.join(dir, ".claude", "skills"), projectName);
|
|
523
671
|
}
|
|
524
672
|
if (!sources.length) {
|
|
525
673
|
console.log("No skills found.");
|
|
@@ -543,7 +691,7 @@ function handleDetectSkills(args) {
|
|
|
543
691
|
return;
|
|
544
692
|
}
|
|
545
693
|
const trackedSkills = new Set();
|
|
546
|
-
const globalSkillsDir = path.join(
|
|
694
|
+
const globalSkillsDir = path.join(getCortexPath(), "global", "skills");
|
|
547
695
|
if (fs.existsSync(globalSkillsDir)) {
|
|
548
696
|
for (const entry of fs.readdirSync(globalSkillsDir)) {
|
|
549
697
|
trackedSkills.add(entry.replace(/\.md$/, ""));
|
|
@@ -552,7 +700,7 @@ function handleDetectSkills(args) {
|
|
|
552
700
|
}
|
|
553
701
|
}
|
|
554
702
|
}
|
|
555
|
-
const projectDirs = getProjectDirs(
|
|
703
|
+
const projectDirs = getProjectDirs(getCortexPath(), profile);
|
|
556
704
|
for (const dir of projectDirs) {
|
|
557
705
|
const projectSkillsDir = path.join(dir, ".claude", "skills");
|
|
558
706
|
if (!fs.existsSync(projectSkillsDir))
|
|
@@ -620,7 +768,7 @@ function handleDetectSkills(args) {
|
|
|
620
768
|
console.log(`\nImported ${imported} skill(s). Run \`cortex link\` to activate.`);
|
|
621
769
|
}
|
|
622
770
|
function handleBacklogView() {
|
|
623
|
-
const docs = readBacklogs(
|
|
771
|
+
const docs = readBacklogs(getCortexPath(), profile);
|
|
624
772
|
if (!docs.length) {
|
|
625
773
|
console.log("No backlogs found.");
|
|
626
774
|
return;
|
|
@@ -670,8 +818,8 @@ async function handleQuickstart() {
|
|
|
670
818
|
});
|
|
671
819
|
console.log(`\nInitializing cortex for "${projectName}"...\n`);
|
|
672
820
|
await runInit({ yes: true });
|
|
673
|
-
await runLink(
|
|
674
|
-
const projectDir = path.join(
|
|
821
|
+
await runLink(getCortexPath(), {});
|
|
822
|
+
const projectDir = path.join(getCortexPath(), projectName);
|
|
675
823
|
if (!fs.existsSync(projectDir)) {
|
|
676
824
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
677
825
|
fs.writeFileSync(path.join(projectDir, "FINDINGS.md"), `# ${projectName} Findings\n`);
|
|
@@ -722,7 +870,7 @@ async function handleDebugInjection(args) {
|
|
|
722
870
|
input: payload,
|
|
723
871
|
env: {
|
|
724
872
|
...process.env,
|
|
725
|
-
CORTEX_PATH:
|
|
873
|
+
CORTEX_PATH: getCortexPath(),
|
|
726
874
|
CORTEX_PROFILE: profile,
|
|
727
875
|
},
|
|
728
876
|
timeout: EXEC_TIMEOUT_MS,
|
|
@@ -766,7 +914,7 @@ async function handleInspectIndex(args) {
|
|
|
766
914
|
return;
|
|
767
915
|
}
|
|
768
916
|
}
|
|
769
|
-
const db = await buildIndex(
|
|
917
|
+
const db = await buildIndex(getCortexPath(), profile);
|
|
770
918
|
const where = [];
|
|
771
919
|
const params = [];
|
|
772
920
|
if (project) {
|
package/mcp/dist/data-access.js
CHANGED
|
@@ -2,55 +2,19 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as yaml from "js-yaml";
|
|
4
4
|
import { appendAuditLog, cortexErr, CortexError, cortexOk, forwardErr, getProjectDirs, } from "./shared.js";
|
|
5
|
-
import { checkPermission, getWorkflowPolicy, getRuntimeHealth, } from "./shared-governance.js";
|
|
5
|
+
import { checkPermission, getWorkflowPolicy, getRuntimeHealth, withFileLock as withFileLockRaw, } from "./shared-governance.js";
|
|
6
6
|
import { addFindingToFile, validateBacklogFormat, } from "./shared-content.js";
|
|
7
7
|
import { isValidProjectName, queueFilePath, safeProjectPath } from "./utils.js";
|
|
8
8
|
function withFileLock(filePath, fn) {
|
|
9
|
-
const lockPath = filePath + ".lock";
|
|
10
|
-
const maxWait = Number.parseInt(process.env.CORTEX_FILE_LOCK_MAX_WAIT_MS || "5000", 10) || 5000;
|
|
11
|
-
const pollInterval = Number.parseInt(process.env.CORTEX_FILE_LOCK_POLL_MS || "100", 10) || 100;
|
|
12
|
-
const staleThreshold = Number.parseInt(process.env.CORTEX_FILE_LOCK_STALE_MS || "30000", 10) || 30000;
|
|
13
|
-
const waiter = new Int32Array(new SharedArrayBuffer(4));
|
|
14
|
-
const sleep = (ms) => Atomics.wait(waiter, 0, 0, ms);
|
|
15
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
16
|
-
let waited = 0;
|
|
17
|
-
let hasLock = false;
|
|
18
|
-
while (waited < maxWait) {
|
|
19
|
-
try {
|
|
20
|
-
fs.writeFileSync(lockPath, `${process.pid}\n${Date.now()}`, { flag: "wx" });
|
|
21
|
-
hasLock = true;
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
try {
|
|
26
|
-
const stat = fs.statSync(lockPath);
|
|
27
|
-
if (Date.now() - stat.mtimeMs > staleThreshold) {
|
|
28
|
-
fs.unlinkSync(lockPath);
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
sleep(pollInterval);
|
|
34
|
-
waited += pollInterval;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
// Block this thread without spin-looping CPU while waiting to retry lock acquisition.
|
|
38
|
-
sleep(pollInterval);
|
|
39
|
-
waited += pollInterval;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (!hasLock)
|
|
43
|
-
return cortexErr(`Could not acquire write lock for "${path.basename(filePath)}" within ${maxWait}ms. Another write may be in progress; please retry.`, CortexError.LOCK_TIMEOUT);
|
|
44
9
|
try {
|
|
45
|
-
return fn
|
|
10
|
+
return withFileLockRaw(filePath, fn);
|
|
46
11
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
catch { /* lock may not exist */ }
|
|
12
|
+
catch (err) {
|
|
13
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14
|
+
if (msg.includes("could not acquire lock")) {
|
|
15
|
+
return cortexErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, CortexError.LOCK_TIMEOUT);
|
|
53
16
|
}
|
|
17
|
+
throw err;
|
|
54
18
|
}
|
|
55
19
|
}
|
|
56
20
|
const SHELL_STATE_VERSION = 1;
|
package/mcp/dist/index.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseMcpMode, runInit } from "./init.js";
|
|
3
3
|
import * as os from "os";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { findCortexPathWithArg, debugLog, runtimeDir, } from "./shared.js";
|
|
10
|
+
import { buildIndex, updateFileInIndex as updateFileInIndexFn } from "./shared-index.js";
|
|
11
|
+
import { runCustomHooks } from "./hooks.js";
|
|
12
|
+
import { register as registerSearch } from "./mcp-search.js";
|
|
13
|
+
import { register as registerBacklog } from "./mcp-backlog.js";
|
|
14
|
+
import { register as registerFinding } from "./mcp-finding.js";
|
|
15
|
+
import { register as registerMemory } from "./mcp-memory.js";
|
|
16
|
+
import { register as registerData } from "./mcp-data.js";
|
|
17
|
+
import { register as registerGraph } from "./mcp-graph.js";
|
|
18
|
+
import { register as registerSession } from "./mcp-session.js";
|
|
4
19
|
if (process.argv[2] === "--help" || process.argv[2] === "-h" || process.argv[2] === "help") {
|
|
5
20
|
console.log(`cortex - Long-term memory for Claude Code
|
|
6
21
|
|
|
@@ -10,6 +25,12 @@ Usage:
|
|
|
10
25
|
cortex init [--machine <n>] [--profile <n>] [--mcp on|off] [--template <t>] [--from-existing <path>] [--dry-run] [-y]
|
|
11
26
|
Set up cortex (templates: python-project, monorepo, library, frontend)
|
|
12
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
|
|
13
34
|
cortex status Health, active project, stats
|
|
14
35
|
cortex search <query> [--project <n>] [--type <t>] [--limit <n>]
|
|
15
36
|
Search your cortex
|
|
@@ -191,6 +212,8 @@ const CLI_COMMANDS = [
|
|
|
191
212
|
"review-ui",
|
|
192
213
|
"quality-feedback",
|
|
193
214
|
"skill-list",
|
|
215
|
+
"skills",
|
|
216
|
+
"hooks",
|
|
194
217
|
"detect-skills",
|
|
195
218
|
"backlog",
|
|
196
219
|
"quickstart",
|
|
@@ -218,21 +241,6 @@ if (CLI_COMMANDS.includes(process.argv[2])) {
|
|
|
218
241
|
await runCliCommand(cmd, process.argv.slice(3));
|
|
219
242
|
process.exit(0);
|
|
220
243
|
}
|
|
221
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
222
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
223
|
-
import * as fs from "fs";
|
|
224
|
-
import * as path from "path";
|
|
225
|
-
import { fileURLToPath } from "url";
|
|
226
|
-
import { findCortexPathWithArg, debugLog, runtimeDir, } from "./shared.js";
|
|
227
|
-
import { buildIndex, updateFileInIndex as updateFileInIndexFn } from "./shared-index.js";
|
|
228
|
-
import { runCustomHooks } from "./hooks.js";
|
|
229
|
-
import { register as registerSearch } from "./mcp-search.js";
|
|
230
|
-
import { register as registerBacklog } from "./mcp-backlog.js";
|
|
231
|
-
import { register as registerFinding } from "./mcp-finding.js";
|
|
232
|
-
import { register as registerMemory } from "./mcp-memory.js";
|
|
233
|
-
import { register as registerData } from "./mcp-data.js";
|
|
234
|
-
import { register as registerGraph } from "./mcp-graph.js";
|
|
235
|
-
import { register as registerSession } from "./mcp-session.js";
|
|
236
244
|
// MCP mode: first non-flag arg is the cortex path
|
|
237
245
|
const cortexArg = process.argv.find((a, i) => i >= 2 && !a.startsWith("-"));
|
|
238
246
|
const cortexPath = findCortexPathWithArg(cortexArg);
|
package/mcp/dist/link-skills.js
CHANGED
|
@@ -215,12 +215,6 @@ Cursor, Codex, and more.
|
|
|
215
215
|
- \`import_project\`: import project from previously exported JSON
|
|
216
216
|
- \`manage_project(project, action: "archive"|"unarchive")\`: archive or restore a project
|
|
217
217
|
|
|
218
|
-
**Graph and session:**
|
|
219
|
-
- \`get_learnings\`: alias for browsing findings/learnings
|
|
220
|
-
- \`add_learning\`: alias for add_finding (backward compat)
|
|
221
|
-
- \`add_learnings\`: alias for add_findings (backward compat)
|
|
222
|
-
- \`remove_learning\`: alias for remove_finding (backward compat)
|
|
223
|
-
- \`remove_learnings\`: alias for remove_findings (backward compat)
|
|
224
218
|
`;
|
|
225
219
|
const dest = path.join(cortexPath, "cortex.SKILL.md");
|
|
226
220
|
fs.writeFileSync(dest, content);
|
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/mcp-finding.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from "fs";
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { isValidProjectName, safeProjectPath } from "./utils.js";
|
|
6
6
|
import { removeFinding as removeFindingCore, removeFindings as removeFindingsCore, } from "./core-finding.js";
|
|
7
|
-
import { debugLog, EXEC_TIMEOUT_MS, } from "./shared.js";
|
|
7
|
+
import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, } from "./shared.js";
|
|
8
8
|
import { addFindingToFile, addFindingsToFile, checkSemanticDedup, checkSemanticConflicts, autoMergeConflicts, } from "./shared-content.js";
|
|
9
9
|
import { runCustomHooks } from "./hooks.js";
|
|
10
10
|
import { incrementSessionFindings } from "./mcp-session.js";
|
|
@@ -26,7 +26,7 @@ export function register(server, ctx) {
|
|
|
26
26
|
commit: z.string().optional().describe("Git commit SHA that supports this finding."),
|
|
27
27
|
supersedes: z.string().optional().describe("First 60 chars of the old finding this one replaces. The old entry will be marked as superseded."),
|
|
28
28
|
}).optional().describe("Optional source citation for traceability."),
|
|
29
|
-
findingType: z.enum(
|
|
29
|
+
findingType: z.enum(FINDING_TYPES)
|
|
30
30
|
.optional()
|
|
31
31
|
.describe("Classify this finding: 'decision' (architectural choice with rationale), 'pitfall' (bug or gotcha to avoid), 'pattern' (reusable approach that works well)."),
|
|
32
32
|
}),
|
package/mcp/dist/mcp-search.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from "fs";
|
|
|
4
4
|
import { isValidProjectName, buildRobustFtsQuery } from "./utils.js";
|
|
5
5
|
import { keywordFallbackSearch } from "./core-search.js";
|
|
6
6
|
import { readFindings } from "./data-access.js";
|
|
7
|
-
import { debugLog, runtimeFile, } from "./shared.js";
|
|
7
|
+
import { debugLog, runtimeFile, DOC_TYPES, FINDING_TAGS, } from "./shared.js";
|
|
8
8
|
import { queryRows, cosineFallback, queryEntityLinks, extractSnippet, queryDocBySourceKey, } from "./shared-index.js";
|
|
9
9
|
import { runCustomHooks } from "./hooks.js";
|
|
10
10
|
import { entryScoreKey, getQualityMultiplier } from "./shared-governance.js";
|
|
@@ -72,10 +72,10 @@ export function register(server, ctx) {
|
|
|
72
72
|
query: z.string().describe("Search query (supports FTS5 syntax: AND, OR, NOT, phrase matching with quotes)"),
|
|
73
73
|
limit: z.number().min(1).max(20).optional().describe("Max results to return (1-20, default 5)"),
|
|
74
74
|
project: z.string().optional().describe("Filter by project name."),
|
|
75
|
-
type: z.enum(
|
|
75
|
+
type: z.enum(DOC_TYPES)
|
|
76
76
|
.optional()
|
|
77
77
|
.describe("Filter by document type: claude, findings, reference, summary, backlog, skill"),
|
|
78
|
-
tag: z.enum(
|
|
78
|
+
tag: z.enum(FINDING_TAGS)
|
|
79
79
|
.optional()
|
|
80
80
|
.describe("Filter findings by type tag (decision, pitfall, pattern are canonical; tradeoff, architecture, bug are legacy aliases)."),
|
|
81
81
|
since: z.string().optional().describe('Filter findings by creation date. Formats: "7d" (last 7 days), "30d" (last 30 days), "YYYY-MM" (since start of month), "YYYY-MM-DD" (since date).'),
|
|
@@ -277,8 +277,8 @@ export function register(server, ctx) {
|
|
|
277
277
|
boost = 1.2;
|
|
278
278
|
}
|
|
279
279
|
catch { /* file may not exist on disk */ }
|
|
280
|
+
const scoreKey = entryScoreKey(rowProject, filename, content);
|
|
280
281
|
const snippet = extractSnippet(content, query);
|
|
281
|
-
const scoreKey = entryScoreKey(rowProject, filename, snippet);
|
|
282
282
|
boost *= getQualityMultiplier(cortexPath, scoreKey);
|
|
283
283
|
return { row, rank: (rows.length - idx) * boost };
|
|
284
284
|
});
|