@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.
@@ -107,32 +107,43 @@ function touchSentinel(cortexPath) {
107
107
  }
108
108
  catch { /* best-effort */ }
109
109
  }
110
- function computeCortexHash(cortexPath, profile) {
111
- const projectDirs = getProjectDirs(cortexPath, profile);
110
+ function computeCortexHash(cortexPath, profile, preGlobbed) {
112
111
  const policy = getIndexPolicy(cortexPath);
113
- const files = [];
114
- for (const dir of projectDirs) {
115
- try {
116
- const matched = new Set();
117
- for (const pattern of policy.includeGlobs) {
118
- const dot = policy.includeHidden || pattern.startsWith(".") || pattern.includes("/.");
119
- const mdFiles = globSync(pattern, { cwd: dir, nodir: true, dot, ignore: policy.excludeGlobs });
120
- for (const f of mdFiles)
121
- matched.add(f);
112
+ const hash = crypto.createHash("sha1");
113
+ if (preGlobbed) {
114
+ for (const f of preGlobbed) {
115
+ try {
116
+ const stat = fs.statSync(f);
117
+ hash.update(`${f}:${stat.mtimeMs}:${stat.size}`);
122
118
  }
123
- for (const f of matched)
124
- files.push(path.join(dir, f));
119
+ catch { /* skip */ }
125
120
  }
126
- catch { /* skip unreadable dirs */ }
127
121
  }
128
- files.sort();
129
- const hash = crypto.createHash("sha1");
130
- for (const f of files) {
131
- try {
132
- const stat = fs.statSync(f);
133
- hash.update(`${f}:${stat.mtimeMs}:${stat.size}`);
122
+ else {
123
+ const projectDirs = getProjectDirs(cortexPath, profile);
124
+ const files = [];
125
+ for (const dir of projectDirs) {
126
+ try {
127
+ const matched = new Set();
128
+ for (const pattern of policy.includeGlobs) {
129
+ const dot = policy.includeHidden || pattern.startsWith(".") || pattern.includes("/.");
130
+ const mdFiles = globSync(pattern, { cwd: dir, nodir: true, dot, ignore: policy.excludeGlobs });
131
+ for (const f of mdFiles)
132
+ matched.add(f);
133
+ }
134
+ for (const f of matched)
135
+ files.push(path.join(dir, f));
136
+ }
137
+ catch { /* skip unreadable dirs */ }
138
+ }
139
+ files.sort();
140
+ for (const f of files) {
141
+ try {
142
+ const stat = fs.statSync(f);
143
+ hash.update(`${f}:${stat.mtimeMs}:${stat.size}`);
144
+ }
145
+ catch { /* skip */ }
134
146
  }
135
- catch { /* skip */ }
136
147
  }
137
148
  for (const mem of collectNativeMemoryFiles()) {
138
149
  try {
@@ -227,10 +238,11 @@ function getEntrySourceDocKey(entry, cortexPath) {
227
238
  }
228
239
  return buildSourceDocKey(entry.project, entry.fullPath, cortexPath, entry.filename);
229
240
  }
230
- function collectAllFiles(cortexPath, profile) {
241
+ function globAllFiles(cortexPath, profile) {
231
242
  const projectDirs = getProjectDirs(cortexPath, profile);
232
243
  const indexPolicy = getIndexPolicy(cortexPath);
233
244
  const entries = [];
245
+ const allAbsolutePaths = [];
234
246
  for (const dir of projectDirs) {
235
247
  const projectName = path.basename(dir);
236
248
  const mdFilesSet = new Set();
@@ -255,12 +267,18 @@ function collectAllFiles(cortexPath, profile) {
255
267
  const fullPath = path.join(dir, relFile);
256
268
  const type = classifyFile(filename, relFile);
257
269
  entries.push({ fullPath, project: projectName, filename, type, relFile });
270
+ allAbsolutePaths.push(fullPath);
258
271
  }
259
272
  }
260
273
  for (const mem of collectNativeMemoryFiles()) {
261
274
  entries.push({ fullPath: mem.fullPath, project: mem.project, filename: mem.file, type: "findings" });
275
+ allAbsolutePaths.push(mem.fullPath);
262
276
  }
263
- return entries;
277
+ allAbsolutePaths.sort();
278
+ return { filePaths: allAbsolutePaths, entries };
279
+ }
280
+ function collectAllFiles(cortexPath, profile) {
281
+ return globAllFiles(cortexPath, profile).entries;
264
282
  }
265
283
  function insertFileIntoIndex(db, entry, cortexPath) {
266
284
  try {
@@ -341,7 +359,8 @@ async function buildIndexImpl(cortexPath, profile) {
341
359
  userSuffix = crypto.createHash("sha1").update(os.homedir()).digest("hex").slice(0, 12);
342
360
  }
343
361
  const cacheDir = path.join(os.tmpdir(), `cortex-fts-${userSuffix}`);
344
- const hash = computeCortexHash(cortexPath, profile);
362
+ const globResult = globAllFiles(cortexPath, profile);
363
+ const hash = computeCortexHash(cortexPath, profile, globResult.filePaths);
345
364
  const cacheFile = path.join(cacheDir, `${hash}.db`);
346
365
  const wasmBinary = findWasmBinary();
347
366
  const SQL = await initSqlJs(wasmBinary ? { wasmBinary } : {});
@@ -358,7 +377,7 @@ async function buildIndexImpl(cortexPath, profile) {
358
377
  try {
359
378
  db = new SQL.Database(cached);
360
379
  // Compute current file hashes and determine what changed
361
- const allFiles = collectAllFiles(cortexPath, profile);
380
+ const allFiles = globResult.entries;
362
381
  const currentHashes = {};
363
382
  const changedFiles = [];
364
383
  const newFiles = [];
@@ -487,7 +506,7 @@ async function buildIndexImpl(cortexPath, profile) {
487
506
  db.run(`CREATE TABLE IF NOT EXISTS entity_links (source_id INTEGER REFERENCES entities(id), target_id INTEGER REFERENCES entities(id), rel_type TEXT NOT NULL, source_doc TEXT, PRIMARY KEY (source_id, target_id, rel_type))`);
488
507
  // Q20: Cross-project entity index
489
508
  ensureGlobalEntitiesTable(db);
490
- const allFiles = collectAllFiles(cortexPath, profile);
509
+ const allFiles = globResult.entries;
491
510
  const newHashes = {};
492
511
  let fileCount = 0;
493
512
  // Try loading cached entity graph
@@ -209,6 +209,12 @@ export function collectNativeMemoryFiles() {
209
209
  }
210
210
  return results;
211
211
  }
212
+ /** Canonical finding type tags */
213
+ export const FINDING_TYPES = ["decision", "pitfall", "pattern"];
214
+ /** All searchable finding tags (canonical + legacy aliases) */
215
+ export const FINDING_TAGS = ["decision", "pitfall", "pattern", "tradeoff", "architecture", "bug"];
216
+ /** Document types in the FTS index */
217
+ export const DOC_TYPES = ["claude", "findings", "reference", "skills", "summary", "backlog", "changelog", "canonical", "memory-queue", "skill", "other"];
212
218
  export function appendAuditLog(cortexPath, event, details) {
213
219
  // Migrate: check old location, use new .runtime/ path
214
220
  const legacyPath = path.join(cortexPath, ".cortex-audit.log");
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@alaarab/cortex",
3
- "version": "1.13.6",
4
- "description": "Long-term memory for AI agents stored as markdown in a git repo you own.",
3
+ "version": "1.15.0",
4
+ "description": "Long-term memory for AI agents. Stored as markdown in a git repo you own.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cortex": "mcp/dist/index.js"
@@ -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": {
package/starter/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # My Cortex
2
2
 
3
- Your personal project store for [cortex](https://github.com/alaarab/cortex) 1.11.1, which gives Claude Code persistent context across sessions and machines.
3
+ Your personal project store for [cortex](https://github.com/alaarab/cortex), which gives Claude Code persistent context across sessions and machines.
4
4
 
5
5
  ## Structure
6
6
 
@@ -67,7 +67,7 @@ work-desktop: work
67
67
  home-laptop: personal
68
68
  ```
69
69
 
70
- Each profile in `profiles/` lists which projects that machine should see. After cloning on a new machine, run `/cortex:sync` to pull everything in.
70
+ Each profile in `profiles/` lists which projects that machine should see. After cloning on a new machine, run `/cortex-sync` to pull everything in.
71
71
 
72
72
  ## Troubleshooting
73
73