@gamaze/hicortex 0.17.6 → 0.18.0
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 +26 -25
- 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/{lessons-context.d.ts → learnings-identity.d.ts} +32 -21
- package/dist/{lessons-context.js → learnings-identity.js} +50 -39
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +168 -58
- 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
|
@@ -1,54 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* surface for the standing
|
|
5
|
-
* (`/
|
|
3
|
+
* identity-cli — `hicortex identity show|edit`, the secondary/headless edit
|
|
4
|
+
* surface for the standing identity layer (spec 2026-07-12 §6; renamed 0.18
|
|
5
|
+
* #264 from `context`). The Web UI (`/identity/ui`) is primary; this exists
|
|
6
|
+
* for boxes without a browser.
|
|
6
7
|
*
|
|
7
|
-
* hicortex
|
|
8
|
-
* hicortex
|
|
8
|
+
* hicortex identity show [name] GET /identity → print all sections, or one
|
|
9
|
+
* hicortex identity edit <name> GET section → $EDITOR → PUT if changed
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
+
* The legacy `hicortex context ...` command remains as a hidden alias so old
|
|
12
|
+
* scripts and muscle memory keep working (#264 backcompat).
|
|
13
|
+
*
|
|
14
|
+
* URL/token resolution mirrors learnings-identity.ts:44-49 (client mode →
|
|
11
15
|
* config.serverUrl; server mode → http://127.0.0.1:<port>; token from
|
|
12
16
|
* config.authToken) — explicitly NOT the hardcoded 127.0.0.1:8787 of
|
|
13
17
|
* status.ts. Fails soft with a clear message + non-zero exit on any server
|
|
14
18
|
* error, distinguishing a down server from an HTTP error (esp. 404 = server
|
|
15
19
|
* too old / wrong endpoint), like the OC plugin's describeGetFailure.
|
|
20
|
+
*
|
|
21
|
+
* The CLI targets the new `/identity` endpoint. A 0.18+ server (this package)
|
|
22
|
+
* also keeps `/context` as an alias, so a new CLI against a not-yet-upgraded
|
|
23
|
+
* server falls back through that alias via describeFailure's 404 hint.
|
|
16
24
|
*/
|
|
17
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.ContextCliError = void 0;
|
|
19
|
-
exports.
|
|
26
|
+
exports.runContextCommand = exports.getContext = exports.resolveContextTarget = exports.ContextCliError = exports.IdentityCliError = void 0;
|
|
27
|
+
exports.resolveIdentityTarget = resolveIdentityTarget;
|
|
20
28
|
exports.loadConfig = loadConfig;
|
|
21
29
|
exports.sectionChanged = sectionChanged;
|
|
22
|
-
exports.
|
|
30
|
+
exports.getIdentity = getIdentity;
|
|
23
31
|
exports.putSection = putSection;
|
|
24
32
|
exports.formatAllSections = formatAllSections;
|
|
25
33
|
exports.formatOneSection = formatOneSection;
|
|
26
34
|
exports.runEdit = runEdit;
|
|
27
35
|
exports.extractAgentFlag = extractAgentFlag;
|
|
28
|
-
exports.
|
|
36
|
+
exports.runIdentityCommand = runIdentityCommand;
|
|
29
37
|
const node_fs_1 = require("node:fs");
|
|
30
38
|
const node_path_1 = require("node:path");
|
|
31
39
|
const node_os_1 = require("node:os");
|
|
32
40
|
const node_child_process_1 = require("node:child_process");
|
|
33
|
-
const
|
|
41
|
+
const identity_store_js_1 = require("./identity-store.js");
|
|
34
42
|
const paths_js_1 = require("./paths.js");
|
|
35
43
|
const DEFAULT_PORT = 8787;
|
|
36
44
|
const REQUEST_TIMEOUT_MS = 5000;
|
|
37
45
|
/** Thrown for any expected, user-facing failure. cli.ts prints .message + exits 1. */
|
|
38
|
-
class
|
|
46
|
+
class IdentityCliError extends Error {
|
|
39
47
|
}
|
|
40
|
-
exports.
|
|
48
|
+
exports.IdentityCliError = IdentityCliError;
|
|
49
|
+
/** Backcompat alias (#264). */
|
|
50
|
+
exports.ContextCliError = IdentityCliError;
|
|
41
51
|
/**
|
|
42
52
|
* Resolve the server URL + token from a parsed config object. Pure + exported
|
|
43
|
-
* so it is unit-testable without a live config. Follows
|
|
53
|
+
* so it is unit-testable without a live config. Follows learnings-identity.ts.
|
|
44
54
|
*/
|
|
45
|
-
function
|
|
55
|
+
function resolveIdentityTarget(config) {
|
|
46
56
|
const baseUrl = config.mode === "client" && typeof config.serverUrl === "string"
|
|
47
57
|
? config.serverUrl.replace(/\/+$/, "")
|
|
48
58
|
: `http://127.0.0.1:${config.port ?? DEFAULT_PORT}`;
|
|
49
59
|
const authToken = typeof config.authToken === "string" ? config.authToken : undefined;
|
|
50
60
|
return { baseUrl, authToken };
|
|
51
61
|
}
|
|
62
|
+
/** Backcompat alias (#264). */
|
|
63
|
+
exports.resolveContextTarget = resolveIdentityTarget;
|
|
52
64
|
/** Read ~/.hicortex/config.json (or $HICORTEX_HOME/config.json). Missing → {}. */
|
|
53
65
|
function loadConfig() {
|
|
54
66
|
try {
|
|
@@ -62,9 +74,9 @@ function loadConfig() {
|
|
|
62
74
|
function sectionChanged(before, after) {
|
|
63
75
|
return before !== after;
|
|
64
76
|
}
|
|
65
|
-
/** Append `?agent=<id>` to a /
|
|
66
|
-
function
|
|
67
|
-
return `${baseUrl}/
|
|
77
|
+
/** Append `?agent=<id>` to a /identity URL when an agent scope is targeted. */
|
|
78
|
+
function identityUrl(baseUrl, agent) {
|
|
79
|
+
return `${baseUrl}/identity${agent ? `?agent=${encodeURIComponent(agent)}` : ""}`;
|
|
68
80
|
}
|
|
69
81
|
function authHeaders(token) {
|
|
70
82
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
@@ -72,14 +84,15 @@ function authHeaders(token) {
|
|
|
72
84
|
/**
|
|
73
85
|
* Human-readable failure. status === null ⇒ the fetch itself threw (server
|
|
74
86
|
* unreachable). 404 is called out because it means the server predates the
|
|
75
|
-
* /
|
|
87
|
+
* /identity rename (it may still serve the legacy /context alias), not a
|
|
88
|
+
* network fault — mirrors index.ts describeGetFailure.
|
|
76
89
|
*/
|
|
77
90
|
function describeFailure(status, bodyError) {
|
|
78
91
|
const suffix = bodyError ? `: ${bodyError}` : "";
|
|
79
92
|
if (status === null)
|
|
80
93
|
return "server unreachable (connection refused or timed out)";
|
|
81
94
|
if (status === 404)
|
|
82
|
-
return `HTTP 404 — /
|
|
95
|
+
return `HTTP 404 — /identity not found; the server is likely too old (needs 0.18+; pre-0.18 serves /context)${suffix}`;
|
|
83
96
|
if (status === 401)
|
|
84
97
|
return `HTTP 401 — unauthorized; check authToken in config${suffix}`;
|
|
85
98
|
return `server returned HTTP ${status}${suffix}`;
|
|
@@ -93,35 +106,37 @@ async function readBodyError(resp) {
|
|
|
93
106
|
return undefined;
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
|
-
/** GET /
|
|
97
|
-
async function
|
|
109
|
+
/** GET /identity. Throws IdentityCliError with a clear message on any failure. */
|
|
110
|
+
async function getIdentity(target, agent) {
|
|
98
111
|
let resp;
|
|
99
112
|
try {
|
|
100
|
-
resp = await fetch(
|
|
113
|
+
resp = await fetch(identityUrl(target.baseUrl, agent), {
|
|
101
114
|
headers: authHeaders(target.authToken),
|
|
102
115
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
103
116
|
});
|
|
104
117
|
}
|
|
105
118
|
catch {
|
|
106
|
-
throw new
|
|
119
|
+
throw new IdentityCliError(`GET /identity failed — ${describeFailure(null)}`);
|
|
107
120
|
}
|
|
108
121
|
if (!resp.ok) {
|
|
109
|
-
throw new
|
|
122
|
+
throw new IdentityCliError(`GET /identity failed — ${describeFailure(resp.status, await readBodyError(resp))}`);
|
|
110
123
|
}
|
|
111
124
|
const data = (await resp.json());
|
|
112
125
|
// Old-server echo guard: a pre-0.13 server ignores ?agent= and answers 200
|
|
113
126
|
// with the GLOBAL set. Editing off that (then PUTting) would clobber shared
|
|
114
|
-
//
|
|
127
|
+
// identity. Refuse unless the server echoes the agent it was asked for.
|
|
115
128
|
if (agent && data.agent !== agent) {
|
|
116
|
-
throw new
|
|
129
|
+
throw new IdentityCliError(`server did not echo agent '${agent}' — it likely predates per-agent identity (needs 0.13+)`);
|
|
117
130
|
}
|
|
118
131
|
return data;
|
|
119
132
|
}
|
|
120
|
-
/**
|
|
133
|
+
/** Backcompat alias (#264). */
|
|
134
|
+
exports.getContext = getIdentity;
|
|
135
|
+
/** PUT one section. Throws IdentityCliError with a clear message on any failure. */
|
|
121
136
|
async function putSection(target, name, content, agent) {
|
|
122
137
|
let resp;
|
|
123
138
|
try {
|
|
124
|
-
resp = await fetch(
|
|
139
|
+
resp = await fetch(identityUrl(target.baseUrl, agent), {
|
|
125
140
|
method: "PUT",
|
|
126
141
|
headers: { "Content-Type": "application/json", ...authHeaders(target.authToken) },
|
|
127
142
|
body: JSON.stringify({ sections: { [name]: content } }),
|
|
@@ -129,10 +144,10 @@ async function putSection(target, name, content, agent) {
|
|
|
129
144
|
});
|
|
130
145
|
}
|
|
131
146
|
catch {
|
|
132
|
-
throw new
|
|
147
|
+
throw new IdentityCliError(`PUT /identity failed — ${describeFailure(null)}`);
|
|
133
148
|
}
|
|
134
149
|
if (!resp.ok) {
|
|
135
|
-
throw new
|
|
150
|
+
throw new IdentityCliError(`PUT /identity failed — ${describeFailure(resp.status, await readBodyError(resp))}`);
|
|
136
151
|
}
|
|
137
152
|
// Old-server echo guard on the WRITE too: a pre-0.13 server silently accepts
|
|
138
153
|
// the PUT as a GLOBAL write and returns 200 without echoing `agent`. Treat a
|
|
@@ -141,8 +156,8 @@ async function putSection(target, name, content, agent) {
|
|
|
141
156
|
if (agent) {
|
|
142
157
|
const body = (await resp.json().catch(() => ({})));
|
|
143
158
|
if (body.agent !== agent) {
|
|
144
|
-
throw new
|
|
145
|
-
`
|
|
159
|
+
throw new IdentityCliError(`PUT /identity?agent=${agent} — server did not echo the agent; it likely predates per-agent ` +
|
|
160
|
+
`identity (needs 0.13+). It may have written to the GLOBAL scope — verify the server version.`);
|
|
146
161
|
}
|
|
147
162
|
}
|
|
148
163
|
}
|
|
@@ -154,7 +169,7 @@ function formatAllSections(data) {
|
|
|
154
169
|
const names = Object.keys(data.sections);
|
|
155
170
|
const parts = [];
|
|
156
171
|
if (names.length === 0) {
|
|
157
|
-
parts.push("(no
|
|
172
|
+
parts.push("(no identity sections)");
|
|
158
173
|
}
|
|
159
174
|
else {
|
|
160
175
|
for (const name of names) {
|
|
@@ -202,7 +217,7 @@ const defaultSpawn = (file) => {
|
|
|
202
217
|
// Commands
|
|
203
218
|
// ---------------------------------------------------------------------------
|
|
204
219
|
async function runShow(name, agent) {
|
|
205
|
-
const data = await
|
|
220
|
+
const data = await getIdentity(resolveIdentityTarget(loadConfig()), agent);
|
|
206
221
|
if (name) {
|
|
207
222
|
const body = formatOneSection(data, name);
|
|
208
223
|
if (body === null) {
|
|
@@ -221,19 +236,19 @@ async function runShow(name, agent) {
|
|
|
221
236
|
* always cleaned up. `spawn` is injectable for tests.
|
|
222
237
|
*/
|
|
223
238
|
async function runEdit(name, spawn = defaultSpawn, agent) {
|
|
224
|
-
if (!(0,
|
|
225
|
-
throw new
|
|
239
|
+
if (!(0, identity_store_js_1.isValidSectionName)(name)) {
|
|
240
|
+
throw new IdentityCliError(`Invalid section name '${name}'. Must match ^[a-z0-9][a-z0-9_-]*$ (max ${identity_store_js_1.SECTION_NAME_MAX} chars).`);
|
|
226
241
|
}
|
|
227
|
-
const target =
|
|
228
|
-
const data = await
|
|
242
|
+
const target = resolveIdentityTarget(loadConfig());
|
|
243
|
+
const data = await getIdentity(target, agent);
|
|
229
244
|
const before = data.sections[name] ?? "";
|
|
230
|
-
const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "hicortex-
|
|
245
|
+
const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "hicortex-id-"));
|
|
231
246
|
const file = (0, node_path_1.join)(dir, `${name}.md`);
|
|
232
247
|
try {
|
|
233
248
|
(0, node_fs_1.writeFileSync)(file, before, "utf-8");
|
|
234
249
|
const ran = spawn(file);
|
|
235
250
|
if (!ran) {
|
|
236
|
-
throw new
|
|
251
|
+
throw new IdentityCliError("No editor available. Set $EDITOR, or install nano or vi.");
|
|
237
252
|
}
|
|
238
253
|
const after = (0, node_fs_1.readFileSync)(file, "utf-8");
|
|
239
254
|
if (!sectionChanged(before, after)) {
|
|
@@ -261,7 +276,7 @@ function extractAgentFlag(args) {
|
|
|
261
276
|
// A missing value (end of args) or the next token being another flag is a
|
|
262
277
|
// typo — never let it silently fall through to the global scope.
|
|
263
278
|
if (val === undefined || val.startsWith("-")) {
|
|
264
|
-
throw new
|
|
279
|
+
throw new IdentityCliError("--agent requires a value, e.g. --agent alice");
|
|
265
280
|
}
|
|
266
281
|
agent = val;
|
|
267
282
|
i++;
|
|
@@ -271,12 +286,13 @@ function extractAgentFlag(args) {
|
|
|
271
286
|
}
|
|
272
287
|
return { agent, rest };
|
|
273
288
|
}
|
|
274
|
-
/** Dispatch for `hicortex
|
|
275
|
-
|
|
289
|
+
/** Dispatch for `hicortex identity <sub>` (and the hidden `context` alias).
|
|
290
|
+
* Throws IdentityCliError on bad usage/failure. */
|
|
291
|
+
async function runIdentityCommand(args) {
|
|
276
292
|
const { agent, rest } = extractAgentFlag(args);
|
|
277
293
|
// Fast-fail on a bad agent id before any HTTP call (the server enforces too).
|
|
278
|
-
if (agent !== undefined && !(0,
|
|
279
|
-
throw new
|
|
294
|
+
if (agent !== undefined && !(0, identity_store_js_1.isValidAgentId)(agent)) {
|
|
295
|
+
throw new IdentityCliError(`Invalid agent id '${agent}'. Must match ^[a-z0-9][a-z0-9_-]*$ (max ${identity_store_js_1.SECTION_NAME_MAX} chars).`);
|
|
280
296
|
}
|
|
281
297
|
const sub = rest[0];
|
|
282
298
|
switch (sub) {
|
|
@@ -285,10 +301,12 @@ async function runContextCommand(args) {
|
|
|
285
301
|
return;
|
|
286
302
|
case "edit":
|
|
287
303
|
if (!rest[1])
|
|
288
|
-
throw new
|
|
304
|
+
throw new IdentityCliError("Usage: hicortex identity edit <name> [--agent <id>]");
|
|
289
305
|
await runEdit(rest[1], undefined, agent);
|
|
290
306
|
return;
|
|
291
307
|
default:
|
|
292
|
-
throw new
|
|
308
|
+
throw new IdentityCliError("Usage: hicortex identity <show [name] | edit <name>> [--agent <id>]");
|
|
293
309
|
}
|
|
294
310
|
}
|
|
311
|
+
/** Backcompat alias (#264). */
|
|
312
|
+
exports.runContextCommand = runIdentityCommand;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Identity layer — file-backed storage (formerly "Context Layer" / L2; 0.12.0,
|
|
3
|
+
* spec 2026-07-12; renamed 0.18 #264).
|
|
3
4
|
*
|
|
4
|
-
* The
|
|
5
|
-
* as plain files in `<hicortex-home>/
|
|
5
|
+
* The identity layer is hand-edited Markdown ("who you are + how to work") stored
|
|
6
|
+
* as plain files in `<hicortex-home>/identity/*.md` — one file per section, one
|
|
6
7
|
* Web UI tab. It lives OUTSIDE the memories table: never distilled, scored,
|
|
7
8
|
* decayed, or pruned. As plain files this guarantee is structural — no code in
|
|
8
9
|
* consolidate.ts / distiller.ts references this directory.
|
|
9
10
|
*
|
|
10
11
|
* This module is pure file-layer logic (no express) so it is unit-testable and
|
|
11
12
|
* reusable by the CLI. The daemon resolves `<hicortex-home>` exactly as it
|
|
12
|
-
* resolves the DB dir (dirname of the resolved DB path) and passes the
|
|
13
|
+
* resolves the DB dir (dirname of the resolved DB path) and passes the identity
|
|
13
14
|
* dir in — nothing here hardcodes `~`.
|
|
14
15
|
*
|
|
15
16
|
* Security contract (spec §1): client-supplied section names are NEVER joined
|
|
@@ -17,6 +18,14 @@
|
|
|
17
18
|
* itself appends `.md`. Reads skip symlinks (lstat, not stat). Writes validate
|
|
18
19
|
* ALL names before touching the disk (atomic request semantics), go via
|
|
19
20
|
* temp-file-then-rename, and keep a one-generation `<name>.md.bak` undo.
|
|
21
|
+
*
|
|
22
|
+
* Backcompat (#264): the pre-rename directory was `<hicortex-home>/context/`.
|
|
23
|
+
* `migrateIdentityDir` renames it to `identity/` on the next init/nightly run
|
|
24
|
+
* when the old dir exists and the new does not. When BOTH exist (partial
|
|
25
|
+
* migration, a disk-full renameSync failure, or an operator re-creating
|
|
26
|
+
* `context/`), reads ADDITIVELY MERGE: per-section `identity/` wins, and
|
|
27
|
+
* legacy-only sections are included so nothing is lost — at both the global
|
|
28
|
+
* and the per-agent (`agents/<id>/`) level.
|
|
20
29
|
*/
|
|
21
30
|
/** A valid section name: lowercase alnum start, then alnum / `_` / `-`. */
|
|
22
31
|
export declare const SECTION_NAME_RE: RegExp;
|
|
@@ -36,11 +45,11 @@ export declare class InvalidSectionNameError extends Error {
|
|
|
36
45
|
}
|
|
37
46
|
/** Resolution mode for a given agent id (spec §3). */
|
|
38
47
|
export type AgentMode = "override" | "global" | "off";
|
|
39
|
-
/** Reserved subdir under <
|
|
48
|
+
/** Reserved subdir under <identity>/ holding per-agent sections. NOT a section. */
|
|
40
49
|
export declare const AGENTS_DIR = "agents";
|
|
41
50
|
/**
|
|
42
51
|
* Agent ids share the section-name allowlist: they are joined into a filesystem
|
|
43
|
-
* path (`
|
|
52
|
+
* path (`identity/agents/<id>`), so this is the same security contract, not just
|
|
44
53
|
* hygiene.
|
|
45
54
|
*/
|
|
46
55
|
export declare const isValidAgentId: typeof isValidSectionName;
|
|
@@ -72,21 +81,41 @@ export interface AgentIdentity {
|
|
|
72
81
|
* - absent, or empty/whitespace-only → null id, "unset". Empty string == unset
|
|
73
82
|
* everywhere (this is the value `init --agent-name ""` writes-then-clears to
|
|
74
83
|
* opt back out): CC's default is NO `?agent=`, so all CC boxes for one user
|
|
75
|
-
* share the global
|
|
84
|
+
* share the global identity — one user = one identity across machines.
|
|
76
85
|
* There is NO hostname fallback: a hostname-derived default would silently give
|
|
77
|
-
* every machine its own
|
|
86
|
+
* every machine its own identity, the opposite of the shared-identity default.
|
|
78
87
|
*/
|
|
79
88
|
export declare function resolveAgentIdentity(config: Record<string, unknown>): AgentIdentity;
|
|
80
89
|
/**
|
|
81
|
-
* Normalize the raw `
|
|
90
|
+
* Normalize the raw `identityAgents` config value into a map of valid agent id →
|
|
82
91
|
* mode. An entry is kept only when its key passes isValidAgentId AND its value
|
|
83
92
|
* is one of the three modes; everything else is dropped and its key collected
|
|
84
|
-
* for a one-time boot warning (mirrors
|
|
93
|
+
* for a one-time boot warning (mirrors resolveIdentityClients).
|
|
94
|
+
*
|
|
95
|
+
* Backcompat (#264): the pre-rename key was `contextAgents`. Callers that want
|
|
96
|
+
* to honour it should pass the already-resolved value here (see
|
|
97
|
+
* `resolveIdentityConfig` in mcp-server.ts which reads new + old).
|
|
85
98
|
*/
|
|
86
|
-
export declare function
|
|
99
|
+
export declare function resolveIdentityAgents(raw: unknown): {
|
|
87
100
|
agents: Record<string, AgentMode>;
|
|
88
101
|
dropped: string[];
|
|
89
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* Rename `<home>/context/` → `<home>/identity/` when the legacy dir exists and
|
|
105
|
+
* the new dir does not. Idempotent + safe: when both exist (a partial
|
|
106
|
+
* migration, or the operator created both) NOTHING is renamed — the caller
|
|
107
|
+
* falls back to a legacy read instead. Returns a description of what happened
|
|
108
|
+
* for a one-time boot log.
|
|
109
|
+
*
|
|
110
|
+
* Called from init and nightly so the migration lands on the next run of either
|
|
111
|
+
* command an existing install hits.
|
|
112
|
+
*/
|
|
113
|
+
export declare function migrateIdentityDir(home: string): {
|
|
114
|
+
renamed: boolean;
|
|
115
|
+
from: string;
|
|
116
|
+
to: string;
|
|
117
|
+
reason?: string;
|
|
118
|
+
};
|
|
90
119
|
export interface ReadResult {
|
|
91
120
|
sections: Record<string, string>;
|
|
92
121
|
/** ISO timestamp of the latest included file's mtime, or null when none. */
|
|
@@ -95,7 +124,7 @@ export interface ReadResult {
|
|
|
95
124
|
mtimes: Record<string, number>;
|
|
96
125
|
}
|
|
97
126
|
/**
|
|
98
|
-
* Enumerate the
|
|
127
|
+
* Enumerate the identity dir and return the served sections. Only regular files
|
|
99
128
|
* whose basename (sans `.md`) passes the allowlist are included; symlinks are
|
|
100
129
|
* skipped (lstat). `<name>.md.bak` and temp files never match the `*.md` filter.
|
|
101
130
|
*
|
|
@@ -103,6 +132,14 @@ export interface ReadResult {
|
|
|
103
132
|
* null }` and NEVER creates the dir or throws.
|
|
104
133
|
*/
|
|
105
134
|
export declare function readSections(dir: string): ReadResult;
|
|
135
|
+
/**
|
|
136
|
+
* Read the identity dir, additively merging the legacy `context/` sibling so
|
|
137
|
+
* nothing is lost when both dirs exist (post-#264 safety net for a partial/no
|
|
138
|
+
* migration: a disk-full renameSync failure, or an operator re-creating
|
|
139
|
+
* `context/`). Per-section, `identity/` wins; legacy-only sections are included
|
|
140
|
+
* (union). Pure function over the two dirs — used by the GET handler.
|
|
141
|
+
*/
|
|
142
|
+
export declare function readSectionsWithFallback(identityDir: string): ReadResult;
|
|
106
143
|
/**
|
|
107
144
|
* Partial upsert of the named sections. Contract (spec §1):
|
|
108
145
|
* - Validate ALL names first; any invalid → throw InvalidSectionNameError and
|
|
@@ -134,29 +171,55 @@ export declare function writeSections(dir: string, sections: Record<string, stri
|
|
|
134
171
|
/** Total UTF-8 byte size of all sections (used for the >16 KB warn). */
|
|
135
172
|
export declare function totalBytes(sections: Record<string, string>): number;
|
|
136
173
|
/** Warn threshold: this layer bypasses token budgeting and injects every session. */
|
|
174
|
+
export declare const IDENTITY_SIZE_WARN_BYTES = 16384;
|
|
175
|
+
/** Backcompat alias for the pre-rename name (#264). */
|
|
137
176
|
export declare const CONTEXT_SIZE_WARN_BYTES = 16384;
|
|
138
|
-
/** Harness names that may inject the
|
|
177
|
+
/** Harness names that may inject the identity layer. */
|
|
178
|
+
export declare const KNOWN_IDENTITY_CLIENTS: readonly ["cc", "hermes", "oc"];
|
|
179
|
+
/** Backcompat alias for the pre-rename name (#264). */
|
|
139
180
|
export declare const KNOWN_CONTEXT_CLIENTS: readonly ["cc", "hermes", "oc"];
|
|
140
|
-
export interface
|
|
181
|
+
export interface ResolvedIdentityClients {
|
|
141
182
|
/** The resolved, de-duped list of known client names. */
|
|
142
183
|
clients: string[];
|
|
143
184
|
/** Unknown names dropped from an array value (for a one-time boot warning). */
|
|
144
185
|
dropped: string[];
|
|
145
186
|
}
|
|
187
|
+
/** Backcompat alias for the pre-rename name (#264). */
|
|
188
|
+
export type ResolvedContextClients = ResolvedIdentityClients;
|
|
146
189
|
/**
|
|
147
|
-
* Normalize the raw `
|
|
190
|
+
* Normalize the raw `identityClients` config value (spec §2):
|
|
148
191
|
* - `"all"` (any case) → ["cc","hermes","oc"]
|
|
149
192
|
* - array → lowercase, keep known names (de-duped), collect dropped unknowns
|
|
150
193
|
* - missing / non-array-non-"all" → default ["cc"]
|
|
151
|
-
* The resolved list is echoed by GET /
|
|
194
|
+
* The resolved list is echoed by GET /identity as `clients` so each harness's
|
|
152
195
|
* hook can self-gate without its own config.
|
|
153
196
|
*/
|
|
154
|
-
export declare function
|
|
197
|
+
export declare function resolveIdentityClients(raw: unknown): ResolvedIdentityClients;
|
|
198
|
+
/**
|
|
199
|
+
* Read the identity-clients config value from a raw config object, honouring
|
|
200
|
+
* the new key (`identityClients`) and falling back to the legacy
|
|
201
|
+
* `contextClients` (#264) with a one-time deprecation signal. Returns the
|
|
202
|
+
* resolved clients, dropped unknowns, and whether the value came from the
|
|
203
|
+
* legacy key.
|
|
204
|
+
*/
|
|
205
|
+
export declare function resolveIdentityClientsConfig(config: Record<string, unknown> | undefined | null): ResolvedIdentityClients & {
|
|
206
|
+
legacy: boolean;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Read the identity-agents config value from a raw config object, honouring
|
|
210
|
+
* the new key (`identityAgents`) and falling back to the legacy
|
|
211
|
+
* `contextAgents` (#264) with a one-time deprecation signal.
|
|
212
|
+
*/
|
|
213
|
+
export declare function resolveIdentityAgentsConfig(config: Record<string, unknown> | undefined | null): {
|
|
214
|
+
agents: Record<string, AgentMode>;
|
|
215
|
+
dropped: string[];
|
|
216
|
+
legacy: boolean;
|
|
217
|
+
};
|
|
155
218
|
/**
|
|
156
|
-
* Classify the on-disk state of `
|
|
219
|
+
* Classify the on-disk state of `identity/agents/<id>`, hardened against a
|
|
157
220
|
* symlink planted at EITHER the `agents` ROOT or the `<id>` leaf — lstat/readdir
|
|
158
221
|
* follow a symlinked intermediate path component, so checking only the leaf
|
|
159
|
-
* would let a symlinked root escape the
|
|
222
|
+
* would let a symlinked root escape the identity root entirely. BOTH must be
|
|
160
223
|
* real, non-symlink directories.
|
|
161
224
|
* - "absent" — the root or the leaf does not exist (fresh install / no dir for
|
|
162
225
|
* this agent). A normal, safe "no agent sections" case.
|
|
@@ -167,24 +230,24 @@ export declare function resolveContextClients(raw: unknown): ResolvedContextClie
|
|
|
167
230
|
* rethrow (a permissions fault must never masquerade as "empty", matching
|
|
168
231
|
* readSections). The single source of truth for the four call sites below.
|
|
169
232
|
*/
|
|
170
|
-
export declare function agentDirState(
|
|
233
|
+
export declare function agentDirState(identityDir: string, agentId: string): "absent" | "real" | "unsafe";
|
|
171
234
|
/**
|
|
172
235
|
* Resolve the mode for a request `?agent=<id>` (spec §3):
|
|
173
236
|
* 1. config entry present → that mode;
|
|
174
|
-
* 2. else `
|
|
237
|
+
* 2. else `identity/agents/<id>/` present as a REAL directory (root + leaf both
|
|
175
238
|
* real, non-symlink) → "override" (dropping in a dir is intent, no config);
|
|
176
239
|
* 3. else → "global".
|
|
177
240
|
* A symlinked/non-dir root or leaf never counts as "present" (agentDirState).
|
|
178
241
|
*/
|
|
179
|
-
export declare function resolveAgentMode(
|
|
242
|
+
export declare function resolveAgentMode(identityDir: string, agentId: string, identityAgents: Record<string, AgentMode>): AgentMode;
|
|
180
243
|
/**
|
|
181
244
|
* Enumerate the agents the UI selector should offer: the union of allowlisted
|
|
182
|
-
* REAL directories under `
|
|
245
|
+
* REAL directories under `identity/agents/` and every configured `identityAgents`
|
|
183
246
|
* key, each mapped to its resolved mode (config wins over presence). A missing
|
|
184
247
|
* agents dir (fresh install) yields the config keys only; a symlinked/non-dir
|
|
185
248
|
* agents root is treated as "no dirs" (config keys only).
|
|
186
249
|
*/
|
|
187
|
-
export declare function listAgents(
|
|
250
|
+
export declare function listAgents(identityDir: string, identityAgents: Record<string, AgentMode>): Record<string, AgentMode>;
|
|
188
251
|
export interface ResolvedRead {
|
|
189
252
|
sections: Record<string, string>;
|
|
190
253
|
updatedAt: string | null;
|
|
@@ -202,7 +265,7 @@ export interface ResolvedRead {
|
|
|
202
265
|
* reading (A1): a symlinked or non-directory `agents/<id>` contributes NO agent
|
|
203
266
|
* sections regardless of how override was selected (config or presence).
|
|
204
267
|
*/
|
|
205
|
-
export declare function readResolvedSections(
|
|
268
|
+
export declare function readResolvedSections(identityDir: string, agentId: string | null, identityAgents: Record<string, AgentMode>): ResolvedRead;
|
|
206
269
|
export interface HandlerResult {
|
|
207
270
|
status: number;
|
|
208
271
|
body: unknown;
|
|
@@ -210,17 +273,17 @@ export interface HandlerResult {
|
|
|
210
273
|
warn?: string;
|
|
211
274
|
}
|
|
212
275
|
/**
|
|
213
|
-
* GET /
|
|
276
|
+
* GET /identity. Stale-client tripwire first: recall moved to /recent, so
|
|
214
277
|
* project/limit/privacy on this route mean a legacy recall caller — return a
|
|
215
|
-
* loud 400 rather than silently degrading to an empty
|
|
216
|
-
* (A bare GET /
|
|
278
|
+
* loud 400 rather than silently degrading to an empty identity-layer response.
|
|
279
|
+
* (A bare GET /identity with no params is the legitimate identity-layer read and
|
|
217
280
|
* is served normally — the two are indistinguishable at the wire, so a
|
|
218
281
|
* paramless legacy caller is covered by the migration docs, not this guard.)
|
|
219
282
|
*/
|
|
220
|
-
export declare function
|
|
283
|
+
export declare function handleIdentityGet(identityDir: string, clients: string[], query: Record<string, unknown>, identityAgents?: Record<string, AgentMode>): HandlerResult;
|
|
221
284
|
/**
|
|
222
|
-
* PUT /
|
|
285
|
+
* PUT /identity. Validates the body shape and section content types, then
|
|
223
286
|
* delegates to writeSections (which owns the name allowlist + atomicity +
|
|
224
287
|
* symlink safety). Throws are left to the adapter to turn into a 500.
|
|
225
288
|
*/
|
|
226
|
-
export declare function
|
|
289
|
+
export declare function handleIdentityPut(identityDir: string, body: unknown, query?: Record<string, unknown>, identityAgents?: Record<string, AgentMode>): HandlerResult;
|