@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
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
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
|
|
6
|
-
* as plain files in `<hicortex-home>/
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
37
|
-
exports.
|
|
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 <
|
|
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 (`
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 —
|
|
388
|
+
// Config — identityClients normalization
|
|
303
389
|
// ---------------------------------------------------------------------------
|
|
304
|
-
/** Harness names that may inject the
|
|
305
|
-
exports.
|
|
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 `
|
|
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 /
|
|
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
|
|
402
|
+
function resolveIdentityClients(raw) {
|
|
315
403
|
if (typeof raw === "string" && raw.toLowerCase() === "all") {
|
|
316
|
-
return { clients: [...exports.
|
|
404
|
+
return { clients: [...exports.KNOWN_IDENTITY_CLIENTS], dropped: [] };
|
|
317
405
|
}
|
|
318
406
|
if (Array.isArray(raw)) {
|
|
319
|
-
const known = new Set(exports.
|
|
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.
|
|
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 `
|
|
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
|
|
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(
|
|
368
|
-
const root = (0, node_path_1.join)(
|
|
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 `
|
|
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(
|
|
402
|
-
const configured =
|
|
519
|
+
function resolveAgentMode(identityDir, agentId, identityAgents) {
|
|
520
|
+
const configured = identityAgents[agentId];
|
|
403
521
|
if (configured)
|
|
404
522
|
return configured;
|
|
405
|
-
|
|
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 `
|
|
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(
|
|
558
|
+
function listAgents(identityDir, identityAgents) {
|
|
415
559
|
const result = {};
|
|
416
|
-
const agentsRoot = (0, node_path_1.join)(
|
|
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(
|
|
434
|
-
result[entry] =
|
|
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(
|
|
582
|
+
for (const id of Object.keys(identityAgents)) {
|
|
439
583
|
if (!(id in result))
|
|
440
|
-
result[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(
|
|
596
|
+
function readResolvedSections(identityDir, agentId, identityAgents) {
|
|
453
597
|
if (agentId === null) {
|
|
454
|
-
const g =
|
|
598
|
+
const g = readSectionsWithFallback(identityDir);
|
|
455
599
|
return { sections: g.sections, updatedAt: g.updatedAt };
|
|
456
600
|
}
|
|
457
|
-
const mode = resolveAgentMode(
|
|
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 =
|
|
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
|
|
466
|
-
//
|
|
467
|
-
|
|
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 /
|
|
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 /
|
|
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
|
|
512
|
-
* (A bare GET /
|
|
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
|
|
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 /
|
|
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 } =
|
|
670
|
+
const { sections, updatedAt } = readSectionsWithFallback(identityDir);
|
|
530
671
|
return {
|
|
531
672
|
status: 200,
|
|
532
|
-
body: { sections, updated_at: updatedAt, clients, agents: listAgents(
|
|
673
|
+
body: { sections, updated_at: updatedAt, clients, agents: listAgents(identityDir, identityAgents) },
|
|
533
674
|
};
|
|
534
675
|
}
|
|
535
|
-
const resolved = readResolvedSections(
|
|
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 /
|
|
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
|
|
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 ?
|
|
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(
|
|
570
|
-
return { status: 400, body: { error: "agent
|
|
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 =
|
|
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
|
|
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(
|
|
743
|
+
const resolved = readResolvedSections(identityDir, agentId, identityAgents);
|
|
603
744
|
const bytes = totalBytes(resolved.sections);
|
|
604
|
-
const warn = bytes > exports.
|
|
605
|
-
? `
|
|
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 /
|
|
13
|
-
* (fail-soft, 3s timeout each, concurrent) → inject
|
|
14
|
-
* every inbound message spawns an embedded run, so this hook fires
|
|
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;
|