@alaarab/cortex 1.15.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/link.js +39 -1
- 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/package.json +1 -1
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/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";
|
|
@@ -335,6 +335,44 @@ function linkProject(cortexPath, project, tools) {
|
|
|
335
335
|
const targetSkills = path.join(target, ".claude", "skills");
|
|
336
336
|
linkSkillsDir(projectSkills, targetSkills, cortexPath, symlinkFile);
|
|
337
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
|
+
}
|
|
338
376
|
}
|
|
339
377
|
// ── Main orchestrator ───────────────────────────────────────────────────────
|
|
340
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 (↵)"));
|