@gamaze/hicortex 0.4.1 → 0.4.3
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 +2 -1
- package/dist/claude-md.d.ts +6 -5
- package/dist/claude-md.js +117 -30
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +23 -9
- package/dist/distiller.d.ts +11 -1
- package/dist/distiller.js +131 -3
- package/dist/init.js +2 -2
- package/dist/llm.d.ts +16 -0
- package/dist/llm.js +55 -0
- package/dist/mcp-server.js +44 -3
- package/dist/nightly-status.d.ts +11 -0
- package/dist/nightly-status.js +167 -0
- package/dist/nightly.js +119 -6
- package/dist/prompts.js +2 -0
- package/dist/status.js +36 -2
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ npx @gamaze/hicortex uninstall # Remove CC integration (keeps DB
|
|
|
71
71
|
## Architecture
|
|
72
72
|
|
|
73
73
|
```
|
|
74
|
-
Client
|
|
74
|
+
Client A Server Client B
|
|
75
75
|
┌──────────┐ ┌──────────────┐ ┌──────────┐
|
|
76
76
|
│CC sessions│ │ Shared DB │ │CC sessions│
|
|
77
77
|
│ ↓ │ POST │ │ POST │ ↓ │
|
|
@@ -99,6 +99,7 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
99
99
|
| `serverUrl` | Remote server URL (client mode) |
|
|
100
100
|
| `llmModel` | Model for importance scoring |
|
|
101
101
|
| `distillModel` | Model for session distillation (9b+ recommended) |
|
|
102
|
+
| `distillBaseUrl` | Separate Ollama instance for distillation |
|
|
102
103
|
| `reflectModel` | Model for nightly reflection (largest available) |
|
|
103
104
|
| `reflectBaseUrl` | Separate Ollama instance for reflection |
|
|
104
105
|
| `authToken` | Bearer token for endpoint auth |
|
package/dist/claude-md.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLAUDE.md lesson injection — manages the Hicortex Learnings block.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* Injects a dynamic, nightly-updated block into ~/.claude/CLAUDE.md:
|
|
5
|
+
* - Top lessons (from reflection, high-confidence)
|
|
6
|
+
* - Memory index (projects + counts, primes the agent to search)
|
|
7
|
+
* - Current project context (recent decisions for this project)
|
|
7
8
|
*
|
|
8
|
-
* Idempotent: calling twice with the same
|
|
9
|
+
* Idempotent: calling twice with the same data produces the same file.
|
|
9
10
|
*/
|
|
10
11
|
import type Database from "better-sqlite3";
|
|
11
12
|
/**
|
|
12
|
-
* Inject lessons
|
|
13
|
+
* Inject lessons, memory index, and project context into CLAUDE.md.
|
|
13
14
|
* Creates the file if it doesn't exist.
|
|
14
15
|
* Replaces existing block if present, appends if not.
|
|
15
16
|
*/
|
package/dist/claude-md.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CLAUDE.md lesson injection — manages the Hicortex Learnings block.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
5
|
+
* Injects a dynamic, nightly-updated block into ~/.claude/CLAUDE.md:
|
|
6
|
+
* - Top lessons (from reflection, high-confidence)
|
|
7
|
+
* - Memory index (projects + counts, primes the agent to search)
|
|
8
|
+
* - Current project context (recent decisions for this project)
|
|
8
9
|
*
|
|
9
|
-
* Idempotent: calling twice with the same
|
|
10
|
+
* Idempotent: calling twice with the same data produces the same file.
|
|
10
11
|
*/
|
|
11
12
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
13
|
if (k2 === undefined) k2 = k;
|
|
@@ -46,42 +47,67 @@ exports.injectLessons = injectLessons;
|
|
|
46
47
|
exports.removeLessonsBlock = removeLessonsBlock;
|
|
47
48
|
const node_fs_1 = require("node:fs");
|
|
48
49
|
const node_path_1 = require("node:path");
|
|
49
|
-
const node_path_2 = require("node:path");
|
|
50
50
|
const node_os_1 = require("node:os");
|
|
51
51
|
const storage = __importStar(require("./storage.js"));
|
|
52
52
|
const license_js_1 = require("./license.js");
|
|
53
53
|
const START_MARKER = "<!-- HICORTEX-LEARNINGS:START -->";
|
|
54
54
|
const END_MARKER = "<!-- HICORTEX-LEARNINGS:END -->";
|
|
55
|
-
const DEFAULT_CLAUDE_MD = (0,
|
|
56
|
-
const AGENT_GUIDANCE = `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.`;
|
|
55
|
+
const DEFAULT_CLAUDE_MD = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
|
|
57
56
|
/**
|
|
58
|
-
* Inject lessons
|
|
57
|
+
* Inject lessons, memory index, and project context into CLAUDE.md.
|
|
59
58
|
* Creates the file if it doesn't exist.
|
|
60
59
|
* Replaces existing block if present, appends if not.
|
|
61
60
|
*/
|
|
62
61
|
function injectLessons(db, options = {}) {
|
|
63
62
|
const claudeMdPath = options.claudeMdPath ?? DEFAULT_CLAUDE_MD;
|
|
64
|
-
const stateDir = options.stateDir ?? (0,
|
|
65
|
-
// Determine
|
|
63
|
+
const stateDir = options.stateDir ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
|
|
64
|
+
// Determine limits based on license
|
|
66
65
|
const features = (0, license_js_1.getFeatures)(stateDir);
|
|
67
|
-
const maxLessons = features.maxMemories === -1 ?
|
|
68
|
-
//
|
|
66
|
+
const maxLessons = features.maxMemories === -1 ? 10 : 5;
|
|
67
|
+
// --- Lessons ---
|
|
69
68
|
const lessons = storage.getLessons(db, 30, options.project);
|
|
70
69
|
const selected = lessons.slice(0, maxLessons);
|
|
71
|
-
// Format lesson lines
|
|
72
70
|
const lessonLines = selected.map((l) => {
|
|
73
|
-
const
|
|
74
|
-
|
|
71
|
+
const titleMatch = l.content.match(/## Lesson: (.+)/);
|
|
72
|
+
const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
|
|
73
|
+
const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
|
|
74
|
+
const title = titleMatch ? titleMatch[1] : l.content.slice(0, 150);
|
|
75
|
+
const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
|
|
76
|
+
return `- ${title}${meta ? ` (${meta})` : ""}`;
|
|
75
77
|
});
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
+
// --- Memory Index ---
|
|
79
|
+
const projectIndex = buildProjectIndex(db);
|
|
80
|
+
const totalCount = storage.countMemories(db);
|
|
81
|
+
const lessonCount = lessons.length;
|
|
82
|
+
const sourceCount = countSources(db);
|
|
83
|
+
// --- Current Project Context ---
|
|
84
|
+
const currentProject = detectCurrentProject(claudeMdPath);
|
|
85
|
+
const projectContext = currentProject
|
|
86
|
+
? buildProjectContext(db, currentProject)
|
|
87
|
+
: [];
|
|
88
|
+
// --- Build Block ---
|
|
89
|
+
const blockParts = [START_MARKER, "## Hicortex Memory"];
|
|
90
|
+
// Mandatory instruction
|
|
91
|
+
blockParts.push("", "You have access to shared long-term memory across all agents and sessions.", "BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.", "Use `hicortex_context` at session start for recent project state.");
|
|
92
|
+
// Lessons
|
|
78
93
|
if (lessonLines.length > 0) {
|
|
79
|
-
blockParts.push("", "###
|
|
94
|
+
blockParts.push("", "### Lessons (updated nightly)");
|
|
80
95
|
blockParts.push(...lessonLines);
|
|
81
96
|
}
|
|
97
|
+
// Project context
|
|
98
|
+
if (projectContext.length > 0) {
|
|
99
|
+
blockParts.push("", `### This Project (${currentProject})`);
|
|
100
|
+
blockParts.push(...projectContext);
|
|
101
|
+
}
|
|
102
|
+
// Memory index
|
|
103
|
+
if (projectIndex.length > 0) {
|
|
104
|
+
blockParts.push("", "### Memory Index");
|
|
105
|
+
blockParts.push(projectIndex.join(" | "));
|
|
106
|
+
blockParts.push(`${totalCount} memories, ${lessonCount} lessons, ${sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
107
|
+
}
|
|
82
108
|
blockParts.push(END_MARKER);
|
|
83
109
|
const block = blockParts.join("\n");
|
|
84
|
-
//
|
|
110
|
+
// --- Write ---
|
|
85
111
|
let content = "";
|
|
86
112
|
try {
|
|
87
113
|
content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
|
|
@@ -89,31 +115,94 @@ function injectLessons(db, options = {}) {
|
|
|
89
115
|
catch {
|
|
90
116
|
// File doesn't exist — will create it
|
|
91
117
|
}
|
|
92
|
-
// Replace or append
|
|
93
118
|
const startIdx = content.indexOf(START_MARKER);
|
|
94
119
|
const endIdx = content.indexOf(END_MARKER);
|
|
95
120
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
96
|
-
// Replace existing block
|
|
97
121
|
content =
|
|
98
122
|
content.slice(0, startIdx) +
|
|
99
123
|
block +
|
|
100
124
|
content.slice(endIdx + END_MARKER.length);
|
|
101
125
|
}
|
|
102
126
|
else {
|
|
103
|
-
|
|
104
|
-
if (content.length > 0 && !content.endsWith("\n")) {
|
|
127
|
+
if (content.length > 0 && !content.endsWith("\n"))
|
|
105
128
|
content += "\n";
|
|
106
|
-
|
|
107
|
-
if (content.length > 0) {
|
|
129
|
+
if (content.length > 0)
|
|
108
130
|
content += "\n";
|
|
109
|
-
}
|
|
110
131
|
content += block + "\n";
|
|
111
132
|
}
|
|
112
|
-
// Write
|
|
113
133
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
|
|
114
134
|
(0, node_fs_1.writeFileSync)(claudeMdPath, content);
|
|
115
135
|
return { lessonsCount: selected.length, path: claudeMdPath };
|
|
116
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Build compact project index: "hicortex: 18 | boat: 24 | health: 45"
|
|
139
|
+
*/
|
|
140
|
+
function buildProjectIndex(db) {
|
|
141
|
+
try {
|
|
142
|
+
const rows = db
|
|
143
|
+
.prepare(`SELECT project, COUNT(*) as cnt FROM memories
|
|
144
|
+
WHERE project IS NOT NULL
|
|
145
|
+
GROUP BY project ORDER BY cnt DESC LIMIT 10`)
|
|
146
|
+
.all();
|
|
147
|
+
return rows.map((r) => `${r.project}: ${r.cnt}`);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Count distinct source agents.
|
|
155
|
+
*/
|
|
156
|
+
function countSources(db) {
|
|
157
|
+
try {
|
|
158
|
+
return db
|
|
159
|
+
.prepare("SELECT COUNT(DISTINCT source_agent) as cnt FROM memories")
|
|
160
|
+
.get().cnt;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Detect current project from the CLAUDE.md path.
|
|
168
|
+
* CC puts project-specific CLAUDE.md files in ~/.claude/projects/<encoded-path>/
|
|
169
|
+
* The global ~/.claude/CLAUDE.md has no project context.
|
|
170
|
+
*/
|
|
171
|
+
function detectCurrentProject(claudeMdPath) {
|
|
172
|
+
// Global CLAUDE.md — no project
|
|
173
|
+
if (claudeMdPath === DEFAULT_CLAUDE_MD)
|
|
174
|
+
return null;
|
|
175
|
+
// Project CLAUDE.md: ~/.claude/projects/-Users-foo-myproject/CLAUDE.md
|
|
176
|
+
// Extract the last segment of the encoded path
|
|
177
|
+
const match = claudeMdPath.match(/projects\/[^/]*-([^/]+)\//);
|
|
178
|
+
if (match)
|
|
179
|
+
return match[1];
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Build recent decisions/facts for a specific project.
|
|
184
|
+
* Returns formatted lines like: "- Shipped v0.4.1 with multi-client (2026-03-28)"
|
|
185
|
+
*/
|
|
186
|
+
function buildProjectContext(db, project) {
|
|
187
|
+
try {
|
|
188
|
+
const rows = db
|
|
189
|
+
.prepare(`SELECT content, created_at FROM memories
|
|
190
|
+
WHERE project = ? AND memory_type IN ('decision', 'episode')
|
|
191
|
+
ORDER BY created_at DESC LIMIT 5`)
|
|
192
|
+
.all(project);
|
|
193
|
+
return rows.map((r) => {
|
|
194
|
+
const date = r.created_at?.slice(0, 10) ?? "";
|
|
195
|
+
// Extract first meaningful line from content
|
|
196
|
+
const lines = r.content.split("\n").filter((l) => l.trim().length > 10);
|
|
197
|
+
const summary = lines.find((l) => l.startsWith("- ") || l.startsWith("### "))?.replace(/^[-#\s]+/, "").slice(0, 120) ??
|
|
198
|
+
r.content.slice(0, 120);
|
|
199
|
+
return `- ${summary} (${date})`;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
117
206
|
/**
|
|
118
207
|
* Remove the Hicortex Learnings block from CLAUDE.md.
|
|
119
208
|
* Used by the uninstall command.
|
|
@@ -124,16 +213,14 @@ function removeLessonsBlock(claudeMdPath = DEFAULT_CLAUDE_MD) {
|
|
|
124
213
|
content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
|
|
125
214
|
}
|
|
126
215
|
catch {
|
|
127
|
-
return false;
|
|
216
|
+
return false;
|
|
128
217
|
}
|
|
129
218
|
const startIdx = content.indexOf(START_MARKER);
|
|
130
219
|
const endIdx = content.indexOf(END_MARKER);
|
|
131
220
|
if (startIdx === -1 || endIdx === -1)
|
|
132
221
|
return false;
|
|
133
|
-
// Remove block and any trailing blank line
|
|
134
222
|
let newContent = content.slice(0, startIdx) +
|
|
135
223
|
content.slice(endIdx + END_MARKER.length);
|
|
136
|
-
// Clean up double blank lines left by removal
|
|
137
224
|
newContent = newContent.replace(/\n{3,}/g, "\n\n").trim();
|
|
138
225
|
if (newContent.length > 0)
|
|
139
226
|
newContent += "\n";
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* server Start the MCP HTTP/SSE server (persistent daemon)
|
|
7
7
|
* init Detect existing setup and configure for CC/OC
|
|
8
8
|
* nightly Run distill + consolidate + inject lessons (manual trigger)
|
|
9
|
+
* nightly --status Show nightly pipeline health check
|
|
9
10
|
* status Show config, DB stats, adapter status
|
|
10
11
|
* uninstall Clean removal of CC integration
|
|
11
12
|
*/
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* server Start the MCP HTTP/SSE server (persistent daemon)
|
|
8
8
|
* init Detect existing setup and configure for CC/OC
|
|
9
9
|
* nightly Run distill + consolidate + inject lessons (manual trigger)
|
|
10
|
+
* nightly --status Show nightly pipeline health check
|
|
10
11
|
* status Show config, DB stats, adapter status
|
|
11
12
|
* uninstall Clean removal of CC integration
|
|
12
13
|
*/
|
|
@@ -38,13 +39,24 @@ switch (command) {
|
|
|
38
39
|
break;
|
|
39
40
|
}
|
|
40
41
|
case "nightly": {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const args = process.argv.slice(3);
|
|
43
|
+
if (args.includes("--status")) {
|
|
44
|
+
import("./nightly-status.js").then(({ showNightlyStatus }) => {
|
|
45
|
+
showNightlyStatus().catch((err) => {
|
|
46
|
+
console.error("[hicortex] Status check failed:", err);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
46
49
|
});
|
|
47
|
-
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const dryRun = args.includes("--dry-run");
|
|
53
|
+
import("./nightly.js").then(({ runNightly }) => {
|
|
54
|
+
runNightly({ dryRun }).catch((err) => {
|
|
55
|
+
console.error("[hicortex] Nightly pipeline failed:", err);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
48
60
|
break;
|
|
49
61
|
}
|
|
50
62
|
case "status":
|
|
@@ -77,13 +89,15 @@ Commands:
|
|
|
77
89
|
uninstall Remove CC integration (preserves DB)
|
|
78
90
|
|
|
79
91
|
Options:
|
|
80
|
-
server --port <n>
|
|
81
|
-
server --host <h>
|
|
82
|
-
nightly --dry-run
|
|
92
|
+
server --port <n> Port (default: 8787)
|
|
93
|
+
server --host <h> Host (default: 127.0.0.1)
|
|
94
|
+
nightly --dry-run Preview without changes
|
|
95
|
+
nightly --status Show nightly pipeline health
|
|
83
96
|
|
|
84
97
|
Examples:
|
|
85
98
|
npx @gamaze/hicortex server
|
|
86
99
|
npx @gamaze/hicortex init
|
|
100
|
+
npx @gamaze/hicortex nightly --status
|
|
87
101
|
npx @gamaze/hicortex init --server https://myserver.example.com
|
|
88
102
|
npx @gamaze/hicortex status`);
|
|
89
103
|
process.exit(command ? 1 : 0);
|
package/dist/distiller.d.ts
CHANGED
|
@@ -4,12 +4,22 @@
|
|
|
4
4
|
* not from filesystem scanning.
|
|
5
5
|
*/
|
|
6
6
|
import type { LlmClient } from "./llm.js";
|
|
7
|
+
/**
|
|
8
|
+
* Estimate a safe chunk size in chars based on the LLM provider and model.
|
|
9
|
+
* - API providers (Anthropic, OpenAI, claude-cli): no chunking needed (large context windows)
|
|
10
|
+
* - Ollama: query /api/show for context_length AND parameter_count, cap based on both
|
|
11
|
+
* - Small models (<8B params): max 20K chars (~5K tokens) — keeps CPU inference under ~60s
|
|
12
|
+
* - Larger models: up to 60K chars (~15K tokens)
|
|
13
|
+
* - Fallback: 20K chars
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectChunkSize(provider: string, model: string, baseUrl?: string): Promise<number>;
|
|
7
16
|
/**
|
|
8
17
|
* Convert OpenClaw hook messages to a filtered transcript string.
|
|
9
18
|
*/
|
|
10
19
|
export declare function extractConversationText(messages: unknown[]): string;
|
|
11
20
|
/**
|
|
12
21
|
* Send filtered conversation to LLM for knowledge extraction.
|
|
22
|
+
* For large transcripts, chunks into segments to avoid overwhelming small models.
|
|
13
23
|
* Returns an array of memory entries to ingest, or empty array if nothing worth extracting.
|
|
14
24
|
*/
|
|
15
|
-
export declare function distillSession(llm: LlmClient, conversation: string, projectName: string, date: string): Promise<string[]>;
|
|
25
|
+
export declare function distillSession(llm: LlmClient, conversation: string, projectName: string, date: string, chunkSizeChars?: number): Promise<string[]>;
|
package/dist/distiller.js
CHANGED
|
@@ -5,11 +5,74 @@
|
|
|
5
5
|
* not from filesystem scanning.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.detectChunkSize = detectChunkSize;
|
|
8
9
|
exports.extractConversationText = extractConversationText;
|
|
9
10
|
exports.distillSession = distillSession;
|
|
10
11
|
const prompts_js_1 = require("./prompts.js");
|
|
11
12
|
const MAX_TRANSCRIPT_CHARS = 80_000;
|
|
12
13
|
const MIN_CONVERSATION_CHARS = 200;
|
|
14
|
+
// Chunk size limits by model parameter count (for local/CPU inference)
|
|
15
|
+
// Small models are slow on CPU — cap input size to keep inference under ~60s
|
|
16
|
+
const SMALL_MODEL_PARAMS = 8_000_000_000; // 8B — threshold for "small"
|
|
17
|
+
const SMALL_MODEL_MAX_CHUNK_CHARS = 20_000; // ~5K tokens — safe for 4-8B on CPU
|
|
18
|
+
const LARGE_MODEL_MAX_CHUNK_CHARS = 60_000; // ~15K tokens — ok for 8B+ on GPU or API
|
|
19
|
+
/**
|
|
20
|
+
* Estimate a safe chunk size in chars based on the LLM provider and model.
|
|
21
|
+
* - API providers (Anthropic, OpenAI, claude-cli): no chunking needed (large context windows)
|
|
22
|
+
* - Ollama: query /api/show for context_length AND parameter_count, cap based on both
|
|
23
|
+
* - Small models (<8B params): max 20K chars (~5K tokens) — keeps CPU inference under ~60s
|
|
24
|
+
* - Larger models: up to 60K chars (~15K tokens)
|
|
25
|
+
* - Fallback: 20K chars
|
|
26
|
+
*/
|
|
27
|
+
async function detectChunkSize(provider, model, baseUrl) {
|
|
28
|
+
// API-based providers handle large contexts natively — no chunking needed
|
|
29
|
+
if (provider !== "ollama") {
|
|
30
|
+
return MAX_TRANSCRIPT_CHARS;
|
|
31
|
+
}
|
|
32
|
+
// Query Ollama for model metadata
|
|
33
|
+
if (baseUrl) {
|
|
34
|
+
try {
|
|
35
|
+
const resp = await fetch(`${baseUrl}/api/show`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({ name: model }),
|
|
39
|
+
signal: AbortSignal.timeout(5000),
|
|
40
|
+
});
|
|
41
|
+
if (resp.ok) {
|
|
42
|
+
const data = await resp.json();
|
|
43
|
+
const info = data.model_info ?? {};
|
|
44
|
+
// Extract parameter count for speed-aware capping
|
|
45
|
+
const paramKey = Object.keys(info).find((k) => k.endsWith("parameter_count"));
|
|
46
|
+
const paramCount = paramKey && typeof info[paramKey] === "number"
|
|
47
|
+
? info[paramKey]
|
|
48
|
+
: 0;
|
|
49
|
+
const isSmallModel = paramCount > 0 && paramCount < SMALL_MODEL_PARAMS;
|
|
50
|
+
// Extract context length for context-aware capping
|
|
51
|
+
const ctxKey = Object.keys(info).find((k) => k.endsWith("context_length") || k.endsWith("context_window"));
|
|
52
|
+
const contextTokens = ctxKey && typeof info[ctxKey] === "number"
|
|
53
|
+
? info[ctxKey]
|
|
54
|
+
: 0;
|
|
55
|
+
// Determine max chunk size based on model size (speed constraint)
|
|
56
|
+
// Unknown param count defaults to conservative (small model) — safe for any hardware
|
|
57
|
+
const maxBySpeed = !isSmallModel && paramCount > 0 ? LARGE_MODEL_MAX_CHUNK_CHARS : SMALL_MODEL_MAX_CHUNK_CHARS;
|
|
58
|
+
// Determine max chunk size based on context window (fits-in-context constraint)
|
|
59
|
+
const maxByContext = contextTokens > 0
|
|
60
|
+
? Math.floor(contextTokens * 0.6 * 4) // 60% of context, ~4 chars/token
|
|
61
|
+
: MAX_TRANSCRIPT_CHARS;
|
|
62
|
+
const chunkChars = Math.min(maxBySpeed, maxByContext);
|
|
63
|
+
console.log(`[hicortex] Model: ${paramCount > 0 ? `${(paramCount / 1e9).toFixed(1)}B params` : "unknown size"}, ` +
|
|
64
|
+
`context: ${contextTokens > 0 ? `${contextTokens} tokens` : "unknown"}, ` +
|
|
65
|
+
`chunk size: ${chunkChars} chars${isSmallModel ? " (small model cap)" : ""}`);
|
|
66
|
+
return chunkChars;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Failed to query — use fallback
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Fallback: 20K chars (~5K tokens) — safe for 4B models with 32K context
|
|
74
|
+
return 20_000;
|
|
75
|
+
}
|
|
13
76
|
// Entry types to skip entirely (from the Python distiller)
|
|
14
77
|
const SKIP_ENTRY_TYPES = new Set([
|
|
15
78
|
"progress",
|
|
@@ -128,17 +191,54 @@ function extractConversationText(messages) {
|
|
|
128
191
|
}
|
|
129
192
|
/**
|
|
130
193
|
* Send filtered conversation to LLM for knowledge extraction.
|
|
194
|
+
* For large transcripts, chunks into segments to avoid overwhelming small models.
|
|
131
195
|
* Returns an array of memory entries to ingest, or empty array if nothing worth extracting.
|
|
132
196
|
*/
|
|
133
|
-
async function distillSession(llm, conversation, projectName, date) {
|
|
197
|
+
async function distillSession(llm, conversation, projectName, date, chunkSizeChars) {
|
|
134
198
|
if (conversation.length < MIN_CONVERSATION_CHARS) {
|
|
135
199
|
return [];
|
|
136
200
|
}
|
|
137
|
-
//
|
|
201
|
+
// Cap total input at MAX_TRANSCRIPT_CHARS
|
|
138
202
|
let transcript = conversation;
|
|
139
203
|
if (transcript.length > MAX_TRANSCRIPT_CHARS) {
|
|
140
204
|
transcript = transcript.slice(0, MAX_TRANSCRIPT_CHARS) + "\n\n[...truncated...]";
|
|
141
205
|
}
|
|
206
|
+
// Use provided chunk size or default to no chunking
|
|
207
|
+
const chunkSize = chunkSizeChars ?? MAX_TRANSCRIPT_CHARS;
|
|
208
|
+
// If transcript fits in one chunk, distill directly
|
|
209
|
+
if (transcript.length <= chunkSize) {
|
|
210
|
+
return distillChunk(llm, transcript, projectName, date);
|
|
211
|
+
}
|
|
212
|
+
// Chunk large transcripts and distill each segment
|
|
213
|
+
const chunks = splitIntoChunks(transcript, chunkSize);
|
|
214
|
+
console.log(`[hicortex] Chunking ${transcript.length} chars into ${chunks.length} segments`);
|
|
215
|
+
const allEntries = [];
|
|
216
|
+
const seen = new Set();
|
|
217
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
218
|
+
console.log(`[hicortex] Chunk ${i + 1}/${chunks.length} (${chunks[i].length} chars)`);
|
|
219
|
+
try {
|
|
220
|
+
const entries = await distillChunk(llm, chunks[i], projectName, date);
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
// Deduplicate by normalized content
|
|
223
|
+
const key = entry.toLowerCase().replace(/\s+/g, " ").slice(0, 100);
|
|
224
|
+
if (!seen.has(key)) {
|
|
225
|
+
seen.add(key);
|
|
226
|
+
allEntries.push(entry);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
232
|
+
console.error(`[hicortex] Chunk ${i + 1} failed: ${msg}`);
|
|
233
|
+
// Continue with remaining chunks — partial extraction is better than none
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return allEntries;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Distill a single chunk of conversation text.
|
|
240
|
+
*/
|
|
241
|
+
async function distillChunk(llm, transcript, projectName, date) {
|
|
142
242
|
const prompt = (0, prompts_js_1.distillation)(projectName, date, transcript);
|
|
143
243
|
try {
|
|
144
244
|
const result = await llm.completeDistill(prompt);
|
|
@@ -147,7 +247,6 @@ async function distillSession(llm, conversation, projectName, date) {
|
|
|
147
247
|
if (result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT")) {
|
|
148
248
|
return [];
|
|
149
249
|
}
|
|
150
|
-
// Split distilled markdown into individual memory entries
|
|
151
250
|
return parseDistilledEntries(result);
|
|
152
251
|
}
|
|
153
252
|
catch (err) {
|
|
@@ -156,6 +255,35 @@ async function distillSession(llm, conversation, projectName, date) {
|
|
|
156
255
|
return [];
|
|
157
256
|
}
|
|
158
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Split transcript text into chunks at natural boundaries (double newlines).
|
|
260
|
+
* Each chunk is at most maxChars, split at the last paragraph boundary.
|
|
261
|
+
*/
|
|
262
|
+
function splitIntoChunks(text, maxChars) {
|
|
263
|
+
if (text.length <= maxChars)
|
|
264
|
+
return [text];
|
|
265
|
+
const chunks = [];
|
|
266
|
+
let remaining = text;
|
|
267
|
+
while (remaining.length > 0) {
|
|
268
|
+
if (remaining.length <= maxChars) {
|
|
269
|
+
chunks.push(remaining);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
// Find the last paragraph break within maxChars
|
|
273
|
+
let splitAt = remaining.lastIndexOf("\n\n", maxChars);
|
|
274
|
+
if (splitAt < maxChars * 0.5) {
|
|
275
|
+
// No good paragraph break — fall back to last newline
|
|
276
|
+
splitAt = remaining.lastIndexOf("\n", maxChars);
|
|
277
|
+
}
|
|
278
|
+
if (splitAt < maxChars * 0.3) {
|
|
279
|
+
// No good break at all — hard split
|
|
280
|
+
splitAt = maxChars;
|
|
281
|
+
}
|
|
282
|
+
chunks.push(remaining.slice(0, splitAt).trim());
|
|
283
|
+
remaining = remaining.slice(splitAt).trim();
|
|
284
|
+
}
|
|
285
|
+
return chunks.filter((c) => c.length >= MIN_CONVERSATION_CHARS);
|
|
286
|
+
}
|
|
159
287
|
/**
|
|
160
288
|
* Parse distilled markdown into individual memory entry strings.
|
|
161
289
|
* Each section item becomes a separate memory.
|
package/dist/init.js
CHANGED
|
@@ -717,7 +717,7 @@ async function runInit(options = {}) {
|
|
|
717
717
|
catch { /* new file */ }
|
|
718
718
|
const block = [
|
|
719
719
|
"<!-- HICORTEX-LEARNINGS:START -->",
|
|
720
|
-
"## Hicortex
|
|
720
|
+
"## Hicortex Memory",
|
|
721
721
|
"",
|
|
722
722
|
"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.",
|
|
723
723
|
"<!-- HICORTEX-LEARNINGS:END -->",
|
|
@@ -858,7 +858,7 @@ async function runClientInit(serverUrl) {
|
|
|
858
858
|
catch { }
|
|
859
859
|
const block = [
|
|
860
860
|
"<!-- HICORTEX-LEARNINGS:START -->",
|
|
861
|
-
"## Hicortex
|
|
861
|
+
"## Hicortex Memory",
|
|
862
862
|
"",
|
|
863
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
864
|
"<!-- HICORTEX-LEARNINGS:END -->",
|
package/dist/llm.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface LlmConfig {
|
|
|
25
25
|
provider: string;
|
|
26
26
|
/** Optional separate model for distillation (defaults to model if unset). */
|
|
27
27
|
distillModel?: string;
|
|
28
|
+
/** Optional separate endpoint for distillation (e.g. remote Ollama with larger/faster model). */
|
|
29
|
+
distillBaseUrl?: string;
|
|
30
|
+
distillApiKey?: string;
|
|
31
|
+
distillProvider?: string;
|
|
28
32
|
/** Optional separate endpoint for reflect-tier LLM (e.g. remote Ollama with larger model). */
|
|
29
33
|
reflectBaseUrl?: string;
|
|
30
34
|
reflectApiKey?: string;
|
|
@@ -59,6 +63,17 @@ export declare function findClaudeBinary(): string | null;
|
|
|
59
63
|
* baseUrl field stores the path to the claude binary.
|
|
60
64
|
*/
|
|
61
65
|
export declare function claudeCliConfig(claudePath: string): LlmConfig;
|
|
66
|
+
/**
|
|
67
|
+
* Check if a local Ollama instance is reachable and has models loaded.
|
|
68
|
+
* Returns the model name if available, null otherwise.
|
|
69
|
+
*/
|
|
70
|
+
export declare function probeOllama(baseUrl?: string): Promise<string | null>;
|
|
71
|
+
/**
|
|
72
|
+
* For batch operations (nightly pipeline), prefer Ollama when available.
|
|
73
|
+
* Claude CLI has strict rate limits that kill batch distillation.
|
|
74
|
+
* Falls back to the provided config if Ollama is unreachable.
|
|
75
|
+
*/
|
|
76
|
+
export declare function preferOllamaForBatch(resolved: LlmConfig, ollamaBaseUrl?: string): Promise<LlmConfig>;
|
|
62
77
|
export declare class RateLimitError extends Error {
|
|
63
78
|
retryAfterMs: number;
|
|
64
79
|
constructor(retryAfterMs: number);
|
|
@@ -81,6 +96,7 @@ export declare class LlmClient {
|
|
|
81
96
|
completeReflect(prompt: string, maxTokens?: number): Promise<string>;
|
|
82
97
|
/**
|
|
83
98
|
* Distillation-tier completion (session knowledge extraction).
|
|
99
|
+
* Routes to distillBaseUrl/distillProvider if configured (e.g. remote Ollama with faster model).
|
|
84
100
|
*/
|
|
85
101
|
completeDistill(prompt: string, maxTokens?: number): Promise<string>;
|
|
86
102
|
/**
|
package/dist/llm.js
CHANGED
|
@@ -24,6 +24,8 @@ exports.resolveLlmConfig = resolveLlmConfig;
|
|
|
24
24
|
exports.resolveLlmConfigForCC = resolveLlmConfigForCC;
|
|
25
25
|
exports.findClaudeBinary = findClaudeBinary;
|
|
26
26
|
exports.claudeCliConfig = claudeCliConfig;
|
|
27
|
+
exports.probeOllama = probeOllama;
|
|
28
|
+
exports.preferOllamaForBatch = preferOllamaForBatch;
|
|
27
29
|
const node_fs_1 = require("node:fs");
|
|
28
30
|
const node_path_1 = require("node:path");
|
|
29
31
|
const node_os_1 = require("node:os");
|
|
@@ -348,6 +350,55 @@ function claudeCliConfig(claudePath) {
|
|
|
348
350
|
provider: "claude-cli",
|
|
349
351
|
};
|
|
350
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Check if a local Ollama instance is reachable and has models loaded.
|
|
355
|
+
* Returns the model name if available, null otherwise.
|
|
356
|
+
*/
|
|
357
|
+
async function probeOllama(baseUrl = "http://localhost:11434") {
|
|
358
|
+
try {
|
|
359
|
+
const resp = await fetch(`${baseUrl}/api/ps`, {
|
|
360
|
+
signal: AbortSignal.timeout(3000),
|
|
361
|
+
});
|
|
362
|
+
if (!resp.ok)
|
|
363
|
+
return null;
|
|
364
|
+
const data = (await resp.json());
|
|
365
|
+
if (data.models && data.models.length > 0) {
|
|
366
|
+
return data.models[0].name;
|
|
367
|
+
}
|
|
368
|
+
// No model loaded — check if any are available
|
|
369
|
+
const tagsResp = await fetch(`${baseUrl}/api/tags`, {
|
|
370
|
+
signal: AbortSignal.timeout(3000),
|
|
371
|
+
});
|
|
372
|
+
if (!tagsResp.ok)
|
|
373
|
+
return null;
|
|
374
|
+
const tags = (await tagsResp.json());
|
|
375
|
+
return tags.models?.[0]?.name ?? null;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* For batch operations (nightly pipeline), prefer Ollama when available.
|
|
383
|
+
* Claude CLI has strict rate limits that kill batch distillation.
|
|
384
|
+
* Falls back to the provided config if Ollama is unreachable.
|
|
385
|
+
*/
|
|
386
|
+
async function preferOllamaForBatch(resolved, ollamaBaseUrl = "http://localhost:11434") {
|
|
387
|
+
// Only override claude-cli (which has rate limits)
|
|
388
|
+
if (resolved.provider !== "claude-cli")
|
|
389
|
+
return resolved;
|
|
390
|
+
const model = await probeOllama(ollamaBaseUrl);
|
|
391
|
+
if (!model)
|
|
392
|
+
return resolved;
|
|
393
|
+
return {
|
|
394
|
+
...resolved,
|
|
395
|
+
baseUrl: ollamaBaseUrl,
|
|
396
|
+
apiKey: "",
|
|
397
|
+
model,
|
|
398
|
+
reflectModel: resolved.reflectModel ?? model,
|
|
399
|
+
provider: "ollama",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
351
402
|
// ---------------------------------------------------------------------------
|
|
352
403
|
// LLM Client class
|
|
353
404
|
// ---------------------------------------------------------------------------
|
|
@@ -401,8 +452,12 @@ class LlmClient {
|
|
|
401
452
|
}
|
|
402
453
|
/**
|
|
403
454
|
* Distillation-tier completion (session knowledge extraction).
|
|
455
|
+
* Routes to distillBaseUrl/distillProvider if configured (e.g. remote Ollama with faster model).
|
|
404
456
|
*/
|
|
405
457
|
async completeDistill(prompt, maxTokens = 2048) {
|
|
458
|
+
if (this.config.distillBaseUrl) {
|
|
459
|
+
return this.completeWithOverride(this.config.distillBaseUrl, this.config.distillApiKey ?? this.config.apiKey, this.config.distillProvider ?? this.config.provider, this.config.distillModel ?? this.config.model, prompt, maxTokens, 900_000);
|
|
460
|
+
}
|
|
406
461
|
return this.complete(this.config.distillModel ?? this.config.model, prompt, maxTokens, 900_000);
|
|
407
462
|
}
|
|
408
463
|
/**
|
package/dist/mcp-server.js
CHANGED
|
@@ -273,10 +273,15 @@ async function startServer(options = {}) {
|
|
|
273
273
|
reflectModel: savedConfig?.reflectModel,
|
|
274
274
|
});
|
|
275
275
|
}
|
|
276
|
-
// Apply optional
|
|
276
|
+
// Apply optional distill endpoint (e.g. remote Ollama with faster model)
|
|
277
277
|
if (savedConfig?.distillModel) {
|
|
278
278
|
llmConfig.distillModel = savedConfig.distillModel;
|
|
279
279
|
}
|
|
280
|
+
if (savedConfig?.distillBaseUrl) {
|
|
281
|
+
llmConfig.distillBaseUrl = savedConfig.distillBaseUrl;
|
|
282
|
+
llmConfig.distillApiKey = savedConfig.distillApiKey ?? llmConfig.apiKey;
|
|
283
|
+
llmConfig.distillProvider = savedConfig.distillProvider ?? llmConfig.provider;
|
|
284
|
+
}
|
|
280
285
|
// Apply separate reflect endpoint if configured (e.g. remote Ollama with larger model)
|
|
281
286
|
if (savedConfig?.reflectBaseUrl) {
|
|
282
287
|
llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
|
|
@@ -284,11 +289,13 @@ async function startServer(options = {}) {
|
|
|
284
289
|
llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
|
|
285
290
|
}
|
|
286
291
|
llm = new llm_js_1.LlmClient(llmConfig);
|
|
287
|
-
const distillInfo = llmConfig.
|
|
292
|
+
const distillInfo = llmConfig.distillBaseUrl
|
|
293
|
+
? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
|
|
294
|
+
: llmConfig.distillModel ? llmConfig.distillModel : "";
|
|
288
295
|
const reflectInfo = llmConfig.reflectBaseUrl
|
|
289
296
|
? `${llmConfig.reflectProvider}/${llmConfig.reflectModel}@${llmConfig.reflectBaseUrl}`
|
|
290
297
|
: llmConfig.reflectModel;
|
|
291
|
-
console.log(`[hicortex] LLM fast: ${llmConfig.provider}/${llmConfig.model}${distillInfo}, reflect: ${reflectInfo}`);
|
|
298
|
+
console.log(`[hicortex] LLM fast: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}, reflect: ${reflectInfo}`);
|
|
292
299
|
// License: read from options, config file, or env var
|
|
293
300
|
const licenseKey = options.licenseKey
|
|
294
301
|
?? savedConfig?.licenseKey
|
|
@@ -361,6 +368,40 @@ async function startServer(options = {}) {
|
|
|
361
368
|
llm: `${llmConfig.provider}/${llmConfig.model}`,
|
|
362
369
|
});
|
|
363
370
|
});
|
|
371
|
+
// REST /lessons — return lessons + memory index for client CLAUDE.md injection
|
|
372
|
+
app.get("/lessons", (_req, res) => {
|
|
373
|
+
if (!db) {
|
|
374
|
+
res.status(503).json({ error: "Server not initialized" });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const lessons = storage.getLessons(db, 30);
|
|
379
|
+
const totalCount = storage.countMemories(db);
|
|
380
|
+
// Project index
|
|
381
|
+
const projects = db
|
|
382
|
+
.prepare("SELECT project, COUNT(*) as cnt FROM memories WHERE project IS NOT NULL GROUP BY project ORDER BY cnt DESC LIMIT 10")
|
|
383
|
+
.all();
|
|
384
|
+
const sourceCount = db.prepare("SELECT COUNT(DISTINCT source_agent) as cnt FROM memories").get().cnt;
|
|
385
|
+
const lessonCount = lessons.length;
|
|
386
|
+
res.json({
|
|
387
|
+
lessons: lessons.map(l => ({
|
|
388
|
+
content: l.content,
|
|
389
|
+
created_at: l.created_at,
|
|
390
|
+
base_strength: l.base_strength,
|
|
391
|
+
access_count: l.access_count,
|
|
392
|
+
})),
|
|
393
|
+
index: {
|
|
394
|
+
total: totalCount,
|
|
395
|
+
lessonCount,
|
|
396
|
+
sourceCount,
|
|
397
|
+
projects: projects.map(p => ({ name: p.project, count: p.cnt })),
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
364
405
|
// REST /ingest — accept pre-distilled memories from remote clients
|
|
365
406
|
app.post("/ingest", async (req, res) => {
|
|
366
407
|
if (!db) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nightly pipeline status — lightweight check without running the pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Shows:
|
|
5
|
+
* - Last run timestamp + age
|
|
6
|
+
* - Timer/schedule status (systemd/launchd)
|
|
7
|
+
* - DB memory count
|
|
8
|
+
* - Distillation source breakdown
|
|
9
|
+
* - Staleness warnings
|
|
10
|
+
*/
|
|
11
|
+
export declare function showNightlyStatus(): Promise<void>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Nightly pipeline status — lightweight check without running the pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Shows:
|
|
6
|
+
* - Last run timestamp + age
|
|
7
|
+
* - Timer/schedule status (systemd/launchd)
|
|
8
|
+
* - DB memory count
|
|
9
|
+
* - Distillation source breakdown
|
|
10
|
+
* - Staleness warnings
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.showNightlyStatus = showNightlyStatus;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const node_os_1 = require("node:os");
|
|
17
|
+
const node_child_process_1 = require("node:child_process");
|
|
18
|
+
const db_js_1 = require("./db.js");
|
|
19
|
+
const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
|
|
20
|
+
const LAST_RUN_PATH = (0, node_path_1.join)(HICORTEX_HOME, "nightly-last-run.txt");
|
|
21
|
+
const CONFIG_PATH = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
|
|
22
|
+
const STALE_THRESHOLD_HOURS = 30;
|
|
23
|
+
async function showNightlyStatus() {
|
|
24
|
+
console.log("Hicortex Nightly Pipeline Status");
|
|
25
|
+
console.log("─".repeat(40));
|
|
26
|
+
// Last run
|
|
27
|
+
let lastRun = null;
|
|
28
|
+
let lastRunStr = "never";
|
|
29
|
+
try {
|
|
30
|
+
const ts = (0, node_fs_1.readFileSync)(LAST_RUN_PATH, "utf-8").trim();
|
|
31
|
+
const d = new Date(ts);
|
|
32
|
+
if (!isNaN(d.getTime())) {
|
|
33
|
+
lastRun = d;
|
|
34
|
+
lastRunStr = ts;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lastRunStr = `${ts} (invalid)`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// No file
|
|
42
|
+
}
|
|
43
|
+
if (lastRun) {
|
|
44
|
+
const ageMs = Date.now() - lastRun.getTime();
|
|
45
|
+
const ageHours = Math.round(ageMs / (60 * 60 * 1000));
|
|
46
|
+
const ageStr = ageHours < 1 ? "just now" :
|
|
47
|
+
ageHours < 24 ? `${ageHours}h ago` :
|
|
48
|
+
`${Math.round(ageHours / 24)}d ago`;
|
|
49
|
+
const isStale = ageHours > STALE_THRESHOLD_HOURS;
|
|
50
|
+
console.log(`Last run: ${lastRunStr} (${ageStr})${isStale ? " ⚠ STALE" : ""}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(`Last run: ${lastRunStr}`);
|
|
54
|
+
}
|
|
55
|
+
// LLM config
|
|
56
|
+
try {
|
|
57
|
+
const config = JSON.parse((0, node_fs_1.readFileSync)(CONFIG_PATH, "utf-8"));
|
|
58
|
+
const backend = config.llmBackend ?? "auto-detect";
|
|
59
|
+
const model = config.llmModel ?? "default";
|
|
60
|
+
const mode = config.mode === "client" ? "client → " + (config.serverUrl ?? "?") : "server (local)";
|
|
61
|
+
console.log(`Mode: ${mode}`);
|
|
62
|
+
console.log(`LLM backend: ${backend}${backend !== "auto-detect" ? ` (${model})` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
console.log("Config: not configured (run: hicortex init)");
|
|
66
|
+
}
|
|
67
|
+
// Timer/schedule
|
|
68
|
+
const os = (0, node_os_1.platform)();
|
|
69
|
+
let timerActive = false;
|
|
70
|
+
let timerInfo = "not installed";
|
|
71
|
+
if (os === "darwin") {
|
|
72
|
+
try {
|
|
73
|
+
const out = (0, node_child_process_1.execSync)("launchctl list 2>/dev/null | grep hicortex-nightly", {
|
|
74
|
+
encoding: "utf-8",
|
|
75
|
+
timeout: 3000,
|
|
76
|
+
});
|
|
77
|
+
if (out.trim()) {
|
|
78
|
+
timerActive = true;
|
|
79
|
+
timerInfo = "launchd (loaded)";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { /* not installed */ }
|
|
83
|
+
}
|
|
84
|
+
else if (os === "linux") {
|
|
85
|
+
try {
|
|
86
|
+
const active = (0, node_child_process_1.execSync)("systemctl --user is-active hicortex-nightly.timer 2>/dev/null", {
|
|
87
|
+
encoding: "utf-8",
|
|
88
|
+
timeout: 3000,
|
|
89
|
+
}).trim();
|
|
90
|
+
if (active === "active" || active === "waiting") {
|
|
91
|
+
timerActive = true;
|
|
92
|
+
try {
|
|
93
|
+
const next = (0, node_child_process_1.execSync)("systemctl --user show hicortex-nightly.timer --property=NextElapseUSecRealtime 2>/dev/null", {
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
timeout: 3000,
|
|
96
|
+
}).trim();
|
|
97
|
+
const match = next.match(/=(\d+)/);
|
|
98
|
+
if (match) {
|
|
99
|
+
const nextDate = new Date(Number(match[1]) / 1000);
|
|
100
|
+
timerInfo = `systemd (active, next: ${nextDate.toISOString()})`;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
timerInfo = `systemd (${active})`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
timerInfo = `systemd (${active})`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { /* not installed */ }
|
|
112
|
+
}
|
|
113
|
+
console.log(`Timer: ${timerInfo}${!timerActive ? " ⚠ Pipeline will NOT run automatically" : ""}`);
|
|
114
|
+
// DB stats
|
|
115
|
+
const dbPath = (0, db_js_1.resolveDbPath)();
|
|
116
|
+
if ((0, node_fs_1.existsSync)(dbPath)) {
|
|
117
|
+
try {
|
|
118
|
+
const { initDb } = await import("./db.js");
|
|
119
|
+
const db = initDb(dbPath);
|
|
120
|
+
const count = db.prepare("SELECT COUNT(*) as c FROM memories").get().c;
|
|
121
|
+
let linkCount = 0;
|
|
122
|
+
try {
|
|
123
|
+
linkCount = db.prepare("SELECT COUNT(*) as c FROM memory_links").get().c;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// memory_links table may not exist in older DBs
|
|
127
|
+
}
|
|
128
|
+
// Source breakdown (top 5)
|
|
129
|
+
const sources = db.prepare("SELECT source_agent, COUNT(*) as cnt FROM memories GROUP BY source_agent ORDER BY cnt DESC LIMIT 5").all();
|
|
130
|
+
console.log(`\nMemories: ${count} (${linkCount} links)`);
|
|
131
|
+
if (sources.length > 0) {
|
|
132
|
+
console.log("Sources:");
|
|
133
|
+
for (const s of sources) {
|
|
134
|
+
console.log(` ${s.source_agent || "unknown"}: ${s.cnt}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
db.close();
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.log(`\nDB: error (${err instanceof Error ? err.message : String(err)})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(`\nDB: not found (run: hicortex init)`);
|
|
145
|
+
}
|
|
146
|
+
// Health assessment
|
|
147
|
+
console.log("\n" + "─".repeat(40));
|
|
148
|
+
const issues = [];
|
|
149
|
+
if (!lastRun)
|
|
150
|
+
issues.push("Pipeline has never run. Run: hicortex nightly");
|
|
151
|
+
else if (lastRun && (Date.now() - lastRun.getTime()) > STALE_THRESHOLD_HOURS * 60 * 60 * 1000) {
|
|
152
|
+
issues.push(`Pipeline hasn't run in ${STALE_THRESHOLD_HOURS}+ hours. Check timer.`);
|
|
153
|
+
}
|
|
154
|
+
if (!timerActive)
|
|
155
|
+
issues.push("No timer installed. Nightly pipeline won't run automatically.");
|
|
156
|
+
if (!(0, node_fs_1.existsSync)(dbPath))
|
|
157
|
+
issues.push("No database found. Run: hicortex init");
|
|
158
|
+
if (issues.length === 0) {
|
|
159
|
+
console.log("✓ Nightly pipeline healthy");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log("Issues:");
|
|
163
|
+
for (const issue of issues) {
|
|
164
|
+
console.log(` ⚠ ${issue}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/nightly.js
CHANGED
|
@@ -147,18 +147,30 @@ async function runNightly(options = {}) {
|
|
|
147
147
|
reflectModel: savedConfig?.reflectModel,
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
|
-
// Apply
|
|
150
|
+
// Apply distill and reflect overrides from config
|
|
151
151
|
if (savedConfig?.distillModel) {
|
|
152
152
|
llmConfig.distillModel = savedConfig.distillModel;
|
|
153
153
|
}
|
|
154
|
+
if (savedConfig?.distillBaseUrl) {
|
|
155
|
+
llmConfig.distillBaseUrl = savedConfig.distillBaseUrl;
|
|
156
|
+
llmConfig.distillApiKey = savedConfig.distillApiKey ?? llmConfig.apiKey;
|
|
157
|
+
llmConfig.distillProvider = savedConfig.distillProvider ?? llmConfig.provider;
|
|
158
|
+
}
|
|
154
159
|
if (savedConfig?.reflectBaseUrl) {
|
|
155
160
|
llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
|
|
156
161
|
llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
|
|
157
162
|
llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
|
|
158
163
|
}
|
|
164
|
+
// Auto-detect Ollama for batch distillation (claude-cli has rate limits)
|
|
165
|
+
llmConfig = await (0, llm_js_1.preferOllamaForBatch)(llmConfig);
|
|
166
|
+
if (llmConfig.provider === "ollama") {
|
|
167
|
+
console.log(`[hicortex] Auto-detected local Ollama (${llmConfig.model}) — using for batch distillation`);
|
|
168
|
+
}
|
|
159
169
|
const llm = new llm_js_1.LlmClient(llmConfig);
|
|
160
|
-
const distillInfo = llmConfig.
|
|
161
|
-
|
|
170
|
+
const distillInfo = llmConfig.distillBaseUrl
|
|
171
|
+
? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
|
|
172
|
+
: llmConfig.distillModel ?? "";
|
|
173
|
+
console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}`);
|
|
162
174
|
// Step 1: Read new CC transcripts
|
|
163
175
|
const since = readLastRun();
|
|
164
176
|
console.log(`[hicortex] Reading CC transcripts since ${since.toISOString()}`);
|
|
@@ -171,6 +183,8 @@ async function runNightly(options = {}) {
|
|
|
171
183
|
// Step 2: Distill each session
|
|
172
184
|
let memoriesIngested = 0;
|
|
173
185
|
const features = (0, license_js_1.getFeatures)(stateDir);
|
|
186
|
+
// Detect safe chunk size based on model context window
|
|
187
|
+
const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
|
|
174
188
|
for (const batch of batches) {
|
|
175
189
|
const transcript = (0, distiller_js_1.extractConversationText)(batch.entries);
|
|
176
190
|
if (transcript.length < 200) {
|
|
@@ -189,7 +203,7 @@ async function runNightly(options = {}) {
|
|
|
189
203
|
break;
|
|
190
204
|
}
|
|
191
205
|
try {
|
|
192
|
-
const entries = await (0, distiller_js_1.distillSession)(llm, transcript, batch.projectName, batch.date);
|
|
206
|
+
const entries = await (0, distiller_js_1.distillSession)(llm, transcript, batch.projectName, batch.date, chunkSize);
|
|
193
207
|
for (const entry of entries) {
|
|
194
208
|
try {
|
|
195
209
|
const embedding = await (0, embedder_js_1.embed)(entry);
|
|
@@ -288,8 +302,23 @@ async function runClientNightly(config, dryRun) {
|
|
|
288
302
|
if (config.distillModel) {
|
|
289
303
|
llmConfig.distillModel = config.distillModel;
|
|
290
304
|
}
|
|
305
|
+
if (config.distillBaseUrl) {
|
|
306
|
+
llmConfig.distillBaseUrl = config.distillBaseUrl;
|
|
307
|
+
llmConfig.distillApiKey = config.distillApiKey ?? llmConfig.apiKey;
|
|
308
|
+
llmConfig.distillProvider = config.distillProvider ?? llmConfig.provider;
|
|
309
|
+
}
|
|
310
|
+
// Auto-detect Ollama for batch distillation when claude-cli was resolved (fallback)
|
|
311
|
+
llmConfig = await (0, llm_js_1.preferOllamaForBatch)(llmConfig);
|
|
312
|
+
if (llmConfig.provider === "ollama" && !config.distillBaseUrl) {
|
|
313
|
+
console.log(`[hicortex] Auto-detected local Ollama (${llmConfig.model}) — using for batch distillation`);
|
|
314
|
+
}
|
|
291
315
|
const llm = new llm_js_1.LlmClient(llmConfig);
|
|
292
|
-
|
|
316
|
+
const distillInfo = llmConfig.distillBaseUrl
|
|
317
|
+
? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
|
|
318
|
+
: llmConfig.distillModel ?? "";
|
|
319
|
+
console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}`);
|
|
320
|
+
// Detect safe chunk size based on model context window
|
|
321
|
+
const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
|
|
293
322
|
// Read new CC transcripts
|
|
294
323
|
const since = readLastRun();
|
|
295
324
|
console.log(`[hicortex] Reading CC transcripts since ${since.toISOString()}`);
|
|
@@ -316,7 +345,7 @@ async function runClientNightly(config, dryRun) {
|
|
|
316
345
|
continue;
|
|
317
346
|
}
|
|
318
347
|
try {
|
|
319
|
-
const entries = await (0, distiller_js_1.distillSession)(llm, transcript, batch.projectName, batch.date);
|
|
348
|
+
const entries = await (0, distiller_js_1.distillSession)(llm, transcript, batch.projectName, batch.date, chunkSize);
|
|
320
349
|
if (entries.length === 0) {
|
|
321
350
|
console.log(`[hicortex] → No memories extracted`);
|
|
322
351
|
continue;
|
|
@@ -371,7 +400,91 @@ async function runClientNightly(config, dryRun) {
|
|
|
371
400
|
console.error(`[hicortex] Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
372
401
|
}
|
|
373
402
|
}
|
|
403
|
+
// Inject lessons from server into CLAUDE.md
|
|
404
|
+
if (!dryRun) {
|
|
405
|
+
try {
|
|
406
|
+
await injectLessonsFromServer(serverUrl, authToken);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
console.error(`[hicortex] CLAUDE.md injection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
374
412
|
if (!dryRun)
|
|
375
413
|
writeLastRun();
|
|
376
414
|
console.log(`[hicortex] Client nightly complete: ${memoriesIngested} memories from ${sessionsSent} sessions → ${serverUrl}`);
|
|
377
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Fetch lessons + memory index from server and inject into CLAUDE.md.
|
|
418
|
+
* Client mode equivalent of the server's injectLessons(db, ...).
|
|
419
|
+
*/
|
|
420
|
+
async function injectLessonsFromServer(serverUrl, authToken) {
|
|
421
|
+
const resp = await fetch(`${serverUrl}/lessons`, {
|
|
422
|
+
headers: authToken ? { "Authorization": `Bearer ${authToken}` } : {},
|
|
423
|
+
signal: AbortSignal.timeout(10_000),
|
|
424
|
+
});
|
|
425
|
+
if (!resp.ok) {
|
|
426
|
+
console.log(`[hicortex] Could not fetch lessons from server (${resp.status})`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const data = await resp.json();
|
|
430
|
+
const maxLessons = 10;
|
|
431
|
+
const selected = data.lessons.slice(0, maxLessons);
|
|
432
|
+
// Format lessons
|
|
433
|
+
const lessonLines = selected.map((l) => {
|
|
434
|
+
const titleMatch = l.content.match(/## Lesson: (.+)/);
|
|
435
|
+
const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
|
|
436
|
+
const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
|
|
437
|
+
const title = titleMatch ? titleMatch[1] : l.content.slice(0, 150);
|
|
438
|
+
const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
|
|
439
|
+
return `- ${title}${meta ? ` (${meta})` : ""}`;
|
|
440
|
+
});
|
|
441
|
+
// Format project index
|
|
442
|
+
const projectIndex = data.index.projects.map(p => `${p.name}: ${p.count}`);
|
|
443
|
+
// Build block
|
|
444
|
+
const START_MARKER = "<!-- HICORTEX-LEARNINGS:START -->";
|
|
445
|
+
const END_MARKER = "<!-- HICORTEX-LEARNINGS:END -->";
|
|
446
|
+
const blockParts = [START_MARKER, "## Hicortex Memory"];
|
|
447
|
+
blockParts.push("", "You have access to shared long-term memory across all agents and sessions.", "BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.", "Use `hicortex_context` at session start for recent project state.");
|
|
448
|
+
if (lessonLines.length > 0) {
|
|
449
|
+
blockParts.push("", "### Lessons (updated nightly)");
|
|
450
|
+
blockParts.push(...lessonLines);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
blockParts.push("", "### Getting Started");
|
|
454
|
+
blockParts.push("- Search past decisions with `hicortex_search` before starting work");
|
|
455
|
+
blockParts.push("- Save important decisions with `hicortex_ingest`");
|
|
456
|
+
blockParts.push("- Lessons will appear here after the first nightly run");
|
|
457
|
+
}
|
|
458
|
+
if (projectIndex.length > 0) {
|
|
459
|
+
blockParts.push("", "### Memory Index");
|
|
460
|
+
blockParts.push(projectIndex.join(" | "));
|
|
461
|
+
blockParts.push(`${data.index.total} memories, ${data.index.lessonCount} lessons, ${data.index.sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
462
|
+
}
|
|
463
|
+
blockParts.push(END_MARKER);
|
|
464
|
+
const block = blockParts.join("\n");
|
|
465
|
+
// Write to CLAUDE.md
|
|
466
|
+
const { readFileSync, writeFileSync, mkdirSync } = await import("node:fs");
|
|
467
|
+
const { join, dirname } = await import("node:path");
|
|
468
|
+
const { homedir } = await import("node:os");
|
|
469
|
+
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
470
|
+
let content = "";
|
|
471
|
+
try {
|
|
472
|
+
content = readFileSync(claudeMdPath, "utf-8");
|
|
473
|
+
}
|
|
474
|
+
catch { }
|
|
475
|
+
const startIdx = content.indexOf(START_MARKER);
|
|
476
|
+
const endIdx = content.indexOf(END_MARKER);
|
|
477
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
478
|
+
content = content.slice(0, startIdx) + block + content.slice(endIdx + END_MARKER.length);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
if (content.length > 0 && !content.endsWith("\n"))
|
|
482
|
+
content += "\n";
|
|
483
|
+
if (content.length > 0)
|
|
484
|
+
content += "\n";
|
|
485
|
+
content += block + "\n";
|
|
486
|
+
}
|
|
487
|
+
mkdirSync(dirname(claudeMdPath), { recursive: true });
|
|
488
|
+
writeFileSync(claudeMdPath, content);
|
|
489
|
+
console.log(`[hicortex] CLAUDE.md updated: ${lessonLines.length} lessons, ${data.index.total} memories indexed`);
|
|
490
|
+
}
|
package/dist/prompts.js
CHANGED
|
@@ -134,6 +134,8 @@ RULES:
|
|
|
134
134
|
tool configurations, API discoveries, project milestones
|
|
135
135
|
- PRIORITIZE Corrections & Rejections — these are high-value signals for learning
|
|
136
136
|
what the user does NOT want. Even a single "no" or style correction is worth extracting.
|
|
137
|
+
- Strong language or profanity from the user is a high-intensity signal — it indicates
|
|
138
|
+
the correction matters deeply. Note the intensity in the extraction.
|
|
137
139
|
- PRIVACY CLASSIFICATION (one of):
|
|
138
140
|
- PUBLIC: general tech knowledge, open-source patterns, publicly available info
|
|
139
141
|
- WORK: project-specific decisions, architecture choices, client/business context
|
package/dist/status.js
CHANGED
|
@@ -112,9 +112,43 @@ async function runStatus() {
|
|
|
112
112
|
const lastRunPath = (0, node_path_1.join)(HICORTEX_HOME, "nightly-last-run.txt");
|
|
113
113
|
try {
|
|
114
114
|
const ts = (0, node_fs_1.readFileSync)(lastRunPath, "utf-8").trim();
|
|
115
|
-
|
|
115
|
+
const lastRun = new Date(ts);
|
|
116
|
+
if (isNaN(lastRun.getTime())) {
|
|
117
|
+
console.log(` Last run: ${ts} (invalid timestamp)`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const STALE_THRESHOLD_HOURS = 30;
|
|
121
|
+
const ageHours = Math.round((Date.now() - lastRun.getTime()) / (60 * 60 * 1000));
|
|
122
|
+
const ageStr = ageHours < 1 ? "just now" : ageHours < 24 ? `${ageHours}h ago` : `${Math.round(ageHours / 24)}d ago`;
|
|
123
|
+
console.log(` Last run: ${ts} (${ageStr})`);
|
|
124
|
+
// Show staleness warning if missed a night
|
|
125
|
+
if (ageHours > STALE_THRESHOLD_HOURS) {
|
|
126
|
+
console.log(` ⚠ Nightly pipeline hasn't run in ${ageHours}h. Check: hicortex nightly --dry-run`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
}
|
|
117
130
|
catch {
|
|
118
|
-
console.log(" Last run: never");
|
|
131
|
+
console.log(" Last run: never (run: hicortex nightly)");
|
|
132
|
+
}
|
|
133
|
+
// Distillation stats (if DB exists)
|
|
134
|
+
if (dbExists) {
|
|
135
|
+
const TOP_SOURCES_LIMIT = 5;
|
|
136
|
+
try {
|
|
137
|
+
const { initDb } = await import("./db.js");
|
|
138
|
+
const db2 = initDb(dbPath);
|
|
139
|
+
// Count memories by source
|
|
140
|
+
const rows = db2.prepare(`SELECT source_agent, COUNT(*) as cnt FROM memories GROUP BY source_agent ORDER BY cnt DESC LIMIT ${TOP_SOURCES_LIMIT}`).all();
|
|
141
|
+
if (rows.length > 0) {
|
|
142
|
+
console.log(" Sources:");
|
|
143
|
+
for (const r of rows) {
|
|
144
|
+
const agent = r.source_agent || "unknown";
|
|
145
|
+
console.log(` ${agent}: ${r.cnt}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
db2.close();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.log(` Sources: (error: ${err instanceof Error ? err.message : String(err)})`);
|
|
152
|
+
}
|
|
119
153
|
}
|
|
120
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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": {
|
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
"./dist/index.js"
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp-server",
|
|
16
|
+
"memory",
|
|
17
|
+
"ai-agents",
|
|
18
|
+
"agent-memory",
|
|
19
|
+
"self-improving",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"openclaw",
|
|
22
|
+
"vector-search",
|
|
23
|
+
"nightly-distillation"
|
|
24
|
+
],
|
|
14
25
|
"types": "dist/index.d.ts",
|
|
15
26
|
"files": [
|
|
16
27
|
"dist/",
|