@gamaze/hicortex 0.7.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -39
- package/dist/claude-md.d.ts +9 -21
- package/dist/claude-md.js +9 -241
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +29 -11
- package/dist/consolidate.js +0 -7
- package/dist/db.js +24 -0
- package/dist/embedder.d.ts +11 -0
- package/dist/embedder.js +27 -0
- package/dist/extensions.d.ts +41 -88
- package/dist/extensions.js +36 -61
- package/dist/features.d.ts +21 -25
- package/dist/features.js +47 -83
- package/dist/hermes-transcript-reader.d.ts +27 -0
- package/dist/hermes-transcript-reader.js +134 -0
- package/dist/index.d.ts +16 -4
- package/dist/index.js +252 -344
- package/dist/init.d.ts +41 -1
- package/dist/init.js +545 -190
- package/dist/lesson-selection.d.ts +62 -0
- package/dist/lesson-selection.js +159 -0
- package/dist/lessons-context.d.ts +17 -0
- package/dist/lessons-context.js +96 -0
- package/dist/llm.d.ts +42 -29
- package/dist/llm.js +89 -270
- package/dist/mcp-server.d.ts +0 -1
- package/dist/mcp-server.js +404 -86
- package/dist/nightly.d.ts +9 -6
- package/dist/nightly.js +197 -357
- package/dist/oc-transcript-reader.d.ts +20 -0
- package/dist/oc-transcript-reader.js +61 -0
- package/dist/pi-transcript-reader.d.ts +1 -0
- package/dist/status.js +22 -2
- package/dist/storage.d.ts +7 -1
- package/dist/storage.js +28 -7
- package/dist/transcript-reader.d.ts +19 -0
- package/dist/transcript-reader.js +17 -3
- package/dist/types.d.ts +10 -0
- package/dist/uninstall.js +31 -1
- package/hermes-plugin/hicortex/README.md +77 -0
- package/hermes-plugin/hicortex/__init__.py +17 -0
- package/hermes-plugin/hicortex/client.py +162 -0
- package/hermes-plugin/hicortex/config.py +105 -0
- package/hermes-plugin/hicortex/plugin.yaml +12 -0
- package/hermes-plugin/hicortex/provider.py +432 -0
- package/openclaw.plugin.json +17 -44
- package/package.json +7 -5
- package/dist/pro-loader.d.ts +0 -33
- package/dist/pro-loader.js +0 -187
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
326
|
-
llmConfig =
|
|
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.
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. Run consolidation (scoring, reflection, linking, decay)
|
|
8
|
-
* 4.
|
|
9
|
-
*
|
|
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>;
|