@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.
@@ -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,12 +30,17 @@ 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) => {
37
42
  const chunks = [];
38
- process.stdin.on("data", (chunk) => chunks.push(chunk));
43
+ process.stdin.on("data", (chunk) => chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
39
44
  process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
40
45
  process.stdin.on("error", reject);
41
46
  });
@@ -67,24 +72,24 @@ 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
- const safeQuery = buildRobustFtsQuery(keywords);
92
+ const safeQuery = buildRobustFtsQuery(keywords, detectedProject);
88
93
  if (!safeQuery)
89
94
  process.exit(0);
90
95
  try {
@@ -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}` : "";