@gamaze/hicortex 0.17.5 → 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.
Files changed (51) hide show
  1. package/README.md +26 -25
  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/{lessons-context.d.ts → learnings-identity.d.ts} +32 -21
  27. package/dist/{lessons-context.js → learnings-identity.js} +50 -39
  28. package/dist/mcp-server.d.ts +2 -0
  29. package/dist/mcp-server.js +168 -58
  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.d.ts +13 -9
  41. package/dist/type-classify.js +26 -12
  42. package/dist/type-labels.d.ts +30 -0
  43. package/dist/type-labels.js +43 -0
  44. package/dist/types.d.ts +28 -0
  45. package/dist/uninstall.d.ts +12 -0
  46. package/dist/uninstall.js +21 -3
  47. package/dist/viz.d.ts +24 -11
  48. package/dist/viz.js +97 -32
  49. package/hermes-plugin/hicortex/README.md +4 -2
  50. package/package.json +2 -2
  51. package/dist/context-cli.d.ts +0 -69
package/dist/paths.js CHANGED
@@ -8,7 +8,7 @@ exports.hicortexHome = hicortexHome;
8
8
  * HICORTEX_DB_PATH convention in db.ts); otherwise defaults to ~/.hicortex.
9
9
  * Every module that needs the home dir routes through here, so the override
10
10
  * behaves consistently across all commands instead of being honored by some
11
- * (context-cli, lessons-context) and hardcoded away by others.
11
+ * (identity-cli, learnings-identity) and hardcoded away by others.
12
12
  */
13
13
  const node_os_1 = require("node:os");
14
14
  const node_path_1 = require("node:path");
@@ -8,7 +8,7 @@
8
8
  * - SessionStart (startup/resume/clear/compact): POST a reset so the
9
9
  * server's per-session shown-set matches the fresh context window.
10
10
  *
11
- * Fail-soft like lessons-context: ANY failure (no config, timeout, non-2xx,
11
+ * Fail-soft like learnings-identity: ANY failure (no config, timeout, non-2xx,
12
12
  * parse error) prints nothing and exits 0 — a broken hook must never block or
13
13
  * slow a CC session beyond the fetch timeout (1000 ms, owner-set).
14
14
  */
@@ -9,14 +9,14 @@
9
9
  * - SessionStart (startup/resume/clear/compact): POST a reset so the
10
10
  * server's per-session shown-set matches the fresh context window.
11
11
  *
12
- * Fail-soft like lessons-context: ANY failure (no config, timeout, non-2xx,
12
+ * Fail-soft like learnings-identity: ANY failure (no config, timeout, non-2xx,
13
13
  * parse error) prints nothing and exits 0 — a broken hook must never block or
14
14
  * slow a CC session beyond the fetch timeout (1000 ms, owner-set).
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.buildHookRequest = buildHookRequest;
18
18
  exports.runRecallHook = runRecallHook;
19
- const lessons_context_js_1 = require("./lessons-context.js");
19
+ const learnings_identity_js_1 = require("./learnings-identity.js");
20
20
  const node_path_1 = require("node:path");
21
21
  const FETCH_TIMEOUT_MS = 1000;
22
22
  /** Read all of stdin (CC pipes the hook payload JSON). */
@@ -51,7 +51,7 @@ function buildHookRequest(payload, cwd = process.cwd()) {
51
51
  return { session_id: sessionId, prompt, project: (0, node_path_1.basename)(cwd) };
52
52
  }
53
53
  async function runRecallHook() {
54
- const cfg = (0, lessons_context_js_1.resolveConfig)();
54
+ const cfg = (0, learnings_identity_js_1.resolveConfig)();
55
55
  if (!cfg)
56
56
  return;
57
57
  let payload;
@@ -62,6 +62,7 @@ exports.handleRecallIndex = handleRecallIndex;
62
62
  exports.handleMemoryGet = handleMemoryGet;
63
63
  exports.formatMemoryGetText = formatMemoryGetText;
64
64
  const storage = __importStar(require("./storage.js"));
65
+ const type_labels_js_1 = require("./type-labels.js");
65
66
  /** Relevance-gate floor for vector-only candidates (config `recallMinSimilarity`).
66
67
  * 0.62 (was 0.55; raised 2026-08-03 on the fine-grain floor sweep — see the
67
68
  * minSimilarity doc above). */
@@ -116,7 +117,7 @@ function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
116
117
  formatDate(r.created_at),
117
118
  r.domain ?? r.project ?? undefined,
118
119
  r.source_agent ?? undefined,
119
- r.memory_type,
120
+ (0, type_labels_js_1.labelForType)(r.memory_type),
120
121
  ]
121
122
  .filter(Boolean)
122
123
  .join(", ");
@@ -281,7 +282,9 @@ function formatMemoryGetText(db, query) {
281
282
  const mem = r.body.memory;
282
283
  const citation = r.body.citation; // carries FETCHED (#204)
283
284
  const date = (mem.created_at ?? "").slice(0, 10);
284
- const header = `[memory ${mem.id} | ${mem.memory_type ?? "episode"} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
285
+ // #264 WS2: render the human-term label (Knowledge/Experience/...), not the
286
+ // internal enum, in the citation header shown to the agent/user.
287
+ const header = `[memory ${mem.id} | ${(0, type_labels_js_1.labelForType)(mem.memory_type ?? "episode")} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
285
288
  `Cite as ${citation} where this shapes your answer; it may be stale — newer memories supersede older.`;
286
289
  return { status: 200, text: `${header}\n\n${mem.content ?? ""}` };
287
290
  }
package/dist/status.d.ts CHANGED
@@ -4,9 +4,9 @@
4
4
  /**
5
5
  * The value shown after "Agent name:" in `hicortex status` (#179, A3). Reports
6
6
  * EXACTLY what the CC hook resolves (shared `resolveAgentIdentity`), so the
7
- * operator never keys `contextAgents`/`agents/<id>/` on an id the install does
7
+ * operator never keys `identityAgents`/`agents/<id>/` on an id the install does
8
8
  * not actually send. Unset → the install sends no `?agent=` and shares the
9
- * global context (CC default). A configured-but-unsanitizable value is called
9
+ * global identity (CC default). A configured-but-unsanitizable value is called
10
10
  * out as invalid (the hook sends none) rather than silently accepted.
11
11
  */
12
12
  export declare function statusAgentLine(config: Record<string, unknown>): string;
package/dist/status.js CHANGED
@@ -13,27 +13,27 @@ const node_child_process_1 = require("node:child_process");
13
13
  const db_js_1 = require("./db.js");
14
14
  const features_js_1 = require("./features.js");
15
15
  const state_js_1 = require("./state.js");
16
- const context_store_js_1 = require("./context-store.js");
16
+ const identity_store_js_1 = require("./identity-store.js");
17
17
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
18
18
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
19
19
  const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
20
20
  /**
21
21
  * The value shown after "Agent name:" in `hicortex status` (#179, A3). Reports
22
22
  * EXACTLY what the CC hook resolves (shared `resolveAgentIdentity`), so the
23
- * operator never keys `contextAgents`/`agents/<id>/` on an id the install does
23
+ * operator never keys `identityAgents`/`agents/<id>/` on an id the install does
24
24
  * not actually send. Unset → the install sends no `?agent=` and shares the
25
- * global context (CC default). A configured-but-unsanitizable value is called
25
+ * global identity (CC default). A configured-but-unsanitizable value is called
26
26
  * out as invalid (the hook sends none) rather than silently accepted.
27
27
  */
28
28
  function statusAgentLine(config) {
29
- const id = (0, context_store_js_1.resolveAgentIdentity)(config);
29
+ const id = (0, identity_store_js_1.resolveAgentIdentity)(config);
30
30
  switch (id.source) {
31
31
  case "configured":
32
32
  return id.agentId;
33
33
  case "invalid-config":
34
34
  return `(invalid configured value "${id.rawConfigured}" — fix config.agentName; hook sends none)`;
35
35
  default: // unset
36
- return "(not set — global context)";
36
+ return "(not set — global identity)";
37
37
  }
38
38
  }
39
39
  async function runStatus() {
@@ -87,8 +87,8 @@ async function runStatus() {
87
87
  else if (!isClientMode && !savedAuthToken) {
88
88
  console.log(`Auth token: not configured (run: npx @gamaze/hicortex init)`);
89
89
  }
90
- // Per-agent context id (#179) — the id this install sends as ?agent= and the
91
- // key operators use for contextAgents / agents/<id>/ dirs.
90
+ // Per-agent identity id (#179) — the id this install sends as ?agent= and the
91
+ // key operators use for identityAgents / agents/<id>/ dirs.
92
92
  console.log(`Agent name: ${statusAgentLine(parsedConfig)}`);
93
93
  console.log();
94
94
  // Adapters
@@ -119,11 +119,13 @@ async function runStatus() {
119
119
  catch { /* no CC settings */ }
120
120
  console.log(` CC MCP: ${ccRegistered ? `registered → ${ccUrl}` : "not registered"}`);
121
121
  console.log();
122
- // Server status
122
+ // Server status. /health/detail carries the diagnostics (version, memories,
123
+ // llm) — /health itself is the public minimal {status:"ok"} probe (#253).
124
+ // localhost bypasses auth, so on-box `hicortex status` gets the fields.
123
125
  console.log("Server:");
124
126
  let serverRunning = false;
125
127
  try {
126
- const resp = await fetch("http://127.0.0.1:8787/health", {
128
+ const resp = await fetch("http://127.0.0.1:8787/health/detail", {
127
129
  signal: AbortSignal.timeout(2000),
128
130
  });
129
131
  if (resp.ok) {
@@ -106,6 +106,16 @@ export interface TelemetryPayload {
106
106
  * wire (that lives in the dashboard snapshot, not the telemetry ping).
107
107
  */
108
108
  tokens_this_run?: number;
109
+ /**
110
+ * True when the per-tenant consolidation budget (`consolidateMaxLlmCalls`)
111
+ * was exhausted this run (#255) — LLM-bound stages deferred remaining work.
112
+ * A quality-degradation signal concentrated on heavy users; absent on a
113
+ * pre-#255 ping, a capture-only/no-LLM/throttled/skipped run, or when the
114
+ * budget was NOT exhausted (additive optional — the aggregate treats absent
115
+ * as "no exhaustion / not measurable"). Per-stage deferred counts live in
116
+ * the dashboard snapshot, not on the wire (mirrors `tokens_this_run`).
117
+ */
118
+ budget_exhausted?: boolean;
109
119
  }
110
120
  /**
111
121
  * Check if telemetry is enabled. Disabled by:
@@ -15,16 +15,20 @@
15
15
  * ------------
16
16
  * Walks memories ordered by rowid in batches (default 200). Default scope =
17
17
  * episodes only (`memory_type = 'episode'`); `--all` reclassifies every memory
18
- * regardless of current type. For each memory, ONE constrained LLM call asks
19
- * the model to classify the content as episode / fact / decision. The reply is
20
- * parsed + validated, and `UPDATE memories SET memory_type = ? WHERE id = ?`
21
- * runs inside a per-batch transaction. The cursor (`typeCursor` in state.json)
22
- * advances to the last committed rowid after each batch — crash-safe and
23
- * infra-abort-safe (same discipline as classify-domains).
18
+ * regardless of current type **except lessons** (see below). For each memory,
19
+ * ONE constrained LLM call asks the model to classify the content as
20
+ * episode / fact / decision. The reply is parsed + validated, and
21
+ * `UPDATE memories SET memory_type = ? WHERE id = ?` runs inside a per-batch
22
+ * transaction. The cursor (`typeCursor` in state.json) advances to the last
23
+ * committed rowid after each batch — crash-safe and infra-abort-safe (same
24
+ * discipline as classify-domains).
24
25
  *
25
- * Lessons are NEVER produced here: the reflection stage owns them. A model that
26
- * replies "lesson" is treated as unparseable (the memory keeps its current type
27
- * and is retried next run via the cursor).
26
+ * Lessons are NEVER touched here: the reflection stage owns them. The `--all`
27
+ * scope explicitly excludes `memory_type = 'lesson'` this is the primary
28
+ * defence. (The prompt asks only for episode/fact/decision, so a lesson that
29
+ * DID enter scope would be overwritten to E/F/D — the model never replies
30
+ * "lesson". `parseTypeReply`'s rejection of a "lesson" reply is a backstop, not
31
+ * the main guard.)
28
32
  *
29
33
  * This command does NOT use the consolidation budget — it is a standalone CLI,
30
34
  * not a nightly stage.
@@ -16,16 +16,20 @@
16
16
  * ------------
17
17
  * Walks memories ordered by rowid in batches (default 200). Default scope =
18
18
  * episodes only (`memory_type = 'episode'`); `--all` reclassifies every memory
19
- * regardless of current type. For each memory, ONE constrained LLM call asks
20
- * the model to classify the content as episode / fact / decision. The reply is
21
- * parsed + validated, and `UPDATE memories SET memory_type = ? WHERE id = ?`
22
- * runs inside a per-batch transaction. The cursor (`typeCursor` in state.json)
23
- * advances to the last committed rowid after each batch — crash-safe and
24
- * infra-abort-safe (same discipline as classify-domains).
19
+ * regardless of current type **except lessons** (see below). For each memory,
20
+ * ONE constrained LLM call asks the model to classify the content as
21
+ * episode / fact / decision. The reply is parsed + validated, and
22
+ * `UPDATE memories SET memory_type = ? WHERE id = ?` runs inside a per-batch
23
+ * transaction. The cursor (`typeCursor` in state.json) advances to the last
24
+ * committed rowid after each batch — crash-safe and infra-abort-safe (same
25
+ * discipline as classify-domains).
25
26
  *
26
- * Lessons are NEVER produced here: the reflection stage owns them. A model that
27
- * replies "lesson" is treated as unparseable (the memory keeps its current type
28
- * and is retried next run via the cursor).
27
+ * Lessons are NEVER touched here: the reflection stage owns them. The `--all`
28
+ * scope explicitly excludes `memory_type = 'lesson'` this is the primary
29
+ * defence. (The prompt asks only for episode/fact/decision, so a lesson that
30
+ * DID enter scope would be overwritten to E/F/D — the model never replies
31
+ * "lesson". `parseTypeReply`'s rejection of a "lesson" reply is a backstop, not
32
+ * the main guard.)
29
33
  *
30
34
  * This command does NOT use the consolidation budget — it is a standalone CLI,
31
35
  * not a nightly stage.
@@ -41,6 +45,7 @@ const node_path_1 = require("node:path");
41
45
  const db_js_1 = require("./db.js");
42
46
  const state_js_1 = require("./state.js");
43
47
  const llm_js_1 = require("./llm.js");
48
+ const type_labels_js_1 = require("./type-labels.js");
44
49
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
45
50
  /** Max chars of memory content fed to the classify prompt. */
46
51
  const CLASSIFY_CONTENT_MAX_CHARS = 1500;
@@ -205,8 +210,15 @@ async function runClassifyTypes(options = {}) {
205
210
  report.cursor = cursor;
206
211
  console.log(`[hicortex] classify-types starting: scope ${all ? "ALL" : "episodes only"}, ` +
207
212
  `batch ${batchSize}, cursor ${cursor}${options.reset ? " (reset)" : ""}`);
208
- // Scope filter: default = episodes only; --all = everything.
209
- const scopeSql = all ? "rowid > ?" : "rowid > ? AND memory_type = 'episode'";
213
+ // Scope filter: default = episodes only; --all = everything EXCEPT lessons.
214
+ // Lessons are owned by the reflection stage they must never be
215
+ // reclassified here. (The prompt asks only for episode/fact/decision, so a
216
+ // lesson row in scope gets overwritten: the model never replies "lesson".
217
+ // The scope exclusion is therefore the real guard; parseTypeReply's
218
+ // "lesson" rejection is a backstop, not the primary defence.)
219
+ const scopeSql = all
220
+ ? "rowid > ? AND (memory_type IS NULL OR memory_type != 'lesson')"
221
+ : "rowid > ? AND memory_type = 'episode'";
210
222
  const batchStmt = db.prepare(`SELECT rowid AS __rowid, id, content, memory_type FROM memories
211
223
  WHERE ${scopeSql} ORDER BY rowid ASC LIMIT ?`);
212
224
  // Set true when the classifier returns null (infra error): finish the
@@ -282,7 +294,9 @@ async function runClassifyTypes(options = {}) {
282
294
  .all();
283
295
  for (const c of counts)
284
296
  report.byType[c.memory_type] = c.cnt;
285
- const breakdown = counts.map((c) => `${c.memory_type}=${c.cnt}`).join(", ") || "none";
297
+ // #264 WS2: display the human-term label (Knowledge/Experience/...), not
298
+ // the internal enum — the operator reading the log sees human terms.
299
+ const breakdown = counts.map((c) => `${(0, type_labels_js_1.labelForType)(c.memory_type)}=${c.cnt}`).join(", ") || "none";
286
300
  console.log(`[hicortex] classify-types ${report.aborted ? "ABORTED" : "complete"}: ` +
287
301
  `${report.scanned} classified, ${report.reclassified} reclassified, ` +
288
302
  `${report.unchanged} unchanged, ${report.failed} infra-skipped`);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Human-term labels for the internal `memory_type` enum (#264 WS2).
3
+ *
4
+ * The data model stores four types under unintuitive internal names
5
+ * (`fact` / `episode` / `decision` / `lesson`). Product-facing surfaces show
6
+ * the human terms instead — Knowledge / Experience / Decisions / Learnings —
7
+ * via this single label map. The enum values themselves are NEVER changed:
8
+ * no DB column, query, distiller tag, or storage path is altered. This is a
9
+ * DISPLAY rename only; every user-facing rendering routes through
10
+ * `labelForType`.
11
+ *
12
+ * Mapping (decided 2026-08-11, research/2026-08-11-identity-reframe-brainstorm.md):
13
+ * fact → Knowledge
14
+ * episode → Experience
15
+ * decision → Decisions
16
+ * lesson → Learnings
17
+ *
18
+ * Unknown / future types fall back to the raw value (never silently remapped).
19
+ */
20
+ /**
21
+ * The four documented memory types mapped to their human-term labels.
22
+ * Kept as a plain record so it can be iterated for coverage assertions.
23
+ */
24
+ export declare const MEMORY_TYPE_LABELS: Record<string, string>;
25
+ /**
26
+ * Return the human-term label for a `memory_type` enum value. Unknown or
27
+ * future types (including null/undefined) fall back to the raw input so new
28
+ * types are visible rather than silently mislabeled.
29
+ */
30
+ export declare function labelForType(t: string | null | undefined): string;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Human-term labels for the internal `memory_type` enum (#264 WS2).
4
+ *
5
+ * The data model stores four types under unintuitive internal names
6
+ * (`fact` / `episode` / `decision` / `lesson`). Product-facing surfaces show
7
+ * the human terms instead — Knowledge / Experience / Decisions / Learnings —
8
+ * via this single label map. The enum values themselves are NEVER changed:
9
+ * no DB column, query, distiller tag, or storage path is altered. This is a
10
+ * DISPLAY rename only; every user-facing rendering routes through
11
+ * `labelForType`.
12
+ *
13
+ * Mapping (decided 2026-08-11, research/2026-08-11-identity-reframe-brainstorm.md):
14
+ * fact → Knowledge
15
+ * episode → Experience
16
+ * decision → Decisions
17
+ * lesson → Learnings
18
+ *
19
+ * Unknown / future types fall back to the raw value (never silently remapped).
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.MEMORY_TYPE_LABELS = void 0;
23
+ exports.labelForType = labelForType;
24
+ /**
25
+ * The four documented memory types mapped to their human-term labels.
26
+ * Kept as a plain record so it can be iterated for coverage assertions.
27
+ */
28
+ exports.MEMORY_TYPE_LABELS = {
29
+ fact: "Knowledge",
30
+ episode: "Experience",
31
+ decision: "Decisions",
32
+ lesson: "Learnings",
33
+ };
34
+ /**
35
+ * Return the human-term label for a `memory_type` enum value. Unknown or
36
+ * future types (including null/undefined) fall back to the raw input so new
37
+ * types are visible rather than silently mislabeled.
38
+ */
39
+ function labelForType(t) {
40
+ if (!t)
41
+ return "—";
42
+ return exports.MEMORY_TYPE_LABELS[t] ?? t;
43
+ }
package/dist/types.d.ts CHANGED
@@ -161,6 +161,26 @@ export interface ConsolidationReport {
161
161
  calls_used: number;
162
162
  calls_remaining: number;
163
163
  calls_by_stage: Record<string, number>;
164
+ /**
165
+ * True when `calls_used >= max_calls` at run end (#255). The run continued
166
+ * to completion (no abort) but LLM-bound stages past the boundary deferred
167
+ * their remaining work — a quality-degradation signal, not a failure.
168
+ * Absent on pre-#255 reports; treat as false.
169
+ */
170
+ exhausted?: boolean;
171
+ /**
172
+ * Per-stage count of LLM-call REQUESTS refused because the budget was
173
+ * exhausted (#255). Keys are the same stage labels passed to
174
+ * `BudgetTracker.use()`. The value is the SUM of the `count` args passed
175
+ * to each refused `use()` call in that stage — in production every `use()`
176
+ * call passes count=1, so a stage present here with count N hit the
177
+ * boundary and had N further single-call requests denied (stages break on
178
+ * first refusal, so N is small per stage). For item-level "how many
179
+ * memories/pairs were skipped" see the per-stage reports (e.g.
180
+ * stages.importance.skipped_budget), which count MEMORIES not call
181
+ * requests. Absent on pre-#255 reports.
182
+ */
183
+ deferred_by_stage?: Record<string, number>;
164
184
  /**
165
185
  * Token usage per stage (#246). Each value sums prompt + completion +
166
186
  * total across every metered LLM call in that stage this run. A stage with
@@ -187,6 +207,14 @@ export interface HicortexConfig {
187
207
  serverUrl?: string;
188
208
  /** Bearer token for the Hicortex server. Localhost bypasses auth by default. */
189
209
  authToken?: string;
210
+ /**
211
+ * Optional PRIOR bearer token kept around during rotation (#254). When set,
212
+ * BOTH `authToken` and `authTokenPrevious` are accepted (constant-time,
213
+ * zero-downtime rotation). Absent/empty → single-token behaviour. Rotate by
214
+ * writing the new value to `authToken` and the old value here, then later
215
+ * clearing this key once all clients have switched.
216
+ */
217
+ authTokenPrevious?: string;
190
218
  /**
191
219
  * Stable per-install UUID generated by `init` (see ensureAgentId in init.ts;
192
220
  * never rotated). Attribution identity of the capturing client — stored on
@@ -2,4 +2,16 @@
2
2
  * Hicortex uninstall — clean removal of CC integration.
3
3
  * Preserves the database (user data).
4
4
  */
5
+ /**
6
+ * Matches a CC SessionStart hook `command` that runs the Hicortex
7
+ * identity/learnings hook — the canonical `learnings-identity` (#264) OR the
8
+ * legacy `lessons-context` alias. Exported so the uninstall behavior (which
9
+ * name variants get cleaned up) is unit-testable without spinning up CC.
10
+ * Word-boundary guard so "learnings-identity" never matches a hypothetical
11
+ * "learnings-identity-foo", the two names stay distinct from each other, and
12
+ * neither collides with the sibling `recall-hook` SessionStart hook.
13
+ */
14
+ export declare const SESSION_START_HOOK_COMMAND_RE: RegExp;
15
+ /** True when a CC hook `command` string runs the Hicortex SessionStart hook. */
16
+ export declare function isHicortexSessionStartHook(command: string): boolean;
5
17
  export declare function runUninstall(): Promise<void>;
package/dist/uninstall.js CHANGED
@@ -4,6 +4,8 @@
4
4
  * Preserves the database (user data).
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SESSION_START_HOOK_COMMAND_RE = void 0;
8
+ exports.isHicortexSessionStartHook = isHicortexSessionStartHook;
7
9
  exports.runUninstall = runUninstall;
8
10
  const paths_js_1 = require("./paths.js");
9
11
  const node_fs_1 = require("node:fs");
@@ -17,6 +19,20 @@ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
17
19
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
18
20
  const CC_COMMANDS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "commands");
19
21
  const CLAUDE_MD = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
22
+ /**
23
+ * Matches a CC SessionStart hook `command` that runs the Hicortex
24
+ * identity/learnings hook — the canonical `learnings-identity` (#264) OR the
25
+ * legacy `lessons-context` alias. Exported so the uninstall behavior (which
26
+ * name variants get cleaned up) is unit-testable without spinning up CC.
27
+ * Word-boundary guard so "learnings-identity" never matches a hypothetical
28
+ * "learnings-identity-foo", the two names stay distinct from each other, and
29
+ * neither collides with the sibling `recall-hook` SessionStart hook.
30
+ */
31
+ exports.SESSION_START_HOOK_COMMAND_RE = /(^|\s)(?:learnings-identity|lessons-context)(\s|$)/;
32
+ /** True when a CC hook `command` string runs the Hicortex SessionStart hook. */
33
+ function isHicortexSessionStartHook(command) {
34
+ return typeof command === "string" && exports.SESSION_START_HOOK_COMMAND_RE.test(command);
35
+ }
20
36
  async function ask(question) {
21
37
  const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
22
38
  return new Promise((resolve) => {
@@ -130,7 +146,9 @@ async function runUninstall() {
130
146
  }
131
147
  if (removedCmds > 0)
132
148
  console.log(` ✓ Removed ${removedCmds} legacy CC command${removedCmds > 1 ? "s" : ""} (/learn, /hicortex-activate)`);
133
- // 4. Remove SessionStart hook (JSON merge — filter out entries containing "lessons-context")
149
+ // 4. Remove SessionStart hook (JSON merge — filter out entries containing
150
+ // EITHER the canonical "learnings-identity" OR the legacy "lessons-context"
151
+ // alias, #264 backcompat: an install may have written either name.)
134
152
  try {
135
153
  const raw = (0, node_fs_1.readFileSync)(CC_SETTINGS, "utf-8");
136
154
  const settings = JSON.parse(raw);
@@ -147,7 +165,7 @@ async function runUninstall() {
147
165
  if (typeof h !== "object" || h === null)
148
166
  return false;
149
167
  const hook = h;
150
- return typeof hook.command === "string" && hook.command.includes("lessons-context");
168
+ return typeof hook.command === "string" && isHicortexSessionStartHook(hook.command);
151
169
  });
152
170
  }
153
171
  return true;
@@ -155,7 +173,7 @@ async function runUninstall() {
155
173
  if (filtered.length < before) {
156
174
  hooks.SessionStart = filtered;
157
175
  (0, node_fs_1.writeFileSync)(CC_SETTINGS, JSON.stringify(settings, null, 2));
158
- console.log(" ✓ Removed SessionStart lessons-context hook");
176
+ console.log(" ✓ Removed SessionStart learnings-identity hook");
159
177
  }
160
178
  }
161
179
  }
package/dist/viz.d.ts CHANGED
@@ -37,8 +37,16 @@ export declare const VIZ_VENDOR_FILES: ReadonlySet<string>;
37
37
  * middleware before this runs). With no token configured, remote requests are
38
38
  * REJECTED (not open): the default bind is 0.0.0.0, so "no token = no auth"
39
39
  * would expose the whole memory store to the network.
40
+ *
41
+ * `authTokenPrevious` (optional, #254) is the prior token kept around during
42
+ * rotation. Both tokens are accepted; this gives a zero-downtime rotation
43
+ * window — in-flight clients configured with the old token keep working until
44
+ * they pick up the new one. BOTH comparisons are constant-time and both are
45
+ * always evaluated (no short-circuit), so a caller cannot learn WHICH token
46
+ * matched from the response timing. Absent/empty `authTokenPrevious` behaves
47
+ * exactly as the single-token middleware always has.
40
48
  */
41
- export declare function createAuthMiddleware(authToken: string | undefined): express.RequestHandler;
49
+ export declare function createAuthMiddleware(authToken: string | undefined, authTokenPrevious?: string): express.RequestHandler;
42
50
  /**
43
51
  * Resolve the on-disk path of the viz page. Throws (fail explicitly) when the
44
52
  * asset is missing — a broken install should surface, not degrade silently.
@@ -52,24 +60,29 @@ export declare function readVizHtml(): string;
52
60
  */
53
61
  export declare function vizHandler(): express.RequestHandler;
54
62
  /**
55
- * Resolve the on-disk path of the context-layer editor page. Throws (fail
63
+ * Resolve the on-disk path of the identity-layer editor page. Throws (fail
56
64
  * explicitly) when the asset is missing — same contract as resolveVizHtmlPath.
57
65
  * assets/ sits next to both dist/ (dist/viz.js → ../assets/) and src/
58
66
  * (src/viz.ts → ../assets/ under tsx), so one sibling candidate covers both.
59
67
  */
60
- export declare function resolveContextHtmlPath(): string;
61
- /** Read the context editor page. Read at request time so a reinstall is live. */
62
- export declare function readContextHtml(): string;
68
+ export declare function resolveIdentityHtmlPath(): string;
69
+ /** Read the identity editor page. Read at request time so a reinstall is live. */
70
+ export declare function readIdentityHtml(): string;
63
71
  /**
64
- * Express handler for GET /context/ui — the PRIMARY edit surface for the
65
- * standing context layer. 503 with the usual {error} shape when the asset
66
- * cannot be read, exactly like vizHandler.
72
+ * Express handler for GET /identity/ui — the PRIMARY edit surface for the
73
+ * standing identity layer. 503 with the usual {error} shape when the asset
74
+ * cannot be read, exactly like vizHandler. Also serves the legacy
75
+ * /context/ui URL (#264 backcompat).
67
76
  */
68
- export declare function contextUiHandler(): express.RequestHandler;
77
+ export declare function identityUiHandler(): express.RequestHandler;
78
+ /** Backcompat aliases (#264). */
79
+ export declare const resolveContextHtmlPath: typeof resolveIdentityHtmlPath;
80
+ export declare const readContextHtml: typeof readIdentityHtml;
81
+ export declare const contextUiHandler: typeof identityUiHandler;
69
82
  /**
70
83
  * Resolve the on-disk path of the dashboard page. Throws (fail explicitly)
71
84
  * when the asset is missing — same contract as resolveVizHtmlPath and
72
- * resolveContextHtmlPath. assets/ sits next to both dist/ and src/ (the
85
+ * resolveIdentityHtmlPath. assets/ sits next to both dist/ and src/ (the
73
86
  * sibling layout the other resolvers rely on).
74
87
  */
75
88
  export declare function resolveDashboardHtmlPath(): string;
@@ -78,7 +91,7 @@ export declare function readDashboardHtml(): string;
78
91
  /**
79
92
  * Express handler for GET /dashboard — the view-only analytics page (#224).
80
93
  * 503 with the usual {error} shape when the asset cannot be read, exactly like
81
- * vizHandler and contextUiHandler. The page SHELL is public (exempted in
94
+ * vizHandler and identityUiHandler. The page SHELL is public (exempted in
82
95
  * createAuthMiddleware); all data comes from GET /dashboard/data (bearer-only).
83
96
  */
84
97
  export declare function dashboardHandler(): express.RequestHandler;