@alaarab/cortex 1.13.5 → 1.14.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.
@@ -11,7 +11,12 @@ import { runDoctor } from "./link.js";
11
11
  import { getHooksEnabledPreference } from "./init.js";
12
12
  import { buildIndex, queryRows, } from "./shared-index.js";
13
13
  import { filterBacklogByPriority } from "./cli-hooks-retrieval.js";
14
- const cortexPath = ensureCortexPath();
14
+ let _cortexPath;
15
+ function getCortexPath() {
16
+ if (!_cortexPath)
17
+ _cortexPath = ensureCortexPath();
18
+ return _cortexPath;
19
+ }
15
20
  const profile = process.env.CORTEX_PROFILE || "";
16
21
  export function getGitContext(cwd) {
17
22
  if (!cwd)
@@ -239,21 +244,21 @@ async function runBestEffortGit(args, cwd) {
239
244
  // ── Hook handlers ────────────────────────────────────────────────────────────
240
245
  export async function handleHookSessionStart() {
241
246
  const startedAt = new Date().toISOString();
242
- if (!getHooksEnabledPreference(cortexPath)) {
243
- updateRuntimeHealth(cortexPath, { lastSessionStartAt: startedAt });
244
- appendAuditLog(cortexPath, "hook_session_start", "status=disabled");
247
+ if (!getHooksEnabledPreference(getCortexPath())) {
248
+ updateRuntimeHealth(getCortexPath(), { lastSessionStartAt: startedAt });
249
+ appendAuditLog(getCortexPath(), "hook_session_start", "status=disabled");
245
250
  return;
246
251
  }
247
- const pull = await runBestEffortGit(["pull", "--rebase", "--quiet"], cortexPath);
248
- const doctor = await runDoctor(cortexPath, false);
249
- const maintenanceScheduled = scheduleBackgroundMaintenance(cortexPath);
252
+ const pull = await runBestEffortGit(["pull", "--rebase", "--quiet"], getCortexPath());
253
+ const doctor = await runDoctor(getCortexPath(), false);
254
+ const maintenanceScheduled = scheduleBackgroundMaintenance(getCortexPath());
250
255
  try {
251
256
  const { trackSession } = await import("./telemetry.js");
252
- trackSession(cortexPath);
257
+ trackSession(getCortexPath());
253
258
  }
254
259
  catch { /* best-effort */ }
255
- updateRuntimeHealth(cortexPath, { lastSessionStartAt: startedAt });
256
- appendAuditLog(cortexPath, "hook_session_start", `pull=${pull.ok ? "ok" : "fail"} doctor=${doctor.ok ? "ok" : "issues"} maintenance=${maintenanceScheduled ? "scheduled" : "skipped"}`);
260
+ updateRuntimeHealth(getCortexPath(), { lastSessionStartAt: startedAt });
261
+ appendAuditLog(getCortexPath(), "hook_session_start", `pull=${pull.ok ? "ok" : "fail"} doctor=${doctor.ok ? "ok" : "issues"} maintenance=${maintenanceScheduled ? "scheduled" : "skipped"}`);
257
262
  }
258
263
  // ── Q21: Conversation memory capture ─────────────────────────────────────────
259
264
  const INSIGHT_KEYWORDS = [
@@ -291,35 +296,35 @@ export function extractConversationInsights(text) {
291
296
  }
292
297
  export async function handleHookStop() {
293
298
  const now = new Date().toISOString();
294
- if (!getHooksEnabledPreference(cortexPath)) {
295
- updateRuntimeHealth(cortexPath, {
299
+ if (!getHooksEnabledPreference(getCortexPath())) {
300
+ updateRuntimeHealth(getCortexPath(), {
296
301
  lastStopAt: now,
297
302
  lastAutoSave: { at: now, status: "clean", detail: "hooks disabled by preference" },
298
303
  });
299
- appendAuditLog(cortexPath, "hook_stop", "status=disabled");
304
+ appendAuditLog(getCortexPath(), "hook_stop", "status=disabled");
300
305
  return;
301
306
  }
302
- const status = await runBestEffortGit(["status", "--porcelain"], cortexPath);
307
+ const status = await runBestEffortGit(["status", "--porcelain"], getCortexPath());
303
308
  if (!status.ok) {
304
- updateRuntimeHealth(cortexPath, {
309
+ updateRuntimeHealth(getCortexPath(), {
305
310
  lastStopAt: now,
306
311
  lastAutoSave: { at: now, status: "error", detail: status.error || "git status failed" },
307
312
  });
308
- appendAuditLog(cortexPath, "hook_stop", `status=error detail=${JSON.stringify(status.error || "git status failed")}`);
313
+ appendAuditLog(getCortexPath(), "hook_stop", `status=error detail=${JSON.stringify(status.error || "git status failed")}`);
309
314
  return;
310
315
  }
311
316
  if (!status.output) {
312
- updateRuntimeHealth(cortexPath, {
317
+ updateRuntimeHealth(getCortexPath(), {
313
318
  lastStopAt: now,
314
319
  lastAutoSave: { at: now, status: "clean", detail: "no changes" },
315
320
  });
316
- appendAuditLog(cortexPath, "hook_stop", "status=clean");
321
+ appendAuditLog(getCortexPath(), "hook_stop", "status=clean");
317
322
  return;
318
323
  }
319
- const add = await runBestEffortGit(["add", "-A"], cortexPath);
320
- const commit = add.ok ? await runBestEffortGit(["commit", "-m", "auto-save cortex"], cortexPath) : { ok: false, error: add.error };
324
+ const add = await runBestEffortGit(["add", "-A"], getCortexPath());
325
+ const commit = add.ok ? await runBestEffortGit(["commit", "-m", "auto-save cortex"], getCortexPath()) : { ok: false, error: add.error };
321
326
  if (!add.ok || !commit.ok) {
322
- updateRuntimeHealth(cortexPath, {
327
+ updateRuntimeHealth(getCortexPath(), {
323
328
  lastStopAt: now,
324
329
  lastAutoSave: {
325
330
  at: now,
@@ -327,36 +332,36 @@ export async function handleHookStop() {
327
332
  detail: add.error || commit.error || "git add/commit failed",
328
333
  },
329
334
  });
330
- appendAuditLog(cortexPath, "hook_stop", `status=error detail=${JSON.stringify(add.error || commit.error || "git add/commit failed")}`);
335
+ appendAuditLog(getCortexPath(), "hook_stop", `status=error detail=${JSON.stringify(add.error || commit.error || "git add/commit failed")}`);
331
336
  return;
332
337
  }
333
- const remotes = await runBestEffortGit(["remote"], cortexPath);
338
+ const remotes = await runBestEffortGit(["remote"], getCortexPath());
334
339
  if (!remotes.ok || !remotes.output) {
335
- updateRuntimeHealth(cortexPath, {
340
+ updateRuntimeHealth(getCortexPath(), {
336
341
  lastStopAt: now,
337
342
  lastAutoSave: { at: now, status: "saved-local", detail: "commit created; no remote configured" },
338
343
  });
339
- appendAuditLog(cortexPath, "hook_stop", "status=saved-local");
344
+ appendAuditLog(getCortexPath(), "hook_stop", "status=saved-local");
340
345
  return;
341
346
  }
342
- const push = await runBestEffortGit(["push"], cortexPath);
347
+ const push = await runBestEffortGit(["push"], getCortexPath());
343
348
  if (push.ok) {
344
- updateRuntimeHealth(cortexPath, {
349
+ updateRuntimeHealth(getCortexPath(), {
345
350
  lastStopAt: now,
346
351
  lastAutoSave: { at: now, status: "saved-pushed", detail: "commit pushed" },
347
352
  });
348
- appendAuditLog(cortexPath, "hook_stop", "status=saved-pushed");
353
+ appendAuditLog(getCortexPath(), "hook_stop", "status=saved-pushed");
349
354
  // Q21: Auto-capture conversation insights (gated behind CORTEX_FEATURE_AUTO_CAPTURE=1)
350
355
  if (isFeatureEnabled("CORTEX_FEATURE_AUTO_CAPTURE", false)) {
351
356
  try {
352
357
  const captureInput = process.env.CORTEX_CONVERSATION_CONTEXT || "";
353
358
  if (captureInput) {
354
359
  const cwd = process.cwd();
355
- const activeProject = detectProject(cortexPath, cwd, profile);
360
+ const activeProject = detectProject(getCortexPath(), cwd, profile);
356
361
  if (activeProject) {
357
362
  const insights = extractConversationInsights(captureInput);
358
363
  for (const insight of insights) {
359
- addFindingToFile(cortexPath, activeProject, `[pattern] ${insight}`);
364
+ addFindingToFile(getCortexPath(), activeProject, `[pattern] ${insight}`);
360
365
  debugLog(`auto-capture: saved insight for ${activeProject}: ${insight.slice(0, 60)}`);
361
366
  }
362
367
  }
@@ -368,7 +373,7 @@ export async function handleHookStop() {
368
373
  }
369
374
  // Auto governance scheduling: run governance weekly if overdue
370
375
  try {
371
- const lastGovPath = runtimeFile(cortexPath, "last-governance.txt");
376
+ const lastGovPath = runtimeFile(getCortexPath(), "last-governance.txt");
372
377
  const lastRun = fs.existsSync(lastGovPath) ? parseInt(fs.readFileSync(lastGovPath, "utf8"), 10) : 0;
373
378
  const daysSince = (Date.now() - lastRun) / 86_400_000;
374
379
  if (daysSince >= 7) {
@@ -386,14 +391,14 @@ export async function handleHookStop() {
386
391
  }
387
392
  return;
388
393
  }
389
- updateRuntimeHealth(cortexPath, {
394
+ updateRuntimeHealth(getCortexPath(), {
390
395
  lastStopAt: now,
391
396
  lastAutoSave: { at: now, status: "saved-local", detail: push.error || "push failed" },
392
397
  });
393
- appendAuditLog(cortexPath, "hook_stop", `status=saved-local detail=${JSON.stringify(push.error || "push failed")}`);
398
+ appendAuditLog(getCortexPath(), "hook_stop", `status=saved-local detail=${JSON.stringify(push.error || "push failed")}`);
394
399
  }
395
400
  export async function handleHookContext() {
396
- if (!getHooksEnabledPreference(cortexPath)) {
401
+ if (!getHooksEnabledPreference(getCortexPath())) {
397
402
  process.exit(0);
398
403
  }
399
404
  let cwd = process.cwd();
@@ -406,8 +411,8 @@ export async function handleHookContext() {
406
411
  catch (err) {
407
412
  debugLog(`hook-context: no stdin or invalid JSON, using cwd: ${err instanceof Error ? err.message : String(err)}`);
408
413
  }
409
- const project = detectProject(cortexPath, cwd, profile);
410
- const db = await buildIndex(cortexPath, profile);
414
+ const project = detectProject(getCortexPath(), cwd, profile);
415
+ const db = await buildIndex(getCortexPath(), profile);
411
416
  const contextLabel = project ? `\u25c6 cortex \u00b7 ${project} \u00b7 context` : `\u25c6 cortex \u00b7 context`;
412
417
  const parts = [contextLabel, "<cortex-context>"];
413
418
  if (project) {
@@ -456,7 +461,7 @@ export async function handleHookContext() {
456
461
  // ── PostToolUse hook ─────────────────────────────────────────────────────────
457
462
  const INTERESTING_TOOLS = new Set(["Read", "Write", "Edit", "Bash", "Glob", "Grep"]);
458
463
  export async function handleHookTool() {
459
- if (!getHooksEnabledPreference(cortexPath)) {
464
+ if (!getHooksEnabledPreference(getCortexPath())) {
460
465
  process.exit(0);
461
466
  }
462
467
  const start = Date.now();
@@ -513,7 +518,7 @@ export async function handleHookTool() {
513
518
  entry.error = responseStr.slice(0, 300);
514
519
  }
515
520
  try {
516
- const logFile = runtimeFile(cortexPath, "tool-log.jsonl");
521
+ const logFile = runtimeFile(getCortexPath(), "tool-log.jsonl");
517
522
  fs.mkdirSync(path.dirname(logFile), { recursive: true });
518
523
  fs.appendFileSync(logFile, JSON.stringify(entry) + "\n");
519
524
  }
@@ -521,17 +526,17 @@ export async function handleHookTool() {
521
526
  // best effort
522
527
  }
523
528
  const cwd = (data.cwd ?? input.cwd ?? undefined);
524
- const activeProject = cwd ? detectProject(cortexPath, cwd, profile) : null;
529
+ const activeProject = cwd ? detectProject(getCortexPath(), cwd, profile) : null;
525
530
  if (activeProject) {
526
531
  try {
527
532
  const candidates = extractToolFindings(toolName, input, responseStr);
528
533
  for (const { text, confidence } of candidates) {
529
534
  if (confidence >= 0.85) {
530
- addFindingToFile(cortexPath, activeProject, text);
535
+ addFindingToFile(getCortexPath(), activeProject, text);
531
536
  debugLog(`hook-tool: auto-added learning (conf=${confidence}): ${text.slice(0, 60)}`);
532
537
  }
533
538
  else {
534
- appendReviewQueue(cortexPath, activeProject, "Review", [text]);
539
+ appendReviewQueue(getCortexPath(), activeProject, "Review", [text]);
535
540
  debugLog(`hook-tool: queued candidate (conf=${confidence}): ${text.slice(0, 60)}`);
536
541
  }
537
542
  }
@@ -30,7 +30,12 @@ import { searchDocuments, applyTrustFilter, rankResults, selectSnippets, detectT
30
30
  import { buildHookOutput } from "./cli-hooks-output.js";
31
31
  import { getGitContext, trackSessionMetrics, scheduleBackgroundMaintenance, } from "./cli-hooks-session.js";
32
32
  import { approximateTokens } from "./cli-hooks-retrieval.js";
33
- const cortexPath = ensureCortexPath();
33
+ let _cortexPath;
34
+ function getCortexPath() {
35
+ if (!_cortexPath)
36
+ _cortexPath = ensureCortexPath();
37
+ return _cortexPath;
38
+ }
34
39
  const profile = process.env.CORTEX_PROFILE || "";
35
40
  async function readStdin() {
36
41
  return new Promise((resolve, reject) => {
@@ -67,21 +72,21 @@ export async function handleHookPrompt() {
67
72
  if (!input)
68
73
  process.exit(0);
69
74
  const { prompt, cwd, sessionId } = input;
70
- if (!getHooksEnabledPreference(cortexPath)) {
71
- appendAuditLog(cortexPath, "hook_prompt", "status=disabled");
75
+ if (!getHooksEnabledPreference(getCortexPath())) {
76
+ appendAuditLog(getCortexPath(), "hook_prompt", "status=disabled");
72
77
  process.exit(0);
73
78
  }
74
- updateRuntimeHealth(cortexPath, { lastPromptAt: new Date().toISOString() });
79
+ updateRuntimeHealth(getCortexPath(), { lastPromptAt: new Date().toISOString() });
75
80
  const keywords = extractKeywords(prompt);
76
81
  if (!keywords)
77
82
  process.exit(0);
78
83
  debugLog(`hook-prompt keywords: "${keywords}"`);
79
84
  const tIndex0 = Date.now();
80
- const db = await buildIndex(cortexPath, profile);
85
+ const db = await buildIndex(getCortexPath(), profile);
81
86
  stage.indexMs = Date.now() - tIndex0;
82
87
  const gitCtx = getGitContext(cwd);
83
88
  const intent = detectTaskIntent(prompt);
84
- const detectedProject = cwd ? detectProject(cortexPath, cwd, profile) : null;
89
+ const detectedProject = cwd ? detectProject(getCortexPath(), cwd, profile) : null;
85
90
  if (detectedProject)
86
91
  debugLog(`Detected project: ${detectedProject}`);
87
92
  const safeQuery = buildRobustFtsQuery(keywords);
@@ -94,14 +99,14 @@ export async function handleHookPrompt() {
94
99
  if (!rows || !rows.length)
95
100
  process.exit(0);
96
101
  const tTrust0 = Date.now();
97
- const policy = getRetentionPolicy(cortexPath);
102
+ const policy = getRetentionPolicy(getCortexPath());
98
103
  const memoryTtlDays = Number.parseInt(process.env.CORTEX_MEMORY_TTL_DAYS || String(policy.ttlDays), 10);
99
- rows = applyTrustFilter(rows, cortexPath, Number.isNaN(memoryTtlDays) ? policy.ttlDays : memoryTtlDays, policy.minInjectConfidence, policy.decay);
104
+ rows = applyTrustFilter(rows, getCortexPath(), Number.isNaN(memoryTtlDays) ? policy.ttlDays : memoryTtlDays, policy.minInjectConfidence, policy.decay);
100
105
  stage.trustMs = Date.now() - tTrust0;
101
106
  if (!rows.length)
102
107
  process.exit(0);
103
108
  if (isFeatureEnabled("CORTEX_FEATURE_AUTO_EXTRACT", true) && sessionId && detectedProject && cwd) {
104
- const marker = sessionMarker(cortexPath, `extracted-${sessionId}-${detectedProject}`);
109
+ const marker = sessionMarker(getCortexPath(), `extracted-${sessionId}-${detectedProject}`);
105
110
  if (!fs.existsSync(marker)) {
106
111
  try {
107
112
  await handleExtractMemories(detectedProject, cwd, true);
@@ -113,7 +118,7 @@ export async function handleHookPrompt() {
113
118
  }
114
119
  }
115
120
  const tRank0 = Date.now();
116
- rows = rankResults(rows, intent, gitCtx, detectedProject, cortexPath, db, cwd, keywords);
121
+ rows = rankResults(rows, intent, gitCtx, detectedProject, getCortexPath(), db, cwd, keywords);
117
122
  stage.rankMs = Date.now() - tRank0;
118
123
  if (!rows.length)
119
124
  process.exit(0);
@@ -155,7 +160,7 @@ export async function handleHookPrompt() {
155
160
  budgetUsedTokens = runningTokens;
156
161
  debugLog(`injection-budget: trimmed ${selected.length} -> ${kept.length} snippets to fit ${maxInjectTokens} token budget`);
157
162
  }
158
- const parts = buildHookOutput(budgetSelected, budgetUsedTokens, intent, gitCtx, detectedProject, stage, safeTokenBudget, cortexPath, sessionId);
163
+ const parts = buildHookOutput(budgetSelected, budgetUsedTokens, intent, gitCtx, detectedProject, stage, safeTokenBudget, getCortexPath(), sessionId);
159
164
  // Add budget info to trace
160
165
  if (parts.length > 0) {
161
166
  const traceIdx = parts.findIndex(p => p.includes("trace:"));
@@ -165,17 +170,17 @@ export async function handleHookPrompt() {
165
170
  }
166
171
  const changedCount = gitCtx?.changedFiles.size ?? 0;
167
172
  if (sessionId) {
168
- trackSessionMetrics(cortexPath, sessionId, selected, changedCount);
173
+ trackSessionMetrics(getCortexPath(), sessionId, selected, changedCount);
169
174
  }
170
- flushEntryScores(cortexPath);
171
- scheduleBackgroundMaintenance(cortexPath);
172
- const noticeFile = sessionId ? sessionMarker(cortexPath, `noticed-${sessionId}`) : null;
175
+ flushEntryScores(getCortexPath());
176
+ scheduleBackgroundMaintenance(getCortexPath());
177
+ const noticeFile = sessionId ? sessionMarker(getCortexPath(), `noticed-${sessionId}`) : null;
173
178
  const alreadyNoticed = noticeFile ? fs.existsSync(noticeFile) : false;
174
179
  if (!alreadyNoticed) {
175
180
  // Clean up stale session markers (>24h old) from .sessions/ dir
176
181
  try {
177
182
  const cutoff = Date.now() - 86400000;
178
- const sessDir = sessionsDir(cortexPath);
183
+ const sessDir = sessionsDir(getCortexPath());
179
184
  if (fs.existsSync(sessDir)) {
180
185
  for (const f of fs.readdirSync(sessDir)) {
181
186
  if (!f.startsWith("noticed-") && !f.startsWith("extracted-"))
@@ -186,10 +191,10 @@ export async function handleHookPrompt() {
186
191
  }
187
192
  }
188
193
  // Also clean legacy markers from root
189
- for (const f of fs.readdirSync(cortexPath)) {
194
+ for (const f of fs.readdirSync(getCortexPath())) {
190
195
  if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
191
196
  continue;
192
- const fp = `${cortexPath}/${f}`;
197
+ const fp = `${getCortexPath()}/${f}`;
193
198
  try {
194
199
  fs.unlinkSync(fp);
195
200
  }
@@ -199,7 +204,7 @@ export async function handleHookPrompt() {
199
204
  catch (err) {
200
205
  debugLog(`stale notice cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
201
206
  }
202
- const needed = checkConsolidationNeeded(cortexPath, profile);
207
+ const needed = checkConsolidationNeeded(getCortexPath(), profile);
203
208
  if (needed.length > 0) {
204
209
  const notices = needed.map((n) => {
205
210
  const since = n.lastConsolidated ? ` since ${n.lastConsolidated}` : "";
package/mcp/dist/cli.js CHANGED
@@ -21,7 +21,12 @@ 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
+ let _cortexPath;
25
+ function getCortexPath() {
26
+ if (!_cortexPath)
27
+ _cortexPath = ensureCortexPath();
28
+ return _cortexPath;
29
+ }
25
30
  const profile = process.env.CORTEX_PROFILE || "";
26
31
  // ── Search types and parsing ─────────────────────────────────────────────────
27
32
  const SEARCH_TYPE_ALIASES = {
@@ -42,7 +47,7 @@ const SEARCH_TYPES = new Set([
42
47
  // ── Search history ───────────────────────────────────────────────────────────
43
48
  const MAX_HISTORY = 20;
44
49
  function historyFile() {
45
- return runtimeFile(cortexPath, "search-history.jsonl");
50
+ return runtimeFile(getCortexPath(), "search-history.jsonl");
46
51
  }
47
52
  function readSearchHistory() {
48
53
  const file = historyFile();
@@ -294,7 +299,7 @@ async function handleSearch(opts) {
294
299
  return;
295
300
  }
296
301
  recordSearchQuery(opts);
297
- const db = await buildIndex(cortexPath, profile);
302
+ const db = await buildIndex(getCortexPath(), profile);
298
303
  try {
299
304
  let sql = "SELECT project, filename, type, content, path FROM docs";
300
305
  const where = [];
@@ -334,7 +339,7 @@ async function handleSearch(opts) {
334
339
  if (opts.query) {
335
340
  try {
336
341
  const { logSearchMiss } = await import("./mcp-search.js");
337
- logSearchMiss(cortexPath, opts.query, opts.project);
342
+ logSearchMiss(getCortexPath(), opts.query, opts.project);
338
343
  }
339
344
  catch { /* best-effort */ }
340
345
  }
@@ -371,7 +376,7 @@ async function handleAddFinding(project, learning) {
371
376
  process.exit(1);
372
377
  }
373
378
  try {
374
- const result = addFindingCore(cortexPath, project, learning);
379
+ const result = addFindingCore(getCortexPath(), project, learning);
375
380
  if (!result.ok) {
376
381
  console.error(result.message);
377
382
  process.exit(1);
@@ -388,14 +393,14 @@ async function handlePinCanonical(project, memory) {
388
393
  console.error('Usage: cortex pin <project> "<memory>"');
389
394
  process.exit(1);
390
395
  }
391
- const result = upsertCanonical(cortexPath, project, memory);
396
+ const result = upsertCanonical(getCortexPath(), project, memory);
392
397
  console.log(result.ok ? result.data : result.error);
393
398
  }
394
399
  async function handleDoctor(args) {
395
400
  const fix = args.includes("--fix");
396
401
  const checkData = args.includes("--check-data");
397
402
  const agentsOnly = args.includes("--agents");
398
- const result = await runDoctor(cortexPath, fix, checkData);
403
+ const result = await runDoctor(getCortexPath(), fix, checkData);
399
404
  if (agentsOnly) {
400
405
  // Filter to only agent-related checks
401
406
  const agentChecks = result.checks.filter((c) => c.name.includes("cursor") || c.name.includes("copilot") || c.name.includes("codex") || c.name.includes("windsurf"));
@@ -418,7 +423,7 @@ async function handleDoctor(args) {
418
423
  }
419
424
  // Q30: Show top search miss patterns
420
425
  try {
421
- const missFile = runtimeFile(cortexPath, "search-misses.jsonl");
426
+ const missFile = runtimeFile(getCortexPath(), "search-misses.jsonl");
422
427
  if (fs.existsSync(missFile)) {
423
428
  const lines = fs.readFileSync(missFile, "utf8").split("\n").filter(Boolean);
424
429
  if (lines.length > 0) {
@@ -455,15 +460,15 @@ async function handleQualityFeedback(args) {
455
460
  console.error("Usage: cortex quality-feedback --key=<entry-key> --type=helpful|reprompt|regression");
456
461
  process.exit(1);
457
462
  }
458
- recordFeedback(cortexPath, key, feedback);
459
- flushEntryScores(cortexPath);
463
+ recordFeedback(getCortexPath(), key, feedback);
464
+ flushEntryScores(getCortexPath());
460
465
  console.log(`Recorded feedback: ${feedback} for ${key}`);
461
466
  }
462
467
  async function handleMemoryUi(args) {
463
468
  const portArg = args.find((a) => a.startsWith("--port="));
464
469
  const port = portArg ? Number.parseInt(portArg.slice("--port=".length), 10) : 3499;
465
470
  const safePort = Number.isNaN(port) ? 3499 : port;
466
- await startReviewUi(cortexPath, safePort);
471
+ await startReviewUi(getCortexPath(), safePort);
467
472
  }
468
473
  async function handleShell(args) {
469
474
  if (args.includes("--help") || args.includes("-h")) {
@@ -471,7 +476,7 @@ async function handleShell(args) {
471
476
  console.log("Interactive shell with views for Projects, Backlog, Learnings, Memory Queue, Machines/Profiles, and Health.");
472
477
  return;
473
478
  }
474
- await startShell(cortexPath, profile);
479
+ await startShell(getCortexPath(), profile);
475
480
  }
476
481
  async function handleUpdate(args) {
477
482
  if (args.includes("--help") || args.includes("-h")) {
@@ -511,9 +516,9 @@ function handleSkillList() {
511
516
  }
512
517
  }
513
518
  }
514
- const globalSkillsDir = path.join(cortexPath, "global", "skills");
519
+ const globalSkillsDir = path.join(getCortexPath(), "global", "skills");
515
520
  collectSkills(globalSkillsDir, "global");
516
- const projectDirs = getProjectDirs(cortexPath, profile);
521
+ const projectDirs = getProjectDirs(getCortexPath(), profile);
517
522
  for (const dir of projectDirs) {
518
523
  const projectName = path.basename(dir);
519
524
  if (projectName === "global")
@@ -543,7 +548,7 @@ function handleDetectSkills(args) {
543
548
  return;
544
549
  }
545
550
  const trackedSkills = new Set();
546
- const globalSkillsDir = path.join(cortexPath, "global", "skills");
551
+ const globalSkillsDir = path.join(getCortexPath(), "global", "skills");
547
552
  if (fs.existsSync(globalSkillsDir)) {
548
553
  for (const entry of fs.readdirSync(globalSkillsDir)) {
549
554
  trackedSkills.add(entry.replace(/\.md$/, ""));
@@ -552,7 +557,7 @@ function handleDetectSkills(args) {
552
557
  }
553
558
  }
554
559
  }
555
- const projectDirs = getProjectDirs(cortexPath, profile);
560
+ const projectDirs = getProjectDirs(getCortexPath(), profile);
556
561
  for (const dir of projectDirs) {
557
562
  const projectSkillsDir = path.join(dir, ".claude", "skills");
558
563
  if (!fs.existsSync(projectSkillsDir))
@@ -620,7 +625,7 @@ function handleDetectSkills(args) {
620
625
  console.log(`\nImported ${imported} skill(s). Run \`cortex link\` to activate.`);
621
626
  }
622
627
  function handleBacklogView() {
623
- const docs = readBacklogs(cortexPath, profile);
628
+ const docs = readBacklogs(getCortexPath(), profile);
624
629
  if (!docs.length) {
625
630
  console.log("No backlogs found.");
626
631
  return;
@@ -670,8 +675,8 @@ async function handleQuickstart() {
670
675
  });
671
676
  console.log(`\nInitializing cortex for "${projectName}"...\n`);
672
677
  await runInit({ yes: true });
673
- await runLink(cortexPath, {});
674
- const projectDir = path.join(cortexPath, projectName);
678
+ await runLink(getCortexPath(), {});
679
+ const projectDir = path.join(getCortexPath(), projectName);
675
680
  if (!fs.existsSync(projectDir)) {
676
681
  fs.mkdirSync(projectDir, { recursive: true });
677
682
  fs.writeFileSync(path.join(projectDir, "FINDINGS.md"), `# ${projectName} Findings\n`);
@@ -722,7 +727,7 @@ async function handleDebugInjection(args) {
722
727
  input: payload,
723
728
  env: {
724
729
  ...process.env,
725
- CORTEX_PATH: cortexPath,
730
+ CORTEX_PATH: getCortexPath(),
726
731
  CORTEX_PROFILE: profile,
727
732
  },
728
733
  timeout: EXEC_TIMEOUT_MS,
@@ -766,7 +771,7 @@ async function handleInspectIndex(args) {
766
771
  return;
767
772
  }
768
773
  }
769
- const db = await buildIndex(cortexPath, profile);
774
+ const db = await buildIndex(getCortexPath(), profile);
770
775
  const where = [];
771
776
  const params = [];
772
777
  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
 
@@ -218,21 +233,6 @@ if (CLI_COMMANDS.includes(process.argv[2])) {
218
233
  await runCliCommand(cmd, process.argv.slice(3));
219
234
  process.exit(0);
220
235
  }
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
236
  // MCP mode: first non-flag arg is the cortex path
237
237
  const cortexArg = process.argv.find((a, i) => i >= 2 && !a.startsWith("-"));
238
238
  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);
@@ -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
  }),