@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/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
- const cortexPath = ensureCortexPath();
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(cortexPath, "search-history.jsonl");
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(cortexPath, profile);
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(cortexPath, opts.query, opts.project);
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(cortexPath, project, learning);
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(cortexPath, project, memory);
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(cortexPath, fix, checkData);
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(cortexPath, "search-misses.jsonl");
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(cortexPath, key, feedback);
459
- flushEntryScores(cortexPath);
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(cortexPath, safePort);
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(cortexPath, profile);
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(cortexPath, "global", "skills");
662
+ const globalSkillsDir = path.join(getCortexPath(), "global", "skills");
515
663
  collectSkills(globalSkillsDir, "global");
516
- const projectDirs = getProjectDirs(cortexPath, profile);
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
- const projectSkillsDir = path.join(dir, "skills");
522
- collectSkills(projectSkillsDir, projectName);
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(cortexPath, "global", "skills");
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(cortexPath, profile);
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(cortexPath, profile);
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(cortexPath, {});
674
- const projectDir = path.join(cortexPath, projectName);
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: cortexPath,
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(cortexPath, profile);
917
+ const db = await buildIndex(getCortexPath(), profile);
770
918
  const where = [];
771
919
  const params = [];
772
920
  if (project) {
@@ -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
- finally {
48
- if (hasLock) {
49
- try {
50
- fs.unlinkSync(lockPath);
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);
@@ -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
- if (fs.existsSync(projectSkills)) {
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
  }
@@ -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(["decision", "pitfall", "pattern"])
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
  }),
@@ -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(["claude", "findings", "reference", "skills", "summary", "backlog", "changelog", "canonical", "memory-queue", "skill", "other"])
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(["decision", "pitfall", "pattern", "tradeoff", "architecture", "bug"])
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
  });