@gamaze/hicortex 0.7.1 → 0.10.1

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.
Files changed (49) hide show
  1. package/README.md +72 -39
  2. package/dist/claude-md.d.ts +9 -21
  3. package/dist/claude-md.js +9 -241
  4. package/dist/cli.d.ts +3 -2
  5. package/dist/cli.js +29 -11
  6. package/dist/consolidate.js +0 -7
  7. package/dist/db.js +24 -0
  8. package/dist/embedder.d.ts +11 -0
  9. package/dist/embedder.js +27 -0
  10. package/dist/extensions.d.ts +41 -88
  11. package/dist/extensions.js +36 -61
  12. package/dist/features.d.ts +21 -25
  13. package/dist/features.js +47 -83
  14. package/dist/hermes-transcript-reader.d.ts +27 -0
  15. package/dist/hermes-transcript-reader.js +134 -0
  16. package/dist/index.d.ts +16 -4
  17. package/dist/index.js +252 -344
  18. package/dist/init.d.ts +41 -1
  19. package/dist/init.js +545 -190
  20. package/dist/lesson-selection.d.ts +62 -0
  21. package/dist/lesson-selection.js +159 -0
  22. package/dist/lessons-context.d.ts +17 -0
  23. package/dist/lessons-context.js +96 -0
  24. package/dist/llm.d.ts +42 -29
  25. package/dist/llm.js +89 -270
  26. package/dist/mcp-server.d.ts +0 -1
  27. package/dist/mcp-server.js +404 -86
  28. package/dist/nightly.d.ts +9 -6
  29. package/dist/nightly.js +197 -357
  30. package/dist/oc-transcript-reader.d.ts +20 -0
  31. package/dist/oc-transcript-reader.js +61 -0
  32. package/dist/pi-transcript-reader.d.ts +1 -0
  33. package/dist/status.js +22 -2
  34. package/dist/storage.d.ts +7 -1
  35. package/dist/storage.js +28 -7
  36. package/dist/transcript-reader.d.ts +19 -0
  37. package/dist/transcript-reader.js +17 -3
  38. package/dist/types.d.ts +10 -0
  39. package/dist/uninstall.js +31 -1
  40. package/hermes-plugin/hicortex/README.md +77 -0
  41. package/hermes-plugin/hicortex/__init__.py +17 -0
  42. package/hermes-plugin/hicortex/client.py +162 -0
  43. package/hermes-plugin/hicortex/config.py +105 -0
  44. package/hermes-plugin/hicortex/plugin.yaml +12 -0
  45. package/hermes-plugin/hicortex/provider.py +432 -0
  46. package/openclaw.plugin.json +17 -44
  47. package/package.json +7 -5
  48. package/dist/pro-loader.d.ts +0 -33
  49. package/dist/pro-loader.js +0 -187
package/dist/index.js CHANGED
@@ -1,238 +1,184 @@
1
1
  "use strict";
2
2
  /**
3
- * Hicortex OpenClaw Plugin — Long-term Memory That Learns.
3
+ * Hicortex OpenClaw Plugin — Long-term Memory That Learns. (0.10.0)
4
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.
5
+ * Thin-client model: the plugin requires a Hicortex server (co-located at
6
+ * http://127.0.0.1:8787 by default, or a remote URL via `serverUrl` config).
7
+ * No local database, no local LLM, no embedder, no consolidation scheduler.
8
+ *
9
+ * Install once: `openclaw plugins install @gamaze/hicortex`
10
+ * Run server: `npx @gamaze/hicortex init`
11
+ *
12
+ * Responsibilities (recall-only adapter, like the Hermes plugin):
13
+ * - before_agent_start → GET /lessons (fail-soft, 3s timeout) → inject context
14
+ * - Tools → HTTP proxies to /search, /context, /ingest, /lessons
15
+ *
16
+ * CAPTURE IS NOT THIS PLUGIN'S JOB. OpenClaw persists sessions at
17
+ * ~/.openclaw/agents/<agentId>/sessions/*.jsonl in the Pi v3 format; the
18
+ * machine's Hicortex nightly reads them via oc-transcript-reader.ts —
19
+ * canonical nightly-from-logs, same as CC JSONL and Hermes state.db.
8
20
  */
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
21
  Object.defineProperty(exports, "__esModule", { value: true });
43
- const node_path_1 = require("node:path");
44
- const db_js_1 = require("./db.js");
45
22
  const features_js_1 = require("./features.js");
46
23
  const extensions_js_1 = require("./extensions.js");
47
24
  const state_js_1 = require("./state.js");
48
- const llm_js_1 = require("./llm.js");
49
25
  const node_fs_1 = require("node:fs");
26
+ const node_path_1 = require("node:path");
50
27
  const node_os_1 = require("node:os");
51
- const embedder_js_1 = require("./embedder.js");
52
- const storage = __importStar(require("./storage.js"));
53
- const seed_lesson_js_1 = require("./seed-lesson.js");
54
- const retrieval = __importStar(require("./retrieval.js"));
55
- const distiller_js_1 = require("./distiller.js");
56
- const consolidate_js_1 = require("./consolidate.js");
28
+ // ---------------------------------------------------------------------------
29
+ // Constants
30
+ // ---------------------------------------------------------------------------
31
+ const DEFAULT_SERVER_URL = "http://127.0.0.1:8787";
32
+ const LESSONS_TIMEOUT_MS = 3000;
33
+ const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
57
34
  // ---------------------------------------------------------------------------
58
35
  // Module state — initialized in registerService.start()
59
36
  // ---------------------------------------------------------------------------
60
- let db = null;
61
- let llm = null;
62
- let cancelConsolidation = null;
63
- let stateDir = "";
37
+ let serverUrl = DEFAULT_SERVER_URL;
38
+ let authToken;
39
+ let hicortexHome = HICORTEX_HOME;
40
+ // ---------------------------------------------------------------------------
41
+ // HTTP helpers
42
+ // ---------------------------------------------------------------------------
43
+ function authHeaders() {
44
+ return authToken ? { Authorization: `Bearer ${authToken}` } : {};
45
+ }
46
+ async function serverGet(path, timeoutMs) {
47
+ try {
48
+ const resp = await fetch(`${serverUrl}${path}`, {
49
+ headers: authHeaders(),
50
+ signal: AbortSignal.timeout(timeoutMs),
51
+ });
52
+ if (!resp.ok)
53
+ return null;
54
+ return await resp.json();
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ async function serverPost(path, body, timeoutMs) {
61
+ try {
62
+ const resp = await fetch(`${serverUrl}${path}`, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json", ...authHeaders() },
65
+ body: JSON.stringify(body),
66
+ signal: AbortSignal.timeout(timeoutMs),
67
+ });
68
+ let data = null;
69
+ try {
70
+ data = await resp.json();
71
+ }
72
+ catch { /* non-JSON body */ }
73
+ return { ok: resp.ok, status: resp.status, data };
74
+ }
75
+ catch (err) {
76
+ return { ok: false, status: 0, data: null };
77
+ }
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // Tool result formatter
81
+ // ---------------------------------------------------------------------------
82
+ function formatToolResults(results) {
83
+ if (results.length === 0) {
84
+ return { content: [{ type: "text", text: "No memories found." }] };
85
+ }
86
+ const text = results
87
+ .map((r) => `[${r.memory_type}] (score: ${r.score.toFixed(3)}, strength: ${r.effective_strength.toFixed(3)}) ${r.content.slice(0, 500)}`)
88
+ .join("\n\n");
89
+ return { content: [{ type: "text", text }] };
90
+ }
64
91
  // ---------------------------------------------------------------------------
65
92
  // Plugin export
66
93
  // ---------------------------------------------------------------------------
67
94
  exports.default = {
68
95
  id: "hicortex",
69
- name: "Hicortex \u2014 Long-term Memory That Learns",
96
+ name: "Hicortex Long-term Memory That Learns",
70
97
  kind: "lifecycle",
71
98
  register(api) {
72
99
  // -----------------------------------------------------------------------
73
- // Background service: init DB, embedder, LLM, consolidation timer
100
+ // Background service: resolve config, verify server, init features
74
101
  // -----------------------------------------------------------------------
75
102
  api.registerService({
76
103
  id: "hicortex-service",
77
104
  async start(ctx) {
78
105
  const config = (ctx.config ?? {});
79
- stateDir = ctx.stateDir ?? (0, node_path_1.join)(process.env.HOME ?? "~", ".hicortex");
80
106
  const log = ctx.logger
81
107
  ? (msg) => ctx.logger.info(msg)
82
108
  : console.log;
83
- // Resolve database path (handles migration from legacy OC location)
84
- const dbPath = (0, db_js_1.resolveDbPath)(config.dbPath);
85
- log(`[hicortex] Initializing database at ${dbPath}`);
86
- db = (0, db_js_1.initDb)(dbPath);
87
- // Auto-configure LLM: resolve → test → persist
88
- const llmConfig = await autoConfigureLlm(config, log);
89
- llm = new llm_js_1.LlmClient(llmConfig);
90
- log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model} ` +
91
- `(reflect: ${llmConfig.reflectModel})`);
92
- // One-time migration of legacy state files (no-op if state.json exists)
93
- (0, state_js_1.migrateLegacyState)(stateDir);
94
- // License: init feature cache (sync after this returns)
95
- await (0, features_js_1.initFeatures)(config.licenseKey, stateDir);
96
- // Schedule nightly consolidation
97
- const consolidateHour = config.consolidateHour ?? 2;
98
- cancelConsolidation = (0, consolidate_js_1.scheduleConsolidation)(db, llm, embedder_js_1.embed, consolidateHour);
99
- // Seed the bootstrap lesson on first run
100
- await (0, seed_lesson_js_1.injectSeedLesson)(db, log);
101
- // Auto-add tools to tools.allow if using a restrictive profile
109
+ // Resolve server URL and auth token from plugin config
110
+ serverUrl = (config.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
111
+ authToken = config.authToken;
112
+ // Use stateDir from context so tests can redirect state writes
113
+ hicortexHome = ctx.stateDir ?? HICORTEX_HOME;
114
+ log(`[hicortex] Thin-client mode server: ${serverUrl}`);
115
+ // License: init feature cache (only needs licenseKey, no DB access)
116
+ await (0, features_js_1.initFeatures)(config.licenseKey, hicortexHome);
117
+ // Verify server reachability at startup (non-fatal — warn only)
118
+ try {
119
+ const resp = await fetch(`${serverUrl}/health`, {
120
+ signal: AbortSignal.timeout(5000),
121
+ });
122
+ if (resp.ok) {
123
+ const data = await resp.json();
124
+ log(`[hicortex] Server OK: v${data.version}, ${data.memories} memories`);
125
+ }
126
+ else {
127
+ log(`[hicortex] Server returned HTTP ${resp.status} capture and tools may fail`);
128
+ }
129
+ }
130
+ catch (err) {
131
+ const msg = err instanceof Error ? err.message : String(err);
132
+ log(`[hicortex] WARNING: Server unreachable at ${serverUrl}: ${msg}. ` +
133
+ `Run \`npx @gamaze/hicortex init\` to start the server. ` +
134
+ `Capture and tool calls will fail until the server is available.`);
135
+ }
102
136
  ensureToolsAllowed(log);
103
- // Log stats
104
- const stats = (0, db_js_1.getStats)(db, dbPath);
105
- log(`[hicortex] Ready: ${stats.memories} memories, ${stats.links} links, ` +
106
- `${Math.round(stats.db_size_bytes / 1024)} KB`);
107
137
  },
108
138
  async stop() {
109
- if (cancelConsolidation) {
110
- cancelConsolidation();
111
- cancelConsolidation = null;
112
- }
113
- if (db) {
114
- db.close();
115
- db = null;
116
- }
117
- llm = null;
139
+ // Nothing to clean up — no DB, no LLM, no timer
118
140
  },
119
141
  });
120
142
  // -----------------------------------------------------------------------
121
- // Hook: before_agent_start — inject lessons into agent context
143
+ // Hook: before_agent_start — fetch lessons from server (fail-soft)
122
144
  // -----------------------------------------------------------------------
123
- api.on("before_agent_start", async (event, ctx) => {
124
- if (!db)
125
- return {};
145
+ api.on("before_agent_start", async (_event, ctx) => {
126
146
  try {
127
- const lessons = storage.getLessons(db, 7, ctx.project);
128
- if (lessons.length === 0)
147
+ // One fetch build context and check cap from the same response.
148
+ const data = await serverGet("/lessons", LESSONS_TIMEOUT_MS);
149
+ if (!data || !data.lessons || data.lessons.length === 0)
129
150
  return {};
130
151
  const maxLessons = (0, features_js_1.lessonsLimit)();
131
- const selected = await (0, extensions_js_1.getLessonSelector)().select(lessons, {
152
+ const state = (0, state_js_1.loadState)(hicortexHome);
153
+ const moduleIndex = data.moduleIndex ?? state.moduleIndex;
154
+ const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, {
132
155
  maxLessons,
133
156
  project: ctx.project,
134
- agentId: ctx.agentId,
157
+ moduleIndex,
135
158
  });
159
+ if (selected.length === 0)
160
+ return {};
136
161
  const formatted = selected.map((l) => {
137
- // Extract just the lesson text from the structured content
138
- const match = l.content.match(/## Lesson: (.+)/);
139
- return match ? `- ${match[1]}` : `- ${l.content.slice(0, 200)}`;
162
+ const titleMatch = l.content.match(/## Lesson: (.+)/);
163
+ const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
164
+ const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
165
+ const title = titleMatch ? titleMatch[1] : l.content.slice(0, 150);
166
+ const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
167
+ return `- ${title}${meta ? ` (${meta})` : ""}`;
140
168
  });
141
- let context = `\n\n## Hicortex Lessons (auto-injected from long-term memory)\n` +
169
+ const context = `\n\n## Hicortex Lessons (auto-injected from long-term memory)\n` +
142
170
  `These are actionable lessons learned from past sessions:\n\n` +
143
171
  formatted.join("\n") +
144
172
  "\n";
145
- // Daily friendly reminder when at memory cap
146
- const memCount = storage.countMemories(db);
147
- if ((0, features_js_1.memoryCapReached)(memCount)) {
148
- context +=
149
- `\n---\nHicortex free tier: ${(0, features_js_1.maxMemoriesAllowed)()} memories stored. ` +
150
- `New memories can no longer be saved, and your agent has stopped learning and self-improving from new sessions. ` +
151
- `Existing memories, lessons, and search still work. ` +
152
- `Upgrade for unlimited usage: https://hicortex.gamaze.com/ ` +
153
- `— after purchase, tell me your key and I'll activate it for you.\n`;
154
- }
155
173
  return { appendSystemContext: context };
156
174
  }
157
175
  catch {
176
+ // Fail-soft — a broken lessons fetch must not block the agent
158
177
  return {};
159
178
  }
160
179
  });
161
180
  // -----------------------------------------------------------------------
162
- // Hook: agent_end distill conversation into memories
163
- // -----------------------------------------------------------------------
164
- api.on("agent_end", async (event, ctx) => {
165
- if (!db || !llm)
166
- return;
167
- if (!event.success || event.messages.length < 4)
168
- return;
169
- try {
170
- const transcript = (0, distiller_js_1.extractConversationText)(event.messages);
171
- if (transcript.length < 200)
172
- return;
173
- const date = new Date().toISOString().slice(0, 10);
174
- const projectName = ctx.project ?? "unknown";
175
- const sourceAgent = `openclaw/${ctx.agentId ?? "unknown"}`;
176
- const entries = await (0, distiller_js_1.distillSession)(llm, transcript, projectName, date);
177
- if (entries.length === 0)
178
- return;
179
- // Check license cap
180
- if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
181
- console.warn(`[hicortex] Free tier limit reached (${(0, features_js_1.maxMemoriesAllowed)()} memories). ` +
182
- `Search and lessons still work, but new memories won't be saved. ` +
183
- `Upgrade for unlimited usage: https://hicortex.gamaze.com/`);
184
- return;
185
- }
186
- // Embed and ingest each entry
187
- for (const entry of entries) {
188
- try {
189
- const embedding = await (0, embedder_js_1.embed)(entry);
190
- storage.insertMemory(db, entry, embedding, {
191
- sourceAgent,
192
- sourceSession: ctx.sessionKey,
193
- project: projectName,
194
- privacy: "WORK",
195
- memoryType: "episode",
196
- });
197
- }
198
- catch (err) {
199
- const msg = err instanceof Error ? err.message : String(err);
200
- console.error(`[hicortex] Failed to ingest entry: ${msg}`);
201
- }
202
- }
203
- }
204
- catch {
205
- // Non-fatal — session capture failing shouldn't break the agent
206
- }
207
- });
208
- // -----------------------------------------------------------------------
209
- // Hook: session_end — check if consolidation is overdue
210
- // -----------------------------------------------------------------------
211
- api.on("session_end", async (event, _ctx) => {
212
- if (!db || !llm || event.messageCount < 4)
213
- return;
214
- // Opportunistic consolidation if no nightly run in 48h
215
- try {
216
- const { readFileSync: readFs } = await import("node:fs");
217
- const { join: joinPath } = await import("node:path");
218
- const { homedir: homeDir } = await import("node:os");
219
- const lastPath = joinPath(homeDir(), ".hicortex", "last-consolidated.txt");
220
- const lastTs = readFs(lastPath, "utf-8").trim();
221
- const lastDate = new Date(lastTs);
222
- const hoursSince = (Date.now() - lastDate.getTime()) / (1000 * 60 * 60);
223
- if (hoursSince > 48) {
224
- console.log("[hicortex] Consolidation overdue — triggering now");
225
- (0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed).catch((err) => {
226
- console.error("[hicortex] Opportunistic consolidation failed:", err);
227
- });
228
- }
229
- }
230
- catch {
231
- // No timestamp file — first run, consolidation will happen on schedule
232
- }
233
- });
234
- // -----------------------------------------------------------------------
235
- // Tools — registered as factory functions per OC plugin API
181
+ // ToolsHTTP proxies to server REST API
236
182
  // -----------------------------------------------------------------------
237
183
  api.registerTool((_ctx) => ({
238
184
  name: "hicortex_search",
@@ -247,14 +193,16 @@ exports.default = {
247
193
  required: ["query"],
248
194
  },
249
195
  async execute(_callId, args, _ctx) {
250
- if (!db)
251
- return { error: "Hicortex not initialized" };
252
196
  try {
253
- const results = await retrieval.retrieve(db, embedder_js_1.embed, args.query, {
254
- limit: args.limit,
255
- project: args.project,
256
- });
257
- return formatToolResults(results);
197
+ const params = new URLSearchParams({ query: args.query });
198
+ if (args.limit)
199
+ params.set("limit", String(args.limit));
200
+ if (args.project)
201
+ params.set("project", args.project);
202
+ const data = await serverGet(`/search?${params}`, 10000);
203
+ if (!data)
204
+ return { error: "Search failed: server unreachable" };
205
+ return formatToolResults(data.results ?? []);
258
206
  }
259
207
  catch (err) {
260
208
  return { error: `Search failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -271,15 +219,18 @@ exports.default = {
271
219
  limit: { type: "number", description: "Max results (default 10)" },
272
220
  },
273
221
  },
274
- execute(_callId, args, _ctx) {
275
- if (!db)
276
- return { error: "Hicortex not initialized" };
222
+ async execute(_callId, args, _ctx) {
277
223
  try {
278
- const results = retrieval.searchContext(db, {
279
- project: args?.project,
280
- limit: args?.limit,
281
- });
282
- return formatToolResults(results);
224
+ const params = new URLSearchParams();
225
+ if (args?.project)
226
+ params.set("project", args.project);
227
+ if (args?.limit)
228
+ params.set("limit", String(args.limit));
229
+ const qs = params.toString();
230
+ const data = await serverGet(`/context${qs ? `?${qs}` : ""}`, 10000);
231
+ if (!data)
232
+ return { error: "Context search failed: server unreachable" };
233
+ return formatToolResults(data.results ?? []);
283
234
  }
284
235
  catch (err) {
285
236
  return { error: `Context search failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -303,27 +254,18 @@ exports.default = {
303
254
  required: ["content"],
304
255
  },
305
256
  async execute(_callId, args, context) {
306
- if (!db)
307
- return { error: "Hicortex not initialized" };
308
- if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
309
- return {
310
- content: [{
311
- type: "text",
312
- text: `Free tier limit reached (${(0, features_js_1.maxMemoriesAllowed)()} memories). ` +
313
- `Your existing memories and lessons still work — search and recall are unaffected. ` +
314
- `New memories won't be saved until you upgrade.\n\n` +
315
- `Upgrade for unlimited usage: https://hicortex.gamaze.com/`
316
- }],
317
- };
318
- }
319
257
  try {
320
- const embedding = await (0, embedder_js_1.embed)(args.content);
321
- const id = storage.insertMemory(db, args.content, embedding, {
322
- sourceAgent: `openclaw/${context?.agentId ?? "manual"}`,
258
+ const result = await serverPost("/ingest", {
259
+ content: args.content,
260
+ source_agent: `openclaw/${context?.agentId ?? "manual"}`,
323
261
  project: args.project,
324
- memoryType: args.memory_type ?? "episode",
262
+ memory_type: args.memory_type ?? "episode",
325
263
  privacy: "WORK",
326
- });
264
+ }, 15000);
265
+ if (!result.ok) {
266
+ return { error: `Ingest failed: ${result.data?.error ?? `HTTP ${result.status}`}` };
267
+ }
268
+ const id = result.data?.id ?? "unknown";
327
269
  return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})` }] };
328
270
  }
329
271
  catch (err) {
@@ -337,17 +279,17 @@ exports.default = {
337
279
  parameters: {
338
280
  type: "object",
339
281
  properties: {
340
- days: { type: "number", description: "Look back N days (default 7)" },
341
- project: { type: "string", description: "Filter by project name" },
282
+ project: { type: "string", description: "Filter by project name (optional)" },
342
283
  },
343
284
  },
344
- execute(_callId, args, _ctx) {
345
- if (!db)
346
- return { error: "Hicortex not initialized" };
285
+ async execute(_callId, args, _ctx) {
347
286
  try {
348
- const lessons = storage.getLessons(db, args.days ?? 7, args.project);
287
+ const data = await serverGet("/lessons", LESSONS_TIMEOUT_MS);
288
+ if (!data)
289
+ return { error: "Lessons fetch failed: server unreachable" };
290
+ const lessons = data.lessons ?? [];
349
291
  if (lessons.length === 0) {
350
- return { content: [{ type: "text", text: "No lessons found for the specified period." }] };
292
+ return { content: [{ type: "text", text: "No lessons found." }] };
351
293
  }
352
294
  const text = lessons.map((l) => `- ${l.content.slice(0, 500)}`).join("\n");
353
295
  return { content: [{ type: "text", text }] };
@@ -357,6 +299,67 @@ exports.default = {
357
299
  }
358
300
  },
359
301
  }), { name: "hicortex_lessons" });
302
+ api.registerTool((_ctx) => ({
303
+ name: "hicortex_index",
304
+ description: "Get the knowledge domain index — shows what topics and projects are stored in memory, grouped by domain.",
305
+ parameters: {
306
+ type: "object",
307
+ properties: {},
308
+ },
309
+ async execute(_callId, _args, _ctx) {
310
+ try {
311
+ const data = await serverGet("/index", 10000);
312
+ if (!data)
313
+ return { error: "Index fetch failed: server unreachable" };
314
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
315
+ }
316
+ catch (err) {
317
+ return { error: `Index fetch failed: ${err instanceof Error ? err.message : String(err)}` };
318
+ }
319
+ },
320
+ }), { name: "hicortex_index" });
321
+ api.registerTool((_ctx) => ({
322
+ name: "hicortex_graph",
323
+ description: "Query the memory knowledge graph — find connected memories, hub nodes, or paths between memories.",
324
+ parameters: {
325
+ type: "object",
326
+ properties: {
327
+ operation: {
328
+ type: "string",
329
+ enum: ["neighbors", "hubs", "path"],
330
+ description: "Graph operation to perform",
331
+ },
332
+ id: { type: "string", description: "Memory ID (required for neighbors and path operations)" },
333
+ target_id: { type: "string", description: "Target memory ID (required for path operation)" },
334
+ limit: { type: "number", description: "Max results (default 10)" },
335
+ domain: { type: "string", description: "Filter hubs by domain" },
336
+ relationship: { type: "string", description: "Filter neighbors by relationship type (e.g., CONTRADICTS, SUPERSEDES, derives)" },
337
+ },
338
+ required: ["operation"],
339
+ },
340
+ async execute(_callId, args, _ctx) {
341
+ try {
342
+ const params = new URLSearchParams({ op: args.operation });
343
+ if (args.id)
344
+ params.set("id", args.id);
345
+ if (args.target_id)
346
+ params.set("target_id", args.target_id);
347
+ if (args.limit)
348
+ params.set("limit", String(args.limit));
349
+ if (args.domain)
350
+ params.set("domain", args.domain);
351
+ if (args.relationship)
352
+ params.set("relationship", args.relationship);
353
+ const data = await serverGet(`/graph?${params}`, 10000);
354
+ if (!data)
355
+ return { error: "Graph query failed: server unreachable" };
356
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
357
+ }
358
+ catch (err) {
359
+ return { error: `Graph query failed: ${err instanceof Error ? err.message : String(err)}` };
360
+ }
361
+ },
362
+ }), { name: "hicortex_graph" });
360
363
  api.registerTool((_ctx) => ({
361
364
  name: "hicortex_update",
362
365
  description: "Update an existing memory. Use after searching to fix incorrect information. If content changes, the embedding is re-computed.",
@@ -366,33 +369,30 @@ exports.default = {
366
369
  id: { type: "string", description: "Memory ID (from search results, first 8 chars or full UUID)" },
367
370
  content: { type: "string", description: "New content text" },
368
371
  project: { type: "string", description: "New project name" },
369
- memory_type: { type: "string", enum: ["episode", "lesson", "fact", "decision"], description: "New memory type" },
372
+ memory_type: {
373
+ type: "string",
374
+ enum: ["episode", "lesson", "fact", "decision"],
375
+ description: "New memory type",
376
+ },
370
377
  },
371
378
  required: ["id"],
372
379
  },
373
380
  async execute(_callId, args, _ctx) {
374
- if (!db)
375
- return { error: "Hicortex not initialized" };
376
381
  try {
377
- const fullId = resolveMemoryId(db, args.id);
378
- if (!fullId)
382
+ const result = await serverPost("/update", {
383
+ id: args.id,
384
+ content: args.content,
385
+ project: args.project,
386
+ memory_type: args.memory_type,
387
+ }, 15000);
388
+ if (result.status === 404) {
379
389
  return { error: `Memory not found: ${args.id}` };
380
- const fields = {};
381
- if (args.content !== undefined)
382
- fields.content = args.content;
383
- if (args.project !== undefined)
384
- fields.project = args.project;
385
- if (args.memory_type !== undefined)
386
- fields.memory_type = args.memory_type;
387
- if (Object.keys(fields).length === 0)
388
- return { error: "No fields to update" };
389
- storage.updateMemory(db, fullId, fields);
390
- if (args.content !== undefined) {
391
- const embedding = await (0, embedder_js_1.embed)(args.content);
392
- db.prepare("DELETE FROM memory_vectors WHERE id = ?").run(fullId);
393
- db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)").run(fullId, Buffer.from(embedding.buffer));
394
390
  }
395
- return { content: [{ type: "text", text: `Memory updated (id: ${fullId.slice(0, 8)})` }] };
391
+ if (!result.ok) {
392
+ return { error: `Update failed: ${result.data?.error ?? `HTTP ${result.status}`}` };
393
+ }
394
+ const id = result.data?.id ?? args.id;
395
+ return { content: [{ type: "text", text: `Memory updated (id: ${String(id).slice(0, 8)})` }] };
396
396
  }
397
397
  catch (err) {
398
398
  return { error: `Update failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -409,15 +409,16 @@ exports.default = {
409
409
  },
410
410
  required: ["id"],
411
411
  },
412
- execute(_callId, args, _ctx) {
413
- if (!db)
414
- return { error: "Hicortex not initialized" };
412
+ async execute(_callId, args, _ctx) {
415
413
  try {
416
- const fullId = resolveMemoryId(db, args.id);
417
- if (!fullId)
414
+ const result = await serverPost("/delete", { id: args.id }, 15000);
415
+ if (result.status === 404) {
418
416
  return { error: `Memory not found: ${args.id}` };
419
- storage.deleteMemory(db, fullId);
420
- return { content: [{ type: "text", text: `Memory deleted (id: ${fullId.slice(0, 8)})` }] };
417
+ }
418
+ if (!result.ok) {
419
+ return { error: `Delete failed: ${result.data?.error ?? `HTTP ${result.status}`}` };
420
+ }
421
+ return { content: [{ type: "text", text: `Memory deleted (id: ${String(args.id).slice(0, 8)})` }] };
421
422
  }
422
423
  catch (err) {
423
424
  return { error: `Delete failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -429,95 +430,13 @@ exports.default = {
429
430
  // ---------------------------------------------------------------------------
430
431
  // Helpers
431
432
  // ---------------------------------------------------------------------------
432
- function resolveMemoryId(database, idPrefix) {
433
- if (idPrefix.length >= 36) {
434
- const row = database.prepare("SELECT id FROM memories WHERE id = ?").get(idPrefix);
435
- return row?.id ?? null;
436
- }
437
- const rows = database.prepare("SELECT id FROM memories WHERE id LIKE ?").all(`${idPrefix}%`);
438
- if (rows.length === 1)
439
- return rows[0].id;
440
- return null;
441
- }
442
- // ---------------------------------------------------------------------------
443
- // Auto-configure LLM: resolve config → test connection → persist if new
444
- // ---------------------------------------------------------------------------
445
- async function autoConfigureLlm(pluginConfig, log) {
446
- // Step 1: Resolve LLM config from all sources
447
- const llmConfig = (0, llm_js_1.resolveLlmConfig)({
448
- llmBaseUrl: pluginConfig.llmBaseUrl,
449
- llmApiKey: pluginConfig.llmApiKey,
450
- llmModel: pluginConfig.llmModel,
451
- reflectModel: pluginConfig.reflectModel,
452
- });
453
- // Step 2: Test the connection
454
- log(`[hicortex] Testing LLM connection: ${llmConfig.provider}/${llmConfig.model} @ ${llmConfig.baseUrl}`);
455
- const testClient = new llm_js_1.LlmClient(llmConfig);
456
- try {
457
- const response = await testClient.completeFast("Respond with just the word OK", 10);
458
- if (response && response.length > 0) {
459
- log(`[hicortex] LLM connection verified`);
460
- // Step 3: Persist to OC config so future startups skip detection
461
- persistProviderConfig(llmConfig, log);
462
- return llmConfig;
463
- }
464
- }
465
- catch (err) {
466
- const msg = err instanceof Error ? err.message : String(err);
467
- log(`[hicortex] LLM test failed (${llmConfig.baseUrl}): ${msg}`);
468
- }
469
- // Step 4: Fall back — return the config anyway, log instructions
470
- log(`[hicortex] WARNING: Could not verify LLM connection. ` +
471
- `Distillation and consolidation may fail. ` +
472
- `To fix: add models.providers.${llmConfig.provider}.baseUrl to ~/.openclaw/openclaw.json ` +
473
- `or set llmBaseUrl in the hicortex plugin config.`);
474
- return llmConfig;
475
- }
476
- /**
477
- * Persist verified provider config to openclaw.json so future startups
478
- * skip detection and go straight to the verified URL.
479
- */
480
- function persistProviderConfig(llmConfig, log) {
481
- try {
482
- const configPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
483
- const raw = (0, node_fs_1.readFileSync)(configPath, "utf-8");
484
- const config = JSON.parse(raw);
485
- // Check if baseUrl already stored for this provider
486
- const existing = config?.models?.providers?.[llmConfig.provider]?.baseUrl;
487
- if (existing === llmConfig.baseUrl)
488
- return; // Already persisted
489
- // Write the provider config
490
- if (!config.models)
491
- config.models = {};
492
- if (!config.models.providers)
493
- config.models.providers = {};
494
- if (!config.models.providers[llmConfig.provider]) {
495
- config.models.providers[llmConfig.provider] = {};
496
- }
497
- const prov = config.models.providers[llmConfig.provider];
498
- prov.baseUrl = llmConfig.baseUrl;
499
- // OC requires a models array — preserve existing or add the active model
500
- if (!prov.models || !Array.isArray(prov.models)) {
501
- prov.models = [{
502
- id: llmConfig.model,
503
- name: llmConfig.model,
504
- input: ["text"],
505
- contextWindow: 128000,
506
- maxTokens: 8192,
507
- }];
508
- }
509
- (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2));
510
- log(`[hicortex] Persisted LLM config: ${llmConfig.provider} → ${llmConfig.baseUrl}`);
511
- }
512
- catch {
513
- // Non-fatal — config works in memory even if we can't persist
514
- }
515
- }
516
433
  const HICORTEX_TOOLS = [
517
434
  "hicortex_search",
518
435
  "hicortex_context",
519
436
  "hicortex_ingest",
520
437
  "hicortex_lessons",
438
+ "hicortex_index",
439
+ "hicortex_graph",
521
440
  "hicortex_update",
522
441
  "hicortex_delete",
523
442
  ];
@@ -542,17 +461,6 @@ function ensureToolsAllowed(log) {
542
461
  log(`[hicortex] Added tools to allow list: ${missing.join(", ")}`);
543
462
  }
544
463
  catch {
545
- // Non-fatal
464
+ // Non-fatal — openclaw.json may not exist in test environments
546
465
  }
547
466
  }
548
- function formatToolResults(results) {
549
- if (results.length === 0) {
550
- return {
551
- content: [{ type: "text", text: "No memories found." }],
552
- };
553
- }
554
- const text = results
555
- .map((r) => `[${r.memory_type}] (score: ${r.score.toFixed(3)}, strength: ${r.effective_strength.toFixed(3)}) ${r.content.slice(0, 500)}`)
556
- .join("\n\n");
557
- return { content: [{ type: "text", text }] };
558
- }