@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
@@ -1,16 +1,17 @@
1
1
  "use strict";
2
2
  /**
3
- * Context layer (L2) — file-backed storage (0.12.0, spec 2026-07-12).
3
+ * Identity layer — file-backed storage (formerly "Context Layer" / L2; 0.12.0,
4
+ * spec 2026-07-12; renamed 0.18 #264).
4
5
  *
5
- * The context layer is hand-edited Markdown ("who you are + how to work") stored
6
- * as plain files in `<hicortex-home>/context/*.md` — one file per section, one
6
+ * The identity layer is hand-edited Markdown ("who you are + how to work") stored
7
+ * as plain files in `<hicortex-home>/identity/*.md` — one file per section, one
7
8
  * Web UI tab. It lives OUTSIDE the memories table: never distilled, scored,
8
9
  * decayed, or pruned. As plain files this guarantee is structural — no code in
9
10
  * consolidate.ts / distiller.ts references this directory.
10
11
  *
11
12
  * This module is pure file-layer logic (no express) so it is unit-testable and
12
13
  * reusable by the CLI. The daemon resolves `<hicortex-home>` exactly as it
13
- * resolves the DB dir (dirname of the resolved DB path) and passes the context
14
+ * resolves the DB dir (dirname of the resolved DB path) and passes the identity
14
15
  * dir in — nothing here hardcodes `~`.
15
16
  *
16
17
  * Security contract (spec §1): client-supplied section names are NEVER joined
@@ -18,23 +19,35 @@
18
19
  * itself appends `.md`. Reads skip symlinks (lstat, not stat). Writes validate
19
20
  * ALL names before touching the disk (atomic request semantics), go via
20
21
  * temp-file-then-rename, and keep a one-generation `<name>.md.bak` undo.
22
+ *
23
+ * Backcompat (#264): the pre-rename directory was `<hicortex-home>/context/`.
24
+ * `migrateIdentityDir` renames it to `identity/` on the next init/nightly run
25
+ * when the old dir exists and the new does not. When BOTH exist (partial
26
+ * migration, a disk-full renameSync failure, or an operator re-creating
27
+ * `context/`), reads ADDITIVELY MERGE: per-section `identity/` wins, and
28
+ * legacy-only sections are included so nothing is lost — at both the global
29
+ * and the per-agent (`agents/<id>/`) level.
21
30
  */
22
31
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.KNOWN_CONTEXT_CLIENTS = exports.CONTEXT_SIZE_WARN_BYTES = exports.isValidAgentId = exports.AGENTS_DIR = exports.InvalidSectionNameError = exports.SECTION_NAME_MAX = exports.SECTION_NAME_RE = void 0;
32
+ exports.KNOWN_CONTEXT_CLIENTS = exports.KNOWN_IDENTITY_CLIENTS = exports.CONTEXT_SIZE_WARN_BYTES = exports.IDENTITY_SIZE_WARN_BYTES = exports.isValidAgentId = exports.AGENTS_DIR = exports.InvalidSectionNameError = exports.SECTION_NAME_MAX = exports.SECTION_NAME_RE = void 0;
24
33
  exports.isValidSectionName = isValidSectionName;
25
34
  exports.sanitizeAgentId = sanitizeAgentId;
26
35
  exports.resolveAgentIdentity = resolveAgentIdentity;
27
- exports.resolveContextAgents = resolveContextAgents;
36
+ exports.resolveIdentityAgents = resolveIdentityAgents;
37
+ exports.migrateIdentityDir = migrateIdentityDir;
28
38
  exports.readSections = readSections;
39
+ exports.readSectionsWithFallback = readSectionsWithFallback;
29
40
  exports.writeSections = writeSections;
30
41
  exports.totalBytes = totalBytes;
31
- exports.resolveContextClients = resolveContextClients;
42
+ exports.resolveIdentityClients = resolveIdentityClients;
43
+ exports.resolveIdentityClientsConfig = resolveIdentityClientsConfig;
44
+ exports.resolveIdentityAgentsConfig = resolveIdentityAgentsConfig;
32
45
  exports.agentDirState = agentDirState;
33
46
  exports.resolveAgentMode = resolveAgentMode;
34
47
  exports.listAgents = listAgents;
35
48
  exports.readResolvedSections = readResolvedSections;
36
- exports.handleContextGet = handleContextGet;
37
- exports.handleContextPut = handleContextPut;
49
+ exports.handleIdentityGet = handleIdentityGet;
50
+ exports.handleIdentityPut = handleIdentityPut;
38
51
  const node_fs_1 = require("node:fs");
39
52
  const node_path_1 = require("node:path");
40
53
  const node_crypto_1 = require("node:crypto");
@@ -64,11 +77,11 @@ class InvalidSectionNameError extends Error {
64
77
  }
65
78
  }
66
79
  exports.InvalidSectionNameError = InvalidSectionNameError;
67
- /** Reserved subdir under <context>/ holding per-agent sections. NOT a section. */
80
+ /** Reserved subdir under <identity>/ holding per-agent sections. NOT a section. */
68
81
  exports.AGENTS_DIR = "agents";
69
82
  /**
70
83
  * Agent ids share the section-name allowlist: they are joined into a filesystem
71
- * path (`context/agents/<id>`), so this is the same security contract, not just
84
+ * path (`identity/agents/<id>`), so this is the same security contract, not just
72
85
  * hygiene.
73
86
  */
74
87
  exports.isValidAgentId = isValidSectionName;
@@ -101,9 +114,9 @@ function sanitizeAgentId(raw) {
101
114
  * - absent, or empty/whitespace-only → null id, "unset". Empty string == unset
102
115
  * everywhere (this is the value `init --agent-name ""` writes-then-clears to
103
116
  * opt back out): CC's default is NO `?agent=`, so all CC boxes for one user
104
- * share the global context — one user = one identity across machines.
117
+ * share the global identity — one user = one identity across machines.
105
118
  * There is NO hostname fallback: a hostname-derived default would silently give
106
- * every machine its own context, the opposite of the shared-identity default.
119
+ * every machine its own identity, the opposite of the shared-identity default.
107
120
  */
108
121
  function resolveAgentIdentity(config) {
109
122
  const value = config.agentName;
@@ -117,12 +130,16 @@ function resolveAgentIdentity(config) {
117
130
  return { agentId: null, source: "unset" };
118
131
  }
119
132
  /**
120
- * Normalize the raw `contextAgents` config value into a map of valid agent id →
133
+ * Normalize the raw `identityAgents` config value into a map of valid agent id →
121
134
  * mode. An entry is kept only when its key passes isValidAgentId AND its value
122
135
  * is one of the three modes; everything else is dropped and its key collected
123
- * for a one-time boot warning (mirrors resolveContextClients).
136
+ * for a one-time boot warning (mirrors resolveIdentityClients).
137
+ *
138
+ * Backcompat (#264): the pre-rename key was `contextAgents`. Callers that want
139
+ * to honour it should pass the already-resolved value here (see
140
+ * `resolveIdentityConfig` in mcp-server.ts which reads new + old).
124
141
  */
125
- function resolveContextAgents(raw) {
142
+ function resolveIdentityAgents(raw) {
126
143
  const agents = {};
127
144
  const dropped = [];
128
145
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
@@ -137,8 +154,39 @@ function resolveContextAgents(raw) {
137
154
  }
138
155
  return { agents, dropped };
139
156
  }
157
+ // ---------------------------------------------------------------------------
158
+ // Directory migration (backcompat — pre-rename was <home>/context/, #264)
159
+ // ---------------------------------------------------------------------------
160
+ /**
161
+ * Rename `<home>/context/` → `<home>/identity/` when the legacy dir exists and
162
+ * the new dir does not. Idempotent + safe: when both exist (a partial
163
+ * migration, or the operator created both) NOTHING is renamed — the caller
164
+ * falls back to a legacy read instead. Returns a description of what happened
165
+ * for a one-time boot log.
166
+ *
167
+ * Called from init and nightly so the migration lands on the next run of either
168
+ * command an existing install hits.
169
+ */
170
+ function migrateIdentityDir(home) {
171
+ const from = (0, node_path_1.join)(home, "context");
172
+ const to = (0, node_path_1.join)(home, "identity");
173
+ if (!(0, node_fs_1.existsSync)(from))
174
+ return { renamed: false, from, to, reason: "no legacy context/ dir" };
175
+ if ((0, node_fs_1.existsSync)(to))
176
+ return { renamed: false, from, to, reason: "identity/ already exists — leaving context/ in place (reads additively merge identity/ over legacy context/ per-section)" };
177
+ try {
178
+ (0, node_fs_1.renameSync)(from, to);
179
+ return { renamed: true, from, to };
180
+ }
181
+ catch (err) {
182
+ // A failed rename is non-fatal: the legacy dir is still readable via the
183
+ // fallback in readSectionsWithFallback, so log + continue rather than
184
+ // blocking the boot.
185
+ return { renamed: false, from, to, reason: `rename failed: ${err instanceof Error ? err.message : String(err)}` };
186
+ }
187
+ }
140
188
  /**
141
- * Enumerate the context dir and return the served sections. Only regular files
189
+ * Enumerate the identity dir and return the served sections. Only regular files
142
190
  * whose basename (sans `.md`) passes the allowlist are included; symlinks are
143
191
  * skipped (lstat). `<name>.md.bak` and temp files never match the `*.md` filter.
144
192
  *
@@ -195,6 +243,42 @@ function readSections(dir) {
195
243
  const updatedAt = Object.keys(sections).length > 0 ? new Date(latestMtimeMs).toISOString() : null;
196
244
  return { sections, updatedAt, mtimes };
197
245
  }
246
+ /**
247
+ * Merge two ReadResults into an additive union: a section present in BOTH is
248
+ * taken from `primary` (it wins per-section); sections present ONLY in
249
+ * `secondary` are included so nothing is lost. `updatedAt` is recomputed across
250
+ * the files that won the merge. Pure helper.
251
+ */
252
+ function mergeSections(primary, secondary) {
253
+ const sections = { ...secondary.sections, ...primary.sections };
254
+ const mtimes = {};
255
+ let latestMtimeMs = 0;
256
+ for (const name of Object.keys(sections)) {
257
+ const mt = name in primary.mtimes ? primary.mtimes[name] : secondary.mtimes[name];
258
+ if (mt !== undefined)
259
+ mtimes[name] = mt;
260
+ if (mt && mt > latestMtimeMs)
261
+ latestMtimeMs = mt;
262
+ }
263
+ const updatedAt = Object.keys(sections).length > 0 ? new Date(latestMtimeMs).toISOString() : null;
264
+ return { sections, updatedAt, mtimes };
265
+ }
266
+ /**
267
+ * Read the identity dir, additively merging the legacy `context/` sibling so
268
+ * nothing is lost when both dirs exist (post-#264 safety net for a partial/no
269
+ * migration: a disk-full renameSync failure, or an operator re-creating
270
+ * `context/`). Per-section, `identity/` wins; legacy-only sections are included
271
+ * (union). Pure function over the two dirs — used by the GET handler.
272
+ */
273
+ function readSectionsWithFallback(identityDir) {
274
+ const primary = readSections(identityDir);
275
+ const legacy = readSections((0, node_path_1.join)(identityDir, "..", "context"));
276
+ if (Object.keys(legacy.sections).length === 0)
277
+ return primary;
278
+ if (Object.keys(primary.sections).length === 0)
279
+ return legacy;
280
+ return mergeSections(primary, legacy);
281
+ }
198
282
  // ---------------------------------------------------------------------------
199
283
  // Write
200
284
  // ---------------------------------------------------------------------------
@@ -297,26 +381,30 @@ function totalBytes(sections) {
297
381
  return total;
298
382
  }
299
383
  /** Warn threshold: this layer bypasses token budgeting and injects every session. */
300
- exports.CONTEXT_SIZE_WARN_BYTES = 16_384;
384
+ exports.IDENTITY_SIZE_WARN_BYTES = 16_384;
385
+ /** Backcompat alias for the pre-rename name (#264). */
386
+ exports.CONTEXT_SIZE_WARN_BYTES = exports.IDENTITY_SIZE_WARN_BYTES;
301
387
  // ---------------------------------------------------------------------------
302
- // Config — contextClients normalization
388
+ // Config — identityClients normalization
303
389
  // ---------------------------------------------------------------------------
304
- /** Harness names that may inject the context layer. */
305
- exports.KNOWN_CONTEXT_CLIENTS = ["cc", "hermes", "oc"];
390
+ /** Harness names that may inject the identity layer. */
391
+ exports.KNOWN_IDENTITY_CLIENTS = ["cc", "hermes", "oc"];
392
+ /** Backcompat alias for the pre-rename name (#264). */
393
+ exports.KNOWN_CONTEXT_CLIENTS = exports.KNOWN_IDENTITY_CLIENTS;
306
394
  /**
307
- * Normalize the raw `contextClients` config value (spec §2):
395
+ * Normalize the raw `identityClients` config value (spec §2):
308
396
  * - `"all"` (any case) → ["cc","hermes","oc"]
309
397
  * - array → lowercase, keep known names (de-duped), collect dropped unknowns
310
398
  * - missing / non-array-non-"all" → default ["cc"]
311
- * The resolved list is echoed by GET /context as `clients` so each harness's
399
+ * The resolved list is echoed by GET /identity as `clients` so each harness's
312
400
  * hook can self-gate without its own config.
313
401
  */
314
- function resolveContextClients(raw) {
402
+ function resolveIdentityClients(raw) {
315
403
  if (typeof raw === "string" && raw.toLowerCase() === "all") {
316
- return { clients: [...exports.KNOWN_CONTEXT_CLIENTS], dropped: [] };
404
+ return { clients: [...exports.KNOWN_IDENTITY_CLIENTS], dropped: [] };
317
405
  }
318
406
  if (Array.isArray(raw)) {
319
- const known = new Set(exports.KNOWN_CONTEXT_CLIENTS);
407
+ const known = new Set(exports.KNOWN_IDENTITY_CLIENTS);
320
408
  const clients = [];
321
409
  const dropped = [];
322
410
  for (const item of raw) {
@@ -329,7 +417,7 @@ function resolveContextClients(raw) {
329
417
  // the natural form of the documented "all" value, so ["all"] must mean
330
418
  // all, not an empty list.
331
419
  if (lower === "all") {
332
- for (const k of exports.KNOWN_CONTEXT_CLIENTS)
420
+ for (const k of exports.KNOWN_IDENTITY_CLIENTS)
333
421
  if (!clients.includes(k))
334
422
  clients.push(k);
335
423
  continue;
@@ -346,14 +434,44 @@ function resolveContextClients(raw) {
346
434
  }
347
435
  return { clients: ["cc"], dropped: [] };
348
436
  }
437
+ /**
438
+ * Read the identity-clients config value from a raw config object, honouring
439
+ * the new key (`identityClients`) and falling back to the legacy
440
+ * `contextClients` (#264) with a one-time deprecation signal. Returns the
441
+ * resolved clients, dropped unknowns, and whether the value came from the
442
+ * legacy key.
443
+ */
444
+ function resolveIdentityClientsConfig(config) {
445
+ if (config && "identityClients" in config) {
446
+ return { ...resolveIdentityClients(config.identityClients), legacy: false };
447
+ }
448
+ if (config && "contextClients" in config) {
449
+ return { ...resolveIdentityClients(config.contextClients), legacy: true };
450
+ }
451
+ return { ...resolveIdentityClients(undefined), legacy: false };
452
+ }
453
+ /**
454
+ * Read the identity-agents config value from a raw config object, honouring
455
+ * the new key (`identityAgents`) and falling back to the legacy
456
+ * `contextAgents` (#264) with a one-time deprecation signal.
457
+ */
458
+ function resolveIdentityAgentsConfig(config) {
459
+ if (config && "identityAgents" in config) {
460
+ return { ...resolveIdentityAgents(config.identityAgents), legacy: false };
461
+ }
462
+ if (config && "contextAgents" in config) {
463
+ return { ...resolveIdentityAgents(config.contextAgents), legacy: true };
464
+ }
465
+ return { ...resolveIdentityAgents(undefined), legacy: false };
466
+ }
349
467
  // ---------------------------------------------------------------------------
350
468
  // Per-agent resolution (0.13)
351
469
  // ---------------------------------------------------------------------------
352
470
  /**
353
- * Classify the on-disk state of `context/agents/<id>`, hardened against a
471
+ * Classify the on-disk state of `identity/agents/<id>`, hardened against a
354
472
  * symlink planted at EITHER the `agents` ROOT or the `<id>` leaf — lstat/readdir
355
473
  * follow a symlinked intermediate path component, so checking only the leaf
356
- * would let a symlinked root escape the context root entirely. BOTH must be
474
+ * would let a symlinked root escape the identity root entirely. BOTH must be
357
475
  * real, non-symlink directories.
358
476
  * - "absent" — the root or the leaf does not exist (fresh install / no dir for
359
477
  * this agent). A normal, safe "no agent sections" case.
@@ -364,8 +482,8 @@ function resolveContextClients(raw) {
364
482
  * rethrow (a permissions fault must never masquerade as "empty", matching
365
483
  * readSections). The single source of truth for the four call sites below.
366
484
  */
367
- function agentDirState(contextDir, agentId) {
368
- const root = (0, node_path_1.join)(contextDir, exports.AGENTS_DIR);
485
+ function agentDirState(identityDir, agentId) {
486
+ const root = (0, node_path_1.join)(identityDir, exports.AGENTS_DIR);
369
487
  let rootSt;
370
488
  try {
371
489
  rootSt = (0, node_fs_1.lstatSync)(root);
@@ -393,27 +511,53 @@ function agentDirState(contextDir, agentId) {
393
511
  /**
394
512
  * Resolve the mode for a request `?agent=<id>` (spec §3):
395
513
  * 1. config entry present → that mode;
396
- * 2. else `context/agents/<id>/` present as a REAL directory (root + leaf both
514
+ * 2. else `identity/agents/<id>/` present as a REAL directory (root + leaf both
397
515
  * real, non-symlink) → "override" (dropping in a dir is intent, no config);
398
516
  * 3. else → "global".
399
517
  * A symlinked/non-dir root or leaf never counts as "present" (agentDirState).
400
518
  */
401
- function resolveAgentMode(contextDir, agentId, contextAgents) {
402
- const configured = contextAgents[agentId];
519
+ function resolveAgentMode(identityDir, agentId, identityAgents) {
520
+ const configured = identityAgents[agentId];
403
521
  if (configured)
404
522
  return configured;
405
- return agentDirState(contextDir, agentId) === "real" ? "override" : "global";
523
+ // Presence of EITHER the identity agent dir OR the legacy `context/agents/<id>/`
524
+ // dir counts as intent to override (post-#264 both-dirs edge case: a partial
525
+ // migration must not silently demote a legacy agent to "global" and lose its
526
+ // per-agent sections).
527
+ if (agentDirState(identityDir, agentId) === "real")
528
+ return "override";
529
+ const legacyDir = (0, node_path_1.join)(identityDir, "..", "context");
530
+ if (agentDirState(legacyDir, agentId) === "real")
531
+ return "override";
532
+ return "global";
533
+ }
534
+ /**
535
+ * Read one agent's sections from BOTH the identity agent dir and the legacy
536
+ * `context/agents/<id>/` dir, additively merged (identity wins per-section;
537
+ * legacy-only sections are included so nothing is lost in the both-dirs edge
538
+ * case). A symlinked/non-dir leaf contributes nothing (agentDirState guard).
539
+ */
540
+ function readAgentSectionsWithFallback(identityDir, agentId) {
541
+ const primaryDir = (0, node_path_1.join)(identityDir, exports.AGENTS_DIR, agentId);
542
+ const primary = agentDirState(identityDir, agentId) === "real" ? readSections(primaryDir) : { sections: {}, updatedAt: null, mtimes: {} };
543
+ const legacyDir = (0, node_path_1.join)(identityDir, "..", "context");
544
+ const legacy = agentDirState(legacyDir, agentId) === "real" ? readSections((0, node_path_1.join)(legacyDir, exports.AGENTS_DIR, agentId)) : { sections: {}, updatedAt: null, mtimes: {} };
545
+ if (Object.keys(legacy.sections).length === 0)
546
+ return primary;
547
+ if (Object.keys(primary.sections).length === 0)
548
+ return legacy;
549
+ return mergeSections(primary, legacy);
406
550
  }
407
551
  /**
408
552
  * Enumerate the agents the UI selector should offer: the union of allowlisted
409
- * REAL directories under `context/agents/` and every configured `contextAgents`
553
+ * REAL directories under `identity/agents/` and every configured `identityAgents`
410
554
  * key, each mapped to its resolved mode (config wins over presence). A missing
411
555
  * agents dir (fresh install) yields the config keys only; a symlinked/non-dir
412
556
  * agents root is treated as "no dirs" (config keys only).
413
557
  */
414
- function listAgents(contextDir, contextAgents) {
558
+ function listAgents(identityDir, identityAgents) {
415
559
  const result = {};
416
- const agentsRoot = (0, node_path_1.join)(contextDir, exports.AGENTS_DIR);
560
+ const agentsRoot = (0, node_path_1.join)(identityDir, exports.AGENTS_DIR);
417
561
  // Guard the root itself before readdir would follow a symlinked root.
418
562
  let rootReal = false;
419
563
  try {
@@ -430,14 +574,14 @@ function listAgents(contextDir, contextAgents) {
430
574
  continue;
431
575
  // Real leaf confirmed by the shared guard → its mode is config-or-override
432
576
  // (config wins over presence); no second lstat via resolveAgentMode.
433
- if (agentDirState(contextDir, entry) === "real") {
434
- result[entry] = contextAgents[entry] ?? "override";
577
+ if (agentDirState(identityDir, entry) === "real") {
578
+ result[entry] = identityAgents[entry] ?? "override";
435
579
  }
436
580
  }
437
581
  }
438
- for (const id of Object.keys(contextAgents)) {
582
+ for (const id of Object.keys(identityAgents)) {
439
583
  if (!(id in result))
440
- result[id] = contextAgents[id];
584
+ result[id] = identityAgents[id];
441
585
  }
442
586
  return result;
443
587
  }
@@ -449,25 +593,22 @@ function listAgents(contextDir, contextAgents) {
449
593
  * reading (A1): a symlinked or non-directory `agents/<id>` contributes NO agent
450
594
  * sections regardless of how override was selected (config or presence).
451
595
  */
452
- function readResolvedSections(contextDir, agentId, contextAgents) {
596
+ function readResolvedSections(identityDir, agentId, identityAgents) {
453
597
  if (agentId === null) {
454
- const g = readSections(contextDir);
598
+ const g = readSectionsWithFallback(identityDir);
455
599
  return { sections: g.sections, updatedAt: g.updatedAt };
456
600
  }
457
- const mode = resolveAgentMode(contextDir, agentId, contextAgents);
601
+ const mode = resolveAgentMode(identityDir, agentId, identityAgents);
458
602
  if (mode === "off") {
459
603
  return { sections: {}, updatedAt: null, agent: agentId, mode };
460
604
  }
461
- const global = readSections(contextDir);
605
+ const global = readSectionsWithFallback(identityDir);
462
606
  if (mode === "global") {
463
607
  return { sections: global.sections, updatedAt: global.updatedAt, agent: agentId, mode };
464
608
  }
465
- // override — read the agent dir, but only if root + leaf are real dirs
466
- // (agentDirState closes the config-forced-override + symlinked-root holes).
467
- let agentRead = { sections: {}, updatedAt: null, mtimes: {} };
468
- if (agentDirState(contextDir, agentId) === "real") {
469
- agentRead = readSections((0, node_path_1.join)(contextDir, exports.AGENTS_DIR, agentId));
470
- }
609
+ // override — read the agent dir (identity + legacy fallback merged so the
610
+ // both-dirs edge case doesn't lose legacy-only agent sections).
611
+ const agentRead = readAgentSectionsWithFallback(identityDir, agentId);
471
612
  const sections = { ...global.sections, ...agentRead.sections };
472
613
  const origins = {};
473
614
  let latestMtimeMs = 0;
@@ -482,7 +623,7 @@ function readResolvedSections(contextDir, agentId, contextAgents) {
482
623
  return { sections, updatedAt, agent: agentId, mode, origins };
483
624
  }
484
625
  // ---------------------------------------------------------------------------
485
- // HTTP-shape handlers (real logic behind GET/PUT /context)
626
+ // HTTP-shape handlers (real logic behind GET/PUT /identity)
486
627
  // ---------------------------------------------------------------------------
487
628
  //
488
629
  // These are the actual request handlers, expressed as pure functions over
@@ -506,18 +647,18 @@ function extractAgentParam(query) {
506
647
  return { agentId: raw };
507
648
  }
508
649
  /**
509
- * GET /context. Stale-client tripwire first: recall moved to /recent, so
650
+ * GET /identity. Stale-client tripwire first: recall moved to /recent, so
510
651
  * project/limit/privacy on this route mean a legacy recall caller — return a
511
- * loud 400 rather than silently degrading to an empty context-layer response.
512
- * (A bare GET /context with no params is the legitimate context-layer read and
652
+ * loud 400 rather than silently degrading to an empty identity-layer response.
653
+ * (A bare GET /identity with no params is the legitimate identity-layer read and
513
654
  * is served normally — the two are indistinguishable at the wire, so a
514
655
  * paramless legacy caller is covered by the migration docs, not this guard.)
515
656
  */
516
- function handleContextGet(contextDir, clients, query, contextAgents = {}) {
657
+ function handleIdentityGet(identityDir, clients, query, identityAgents = {}) {
517
658
  if (RECALL_PARAMS.some((p) => p in query)) {
518
659
  return {
519
660
  status: 400,
520
- body: { error: "recall moved to /recent — GET /context now serves the standing context layer (0.12)" },
661
+ body: { error: "recall moved to /recent — GET /identity now serves the standing identity layer (0.12)" },
521
662
  };
522
663
  }
523
664
  const { agentId, error } = extractAgentParam(query);
@@ -526,13 +667,13 @@ function handleContextGet(contextDir, clients, query, contextAgents = {}) {
526
667
  // No agent → the plain global read plus the additive `agents` map the UI
527
668
  // selector needs (backward compatible: existing callers ignore unknown keys).
528
669
  if (agentId === null) {
529
- const { sections, updatedAt } = readSections(contextDir);
670
+ const { sections, updatedAt } = readSectionsWithFallback(identityDir);
530
671
  return {
531
672
  status: 200,
532
- body: { sections, updated_at: updatedAt, clients, agents: listAgents(contextDir, contextAgents) },
673
+ body: { sections, updated_at: updatedAt, clients, agents: listAgents(identityDir, identityAgents) },
533
674
  };
534
675
  }
535
- const resolved = readResolvedSections(contextDir, agentId, contextAgents);
676
+ const resolved = readResolvedSections(identityDir, agentId, identityAgents);
536
677
  const body = {
537
678
  sections: resolved.sections,
538
679
  updated_at: resolved.updatedAt,
@@ -545,11 +686,11 @@ function handleContextGet(contextDir, clients, query, contextAgents = {}) {
545
686
  return { status: 200, body };
546
687
  }
547
688
  /**
548
- * PUT /context. Validates the body shape and section content types, then
689
+ * PUT /identity. Validates the body shape and section content types, then
549
690
  * delegates to writeSections (which owns the name allowlist + atomicity +
550
691
  * symlink safety). Throws are left to the adapter to turn into a 500.
551
692
  */
552
- function handleContextPut(contextDir, body, query = {}, contextAgents = {}) {
693
+ function handleIdentityPut(identityDir, body, query = {}, identityAgents = {}) {
553
694
  const { agentId, error } = extractAgentParam(query);
554
695
  if (error)
555
696
  return { status: 400, body: { error } };
@@ -562,23 +703,23 @@ function handleContextPut(contextDir, body, query = {}, contextAgents = {}) {
562
703
  return { status: 400, body: { error: `Section '${name}' content must be a string` } };
563
704
  }
564
705
  }
565
- const targetDir = agentId === null ? contextDir : (0, node_path_1.join)(contextDir, exports.AGENTS_DIR, agentId);
706
+ const targetDir = agentId === null ? identityDir : (0, node_path_1.join)(identityDir, exports.AGENTS_DIR, agentId);
566
707
  if (agentId !== null) {
567
708
  // A1 write-path guard: never write through a symlinked/non-dir root or leaf
568
709
  // (agentDirState checks both). "absent" is fine — writeSections creates it.
569
- if (agentDirState(contextDir, agentId) === "unsafe") {
570
- return { status: 400, body: { error: "agent context path exists but is not a directory" } };
710
+ if (agentDirState(identityDir, agentId) === "unsafe") {
711
+ return { status: 400, body: { error: "agent identity path exists but is not a directory" } };
571
712
  }
572
713
  // Black-hole guard: if config FORCES off/global for this agent, sections
573
714
  // written under agents/<id>/ could never be served (resolution ignores the
574
715
  // dir). Reject loudly rather than accept a write that silently vanishes.
575
716
  // No config entry → writing creates the dir ⇒ override ⇒ served (allowed).
576
- const configMode = contextAgents[agentId];
717
+ const configMode = identityAgents[agentId];
577
718
  if (configMode === "off" || configMode === "global") {
578
719
  return {
579
720
  status: 409,
580
721
  body: {
581
- error: `config contextAgents['${agentId}']='${configMode}' — sections written here would never be served; ` +
722
+ error: `config identityAgents['${agentId}']='${configMode}' — sections written here would never be served; ` +
582
723
  `set it to 'override' (or remove the entry) first`,
583
724
  },
584
725
  };
@@ -599,10 +740,10 @@ function handleContextPut(contextDir, body, query = {}, contextAgents = {}) {
599
740
  throw err; // real I/O fault → adapter returns 500
600
741
  }
601
742
  }
602
- const resolved = readResolvedSections(contextDir, agentId, contextAgents);
743
+ const resolved = readResolvedSections(identityDir, agentId, identityAgents);
603
744
  const bytes = totalBytes(resolved.sections);
604
- const warn = bytes > exports.CONTEXT_SIZE_WARN_BYTES
605
- ? `Context layer total size ${bytes} bytes exceeds ${exports.CONTEXT_SIZE_WARN_BYTES} — injected into every session; consider trimming.`
745
+ const warn = bytes > exports.IDENTITY_SIZE_WARN_BYTES
746
+ ? `Identity layer total size ${bytes} bytes exceeds ${exports.IDENTITY_SIZE_WARN_BYTES} — injected into every session; consider trimming.`
606
747
  : undefined;
607
748
  const respBody = { ok: true, updated_at: resolved.updatedAt };
608
749
  if (agentId !== null) {
package/dist/index.d.ts CHANGED
@@ -9,13 +9,13 @@
9
9
  * Run server: `npx @gamaze/hicortex init`
10
10
  *
11
11
  * Responsibilities (recall-only adapter, like the Hermes plugin):
12
- * - before_agent_start → GET /context + GET /lessons + POST /recall-index
13
- * (fail-soft, 3s timeout each, concurrent) → inject context. In OpenClaw
14
- * every inbound message spawns an embedded run, so this hook fires PER
15
- * TURN with the current prompt and session id — it is the per-turn
12
+ * - before_agent_start → GET /identity + GET /lessons + POST /recall-index
13
+ * (fail-soft, 3s timeout each, concurrent) → inject identity + lessons. In
14
+ * OpenClaw every inbound message spawns an embedded run, so this hook fires
15
+ * PER TURN with the current prompt and session id — it is the per-turn
16
16
  * /recall-index surface, not just session start.
17
17
  * - after_compaction / before_reset → POST /recall-index {reset:true}
18
- * (context rebuilt → the server's per-session shown-set is stale)
18
+ * (context window rebuilt → the server's per-session shown-set is stale)
19
19
  * - Tools → HTTP proxies to /search, /memory, /recent, /ingest, /lessons
20
20
  *
21
21
  * CAPTURE IS NOT THIS PLUGIN'S JOB. OpenClaw persists sessions at
@@ -23,6 +23,13 @@
23
23
  * machine's Hicortex nightly reads them via oc-transcript-reader.ts —
24
24
  * canonical nightly-from-logs, same as CC JSONL and Hermes state.db.
25
25
  */
26
+ import type { MemorySearchResult } from "./types.js";
27
+ export declare function formatToolResults(results: MemorySearchResult[]): {
28
+ content: Array<{
29
+ type: string;
30
+ text: string;
31
+ }>;
32
+ };
26
33
  declare const _default: {
27
34
  id: string;
28
35
  name: string;