@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 +4 -3
- package/dist/learnings-identity.d.ts +36 -0
- package/dist/learnings-identity.js +49 -16
- package/dist/mcp-server.js +50 -11
- package/package.json +1 -1
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
|
-
|
|
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
|
-
- **
|
|
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
|
-
| `/
|
|
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
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
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;
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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: `
|
|
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 /
|
|
610
|
-
|
|
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, "
|
|
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.
|
|
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": {
|