@ubundi/openclaw-cortex 2.0.0 → 2.0.2

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 (136) hide show
  1. package/dist/cortex/client.d.ts.map +1 -0
  2. package/dist/cortex/client.js.map +1 -0
  3. package/dist/features/capture/filter.d.ts +1 -1
  4. package/dist/features/capture/filter.d.ts.map +1 -1
  5. package/dist/features/capture/handler.d.ts +4 -4
  6. package/dist/features/capture/handler.d.ts.map +1 -1
  7. package/dist/features/capture/index.d.ts +3 -0
  8. package/dist/features/capture/index.d.ts.map +1 -0
  9. package/dist/features/capture/index.js +3 -0
  10. package/dist/features/capture/index.js.map +1 -0
  11. package/dist/features/checkpoint/handler.d.ts +3 -3
  12. package/dist/features/checkpoint/handler.d.ts.map +1 -1
  13. package/dist/features/heartbeat/handler.d.ts +2 -2
  14. package/dist/features/heartbeat/handler.d.ts.map +1 -1
  15. package/dist/features/index.d.ts +7 -0
  16. package/dist/features/index.d.ts.map +1 -0
  17. package/dist/features/index.js +7 -0
  18. package/dist/features/index.js.map +1 -0
  19. package/dist/features/recall/context-profile.d.ts +1 -1
  20. package/dist/features/recall/context-profile.d.ts.map +1 -1
  21. package/dist/features/recall/formatter.d.ts +1 -1
  22. package/dist/features/recall/formatter.d.ts.map +1 -1
  23. package/dist/features/recall/handler.d.ts +4 -4
  24. package/dist/features/recall/handler.d.ts.map +1 -1
  25. package/dist/features/recall/handler.js +1 -1
  26. package/dist/features/recall/handler.js.map +1 -1
  27. package/dist/features/recall/index.d.ts +4 -0
  28. package/dist/features/recall/index.d.ts.map +1 -0
  29. package/dist/features/recall/index.js +4 -0
  30. package/dist/features/recall/index.js.map +1 -0
  31. package/dist/features/sync/daily-logs-sync.d.ts +3 -3
  32. package/dist/features/sync/daily-logs-sync.d.ts.map +1 -1
  33. package/dist/features/sync/daily-logs-sync.js +1 -1
  34. package/dist/features/sync/daily-logs-sync.js.map +1 -1
  35. package/dist/features/sync/index.d.ts +5 -0
  36. package/dist/features/sync/index.d.ts.map +1 -0
  37. package/dist/features/sync/index.js +5 -0
  38. package/dist/features/sync/index.js.map +1 -0
  39. package/dist/features/sync/memory-md-sync.d.ts +3 -3
  40. package/dist/features/sync/memory-md-sync.d.ts.map +1 -1
  41. package/dist/features/sync/memory-md-sync.js +1 -1
  42. package/dist/features/sync/memory-md-sync.js.map +1 -1
  43. package/dist/features/sync/transcripts-sync.d.ts +3 -3
  44. package/dist/features/sync/transcripts-sync.d.ts.map +1 -1
  45. package/dist/features/sync/transcripts-sync.js +2 -2
  46. package/dist/features/sync/transcripts-sync.js.map +1 -1
  47. package/dist/features/sync/watcher.d.ts +3 -3
  48. package/dist/features/sync/watcher.d.ts.map +1 -1
  49. package/dist/index.d.ts +5 -5
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +5 -5
  52. package/dist/index.js.map +1 -1
  53. package/dist/internal/api-key.d.ts.map +1 -0
  54. package/dist/internal/api-key.js.map +1 -0
  55. package/dist/internal/audit-logger.d.ts.map +1 -0
  56. package/dist/internal/audit-logger.js.map +1 -0
  57. package/dist/internal/{transcript/cleaner.d.ts → cleaner.d.ts} +1 -1
  58. package/dist/internal/cleaner.d.ts.map +1 -0
  59. package/dist/internal/cleaner.js.map +1 -0
  60. package/dist/internal/index.d.ts +11 -0
  61. package/dist/internal/index.d.ts.map +1 -0
  62. package/dist/internal/index.js +11 -0
  63. package/dist/internal/index.js.map +1 -0
  64. package/dist/internal/latency-metrics.d.ts.map +1 -0
  65. package/dist/internal/latency-metrics.js.map +1 -0
  66. package/dist/internal/retry-queue.d.ts.map +1 -0
  67. package/dist/internal/retry-queue.js.map +1 -0
  68. package/dist/internal/safe-path.d.ts.map +1 -0
  69. package/dist/internal/safe-path.js.map +1 -0
  70. package/dist/internal/session-state.d.ts.map +1 -0
  71. package/dist/internal/session-state.js.map +1 -0
  72. package/dist/internal/user-id.d.ts.map +1 -0
  73. package/dist/internal/user-id.js.map +1 -0
  74. package/dist/plugin/cli.d.ts +33 -0
  75. package/dist/plugin/cli.d.ts.map +1 -0
  76. package/dist/plugin/cli.js +278 -0
  77. package/dist/plugin/cli.js.map +1 -0
  78. package/dist/plugin/commands.d.ts +21 -0
  79. package/dist/plugin/commands.d.ts.map +1 -0
  80. package/dist/plugin/commands.js +94 -0
  81. package/dist/plugin/commands.js.map +1 -0
  82. package/dist/plugin/{config/schema.d.ts → config.d.ts} +1 -1
  83. package/dist/plugin/config.d.ts.map +1 -0
  84. package/dist/plugin/{config/schema.js → config.js} +1 -1
  85. package/dist/plugin/config.js.map +1 -0
  86. package/dist/plugin/index.d.ts +1 -87
  87. package/dist/plugin/index.d.ts.map +1 -1
  88. package/dist/plugin/index.js +126 -578
  89. package/dist/plugin/index.js.map +1 -1
  90. package/dist/plugin/tools.d.ts +31 -0
  91. package/dist/plugin/tools.d.ts.map +1 -0
  92. package/dist/plugin/tools.js +230 -0
  93. package/dist/plugin/tools.js.map +1 -0
  94. package/dist/plugin/types.d.ts +83 -0
  95. package/dist/plugin/types.d.ts.map +1 -0
  96. package/dist/plugin/types.js +3 -0
  97. package/dist/plugin/types.js.map +1 -0
  98. package/openclaw.plugin.json +1 -1
  99. package/package.json +1 -1
  100. package/dist/adapters/cortex/client.d.ts.map +0 -1
  101. package/dist/adapters/cortex/client.js.map +0 -1
  102. package/dist/internal/audit/audit-logger.d.ts.map +0 -1
  103. package/dist/internal/audit/audit-logger.js.map +0 -1
  104. package/dist/internal/fs/safe-path.d.ts.map +0 -1
  105. package/dist/internal/fs/safe-path.js.map +0 -1
  106. package/dist/internal/identity/api-key.d.ts.map +0 -1
  107. package/dist/internal/identity/api-key.js.map +0 -1
  108. package/dist/internal/identity/user-id.d.ts.map +0 -1
  109. package/dist/internal/identity/user-id.js.map +0 -1
  110. package/dist/internal/metrics/latency-metrics.d.ts.map +0 -1
  111. package/dist/internal/metrics/latency-metrics.js.map +0 -1
  112. package/dist/internal/queue/retry-queue.d.ts.map +0 -1
  113. package/dist/internal/queue/retry-queue.js.map +0 -1
  114. package/dist/internal/session/session-state.d.ts.map +0 -1
  115. package/dist/internal/session/session-state.js.map +0 -1
  116. package/dist/internal/transcript/cleaner.d.ts.map +0 -1
  117. package/dist/internal/transcript/cleaner.js.map +0 -1
  118. package/dist/plugin/config/schema.d.ts.map +0 -1
  119. package/dist/plugin/config/schema.js.map +0 -1
  120. /package/dist/{adapters/cortex → cortex}/client.d.ts +0 -0
  121. /package/dist/{adapters/cortex → cortex}/client.js +0 -0
  122. /package/dist/internal/{identity/api-key.d.ts → api-key.d.ts} +0 -0
  123. /package/dist/internal/{identity/api-key.js → api-key.js} +0 -0
  124. /package/dist/internal/{audit/audit-logger.d.ts → audit-logger.d.ts} +0 -0
  125. /package/dist/internal/{audit/audit-logger.js → audit-logger.js} +0 -0
  126. /package/dist/internal/{transcript/cleaner.js → cleaner.js} +0 -0
  127. /package/dist/internal/{metrics/latency-metrics.d.ts → latency-metrics.d.ts} +0 -0
  128. /package/dist/internal/{metrics/latency-metrics.js → latency-metrics.js} +0 -0
  129. /package/dist/internal/{queue/retry-queue.d.ts → retry-queue.d.ts} +0 -0
  130. /package/dist/internal/{queue/retry-queue.js → retry-queue.js} +0 -0
  131. /package/dist/internal/{fs/safe-path.d.ts → safe-path.d.ts} +0 -0
  132. /package/dist/internal/{fs/safe-path.js → safe-path.js} +0 -0
  133. /package/dist/internal/{session/session-state.d.ts → session-state.d.ts} +0 -0
  134. /package/dist/internal/{session/session-state.js → session-state.js} +0 -0
  135. /package/dist/internal/{identity/user-id.d.ts → user-id.d.ts} +0 -0
  136. /package/dist/internal/{identity/user-id.js → user-id.js} +0 -0
@@ -1,23 +1,52 @@
1
- import { basename } from "node:path";
1
+ import { basename, join } from "node:path";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
+ import { readFileSync, writeFileSync, statSync } from "node:fs";
4
+ import { homedir } from "node:os";
3
5
  import packageJson from "../../package.json" with { type: "json" };
4
- import { CortexConfigSchema, configSchema } from "./config/schema.js";
5
- import { CortexClient } from "../adapters/cortex/client.js";
6
+ import { CortexConfigSchema, configSchema } from "./config.js";
7
+ import { CortexClient } from "../cortex/client.js";
6
8
  import { createRecallHandler } from "../features/recall/handler.js";
7
9
  import { createCaptureHandler } from "../features/capture/handler.js";
8
10
  import { FileSyncWatcher } from "../features/sync/watcher.js";
9
- import { RetryQueue } from "../internal/queue/retry-queue.js";
10
- import { LatencyMetrics } from "../internal/metrics/latency-metrics.js";
11
- import { loadOrCreateUserId } from "../internal/identity/user-id.js";
12
- import { BAKED_API_KEY } from "../internal/identity/api-key.js";
13
- import { formatMemories } from "../features/recall/formatter.js";
14
- import { AuditLogger } from "../internal/audit/audit-logger.js";
11
+ import { RetryQueue } from "../internal/retry-queue.js";
12
+ import { LatencyMetrics } from "../internal/latency-metrics.js";
13
+ import { loadOrCreateUserId } from "../internal/user-id.js";
14
+ import { BAKED_API_KEY } from "../internal/api-key.js";
15
+ import { AuditLogger } from "../internal/audit-logger.js";
15
16
  import { RecentSaves } from "../internal/dedupe.js";
16
17
  import { injectAgentInstructions } from "../internal/agent-instructions.js";
17
- import { createCheckpointHandler } from "../features/checkpoint/handler.js";
18
18
  import { createHeartbeatHandler } from "../features/heartbeat/handler.js";
19
- import { buildSessionSummaryFromMessages, formatRecoveryContext, SessionStateStore, } from "../internal/session/session-state.js";
19
+ import { buildSessionSummaryFromMessages, formatRecoveryContext, SessionStateStore, } from "../internal/session-state.js";
20
+ import { registerCliCommands } from "./cli.js";
21
+ import { buildSearchMemoryTool, buildSaveMemoryTool } from "./tools.js";
22
+ import { buildCommands } from "./commands.js";
20
23
  const version = packageJson.version;
24
+ const STATS_FILE = join(homedir(), ".openclaw", "cortex-session-stats.json");
25
+ function persistStats(stats) {
26
+ try {
27
+ writeFileSync(STATS_FILE, JSON.stringify({ ...stats, updatedAt: Date.now() }) + "\n", { encoding: "utf-8", mode: 0o600 });
28
+ }
29
+ catch {
30
+ // Best-effort — stats display is non-critical
31
+ }
32
+ }
33
+ function loadPersistedStats() {
34
+ try {
35
+ const raw = JSON.parse(readFileSync(STATS_FILE, "utf-8"));
36
+ return {
37
+ saves: raw.saves ?? 0,
38
+ savesSkippedDedupe: raw.savesSkippedDedupe ?? 0,
39
+ savesSkippedNovelty: raw.savesSkippedNovelty ?? 0,
40
+ searches: raw.searches ?? 0,
41
+ recallCount: raw.recallCount ?? 0,
42
+ recallMemoriesTotal: raw.recallMemoriesTotal ?? 0,
43
+ recallDuplicatesCollapsed: raw.recallDuplicatesCollapsed ?? 0,
44
+ };
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
21
50
  /**
22
51
  * Derives a workspace-scoped namespace from the workspace directory path.
23
52
  * Uses the directory basename plus a short hash of the full path to avoid collisions
@@ -101,6 +130,46 @@ function registerHookCompat(api, hookName, handler, metadata) {
101
130
  api.logger.warn(`Cortex: cannot register hook "${hookName}" — no registerHook or on method available`);
102
131
  }
103
132
  }
133
+ /** Tool names that must survive the tools.profile allowlist filter. */
134
+ const CORTEX_TOOL_NAMES = ["cortex_search_memory", "cortex_save_memory"];
135
+ /**
136
+ * Ensures `tools.alsoAllow` in the OpenClaw config includes our tool names.
137
+ * Without this, profiles like "coding" silently filter out plugin tools —
138
+ * auto-recall/capture hooks still work, but the agent can't explicitly
139
+ * search or save memories.
140
+ *
141
+ * Runs once on first registration; idempotent thereafter.
142
+ */
143
+ function ensureToolsAllowlist(logger) {
144
+ try {
145
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
146
+ const raw = readFileSync(configPath, "utf-8");
147
+ const config = JSON.parse(raw);
148
+ // Only needed when a tools profile is active (which creates a fixed allowlist)
149
+ if (!config.tools?.profile)
150
+ return;
151
+ const existing = Array.isArray(config.tools.alsoAllow)
152
+ ? config.tools.alsoAllow
153
+ : [];
154
+ const missing = CORTEX_TOOL_NAMES.filter((name) => !existing.includes(name));
155
+ if (missing.length === 0)
156
+ return;
157
+ config.tools.alsoAllow = [...existing, ...missing];
158
+ // Preserve original file permissions (e.g. 0o600) to avoid security audit warnings
159
+ let mode;
160
+ try {
161
+ mode = statSync(configPath).mode & 0o777;
162
+ }
163
+ catch { /* ignore */ }
164
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: mode ?? 0o600 });
165
+ logger.info(`Cortex: enabled memory tools for "${config.tools.profile}" profile`);
166
+ }
167
+ catch {
168
+ // Config unreadable — tools may be filtered by the profile allowlist.
169
+ // Hooks (auto-recall, auto-capture) still work regardless.
170
+ logger.warn(`Cortex: could not verify tool access — if the agent cannot use cortex_search_memory or cortex_save_memory, add them to tools.alsoAllow in openclaw.json`);
171
+ }
172
+ }
104
173
  const plugin = {
105
174
  id: "openclaw-cortex",
106
175
  name: "Cortex Memory",
@@ -124,6 +193,8 @@ const plugin = {
124
193
  api.logger.error("Cortex plugin misconfigured: empty API key. Rebuild with BUILD_API_KEY=... npm run build, or install the published package.");
125
194
  return;
126
195
  }
196
+ // Ensure our tools survive the profile allowlist filter (one-time config patch)
197
+ ensureToolsAllowlist(api.logger);
127
198
  const config = parsed.data;
128
199
  const client = new CortexClient(config.baseUrl, BAKED_API_KEY);
129
200
  const retryQueue = new RetryQueue(api.logger);
@@ -184,6 +255,7 @@ const plugin = {
184
255
  sessionStats.recallCount++;
185
256
  sessionStats.recallMemoriesTotal += stats.memoriesReturned;
186
257
  sessionStats.recallDuplicatesCollapsed += stats.collapsedCount;
258
+ persistStats(sessionStats);
187
259
  });
188
260
  // Auto-Recall: inject relevant memories before every agent turn
189
261
  registerHookCompat(api, "before_agent_start", async (event, ctx) => {
@@ -250,316 +322,46 @@ const plugin = {
250
322
  recallMemoriesTotal: 0,
251
323
  recallDuplicatesCollapsed: 0,
252
324
  };
325
+ // Don't eagerly reset the stats file here — the dual-instance runtime
326
+ // means [plugins] would clobber stats that [gateway] already persisted.
327
+ // In-memory counters start at zero and get persisted on first real activity.
253
328
  // --- Agent Tools ---
254
329
  const recentSaves = config.dedupeWindowMinutes > 0
255
330
  ? new RecentSaves(config.dedupeWindowMinutes)
256
331
  : null;
257
332
  if (api.registerTool) {
258
- api.registerTool({
259
- name: "cortex_search_memory",
260
- description: "Search long-term memory for facts, preferences, and past context. Use when you need to recall something the user mentioned before or retrieve stored knowledge. Use 'mode' to narrow results to a specific category.",
261
- parameters: {
262
- type: "object",
263
- properties: {
264
- query: {
265
- type: "string",
266
- description: "Natural language search query for memory retrieval",
267
- },
268
- limit: {
269
- type: "number",
270
- description: "Maximum number of memories to return (1-50)",
271
- default: 10,
272
- },
273
- mode: {
274
- type: "string",
275
- enum: ["all", "decisions", "preferences", "facts", "recent"],
276
- description: "Filter memories by category. 'all' returns everything (default), 'decisions' returns architectural/design choices, 'preferences' returns user likes/settings, 'facts' returns durable knowledge, 'recent' prioritizes recency over relevance.",
277
- },
278
- },
279
- required: ["query"],
280
- },
281
- async execute(_id, params) {
282
- const query = String(params.query ?? "");
283
- const limit = Math.min(Math.max(Number(params.limit) || 10, 1), 50);
284
- const mode = typeof params.mode === "string" ? params.mode : "all";
285
- await userIdReady;
286
- // Augment query based on mode to improve retrieval precision
287
- let effectiveQuery = query;
288
- let queryType = "combined";
289
- switch (mode) {
290
- case "decisions":
291
- effectiveQuery = `[type:decision] ${query}`;
292
- queryType = "factual";
293
- break;
294
- case "preferences":
295
- effectiveQuery = `[type:preference] ${query}`;
296
- queryType = "factual";
297
- break;
298
- case "facts":
299
- effectiveQuery = `[type:fact] ${query}`;
300
- queryType = "factual";
301
- break;
302
- case "recent":
303
- queryType = "combined";
304
- break;
305
- }
306
- api.logger.debug?.(`Cortex search: "${effectiveQuery.slice(0, 80)}" (limit=${limit}, mode=${mode})`);
307
- sessionStats.searches++;
308
- void auditLoggerProxy.log({
309
- feature: "tool-search-memory",
310
- method: "POST",
311
- endpoint: "/v1/recall",
312
- payload: effectiveQuery,
313
- userId,
314
- });
315
- try {
316
- const doRecall = async (attempt = 0) => {
317
- try {
318
- return await client.recall(effectiveQuery, config.toolTimeoutMs, {
319
- limit,
320
- userId: userId,
321
- queryType,
322
- });
323
- }
324
- catch (err) {
325
- if (attempt < 1 && /50[23]/.test(String(err))) {
326
- await new Promise((r) => setTimeout(r, 1500));
327
- return doRecall(attempt + 1);
328
- }
329
- throw err;
330
- }
331
- };
332
- const response = await doRecall();
333
- if (!response.memories?.length) {
334
- return { content: [{ type: "text", text: "No memories found matching that query." }] };
335
- }
336
- api.logger.debug?.(`Cortex search returned ${response.memories.length} memories`);
337
- const formatted = formatMemories(response.memories, config.recallTopK);
338
- return { content: [{ type: "text", text: formatted }] };
339
- }
340
- catch (err) {
341
- api.logger.warn(`Cortex search failed: ${String(err)}`);
342
- return { content: [{ type: "text", text: `Memory search failed: ${String(err)}` }] };
343
- }
344
- },
345
- });
346
- api.registerTool({
347
- name: "cortex_save_memory",
348
- description: "Explicitly save a fact, preference, or piece of information to long-term memory. Use when the user asks you to remember something specific. Provide type and importance to help organize memories for better retrieval.",
349
- parameters: {
350
- type: "object",
351
- properties: {
352
- text: {
353
- type: "string",
354
- description: "The information to save to memory (a fact, preference, or context)",
355
- },
356
- type: {
357
- type: "string",
358
- enum: ["preference", "decision", "fact", "transient"],
359
- description: "Category of this memory. 'preference' for user likes/dislikes/settings, 'decision' for architectural or design choices, 'fact' for durable knowledge, 'transient' for temporary state that may change soon.",
360
- },
361
- importance: {
362
- type: "string",
363
- enum: ["high", "normal", "low"],
364
- description: "How important this memory is for future recall. 'high' for critical preferences or decisions, 'normal' for general facts, 'low' for minor context.",
365
- },
366
- checkNovelty: {
367
- type: "boolean",
368
- description: "When true, checks if a similar memory already exists before saving. Skips the save if a near-duplicate is found. Defaults to false.",
369
- },
370
- },
371
- required: ["text"],
372
- },
373
- async execute(_id, params) {
374
- const text = String(params.text ?? "");
375
- if (!text || text.length < 5) {
376
- return { content: [{ type: "text", text: "Text too short to save as a memory." }] };
377
- }
378
- const memoryType = typeof params.type === "string" ? params.type : undefined;
379
- const importance = typeof params.importance === "string" ? params.importance : undefined;
380
- const checkNovelty = params.checkNovelty === true;
381
- await userIdReady;
382
- if (!userId) {
383
- api.logger.warn("Cortex save: missing user_id");
384
- return { content: [{ type: "text", text: "Failed to save memory: Cortex ingest requires user_id." }] };
385
- }
386
- // Prepend metadata tags so they're stored with the memory text
387
- const metaTags = [];
388
- if (memoryType)
389
- metaTags.push(`[type:${memoryType}]`);
390
- if (importance)
391
- metaTags.push(`[importance:${importance}]`);
392
- const enrichedText = metaTags.length > 0 ? `${metaTags.join(" ")} ${text}` : text;
393
- // Item 5: Client-side dedupe — skip if near-duplicate was saved recently
394
- if (recentSaves?.isDuplicate(text)) {
395
- api.logger.debug?.(`Cortex save skipped (duplicate within window): "${text.slice(0, 60)}"`);
396
- sessionStats.savesSkippedDedupe++;
397
- return {
398
- content: [{ type: "text", text: "This memory is very similar to one saved recently. Skipped to avoid duplication." }],
399
- };
400
- }
401
- // Item 7: Novelty check — query existing memories to see if this is already stored
402
- if (checkNovelty) {
403
- try {
404
- const existing = await client.retrieve(text, 1, "fast", config.toolTimeoutMs, "factual", { userId });
405
- const topScore = existing.results?.[0]?.score ?? 0;
406
- if (topScore >= config.noveltyThreshold) {
407
- api.logger.debug?.(`Cortex save skipped (not novel, score=${topScore.toFixed(2)}): "${text.slice(0, 60)}"`);
408
- sessionStats.savesSkippedNovelty++;
409
- recentSaves?.record(text);
410
- return {
411
- content: [{
412
- type: "text",
413
- text: `This memory already exists (similarity ${(topScore * 100).toFixed(0)}%). Skipped to avoid duplication.`,
414
- }],
415
- };
416
- }
417
- }
418
- catch (err) {
419
- // Novelty check is best-effort — proceed with save on failure
420
- api.logger.debug?.(`Cortex novelty check failed, proceeding with save: ${String(err)}`);
421
- }
422
- }
423
- api.logger.debug?.(`Cortex save: "${enrichedText.slice(0, 80)}"`);
424
- void auditLoggerProxy.log({
425
- feature: "tool-save-memory",
426
- method: "POST",
427
- endpoint: "/v1/remember",
428
- payload: enrichedText,
429
- sessionId,
430
- userId,
431
- });
432
- try {
433
- const now = new Date();
434
- const referenceDate = now.toISOString().slice(0, 10);
435
- await client.remember(enrichedText, sessionId, config.toolTimeoutMs, referenceDate, userId, "openclaw", "OpenClaw");
436
- if (knowledgeState) {
437
- knowledgeState.hasMemories = true;
438
- }
439
- recentSaves?.record(text);
440
- sessionStats.saves++;
441
- api.logger.debug?.("Cortex remember accepted");
442
- return {
443
- content: [{
444
- type: "text",
445
- text: "Memory submitted for processing. It should be available shortly.",
446
- }],
447
- };
448
- }
449
- catch (err) {
450
- api.logger.warn(`Cortex save failed, falling back to async ingest: ${String(err)}`);
451
- try {
452
- const referenceDate = new Date().toISOString();
453
- const job = await client.submitIngest(enrichedText, sessionId, referenceDate, userId, "openclaw", "OpenClaw");
454
- if (knowledgeState) {
455
- knowledgeState.hasMemories = true;
456
- }
457
- recentSaves?.record(text);
458
- sessionStats.saves++;
459
- return {
460
- content: [{
461
- type: "text",
462
- text: `Memory save queued (job ${job.job_id}, status=${job.status}). It should be available shortly.`,
463
- }],
464
- };
465
- }
466
- catch (fallbackErr) {
467
- api.logger.warn(`Cortex save fallback failed: ${String(fallbackErr)}`);
468
- return { content: [{ type: "text", text: `Failed to save memory: ${String(err)}` }] };
469
- }
470
- }
471
- },
472
- });
333
+ const toolsDeps = {
334
+ client,
335
+ config,
336
+ logger: api.logger,
337
+ getUserId: () => userId,
338
+ userIdReady,
339
+ sessionId,
340
+ sessionStats,
341
+ persistStats,
342
+ auditLoggerProxy,
343
+ knowledgeState,
344
+ recentSaves,
345
+ };
346
+ api.registerTool(buildSearchMemoryTool(toolsDeps));
347
+ api.registerTool(buildSaveMemoryTool(toolsDeps));
473
348
  api.logger.debug?.("Cortex tools registered: cortex_search_memory, cortex_save_memory");
474
349
  }
475
350
  // --- Auto-Reply Commands ---
476
351
  if (api.registerCommand) {
477
- const checkpointHandler = createCheckpointHandler(client, config, api.logger, () => userId, userIdReady, () => lastMessages, sessionId, auditLoggerProxy);
478
- api.registerCommand({
479
- name: "audit",
480
- description: "Toggle or check Cortex audit log (records all data sent to Cortex)",
481
- acceptsArgs: true,
482
- handler: (ctx) => {
483
- const arg = ctx.args?.trim().toLowerCase();
484
- if (arg === "on") {
485
- if (!workspaceDirResolved) {
486
- return { text: "Cannot enable audit log — no workspace directory available. The plugin must be started with a workspace first." };
487
- }
488
- if (auditLoggerInner) {
489
- return { text: `Audit log is already enabled.\nAll Cortex API calls are being recorded at:\n\`${workspaceDirResolved}/.cortex/audit/\`` };
490
- }
491
- auditLoggerInner = new AuditLogger(workspaceDirResolved, api.logger);
492
- api.logger.info(`Cortex audit log enabled via command: ${workspaceDirResolved}/.cortex/audit/`);
493
- return {
494
- text: [
495
- `**Audit log enabled.**`,
496
- ``,
497
- `All data sent to and received from Cortex will be recorded locally.`,
498
- `Log path: \`${workspaceDirResolved}/.cortex/audit/\``,
499
- ``,
500
- `Turn off with \`/audit off\`. Log files are preserved when disabled.`,
501
- ].join("\n"),
502
- };
503
- }
504
- if (arg === "off") {
505
- if (!auditLoggerInner) {
506
- return { text: "Audit log is already off. No data is being recorded." };
507
- }
508
- auditLoggerInner = undefined;
509
- api.logger.info("Cortex audit log disabled via command");
510
- return {
511
- text: [
512
- `**Audit log disabled.**`,
513
- ``,
514
- `Cortex API calls are no longer being recorded.`,
515
- `Existing log files are preserved and can be reviewed at:`,
516
- `\`${workspaceDirResolved}/.cortex/audit/\``,
517
- ].join("\n"),
518
- };
519
- }
520
- // No args — show status
521
- const status = auditLoggerInner ? "on" : "off";
522
- const lines = [
523
- `**Cortex Audit Log**`,
524
- ``,
525
- `The audit log records all data sent to and received from the Cortex API, stored locally for inspection.`,
526
- ``,
527
- `- Status: **${status}**`,
528
- `- Config default: ${config.auditLog ? "on" : "off"}`,
529
- ];
530
- if (workspaceDirResolved) {
531
- lines.push(`- Log path: \`${workspaceDirResolved}/.cortex/audit/\``);
532
- }
533
- lines.push("", "Toggle: `/audit on` · `/audit off`");
534
- return { text: lines.join("\n") };
535
- },
536
- });
537
- api.registerCommand({
538
- name: "checkpoint",
539
- description: "Save a session checkpoint to Cortex before resetting",
540
- acceptsArgs: true,
541
- handler: checkpointHandler,
542
- });
543
- api.registerCommand({
544
- name: "sleep",
545
- description: "Mark the current session as cleanly ended (clears recovery warning state)",
546
- acceptsArgs: false,
547
- handler: async () => {
548
- try {
549
- await sessionState.clear();
550
- return {
551
- text: [
552
- `**Session ended cleanly.**`,
553
- ``,
554
- `Cortex will not show a recovery warning when you start your next session.`,
555
- `Use \`/checkpoint\` before \`/sleep\` if you want to save a summary of what you were working on.`,
556
- ].join("\n"),
557
- };
558
- }
559
- catch (err) {
560
- return { text: `Failed to mark session clean: ${String(err)}` };
561
- }
562
- },
352
+ buildCommands(api.registerCommand.bind(api), {
353
+ client,
354
+ config,
355
+ logger: api.logger,
356
+ getUserId: () => userId,
357
+ userIdReady,
358
+ getLastMessages: () => lastMessages,
359
+ sessionId,
360
+ auditLoggerProxy,
361
+ sessionState,
362
+ getWorkspaceDir: () => workspaceDirResolved,
363
+ getAuditLoggerInner: () => auditLoggerInner,
364
+ setAuditLoggerInner: (l) => { auditLoggerInner = l; },
563
365
  });
564
366
  api.logger.debug?.("Cortex commands registered: /audit, /checkpoint, /sleep");
565
367
  }
@@ -592,272 +394,18 @@ const plugin = {
592
394
  }
593
395
  // --- CLI Commands (terminal-level) ---
594
396
  if (api.registerCli) {
595
- api.registerCli(({ program }) => {
596
- const cortex = program.command("cortex").description("Cortex memory CLI commands");
597
- cortex
598
- .command("status")
599
- .description("Check Cortex API health and show memory status")
600
- .action(async () => {
601
- await userIdReady;
602
- console.log("Cortex Status Check");
603
- console.log("=".repeat(50));
604
- // Health check
605
- const startHealth = Date.now();
606
- let healthy = false;
607
- try {
608
- healthy = await client.healthCheck();
609
- const ms = Date.now() - startHealth;
610
- console.log(` API Health: ${healthy ? "OK" : "UNREACHABLE"} (${ms}ms)`);
611
- }
612
- catch {
613
- console.log(` API Health: UNREACHABLE`);
614
- }
615
- if (!healthy) {
616
- console.log("\nAPI is unreachable. Check baseUrl and network connectivity.");
617
- return;
618
- }
619
- // Knowledge
620
- try {
621
- const startKnowledge = Date.now();
622
- const knowledge = await client.knowledge(undefined, userId);
623
- const ms = Date.now() - startKnowledge;
624
- console.log(` Knowledge: OK (${ms}ms)`);
625
- console.log(` Memories: ${knowledge.total_memories.toLocaleString()}`);
626
- console.log(` Sessions: ${knowledge.total_sessions}`);
627
- console.log(` Maturity: ${knowledge.maturity}`);
628
- }
629
- catch (err) {
630
- console.log(` Knowledge: FAILED — ${String(err)}`);
631
- }
632
- // Stats
633
- try {
634
- const startStats = Date.now();
635
- const stats = await client.stats(undefined, userId);
636
- const ms = Date.now() - startStats;
637
- console.log(` Stats: OK (${ms}ms)`);
638
- console.log(` Pipeline: tier ${stats.pipeline_tier}`);
639
- }
640
- catch (err) {
641
- console.log(` Stats: FAILED — ${String(err)}`);
642
- }
643
- // Recall
644
- try {
645
- const startRecall = Date.now();
646
- await client.recall("test", 5000, { limit: 1, userId });
647
- const ms = Date.now() - startRecall;
648
- console.log(` Recall: OK (${ms}ms)`);
649
- }
650
- catch (err) {
651
- console.log(` Recall: FAILED — ${String(err)}`);
652
- }
653
- // Retrieve
654
- try {
655
- const startRetrieve = Date.now();
656
- await client.retrieve("test", 1, "fast", 5000, undefined, { userId });
657
- const ms = Date.now() - startRetrieve;
658
- console.log(` Retrieve: OK (${ms}ms)`);
659
- }
660
- catch (err) {
661
- console.log(` Retrieve: FAILED — ${String(err)}`);
662
- }
663
- console.log("");
664
- console.log(` Version: ${version}`);
665
- console.log(` User ID: ${userId ?? "unknown"}`);
666
- console.log(` Base URL: ${config.baseUrl}`);
667
- console.log(` Auto-Recall: ${config.autoRecall ? "on" : "off"}`);
668
- console.log(` Auto-Capture: ${config.autoCapture ? "on" : "off"}`);
669
- console.log(` File Sync: ${config.fileSync ? "on" : "off"}`);
670
- console.log(` Dedupe Window: ${config.dedupeWindowMinutes > 0 ? `${config.dedupeWindowMinutes}min` : "off"}`);
671
- // Session activity stats
672
- const totalSkipped = sessionStats.savesSkippedDedupe + sessionStats.savesSkippedNovelty;
673
- const avgRecallMemories = sessionStats.recallCount > 0
674
- ? (sessionStats.recallMemoriesTotal / sessionStats.recallCount).toFixed(1)
675
- : "0";
676
- console.log("");
677
- console.log("Session Activity");
678
- console.log("-".repeat(50));
679
- console.log(` Saves: ${sessionStats.saves}`);
680
- if (totalSkipped > 0) {
681
- console.log(` Skipped: ${totalSkipped} (${sessionStats.savesSkippedDedupe} dedupe, ${sessionStats.savesSkippedNovelty} novelty)`);
682
- }
683
- console.log(` Searches: ${sessionStats.searches}`);
684
- console.log(` Recalls: ${sessionStats.recallCount}`);
685
- console.log(` Avg memories/recall: ${avgRecallMemories}`);
686
- if (sessionStats.recallDuplicatesCollapsed > 0) {
687
- console.log(` Duplicates collapsed: ${sessionStats.recallDuplicatesCollapsed}`);
688
- }
689
- });
690
- cortex
691
- .command("memories")
692
- .description("Show memory count and maturity")
693
- .action(async () => {
694
- await userIdReady;
695
- try {
696
- const knowledge = await client.knowledge(undefined, userId);
697
- console.log(`Memories: ${knowledge.total_memories.toLocaleString()}`);
698
- console.log(`Sessions: ${knowledge.total_sessions}`);
699
- console.log(`Maturity: ${knowledge.maturity}`);
700
- if (knowledge.entities.length > 0) {
701
- console.log(`\nTop Entities:`);
702
- knowledge.entities.slice(0, 10).forEach((e) => {
703
- console.log(` ${e.name} (${e.memory_count} memories, last seen ${e.last_seen})`);
704
- });
705
- }
706
- }
707
- catch (err) {
708
- console.error(`Failed: ${String(err)}`);
709
- process.exitCode = 1;
710
- }
711
- });
712
- cortex
713
- .command("search")
714
- .description("Search memories from the terminal")
715
- .argument("<query>", "Search query")
716
- .option("--limit <n>", "Max results", "10")
717
- .action(async (query, opts) => {
718
- await userIdReady;
719
- try {
720
- const response = await client.recall(query, config.toolTimeoutMs, {
721
- limit: parseInt(opts.limit),
722
- userId,
723
- queryType: "combined",
724
- });
725
- if (!response.memories?.length) {
726
- console.log(`No memories found for: "${query}"`);
727
- return;
728
- }
729
- console.log(`Found ${response.memories.length} memories:\n`);
730
- response.memories.forEach((m, i) => {
731
- console.log(`${i + 1}. [${m.confidence.toFixed(2)}] ${m.content}`);
732
- if (m.entities.length > 0) {
733
- console.log(` entities: ${m.entities.join(", ")}`);
734
- }
735
- console.log("");
736
- });
737
- }
738
- catch (err) {
739
- console.error(`Search failed: ${String(err)}`);
740
- process.exitCode = 1;
741
- }
742
- });
743
- cortex
744
- .command("config")
745
- .description("Show current Cortex plugin configuration")
746
- .action(async () => {
747
- await userIdReady;
748
- console.log(`Version: ${version}`);
749
- console.log(`Base URL: ${config.baseUrl}`);
750
- console.log(`User ID: ${userId ?? "unknown"}`);
751
- console.log(`Namespace: ${namespace}`);
752
- console.log(`Auto-Recall: ${config.autoRecall ? "on" : "off"}`);
753
- console.log(`Auto-Capture: ${config.autoCapture ? "on" : "off"}`);
754
- console.log(`File Sync: ${config.fileSync ? "on" : "off"}`);
755
- console.log(`Transcript Sync: ${config.transcriptSync ? "on" : "off"}`);
756
- console.log(`Recall Limit: ${config.recallLimit}`);
757
- console.log(`Recall Timeout: ${config.recallTimeoutMs}ms`);
758
- console.log(`Tool Timeout: ${config.toolTimeoutMs}ms`);
759
- console.log(`Audit Log: ${config.auditLog ? "on" : "off"}`);
760
- });
761
- cortex
762
- .command("pair")
763
- .description("Generate a TooToo pairing code to link your agent")
764
- .action(async () => {
765
- await userIdReady;
766
- if (!userId) {
767
- console.error("Cannot generate pairing code: user ID not available.");
768
- process.exitCode = 1;
769
- return;
770
- }
771
- try {
772
- const { user_code, expires_in } = await client.generatePairingCode(userId);
773
- const mins = Math.floor(expires_in / 60);
774
- console.log(`Agent ID: ${userId}`);
775
- console.log(`Pairing code: ${user_code}`);
776
- console.log(`Expires in: ${mins} minute${mins !== 1 ? "s" : ""}`);
777
- console.log("");
778
- console.log("To link your TooToo account:");
779
- console.log(" 1. Open app.tootoo.io/settings/agents");
780
- console.log(' 2. Click "Connect Agent"');
781
- console.log(" 3. Enter the code above");
782
- }
783
- catch (err) {
784
- console.error(`Failed to generate pairing code: ${String(err)}`);
785
- process.exitCode = 1;
786
- }
787
- });
788
- cortex
789
- .command("reset")
790
- .description("Permanently delete ALL memories for this agent (irreversible)")
791
- .option("--yes", "Skip confirmation prompt")
792
- .action(async (opts) => {
793
- await userIdReady;
794
- if (!userId) {
795
- console.error("Cannot reset: user ID not available.");
796
- process.exitCode = 1;
797
- return;
798
- }
799
- // Show what will be deleted
800
- let memoryCount = 0;
801
- let sessionCount = 0;
802
- try {
803
- const knowledge = await client.knowledge(undefined, userId);
804
- memoryCount = knowledge.total_memories;
805
- sessionCount = knowledge.total_sessions;
806
- }
807
- catch {
808
- // Continue even if we can't get counts
809
- }
810
- console.log("");
811
- console.log(" WARNING: This will permanently delete ALL data for this agent.");
812
- console.log("");
813
- console.log(` Agent ID: ${userId}`);
814
- if (memoryCount > 0 || sessionCount > 0) {
815
- console.log(` Memories: ${memoryCount.toLocaleString()}`);
816
- console.log(` Sessions: ${sessionCount}`);
817
- }
818
- console.log("");
819
- console.log(" This includes all memories, facts, suggestions, and graph data.");
820
- console.log(" Agent links (TooToo pairing) will be preserved.");
821
- console.log(" This action CANNOT be undone.");
822
- console.log("");
823
- if (!opts.yes) {
824
- const { createInterface } = await import("node:readline");
825
- const rl = createInterface({ input: process.stdin, output: process.stdout });
826
- const answer = await new Promise((resolve) => {
827
- rl.question(" Type 'reset' to confirm: ", resolve);
828
- });
829
- rl.close();
830
- if (answer.trim().toLowerCase() !== "reset") {
831
- console.log("\n Aborted. No data was deleted.");
832
- return;
833
- }
834
- }
835
- try {
836
- const result = await client.forgetUser(userId);
837
- const d = result.deleted;
838
- console.log("");
839
- console.log(" Memory reset complete.");
840
- console.log("");
841
- console.log(` Deleted:`);
842
- console.log(` Engraved memories: ${d.engraved_memories}`);
843
- console.log(` Resonated memories: ${d.resonated_memories}`);
844
- console.log(` Graph nodes: ${d.nodes}`);
845
- console.log(` Codex suggestions: ${d.codex_suggestions}`);
846
- console.log(` Suppressions: ${d.codex_suggestion_suppressions}`);
847
- }
848
- catch (err) {
849
- if (isAbortError(err) && await resetCompletedAfterAbort(client, userId)) {
850
- console.log("");
851
- console.log(" Memory reset complete.");
852
- console.log("");
853
- console.log(" The server finished the reset, but the request ended before deletion stats were returned.");
854
- return;
855
- }
856
- console.error(`\n Reset failed: ${String(err)}`);
857
- process.exitCode = 1;
858
- }
859
- });
860
- }, { commands: ["cortex"] });
397
+ registerCliCommands(api.registerCli.bind(api), {
398
+ client,
399
+ config,
400
+ version,
401
+ getUserId: () => userId,
402
+ userIdReady,
403
+ getNamespace: () => namespace,
404
+ sessionStats,
405
+ loadPersistedStats,
406
+ isAbortError,
407
+ resetCompletedAfterAbort,
408
+ });
861
409
  api.logger.debug?.("Cortex CLI registered: openclaw cortex {status,memories,search,config,pair,reset}");
862
410
  }
863
411
  // --- Services: retry queue, file sync ---