@alaarab/cortex 1.14.0 → 1.15.1
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 +54 -13
- package/mcp/dist/cli-config.js +0 -1
- package/mcp/dist/cli-hooks-citations.js +1 -1
- package/mcp/dist/cli-hooks-retrieval.js +42 -9
- package/mcp/dist/cli-hooks.js +2 -2
- package/mcp/dist/cli.js +146 -3
- package/mcp/dist/index.js +8 -0
- package/mcp/dist/link.js +53 -2
- package/mcp/dist/shell-input.js +66 -3
- package/mcp/dist/shell-types.js +3 -1
- package/mcp/dist/shell-view.js +122 -1
- package/mcp/dist/shell.js +14 -0
- package/mcp/dist/utils.js +49 -5
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ On a new machine: clone, run init, done.
|
|
|
98
98
|
|
|
99
99
|
## What's new
|
|
100
100
|
|
|
101
|
-
- **Terminal shell**: open `cortex` and get tabs for Backlog, Findings,
|
|
101
|
+
- **Terminal shell**: open `cortex` and get tabs for Backlog, Findings, Review Queue, Skills, Hooks, and Health. No agent needed
|
|
102
102
|
- **Synonym search**: type "throttling" and find "rate limit" and "429". You don't need to remember what you called it
|
|
103
103
|
- **Bulk operations**: `add_findings`, `add_backlog_items`, `complete_backlog_items`, `remove_findings` for batch work
|
|
104
104
|
- **Memory quality**: confidence scoring, age decay, and a feedback loop. Stale or low-signal entries stop appearing
|
|
@@ -148,7 +148,7 @@ On a new machine: clone, run init, done.
|
|
|
148
148
|
|
|
149
149
|
**Consolidation.** When findings accumulate past the threshold, cortex flags it once per session. The `/cortex-consolidate` skill archives old entries and promotes cross-project patterns to global findings.
|
|
150
150
|
|
|
151
|
-
**
|
|
151
|
+
**Review queue.** Findings that fail trust filtering land in `MEMORY_QUEUE.md` for review. Triage from the shell (press `m`) or with `:mq approve`, `:mq reject`, `:mq edit`.
|
|
152
152
|
|
|
153
153
|
---
|
|
154
154
|
|
|
@@ -160,7 +160,8 @@ The server indexes your cortex into a local SQLite FTS5 database. Tools are grou
|
|
|
160
160
|
|
|
161
161
|
| Tool | What it does |
|
|
162
162
|
|------|-------------|
|
|
163
|
-
| `
|
|
163
|
+
| `search_knowledge` | FTS5 search with synonym expansion. Filters by project, type, limit. |
|
|
164
|
+
| `get_memory_detail` | Fetch full content of a memory by id (e.g. `mem:project/filename`). |
|
|
164
165
|
| `get_project_summary` | Summary card and file list for a project. |
|
|
165
166
|
| `list_projects` | Everything in your active profile. |
|
|
166
167
|
| `get_findings` | Read recent findings for a project without a search query. |
|
|
@@ -225,14 +226,16 @@ Governance, policy, and maintenance tools are CLI-only (see `cortex config` and
|
|
|
225
226
|
|
|
226
227
|
## Interactive shell
|
|
227
228
|
|
|
228
|
-
`cortex` in a terminal opens the shell.
|
|
229
|
+
`cortex` in a terminal opens the shell. Seven views, single-key navigation:
|
|
229
230
|
|
|
230
231
|
| Key | View |
|
|
231
232
|
|-----|------|
|
|
232
233
|
| `p` | Projects |
|
|
233
234
|
| `b` | Backlog |
|
|
234
235
|
| `l` | Findings |
|
|
235
|
-
| `m` |
|
|
236
|
+
| `m` | Review Queue |
|
|
237
|
+
| `s` | Skills |
|
|
238
|
+
| `k` | Hooks |
|
|
236
239
|
| `h` | Health |
|
|
237
240
|
| `/` | Filter current view |
|
|
238
241
|
| `:` | Command palette |
|
|
@@ -263,16 +266,33 @@ The shell works the same on every machine, for every agent.
|
|
|
263
266
|
For scripting, hooks, and quick lookups from the terminal:
|
|
264
267
|
|
|
265
268
|
```bash
|
|
266
|
-
cortex
|
|
267
|
-
cortex search "rate limiting"
|
|
268
|
-
cortex add-finding <project> "..."
|
|
269
|
-
cortex pin <project> "..."
|
|
270
|
-
cortex
|
|
271
|
-
cortex
|
|
272
|
-
cortex
|
|
269
|
+
cortex # interactive shell (TTY default)
|
|
270
|
+
cortex search "rate limiting" # FTS5 search with synonym expansion
|
|
271
|
+
cortex add-finding <project> "..." # append a finding from the terminal
|
|
272
|
+
cortex pin <project> "..." # promote canonical memory
|
|
273
|
+
cortex backlog [project] # cross-project backlog view
|
|
274
|
+
cortex status # health, active project, stats
|
|
275
|
+
cortex doctor [--fix] # health checks + optional self-heal
|
|
276
|
+
cortex verify # check init completed correctly
|
|
277
|
+
cortex review-ui [--port=3499] # lightweight review UI in the browser
|
|
278
|
+
cortex update # update to latest version
|
|
279
|
+
cortex uninstall # remove cortex config and hooks
|
|
280
|
+
|
|
281
|
+
cortex link [--machine <n>] [--profile <n>] # sync profile, symlinks, hooks
|
|
282
|
+
cortex mcp-mode [on|off|status] # toggle MCP integration
|
|
283
|
+
cortex hooks-mode [on|off|status] # toggle hook execution
|
|
284
|
+
|
|
285
|
+
cortex skills list # list all installed skills
|
|
286
|
+
cortex skills add <project> <path> # add a skill to a project
|
|
287
|
+
cortex skills remove <project> <name> # remove a skill from a project
|
|
288
|
+
cortex skill-list # alias for skills list
|
|
289
|
+
|
|
290
|
+
cortex hooks list # show hook status per tool
|
|
291
|
+
cortex hooks enable <tool> # enable hooks for tool (claude/copilot/cursor/codex)
|
|
292
|
+
cortex hooks disable <tool> # disable hooks for tool
|
|
273
293
|
```
|
|
274
294
|
|
|
275
|
-
Use `cortex config` for policy tuning and `cortex maintain` for governance operations.
|
|
295
|
+
Use `cortex config` for policy tuning and `cortex maintain` for governance operations. Run `--dry-run` before destructive maintenance commands.
|
|
276
296
|
|
|
277
297
|
### cortex doctor
|
|
278
298
|
|
|
@@ -365,6 +385,27 @@ Four skills for the things that can't be automatic:
|
|
|
365
385
|
|
|
366
386
|
Put personal workflow skills in `~/.cortex/global/skills/`. `cortex link` symlinks them to `~/.claude/skills/` so they're available everywhere.
|
|
367
387
|
|
|
388
|
+
### Per-project agent config
|
|
389
|
+
|
|
390
|
+
Drop a `cortex.project.yaml` in `~/.cortex/<project>/` to control what gets injected for that project:
|
|
391
|
+
|
|
392
|
+
```yaml
|
|
393
|
+
# Opt out of global skill injection for this project
|
|
394
|
+
skills: false
|
|
395
|
+
|
|
396
|
+
# Register extra MCP servers when this project is linked
|
|
397
|
+
mcpServers:
|
|
398
|
+
my-tool:
|
|
399
|
+
command: node
|
|
400
|
+
args: [/path/to/server.js]
|
|
401
|
+
my-api:
|
|
402
|
+
command: /usr/local/bin/api-server
|
|
403
|
+
env:
|
|
404
|
+
API_KEY: "from-your-env"
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`cortex link` merges project MCP servers into your agent config under namespaced keys (`cortex__<project>__<name>`) and cleans them up automatically when the config changes.
|
|
408
|
+
|
|
368
409
|
---
|
|
369
410
|
|
|
370
411
|
## Adding projects
|
package/mcp/dist/cli-config.js
CHANGED
|
@@ -8,7 +8,6 @@ function getCortexPath() {
|
|
|
8
8
|
_cortexPath = ensureCortexPath();
|
|
9
9
|
return _cortexPath;
|
|
10
10
|
}
|
|
11
|
-
const profile = process.env.CORTEX_PROFILE || "";
|
|
12
11
|
// ── Config router ────────────────────────────────────────────────────────────
|
|
13
12
|
export async function handleConfig(args) {
|
|
14
13
|
const sub = args[0];
|
|
@@ -20,7 +20,7 @@ export function clearCitationValidCache() {
|
|
|
20
20
|
citationValidCache.clear();
|
|
21
21
|
}
|
|
22
22
|
/** @deprecated Legacy citation formats. Use `<!-- cortex:cite {...} -->` instead. */
|
|
23
|
-
const CITATION_PATTERN = /<!-- source: ([^:]+):(\d+) -->|\[file:([^:]+):(\d+)\]/g;
|
|
23
|
+
const CITATION_PATTERN = /<!-- source: ((?:[a-zA-Z]:[\\\/])?[^:]+):(\d+) -->|\[file:((?:[a-zA-Z]:[\\\/])?[^:]+):(\d+)\]/g;
|
|
24
24
|
export function parseCitations(text) {
|
|
25
25
|
const results = [];
|
|
26
26
|
let m;
|
|
@@ -122,7 +122,7 @@ function semanticFallbackDocs(db, prompt, project) {
|
|
|
122
122
|
const queryTokens = tokenizeForOverlap(prompt);
|
|
123
123
|
if (!queryTokens.length)
|
|
124
124
|
return [];
|
|
125
|
-
const sampleLimit =
|
|
125
|
+
const sampleLimit = 100;
|
|
126
126
|
const docs = project
|
|
127
127
|
? queryDocRows(db, "SELECT project, filename, type, content, path FROM docs WHERE project = ? LIMIT ?", [project, sampleLimit]) || []
|
|
128
128
|
: queryDocRows(db, "SELECT project, filename, type, content, path FROM docs LIMIT ?", [sampleLimit]) || [];
|
|
@@ -132,7 +132,7 @@ function semanticFallbackDocs(db, prompt, project) {
|
|
|
132
132
|
const score = overlapScore(queryTokens, corpus);
|
|
133
133
|
return { doc, score };
|
|
134
134
|
})
|
|
135
|
-
.filter((x) => x.score >= 0.
|
|
135
|
+
.filter((x) => x.score >= 0.25)
|
|
136
136
|
.sort((a, b) => b.score - a.score)
|
|
137
137
|
.slice(0, 8)
|
|
138
138
|
.map((x) => x.doc);
|
|
@@ -219,6 +219,22 @@ function mostRecentDate(content) {
|
|
|
219
219
|
return "0000-00-00";
|
|
220
220
|
return matches.map((m) => m.slice(3)).sort().reverse()[0];
|
|
221
221
|
}
|
|
222
|
+
function crossProjectAgeMultiplier(doc, detectedProject) {
|
|
223
|
+
if (doc.type !== "findings" || !detectedProject || doc.project === detectedProject)
|
|
224
|
+
return 1;
|
|
225
|
+
const decayDaysRaw = Number.parseInt(process.env.CORTEX_CROSS_PROJECT_DECAY_DAYS ?? "30", 10);
|
|
226
|
+
const decayDays = Number.isFinite(decayDaysRaw) && decayDaysRaw > 0 ? decayDaysRaw : 30;
|
|
227
|
+
const latest = mostRecentDate(doc.content);
|
|
228
|
+
const todayUtc = Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate());
|
|
229
|
+
let ageInDays = 90;
|
|
230
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(latest) && latest !== "0000-00-00") {
|
|
231
|
+
const entryUtc = Date.parse(`${latest}T00:00:00Z`);
|
|
232
|
+
if (!Number.isNaN(entryUtc)) {
|
|
233
|
+
ageInDays = Math.max(0, Math.floor((todayUtc - entryUtc) / 86_400_000));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return Math.max(0.1, 1 - (ageInDays / decayDays));
|
|
237
|
+
}
|
|
222
238
|
export function rankResults(rows, intent, gitCtx, detectedProject, cortexPathLocal, db, cwd, query) {
|
|
223
239
|
let ranked = [...rows];
|
|
224
240
|
if (detectedProject) {
|
|
@@ -260,31 +276,48 @@ export function rankResults(rows, intent, gitCtx, detectedProject, cortexPathLoc
|
|
|
260
276
|
if (byDate !== 0)
|
|
261
277
|
return byDate;
|
|
262
278
|
}
|
|
279
|
+
const changedFiles = gitCtx?.changedFiles || new Set();
|
|
280
|
+
const globBoostA = getProjectGlobBoost(cortexPathLocal, a.project, cwd, gitCtx?.changedFiles);
|
|
281
|
+
const globBoostB = getProjectGlobBoost(cortexPathLocal, b.project, cwd, gitCtx?.changedFiles);
|
|
282
|
+
const keyA = entryScoreKey(a.project, a.filename, a.content);
|
|
283
|
+
const keyB = entryScoreKey(b.project, b.filename, b.content);
|
|
284
|
+
const entityA = entityBoostPaths.has(a.path) ? 1.3 : 1;
|
|
285
|
+
const entityB = entityBoostPaths.has(b.path) ? 1.3 : 1;
|
|
286
|
+
const scoreA = (intentBoost(intent, a.type) +
|
|
287
|
+
fileRelevanceBoost(a.path, changedFiles) +
|
|
288
|
+
branchMatchBoost(a.content, gitCtx?.branch) +
|
|
289
|
+
globBoostA +
|
|
290
|
+
getQualityMultiplier(cortexPathLocal, keyA) +
|
|
291
|
+
entityA -
|
|
292
|
+
lowValuePenalty(a.content, a.type)) * crossProjectAgeMultiplier(a, detectedProject);
|
|
293
|
+
const scoreB = (intentBoost(intent, b.type) +
|
|
294
|
+
fileRelevanceBoost(b.path, changedFiles) +
|
|
295
|
+
branchMatchBoost(b.content, gitCtx?.branch) +
|
|
296
|
+
globBoostB +
|
|
297
|
+
getQualityMultiplier(cortexPathLocal, keyB) +
|
|
298
|
+
entityB -
|
|
299
|
+
lowValuePenalty(b.content, b.type)) * crossProjectAgeMultiplier(b, detectedProject);
|
|
300
|
+
const scoreDelta = scoreB - scoreA;
|
|
301
|
+
if (Math.abs(scoreDelta) > 0.01)
|
|
302
|
+
return scoreDelta;
|
|
263
303
|
const intentDelta = intentBoost(intent, b.type) - intentBoost(intent, a.type);
|
|
264
304
|
if (intentDelta !== 0)
|
|
265
305
|
return intentDelta;
|
|
266
|
-
const changedFiles = gitCtx?.changedFiles || new Set();
|
|
267
306
|
const fileDelta = fileRelevanceBoost(b.path, changedFiles) - fileRelevanceBoost(a.path, changedFiles);
|
|
268
307
|
if (fileDelta !== 0)
|
|
269
308
|
return fileDelta;
|
|
270
309
|
const branchDelta = branchMatchBoost(b.content, gitCtx?.branch) - branchMatchBoost(a.content, gitCtx?.branch);
|
|
271
310
|
if (branchDelta !== 0)
|
|
272
311
|
return branchDelta;
|
|
273
|
-
const globBoostA = getProjectGlobBoost(cortexPathLocal, a.project, cwd, gitCtx?.changedFiles);
|
|
274
|
-
const globBoostB = getProjectGlobBoost(cortexPathLocal, b.project, cwd, gitCtx?.changedFiles);
|
|
275
312
|
const globDelta = globBoostB - globBoostA;
|
|
276
313
|
if (Math.abs(globDelta) > 0.01)
|
|
277
314
|
return globDelta;
|
|
278
|
-
const keyA = entryScoreKey(a.project, a.filename, a.content);
|
|
279
|
-
const keyB = entryScoreKey(b.project, b.filename, b.content);
|
|
280
315
|
const qualityDelta = getQualityMultiplier(cortexPathLocal, keyB) - getQualityMultiplier(cortexPathLocal, keyA);
|
|
281
316
|
if (qualityDelta !== 0)
|
|
282
317
|
return qualityDelta;
|
|
283
318
|
const penaltyDelta = lowValuePenalty(a.content, a.type) - lowValuePenalty(b.content, b.type);
|
|
284
319
|
if (penaltyDelta !== 0)
|
|
285
320
|
return penaltyDelta;
|
|
286
|
-
const entityA = entityBoostPaths.has(a.path) ? 1.3 : 1;
|
|
287
|
-
const entityB = entityBoostPaths.has(b.path) ? 1.3 : 1;
|
|
288
321
|
if (entityB !== entityA)
|
|
289
322
|
return entityB - entityA;
|
|
290
323
|
return 0;
|
package/mcp/dist/cli-hooks.js
CHANGED
|
@@ -40,7 +40,7 @@ const profile = process.env.CORTEX_PROFILE || "";
|
|
|
40
40
|
async function readStdin() {
|
|
41
41
|
return new Promise((resolve, reject) => {
|
|
42
42
|
const chunks = [];
|
|
43
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
43
|
+
process.stdin.on("data", (chunk) => chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
|
|
44
44
|
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
45
45
|
process.stdin.on("error", reject);
|
|
46
46
|
});
|
|
@@ -89,7 +89,7 @@ export async function handleHookPrompt() {
|
|
|
89
89
|
const detectedProject = cwd ? detectProject(getCortexPath(), cwd, profile) : null;
|
|
90
90
|
if (detectedProject)
|
|
91
91
|
debugLog(`Detected project: ${detectedProject}`);
|
|
92
|
-
const safeQuery = buildRobustFtsQuery(keywords);
|
|
92
|
+
const safeQuery = buildRobustFtsQuery(keywords, detectedProject);
|
|
93
93
|
if (!safeQuery)
|
|
94
94
|
process.exit(0);
|
|
95
95
|
try {
|
package/mcp/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ 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
|
+
import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
|
|
24
25
|
let _cortexPath;
|
|
25
26
|
function getCortexPath() {
|
|
26
27
|
if (!_cortexPath)
|
|
@@ -275,6 +276,10 @@ export async function runCliCommand(command, args) {
|
|
|
275
276
|
return handleMaintain(args);
|
|
276
277
|
case "skill-list":
|
|
277
278
|
return handleSkillList();
|
|
279
|
+
case "skills":
|
|
280
|
+
return handleSkillsNamespace(args);
|
|
281
|
+
case "hooks":
|
|
282
|
+
return handleHooksNamespace(args);
|
|
278
283
|
case "backlog":
|
|
279
284
|
return handleBacklogView();
|
|
280
285
|
case "quickstart":
|
|
@@ -305,7 +310,7 @@ async function handleSearch(opts) {
|
|
|
305
310
|
const where = [];
|
|
306
311
|
const params = [];
|
|
307
312
|
if (opts.query) {
|
|
308
|
-
const safeQuery = buildRobustFtsQuery(opts.query);
|
|
313
|
+
const safeQuery = buildRobustFtsQuery(opts.query, opts.project);
|
|
309
314
|
if (!safeQuery) {
|
|
310
315
|
console.error("Query empty after sanitization.");
|
|
311
316
|
process.exit(1);
|
|
@@ -487,14 +492,149 @@ async function handleUpdate(args) {
|
|
|
487
492
|
const result = await runCortexUpdate();
|
|
488
493
|
console.log(result);
|
|
489
494
|
}
|
|
495
|
+
const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
|
|
496
|
+
function printSkillsUsage() {
|
|
497
|
+
console.log("Usage:");
|
|
498
|
+
console.log(" cortex skills list");
|
|
499
|
+
console.log(" cortex skills add <project> <path>");
|
|
500
|
+
console.log(" cortex skills remove <project> <name>");
|
|
501
|
+
}
|
|
502
|
+
function printHooksUsage() {
|
|
503
|
+
console.log("Usage:");
|
|
504
|
+
console.log(" cortex hooks list");
|
|
505
|
+
console.log(" cortex hooks enable <tool>");
|
|
506
|
+
console.log(" cortex hooks disable <tool>");
|
|
507
|
+
console.log(" tools: claude|copilot|cursor|codex");
|
|
508
|
+
}
|
|
509
|
+
function normalizeHookTool(raw) {
|
|
510
|
+
if (!raw)
|
|
511
|
+
return null;
|
|
512
|
+
const tool = raw.toLowerCase();
|
|
513
|
+
return HOOK_TOOLS.includes(tool) ? tool : null;
|
|
514
|
+
}
|
|
515
|
+
function handleSkillsNamespace(args) {
|
|
516
|
+
const subcommand = args[0];
|
|
517
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
518
|
+
printSkillsUsage();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (subcommand === "list") {
|
|
522
|
+
handleSkillList();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (subcommand === "add") {
|
|
526
|
+
const project = args[1];
|
|
527
|
+
const skillPath = args[2];
|
|
528
|
+
if (!project || !skillPath) {
|
|
529
|
+
printSkillsUsage();
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
if (!isValidProjectName(project)) {
|
|
533
|
+
console.error(`Invalid project name: "${project}"`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
const source = path.resolve(skillPath.replace(/^~/, os.homedir()));
|
|
537
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
|
|
538
|
+
console.error(`Skill file not found: ${source}`);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
const baseName = path.basename(source);
|
|
542
|
+
const fileName = baseName.toLowerCase().endsWith(".md") ? baseName : `${baseName}.md`;
|
|
543
|
+
const destDir = path.join(getCortexPath(), project, ".claude", "skills");
|
|
544
|
+
const dest = path.join(destDir, fileName);
|
|
545
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
546
|
+
if (fs.existsSync(dest)) {
|
|
547
|
+
console.error(`Skill already exists: ${dest}`);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
fs.symlinkSync(source, dest);
|
|
552
|
+
console.log(`Linked skill ${fileName} into ${project}.`);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
fs.copyFileSync(source, dest);
|
|
556
|
+
console.log(`Copied skill ${fileName} into ${project}.`);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (subcommand === "remove") {
|
|
561
|
+
const project = args[1];
|
|
562
|
+
const name = args[2];
|
|
563
|
+
if (!project || !name) {
|
|
564
|
+
printSkillsUsage();
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
if (!isValidProjectName(project)) {
|
|
568
|
+
console.error(`Invalid project name: "${project}"`);
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
const dest = path.join(getCortexPath(), project, ".claude", "skills", `${name.replace(/\.md$/i, "")}.md`);
|
|
572
|
+
if (!fs.existsSync(dest)) {
|
|
573
|
+
console.error(`Skill not found: ${dest}`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
fs.unlinkSync(dest);
|
|
577
|
+
console.log(`Removed skill ${name.replace(/\.md$/i, "")}.md from ${project}.`);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
console.error(`Unknown skills subcommand: ${subcommand}`);
|
|
581
|
+
printSkillsUsage();
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
function handleHooksNamespace(args) {
|
|
585
|
+
const subcommand = args[0];
|
|
586
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
587
|
+
printHooksUsage();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (subcommand === "list") {
|
|
591
|
+
const prefs = readInstallPreferences(getCortexPath());
|
|
592
|
+
const hooksEnabled = prefs.hooksEnabled !== false;
|
|
593
|
+
const toolPrefs = prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {};
|
|
594
|
+
const rows = HOOK_TOOLS.map((tool) => ({
|
|
595
|
+
tool,
|
|
596
|
+
hookType: "lifecycle",
|
|
597
|
+
status: hooksEnabled && toolPrefs[tool] !== false ? "enabled" : "disabled",
|
|
598
|
+
}));
|
|
599
|
+
console.log(`Tool Hook Type Status`);
|
|
600
|
+
console.log(`-------- --------- --------`);
|
|
601
|
+
for (const row of rows) {
|
|
602
|
+
console.log(`${row.tool.padEnd(8)} ${row.hookType.padEnd(9)} ${row.status}`);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (subcommand === "enable" || subcommand === "disable") {
|
|
607
|
+
const tool = normalizeHookTool(args[1]);
|
|
608
|
+
if (!tool) {
|
|
609
|
+
printHooksUsage();
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
const prefs = readInstallPreferences(getCortexPath());
|
|
613
|
+
writeInstallPreferences(getCortexPath(), {
|
|
614
|
+
hookTools: {
|
|
615
|
+
...(prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {}),
|
|
616
|
+
[tool]: subcommand === "enable",
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
console.log(`${subcommand === "enable" ? "Enabled" : "Disabled"} hooks for ${tool}.`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
console.error(`Unknown hooks subcommand: ${subcommand}`);
|
|
623
|
+
printHooksUsage();
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
490
626
|
function handleSkillList() {
|
|
491
627
|
const sources = [];
|
|
628
|
+
const seenPaths = new Set();
|
|
492
629
|
function collectSkills(root, sourceLabel) {
|
|
493
630
|
if (!fs.existsSync(root))
|
|
494
631
|
return;
|
|
495
632
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
496
633
|
const entryPath = path.join(root, entry.name);
|
|
497
634
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
635
|
+
if (seenPaths.has(entryPath))
|
|
636
|
+
continue;
|
|
637
|
+
seenPaths.add(entryPath);
|
|
498
638
|
sources.push({
|
|
499
639
|
name: entry.name.replace(/\.md$/, ""),
|
|
500
640
|
source: sourceLabel,
|
|
@@ -507,6 +647,9 @@ function handleSkillList() {
|
|
|
507
647
|
const skillFile = path.join(entryPath, "SKILL.md");
|
|
508
648
|
if (!fs.existsSync(skillFile))
|
|
509
649
|
continue;
|
|
650
|
+
if (seenPaths.has(skillFile))
|
|
651
|
+
continue;
|
|
652
|
+
seenPaths.add(skillFile);
|
|
510
653
|
sources.push({
|
|
511
654
|
name: entry.name,
|
|
512
655
|
source: sourceLabel,
|
|
@@ -523,8 +666,8 @@ function handleSkillList() {
|
|
|
523
666
|
const projectName = path.basename(dir);
|
|
524
667
|
if (projectName === "global")
|
|
525
668
|
continue;
|
|
526
|
-
|
|
527
|
-
collectSkills(
|
|
669
|
+
collectSkills(path.join(dir, "skills"), projectName);
|
|
670
|
+
collectSkills(path.join(dir, ".claude", "skills"), projectName);
|
|
528
671
|
}
|
|
529
672
|
if (!sources.length) {
|
|
530
673
|
console.log("No skills found.");
|
package/mcp/dist/index.js
CHANGED
|
@@ -25,6 +25,12 @@ Usage:
|
|
|
25
25
|
cortex init [--machine <n>] [--profile <n>] [--mcp on|off] [--template <t>] [--from-existing <path>] [--dry-run] [-y]
|
|
26
26
|
Set up cortex (templates: python-project, monorepo, library, frontend)
|
|
27
27
|
cortex detect-skills [--import] Find untracked skills in ~/.claude/skills/
|
|
28
|
+
cortex skills list List installed skills
|
|
29
|
+
cortex skills add <project> <path> Link or copy a skill file into one project
|
|
30
|
+
cortex skills remove <project> <name> Remove a project skill by name
|
|
31
|
+
cortex hooks list Show hook tool preferences
|
|
32
|
+
cortex hooks enable <tool> Enable hooks for one tool
|
|
33
|
+
cortex hooks disable <tool> Disable hooks for one tool
|
|
28
34
|
cortex status Health, active project, stats
|
|
29
35
|
cortex search <query> [--project <n>] [--type <t>] [--limit <n>]
|
|
30
36
|
Search your cortex
|
|
@@ -206,6 +212,8 @@ const CLI_COMMANDS = [
|
|
|
206
212
|
"review-ui",
|
|
207
213
|
"quality-feedback",
|
|
208
214
|
"skill-list",
|
|
215
|
+
"skills",
|
|
216
|
+
"hooks",
|
|
209
217
|
"detect-skills",
|
|
210
218
|
"backlog",
|
|
211
219
|
"quickstart",
|
package/mcp/dist/link.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as readline from "readline";
|
|
|
5
5
|
import * as yaml from "js-yaml";
|
|
6
6
|
import { execFileSync } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
-
import { configureClaude, configureCodexMcp, configureCopilotMcp, configureCursorMcp, configureVSCode, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, logMcpTargetStatus, setMcpEnabledPreference, } from "./init.js";
|
|
8
|
+
import { configureClaude, configureCodexMcp, configureCopilotMcp, configureCursorMcp, configureVSCode, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, logMcpTargetStatus, patchJsonFile, setMcpEnabledPreference, } from "./init.js";
|
|
9
9
|
import { configureAllHooks, detectInstalledTools } from "./hooks.js";
|
|
10
10
|
import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, } from "./shared.js";
|
|
11
11
|
import { linkSkillsDir, writeSkillMd } from "./link-skills.js";
|
|
@@ -124,6 +124,18 @@ function currentPackageVersion() {
|
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
function readProjectConfig(cortexPath, project) {
|
|
128
|
+
const configPath = path.join(cortexPath, project, "cortex.project.yaml");
|
|
129
|
+
if (!fs.existsSync(configPath))
|
|
130
|
+
return {};
|
|
131
|
+
try {
|
|
132
|
+
const parsed = yaml.load(fs.readFileSync(configPath, "utf8"), { schema: yaml.CORE_SCHEMA });
|
|
133
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
127
139
|
function maybeOfferStarterTemplateUpdate(cortexPath) {
|
|
128
140
|
const current = currentPackageVersion();
|
|
129
141
|
if (!current)
|
|
@@ -318,10 +330,49 @@ function linkProject(cortexPath, project, tools) {
|
|
|
318
330
|
}
|
|
319
331
|
// Project-level skills
|
|
320
332
|
const projectSkills = path.join(cortexPath, project, ".claude", "skills");
|
|
321
|
-
|
|
333
|
+
const config = readProjectConfig(cortexPath, project);
|
|
334
|
+
if (config.skills !== false && fs.existsSync(projectSkills)) {
|
|
322
335
|
const targetSkills = path.join(target, ".claude", "skills");
|
|
323
336
|
linkSkillsDir(projectSkills, targetSkills, cortexPath, symlinkFile);
|
|
324
337
|
}
|
|
338
|
+
// Per-project MCP servers
|
|
339
|
+
if (config.mcpServers && typeof config.mcpServers === "object") {
|
|
340
|
+
linkProjectMcpServers(project, config.mcpServers);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Merge per-project MCP servers into Claude's settings.json.
|
|
345
|
+
* Keys are namespaced as "cortex__<project>__<name>" so we can identify
|
|
346
|
+
* and clean them up without touching user-managed servers.
|
|
347
|
+
*/
|
|
348
|
+
function linkProjectMcpServers(project, servers) {
|
|
349
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
350
|
+
if (!fs.existsSync(settingsPath) && Object.keys(servers).length === 0)
|
|
351
|
+
return;
|
|
352
|
+
try {
|
|
353
|
+
patchJsonFile(settingsPath, (data) => {
|
|
354
|
+
if (!data.mcpServers || typeof data.mcpServers !== "object")
|
|
355
|
+
data.mcpServers = {};
|
|
356
|
+
// Remove stale entries for this project (keys we previously wrote)
|
|
357
|
+
for (const key of Object.keys(data.mcpServers)) {
|
|
358
|
+
if (key.startsWith(`cortex__${project}__`))
|
|
359
|
+
delete data.mcpServers[key];
|
|
360
|
+
}
|
|
361
|
+
// Add current entries
|
|
362
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
363
|
+
const key = `cortex__${project}__${name}`;
|
|
364
|
+
const server = { command: entry.command };
|
|
365
|
+
if (Array.isArray(entry.args))
|
|
366
|
+
server.args = entry.args;
|
|
367
|
+
if (entry.env && typeof entry.env === "object")
|
|
368
|
+
server.env = entry.env;
|
|
369
|
+
data.mcpServers[key] = server;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
debugLog(`linkProjectMcpServers: failed for ${project}: ${err instanceof Error ? err.message : String(err)}`);
|
|
375
|
+
}
|
|
325
376
|
}
|
|
326
377
|
// ── Main orchestrator ───────────────────────────────────────────────────────
|
|
327
378
|
export async function runLink(cortexPath, opts = {}) {
|
package/mcp/dist/shell-input.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as path from "path";
|
|
|
8
8
|
import { addBacklogItem, addFinding, addProjectToProfile, approveQueueItem, completeBacklogItem, editQueueItem, listProjectCards, pinBacklogItem, readBacklog, readFindings, readReviewQueue, rejectQueueItem, removeFinding, removeProjectFromProfile, resetShellState, saveShellState, setMachineProfile, tidyBacklogDone, unpinBacklogItem, updateBacklogItem, workNextBacklogItem, loadShellState, } from "./data-access.js";
|
|
9
9
|
import { style } from "./shell-render.js";
|
|
10
10
|
import { SUB_VIEWS, TAB_ICONS } from "./shell-types.js";
|
|
11
|
+
import { getProjectSkills, getHookEntries, writeInstallPreferences } from "./shell-view.js";
|
|
11
12
|
import { resultMsg, editDistance, tokenize, expandIds, normalizeSection, resolveEntryScript, backlogsByFilter, queueByFilter, } from "./shell-palette.js";
|
|
12
13
|
export async function executePalette(host, input) {
|
|
13
14
|
const trimmed = input.trim();
|
|
@@ -600,6 +601,14 @@ export function getListItems(cortexPath, profile, state, healthLineCount) {
|
|
|
600
601
|
return [];
|
|
601
602
|
return state.filter ? queueByFilter(result.data, state.filter) : result.data;
|
|
602
603
|
}
|
|
604
|
+
case "Skills": {
|
|
605
|
+
if (!state.project)
|
|
606
|
+
return [];
|
|
607
|
+
return getProjectSkills(cortexPath, state.project).map((s) => ({ name: s.name, text: s.path }));
|
|
608
|
+
}
|
|
609
|
+
case "Hooks": {
|
|
610
|
+
return getHookEntries(cortexPath).map((e) => ({ name: e.tool, text: e.enabled ? "enabled" : "disabled" }));
|
|
611
|
+
}
|
|
603
612
|
case "Health":
|
|
604
613
|
return Array.from({ length: Math.max(1, healthLineCount) }, (_, i) => ({ id: String(i) }));
|
|
605
614
|
default:
|
|
@@ -644,7 +653,17 @@ export async function activateSelected(host) {
|
|
|
644
653
|
break;
|
|
645
654
|
case "Review Queue":
|
|
646
655
|
if (item.text) {
|
|
647
|
-
host.setMessage(` ${style.dim(item.id ?? "")} ${item.text} ${style.dim("[ a approve ·
|
|
656
|
+
host.setMessage(` ${style.dim(item.id ?? "")} ${item.text} ${style.dim("[ a approve · d reject ]")}`);
|
|
657
|
+
}
|
|
658
|
+
break;
|
|
659
|
+
case "Skills":
|
|
660
|
+
if (item.name) {
|
|
661
|
+
host.setMessage(` ${style.bold(item.name)} ${style.dim(item.text ?? "")}`);
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
case "Hooks":
|
|
665
|
+
if (item.name) {
|
|
666
|
+
host.setMessage(` ${item.text === "enabled" ? style.boldGreen("enabled") : style.dim("disabled")} ${style.bold(item.name)}`);
|
|
648
667
|
}
|
|
649
668
|
break;
|
|
650
669
|
}
|
|
@@ -705,7 +724,7 @@ export async function doViewAction(host, key) {
|
|
|
705
724
|
host.setCursor(Math.max(0, cursor - 1));
|
|
706
725
|
});
|
|
707
726
|
}
|
|
708
|
-
else if (key === "
|
|
727
|
+
else if (key === "d" && item?.id) {
|
|
709
728
|
if (!project) {
|
|
710
729
|
host.setMessage("Select a project first.");
|
|
711
730
|
return;
|
|
@@ -720,6 +739,36 @@ export async function doViewAction(host, key) {
|
|
|
720
739
|
host.startInput("mq-edit", item.text || "");
|
|
721
740
|
}
|
|
722
741
|
break;
|
|
742
|
+
case "Skills":
|
|
743
|
+
if ((key === "d" || key === "\x7f") && item?.name) {
|
|
744
|
+
if (!project) {
|
|
745
|
+
host.setMessage("Select a project first.");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const skillPath = item.text;
|
|
749
|
+
host.confirmThen(`Remove skill "${item.name}"?`, () => {
|
|
750
|
+
try {
|
|
751
|
+
fs.unlinkSync(skillPath);
|
|
752
|
+
host.setMessage(` Removed ${item.name}`);
|
|
753
|
+
host.setCursor(Math.max(0, cursor - 1));
|
|
754
|
+
}
|
|
755
|
+
catch (err) {
|
|
756
|
+
host.setMessage(` Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
else if (key === "a") {
|
|
761
|
+
host.setMessage(` Use "cortex skills add ${project ?? "<project>"} <path>" to add a skill`);
|
|
762
|
+
}
|
|
763
|
+
break;
|
|
764
|
+
case "Hooks":
|
|
765
|
+
if ((key === "a" || key === "d") && item?.name) {
|
|
766
|
+
const enable = key === "a";
|
|
767
|
+
const prefs = { hookTools: { ...((getHookEntries(host.cortexPath).reduce((acc, e) => ({ ...acc, [e.tool]: e.enabled }), {}))), [item.name]: enable } };
|
|
768
|
+
writeInstallPreferences(host.cortexPath, prefs);
|
|
769
|
+
host.setMessage(` ${enable ? style.boldGreen("Enabled") : style.dim("Disabled")} hooks for ${item.name}`);
|
|
770
|
+
}
|
|
771
|
+
break;
|
|
723
772
|
}
|
|
724
773
|
}
|
|
725
774
|
// ── Cursor position display ───────────────────────────────────────────────────
|
|
@@ -862,6 +911,20 @@ export async function handleNavigateKey(host, key) {
|
|
|
862
911
|
host.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
|
|
863
912
|
return true;
|
|
864
913
|
}
|
|
914
|
+
if (key === "s") {
|
|
915
|
+
if (!host.state.project) {
|
|
916
|
+
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
host.setView("Skills");
|
|
920
|
+
host.setMessage(` ${TAB_ICONS.Skills} Skills`);
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
if (key === "k") {
|
|
924
|
+
host.setView("Hooks");
|
|
925
|
+
host.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
865
928
|
if (key === "h") {
|
|
866
929
|
host.prevHealthView = host.state.view === "Health" ? host.prevHealthView : host.state.view;
|
|
867
930
|
host.healthCache = undefined;
|
|
@@ -869,7 +932,7 @@ export async function handleNavigateKey(host, key) {
|
|
|
869
932
|
host.setMessage(` ${TAB_ICONS.Health} Health ${style.dim("(esc to return)")}`);
|
|
870
933
|
return true;
|
|
871
934
|
}
|
|
872
|
-
if (["a", "d", "
|
|
935
|
+
if (["a", "d", "e", "\x7f"].includes(key)) {
|
|
873
936
|
await doViewAction(host, key);
|
|
874
937
|
return true;
|
|
875
938
|
}
|
package/mcp/dist/shell-types.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Projects is level 0 (the home screen); these sub-views are level 1 (drill-down into a project)
|
|
2
2
|
// Health is NOT a sub-view — it's a global overlay accessible from anywhere via [h]
|
|
3
|
-
export const SUB_VIEWS = ["Backlog", "Findings", "Review Queue"];
|
|
3
|
+
export const SUB_VIEWS = ["Backlog", "Findings", "Review Queue", "Skills", "Hooks"];
|
|
4
4
|
export const TAB_ICONS = {
|
|
5
5
|
Projects: "◉",
|
|
6
6
|
Backlog: "▤",
|
|
7
7
|
Findings: "✦",
|
|
8
8
|
"Review Queue": "◈",
|
|
9
|
+
Skills: "◆",
|
|
10
|
+
Hooks: "⚡",
|
|
9
11
|
Health: "♡",
|
|
10
12
|
};
|
|
11
13
|
export const MAX_UNDO_STACK = 10;
|
package/mcp/dist/shell-view.js
CHANGED
|
@@ -9,6 +9,7 @@ import { RESET, style, badge, separator, stripAnsi, padToWidth, truncateLine, li
|
|
|
9
9
|
import { SUB_VIEWS, TAB_ICONS, } from "./shell-types.js";
|
|
10
10
|
import { backlogsByFilter, queueByFilter, } from "./shell-palette.js";
|
|
11
11
|
import { listMachines, listProfiles, } from "./data-access.js";
|
|
12
|
+
import { readInstallPreferences } from "./init-preferences.js";
|
|
12
13
|
// ── Tab bar ────────────────────────────────────────────────────────────────
|
|
13
14
|
export function renderTabBar(state) {
|
|
14
15
|
const cols = process.stdout.columns || 80;
|
|
@@ -56,7 +57,9 @@ export function renderBottomBar(state, navMode, inputCtx, inputBuf) {
|
|
|
56
57
|
Projects: [`${k("↵")} ${d("open project")}`],
|
|
57
58
|
Backlog: [`${k("a")} ${d("add")}`, `${k("↵")} ${d("mark done")}`, `${k("d")} ${d("toggle active")}`],
|
|
58
59
|
Findings: [`${k("a")} ${d("add")}`, `${k("d")} ${d("remove")}`],
|
|
59
|
-
"Review Queue": [`${k("a")} ${d("keep")}`, `${k("
|
|
60
|
+
"Review Queue": [`${k("a")} ${d("keep")}`, `${k("d")} ${d("discard")}`, `${k("e")} ${d("edit")}`],
|
|
61
|
+
Skills: [`${k("d")} ${d("remove")}`],
|
|
62
|
+
Hooks: [`${k("a")} ${d("enable")}`, `${k("d")} ${d("disable")}`],
|
|
60
63
|
Health: [`${k("↑↓")} ${d("scroll")}`, `${k("esc")} ${d("back")}`],
|
|
61
64
|
};
|
|
62
65
|
const extra = viewHints[state.view] ?? [];
|
|
@@ -356,6 +359,118 @@ export function renderMemoryQueueView(ctx, cursor, height) {
|
|
|
356
359
|
}
|
|
357
360
|
return vp.lines;
|
|
358
361
|
}
|
|
362
|
+
export function getProjectSkills(cortexPath, project) {
|
|
363
|
+
const dirs = [
|
|
364
|
+
path.join(cortexPath, project, "skills"),
|
|
365
|
+
path.join(cortexPath, project, ".claude", "skills"),
|
|
366
|
+
];
|
|
367
|
+
const seen = new Set();
|
|
368
|
+
const skills = [];
|
|
369
|
+
for (const dir of dirs) {
|
|
370
|
+
if (!fs.existsSync(dir))
|
|
371
|
+
continue;
|
|
372
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
373
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
374
|
+
const name = entry.name.replace(/\.md$/, "");
|
|
375
|
+
if (!seen.has(name)) {
|
|
376
|
+
seen.add(name);
|
|
377
|
+
skills.push({ name, path: path.join(dir, entry.name) });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else if (entry.isDirectory()) {
|
|
381
|
+
const skillFile = path.join(dir, entry.name, "SKILL.md");
|
|
382
|
+
if (fs.existsSync(skillFile) && !seen.has(entry.name)) {
|
|
383
|
+
seen.add(entry.name);
|
|
384
|
+
skills.push({ name: entry.name, path: skillFile });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return skills;
|
|
390
|
+
}
|
|
391
|
+
export function renderSkillsView(ctx, cursor, height) {
|
|
392
|
+
const cols = process.stdout.columns || 80;
|
|
393
|
+
const project = ctx.state.project;
|
|
394
|
+
if (!project)
|
|
395
|
+
return [style.dim(" No project selected.")];
|
|
396
|
+
const skills = getProjectSkills(ctx.cortexPath, project);
|
|
397
|
+
const filtered = ctx.state.filter
|
|
398
|
+
? skills.filter((s) => s.name.toLowerCase().includes(ctx.state.filter.toLowerCase()))
|
|
399
|
+
: skills;
|
|
400
|
+
if (!filtered.length) {
|
|
401
|
+
return [style.dim(` No skills for ${project}. Use "cortex skills add ${project} <path>" to add one.`)];
|
|
402
|
+
}
|
|
403
|
+
const allLines = [];
|
|
404
|
+
let cursorFirstLine = 0;
|
|
405
|
+
let cursorLastLine = 0;
|
|
406
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
407
|
+
const s = filtered[i];
|
|
408
|
+
const isSelected = i === cursor;
|
|
409
|
+
if (isSelected)
|
|
410
|
+
cursorFirstLine = allLines.length;
|
|
411
|
+
const isSymlink = (() => { try {
|
|
412
|
+
return fs.lstatSync(s.path).isSymbolicLink();
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return false;
|
|
416
|
+
} })();
|
|
417
|
+
const linkTag = isSymlink ? style.dim(" →") : "";
|
|
418
|
+
let row = ` ${style.dim((i + 1).toString().padEnd(3))} ${style.bold(s.name)}${linkTag}`;
|
|
419
|
+
if (isSelected)
|
|
420
|
+
row = `\x1b[7m${padToWidth(row, cols)}${RESET}`;
|
|
421
|
+
else
|
|
422
|
+
row = truncateLine(row, cols);
|
|
423
|
+
allLines.push(row);
|
|
424
|
+
if (isSelected)
|
|
425
|
+
cursorLastLine = allLines.length - 1;
|
|
426
|
+
}
|
|
427
|
+
const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
|
|
428
|
+
const vp = lineViewport(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll());
|
|
429
|
+
ctx.setScroll(vp.scrollStart);
|
|
430
|
+
if (allLines.length > usableHeight) {
|
|
431
|
+
const pct = filtered.length <= 1 ? 100 : Math.round((cursor / (filtered.length - 1)) * 100);
|
|
432
|
+
vp.lines.push(style.dim(` ─── ${cursor + 1}/${filtered.length} ${pct}%`));
|
|
433
|
+
}
|
|
434
|
+
return vp.lines;
|
|
435
|
+
}
|
|
436
|
+
// ── Hooks view ─────────────────────────────────────────────────────────────
|
|
437
|
+
const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
|
|
438
|
+
export function getHookEntries(cortexPath) {
|
|
439
|
+
const prefs = readInstallPreferences(cortexPath);
|
|
440
|
+
const hooksEnabled = prefs.hooksEnabled !== false;
|
|
441
|
+
const toolPrefs = (prefs.hookTools && typeof prefs.hookTools === "object") ? prefs.hookTools : {};
|
|
442
|
+
return HOOK_TOOLS.map((tool) => ({
|
|
443
|
+
tool,
|
|
444
|
+
enabled: hooksEnabled && toolPrefs[tool] !== false,
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
export function renderHooksView(ctx, cursor, height) {
|
|
448
|
+
const cols = process.stdout.columns || 80;
|
|
449
|
+
const entries = getHookEntries(ctx.cortexPath);
|
|
450
|
+
const allLines = [];
|
|
451
|
+
let cursorFirstLine = 0;
|
|
452
|
+
let cursorLastLine = 0;
|
|
453
|
+
for (let i = 0; i < entries.length; i++) {
|
|
454
|
+
const e = entries[i];
|
|
455
|
+
const isSelected = i === cursor;
|
|
456
|
+
if (isSelected)
|
|
457
|
+
cursorFirstLine = allLines.length;
|
|
458
|
+
const statusBadge = e.enabled ? style.boldGreen("enabled ") : style.dim("disabled");
|
|
459
|
+
let row = ` ${style.dim((i + 1).toString().padEnd(3))} ${statusBadge} ${style.bold(e.tool)}`;
|
|
460
|
+
if (isSelected)
|
|
461
|
+
row = `\x1b[7m${padToWidth(row, cols)}${RESET}`;
|
|
462
|
+
else
|
|
463
|
+
row = truncateLine(row, cols);
|
|
464
|
+
allLines.push(row);
|
|
465
|
+
if (isSelected)
|
|
466
|
+
cursorLastLine = allLines.length - 1;
|
|
467
|
+
}
|
|
468
|
+
const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
|
|
469
|
+
const vp = lineViewport(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll());
|
|
470
|
+
ctx.setScroll(vp.scrollStart);
|
|
471
|
+
return vp.lines;
|
|
472
|
+
}
|
|
473
|
+
export { writeInstallPreferences } from "./init-preferences.js";
|
|
359
474
|
// ── Machines/Profiles view ─────────────────────────────────────────────────
|
|
360
475
|
export function renderMachinesView(cortexPath) {
|
|
361
476
|
const machines = listMachines(cortexPath);
|
|
@@ -455,6 +570,12 @@ export async function renderShell(ctx, navMode, inputCtx, inputBuf, showHelp, me
|
|
|
455
570
|
case "Review Queue":
|
|
456
571
|
contentLines = renderMemoryQueueView(ctx, cursor, height);
|
|
457
572
|
break;
|
|
573
|
+
case "Skills":
|
|
574
|
+
contentLines = renderSkillsView(ctx, cursor, height);
|
|
575
|
+
break;
|
|
576
|
+
case "Hooks":
|
|
577
|
+
contentLines = renderHooksView(ctx, cursor, height);
|
|
578
|
+
break;
|
|
458
579
|
case "Machines/Profiles":
|
|
459
580
|
contentLines = renderMachinesView(ctx.cortexPath);
|
|
460
581
|
break;
|
package/mcp/dist/shell.js
CHANGED
|
@@ -298,6 +298,20 @@ export class CortexShell {
|
|
|
298
298
|
this.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
|
|
299
299
|
return true;
|
|
300
300
|
}
|
|
301
|
+
if (input === "s") {
|
|
302
|
+
if (!this.state.project) {
|
|
303
|
+
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
this.setView("Skills");
|
|
307
|
+
this.setMessage(` ${TAB_ICONS.Skills} Skills`);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (input === "k") {
|
|
311
|
+
this.setView("Hooks");
|
|
312
|
+
this.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
301
315
|
if (input === "h") {
|
|
302
316
|
if (!this.state.project) {
|
|
303
317
|
this.setMessage(style.dim(" Select a project first (↵)"));
|
package/mcp/dist/utils.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
1
2
|
import * as path from "path";
|
|
2
3
|
import { execFileSync } from "child_process";
|
|
4
|
+
import * as yaml from "js-yaml";
|
|
5
|
+
import { findCortexPath } from "./shared.js";
|
|
3
6
|
// ── Shared Git helper ────────────────────────────────────────────────────────
|
|
4
7
|
export function runGit(cwd, args, timeoutMs, debugLogFn) {
|
|
5
8
|
try {
|
|
@@ -266,17 +269,59 @@ export function sanitizeFts5Query(raw) {
|
|
|
266
269
|
q = q.replace(/\s+/g, " ");
|
|
267
270
|
return q.trim();
|
|
268
271
|
}
|
|
272
|
+
function parseSynonymsYaml(filePath) {
|
|
273
|
+
if (!fs.existsSync(filePath))
|
|
274
|
+
return {};
|
|
275
|
+
try {
|
|
276
|
+
const parsed = yaml.load(fs.readFileSync(filePath, "utf8"), { schema: yaml.CORE_SCHEMA });
|
|
277
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
278
|
+
return {};
|
|
279
|
+
const loaded = {};
|
|
280
|
+
for (const [rawKey, value] of Object.entries(parsed)) {
|
|
281
|
+
const key = String(rawKey).trim().toLowerCase();
|
|
282
|
+
if (!key || !Array.isArray(value))
|
|
283
|
+
continue;
|
|
284
|
+
const synonyms = value
|
|
285
|
+
.filter((item) => typeof item === "string")
|
|
286
|
+
.map((item) => item.replace(/"/g, "").trim())
|
|
287
|
+
.filter((item) => item.length > 1);
|
|
288
|
+
if (synonyms.length > 0)
|
|
289
|
+
loaded[key] = synonyms;
|
|
290
|
+
}
|
|
291
|
+
return loaded;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return {};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function loadUserSynonyms(project) {
|
|
298
|
+
const cortexPath = findCortexPath();
|
|
299
|
+
if (!cortexPath)
|
|
300
|
+
return {};
|
|
301
|
+
const globalSynonyms = parseSynonymsYaml(path.join(cortexPath, "global", "synonyms.yaml"));
|
|
302
|
+
if (!project || !isValidProjectName(project))
|
|
303
|
+
return globalSynonyms;
|
|
304
|
+
const projectSynonyms = parseSynonymsYaml(path.join(cortexPath, project, "synonyms.yaml"));
|
|
305
|
+
return {
|
|
306
|
+
...globalSynonyms,
|
|
307
|
+
...projectSynonyms,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
269
310
|
// Build a defensive FTS5 MATCH query:
|
|
270
311
|
// - sanitizes user input
|
|
271
312
|
// - extracts bigrams and treats them as quoted phrases
|
|
272
313
|
// - expands known synonyms (capped at 10 total terms)
|
|
273
314
|
// - applies AND between core terms, with synonyms as OR alternatives
|
|
274
|
-
export function buildRobustFtsQuery(raw) {
|
|
315
|
+
export function buildRobustFtsQuery(raw, project) {
|
|
275
316
|
const MAX_TOTAL_TERMS = 10;
|
|
276
317
|
const MAX_SYNONYM_GROUPS = 3;
|
|
277
318
|
const safe = sanitizeFts5Query(raw);
|
|
278
319
|
if (!safe)
|
|
279
320
|
return "";
|
|
321
|
+
const synonymsMap = {
|
|
322
|
+
...SYNONYMS,
|
|
323
|
+
...loadUserSynonyms(project),
|
|
324
|
+
};
|
|
280
325
|
const baseWords = safe.split(/\s+/).filter((t) => t.length > 1);
|
|
281
326
|
if (baseWords.length === 0)
|
|
282
327
|
return "";
|
|
@@ -286,12 +331,11 @@ export function buildRobustFtsQuery(raw) {
|
|
|
286
331
|
bigrams.push(`${baseWords[i]} ${baseWords[i + 1]}`);
|
|
287
332
|
}
|
|
288
333
|
// Determine which words are consumed by bigrams that match synonym keys
|
|
289
|
-
const lowered = safe.toLowerCase();
|
|
290
334
|
const consumedIndices = new Set();
|
|
291
335
|
const matchedBigrams = [];
|
|
292
336
|
for (let i = 0; i < bigrams.length; i++) {
|
|
293
337
|
const bg = bigrams[i].toLowerCase();
|
|
294
|
-
if (
|
|
338
|
+
if (synonymsMap[bg]) {
|
|
295
339
|
consumedIndices.add(i);
|
|
296
340
|
consumedIndices.add(i + 1);
|
|
297
341
|
matchedBigrams.push(bigrams[i]);
|
|
@@ -326,8 +370,8 @@ export function buildRobustFtsQuery(raw) {
|
|
|
326
370
|
for (const coreTerm of coreTerms) {
|
|
327
371
|
const termText = coreTerm.slice(1, -1).toLowerCase(); // strip quotes
|
|
328
372
|
const synonyms = [];
|
|
329
|
-
if (groupsExpanded < MAX_SYNONYM_GROUPS &&
|
|
330
|
-
for (const syn of
|
|
373
|
+
if (groupsExpanded < MAX_SYNONYM_GROUPS && synonymsMap[termText]) {
|
|
374
|
+
for (const syn of synonymsMap[termText]) {
|
|
331
375
|
if (totalTermCount >= MAX_TOTAL_TERMS)
|
|
332
376
|
break;
|
|
333
377
|
const cleanSyn = syn.replace(/"/g, "").trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/cortex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "Long-term memory for AI agents. Stored as markdown in a git repo you own.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,21 +12,21 @@
|
|
|
12
12
|
"skills"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
16
16
|
"@xenova/transformers": "^2.17.2",
|
|
17
|
-
"glob": "^
|
|
18
|
-
"js-yaml": "^4.1.
|
|
17
|
+
"glob": "^13.0.6",
|
|
18
|
+
"js-yaml": "^4.1.1",
|
|
19
19
|
"sql.js-fts5": "^1.4.0",
|
|
20
|
-
"zod": "^4.
|
|
20
|
+
"zod": "^4.3.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/js-yaml": "^4.0.
|
|
24
|
-
"@types/node": "^
|
|
23
|
+
"@types/js-yaml": "^4.0.9",
|
|
24
|
+
"@types/node": "^25.3.5",
|
|
25
25
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
26
26
|
"@typescript-eslint/parser": "^8.56.1",
|
|
27
|
-
"eslint": "^10.0.
|
|
28
|
-
"tsx": "^4.
|
|
29
|
-
"typescript": "^5.
|
|
27
|
+
"eslint": "^10.0.3",
|
|
28
|
+
"tsx": "^4.21.0",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
30
|
"vitest": "^4.0.18"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|