@nowledge/openclaw-nowledge-mem 0.6.9 → 0.6.12
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/CHANGELOG.md +30 -0
- package/CLAUDE.md +17 -8
- package/README.md +59 -15
- package/SKILL.md +245 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +11 -2
- package/skills/memory-guide/SKILL.md +65 -0
- package/src/client.js +105 -73
- package/src/commands/slash.js +2 -2
- package/src/hooks/behavioral.js +11 -11
- package/src/index.js +1 -1
- package/src/spawn-env.js +19 -0
- package/src/tools/forget.js +2 -2
- package/src/tools/save.js +1 -1
- package/src/tools/status.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.6.12] - 2026-03-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Agent behavioral skill**: the plugin now registers a `skills/memory-guide/SKILL.md` skill via the OpenClaw manifest `skills[]` array. OpenClaw auto-discovers it and presents it to the agent as an available skill. The skill teaches the agent when and how to search, save, explore connections, browse timelines, fetch past conversations, and use Working Memory. This is the single biggest change for memory recall quality: agents now receive structured guidance before their first memory interaction, rather than relying on tool descriptions alone.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Behavioral guidance is now directive**: the always-on system hint now says "Before answering questions about prior work, decisions, dates, people, preferences, or plans: search with memory_search" instead of the previous "When prior context would improve your response, search." This matches the imperative pattern that LLMs follow most reliably, and explicitly notes "natural language queries, not file paths" to prevent confusion with OpenClaw's built-in file-based memory guidance.
|
|
14
|
+
|
|
15
|
+
## [0.6.11] - 2026-03-15
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Release-cycle alignment**: the OpenClaw plugin version now tracks the main Nowledge Mem `0.6.11` release line, so the published plugin and app-facing changelog stay in sync for this release.
|
|
20
|
+
- **Agent-assisted onboarding added**: the plugin now ships with a bundled `SKILL.md` playbook so AI agents can install, configure, verify, and explain the integration without collapsing the human docs into an agent script.
|
|
21
|
+
- **Aligned command execution with OpenClaw runtime**: the plugin now runs `nmem` through OpenClaw's runtime command runner instead of importing `child_process` directly. This removes gateway-blocking synchronous process calls from the plugin surface and matches current OpenClaw plugin abstractions.
|
|
22
|
+
- **Static guidance moved into system-prompt space**: the always-on behavioral hint now uses system-context injection instead of user-prompt prepend text, matching OpenClaw's newer `prependSystemContext` / `appendSystemContext` guidance for cacheable stable instructions.
|
|
23
|
+
- **Minimum supported OpenClaw version is now `2026.3.7`**: this is the first release with the system-context fields the plugin now relies on.
|
|
24
|
+
- **Package publishing is now explicit**: npm contents are pinned through the package manifest instead of relying on `.gitignore` fallback behavior, making releases more deterministic.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Health checks now keep the JSON contract**: the internal `status` probe now uses `--json`, matching the rest of the plugin's parseable CLI surface.
|
|
29
|
+
- **Resolved config now overrides inherited `NMEM_*` values cleanly**: child-process env construction now clears inherited `NMEM_API_URL` and `NMEM_API_KEY` before applying the effective plugin config, so local mode cannot accidentally inherit a stale remote backend.
|
|
30
|
+
- **Failure logs no longer echo full CLI argv**: plugin logs now record a sanitized command label instead of raw `nmem` arguments, avoiding retention of user queries or message batches in log files.
|
|
31
|
+
- **Thread CLI flag drift**: thread search and thread fetch now use `-n`, matching the current `nmem` thread CLI surface and the plugin's own internal docs.
|
|
32
|
+
- **Install and trust docs now match current OpenClaw behavior**: the docs now explain that `openclaw plugins install` enables the plugin and selects the memory slot automatically, and they document the recommended `plugins.allow` hardening path for non-bundled plugins.
|
|
33
|
+
- **Default-mode docs no longer over-promise auto-injected context**: Working Memory and recalled memories are now described as opt-in `sessionContext` behavior, not something every default install gets automatically.
|
|
34
|
+
|
|
5
35
|
## [0.6.9] - 2026-03-05
|
|
6
36
|
|
|
7
37
|
### Fixed
|
package/CLAUDE.md
CHANGED
|
@@ -7,7 +7,8 @@ Continuation guide for `community/nowledge-mem-openclaw-plugin`.
|
|
|
7
7
|
- Plugin target: OpenClaw plugin runtime (memory slot provider)
|
|
8
8
|
- Runtime: JS ESM modules under `src/`, no TS build pipeline
|
|
9
9
|
- Memory backend: `nmem` CLI (fallback: `uvx --from nmem-cli nmem`)
|
|
10
|
-
-
|
|
10
|
+
- OpenClaw minimum: `2026.3.7` (`appendSystemContext` / system-context guidance required)
|
|
11
|
+
- Architecture: **CLI-first via OpenClaw runtime** - all CLI execution goes through `api.runtime.system.runCommandWithTimeout`, not direct `child_process`
|
|
11
12
|
- Remote mode: set `NMEM_API_URL` + `NMEM_API_KEY` env vars or config file `apiUrl` + `apiKey`
|
|
12
13
|
|
|
13
14
|
## Design Philosophy
|
|
@@ -27,7 +28,8 @@ Reflects Nowledge Mem's genuine v0.6 strengths:
|
|
|
27
28
|
```
|
|
28
29
|
src/
|
|
29
30
|
index.js - plugin registration (tools, hooks, commands, CLI)
|
|
30
|
-
client.js - CLI wrapper with API fallback; credential handling
|
|
31
|
+
client.js - CLI wrapper with API fallback; async runtime command execution; credential handling
|
|
32
|
+
spawn-env.js - env-only credential injection for the nmem runner
|
|
31
33
|
config.js - config cascade: ~/.nowledge-mem/openclaw.json > pluginConfig > env vars > defaults
|
|
32
34
|
hooks/
|
|
33
35
|
behavioral.js - always-on behavioral guidance (~50 tokens/turn)
|
|
@@ -43,7 +45,10 @@ src/
|
|
|
43
45
|
forget.js - memory deletion by ID or search
|
|
44
46
|
thread-search.js - search past conversations by keyword
|
|
45
47
|
thread-fetch.js - fetch full messages from a thread with pagination
|
|
46
|
-
|
|
48
|
+
skills/
|
|
49
|
+
memory-guide/
|
|
50
|
+
SKILL.md - agent behavioral skill: when/how to search, save, explore memory (auto-discovered by OpenClaw)
|
|
51
|
+
openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema, skills)
|
|
47
52
|
~/.nowledge-mem/openclaw.json - optional user config file (overrides OpenClaw settings when present)
|
|
48
53
|
```
|
|
49
54
|
|
|
@@ -62,15 +67,19 @@ openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema)
|
|
|
62
67
|
|
|
63
68
|
### Thread Tools (progressive conversation retrieval)
|
|
64
69
|
- `nowledge_mem_thread_search` - search past conversations by keyword. Returns threads with matched message snippets, relevance scores, and message counts. Supports `source` filter.
|
|
65
|
-
- `nowledge_mem_thread_fetch` - fetch
|
|
70
|
+
- `nowledge_mem_thread_fetch` - fetch messages from a specific thread. Start with a small page, then use `offset` + `limit` for progressive retrieval only when more context is needed.
|
|
66
71
|
|
|
67
72
|
### Diagnostics
|
|
68
73
|
- `nowledge_mem_status` - show effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. No parameters.
|
|
69
74
|
|
|
75
|
+
## Skill Surface
|
|
76
|
+
|
|
77
|
+
- `skills/memory-guide/SKILL.md` - agent behavioral skill auto-discovered by OpenClaw via manifest `skills[]`. Teaches the agent when/how to search, save, explore connections, fetch threads, and use Working Memory. Read on-demand when the agent identifies a memory-related task.
|
|
78
|
+
|
|
70
79
|
## Hook Surface
|
|
71
80
|
|
|
72
|
-
- `before_prompt_build` (always-on) - behavioral guidance
|
|
73
|
-
- `before_prompt_build` (sessionContext) - session context: Working Memory + `searchRich()` with `relevanceReason`
|
|
81
|
+
- `before_prompt_build` (always-on) - directive behavioral guidance in system-prompt space: tells agent to search before answering questions about prior work/decisions/preferences, and to save decisions/learnings proactively. Explicitly notes semantic search (not file paths) to counter OpenClaw's hardcoded memory section. Adjusts when sessionContext is on to avoid redundant searches.
|
|
82
|
+
- `before_prompt_build` (sessionContext) - session context: Working Memory + `searchRich()` memories with `relevanceReason`. Note: does NOT inject thread snippets — threads are available via `memory_search` tool and `nowledge_mem_thread_fetch`.
|
|
74
83
|
- `agent_end` - thread capture + LLM triage/distillation (requires `sessionDigest: true`)
|
|
75
84
|
- `after_compaction` - thread append
|
|
76
85
|
- `before_reset` - thread append
|
|
@@ -121,8 +130,8 @@ Legacy aliases accepted silently in all sources for backward compat:
|
|
|
121
130
|
| `nmem --json wm patch --heading "## S" --content/--append` | `client.patchWorkingMemory()` | Section-level WM update |
|
|
122
131
|
| `nmem --json m delete <id>` | `client.execJson()` in forget.js | Delete |
|
|
123
132
|
| `GET /threads/search?query=&limit=` | `client.searchThreads()` | Thread search enrichment (best-effort, API-only) |
|
|
124
|
-
| `nmem --json t search <q>
|
|
125
|
-
| `nmem --json t show <id>
|
|
133
|
+
| `nmem --json t search <q> -n N` | `client.searchThreadsFull()` | Full thread search (CLI-first, API fallback) |
|
|
134
|
+
| `nmem --json t show <id> -n N --offset O` | `client.fetchThread()` | Fetch thread messages with pagination |
|
|
126
135
|
|
|
127
136
|
All commands have API fallback for older CLI versions.
|
|
128
137
|
|
package/README.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Local-first knowledge graph memory for [OpenClaw](https://openclaw.ai) agents, powered by [Nowledge Mem](https://mem.nowledge.co).
|
|
4
4
|
|
|
5
|
-
Your AI tools forget. We remember. Everywhere. This plugin gives
|
|
5
|
+
Your AI tools forget. We remember. Everywhere. This plugin gives OpenClaw both lossless session memory and the same graph-connected memory layer your other AI tools can use too. Every OpenClaw conversation is captured as a searchable thread, important moments can distill into linked memories with `sourceThreadId`, and knowledge from Claude, Cursor, web chats, and imported threads stays searchable inside OpenClaw. Local-first by default, with remote mode when you want a shared server.
|
|
6
|
+
|
|
7
|
+
This is not a flat note store. Nowledge Mem links related knowledge into a graph, tracks how ideas evolve, deduplicates repeated context, and can run background intelligence to produce daily briefings, contradiction checks, and crystals from converging evidence. OpenClaw can search that same growing memory layer instead of starting from scratch every time.
|
|
6
8
|
|
|
7
9
|
## Requirements
|
|
8
10
|
|
|
9
11
|
- [Nowledge Mem](https://mem.nowledge.co) desktop app **or** `nmem` CLI
|
|
10
|
-
- [OpenClaw](https://openclaw.ai) >= 2026.
|
|
12
|
+
- [OpenClaw](https://openclaw.ai) >= 2026.3.7
|
|
11
13
|
|
|
12
14
|
## Installation
|
|
13
15
|
|
|
@@ -15,9 +17,37 @@ Your AI tools forget. We remember. Everywhere. This plugin gives your OpenClaw a
|
|
|
15
17
|
openclaw plugins install @nowledge/openclaw-nowledge-mem
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
OpenClaw's installer writes the install record, enables the plugin, and switches the `memory` slot to `openclaw-nowledge-mem`. Restart OpenClaw after install to load it.
|
|
21
|
+
|
|
22
|
+
### Agent-assisted setup
|
|
23
|
+
|
|
24
|
+
If another AI agent is helping you install this plugin, point it at the bundled [SKILL.md](./SKILL.md).
|
|
25
|
+
|
|
26
|
+
Copyable instruction:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Read https://raw.githubusercontent.com/nowledge-co/community/main/nowledge-mem-openclaw-plugin/SKILL.md and follow it to install, configure, verify, and explain Nowledge Mem for OpenClaw.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That guide is written for AI agents, not for humans. It handles local vs remote mode, optional API auth, trust pinning, restart, verification, and next steps.
|
|
33
|
+
|
|
34
|
+
### Recommended hardening
|
|
35
|
+
|
|
36
|
+
If you keep non-bundled plugins on an explicit trust allowlist, add:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"plugins": {
|
|
41
|
+
"allow": ["openclaw-nowledge-mem"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`plugins.allow` is id-based, not source-based. If you also use `plugins.load.paths` or `openclaw plugins install --link`, review those paths too. An allowlist trusts the active plugin with that id; it does not pin where it came from.
|
|
47
|
+
|
|
48
|
+
### Manual config or verification
|
|
19
49
|
|
|
20
|
-
|
|
50
|
+
If you manage config manually, or you want to verify what the installer selected, this is the expected shape:
|
|
21
51
|
|
|
22
52
|
```json
|
|
23
53
|
{
|
|
@@ -32,7 +62,7 @@ Start Nowledge Mem desktop app or run `nmem serve`, then enable the plugin:
|
|
|
32
62
|
}
|
|
33
63
|
```
|
|
34
64
|
|
|
35
|
-
|
|
65
|
+
By default the agent gets 10 tools on demand, a behavioral skill that teaches it when to search and save, a short always-on system hint, and end-of-session capture. Working Memory injection is optional and controlled by `sessionContext`.
|
|
36
66
|
|
|
37
67
|
### Remote mode
|
|
38
68
|
|
|
@@ -66,7 +96,7 @@ Every user message triggers hooks before the agent sees it, then the agent decid
|
|
|
66
96
|
flowchart TD
|
|
67
97
|
A["User sends message"] --> B["before_prompt_build hook"]
|
|
68
98
|
|
|
69
|
-
B --> C["Behavioral guidance(
|
|
99
|
+
B --> C["Behavioral guidance(cacheable system hint)"]
|
|
70
100
|
B -.->|"sessionContext: true"| D["Working Memory +relevant memories (~1-2 KB)"]
|
|
71
101
|
|
|
72
102
|
C --> E["Agent processes message(10 tools available)"]
|
|
@@ -85,7 +115,7 @@ flowchart TD
|
|
|
85
115
|
|
|
86
116
|
### When Each Tool Gets Called
|
|
87
117
|
|
|
88
|
-
The behavioral hook
|
|
118
|
+
The behavioral skill and always-on hook nudge the agent to **search before answering** and **save after deciding**. Here's when each tool fires:
|
|
89
119
|
|
|
90
120
|
| Scenario | Tool | What happens |
|
|
91
121
|
|----------|------|--------------|
|
|
@@ -160,7 +190,7 @@ Two entry points:
|
|
|
160
190
|
flowchart LR
|
|
161
191
|
subgraph "default" ["Default (recommended)"]
|
|
162
192
|
direction TB
|
|
163
|
-
D1["Every turn:
|
|
193
|
+
D1["Every turn: short system hint"]
|
|
164
194
|
D2["Agent calls 10 tools on demand"]
|
|
165
195
|
D3["Session end: thread capture + LLM distillation"]
|
|
166
196
|
end
|
|
@@ -174,7 +204,7 @@ flowchart LR
|
|
|
174
204
|
|
|
175
205
|
subgraph minimal ["Minimal"]
|
|
176
206
|
direction TB
|
|
177
|
-
M1["Every turn:
|
|
207
|
+
M1["Every turn: short system hint"]
|
|
178
208
|
M2["Agent calls 10 tools on demand"]
|
|
179
209
|
M3["No automatic capture"]
|
|
180
210
|
end
|
|
@@ -262,14 +292,14 @@ The plugin supports three modes. Choose based on how much you want to guarantee
|
|
|
262
292
|
|
|
263
293
|
| Mode | Config | Behavior | Tradeoff |
|
|
264
294
|
|------|--------|----------|----------|
|
|
265
|
-
| **Default** (recommended) | `sessionContext: false, sessionDigest: true` | Agent calls 10 tools on demand.
|
|
266
|
-
| **Session context** | `sessionContext: true` | Working Memory + relevant memories injected at prompt time, plus all 10 tools still available. |
|
|
267
|
-
| **Minimal** | `sessionDigest: false` | Tool-only, no automatic capture. |
|
|
295
|
+
| **Default** (recommended) | `sessionContext: false, sessionDigest: true` | Agent calls 10 tools on demand. A short system hint stays on. Conversations captured + distilled at session end. | Lowest overhead. Agent decides when to search, usually smart, occasionally forgets. |
|
|
296
|
+
| **Session context** | `sessionContext: true` | Working Memory + relevant memories injected at prompt time, plus all 10 tools still available. | Higher per-turn context cost. Guarantees context is present from the first turn. |
|
|
297
|
+
| **Minimal** | `sessionDigest: false` | Tool-only, no automatic capture. | Small overhead from the always-on hint only. Use when you handle memory manually. |
|
|
268
298
|
|
|
269
299
|
**Which mode should you use?**
|
|
270
300
|
|
|
271
301
|
- **Most users**: start with default. The agent gets behavioral guidance nudging it to search before answering and save after deciding. It works well for most conversations.
|
|
272
|
-
- **Short sessions or critical accuracy**: enable `sessionContext`. This guarantees relevant memories are present from the first turn. The agent doesn't need to decide whether to search. The tradeoff is
|
|
302
|
+
- **Short sessions or critical accuracy**: enable `sessionContext`. This guarantees relevant memories are present from the first turn. The agent doesn't need to decide whether to search. The tradeoff is a larger per-turn prompt.
|
|
273
303
|
- **Full manual control**: set `sessionDigest: false`. You control what gets saved (via `/remember` or `nowledge_mem_save`) and nothing is captured automatically.
|
|
274
304
|
|
|
275
305
|
### Session Context (`sessionContext`, default: false)
|
|
@@ -292,7 +322,7 @@ Honest answers to common questions about how the memory system works.
|
|
|
292
322
|
|
|
293
323
|
**Does the agent always search before answering?**
|
|
294
324
|
|
|
295
|
-
|
|
325
|
+
The plugin uses two layers to drive recall. First, a behavioral skill (auto-discovered by OpenClaw) teaches the agent when and how to use memory tools. Second, a short always-on system hint reminds it to "search before answering questions about prior work, decisions, dates, people, preferences, or plans." In practice, modern LLMs follow this directive guidance reliably for knowledge-related questions. For messages that don't need past context (like "hello" or "thanks"), the agent skips the search, which is the right tradeoff. If guaranteed recall matters for your use case, enable `sessionContext: true`. That injects relevant memories at prompt time, before the agent even processes your message.
|
|
296
326
|
|
|
297
327
|
**What stops the agent from saving duplicate memories?**
|
|
298
328
|
|
|
@@ -327,7 +357,7 @@ openclaw nowledge-mem status
|
|
|
327
357
|
|
|
328
358
|
## Configuration
|
|
329
359
|
|
|
330
|
-
No config
|
|
360
|
+
No config is required for a normal npm install. The defaults work for local mode, and `openclaw plugins install` already enables the plugin and selects the memory slot.
|
|
331
361
|
|
|
332
362
|
To change settings, use the OpenClaw plugin settings UI. Changes take effect on restart.
|
|
333
363
|
|
|
@@ -381,6 +411,20 @@ config file > OpenClaw settings > env vars > defaults
|
|
|
381
411
|
|
|
382
412
|
Use `nowledge_mem_status` (or `openclaw nowledge-mem status`) to see where each value comes from.
|
|
383
413
|
|
|
414
|
+
### Trust warning after install
|
|
415
|
+
|
|
416
|
+
If OpenClaw logs a warning like `plugins.allow is empty; discovered non-bundled plugins may auto-load`, the plugin is installed correctly. OpenClaw is warning that you have not pinned trust for non-bundled plugins yet. Add:
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
{
|
|
420
|
+
"plugins": {
|
|
421
|
+
"allow": ["openclaw-nowledge-mem"]
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
If you also use linked or workspace copies, review `plugins.load.paths` before relying on an allowlist. OpenClaw allowlists plugin ids, not install provenance.
|
|
427
|
+
|
|
384
428
|
## What Makes This Different
|
|
385
429
|
|
|
386
430
|
- **Local-first**: no API key, no cloud account. Your knowledge stays on your machine.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Nowledge Mem for OpenClaw
|
|
2
|
+
|
|
3
|
+
Use this guide when an AI agent is helping a user install, configure, verify, or explain the Nowledge Mem OpenClaw plugin.
|
|
4
|
+
|
|
5
|
+
Nowledge Mem is not just "memory for OpenClaw." It is the user's shared memory layer across OpenClaw, Claude Code, Cursor, browser capture, imported threads, and documents. OpenClaw joins that context exchange.
|
|
6
|
+
|
|
7
|
+
## Use This When
|
|
8
|
+
|
|
9
|
+
- The user wants to install or configure the OpenClaw plugin
|
|
10
|
+
- The user wants OpenClaw to connect to existing Nowledge Mem data
|
|
11
|
+
- The user wants local-first memory on this machine
|
|
12
|
+
- The user wants remote or multi-machine memory through a remote `apiUrl`
|
|
13
|
+
- The user wants to verify that OpenClaw can recall, save, and search memory
|
|
14
|
+
|
|
15
|
+
## Do Not Use This When
|
|
16
|
+
|
|
17
|
+
- The user only wants temporary context in the current OpenClaw session
|
|
18
|
+
- The user is troubleshooting unrelated OpenClaw issues
|
|
19
|
+
- The user only wants direct MCP setup for another client
|
|
20
|
+
|
|
21
|
+
## Product Framing
|
|
22
|
+
|
|
23
|
+
When talking to users:
|
|
24
|
+
|
|
25
|
+
- Say "Nowledge Mem gives OpenClaw the same shared memory your other AI tools can use."
|
|
26
|
+
- Lead with cross-tool continuity, not with plugin internals.
|
|
27
|
+
- For local mode, say "No API key is needed for local mode."
|
|
28
|
+
- For remote mode, say "Set `apiUrl`, and add `apiKey` when the server requires auth."
|
|
29
|
+
- Do not imply that OpenClaw is the only source of truth. It is one client of the user's larger memory system.
|
|
30
|
+
- Do not imply that cloud backup is the default. Our default story is local-first.
|
|
31
|
+
|
|
32
|
+
Good user-facing explanation:
|
|
33
|
+
|
|
34
|
+
> OpenClaw can now read the same memory you already built in your other AI tools, and what happens in OpenClaw can enrich that shared memory too.
|
|
35
|
+
|
|
36
|
+
## Definition of Done
|
|
37
|
+
|
|
38
|
+
This task is not complete until all of the following are true:
|
|
39
|
+
|
|
40
|
+
1. `nmem` is available
|
|
41
|
+
2. The intended backend is verified reachable with `nmem`
|
|
42
|
+
3. The OpenClaw plugin is installed
|
|
43
|
+
4. OpenClaw is using the Nowledge Mem memory slot
|
|
44
|
+
5. Remote config is written only if the user chose remote mode
|
|
45
|
+
6. OpenClaw is restarted
|
|
46
|
+
7. `openclaw nowledge-mem status` succeeds
|
|
47
|
+
8. The user receives a short handoff covering:
|
|
48
|
+
- one real first question to ask
|
|
49
|
+
- how to bring in older history
|
|
50
|
+
- local vs remote expectations
|
|
51
|
+
- trust pinning if they see `plugins.allow` warnings
|
|
52
|
+
|
|
53
|
+
## Setup Flow
|
|
54
|
+
|
|
55
|
+
### Step 0 — Choose Local or Remote
|
|
56
|
+
|
|
57
|
+
Ask the user which setup they want:
|
|
58
|
+
|
|
59
|
+
1. Local Nowledge Mem already running on this machine
|
|
60
|
+
2. Existing remote Nowledge Mem server
|
|
61
|
+
|
|
62
|
+
Explain the difference plainly:
|
|
63
|
+
|
|
64
|
+
- Local mode: OpenClaw talks to the local Nowledge Mem app/server through `nmem`
|
|
65
|
+
- Remote mode: OpenClaw talks to a remote Nowledge Mem server through `nmem --api-url ...`; add `apiKey` when that server has auth enabled
|
|
66
|
+
|
|
67
|
+
### Step 1 — Verify `nmem`
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
nmem --version
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If this fails:
|
|
74
|
+
|
|
75
|
+
- For users with the desktop app, direct them to **Settings > Preferences > Developer Tools > Install CLI**
|
|
76
|
+
- Otherwise suggest `pip install nmem-cli`
|
|
77
|
+
|
|
78
|
+
### Step 2 — Verify the Backend Before Plugin Setup
|
|
79
|
+
|
|
80
|
+
Local mode:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
nmem --json status
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Remote mode without auth:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
nmem --json --api-url "$API_URL" status
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Remote mode with auth:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
NMEM_API_KEY="$API_KEY" nmem --json --api-url "$API_URL" status
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Success means the backend responds correctly through `nmem`. Do not skip this step and assume the URL/auth is right.
|
|
99
|
+
|
|
100
|
+
### Step 3 — Install the Plugin
|
|
101
|
+
|
|
102
|
+
Default install:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
openclaw plugins install @nowledge/openclaw-nowledge-mem
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If the user is on mainland China or npmjs is unusually slow, prefer a one-shot mirror override instead of changing global npm config:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
NPMJS_TIME="$(curl -o /dev/null -sS --connect-timeout 2 --max-time 6 -w '%{time_total}' https://registry.npmjs.org/@nowledge%2fopenclaw-nowledge-mem || echo timeout)"
|
|
112
|
+
MIRROR_TIME="$(curl -o /dev/null -sS --connect-timeout 2 --max-time 6 -w '%{time_total}' https://registry.npmmirror.com/@nowledge%2fopenclaw-nowledge-mem || echo timeout)"
|
|
113
|
+
|
|
114
|
+
if [ "$MIRROR_TIME" != "timeout" ] && { [ "$NPMJS_TIME" = "timeout" ] || awk "BEGIN { exit !($NPMJS_TIME > 2 && $MIRROR_TIME < $NPMJS_TIME) }"; }; then
|
|
115
|
+
NPM_CONFIG_REGISTRY=https://registry.npmmirror.com openclaw plugins install @nowledge/openclaw-nowledge-mem
|
|
116
|
+
else
|
|
117
|
+
openclaw plugins install @nowledge/openclaw-nowledge-mem
|
|
118
|
+
fi
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Important facts:
|
|
122
|
+
|
|
123
|
+
- The installer already writes the install record
|
|
124
|
+
- The installer already enables the plugin
|
|
125
|
+
- The installer already switches the `memory` slot to `openclaw-nowledge-mem`
|
|
126
|
+
|
|
127
|
+
Do not tell the user to hand-edit those settings unless they are using a manual/path setup.
|
|
128
|
+
|
|
129
|
+
### Step 4 — Configure Remote Only When Needed
|
|
130
|
+
|
|
131
|
+
Local mode:
|
|
132
|
+
|
|
133
|
+
- No extra config is required
|
|
134
|
+
|
|
135
|
+
Remote mode:
|
|
136
|
+
|
|
137
|
+
- Prefer setting `apiUrl` and `apiKey` in the OpenClaw plugin settings
|
|
138
|
+
- Use `~/.nowledge-mem/openclaw.json` only when the user wants a stable machine-local override or scripted setup
|
|
139
|
+
|
|
140
|
+
`~/.nowledge-mem/openclaw.json` example:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"apiUrl": "https://nowledge.example.com",
|
|
145
|
+
"apiKey": "your-api-key-here"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Important precedence rule:
|
|
150
|
+
|
|
151
|
+
- `~/.nowledge-mem/openclaw.json` overrides OpenClaw plugin settings
|
|
152
|
+
- Then plugin settings override inherited environment variables
|
|
153
|
+
- Do not assume stale `NMEM_API_URL` or `NMEM_API_KEY` env vars should keep winning
|
|
154
|
+
|
|
155
|
+
If the remote server does not require auth, omit `apiKey`.
|
|
156
|
+
|
|
157
|
+
### Step 5 — Pin Trust When Appropriate
|
|
158
|
+
|
|
159
|
+
If OpenClaw warns that `plugins.allow` is empty, or the user wants explicit trust for non-bundled plugins, add:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"plugins": {
|
|
164
|
+
"allow": ["openclaw-nowledge-mem"]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Explain this correctly:
|
|
170
|
+
|
|
171
|
+
- `plugins.allow` trusts the plugin id
|
|
172
|
+
- It does not pin the plugin source path
|
|
173
|
+
- If the user also has linked or workspace copies in `plugins.load.paths`, they should review those too
|
|
174
|
+
|
|
175
|
+
### Step 6 — Restart OpenClaw
|
|
176
|
+
|
|
177
|
+
Restart OpenClaw so the plugin and hooks are active.
|
|
178
|
+
|
|
179
|
+
If an agent is doing the restart automatically, tell the user before triggering it.
|
|
180
|
+
|
|
181
|
+
### Step 7 — Verify the Plugin
|
|
182
|
+
|
|
183
|
+
First check the plugin status:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
openclaw nowledge-mem status
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Then verify in chat.
|
|
190
|
+
|
|
191
|
+
For a clean synthetic check:
|
|
192
|
+
|
|
193
|
+
1. `/remember We chose PostgreSQL for task events`
|
|
194
|
+
2. `/recall PostgreSQL`
|
|
195
|
+
3. `/new`
|
|
196
|
+
4. Ask `What database did we choose for task events?`
|
|
197
|
+
|
|
198
|
+
If the user already has real memory in Nowledge Mem, prefer a real question over a synthetic demo. Good examples:
|
|
199
|
+
|
|
200
|
+
- `What was I working on this week?`
|
|
201
|
+
- `What did we already decide about the task API?`
|
|
202
|
+
- `Find the conversation where we discussed Redis caching`
|
|
203
|
+
|
|
204
|
+
## Handoff Message
|
|
205
|
+
|
|
206
|
+
After setup succeeds, send a short handoff that covers all four points:
|
|
207
|
+
|
|
208
|
+
1. OpenClaw is now connected
|
|
209
|
+
2. The best next question to ask
|
|
210
|
+
3. How to bring in older history
|
|
211
|
+
4. What the local/remote trust model is
|
|
212
|
+
|
|
213
|
+
Good structure:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
OpenClaw is now connected to Nowledge Mem.
|
|
217
|
+
|
|
218
|
+
Try this next:
|
|
219
|
+
- Ask OpenClaw: "What was I working on this week?"
|
|
220
|
+
|
|
221
|
+
If you want more history:
|
|
222
|
+
- Import old AI threads or browser-captured chats into Nowledge Mem so OpenClaw can search them too.
|
|
223
|
+
|
|
224
|
+
What to expect:
|
|
225
|
+
- Local mode needs no API key.
|
|
226
|
+
- Remote mode uses the same apiUrl and, when auth is enabled, the same apiKey you already verified with nmem.
|
|
227
|
+
- If OpenClaw warns that plugins.allow is empty, add openclaw-nowledge-mem to the allowlist to pin trust explicitly.
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Troubleshooting
|
|
231
|
+
|
|
232
|
+
| Symptom | What to do |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `nmem --json status` fails in local mode | Start Nowledge Mem locally first |
|
|
235
|
+
| Remote mode fails | Re-run `nmem --json --api-url ... status` with the exact URL and auth you plan to use |
|
|
236
|
+
| `openclaw nowledge-mem status` fails after install | Restart OpenClaw and confirm the plugin was installed successfully |
|
|
237
|
+
| `plugins.allow is empty` warning | Add `openclaw-nowledge-mem` to `plugins.allow` if the user wants explicit trust |
|
|
238
|
+
| Remote config seems ignored | Check whether `~/.nowledge-mem/openclaw.json` is overriding plugin settings |
|
|
239
|
+
| Local mode unexpectedly talks to a remote server | Check for stale `NMEM_API_URL` / `NMEM_API_KEY` in the environment or an overriding `~/.nowledge-mem/openclaw.json` |
|
|
240
|
+
|
|
241
|
+
## Notes for Agents
|
|
242
|
+
|
|
243
|
+
- Human docs and AI setup instructions are different things. Keep the human explanation short and concrete.
|
|
244
|
+
- Do not dump the whole configuration model on the user up front.
|
|
245
|
+
- Do not reduce Nowledge Mem to "OpenClaw memory." The real value is shared context across tools and growth of knowledge over time.
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowledge/openclaw-nowledge-mem",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
|
|
6
6
|
"author": {
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
"url": "https://github.com/nowledge-co/community.git",
|
|
15
15
|
"directory": "nowledge-mem-openclaw-plugin"
|
|
16
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"skills",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"CLAUDE.md",
|
|
23
|
+
"SKILL.md",
|
|
24
|
+
"openclaw.plugin.json"
|
|
25
|
+
],
|
|
17
26
|
"openclaw": {
|
|
18
27
|
"extensions": ["./src/index.js"]
|
|
19
28
|
},
|
|
@@ -23,7 +32,7 @@
|
|
|
23
32
|
"format": "biome format --write ."
|
|
24
33
|
},
|
|
25
34
|
"peerDependencies": {
|
|
26
|
-
"openclaw": ">=2026.
|
|
35
|
+
"openclaw": ">=2026.3.7"
|
|
27
36
|
},
|
|
28
37
|
"devDependencies": {
|
|
29
38
|
"@biomejs/biome": "^1.9.0"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nowledge-mem-guide
|
|
3
|
+
description: >
|
|
4
|
+
Cross-AI personal knowledge graph memory (Nowledge Mem). Stores memories from this tool,
|
|
5
|
+
Claude Code, Cursor, browser capture, imported documents, and more. Use when: (1) the user
|
|
6
|
+
asks about prior work, decisions, preferences, people, dates, plans, or todos, (2) the user
|
|
7
|
+
references something discussed before or says "remind me" / "what did we decide" / "what was
|
|
8
|
+
I working on", (3) the conversation produces a decision, preference, plan, or learning worth
|
|
9
|
+
keeping, (4) the user wants to browse recent activity or explore how ideas connect, (5) the
|
|
10
|
+
user asks about a past conversation or wants to find a specific thread.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Nowledge Mem
|
|
14
|
+
|
|
15
|
+
## Search (memory_search)
|
|
16
|
+
|
|
17
|
+
Use natural language queries. This is semantic search, not file search.
|
|
18
|
+
|
|
19
|
+
- "database choice for task events" (not "MEMORY.md" or file paths)
|
|
20
|
+
- "meeting with Sarah about API redesign"
|
|
21
|
+
- "deployment procedure for production"
|
|
22
|
+
|
|
23
|
+
Results include `matchedVia` (scoring breakdown), `importance`, `labels`, `sourceThreadId`.
|
|
24
|
+
|
|
25
|
+
When results include `relatedThreads`, use `nowledge_mem_thread_fetch` with the `threadId` for full conversation context. Start with a small page; fetch more via `offset` + `limit` only when needed.
|
|
26
|
+
|
|
27
|
+
Bi-temporal filters narrow results by time:
|
|
28
|
+
|
|
29
|
+
- `event_date_from` / `event_date_to` — when the fact happened
|
|
30
|
+
- `recorded_date_from` / `recorded_date_to` — when it was saved
|
|
31
|
+
|
|
32
|
+
## Save (nowledge_mem_save)
|
|
33
|
+
|
|
34
|
+
Save proactively when the conversation produces a decision, preference, plan, procedure, learning, or important context. Do not wait to be asked.
|
|
35
|
+
|
|
36
|
+
Structure saves:
|
|
37
|
+
|
|
38
|
+
- `unit_type`: fact | preference | decision | plan | procedure | learning | context | event
|
|
39
|
+
- `labels`: lowercase-hyphenated topic tags
|
|
40
|
+
- `importance`: 0.0-1.0 (most: 0.5; key decisions/preferences: 0.7-0.9)
|
|
41
|
+
- `event_start` / `event_end`: ISO dates when temporal
|
|
42
|
+
|
|
43
|
+
Skip trivial exchanges, greetings, and meta-conversation about memory itself.
|
|
44
|
+
|
|
45
|
+
## Connections (nowledge_mem_connections)
|
|
46
|
+
|
|
47
|
+
Pass a `memoryId` from search results to explore related memories, evolution chains, and source documents. Edge types: EVOLVES, CRYSTALLIZED_FROM, SOURCED_FROM, MENTIONS.
|
|
48
|
+
|
|
49
|
+
## Timeline (nowledge_mem_timeline)
|
|
50
|
+
|
|
51
|
+
Browse recent activity grouped by day. Filter by `event_type` or exact `date_from` / `date_to` range.
|
|
52
|
+
|
|
53
|
+
## Threads (nowledge_mem_thread_search + nowledge_mem_thread_fetch)
|
|
54
|
+
|
|
55
|
+
Find past conversations by keyword, then progressively fetch messages. Threads span all sources: this tool, other AI tools, browser capture, imports.
|
|
56
|
+
|
|
57
|
+
## Working Memory (nowledge_mem_context)
|
|
58
|
+
|
|
59
|
+
Read the user's daily briefing (priorities, active projects, flags). Patch a specific section with `patch_section` + `patch_content`/`patch_append` when the conversation updates a priority or resolves a flag.
|
|
60
|
+
|
|
61
|
+
## Important
|
|
62
|
+
|
|
63
|
+
- Search uses natural language queries, not file paths. Never search for "MEMORY.md" or "memory/*.md".
|
|
64
|
+
- When `relatedThreads` appear in results, they often contain the most useful context.
|
|
65
|
+
- Use tools directly; do not tell the user to "check your memory."
|
package/src/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildNmemSpawnEnv } from "./spawn-env.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Patch a single markdown section in a Working Memory document.
|
|
@@ -66,18 +66,21 @@ function patchWmSection(currentContent, heading, { content, append } = {}) {
|
|
|
66
66
|
*
|
|
67
67
|
* Credential rules:
|
|
68
68
|
* - apiUrl: passed to CLI via --api-url flag (not a secret)
|
|
69
|
-
* - apiKey: injected into the
|
|
69
|
+
* - apiKey: injected into the command runner env as NMEM_API_KEY ONLY
|
|
70
70
|
* (never passed as a CLI arg to avoid exposure in `ps aux`)
|
|
71
71
|
* (never logged, even at debug level)
|
|
72
72
|
*/
|
|
73
73
|
export class NowledgeMemClient {
|
|
74
74
|
/**
|
|
75
75
|
* @param {object} logger
|
|
76
|
+
* @param {object} runtimeSystem
|
|
76
77
|
* @param {{ apiUrl?: string; apiKey?: string }} [credentials]
|
|
77
78
|
*/
|
|
78
|
-
constructor(logger, credentials = {}) {
|
|
79
|
+
constructor(logger, runtimeSystem, credentials = {}) {
|
|
79
80
|
this.logger = logger;
|
|
81
|
+
this.runtimeSystem = runtimeSystem;
|
|
80
82
|
this.nmemCmd = null;
|
|
83
|
+
this.nmemCmdPromise = null;
|
|
81
84
|
// Resolved once from config + env (config wins over env, both win over default)
|
|
82
85
|
this._apiUrl =
|
|
83
86
|
(credentials.apiUrl || "").trim() || "http://127.0.0.1:14242";
|
|
@@ -138,28 +141,42 @@ export class NowledgeMemClient {
|
|
|
138
141
|
|
|
139
142
|
// ── CLI helpers ────────────────────────────────────────────────────────────
|
|
140
143
|
|
|
141
|
-
resolveCommand() {
|
|
144
|
+
async resolveCommand() {
|
|
142
145
|
if (this.nmemCmd) return this.nmemCmd;
|
|
146
|
+
if (this.nmemCmdPromise) return this.nmemCmdPromise;
|
|
143
147
|
|
|
144
148
|
const candidates = [["nmem"], ["uvx", "--from", "nmem-cli", "nmem"]];
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
this.nmemCmdPromise = (async () => {
|
|
151
|
+
for (const cmd of candidates) {
|
|
152
|
+
try {
|
|
153
|
+
const result = await this.runtimeSystem.runCommandWithTimeout(
|
|
154
|
+
[...cmd, "--version"],
|
|
155
|
+
{
|
|
156
|
+
timeoutMs: 10_000,
|
|
157
|
+
env: this._spawnEnv(),
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
if ((result.code ?? 1) === 0) {
|
|
161
|
+
this.nmemCmd = cmd;
|
|
162
|
+
this.logger.info(`nmem resolved: ${cmd.join(" ")}`);
|
|
163
|
+
return cmd;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Try the next candidate.
|
|
167
|
+
}
|
|
157
168
|
}
|
|
158
|
-
}
|
|
159
169
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
throw new Error(
|
|
171
|
+
"nmem CLI not found. Install with: pip install nmem-cli (or use uvx)",
|
|
172
|
+
);
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
return await this.nmemCmdPromise;
|
|
177
|
+
} finally {
|
|
178
|
+
this.nmemCmdPromise = null;
|
|
179
|
+
}
|
|
163
180
|
}
|
|
164
181
|
|
|
165
182
|
/**
|
|
@@ -167,15 +184,10 @@ export class NowledgeMemClient {
|
|
|
167
184
|
* apiKey is injected here — NEVER via CLI args.
|
|
168
185
|
*/
|
|
169
186
|
_spawnEnv() {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
if (this._apiKey) {
|
|
176
|
-
env.NMEM_API_KEY = this._apiKey;
|
|
177
|
-
}
|
|
178
|
-
return env;
|
|
187
|
+
return buildNmemSpawnEnv({
|
|
188
|
+
apiUrl: this._apiUrl,
|
|
189
|
+
apiKey: this._apiKey,
|
|
190
|
+
});
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
/**
|
|
@@ -188,41 +200,68 @@ export class NowledgeMemClient {
|
|
|
188
200
|
: [];
|
|
189
201
|
}
|
|
190
202
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const [
|
|
203
|
+
_commandLabel(cmd, args) {
|
|
204
|
+
let i = cmd.length;
|
|
205
|
+
const argv = [...cmd, ...this._apiUrlArgs(), ...args];
|
|
206
|
+
while (i < argv.length) {
|
|
207
|
+
const token = argv[i];
|
|
208
|
+
if (token === "--api-url") {
|
|
209
|
+
i += 2;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
const route = argv
|
|
215
|
+
.slice(i, i + 2)
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.join(" ");
|
|
218
|
+
return route ? `nmem ${route}` : cmd.join(" ");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_summarizeExecError(message) {
|
|
222
|
+
if (!message) return "failed";
|
|
223
|
+
if (message.includes("timed out")) return "timed out";
|
|
224
|
+
const codeMatch = message.match(/exited with code\s+(\d+)/i);
|
|
225
|
+
if (codeMatch) return `exited with code ${codeMatch[1]}`;
|
|
226
|
+
return "failed";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async exec(args, timeout = 30_000) {
|
|
230
|
+
const cmd = await this.resolveCommand();
|
|
231
|
+
const argv = [...cmd, ...this._apiUrlArgs(), ...args];
|
|
232
|
+
const commandLabel = this._commandLabel(cmd, args);
|
|
194
233
|
try {
|
|
195
|
-
const result =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
throw
|
|
234
|
+
const result = await this.runtimeSystem.runCommandWithTimeout(argv, {
|
|
235
|
+
timeoutMs: timeout,
|
|
236
|
+
env: this._spawnEnv(),
|
|
237
|
+
});
|
|
238
|
+
if (
|
|
239
|
+
result.termination === "timeout" ||
|
|
240
|
+
result.termination === "no-output-timeout"
|
|
241
|
+
) {
|
|
242
|
+
const message =
|
|
243
|
+
(result.stderr || result.stdout || "").trim() ||
|
|
244
|
+
`nmem timed out after ${timeout}ms`;
|
|
245
|
+
throw new Error(message);
|
|
207
246
|
}
|
|
208
|
-
if (result.
|
|
247
|
+
if ((result.code ?? 1) !== 0) {
|
|
209
248
|
const message = (result.stderr || result.stdout || "").trim();
|
|
210
249
|
throw new Error(
|
|
211
|
-
message || `nmem exited with code ${String(result.
|
|
250
|
+
message || `nmem exited with code ${String(result.code)}`,
|
|
212
251
|
);
|
|
213
252
|
}
|
|
214
253
|
return String(result.stdout ?? "").trim();
|
|
215
254
|
} catch (err) {
|
|
216
255
|
const message = err instanceof Error ? err.message : String(err);
|
|
217
256
|
this.logger.error(
|
|
218
|
-
`nmem command failed
|
|
257
|
+
`nmem command failed (${commandLabel}): ${this._summarizeExecError(message)}`,
|
|
219
258
|
);
|
|
220
259
|
throw err;
|
|
221
260
|
}
|
|
222
261
|
}
|
|
223
262
|
|
|
224
|
-
execJson(args, timeout = 30_000) {
|
|
225
|
-
const raw = this.exec(args, timeout);
|
|
263
|
+
async execJson(args, timeout = 30_000) {
|
|
264
|
+
const raw = await this.exec(args, timeout);
|
|
226
265
|
return JSON.parse(raw);
|
|
227
266
|
}
|
|
228
267
|
|
|
@@ -237,7 +276,7 @@ export class NowledgeMemClient {
|
|
|
237
276
|
100,
|
|
238
277
|
Math.max(1, Math.trunc(Number(limit) || 5)),
|
|
239
278
|
);
|
|
240
|
-
const data = this.execJson([
|
|
279
|
+
const data = await this.execJson([
|
|
241
280
|
"--json",
|
|
242
281
|
"m",
|
|
243
282
|
"search",
|
|
@@ -296,7 +335,7 @@ export class NowledgeMemClient {
|
|
|
296
335
|
|
|
297
336
|
let data;
|
|
298
337
|
try {
|
|
299
|
-
data = this.execJson(args);
|
|
338
|
+
data = await this.execJson(args);
|
|
300
339
|
} catch (err) {
|
|
301
340
|
// Graceful fallback: CLI doesn't know --event-from yet → use API directly
|
|
302
341
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -388,7 +427,7 @@ export class NowledgeMemClient {
|
|
|
388
427
|
];
|
|
389
428
|
|
|
390
429
|
try {
|
|
391
|
-
return this.execJson(args);
|
|
430
|
+
return await this.execJson(args);
|
|
392
431
|
} catch (err) {
|
|
393
432
|
const message = err instanceof Error ? err.message : String(err);
|
|
394
433
|
const needsFallback =
|
|
@@ -422,7 +461,7 @@ export class NowledgeMemClient {
|
|
|
422
461
|
String(limit),
|
|
423
462
|
];
|
|
424
463
|
try {
|
|
425
|
-
return this.execJson(args);
|
|
464
|
+
return await this.execJson(args);
|
|
426
465
|
} catch (err) {
|
|
427
466
|
const message = err instanceof Error ? err.message : String(err);
|
|
428
467
|
const needsFallback =
|
|
@@ -472,7 +511,7 @@ export class NowledgeMemClient {
|
|
|
472
511
|
if (dateTo) args.push("--to", String(dateTo));
|
|
473
512
|
|
|
474
513
|
try {
|
|
475
|
-
const data = this.execJson(args);
|
|
514
|
+
const data = await this.execJson(args);
|
|
476
515
|
return Array.isArray(data) ? data : (data.events ?? []);
|
|
477
516
|
} catch (err) {
|
|
478
517
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -506,7 +545,7 @@ export class NowledgeMemClient {
|
|
|
506
545
|
if (importance !== undefined && Number.isFinite(Number(importance))) {
|
|
507
546
|
args.push("-i", String(importance));
|
|
508
547
|
}
|
|
509
|
-
const data = this.execJson(args);
|
|
548
|
+
const data = await this.execJson(args);
|
|
510
549
|
return String(data.id ?? data.memory?.id ?? data.memory_id ?? "created");
|
|
511
550
|
}
|
|
512
551
|
|
|
@@ -535,7 +574,7 @@ export class NowledgeMemClient {
|
|
|
535
574
|
if (threadId) {
|
|
536
575
|
args.push("--id", String(threadId));
|
|
537
576
|
}
|
|
538
|
-
data = this.execJson(args);
|
|
577
|
+
data = await this.execJson(args);
|
|
539
578
|
} catch (err) {
|
|
540
579
|
const message = err instanceof Error ? err.message : String(err);
|
|
541
580
|
const needsApiFallback =
|
|
@@ -588,7 +627,7 @@ export class NowledgeMemClient {
|
|
|
588
627
|
if (idempotencyKey) {
|
|
589
628
|
args.push("--idempotency-key", String(idempotencyKey));
|
|
590
629
|
}
|
|
591
|
-
const data = this.execJson(args);
|
|
630
|
+
const data = await this.execJson(args);
|
|
592
631
|
return {
|
|
593
632
|
messagesAdded: Number(data.messages_added ?? 0),
|
|
594
633
|
totalMessages: Number(data.total_messages ?? 0),
|
|
@@ -627,7 +666,7 @@ export class NowledgeMemClient {
|
|
|
627
666
|
if (!id) {
|
|
628
667
|
throw new Error("getMemory requires memoryId");
|
|
629
668
|
}
|
|
630
|
-
return this.execJson(["--json", "m", "show", id]);
|
|
669
|
+
return await this.execJson(["--json", "m", "show", id]);
|
|
631
670
|
}
|
|
632
671
|
|
|
633
672
|
isThreadNotFoundError(err) {
|
|
@@ -643,7 +682,7 @@ export class NowledgeMemClient {
|
|
|
643
682
|
|
|
644
683
|
async readWorkingMemory() {
|
|
645
684
|
try {
|
|
646
|
-
const data = this.execJson(["--json", "wm", "read"], 15_000);
|
|
685
|
+
const data = await this.execJson(["--json", "wm", "read"], 15_000);
|
|
647
686
|
const content = String(data.content ?? "").trim();
|
|
648
687
|
const exists = Boolean(data.exists);
|
|
649
688
|
return { content, available: exists && content.length > 0 };
|
|
@@ -667,7 +706,7 @@ export class NowledgeMemClient {
|
|
|
667
706
|
if (append !== undefined) args.push("--append", String(append));
|
|
668
707
|
|
|
669
708
|
try {
|
|
670
|
-
return this.execJson(args, 20_000);
|
|
709
|
+
return await this.execJson(args, 20_000);
|
|
671
710
|
} catch (err) {
|
|
672
711
|
const message = err instanceof Error ? err.message : String(err);
|
|
673
712
|
const notSupported =
|
|
@@ -772,7 +811,7 @@ export class NowledgeMemClient {
|
|
|
772
811
|
* Unlike searchThreads() (best-effort, swallows errors), this throws on failure
|
|
773
812
|
* and supports additional parameters.
|
|
774
813
|
*
|
|
775
|
-
* Uses CLI: `nmem --json t search "<query>"
|
|
814
|
+
* Uses CLI: `nmem --json t search "<query>" -n N`
|
|
776
815
|
* Falls back to API: GET /threads/search
|
|
777
816
|
*
|
|
778
817
|
* @param {string} query Search keywords
|
|
@@ -789,13 +828,13 @@ export class NowledgeMemClient {
|
|
|
789
828
|
"t",
|
|
790
829
|
"search",
|
|
791
830
|
String(query || ""),
|
|
792
|
-
"
|
|
831
|
+
"-n",
|
|
793
832
|
String(normalizedLimit),
|
|
794
833
|
];
|
|
795
834
|
if (source) args.push("--source", String(source));
|
|
796
835
|
|
|
797
836
|
try {
|
|
798
|
-
const data = this.execJson(args);
|
|
837
|
+
const data = await this.execJson(args);
|
|
799
838
|
return {
|
|
800
839
|
threads: data.threads ?? [],
|
|
801
840
|
totalFound: Number(data.total_found ?? data.total ?? 0),
|
|
@@ -830,7 +869,7 @@ export class NowledgeMemClient {
|
|
|
830
869
|
* Fetch full messages from a specific thread.
|
|
831
870
|
* Supports pagination for progressive retrieval of long conversations.
|
|
832
871
|
*
|
|
833
|
-
* Uses CLI: `nmem --json t show <id>
|
|
872
|
+
* Uses CLI: `nmem --json t show <id> -n N --offset O`
|
|
834
873
|
* Falls back to API: GET /threads/{id}
|
|
835
874
|
*
|
|
836
875
|
* @param {string} threadId Thread ID
|
|
@@ -846,20 +885,13 @@ export class NowledgeMemClient {
|
|
|
846
885
|
);
|
|
847
886
|
const normalizedOffset = Math.max(0, Math.trunc(Number(offset) || 0));
|
|
848
887
|
|
|
849
|
-
const args = [
|
|
850
|
-
"--json",
|
|
851
|
-
"t",
|
|
852
|
-
"show",
|
|
853
|
-
id,
|
|
854
|
-
"--limit",
|
|
855
|
-
String(normalizedLimit),
|
|
856
|
-
];
|
|
888
|
+
const args = ["--json", "t", "show", id, "-n", String(normalizedLimit)];
|
|
857
889
|
if (normalizedOffset > 0) {
|
|
858
890
|
args.push("--offset", String(normalizedOffset));
|
|
859
891
|
}
|
|
860
892
|
|
|
861
893
|
try {
|
|
862
|
-
const data = this.execJson(args);
|
|
894
|
+
const data = await this.execJson(args);
|
|
863
895
|
return {
|
|
864
896
|
threadId: data.thread_id ?? data.id ?? id,
|
|
865
897
|
title: data.title ?? "(untitled)",
|
|
@@ -906,7 +938,7 @@ export class NowledgeMemClient {
|
|
|
906
938
|
|
|
907
939
|
async checkHealth() {
|
|
908
940
|
try {
|
|
909
|
-
this.
|
|
941
|
+
await this.execJson(["--json", "status"]);
|
|
910
942
|
return true;
|
|
911
943
|
} catch {
|
|
912
944
|
return false;
|
package/src/commands/slash.js
CHANGED
|
@@ -38,7 +38,7 @@ export function createForgetCommand(client, logger) {
|
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
40
|
if (isLikelyId) {
|
|
41
|
-
client.exec(["--json", "m", "delete", "-f", text]);
|
|
41
|
+
await client.exec(["--json", "m", "delete", "-f", text]);
|
|
42
42
|
logger.info(`/forget: deleted memory ${text}`);
|
|
43
43
|
return { text: `Forgotten: memory ${text} deleted.` };
|
|
44
44
|
}
|
|
@@ -50,7 +50,7 @@ export function createForgetCommand(client, logger) {
|
|
|
50
50
|
|
|
51
51
|
if (results[0].score >= 0.85) {
|
|
52
52
|
const target = results[0];
|
|
53
|
-
client.exec(["--json", "m", "delete", "-f", target.id]);
|
|
53
|
+
await client.exec(["--json", "m", "delete", "-f", target.id]);
|
|
54
54
|
logger.info(`/forget: deleted memory ${target.id}`);
|
|
55
55
|
const preview = target.title || target.content.slice(0, 60);
|
|
56
56
|
return {
|
package/src/hooks/behavioral.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the single most impactful change for memory adoption:
|
|
5
5
|
* most LLMs ignore "call this proactively" in tool descriptions,
|
|
6
|
-
* but reliably follow behavioral guidance in
|
|
6
|
+
* but reliably follow behavioral guidance in system-prompt space.
|
|
7
7
|
*
|
|
8
8
|
* Cost: ~50 tokens per turn. Negligible.
|
|
9
9
|
*
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
const BASE_GUIDANCE = [
|
|
16
16
|
"<nowledge-mem-guidance>",
|
|
17
17
|
"You have access to the user's personal knowledge graph (Nowledge Mem).",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"When
|
|
18
|
+
"Before answering questions about prior work, decisions, dates, people, preferences, or plans:",
|
|
19
|
+
"search with memory_search using natural language queries (not file paths).",
|
|
20
|
+
"memory_search returns memories with scores, labels, and relevant past conversation snippets.",
|
|
21
|
+
"When results include a sourceThreadId, use nowledge_mem_thread_fetch for full conversation context.",
|
|
22
|
+
"When the conversation produces a decision, preference, plan, or learning, save it with nowledge_mem_save.",
|
|
23
23
|
"</nowledge-mem-guidance>",
|
|
24
24
|
].join("\n");
|
|
25
25
|
|
|
@@ -27,10 +27,10 @@ const SESSION_CONTEXT_GUIDANCE = [
|
|
|
27
27
|
"<nowledge-mem-guidance>",
|
|
28
28
|
"You have access to the user's personal knowledge graph (Nowledge Mem).",
|
|
29
29
|
"Relevant memories and your Working Memory have already been injected into this prompt.",
|
|
30
|
-
"Use memory_search
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"When
|
|
30
|
+
"Use memory_search when you need something specific beyond what was auto-recalled (natural language queries, not file paths).",
|
|
31
|
+
"memory_search also returns relevant past conversation snippets alongside memories.",
|
|
32
|
+
"When results include a sourceThreadId, use nowledge_mem_thread_fetch for full conversation context.",
|
|
33
|
+
"When the conversation produces a decision, preference, plan, or learning, save it with nowledge_mem_save.",
|
|
34
34
|
"</nowledge-mem-guidance>",
|
|
35
35
|
].join("\n");
|
|
36
36
|
|
|
@@ -38,6 +38,6 @@ export function buildBehavioralHook(logger, { sessionContext = false } = {}) {
|
|
|
38
38
|
const guidance = sessionContext ? SESSION_CONTEXT_GUIDANCE : BASE_GUIDANCE;
|
|
39
39
|
return (_event, _ctx) => {
|
|
40
40
|
logger.debug?.("behavioral: injecting guidance");
|
|
41
|
-
return {
|
|
41
|
+
return { appendSystemContext: guidance };
|
|
42
42
|
};
|
|
43
43
|
}
|
package/src/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export default {
|
|
|
33
33
|
register(api) {
|
|
34
34
|
const logger = api.logger;
|
|
35
35
|
const cfg = parseConfig(api.pluginConfig, logger);
|
|
36
|
-
const client = new NowledgeMemClient(logger, {
|
|
36
|
+
const client = new NowledgeMemClient(logger, api.runtime.system, {
|
|
37
37
|
apiUrl: cfg.apiUrl,
|
|
38
38
|
apiKey: cfg.apiKey,
|
|
39
39
|
});
|
package/src/spawn-env.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const LOCAL_API_URL = "http://127.0.0.1:14242";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the child env for nmem CLI execution.
|
|
5
|
+
* apiKey stays in env only and is never passed as a CLI flag.
|
|
6
|
+
* Clear inherited NMEM_* values first so the resolved plugin config wins.
|
|
7
|
+
*/
|
|
8
|
+
export function buildNmemSpawnEnv({ apiUrl, apiKey } = {}) {
|
|
9
|
+
const env = { ...process.env };
|
|
10
|
+
env.NMEM_API_URL = undefined;
|
|
11
|
+
env.NMEM_API_KEY = undefined;
|
|
12
|
+
if (apiUrl && apiUrl !== LOCAL_API_URL) {
|
|
13
|
+
env.NMEM_API_URL = apiUrl;
|
|
14
|
+
}
|
|
15
|
+
if (apiKey) {
|
|
16
|
+
env.NMEM_API_KEY = apiKey;
|
|
17
|
+
}
|
|
18
|
+
return env;
|
|
19
|
+
}
|
package/src/tools/forget.js
CHANGED
|
@@ -46,7 +46,7 @@ export function createForgetTool(client, logger) {
|
|
|
46
46
|
// Mode 1: Direct delete by ID
|
|
47
47
|
if (memoryId) {
|
|
48
48
|
try {
|
|
49
|
-
client.exec(["--json", "m", "delete", "-f", memoryId]);
|
|
49
|
+
await client.exec(["--json", "m", "delete", "-f", memoryId]);
|
|
50
50
|
logger.info(`forget: deleted memory ${memoryId}`);
|
|
51
51
|
return {
|
|
52
52
|
content: [
|
|
@@ -92,7 +92,7 @@ export function createForgetTool(client, logger) {
|
|
|
92
92
|
if (results.length === 1 && results[0].score >= 0.85) {
|
|
93
93
|
const target = results[0];
|
|
94
94
|
try {
|
|
95
|
-
client.exec(["--json", "m", "delete", "-f", target.id]);
|
|
95
|
+
await client.exec(["--json", "m", "delete", "-f", target.id]);
|
|
96
96
|
logger.info(`forget: deleted memory ${target.id} via search`);
|
|
97
97
|
return {
|
|
98
98
|
content: [
|
package/src/tools/save.js
CHANGED
|
@@ -169,7 +169,7 @@ export function createSaveTool(client, logger) {
|
|
|
169
169
|
if (eventEnd) args.push("--event-end", eventEnd);
|
|
170
170
|
if (temporalContext) args.push("--when", temporalContext);
|
|
171
171
|
|
|
172
|
-
const data = client.execJson(args);
|
|
172
|
+
const data = await client.execJson(args);
|
|
173
173
|
const id = String(
|
|
174
174
|
data.id ?? data.memory?.id ?? data.memory_id ?? "created",
|
|
175
175
|
);
|
package/src/tools/status.js
CHANGED
|
@@ -37,7 +37,7 @@ export function createStatusTool(client, _logger, cfg) {
|
|
|
37
37
|
|
|
38
38
|
// 2. CLI resolution
|
|
39
39
|
try {
|
|
40
|
-
const cmd = client.resolveCommand();
|
|
40
|
+
const cmd = await client.resolveCommand();
|
|
41
41
|
details.cli = cmd.join(" ");
|
|
42
42
|
lines.push(`CLI: ${cmd.join(" ")}`);
|
|
43
43
|
} catch {
|