@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/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
- // Try auto-detect from environment
320
- const hcKey = process.env.HICORTEX_LLM_API_KEY;
321
- const hcUrl = process.env.HICORTEX_LLM_BASE_URL;
322
- if (hcKey && hcUrl) {
323
- config.llmApiKey = hcKey;
324
- config.llmBaseUrl = hcUrl;
325
- if (process.env.HICORTEX_LLM_MODEL)
326
- config.llmModel = process.env.HICORTEX_LLM_MODEL;
327
- saveConfig(configPath, config);
328
- console.log(` LLM config auto-detected from HICORTEX_LLM_* env vars`);
329
- return;
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
- config.llmApiKey = process.env.ANTHROPIC_API_KEY;
333
- config.llmBaseUrl = process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com";
334
- config.llmProvider = "anthropic";
335
- saveConfig(configPath, config);
336
- console.log(` ✓ LLM config auto-detected from ANTHROPIC_API_KEY`);
337
- return;
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
- // Try OC auth-profiles
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
- config.llmApiKey = ocLlm.apiKey;
343
- config.llmBaseUrl = ocLlm.baseUrl;
344
- config.llmProvider = ocLlm.provider;
345
- if (ocLlm.model)
346
- config.llmModel = ocLlm.model;
347
- saveConfig(configPath, config);
348
- console.log(` ✓ LLM config auto-detected from OpenClaw (${ocLlm.provider})`);
349
- return;
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
- // Not auto-detected ask the user
352
- console.log("\n LLM config not auto-detected. Hicortex needs an LLM for nightly learning.\n");
353
- console.log(" How should Hicortex access an LLM?\n");
354
- console.log(" 1. Use Claude subscription via CLI (Haiku model, no API key needed)");
355
- console.log(" 2. Enter an API key manually (Anthropic, OpenAI, or other)");
356
- console.log(" 3. Cancel installation\n");
357
- const choice = await ask(" Choice [1]: ");
358
- const selected = choice === "2" ? 2 : choice === "3" ? 3 : 1;
359
- if (selected === 1) {
360
- config.llmBackend = "claude-cli";
361
- saveConfig(configPath, config);
362
- console.log(` ✓ Configured to use Claude CLI (Haiku model via subscription)`);
363
- }
364
- else if (selected === 2) {
365
- const apiKey = await ask(" API key: ");
366
- if (!apiKey) {
367
- console.log(" ⚠ No key entered. Skipping.");
368
- return;
369
- }
370
- const baseUrl = await ask(" Base URL [https://api.anthropic.com]: ");
371
- config.llmApiKey = apiKey;
372
- config.llmBaseUrl = baseUrl || "https://api.anthropic.com";
373
- config.llmProvider = "anthropic";
374
- saveConfig(configPath, config);
375
- console.log(` ✓ API key saved`);
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
- else {
378
- console.log("\n Hicortex requires an LLM to function. Installation cancelled.");
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
@@ -18,6 +18,7 @@ const FREE_LICENSE = {
18
18
  vectorSearch: true,
19
19
  maxMemories: 250,
20
20
  crossAgent: true,
21
+ remoteIngest: true,
21
22
  },
22
23
  };
23
24
  /** Validate a license key against the Hicortex API */
package/dist/llm.d.ts CHANGED
@@ -89,6 +89,7 @@ export declare class LlmClient {
89
89
  */
90
90
  private completeWithOverride;
91
91
  private complete;
92
+ private completeOnce;
92
93
  /**
93
94
  * Claude CLI: shell out to `claude -p` for subscription users.
94
95
  * No API key needed — uses CC's authenticated session.
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: false,
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); // z.ai/Ollama Cloud quota exceeded
505
+ this.handleRateLimit(resp);
482
506
  }
483
507
  throw new Error(`Ollama error ${resp.status}: ${text}`);
484
508
  }
485
- const data = (await resp.json());
486
- return (data.response ?? "").trim();
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.
@@ -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 options, config file, or env var
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);