@gamaze/hicortex 0.7.1 → 0.10.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 (49) hide show
  1. package/README.md +57 -39
  2. package/dist/claude-md.d.ts +9 -21
  3. package/dist/claude-md.js +9 -241
  4. package/dist/cli.d.ts +3 -2
  5. package/dist/cli.js +29 -11
  6. package/dist/consolidate.js +0 -7
  7. package/dist/db.js +24 -0
  8. package/dist/embedder.d.ts +11 -0
  9. package/dist/embedder.js +27 -0
  10. package/dist/extensions.d.ts +41 -88
  11. package/dist/extensions.js +36 -61
  12. package/dist/features.d.ts +21 -25
  13. package/dist/features.js +47 -83
  14. package/dist/hermes-transcript-reader.d.ts +27 -0
  15. package/dist/hermes-transcript-reader.js +134 -0
  16. package/dist/index.d.ts +16 -4
  17. package/dist/index.js +252 -344
  18. package/dist/init.d.ts +41 -1
  19. package/dist/init.js +545 -190
  20. package/dist/lesson-selection.d.ts +62 -0
  21. package/dist/lesson-selection.js +159 -0
  22. package/dist/lessons-context.d.ts +17 -0
  23. package/dist/lessons-context.js +96 -0
  24. package/dist/llm.d.ts +42 -29
  25. package/dist/llm.js +89 -270
  26. package/dist/mcp-server.d.ts +0 -1
  27. package/dist/mcp-server.js +404 -86
  28. package/dist/nightly.d.ts +9 -6
  29. package/dist/nightly.js +197 -357
  30. package/dist/oc-transcript-reader.d.ts +20 -0
  31. package/dist/oc-transcript-reader.js +61 -0
  32. package/dist/pi-transcript-reader.d.ts +1 -0
  33. package/dist/status.js +22 -2
  34. package/dist/storage.d.ts +7 -1
  35. package/dist/storage.js +28 -7
  36. package/dist/transcript-reader.d.ts +19 -0
  37. package/dist/transcript-reader.js +17 -3
  38. package/dist/types.d.ts +10 -0
  39. package/dist/uninstall.js +31 -1
  40. package/hermes-plugin/hicortex/README.md +77 -0
  41. package/hermes-plugin/hicortex/__init__.py +17 -0
  42. package/hermes-plugin/hicortex/client.py +162 -0
  43. package/hermes-plugin/hicortex/config.py +105 -0
  44. package/hermes-plugin/hicortex/plugin.yaml +12 -0
  45. package/hermes-plugin/hicortex/provider.py +432 -0
  46. package/openclaw.plugin.json +17 -44
  47. package/package.json +7 -5
  48. package/dist/pro-loader.d.ts +0 -33
  49. package/dist/pro-loader.js +0 -187
@@ -61,15 +61,23 @@ const embedder_js_1 = require("./embedder.js");
61
61
  const storage = __importStar(require("./storage.js"));
62
62
  const graph_js_1 = require("./graph.js");
63
63
  const retrieval = __importStar(require("./retrieval.js"));
64
- const consolidate_js_1 = require("./consolidate.js");
65
64
  const seed_lesson_js_1 = require("./seed-lesson.js");
65
+ const distiller_js_1 = require("./distiller.js");
66
66
  // ---------------------------------------------------------------------------
67
67
  // Server state
68
68
  // ---------------------------------------------------------------------------
69
69
  let db = null;
70
70
  let llm = null;
71
- let cancelConsolidation = null;
71
+ // llmConfig is module-level so the /distill handler can call resolveDistillFallback
72
+ // without having to read config on every request. null when no LLM is configured.
73
+ let llmConfig = null;
74
+ // distillFallbackMode controls whether a failed remote distill endpoint causes an
75
+ // immediate abort ("strict", default) or a fallback to the base model ("local").
76
+ let distillFallbackMode = "strict";
72
77
  let stateDir = "";
78
+ // Cache detectChunkSize results keyed by "<provider>/<model>@<baseUrl>" so we
79
+ // probe each endpoint once per server boot rather than once per /distill request.
80
+ const chunkSizeCache = new Map();
73
81
  let VERSION = "0.3.x";
74
82
  try {
75
83
  const pkg = JSON.parse(require("node:fs").readFileSync(require("node:path").join(__dirname, "..", "package.json"), "utf-8"));
@@ -123,17 +131,6 @@ function createMcpServer() {
123
131
  }, async ({ content, project, memory_type }) => {
124
132
  if (!db)
125
133
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
126
- if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
127
- return {
128
- content: [{
129
- type: "text",
130
- text: `Free tier limit reached (${(0, features_js_1.maxMemoriesAllowed)()} memories). ` +
131
- `Your existing memories and lessons still work — search and recall are unaffected. ` +
132
- `New memories won't be saved until you upgrade.\n\n` +
133
- `Upgrade for unlimited usage: https://hicortex.gamaze.com/`
134
- }],
135
- };
136
- }
137
134
  try {
138
135
  const embedding = await (0, embedder_js_1.embed)(content);
139
136
  const id = storage.insertMemory(db, content, embedding, {
@@ -313,22 +310,19 @@ async function startServer(options = {}) {
313
310
  console.log(`[hicortex] Initializing database at ${dbPath}`);
314
311
  db = (0, db_js_1.initDb)(dbPath);
315
312
  stateDir = require("node:path").dirname(dbPath);
316
- // LLM config: check config.json first, then env vars, then claude CLI
313
+ // LLM config: explicit config only no silent harness auto-detection.
314
+ // Named backends (claude-cli, ollama) → immediate config; everything else
315
+ // goes through resolveExplicitLlmConfig which requires a user-chosen provider.
316
+ // If nothing is configured: start recall-only with an unmissable warning.
317
317
  const savedConfig = readConfigFile(stateDir);
318
- let llmConfig;
319
318
  if (savedConfig?.llmBackend === "claude-cli") {
320
319
  const claudePath = (0, llm_js_1.findClaudeBinary)();
321
320
  if (claudePath) {
322
321
  llmConfig = (0, llm_js_1.claudeCliConfig)(claudePath);
323
322
  }
324
323
  else {
325
- console.warn("[hicortex] claude-cli configured but claude binary not found, falling back");
326
- llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
327
- llmBaseUrl: savedConfig?.llmBaseUrl,
328
- llmApiKey: savedConfig?.llmApiKey,
329
- llmModel: savedConfig?.llmModel,
330
- reflectModel: savedConfig?.reflectModel,
331
- });
324
+ console.warn("[hicortex] claude-cli configured but claude binary not found LLM disabled");
325
+ llmConfig = null;
332
326
  }
333
327
  }
334
328
  else if (savedConfig?.llmBackend === "ollama") {
@@ -342,36 +336,56 @@ async function startServer(options = {}) {
342
336
  };
343
337
  }
344
338
  else {
345
- llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
339
+ llmConfig = (0, llm_js_1.resolveExplicitLlmConfig)({
346
340
  llmBaseUrl: savedConfig?.llmBaseUrl,
347
341
  llmApiKey: savedConfig?.llmApiKey,
348
342
  llmModel: savedConfig?.llmModel,
349
343
  reflectModel: savedConfig?.reflectModel,
350
344
  });
351
345
  }
352
- // Apply optional distill endpoint (e.g. remote Ollama with faster model)
353
- if (savedConfig?.distillModel) {
354
- llmConfig.distillModel = savedConfig.distillModel;
355
- }
356
- if (savedConfig?.distillBaseUrl) {
357
- llmConfig.distillBaseUrl = savedConfig.distillBaseUrl;
358
- llmConfig.distillApiKey = savedConfig.distillApiKey ?? llmConfig.apiKey;
359
- llmConfig.distillProvider = savedConfig.distillProvider ?? llmConfig.provider;
346
+ if (llmConfig) {
347
+ // Apply optional distill endpoint (e.g. remote Ollama with faster model)
348
+ if (savedConfig?.distillModel) {
349
+ llmConfig.distillModel = savedConfig.distillModel;
350
+ }
351
+ if (savedConfig?.distillBaseUrl) {
352
+ llmConfig.distillBaseUrl = savedConfig.distillBaseUrl;
353
+ llmConfig.distillApiKey = savedConfig.distillApiKey ?? llmConfig.apiKey;
354
+ llmConfig.distillProvider = savedConfig.distillProvider ?? llmConfig.provider;
355
+ }
356
+ // Apply separate reflect endpoint if configured (e.g. remote Ollama with larger model)
357
+ if (savedConfig?.reflectBaseUrl) {
358
+ llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
359
+ llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
360
+ llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
361
+ }
362
+ // distillFallback: "strict" (default) aborts on remote failure so the session
363
+ // is retried next run. "local" restores 0.9.0 fallback-to-base-model behavior.
364
+ const df = savedConfig?.distillFallback;
365
+ distillFallbackMode = df === "local" ? "local" : "strict";
366
+ llm = new llm_js_1.LlmClient(llmConfig);
367
+ const distillInfo = llmConfig.distillBaseUrl
368
+ ? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
369
+ : llmConfig.distillModel ? llmConfig.distillModel : "";
370
+ const reflectInfo = llmConfig.reflectBaseUrl
371
+ ? `${llmConfig.reflectProvider}/${llmConfig.reflectModel}@${llmConfig.reflectBaseUrl}`
372
+ : llmConfig.reflectModel;
373
+ console.log(`[hicortex] LLM fast: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}, reflect: ${reflectInfo}`);
360
374
  }
361
- // Apply separate reflect endpoint if configured (e.g. remote Ollama with larger model)
362
- if (savedConfig?.reflectBaseUrl) {
363
- llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
364
- llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
365
- llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
375
+ else {
376
+ llm = null;
377
+ console.warn("");
378
+ console.warn("╔══════════════════════════════════════════════════════════════╗");
379
+ console.warn("║ NO LLM CONFIGURED running in recall-only mode ║");
380
+ console.warn("║ ║");
381
+ console.warn("║ search / lessons / context: ENABLED ║");
382
+ console.warn("║ /distill (capture) and consolidation: DISABLED ║");
383
+ console.warn("║ ║");
384
+ console.warn("║ To enable capture, run: ║");
385
+ console.warn("║ npx @gamaze/hicortex init ║");
386
+ console.warn("╚══════════════════════════════════════════════════════════════╝");
387
+ console.warn("");
366
388
  }
367
- llm = new llm_js_1.LlmClient(llmConfig);
368
- const distillInfo = llmConfig.distillBaseUrl
369
- ? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
370
- : llmConfig.distillModel ? llmConfig.distillModel : "";
371
- const reflectInfo = llmConfig.reflectBaseUrl
372
- ? `${llmConfig.reflectProvider}/${llmConfig.reflectModel}@${llmConfig.reflectBaseUrl}`
373
- : llmConfig.reflectModel;
374
- console.log(`[hicortex] LLM fast: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}, reflect: ${reflectInfo}`);
375
389
  // One-time migration of legacy state files (no-op if state.json exists)
376
390
  (0, state_js_1.migrateLegacyState)(stateDir);
377
391
  // License: read from options, config file, or env var, init feature cache
@@ -382,9 +396,6 @@ async function startServer(options = {}) {
382
396
  if (licenseKey) {
383
397
  console.log(`[hicortex] License key configured`);
384
398
  }
385
- // Schedule nightly consolidation
386
- const consolidateHour = options.consolidateHour ?? 2;
387
- cancelConsolidation = (0, consolidate_js_1.scheduleConsolidation)(db, llm, embedder_js_1.embed, consolidateHour);
388
399
  // Seed lesson on first run
389
400
  await (0, seed_lesson_js_1.injectSeedLesson)(db);
390
401
  // Self-heal: fix pinned version in daemon config
@@ -393,14 +404,19 @@ async function startServer(options = {}) {
393
404
  const stats = (0, db_js_1.getStats)(db, dbPath);
394
405
  console.log(`[hicortex] Ready: ${stats.memories} memories, ${stats.links} links, ` +
395
406
  `${Math.round(stats.db_size_bytes / 1024)} KB`);
396
- // Auth token: from config, env var, or default (always-on baseline security)
397
- const DEFAULT_AUTH_TOKEN = "hctx-default-token";
407
+ // Auth token: from config file or HICORTEX_AUTH_TOKEN env var.
408
+ // No hardcoded default — each server install generates its own token via init.
409
+ // Localhost connections bypass auth regardless (unchanged).
398
410
  const authToken = savedConfig?.authToken
399
- ?? process.env.HICORTEX_AUTH_TOKEN
400
- ?? DEFAULT_AUTH_TOKEN;
411
+ ?? process.env.HICORTEX_AUTH_TOKEN;
412
+ if (!authToken) {
413
+ console.warn("[hicortex] WARNING: no authToken configured — remote connections will be rejected " +
414
+ "(localhost still works). Run `npx @gamaze/hicortex init` to generate a token.");
415
+ }
401
416
  // Express app
402
417
  const app = (0, express_1.default)();
403
- app.use(express_1.default.json());
418
+ // Raise the body limit — whole-session denoised transcripts exceed the 100 kB default.
419
+ app.use(express_1.default.json({ limit: "25mb" }));
404
420
  // CORS: must be before auth so preflight OPTIONS requests get proper headers
405
421
  app.use((req, res, next) => {
406
422
  const origin = req.headers.origin;
@@ -417,21 +433,27 @@ async function startServer(options = {}) {
417
433
  }
418
434
  next();
419
435
  });
420
- // Optional bearer token auth (skip for /health, OPTIONS, and localhost)
421
- if (authToken) {
422
- console.log(`[hicortex] Bearer token auth enabled`);
423
- app.use((req, res, next) => {
424
- if (req.path === "/health")
425
- return next();
426
- const ip = req.ip ?? req.socket.remoteAddress ?? "";
427
- if (ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1")
428
- return next();
429
- const auth = req.headers.authorization;
430
- if (auth === `Bearer ${authToken}`)
431
- return next();
432
- res.status(401).json({ error: "Unauthorized" });
436
+ // Bearer token auth ALWAYS installed, fail-closed. /health, OPTIONS, and
437
+ // localhost bypass. With no token configured, remote requests are REJECTED
438
+ // (not open): the default bind is 0.0.0.0, so "no token = no auth" would
439
+ // expose the whole memory store to the network.
440
+ console.log(authToken
441
+ ? `[hicortex] Bearer token auth enabled`
442
+ : `[hicortex] No auth token configured — remote access DISABLED (localhost only). Run init to generate a token.`);
443
+ app.use((req, res, next) => {
444
+ if (req.path === "/health")
445
+ return next();
446
+ const ip = req.ip ?? req.socket.remoteAddress ?? "";
447
+ if (ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1")
448
+ return next();
449
+ if (authToken && req.headers.authorization === `Bearer ${authToken}`)
450
+ return next();
451
+ res.status(401).json({
452
+ error: authToken
453
+ ? "Unauthorized"
454
+ : "No auth token configured on this server — run `npx @gamaze/hicortex init` on the server, then connect with its token.",
433
455
  });
434
- }
456
+ });
435
457
  // SSE transport management — each connection gets its own McpServer instance
436
458
  const transports = new Map();
437
459
  // Health endpoint
@@ -443,7 +465,7 @@ async function startServer(options = {}) {
443
465
  memories: s.memories,
444
466
  links: s.links,
445
467
  db_size_kb: Math.round(s.db_size_bytes / 1024),
446
- llm: `${llmConfig.provider}/${llmConfig.model}`,
468
+ llm: llmConfig ? `${llmConfig.provider}/${llmConfig.model}` : "not configured",
447
469
  });
448
470
  });
449
471
  // REST /lessons — return lessons + memory index for client CLAUDE.md injection
@@ -488,16 +510,6 @@ async function startServer(options = {}) {
488
510
  res.status(503).json({ error: "Server not initialized" });
489
511
  return;
490
512
  }
491
- // Pro license blocks remote ingest (upgrade to Team for multi-client)
492
- const ip = req.ip ?? req.socket.remoteAddress ?? "";
493
- const isLocal = ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
494
- if (!isLocal && !(0, features_js_1.remoteIngestAllowed)()) {
495
- res.status(403).json({
496
- error: "Pro license is single-machine. Upgrade to Team for multi-client remote ingestion.",
497
- upgrade: "https://hicortex.gamaze.com/",
498
- });
499
- return;
500
- }
501
513
  const { content, source_agent, project, memory_type, privacy, source_session, session_date } = req.body ?? {};
502
514
  if (!content || typeof content !== "string") {
503
515
  res.status(400).json({ error: "Missing or invalid 'content' field" });
@@ -516,11 +528,6 @@ async function startServer(options = {}) {
516
528
  return;
517
529
  }
518
530
  }
519
- // License check
520
- if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
521
- res.status(429).json({ error: "Memory limit reached", limit: (0, features_js_1.maxMemoriesAllowed)() });
522
- return;
523
- }
524
531
  try {
525
532
  const embedding = await (0, embedder_js_1.embed)(content);
526
533
  const id = storage.insertMemory(db, content, embedding, {
@@ -537,6 +544,321 @@ async function startServer(options = {}) {
537
544
  res.status(500).json({ error: "Ingestion failed", message: err instanceof Error ? err.message : String(err) });
538
545
  }
539
546
  });
547
+ // REST /search — semantic search over the memory store.
548
+ // Common recall path for adapters (Hermes prefetch, CC push-hook). Stateless GET.
549
+ app.get("/search", async (req, res) => {
550
+ if (!db) {
551
+ res.status(503).json({ error: "Server not initialized" });
552
+ return;
553
+ }
554
+ const query = typeof req.query.query === "string" ? req.query.query.trim() : "";
555
+ if (!query) {
556
+ res.status(400).json({ error: "Missing 'query'" });
557
+ return;
558
+ }
559
+ const limit = req.query.limit ? Number(req.query.limit) : 5;
560
+ const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
561
+ const privacy = typeof req.query.privacy === "string" && req.query.privacy
562
+ ? req.query.privacy.split(",").map((s) => s.trim()).filter(Boolean)
563
+ : undefined;
564
+ try {
565
+ const results = await retrieval.retrieve(db, embedder_js_1.embed, query, { limit, project, privacy });
566
+ res.json({ results });
567
+ }
568
+ catch (err) {
569
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
570
+ }
571
+ });
572
+ // REST /context — recent context memories, optionally filtered by project.
573
+ app.get("/context", (req, res) => {
574
+ if (!db) {
575
+ res.status(503).json({ error: "Server not initialized" });
576
+ return;
577
+ }
578
+ const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
579
+ const limit = req.query.limit ? Number(req.query.limit) : 10;
580
+ const privacy = typeof req.query.privacy === "string" && req.query.privacy
581
+ ? req.query.privacy.split(",").map((s) => s.trim()).filter(Boolean)
582
+ : undefined;
583
+ try {
584
+ const results = retrieval.searchContext(db, { project, limit, privacy });
585
+ res.json({ results });
586
+ }
587
+ catch (err) {
588
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
589
+ }
590
+ });
591
+ // REST /distill — canonical capture endpoint (0.9.0+).
592
+ // Every machine (including the server itself) POSTs denoised session text here.
593
+ // The server distills, embeds, stores. Body limit: 25 MB (raised at app init).
594
+ //
595
+ // Accepts text (string, preferred nightly path) OR messages (array, legacy).
596
+ // Performs session-level dedup when session_id is present without segment_id.
597
+ // Uses cached detectChunkSize per endpoint so the probe runs once per boot.
598
+ app.post("/distill", async (req, res) => {
599
+ if (!db) {
600
+ res.status(503).json({ error: "Server not initialized" });
601
+ return;
602
+ }
603
+ if (!llm || !llmConfig) {
604
+ res.status(503).json({ error: "No LLM configured — run npx @gamaze/hicortex init. Session will be retried." });
605
+ return;
606
+ }
607
+ const { text, messages, source_agent, project, session_id, segment_id, session_date, privacy } = req.body ?? {};
608
+ // Resolve the conversation text from either the pre-denoised string or raw messages array.
609
+ let conversationText;
610
+ if (typeof text === "string" && text.length > 0) {
611
+ conversationText = text;
612
+ }
613
+ else if (Array.isArray(messages) && messages.length > 0) {
614
+ conversationText = (0, distiller_js_1.extractConversationText)(messages);
615
+ }
616
+ else {
617
+ res.status(400).json({ error: "Provide either 'text' (string) or 'messages' (array)" });
618
+ return;
619
+ }
620
+ // Session-level dedup: when session_id is present and this is a whole-session
621
+ // POST (no segment_id), skip if any chunk of this session is already stored.
622
+ if (session_id && !segment_id) {
623
+ // Escape LIKE wildcards — Hermes ids contain "_" (e.g. 20260701_045744_...).
624
+ const likePrefix = `${session_id.replace(/[\\%_]/g, (m) => "\\" + m)}#%`;
625
+ const existing = db.prepare("SELECT COUNT(*) as c FROM memories WHERE source_session = ? OR source_session LIKE ? ESCAPE '\\'").get(session_id, likePrefix);
626
+ if (existing.c > 0) {
627
+ res.status(200).json({ skipped: true, existing_count: existing.c });
628
+ return;
629
+ }
630
+ }
631
+ // Pre-flight the distill endpoint. In strict mode (default) a failed remote
632
+ // probe returns "abort" immediately without mutating llmConfig — the nightly
633
+ // watermark stays put so the session is re-shipped next run. In "local" mode
634
+ // the config is mutated to fall back to the base model.
635
+ const cfg = llmConfig;
636
+ const distillFallbackStatus = await (0, llm_js_1.resolveDistillFallback)(cfg, distillFallbackMode);
637
+ if (distillFallbackStatus === "abort") {
638
+ res.status(503).json({ error: "Distill endpoint unavailable — session will be retried next run" });
639
+ return;
640
+ }
641
+ // Cache detectChunkSize per endpoint so we probe at most once per server boot.
642
+ const effectiveProvider = cfg.distillProvider ?? cfg.provider;
643
+ const effectiveModel = cfg.distillModel ?? cfg.model;
644
+ const effectiveBaseUrl = cfg.distillBaseUrl ?? cfg.baseUrl;
645
+ const cacheKey = `${effectiveProvider}/${effectiveModel}@${effectiveBaseUrl}`;
646
+ if (!chunkSizeCache.has(cacheKey)) {
647
+ chunkSizeCache.set(cacheKey, await (0, distiller_js_1.detectChunkSize)(effectiveProvider, effectiveModel, effectiveBaseUrl));
648
+ }
649
+ const chunkSize = chunkSizeCache.get(cacheKey);
650
+ const date = typeof session_date === "string" && session_date ? session_date : new Date().toISOString().slice(0, 10);
651
+ // Per-entry idempotency prefix for the legacy segment_id path.
652
+ const sourcePrefix = session_id
653
+ ? `${session_id}${segment_id ? `#${segment_id}` : ""}`
654
+ : undefined;
655
+ try {
656
+ const entries = await (0, distiller_js_1.distillSession)(llm, conversationText, project ?? "unknown", date, chunkSize);
657
+ const ids = [];
658
+ for (let i = 0; i < entries.length; i++) {
659
+ const entry = entries[i];
660
+ if (typeof entry !== "string" || !entry.trim())
661
+ continue;
662
+ const embedding = await (0, embedder_js_1.embed)(entry);
663
+ const id = storage.insertMemory(db, entry, embedding, {
664
+ sourceAgent: source_agent ?? "unknown",
665
+ // Per-chunk key: "<session_id>#<i>". The prefix matches the nightly
666
+ // dedup check above, so a re-run of the same session is fully idempotent.
667
+ sourceSession: sourcePrefix ? `${sourcePrefix}#${i}` : undefined,
668
+ project: project ?? undefined,
669
+ memoryType: "episode",
670
+ privacy: privacy ?? "WORK",
671
+ createdAt: new Date(date).toISOString(),
672
+ });
673
+ ids.push(id);
674
+ }
675
+ res.status(201).json({ ids, distilled: ids.length });
676
+ }
677
+ catch (err) {
678
+ res.status(500).json({ error: "Distillation failed", message: err instanceof Error ? err.message : String(err) });
679
+ }
680
+ });
681
+ // -------------------------------------------------------------------------
682
+ // REST /update — update a memory (and re-embed when content changes).
683
+ //
684
+ // NOTE for #124: this endpoint returns the clean {updated: true, id} JSON
685
+ // shape that the future /viz surface can consume directly — no MCP wrapping.
686
+ // -------------------------------------------------------------------------
687
+ app.post("/update", async (req, res) => {
688
+ if (!db) {
689
+ res.status(503).json({ error: "Server not initialized" });
690
+ return;
691
+ }
692
+ const { id, content, project, memory_type, privacy } = req.body ?? {};
693
+ if (!id || typeof id !== "string") {
694
+ res.status(400).json({ error: "Missing or invalid 'id' field" });
695
+ return;
696
+ }
697
+ const fullId = resolveMemoryId(db, id);
698
+ if (!fullId) {
699
+ res.status(404).json({ error: `Memory not found: ${id}` });
700
+ return;
701
+ }
702
+ const fields = {};
703
+ if (content !== undefined)
704
+ fields.content = content;
705
+ if (project !== undefined)
706
+ fields.project = project;
707
+ if (memory_type !== undefined)
708
+ fields.memory_type = memory_type;
709
+ if (privacy !== undefined)
710
+ fields.privacy = privacy;
711
+ if (Object.keys(fields).length === 0) {
712
+ res.status(400).json({ error: "No fields to update" });
713
+ return;
714
+ }
715
+ const validTypes = ["episode", "lesson", "fact", "decision"];
716
+ if (memory_type !== undefined && !validTypes.includes(memory_type)) {
717
+ res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
718
+ return;
719
+ }
720
+ try {
721
+ storage.updateMemory(db, fullId, fields);
722
+ // Re-embed when content changes
723
+ if (content !== undefined) {
724
+ const embedding = await (0, embedder_js_1.embed)(content);
725
+ db.prepare("DELETE FROM memory_vectors WHERE id = ?").run(fullId);
726
+ db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)").run(fullId, Buffer.from(embedding.buffer));
727
+ }
728
+ res.json({ updated: true, id: fullId });
729
+ }
730
+ catch (err) {
731
+ res.status(500).json({ error: "Update failed", message: err instanceof Error ? err.message : String(err) });
732
+ }
733
+ });
734
+ // -------------------------------------------------------------------------
735
+ // REST /delete — permanently delete a memory and its links.
736
+ //
737
+ // NOTE for #124: returns {deleted: true, id} — clean JSON for future /viz.
738
+ // -------------------------------------------------------------------------
739
+ app.post("/delete", async (req, res) => {
740
+ if (!db) {
741
+ res.status(503).json({ error: "Server not initialized" });
742
+ return;
743
+ }
744
+ const { id } = req.body ?? {};
745
+ if (!id || typeof id !== "string") {
746
+ res.status(400).json({ error: "Missing or invalid 'id' field" });
747
+ return;
748
+ }
749
+ const fullId = resolveMemoryId(db, id);
750
+ if (!fullId) {
751
+ res.status(404).json({ error: `Memory not found: ${id}` });
752
+ return;
753
+ }
754
+ try {
755
+ storage.deleteMemory(db, fullId);
756
+ res.json({ deleted: true, id: fullId });
757
+ }
758
+ catch (err) {
759
+ res.status(500).json({ error: "Delete failed", message: err instanceof Error ? err.message : String(err) });
760
+ }
761
+ });
762
+ // -------------------------------------------------------------------------
763
+ // REST /index — knowledge domain index (same payload as hicortex_index MCP).
764
+ //
765
+ // NOTE for #124: this is the JSON surface that /viz will later consume.
766
+ // Keep the response shape clean: {domains} or {projects} fallback.
767
+ // -------------------------------------------------------------------------
768
+ app.get("/index", (_req, res) => {
769
+ try {
770
+ const state = (0, state_js_1.loadState)(stateDir);
771
+ const moduleIndex = state.moduleIndex;
772
+ if (moduleIndex && moduleIndex.domains && moduleIndex.domains.length > 0) {
773
+ res.json({ domains: moduleIndex.domains });
774
+ return;
775
+ }
776
+ // Fallback: flat project counts when moduleIndex is not yet built
777
+ if (!db) {
778
+ res.json({ domains: [] });
779
+ return;
780
+ }
781
+ const rows = db.prepare("SELECT project, COUNT(*) as cnt FROM memories WHERE project IS NOT NULL GROUP BY project ORDER BY cnt DESC LIMIT 20").all();
782
+ res.json({ projects: rows.map((r) => ({ name: r.project, count: r.cnt })) });
783
+ }
784
+ catch (err) {
785
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
786
+ }
787
+ });
788
+ // -------------------------------------------------------------------------
789
+ // REST /graph — knowledge graph query (same operations as hicortex_graph MCP).
790
+ //
791
+ // Supported ops: neighbors, hubs, path
792
+ // GET /graph?op=neighbors&id=<id>&limit=10&relationship=<rel>
793
+ // GET /graph?op=hubs&limit=10&domain=<domain>
794
+ // GET /graph?op=path&id=<from>&target_id=<to>
795
+ //
796
+ // NOTE for #124 (/viz): this endpoint IS the JSON surface that /viz will
797
+ // reuse for its graph visualisation. The response shape is intentionally
798
+ // clean ({results} for neighbors/path, {hubs} for hubs) so /viz can consume
799
+ // it without transformation. Do not add MCP-style text formatting here.
800
+ // -------------------------------------------------------------------------
801
+ app.get("/graph", (req, res) => {
802
+ if (!db) {
803
+ res.status(503).json({ error: "Server not initialized" });
804
+ return;
805
+ }
806
+ const op = typeof req.query.op === "string" ? req.query.op : "";
807
+ const VALID_OPS = ["neighbors", "hubs", "path"];
808
+ if (!VALID_OPS.includes(op)) {
809
+ res.status(400).json({ error: `Invalid op: must be one of ${VALID_OPS.join(", ")}` });
810
+ return;
811
+ }
812
+ const rawLimit = req.query.limit ? Number(req.query.limit) : undefined;
813
+ const resultLimit = rawLimit && Number.isFinite(rawLimit) ? rawLimit : 10;
814
+ const filterDomain = typeof req.query.domain === "string" && req.query.domain ? req.query.domain : undefined;
815
+ const filterRelationship = typeof req.query.relationship === "string" && req.query.relationship ? req.query.relationship : undefined;
816
+ try {
817
+ if (op === "neighbors") {
818
+ const idParam = typeof req.query.id === "string" ? req.query.id : "";
819
+ if (!idParam) {
820
+ res.status(400).json({ error: "id is required for neighbors operation" });
821
+ return;
822
+ }
823
+ const resolvedId = resolveMemoryId(db, idParam);
824
+ if (!resolvedId) {
825
+ res.status(404).json({ error: `Memory not found: ${idParam}` });
826
+ return;
827
+ }
828
+ const neighbors = (0, graph_js_1.getNeighbors)(db, resolvedId, resultLimit, filterRelationship);
829
+ res.json({ results: neighbors });
830
+ return;
831
+ }
832
+ if (op === "hubs") {
833
+ let hubs = (0, graph_js_1.detectHubs)(db);
834
+ if (filterDomain) {
835
+ hubs = hubs.filter((h) => h.domain === filterDomain || h.project === filterDomain);
836
+ }
837
+ res.json({ hubs: hubs.slice(0, resultLimit) });
838
+ return;
839
+ }
840
+ if (op === "path") {
841
+ const fromParam = typeof req.query.id === "string" ? req.query.id : "";
842
+ const toParam = typeof req.query.target_id === "string" ? req.query.target_id : "";
843
+ if (!fromParam || !toParam) {
844
+ res.status(400).json({ error: "id and target_id are required for path operation" });
845
+ return;
846
+ }
847
+ const fromId = resolveMemoryId(db, fromParam);
848
+ const toId = resolveMemoryId(db, toParam);
849
+ if (!fromId || !toId) {
850
+ res.status(404).json({ error: "One or both memory IDs not found" });
851
+ return;
852
+ }
853
+ const path = (0, graph_js_1.shortestPath)(db, fromId, toId);
854
+ res.json({ path: path ?? null });
855
+ return;
856
+ }
857
+ }
858
+ catch (err) {
859
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
860
+ }
861
+ });
540
862
  // SSE endpoint — each connection gets its own McpServer + transport
541
863
  app.get("/sse", async (req, res) => {
542
864
  const transport = new sse_js_1.SSEServerTransport("/messages", res);
@@ -590,10 +912,6 @@ async function startServer(options = {}) {
590
912
  // Graceful shutdown
591
913
  const shutdown = () => {
592
914
  console.log("[hicortex] Shutting down...");
593
- if (cancelConsolidation) {
594
- cancelConsolidation();
595
- cancelConsolidation = null;
596
- }
597
915
  for (const transport of transports.values()) {
598
916
  transport.close().catch(() => { });
599
917
  }
package/dist/nightly.d.ts CHANGED
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Nightly pipeline — manual trigger or called by the persistent server.
3
3
  *
4
- * Steps:
5
- * 1. Read new CC transcripts since last run
6
- * 2. Distill each session into memories via LLM
7
- * 3. Run consolidation (scoring, reflection, linking, decay)
8
- * 4. Inject lessons into CLAUDE.md
9
- * 5. Update last-run timestamp
4
+ * Steps (0.9.0+):
5
+ * 1. Read new harness transcripts since last run
6
+ * 2. Denoise + POST each session to /distill (server captures for itself via localhost)
7
+ * 3. Run consolidation (scoring, reflection, linking, decay) — server mode only
8
+ * 4. Update last-run timestamp
9
+ *
10
+ * Every machine (server + clients) uses the same capture path: denoise locally,
11
+ * POST to /distill. No local LLM required for capture; distillation is server-side.
10
12
  */
11
13
  export declare function runNightly(options?: {
12
14
  dryRun?: boolean;
15
+ captureOnly?: boolean;
13
16
  dbPath?: string;
14
17
  stateDir?: string;
15
18
  }): Promise<void>;