@alaarab/cortex 1.15.0 → 1.15.2

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 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, Memory Queue, and Health. No agent needed
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
- **Memory 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`.
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
- | `search_cortex` | FTS5 search with synonym expansion. Filters by project, type, limit. |
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. Five views, single-key navigation:
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` | Memory Queue |
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 # interactive shell (TTY default)
267
- cortex search "rate limiting" # FTS5 search with synonym expansion
268
- cortex add-finding <project> "..." # append a finding from the terminal
269
- cortex pin <project> "..." # promote canonical memory
270
- cortex doctor [--fix] # health checks + optional self-heal
271
- cortex review-ui [--port=3499] # lightweight review UI in the browser
272
- cortex update # update to latest version
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. Top-level aliases still work for backwards compatibility. Run `--dry-run` before destructive maintenance commands.
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 = {}) {
@@ -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 · r reject ]")}`);
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 === "r" && item?.id) {
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", "r", "e", "\x7f"].includes(key)) {
935
+ if (["a", "d", "e", "\x7f"].includes(key)) {
873
936
  await doViewAction(host, key);
874
937
  return true;
875
938
  }
@@ -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;
@@ -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("r")} ${d("discard")}`, `${k("e")} ${d("edit")}`],
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/cortex",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
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": {