@gamaze/hicortex 0.11.1 → 0.12.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.
@@ -50,6 +50,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.startServer = startServer;
52
52
  const express_1 = __importDefault(require("express"));
53
+ const node_path_1 = require("node:path");
53
54
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
54
55
  const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
55
56
  const zod_1 = require("zod");
@@ -61,6 +62,7 @@ const embedder_js_1 = require("./embedder.js");
61
62
  const storage = __importStar(require("./storage.js"));
62
63
  const graph_js_1 = require("./graph.js");
63
64
  const viz_js_1 = require("./viz.js");
65
+ const context_store_js_1 = require("./context-store.js");
64
66
  const retrieval = __importStar(require("./retrieval.js"));
65
67
  const seed_lesson_js_1 = require("./seed-lesson.js");
66
68
  const distiller_js_1 = require("./distiller.js");
@@ -76,6 +78,9 @@ let llmConfig = null;
76
78
  // immediate abort ("strict", default) or a fallback to the base model ("local").
77
79
  let distillFallbackMode = "strict";
78
80
  let stateDir = "";
81
+ // Resolved contextClients list (spec §2) — the harness names allowed to inject
82
+ // the standing context layer. Echoed by GET /context so each hook self-gates.
83
+ let contextClients = ["cc"];
79
84
  // Cache detectChunkSize results keyed by "<provider>/<model>@<baseUrl>" so we
80
85
  // probe each endpoint once per server boot rather than once per /distill request.
81
86
  const chunkSizeCache = new Map();
@@ -109,19 +114,19 @@ function createMcpServer() {
109
114
  return { content: [{ type: "text", text: `Search failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
110
115
  }
111
116
  });
112
- // -- hicortex_context --
113
- server.tool("hicortex_context", "Get recent context memories, optionally filtered by project. Useful to recall what happened recently.", {
117
+ // -- hicortex_recent --
118
+ server.tool("hicortex_recent", "Get recent memories, optionally filtered by project. Queryless recall of the latest memories by project, ranked by importance. Useful to catch up on what happened recently.", {
114
119
  project: zod_1.z.string().optional().describe("Filter by project name"),
115
120
  limit: zod_1.z.coerce.number().optional().describe("Max results (default 10)"),
116
121
  }, async ({ project, limit }) => {
117
122
  if (!db)
118
123
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
119
124
  try {
120
- const results = retrieval.searchContext(db, { project, limit });
125
+ const results = retrieval.searchRecent(db, { project, limit });
121
126
  return { content: [{ type: "text", text: formatResults(results) }] };
122
127
  }
123
128
  catch (err) {
124
- return { content: [{ type: "text", text: `Context search failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
129
+ return { content: [{ type: "text", text: `Recent recall failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
125
130
  }
126
131
  });
127
132
  // -- hicortex_ingest --
@@ -387,7 +392,7 @@ async function startServer(options = {}) {
387
392
  console.warn("╔══════════════════════════════════════════════════════════════╗");
388
393
  console.warn("║ NO LLM CONFIGURED — running in recall-only mode ║");
389
394
  console.warn("║ ║");
390
- console.warn("║ search / lessons / context: ENABLED ║");
395
+ console.warn("║ search / lessons / recent: ENABLED ║");
391
396
  console.warn("║ /distill (capture) and consolidation: DISABLED ║");
392
397
  console.warn("║ ║");
393
398
  console.warn("║ To enable capture, run: ║");
@@ -422,6 +427,15 @@ async function startServer(options = {}) {
422
427
  console.warn("[hicortex] WARNING: no authToken configured — remote connections will be rejected " +
423
428
  "(localhost still works). Run `npx @gamaze/hicortex init` to generate a token.");
424
429
  }
430
+ // Context layer (0.12): resolve which harnesses may inject the standing
431
+ // context. Warn once per boot on unknown names so typos (e.g. "herms")
432
+ // surface instead of silently dropping.
433
+ const resolvedClients = (0, context_store_js_1.resolveContextClients)(savedConfig?.contextClients);
434
+ contextClients = resolvedClients.clients;
435
+ if (resolvedClients.dropped.length > 0) {
436
+ console.warn(`[hicortex] Ignoring unknown contextClients: ${resolvedClients.dropped.join(", ")} ` +
437
+ `(known: cc, hermes, oc)`);
438
+ }
425
439
  // Express app
426
440
  const app = (0, express_1.default)();
427
441
  // Raise the body limit — whole-session denoised transcripts exceed the 100 kB default.
@@ -431,7 +445,7 @@ async function startServer(options = {}) {
431
445
  const origin = req.headers.origin;
432
446
  if (origin) {
433
447
  res.setHeader("Access-Control-Allow-Origin", origin);
434
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
448
+ res.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS");
435
449
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
436
450
  res.setHeader("Access-Control-Allow-Credentials", "true");
437
451
  res.setHeader("Vary", "Origin");
@@ -567,8 +581,8 @@ async function startServer(options = {}) {
567
581
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
568
582
  }
569
583
  });
570
- // REST /context — recent context memories, optionally filtered by project.
571
- app.get("/context", (req, res) => {
584
+ // REST /recent — recent memories, optionally filtered by project.
585
+ app.get("/recent", (req, res) => {
572
586
  if (!db) {
573
587
  res.status(503).json({ error: "Server not initialized" });
574
588
  return;
@@ -579,13 +593,51 @@ async function startServer(options = {}) {
579
593
  ? req.query.privacy.split(",").map((s) => s.trim()).filter(Boolean)
580
594
  : undefined;
581
595
  try {
582
- const results = retrieval.searchContext(db, { project, limit, privacy });
596
+ const results = retrieval.searchRecent(db, { project, limit, privacy });
583
597
  res.json({ results });
584
598
  }
585
599
  catch (err) {
586
600
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
587
601
  }
588
602
  });
603
+ // -------------------------------------------------------------------------
604
+ // REST /context — standing context layer (0.12, spec 2026-07-12).
605
+ //
606
+ // GET → { sections, updated_at, clients } read from <hicortex-home>/context/.
607
+ // PUT → partial upsert of named sections (allowlisted names, atomic).
608
+ //
609
+ // This is NOT recall. The recall endpoint that previously held this name is
610
+ // now /recent (§Naming). Stale-client tripwire: old recall callers always
611
+ // send project/limit/privacy query params; context-layer callers never do —
612
+ // so those params on GET /context return a loud, self-explaining 400 instead
613
+ // of silently degrading recall to an empty {sections} response.
614
+ //
615
+ // Auth is the standard model (bearer; localhost bypass) via the shared
616
+ // middleware — no special-casing here.
617
+ // -------------------------------------------------------------------------
618
+ // Thin adapters: all logic (tripwire, validation, allowlist, atomicity,
619
+ // symlink safety, size warn) lives in the pure handlers in context-store.ts,
620
+ // which the tests exercise directly — no mirror-app drift.
621
+ app.get("/context", (req, res) => {
622
+ try {
623
+ const r = (0, context_store_js_1.handleContextGet)((0, node_path_1.join)(stateDir, "context"), contextClients, req.query);
624
+ res.status(r.status).json(r.body);
625
+ }
626
+ catch (err) {
627
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
628
+ }
629
+ });
630
+ app.put("/context", (req, res) => {
631
+ try {
632
+ const r = (0, context_store_js_1.handleContextPut)((0, node_path_1.join)(stateDir, "context"), req.body);
633
+ if (r.warn)
634
+ console.warn(`[hicortex] ${r.warn}`);
635
+ res.status(r.status).json(r.body);
636
+ }
637
+ catch (err) {
638
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
639
+ }
640
+ });
589
641
  // REST /distill — canonical capture endpoint (0.9.0+).
590
642
  // Every machine (including the server itself) POSTs denoised session text here.
591
643
  // The server distills, embeds, stores. Body limit: 25 MB (raised at app init).
@@ -916,6 +968,16 @@ async function startServer(options = {}) {
916
968
  // (static third-party code from the npm tarball, no data) — the exemption
917
969
  // lives in createAuthMiddleware next to the /viz one.
918
970
  app.get("/viz/vendor/:file", (0, viz_js_1.vizVendorHandler)());
971
+ // GET /context/ui — standing-context editor page (0.12, spec 2026-07-12 §5).
972
+ //
973
+ // The PRIMARY edit surface for the context layer. Self-contained HTML (inline
974
+ // CSS/JS, zero external requests) served from assets/context.html; builds one
975
+ // tab per section from GET /context and saves via PUT /context. The page
976
+ // SHELL is public (exempted in createAuthMiddleware, like /viz — it carries
977
+ // no data); the GET/PUT /context data calls stay bearer-only (localhost
978
+ // bypass). The page collects the token client-side: ?token= URL param
979
+ // (stripped on load) or an in-page prompt on 401, persisted in localStorage.
980
+ app.get("/context/ui", (0, viz_js_1.contextUiHandler)());
919
981
  // SSE endpoint — each connection gets its own McpServer + transport
920
982
  app.get("/sse", async (req, res) => {
921
983
  const transport = new sse_js_1.SSEServerTransport("/messages", res);
@@ -11,13 +11,14 @@
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.showNightlyStatus = showNightlyStatus;
14
+ const paths_js_1 = require("./paths.js");
14
15
  const node_fs_1 = require("node:fs");
15
16
  const node_path_1 = require("node:path");
16
17
  const node_os_1 = require("node:os");
17
18
  const node_child_process_1 = require("node:child_process");
18
19
  const db_js_1 = require("./db.js");
19
20
  const state_js_1 = require("./state.js");
20
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
21
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
21
22
  const CONFIG_PATH = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
22
23
  async function showNightlyStatus() {
23
24
  console.log("Hicortex Nightly Pipeline Status");
@@ -92,6 +93,12 @@ async function showNightlyStatus() {
92
93
  catch { /* not installed */ }
93
94
  }
94
95
  console.log(`Timer: ${timerInfo}${!timerActive ? " ⚠ Pipeline will NOT run automatically" : ""}`);
96
+ // Scheduled runs log here (launchd plist / systemd unit both append) —
97
+ // point operators at it, since the runs' output is not in journalctl.
98
+ const nightlyLogPath = (0, node_path_1.join)(HICORTEX_HOME, "nightly.log");
99
+ if ((0, node_fs_1.existsSync)(nightlyLogPath)) {
100
+ console.log(`Log: ${nightlyLogPath}`);
101
+ }
95
102
  // DB stats
96
103
  const dbPath = (0, db_js_1.resolveDbPath)();
97
104
  if ((0, node_fs_1.existsSync)(dbPath)) {
package/dist/nightly.js CHANGED
@@ -46,9 +46,9 @@ var __importStar = (this && this.__importStar) || (function () {
46
46
  })();
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.runNightly = runNightly;
49
+ const paths_js_1 = require("./paths.js");
49
50
  const node_fs_1 = require("node:fs");
50
51
  const node_path_1 = require("node:path");
51
- const node_os_1 = require("node:os");
52
52
  let VERSION = "0.0.0";
53
53
  try {
54
54
  VERSION = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8")).version;
@@ -69,7 +69,7 @@ const oc_transcript_reader_js_1 = require("./oc-transcript-reader.js");
69
69
  const features_js_1 = require("./features.js");
70
70
  const state_js_1 = require("./state.js");
71
71
  const telemetry_js_1 = require("./telemetry.js");
72
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
72
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
73
73
  function readNightlyConfig(stateDir) {
74
74
  try {
75
75
  const configPath = (0, node_path_1.join)(stateDir, "config.json");
@@ -103,10 +103,29 @@ function writeLastRun(stateDir = HICORTEX_HOME) {
103
103
  return s;
104
104
  }, stateDir);
105
105
  }
106
+ const NIGHTLY_LOG_MAX_BYTES = 1024 * 1024; // 1 MB — years of normal runs
107
+ /**
108
+ * Keep ~/.hicortex/nightly.log bounded. The launchd plist and systemd unit
109
+ * both append to it forever with no rotation, and the typical volatile-journal
110
+ * target is a Raspberry Pi on a small SD card. Copy-then-truncate (not rename)
111
+ * because the process's own stdout may hold an O_APPEND fd on this very file —
112
+ * truncation keeps that fd valid and subsequent writes land at the new end.
113
+ */
114
+ function rotateNightlyLog(stateDir = HICORTEX_HOME) {
115
+ const logPath = (0, node_path_1.join)(stateDir, "nightly.log");
116
+ try {
117
+ if ((0, node_fs_1.statSync)(logPath).size <= NIGHTLY_LOG_MAX_BYTES)
118
+ return;
119
+ (0, node_fs_1.copyFileSync)(logPath, `${logPath}.old`);
120
+ (0, node_fs_1.truncateSync)(logPath);
121
+ }
122
+ catch { /* no log file, or unreadable — nothing to rotate */ }
123
+ }
106
124
  async function runNightly(options = {}) {
107
125
  const dryRun = options.dryRun ?? false;
108
126
  const captureOnly = options.captureOnly ?? false;
109
127
  const stateDir = options.stateDir ?? HICORTEX_HOME;
128
+ rotateNightlyLog(stateDir);
110
129
  // One-time migration of legacy state files (no-op if state.json exists)
111
130
  (0, state_js_1.migrateLegacyState)(stateDir);
112
131
  // Check mode: client or server
@@ -0,0 +1 @@
1
+ export declare function hicortexHome(): string;
package/dist/paths.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hicortexHome = hicortexHome;
4
+ /**
5
+ * Canonical Hicortex home resolution — the single source of truth (#174).
6
+ *
7
+ * Honors the HICORTEX_HOME env override (a headless/test seam, mirroring the
8
+ * HICORTEX_DB_PATH convention in db.ts); otherwise defaults to ~/.hicortex.
9
+ * Every module that needs the home dir routes through here, so the override
10
+ * behaves consistently across all commands instead of being honored by some
11
+ * (context-cli, lessons-context) and hardcoded away by others.
12
+ */
13
+ const node_os_1 = require("node:os");
14
+ const node_path_1 = require("node:path");
15
+ function hicortexHome() {
16
+ return process.env.HICORTEX_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
17
+ }
package/dist/relink.js CHANGED
@@ -74,14 +74,14 @@ var __importStar = (this && this.__importStar) || (function () {
74
74
  Object.defineProperty(exports, "__esModule", { value: true });
75
75
  exports.getStoredEmbedding = getStoredEmbedding;
76
76
  exports.runRelink = runRelink;
77
+ const paths_js_1 = require("./paths.js");
77
78
  const node_fs_1 = require("node:fs");
78
79
  const node_path_1 = require("node:path");
79
- const node_os_1 = require("node:os");
80
80
  const db_js_1 = require("./db.js");
81
81
  const storage = __importStar(require("./storage.js"));
82
82
  const consolidate_js_1 = require("./consolidate.js");
83
83
  const state_js_1 = require("./state.js");
84
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
84
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
85
85
  function readConfig(stateDir) {
86
86
  try {
87
87
  return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(stateDir, "config.json"), "utf-8"));
@@ -55,7 +55,7 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
55
55
  /**
56
56
  * Get recent context, optionally filtered by project and privacy.
57
57
  */
58
- export declare function searchContext(db: Database.Database, options?: {
58
+ export declare function searchRecent(db: Database.Database, options?: {
59
59
  project?: string | null;
60
60
  limit?: number;
61
61
  privacy?: string[];
package/dist/retrieval.js CHANGED
@@ -52,7 +52,7 @@ exports.l2ToCosine = l2ToCosine;
52
52
  exports.effectiveStrength = effectiveStrength;
53
53
  exports.computeScore = computeScore;
54
54
  exports.retrieve = retrieve;
55
- exports.searchContext = searchContext;
55
+ exports.searchRecent = searchRecent;
56
56
  const storage = __importStar(require("./storage.js"));
57
57
  const BASE_DECAY = 0.0005;
58
58
  /**
@@ -315,7 +315,7 @@ async function retrieve(db, embedFn, query, options) {
315
315
  /**
316
316
  * Get recent context, optionally filtered by project and privacy.
317
317
  */
318
- function searchContext(db, options) {
318
+ function searchRecent(db, options) {
319
319
  const limit = options?.limit ?? 10;
320
320
  const project = options?.project;
321
321
  const privacy = options?.privacy;
package/dist/state.js CHANGED
@@ -23,10 +23,10 @@ exports.saveState = saveState;
23
23
  exports.updateState = updateState;
24
24
  exports.migrateLegacyState = migrateLegacyState;
25
25
  exports.describeLastNightly = describeLastNightly;
26
+ const paths_js_1 = require("./paths.js");
26
27
  const node_fs_1 = require("node:fs");
27
28
  const node_path_1 = require("node:path");
28
- const node_os_1 = require("node:os");
29
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
29
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
30
30
  const STATE_FILE = "state.json";
31
31
  /**
32
32
  * Load the state file. Returns an empty state if the file is missing
package/dist/status.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runStatus = runStatus;
7
+ const paths_js_1 = require("./paths.js");
7
8
  const node_fs_1 = require("node:fs");
8
9
  const node_path_1 = require("node:path");
9
10
  const node_os_1 = require("node:os");
@@ -11,7 +12,7 @@ const node_child_process_1 = require("node:child_process");
11
12
  const db_js_1 = require("./db.js");
12
13
  const features_js_1 = require("./features.js");
13
14
  const state_js_1 = require("./state.js");
14
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
15
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
15
16
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
16
17
  const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
17
18
  async function runStatus() {
package/dist/uninstall.js CHANGED
@@ -5,13 +5,14 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.runUninstall = runUninstall;
8
+ const paths_js_1 = require("./paths.js");
8
9
  const node_fs_1 = require("node:fs");
9
10
  const node_path_1 = require("node:path");
10
11
  const node_os_1 = require("node:os");
11
12
  const node_child_process_1 = require("node:child_process");
12
13
  const node_readline_1 = require("node:readline");
13
14
  const claude_md_js_1 = require("./claude-md.js");
14
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
15
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
15
16
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
16
17
  const CC_COMMANDS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "commands");
17
18
  const CLAUDE_MD = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
@@ -32,29 +33,45 @@ async function runUninstall() {
32
33
  return;
33
34
  }
34
35
  console.log();
35
- // 1. Stop and remove daemon
36
+ // 1. Stop and remove daemon + nightly timer (both units, or the timer
37
+ // keeps firing against a half-removed install)
36
38
  const os = (0, node_os_1.platform)();
37
39
  if (os === "darwin") {
38
- const plistPath = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents", "com.gamaze.hicortex.plist");
39
- if ((0, node_fs_1.existsSync)(plistPath)) {
40
- try {
41
- (0, node_child_process_1.execSync)(`launchctl unload ${plistPath} 2>/dev/null`);
40
+ for (const name of ["com.gamaze.hicortex.plist", "com.gamaze.hicortex-nightly.plist"]) {
41
+ const plistPath = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents", name);
42
+ if ((0, node_fs_1.existsSync)(plistPath)) {
43
+ try {
44
+ (0, node_child_process_1.execSync)(`launchctl unload ${plistPath} 2>/dev/null`);
45
+ }
46
+ catch { /* not loaded */ }
47
+ (0, node_fs_1.unlinkSync)(plistPath);
48
+ console.log(` ✓ Removed ${name}`);
42
49
  }
43
- catch { /* not loaded */ }
44
- (0, node_fs_1.unlinkSync)(plistPath);
45
- console.log(" ✓ Removed launchd daemon");
46
50
  }
47
51
  }
48
52
  else if (os === "linux") {
49
53
  try {
50
54
  (0, node_child_process_1.execSync)("systemctl --user disable --now hicortex.service 2>/dev/null");
51
- const servicePath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user", "hicortex.service");
52
- if ((0, node_fs_1.existsSync)(servicePath))
53
- (0, node_fs_1.unlinkSync)(servicePath);
54
- (0, node_child_process_1.execSync)("systemctl --user daemon-reload 2>/dev/null");
55
- console.log(" ✓ Removed systemd service");
56
55
  }
57
56
  catch { /* not installed */ }
57
+ try {
58
+ (0, node_child_process_1.execSync)("systemctl --user disable --now hicortex-nightly.timer 2>/dev/null");
59
+ }
60
+ catch { /* not installed */ }
61
+ const unitDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user");
62
+ for (const name of ["hicortex.service", "hicortex-nightly.timer", "hicortex-nightly.service"]) {
63
+ const unitPath = (0, node_path_1.join)(unitDir, name);
64
+ try {
65
+ if ((0, node_fs_1.existsSync)(unitPath))
66
+ (0, node_fs_1.unlinkSync)(unitPath);
67
+ }
68
+ catch { /* leave it */ }
69
+ }
70
+ try {
71
+ (0, node_child_process_1.execSync)("systemctl --user daemon-reload 2>/dev/null");
72
+ }
73
+ catch { /* fine */ }
74
+ console.log(" ✓ Removed systemd service + nightly timer");
58
75
  }
59
76
  // 2. Remove MCP from CC
60
77
  try {
package/dist/viz.d.ts CHANGED
@@ -51,6 +51,21 @@ export declare function readVizHtml(): string;
51
51
  * when the asset cannot be read.
52
52
  */
53
53
  export declare function vizHandler(): express.RequestHandler;
54
+ /**
55
+ * Resolve the on-disk path of the context-layer editor page. Throws (fail
56
+ * explicitly) when the asset is missing — same contract as resolveVizHtmlPath.
57
+ * assets/ sits next to both dist/ (dist/viz.js → ../assets/) and src/
58
+ * (src/viz.ts → ../assets/ under tsx), so one sibling candidate covers both.
59
+ */
60
+ export declare function resolveContextHtmlPath(): string;
61
+ /** Read the context editor page. Read at request time so a reinstall is live. */
62
+ export declare function readContextHtml(): string;
63
+ /**
64
+ * Express handler for GET /context/ui — the PRIMARY edit surface for the
65
+ * standing context layer. 503 with the usual {error} shape when the asset
66
+ * cannot be read, exactly like vizHandler.
67
+ */
68
+ export declare function contextUiHandler(): express.RequestHandler;
54
69
  /**
55
70
  * Resolve the on-disk path of an allowlisted vendor bundle, or null when the
56
71
  * requested name is not on the allowlist. The filesystem path is built ONLY
package/dist/viz.js CHANGED
@@ -31,6 +31,9 @@ exports.createAuthMiddleware = createAuthMiddleware;
31
31
  exports.resolveVizHtmlPath = resolveVizHtmlPath;
32
32
  exports.readVizHtml = readVizHtml;
33
33
  exports.vizHandler = vizHandler;
34
+ exports.resolveContextHtmlPath = resolveContextHtmlPath;
35
+ exports.readContextHtml = readContextHtml;
36
+ exports.contextUiHandler = contextUiHandler;
34
37
  exports.resolveVizVendorPath = resolveVizVendorPath;
35
38
  exports.vizVendorHandler = vizVendorHandler;
36
39
  const node_fs_1 = require("node:fs");
@@ -70,6 +73,14 @@ function createAuthMiddleware(authToken) {
70
73
  // normal Authorization header on its data fetches.
71
74
  if (req.method === "GET" && req.path === "/viz")
72
75
  return next();
76
+ // The /context/ui page SHELL is public for the same reason as /viz: a
77
+ // self-contained static editor page, no data and no secrets (it ships
78
+ // verbatim in the npm tarball). The standing-context DATA it edits comes
79
+ // from GET/PUT /context, which stay bearer-only (localhost bypass) like
80
+ // every other data route; the page collects the token client-side and
81
+ // sends it as a normal Authorization header on its /context fetches.
82
+ if (req.method === "GET" && req.path === "/context/ui")
83
+ return next();
73
84
  // The pinned renderer bundles the /viz page loads (#139) are public for
74
85
  // the same reason as the shell: static third-party code shipped verbatim
75
86
  // in the npm tarball, zero data. Kept tight: GET only, and ONLY names on
@@ -136,6 +147,43 @@ function vizHandler() {
136
147
  }
137
148
  };
138
149
  }
150
+ // ---------------------------------------------------------------------------
151
+ // Context layer editor page (/context/ui, 0.12 — spec 2026-07-12 §5)
152
+ // ---------------------------------------------------------------------------
153
+ /**
154
+ * Resolve the on-disk path of the context-layer editor page. Throws (fail
155
+ * explicitly) when the asset is missing — same contract as resolveVizHtmlPath.
156
+ * assets/ sits next to both dist/ (dist/viz.js → ../assets/) and src/
157
+ * (src/viz.ts → ../assets/ under tsx), so one sibling candidate covers both.
158
+ */
159
+ function resolveContextHtmlPath() {
160
+ const candidates = [(0, node_path_1.join)(__dirname, "..", "assets", "context.html")];
161
+ for (const candidate of candidates) {
162
+ if ((0, node_fs_1.existsSync)(candidate))
163
+ return candidate;
164
+ }
165
+ throw new Error(`context.html asset not found — looked in: ${candidates.join(", ")}. ` +
166
+ `The package install is incomplete (assets/ missing).`);
167
+ }
168
+ /** Read the context editor page. Read at request time so a reinstall is live. */
169
+ function readContextHtml() {
170
+ return (0, node_fs_1.readFileSync)(resolveContextHtmlPath(), "utf-8");
171
+ }
172
+ /**
173
+ * Express handler for GET /context/ui — the PRIMARY edit surface for the
174
+ * standing context layer. 503 with the usual {error} shape when the asset
175
+ * cannot be read, exactly like vizHandler.
176
+ */
177
+ function contextUiHandler() {
178
+ return (_req, res) => {
179
+ try {
180
+ res.type("html").send(readContextHtml());
181
+ }
182
+ catch (err) {
183
+ res.status(503).json({ error: err instanceof Error ? err.message : String(err) });
184
+ }
185
+ };
186
+ }
139
187
  /**
140
188
  * Resolve the on-disk path of an allowlisted vendor bundle, or null when the
141
189
  * requested name is not on the allowlist. The filesystem path is built ONLY
@@ -15,28 +15,27 @@ Gives [Hermes](https://github.com/nousresearch/hermes-agent) agents self-learnin
15
15
  | `prefetch(query)` | recall relevant memories before each turn | `GET /search` |
16
16
  | `queue_prefetch(query)` | background recall for the next turn | `GET /search` |
17
17
  | `system_prompt_block()` | inject distilled lessons + memory index | `GET /lessons` |
18
- | `get_tool_schemas()` | exposes the 8 unified tools + `hicortex_recall_recent` | see tool table below |
18
+ | `get_tool_schemas()` | exposes the 8 unified tools | see tool table below |
19
19
 
20
20
  That's the whole surface. No `sync_turn`, no compaction/session-end capture — those are intentionally absent.
21
21
 
22
- ### Tools (unified 8 + 1 Hermes-specific)
22
+ ### Tools (unified 8)
23
23
 
24
24
  | Tool | REST call | Description |
25
25
  |---|---|---|
26
26
  | `hicortex_search` | `GET /search` | Semantic search over long-term memory |
27
- | `hicortex_context` | `GET /context` | Recent context memories by project |
27
+ | `hicortex_recent` | `GET /recent` | Recent memories by project (queryless recall; was `hicortex_context`/`hicortex_recall_recent` before 0.12) |
28
28
  | `hicortex_ingest` | `POST /ingest` | Store a new memory |
29
29
  | `hicortex_lessons` | `GET /lessons` | Get distilled lessons |
30
30
  | `hicortex_index` | `GET /index` | Knowledge domain index |
31
31
  | `hicortex_graph` | `GET /graph` | Graph queries (neighbors/hubs/path) |
32
32
  | `hicortex_update` | `POST /update` | Update a memory (re-embeds on content change) |
33
33
  | `hicortex_delete` | `POST /delete` | Permanently delete a memory and its links |
34
- | `hicortex_recall_recent` | `GET /context` | Hermes-specific alias for context recall |
35
34
 
36
35
  ## Prerequisites
37
36
 
38
37
  - A reachable Hicortex server (default `http://localhost:8787`). Stand one up with `npx @gamaze/hicortex init`.
39
- - The server needs the REST `/search`, `/context`, `/lessons` endpoints (Hicortex ≥ 0.7).
38
+ - The server needs the REST `/search`, `/recent`, `/lessons` endpoints (Hicortex ≥ 0.12 — this plugin version does not talk to older servers; upgrade the server first).
40
39
 
41
40
  ## Install
42
41
 
@@ -1,7 +1,7 @@
1
1
  """Hicortex memory provider plugin for Hermes — recall-only.
2
2
 
3
3
  Recall: prefetch() -> GET /search (relevant memories before each turn)
4
- tools -> hicortex_search / hicortex_recall_recent
4
+ tools -> hicortex_search / hicortex_recent
5
5
  system_prompt_block -> lessons injected into the system prompt
6
6
 
7
7
  Capture is NOT the plugin's job. A nightly reader on the Hicortex server
@@ -83,14 +83,14 @@ class HicortexClient:
83
83
  {"query": query, "limit": limit, "project": project, "privacy": privacy},
84
84
  ).get("results", [])
85
85
 
86
- def context(
86
+ def recent(
87
87
  self,
88
88
  project: Optional[str] = None,
89
89
  limit: int = 10,
90
90
  privacy: Optional[str] = None,
91
91
  ) -> list[dict]:
92
92
  return self._get(
93
- "/context", {"project": project, "limit": limit, "privacy": privacy}
93
+ "/recent", {"project": project, "limit": limit, "privacy": privacy}
94
94
  ).get("results", [])
95
95
 
96
96
  def lessons(self) -> dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  name: hicortex
2
- version: 0.4.0
3
- description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Injects fresh lessons each turn and exposes the full 8-tool memory surface (search, context, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only."
2
+ version: 0.5.0
3
+ description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Injects fresh lessons each turn and exposes the full 8-tool memory surface (search, recent, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only."
4
4
  pip_dependencies: []
5
5
  hooks: []
6
6
  requires_env:
@@ -2,7 +2,7 @@
2
2
 
3
3
  Recall: prefetch() -> GET /search (relevant memories before each turn)
4
4
  queue_prefetch() -> GET /search (background recall for the next turn)
5
- tools -> hicortex_search / hicortex_recall_recent
5
+ tools -> hicortex_search / hicortex_recent
6
6
  system_prompt_block -> lessons + memory index injected into the prompt
7
7
 
8
8
  Capture is NOT the plugin's job. A nightly reader on the Hicortex server
@@ -152,7 +152,7 @@ class HicortexProvider(MemoryProvider):
152
152
  lines = [
153
153
  "## Hicortex long-term memory",
154
154
  "You have shared long-term memory across sessions. Use `hicortex_search` "
155
- "for specific recall and `hicortex_recall_recent` for recent context.",
155
+ "for specific recall and `hicortex_recent` for recent memories by project.",
156
156
  ]
157
157
  if lessons:
158
158
  lines.append("Lessons:")
@@ -188,24 +188,11 @@ class HicortexProvider(MemoryProvider):
188
188
  },
189
189
  },
190
190
  {
191
- "name": "hicortex_recall_recent",
192
- "description": "Recall recent context memories, optionally filtered by project.",
193
- "parameters": {
194
- "type": "object",
195
- "properties": {
196
- "project": {"type": "string"},
197
- "limit": {
198
- "type": "number",
199
- "description": "Max results (default 10)",
200
- },
201
- },
202
- },
203
- },
204
- {
205
- "name": "hicortex_context",
191
+ "name": "hicortex_recent",
206
192
  "description": (
207
- "Get recent context memories, optionally filtered by project. "
208
- "Useful to recall what happened recently."
193
+ "Get recent memories, optionally filtered by project. Queryless recall "
194
+ "of the latest memories by project, ranked by importance. Useful to "
195
+ "catch up on what happened recently."
209
196
  ),
210
197
  "parameters": {
211
198
  "type": "object",
@@ -335,8 +322,8 @@ class HicortexProvider(MemoryProvider):
335
322
  )
336
323
  return json.dumps(hits)
337
324
 
338
- elif tool_name in ("hicortex_recall_recent", "hicortex_context"):
339
- hits = client.context(
325
+ elif tool_name == "hicortex_recent":
326
+ hits = client.recent(
340
327
  project=args.get("project") or self._project,
341
328
  limit=int(args.get("limit", 10)),
342
329
  )