@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.
Files changed (51) hide show
  1. package/README.md +30 -28
  2. package/assets/dashboard.html +121 -5
  3. package/assets/{context.html → identity.html} +18 -18
  4. package/assets/viz.html +19 -7
  5. package/dist/claude-md.d.ts +2 -1
  6. package/dist/claude-md.js +2 -1
  7. package/dist/cli-args.d.ts +9 -0
  8. package/dist/cli-args.js +16 -0
  9. package/dist/cli.js +29 -20
  10. package/dist/consolidate.d.ts +15 -0
  11. package/dist/consolidate.js +30 -3
  12. package/dist/dashboard.d.ts +58 -1
  13. package/dist/dashboard.js +27 -1
  14. package/dist/extensions.d.ts +1 -1
  15. package/dist/extensions.js +1 -1
  16. package/dist/health.d.ts +68 -0
  17. package/dist/health.js +73 -0
  18. package/dist/identity-cli.d.ts +90 -0
  19. package/dist/{context-cli.js → identity-cli.js} +66 -48
  20. package/dist/{context-store.d.ts → identity-store.d.ts} +94 -31
  21. package/dist/{context-store.js → identity-store.js} +212 -71
  22. package/dist/index.d.ts +12 -5
  23. package/dist/index.js +57 -29
  24. package/dist/init.d.ts +44 -8
  25. package/dist/init.js +142 -37
  26. package/dist/learnings-identity.d.ts +149 -0
  27. package/dist/{lessons-context.js → learnings-identity.js} +96 -52
  28. package/dist/mcp-server.d.ts +2 -0
  29. package/dist/mcp-server.js +217 -68
  30. package/dist/memory-instructions.d.ts +6 -6
  31. package/dist/memory-instructions.js +6 -6
  32. package/dist/nightly.js +65 -6
  33. package/dist/paths.js +1 -1
  34. package/dist/recall-hook-cli.d.ts +1 -1
  35. package/dist/recall-hook-cli.js +3 -3
  36. package/dist/recall-index.js +5 -2
  37. package/dist/status.d.ts +2 -2
  38. package/dist/status.js +11 -9
  39. package/dist/telemetry.d.ts +10 -0
  40. package/dist/type-classify.js +4 -1
  41. package/dist/type-labels.d.ts +30 -0
  42. package/dist/type-labels.js +43 -0
  43. package/dist/types.d.ts +28 -0
  44. package/dist/uninstall.d.ts +12 -0
  45. package/dist/uninstall.js +21 -3
  46. package/dist/viz.d.ts +24 -11
  47. package/dist/viz.js +97 -32
  48. package/hermes-plugin/hicortex/README.md +4 -2
  49. package/package.json +2 -2
  50. package/dist/context-cli.d.ts +0 -69
  51. package/dist/lessons-context.d.ts +0 -102
@@ -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 context_store_js_1 = require("./context-store.js");
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 contextClients list (spec §2) — the harness names allowed to inject
101
- // the standing context layer. Echoed by GET /context so each hook self-gates.
102
- let contextClients = ["cc"];
103
- // Resolved contextAgents map (0.13) — agent id → mode (override/global/off).
104
- // Read once at boot (like contextClients); the drop-in-a-dir presence path is
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 contextAgents = {};
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
- server.tool("hicortex_lessons", "Get actionable lessons learned from past sessions. Auto-generated insights about mistakes to avoid.", {
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 lessons found for the specified period." }] };
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: `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 };
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
- // Context layer (0.12): resolve which harnesses may inject the standing
468
- // context. Warn once per boot on unknown names so typos (e.g. "herms")
469
- // surface instead of silently dropping.
470
- const resolvedClients = (0, context_store_js_1.resolveContextClients)(savedConfig?.contextClients);
471
- contextClients = resolvedClients.clients;
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 contextClients: ${resolvedClients.dropped.join(", ")} ` +
521
+ console.warn(`[hicortex] Ignoring unknown identityClients: ${resolvedClients.dropped.join(", ")} ` +
474
522
  `(known: cc, hermes, oc)`);
475
523
  }
476
- // Per-agent context (0.13): resolve the config-declared modes. Warn once per
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 contextAgents needs a daemon restart. Dropping an
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, context_store_js_1.resolveContextAgents)(savedConfig?.contextAgents);
481
- contextAgents = resolvedAgents.agents;
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 contextAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
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
- db_size_kb: Math.round(s.db_size_bytes / 1024),
566
- llm: llmConfig ? `${llmConfig.provider}/${llmConfig.model}` : "not configured",
567
- });
638
+ dbSizeBytes: s.db_size_bytes,
639
+ version: VERSION,
640
+ llmLabel: llmConfig ? `${llmConfig.provider}/${llmConfig.model}` : "not configured",
641
+ }));
568
642
  });
569
- // REST /lessons return lessons + memory index for client CLAUDE.md injection
570
- 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) => {
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
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
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", message: err instanceof Error ? err.message : String(err) });
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
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
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
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
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
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
837
+ (0, health_js_1.logAndSendInternalError)(res, "recent", err);
758
838
  }
759
839
  });
760
840
  // -------------------------------------------------------------------------
761
- // REST /context — standing context layer (0.12, spec 2026-07-12).
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>/context/.
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 this name is
767
- // now /recent (§Naming). Stale-client tripwire: old recall callers always
768
- // send project/limit/privacy query params; context-layer callers never do —
769
- // so those params on GET /context return a loud, self-explaining 400 instead
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 context-store.ts,
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
- app.get("/context", (req, res) => {
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, context_store_js_1.handleContextGet)((0, node_path_1.join)(stateDir, "context"), contextClients, req.query, contextAgents);
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
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
877
+ (0, health_js_1.logAndSendInternalError)(res, "identity", err);
792
878
  }
793
879
  });
794
- app.put("/context", (req, res) => {
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, context_store_js_1.handleContextPut)((0, node_path_1.join)(stateDir, "context"), req.body, req.query, contextAgents);
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
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
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 (typeof text === "string" && text.length > 0) {
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", message: err instanceof Error ? err.message : String(err) });
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", message: err instanceof Error ? err.message : String(err) });
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", message: err instanceof Error ? err.message : String(err) });
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
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
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
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
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 /context/ui — standing-context editor page (0.12, spec 2026-07-12 §5).
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 context layer. Self-contained HTML (inline
1187
- // CSS/JS, zero external requests) served from assets/context.html; builds one
1188
- // tab per section from GET /context and saves via PUT /context. The page
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 /context data calls stay bearer-only (localhost
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
- app.get("/context/ui", (0, viz_js_1.contextUiHandler)());
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 /context response. Rationale ("enforced, built-in"):
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 context files (user.md / rules.md) stay purely personal — norms the
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 `## Context` sections through the
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 /context rejects it, and the synthetic
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 /context body.
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 context for that agent) and
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 /context response. Rationale ("enforced, built-in"):
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 context files (user.md / rules.md) stay purely personal — norms the
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 `## Context` sections through the
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 /context rejects it, and the synthetic
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 /context body.
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 context for that agent) and
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) {