@alaarab/cortex 1.15.3 → 1.15.5
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/mcp/dist/cli-hooks-output.js +12 -1
- package/mcp/dist/cli-hooks-session.js +52 -2
- package/mcp/dist/data-access.js +22 -17
- package/mcp/dist/mcp-backlog.js +61 -11
- package/mcp/dist/shared-governance.js +31 -1
- package/mcp/dist/shell-input.js +5 -6
- package/mcp/dist/shell-view.js +22 -14
- package/package.json +1 -1
|
@@ -47,7 +47,18 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
|
-
|
|
50
|
+
// Position-aware injection: place most relevant at START and END (LaRA benchmark).
|
|
51
|
+
// Input `selected` is already ranked by relevance (best first).
|
|
52
|
+
// Reorder so: [0] stays first, [1] goes last, middle positions get [2..N-1].
|
|
53
|
+
let ordered = selected;
|
|
54
|
+
if (selected.length >= 3) {
|
|
55
|
+
ordered = [
|
|
56
|
+
selected[0], // most relevant → start
|
|
57
|
+
...selected.slice(2), // remaining → middle
|
|
58
|
+
selected[1], // second most → end
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
for (const injected of ordered) {
|
|
51
62
|
const { doc, snippet, key } = injected;
|
|
52
63
|
recordInjection(cortexPathLocal, key, sessionId);
|
|
53
64
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { debugLog, appendAuditLog, runtimeFile, EXEC_TIMEOUT_MS, ensureCortexPath, } from "./shared.js";
|
|
1
|
+
import { debugLog, appendAuditLog, runtimeFile, sessionMarker, EXEC_TIMEOUT_MS, ensureCortexPath, } from "./shared.js";
|
|
2
2
|
import { appendReviewQueue, recordFeedback, getQualityMultiplier, updateRuntimeHealth, withFileLock, } from "./shared-governance.js";
|
|
3
3
|
import { detectProject, } from "./shared-index.js";
|
|
4
4
|
import { addFindingToFile, } from "./shared-content.js";
|
|
@@ -526,7 +526,37 @@ export async function handleHookTool() {
|
|
|
526
526
|
// best effort
|
|
527
527
|
}
|
|
528
528
|
const cwd = (data.cwd ?? input.cwd ?? undefined);
|
|
529
|
-
|
|
529
|
+
let activeProject = cwd ? detectProject(getCortexPath(), cwd, profile) : null;
|
|
530
|
+
// Check cooldown
|
|
531
|
+
const cooldownMs = Number.parseInt(process.env.CORTEX_HOOK_TOOL_COOLDOWN_MS || "30000", 10) || 30000;
|
|
532
|
+
const cooldownFile = runtimeFile(getCortexPath(), "hook-tool-cooldown");
|
|
533
|
+
try {
|
|
534
|
+
if (fs.existsSync(cooldownFile)) {
|
|
535
|
+
const age = Date.now() - fs.statSync(cooldownFile).mtimeMs;
|
|
536
|
+
if (age < cooldownMs) {
|
|
537
|
+
debugLog(`hook-tool: cooldown active (${Math.round(age / 1000)}s < ${Math.round(cooldownMs / 1000)}s), skipping extraction`);
|
|
538
|
+
// Skip to the log-only path
|
|
539
|
+
activeProject = null; // prevent extraction below
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch { /* best effort */ }
|
|
544
|
+
// Check session cap
|
|
545
|
+
const sessionCap = Number.parseInt(process.env.CORTEX_HOOK_TOOL_SESSION_CAP || "10", 10) || 10;
|
|
546
|
+
if (activeProject && sessionId) {
|
|
547
|
+
try {
|
|
548
|
+
const capFile = sessionMarker(getCortexPath(), `tool-findings-${sessionId}`);
|
|
549
|
+
let count = 0;
|
|
550
|
+
if (fs.existsSync(capFile)) {
|
|
551
|
+
count = Number.parseInt(fs.readFileSync(capFile, "utf8").trim(), 10) || 0;
|
|
552
|
+
}
|
|
553
|
+
if (count >= sessionCap) {
|
|
554
|
+
debugLog(`hook-tool: session cap reached (${count}/${sessionCap}), skipping extraction`);
|
|
555
|
+
activeProject = null; // prevent extraction below
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch { /* best effort */ }
|
|
559
|
+
}
|
|
530
560
|
if (activeProject) {
|
|
531
561
|
try {
|
|
532
562
|
const candidates = extractToolFindings(toolName, input, responseStr);
|
|
@@ -540,6 +570,26 @@ export async function handleHookTool() {
|
|
|
540
570
|
debugLog(`hook-tool: queued candidate (conf=${confidence}): ${text.slice(0, 60)}`);
|
|
541
571
|
}
|
|
542
572
|
}
|
|
573
|
+
// Update cooldown and session counter
|
|
574
|
+
if (candidates.length > 0) {
|
|
575
|
+
try {
|
|
576
|
+
fs.writeFileSync(cooldownFile, Date.now().toString());
|
|
577
|
+
}
|
|
578
|
+
catch { /* best effort */ }
|
|
579
|
+
if (sessionId) {
|
|
580
|
+
try {
|
|
581
|
+
const capFile = sessionMarker(getCortexPath(), `tool-findings-${sessionId}`);
|
|
582
|
+
let count = 0;
|
|
583
|
+
try {
|
|
584
|
+
count = Number.parseInt(fs.readFileSync(capFile, "utf8").trim(), 10) || 0;
|
|
585
|
+
}
|
|
586
|
+
catch { /* new file */ }
|
|
587
|
+
count += candidates.filter(c => c.confidence >= 0.85).length; // only count directly added ones
|
|
588
|
+
fs.writeFileSync(capFile, count.toString());
|
|
589
|
+
}
|
|
590
|
+
catch { /* best effort */ }
|
|
591
|
+
}
|
|
592
|
+
}
|
|
543
593
|
}
|
|
544
594
|
catch (err) {
|
|
545
595
|
debugLog(`hook-tool: learning extraction failed: ${err instanceof Error ? err.message : String(err)}`);
|
package/mcp/dist/data-access.js
CHANGED
|
@@ -805,25 +805,30 @@ export function removeProjectFromProfile(cortexPath, profile, project) {
|
|
|
805
805
|
return cortexOk(`Removed ${project} from profile ${profile}.`);
|
|
806
806
|
});
|
|
807
807
|
}
|
|
808
|
+
function buildProjectCard(dir) {
|
|
809
|
+
const name = path.basename(dir);
|
|
810
|
+
const summaryFile = path.join(dir, "summary.md");
|
|
811
|
+
const claudeFile = path.join(dir, "CLAUDE.md");
|
|
812
|
+
const summarySource = fs.existsSync(summaryFile)
|
|
813
|
+
? fs.readFileSync(summaryFile, "utf8")
|
|
814
|
+
: fs.existsSync(claudeFile)
|
|
815
|
+
? fs.readFileSync(claudeFile, "utf8")
|
|
816
|
+
: "";
|
|
817
|
+
const summary = summarySource
|
|
818
|
+
.split("\n")
|
|
819
|
+
.map((line) => line.trim())
|
|
820
|
+
.find((line) => line && !line.startsWith("#")) || "";
|
|
821
|
+
const docs = ["CLAUDE.md", "FINDINGS.md", "LEARNINGS.md", "summary.md", "backlog.md", "MEMORY_QUEUE.md"]
|
|
822
|
+
.filter((file) => fs.existsSync(path.join(dir, file)));
|
|
823
|
+
return { name, summary, docs };
|
|
824
|
+
}
|
|
808
825
|
export function listProjectCards(cortexPath, profile) {
|
|
809
826
|
const dirs = getProjectDirs(cortexPath, profile).sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
810
|
-
const cards =
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const summarySource = fs.existsSync(summaryFile)
|
|
816
|
-
? fs.readFileSync(summaryFile, "utf8")
|
|
817
|
-
: fs.existsSync(claudeFile)
|
|
818
|
-
? fs.readFileSync(claudeFile, "utf8")
|
|
819
|
-
: "";
|
|
820
|
-
const summary = summarySource
|
|
821
|
-
.split("\n")
|
|
822
|
-
.map((line) => line.trim())
|
|
823
|
-
.find((line) => line && !line.startsWith("#")) || "";
|
|
824
|
-
const docs = ["CLAUDE.md", "FINDINGS.md", "LEARNINGS.md", "summary.md", "backlog.md", "MEMORY_QUEUE.md"]
|
|
825
|
-
.filter((file) => fs.existsSync(path.join(dir, file)));
|
|
826
|
-
cards.push({ name, summary, docs });
|
|
827
|
+
const cards = dirs.map(buildProjectCard);
|
|
828
|
+
// Prepend global as a pinned entry so it's always accessible from the shell
|
|
829
|
+
const globalDir = path.join(cortexPath, "global");
|
|
830
|
+
if (fs.existsSync(globalDir)) {
|
|
831
|
+
cards.unshift(buildProjectCard(globalDir));
|
|
827
832
|
}
|
|
828
833
|
return cards;
|
|
829
834
|
}
|
package/mcp/dist/mcp-backlog.js
CHANGED
|
@@ -5,7 +5,8 @@ import * as path from "path";
|
|
|
5
5
|
import { isValidProjectName } from "./utils.js";
|
|
6
6
|
import { addBacklogItem as addBacklogItemStore, addBacklogItems as addBacklogItemsBatch, backlogMarkdown, completeBacklogItem as completeBacklogItemStore, completeBacklogItems as completeBacklogItemsBatch, readBacklog, readBacklogs, updateBacklogItem as updateBacklogItemStore, } from "./data-access.js";
|
|
7
7
|
const BACKLOG_SECTION_ORDER = ["Active", "Queue", "Done"];
|
|
8
|
-
|
|
8
|
+
const DEFAULT_BACKLOG_LIMIT = 20;
|
|
9
|
+
function buildBacklogView(doc, status, limit) {
|
|
9
10
|
let includedSections;
|
|
10
11
|
if (status === "all") {
|
|
11
12
|
includedSections = BACKLOG_SECTION_ORDER;
|
|
@@ -22,11 +23,23 @@ function buildBacklogView(doc, status) {
|
|
|
22
23
|
else {
|
|
23
24
|
includedSections = ["Active", "Queue"];
|
|
24
25
|
}
|
|
26
|
+
const effectiveLimit = limit ?? DEFAULT_BACKLOG_LIMIT;
|
|
27
|
+
let truncated = false;
|
|
25
28
|
const items = {
|
|
26
|
-
Active:
|
|
27
|
-
Queue:
|
|
28
|
-
Done:
|
|
29
|
+
Active: [],
|
|
30
|
+
Queue: [],
|
|
31
|
+
Done: [],
|
|
29
32
|
};
|
|
33
|
+
for (const section of includedSections) {
|
|
34
|
+
const sectionItems = doc.items[section];
|
|
35
|
+
if (sectionItems.length > effectiveLimit) {
|
|
36
|
+
items[section] = sectionItems.slice(0, effectiveLimit);
|
|
37
|
+
truncated = true;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
items[section] = sectionItems;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
30
43
|
const totalItems = BACKLOG_SECTION_ORDER.reduce((sum, section) => sum + items[section].length, 0);
|
|
31
44
|
return {
|
|
32
45
|
doc: {
|
|
@@ -35,8 +48,26 @@ function buildBacklogView(doc, status) {
|
|
|
35
48
|
},
|
|
36
49
|
includedSections,
|
|
37
50
|
totalItems,
|
|
51
|
+
truncated,
|
|
38
52
|
};
|
|
39
53
|
}
|
|
54
|
+
function buildBacklogSummary(doc, includedSections) {
|
|
55
|
+
const lines = [`## ${doc.project}`];
|
|
56
|
+
for (const section of includedSections) {
|
|
57
|
+
const items = doc.items[section];
|
|
58
|
+
const highCount = items.filter(i => i.priority === "high").length;
|
|
59
|
+
const medCount = items.filter(i => i.priority === "medium").length;
|
|
60
|
+
lines.push(`**${section}**: ${items.length} items${highCount ? ` (${highCount} high` + (medCount ? `, ${medCount} medium` : "") + ")" : ""}`);
|
|
61
|
+
// Show first 3 items as preview
|
|
62
|
+
for (const item of items.slice(0, 3)) {
|
|
63
|
+
const prio = item.priority ? ` [${item.priority}]` : "";
|
|
64
|
+
lines.push(` - ${item.line.slice(0, 80)}${item.line.length > 80 ? "\u2026" : ""}${prio}`);
|
|
65
|
+
}
|
|
66
|
+
if (items.length > 3)
|
|
67
|
+
lines.push(` ... and ${items.length - 3} more`);
|
|
68
|
+
}
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
40
71
|
export function register(server, ctx) {
|
|
41
72
|
const { cortexPath, profile, withWriteQueue, updateFileInIndex } = ctx;
|
|
42
73
|
server.registerTool("get_backlog", {
|
|
@@ -47,8 +78,10 @@ export function register(server, ctx) {
|
|
|
47
78
|
id: z.string().optional().describe("Backlog item ID like A1, Q3, D2. Requires project."),
|
|
48
79
|
item: z.string().optional().describe("Exact backlog item text. Requires project."),
|
|
49
80
|
status: z.enum(["all", "active", "queue", "done", "active+queue"]).optional().describe("Which backlog sections to include. Defaults to 'active+queue'."),
|
|
81
|
+
limit: z.number().optional().describe("Max items per section to return. Default 20."),
|
|
82
|
+
summary: z.boolean().optional().describe("If true, return counts and titles only (no full content). Reduces token usage."),
|
|
50
83
|
}),
|
|
51
|
-
}, async ({ project, id, item, status }) => {
|
|
84
|
+
}, async ({ project, id, item, status, limit, summary }) => {
|
|
52
85
|
// Single item lookup
|
|
53
86
|
if (id || item) {
|
|
54
87
|
if (!project)
|
|
@@ -78,7 +111,7 @@ export function register(server, ctx) {
|
|
|
78
111
|
if (!result.ok)
|
|
79
112
|
return mcpResponse({ ok: false, error: result.error });
|
|
80
113
|
const doc = result.data;
|
|
81
|
-
const view = buildBacklogView(doc, status);
|
|
114
|
+
const view = buildBacklogView(doc, status, limit);
|
|
82
115
|
if (!fs.existsSync(doc.path)) {
|
|
83
116
|
return mcpResponse({
|
|
84
117
|
ok: true,
|
|
@@ -86,26 +119,43 @@ export function register(server, ctx) {
|
|
|
86
119
|
data: { project, items: view.doc.items, includedSections: view.includedSections, totalItems: view.totalItems },
|
|
87
120
|
});
|
|
88
121
|
}
|
|
122
|
+
if (summary) {
|
|
123
|
+
return mcpResponse({
|
|
124
|
+
ok: true,
|
|
125
|
+
message: buildBacklogSummary(doc, view.includedSections),
|
|
126
|
+
data: { project, includedSections: view.includedSections, totalItems: view.totalItems, summary: true },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const truncationNote = view.truncated ? `\n\n_Showing first ${limit ?? DEFAULT_BACKLOG_LIMIT} items per section. Pass a higher limit to see more._` : "";
|
|
89
130
|
return mcpResponse({
|
|
90
131
|
ok: true,
|
|
91
|
-
message: `## ${project}\n${backlogMarkdown(view.doc)}`,
|
|
92
|
-
data: { project, items: view.doc.items, issues: doc.issues, includedSections: view.includedSections, totalItems: view.totalItems },
|
|
132
|
+
message: `## ${project}\n${backlogMarkdown(view.doc)}${truncationNote}`,
|
|
133
|
+
data: { project, items: view.doc.items, issues: doc.issues, includedSections: view.includedSections, totalItems: view.totalItems, truncated: view.truncated },
|
|
93
134
|
});
|
|
94
135
|
}
|
|
95
136
|
// All projects
|
|
96
137
|
const docs = readBacklogs(cortexPath, profile);
|
|
97
138
|
if (!docs.length)
|
|
98
139
|
return mcpResponse({ ok: true, message: "No backlogs found.", data: { projects: [] } });
|
|
99
|
-
const views = docs.map((doc) => ({ project: doc.project, view: buildBacklogView(doc, status), issues: doc.issues }));
|
|
100
|
-
const
|
|
140
|
+
const views = docs.map((doc) => ({ project: doc.project, doc, view: buildBacklogView(doc, status, limit), issues: doc.issues }));
|
|
141
|
+
const anyTruncated = views.some(({ view }) => view.truncated);
|
|
142
|
+
let parts;
|
|
143
|
+
if (summary) {
|
|
144
|
+
parts = views.map(({ doc, view }) => buildBacklogSummary(doc, view.includedSections));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
parts = views.map(({ project, view }) => `## ${project}\n${backlogMarkdown(view.doc)}`);
|
|
148
|
+
}
|
|
149
|
+
const truncationNote = anyTruncated && !summary ? `\n\n_Showing first ${limit ?? DEFAULT_BACKLOG_LIMIT} items per section. Pass a higher limit to see more._` : "";
|
|
101
150
|
const projectData = views.map(({ project, view, issues }) => ({
|
|
102
151
|
project,
|
|
103
152
|
items: view.doc.items,
|
|
104
153
|
issues,
|
|
105
154
|
includedSections: view.includedSections,
|
|
106
155
|
totalItems: view.totalItems,
|
|
156
|
+
truncated: view.truncated,
|
|
107
157
|
}));
|
|
108
|
-
return mcpResponse({ ok: true, message: parts.join("\n\n"), data: { projects: projectData } });
|
|
158
|
+
return mcpResponse({ ok: true, message: parts.join("\n\n") + truncationNote, data: { projects: projectData, summary: summary || false } });
|
|
109
159
|
});
|
|
110
160
|
server.registerTool("add_backlog_item", {
|
|
111
161
|
title: "◆ cortex · add task",
|
|
@@ -835,6 +835,8 @@ export function getQualityMultiplier(cortexPath, key) {
|
|
|
835
835
|
let helpful = entry ? entry.helpful : 0;
|
|
836
836
|
let repromptPenalty = entry ? entry.repromptPenalty : 0;
|
|
837
837
|
let regressionPenalty = entry ? entry.regressionPenalty : 0;
|
|
838
|
+
let impressions = entry ? entry.impressions : 0;
|
|
839
|
+
let lastUsedAt = entry ? entry.lastUsedAt : "";
|
|
838
840
|
// Include unflushed journal deltas
|
|
839
841
|
const journalEntries = readScoreJournal(cortexPath).filter(e => e.key === key);
|
|
840
842
|
for (const je of journalEntries) {
|
|
@@ -844,11 +846,39 @@ export function getQualityMultiplier(cortexPath, key) {
|
|
|
844
846
|
repromptPenalty += je.delta.repromptPenalty;
|
|
845
847
|
if (je.delta.regressionPenalty)
|
|
846
848
|
regressionPenalty += je.delta.regressionPenalty;
|
|
849
|
+
if (je.delta.impressions)
|
|
850
|
+
impressions += je.delta.impressions;
|
|
851
|
+
// Track most recent journal timestamp as lastUsedAt
|
|
852
|
+
if (je.at && (!lastUsedAt || je.at > lastUsedAt))
|
|
853
|
+
lastUsedAt = je.at;
|
|
847
854
|
}
|
|
848
855
|
if (!entry && journalEntries.length === 0)
|
|
849
856
|
return 1;
|
|
857
|
+
// ACT-R activation scoring components:
|
|
858
|
+
// 1. Temporal decay: recency of last retrieval
|
|
859
|
+
let recencyBoost = 0;
|
|
860
|
+
if (lastUsedAt) {
|
|
861
|
+
const lastUsedMs = new Date(lastUsedAt).getTime();
|
|
862
|
+
if (!Number.isNaN(lastUsedMs)) {
|
|
863
|
+
const daysSinceUse = Math.max(0, (Date.now() - lastUsedMs) / 86_400_000);
|
|
864
|
+
if (daysSinceUse <= 7) {
|
|
865
|
+
recencyBoost = 0.15; // recently used: boost
|
|
866
|
+
}
|
|
867
|
+
else if (daysSinceUse <= 30) {
|
|
868
|
+
recencyBoost = 0; // neutral
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
recencyBoost = -0.1 * Math.min(3, (daysSinceUse - 30) / 30); // decay up to -0.3
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// 2. Usage frequency: log-scaled impressions (diminishing returns)
|
|
876
|
+
const frequencyBoost = impressions > 0 ? Math.min(0.2, Math.log2(impressions + 1) * 0.05) : 0;
|
|
877
|
+
// 3. Feedback signals (existing)
|
|
850
878
|
const penalties = repromptPenalty + regressionPenalty * 2;
|
|
851
|
-
const
|
|
879
|
+
const feedbackScore = helpful * 0.15 - penalties * 0.2;
|
|
880
|
+
// Combine all components
|
|
881
|
+
const raw = 1 + feedbackScore + recencyBoost + frequencyBoost;
|
|
852
882
|
return Math.max(0.2, Math.min(1.5, raw));
|
|
853
883
|
}
|
|
854
884
|
export function recordRetrieval(cortexPath, file, section) {
|
package/mcp/dist/shell-input.js
CHANGED
|
@@ -607,7 +607,7 @@ export function getListItems(cortexPath, profile, state, healthLineCount) {
|
|
|
607
607
|
return getProjectSkills(cortexPath, state.project).map((s) => ({ name: s.name, text: s.path }));
|
|
608
608
|
}
|
|
609
609
|
case "Hooks": {
|
|
610
|
-
return getHookEntries(cortexPath).map((e) => ({ name: e.
|
|
610
|
+
return getHookEntries(cortexPath).map((e) => ({ name: e.event, text: e.enabled ? "active" : "inactive" }));
|
|
611
611
|
}
|
|
612
612
|
case "Health":
|
|
613
613
|
return Array.from({ length: Math.max(1, healthLineCount) }, (_, i) => ({ id: String(i) }));
|
|
@@ -663,7 +663,7 @@ export async function activateSelected(host) {
|
|
|
663
663
|
break;
|
|
664
664
|
case "Hooks":
|
|
665
665
|
if (item.name) {
|
|
666
|
-
host.setMessage(` ${item.text === "
|
|
666
|
+
host.setMessage(` ${item.text === "active" ? style.boldGreen("active") : style.dim("inactive")} ${style.bold(item.name)}`);
|
|
667
667
|
}
|
|
668
668
|
break;
|
|
669
669
|
}
|
|
@@ -766,11 +766,10 @@ export async function doViewAction(host, key) {
|
|
|
766
766
|
}
|
|
767
767
|
break;
|
|
768
768
|
case "Hooks":
|
|
769
|
-
if (
|
|
769
|
+
if (key === "a" || key === "d") {
|
|
770
770
|
const enable = key === "a";
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
host.setMessage(` ${enable ? style.boldGreen("Enabled") : style.dim("Disabled")} hooks for ${item.name}`);
|
|
771
|
+
writeInstallPreferences(host.cortexPath, { hooksEnabled: enable });
|
|
772
|
+
host.setMessage(` Hooks ${enable ? style.boldGreen("enabled") : style.dim("disabled")} — takes effect next session`);
|
|
774
773
|
}
|
|
775
774
|
break;
|
|
776
775
|
}
|
package/mcp/dist/shell-view.js
CHANGED
|
@@ -434,20 +434,20 @@ export function renderSkillsView(ctx, cursor, height) {
|
|
|
434
434
|
}
|
|
435
435
|
return vp.lines;
|
|
436
436
|
}
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
const LIFECYCLE_HOOKS = [
|
|
438
|
+
{ event: "UserPromptSubmit", description: "inject context before each prompt" },
|
|
439
|
+
{ event: "Stop", description: "auto-save findings after each response" },
|
|
440
|
+
{ event: "SessionStart", description: "git pull at session start" },
|
|
441
|
+
];
|
|
439
442
|
export function getHookEntries(cortexPath) {
|
|
440
443
|
const prefs = readInstallPreferences(cortexPath);
|
|
441
444
|
const hooksEnabled = prefs.hooksEnabled !== false;
|
|
442
|
-
|
|
443
|
-
return HOOK_TOOLS.map((tool) => ({
|
|
444
|
-
tool,
|
|
445
|
-
enabled: hooksEnabled && toolPrefs[tool] !== false,
|
|
446
|
-
}));
|
|
445
|
+
return LIFECYCLE_HOOKS.map((h) => ({ ...h, enabled: hooksEnabled }));
|
|
447
446
|
}
|
|
448
447
|
export function renderHooksView(ctx, cursor, height) {
|
|
449
448
|
const cols = process.stdout.columns || 80;
|
|
450
449
|
const entries = getHookEntries(ctx.cortexPath);
|
|
450
|
+
const allEnabled = entries.every((e) => e.enabled);
|
|
451
451
|
const allLines = [];
|
|
452
452
|
let cursorFirstLine = 0;
|
|
453
453
|
let cursorLastLine = 0;
|
|
@@ -456,16 +456,24 @@ export function renderHooksView(ctx, cursor, height) {
|
|
|
456
456
|
const isSelected = i === cursor;
|
|
457
457
|
if (isSelected)
|
|
458
458
|
cursorFirstLine = allLines.length;
|
|
459
|
-
const statusBadge = e.enabled ? style.boldGreen("
|
|
460
|
-
let
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
459
|
+
const statusBadge = e.enabled ? style.boldGreen("active ") : style.dim("inactive");
|
|
460
|
+
let nameRow = ` ${style.dim((i + 1).toString().padEnd(3))} ${statusBadge} ${style.bold(e.event)}`;
|
|
461
|
+
let descRow = ` ${style.dim(e.description)}`;
|
|
462
|
+
if (isSelected) {
|
|
463
|
+
nameRow = `\x1b[7m${padToWidth(nameRow, cols)}${RESET}`;
|
|
464
|
+
descRow = `\x1b[7m${padToWidth(descRow, cols)}${RESET}`;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
nameRow = truncateLine(nameRow, cols);
|
|
468
|
+
descRow = truncateLine(descRow, cols);
|
|
469
|
+
}
|
|
470
|
+
allLines.push(nameRow);
|
|
471
|
+
allLines.push(descRow);
|
|
466
472
|
if (isSelected)
|
|
467
473
|
cursorLastLine = allLines.length - 1;
|
|
468
474
|
}
|
|
475
|
+
allLines.push("");
|
|
476
|
+
allLines.push(style.dim(` hooks: ${allEnabled ? style.boldGreen("ON") : style.boldRed("OFF")} · ${style.dim("a = enable all · d = disable all")}`));
|
|
469
477
|
const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
|
|
470
478
|
const vp = lineViewport(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll());
|
|
471
479
|
ctx.setScroll(vp.scrollStart);
|