neuralmemory 1.11.2 → 1.13.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/dist/index.js +9 -9
- package/openclaw.plugin.json +93 -93
- package/package.json +53 -53
- package/src/index.ts +342 -342
package/dist/index.js
CHANGED
|
@@ -41,14 +41,14 @@ function buildToolInstructions(tools) {
|
|
|
41
41
|
const toolList = tools
|
|
42
42
|
.map((t) => `- ${t.name}: ${t.description.slice(0, 100)}`)
|
|
43
43
|
.join("\n");
|
|
44
|
-
return `You have NeuralMemory tools for persistent memory across sessions. Call these as TOOL CALLS (not CLI commands):
|
|
45
|
-
|
|
46
|
-
${toolList}
|
|
47
|
-
|
|
48
|
-
NeuralMemory (nmem_*) is your primary memory system. memory_search and memory_get are legacy aliases that redirect to nmem_recall — prefer nmem_* tools directly for full functionality.
|
|
49
|
-
|
|
50
|
-
These are tool calls, NOT shell commands. Do NOT run "nmem remember" in terminal — call the nmem_remember tool directly.
|
|
51
|
-
|
|
44
|
+
return `You have NeuralMemory tools for persistent memory across sessions. Call these as TOOL CALLS (not CLI commands):
|
|
45
|
+
|
|
46
|
+
${toolList}
|
|
47
|
+
|
|
48
|
+
NeuralMemory (nmem_*) is your primary memory system. memory_search and memory_get are legacy aliases that redirect to nmem_recall — prefer nmem_* tools directly for full functionality.
|
|
49
|
+
|
|
50
|
+
These are tool calls, NOT shell commands. Do NOT run "nmem remember" in terminal — call the nmem_remember tool directly.
|
|
51
|
+
|
|
52
52
|
PROACTIVE MEMORY: Use nmem_remember after decisions, errors, and insights. Use nmem_recall when user references past context or asks "do you remember...". Use nmem_remember_batch to store multiple memories at once.`;
|
|
53
53
|
}
|
|
54
54
|
const DEFAULT_CONFIG = {
|
|
@@ -130,7 +130,7 @@ const plugin = {
|
|
|
130
130
|
id: "neuralmemory",
|
|
131
131
|
name: "NeuralMemory",
|
|
132
132
|
description: "Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers",
|
|
133
|
-
version: "1.
|
|
133
|
+
version: "1.13.0",
|
|
134
134
|
kind: "memory",
|
|
135
135
|
register(api) {
|
|
136
136
|
const cfg = resolveConfig(api.pluginConfig);
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "neuralmemory",
|
|
3
|
-
"kind": "memory",
|
|
4
|
-
"name": "NeuralMemory",
|
|
5
|
-
"description": "Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers. REQUIRED: set plugins.slots.memory = \"neuralmemory\" in openclaw.json to disable the default memory-core plugin and activate NeuralMemory as the exclusive memory provider.",
|
|
6
|
-
"version": "1.
|
|
7
|
-
"configSchema": {
|
|
8
|
-
"jsonSchema": {
|
|
9
|
-
"type": "object",
|
|
10
|
-
"additionalProperties": false,
|
|
11
|
-
"properties": {
|
|
12
|
-
"pythonPath": {
|
|
13
|
-
"type": "string",
|
|
14
|
-
"description": "Path to Python executable with neural-memory installed",
|
|
15
|
-
"default": "python"
|
|
16
|
-
},
|
|
17
|
-
"brain": {
|
|
18
|
-
"type": "string",
|
|
19
|
-
"pattern": "^[a-zA-Z0-9_\\-.]{1,64}$",
|
|
20
|
-
"description": "Brain name to use for this workspace",
|
|
21
|
-
"default": "default"
|
|
22
|
-
},
|
|
23
|
-
"autoContext": {
|
|
24
|
-
"type": "boolean",
|
|
25
|
-
"description": "Inject relevant memory context before each agent run",
|
|
26
|
-
"default": true
|
|
27
|
-
},
|
|
28
|
-
"autoCapture": {
|
|
29
|
-
"type": "boolean",
|
|
30
|
-
"description": "Auto-extract and store memories after each agent run",
|
|
31
|
-
"default": true
|
|
32
|
-
},
|
|
33
|
-
"contextDepth": {
|
|
34
|
-
"type": "integer",
|
|
35
|
-
"minimum": 0,
|
|
36
|
-
"maximum": 3,
|
|
37
|
-
"description": "Recall depth for auto-context: 0=instant, 1=context, 2=habit, 3=deep",
|
|
38
|
-
"default": 1
|
|
39
|
-
},
|
|
40
|
-
"maxContextTokens": {
|
|
41
|
-
"type": "integer",
|
|
42
|
-
"minimum": 100,
|
|
43
|
-
"maximum": 10000,
|
|
44
|
-
"description": "Maximum tokens for auto-context injection",
|
|
45
|
-
"default": 500
|
|
46
|
-
},
|
|
47
|
-
"timeout": {
|
|
48
|
-
"type": "integer",
|
|
49
|
-
"minimum": 5000,
|
|
50
|
-
"maximum": 120000,
|
|
51
|
-
"description": "MCP request timeout in milliseconds",
|
|
52
|
-
"default": 30000
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"uiHints": {
|
|
57
|
-
"_setup": {
|
|
58
|
-
"label": "Required Setup",
|
|
59
|
-
"help": "Add to openclaw.json: { \"plugins\": { \"slots\": { \"memory\": \"neuralmemory\" } } } — this disables the default memory-core plugin so agents use NeuralMemory exclusively. Without this, agents may still call memory_search (memory-core) even with AGENTS.MD rules."
|
|
60
|
-
},
|
|
61
|
-
"pythonPath": {
|
|
62
|
-
"label": "Python Path",
|
|
63
|
-
"placeholder": "python",
|
|
64
|
-
"help": "Path to Python executable that has neural-memory installed (pip install neural-memory)"
|
|
65
|
-
},
|
|
66
|
-
"brain": {
|
|
67
|
-
"label": "Brain Name",
|
|
68
|
-
"placeholder": "default",
|
|
69
|
-
"help": "Which brain to use — each workspace can have its own brain"
|
|
70
|
-
},
|
|
71
|
-
"autoContext": {
|
|
72
|
-
"label": "Auto-inject Context",
|
|
73
|
-
"help": "Automatically query relevant memories and inject them before each agent run"
|
|
74
|
-
},
|
|
75
|
-
"autoCapture": {
|
|
76
|
-
"label": "Auto-capture Memories",
|
|
77
|
-
"help": "Automatically extract facts, decisions, and insights after each agent run"
|
|
78
|
-
},
|
|
79
|
-
"contextDepth": {
|
|
80
|
-
"label": "Context Depth",
|
|
81
|
-
"help": "How deep to search: 0=instant lookup, 1=contextual, 2=habit patterns, 3=full graph traversal"
|
|
82
|
-
},
|
|
83
|
-
"maxContextTokens": {
|
|
84
|
-
"label": "Max Context Tokens",
|
|
85
|
-
"help": "Maximum tokens to inject as context (higher = more context but uses more prompt space)"
|
|
86
|
-
},
|
|
87
|
-
"timeout": {
|
|
88
|
-
"label": "MCP Timeout (ms)",
|
|
89
|
-
"help": "How long to wait for MCP server responses. Increase if you see timeout errors on slow machines (default: 30000)"
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "neuralmemory",
|
|
3
|
+
"kind": "memory",
|
|
4
|
+
"name": "NeuralMemory",
|
|
5
|
+
"description": "Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers. REQUIRED: set plugins.slots.memory = \"neuralmemory\" in openclaw.json to disable the default memory-core plugin and activate NeuralMemory as the exclusive memory provider.",
|
|
6
|
+
"version": "1.13.0",
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"jsonSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"properties": {
|
|
12
|
+
"pythonPath": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Path to Python executable with neural-memory installed",
|
|
15
|
+
"default": "python"
|
|
16
|
+
},
|
|
17
|
+
"brain": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^[a-zA-Z0-9_\\-.]{1,64}$",
|
|
20
|
+
"description": "Brain name to use for this workspace",
|
|
21
|
+
"default": "default"
|
|
22
|
+
},
|
|
23
|
+
"autoContext": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"description": "Inject relevant memory context before each agent run",
|
|
26
|
+
"default": true
|
|
27
|
+
},
|
|
28
|
+
"autoCapture": {
|
|
29
|
+
"type": "boolean",
|
|
30
|
+
"description": "Auto-extract and store memories after each agent run",
|
|
31
|
+
"default": true
|
|
32
|
+
},
|
|
33
|
+
"contextDepth": {
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"minimum": 0,
|
|
36
|
+
"maximum": 3,
|
|
37
|
+
"description": "Recall depth for auto-context: 0=instant, 1=context, 2=habit, 3=deep",
|
|
38
|
+
"default": 1
|
|
39
|
+
},
|
|
40
|
+
"maxContextTokens": {
|
|
41
|
+
"type": "integer",
|
|
42
|
+
"minimum": 100,
|
|
43
|
+
"maximum": 10000,
|
|
44
|
+
"description": "Maximum tokens for auto-context injection",
|
|
45
|
+
"default": 500
|
|
46
|
+
},
|
|
47
|
+
"timeout": {
|
|
48
|
+
"type": "integer",
|
|
49
|
+
"minimum": 5000,
|
|
50
|
+
"maximum": 120000,
|
|
51
|
+
"description": "MCP request timeout in milliseconds",
|
|
52
|
+
"default": 30000
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"uiHints": {
|
|
57
|
+
"_setup": {
|
|
58
|
+
"label": "Required Setup",
|
|
59
|
+
"help": "Add to openclaw.json: { \"plugins\": { \"slots\": { \"memory\": \"neuralmemory\" } } } — this disables the default memory-core plugin so agents use NeuralMemory exclusively. Without this, agents may still call memory_search (memory-core) even with AGENTS.MD rules."
|
|
60
|
+
},
|
|
61
|
+
"pythonPath": {
|
|
62
|
+
"label": "Python Path",
|
|
63
|
+
"placeholder": "python",
|
|
64
|
+
"help": "Path to Python executable that has neural-memory installed (pip install neural-memory)"
|
|
65
|
+
},
|
|
66
|
+
"brain": {
|
|
67
|
+
"label": "Brain Name",
|
|
68
|
+
"placeholder": "default",
|
|
69
|
+
"help": "Which brain to use — each workspace can have its own brain"
|
|
70
|
+
},
|
|
71
|
+
"autoContext": {
|
|
72
|
+
"label": "Auto-inject Context",
|
|
73
|
+
"help": "Automatically query relevant memories and inject them before each agent run"
|
|
74
|
+
},
|
|
75
|
+
"autoCapture": {
|
|
76
|
+
"label": "Auto-capture Memories",
|
|
77
|
+
"help": "Automatically extract facts, decisions, and insights after each agent run"
|
|
78
|
+
},
|
|
79
|
+
"contextDepth": {
|
|
80
|
+
"label": "Context Depth",
|
|
81
|
+
"help": "How deep to search: 0=instant lookup, 1=contextual, 2=habit patterns, 3=full graph traversal"
|
|
82
|
+
},
|
|
83
|
+
"maxContextTokens": {
|
|
84
|
+
"label": "Max Context Tokens",
|
|
85
|
+
"help": "Maximum tokens to inject as context (higher = more context but uses more prompt space)"
|
|
86
|
+
},
|
|
87
|
+
"timeout": {
|
|
88
|
+
"label": "MCP Timeout (ms)",
|
|
89
|
+
"help": "How long to wait for MCP server responses. Increase if you see timeout errors on slow machines (default: 30000)"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "neuralmemory",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "NeuralMemory plugin for OpenClaw — brain-inspired persistent memory for AI agents",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"openclaw": {
|
|
9
|
-
"id": "neuralmemory",
|
|
10
|
-
"extensions": [
|
|
11
|
-
"./dist/index.js"
|
|
12
|
-
],
|
|
13
|
-
"source": "./src/index.ts"
|
|
14
|
-
},
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsc",
|
|
17
|
-
"typecheck": "tsc --noEmit",
|
|
18
|
-
"prepublishOnly": "npm run build",
|
|
19
|
-
"test": "vitest run",
|
|
20
|
-
"test:watch": "vitest",
|
|
21
|
-
"test:coverage": "vitest run --coverage"
|
|
22
|
-
},
|
|
23
|
-
"peerDependencies": {},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"openclaw",
|
|
26
|
-
"openclaw-plugin",
|
|
27
|
-
"memory",
|
|
28
|
-
"neuralmemory",
|
|
29
|
-
"ai-agent",
|
|
30
|
-
"persistent-memory"
|
|
31
|
-
],
|
|
32
|
-
"author": "NeuralMemory Contributors",
|
|
33
|
-
"license": "MIT",
|
|
34
|
-
"homepage": "https://github.com/nhadaututtheky/neural-memory#readme",
|
|
35
|
-
"repository": {
|
|
36
|
-
"type": "git",
|
|
37
|
-
"url": "https://github.com/nhadaututtheky/neural-memory"
|
|
38
|
-
},
|
|
39
|
-
"files": [
|
|
40
|
-
"src/",
|
|
41
|
-
"dist/",
|
|
42
|
-
"openclaw.plugin.json"
|
|
43
|
-
],
|
|
44
|
-
"engines": {
|
|
45
|
-
"node": ">=18.0.0"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@types/node": "^25.2.2",
|
|
49
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
50
|
-
"typescript": "^5.9.3",
|
|
51
|
-
"vitest": "^3.0.0"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "neuralmemory",
|
|
3
|
+
"version": "1.13.0",
|
|
4
|
+
"description": "NeuralMemory plugin for OpenClaw — brain-inspired persistent memory for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"openclaw": {
|
|
9
|
+
"id": "neuralmemory",
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./dist/index.js"
|
|
12
|
+
],
|
|
13
|
+
"source": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:coverage": "vitest run --coverage"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"openclaw",
|
|
26
|
+
"openclaw-plugin",
|
|
27
|
+
"memory",
|
|
28
|
+
"neuralmemory",
|
|
29
|
+
"ai-agent",
|
|
30
|
+
"persistent-memory"
|
|
31
|
+
],
|
|
32
|
+
"author": "NeuralMemory Contributors",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"homepage": "https://github.com/nhadaututtheky/neural-memory#readme",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/nhadaututtheky/neural-memory"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src/",
|
|
41
|
+
"dist/",
|
|
42
|
+
"openclaw.plugin.json"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^25.2.2",
|
|
49
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vitest": "^3.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,342 +1,342 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NeuralMemory — OpenClaw Memory Plugin
|
|
3
|
-
*
|
|
4
|
-
* Brain-inspired persistent memory for AI agents.
|
|
5
|
-
* Occupies the exclusive "memory" plugin slot.
|
|
6
|
-
*
|
|
7
|
-
* Architecture:
|
|
8
|
-
* OpenClaw ←→ Plugin (TypeScript) ←→ MCP stdio ←→ NeuralMemory (Python)
|
|
9
|
-
*
|
|
10
|
-
* v1.7.0: Dynamic tool proxy — fetches all tools from MCP `tools/list`
|
|
11
|
-
* instead of hardcoding 6 tools. Automatically exposes every tool the
|
|
12
|
-
* MCP server provides (39+ tools in NM v2.28.0).
|
|
13
|
-
*
|
|
14
|
-
* v1.8.0: Compatible with NM v2.29.0 — RRF score fusion, graph-based
|
|
15
|
-
* query expansion, and Personalized PageRank activation.
|
|
16
|
-
*
|
|
17
|
-
* v1.8.1: Fix async register() — OpenClaw requires synchronous registration.
|
|
18
|
-
* Fallback tools registered sync; MCP connection deferred to service.start().
|
|
19
|
-
*
|
|
20
|
-
* v1.9.0: Backward-compat shim tools (memory_search, memory_get) to prevent
|
|
21
|
-
* "allowList contains unknown entries" warnings when NM replaces memory-core.
|
|
22
|
-
*
|
|
23
|
-
* v1.10.0: Singleton MCP client — multiple workspaces (multi-agent) share
|
|
24
|
-
* the same connected client instance, keyed by (pythonPath, brain). Fixes
|
|
25
|
-
* "NeuralMemory service not running" when OpenClaw registers the plugin
|
|
26
|
-
* for a second workspace after gateway startup.
|
|
27
|
-
*
|
|
28
|
-
* Registers:
|
|
29
|
-
* N tools — dynamically from MCP server (fallback: 5 core + 2 compat)
|
|
30
|
-
* 1 service — MCP process lifecycle (start/stop)
|
|
31
|
-
* 2 hooks — before_agent_start (auto-context), agent_end (auto-capture)
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import type {
|
|
35
|
-
OpenClawPluginDefinition,
|
|
36
|
-
OpenClawPluginApi,
|
|
37
|
-
BeforeAgentStartEvent,
|
|
38
|
-
BeforeAgentStartResult,
|
|
39
|
-
AgentEndEvent,
|
|
40
|
-
} from "./types.js";
|
|
41
|
-
import { NeuralMemoryMcpClient } from "./mcp-client.js";
|
|
42
|
-
import type { PluginLogger } from "./types.js";
|
|
43
|
-
import { createToolsFromMcp, createFallbackTools, createCompatibilityTools } from "./tools.js";
|
|
44
|
-
import type { ToolDefinition } from "./tools.js";
|
|
45
|
-
|
|
46
|
-
// ── System prompt for tool awareness ──────────────────────
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Build a system prompt listing all registered tool names.
|
|
50
|
-
* This makes the agent aware of which nmem_* tools are available.
|
|
51
|
-
*/
|
|
52
|
-
function buildToolInstructions(tools: ToolDefinition[]): string {
|
|
53
|
-
const toolList = tools
|
|
54
|
-
.map((t) => `- ${t.name}: ${t.description.slice(0, 100)}`)
|
|
55
|
-
.join("\n");
|
|
56
|
-
|
|
57
|
-
return `You have NeuralMemory tools for persistent memory across sessions. Call these as TOOL CALLS (not CLI commands):
|
|
58
|
-
|
|
59
|
-
${toolList}
|
|
60
|
-
|
|
61
|
-
NeuralMemory (nmem_*) is your primary memory system. memory_search and memory_get are legacy aliases that redirect to nmem_recall — prefer nmem_* tools directly for full functionality.
|
|
62
|
-
|
|
63
|
-
These are tool calls, NOT shell commands. Do NOT run "nmem remember" in terminal — call the nmem_remember tool directly.
|
|
64
|
-
|
|
65
|
-
PROACTIVE MEMORY: Use nmem_remember after decisions, errors, and insights. Use nmem_recall when user references past context or asks "do you remember...". Use nmem_remember_batch to store multiple memories at once.`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ── Config ─────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
type PluginConfig = {
|
|
71
|
-
pythonPath: string;
|
|
72
|
-
brain: string;
|
|
73
|
-
autoContext: boolean;
|
|
74
|
-
autoCapture: boolean;
|
|
75
|
-
contextDepth: number;
|
|
76
|
-
maxContextTokens: number;
|
|
77
|
-
timeout: number;
|
|
78
|
-
initTimeout: number;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const DEFAULT_CONFIG: Readonly<PluginConfig> = {
|
|
82
|
-
pythonPath: "python",
|
|
83
|
-
brain: "default",
|
|
84
|
-
autoContext: true,
|
|
85
|
-
autoCapture: true,
|
|
86
|
-
contextDepth: 1,
|
|
87
|
-
maxContextTokens: 500,
|
|
88
|
-
timeout: 30_000,
|
|
89
|
-
initTimeout: 90_000,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export const BRAIN_NAME_RE = /^[a-zA-Z0-9_\-.]{1,64}$/;
|
|
93
|
-
export const MAX_AUTO_CAPTURE_CHARS = 50_000;
|
|
94
|
-
|
|
95
|
-
export function resolveConfig(raw?: Record<string, unknown>): PluginConfig {
|
|
96
|
-
const merged = { ...DEFAULT_CONFIG, ...(raw ?? {}) };
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
pythonPath:
|
|
100
|
-
typeof merged.pythonPath === "string" && merged.pythonPath.length > 0
|
|
101
|
-
? merged.pythonPath
|
|
102
|
-
: DEFAULT_CONFIG.pythonPath,
|
|
103
|
-
brain:
|
|
104
|
-
typeof merged.brain === "string" && BRAIN_NAME_RE.test(merged.brain)
|
|
105
|
-
? merged.brain
|
|
106
|
-
: DEFAULT_CONFIG.brain,
|
|
107
|
-
autoContext:
|
|
108
|
-
typeof merged.autoContext === "boolean"
|
|
109
|
-
? merged.autoContext
|
|
110
|
-
: DEFAULT_CONFIG.autoContext,
|
|
111
|
-
autoCapture:
|
|
112
|
-
typeof merged.autoCapture === "boolean"
|
|
113
|
-
? merged.autoCapture
|
|
114
|
-
: DEFAULT_CONFIG.autoCapture,
|
|
115
|
-
contextDepth:
|
|
116
|
-
typeof merged.contextDepth === "number" &&
|
|
117
|
-
Number.isInteger(merged.contextDepth) &&
|
|
118
|
-
merged.contextDepth >= 0 &&
|
|
119
|
-
merged.contextDepth <= 3
|
|
120
|
-
? merged.contextDepth
|
|
121
|
-
: DEFAULT_CONFIG.contextDepth,
|
|
122
|
-
maxContextTokens:
|
|
123
|
-
typeof merged.maxContextTokens === "number" &&
|
|
124
|
-
Number.isInteger(merged.maxContextTokens) &&
|
|
125
|
-
merged.maxContextTokens >= 100 &&
|
|
126
|
-
merged.maxContextTokens <= 10_000
|
|
127
|
-
? merged.maxContextTokens
|
|
128
|
-
: DEFAULT_CONFIG.maxContextTokens,
|
|
129
|
-
timeout:
|
|
130
|
-
typeof merged.timeout === "number" &&
|
|
131
|
-
Number.isFinite(merged.timeout) &&
|
|
132
|
-
merged.timeout >= 5_000 &&
|
|
133
|
-
merged.timeout <= 120_000
|
|
134
|
-
? merged.timeout
|
|
135
|
-
: DEFAULT_CONFIG.timeout,
|
|
136
|
-
initTimeout:
|
|
137
|
-
typeof merged.initTimeout === "number" &&
|
|
138
|
-
Number.isFinite(merged.initTimeout) &&
|
|
139
|
-
merged.initTimeout >= 10_000 &&
|
|
140
|
-
merged.initTimeout <= 300_000
|
|
141
|
-
? merged.initTimeout
|
|
142
|
-
: DEFAULT_CONFIG.initTimeout,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ── Singleton MCP client pool ────────────────────────────────
|
|
147
|
-
// Multiple workspaces may call register() independently, but all
|
|
148
|
-
// should share the same MCP process per (pythonPath, brain) combo.
|
|
149
|
-
|
|
150
|
-
const mcpClients = new Map<string, NeuralMemoryMcpClient>();
|
|
151
|
-
|
|
152
|
-
function getOrCreateMcpClient(
|
|
153
|
-
cfg: PluginConfig,
|
|
154
|
-
logger: PluginLogger,
|
|
155
|
-
): NeuralMemoryMcpClient {
|
|
156
|
-
const key = `${cfg.pythonPath}::${cfg.brain}`;
|
|
157
|
-
|
|
158
|
-
const existing = mcpClients.get(key);
|
|
159
|
-
if (existing) {
|
|
160
|
-
logger.info(`Reusing existing MCP client for brain "${cfg.brain}"`);
|
|
161
|
-
return existing;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const mcp = new NeuralMemoryMcpClient({
|
|
165
|
-
pythonPath: cfg.pythonPath,
|
|
166
|
-
brain: cfg.brain,
|
|
167
|
-
logger,
|
|
168
|
-
timeout: cfg.timeout,
|
|
169
|
-
initTimeout: cfg.initTimeout,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
mcpClients.set(key, mcp);
|
|
173
|
-
return mcp;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ── Plugin definition ──────────────────────────────────────
|
|
177
|
-
|
|
178
|
-
const plugin: OpenClawPluginDefinition = {
|
|
179
|
-
id: "neuralmemory",
|
|
180
|
-
name: "NeuralMemory",
|
|
181
|
-
description:
|
|
182
|
-
"Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers",
|
|
183
|
-
version: "1.
|
|
184
|
-
kind: "memory",
|
|
185
|
-
|
|
186
|
-
register(api: OpenClawPluginApi): void {
|
|
187
|
-
const cfg = resolveConfig(api.pluginConfig);
|
|
188
|
-
|
|
189
|
-
const mcp = getOrCreateMcpClient(cfg, api.logger);
|
|
190
|
-
|
|
191
|
-
// ── Register fallback tools synchronously ────────────
|
|
192
|
-
// OpenClaw requires register() to be synchronous.
|
|
193
|
-
// Register stable fallback tools immediately; MCP connection
|
|
194
|
-
// and dynamic tool discovery happen in service.start().
|
|
195
|
-
// Fallback tools auto-reconnect MCP on first call.
|
|
196
|
-
|
|
197
|
-
const registeredTools = createFallbackTools(mcp);
|
|
198
|
-
const compatTools = createCompatibilityTools(mcp);
|
|
199
|
-
|
|
200
|
-
for (const t of [...registeredTools, ...compatTools]) {
|
|
201
|
-
api.registerTool(t, { name: t.name });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
api.logger.info(
|
|
205
|
-
`Registered ${registeredTools.length} NeuralMemory tools + ${compatTools.length} compat shims (sync)`,
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
// ── Service: MCP process lifecycle ───────────────────
|
|
209
|
-
|
|
210
|
-
api.registerService({
|
|
211
|
-
id: "neuralmemory-mcp",
|
|
212
|
-
|
|
213
|
-
async start(): Promise<void> {
|
|
214
|
-
if (!mcp.connected) {
|
|
215
|
-
try {
|
|
216
|
-
await mcp.connect();
|
|
217
|
-
api.logger.info("NeuralMemory MCP connected in service.start()");
|
|
218
|
-
|
|
219
|
-
// Log discovered tools for diagnostics (cannot re-register
|
|
220
|
-
// after register() — OpenClaw freezes the tool list).
|
|
221
|
-
try {
|
|
222
|
-
const dynamicTools = await createToolsFromMcp(mcp);
|
|
223
|
-
api.logger.info(
|
|
224
|
-
`NeuralMemory MCP discovered ${dynamicTools.length} tools`,
|
|
225
|
-
);
|
|
226
|
-
} catch (err) {
|
|
227
|
-
api.logger.warn(
|
|
228
|
-
`Tool discovery failed: ${(err as Error).message}`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
} catch (err) {
|
|
232
|
-
api.logger.error(
|
|
233
|
-
`Failed to start NeuralMemory MCP: ${(err as Error).message}`,
|
|
234
|
-
);
|
|
235
|
-
throw err;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
async stop(): Promise<void> {
|
|
241
|
-
// Remove from singleton pool so next register() creates fresh client
|
|
242
|
-
const key = `${cfg.pythonPath}::${cfg.brain}`;
|
|
243
|
-
mcpClients.delete(key);
|
|
244
|
-
await mcp.close();
|
|
245
|
-
api.logger.info("NeuralMemory MCP service stopped");
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// ── Hook: tool awareness + auto-context before agent start ───
|
|
250
|
-
|
|
251
|
-
api.on(
|
|
252
|
-
"before_agent_start",
|
|
253
|
-
async (
|
|
254
|
-
event: unknown,
|
|
255
|
-
_ctx: unknown,
|
|
256
|
-
): Promise<BeforeAgentStartResult | void> => {
|
|
257
|
-
const result: BeforeAgentStartResult = {
|
|
258
|
-
systemPrompt: buildToolInstructions(registeredTools),
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
if (cfg.autoContext && mcp.connected) {
|
|
262
|
-
const ev = event as BeforeAgentStartEvent;
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
const raw = await mcp.callTool("nmem_recall", {
|
|
266
|
-
query: ev.prompt,
|
|
267
|
-
depth: cfg.contextDepth,
|
|
268
|
-
max_tokens: cfg.maxContextTokens,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const data = JSON.parse(raw) as {
|
|
272
|
-
answer?: string;
|
|
273
|
-
confidence?: number;
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
if (data.answer && (data.confidence ?? 0) > 0.1) {
|
|
277
|
-
result.prependContext = `[NeuralMemory — relevant context]\n${data.answer}`;
|
|
278
|
-
}
|
|
279
|
-
} catch (err) {
|
|
280
|
-
api.logger.warn(
|
|
281
|
-
`Auto-context failed: ${(err as Error).message}`,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return result;
|
|
287
|
-
},
|
|
288
|
-
{ priority: 10 },
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
// ── Hook: auto-capture after agent completes ────────
|
|
292
|
-
|
|
293
|
-
if (cfg.autoCapture) {
|
|
294
|
-
api.on(
|
|
295
|
-
"agent_end",
|
|
296
|
-
async (event: unknown, _ctx: unknown): Promise<void> => {
|
|
297
|
-
if (!mcp.connected) return;
|
|
298
|
-
|
|
299
|
-
const ev = event as AgentEndEvent;
|
|
300
|
-
if (!ev.success) return;
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
const messages = ev.messages?.slice(-5) ?? [];
|
|
304
|
-
const text = messages
|
|
305
|
-
.filter(
|
|
306
|
-
(m: unknown): m is { role: string; content: string } =>
|
|
307
|
-
typeof m === "object" &&
|
|
308
|
-
m !== null &&
|
|
309
|
-
(m as { role?: string }).role === "assistant" &&
|
|
310
|
-
typeof (m as { content?: unknown }).content === "string",
|
|
311
|
-
)
|
|
312
|
-
.map((m) => m.content)
|
|
313
|
-
.join("\n")
|
|
314
|
-
.slice(0, MAX_AUTO_CAPTURE_CHARS);
|
|
315
|
-
|
|
316
|
-
if (text.length > 50) {
|
|
317
|
-
await mcp.callTool("nmem_auto", {
|
|
318
|
-
action: "process",
|
|
319
|
-
text,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
} catch (err) {
|
|
323
|
-
api.logger.warn(
|
|
324
|
-
`Auto-capture failed: ${(err as Error).message}`,
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
{ priority: 90 },
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// ── Done ────────────────────────────────────────────
|
|
333
|
-
|
|
334
|
-
api.logger.info(
|
|
335
|
-
`NeuralMemory registered (brain: ${cfg.brain}, ` +
|
|
336
|
-
`autoContext: ${cfg.autoContext}, autoCapture: ${cfg.autoCapture}) — ` +
|
|
337
|
-
`tools will be loaded dynamically from MCP on service start`,
|
|
338
|
-
);
|
|
339
|
-
},
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
export default plugin;
|
|
1
|
+
/**
|
|
2
|
+
* NeuralMemory — OpenClaw Memory Plugin
|
|
3
|
+
*
|
|
4
|
+
* Brain-inspired persistent memory for AI agents.
|
|
5
|
+
* Occupies the exclusive "memory" plugin slot.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* OpenClaw ←→ Plugin (TypeScript) ←→ MCP stdio ←→ NeuralMemory (Python)
|
|
9
|
+
*
|
|
10
|
+
* v1.7.0: Dynamic tool proxy — fetches all tools from MCP `tools/list`
|
|
11
|
+
* instead of hardcoding 6 tools. Automatically exposes every tool the
|
|
12
|
+
* MCP server provides (39+ tools in NM v2.28.0).
|
|
13
|
+
*
|
|
14
|
+
* v1.8.0: Compatible with NM v2.29.0 — RRF score fusion, graph-based
|
|
15
|
+
* query expansion, and Personalized PageRank activation.
|
|
16
|
+
*
|
|
17
|
+
* v1.8.1: Fix async register() — OpenClaw requires synchronous registration.
|
|
18
|
+
* Fallback tools registered sync; MCP connection deferred to service.start().
|
|
19
|
+
*
|
|
20
|
+
* v1.9.0: Backward-compat shim tools (memory_search, memory_get) to prevent
|
|
21
|
+
* "allowList contains unknown entries" warnings when NM replaces memory-core.
|
|
22
|
+
*
|
|
23
|
+
* v1.10.0: Singleton MCP client — multiple workspaces (multi-agent) share
|
|
24
|
+
* the same connected client instance, keyed by (pythonPath, brain). Fixes
|
|
25
|
+
* "NeuralMemory service not running" when OpenClaw registers the plugin
|
|
26
|
+
* for a second workspace after gateway startup.
|
|
27
|
+
*
|
|
28
|
+
* Registers:
|
|
29
|
+
* N tools — dynamically from MCP server (fallback: 5 core + 2 compat)
|
|
30
|
+
* 1 service — MCP process lifecycle (start/stop)
|
|
31
|
+
* 2 hooks — before_agent_start (auto-context), agent_end (auto-capture)
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type {
|
|
35
|
+
OpenClawPluginDefinition,
|
|
36
|
+
OpenClawPluginApi,
|
|
37
|
+
BeforeAgentStartEvent,
|
|
38
|
+
BeforeAgentStartResult,
|
|
39
|
+
AgentEndEvent,
|
|
40
|
+
} from "./types.js";
|
|
41
|
+
import { NeuralMemoryMcpClient } from "./mcp-client.js";
|
|
42
|
+
import type { PluginLogger } from "./types.js";
|
|
43
|
+
import { createToolsFromMcp, createFallbackTools, createCompatibilityTools } from "./tools.js";
|
|
44
|
+
import type { ToolDefinition } from "./tools.js";
|
|
45
|
+
|
|
46
|
+
// ── System prompt for tool awareness ──────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a system prompt listing all registered tool names.
|
|
50
|
+
* This makes the agent aware of which nmem_* tools are available.
|
|
51
|
+
*/
|
|
52
|
+
function buildToolInstructions(tools: ToolDefinition[]): string {
|
|
53
|
+
const toolList = tools
|
|
54
|
+
.map((t) => `- ${t.name}: ${t.description.slice(0, 100)}`)
|
|
55
|
+
.join("\n");
|
|
56
|
+
|
|
57
|
+
return `You have NeuralMemory tools for persistent memory across sessions. Call these as TOOL CALLS (not CLI commands):
|
|
58
|
+
|
|
59
|
+
${toolList}
|
|
60
|
+
|
|
61
|
+
NeuralMemory (nmem_*) is your primary memory system. memory_search and memory_get are legacy aliases that redirect to nmem_recall — prefer nmem_* tools directly for full functionality.
|
|
62
|
+
|
|
63
|
+
These are tool calls, NOT shell commands. Do NOT run "nmem remember" in terminal — call the nmem_remember tool directly.
|
|
64
|
+
|
|
65
|
+
PROACTIVE MEMORY: Use nmem_remember after decisions, errors, and insights. Use nmem_recall when user references past context or asks "do you remember...". Use nmem_remember_batch to store multiple memories at once.`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Config ─────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
type PluginConfig = {
|
|
71
|
+
pythonPath: string;
|
|
72
|
+
brain: string;
|
|
73
|
+
autoContext: boolean;
|
|
74
|
+
autoCapture: boolean;
|
|
75
|
+
contextDepth: number;
|
|
76
|
+
maxContextTokens: number;
|
|
77
|
+
timeout: number;
|
|
78
|
+
initTimeout: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const DEFAULT_CONFIG: Readonly<PluginConfig> = {
|
|
82
|
+
pythonPath: "python",
|
|
83
|
+
brain: "default",
|
|
84
|
+
autoContext: true,
|
|
85
|
+
autoCapture: true,
|
|
86
|
+
contextDepth: 1,
|
|
87
|
+
maxContextTokens: 500,
|
|
88
|
+
timeout: 30_000,
|
|
89
|
+
initTimeout: 90_000,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const BRAIN_NAME_RE = /^[a-zA-Z0-9_\-.]{1,64}$/;
|
|
93
|
+
export const MAX_AUTO_CAPTURE_CHARS = 50_000;
|
|
94
|
+
|
|
95
|
+
export function resolveConfig(raw?: Record<string, unknown>): PluginConfig {
|
|
96
|
+
const merged = { ...DEFAULT_CONFIG, ...(raw ?? {}) };
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
pythonPath:
|
|
100
|
+
typeof merged.pythonPath === "string" && merged.pythonPath.length > 0
|
|
101
|
+
? merged.pythonPath
|
|
102
|
+
: DEFAULT_CONFIG.pythonPath,
|
|
103
|
+
brain:
|
|
104
|
+
typeof merged.brain === "string" && BRAIN_NAME_RE.test(merged.brain)
|
|
105
|
+
? merged.brain
|
|
106
|
+
: DEFAULT_CONFIG.brain,
|
|
107
|
+
autoContext:
|
|
108
|
+
typeof merged.autoContext === "boolean"
|
|
109
|
+
? merged.autoContext
|
|
110
|
+
: DEFAULT_CONFIG.autoContext,
|
|
111
|
+
autoCapture:
|
|
112
|
+
typeof merged.autoCapture === "boolean"
|
|
113
|
+
? merged.autoCapture
|
|
114
|
+
: DEFAULT_CONFIG.autoCapture,
|
|
115
|
+
contextDepth:
|
|
116
|
+
typeof merged.contextDepth === "number" &&
|
|
117
|
+
Number.isInteger(merged.contextDepth) &&
|
|
118
|
+
merged.contextDepth >= 0 &&
|
|
119
|
+
merged.contextDepth <= 3
|
|
120
|
+
? merged.contextDepth
|
|
121
|
+
: DEFAULT_CONFIG.contextDepth,
|
|
122
|
+
maxContextTokens:
|
|
123
|
+
typeof merged.maxContextTokens === "number" &&
|
|
124
|
+
Number.isInteger(merged.maxContextTokens) &&
|
|
125
|
+
merged.maxContextTokens >= 100 &&
|
|
126
|
+
merged.maxContextTokens <= 10_000
|
|
127
|
+
? merged.maxContextTokens
|
|
128
|
+
: DEFAULT_CONFIG.maxContextTokens,
|
|
129
|
+
timeout:
|
|
130
|
+
typeof merged.timeout === "number" &&
|
|
131
|
+
Number.isFinite(merged.timeout) &&
|
|
132
|
+
merged.timeout >= 5_000 &&
|
|
133
|
+
merged.timeout <= 120_000
|
|
134
|
+
? merged.timeout
|
|
135
|
+
: DEFAULT_CONFIG.timeout,
|
|
136
|
+
initTimeout:
|
|
137
|
+
typeof merged.initTimeout === "number" &&
|
|
138
|
+
Number.isFinite(merged.initTimeout) &&
|
|
139
|
+
merged.initTimeout >= 10_000 &&
|
|
140
|
+
merged.initTimeout <= 300_000
|
|
141
|
+
? merged.initTimeout
|
|
142
|
+
: DEFAULT_CONFIG.initTimeout,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Singleton MCP client pool ────────────────────────────────
|
|
147
|
+
// Multiple workspaces may call register() independently, but all
|
|
148
|
+
// should share the same MCP process per (pythonPath, brain) combo.
|
|
149
|
+
|
|
150
|
+
const mcpClients = new Map<string, NeuralMemoryMcpClient>();
|
|
151
|
+
|
|
152
|
+
function getOrCreateMcpClient(
|
|
153
|
+
cfg: PluginConfig,
|
|
154
|
+
logger: PluginLogger,
|
|
155
|
+
): NeuralMemoryMcpClient {
|
|
156
|
+
const key = `${cfg.pythonPath}::${cfg.brain}`;
|
|
157
|
+
|
|
158
|
+
const existing = mcpClients.get(key);
|
|
159
|
+
if (existing) {
|
|
160
|
+
logger.info(`Reusing existing MCP client for brain "${cfg.brain}"`);
|
|
161
|
+
return existing;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const mcp = new NeuralMemoryMcpClient({
|
|
165
|
+
pythonPath: cfg.pythonPath,
|
|
166
|
+
brain: cfg.brain,
|
|
167
|
+
logger,
|
|
168
|
+
timeout: cfg.timeout,
|
|
169
|
+
initTimeout: cfg.initTimeout,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
mcpClients.set(key, mcp);
|
|
173
|
+
return mcp;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Plugin definition ──────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
const plugin: OpenClawPluginDefinition = {
|
|
179
|
+
id: "neuralmemory",
|
|
180
|
+
name: "NeuralMemory",
|
|
181
|
+
description:
|
|
182
|
+
"Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers",
|
|
183
|
+
version: "1.13.0",
|
|
184
|
+
kind: "memory",
|
|
185
|
+
|
|
186
|
+
register(api: OpenClawPluginApi): void {
|
|
187
|
+
const cfg = resolveConfig(api.pluginConfig);
|
|
188
|
+
|
|
189
|
+
const mcp = getOrCreateMcpClient(cfg, api.logger);
|
|
190
|
+
|
|
191
|
+
// ── Register fallback tools synchronously ────────────
|
|
192
|
+
// OpenClaw requires register() to be synchronous.
|
|
193
|
+
// Register stable fallback tools immediately; MCP connection
|
|
194
|
+
// and dynamic tool discovery happen in service.start().
|
|
195
|
+
// Fallback tools auto-reconnect MCP on first call.
|
|
196
|
+
|
|
197
|
+
const registeredTools = createFallbackTools(mcp);
|
|
198
|
+
const compatTools = createCompatibilityTools(mcp);
|
|
199
|
+
|
|
200
|
+
for (const t of [...registeredTools, ...compatTools]) {
|
|
201
|
+
api.registerTool(t, { name: t.name });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
api.logger.info(
|
|
205
|
+
`Registered ${registeredTools.length} NeuralMemory tools + ${compatTools.length} compat shims (sync)`,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// ── Service: MCP process lifecycle ───────────────────
|
|
209
|
+
|
|
210
|
+
api.registerService({
|
|
211
|
+
id: "neuralmemory-mcp",
|
|
212
|
+
|
|
213
|
+
async start(): Promise<void> {
|
|
214
|
+
if (!mcp.connected) {
|
|
215
|
+
try {
|
|
216
|
+
await mcp.connect();
|
|
217
|
+
api.logger.info("NeuralMemory MCP connected in service.start()");
|
|
218
|
+
|
|
219
|
+
// Log discovered tools for diagnostics (cannot re-register
|
|
220
|
+
// after register() — OpenClaw freezes the tool list).
|
|
221
|
+
try {
|
|
222
|
+
const dynamicTools = await createToolsFromMcp(mcp);
|
|
223
|
+
api.logger.info(
|
|
224
|
+
`NeuralMemory MCP discovered ${dynamicTools.length} tools`,
|
|
225
|
+
);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
api.logger.warn(
|
|
228
|
+
`Tool discovery failed: ${(err as Error).message}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
api.logger.error(
|
|
233
|
+
`Failed to start NeuralMemory MCP: ${(err as Error).message}`,
|
|
234
|
+
);
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async stop(): Promise<void> {
|
|
241
|
+
// Remove from singleton pool so next register() creates fresh client
|
|
242
|
+
const key = `${cfg.pythonPath}::${cfg.brain}`;
|
|
243
|
+
mcpClients.delete(key);
|
|
244
|
+
await mcp.close();
|
|
245
|
+
api.logger.info("NeuralMemory MCP service stopped");
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ── Hook: tool awareness + auto-context before agent start ───
|
|
250
|
+
|
|
251
|
+
api.on(
|
|
252
|
+
"before_agent_start",
|
|
253
|
+
async (
|
|
254
|
+
event: unknown,
|
|
255
|
+
_ctx: unknown,
|
|
256
|
+
): Promise<BeforeAgentStartResult | void> => {
|
|
257
|
+
const result: BeforeAgentStartResult = {
|
|
258
|
+
systemPrompt: buildToolInstructions(registeredTools),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (cfg.autoContext && mcp.connected) {
|
|
262
|
+
const ev = event as BeforeAgentStartEvent;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const raw = await mcp.callTool("nmem_recall", {
|
|
266
|
+
query: ev.prompt,
|
|
267
|
+
depth: cfg.contextDepth,
|
|
268
|
+
max_tokens: cfg.maxContextTokens,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const data = JSON.parse(raw) as {
|
|
272
|
+
answer?: string;
|
|
273
|
+
confidence?: number;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (data.answer && (data.confidence ?? 0) > 0.1) {
|
|
277
|
+
result.prependContext = `[NeuralMemory — relevant context]\n${data.answer}`;
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
api.logger.warn(
|
|
281
|
+
`Auto-context failed: ${(err as Error).message}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return result;
|
|
287
|
+
},
|
|
288
|
+
{ priority: 10 },
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// ── Hook: auto-capture after agent completes ────────
|
|
292
|
+
|
|
293
|
+
if (cfg.autoCapture) {
|
|
294
|
+
api.on(
|
|
295
|
+
"agent_end",
|
|
296
|
+
async (event: unknown, _ctx: unknown): Promise<void> => {
|
|
297
|
+
if (!mcp.connected) return;
|
|
298
|
+
|
|
299
|
+
const ev = event as AgentEndEvent;
|
|
300
|
+
if (!ev.success) return;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const messages = ev.messages?.slice(-5) ?? [];
|
|
304
|
+
const text = messages
|
|
305
|
+
.filter(
|
|
306
|
+
(m: unknown): m is { role: string; content: string } =>
|
|
307
|
+
typeof m === "object" &&
|
|
308
|
+
m !== null &&
|
|
309
|
+
(m as { role?: string }).role === "assistant" &&
|
|
310
|
+
typeof (m as { content?: unknown }).content === "string",
|
|
311
|
+
)
|
|
312
|
+
.map((m) => m.content)
|
|
313
|
+
.join("\n")
|
|
314
|
+
.slice(0, MAX_AUTO_CAPTURE_CHARS);
|
|
315
|
+
|
|
316
|
+
if (text.length > 50) {
|
|
317
|
+
await mcp.callTool("nmem_auto", {
|
|
318
|
+
action: "process",
|
|
319
|
+
text,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
api.logger.warn(
|
|
324
|
+
`Auto-capture failed: ${(err as Error).message}`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{ priority: 90 },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Done ────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
api.logger.info(
|
|
335
|
+
`NeuralMemory registered (brain: ${cfg.brain}, ` +
|
|
336
|
+
`autoContext: ${cfg.autoContext}, autoCapture: ${cfg.autoCapture}) — ` +
|
|
337
|
+
`tools will be loaded dynamically from MCP on service start`,
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export default plugin;
|