@gamaze/hicortex 0.16.10 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/assets/dashboard.html +454 -0
- package/dist/cli.js +12 -1
- package/dist/config-read.d.ts +13 -0
- package/dist/config-read.js +37 -0
- package/dist/consolidate.d.ts +17 -1
- package/dist/consolidate.js +22 -4
- package/dist/dashboard.d.ts +162 -0
- package/dist/dashboard.js +410 -0
- package/dist/db.js +24 -0
- package/dist/hermes-transcript-reader.d.ts +15 -6
- package/dist/hermes-transcript-reader.js +32 -7
- package/dist/init.d.ts +26 -4
- package/dist/init.js +254 -55
- package/dist/mcp-server.js +17 -0
- package/dist/nightly.d.ts +8 -0
- package/dist/nightly.js +124 -12
- package/dist/telemetry.d.ts +11 -0
- package/dist/types.d.ts +29 -0
- package/dist/uninstall.js +12 -5
- package/dist/viz.d.ts +16 -0
- package/dist/viz.js +49 -0
- package/package.json +1 -1
package/dist/nightly.js
CHANGED
|
@@ -71,6 +71,7 @@ const retrieval_js_1 = require("./retrieval.js");
|
|
|
71
71
|
const state_js_1 = require("./state.js");
|
|
72
72
|
const capture_cursors_js_1 = require("./capture-cursors.js");
|
|
73
73
|
const capture_js_1 = require("./capture.js");
|
|
74
|
+
const dashboard_js_1 = require("./dashboard.js");
|
|
74
75
|
const telemetry_js_1 = require("./telemetry.js");
|
|
75
76
|
const init_js_1 = require("./init.js");
|
|
76
77
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
@@ -211,7 +212,8 @@ function rotateNightlyLog(stateDir = HICORTEX_HOME) {
|
|
|
211
212
|
}
|
|
212
213
|
async function runNightly(options = {}) {
|
|
213
214
|
const dryRun = options.dryRun ?? false;
|
|
214
|
-
|
|
215
|
+
let captureOnly = options.captureOnly ?? false;
|
|
216
|
+
const watchdog = options.watchdog ?? false;
|
|
215
217
|
const stateDir = options.stateDir ?? HICORTEX_HOME;
|
|
216
218
|
const recaptureWindowDays = options.recaptureWindowDays;
|
|
217
219
|
rotateNightlyLog(stateDir);
|
|
@@ -232,14 +234,59 @@ async function runNightly(options = {}) {
|
|
|
232
234
|
const { agentId } = (0, init_js_1.ensureAndPersistAgentId)((0, node_path_1.join)(stateDir, "config.json"));
|
|
233
235
|
savedConfig.agentId = agentId;
|
|
234
236
|
}
|
|
237
|
+
const port = savedConfig?.port ?? 8787;
|
|
238
|
+
// Watchdog gate (before the client/server branch so it is uniform). See the
|
|
239
|
+
// option doc above. On skip it returns early; on proceed it forces
|
|
240
|
+
// capture-only and falls through to the normal capture path (the capture
|
|
241
|
+
// lock with waitMs=0 provides single-flight; writeLastRun advances the
|
|
242
|
+
// cooldown marker on success).
|
|
243
|
+
if (watchdog && !dryRun) {
|
|
244
|
+
captureOnly = true; // the watchdog captures only — never consolidates.
|
|
245
|
+
// Success-cooldown: lastNightly is advanced ONLY on a clean capture
|
|
246
|
+
// (writeLastRun is success-gated), so reusing it gives SUCCESS-based
|
|
247
|
+
// cooldown — a FAILED preflight/capture retries on the next tick (minutes),
|
|
248
|
+
// a SUCCESS waits the cooldown. This is the better semantics the custom
|
|
249
|
+
// server watchdog (trigger-based) got wrong.
|
|
250
|
+
// readNonNegativeConfig (not readPositiveConfig) so 0 is honoured: 0 = no
|
|
251
|
+
// cooldown = capture every poll (a valid opt-in for a wired/high-frequency
|
|
252
|
+
// source), not a silent fallback to the default.
|
|
253
|
+
const cooldownH = (0, config_read_js_1.readNonNegativeConfig)(savedConfig ?? {}, "captureCooldownHours", 6);
|
|
254
|
+
const last = (0, state_js_1.loadState)(stateDir).lastNightly;
|
|
255
|
+
if (last) {
|
|
256
|
+
const ageH = (Date.now() - new Date(last).getTime()) / 3_600_000;
|
|
257
|
+
if (ageH < cooldownH) {
|
|
258
|
+
console.log(`[hicortex] watchdog: last capture ${ageH.toFixed(1)}h ago (< ${cooldownH}h cooldown) — skipping`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Quick reachability preflight (5s) — a cheap gate so a 20-min poll against
|
|
263
|
+
// a down link costs one short fetch, not the full in-run preflight. Client
|
|
264
|
+
// → remote server; server/co-located → localhost daemon.
|
|
265
|
+
const target = (savedConfig?.mode === "client"
|
|
266
|
+
? savedConfig.serverUrl
|
|
267
|
+
: `http://127.0.0.1:${port}`).replace(/\/+$/, "");
|
|
268
|
+
try {
|
|
269
|
+
const resp = await fetch(`${target}/health`, { signal: AbortSignal.timeout(5_000) });
|
|
270
|
+
if (!resp.ok) {
|
|
271
|
+
console.log(`[hicortex] watchdog: ${target}/health not ok (${resp.status}) — skipping (retry next tick)`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
console.log(`[hicortex] watchdog: ${target} unreachable (` +
|
|
277
|
+
`${e instanceof Error ? e.message : String(e)}) — skipping (retry next tick)`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
console.log(`[hicortex] watchdog: cooldown elapsed + ${target} reachable — capturing`);
|
|
281
|
+
}
|
|
235
282
|
if (savedConfig?.mode === "client") {
|
|
236
283
|
// --capture-only is accepted in client mode but irrelevant: client nightly
|
|
237
|
-
// is already capture-only (no consolidation step).
|
|
238
|
-
|
|
284
|
+
// is already capture-only (no consolidation step). (If watchdog-gated, the
|
|
285
|
+
// gate above already ran.)
|
|
286
|
+
await runClientNightly(savedConfig, dryRun, stateDir, recaptureWindowDays, watchdog);
|
|
239
287
|
return;
|
|
240
288
|
}
|
|
241
289
|
const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
|
|
242
|
-
const port = savedConfig?.port ?? 8787;
|
|
243
290
|
// #192: consolidation's decay/prune stage must score with the same clock as
|
|
244
291
|
// the server's retrieval path (config decayHalfLifeDays, default 365).
|
|
245
292
|
(0, retrieval_js_1.configureDecay)({ halfLifeDays: savedConfig?.decayHalfLifeDays });
|
|
@@ -369,9 +416,14 @@ async function runNightly(options = {}) {
|
|
|
369
416
|
// of the HTTP capture path). Full nightly only — capture-only runs are
|
|
370
417
|
// intended to run more frequently than once daily.
|
|
371
418
|
let lessonsGenerated; // hoisted for telemetry; undefined when reflection didn't run (skipped) — bucketed apart from a real 0
|
|
419
|
+
// Consolidation outcome for telemetry (0.17). undefined on capture-only runs
|
|
420
|
+
// (which send no nightly ping). "skipped" = runConsolidation's built-in
|
|
421
|
+
// nothing-to-do short-circuit (zero LLM calls), NOT a failure.
|
|
422
|
+
let consolidationStatus;
|
|
372
423
|
if (!dryRun && !captureOnly) {
|
|
373
424
|
if (!llm || !llmConfig) {
|
|
374
425
|
console.error("[hicortex] consolidation skipped: no LLM configured — run npx @gamaze/hicortex init");
|
|
426
|
+
consolidationStatus = "no_llm";
|
|
375
427
|
}
|
|
376
428
|
else {
|
|
377
429
|
// One model serves all phases (#231) — there is no separate endpoint to
|
|
@@ -390,9 +442,12 @@ async function runNightly(options = {}) {
|
|
|
390
442
|
}, {
|
|
391
443
|
minSimilarity: savedConfig?.supersessionMinSimilarity,
|
|
392
444
|
maxCalls: savedConfig?.supersessionMaxCalls,
|
|
393
|
-
}
|
|
445
|
+
},
|
|
446
|
+
// #241: config-driven total LLM-call ceiling (default 5000, was 200).
|
|
447
|
+
(0, config_read_js_1.readPositiveConfig)(savedConfig ?? {}, "consolidateMaxLlmCalls", consolidate_js_1.CONSOLIDATE_MAX_LLM_CALLS));
|
|
394
448
|
console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
|
|
395
449
|
(report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
|
|
450
|
+
consolidationStatus = report.status;
|
|
396
451
|
// Only set when reflection actually RAN (not skipped). A skipped stage
|
|
397
452
|
// (e.g. endpoint offline, #232 fail-soft) must NOT collapse to 0 — that
|
|
398
453
|
// would make "endpoint down" indistinguishable from "prompt too tight"
|
|
@@ -417,6 +472,53 @@ async function runNightly(options = {}) {
|
|
|
417
472
|
}
|
|
418
473
|
}
|
|
419
474
|
console.log(`[hicortex] Nightly pipeline complete.`);
|
|
475
|
+
// Dashboard snapshot (#224) — full nightly only. The snapshot reflects
|
|
476
|
+
// corpus state regardless of whether consolidation/LLM ran, so it is
|
|
477
|
+
// ALWAYS written here (the use case is history; an LLM-less install still
|
|
478
|
+
// accrues memories). Capture-only runs SKIP it (above, the block guards
|
|
479
|
+
// on !captureOnly). On the first run after deploy the table is empty →
|
|
480
|
+
// backfill synthesizes one row per day from created_at so the growth/
|
|
481
|
+
// composition charts have real history on day one.
|
|
482
|
+
if (!dryRun && !captureOnly) {
|
|
483
|
+
try {
|
|
484
|
+
const backfilled = (0, dashboard_js_1.backfillSnapshots)(db);
|
|
485
|
+
if (backfilled > 0) {
|
|
486
|
+
console.log(`[hicortex] Dashboard backfill: ${backfilled} day(s) synthesized from created_at`);
|
|
487
|
+
}
|
|
488
|
+
// Per-run deltas. `added` = this run's captured memory count (only
|
|
489
|
+
// available on the server path; client mode never reaches here — it
|
|
490
|
+
// POSTs to a remote /distill). dedup/supersession are derived from
|
|
491
|
+
// the dedup_log / superseded_by link tables: count rows created since
|
|
492
|
+
// the last snapshot (real OR backfilled — both carry valid ISO run_at,
|
|
493
|
+
// so a plain ORDER BY run_at DESC LIMIT 1 is the correct floor) so a
|
|
494
|
+
// manual `hicortex dedup` run between nightlies is still reflected,
|
|
495
|
+
// and a first-write after backfill counts only what landed AFTER the
|
|
496
|
+
// last backfilled day.
|
|
497
|
+
const lastSnap = db
|
|
498
|
+
.prepare("SELECT run_at FROM dashboard_snapshots ORDER BY run_at DESC LIMIT 1")
|
|
499
|
+
.get();
|
|
500
|
+
const sinceTs = lastSnap?.run_at ?? "1970-01-01T00:00:00.000Z";
|
|
501
|
+
const dedup = db
|
|
502
|
+
.prepare("SELECT COUNT(*) AS c FROM dedup_log WHERE merged_at > ?")
|
|
503
|
+
.get(sinceTs).c;
|
|
504
|
+
const supersession = db
|
|
505
|
+
.prepare(`SELECT COUNT(*) AS c FROM memory_links
|
|
506
|
+
WHERE relationship = 'superseded_by' AND created_at > ?`)
|
|
507
|
+
.get(sinceTs).c;
|
|
508
|
+
(0, dashboard_js_1.writeSnapshot)(db, new Date().toISOString(), {
|
|
509
|
+
added: memoriesIngested,
|
|
510
|
+
lessonsGenerated,
|
|
511
|
+
dedup,
|
|
512
|
+
supersession,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch (snapErr) {
|
|
516
|
+
// The snapshot is a monitoring side-effect — a failure here must NOT
|
|
517
|
+
// advance to a telemetry gap or move the watermark. Surface + continue.
|
|
518
|
+
console.warn(`[hicortex] Dashboard snapshot write failed: ` +
|
|
519
|
+
`${snapErr instanceof Error ? snapErr.message : String(snapErr)}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
420
522
|
// Anonymous telemetry (fire-and-forget, full nightly only).
|
|
421
523
|
// Capture-only runs are excluded to avoid inflating install pings.
|
|
422
524
|
if (!dryRun && !captureOnly && (0, telemetry_js_1.isTelemetryEnabled)(savedConfig)) {
|
|
@@ -446,6 +548,7 @@ async function runNightly(options = {}) {
|
|
|
446
548
|
mem: storage.countMemories(db),
|
|
447
549
|
lessons: storage.getLessons(db, 365).length,
|
|
448
550
|
lessonsGenerated,
|
|
551
|
+
consolidation: consolidationStatus,
|
|
449
552
|
sessions: batches.length,
|
|
450
553
|
ok: !hadTransientFailure,
|
|
451
554
|
shown: adoption.shown,
|
|
@@ -461,7 +564,7 @@ async function runNightly(options = {}) {
|
|
|
461
564
|
// ---------------------------------------------------------------------------
|
|
462
565
|
// Client Mode Nightly — denoise locally, POST to remote server's /distill
|
|
463
566
|
// ---------------------------------------------------------------------------
|
|
464
|
-
async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recaptureWindowDays) {
|
|
567
|
+
async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recaptureWindowDays, watchdog = false) {
|
|
465
568
|
const serverUrl = config.serverUrl.replace(/\/+$/, "");
|
|
466
569
|
const authToken = config.authToken;
|
|
467
570
|
console.log(`[hicortex] Client nightly starting${dryRun ? " (dry run)" : ""}`);
|
|
@@ -471,16 +574,20 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
|
|
|
471
574
|
// the whole run — the pre-flight only needs the link back, which can take
|
|
472
575
|
// ~1 min after wake.
|
|
473
576
|
//
|
|
474
|
-
// Config-overridable (#163): a wired Pi vs a sleeping laptop want
|
|
475
|
-
// values. Defaults:
|
|
577
|
+
// Config-overridable (#163): a wired Pi vs a sleeping/roaming laptop want
|
|
578
|
+
// different values. Defaults: 20s per-attempt timeout, 3 attempts, 60s gap.
|
|
579
|
+
// The 20s per-attempt (bumped from 15s in 0.17) absorbs a cold Tailscale
|
|
580
|
+
// handshake to a home server (DERP relay + NAT traversal can take 5–30s on a
|
|
581
|
+
// roaming laptop). For a genuinely DOWN link (`fetch failed`) no timeout
|
|
582
|
+
// length helps — the capture watchdog's frequent retry handles that (#239).
|
|
476
583
|
//
|
|
477
584
|
// WALL-CLOCK NOTE: setTimeout and AbortSignal.timeout do NOT advance while
|
|
478
|
-
// macOS is asleep, so the ~
|
|
585
|
+
// macOS is asleep, so the ~3m worst case (3×20s + 2×60s) is wall-clock-
|
|
479
586
|
// optimistic — a sleeping laptop can straddle sleep cycles and the real
|
|
480
587
|
// elapsed time can exceed it. Not a defect: the capture lock isn't held
|
|
481
588
|
// during the retry and the cursor design is dup-over-loss, so a late success
|
|
482
|
-
// is harmless. Just don't treat
|
|
483
|
-
const PREFLIGHT_TIMEOUT_MS = (0, config_read_js_1.readPositiveConfig)(config, "preflightTimeoutMs",
|
|
589
|
+
// is harmless. Just don't treat 3m as a hard wall-clock bound.
|
|
590
|
+
const PREFLIGHT_TIMEOUT_MS = (0, config_read_js_1.readPositiveConfig)(config, "preflightTimeoutMs", 20_000);
|
|
484
591
|
const PREFLIGHT_ATTEMPTS = Math.max(1, Math.floor((0, config_read_js_1.readPositiveConfig)(config, "preflightAttempts", 3)));
|
|
485
592
|
const PREFLIGHT_RETRY_GAP_MS = (0, config_read_js_1.readPositiveConfig)(config, "preflightRetryGapMs", 60_000);
|
|
486
593
|
let reachable = false;
|
|
@@ -513,7 +620,12 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
|
|
|
513
620
|
// no loop), and fire the telemetry ping (ok=false) so the abort is
|
|
514
621
|
// distinguishable from "powered off / uninstalled" in the activity aggregate.
|
|
515
622
|
process.exitCode = 1;
|
|
516
|
-
|
|
623
|
+
// In watchdog mode, suppress this failed-preflight ping: the watchdog
|
|
624
|
+
// retries every ~20 min, so a flaky link would otherwise emit up to ~72
|
|
625
|
+
// ok:false pings/day and distort the fleet health ratio (#239 CR). A
|
|
626
|
+
// sustained outage is still visible — as the ABSENCE of success pings, and
|
|
627
|
+
// a non-watchdog (manual) run still emits ok:false.
|
|
628
|
+
if (!dryRun && !watchdog && (0, telemetry_js_1.isTelemetryEnabled)(config)) {
|
|
517
629
|
await (0, telemetry_js_1.sendTelemetry)({
|
|
518
630
|
id: (0, telemetry_js_1.getTelemetryId)(stateDir),
|
|
519
631
|
v: VERSION,
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -85,6 +85,17 @@ export interface TelemetryPayload {
|
|
|
85
85
|
* and can't reveal a sudden drop in reflection output. 0.16.9+.
|
|
86
86
|
*/
|
|
87
87
|
lessonsGenerated?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Consolidation outcome for THIS full nightly (server mode only —
|
|
90
|
+
* capture-only runs send no nightly ping, so the field is absent there).
|
|
91
|
+
* `runConsolidation`'s status: "completed" | "skipped" | "failed", plus
|
|
92
|
+
* "no_llm" when consolidation was skipped because no LLM was configured.
|
|
93
|
+
* "skipped" = the built-in nothing-to-do short-circuit (no new + no unscored
|
|
94
|
+
* memories → zero LLM calls), NOT a failure. Lets the fleet aggregate tell a
|
|
95
|
+
* real consolidation run from a no-op without repurposing `ok` (which is the
|
|
96
|
+
* capture-health signal). 0.17+.
|
|
97
|
+
*/
|
|
98
|
+
consolidation?: "completed" | "skipped" | "failed" | "no_llm";
|
|
88
99
|
}
|
|
89
100
|
/**
|
|
90
101
|
* Check if telemetry is enabled. Disabled by:
|
package/dist/types.d.ts
CHANGED
|
@@ -215,6 +215,35 @@ export interface HicortexConfig {
|
|
|
215
215
|
* "Unsorted" — if configured — is just a normal domain.
|
|
216
216
|
*/
|
|
217
217
|
domains?: DomainDef[];
|
|
218
|
+
/**
|
|
219
|
+
* Success-cooldown (hours) for the CAPTURE watchdog (0.17). The capture
|
|
220
|
+
* timer fires `nightly --watchdog` on a short interval (~20 min); the
|
|
221
|
+
* watchdog captures only if MORE than this many hours have passed since the
|
|
222
|
+
* last SUCCESSFUL capture (state `lastNightly`). A failed preflight retries
|
|
223
|
+
* on the next tick (~20 min) — so a transient fire-instant network miss
|
|
224
|
+
* costs minutes, not a day (#239). Default 6 (≈4 captures/day). Read at
|
|
225
|
+
* runtime by the watchdog, not by `init`.
|
|
226
|
+
*/
|
|
227
|
+
captureCooldownHours?: number;
|
|
228
|
+
/**
|
|
229
|
+
* Hours (0–23, local time) for the CONSOLIDATION timer — the full nightly
|
|
230
|
+
* (capture + distill + score + reflect + link), installed by `init` for
|
|
231
|
+
* server/co-located mode ONLY (0.17). Default [10, 22]: the 22:00 evening
|
|
232
|
+
* slot runs after the day's capture waves (same-day results); the 10:00
|
|
233
|
+
* morning slot runs AFTER the morning's wake-up capture so it catches those
|
|
234
|
+
* pushes. Omitted on client installs (no local DB → no timer). Validated by
|
|
235
|
+
* parseHours.
|
|
236
|
+
*/
|
|
237
|
+
consolidationHours?: number[];
|
|
238
|
+
/**
|
|
239
|
+
* Ceiling on total LLM calls across all classify-tier consolidation stages
|
|
240
|
+
* (content-domain, link discovery, supersession) per nightly run (0.17, #241).
|
|
241
|
+
* A runaway backstop, not a throughput throttle — on a free local model the
|
|
242
|
+
* binding constraint is the nightly unit's wall-clock timeout, not call count.
|
|
243
|
+
* Default `5000` (was a hard-coded 200 that starved link/supersession during a
|
|
244
|
+
* classification backlog and drained large backlogs at ~cap/night).
|
|
245
|
+
*/
|
|
246
|
+
consolidateMaxLlmCalls?: number;
|
|
218
247
|
/**
|
|
219
248
|
* Minimum cosine(memory embedding, best domain prototype) for a no-fit
|
|
220
249
|
* memory to earn a WEAK primary instead of decaying (see nofit.ts).
|
package/dist/uninstall.js
CHANGED
|
@@ -43,11 +43,14 @@ async function runUninstall() {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
console.log();
|
|
46
|
-
// 1. Stop and remove daemon + nightly
|
|
47
|
-
// keeps firing against a half-removed install)
|
|
46
|
+
// 1. Stop and remove daemon + capture/nightly timers (all units, or a timer
|
|
47
|
+
// keeps firing against a half-removed install). Since 0.17 there are two
|
|
48
|
+
// scheduled jobs: the capture timer (hicortex-capture) and the
|
|
49
|
+
// consolidation timer (hicortex-nightly); remove both either way (a client
|
|
50
|
+
// install has no consolidation timer, a server install has both).
|
|
48
51
|
const os = (0, node_os_1.platform)();
|
|
49
52
|
if (os === "darwin") {
|
|
50
|
-
for (const name of ["com.gamaze.hicortex.plist", "com.gamaze.hicortex-nightly.plist"]) {
|
|
53
|
+
for (const name of ["com.gamaze.hicortex.plist", "com.gamaze.hicortex-nightly.plist", "com.gamaze.hicortex-capture.plist"]) {
|
|
51
54
|
const plistPath = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents", name);
|
|
52
55
|
if ((0, node_fs_1.existsSync)(plistPath)) {
|
|
53
56
|
try {
|
|
@@ -68,8 +71,12 @@ async function runUninstall() {
|
|
|
68
71
|
(0, node_child_process_1.execSync)("systemctl --user disable --now hicortex-nightly.timer 2>/dev/null");
|
|
69
72
|
}
|
|
70
73
|
catch { /* not installed */ }
|
|
74
|
+
try {
|
|
75
|
+
(0, node_child_process_1.execSync)("systemctl --user disable --now hicortex-capture.timer 2>/dev/null");
|
|
76
|
+
}
|
|
77
|
+
catch { /* not installed */ }
|
|
71
78
|
const unitDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user");
|
|
72
|
-
for (const name of ["hicortex.service", "hicortex-nightly.timer", "hicortex-nightly.service"]) {
|
|
79
|
+
for (const name of ["hicortex.service", "hicortex-nightly.timer", "hicortex-nightly.service", "hicortex-capture.timer", "hicortex-capture.service"]) {
|
|
73
80
|
const unitPath = (0, node_path_1.join)(unitDir, name);
|
|
74
81
|
try {
|
|
75
82
|
if ((0, node_fs_1.existsSync)(unitPath))
|
|
@@ -81,7 +88,7 @@ async function runUninstall() {
|
|
|
81
88
|
(0, node_child_process_1.execSync)("systemctl --user daemon-reload 2>/dev/null");
|
|
82
89
|
}
|
|
83
90
|
catch { /* fine */ }
|
|
84
|
-
console.log(" ✓ Removed systemd service + nightly
|
|
91
|
+
console.log(" ✓ Removed systemd service + capture/nightly timers");
|
|
85
92
|
}
|
|
86
93
|
// 2. Remove MCP from CC
|
|
87
94
|
try {
|
package/dist/viz.d.ts
CHANGED
|
@@ -66,6 +66,22 @@ export declare function readContextHtml(): string;
|
|
|
66
66
|
* cannot be read, exactly like vizHandler.
|
|
67
67
|
*/
|
|
68
68
|
export declare function contextUiHandler(): express.RequestHandler;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the on-disk path of the dashboard page. Throws (fail explicitly)
|
|
71
|
+
* when the asset is missing — same contract as resolveVizHtmlPath and
|
|
72
|
+
* resolveContextHtmlPath. assets/ sits next to both dist/ and src/ (the
|
|
73
|
+
* sibling layout the other resolvers rely on).
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveDashboardHtmlPath(): string;
|
|
76
|
+
/** Read the dashboard page. Read at request time so a reinstall is picked up live. */
|
|
77
|
+
export declare function readDashboardHtml(): string;
|
|
78
|
+
/**
|
|
79
|
+
* Express handler for GET /dashboard — the view-only analytics page (#224).
|
|
80
|
+
* 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
|
|
82
|
+
* createAuthMiddleware); all data comes from GET /dashboard/data (bearer-only).
|
|
83
|
+
*/
|
|
84
|
+
export declare function dashboardHandler(): express.RequestHandler;
|
|
69
85
|
/**
|
|
70
86
|
* Resolve the on-disk path of an allowlisted vendor bundle, or null when the
|
|
71
87
|
* requested name is not on the allowlist. The filesystem path is built ONLY
|
package/dist/viz.js
CHANGED
|
@@ -34,6 +34,9 @@ exports.vizHandler = vizHandler;
|
|
|
34
34
|
exports.resolveContextHtmlPath = resolveContextHtmlPath;
|
|
35
35
|
exports.readContextHtml = readContextHtml;
|
|
36
36
|
exports.contextUiHandler = contextUiHandler;
|
|
37
|
+
exports.resolveDashboardHtmlPath = resolveDashboardHtmlPath;
|
|
38
|
+
exports.readDashboardHtml = readDashboardHtml;
|
|
39
|
+
exports.dashboardHandler = dashboardHandler;
|
|
37
40
|
exports.resolveVizVendorPath = resolveVizVendorPath;
|
|
38
41
|
exports.vizVendorHandler = vizVendorHandler;
|
|
39
42
|
const node_fs_1 = require("node:fs");
|
|
@@ -81,6 +84,14 @@ function createAuthMiddleware(authToken) {
|
|
|
81
84
|
// sends it as a normal Authorization header on its /context fetches.
|
|
82
85
|
if (req.method === "GET" && req.path === "/context/ui")
|
|
83
86
|
return next();
|
|
87
|
+
// The /dashboard page SHELL is public for the same reason as /viz and
|
|
88
|
+
// /context/ui: a self-contained view-only analytics page, no data and no
|
|
89
|
+
// secrets (it ships verbatim in the npm tarball). All metric data comes
|
|
90
|
+
// from GET /dashboard/data, which stays bearer-only (localhost bypass) like
|
|
91
|
+
// every other data route; the page collects the token client-side and
|
|
92
|
+
// sends it as a normal Authorization header on its /dashboard/data fetch.
|
|
93
|
+
if (req.method === "GET" && req.path === "/dashboard")
|
|
94
|
+
return next();
|
|
84
95
|
// The pinned renderer bundles the /viz page loads (#139) are public for
|
|
85
96
|
// the same reason as the shell: static third-party code shipped verbatim
|
|
86
97
|
// in the npm tarball, zero data. Kept tight: GET only, and ONLY names on
|
|
@@ -184,6 +195,44 @@ function contextUiHandler() {
|
|
|
184
195
|
}
|
|
185
196
|
};
|
|
186
197
|
}
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Dashboard page (/dashboard, #224 — view-only memory analytics)
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the on-disk path of the dashboard page. Throws (fail explicitly)
|
|
203
|
+
* when the asset is missing — same contract as resolveVizHtmlPath and
|
|
204
|
+
* resolveContextHtmlPath. assets/ sits next to both dist/ and src/ (the
|
|
205
|
+
* sibling layout the other resolvers rely on).
|
|
206
|
+
*/
|
|
207
|
+
function resolveDashboardHtmlPath() {
|
|
208
|
+
const candidates = [(0, node_path_1.join)(__dirname, "..", "assets", "dashboard.html")];
|
|
209
|
+
for (const candidate of candidates) {
|
|
210
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
211
|
+
return candidate;
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`dashboard.html asset not found — looked in: ${candidates.join(", ")}. ` +
|
|
214
|
+
`The package install is incomplete (assets/ missing).`);
|
|
215
|
+
}
|
|
216
|
+
/** Read the dashboard page. Read at request time so a reinstall is picked up live. */
|
|
217
|
+
function readDashboardHtml() {
|
|
218
|
+
return (0, node_fs_1.readFileSync)(resolveDashboardHtmlPath(), "utf-8");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Express handler for GET /dashboard — the view-only analytics page (#224).
|
|
222
|
+
* 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
|
|
224
|
+
* createAuthMiddleware); all data comes from GET /dashboard/data (bearer-only).
|
|
225
|
+
*/
|
|
226
|
+
function dashboardHandler() {
|
|
227
|
+
return (_req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
res.type("html").send(readDashboardHtml());
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
res.status(503).json({ error: err instanceof Error ? err.message : String(err) });
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
187
236
|
/**
|
|
188
237
|
* Resolve the on-disk path of an allowlisted vendor bundle, or null when the
|
|
189
238
|
* requested name is not on the allowlist. The filesystem path is built ONLY
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
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.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|