@alaarab/cortex 1.14.0 → 1.15.1

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 CHANGED
@@ -98,7 +98,7 @@ On a new machine: clone, run init, done.
98
98
 
99
99
  ## What's new
100
100
 
101
- - **Terminal shell**: open `cortex` and get tabs for Backlog, Findings, Memory Queue, and Health. No agent needed
101
+ - **Terminal shell**: open `cortex` and get tabs for Backlog, Findings, Review Queue, Skills, Hooks, and Health. No agent needed
102
102
  - **Synonym search**: type "throttling" and find "rate limit" and "429". You don't need to remember what you called it
103
103
  - **Bulk operations**: `add_findings`, `add_backlog_items`, `complete_backlog_items`, `remove_findings` for batch work
104
104
  - **Memory quality**: confidence scoring, age decay, and a feedback loop. Stale or low-signal entries stop appearing
@@ -148,7 +148,7 @@ On a new machine: clone, run init, done.
148
148
 
149
149
  **Consolidation.** When findings accumulate past the threshold, cortex flags it once per session. The `/cortex-consolidate` skill archives old entries and promotes cross-project patterns to global findings.
150
150
 
151
- **Memory queue.** Findings that fail trust filtering land in `MEMORY_QUEUE.md` for review. Triage from the shell (press `m`) or with `:mq approve`, `:mq reject`, `:mq edit`.
151
+ **Review queue.** Findings that fail trust filtering land in `MEMORY_QUEUE.md` for review. Triage from the shell (press `m`) or with `:mq approve`, `:mq reject`, `:mq edit`.
152
152
 
153
153
  ---
154
154
 
@@ -160,7 +160,8 @@ The server indexes your cortex into a local SQLite FTS5 database. Tools are grou
160
160
 
161
161
  | Tool | What it does |
162
162
  |------|-------------|
163
- | `search_cortex` | FTS5 search with synonym expansion. Filters by project, type, limit. |
163
+ | `search_knowledge` | FTS5 search with synonym expansion. Filters by project, type, limit. |
164
+ | `get_memory_detail` | Fetch full content of a memory by id (e.g. `mem:project/filename`). |
164
165
  | `get_project_summary` | Summary card and file list for a project. |
165
166
  | `list_projects` | Everything in your active profile. |
166
167
  | `get_findings` | Read recent findings for a project without a search query. |
@@ -225,14 +226,16 @@ Governance, policy, and maintenance tools are CLI-only (see `cortex config` and
225
226
 
226
227
  ## Interactive shell
227
228
 
228
- `cortex` in a terminal opens the shell. Five views, single-key navigation:
229
+ `cortex` in a terminal opens the shell. Seven views, single-key navigation:
229
230
 
230
231
  | Key | View |
231
232
  |-----|------|
232
233
  | `p` | Projects |
233
234
  | `b` | Backlog |
234
235
  | `l` | Findings |
235
- | `m` | Memory Queue |
236
+ | `m` | Review Queue |
237
+ | `s` | Skills |
238
+ | `k` | Hooks |
236
239
  | `h` | Health |
237
240
  | `/` | Filter current view |
238
241
  | `:` | Command palette |
@@ -263,16 +266,33 @@ The shell works the same on every machine, for every agent.
263
266
  For scripting, hooks, and quick lookups from the terminal:
264
267
 
265
268
  ```bash
266
- cortex # interactive shell (TTY default)
267
- cortex search "rate limiting" # FTS5 search with synonym expansion
268
- cortex add-finding <project> "..." # append a finding from the terminal
269
- cortex pin <project> "..." # promote canonical memory
270
- cortex doctor [--fix] # health checks + optional self-heal
271
- cortex review-ui [--port=3499] # lightweight review UI in the browser
272
- cortex update # update to latest version
269
+ cortex # interactive shell (TTY default)
270
+ cortex search "rate limiting" # FTS5 search with synonym expansion
271
+ cortex add-finding <project> "..." # append a finding from the terminal
272
+ cortex pin <project> "..." # promote canonical memory
273
+ cortex backlog [project] # cross-project backlog view
274
+ cortex status # health, active project, stats
275
+ cortex doctor [--fix] # health checks + optional self-heal
276
+ cortex verify # check init completed correctly
277
+ cortex review-ui [--port=3499] # lightweight review UI in the browser
278
+ cortex update # update to latest version
279
+ cortex uninstall # remove cortex config and hooks
280
+
281
+ cortex link [--machine <n>] [--profile <n>] # sync profile, symlinks, hooks
282
+ cortex mcp-mode [on|off|status] # toggle MCP integration
283
+ cortex hooks-mode [on|off|status] # toggle hook execution
284
+
285
+ cortex skills list # list all installed skills
286
+ cortex skills add <project> <path> # add a skill to a project
287
+ cortex skills remove <project> <name> # remove a skill from a project
288
+ cortex skill-list # alias for skills list
289
+
290
+ cortex hooks list # show hook status per tool
291
+ cortex hooks enable <tool> # enable hooks for tool (claude/copilot/cursor/codex)
292
+ cortex hooks disable <tool> # disable hooks for tool
273
293
  ```
274
294
 
275
- Use `cortex config` for policy tuning and `cortex maintain` for governance operations. Top-level aliases still work for backwards compatibility. Run `--dry-run` before destructive maintenance commands.
295
+ Use `cortex config` for policy tuning and `cortex maintain` for governance operations. Run `--dry-run` before destructive maintenance commands.
276
296
 
277
297
  ### cortex doctor
278
298
 
@@ -365,6 +385,27 @@ Four skills for the things that can't be automatic:
365
385
 
366
386
  Put personal workflow skills in `~/.cortex/global/skills/`. `cortex link` symlinks them to `~/.claude/skills/` so they're available everywhere.
367
387
 
388
+ ### Per-project agent config
389
+
390
+ Drop a `cortex.project.yaml` in `~/.cortex/<project>/` to control what gets injected for that project:
391
+
392
+ ```yaml
393
+ # Opt out of global skill injection for this project
394
+ skills: false
395
+
396
+ # Register extra MCP servers when this project is linked
397
+ mcpServers:
398
+ my-tool:
399
+ command: node
400
+ args: [/path/to/server.js]
401
+ my-api:
402
+ command: /usr/local/bin/api-server
403
+ env:
404
+ API_KEY: "from-your-env"
405
+ ```
406
+
407
+ `cortex link` merges project MCP servers into your agent config under namespaced keys (`cortex__<project>__<name>`) and cleans them up automatically when the config changes.
408
+
368
409
  ---
369
410
 
370
411
  ## Adding projects
@@ -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 = project ? 180 : 260;
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.15)
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;
@@ -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
- const projectSkillsDir = path.join(dir, "skills");
527
- collectSkills(projectSkillsDir, projectName);
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
@@ -5,7 +5,7 @@ import * as readline from "readline";
5
5
  import * as yaml from "js-yaml";
6
6
  import { execFileSync } from "child_process";
7
7
  import { fileURLToPath } from "url";
8
- import { configureClaude, configureCodexMcp, configureCopilotMcp, configureCursorMcp, configureVSCode, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, logMcpTargetStatus, setMcpEnabledPreference, } from "./init.js";
8
+ import { configureClaude, configureCodexMcp, configureCopilotMcp, configureCursorMcp, configureVSCode, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, logMcpTargetStatus, patchJsonFile, setMcpEnabledPreference, } from "./init.js";
9
9
  import { configureAllHooks, detectInstalledTools } from "./hooks.js";
10
10
  import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, } from "./shared.js";
11
11
  import { linkSkillsDir, writeSkillMd } from "./link-skills.js";
@@ -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,10 +330,49 @@ 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
  }
338
+ // Per-project MCP servers
339
+ if (config.mcpServers && typeof config.mcpServers === "object") {
340
+ linkProjectMcpServers(project, config.mcpServers);
341
+ }
342
+ }
343
+ /**
344
+ * Merge per-project MCP servers into Claude's settings.json.
345
+ * Keys are namespaced as "cortex__<project>__<name>" so we can identify
346
+ * and clean them up without touching user-managed servers.
347
+ */
348
+ function linkProjectMcpServers(project, servers) {
349
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
350
+ if (!fs.existsSync(settingsPath) && Object.keys(servers).length === 0)
351
+ return;
352
+ try {
353
+ patchJsonFile(settingsPath, (data) => {
354
+ if (!data.mcpServers || typeof data.mcpServers !== "object")
355
+ data.mcpServers = {};
356
+ // Remove stale entries for this project (keys we previously wrote)
357
+ for (const key of Object.keys(data.mcpServers)) {
358
+ if (key.startsWith(`cortex__${project}__`))
359
+ delete data.mcpServers[key];
360
+ }
361
+ // Add current entries
362
+ for (const [name, entry] of Object.entries(servers)) {
363
+ const key = `cortex__${project}__${name}`;
364
+ const server = { command: entry.command };
365
+ if (Array.isArray(entry.args))
366
+ server.args = entry.args;
367
+ if (entry.env && typeof entry.env === "object")
368
+ server.env = entry.env;
369
+ data.mcpServers[key] = server;
370
+ }
371
+ });
372
+ }
373
+ catch (err) {
374
+ debugLog(`linkProjectMcpServers: failed for ${project}: ${err instanceof Error ? err.message : String(err)}`);
375
+ }
325
376
  }
326
377
  // ── Main orchestrator ───────────────────────────────────────────────────────
327
378
  export async function runLink(cortexPath, opts = {}) {
@@ -8,6 +8,7 @@ import * as path from "path";
8
8
  import { addBacklogItem, addFinding, addProjectToProfile, approveQueueItem, completeBacklogItem, editQueueItem, listProjectCards, pinBacklogItem, readBacklog, readFindings, readReviewQueue, rejectQueueItem, removeFinding, removeProjectFromProfile, resetShellState, saveShellState, setMachineProfile, tidyBacklogDone, unpinBacklogItem, updateBacklogItem, workNextBacklogItem, loadShellState, } from "./data-access.js";
9
9
  import { style } from "./shell-render.js";
10
10
  import { SUB_VIEWS, TAB_ICONS } from "./shell-types.js";
11
+ import { getProjectSkills, getHookEntries, writeInstallPreferences } from "./shell-view.js";
11
12
  import { resultMsg, editDistance, tokenize, expandIds, normalizeSection, resolveEntryScript, backlogsByFilter, queueByFilter, } from "./shell-palette.js";
12
13
  export async function executePalette(host, input) {
13
14
  const trimmed = input.trim();
@@ -600,6 +601,14 @@ export function getListItems(cortexPath, profile, state, healthLineCount) {
600
601
  return [];
601
602
  return state.filter ? queueByFilter(result.data, state.filter) : result.data;
602
603
  }
604
+ case "Skills": {
605
+ if (!state.project)
606
+ return [];
607
+ return getProjectSkills(cortexPath, state.project).map((s) => ({ name: s.name, text: s.path }));
608
+ }
609
+ case "Hooks": {
610
+ return getHookEntries(cortexPath).map((e) => ({ name: e.tool, text: e.enabled ? "enabled" : "disabled" }));
611
+ }
603
612
  case "Health":
604
613
  return Array.from({ length: Math.max(1, healthLineCount) }, (_, i) => ({ id: String(i) }));
605
614
  default:
@@ -644,7 +653,17 @@ export async function activateSelected(host) {
644
653
  break;
645
654
  case "Review Queue":
646
655
  if (item.text) {
647
- host.setMessage(` ${style.dim(item.id ?? "")} ${item.text} ${style.dim("[ a approve · r reject ]")}`);
656
+ host.setMessage(` ${style.dim(item.id ?? "")} ${item.text} ${style.dim("[ a approve · d reject ]")}`);
657
+ }
658
+ break;
659
+ case "Skills":
660
+ if (item.name) {
661
+ host.setMessage(` ${style.bold(item.name)} ${style.dim(item.text ?? "")}`);
662
+ }
663
+ break;
664
+ case "Hooks":
665
+ if (item.name) {
666
+ host.setMessage(` ${item.text === "enabled" ? style.boldGreen("enabled") : style.dim("disabled")} ${style.bold(item.name)}`);
648
667
  }
649
668
  break;
650
669
  }
@@ -705,7 +724,7 @@ export async function doViewAction(host, key) {
705
724
  host.setCursor(Math.max(0, cursor - 1));
706
725
  });
707
726
  }
708
- else if (key === "r" && item?.id) {
727
+ else if (key === "d" && item?.id) {
709
728
  if (!project) {
710
729
  host.setMessage("Select a project first.");
711
730
  return;
@@ -720,6 +739,36 @@ export async function doViewAction(host, key) {
720
739
  host.startInput("mq-edit", item.text || "");
721
740
  }
722
741
  break;
742
+ case "Skills":
743
+ if ((key === "d" || key === "\x7f") && item?.name) {
744
+ if (!project) {
745
+ host.setMessage("Select a project first.");
746
+ return;
747
+ }
748
+ const skillPath = item.text;
749
+ host.confirmThen(`Remove skill "${item.name}"?`, () => {
750
+ try {
751
+ fs.unlinkSync(skillPath);
752
+ host.setMessage(` Removed ${item.name}`);
753
+ host.setCursor(Math.max(0, cursor - 1));
754
+ }
755
+ catch (err) {
756
+ host.setMessage(` Failed: ${err instanceof Error ? err.message : String(err)}`);
757
+ }
758
+ });
759
+ }
760
+ else if (key === "a") {
761
+ host.setMessage(` Use "cortex skills add ${project ?? "<project>"} <path>" to add a skill`);
762
+ }
763
+ break;
764
+ case "Hooks":
765
+ if ((key === "a" || key === "d") && item?.name) {
766
+ const enable = key === "a";
767
+ const prefs = { hookTools: { ...((getHookEntries(host.cortexPath).reduce((acc, e) => ({ ...acc, [e.tool]: e.enabled }), {}))), [item.name]: enable } };
768
+ writeInstallPreferences(host.cortexPath, prefs);
769
+ host.setMessage(` ${enable ? style.boldGreen("Enabled") : style.dim("Disabled")} hooks for ${item.name}`);
770
+ }
771
+ break;
723
772
  }
724
773
  }
725
774
  // ── Cursor position display ───────────────────────────────────────────────────
@@ -862,6 +911,20 @@ export async function handleNavigateKey(host, key) {
862
911
  host.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
863
912
  return true;
864
913
  }
914
+ if (key === "s") {
915
+ if (!host.state.project) {
916
+ host.setMessage(style.dim(" Select a project first (↵)"));
917
+ return true;
918
+ }
919
+ host.setView("Skills");
920
+ host.setMessage(` ${TAB_ICONS.Skills} Skills`);
921
+ return true;
922
+ }
923
+ if (key === "k") {
924
+ host.setView("Hooks");
925
+ host.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
926
+ return true;
927
+ }
865
928
  if (key === "h") {
866
929
  host.prevHealthView = host.state.view === "Health" ? host.prevHealthView : host.state.view;
867
930
  host.healthCache = undefined;
@@ -869,7 +932,7 @@ export async function handleNavigateKey(host, key) {
869
932
  host.setMessage(` ${TAB_ICONS.Health} Health ${style.dim("(esc to return)")}`);
870
933
  return true;
871
934
  }
872
- if (["a", "d", "r", "e", "\x7f"].includes(key)) {
935
+ if (["a", "d", "e", "\x7f"].includes(key)) {
873
936
  await doViewAction(host, key);
874
937
  return true;
875
938
  }
@@ -1,11 +1,13 @@
1
1
  // Projects is level 0 (the home screen); these sub-views are level 1 (drill-down into a project)
2
2
  // Health is NOT a sub-view — it's a global overlay accessible from anywhere via [h]
3
- export const SUB_VIEWS = ["Backlog", "Findings", "Review Queue"];
3
+ export const SUB_VIEWS = ["Backlog", "Findings", "Review Queue", "Skills", "Hooks"];
4
4
  export const TAB_ICONS = {
5
5
  Projects: "◉",
6
6
  Backlog: "▤",
7
7
  Findings: "✦",
8
8
  "Review Queue": "◈",
9
+ Skills: "◆",
10
+ Hooks: "⚡",
9
11
  Health: "♡",
10
12
  };
11
13
  export const MAX_UNDO_STACK = 10;
@@ -9,6 +9,7 @@ import { RESET, style, badge, separator, stripAnsi, padToWidth, truncateLine, li
9
9
  import { SUB_VIEWS, TAB_ICONS, } from "./shell-types.js";
10
10
  import { backlogsByFilter, queueByFilter, } from "./shell-palette.js";
11
11
  import { listMachines, listProfiles, } from "./data-access.js";
12
+ import { readInstallPreferences } from "./init-preferences.js";
12
13
  // ── Tab bar ────────────────────────────────────────────────────────────────
13
14
  export function renderTabBar(state) {
14
15
  const cols = process.stdout.columns || 80;
@@ -56,7 +57,9 @@ export function renderBottomBar(state, navMode, inputCtx, inputBuf) {
56
57
  Projects: [`${k("↵")} ${d("open project")}`],
57
58
  Backlog: [`${k("a")} ${d("add")}`, `${k("↵")} ${d("mark done")}`, `${k("d")} ${d("toggle active")}`],
58
59
  Findings: [`${k("a")} ${d("add")}`, `${k("d")} ${d("remove")}`],
59
- "Review Queue": [`${k("a")} ${d("keep")}`, `${k("r")} ${d("discard")}`, `${k("e")} ${d("edit")}`],
60
+ "Review Queue": [`${k("a")} ${d("keep")}`, `${k("d")} ${d("discard")}`, `${k("e")} ${d("edit")}`],
61
+ Skills: [`${k("d")} ${d("remove")}`],
62
+ Hooks: [`${k("a")} ${d("enable")}`, `${k("d")} ${d("disable")}`],
60
63
  Health: [`${k("↑↓")} ${d("scroll")}`, `${k("esc")} ${d("back")}`],
61
64
  };
62
65
  const extra = viewHints[state.view] ?? [];
@@ -356,6 +359,118 @@ export function renderMemoryQueueView(ctx, cursor, height) {
356
359
  }
357
360
  return vp.lines;
358
361
  }
362
+ export function getProjectSkills(cortexPath, project) {
363
+ const dirs = [
364
+ path.join(cortexPath, project, "skills"),
365
+ path.join(cortexPath, project, ".claude", "skills"),
366
+ ];
367
+ const seen = new Set();
368
+ const skills = [];
369
+ for (const dir of dirs) {
370
+ if (!fs.existsSync(dir))
371
+ continue;
372
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
373
+ if (entry.isFile() && entry.name.endsWith(".md")) {
374
+ const name = entry.name.replace(/\.md$/, "");
375
+ if (!seen.has(name)) {
376
+ seen.add(name);
377
+ skills.push({ name, path: path.join(dir, entry.name) });
378
+ }
379
+ }
380
+ else if (entry.isDirectory()) {
381
+ const skillFile = path.join(dir, entry.name, "SKILL.md");
382
+ if (fs.existsSync(skillFile) && !seen.has(entry.name)) {
383
+ seen.add(entry.name);
384
+ skills.push({ name: entry.name, path: skillFile });
385
+ }
386
+ }
387
+ }
388
+ }
389
+ return skills;
390
+ }
391
+ export function renderSkillsView(ctx, cursor, height) {
392
+ const cols = process.stdout.columns || 80;
393
+ const project = ctx.state.project;
394
+ if (!project)
395
+ return [style.dim(" No project selected.")];
396
+ const skills = getProjectSkills(ctx.cortexPath, project);
397
+ const filtered = ctx.state.filter
398
+ ? skills.filter((s) => s.name.toLowerCase().includes(ctx.state.filter.toLowerCase()))
399
+ : skills;
400
+ if (!filtered.length) {
401
+ return [style.dim(` No skills for ${project}. Use "cortex skills add ${project} <path>" to add one.`)];
402
+ }
403
+ const allLines = [];
404
+ let cursorFirstLine = 0;
405
+ let cursorLastLine = 0;
406
+ for (let i = 0; i < filtered.length; i++) {
407
+ const s = filtered[i];
408
+ const isSelected = i === cursor;
409
+ if (isSelected)
410
+ cursorFirstLine = allLines.length;
411
+ const isSymlink = (() => { try {
412
+ return fs.lstatSync(s.path).isSymbolicLink();
413
+ }
414
+ catch {
415
+ return false;
416
+ } })();
417
+ const linkTag = isSymlink ? style.dim(" →") : "";
418
+ let row = ` ${style.dim((i + 1).toString().padEnd(3))} ${style.bold(s.name)}${linkTag}`;
419
+ if (isSelected)
420
+ row = `\x1b[7m${padToWidth(row, cols)}${RESET}`;
421
+ else
422
+ row = truncateLine(row, cols);
423
+ allLines.push(row);
424
+ if (isSelected)
425
+ cursorLastLine = allLines.length - 1;
426
+ }
427
+ const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
428
+ const vp = lineViewport(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll());
429
+ ctx.setScroll(vp.scrollStart);
430
+ if (allLines.length > usableHeight) {
431
+ const pct = filtered.length <= 1 ? 100 : Math.round((cursor / (filtered.length - 1)) * 100);
432
+ vp.lines.push(style.dim(` ─── ${cursor + 1}/${filtered.length} ${pct}%`));
433
+ }
434
+ return vp.lines;
435
+ }
436
+ // ── Hooks view ─────────────────────────────────────────────────────────────
437
+ const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
438
+ export function getHookEntries(cortexPath) {
439
+ const prefs = readInstallPreferences(cortexPath);
440
+ const hooksEnabled = prefs.hooksEnabled !== false;
441
+ const toolPrefs = (prefs.hookTools && typeof prefs.hookTools === "object") ? prefs.hookTools : {};
442
+ return HOOK_TOOLS.map((tool) => ({
443
+ tool,
444
+ enabled: hooksEnabled && toolPrefs[tool] !== false,
445
+ }));
446
+ }
447
+ export function renderHooksView(ctx, cursor, height) {
448
+ const cols = process.stdout.columns || 80;
449
+ const entries = getHookEntries(ctx.cortexPath);
450
+ const allLines = [];
451
+ let cursorFirstLine = 0;
452
+ let cursorLastLine = 0;
453
+ for (let i = 0; i < entries.length; i++) {
454
+ const e = entries[i];
455
+ const isSelected = i === cursor;
456
+ if (isSelected)
457
+ cursorFirstLine = allLines.length;
458
+ const statusBadge = e.enabled ? style.boldGreen("enabled ") : style.dim("disabled");
459
+ let row = ` ${style.dim((i + 1).toString().padEnd(3))} ${statusBadge} ${style.bold(e.tool)}`;
460
+ if (isSelected)
461
+ row = `\x1b[7m${padToWidth(row, cols)}${RESET}`;
462
+ else
463
+ row = truncateLine(row, cols);
464
+ allLines.push(row);
465
+ if (isSelected)
466
+ cursorLastLine = allLines.length - 1;
467
+ }
468
+ const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
469
+ const vp = lineViewport(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll());
470
+ ctx.setScroll(vp.scrollStart);
471
+ return vp.lines;
472
+ }
473
+ export { writeInstallPreferences } from "./init-preferences.js";
359
474
  // ── Machines/Profiles view ─────────────────────────────────────────────────
360
475
  export function renderMachinesView(cortexPath) {
361
476
  const machines = listMachines(cortexPath);
@@ -455,6 +570,12 @@ export async function renderShell(ctx, navMode, inputCtx, inputBuf, showHelp, me
455
570
  case "Review Queue":
456
571
  contentLines = renderMemoryQueueView(ctx, cursor, height);
457
572
  break;
573
+ case "Skills":
574
+ contentLines = renderSkillsView(ctx, cursor, height);
575
+ break;
576
+ case "Hooks":
577
+ contentLines = renderHooksView(ctx, cursor, height);
578
+ break;
458
579
  case "Machines/Profiles":
459
580
  contentLines = renderMachinesView(ctx.cortexPath);
460
581
  break;
package/mcp/dist/shell.js CHANGED
@@ -298,6 +298,20 @@ export class CortexShell {
298
298
  this.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
299
299
  return true;
300
300
  }
301
+ if (input === "s") {
302
+ if (!this.state.project) {
303
+ this.setMessage(style.dim(" Select a project first (↵)"));
304
+ return true;
305
+ }
306
+ this.setView("Skills");
307
+ this.setMessage(` ${TAB_ICONS.Skills} Skills`);
308
+ return true;
309
+ }
310
+ if (input === "k") {
311
+ this.setView("Hooks");
312
+ this.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
313
+ return true;
314
+ }
301
315
  if (input === "h") {
302
316
  if (!this.state.project) {
303
317
  this.setMessage(style.dim(" Select a project first (↵)"));
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 (SYNONYMS[bg]) {
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 && SYNONYMS[termText]) {
330
- for (const syn of SYNONYMS[termText]) {
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.14.0",
3
+ "version": "1.15.1",
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.0.0",
15
+ "@modelcontextprotocol/sdk": "^1.27.1",
16
16
  "@xenova/transformers": "^2.17.2",
17
- "glob": "^12.0.0",
18
- "js-yaml": "^4.1.0",
17
+ "glob": "^13.0.6",
18
+ "js-yaml": "^4.1.1",
19
19
  "sql.js-fts5": "^1.4.0",
20
- "zod": "^4.0.0"
20
+ "zod": "^4.3.6"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/js-yaml": "^4.0.0",
24
- "@types/node": "^22.0.0",
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.2",
28
- "tsx": "^4.0.0",
29
- "typescript": "^5.7.0",
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": {