@gamaze/hicortex 0.3.16 → 0.4.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 +102 -78
- package/dist/cli.js +10 -4
- package/dist/consolidate.js +33 -2
- package/dist/db.js +5 -1
- package/dist/init.d.ts +3 -1
- package/dist/init.js +375 -55
- package/dist/license.js +1 -0
- package/dist/llm.d.ts +1 -0
- package/dist/llm.js +49 -4
- package/dist/mcp-server.js +62 -2
- package/dist/nightly.js +172 -3
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +41 -13
- package/dist/storage.js +5 -4
- package/dist/types.d.ts +2 -0
- package/package.json +2 -1
package/dist/init.js
CHANGED
|
@@ -316,67 +316,154 @@ async function persistLlmConfig() {
|
|
|
316
316
|
console.log(` ✓ LLM config already configured`);
|
|
317
317
|
return;
|
|
318
318
|
}
|
|
319
|
-
//
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
319
|
+
// Auto-detect all available LLM options
|
|
320
|
+
const options = [];
|
|
321
|
+
// 1. Check Ollama (local models — best for privacy and quality)
|
|
322
|
+
const ollamaModels = detectOllamaModels();
|
|
323
|
+
if (ollamaModels.length > 0) {
|
|
324
|
+
// Pick the largest model — only recommend if >= 7GB (~7b+ parameter models)
|
|
325
|
+
const best = ollamaModels[0]; // already sorted by size desc
|
|
326
|
+
const goodEnough = best.sizeGb >= 7;
|
|
327
|
+
options.push({
|
|
328
|
+
label: `Ollama ${best.name} (local${best.sizeGb ? `, ${best.sizeGb}GB` : ""}${goodEnough ? "" : ", small model"})`,
|
|
329
|
+
recommended: goodEnough,
|
|
330
|
+
save: () => {
|
|
331
|
+
config.llmBackend = "ollama";
|
|
332
|
+
config.llmBaseUrl = "http://localhost:11434";
|
|
333
|
+
config.llmModel = best.name;
|
|
334
|
+
saveConfig(configPath, config);
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
// Add other models if available
|
|
338
|
+
for (const m of ollamaModels.slice(1, 3)) {
|
|
339
|
+
options.push({
|
|
340
|
+
label: `Ollama ${m.name} (local${m.sizeGb ? `, ${m.sizeGb}GB` : ""})`,
|
|
341
|
+
save: () => {
|
|
342
|
+
config.llmBackend = "ollama";
|
|
343
|
+
config.llmBaseUrl = "http://localhost:11434";
|
|
344
|
+
config.llmModel = m.name;
|
|
345
|
+
saveConfig(configPath, config);
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
330
349
|
}
|
|
350
|
+
// 2. Check Claude CLI
|
|
351
|
+
const { findClaudeBinary } = await import("./llm.js");
|
|
352
|
+
const claudePath = findClaudeBinary();
|
|
353
|
+
if (claudePath) {
|
|
354
|
+
options.push({
|
|
355
|
+
label: "Claude CLI (subscription, Haiku model)",
|
|
356
|
+
recommended: ollamaModels.length === 0,
|
|
357
|
+
save: () => {
|
|
358
|
+
config.llmBackend = "claude-cli";
|
|
359
|
+
saveConfig(configPath, config);
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// 3. Check env vars
|
|
331
364
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
365
|
+
options.push({
|
|
366
|
+
label: "Anthropic API (from ANTHROPIC_API_KEY)",
|
|
367
|
+
save: () => {
|
|
368
|
+
config.llmApiKey = process.env.ANTHROPIC_API_KEY;
|
|
369
|
+
config.llmBaseUrl = process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com";
|
|
370
|
+
config.llmProvider = "anthropic";
|
|
371
|
+
saveConfig(configPath, config);
|
|
372
|
+
},
|
|
373
|
+
});
|
|
338
374
|
}
|
|
339
|
-
|
|
375
|
+
if (process.env.OPENAI_API_KEY) {
|
|
376
|
+
options.push({
|
|
377
|
+
label: "OpenAI API (from OPENAI_API_KEY)",
|
|
378
|
+
save: () => {
|
|
379
|
+
config.llmApiKey = process.env.OPENAI_API_KEY;
|
|
380
|
+
config.llmBaseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com";
|
|
381
|
+
config.llmProvider = "openai";
|
|
382
|
+
saveConfig(configPath, config);
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
// 4. Check OC auth-profiles
|
|
340
387
|
const ocLlm = readOcLlmConfig();
|
|
341
388
|
if (ocLlm) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
389
|
+
options.push({
|
|
390
|
+
label: `OpenClaw (${ocLlm.provider}/${ocLlm.model ?? "default"})`,
|
|
391
|
+
save: () => {
|
|
392
|
+
config.llmApiKey = ocLlm.apiKey;
|
|
393
|
+
config.llmBaseUrl = ocLlm.baseUrl;
|
|
394
|
+
config.llmProvider = ocLlm.provider;
|
|
395
|
+
if (ocLlm.model)
|
|
396
|
+
config.llmModel = ocLlm.model;
|
|
397
|
+
saveConfig(configPath, config);
|
|
398
|
+
},
|
|
399
|
+
});
|
|
350
400
|
}
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
401
|
+
// 5. Always offer manual entry and cancel
|
|
402
|
+
options.push({
|
|
403
|
+
label: "Other provider (requires API key)",
|
|
404
|
+
save: async () => {
|
|
405
|
+
console.log("\n Providers: Anthropic, OpenAI, Google, z.ai, OpenRouter, or any OpenAI-compatible");
|
|
406
|
+
const baseUrl = await ask(" Provider base URL: ");
|
|
407
|
+
if (!baseUrl) {
|
|
408
|
+
console.log(" ⚠ Cancelled.");
|
|
409
|
+
process.exit(0);
|
|
410
|
+
}
|
|
411
|
+
const apiKey = await ask(" API key: ");
|
|
412
|
+
if (!apiKey) {
|
|
413
|
+
console.log(" ⚠ Cancelled.");
|
|
414
|
+
process.exit(0);
|
|
415
|
+
}
|
|
416
|
+
const model = await ask(" Model name (optional): ");
|
|
417
|
+
config.llmApiKey = apiKey;
|
|
418
|
+
config.llmBaseUrl = baseUrl;
|
|
419
|
+
if (model)
|
|
420
|
+
config.llmModel = model;
|
|
421
|
+
saveConfig(configPath, config);
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
options.push({
|
|
425
|
+
label: "Cancel installation",
|
|
426
|
+
save: () => {
|
|
427
|
+
console.log("\n Hicortex requires an LLM to function. Installation cancelled.");
|
|
428
|
+
process.exit(0);
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
// Find recommended index
|
|
432
|
+
const recommendedIdx = options.findIndex(o => o.recommended);
|
|
433
|
+
const defaultIdx = recommendedIdx >= 0 ? recommendedIdx : 0;
|
|
434
|
+
// Display
|
|
435
|
+
console.log("\n LLM for nightly distillation:\n");
|
|
436
|
+
for (let i = 0; i < options.length; i++) {
|
|
437
|
+
const marker = i === defaultIdx ? " (recommended)" : "";
|
|
438
|
+
console.log(` ${i + 1}. ${options[i].label}${marker}`);
|
|
439
|
+
}
|
|
440
|
+
const choice = await ask(`\n Choice [${defaultIdx + 1}]: `);
|
|
441
|
+
const selected = choice ? parseInt(choice, 10) - 1 : defaultIdx;
|
|
442
|
+
if (selected < 0 || selected >= options.length) {
|
|
443
|
+
console.log(" Invalid choice.");
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
await options[selected].save();
|
|
447
|
+
console.log(` ✓ LLM configured: ${options[selected].label}`);
|
|
448
|
+
}
|
|
449
|
+
function detectOllamaModels() {
|
|
450
|
+
try {
|
|
451
|
+
const resp = (0, node_child_process_1.execSync)("curl -s --max-time 2 http://localhost:11434/api/tags", {
|
|
452
|
+
encoding: "utf-8",
|
|
453
|
+
timeout: 3000,
|
|
454
|
+
});
|
|
455
|
+
const data = JSON.parse(resp);
|
|
456
|
+
const models = (data.models ?? [])
|
|
457
|
+
.filter((m) => !m.name.includes("embed")) // skip embedding models
|
|
458
|
+
.map((m) => ({
|
|
459
|
+
name: m.name,
|
|
460
|
+
sizeGb: Math.round((m.size ?? 0) / 1e9 * 10) / 10,
|
|
461
|
+
}))
|
|
462
|
+
.sort((a, b) => b.sizeGb - a.sizeGb); // largest first
|
|
463
|
+
return models;
|
|
376
464
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
process.exit(0);
|
|
465
|
+
catch {
|
|
466
|
+
return [];
|
|
380
467
|
}
|
|
381
468
|
}
|
|
382
469
|
function saveConfig(configPath, config) {
|
|
@@ -525,7 +612,11 @@ async function ask(question) {
|
|
|
525
612
|
// ---------------------------------------------------------------------------
|
|
526
613
|
// Main
|
|
527
614
|
// ---------------------------------------------------------------------------
|
|
528
|
-
async function runInit() {
|
|
615
|
+
async function runInit(options = {}) {
|
|
616
|
+
if (options.serverUrl) {
|
|
617
|
+
await runClientInit(options.serverUrl);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
529
620
|
console.log("Hicortex — Setup for Claude Code\n");
|
|
530
621
|
// Phase 1: Detect
|
|
531
622
|
console.log("Detecting existing setup...\n");
|
|
@@ -649,3 +740,232 @@ async function runInit() {
|
|
|
649
740
|
console.log(" 3. Try /learn to save something to long-term memory");
|
|
650
741
|
console.log(` 4. Check server: curl ${serverUrl}/health`);
|
|
651
742
|
}
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
// Client Mode Init
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
async function runClientInit(serverUrl) {
|
|
747
|
+
console.log("Hicortex — Client Mode Setup\n");
|
|
748
|
+
serverUrl = serverUrl.replace(/\/+$/, "");
|
|
749
|
+
// Step 1: Verify server is reachable
|
|
750
|
+
console.log(`Checking server at ${serverUrl}...`);
|
|
751
|
+
try {
|
|
752
|
+
const resp = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(5000) });
|
|
753
|
+
if (!resp.ok)
|
|
754
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
755
|
+
const info = await resp.json();
|
|
756
|
+
console.log(` ✓ Server: v${info.version}, ${info.memories} memories, LLM: ${info.llm}`);
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
console.error(` ✗ Cannot reach server at ${serverUrl}`);
|
|
760
|
+
console.error(` ${err instanceof Error ? err.message : String(err)}`);
|
|
761
|
+
console.error(`\n Make sure the Hicortex server is running and accessible.`);
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
// Step 2: Auth — try default token first, prompt only if rejected
|
|
765
|
+
const DEFAULT_AUTH_TOKEN = "hctx-default-token";
|
|
766
|
+
let authToken = DEFAULT_AUTH_TOKEN;
|
|
767
|
+
try {
|
|
768
|
+
const probe = await fetch(`${serverUrl}/ingest`, {
|
|
769
|
+
method: "POST",
|
|
770
|
+
headers: {
|
|
771
|
+
"Content-Type": "application/json",
|
|
772
|
+
"Authorization": `Bearer ${DEFAULT_AUTH_TOKEN}`,
|
|
773
|
+
},
|
|
774
|
+
body: JSON.stringify({ content: "" }),
|
|
775
|
+
signal: AbortSignal.timeout(5000),
|
|
776
|
+
});
|
|
777
|
+
if (probe.status === 401) {
|
|
778
|
+
// Server uses a custom token — ask the user
|
|
779
|
+
const tokenAnswer = await ask("\nServer uses a custom auth token. Enter token: ");
|
|
780
|
+
authToken = tokenAnswer.trim();
|
|
781
|
+
if (!authToken) {
|
|
782
|
+
console.error(" ✗ Auth token required but not provided.");
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
785
|
+
// Verify
|
|
786
|
+
const verify = await fetch(`${serverUrl}/ingest`, {
|
|
787
|
+
method: "POST",
|
|
788
|
+
headers: {
|
|
789
|
+
"Content-Type": "application/json",
|
|
790
|
+
"Authorization": `Bearer ${authToken}`,
|
|
791
|
+
},
|
|
792
|
+
body: JSON.stringify({ content: "" }),
|
|
793
|
+
signal: AbortSignal.timeout(5000),
|
|
794
|
+
});
|
|
795
|
+
if (verify.status === 401) {
|
|
796
|
+
console.error(" ✗ Auth token rejected by server.");
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
console.log(" ✓ Custom auth token verified");
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
console.log(" ✓ Server connected (default auth)");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
// Probe failed but health passed — continue with default token
|
|
807
|
+
}
|
|
808
|
+
// Step 3: Configure LLM for local distillation
|
|
809
|
+
console.log("\nConfigure LLM for local session distillation:");
|
|
810
|
+
await persistLlmConfig();
|
|
811
|
+
// Step 4: Save client config
|
|
812
|
+
(0, node_fs_1.mkdirSync)(HICORTEX_HOME, { recursive: true });
|
|
813
|
+
const configPath = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
|
|
814
|
+
let config = {};
|
|
815
|
+
try {
|
|
816
|
+
config = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
|
|
817
|
+
}
|
|
818
|
+
catch { }
|
|
819
|
+
config.mode = "client";
|
|
820
|
+
config.serverUrl = serverUrl;
|
|
821
|
+
if (authToken)
|
|
822
|
+
config.authToken = authToken;
|
|
823
|
+
saveConfig(configPath, config);
|
|
824
|
+
console.log(` ✓ Client config saved to ${configPath}`);
|
|
825
|
+
// Step 5: Register CC MCP pointing to remote server
|
|
826
|
+
if (authToken) {
|
|
827
|
+
// Write directly with auth header
|
|
828
|
+
const claudeJsonPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude.json");
|
|
829
|
+
let claudeConfig = {};
|
|
830
|
+
try {
|
|
831
|
+
claudeConfig = JSON.parse((0, node_fs_1.readFileSync)(claudeJsonPath, "utf-8"));
|
|
832
|
+
}
|
|
833
|
+
catch { }
|
|
834
|
+
if (!claudeConfig.mcpServers)
|
|
835
|
+
claudeConfig.mcpServers = {};
|
|
836
|
+
claudeConfig.mcpServers.hicortex = {
|
|
837
|
+
type: "sse",
|
|
838
|
+
url: `${serverUrl}/sse`,
|
|
839
|
+
headers: { "Authorization": `Bearer ${authToken}` },
|
|
840
|
+
};
|
|
841
|
+
(0, node_fs_1.writeFileSync)(claudeJsonPath, JSON.stringify(claudeConfig, null, 2));
|
|
842
|
+
console.log(` ✓ Registered MCP server with auth`);
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
registerCcMcp(serverUrl);
|
|
846
|
+
}
|
|
847
|
+
allowHicortexTools();
|
|
848
|
+
// Step 6: Install CC commands
|
|
849
|
+
installCcCommands();
|
|
850
|
+
// Step 7: Inject CLAUDE.md learnings block
|
|
851
|
+
const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
|
|
852
|
+
if (!(0, node_fs_1.existsSync)(claudeMdPath) || !(0, node_fs_1.readFileSync)(claudeMdPath, "utf-8").includes("HICORTEX-LEARNINGS")) {
|
|
853
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
|
|
854
|
+
let content = "";
|
|
855
|
+
try {
|
|
856
|
+
content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
|
|
857
|
+
}
|
|
858
|
+
catch { }
|
|
859
|
+
const block = [
|
|
860
|
+
"<!-- HICORTEX-LEARNINGS:START -->",
|
|
861
|
+
"## Hicortex Learnings",
|
|
862
|
+
"",
|
|
863
|
+
"You have access to long-term memory via Hicortex MCP tools. Use `hicortex_search` when you need context from past sessions, decisions, or prior work. Use `hicortex_context` at session start to recall recent project state. Use `hicortex_ingest` to save important decisions or learnings. Sessions are auto-captured nightly.",
|
|
864
|
+
"<!-- HICORTEX-LEARNINGS:END -->",
|
|
865
|
+
].join("\n");
|
|
866
|
+
if (content.length > 0 && !content.endsWith("\n"))
|
|
867
|
+
content += "\n";
|
|
868
|
+
if (content.length > 0)
|
|
869
|
+
content += "\n";
|
|
870
|
+
content += block + "\n";
|
|
871
|
+
(0, node_fs_1.writeFileSync)(claudeMdPath, content);
|
|
872
|
+
console.log(` ✓ Added Hicortex Learnings block`);
|
|
873
|
+
}
|
|
874
|
+
// Step 8: Install nightly cron (distill locally, POST to server)
|
|
875
|
+
installNightlyCron();
|
|
876
|
+
console.log("\n✓ Hicortex client setup complete!\n");
|
|
877
|
+
console.log("How it works:");
|
|
878
|
+
console.log(" • MCP tools (search, context, ingest) talk to the remote server");
|
|
879
|
+
console.log(" • Nightly pipeline distills CC transcripts locally, POSTs memories to server");
|
|
880
|
+
console.log(" • No local database — all memories stored on the server");
|
|
881
|
+
console.log(`\nServer: ${serverUrl}`);
|
|
882
|
+
console.log("Restart Claude Code to activate.");
|
|
883
|
+
}
|
|
884
|
+
function installNightlyCron() {
|
|
885
|
+
const npxPath = findNpxPath();
|
|
886
|
+
const packageSpec = getPackageSpec();
|
|
887
|
+
const os = (0, node_os_1.platform)();
|
|
888
|
+
if (os === "darwin") {
|
|
889
|
+
const plistDir = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents");
|
|
890
|
+
const plistPath = (0, node_path_1.join)(plistDir, "com.gamaze.hicortex-nightly.plist");
|
|
891
|
+
const logPath = (0, node_path_1.join)(HICORTEX_HOME, "nightly.log");
|
|
892
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
893
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
894
|
+
<plist version="1.0">
|
|
895
|
+
<dict>
|
|
896
|
+
<key>Label</key>
|
|
897
|
+
<string>com.gamaze.hicortex-nightly</string>
|
|
898
|
+
<key>ProgramArguments</key>
|
|
899
|
+
<array>
|
|
900
|
+
<string>${npxPath}</string>
|
|
901
|
+
<string>-y</string>
|
|
902
|
+
<string>${packageSpec}</string>
|
|
903
|
+
<string>nightly</string>
|
|
904
|
+
</array>
|
|
905
|
+
<key>StartCalendarInterval</key>
|
|
906
|
+
<dict>
|
|
907
|
+
<key>Hour</key>
|
|
908
|
+
<integer>2</integer>
|
|
909
|
+
<key>Minute</key>
|
|
910
|
+
<integer>0</integer>
|
|
911
|
+
</dict>
|
|
912
|
+
<key>StandardOutPath</key>
|
|
913
|
+
<string>${logPath}</string>
|
|
914
|
+
<key>StandardErrorPath</key>
|
|
915
|
+
<string>${logPath}</string>
|
|
916
|
+
<key>EnvironmentVariables</key>
|
|
917
|
+
<dict>
|
|
918
|
+
<key>PATH</key>
|
|
919
|
+
<string>${(0, node_path_1.dirname)(npxPath)}:/usr/local/bin:/usr/bin:/bin</string>
|
|
920
|
+
</dict>
|
|
921
|
+
</dict>
|
|
922
|
+
</plist>`;
|
|
923
|
+
(0, node_fs_1.mkdirSync)(plistDir, { recursive: true });
|
|
924
|
+
(0, node_fs_1.writeFileSync)(plistPath, plist);
|
|
925
|
+
try {
|
|
926
|
+
try {
|
|
927
|
+
(0, node_child_process_1.execSync)(`launchctl unload ${plistPath} 2>/dev/null`, { stdio: "pipe" });
|
|
928
|
+
}
|
|
929
|
+
catch { }
|
|
930
|
+
(0, node_child_process_1.execSync)(`launchctl load ${plistPath}`, { stdio: "pipe" });
|
|
931
|
+
console.log(` ✓ Installed nightly cron (runs daily at 02:00)`);
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
console.log(` ⚠ Could not load nightly plist. Load manually: launchctl load ${plistPath}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
else if (os === "linux") {
|
|
938
|
+
const configDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user");
|
|
939
|
+
const servicePath = (0, node_path_1.join)(configDir, "hicortex-nightly.service");
|
|
940
|
+
const timerPath = (0, node_path_1.join)(configDir, "hicortex-nightly.timer");
|
|
941
|
+
const service = `[Unit]
|
|
942
|
+
Description=Hicortex Nightly (distill + POST)
|
|
943
|
+
|
|
944
|
+
[Service]
|
|
945
|
+
Type=oneshot
|
|
946
|
+
ExecStart=${npxPath} -y ${packageSpec} nightly
|
|
947
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
948
|
+
Environment=HOME=${(0, node_os_1.homedir)()}
|
|
949
|
+
WorkingDirectory=${(0, node_os_1.homedir)()}`;
|
|
950
|
+
const timer = `[Unit]
|
|
951
|
+
Description=Hicortex Nightly Timer
|
|
952
|
+
|
|
953
|
+
[Timer]
|
|
954
|
+
OnCalendar=*-*-* 02:00:00
|
|
955
|
+
Persistent=true
|
|
956
|
+
|
|
957
|
+
[Install]
|
|
958
|
+
WantedBy=timers.target`;
|
|
959
|
+
(0, node_fs_1.mkdirSync)(configDir, { recursive: true });
|
|
960
|
+
(0, node_fs_1.writeFileSync)(servicePath, service);
|
|
961
|
+
(0, node_fs_1.writeFileSync)(timerPath, timer);
|
|
962
|
+
try {
|
|
963
|
+
(0, node_child_process_1.execSync)("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
964
|
+
(0, node_child_process_1.execSync)("systemctl --user enable --now hicortex-nightly.timer", { stdio: "pipe" });
|
|
965
|
+
console.log(` ✓ Installed nightly timer (runs daily at 02:00)`);
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
console.log(` ⚠ Could not enable nightly timer. Enable manually: systemctl --user enable --now hicortex-nightly.timer`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
package/dist/license.js
CHANGED
package/dist/llm.d.ts
CHANGED
package/dist/llm.js
CHANGED
|
@@ -422,6 +422,28 @@ class LlmClient {
|
|
|
422
422
|
if (this.isRateLimited) {
|
|
423
423
|
throw new RateLimitError(this.rateLimitedUntil - Date.now());
|
|
424
424
|
}
|
|
425
|
+
const retryDelays = [30_000, 60_000, 120_000]; // 30s, 60s, 120s
|
|
426
|
+
let lastErr;
|
|
427
|
+
for (let attempt = 0; attempt <= retryDelays.length; attempt++) {
|
|
428
|
+
try {
|
|
429
|
+
return await this.completeOnce(model, prompt, maxTokens, timeoutMs);
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
433
|
+
const msg = lastErr.message;
|
|
434
|
+
if (attempt < retryDelays.length && (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("timeout") || msg.includes("Headers Timeout"))) {
|
|
435
|
+
const delay = retryDelays[attempt];
|
|
436
|
+
console.log(`[hicortex] LLM call failed (${msg.slice(0, 60)}), retry ${attempt + 1}/${retryDelays.length} in ${delay / 1000}s...`);
|
|
437
|
+
await new Promise(r => setTimeout(r, delay));
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
throw lastErr;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
throw lastErr;
|
|
445
|
+
}
|
|
446
|
+
async completeOnce(model, prompt, maxTokens, timeoutMs) {
|
|
425
447
|
if (this.config.provider === "claude-cli") {
|
|
426
448
|
return this.completeClaude(model, prompt, timeoutMs);
|
|
427
449
|
}
|
|
@@ -461,13 +483,15 @@ class LlmClient {
|
|
|
461
483
|
*/
|
|
462
484
|
async completeOllama(model, prompt, maxTokens, timeoutMs) {
|
|
463
485
|
const url = `${this.config.baseUrl.replace(/\/$/, "")}/api/generate`;
|
|
486
|
+
// Ollama can take minutes to process large contexts — use streaming to avoid
|
|
487
|
+
// Node.js fetch headers timeout (default ~300s kills long Ollama inferences)
|
|
464
488
|
const resp = await fetch(url, {
|
|
465
489
|
method: "POST",
|
|
466
490
|
headers: { "Content-Type": "application/json" },
|
|
467
491
|
body: JSON.stringify({
|
|
468
492
|
model,
|
|
469
493
|
prompt,
|
|
470
|
-
stream:
|
|
494
|
+
stream: true,
|
|
471
495
|
think: false,
|
|
472
496
|
options: { num_predict: maxTokens, num_ctx: 32768 },
|
|
473
497
|
}),
|
|
@@ -478,12 +502,33 @@ class LlmClient {
|
|
|
478
502
|
if (!resp.ok) {
|
|
479
503
|
const text = await resp.text().catch(() => "");
|
|
480
504
|
if (text.includes("1113") || text.includes("Insufficient balance")) {
|
|
481
|
-
this.handleRateLimit(resp);
|
|
505
|
+
this.handleRateLimit(resp);
|
|
482
506
|
}
|
|
483
507
|
throw new Error(`Ollama error ${resp.status}: ${text}`);
|
|
484
508
|
}
|
|
485
|
-
|
|
486
|
-
|
|
509
|
+
// Collect streamed response chunks
|
|
510
|
+
let result = "";
|
|
511
|
+
const reader = resp.body?.getReader();
|
|
512
|
+
if (!reader)
|
|
513
|
+
throw new Error("No response body");
|
|
514
|
+
const decoder = new TextDecoder();
|
|
515
|
+
while (true) {
|
|
516
|
+
const { done, value } = await reader.read();
|
|
517
|
+
if (done)
|
|
518
|
+
break;
|
|
519
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
520
|
+
for (const line of chunk.split("\n")) {
|
|
521
|
+
if (!line.trim())
|
|
522
|
+
continue;
|
|
523
|
+
try {
|
|
524
|
+
const data = JSON.parse(line);
|
|
525
|
+
if (data.response)
|
|
526
|
+
result += data.response;
|
|
527
|
+
}
|
|
528
|
+
catch { /* skip malformed lines */ }
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return result.trim();
|
|
487
532
|
}
|
|
488
533
|
/**
|
|
489
534
|
* Anthropic Messages API (/v1/messages). Used for Anthropic and z.ai.
|
package/dist/mcp-server.js
CHANGED
|
@@ -308,10 +308,11 @@ async function startServer(options = {}) {
|
|
|
308
308
|
const stats = (0, db_js_1.getStats)(db, dbPath);
|
|
309
309
|
console.log(`[hicortex] Ready: ${stats.memories} memories, ${stats.links} links, ` +
|
|
310
310
|
`${Math.round(stats.db_size_bytes / 1024)} KB`);
|
|
311
|
-
// Auth token: from
|
|
311
|
+
// Auth token: from config, env var, or default (always-on baseline security)
|
|
312
|
+
const DEFAULT_AUTH_TOKEN = "hctx-default-token";
|
|
312
313
|
const authToken = savedConfig?.authToken
|
|
313
314
|
?? process.env.HICORTEX_AUTH_TOKEN
|
|
314
|
-
??
|
|
315
|
+
?? DEFAULT_AUTH_TOKEN;
|
|
315
316
|
// Express app
|
|
316
317
|
const app = (0, express_1.default)();
|
|
317
318
|
app.use(express_1.default.json());
|
|
@@ -360,6 +361,65 @@ async function startServer(options = {}) {
|
|
|
360
361
|
llm: `${llmConfig.provider}/${llmConfig.model}`,
|
|
361
362
|
});
|
|
362
363
|
});
|
|
364
|
+
// REST /ingest — accept pre-distilled memories from remote clients
|
|
365
|
+
app.post("/ingest", async (req, res) => {
|
|
366
|
+
if (!db) {
|
|
367
|
+
res.status(503).json({ error: "Server not initialized" });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Pro license blocks remote ingest (upgrade to Team for multi-client)
|
|
371
|
+
const ip = req.ip ?? req.socket.remoteAddress ?? "";
|
|
372
|
+
const isLocal = ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
373
|
+
if (!isLocal) {
|
|
374
|
+
const features = (0, license_js_1.getFeatures)(stateDir);
|
|
375
|
+
if (features.remoteIngest === false) {
|
|
376
|
+
res.status(403).json({
|
|
377
|
+
error: "Pro license is single-machine. Upgrade to Team for multi-client remote ingestion.",
|
|
378
|
+
upgrade: "https://hicortex.gamaze.com/",
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const { content, source_agent, project, memory_type, privacy, source_session, session_date } = req.body ?? {};
|
|
384
|
+
if (!content || typeof content !== "string") {
|
|
385
|
+
res.status(400).json({ error: "Missing or invalid 'content' field" });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const validTypes = ["episode", "lesson", "fact", "decision"];
|
|
389
|
+
if (memory_type && !validTypes.includes(memory_type)) {
|
|
390
|
+
res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// Dedup by source_session (idempotent — skip if already ingested)
|
|
394
|
+
if (source_session) {
|
|
395
|
+
const existing = db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE source_session = ?").get(source_session);
|
|
396
|
+
if (existing.cnt > 0) {
|
|
397
|
+
res.status(200).json({ id: null, skipped: true, existing_count: existing.cnt });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// License check
|
|
402
|
+
const features = (0, license_js_1.getFeatures)(stateDir);
|
|
403
|
+
if (features.maxMemories > 0 && storage.countMemories(db) >= features.maxMemories) {
|
|
404
|
+
res.status(429).json({ error: "Memory limit reached", limit: features.maxMemories });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const embedding = await (0, embedder_js_1.embed)(content);
|
|
409
|
+
const id = storage.insertMemory(db, content, embedding, {
|
|
410
|
+
sourceAgent: source_agent ?? "remote-client",
|
|
411
|
+
sourceSession: source_session ?? undefined,
|
|
412
|
+
project: project ?? undefined,
|
|
413
|
+
memoryType: memory_type ?? "episode",
|
|
414
|
+
privacy: privacy ?? "WORK",
|
|
415
|
+
createdAt: session_date ? new Date(session_date).toISOString() : undefined,
|
|
416
|
+
});
|
|
417
|
+
res.status(201).json({ id, message: "Memory ingested" });
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
res.status(500).json({ error: "Ingestion failed", message: err instanceof Error ? err.message : String(err) });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
363
423
|
// SSE endpoint — each connection gets its own McpServer + transport
|
|
364
424
|
app.get("/sse", async (req, res) => {
|
|
365
425
|
const transport = new sse_js_1.SSEServerTransport("/messages", res);
|