@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
package/dist/viz.js CHANGED
@@ -26,19 +26,20 @@
26
26
  * construction. Public like the /viz shell (static public code, no data).
27
27
  */
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.VIZ_VENDOR_FILES = void 0;
29
+ exports.contextUiHandler = exports.readContextHtml = exports.resolveContextHtmlPath = exports.VIZ_VENDOR_FILES = void 0;
30
30
  exports.createAuthMiddleware = createAuthMiddleware;
31
31
  exports.resolveVizHtmlPath = resolveVizHtmlPath;
32
32
  exports.readVizHtml = readVizHtml;
33
33
  exports.vizHandler = vizHandler;
34
- exports.resolveContextHtmlPath = resolveContextHtmlPath;
35
- exports.readContextHtml = readContextHtml;
36
- exports.contextUiHandler = contextUiHandler;
34
+ exports.resolveIdentityHtmlPath = resolveIdentityHtmlPath;
35
+ exports.readIdentityHtml = readIdentityHtml;
36
+ exports.identityUiHandler = identityUiHandler;
37
37
  exports.resolveDashboardHtmlPath = resolveDashboardHtmlPath;
38
38
  exports.readDashboardHtml = readDashboardHtml;
39
39
  exports.dashboardHandler = dashboardHandler;
40
40
  exports.resolveVizVendorPath = resolveVizVendorPath;
41
41
  exports.vizVendorHandler = vizVendorHandler;
42
+ const node_crypto_1 = require("node:crypto");
42
43
  const node_fs_1 = require("node:fs");
43
44
  const node_path_1 = require("node:path");
44
45
  // ---------------------------------------------------------------------------
@@ -58,14 +59,42 @@ exports.VIZ_VENDOR_FILES = new Set([
58
59
  // ---------------------------------------------------------------------------
59
60
  // Auth middleware
60
61
  // ---------------------------------------------------------------------------
62
+ /**
63
+ * Constant-time bearer comparison (#254). Compares the FULL `Authorization`
64
+ * header (not just the token portion) against the expected `Bearer <token>`
65
+ * form. This couples the scheme + token into one fixed-length secret, so a
66
+ * caller cannot learn anything about the token by varying the scheme prefix.
67
+ *
68
+ * `crypto.timingSafeEqual` THROWS RangeError on mismatched buffer lengths, so
69
+ * the length check MUST gate the call. The short-circuit on `a.length !==
70
+ * b.length` leaks only the header LENGTH — which is already visible on the
71
+ * wire (HTTP header sizes are not secret); the secret token bytes are never
72
+ * compared byte-by-byte through a timing side channel.
73
+ */
74
+ function safeBearerMatch(headerValue, expectedToken) {
75
+ if (!headerValue)
76
+ return false;
77
+ const a = Buffer.from(headerValue);
78
+ const b = Buffer.from(`Bearer ${expectedToken}`);
79
+ return a.length === b.length && (0, node_crypto_1.timingSafeEqual)(a, b);
80
+ }
61
81
  /**
62
82
  * Bearer token auth — ALWAYS installed, fail-closed. /health, the /viz page
63
83
  * shell, and localhost bypass (OPTIONS is short-circuited by the CORS
64
84
  * middleware before this runs). With no token configured, remote requests are
65
85
  * REJECTED (not open): the default bind is 0.0.0.0, so "no token = no auth"
66
86
  * would expose the whole memory store to the network.
87
+ *
88
+ * `authTokenPrevious` (optional, #254) is the prior token kept around during
89
+ * rotation. Both tokens are accepted; this gives a zero-downtime rotation
90
+ * window — in-flight clients configured with the old token keep working until
91
+ * they pick up the new one. BOTH comparisons are constant-time and both are
92
+ * always evaluated (no short-circuit), so a caller cannot learn WHICH token
93
+ * matched from the response timing. Absent/empty `authTokenPrevious` behaves
94
+ * exactly as the single-token middleware always has.
67
95
  */
68
- function createAuthMiddleware(authToken) {
96
+ function createAuthMiddleware(authToken, authTokenPrevious) {
97
+ const previous = authTokenPrevious && authTokenPrevious.length > 0 ? authTokenPrevious : undefined;
69
98
  return (req, res, next) => {
70
99
  if (req.path === "/health")
71
100
  return next();
@@ -76,13 +105,15 @@ function createAuthMiddleware(authToken) {
76
105
  // normal Authorization header on its data fetches.
77
106
  if (req.method === "GET" && req.path === "/viz")
78
107
  return next();
79
- // The /context/ui page SHELL is public for the same reason as /viz: a
108
+ // The /identity/ui page SHELL is public for the same reason as /viz: a
80
109
  // self-contained static editor page, no data and no secrets (it ships
81
- // verbatim in the npm tarball). The standing-context DATA it edits comes
82
- // from GET/PUT /context, which stay bearer-only (localhost bypass) like
110
+ // verbatim in the npm tarball). The standing-identity DATA it edits comes
111
+ // from GET/PUT /identity, which stay bearer-only (localhost bypass) like
83
112
  // every other data route; the page collects the token client-side and
84
- // sends it as a normal Authorization header on its /context fetches.
85
- if (req.method === "GET" && req.path === "/context/ui")
113
+ // sends it as a normal Authorization header on its /identity fetches.
114
+ // #264 backcompat: the legacy /context/ui URL is also exempted (the alias
115
+ // route serves the same shell).
116
+ if (req.method === "GET" && (req.path === "/identity/ui" || req.path === "/context/ui"))
86
117
  return next();
87
118
  // The /dashboard page SHELL is public for the same reason as /viz and
88
119
  // /context/ui: a self-contained view-only analytics page, no data and no
@@ -113,10 +144,20 @@ function createAuthMiddleware(authToken) {
113
144
  const ip = req.ip ?? req.socket.remoteAddress ?? "";
114
145
  if (ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1")
115
146
  return next();
116
- if (authToken && req.headers.authorization === `Bearer ${authToken}`)
147
+ // Constant-time bearer check (#254). When authTokenPrevious is set, BOTH
148
+ // tokens are compared every request (no short-circuit) so timing cannot
149
+ // reveal which one matched. The OR of the two booleans is the accept
150
+ // signal — evaluated after both comparisons complete.
151
+ const header = req.headers.authorization;
152
+ const matchesCurrent = authToken ? safeBearerMatch(header, authToken) : false;
153
+ const matchesPrevious = previous ? safeBearerMatch(header, previous) : false;
154
+ if (matchesCurrent || matchesPrevious)
117
155
  return next();
118
156
  res.status(401).json({
119
- error: authToken
157
+ // Gate on (authToken || previous): in the mixed-config edge case
158
+ // (authToken unset, authTokenPrevious set) the middleware still accepts
159
+ // the previous token — so "No auth token configured" would mislead.
160
+ error: (authToken || previous)
120
161
  ? "Unauthorized"
121
162
  : "No auth token configured on this server — run `npx @gamaze/hicortex init` on the server, then connect with its token.",
122
163
  });
@@ -154,54 +195,69 @@ function vizHandler() {
154
195
  res.type("html").send(readVizHtml());
155
196
  }
156
197
  catch (err) {
157
- res.status(503).json({ error: err instanceof Error ? err.message : String(err) });
198
+ // /viz is a PUBLIC, UNAUTHENTICATED route the catch must NOT echo the
199
+ // filesystem path of the failed readFileSync to an anonymous remote
200
+ // caller (#253). Log full detail server-side, return a generic body.
201
+ const detail = err instanceof Error ? (err.stack ?? `${err.name}: ${err.message}`) : String(err);
202
+ console.error(`[hicortex] /viz: ${detail}`);
203
+ res.status(503).json({ error: "Asset unavailable" });
158
204
  }
159
205
  };
160
206
  }
161
207
  // ---------------------------------------------------------------------------
162
- // Context layer editor page (/context/ui, 0.12 — spec 2026-07-12 §5)
208
+ // Identity layer editor page (/identity/ui, 0.12 — spec 2026-07-12 §5; renamed
209
+ // from /context/ui in 0.18 #264)
163
210
  // ---------------------------------------------------------------------------
164
211
  /**
165
- * Resolve the on-disk path of the context-layer editor page. Throws (fail
212
+ * Resolve the on-disk path of the identity-layer editor page. Throws (fail
166
213
  * explicitly) when the asset is missing — same contract as resolveVizHtmlPath.
167
214
  * assets/ sits next to both dist/ (dist/viz.js → ../assets/) and src/
168
215
  * (src/viz.ts → ../assets/ under tsx), so one sibling candidate covers both.
169
216
  */
170
- function resolveContextHtmlPath() {
171
- const candidates = [(0, node_path_1.join)(__dirname, "..", "assets", "context.html")];
217
+ function resolveIdentityHtmlPath() {
218
+ const candidates = [(0, node_path_1.join)(__dirname, "..", "assets", "identity.html")];
172
219
  for (const candidate of candidates) {
173
220
  if ((0, node_fs_1.existsSync)(candidate))
174
221
  return candidate;
175
222
  }
176
- throw new Error(`context.html asset not found — looked in: ${candidates.join(", ")}. ` +
223
+ throw new Error(`identity.html asset not found — looked in: ${candidates.join(", ")}. ` +
177
224
  `The package install is incomplete (assets/ missing).`);
178
225
  }
179
- /** Read the context editor page. Read at request time so a reinstall is live. */
180
- function readContextHtml() {
181
- return (0, node_fs_1.readFileSync)(resolveContextHtmlPath(), "utf-8");
226
+ /** Read the identity editor page. Read at request time so a reinstall is live. */
227
+ function readIdentityHtml() {
228
+ return (0, node_fs_1.readFileSync)(resolveIdentityHtmlPath(), "utf-8");
182
229
  }
183
230
  /**
184
- * Express handler for GET /context/ui — the PRIMARY edit surface for the
185
- * standing context layer. 503 with the usual {error} shape when the asset
186
- * cannot be read, exactly like vizHandler.
231
+ * Express handler for GET /identity/ui — the PRIMARY edit surface for the
232
+ * standing identity layer. 503 with the usual {error} shape when the asset
233
+ * cannot be read, exactly like vizHandler. Also serves the legacy
234
+ * /context/ui URL (#264 backcompat).
187
235
  */
188
- function contextUiHandler() {
236
+ function identityUiHandler() {
189
237
  return (_req, res) => {
190
238
  try {
191
- res.type("html").send(readContextHtml());
239
+ res.type("html").send(readIdentityHtml());
192
240
  }
193
241
  catch (err) {
194
- res.status(503).json({ error: err instanceof Error ? err.message : String(err) });
242
+ // /identity/ui is a PUBLIC, UNAUTHENTICATED route (#253) — same
243
+ // sanitisation as vizHandler: log detail server-side only.
244
+ const detail = err instanceof Error ? (err.stack ?? `${err.name}: ${err.message}`) : String(err);
245
+ console.error(`[hicortex] /identity/ui: ${detail}`);
246
+ res.status(503).json({ error: "Asset unavailable" });
195
247
  }
196
248
  };
197
249
  }
250
+ /** Backcompat aliases (#264). */
251
+ exports.resolveContextHtmlPath = resolveIdentityHtmlPath;
252
+ exports.readContextHtml = readIdentityHtml;
253
+ exports.contextUiHandler = identityUiHandler;
198
254
  // ---------------------------------------------------------------------------
199
255
  // Dashboard page (/dashboard, #224 — view-only memory analytics)
200
256
  // ---------------------------------------------------------------------------
201
257
  /**
202
258
  * Resolve the on-disk path of the dashboard page. Throws (fail explicitly)
203
259
  * when the asset is missing — same contract as resolveVizHtmlPath and
204
- * resolveContextHtmlPath. assets/ sits next to both dist/ and src/ (the
260
+ * resolveIdentityHtmlPath. assets/ sits next to both dist/ and src/ (the
205
261
  * sibling layout the other resolvers rely on).
206
262
  */
207
263
  function resolveDashboardHtmlPath() {
@@ -220,7 +276,7 @@ function readDashboardHtml() {
220
276
  /**
221
277
  * Express handler for GET /dashboard — the view-only analytics page (#224).
222
278
  * 503 with the usual {error} shape when the asset cannot be read, exactly like
223
- * vizHandler and contextUiHandler. The page SHELL is public (exempted in
279
+ * vizHandler and identityUiHandler. The page SHELL is public (exempted in
224
280
  * createAuthMiddleware); all data comes from GET /dashboard/data (bearer-only).
225
281
  */
226
282
  function dashboardHandler() {
@@ -229,7 +285,11 @@ function dashboardHandler() {
229
285
  res.type("html").send(readDashboardHtml());
230
286
  }
231
287
  catch (err) {
232
- res.status(503).json({ error: err instanceof Error ? err.message : String(err) });
288
+ // /dashboard is a PUBLIC, UNAUTHENTICATED route (#253) — same
289
+ // sanitisation as vizHandler: log detail server-side only.
290
+ const detail = err instanceof Error ? (err.stack ?? `${err.name}: ${err.message}`) : String(err);
291
+ console.error(`[hicortex] /dashboard: ${detail}`);
292
+ res.status(503).json({ error: "Asset unavailable" });
233
293
  }
234
294
  };
235
295
  }
@@ -268,9 +328,14 @@ function vizVendorHandler() {
268
328
  res.send(body);
269
329
  }
270
330
  catch (err) {
331
+ // /viz/vendor/* is a PUBLIC, UNAUTHENTICATED route (#253) — same
332
+ // sanitisation as the other public asset routes: log detail server-side
333
+ // only, return a generic body. The allowlisted filename is fine to echo
334
+ // (it came from the fixed VIZ_VENDOR_FILES table, not the filesystem).
335
+ const detail = err instanceof Error ? (err.stack ?? `${err.name}: ${err.message}`) : String(err);
336
+ console.error(`[hicortex] /viz/vendor/${file}: ${detail}`);
271
337
  res.status(503).json({
272
- error: `Vendor asset ${file} is missing — the package install is incomplete ` +
273
- `(assets/vendor/ missing): ${err instanceof Error ? err.message : String(err)}`,
338
+ error: `Vendor asset ${file} unavailable — the package install is incomplete (assets/vendor/ missing)`,
274
339
  });
275
340
  }
276
341
  };
@@ -22,12 +22,14 @@ That's the whole surface. No `sync_turn`, no compaction/session-end capture —
22
22
 
23
23
  ### Pushed recall index (0.7.0, server ≥ 0.14)
24
24
 
25
- Instead of injecting full memory content every turn, `prefetch` sends the user's message to the server's `POST /recall-index` and injects the returned **index block** verbatim — one line per memory (id, title, date), capped and relevance-gated server-side. The agent fetches full content with `hicortex_get(id)` only when a line is actually relevant; that fetch is what strengthens the memory (exposure ≠ use). All tuning knobs (`recallMaxItems`, `recallMinSimilarity`, `recallReshowTurns`, `recallMinPromptChars`, …) live in the **server** config — the plugin carries none. Dedup is turn-based and server-side per session; the plugin resets it at `initialize` (the Hermes `MemoryProvider` interface exposes no compaction signal, so a mid-session context rebuild cannot trigger a reset — the server's turn-based re-show window covers that gap). Against a pre-0.14 server (404) the plugin falls back to the 0.6.x `GET /search` full-content prefetch, fail-soft, re-probing the endpoint every 10 minutes so a later server upgrade is picked up without a gateway restart. The recall calls carry the profile's configured `default_project` (and `mission_domains`) and use a short dedicated timeout (1.5 s) so a slow server can never stall a turn. (`privacy_filter` is deprecated since 0.7.2 — the server ignores privacy; see [Configuration](#configuration).)
25
+ Instead of injecting full memory content every turn, `prefetch` sends the user's message to the server's `POST /recall-index` and injects the returned **index block** verbatim — one line per memory (id, title, date), capped and relevance-gated server-side. The agent fetches full content with `hicortex_get(id)` only when a line is actually relevant; that fetch is what strengthens the memory (exposure ≠ use). All tuning knobs (`recallMaxItems`, `recallMinSimilarity`, `recallReshowTurns`, `recallMinPromptChars`, …) live in the **server** config — the plugin carries none. Dedup is turn-based and server-side per session; the plugin resets it at `initialize` (the Hermes `MemoryProvider` interface exposes no compaction signal, so a mid-session context rebuild cannot trigger a reset — the server's turn-based re-show window covers that gap). Against a pre-0.14 server (404) the plugin falls back to the 0.6.x `GET /search` full-content prefetch, fail-soft, re-probing the endpoint every 10 minutes so a later server upgrade is picked up without a gateway restart. The recall calls carry the profile's configured `default_project` (and `mission_domains`) and use a short dedicated timeout (1.5 s) so a slow server can never stall a turn. (`privacy_filter` is deprecated since 0.7.2 — the server ignores privacy; see [Configuration](#configure-activate).)
26
26
 
27
27
  ### Per-agent standing context (0.13)
28
28
 
29
29
  `system_prompt_block()` also injects the hand-edited **standing context layer** (`## Context`, above the lessons block) — "who you are + how to work", distinct from episodic memory. The server resolves it **per agent**: this profile's own sections override the global set (`override`), or it can be `global` or `off`. See the main repo's `/context` layer docs.
30
30
 
31
+ > **Note (#264 rename):** the server-side layer was renamed Context → Identity in 0.18. The `/context` endpoint remains as an alias so this plugin keeps working unchanged; the heading is still rendered as `## Context` here and will switch to `## Identity` in a follow-up plugin release. No action needed.
32
+
31
33
  The plugin sends its **profile name** as `?agent=`, resolved in this order:
32
34
 
33
35
  1. `agent_name` in the plugin config (explicit override);
@@ -79,7 +81,7 @@ Run it once per profile if you use Hermes profiles. Hermes allows **one** extern
79
81
  Config fields (`hicortex_url`, `default_project`, `recall_limit`, `privacy_filter`, `agent_name`) can also be written to `$HERMES_HOME/plugins/hicortex/config.json` directly. `agent_name` pins the per-agent context id for this profile (leave blank to auto-derive — see [Per-agent standing context](#per-agent-standing-context-013)). The auth token is a **secret** — set it via env, not the JSON file:
80
82
 
81
83
  ```bash
82
- export HICORTEX_AUTH_TOKEN=hctx-default-token # or your custom token
84
+ export HICORTEX_AUTH_TOKEN=hctx-<your-token> # or your custom token
83
85
  ```
84
86
 
85
87
  Env overrides: `HICORTEX_URL`, `HICORTEX_AUTH_TOKEN`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.17.6",
4
- "description": "Self-learning memory for AI agents — experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
3
+ "version": "0.18.1",
4
+ "description": "Persistent agent identity for AI agents — a hand-edited identity layer, nightly-distilled experience, and lessons injected every session, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "hicortex": "dist/cli.js"
@@ -1,69 +0,0 @@
1
- /**
2
- * context-cli — `hicortex context show|edit`, the secondary/headless edit
3
- * surface for the standing context layer (spec 2026-07-12 §6). The Web UI
4
- * (`/context/ui`) is primary; this exists for boxes without a browser.
5
- *
6
- * hicortex context show [name] GET /context → print all sections, or one
7
- * hicortex context edit <name> GET section → $EDITOR → PUT if changed
8
- *
9
- * URL/token resolution mirrors lessons-context.ts:44-49 (client mode →
10
- * config.serverUrl; server mode → http://127.0.0.1:<port>; token from
11
- * config.authToken) — explicitly NOT the hardcoded 127.0.0.1:8787 of
12
- * status.ts. Fails soft with a clear message + non-zero exit on any server
13
- * error, distinguishing a down server from an HTTP error (esp. 404 = server
14
- * too old / wrong endpoint), like the OC plugin's describeGetFailure.
15
- */
16
- /** Thrown for any expected, user-facing failure. cli.ts prints .message + exits 1. */
17
- export declare class ContextCliError extends Error {
18
- }
19
- export interface ContextServerTarget {
20
- baseUrl: string;
21
- authToken?: string;
22
- }
23
- /**
24
- * Resolve the server URL + token from a parsed config object. Pure + exported
25
- * so it is unit-testable without a live config. Follows lessons-context.ts.
26
- */
27
- export declare function resolveContextTarget(config: Record<string, unknown>): ContextServerTarget;
28
- /** Read ~/.hicortex/config.json (or $HICORTEX_HOME/config.json). Missing → {}. */
29
- export declare function loadConfig(): Record<string, unknown>;
30
- /** The Save decision: PUT only when the edited content differs. Pure + tested. */
31
- export declare function sectionChanged(before: string, after: string): boolean;
32
- export interface ContextGetResponse {
33
- sections: Record<string, string>;
34
- updated_at: string | null;
35
- clients: string[];
36
- /** Present only for an agent-scoped read (0.13). */
37
- agent?: string;
38
- mode?: string;
39
- }
40
- /** GET /context. Throws ContextCliError with a clear message on any failure. */
41
- export declare function getContext(target: ContextServerTarget, agent?: string): Promise<ContextGetResponse>;
42
- /** PUT one section. Throws ContextCliError with a clear message on any failure. */
43
- export declare function putSection(target: ContextServerTarget, name: string, content: string, agent?: string): Promise<void>;
44
- /** Readable rendering of every section + the resolved clients line (show, no name). */
45
- export declare function formatAllSections(data: ContextGetResponse): string;
46
- /** Raw markdown for one section (show <name>), or null if it does not exist. */
47
- export declare function formatOneSection(data: ContextGetResponse, name: string): string | null;
48
- /**
49
- * Default editor spawn: opens `file` in the first available editor, blocking
50
- * until it exits. Returns true if an editor ran, false if none was found.
51
- * Injectable so tests never actually spawn an editor.
52
- */
53
- export type EditorSpawn = (file: string) => boolean;
54
- /**
55
- * `edit <name>`: validate name (fast-fail; the server enforces too) → fetch
56
- * current content → $EDITOR on a temp file → PUT only if changed. Temp file is
57
- * always cleaned up. `spawn` is injectable for tests.
58
- */
59
- export declare function runEdit(name: string, spawn?: EditorSpawn, agent?: string): Promise<void>;
60
- /**
61
- * Split out a `--agent <id>` flag (anywhere in argv) from the positional args.
62
- * The flag omitted → the global scope.
63
- */
64
- export declare function extractAgentFlag(args: string[]): {
65
- agent?: string;
66
- rest: string[];
67
- };
68
- /** Dispatch for `hicortex context <sub>`. Throws ContextCliError on bad usage/failure. */
69
- export declare function runContextCommand(args: string[]): Promise<void>;
@@ -1,102 +0,0 @@
1
- /**
2
- * lessons-context — query-time context injection for the CC SessionStart hook.
3
- *
4
- * Replaces file-based injection (injectLessons / injectLessonsFromServer).
5
- * Reads ~/.hicortex/config.json to find the server URL, then fetches TWO
6
- * endpoints concurrently and prints a compact Markdown block to stdout so CC
7
- * picks it up as session context:
8
- *
9
- * GET /context → the standing context layer (user info + rules; 0.12).
10
- * Injected as a `## Context` block ONLY when this harness
11
- * ("cc") is in the server-resolved `clients` list (self-gate).
12
- * GET /lessons → episodic memory lessons + memory index, rendered as the
13
- * existing `## Hicortex Memory` block.
14
- *
15
- * The two fetches run in Promise.all, each with its OWN 3 s timeout and
16
- * INDEPENDENT fail-soft: a /context failure must never cost the lessons block,
17
- * and vice versa. Sequential fetches would double worst-case SessionStart
18
- * latency (~6 s) — see spec §7.
19
- *
20
- * Fail-soft by design: ANY failure (missing config, network error, non-2xx,
21
- * parse error) results in silent exit-0. A broken hook must never block a
22
- * CC session, and a broken /context fetch must never blank the whole output.
23
- */
24
- /**
25
- * The GET /context response shape, shared by the CC hook and the OC plugin so
26
- * their gating cannot drift. `agent`/`mode` are echoed by a 0.13 server whenever
27
- * `?agent=` was sent (in EVERY mode); a pre-0.13 server omits them.
28
- */
29
- export interface ContextResponse {
30
- sections?: Record<string, string>;
31
- updated_at?: string;
32
- clients?: string[];
33
- agent?: string;
34
- mode?: string;
35
- }
36
- export interface ResolvedConfig {
37
- serverUrl: string;
38
- authToken: string | undefined;
39
- home: string;
40
- /** Per-agent context id sent as ?agent= (0.13); null → global (no param). */
41
- agentName: string | null;
42
- /** Max lessons to inject (config.lessonsLimit, default 10). */
43
- lessonsLimit?: number;
44
- }
45
- /**
46
- * Read ~/.hicortex/config.json and resolve the server URL + auth token, or
47
- * null when there is no usable config (server not set up yet — fail soft).
48
- * Exported for reuse by the recall-hook CLI (#192) so the two CC hooks can
49
- * never resolve the server differently.
50
- */
51
- export declare function resolveConfig(): ResolvedConfig | null;
52
- /**
53
- * Title-case a section name for its heading: split on `-`/`_`, capitalize each
54
- * word ("user" → "User", "my_notes" → "My Notes").
55
- * Exported so the OC plugin (index.ts) renders the `## Context` block
56
- * identically to the CC hook rather than duplicating the logic.
57
- */
58
- export declare function titleCaseSection(name: string): string;
59
- /**
60
- * Stable section ordering: `user` first, then `rules` (the seeded primary
61
- * sections, spec §8), then every other section alphabetically. Server-side
62
- * enumeration order (readdirSync) is FS-dependent, so we sort here for a
63
- * deterministic injection block. Exported for reuse by the OC plugin.
64
- */
65
- export declare function orderSectionNames(names: string[]): string[];
66
- /**
67
- * Render the `## Context` block from a resolved section map, or null when there
68
- * is nothing to inject (no sections, or every section blank after trimming).
69
- * Pure — no gating, no I/O. Shared verbatim by the CC hook and the OC plugin so
70
- * both harnesses emit an identical block. Sections are ordered (user, rules,
71
- * then alphabetical) and rendered under title-cased `###` headings.
72
- */
73
- export declare function renderContextBlock(sections: Record<string, string>): string | null;
74
- /**
75
- * Gate a GET /context response and render the `## Context` block, or null when
76
- * nothing should be injected: `harness` not in the server-resolved `clients`,
77
- * an empty/blank section set, or — when `requireAgentEcho` — a response that
78
- * does not echo `agent`. The SINGLE gate used by both CC and OC so the two can
79
- * never drift (the Python Hermes plugin `provider.py::_context_block` mirrors
80
- * this logic — keep them in sync).
81
- *
82
- * `requireAgentEcho` is the old-server guard, and it is the CALLER's decision:
83
- * - OC passes `agentId !== null` — when it actually sent an id, a 0.12 server
84
- * that ignores `?agent=` (200 global, no echo) must NOT leak global context
85
- * into every persona; on a bare fetch (no id) the guard is off (amendment
86
- * A2).
87
- * - CC passes `false` ALWAYS and deliberately (see the call site): a thin CC
88
- * client auto-upgrades via npx BEFORE the server does, so during the upgrade
89
- * window it talks to a 0.12 server that cannot hold ANY per-agent config —
90
- * global IS the operator's intended state there, and a guard would instead
91
- * blank ALL context for every CC session in that window.
92
- */
93
- export declare function gateAndRenderContext(data: ContextResponse, harness: string, opts: {
94
- requireAgentEcho: boolean;
95
- }): string | null;
96
- /**
97
- * Fetch context + lessons concurrently and return the combined Markdown block,
98
- * or null when neither yields anything (nothing to inject; caller prints
99
- * nothing and exits 0). The `## Context` block is prepended before the existing
100
- * `## Hicortex Memory` block.
101
- */
102
- export declare function fetchLessonsContext(): Promise<string | null>;