@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.
Files changed (50) 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.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
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  const cli_args_js_1 = require("./cli-args.js");
21
- const command = process.argv[2];
21
+ const command = (0, cli_args_js_1.resolveCommandAlias)(process.argv[2]);
22
22
  switch (command) {
23
23
  case "server": {
24
24
  const portArg = process.argv.indexOf("--port");
@@ -218,16 +218,19 @@ switch (command) {
218
218
  });
219
219
  break;
220
220
  }
221
- case "context": {
222
- // Standing context layer edit surface (spec §6): show|edit against the
223
- // configured server. Secondary to the /context/ui Web UI; for headless boxes.
221
+ case "identity": {
222
+ // Standing identity layer edit surface (spec §6; renamed from context in
223
+ // 0.18 #264): show|edit against the configured server. Secondary to the
224
+ // /identity/ui Web UI; for headless boxes. The legacy `context` command is
225
+ // kept as a hidden backcompat alias via resolveCommandAlias so old scripts
226
+ // and muscle memory keep working.
224
227
  const args = process.argv.slice(3);
225
- import("./context-cli.js").then(({ runContextCommand, ContextCliError }) => {
226
- runContextCommand(args).catch((err) => {
227
- if (err instanceof ContextCliError)
228
+ import("./identity-cli.js").then(({ runIdentityCommand, IdentityCliError }) => {
229
+ runIdentityCommand(args).catch((err) => {
230
+ if (err instanceof IdentityCliError)
228
231
  console.error(err.message);
229
232
  else
230
- console.error("[hicortex] context command failed:", err instanceof Error ? err.message : err);
233
+ console.error("[hicortex] identity command failed:", err instanceof Error ? err.message : err);
231
234
  process.exit(1);
232
235
  });
233
236
  });
@@ -271,12 +274,14 @@ switch (command) {
271
274
  .catch(() => process.exit(0));
272
275
  }).catch(() => process.exit(0));
273
276
  break;
274
- case "lessons-context":
275
- // CC SessionStart hook: fetch lessons from the configured server and print
276
- // a Markdown block to stdout. Fail-soft any error = silent exit 0 so a
277
- // broken hook never blocks a CC session.
278
- import("./lessons-context.js").then(({ fetchLessonsContext }) => {
279
- fetchLessonsContext()
277
+ case "learnings-identity": {
278
+ // CC SessionStart hook: fetch identity + lessons from the configured server
279
+ // and print a Markdown block to stdout. Canonical command name since #264;
280
+ // the legacy `lessons-context` subcommand is kept as a backcompat alias via
281
+ // resolveCommandAlias so existing installed hooks keep working. Fail-soft —
282
+ // any error = silent exit 0 so a broken hook never blocks a CC session.
283
+ import("./learnings-identity.js").then(({ fetchLessonsIdentity }) => {
284
+ fetchLessonsIdentity()
280
285
  .then((block) => {
281
286
  if (block)
282
287
  process.stdout.write(block + "\n");
@@ -285,6 +290,7 @@ switch (command) {
285
290
  .catch(() => process.exit(0));
286
291
  }).catch(() => process.exit(0));
287
292
  break;
293
+ }
288
294
  default:
289
295
  console.log(`Hicortex — Human-like memory for self-improving AI agents
290
296
 
@@ -296,7 +302,7 @@ Commands:
296
302
  Scaffolds 5 editable default memory domains (Work, Personal,
297
303
  People, Health, Finance) in ~/.hicortex/config.json
298
304
  init --server <url> Set up as client (remote server)
299
- init --agent-name <name> Opt in to a per-agent context id (default: unset — shared global context)
305
+ init --agent-name <name> Opt in to a per-agent identity id (default: unset — shared global identity)
300
306
  Pass --agent-name "" to clear it back to global
301
307
  init --repair-config Recover from a malformed ~/.hicortex/config.json: move it to
302
308
  config.json.corrupt-<timestamp> and rebuild. Nothing is deleted.
@@ -306,9 +312,11 @@ Commands:
306
312
  dedup Cluster + merge near-duplicate memories (server mode; dry run by default)
307
313
  classify-domains Backfill content-based domain tags over the corpus (server mode, needs config.domains)
308
314
  classify-types Backfill episode→fact/decision type tags over the corpus (server mode)
309
- lessons-context Fetch lessons and print Markdown to stdout (CC SessionStart hook)
315
+ learnings-identity Fetch identity + lessons and print Markdown to stdout (CC SessionStart hook)
316
+ (alias: lessons-context — the pre-#264 name, kept for backcompat)
310
317
  recall-hook Pushed recall index for the current prompt (CC UserPromptSubmit/SessionStart hook)
311
- context Standing context layer (show|edit) against the configured server
318
+ identity Standing identity layer (show|edit) against the configured server
319
+ (alias: context — the pre-0.18 name, kept for backcompat)
312
320
  telemetry Show exactly what anonymous telemetry sends (read-only)
313
321
  status Show current configuration and stats
314
322
  uninstall Remove CC integration (preserves DB)
@@ -333,9 +341,10 @@ Options:
333
341
  classify-types --all Reclassify every memory (default: only episodes)
334
342
  classify-types --batch <n> Memories per batch (default: 200)
335
343
  classify-types --reset Restart from the beginning (ignore saved cursor)
336
- context show [name] Print all context sections, or just <name> (raw, pipeable)
337
- context edit <name> Edit a section in $EDITOR; PUT only if changed
338
- context … --agent <id> Target a per-agent scope instead of the global set
344
+ identity show [name] Print all identity sections, or just <name> (raw, pipeable)
345
+ identity edit <name> Edit a section in $EDITOR; PUT only if changed
346
+ identity … --agent <id> Target a per-agent scope instead of the global set
347
+ (the legacy 'context' command remains as a hidden alias for 'identity')
339
348
 
340
349
  Examples:
341
350
  npx @gamaze/hicortex server
@@ -63,6 +63,21 @@ export declare class BudgetTracker {
63
63
  maxCalls: number;
64
64
  callsUsed: number;
65
65
  callsByStage: Record<string, number>;
66
+ /**
67
+ * Per-stage count of LLM-call REQUESTS refused because the budget was
68
+ * exhausted (#255). Keys are the same stage labels passed to `use()`. The
69
+ * value is the SUM of the `count` args passed to each refused `use()` call
70
+ * in that stage (in production every `use()` call passes count=1, so each
71
+ * refused call adds 1 — but the API accepts a batch count, so a single
72
+ * refused batch request accrues its full count). Stages break on the first
73
+ * refusal, so a stage's value is the count of the one request that crossed
74
+ * the boundary. For item-level skip counts (how many memories or pairs were
75
+ * left unprocessed), see the per-stage reports — e.g.
76
+ * `stages.importance.skipped_budget` — which count MEMORIES, not call
77
+ * requests. Surfaced in summary() and ConsolidationReport as
78
+ * `deferred_by_stage`.
79
+ */
80
+ deferredByStage: Record<string, number>;
66
81
  /**
67
82
  * Token usage per stage (#246). Keys are the same stage labels passed to
68
83
  * `use()`. A stage that made no metered calls (no usage returned — never the
@@ -127,6 +127,21 @@ class BudgetTracker {
127
127
  maxCalls;
128
128
  callsUsed = 0;
129
129
  callsByStage = {};
130
+ /**
131
+ * Per-stage count of LLM-call REQUESTS refused because the budget was
132
+ * exhausted (#255). Keys are the same stage labels passed to `use()`. The
133
+ * value is the SUM of the `count` args passed to each refused `use()` call
134
+ * in that stage (in production every `use()` call passes count=1, so each
135
+ * refused call adds 1 — but the API accepts a batch count, so a single
136
+ * refused batch request accrues its full count). Stages break on the first
137
+ * refusal, so a stage's value is the count of the one request that crossed
138
+ * the boundary. For item-level skip counts (how many memories or pairs were
139
+ * left unprocessed), see the per-stage reports — e.g.
140
+ * `stages.importance.skipped_budget` — which count MEMORIES, not call
141
+ * requests. Surfaced in summary() and ConsolidationReport as
142
+ * `deferred_by_stage`.
143
+ */
144
+ deferredByStage = {};
130
145
  /**
131
146
  * Token usage per stage (#246). Keys are the same stage labels passed to
132
147
  * `use()`. A stage that made no metered calls (no usage returned — never the
@@ -151,8 +166,15 @@ class BudgetTracker {
151
166
  }
152
167
  use(stage, count = 1) {
153
168
  if (this.callsUsed + count > this.maxCalls) {
154
- console.warn(`[hicortex] Budget exhausted: ${this.callsUsed}/${this.maxCalls} used, ` +
155
- `requested ${count} more (stage: ${stage})`);
169
+ // #255: emit as a STRUCTURED event (not a bare prose warn) so a monitor
170
+ // can grep/parse `event=budget_exhausted` from journald. The line stays
171
+ // human-readable (key=value tokens after the [hicortex] prefix). Deferred
172
+ // counts are accrued BEFORE the log so the line reflects the up-to-date
173
+ // per-stage toll — the refused count is added to this stage's slot.
174
+ this.deferredByStage[stage] = (this.deferredByStage[stage] ?? 0) + count;
175
+ console.warn(`[hicortex] event=budget_exhausted stage=${stage} ` +
176
+ `calls_used=${this.callsUsed} max_calls=${this.maxCalls} ` +
177
+ `deferred_by_stage=${JSON.stringify(this.deferredByStage)}`);
156
178
  return false;
157
179
  }
158
180
  this.callsUsed += count;
@@ -183,6 +205,11 @@ class BudgetTracker {
183
205
  calls_used: this.callsUsed,
184
206
  calls_remaining: this.remaining,
185
207
  calls_by_stage: { ...this.callsByStage },
208
+ // #255: exhaustion + per-stage deferred counts flow into the report →
209
+ // telemetry + dashboard. Always present (post-#255); absent implies a
210
+ // pre-#255 report (treat as false / no deferrals).
211
+ exhausted: this.exhausted,
212
+ deferred_by_stage: { ...this.deferredByStage },
186
213
  tokens_by_stage: Object.fromEntries(Object.entries(this.tokensByStage).map(([k, v]) => [k, { ...v }])),
187
214
  tokens_total: { ...this.totalTokens },
188
215
  };
@@ -393,7 +420,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
393
420
  const sourcePattern = String(lo.source_pattern ?? "");
394
421
  // No `## Lesson:` prefix: memory_type='lesson' carries the type, and the
395
422
  // text is the topic-first first line (display reads the first line, not a
396
- // header parse — see lessons-context.ts / index.ts).
423
+ // header parse — see learnings-identity.ts / index.ts).
397
424
  let content = `${lessonText}\n\n`;
398
425
  content += `**Type:** ${lessonType}\n`;
399
426
  content += `**Severity:** ${severity}\n`;
@@ -53,6 +53,32 @@ export interface DashboardMetrics {
53
53
  completion: number;
54
54
  total: number;
55
55
  }>;
56
+ /**
57
+ * Always-on consolidation-budget usage metric (#255 CR). `calls_used` is
58
+ * how many LLM calls the run actually spent; `max_calls` is the configured
59
+ * `consolidateMaxLlmCalls` ceiling. Forwarded whenever consolidation ran
60
+ * (the digest renders a continuous used/max bar, like the token-usage
61
+ * metric, so you can see the budget climbing before exhaustion). Undefined
62
+ * on backfill rows and on runs where consolidation didn't execute
63
+ * (capture-only / no_llm / throttled / skipped) — the page renders no bar.
64
+ */
65
+ budget_calls_used?: number;
66
+ budget_max_calls?: number;
67
+ /**
68
+ * True when this run's consolidation budget (`consolidateMaxLlmCalls`)
69
+ * was exhausted — LLM-bound stages deferred remaining work (#255).
70
+ * Forwarded ONLY on exhaustion (the alert state on top of the always-on
71
+ * usage bar). Undefined on healthy runs (where budget_calls_used is still
72
+ * present), backfill rows, and non-run nights.
73
+ */
74
+ budget_exhausted?: boolean;
75
+ /**
76
+ * Per-stage count of refused LLM-call requests on an exhausted run (#255).
77
+ * Mirrors ConsolidationReport.budget.deferred_by_stage. Forwarded in
78
+ * lockstep with `budget_exhausted` (exhausted runs only) so the snapshot
79
+ * shape is clean on healthy runs.
80
+ */
81
+ budget_deferred_by_stage?: Record<string, number>;
56
82
  };
57
83
  /** Corpus capacity (#245). `memory_soft_cap` is the configured ceiling (0 =
58
84
  * disabled); always present in real snapshots, undefined on backfilled
@@ -131,6 +157,17 @@ export interface DashboardData {
131
157
  completion: number;
132
158
  total: number;
133
159
  }>;
160
+ /**
161
+ * Always-on consolidation-budget usage (#255 CR). Present whenever the
162
+ * day's snapshot carries them (consolidation ran). The page renders a
163
+ * used/max bar in the same style as the token-usage metric.
164
+ */
165
+ budget_calls_used?: number;
166
+ budget_max_calls?: number;
167
+ /** True when the run exhausted its consolidation budget (#255). */
168
+ budget_exhausted?: boolean;
169
+ /** Per-stage refused-request counts on an exhausted run (#255). */
170
+ budget_deferred_by_stage?: Record<string, number>;
134
171
  };
135
172
  dedup_merges: {
136
173
  loser_id: string;
@@ -170,6 +207,26 @@ export interface NightlyDelta {
170
207
  completion: number;
171
208
  total: number;
172
209
  }>;
210
+ /**
211
+ * Always-on consolidation-budget usage (#255 CR). Forwarded whenever
212
+ * consolidation ran so the dashboard renders a continuous used/max bar.
213
+ * `callsUsed` = LLM calls spent this run; `maxCalls` = configured ceiling.
214
+ * Undefined in lockstep (both set together when consolidation ran).
215
+ */
216
+ budgetCallsUsed?: number;
217
+ budgetMaxCalls?: number;
218
+ /**
219
+ * True when this run's consolidation budget was exhausted (#255) — same as
220
+ * ConsolidationReport.budget.exhausted. Undefined when consolidation didn't
221
+ * run (capture-only / no_llm / throttled / skipped) or didn't exhaust.
222
+ */
223
+ budgetExhausted?: boolean;
224
+ /**
225
+ * Per-stage refused-request counts on an exhausted run (#255) — same shape
226
+ * as ConsolidationReport.budget.deferred_by_stage. Undefined in lockstep
227
+ * with `budgetExhausted`.
228
+ */
229
+ budgetDeferredByStage?: Record<string, number>;
173
230
  }
174
231
  /**
175
232
  * Write one snapshot row for `runAt` (an ISO timestamp the caller chooses —
@@ -225,7 +282,7 @@ export declare function handleDashboardData(db: Database.Database, query: {
225
282
  };
226
283
  /**
227
284
  * Express adapter for GET /dashboard/data. Wraps the pure handler so the
228
- * route stays thin (same convention as context-store handlers in viz.ts).
285
+ * route stays thin (same convention as identity-store handlers in viz.ts).
229
286
  * Failures surface as a 500 with the usual {error} shape — no silent degrade.
230
287
  */
231
288
  export declare function dashboardDataHandler(getDb: () => Database.Database, getConfig: () => Record<string, unknown> | null | undefined): express.RequestHandler;
package/dist/dashboard.js CHANGED
@@ -103,6 +103,24 @@ function writeSnapshot(db, runAt, delta, memorySoftCap) {
103
103
  // page treats undefined as "no data for this day", matching adoption.
104
104
  ...(delta.tokensThisRun !== undefined ? { tokens: delta.tokensThisRun } : {}),
105
105
  ...(delta.tokensByStage !== undefined ? { tokens_by_stage: delta.tokensByStage } : {}),
106
+ // #255 CR: always-on usage metric — forward calls_used + max_calls
107
+ // whenever consolidation ran (regardless of exhaustion) so the page can
108
+ // render a continuous used/max bar. Presence = a run happened; absence =
109
+ // consolidation didn't execute (capture-only / no_llm / throttled / skipped).
110
+ ...(delta.budgetCallsUsed !== undefined ? { budget_calls_used: delta.budgetCallsUsed } : {}),
111
+ ...(delta.budgetMaxCalls !== undefined ? { budget_max_calls: delta.budgetMaxCalls } : {}),
112
+ // #255: the alert state. budget_exhausted + budget_deferred_by_stage are
113
+ // gated on the SAME condition (exhausted=true) so the snapshot shape is
114
+ // clean on healthy runs — no empty {} leaking through (W4). The healthy
115
+ // signal is carried by budget_calls_used above, not by a false flag here.
116
+ ...(delta.budgetExhausted
117
+ ? {
118
+ budget_exhausted: true,
119
+ ...(delta.budgetDeferredByStage !== undefined
120
+ ? { budget_deferred_by_stage: delta.budgetDeferredByStage }
121
+ : {}),
122
+ }
123
+ : {}),
106
124
  };
107
125
  if (memorySoftCap !== undefined) {
108
126
  metrics.capacity = { memory_soft_cap: memorySoftCap };
@@ -404,6 +422,14 @@ function handleDashboardData(db, query, config) {
404
422
  // are forwarded together — the page renders either the breakdown or nothing.
405
423
  tokens: dayMetrics?.new_this_run?.tokens,
406
424
  tokens_by_stage: dayMetrics?.new_this_run?.tokens_by_stage,
425
+ // #255 CR: always-on usage metric — present whenever consolidation ran
426
+ // (under-cap OR exhausted). The page renders a continuous used/max bar.
427
+ budget_calls_used: dayMetrics?.new_this_run?.budget_calls_used,
428
+ budget_max_calls: dayMetrics?.new_this_run?.budget_max_calls,
429
+ // #255: budget exhaustion — only present when the run actually hit the
430
+ // cap (undefined on backfill / non-run / under-cap nights).
431
+ budget_exhausted: dayMetrics?.new_this_run?.budget_exhausted,
432
+ budget_deferred_by_stage: dayMetrics?.new_this_run?.budget_deferred_by_stage,
407
433
  },
408
434
  dedup_merges: dedupRows.map((r) => ({
409
435
  loser_id: r.loser_id,
@@ -429,7 +455,7 @@ function handleDashboardData(db, query, config) {
429
455
  }
430
456
  /**
431
457
  * Express adapter for GET /dashboard/data. Wraps the pure handler so the
432
- * route stays thin (same convention as context-store handlers in viz.ts).
458
+ * route stays thin (same convention as identity-store handlers in viz.ts).
433
459
  * Failures surface as a 500 with the usual {error} shape — no silent degrade.
434
460
  */
435
461
  function dashboardDataHandler(getDb, getConfig) {
@@ -7,7 +7,7 @@
7
7
  * still be swapped in (tests, future experiments).
8
8
  *
9
9
  * LessonSelector — lesson selection sites:
10
- * lessons-context.ts:fetchLessonsContext (CC SessionStart hook),
10
+ * learnings-identity.ts:fetchLessonsIdentity (CC SessionStart hook),
11
11
  * index.ts:before_agent_start (OC in-process plugin).
12
12
  * The default is the domain-aware scoring selector (lesson-selection.ts):
13
13
  * ranks lessons by project match + domain affinity + recency +
@@ -8,7 +8,7 @@
8
8
  * still be swapped in (tests, future experiments).
9
9
  *
10
10
  * LessonSelector — lesson selection sites:
11
- * lessons-context.ts:fetchLessonsContext (CC SessionStart hook),
11
+ * learnings-identity.ts:fetchLessonsIdentity (CC SessionStart hook),
12
12
  * index.ts:before_agent_start (OC in-process plugin).
13
13
  * The default is the domain-aware scoring selector (lesson-selection.ts):
14
14
  * ranks lessons by project match + domain affinity + recency +
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Health-endpoint + REST error-sanitisation helpers (#253).
3
+ *
4
+ * Two concerns live here:
5
+ *
6
+ * 1. The public `GET /health` probe is UNAUTHENTICATED and was previously
7
+ * echoing tenant/install business-intelligence (memory count, link count,
8
+ * DB size, version, the full LLM backend string) plus running `COUNT(*)`
9
+ * on every hit. The public response is now just `{status:"ok"}`. The
10
+ * diagnostics moved to `GET /health/detail`, which goes through the normal
11
+ * bearer-token auth middleware (localhost bypasses as usual) so an
12
+ * operator running `hicortex status` on the server box, or a co-located
13
+ * nightly preflight, still gets them — but a remote/anonymous caller does
14
+ * not. Spec: `specs/2026-07-27-hosted-service.md` §6, Phase 0a item 5a/b.
15
+ *
16
+ * 2. REST `res.status(500).json({error: err.message})` sites were echoing
17
+ * internal detail (LLM upstream URLs, hostnames, stack frames) to the HTTP
18
+ * caller. `logAndSendInternalError` logs the full detail server-side and
19
+ * returns a generic `{error:"Internal error"}` body. Validation errors
20
+ * (400 with a useful, non-leaking message) stay specific at their call
21
+ * sites — only the catch blocks that could leak infrastructure route
22
+ * through here.
23
+ */
24
+ /**
25
+ * Public, unauthenticated health probe. Carries NO data — just liveness.
26
+ * Anyone hitting `/health` (load balancer, watchdog, anonymous prober) gets
27
+ * this and nothing else.
28
+ */
29
+ export declare function publicHealthResponse(): {
30
+ status: "ok";
31
+ };
32
+ /**
33
+ * Operator-only diagnostics. Returned by `GET /health/detail` behind the
34
+ * standard auth middleware (localhost bypasses auth, so co-located tooling
35
+ * — `hicortex status`, nightly preflight, `init` detect — sees it without a
36
+ * token; a remote caller needs the bearer token).
37
+ */
38
+ export declare function detailedHealthResponse(opts: {
39
+ memories: number;
40
+ links: number;
41
+ dbSizeBytes: number;
42
+ version: string;
43
+ llmLabel: string;
44
+ }): {
45
+ status: "ok";
46
+ version: string;
47
+ memories: number;
48
+ links: number;
49
+ db_size_kb: number;
50
+ llm: string;
51
+ };
52
+ /**
53
+ * Log the full internal error detail server-side and return a generic
54
+ * client-facing 500 body. Use this in every REST catch block whose
55
+ * `err.message` could leak infrastructure (LLM upstream URLs, hostnames,
56
+ * stack frames, DB paths). Validation errors (400) with a useful,
57
+ * non-leaking message stay inline at the call site — this is for the
58
+ * catch-all 500 paths only.
59
+ *
60
+ * The `route` arg (e.g. "lessons", "distill") scopes the log line so an
61
+ * operator can tell which endpoint failed without the response body
62
+ * carrying that detail to the caller.
63
+ */
64
+ export declare function logAndSendInternalError(res: {
65
+ status(code: number): {
66
+ json(body: unknown): void;
67
+ };
68
+ }, route: string, err: unknown): void;
package/dist/health.js ADDED
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ /**
3
+ * Health-endpoint + REST error-sanitisation helpers (#253).
4
+ *
5
+ * Two concerns live here:
6
+ *
7
+ * 1. The public `GET /health` probe is UNAUTHENTICATED and was previously
8
+ * echoing tenant/install business-intelligence (memory count, link count,
9
+ * DB size, version, the full LLM backend string) plus running `COUNT(*)`
10
+ * on every hit. The public response is now just `{status:"ok"}`. The
11
+ * diagnostics moved to `GET /health/detail`, which goes through the normal
12
+ * bearer-token auth middleware (localhost bypasses as usual) so an
13
+ * operator running `hicortex status` on the server box, or a co-located
14
+ * nightly preflight, still gets them — but a remote/anonymous caller does
15
+ * not. Spec: `specs/2026-07-27-hosted-service.md` §6, Phase 0a item 5a/b.
16
+ *
17
+ * 2. REST `res.status(500).json({error: err.message})` sites were echoing
18
+ * internal detail (LLM upstream URLs, hostnames, stack frames) to the HTTP
19
+ * caller. `logAndSendInternalError` logs the full detail server-side and
20
+ * returns a generic `{error:"Internal error"}` body. Validation errors
21
+ * (400 with a useful, non-leaking message) stay specific at their call
22
+ * sites — only the catch blocks that could leak infrastructure route
23
+ * through here.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.publicHealthResponse = publicHealthResponse;
27
+ exports.detailedHealthResponse = detailedHealthResponse;
28
+ exports.logAndSendInternalError = logAndSendInternalError;
29
+ /**
30
+ * Public, unauthenticated health probe. Carries NO data — just liveness.
31
+ * Anyone hitting `/health` (load balancer, watchdog, anonymous prober) gets
32
+ * this and nothing else.
33
+ */
34
+ function publicHealthResponse() {
35
+ return { status: "ok" };
36
+ }
37
+ /**
38
+ * Operator-only diagnostics. Returned by `GET /health/detail` behind the
39
+ * standard auth middleware (localhost bypasses auth, so co-located tooling
40
+ * — `hicortex status`, nightly preflight, `init` detect — sees it without a
41
+ * token; a remote caller needs the bearer token).
42
+ */
43
+ function detailedHealthResponse(opts) {
44
+ return {
45
+ status: "ok",
46
+ version: opts.version,
47
+ memories: opts.memories,
48
+ links: opts.links,
49
+ db_size_kb: Math.round(opts.dbSizeBytes / 1024),
50
+ llm: opts.llmLabel,
51
+ };
52
+ }
53
+ /**
54
+ * Log the full internal error detail server-side and return a generic
55
+ * client-facing 500 body. Use this in every REST catch block whose
56
+ * `err.message` could leak infrastructure (LLM upstream URLs, hostnames,
57
+ * stack frames, DB paths). Validation errors (400) with a useful,
58
+ * non-leaking message stay inline at the call site — this is for the
59
+ * catch-all 500 paths only.
60
+ *
61
+ * The `route` arg (e.g. "lessons", "distill") scopes the log line so an
62
+ * operator can tell which endpoint failed without the response body
63
+ * carrying that detail to the caller.
64
+ */
65
+ function logAndSendInternalError(res, route, err) {
66
+ // Stack when available (most informative), else name+message for Errors,
67
+ // else String(). NEVER echo this to the response body.
68
+ const detail = err instanceof Error
69
+ ? (err.stack ?? `${err.name}: ${err.message}`)
70
+ : String(err);
71
+ console.error(`[hicortex] /${route}: ${detail}`);
72
+ res.status(500).json({ error: "Internal error" });
73
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * identity-cli — `hicortex identity show|edit`, the secondary/headless edit
3
+ * surface for the standing identity layer (spec 2026-07-12 §6; renamed 0.18
4
+ * #264 from `context`). The Web UI (`/identity/ui`) is primary; this exists
5
+ * for boxes without a browser.
6
+ *
7
+ * hicortex identity show [name] GET /identity → print all sections, or one
8
+ * hicortex identity edit <name> GET section → $EDITOR → PUT if changed
9
+ *
10
+ * The legacy `hicortex context ...` command remains as a hidden alias so old
11
+ * scripts and muscle memory keep working (#264 backcompat).
12
+ *
13
+ * URL/token resolution mirrors learnings-identity.ts:44-49 (client mode →
14
+ * config.serverUrl; server mode → http://127.0.0.1:<port>; token from
15
+ * config.authToken) — explicitly NOT the hardcoded 127.0.0.1:8787 of
16
+ * status.ts. Fails soft with a clear message + non-zero exit on any server
17
+ * error, distinguishing a down server from an HTTP error (esp. 404 = server
18
+ * too old / wrong endpoint), like the OC plugin's describeGetFailure.
19
+ *
20
+ * The CLI targets the new `/identity` endpoint. A 0.18+ server (this package)
21
+ * also keeps `/context` as an alias, so a new CLI against a not-yet-upgraded
22
+ * server falls back through that alias via describeFailure's 404 hint.
23
+ */
24
+ /** Thrown for any expected, user-facing failure. cli.ts prints .message + exits 1. */
25
+ export declare class IdentityCliError extends Error {
26
+ }
27
+ /** Backcompat alias (#264). */
28
+ export declare const ContextCliError: typeof IdentityCliError;
29
+ export interface IdentityServerTarget {
30
+ baseUrl: string;
31
+ authToken?: string;
32
+ }
33
+ /**
34
+ * Resolve the server URL + token from a parsed config object. Pure + exported
35
+ * so it is unit-testable without a live config. Follows learnings-identity.ts.
36
+ */
37
+ export declare function resolveIdentityTarget(config: Record<string, unknown>): IdentityServerTarget;
38
+ /** Backcompat alias (#264). */
39
+ export declare const resolveContextTarget: typeof resolveIdentityTarget;
40
+ /** Backcompat alias (#264). */
41
+ export type ContextServerTarget = IdentityServerTarget;
42
+ /** Read ~/.hicortex/config.json (or $HICORTEX_HOME/config.json). Missing → {}. */
43
+ export declare function loadConfig(): Record<string, unknown>;
44
+ /** The Save decision: PUT only when the edited content differs. Pure + tested. */
45
+ export declare function sectionChanged(before: string, after: string): boolean;
46
+ export interface IdentityGetResponse {
47
+ sections: Record<string, string>;
48
+ updated_at: string | null;
49
+ clients: string[];
50
+ /** Present only for an agent-scoped read (0.13). */
51
+ agent?: string;
52
+ mode?: string;
53
+ }
54
+ /** Backcompat alias (#264). */
55
+ export type ContextGetResponse = IdentityGetResponse;
56
+ /** GET /identity. Throws IdentityCliError with a clear message on any failure. */
57
+ export declare function getIdentity(target: IdentityServerTarget, agent?: string): Promise<IdentityGetResponse>;
58
+ /** Backcompat alias (#264). */
59
+ export declare const getContext: typeof getIdentity;
60
+ /** PUT one section. Throws IdentityCliError with a clear message on any failure. */
61
+ export declare function putSection(target: IdentityServerTarget, name: string, content: string, agent?: string): Promise<void>;
62
+ /** Readable rendering of every section + the resolved clients line (show, no name). */
63
+ export declare function formatAllSections(data: IdentityGetResponse): string;
64
+ /** Raw markdown for one section (show <name>), or null if it does not exist. */
65
+ export declare function formatOneSection(data: IdentityGetResponse, name: string): string | null;
66
+ /**
67
+ * Default editor spawn: opens `file` in the first available editor, blocking
68
+ * until it exits. Returns true if an editor ran, false if none was found.
69
+ * Injectable so tests never actually spawn an editor.
70
+ */
71
+ export type EditorSpawn = (file: string) => boolean;
72
+ /**
73
+ * `edit <name>`: validate name (fast-fail; the server enforces too) → fetch
74
+ * current content → $EDITOR on a temp file → PUT only if changed. Temp file is
75
+ * always cleaned up. `spawn` is injectable for tests.
76
+ */
77
+ export declare function runEdit(name: string, spawn?: EditorSpawn, agent?: string): Promise<void>;
78
+ /**
79
+ * Split out a `--agent <id>` flag (anywhere in argv) from the positional args.
80
+ * The flag omitted → the global scope.
81
+ */
82
+ export declare function extractAgentFlag(args: string[]): {
83
+ agent?: string;
84
+ rest: string[];
85
+ };
86
+ /** Dispatch for `hicortex identity <sub>` (and the hidden `context` alias).
87
+ * Throws IdentityCliError on bad usage/failure. */
88
+ export declare function runIdentityCommand(args: string[]): Promise<void>;
89
+ /** Backcompat alias (#264). */
90
+ export declare const runContextCommand: typeof runIdentityCommand;