@gamaze/hicortex 0.17.6 → 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 +30 -28
- package/assets/dashboard.html +121 -5
- package/assets/{context.html → identity.html} +18 -18
- package/assets/viz.html +19 -7
- package/dist/claude-md.d.ts +2 -1
- package/dist/claude-md.js +2 -1
- package/dist/cli-args.d.ts +9 -0
- package/dist/cli-args.js +16 -0
- package/dist/cli.js +29 -20
- package/dist/consolidate.d.ts +15 -0
- package/dist/consolidate.js +30 -3
- package/dist/dashboard.d.ts +58 -1
- package/dist/dashboard.js +27 -1
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/health.d.ts +68 -0
- package/dist/health.js +73 -0
- package/dist/identity-cli.d.ts +90 -0
- package/dist/{context-cli.js → identity-cli.js} +66 -48
- package/dist/{context-store.d.ts → identity-store.d.ts} +94 -31
- package/dist/{context-store.js → identity-store.js} +212 -71
- package/dist/index.d.ts +12 -5
- package/dist/index.js +57 -29
- package/dist/init.d.ts +44 -8
- package/dist/init.js +142 -37
- package/dist/learnings-identity.d.ts +149 -0
- package/dist/{lessons-context.js → learnings-identity.js} +96 -52
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +217 -68
- package/dist/memory-instructions.d.ts +6 -6
- package/dist/memory-instructions.js +6 -6
- package/dist/nightly.js +65 -6
- package/dist/paths.js +1 -1
- package/dist/recall-hook-cli.d.ts +1 -1
- package/dist/recall-hook-cli.js +3 -3
- package/dist/recall-index.js +5 -2
- package/dist/status.d.ts +2 -2
- package/dist/status.js +11 -9
- package/dist/telemetry.d.ts +10 -0
- package/dist/type-classify.js +4 -1
- package/dist/type-labels.d.ts +30 -0
- package/dist/type-labels.js +43 -0
- package/dist/types.d.ts +28 -0
- package/dist/uninstall.d.ts +12 -0
- package/dist/uninstall.js +21 -3
- package/dist/viz.d.ts +24 -11
- package/dist/viz.js +97 -32
- package/hermes-plugin/hicortex/README.md +4 -2
- package/package.json +2 -2
- package/dist/context-cli.d.ts +0 -69
- package/dist/lessons-context.d.ts +0 -102
package/dist/mcp-server.js
CHANGED
|
@@ -49,6 +49,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
49
49
|
};
|
|
50
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
51
|
exports.startServer = startServer;
|
|
52
|
+
exports.formatResults = formatResults;
|
|
52
53
|
const express_1 = __importDefault(require("express"));
|
|
53
54
|
const node_path_1 = require("node:path");
|
|
54
55
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
@@ -64,14 +65,18 @@ const storage = __importStar(require("./storage.js"));
|
|
|
64
65
|
const graph_js_1 = require("./graph.js");
|
|
65
66
|
const viz_js_1 = require("./viz.js");
|
|
66
67
|
const dashboard_js_1 = require("./dashboard.js");
|
|
67
|
-
const
|
|
68
|
+
const identity_store_js_1 = require("./identity-store.js");
|
|
68
69
|
const retrieval = __importStar(require("./retrieval.js"));
|
|
69
70
|
const recall_registry_js_1 = require("./recall-registry.js");
|
|
70
71
|
const memory_instructions_js_1 = require("./memory-instructions.js");
|
|
71
72
|
const recall_index_js_1 = require("./recall-index.js");
|
|
73
|
+
const type_labels_js_1 = require("./type-labels.js");
|
|
74
|
+
const health_js_1 = require("./health.js");
|
|
72
75
|
const seed_lesson_js_1 = require("./seed-lesson.js");
|
|
76
|
+
const learnings_identity_js_1 = require("./learnings-identity.js");
|
|
73
77
|
const distiller_js_1 = require("./distiller.js");
|
|
74
78
|
const dedup_js_1 = require("./dedup.js");
|
|
79
|
+
const redact_js_1 = require("./redact.js");
|
|
75
80
|
const init_js_1 = require("./init.js");
|
|
76
81
|
// ---------------------------------------------------------------------------
|
|
77
82
|
// Server state
|
|
@@ -97,13 +102,15 @@ function warnDeprecatedPrivacyParamIfPresent(query, route) {
|
|
|
97
102
|
`Use a separate Hicortex server for isolation. (This warning fires once per process.)`);
|
|
98
103
|
}
|
|
99
104
|
let stateDir = "";
|
|
100
|
-
// Resolved
|
|
101
|
-
// the standing
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
+
// Resolved identityClients list (spec §2) — the harness names allowed to inject
|
|
106
|
+
// the standing identity layer. Echoed by GET /identity so each hook self-gates.
|
|
107
|
+
// (#264: was contextClients; the legacy key is still read with a one-time
|
|
108
|
+
// deprecation warning via resolveIdentityClientsConfig.)
|
|
109
|
+
let identityClients = ["cc"];
|
|
110
|
+
// Resolved identityAgents map (0.13) — agent id → mode (override/global/off).
|
|
111
|
+
// Read once at boot (like identityClients); the drop-in-a-dir presence path is
|
|
105
112
|
// per-request, so only explicit config entries need a daemon restart to apply.
|
|
106
|
-
let
|
|
113
|
+
let identityAgents = {};
|
|
107
114
|
// Pushed-recall dedup registry (#192) + options; configured at boot.
|
|
108
115
|
let recallRegistry = new recall_registry_js_1.SessionRecallRegistry();
|
|
109
116
|
let recallIndexOptions = {};
|
|
@@ -254,23 +261,56 @@ function createMcpServer() {
|
|
|
254
261
|
return { content: [{ type: "text", text: `Delete failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
255
262
|
}
|
|
256
263
|
});
|
|
257
|
-
// -- hicortex_lessons --
|
|
258
|
-
|
|
259
|
-
days: zod_1.z.coerce.number().optional().describe("Look back N days (default 7)"),
|
|
260
|
-
project: zod_1.z.string().optional().describe("Filter by project name"),
|
|
261
|
-
}, async ({ days, project }) => {
|
|
264
|
+
// -- hicortex_learnings (canonical) + hicortex_lessons (alias) --
|
|
265
|
+
const learningsHandler = async ({ days, project }) => {
|
|
262
266
|
if (!db)
|
|
263
267
|
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
264
268
|
try {
|
|
265
269
|
const lessons = storage.getLessons(db, days ?? 7, project);
|
|
266
270
|
if (lessons.length === 0) {
|
|
267
|
-
return { content: [{ type: "text", text: "No
|
|
271
|
+
return { content: [{ type: "text", text: "No Learnings found for the specified period." }] };
|
|
268
272
|
}
|
|
269
273
|
const text = lessons.map((l) => `- ${l.content.slice(0, 500)}`).join("\n");
|
|
270
274
|
return { content: [{ type: "text", text }] };
|
|
271
275
|
}
|
|
272
276
|
catch (err) {
|
|
273
|
-
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 };
|
|
274
314
|
}
|
|
275
315
|
});
|
|
276
316
|
// -- hicortex_index --
|
|
@@ -464,21 +504,33 @@ async function startServer(options = {}) {
|
|
|
464
504
|
console.warn("[hicortex] WARNING: no authToken configured — remote connections will be rejected " +
|
|
465
505
|
"(localhost still works). Run `npx @gamaze/hicortex init` to generate a token.");
|
|
466
506
|
}
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
const
|
|
471
|
-
|
|
507
|
+
// Optional rotation-grace token (#254): config-only (no env var — rotation is
|
|
508
|
+
// an explicit, deliberate op). When set, both tokens are accepted so client
|
|
509
|
+
// reconfiguration never causes failed requests.
|
|
510
|
+
const authTokenPrevious = savedConfig?.authTokenPrevious;
|
|
511
|
+
// Identity layer (0.12; renamed from context layer in 0.18 #264): resolve
|
|
512
|
+
// which harnesses may inject the standing identity. Warn once per boot on
|
|
513
|
+
// unknown names so typos (e.g. "herms") surface instead of silently dropping.
|
|
514
|
+
const resolvedClients = (0, identity_store_js_1.resolveIdentityClientsConfig)(savedConfig);
|
|
515
|
+
identityClients = resolvedClients.clients;
|
|
516
|
+
if (resolvedClients.legacy) {
|
|
517
|
+
console.warn("[hicortex] Config uses the legacy 'contextClients' key — renamed to 'identityClients' in 0.18 (#264). " +
|
|
518
|
+
"The legacy key still works; update your config to silence this warning.");
|
|
519
|
+
}
|
|
472
520
|
if (resolvedClients.dropped.length > 0) {
|
|
473
|
-
console.warn(`[hicortex] Ignoring unknown
|
|
521
|
+
console.warn(`[hicortex] Ignoring unknown identityClients: ${resolvedClients.dropped.join(", ")} ` +
|
|
474
522
|
`(known: cc, hermes, oc)`);
|
|
475
523
|
}
|
|
476
|
-
// Per-agent
|
|
524
|
+
// Per-agent identity (0.13): resolve the config-declared modes. Warn once per
|
|
477
525
|
// boot on dropped entries (bad key or bad mode) so typos surface. NOTE: this
|
|
478
|
-
// map is boot-time; editing
|
|
526
|
+
// map is boot-time; editing identityAgents needs a daemon restart. Dropping an
|
|
479
527
|
// agents/<id> dir onto disk takes effect immediately (per-request presence).
|
|
480
|
-
const resolvedAgents = (0,
|
|
481
|
-
|
|
528
|
+
const resolvedAgents = (0, identity_store_js_1.resolveIdentityAgentsConfig)(savedConfig);
|
|
529
|
+
identityAgents = resolvedAgents.agents;
|
|
530
|
+
if (resolvedAgents.legacy) {
|
|
531
|
+
console.warn("[hicortex] Config uses the legacy 'contextAgents' key — renamed to 'identityAgents' in 0.18 (#264). " +
|
|
532
|
+
"The legacy key still works; update your config to silence this warning.");
|
|
533
|
+
}
|
|
482
534
|
// #192 recall/decay alignment: decay speed + recall breadth + pushed-recall
|
|
483
535
|
// knobs, ALL from config (see retrieval.ts configureRecall for the key list)
|
|
484
536
|
// so calibration is a config edit + restart, never a release.
|
|
@@ -504,9 +556,19 @@ async function startServer(options = {}) {
|
|
|
504
556
|
};
|
|
505
557
|
memoryInstructionsEnabled = savedConfig?.memoryInstructions !== false;
|
|
506
558
|
if (resolvedAgents.dropped.length > 0) {
|
|
507
|
-
console.warn(`[hicortex] Ignoring invalid
|
|
559
|
+
console.warn(`[hicortex] Ignoring invalid identityAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
|
|
508
560
|
`(keys must match ^[a-z0-9][a-z0-9_-]*$; modes must be override|global|off)`);
|
|
509
561
|
}
|
|
562
|
+
// #264 dir migration: rename <hicortex-home>/context/ → identity/ on boot
|
|
563
|
+
// when only the legacy dir exists. The fallback read in identity-store.ts
|
|
564
|
+
// (readSectionsWithFallback) is the safety net for a partial/no migration.
|
|
565
|
+
const idMig = (0, identity_store_js_1.migrateIdentityDir)(stateDir);
|
|
566
|
+
if (idMig.renamed) {
|
|
567
|
+
console.log(`[hicortex] Migrated identity dir: ${idMig.from} → ${idMig.to}`);
|
|
568
|
+
}
|
|
569
|
+
else if (idMig.reason && idMig.reason !== "no legacy context/ dir" && !idMig.reason.startsWith("identity/ already exists")) {
|
|
570
|
+
console.warn(`[hicortex] Identity dir migration skipped: ${idMig.reason}`);
|
|
571
|
+
}
|
|
510
572
|
// Express app
|
|
511
573
|
const app = (0, express_1.default)();
|
|
512
574
|
// Raise the body limit — whole-session denoised transcripts exceed the 100 kB default.
|
|
@@ -551,23 +613,38 @@ async function startServer(options = {}) {
|
|
|
551
613
|
// /dashboard has its own shell-exemption pattern. Gives the console one entry
|
|
552
614
|
// point: http://<host>:8787/ → /dashboard.
|
|
553
615
|
app.get("/", (_req, res) => res.redirect("/dashboard"));
|
|
554
|
-
app.use((0, viz_js_1.createAuthMiddleware)(authToken));
|
|
616
|
+
app.use((0, viz_js_1.createAuthMiddleware)(authToken, authTokenPrevious));
|
|
555
617
|
// SSE transport management — each connection gets its own McpServer instance
|
|
556
618
|
const transports = new Map();
|
|
557
|
-
// Health endpoint
|
|
619
|
+
// Health endpoint — PUBLIC minimal probe. Unauthenticated (the auth
|
|
620
|
+
// middleware exempts /health) and carries NO data: just liveness for load
|
|
621
|
+
// balancers, watchdogs, and anonymous probers. Tenant/install BI (memory
|
|
622
|
+
// count, link count, DB size, version, the full LLM backend string) lives
|
|
623
|
+
// on /health/detail, which is auth-gated (localhost bypasses auth so
|
|
624
|
+
// co-located tooling — `hicortex status`, nightly preflight, `init` detect
|
|
625
|
+
// — sees it without a token). #253 — spec 2026-07-27-hosted-service §6.
|
|
558
626
|
app.get("/health", (_req, res) => {
|
|
627
|
+
res.json((0, health_js_1.publicHealthResponse)());
|
|
628
|
+
});
|
|
629
|
+
// Operator-only diagnostics. Goes through the standard auth middleware
|
|
630
|
+
// (not in the public-path exemption list in viz.ts); localhost bypasses
|
|
631
|
+
// auth, remote needs the bearer token. Keeps the public LB/watchdog path
|
|
632
|
+
// cheap (no COUNT(*)) and the diagnostics off the public surface.
|
|
633
|
+
app.get("/health/detail", (_req, res) => {
|
|
559
634
|
const s = db ? (0, db_js_1.getStats)(db, dbPath) : { memories: 0, links: 0, db_size_bytes: 0, by_type: {} };
|
|
560
|
-
res.json({
|
|
561
|
-
status: "ok",
|
|
562
|
-
version: VERSION,
|
|
635
|
+
res.json((0, health_js_1.detailedHealthResponse)({
|
|
563
636
|
memories: s.memories,
|
|
564
637
|
links: s.links,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
638
|
+
dbSizeBytes: s.db_size_bytes,
|
|
639
|
+
version: VERSION,
|
|
640
|
+
llmLabel: llmConfig ? `${llmConfig.provider}/${llmConfig.model}` : "not configured",
|
|
641
|
+
}));
|
|
568
642
|
});
|
|
569
|
-
// REST /
|
|
570
|
-
|
|
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) => {
|
|
571
648
|
if (!db) {
|
|
572
649
|
res.status(503).json({ error: "Server not initialized" });
|
|
573
650
|
return;
|
|
@@ -599,9 +676,11 @@ async function startServer(options = {}) {
|
|
|
599
676
|
});
|
|
600
677
|
}
|
|
601
678
|
catch (err) {
|
|
602
|
-
|
|
679
|
+
(0, health_js_1.logAndSendInternalError)(res, "learnings", err);
|
|
603
680
|
}
|
|
604
|
-
}
|
|
681
|
+
};
|
|
682
|
+
app.get("/learnings", learningsIndexHandler);
|
|
683
|
+
app.get("/lessons", learningsIndexHandler); // #264 backcompat alias
|
|
605
684
|
// REST /ingest — accept pre-distilled memories from remote clients
|
|
606
685
|
app.post("/ingest", async (req, res) => {
|
|
607
686
|
if (!db) {
|
|
@@ -644,7 +723,8 @@ async function startServer(options = {}) {
|
|
|
644
723
|
res.status(201).json({ id, message: "Memory ingested" });
|
|
645
724
|
}
|
|
646
725
|
catch (err) {
|
|
647
|
-
res.status(500).json({ error: "Ingestion failed"
|
|
726
|
+
res.status(500).json({ error: "Ingestion failed" });
|
|
727
|
+
console.error(`[hicortex] /ingest: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
648
728
|
}
|
|
649
729
|
});
|
|
650
730
|
// REST /search — semantic search over the memory store.
|
|
@@ -671,7 +751,7 @@ async function startServer(options = {}) {
|
|
|
671
751
|
res.json({ results });
|
|
672
752
|
}
|
|
673
753
|
catch (err) {
|
|
674
|
-
|
|
754
|
+
(0, health_js_1.logAndSendInternalError)(res, "search", err);
|
|
675
755
|
}
|
|
676
756
|
});
|
|
677
757
|
// REST /recall-index — pushed recall index (#192). One recall logic for all
|
|
@@ -735,7 +815,7 @@ async function startServer(options = {}) {
|
|
|
735
815
|
res.status(r.status).json(r.body);
|
|
736
816
|
}
|
|
737
817
|
catch (err) {
|
|
738
|
-
|
|
818
|
+
(0, health_js_1.logAndSendInternalError)(res, "memory", err);
|
|
739
819
|
}
|
|
740
820
|
});
|
|
741
821
|
// REST /recent — recent memories, optionally filtered by project.
|
|
@@ -754,30 +834,36 @@ async function startServer(options = {}) {
|
|
|
754
834
|
res.json({ results });
|
|
755
835
|
}
|
|
756
836
|
catch (err) {
|
|
757
|
-
|
|
837
|
+
(0, health_js_1.logAndSendInternalError)(res, "recent", err);
|
|
758
838
|
}
|
|
759
839
|
});
|
|
760
840
|
// -------------------------------------------------------------------------
|
|
761
|
-
// REST /
|
|
841
|
+
// REST /identity — standing identity layer (0.12, spec 2026-07-12; renamed
|
|
842
|
+
// from /context in 0.18 #264).
|
|
762
843
|
//
|
|
763
|
-
// GET → { sections, updated_at, clients } read from <hicortex-home>/
|
|
844
|
+
// GET → { sections, updated_at, clients } read from <hicortex-home>/identity/.
|
|
764
845
|
// PUT → partial upsert of named sections (allowlisted names, atomic).
|
|
765
846
|
//
|
|
766
|
-
// This is NOT recall. The recall endpoint that previously held
|
|
767
|
-
// now /recent (§Naming). Stale-client tripwire: old recall callers
|
|
768
|
-
// send project/limit/privacy query params;
|
|
769
|
-
// so those params on GET /
|
|
770
|
-
// of silently degrading recall to an empty {sections} response.
|
|
847
|
+
// This is NOT recall. The recall endpoint that previously held the /context
|
|
848
|
+
// name is now /recent (§Naming). Stale-client tripwire: old recall callers
|
|
849
|
+
// always send project/limit/privacy query params; identity-layer callers
|
|
850
|
+
// never do — so those params on GET /identity return a loud, self-explaining
|
|
851
|
+
// 400 instead of silently degrading recall to an empty {sections} response.
|
|
771
852
|
//
|
|
772
853
|
// Auth is the standard model (bearer; localhost bypass) via the shared
|
|
773
854
|
// middleware — no special-casing here.
|
|
774
855
|
// -------------------------------------------------------------------------
|
|
775
856
|
// Thin adapters: all logic (tripwire, validation, allowlist, atomicity,
|
|
776
|
-
// symlink safety, size warn) lives in the pure handlers in
|
|
857
|
+
// symlink safety, size warn) lives in the pure handlers in identity-store.ts,
|
|
777
858
|
// which the tests exercise directly — no mirror-app drift.
|
|
778
|
-
|
|
859
|
+
//
|
|
860
|
+
// #264 backcompat: GET/PUT /context remain mounted BELOW as aliases that
|
|
861
|
+
// route to the SAME handlers (Hermes/OC plugins and pre-0.18 clients keep
|
|
862
|
+
// working unchanged). Both endpoints read/write the SAME identity dir.
|
|
863
|
+
const identityDir = (0, node_path_1.join)(stateDir, "identity");
|
|
864
|
+
app.get("/identity", (req, res) => {
|
|
779
865
|
try {
|
|
780
|
-
const r = (0,
|
|
866
|
+
const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, req.query, identityAgents);
|
|
781
867
|
// #192: product-owned memory instructions ride as a synthetic read-only
|
|
782
868
|
// `memory` section (config memoryInstructions !== false; agent mode
|
|
783
869
|
// "off" respected inside the helper). Every harness renders it via the
|
|
@@ -788,10 +874,10 @@ async function startServer(options = {}) {
|
|
|
788
874
|
res.status(r.status).json(r.body);
|
|
789
875
|
}
|
|
790
876
|
catch (err) {
|
|
791
|
-
|
|
877
|
+
(0, health_js_1.logAndSendInternalError)(res, "identity", err);
|
|
792
878
|
}
|
|
793
879
|
});
|
|
794
|
-
app.put("/
|
|
880
|
+
app.put("/identity", (req, res) => {
|
|
795
881
|
try {
|
|
796
882
|
// Reserved product section: never writable, loud error (no silent skip).
|
|
797
883
|
const putSections = req.body?.sections;
|
|
@@ -799,13 +885,46 @@ async function startServer(options = {}) {
|
|
|
799
885
|
res.status(400).json({ error: `Section name '${memory_instructions_js_1.MEMORY_SECTION_NAME}' is reserved for the product-owned memory instructions (config memoryInstructions to disable them)` });
|
|
800
886
|
return;
|
|
801
887
|
}
|
|
802
|
-
const r = (0,
|
|
888
|
+
const r = (0, identity_store_js_1.handleIdentityPut)(identityDir, req.body, req.query, identityAgents);
|
|
803
889
|
if (r.warn)
|
|
804
890
|
console.warn(`[hicortex] ${r.warn}`);
|
|
805
891
|
res.status(r.status).json(r.body);
|
|
806
892
|
}
|
|
807
893
|
catch (err) {
|
|
808
|
-
|
|
894
|
+
(0, health_js_1.logAndSendInternalError)(res, "identity", err);
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
// #264 backcompat aliases: /context → /identity handlers (same dir, same
|
|
898
|
+
// clients/agents). Kept indefinitely so external callers (the Hermes plugin,
|
|
899
|
+
// pre-0.18 OC clients, operator scripts) never break. The dir is "identity"
|
|
900
|
+
// in BOTH aliases — the rename + migration is server-side; clients see no
|
|
901
|
+
// difference in behaviour, only the URL.
|
|
902
|
+
app.get("/context", (req, res) => {
|
|
903
|
+
try {
|
|
904
|
+
const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, req.query, identityAgents);
|
|
905
|
+
if (r.status === 200) {
|
|
906
|
+
(0, memory_instructions_js_1.injectMemorySection)(r.body, memoryInstructionsEnabled);
|
|
907
|
+
}
|
|
908
|
+
res.status(r.status).json(r.body);
|
|
909
|
+
}
|
|
910
|
+
catch (err) {
|
|
911
|
+
(0, health_js_1.logAndSendInternalError)(res, "context", err);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
app.put("/context", (req, res) => {
|
|
915
|
+
try {
|
|
916
|
+
const putSections = req.body?.sections;
|
|
917
|
+
if (putSections && Object.keys(putSections).some((n) => (0, memory_instructions_js_1.isReservedSectionName)(n))) {
|
|
918
|
+
res.status(400).json({ error: `Section name '${memory_instructions_js_1.MEMORY_SECTION_NAME}' is reserved for the product-owned memory instructions (config memoryInstructions to disable them)` });
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const r = (0, identity_store_js_1.handleIdentityPut)(identityDir, req.body, req.query, identityAgents);
|
|
922
|
+
if (r.warn)
|
|
923
|
+
console.warn(`[hicortex] ${r.warn}`);
|
|
924
|
+
res.status(r.status).json(r.body);
|
|
925
|
+
}
|
|
926
|
+
catch (err) {
|
|
927
|
+
(0, health_js_1.logAndSendInternalError)(res, "context", err);
|
|
809
928
|
}
|
|
810
929
|
});
|
|
811
930
|
// REST /distill — canonical capture endpoint (0.9.0+).
|
|
@@ -826,17 +945,41 @@ async function startServer(options = {}) {
|
|
|
826
945
|
}
|
|
827
946
|
const { text, messages, source_agent, source_agent_id, source_domain, project, session_id, segment_id, session_date, privacy } = req.body ?? {};
|
|
828
947
|
// Resolve the conversation text from either the pre-denoised string or raw messages array.
|
|
948
|
+
// `fromTextBranch` is captured once so the redaction gate below uses the SAME
|
|
949
|
+
// discriminator as the resolution (avoids re-redacting the messages-derived
|
|
950
|
+
// text in the `{text: "", messages: [...]}` edge case — harmless only because
|
|
951
|
+
// redaction is idempotent, but the comment/code must agree).
|
|
952
|
+
const fromTextBranch = typeof text === "string" && text.length > 0;
|
|
829
953
|
let conversationText;
|
|
830
|
-
if (
|
|
954
|
+
if (fromTextBranch) {
|
|
831
955
|
conversationText = text;
|
|
832
956
|
}
|
|
833
957
|
else if (Array.isArray(messages) && messages.length > 0) {
|
|
958
|
+
// The messages branch already redacts via extractConversationText
|
|
959
|
+
// (distiller.ts), which calls redact() as its final step.
|
|
834
960
|
conversationText = (0, distiller_js_1.extractConversationText)(messages);
|
|
835
961
|
}
|
|
836
962
|
else {
|
|
837
963
|
res.status(400).json({ error: "Provide either 'text' (string) or 'messages' (array)" });
|
|
838
964
|
return;
|
|
839
965
|
}
|
|
966
|
+
// SERVER-SIDE REDACTION (#252): scrub secrets/PII from the text branch
|
|
967
|
+
// BEFORE it reaches the distillation LLM or storage. Client-side redaction
|
|
968
|
+
// (capture.ts) is customer-disableable; a processor cannot base a privacy
|
|
969
|
+
// claim on scrubbing the caller can switch off, and unredacted secrets
|
|
970
|
+
// would reach the LLM subprocessor. Unconditional + idempotent — safe for
|
|
971
|
+
// self-hosted too (a second pass over already-redacted text is a no-op;
|
|
972
|
+
// the [REDACTED] marker is excluded by the generic_secret pattern's
|
|
973
|
+
// negative lookahead, and format-specific patterns don't match it). The
|
|
974
|
+
// messages branch is already covered above. Disable-resistance
|
|
975
|
+
// (hostedMode) is Phase 0b.
|
|
976
|
+
if (fromTextBranch) {
|
|
977
|
+
const { text: redacted, count } = (0, redact_js_1.redact)(conversationText);
|
|
978
|
+
if (count > 0) {
|
|
979
|
+
console.log(`[hicortex] Redacted ${count} secret(s) from /distill text`);
|
|
980
|
+
}
|
|
981
|
+
conversationText = redacted;
|
|
982
|
+
}
|
|
840
983
|
// Segment-exact dedup (#189): an incremental capture POST carries
|
|
841
984
|
// segment_id "<start>-<end>[.pN]". Skip iff THIS exact segment's chunks are
|
|
842
985
|
// already stored (keys "<sid>#<segment_id>#<i>"). This is what lets a failed
|
|
@@ -938,7 +1081,8 @@ async function startServer(options = {}) {
|
|
|
938
1081
|
});
|
|
939
1082
|
}
|
|
940
1083
|
catch (err) {
|
|
941
|
-
res.status(500).json({ error: "Distillation failed"
|
|
1084
|
+
res.status(500).json({ error: "Distillation failed" });
|
|
1085
|
+
console.error(`[hicortex] /distill: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
942
1086
|
}
|
|
943
1087
|
});
|
|
944
1088
|
// -------------------------------------------------------------------------
|
|
@@ -991,7 +1135,8 @@ async function startServer(options = {}) {
|
|
|
991
1135
|
res.json({ updated: true, id: fullId });
|
|
992
1136
|
}
|
|
993
1137
|
catch (err) {
|
|
994
|
-
res.status(500).json({ error: "Update failed"
|
|
1138
|
+
res.status(500).json({ error: "Update failed" });
|
|
1139
|
+
console.error(`[hicortex] /update: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
995
1140
|
}
|
|
996
1141
|
});
|
|
997
1142
|
// -------------------------------------------------------------------------
|
|
@@ -1019,7 +1164,8 @@ async function startServer(options = {}) {
|
|
|
1019
1164
|
res.json({ deleted: true, id: fullId });
|
|
1020
1165
|
}
|
|
1021
1166
|
catch (err) {
|
|
1022
|
-
res.status(500).json({ error: "Delete failed"
|
|
1167
|
+
res.status(500).json({ error: "Delete failed" });
|
|
1168
|
+
console.error(`[hicortex] /delete: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
1023
1169
|
}
|
|
1024
1170
|
});
|
|
1025
1171
|
// -------------------------------------------------------------------------
|
|
@@ -1057,7 +1203,7 @@ async function startServer(options = {}) {
|
|
|
1057
1203
|
res.json({ projects: rows.map((r) => ({ name: r.project, count: r.cnt })) });
|
|
1058
1204
|
}
|
|
1059
1205
|
catch (err) {
|
|
1060
|
-
|
|
1206
|
+
(0, health_js_1.logAndSendInternalError)(res, "index", err);
|
|
1061
1207
|
}
|
|
1062
1208
|
});
|
|
1063
1209
|
// -------------------------------------------------------------------------
|
|
@@ -1160,7 +1306,7 @@ async function startServer(options = {}) {
|
|
|
1160
1306
|
}
|
|
1161
1307
|
}
|
|
1162
1308
|
catch (err) {
|
|
1163
|
-
|
|
1309
|
+
(0, health_js_1.logAndSendInternalError)(res, "graph", err);
|
|
1164
1310
|
}
|
|
1165
1311
|
});
|
|
1166
1312
|
// -------------------------------------------------------------------------
|
|
@@ -1181,16 +1327,19 @@ async function startServer(options = {}) {
|
|
|
1181
1327
|
// (static third-party code from the npm tarball, no data) — the exemption
|
|
1182
1328
|
// lives in createAuthMiddleware next to the /viz one.
|
|
1183
1329
|
app.get("/viz/vendor/:file", (0, viz_js_1.vizVendorHandler)());
|
|
1184
|
-
// GET /
|
|
1330
|
+
// GET /identity/ui — standing-identity editor page (0.12, spec 2026-07-12 §5;
|
|
1331
|
+
// renamed from /context/ui in 0.18 #264).
|
|
1185
1332
|
//
|
|
1186
|
-
// The PRIMARY edit surface for the
|
|
1187
|
-
// CSS/JS, zero external requests) served from assets/
|
|
1188
|
-
// tab per section from GET /
|
|
1333
|
+
// The PRIMARY edit surface for the identity layer. Self-contained HTML (inline
|
|
1334
|
+
// CSS/JS, zero external requests) served from assets/identity.html; builds one
|
|
1335
|
+
// tab per section from GET /identity and saves via PUT /identity. The page
|
|
1189
1336
|
// SHELL is public (exempted in createAuthMiddleware, like /viz — it carries
|
|
1190
|
-
// no data); the GET/PUT /
|
|
1337
|
+
// no data); the GET/PUT /identity data calls stay bearer-only (localhost
|
|
1191
1338
|
// bypass). The page collects the token client-side: ?token= URL param
|
|
1192
1339
|
// (stripped on load) or an in-page prompt on 401, persisted in localStorage.
|
|
1193
|
-
|
|
1340
|
+
// #264 backcompat: /context/ui remains mounted below as an alias.
|
|
1341
|
+
app.get("/identity/ui", (0, viz_js_1.identityUiHandler)());
|
|
1342
|
+
app.get("/context/ui", (0, viz_js_1.identityUiHandler)());
|
|
1194
1343
|
// GET /dashboard — view-only memory analytics page (#224).
|
|
1195
1344
|
//
|
|
1196
1345
|
// Self-contained HTML (inline CSS/JS, hand-rolled inline SVG charts, zero
|
|
@@ -1352,6 +1501,6 @@ function formatResults(results) {
|
|
|
1352
1501
|
// (id + date), hicortex_get lazy-load of truncated content, hicortex_graph
|
|
1353
1502
|
// entry points, and hicortex_update/delete self-correction (#192).
|
|
1354
1503
|
return results
|
|
1355
|
-
.map((r) => `[${r.id}] [${r.memory_type}] (${(r.created_at ?? "").slice(0, 10)}, score: ${r.score.toFixed(3)}, strength: ${r.effective_strength.toFixed(3)}) ${r.content.slice(0, 500)}`)
|
|
1504
|
+
.map((r) => `[${r.id}] [${(0, type_labels_js_1.labelForType)(r.memory_type)}] (${(r.created_at ?? "").slice(0, 10)}, score: ${r.score.toFixed(3)}, strength: ${r.effective_strength.toFixed(3)}) ${r.content.slice(0, 500)}`)
|
|
1356
1505
|
.join("\n\n");
|
|
1357
1506
|
}
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The instructions for HOW agents use Hicortex are shipped BY the product,
|
|
5
5
|
* versioned with the server, and injected as a synthetic read-only `memory`
|
|
6
|
-
* section in the GET /
|
|
6
|
+
* section in the GET /identity response. Rationale ("enforced, built-in"):
|
|
7
7
|
* - Harness personas (SOUL.md etc.) carry ZERO hicortex content — mechanics
|
|
8
8
|
* described there rot silently when the product changes (field evidence:
|
|
9
9
|
* stale "captured via hooks" sentences; an agent shell-spelunking its own
|
|
10
10
|
* plugin infrastructure when told "the plugin was updated").
|
|
11
|
-
* - User
|
|
11
|
+
* - User identity files (user.md / rules.md) stay purely personal — norms the
|
|
12
12
|
* product depends on must not live in user-editable files (same principle
|
|
13
13
|
* as the built-in citation norm, 0.14.1).
|
|
14
|
-
* - Because every harness already renders `##
|
|
14
|
+
* - Because every harness already renders `## Identity` sections through the
|
|
15
15
|
* shared gate/render path, a synthetic section ships fleet-wide with zero
|
|
16
16
|
* client changes — including plugins that predate this feature.
|
|
17
17
|
*
|
|
18
|
-
* The section name is RESERVED: PUT /
|
|
18
|
+
* The section name is RESERVED: PUT /identity rejects it, and the synthetic
|
|
19
19
|
* text overrides any user file of the same name (enforced means enforced).
|
|
20
20
|
* Off-switch: config `memoryInstructions: false`.
|
|
21
21
|
*/
|
|
@@ -27,9 +27,9 @@ export declare function renderMemoryInstructions(): string;
|
|
|
27
27
|
* section names are lowercase by allowlist, but be safe). */
|
|
28
28
|
export declare function isReservedSectionName(name: unknown): boolean;
|
|
29
29
|
/**
|
|
30
|
-
* Inject the synthetic section into a successful GET /
|
|
30
|
+
* Inject the synthetic section into a successful GET /identity body.
|
|
31
31
|
* Pure: returns the same body object with sections.memory set. Skips agent
|
|
32
|
-
* mode "off" (operator explicitly silenced
|
|
32
|
+
* mode "off" (operator explicitly silenced identity for that agent) and
|
|
33
33
|
* non-object bodies (error shapes). Overrides a user file named memory.md.
|
|
34
34
|
*/
|
|
35
35
|
export declare function injectMemorySection<T extends {
|
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The instructions for HOW agents use Hicortex are shipped BY the product,
|
|
6
6
|
* versioned with the server, and injected as a synthetic read-only `memory`
|
|
7
|
-
* section in the GET /
|
|
7
|
+
* section in the GET /identity response. Rationale ("enforced, built-in"):
|
|
8
8
|
* - Harness personas (SOUL.md etc.) carry ZERO hicortex content — mechanics
|
|
9
9
|
* described there rot silently when the product changes (field evidence:
|
|
10
10
|
* stale "captured via hooks" sentences; an agent shell-spelunking its own
|
|
11
11
|
* plugin infrastructure when told "the plugin was updated").
|
|
12
|
-
* - User
|
|
12
|
+
* - User identity files (user.md / rules.md) stay purely personal — norms the
|
|
13
13
|
* product depends on must not live in user-editable files (same principle
|
|
14
14
|
* as the built-in citation norm, 0.14.1).
|
|
15
|
-
* - Because every harness already renders `##
|
|
15
|
+
* - Because every harness already renders `## Identity` sections through the
|
|
16
16
|
* shared gate/render path, a synthetic section ships fleet-wide with zero
|
|
17
17
|
* client changes — including plugins that predate this feature.
|
|
18
18
|
*
|
|
19
|
-
* The section name is RESERVED: PUT /
|
|
19
|
+
* The section name is RESERVED: PUT /identity rejects it, and the synthetic
|
|
20
20
|
* text overrides any user file of the same name (enforced means enforced).
|
|
21
21
|
* Off-switch: config `memoryInstructions: false`.
|
|
22
22
|
*/
|
|
@@ -44,9 +44,9 @@ function isReservedSectionName(name) {
|
|
|
44
44
|
return typeof name === "string" && name.trim().toLowerCase() === exports.MEMORY_SECTION_NAME;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* Inject the synthetic section into a successful GET /
|
|
47
|
+
* Inject the synthetic section into a successful GET /identity body.
|
|
48
48
|
* Pure: returns the same body object with sections.memory set. Skips agent
|
|
49
|
-
* mode "off" (operator explicitly silenced
|
|
49
|
+
* mode "off" (operator explicitly silenced identity for that agent) and
|
|
50
50
|
* non-object bodies (error shapes). Overrides a user file named memory.md.
|
|
51
51
|
*/
|
|
52
52
|
function injectMemorySection(body, enabled) {
|