@gamaze/hicortex 0.3.17 → 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 CHANGED
@@ -1,137 +1,159 @@
1
- # @gamaze/hicortex — Human-like Memory for Self-Improving AI Agents
1
+ # @gamaze/hicortex — Self-Learning Memory for AI Agents
2
2
 
3
- Upgrade your agents with Hicortex for automatic capturing of experiences, feedback, and decisions across all your connected agents. They start learning from every session, self-reflect, avoid past mistakes, and improve on their own — overnight, automatically. No configuration needed.
3
+ Your agents learn from every session successes and mistakes. Hicortex captures experiences, distills lessons, and applies them automatically. Connect multiple agents to shared memory and they improve together, overnight.
4
4
 
5
- Works with **OpenClaw** (in-process plugin) and **Claude Code** (HTTP/SSE MCP server).
5
+ Works with **Claude Code** (primary), **OpenClaw** (in-process plugin), and any MCP-compatible agent.
6
6
 
7
- ## Requirements
7
+ **Website:** [hicortex.gamaze.com](https://hicortex.gamaze.com) · **Docs:** [hicortex.gamaze.com/docs](https://hicortex.gamaze.com/docs/)
8
8
 
9
- - Node.js 18+
10
- - LLM provider (auto-detected from OC config or `ANTHROPIC_API_KEY` for CC)
11
- - ~500MB disk for database + embedding model
12
-
13
- ## Install — OpenClaw
9
+ ## Install — Server Mode (single machine)
14
10
 
15
11
  ```bash
16
- openclaw plugins install @gamaze/hicortex
17
- openclaw gateway restart
12
+ npx @gamaze/hicortex init
18
13
  ```
19
14
 
20
- No configuration needed. The plugin auto-detects your LLM provider from OpenClaw settings on first startup.
15
+ Detects your environment, installs a persistent MCP server daemon, auto-detects Ollama/Claude CLI/API keys, and registers with Claude Code. One command.
21
16
 
22
- ## Install — Claude Code
17
+ ## Install — Client Mode (multi-client)
23
18
 
24
19
  ```bash
25
- npx @gamaze/hicortex init
20
+ npx @gamaze/hicortex init --server https://your-server.example.com
26
21
  ```
27
22
 
28
- This detects your environment, installs a persistent MCP server daemon, registers it with Claude Code, and adds `/learn` and `/hicortex-activate` commands. Restart CC after setup.
23
+ Connects to a remote Hicortex server. Sessions are distilled locally (privacy), memories are sent to the shared server. No local database needed.
29
24
 
30
- Or manually:
25
+ ## Install — OpenClaw
31
26
 
32
27
  ```bash
33
- # Start server
34
- npx @gamaze/hicortex server
35
-
36
- # Register with CC
37
- claude mcp add hicortex --transport http http://localhost:8787/sse
28
+ openclaw plugins install @gamaze/hicortex
29
+ openclaw gateway restart
38
30
  ```
39
31
 
40
- ## Configure
41
-
42
- Optional config for OC (add to plugin entry in `~/.openclaw/openclaw.json`):
43
-
44
- | Field | Default | Description |
45
- |-------|---------|-------------|
46
- | `licenseKey` | _(none)_ | License key. Free tier (250 memories) without key. |
47
- | `llmBaseUrl` | _(auto)_ | Override LLM base URL |
48
- | `llmApiKey` | _(auto)_ | Override LLM API key |
49
- | `llmModel` | _(auto)_ | Override model for scoring and distillation |
50
- | `reflectModel` | _(auto)_ | Override model for nightly reflection |
51
- | `consolidateHour` | `2` | Hour (0-23, local time) for nightly consolidation |
52
- | `dbPath` | _(auto)_ | Custom database path |
32
+ ## Requirements
53
33
 
54
- For CC, set environment variables: `ANTHROPIC_API_KEY` (auto-detected), or `HICORTEX_LLM_BASE_URL` + `HICORTEX_LLM_API_KEY` + `HICORTEX_LLM_MODEL` for custom providers.
34
+ - Node.js 18+
35
+ - LLM: Ollama 9b+ (recommended), Claude CLI, or API key (Anthropic, OpenAI, etc.)
36
+ - ~500MB disk for database + embedding model
55
37
 
56
38
  ## What Happens Automatically
57
39
 
58
40
  | When | What | How |
59
41
  |------|------|-----|
60
- | Agent start | Recent lessons injected into context | OC: `before_agent_start` hook / CC: CLAUDE.md block |
61
- | Agent end | Conversation captured and distilled | OC: `agent_end` hook / CC: nightly transcript scan |
62
- | Nightly | Score importance, reflect, link, decay | In-process consolidation pipeline |
42
+ | Agent start | Recent lessons injected into context | CLAUDE.md block / OC hook |
43
+ | Agent end | Conversation captured | CC: nightly transcript scan / OC: hook |
44
+ | Nightly | Distill score → reflect link inject | Automatic pipeline |
63
45
 
64
- ## Agent Tools
46
+ ## Agent Tools (MCP)
65
47
 
66
- Available via MCP (both OC and CC):
48
+ 6 tools available via MCP:
67
49
 
68
- - **hicortex_search** — Semantic search across all stored knowledge
50
+ - **hicortex_search** — Semantic search across all stored memories
69
51
  - **hicortex_context** — Get recent decisions and project state
70
- - **hicortex_ingest** — Store a piece of knowledge directly
52
+ - **hicortex_ingest** — Store a memory directly
71
53
  - **hicortex_lessons** — Get actionable lessons from reflection
54
+ - **hicortex_update** — Fix incorrect memories (re-embeds on content change)
55
+ - **hicortex_delete** — Remove memories with cascade cleanup
72
56
 
73
57
  Skills: `/learn` to save explicit learnings.
74
58
 
75
59
  ## CLI Commands
76
60
 
77
61
  ```bash
78
- npx @gamaze/hicortex server # Start MCP HTTP/SSE server (port 8787)
79
- npx @gamaze/hicortex init # Set up for Claude Code
80
- npx @gamaze/hicortex nightly # Run distill + consolidate + inject
81
- npx @gamaze/hicortex status # Show config, DB stats, adapters
82
- npx @gamaze/hicortex uninstall # Remove CC integration (keeps DB)
62
+ npx @gamaze/hicortex server # Start MCP server (port 8787)
63
+ npx @gamaze/hicortex init # Set up server mode
64
+ npx @gamaze/hicortex init --server <url> # Set up client mode
65
+ npx @gamaze/hicortex nightly # Run distill + consolidate
66
+ npx @gamaze/hicortex nightly --dry-run # Preview without changes
67
+ npx @gamaze/hicortex status # Show config, DB stats
68
+ npx @gamaze/hicortex uninstall # Remove CC integration (keeps DB)
83
69
  ```
84
70
 
85
71
  ## Architecture
86
72
 
87
73
  ```
88
- @gamaze/hicortex (single npm package, dual mode)
89
- ├── OpenClaw mode (in-process plugin)
90
- ├── before_agent_start → inject lessons
91
- ├── agent_end → capture + distill
92
- └── registerService → DB, LLM, consolidation timer
93
-
94
- └── Claude Code mode (persistent HTTP/SSE server)
95
- ├── MCP tools → hicortex_search, hicortex_context, hicortex_ingest, hicortex_lessons
96
- ├── /health endpoint → monitoring
97
- ├── Nightly → scan CC transcripts, distill, consolidate, inject CLAUDE.md
98
- └── Shared DB at ~/.hicortex/hicortex.db
74
+ Client (MBP) Server (bedrock) Client (MBA)
75
+ ┌──────────┐ ┌──────────────┐ ┌──────────┐
76
+ CC sessions│ │ Shared DB │ │CC sessions│
77
+ ↓ │ POST │ │ POST │ ↓ │
78
+ Distill │──/ingest───→│ Embed+Store │←/ingest──│ Distill │
79
+ (local) │ │ ↓ │ │ (local) │
80
+ │ │ MCP │ Consolidate │ MCP │ │
81
+ CC ←│──(search)───│ (score,link, │──(search)→│ CC │
82
+ │ │ │ reflect) │ │ │
83
+ └──────────┘ └──────────────┘ └──────────┘
99
84
 
100
85
  Shared core:
101
- ├── SQLite + sqlite-vec + FTS5 (single file)
86
+ ├── SQLite + sqlite-vec + FTS5
102
87
  ├── bge-small-en-v1.5 embeddings (ONNX, local CPU)
103
- ├── BM25 + vector search with RRF fusion
104
- └── Multi-provider LLM (20+ providers)
88
+ ├── BM25 + vector search with RRF fusion + graph traversal
89
+ └── Multi-provider LLM (Ollama, Claude CLI, 20+ cloud providers)
105
90
  ```
106
91
 
107
- ## Database
92
+ ## Configuration
108
93
 
109
- Canonical location: `~/.hicortex/hicortex.db`. Existing OC installations at `~/.openclaw/data/hicortex.db` are automatically migrated on upgrade.
94
+ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
95
+
96
+ | Field | Description |
97
+ |-------|-------------|
98
+ | `mode` | `"server"` (default) or `"client"` |
99
+ | `serverUrl` | Remote server URL (client mode) |
100
+ | `llmModel` | Model for importance scoring |
101
+ | `distillModel` | Model for session distillation (9b+ recommended) |
102
+ | `reflectModel` | Model for nightly reflection (largest available) |
103
+ | `reflectBaseUrl` | Separate Ollama instance for reflection |
104
+ | `authToken` | Bearer token for endpoint auth |
105
+ | `licenseKey` | License key for higher tiers |
106
+
107
+ Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze.com/docs/configuration.html)
108
+
109
+ ## REST API
110
+
111
+ | Endpoint | Method | Auth | Description |
112
+ |----------|--------|------|-------------|
113
+ | `/health` | GET | No | Server status, memory count, version |
114
+ | `/ingest` | POST | Yes | Accept pre-distilled memories from clients |
115
+ | `/sse` | GET | Yes | MCP SSE stream for agent connections |
116
+ | `/messages` | POST | Yes | MCP message endpoint |
110
117
 
111
118
  ## Pricing
112
119
 
113
- | Tier | Price | Memories | Features |
114
- |------|-------|----------|----------|
115
- | Free | $0 | 250 | Full features: search, reflection, lessons, linking |
116
- | Pro | $9/month | Unlimited | Everything in Free, unlimited |
117
- | Lifetime | $149 | Unlimited | Pro forever |
118
- | Team | $29/month | Unlimited | Multi-agent shared memory |
120
+ | Tier | Price | Memories | Clients |
121
+ |------|-------|----------|---------|
122
+ | Free | $0 | 250 | Unlimited (trial) |
123
+ | Pro | $9/month | Unlimited | Single |
124
+ | Team | $29/month | Unlimited | Unlimited |
125
+ | Lifetime | $149 | Unlimited | Single |
119
126
 
120
- Get a license key at [hicortex.gamaze.com](https://hicortex.gamaze.com).
127
+ [hicortex.gamaze.com](https://hicortex.gamaze.com)
121
128
 
122
129
  ## Uninstall
123
130
 
124
- **OpenClaw:**
125
131
  ```bash
126
- openclaw plugins uninstall hicortex
132
+ npx @gamaze/hicortex uninstall # Claude Code
133
+ openclaw plugins uninstall hicortex # OpenClaw
127
134
  ```
128
135
 
129
- **Claude Code:**
130
- ```bash
131
- npx @gamaze/hicortex uninstall
132
- ```
136
+ Database preserved by default. Remove all data: `rm -rf ~/.hicortex`
133
137
 
134
- Your memory database is preserved by default. To remove all data: `rm -rf ~/.hicortex`
138
+ ## Configure OpenClaw
139
+
140
+ Optional config (add to plugin entry in `~/.openclaw/openclaw.json`):
141
+
142
+ | Field | Default | Description |
143
+ |-------|---------|-------------|
144
+ | `licenseKey` | _(none)_ | License key. Free tier (250 memories) without key. |
145
+ | `llmBaseUrl` | _(auto)_ | Override LLM base URL |
146
+ | `llmApiKey` | _(auto)_ | Override LLM API key |
147
+ | `llmModel` | _(auto)_ | Override model for scoring and distillation |
148
+ | `reflectModel` | _(auto)_ | Override model for nightly reflection |
149
+ | `consolidateHour` | `2` | Hour (0-23, local time) for nightly consolidation |
150
+ | `dbPath` | _(auto)_ | Custom database path |
151
+
152
+ For CC, set environment variables: `ANTHROPIC_API_KEY` (auto-detected), or `HICORTEX_LLM_BASE_URL` + `HICORTEX_LLM_API_KEY` + `HICORTEX_LLM_MODEL` for custom providers.
153
+
154
+ ## Database
155
+
156
+ Canonical location: `~/.hicortex/hicortex.db`. Existing OC installations at `~/.openclaw/data/hicortex.db` are automatically migrated on upgrade.
135
157
 
136
158
  ## Development
137
159
 
@@ -152,6 +174,8 @@ npm test
152
174
 
153
175
  **First startup slow:** The embedding model (~130MB) downloads on first run. Allow up to 2 minutes.
154
176
 
155
- **Server won't start (CC):** Check `~/.hicortex/server.log` for errors. Verify port 8787 is free: `lsof -i :8787`.
177
+ **Server won't start (CC):** Check `~/.hicortex/nightly.log` for errors. Verify port 8787 is free: `lsof -i :8787`.
156
178
 
157
179
  **Multiple CC sessions:** The HTTP server handles multiple concurrent sessions. Do not use stdio transport — it spawns separate processes per session.
180
+
181
+ **Ollama timeout on large sessions:** Hicortex uses streaming mode with 3 retries (30s, 60s, 120s backoff). If first call fails (model loading), retry handles it automatically.
package/dist/cli.js CHANGED
@@ -26,14 +26,17 @@ switch (command) {
26
26
  });
27
27
  break;
28
28
  }
29
- case "init":
29
+ case "init": {
30
+ const serverArg = process.argv.indexOf("--server");
31
+ const serverUrl = serverArg !== -1 ? process.argv[serverArg + 1] : undefined;
30
32
  import("./init.js").then(({ runInit }) => {
31
- runInit().catch((err) => {
33
+ runInit({ serverUrl }).catch((err) => {
32
34
  console.error("[hicortex] Init failed:", err);
33
35
  process.exit(1);
34
36
  });
35
37
  });
36
38
  break;
39
+ }
37
40
  case "nightly": {
38
41
  const dryRun = process.argv.includes("--dry-run");
39
42
  import("./nightly.js").then(({ runNightly }) => {
@@ -66,8 +69,9 @@ switch (command) {
66
69
  Usage: hicortex <command> [options]
67
70
 
68
71
  Commands:
69
- server Start the MCP HTTP/SSE server
70
- init Set up Hicortex for Claude Code
72
+ server Start the MCP HTTP/SSE server (server mode)
73
+ init Set up Hicortex (server mode, local DB + daemon)
74
+ init --server <url> Set up as client (remote server, local distillation)
71
75
  nightly Run nightly distill + consolidate + inject
72
76
  status Show current configuration and stats
73
77
  uninstall Remove CC integration (preserves DB)
@@ -75,10 +79,12 @@ Commands:
75
79
  Options:
76
80
  server --port <n> Port (default: 8787)
77
81
  server --host <h> Host (default: 127.0.0.1)
82
+ nightly --dry-run Preview without changes
78
83
 
79
84
  Examples:
80
85
  npx @gamaze/hicortex server
81
86
  npx @gamaze/hicortex init
87
+ npx @gamaze/hicortex init --server https://myserver.example.com
82
88
  npx @gamaze/hicortex status`);
83
89
  process.exit(command ? 1 : 0);
84
90
  }
@@ -228,7 +228,12 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
228
228
  return `[${project}] [${agent}] ${content}`;
229
229
  });
230
230
  const memoriesBlock = lines.join("\n\n");
231
- const prompt = (0, prompts_js_1.reflection)(memoriesBlock);
231
+ // Feed recent lessons to prevent duplicates and enable escalation
232
+ const recentLessons = storage.getLessons(db, 7).slice(0, 10);
233
+ const recentBlock = recentLessons.length > 0
234
+ ? recentLessons.map(l => `- ${l.content.slice(0, 150)}`).join("\n")
235
+ : undefined;
236
+ const prompt = (0, prompts_js_1.reflection)(memoriesBlock, recentBlock);
232
237
  if (dryRun) {
233
238
  return { lessons_generated: 0, skipped: false };
234
239
  }
@@ -250,10 +255,12 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
250
255
  if (!lessonText)
251
256
  continue;
252
257
  const project = String(lo.project ?? "global");
258
+ const lessonType = String(lo.type ?? "principle");
253
259
  const severity = String(lo.severity ?? "important");
254
260
  const confidence = String(lo.confidence ?? "medium");
255
261
  const sourcePattern = String(lo.source_pattern ?? "");
256
262
  let content = `## Lesson: ${lessonText}\n\n`;
263
+ content += `**Type:** ${lessonType}\n`;
257
264
  content += `**Severity:** ${severity}\n`;
258
265
  content += `**Confidence:** ${confidence}\n`;
259
266
  if (sourcePattern)
package/dist/init.d.ts CHANGED
@@ -14,4 +14,6 @@
14
14
  * - Inject CLAUDE.md learnings block
15
15
  * - Install CC custom commands (/learn, /hicortex-activate)
16
16
  */
17
- export declare function runInit(): Promise<void>;
17
+ export declare function runInit(options?: {
18
+ serverUrl?: string;
19
+ }): Promise<void>;
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);
package/dist/nightly.js CHANGED
@@ -96,8 +96,14 @@ function writeLastRun() {
96
96
  }
97
97
  async function runNightly(options = {}) {
98
98
  const dryRun = options.dryRun ?? false;
99
- const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
100
99
  const stateDir = options.stateDir ?? HICORTEX_HOME;
100
+ // Check mode: client or server
101
+ const savedConfig = readNightlyConfig(stateDir);
102
+ if (savedConfig?.mode === "client") {
103
+ await runClientNightly(savedConfig, dryRun);
104
+ return;
105
+ }
106
+ const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
101
107
  console.log(`[hicortex] Nightly pipeline starting${dryRun ? " (dry run)" : ""}`);
102
108
  console.log(`[hicortex] DB: ${dbPath}`);
103
109
  // Init DB
@@ -116,18 +122,43 @@ async function runNightly(options = {}) {
116
122
  }
117
123
  else {
118
124
  console.warn("[hicortex] claude-cli configured but binary not found, falling back");
119
- llmConfig = (0, llm_js_1.resolveLlmConfigForCC)();
125
+ llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
126
+ llmBaseUrl: savedConfig?.llmBaseUrl,
127
+ llmApiKey: savedConfig?.llmApiKey,
128
+ llmModel: savedConfig?.llmModel,
129
+ reflectModel: savedConfig?.reflectModel,
130
+ });
120
131
  }
121
132
  }
133
+ else if (savedConfig?.llmBackend === "ollama") {
134
+ llmConfig = {
135
+ baseUrl: savedConfig.llmBaseUrl ?? "http://localhost:11434",
136
+ apiKey: "",
137
+ model: savedConfig.llmModel ?? "qwen3.5:4b",
138
+ reflectModel: savedConfig.reflectModel ?? savedConfig.llmModel ?? "qwen3.5:4b",
139
+ provider: "ollama",
140
+ };
141
+ }
122
142
  else {
123
143
  llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
124
144
  llmBaseUrl: savedConfig?.llmBaseUrl,
125
145
  llmApiKey: savedConfig?.llmApiKey,
126
146
  llmModel: savedConfig?.llmModel,
147
+ reflectModel: savedConfig?.reflectModel,
127
148
  });
128
149
  }
150
+ // Apply distillModel and reflect overrides from config
151
+ if (savedConfig?.distillModel) {
152
+ llmConfig.distillModel = savedConfig.distillModel;
153
+ }
154
+ if (savedConfig?.reflectBaseUrl) {
155
+ llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
156
+ llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
157
+ llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
158
+ }
129
159
  const llm = new llm_js_1.LlmClient(llmConfig);
130
- console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}`);
160
+ const distillInfo = llmConfig.distillModel ? `, distill: ${llmConfig.distillModel}` : "";
161
+ console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}${distillInfo}`);
131
162
  // Step 1: Read new CC transcripts
132
163
  const since = readLastRun();
133
164
  console.log(`[hicortex] Reading CC transcripts since ${since.toISOString()}`);
@@ -206,3 +237,141 @@ async function runNightly(options = {}) {
206
237
  db.close();
207
238
  }
208
239
  }
240
+ // ---------------------------------------------------------------------------
241
+ // Client Mode Nightly — distill locally, POST to remote server
242
+ // ---------------------------------------------------------------------------
243
+ async function runClientNightly(config, dryRun) {
244
+ const serverUrl = config.serverUrl.replace(/\/+$/, "");
245
+ const authToken = config.authToken;
246
+ console.log(`[hicortex] Client nightly starting${dryRun ? " (dry run)" : ""}`);
247
+ console.log(`[hicortex] Server: ${serverUrl}`);
248
+ // Verify server is reachable
249
+ try {
250
+ const resp = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(5000) });
251
+ if (!resp.ok)
252
+ throw new Error(`HTTP ${resp.status}`);
253
+ const data = await resp.json();
254
+ console.log(`[hicortex] Server OK: v${data.version}, ${data.memories} memories`);
255
+ }
256
+ catch (err) {
257
+ console.error(`[hicortex] Server unreachable at ${serverUrl}: ${err instanceof Error ? err.message : String(err)}`);
258
+ console.error(`[hicortex] Aborting. Will retry next run.`);
259
+ return; // Don't update last-run so we retry
260
+ }
261
+ // Init LLM for local distillation
262
+ let llmConfig;
263
+ if (config.llmBackend === "claude-cli") {
264
+ const claudePath = (0, llm_js_1.findClaudeBinary)();
265
+ if (claudePath) {
266
+ llmConfig = (0, llm_js_1.claudeCliConfig)(claudePath);
267
+ }
268
+ else {
269
+ llmConfig = (0, llm_js_1.resolveLlmConfigForCC)();
270
+ }
271
+ }
272
+ else if (config.llmBackend === "ollama") {
273
+ llmConfig = {
274
+ baseUrl: config.llmBaseUrl ?? "http://localhost:11434",
275
+ apiKey: "",
276
+ model: config.llmModel ?? "qwen3.5:4b",
277
+ reflectModel: config.reflectModel ?? config.llmModel ?? "qwen3.5:4b",
278
+ provider: "ollama",
279
+ };
280
+ }
281
+ else {
282
+ llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
283
+ llmBaseUrl: config.llmBaseUrl,
284
+ llmApiKey: config.llmApiKey,
285
+ llmModel: config.llmModel,
286
+ });
287
+ }
288
+ if (config.distillModel) {
289
+ llmConfig.distillModel = config.distillModel;
290
+ }
291
+ const llm = new llm_js_1.LlmClient(llmConfig);
292
+ console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.distillModel ?? llmConfig.model}`);
293
+ // Read new CC transcripts
294
+ const since = readLastRun();
295
+ console.log(`[hicortex] Reading CC transcripts since ${since.toISOString()}`);
296
+ const batches = (0, transcript_reader_js_1.readCcTranscripts)(since);
297
+ console.log(`[hicortex] Found ${batches.length} new session(s)`);
298
+ if (batches.length === 0) {
299
+ console.log(`[hicortex] Nothing to distill.`);
300
+ if (!dryRun)
301
+ writeLastRun();
302
+ return;
303
+ }
304
+ // Distill each session and POST to server
305
+ let memoriesIngested = 0;
306
+ let sessionsSent = 0;
307
+ for (const batch of batches) {
308
+ const transcript = (0, distiller_js_1.extractConversationText)(batch.entries);
309
+ if (transcript.length < 200) {
310
+ console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): too short`);
311
+ continue;
312
+ }
313
+ console.log(`[hicortex] Distilling ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
314
+ if (dryRun) {
315
+ console.log(`[hicortex] [dry-run] Would distill ${transcript.length} chars`);
316
+ continue;
317
+ }
318
+ try {
319
+ const entries = await (0, distiller_js_1.distillSession)(llm, transcript, batch.projectName, batch.date);
320
+ if (entries.length === 0) {
321
+ console.log(`[hicortex] → No memories extracted`);
322
+ continue;
323
+ }
324
+ // POST each extracted memory to the server
325
+ let sessionCount = 0;
326
+ for (const entry of entries) {
327
+ const resp = await fetch(`${serverUrl}/ingest`, {
328
+ method: "POST",
329
+ headers: {
330
+ "Content-Type": "application/json",
331
+ ...(authToken ? { "Authorization": `Bearer ${authToken}` } : {}),
332
+ },
333
+ body: JSON.stringify({
334
+ content: entry,
335
+ source_agent: `claude-code/${batch.projectName}`,
336
+ project: batch.projectName,
337
+ memory_type: "episode",
338
+ privacy: "WORK",
339
+ source_session: batch.sessionId,
340
+ session_date: batch.date,
341
+ }),
342
+ signal: AbortSignal.timeout(30_000),
343
+ });
344
+ const result = await resp.json();
345
+ if (resp.status === 201) {
346
+ sessionCount++;
347
+ memoriesIngested++;
348
+ }
349
+ else if (result.skipped) {
350
+ console.log(`[hicortex] → Already ingested (${result.existing_count} existing)`);
351
+ break;
352
+ }
353
+ else if (resp.status === 401) {
354
+ console.error(`[hicortex] Auth failed. Check authToken in ~/.hicortex/config.json`);
355
+ return;
356
+ }
357
+ else if (resp.status === 429) {
358
+ console.log(`[hicortex] Server memory limit reached.`);
359
+ return;
360
+ }
361
+ else {
362
+ console.error(`[hicortex] Ingest failed (${resp.status}): ${result.error}`);
363
+ }
364
+ }
365
+ if (sessionCount > 0) {
366
+ sessionsSent++;
367
+ console.log(`[hicortex] → ${sessionCount} memories sent to server`);
368
+ }
369
+ }
370
+ catch (err) {
371
+ console.error(`[hicortex] Failed: ${err instanceof Error ? err.message : String(err)}`);
372
+ }
373
+ }
374
+ if (!dryRun)
375
+ writeLastRun();
376
+ console.log(`[hicortex] Client nightly complete: ${memoriesIngested} memories from ${sessionsSent} sessions → ${serverUrl}`);
377
+ }
package/dist/prompts.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare function importanceScoring(memoriesBlock: string): string;
9
9
  /**
10
10
  * Reflection prompt. Takes a {memories_block} with today's memories.
11
11
  */
12
- export declare function reflection(memoriesBlock: string): string;
12
+ export declare function reflection(memoriesBlock: string, recentLessons?: string): string;
13
13
  /**
14
14
  * Distillation prompt. Extracts knowledge from a session transcript.
15
15
  */
package/dist/prompts.js CHANGED
@@ -32,33 +32,61 @@ No explanations. Just the JSON array.`;
32
32
  /**
33
33
  * Reflection prompt. Takes a {memories_block} with today's memories.
34
34
  */
35
- function reflection(memoriesBlock) {
36
- return `You are a self-improvement analyst for a multi-agent AI system. Review today's memories and extract 1-3 actionable lessons.
35
+ function reflection(memoriesBlock, recentLessons) {
36
+ const recentSection = recentLessons
37
+ ? `\nRECENT LESSONS (already generated — do NOT duplicate, but DO escalate if patterns recur):\n${recentLessons}\n`
38
+ : "";
39
+ return `You are a learning analyst for a multi-agent AI system. Review today's memories and extract actionable lessons from BOTH successes and failures.
37
40
 
38
- Quality over quantity. One genuine insight that prevents a future mistake is worth more than five restatements of what happened. Most days, 1-2 lessons is ideal. An empty array is fine if nothing warrants a lesson.
41
+ Like human learning: we grow fastest when we reinforce what works AND correct what doesn't. A system that only learns from mistakes becomes overly cautious. A system that only learns from successes never improves. The combination multiplies.
39
42
 
40
- Prefer GLOBAL lessons (applicable across all projects and agents) over project-specific ones. Only mark a lesson as project-specific if it truly cannot generalize.
41
-
42
- Good lesson: "When modifying template files processed by sed, always verify ALL substitution targets by diffing the output — partial fixes cause silent failures on other deployments"
43
- Bad lesson: "The deploy script had a bug" (restatement, not actionable)
43
+ Quality over quantity. 1-3 lessons is typical. An empty array [] is the CORRECT response when memories show routine competent work without noteworthy patterns, surprises, or friction. Do not manufacture lessons from nothing.
44
44
 
45
+ LESSON TYPES:
46
+ - "reinforce": An approach or strategy that worked well — repeat and spread it
47
+ - "correct": A mistake, gap, or near-miss that should be avoided
48
+ - "principle": A general insight derived from either success or failure
49
+ ${recentSection}
45
50
  TODAY'S MEMORIES:
46
51
  ${memoriesBlock}
47
52
 
53
+ EXAMPLES:
54
+ Good reinforce: "Bundling related changes into a single PR with clear narrative gets faster approval — apply for all refactors"
55
+ Good reinforce: "When presenting multi-scenario analysis, show assumptions side-by-side so stakeholders evaluate trade-offs rather than reacting to isolated worst-cases"
56
+ Good correct: "Always verify ALL substitution targets by diffing output — partial fixes cause silent failures"
57
+ Good principle: "Gather evidence from logs before forming hypotheses — evidence-first debugging resolved issues 3x faster today"
58
+ Bad lesson: "The deploy script had a bug" (restatement, not actionable)
59
+
48
60
  For each lesson, output a JSON object:
49
61
  - "lesson": Concise, actionable rule in imperative voice
50
- - "project": "global" unless genuinely project-specific
62
+ - "type": "reinforce" | "correct" | "principle"
63
+ - "project": "global" unless genuinely project-specific (project-specific lessons are still valuable)
51
64
  - "severity": "critical" | "important" | "minor"
52
65
  - "confidence": "high" | "medium" | "low"
53
- - "source_pattern": What triggered this (1 sentence)
66
+ - "source_pattern": What triggered this (1 sentence, no personal data)
54
67
 
55
- Confidence:
56
- - "high": Pattern across multiple events, or clear mistake with obvious fix. Safe to auto-inject into agent instructions.
68
+ Severity guide:
69
+ - "critical": Near-misses that could have caused data loss or security breach, even if caught in time. Also: recurring patterns that keep appearing despite prior corrections.
70
+ - "important": Clear cause-effect, likely to recur. Worth sharing across agents.
71
+ - "minor": Useful optimization, single incident.
72
+
73
+ Confidence guide:
74
+ - "high": Pattern across multiple events, or clear cause-effect. Safe to auto-inject into agent instructions.
57
75
  - "medium": Single incident but likely to recur. Store but don't auto-propagate.
58
76
  - "low": Speculative. Store for retrieval only.
59
77
 
60
- Focus on: process gaps, repeated friction, silent failures, user corrections to agent behavior, cross-agent patterns.
61
- Skip: trivial actions, already-documented rules, one-off events.
78
+ Focus on:
79
+ - SUCCESSES: effective strategies, approaches the user validated, patterns that saved time, clean solutions
80
+ - FAILURES: process gaps, repeated friction, silent failures, user corrections
81
+ - OMISSIONS: things that should have been done but weren't (missing tests, unchecked code paths, forgotten follow-ups)
82
+ - NEAR-MISSES: problems caught before damage — these deserve critical severity
83
+ - CONTRADICTIONS: cases where something appeared to work but didn't, or agents reached opposite conclusions
84
+ - CROSS-AGENT PATTERNS: same issue or success across different agents — especially high-value
85
+ - PROCESS FEEDBACK: user feedback about the agent's approach/behavior, not just its output
86
+
87
+ Privacy: Never include personal data (names, health, finances, credentials) in lesson text. Abstract to the process level.
88
+
89
+ Skip: isolated trivial actions, already-documented rules. However, if multiple small successes form a consistent pattern of quality, extract that pattern as a reinforcement.
62
90
 
63
91
  Respond with a JSON array. Empty array [] is a valid response.`;
64
92
  }
package/dist/types.d.ts CHANGED
@@ -99,6 +99,7 @@ export interface LicenseInfo {
99
99
  vectorSearch: boolean;
100
100
  maxMemories: number;
101
101
  crossAgent: boolean;
102
+ remoteIngest?: boolean;
102
103
  };
103
104
  email?: string;
104
105
  expires_at?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.3.17",
3
+ "version": "0.4.0",
4
4
  "description": "Human-like memory for self-improving AI agents. Automatic capturing, nightly reflection, and cross-agent learning. Works with Claude Code and OpenClaw.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "node": ">=18"
36
36
  },
37
37
  "license": "UNLICENSED",
38
+ "homepage": "https://hicortex.gamaze.com",
38
39
  "repository": {
39
40
  "type": "git",
40
41
  "url": "https://github.com/mha33/hicortex.git",