@gamaze/hicortex 0.2.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 +119 -0
- package/dist/consolidate.d.ts +36 -0
- package/dist/consolidate.js +482 -0
- package/dist/db.d.ts +19 -0
- package/dist/db.js +140 -0
- package/dist/distiller.d.ts +15 -0
- package/dist/distiller.js +186 -0
- package/dist/embedder.d.ts +20 -0
- package/dist/embedder.js +85 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +557 -0
- package/dist/license.d.ts +5 -0
- package/dist/license.js +96 -0
- package/dist/llm.d.ts +66 -0
- package/dist/llm.js +421 -0
- package/dist/prompts.d.ts +16 -0
- package/dist/prompts.js +117 -0
- package/dist/retrieval.d.ts +47 -0
- package/dist/retrieval.js +320 -0
- package/dist/storage.d.ts +98 -0
- package/dist/storage.js +326 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +6 -0
- package/openclaw.plugin.json +70 -0
- package/package.json +42 -0
- package/skills/hicortex-activate/SKILL.md +53 -0
- package/skills/hicortex-learn/SKILL.md +40 -0
- package/skills/hicortex-memory/SKILL.md +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hicortex OpenClaw Plugin — Long-term Memory That Learns.
|
|
4
|
+
*
|
|
5
|
+
* Pure in-process plugin: no sidecar, no HTTP. Uses better-sqlite3 + sqlite-vec
|
|
6
|
+
* for storage, @huggingface/transformers for embeddings, and multi-provider LLM
|
|
7
|
+
* for distillation and consolidation.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
const node_path_1 = require("node:path");
|
|
44
|
+
const node_fs_1 = require("node:fs");
|
|
45
|
+
const db_js_1 = require("./db.js");
|
|
46
|
+
const license_js_1 = require("./license.js");
|
|
47
|
+
const llm_js_1 = require("./llm.js");
|
|
48
|
+
const node_fs_2 = require("node:fs");
|
|
49
|
+
const node_os_1 = require("node:os");
|
|
50
|
+
const embedder_js_1 = require("./embedder.js");
|
|
51
|
+
const storage = __importStar(require("./storage.js"));
|
|
52
|
+
const retrieval = __importStar(require("./retrieval.js"));
|
|
53
|
+
const distiller_js_1 = require("./distiller.js");
|
|
54
|
+
const consolidate_js_1 = require("./consolidate.js");
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Module state — initialized in registerService.start()
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
let db = null;
|
|
59
|
+
let llm = null;
|
|
60
|
+
let cancelConsolidation = null;
|
|
61
|
+
let stateDir = "";
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Plugin export
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
exports.default = {
|
|
66
|
+
id: "hicortex",
|
|
67
|
+
name: "Hicortex \u2014 Long-term Memory That Learns",
|
|
68
|
+
kind: "lifecycle",
|
|
69
|
+
register(api) {
|
|
70
|
+
// -----------------------------------------------------------------------
|
|
71
|
+
// Background service: init DB, embedder, LLM, consolidation timer
|
|
72
|
+
// -----------------------------------------------------------------------
|
|
73
|
+
api.registerService({
|
|
74
|
+
id: "hicortex-service",
|
|
75
|
+
async start(ctx) {
|
|
76
|
+
const config = (ctx.config ?? {});
|
|
77
|
+
stateDir = ctx.stateDir ?? (0, node_path_1.join)(process.env.HOME ?? "~", ".hicortex");
|
|
78
|
+
const log = ctx.logger
|
|
79
|
+
? (msg) => ctx.logger.info(msg)
|
|
80
|
+
: console.log;
|
|
81
|
+
// Ensure data directory exists
|
|
82
|
+
const dataDir = (0, node_path_1.join)(stateDir, "data");
|
|
83
|
+
(0, node_fs_1.mkdirSync)(dataDir, { recursive: true });
|
|
84
|
+
// Initialize database
|
|
85
|
+
const dbPath = config.dbPath ?? (0, node_path_1.join)(dataDir, "hicortex.db");
|
|
86
|
+
log(`[hicortex] Initializing database at ${dbPath}`);
|
|
87
|
+
db = (0, db_js_1.initDb)(dbPath);
|
|
88
|
+
// Auto-configure LLM: resolve → test → persist
|
|
89
|
+
const llmConfig = await autoConfigureLlm(config, log);
|
|
90
|
+
llm = new llm_js_1.LlmClient(llmConfig);
|
|
91
|
+
log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model} ` +
|
|
92
|
+
`(reflect: ${llmConfig.reflectModel})`);
|
|
93
|
+
// License check (non-blocking)
|
|
94
|
+
(0, license_js_1.validateLicense)(config.licenseKey, stateDir).catch((err) => log(`[hicortex] License validation failed: ${err}`));
|
|
95
|
+
// Schedule nightly consolidation
|
|
96
|
+
const consolidateHour = config.consolidateHour ?? 2;
|
|
97
|
+
cancelConsolidation = (0, consolidate_js_1.scheduleConsolidation)(db, llm, embedder_js_1.embed, consolidateHour);
|
|
98
|
+
// Seed the bootstrap lesson on first run
|
|
99
|
+
await injectSeedLesson(db, log);
|
|
100
|
+
// Auto-add tools to tools.allow if using a restrictive profile
|
|
101
|
+
ensureToolsAllowed(log);
|
|
102
|
+
// Log stats
|
|
103
|
+
const stats = (0, db_js_1.getStats)(db, dbPath);
|
|
104
|
+
log(`[hicortex] Ready: ${stats.memories} memories, ${stats.links} links, ` +
|
|
105
|
+
`${Math.round(stats.db_size_bytes / 1024)} KB`);
|
|
106
|
+
},
|
|
107
|
+
async stop() {
|
|
108
|
+
if (cancelConsolidation) {
|
|
109
|
+
cancelConsolidation();
|
|
110
|
+
cancelConsolidation = null;
|
|
111
|
+
}
|
|
112
|
+
if (db) {
|
|
113
|
+
db.close();
|
|
114
|
+
db = null;
|
|
115
|
+
}
|
|
116
|
+
llm = null;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// -----------------------------------------------------------------------
|
|
120
|
+
// Hook: before_agent_start — inject lessons into agent context
|
|
121
|
+
// -----------------------------------------------------------------------
|
|
122
|
+
api.on("before_agent_start", async (event, ctx) => {
|
|
123
|
+
if (!db)
|
|
124
|
+
return {};
|
|
125
|
+
try {
|
|
126
|
+
const lessons = storage.getLessons(db, 7, ctx.project);
|
|
127
|
+
if (lessons.length === 0)
|
|
128
|
+
return {};
|
|
129
|
+
const maxFeatures = (0, license_js_1.getFeatures)(stateDir);
|
|
130
|
+
const maxLessons = maxFeatures.maxMemories === -1 ? 20 : 10;
|
|
131
|
+
const formatted = lessons.slice(0, maxLessons).map((l) => {
|
|
132
|
+
// Extract just the lesson text from the structured content
|
|
133
|
+
const match = l.content.match(/## Lesson: (.+)/);
|
|
134
|
+
return match ? `- ${match[1]}` : `- ${l.content.slice(0, 200)}`;
|
|
135
|
+
});
|
|
136
|
+
let context = `\n\n## Hicortex Lessons (auto-injected from long-term memory)\n` +
|
|
137
|
+
`These are actionable lessons learned from past sessions:\n\n` +
|
|
138
|
+
formatted.join("\n") +
|
|
139
|
+
"\n";
|
|
140
|
+
// Daily friendly reminder when at memory cap
|
|
141
|
+
const memCount = storage.countMemories(db);
|
|
142
|
+
if (maxFeatures.maxMemories > 0 && memCount >= maxFeatures.maxMemories) {
|
|
143
|
+
context +=
|
|
144
|
+
`\n---\nHicortex free tier: ${maxFeatures.maxMemories} memories stored. ` +
|
|
145
|
+
`New memories can no longer be saved, and your agent has stopped learning and self-improving from new sessions. ` +
|
|
146
|
+
`Existing memories, lessons, and search still work. ` +
|
|
147
|
+
`Upgrade for unlimited usage: https://hicortex.gamaze.com/ ` +
|
|
148
|
+
`— after purchase, tell me your key and I'll activate it for you.\n`;
|
|
149
|
+
}
|
|
150
|
+
return { appendSystemContext: context };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// -----------------------------------------------------------------------
|
|
157
|
+
// Hook: agent_end — distill conversation into memories
|
|
158
|
+
// -----------------------------------------------------------------------
|
|
159
|
+
api.on("agent_end", async (event, ctx) => {
|
|
160
|
+
if (!db || !llm)
|
|
161
|
+
return;
|
|
162
|
+
if (!event.success || event.messages.length < 4)
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
const transcript = (0, distiller_js_1.extractConversationText)(event.messages);
|
|
166
|
+
if (transcript.length < 200)
|
|
167
|
+
return;
|
|
168
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
169
|
+
const projectName = ctx.project ?? "unknown";
|
|
170
|
+
const sourceAgent = `openclaw/${ctx.agentId ?? "unknown"}`;
|
|
171
|
+
const entries = await (0, distiller_js_1.distillSession)(llm, transcript, projectName, date);
|
|
172
|
+
if (entries.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
// Check license cap
|
|
175
|
+
const features = (0, license_js_1.getFeatures)(stateDir);
|
|
176
|
+
if (features.maxMemories > 0 &&
|
|
177
|
+
storage.countMemories(db) >= features.maxMemories) {
|
|
178
|
+
console.warn(`[hicortex] Free tier limit reached (${features.maxMemories} memories). ` +
|
|
179
|
+
`Search and lessons still work, but new memories won't be saved. ` +
|
|
180
|
+
`Upgrade for unlimited usage: https://hicortex.gamaze.com/`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Embed and ingest each entry
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
try {
|
|
186
|
+
const embedding = await (0, embedder_js_1.embed)(entry);
|
|
187
|
+
storage.insertMemory(db, entry, embedding, {
|
|
188
|
+
sourceAgent,
|
|
189
|
+
sourceSession: ctx.sessionKey,
|
|
190
|
+
project: projectName,
|
|
191
|
+
privacy: "WORK",
|
|
192
|
+
memoryType: "episode",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
console.error(`[hicortex] Failed to ingest entry: ${msg}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Non-fatal — session capture failing shouldn't break the agent
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// -----------------------------------------------------------------------
|
|
206
|
+
// Hook: session_end — check if consolidation is overdue
|
|
207
|
+
// -----------------------------------------------------------------------
|
|
208
|
+
api.on("session_end", async (event, _ctx) => {
|
|
209
|
+
if (!db || !llm || event.messageCount < 4)
|
|
210
|
+
return;
|
|
211
|
+
// Opportunistic consolidation if no nightly run in 48h
|
|
212
|
+
try {
|
|
213
|
+
const { readFileSync: readFs } = await import("node:fs");
|
|
214
|
+
const { join: joinPath } = await import("node:path");
|
|
215
|
+
const { homedir: homeDir } = await import("node:os");
|
|
216
|
+
const lastPath = joinPath(homeDir(), ".claude", "memory", "last-consolidated.txt");
|
|
217
|
+
const lastTs = readFs(lastPath, "utf-8").trim();
|
|
218
|
+
const lastDate = new Date(lastTs);
|
|
219
|
+
const hoursSince = (Date.now() - lastDate.getTime()) / (1000 * 60 * 60);
|
|
220
|
+
if (hoursSince > 48) {
|
|
221
|
+
console.log("[hicortex] Consolidation overdue — triggering now");
|
|
222
|
+
(0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed).catch((err) => {
|
|
223
|
+
console.error("[hicortex] Opportunistic consolidation failed:", err);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// No timestamp file — first run, consolidation will happen on schedule
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
// -----------------------------------------------------------------------
|
|
232
|
+
// Tools — registered as factory functions per OC plugin API
|
|
233
|
+
// -----------------------------------------------------------------------
|
|
234
|
+
api.registerTool((_ctx) => ({
|
|
235
|
+
name: "hicortex_search",
|
|
236
|
+
description: "Search long-term memory using semantic similarity. Returns the most relevant memories from past sessions.",
|
|
237
|
+
parameters: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
query: { type: "string", description: "Search query text" },
|
|
241
|
+
limit: { type: "number", description: "Max results (default 5)" },
|
|
242
|
+
project: { type: "string", description: "Filter by project name" },
|
|
243
|
+
},
|
|
244
|
+
required: ["query"],
|
|
245
|
+
},
|
|
246
|
+
async execute(_callId, args, _ctx) {
|
|
247
|
+
if (!db)
|
|
248
|
+
return { error: "Hicortex not initialized" };
|
|
249
|
+
try {
|
|
250
|
+
const results = await retrieval.retrieve(db, embedder_js_1.embed, args.query, {
|
|
251
|
+
limit: args.limit,
|
|
252
|
+
project: args.project,
|
|
253
|
+
});
|
|
254
|
+
return formatToolResults(results);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
return { error: `Search failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
}), { name: "hicortex_search" });
|
|
261
|
+
api.registerTool((_ctx) => ({
|
|
262
|
+
name: "hicortex_context",
|
|
263
|
+
description: "Get recent context memories, optionally filtered by project. Useful to recall what happened recently.",
|
|
264
|
+
parameters: {
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: {
|
|
267
|
+
project: { type: "string", description: "Filter by project name" },
|
|
268
|
+
limit: { type: "number", description: "Max results (default 10)" },
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
execute(_callId, args, _ctx) {
|
|
272
|
+
if (!db)
|
|
273
|
+
return { error: "Hicortex not initialized" };
|
|
274
|
+
try {
|
|
275
|
+
const results = retrieval.searchContext(db, {
|
|
276
|
+
project: args?.project,
|
|
277
|
+
limit: args?.limit,
|
|
278
|
+
});
|
|
279
|
+
return formatToolResults(results);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
return { error: `Context search failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
}), { name: "hicortex_context" });
|
|
286
|
+
api.registerTool((_ctx) => ({
|
|
287
|
+
name: "hicortex_ingest",
|
|
288
|
+
description: "Store a new memory in long-term storage. Use for important facts, decisions, or lessons.",
|
|
289
|
+
parameters: {
|
|
290
|
+
type: "object",
|
|
291
|
+
properties: {
|
|
292
|
+
content: { type: "string", description: "Memory content to store" },
|
|
293
|
+
project: { type: "string", description: "Project this memory belongs to" },
|
|
294
|
+
memory_type: {
|
|
295
|
+
type: "string",
|
|
296
|
+
enum: ["episode", "lesson", "fact", "decision"],
|
|
297
|
+
description: "Type of memory (default: episode)",
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
required: ["content"],
|
|
301
|
+
},
|
|
302
|
+
async execute(_callId, args, context) {
|
|
303
|
+
if (!db)
|
|
304
|
+
return { error: "Hicortex not initialized" };
|
|
305
|
+
const features = (0, license_js_1.getFeatures)(stateDir);
|
|
306
|
+
if (features.maxMemories > 0 && storage.countMemories(db) >= features.maxMemories) {
|
|
307
|
+
return {
|
|
308
|
+
content: [{
|
|
309
|
+
type: "text",
|
|
310
|
+
text: `Free tier limit reached (${features.maxMemories} memories). ` +
|
|
311
|
+
`Your existing memories and lessons still work — search and recall are unaffected. ` +
|
|
312
|
+
`New memories won't be saved until you upgrade.\n\n` +
|
|
313
|
+
`Upgrade for unlimited usage: https://hicortex.gamaze.com/`
|
|
314
|
+
}],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const embedding = await (0, embedder_js_1.embed)(args.content);
|
|
319
|
+
const id = storage.insertMemory(db, args.content, embedding, {
|
|
320
|
+
sourceAgent: `openclaw/${context?.agentId ?? "manual"}`,
|
|
321
|
+
project: args.project,
|
|
322
|
+
memoryType: args.memory_type ?? "episode",
|
|
323
|
+
privacy: "WORK",
|
|
324
|
+
});
|
|
325
|
+
return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})` }] };
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
return { error: `Ingest failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
}), { name: "hicortex_ingest" });
|
|
332
|
+
api.registerTool((_ctx) => ({
|
|
333
|
+
name: "hicortex_lessons",
|
|
334
|
+
description: "Get actionable lessons learned from past sessions. Auto-generated insights about mistakes to avoid.",
|
|
335
|
+
parameters: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {
|
|
338
|
+
days: { type: "number", description: "Look back N days (default 7)" },
|
|
339
|
+
project: { type: "string", description: "Filter by project name" },
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
execute(_callId, args, _ctx) {
|
|
343
|
+
if (!db)
|
|
344
|
+
return { error: "Hicortex not initialized" };
|
|
345
|
+
try {
|
|
346
|
+
const lessons = storage.getLessons(db, args.days ?? 7, args.project);
|
|
347
|
+
if (lessons.length === 0) {
|
|
348
|
+
return { content: [{ type: "text", text: "No lessons found for the specified period." }] };
|
|
349
|
+
}
|
|
350
|
+
const text = lessons.map((l) => `- ${l.content.slice(0, 500)}`).join("\n");
|
|
351
|
+
return { content: [{ type: "text", text }] };
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
return { error: `Lessons fetch failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
}), { name: "hicortex_lessons" });
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Helpers
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Auto-configure LLM: resolve config → test connection → persist if new
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
async function autoConfigureLlm(pluginConfig, log) {
|
|
367
|
+
// Step 1: Resolve LLM config from all sources
|
|
368
|
+
const llmConfig = (0, llm_js_1.resolveLlmConfig)({
|
|
369
|
+
llmBaseUrl: pluginConfig.llmBaseUrl,
|
|
370
|
+
llmApiKey: pluginConfig.llmApiKey,
|
|
371
|
+
llmModel: pluginConfig.llmModel,
|
|
372
|
+
reflectModel: pluginConfig.reflectModel,
|
|
373
|
+
});
|
|
374
|
+
// Step 2: Test the connection
|
|
375
|
+
log(`[hicortex] Testing LLM connection: ${llmConfig.provider}/${llmConfig.model} @ ${llmConfig.baseUrl}`);
|
|
376
|
+
const testClient = new llm_js_1.LlmClient(llmConfig);
|
|
377
|
+
try {
|
|
378
|
+
const response = await testClient.completeFast("Respond with just the word OK", 10);
|
|
379
|
+
if (response && response.length > 0) {
|
|
380
|
+
log(`[hicortex] LLM connection verified`);
|
|
381
|
+
// Step 3: Persist to OC config so future startups skip detection
|
|
382
|
+
persistProviderConfig(llmConfig, log);
|
|
383
|
+
return llmConfig;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
388
|
+
log(`[hicortex] LLM test failed (${llmConfig.baseUrl}): ${msg}`);
|
|
389
|
+
}
|
|
390
|
+
// Step 4: If test failed and this is z.ai, try the alternate endpoint
|
|
391
|
+
if (llmConfig.provider === "zai") {
|
|
392
|
+
const altUrl = llmConfig.baseUrl.includes("/coding/")
|
|
393
|
+
? llmConfig.baseUrl.replace("/coding/", "/")
|
|
394
|
+
: llmConfig.baseUrl.replace("/api/paas/", "/api/coding/paas/");
|
|
395
|
+
log(`[hicortex] Trying alternate z.ai endpoint: ${altUrl}`);
|
|
396
|
+
llmConfig.baseUrl = altUrl;
|
|
397
|
+
const altClient = new llm_js_1.LlmClient(llmConfig);
|
|
398
|
+
try {
|
|
399
|
+
const response = await altClient.completeFast("Respond with just the word OK", 10);
|
|
400
|
+
if (response && response.length > 0) {
|
|
401
|
+
log(`[hicortex] LLM connection verified on alternate endpoint`);
|
|
402
|
+
persistProviderConfig(llmConfig, log);
|
|
403
|
+
return llmConfig;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
408
|
+
log(`[hicortex] Alternate z.ai endpoint also failed: ${msg}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Step 5: Fall back — return the config anyway, log instructions
|
|
412
|
+
log(`[hicortex] WARNING: Could not verify LLM connection. ` +
|
|
413
|
+
`Distillation and consolidation may fail. ` +
|
|
414
|
+
`To fix: add models.providers.${llmConfig.provider}.baseUrl to ~/.openclaw/openclaw.json ` +
|
|
415
|
+
`or set llmBaseUrl in the hicortex plugin config.`);
|
|
416
|
+
return llmConfig;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Persist verified provider config to openclaw.json so future startups
|
|
420
|
+
* skip detection and go straight to the verified URL.
|
|
421
|
+
*/
|
|
422
|
+
function persistProviderConfig(llmConfig, log) {
|
|
423
|
+
try {
|
|
424
|
+
const configPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
|
|
425
|
+
const raw = (0, node_fs_2.readFileSync)(configPath, "utf-8");
|
|
426
|
+
const config = JSON.parse(raw);
|
|
427
|
+
// Check if baseUrl already stored for this provider
|
|
428
|
+
const existing = config?.models?.providers?.[llmConfig.provider]?.baseUrl;
|
|
429
|
+
if (existing === llmConfig.baseUrl)
|
|
430
|
+
return; // Already persisted
|
|
431
|
+
// Write the provider config
|
|
432
|
+
if (!config.models)
|
|
433
|
+
config.models = {};
|
|
434
|
+
if (!config.models.providers)
|
|
435
|
+
config.models.providers = {};
|
|
436
|
+
if (!config.models.providers[llmConfig.provider]) {
|
|
437
|
+
config.models.providers[llmConfig.provider] = {};
|
|
438
|
+
}
|
|
439
|
+
const prov = config.models.providers[llmConfig.provider];
|
|
440
|
+
prov.baseUrl = llmConfig.baseUrl;
|
|
441
|
+
// OC requires a models array — preserve existing or add the active model
|
|
442
|
+
if (!prov.models || !Array.isArray(prov.models)) {
|
|
443
|
+
prov.models = [{
|
|
444
|
+
id: llmConfig.model,
|
|
445
|
+
name: llmConfig.model,
|
|
446
|
+
input: ["text"],
|
|
447
|
+
contextWindow: 128000,
|
|
448
|
+
maxTokens: 8192,
|
|
449
|
+
}];
|
|
450
|
+
}
|
|
451
|
+
(0, node_fs_2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
452
|
+
log(`[hicortex] Persisted LLM config: ${llmConfig.provider} → ${llmConfig.baseUrl}`);
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
// Non-fatal — config works in memory even if we can't persist
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Track when the memory cap was first hit. Returns days since cap was reached.
|
|
460
|
+
* Stores timestamp in stateDir/cap-hit.txt on first detection.
|
|
461
|
+
*/
|
|
462
|
+
function getDaysSinceCapHit(dir) {
|
|
463
|
+
const capFile = (0, node_path_1.join)(dir, "cap-hit.txt");
|
|
464
|
+
try {
|
|
465
|
+
const ts = (0, node_fs_2.readFileSync)(capFile, "utf-8").trim();
|
|
466
|
+
const hitDate = new Date(ts);
|
|
467
|
+
return Math.floor((Date.now() - hitDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// First time hitting cap — record it
|
|
471
|
+
try {
|
|
472
|
+
(0, node_fs_2.writeFileSync)(capFile, new Date().toISOString());
|
|
473
|
+
}
|
|
474
|
+
catch { /* non-fatal */ }
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const HICORTEX_TOOLS = [
|
|
479
|
+
"hicortex_search",
|
|
480
|
+
"hicortex_context",
|
|
481
|
+
"hicortex_ingest",
|
|
482
|
+
"hicortex_lessons",
|
|
483
|
+
];
|
|
484
|
+
/**
|
|
485
|
+
* Ensure hicortex tools are in tools.allow so they're visible to agents
|
|
486
|
+
* regardless of the tools.profile setting.
|
|
487
|
+
*/
|
|
488
|
+
function ensureToolsAllowed(log) {
|
|
489
|
+
try {
|
|
490
|
+
const configPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
|
|
491
|
+
const raw = (0, node_fs_2.readFileSync)(configPath, "utf-8");
|
|
492
|
+
const config = JSON.parse(raw);
|
|
493
|
+
if (!config.tools)
|
|
494
|
+
config.tools = {};
|
|
495
|
+
if (!Array.isArray(config.tools.allow))
|
|
496
|
+
config.tools.allow = [];
|
|
497
|
+
const missing = HICORTEX_TOOLS.filter((t) => !config.tools.allow.includes(t));
|
|
498
|
+
if (missing.length === 0)
|
|
499
|
+
return;
|
|
500
|
+
config.tools.allow.push(...missing);
|
|
501
|
+
(0, node_fs_2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
502
|
+
log(`[hicortex] Added tools to allow list: ${missing.join(", ")}`);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Non-fatal
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// Seed lesson — injected on first run, teaches the agent to self-improve
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
const SEED_LESSON = `## Lesson: 1% Daily Self-Improvement
|
|
512
|
+
|
|
513
|
+
Self-improvement protocol: Review your sessions daily. Scan for mistakes, gaps, user corrections, and friction. Identify 1-2 issues maximum. Apply one targeted fix to your instruction files (AGENT.md, IDENTITY.md, TOOLS.md, SOUL.md, or CLAUDE.md). Never overcorrect — feedback is guidance, not emergency. One fix per issue. Never rewrite entire systems. Before changing something, ask: is this a real pattern or am I reacting to noise?
|
|
514
|
+
|
|
515
|
+
The flywheel: Sessions → Hicortex captures → Nightly consolidation → Lessons generated → Lessons injected into your context → You apply one fix → Better sessions tomorrow.
|
|
516
|
+
|
|
517
|
+
Use hicortex_lessons to check for new insights. If a lesson has high confidence, apply it. If medium, observe for one more day. If nothing needs fixing today, skip — that's fine.
|
|
518
|
+
|
|
519
|
+
**Severity:** critical
|
|
520
|
+
**Confidence:** high
|
|
521
|
+
**Generated:** seed-lesson`;
|
|
522
|
+
async function injectSeedLesson(database, log) {
|
|
523
|
+
try {
|
|
524
|
+
// Check if seed lesson already exists (match on unique content)
|
|
525
|
+
const existing = storage.getLessons(database, 365);
|
|
526
|
+
const hasSeed = existing.some((l) => l.content.includes("1% Daily Self-Improvement") ||
|
|
527
|
+
l.source_agent === "hicortex/seed");
|
|
528
|
+
if (hasSeed)
|
|
529
|
+
return;
|
|
530
|
+
// Embed and store
|
|
531
|
+
const embedding = await (0, embedder_js_1.embed)(SEED_LESSON);
|
|
532
|
+
storage.insertMemory(database, SEED_LESSON, embedding, {
|
|
533
|
+
sourceAgent: "hicortex/seed",
|
|
534
|
+
project: "global",
|
|
535
|
+
memoryType: "lesson",
|
|
536
|
+
baseStrength: 0.95,
|
|
537
|
+
privacy: "WORK",
|
|
538
|
+
});
|
|
539
|
+
log("[hicortex] Seed lesson injected: Daily Self-Improvement Protocol");
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
// Non-fatal — log but don't crash the plugin
|
|
543
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
544
|
+
log(`[hicortex] Warning: could not inject seed lesson: ${msg}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function formatToolResults(results) {
|
|
548
|
+
if (results.length === 0) {
|
|
549
|
+
return {
|
|
550
|
+
content: [{ type: "text", text: "No memories found." }],
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const text = results
|
|
554
|
+
.map((r) => `[${r.memory_type}] (score: ${r.score.toFixed(3)}, strength: ${r.effective_strength.toFixed(3)}) ${r.content.slice(0, 500)}`)
|
|
555
|
+
.join("\n\n");
|
|
556
|
+
return { content: [{ type: "text", text }] };
|
|
557
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LicenseInfo } from "./types.js";
|
|
2
|
+
/** Validate a license key against the Hicortex API */
|
|
3
|
+
export declare function validateLicense(key: string | undefined, stateDir: string): Promise<LicenseInfo>;
|
|
4
|
+
/** Get current features, using cache or free tier defaults */
|
|
5
|
+
export declare function getFeatures(stateDir: string): LicenseInfo["features"];
|
package/dist/license.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateLicense = validateLicense;
|
|
4
|
+
exports.getFeatures = getFeatures;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const VALIDATE_URL = "https://hicortex.gamaze.com/api/validate";
|
|
8
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
9
|
+
const OFFLINE_GRACE_DAYS = 7;
|
|
10
|
+
// In-memory cache
|
|
11
|
+
let cachedLicense = null;
|
|
12
|
+
let cacheTimestamp = 0;
|
|
13
|
+
const FREE_LICENSE = {
|
|
14
|
+
valid: false,
|
|
15
|
+
tier: "free",
|
|
16
|
+
features: {
|
|
17
|
+
reflection: true,
|
|
18
|
+
vectorSearch: true,
|
|
19
|
+
maxMemories: 250,
|
|
20
|
+
crossAgent: true,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
/** Validate a license key against the Hicortex API */
|
|
24
|
+
async function validateLicense(key, stateDir) {
|
|
25
|
+
// No key = free tier
|
|
26
|
+
if (!key)
|
|
27
|
+
return FREE_LICENSE;
|
|
28
|
+
// Check in-memory cache
|
|
29
|
+
if (cachedLicense && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
|
|
30
|
+
return cachedLicense;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const resp = await fetch(VALIDATE_URL, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({ key }),
|
|
37
|
+
signal: AbortSignal.timeout(10_000),
|
|
38
|
+
});
|
|
39
|
+
if (!resp.ok) {
|
|
40
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
41
|
+
}
|
|
42
|
+
const data = (await resp.json());
|
|
43
|
+
// Cache result
|
|
44
|
+
cachedLicense = data;
|
|
45
|
+
cacheTimestamp = Date.now();
|
|
46
|
+
// Persist last successful validation timestamp for offline grace
|
|
47
|
+
if (data.valid) {
|
|
48
|
+
persistValidationTimestamp(stateDir);
|
|
49
|
+
}
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Network failure — check offline grace period
|
|
54
|
+
return offlineFallback(key, stateDir);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Get current features, using cache or free tier defaults */
|
|
58
|
+
function getFeatures(stateDir) {
|
|
59
|
+
if (cachedLicense)
|
|
60
|
+
return cachedLicense.features;
|
|
61
|
+
return FREE_LICENSE.features;
|
|
62
|
+
}
|
|
63
|
+
function persistValidationTimestamp(stateDir) {
|
|
64
|
+
try {
|
|
65
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(stateDir, "license-validated.txt"), new Date().toISOString());
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Non-critical
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function offlineFallback(key, stateDir) {
|
|
72
|
+
const tsPath = (0, node_path_1.join)(stateDir, "license-validated.txt");
|
|
73
|
+
if (!(0, node_fs_1.existsSync)(tsPath))
|
|
74
|
+
return FREE_LICENSE;
|
|
75
|
+
try {
|
|
76
|
+
const lastValidated = new Date((0, node_fs_1.readFileSync)(tsPath, "utf-8").trim());
|
|
77
|
+
const daysSince = (Date.now() - lastValidated.getTime()) / (1000 * 60 * 60 * 24);
|
|
78
|
+
if (daysSince <= OFFLINE_GRACE_DAYS) {
|
|
79
|
+
// Within grace period — assume last known state was valid
|
|
80
|
+
return cachedLicense ?? {
|
|
81
|
+
valid: true,
|
|
82
|
+
tier: "pro",
|
|
83
|
+
features: {
|
|
84
|
+
reflection: true,
|
|
85
|
+
vectorSearch: true,
|
|
86
|
+
maxMemories: -1,
|
|
87
|
+
crossAgent: true,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Corrupted file
|
|
94
|
+
}
|
|
95
|
+
return FREE_LICENSE;
|
|
96
|
+
}
|