@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.
- package/README.md +16 -15
- package/mcp/dist/cli-config.js +20 -15
- package/mcp/dist/cli-extract.js +15 -10
- package/mcp/dist/cli-govern.js +37 -32
- package/mcp/dist/cli-hooks-session.js +46 -41
- package/mcp/dist/cli-hooks.js +24 -19
- package/mcp/dist/cli.js +26 -21
- package/mcp/dist/data-access.js +7 -43
- package/mcp/dist/index.js +15 -15
- package/mcp/dist/link-skills.js +0 -6
- package/mcp/dist/mcp-finding.js +2 -2
- package/mcp/dist/mcp-search.js +4 -4
- package/mcp/dist/shared-index.js +45 -26
- package/mcp/dist/shared-search-fallback.js +22 -2
- package/mcp/dist/shared.js +6 -0
- package/package.json +2 -2
- package/starter/README.md +2 -2
|
@@ -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
|
-
|
|
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(
|
|
243
|
-
updateRuntimeHealth(
|
|
244
|
-
appendAuditLog(
|
|
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"],
|
|
248
|
-
const doctor = await runDoctor(
|
|
249
|
-
const maintenanceScheduled = scheduleBackgroundMaintenance(
|
|
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(
|
|
257
|
+
trackSession(getCortexPath());
|
|
253
258
|
}
|
|
254
259
|
catch { /* best-effort */ }
|
|
255
|
-
updateRuntimeHealth(
|
|
256
|
-
appendAuditLog(
|
|
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(
|
|
295
|
-
updateRuntimeHealth(
|
|
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(
|
|
304
|
+
appendAuditLog(getCortexPath(), "hook_stop", "status=disabled");
|
|
300
305
|
return;
|
|
301
306
|
}
|
|
302
|
-
const status = await runBestEffortGit(["status", "--porcelain"],
|
|
307
|
+
const status = await runBestEffortGit(["status", "--porcelain"], getCortexPath());
|
|
303
308
|
if (!status.ok) {
|
|
304
|
-
updateRuntimeHealth(
|
|
309
|
+
updateRuntimeHealth(getCortexPath(), {
|
|
305
310
|
lastStopAt: now,
|
|
306
311
|
lastAutoSave: { at: now, status: "error", detail: status.error || "git status failed" },
|
|
307
312
|
});
|
|
308
|
-
appendAuditLog(
|
|
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(
|
|
317
|
+
updateRuntimeHealth(getCortexPath(), {
|
|
313
318
|
lastStopAt: now,
|
|
314
319
|
lastAutoSave: { at: now, status: "clean", detail: "no changes" },
|
|
315
320
|
});
|
|
316
|
-
appendAuditLog(
|
|
321
|
+
appendAuditLog(getCortexPath(), "hook_stop", "status=clean");
|
|
317
322
|
return;
|
|
318
323
|
}
|
|
319
|
-
const add = await runBestEffortGit(["add", "-A"],
|
|
320
|
-
const commit = add.ok ? await runBestEffortGit(["commit", "-m", "auto-save cortex"],
|
|
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(
|
|
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(
|
|
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"],
|
|
338
|
+
const remotes = await runBestEffortGit(["remote"], getCortexPath());
|
|
334
339
|
if (!remotes.ok || !remotes.output) {
|
|
335
|
-
updateRuntimeHealth(
|
|
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(
|
|
344
|
+
appendAuditLog(getCortexPath(), "hook_stop", "status=saved-local");
|
|
340
345
|
return;
|
|
341
346
|
}
|
|
342
|
-
const push = await runBestEffortGit(["push"],
|
|
347
|
+
const push = await runBestEffortGit(["push"], getCortexPath());
|
|
343
348
|
if (push.ok) {
|
|
344
|
-
updateRuntimeHealth(
|
|
349
|
+
updateRuntimeHealth(getCortexPath(), {
|
|
345
350
|
lastStopAt: now,
|
|
346
351
|
lastAutoSave: { at: now, status: "saved-pushed", detail: "commit pushed" },
|
|
347
352
|
});
|
|
348
|
-
appendAuditLog(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
394
|
+
updateRuntimeHealth(getCortexPath(), {
|
|
390
395
|
lastStopAt: now,
|
|
391
396
|
lastAutoSave: { at: now, status: "saved-local", detail: push.error || "push failed" },
|
|
392
397
|
});
|
|
393
|
-
appendAuditLog(
|
|
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(
|
|
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(
|
|
410
|
-
const db = await buildIndex(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
539
|
+
appendReviewQueue(getCortexPath(), activeProject, "Review", [text]);
|
|
535
540
|
debugLog(`hook-tool: queued candidate (conf=${confidence}): ${text.slice(0, 60)}`);
|
|
536
541
|
}
|
|
537
542
|
}
|
package/mcp/dist/cli-hooks.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
71
|
-
appendAuditLog(
|
|
75
|
+
if (!getHooksEnabledPreference(getCortexPath())) {
|
|
76
|
+
appendAuditLog(getCortexPath(), "hook_prompt", "status=disabled");
|
|
72
77
|
process.exit(0);
|
|
73
78
|
}
|
|
74
|
-
updateRuntimeHealth(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
173
|
+
trackSessionMetrics(getCortexPath(), sessionId, selected, changedCount);
|
|
169
174
|
}
|
|
170
|
-
flushEntryScores(
|
|
171
|
-
scheduleBackgroundMaintenance(
|
|
172
|
-
const noticeFile = sessionId ? sessionMarker(
|
|
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(
|
|
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(
|
|
194
|
+
for (const f of fs.readdirSync(getCortexPath())) {
|
|
190
195
|
if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
|
|
191
196
|
continue;
|
|
192
|
-
const fp = `${
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
459
|
-
flushEntryScores(
|
|
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(
|
|
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(
|
|
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(
|
|
519
|
+
const globalSkillsDir = path.join(getCortexPath(), "global", "skills");
|
|
515
520
|
collectSkills(globalSkillsDir, "global");
|
|
516
|
-
const projectDirs = getProjectDirs(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
674
|
-
const projectDir = path.join(
|
|
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:
|
|
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(
|
|
774
|
+
const db = await buildIndex(getCortexPath(), profile);
|
|
770
775
|
const where = [];
|
|
771
776
|
const params = [];
|
|
772
777
|
if (project) {
|
package/mcp/dist/data-access.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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);
|
package/mcp/dist/link-skills.js
CHANGED
|
@@ -215,12 +215,6 @@ Cursor, Codex, and more.
|
|
|
215
215
|
- \`import_project\`: import project from previously exported JSON
|
|
216
216
|
- \`manage_project(project, action: "archive"|"unarchive")\`: archive or restore a project
|
|
217
217
|
|
|
218
|
-
**Graph and session:**
|
|
219
|
-
- \`get_learnings\`: alias for browsing findings/learnings
|
|
220
|
-
- \`add_learning\`: alias for add_finding (backward compat)
|
|
221
|
-
- \`add_learnings\`: alias for add_findings (backward compat)
|
|
222
|
-
- \`remove_learning\`: alias for remove_finding (backward compat)
|
|
223
|
-
- \`remove_learnings\`: alias for remove_findings (backward compat)
|
|
224
218
|
`;
|
|
225
219
|
const dest = path.join(cortexPath, "cortex.SKILL.md");
|
|
226
220
|
fs.writeFileSync(dest, content);
|
package/mcp/dist/mcp-finding.js
CHANGED
|
@@ -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(
|
|
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
|
}),
|