@gamaze/hicortex 0.18.0 → 0.18.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 CHANGED
@@ -118,17 +118,18 @@ The run is resumable — interrupt it any time and it continues where it stopped
118
118
 
119
119
  ## Agent Tools (MCP)
120
120
 
121
- 9 tools available via MCP:
121
+ 10 canonical tools available via MCP (plus `hicortex_lessons` as a backcompat alias of `hicortex_learnings`):
122
122
 
123
123
  - **hicortex_search** — Semantic search across all stored memories
124
124
  - **hicortex_get** — Fetch one memory's full content by id (0.14) — the lazy-load counterpart of the recall index; fetching marks the memory as used
125
125
  - **hicortex_recent** — Get recent decisions and project state (queryless recall; renamed in 0.12)
126
126
  - **hicortex_ingest** — Store a memory directly
127
- - **hicortex_lessons** — Get actionable lessons from reflection
127
+ - **hicortex_learnings** — Get actionable Learnings from reflection (`hicortex_lessons` is kept as a backcompat alias)
128
128
  - **hicortex_index** — Get the knowledge domain index (what topics are stored)
129
129
  - **hicortex_graph** — Graph traversal: neighbors, hubs, shortest paths
130
130
  - **hicortex_update** — Fix incorrect memories (re-embeds on content change)
131
131
  - **hicortex_delete** — Remove memories with cascade cleanup
132
+ - **hicortex_identity** — Fetch your standing identity layer on demand (all sections, or one by name; pass `agent` on multi-agent installs to scope to a specific agent)
132
133
 
133
134
  Explicit learnings: call `hicortex_ingest` directly (capture is otherwise automatic, nightly).
134
135
 
@@ -271,7 +272,7 @@ Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze
271
272
  | `/recent` | GET | Yes | Recent memories, queryless recall (renamed from `/context` in 0.12) |
272
273
  | `/identity` | GET / PUT | Yes | Standing [identity layer](#identity-layer): read all sections / partial-upsert named sections. `?agent=<id>` selects a [per-agent scope](#per-agent-identity-013) (server resolves override/global/off + merge); invalid id → 400. Recall-style query params on GET → 400 (use `/recent`). (`/context` remains as a backcompat alias.) |
273
274
  | `/identity/ui` | GET | No* | Web editor for the identity layer (shell served without auth, like `/viz`; data via `/identity`) |
274
- | `/lessons` | GET | Yes | Lessons + memory index (used by CC SessionStart hook) |
275
+ | `/learnings` | GET | Yes | Learnings + memory index (used by CC SessionStart hook). `/lessons` is a backcompat alias for the same handler |
275
276
  | `/ingest` | POST | Yes | Legacy: accept a single pre-distilled memory from older clients |
276
277
  | `/sse` | GET | Yes | MCP SSE stream for agent connections |
277
278
  | `/messages` | POST | Yes | MCP message endpoint |
@@ -26,6 +26,7 @@
26
26
  * parse error) results in silent exit-0. A broken hook must never block a
27
27
  * CC session, and a broken /identity fetch must never blank the whole output.
28
28
  */
29
+ import { type AgentMode } from "./identity-store.js";
29
30
  /**
30
31
  * The GET /identity response shape, shared by the CC hook and the OC plugin so
31
32
  * their gating cannot drift. `agent`/`mode` are echoed by a 0.13 server whenever
@@ -95,6 +96,41 @@ export declare function renderIdentityBlock(sections: Record<string, string>): s
95
96
  * global IS the operator's intended state there, and a guard would instead
96
97
  * blank ALL identity for every CC session in that window.
97
98
  */
99
+ /**
100
+ * Result of `buildIdentityToolResult` — the MCP tool handler maps this to its
101
+ * `{content:[{type:"text",text}],isError?}` shape. Pure value: no MCP SDK
102
+ * types leak here so the function is unit-testable with no harness.
103
+ */
104
+ export interface IdentityToolResult {
105
+ text: string;
106
+ isError?: boolean;
107
+ }
108
+ /**
109
+ * Build the `hicortex_identity` MCP tool result from the SAME pure pipeline the
110
+ * REST `GET /identity` route and the SessionStart hook use (#264 CRITICAL fix:
111
+ * previously the tool was a closure inside `createMcpServer()` that tests
112
+ * re-implemented locally, so the production path was never exercised).
113
+ *
114
+ * Pipeline (kept identical to REST + hook by CONSTRUCTION, not by mirroring):
115
+ * 1. `handleIdentityGet` — the real GET /identity handler, with the optional
116
+ * `agent` param forwarded so per-agent installs resolve the right scope
117
+ * (WARNING-2: previously the tool always passed `{}` → global, so an agent
118
+ * with an override saw the wrong identity).
119
+ * 2. `injectMemorySection` — the synthetic product-owned `memory` section
120
+ * (WARNING-1: the REST route + SessionStart hook inject it; the tool did
121
+ * not, contradicting its "same data" docs).
122
+ * 3. optional `name` filter, then `renderIdentityBlock` for the `### <Title>`
123
+ * markdown the hook injects.
124
+ *
125
+ * Pure: no I/O of its own (the only I/O is `handleIdentityGet` reading the
126
+ * identity dir, which is the same I/O the REST route does). Takes the resolved
127
+ * `identityClients` / `identityAgents` the daemon already holds at boot.
128
+ */
129
+ export declare function buildIdentityToolResult(identityDir: string, identityClients: string[], identityAgents: Record<string, AgentMode>, opts: {
130
+ name?: string;
131
+ agent?: string;
132
+ memoryInstructionsEnabled: boolean;
133
+ }): IdentityToolResult;
98
134
  export declare function gateAndRenderIdentity(data: IdentityResponse, harness: string, opts: {
99
135
  requireAgentEcho: boolean;
100
136
  }): string | null;
@@ -33,11 +33,13 @@ exports.resolveConfig = resolveConfig;
33
33
  exports.titleCaseSection = titleCaseSection;
34
34
  exports.orderSectionNames = orderSectionNames;
35
35
  exports.renderIdentityBlock = renderIdentityBlock;
36
+ exports.buildIdentityToolResult = buildIdentityToolResult;
36
37
  exports.gateAndRenderIdentity = gateAndRenderIdentity;
37
38
  exports.fetchLessonsIdentity = fetchLessonsIdentity;
38
39
  const node_fs_1 = require("node:fs");
39
40
  const node_path_1 = require("node:path");
40
41
  const identity_store_js_1 = require("./identity-store.js");
42
+ const memory_instructions_js_1 = require("./memory-instructions.js");
41
43
  const features_js_1 = require("./features.js");
42
44
  const extensions_js_1 = require("./extensions.js");
43
45
  const state_js_1 = require("./state.js");
@@ -183,24 +185,55 @@ function renderIdentityBlock(sections) {
183
185
  return ["## Identity", "", ...bodyParts].join("\n");
184
186
  }
185
187
  /**
186
- * Gate a GET /identity response and render the `## Identity` block, or null when
187
- * nothing should be injected: `harness` not in the server-resolved `clients`,
188
- * an empty/blank section set, or when `requireAgentEcho` a response that
189
- * does not echo `agent`. The SINGLE gate used by both CC and OC so the two can
190
- * never drift (the Python Hermes plugin `provider.py::_context_block` mirrors
191
- * this logic — keep them in sync).
188
+ * Build the `hicortex_identity` MCP tool result from the SAME pure pipeline the
189
+ * REST `GET /identity` route and the SessionStart hook use (#264 CRITICAL fix:
190
+ * previously the tool was a closure inside `createMcpServer()` that tests
191
+ * re-implemented locally, so the production path was never exercised).
192
192
  *
193
- * `requireAgentEcho` is the old-server guard, and it is the CALLER's decision:
194
- * - OC passes `agentId !== null` — when it actually sent an id, a 0.12 server
195
- * that ignores `?agent=` (200 global, no echo) must NOT leak global identity
196
- * into every persona; on a bare fetch (no id) the guard is off (amendment
197
- * A2).
198
- * - CC passes `false` ALWAYS and deliberately (see the call site): a thin CC
199
- * client auto-upgrades via npx BEFORE the server does, so during the upgrade
200
- * window it talks to a 0.12 server that cannot hold ANY per-agent config —
201
- * global IS the operator's intended state there, and a guard would instead
202
- * blank ALL identity for every CC session in that window.
193
+ * Pipeline (kept identical to REST + hook by CONSTRUCTION, not by mirroring):
194
+ * 1. `handleIdentityGet` — the real GET /identity handler, with the optional
195
+ * `agent` param forwarded so per-agent installs resolve the right scope
196
+ * (WARNING-2: previously the tool always passed `{}` global, so an agent
197
+ * with an override saw the wrong identity).
198
+ * 2. `injectMemorySection` the synthetic product-owned `memory` section
199
+ * (WARNING-1: the REST route + SessionStart hook inject it; the tool did
200
+ * not, contradicting its "same data" docs).
201
+ * 3. optional `name` filter, then `renderIdentityBlock` for the `### <Title>`
202
+ * markdown the hook injects.
203
+ *
204
+ * Pure: no I/O of its own (the only I/O is `handleIdentityGet` reading the
205
+ * identity dir, which is the same I/O the REST route does). Takes the resolved
206
+ * `identityClients` / `identityAgents` the daemon already holds at boot.
203
207
  */
208
+ function buildIdentityToolResult(identityDir, identityClients, identityAgents, opts) {
209
+ // WARNING-2: forward `agent` so per-agent installs resolve the right scope.
210
+ // An invalid id makes handleIdentityGet return a 400 → surfaced as isError.
211
+ const query = opts.agent ? { agent: opts.agent } : {};
212
+ const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, query, identityAgents);
213
+ if (r.status !== 200) {
214
+ const errBody = r.body;
215
+ return {
216
+ text: `Identity fetch failed: ${JSON.stringify(errBody.error ?? r.body)}`,
217
+ isError: true,
218
+ };
219
+ }
220
+ // WARNING-1: inject the synthetic `memory` section exactly like REST + the
221
+ // SessionStart hook. `injectMemorySection` is a no-op when disabled or when
222
+ // agent mode === "off".
223
+ (0, memory_instructions_js_1.injectMemorySection)(r.body, opts.memoryInstructionsEnabled);
224
+ const sections = r.body.sections ?? {};
225
+ const filtered = opts.name
226
+ ? (sections[opts.name] !== undefined ? { [opts.name]: sections[opts.name] } : {})
227
+ : sections;
228
+ const block = renderIdentityBlock(filtered);
229
+ if (block === null) {
230
+ const text = opts.name
231
+ ? `No identity section named '${opts.name}'.`
232
+ : "No identity sections configured.";
233
+ return { text };
234
+ }
235
+ return { text: block };
236
+ }
204
237
  function gateAndRenderIdentity(data, harness, opts) {
205
238
  if (!data || typeof data !== "object")
206
239
  return null;
@@ -73,6 +73,7 @@ const recall_index_js_1 = require("./recall-index.js");
73
73
  const type_labels_js_1 = require("./type-labels.js");
74
74
  const health_js_1 = require("./health.js");
75
75
  const seed_lesson_js_1 = require("./seed-lesson.js");
76
+ const learnings_identity_js_1 = require("./learnings-identity.js");
76
77
  const distiller_js_1 = require("./distiller.js");
77
78
  const dedup_js_1 = require("./dedup.js");
78
79
  const redact_js_1 = require("./redact.js");
@@ -260,23 +261,56 @@ function createMcpServer() {
260
261
  return { content: [{ type: "text", text: `Delete failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
261
262
  }
262
263
  });
263
- // -- hicortex_lessons --
264
- server.tool("hicortex_lessons", "Get actionable lessons learned from past sessions. Auto-generated insights about mistakes to avoid.", {
265
- days: zod_1.z.coerce.number().optional().describe("Look back N days (default 7)"),
266
- project: zod_1.z.string().optional().describe("Filter by project name"),
267
- }, async ({ days, project }) => {
264
+ // -- hicortex_learnings (canonical) + hicortex_lessons (alias) --
265
+ const learningsHandler = async ({ days, project }) => {
268
266
  if (!db)
269
267
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
270
268
  try {
271
269
  const lessons = storage.getLessons(db, days ?? 7, project);
272
270
  if (lessons.length === 0) {
273
- return { content: [{ type: "text", text: "No lessons found for the specified period." }] };
271
+ return { content: [{ type: "text", text: "No Learnings found for the specified period." }] };
274
272
  }
275
273
  const text = lessons.map((l) => `- ${l.content.slice(0, 500)}`).join("\n");
276
274
  return { content: [{ type: "text", text }] };
277
275
  }
278
276
  catch (err) {
279
- return { content: [{ type: "text", text: `Lessons fetch failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
277
+ return { content: [{ type: "text", text: `Learnings fetch failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
278
+ }
279
+ };
280
+ const learningsSchema = {
281
+ days: zod_1.z.coerce.number().optional().describe("Look back N days (default 7)"),
282
+ project: zod_1.z.string().optional().describe("Filter by project name"),
283
+ };
284
+ server.tool("hicortex_learnings", "Get actionable Learnings from past sessions. Auto-generated insights about mistakes to avoid.", learningsSchema, learningsHandler);
285
+ server.tool("hicortex_lessons", "Get actionable Learnings from past sessions. (Alias for hicortex_learnings.)", learningsSchema, learningsHandler);
286
+ // -- hicortex_identity --
287
+ // Standing identity layer on-demand (the same data GET /identity returns and
288
+ // the SessionStart hook injects). Lets an agent re-read its identity after
289
+ // context compaction, or look up one named section, mid-session. Renders the
290
+ // same `### <Title>` section markdown the hook injects (shared pipeline in
291
+ // learnings-identity.ts → buildIdentityToolResult) so the agent sees one
292
+ // consistent shape. The handler is a thin wrapper over that pure function;
293
+ // tests exercise it directly (no MCP SDK plumbing re-implemented).
294
+ server.tool("hicortex_identity", "Fetch your standing identity — the hand-edited 'who you are + how you work' layer (personality, rules, preferences). Returns all sections or a specific one. Use this to re-read your identity after context compaction or to look up a specific rule. On multi-agent installs, pass `agent` to fetch a specific agent's scoped identity; omit for the global identity.", {
295
+ name: zod_1.z.string().optional().describe("Fetch a specific identity section by name (e.g. 'rules'). Omit for all sections."),
296
+ agent: zod_1.z.string().optional().describe("Fetch a specific agent's identity scope (for per-agent installs). Omit for global."),
297
+ }, async ({ name, agent }) => {
298
+ if (!db)
299
+ return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
300
+ try {
301
+ const identityDir = (0, node_path_1.join)(stateDir, "identity");
302
+ // Single pipeline shared with REST /identity + the SessionStart hook
303
+ // (#264 CRITICAL + WARNING-1 + WARNING-2). The pure function owns
304
+ // handleIdentityGet → injectMemorySection → renderIdentityBlock.
305
+ const result = (0, learnings_identity_js_1.buildIdentityToolResult)(identityDir, identityClients, identityAgents, {
306
+ name,
307
+ agent,
308
+ memoryInstructionsEnabled,
309
+ });
310
+ return { content: [{ type: "text", text: result.text }], isError: result.isError };
311
+ }
312
+ catch (err) {
313
+ return { content: [{ type: "text", text: `Identity fetch failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
280
314
  }
281
315
  });
282
316
  // -- hicortex_index --
@@ -606,8 +640,11 @@ async function startServer(options = {}) {
606
640
  llmLabel: llmConfig ? `${llmConfig.provider}/${llmConfig.model}` : "not configured",
607
641
  }));
608
642
  });
609
- // REST /lessons return lessons + memory index for client CLAUDE.md injection
610
- app.get("/lessons", (_req, res) => {
643
+ // REST /learnings (canonical, #264) + /lessons (alias) return lessons +
644
+ // memory index for client CLAUDE.md injection. Both routes share ONE handler
645
+ // so the alias can never drift from the canonical shape. The legacy name is
646
+ // kept indefinitely (existing SessionStart hooks literally fetch /lessons).
647
+ const learningsIndexHandler = (_req, res) => {
611
648
  if (!db) {
612
649
  res.status(503).json({ error: "Server not initialized" });
613
650
  return;
@@ -639,9 +676,11 @@ async function startServer(options = {}) {
639
676
  });
640
677
  }
641
678
  catch (err) {
642
- (0, health_js_1.logAndSendInternalError)(res, "lessons", err);
679
+ (0, health_js_1.logAndSendInternalError)(res, "learnings", err);
643
680
  }
644
- });
681
+ };
682
+ app.get("/learnings", learningsIndexHandler);
683
+ app.get("/lessons", learningsIndexHandler); // #264 backcompat alias
645
684
  // REST /ingest — accept pre-distilled memories from remote clients
646
685
  app.post("/ingest", async (req, res) => {
647
686
  if (!db) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Persistent agent identity for AI agents — a hand-edited identity layer, nightly-distilled experience, and lessons injected every session, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {