@nowledge/openclaw-nowledge-mem 0.6.6 → 0.6.8

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 CHANGED
@@ -2,15 +2,38 @@
2
2
 
3
3
  All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
4
4
 
5
+ ## [0.6.8] - 2026-02-27
6
+
7
+ ### Changed
8
+
9
+ - **Config priority flipped**: `~/.nowledge-mem/openclaw.json` now takes priority over OpenClaw plugin settings. Most users configure via OpenClaw UI (no file needed). Users who create the file get predictable overrides.
10
+ - **No auto-created config file**: The plugin no longer creates `~/.nowledge-mem/openclaw.json` on first startup. OpenClaw's settings UI works out of the box. Create the file manually when you need persistent or scripted config.
11
+ - **Status tool shows config sources**: `nowledge_mem_status` now shows where each setting comes from (`file`, `pluginConfig`, `env`, or `default`).
12
+
13
+ ### Fixed
14
+
15
+ - **configSchema broke OpenClaw form UI**: `additionalProperties: true` (added in 0.6.7) caused OpenClaw's config form to show "Unsupported schema mode. Use Raw mode." Removed the field entirely - JSON Schema defaults to allowing additional properties when omitted, so typos still don't crash the plugin, and the form renders normally.
16
+
17
+ ## [0.6.7] - 2026-02-27
18
+
19
+ ### Added
20
+
21
+ - `nowledge_mem_status` tool: verify effective config, backend connectivity, and version. Shows mode (local/remote), whether API key is set, backend health, and all config values. 10 tools total.
22
+
23
+ ### Fixed
24
+
25
+ - **configSchema too strict**: `additionalProperties` changed from `false` to `true`. Previously, a typo in openclaw.json config (e.g. `sesionContext`) caused the plugin to silently fail to load. Now OpenClaw passes validation and the plugin's own parser gives a descriptive error listing valid keys.
26
+ - **apiUrl port typo**: configSchema description had port 14142 instead of 14242.
27
+
5
28
  ## [0.6.6] - 2026-02-27
6
29
 
7
30
  ### Changed - Config file + env var cascade
8
31
 
9
32
  **Config file at `~/.nowledge-mem/openclaw.json`**
10
33
 
11
- The plugin now reads configuration from a dedicated config file, independent of OpenClaw's `openclaw.json`. The file is auto-created on first startup with sensible defaults. If you had settings in OpenClaw's plugin config, those values are automatically migrated into the file on first run. No manual steps needed.
34
+ The plugin now reads configuration from a dedicated config file, independent of OpenClaw's `openclaw.json`. The file is auto-created on first startup with sensible defaults. If you had settings in OpenClaw's plugin config, those values are automatically seeded into the file on first run. No manual steps needed.
12
35
 
13
- This ensures your settings survive OpenClaw version upgrades that may strip custom plugin config fields (observed in OpenClaw >= 2026.2.25).
36
+ This gives you a single, easy-to-find place for all Nowledge Mem settings. The file persists across plugin reinstalls and OpenClaw upgrades.
14
37
 
15
38
  **Config cascade** (4 layers, highest to lowest priority):
16
39
  1. `api.pluginConfig` - from OpenClaw's plugin config (when supported)
@@ -34,7 +57,7 @@ This ensures your settings survive OpenClaw version upgrades that may strip cust
34
57
 
35
58
  ### Added - Thread tools + sourceThreadId linkage
36
59
 
37
- **Two new tools for conversation retrieval.** The plugin now provides 9 tools total.
60
+ **Two new tools for conversation retrieval.**
38
61
 
39
62
  - `nowledge_mem_thread_search` - search past conversations by keyword. Returns matched threads with message snippets and relevance scores. Use when the user asks about a past discussion or wants to find a specific conversation.
40
63
  - `nowledge_mem_thread_fetch` - fetch full messages from a specific thread with pagination support. Use to progressively retrieve long conversations. Accepts `offset` and `limit` for paginated retrieval.
package/CLAUDE.md CHANGED
@@ -28,7 +28,7 @@ Reflects Nowledge Mem's genuine v0.6 strengths:
28
28
  src/
29
29
  index.js - plugin registration (tools, hooks, commands, CLI)
30
30
  client.js - CLI wrapper with API fallback; credential handling
31
- config.js - config cascade: pluginConfig > ~/.nowledge-mem/openclaw.json > env vars > defaults
31
+ config.js - config cascade: ~/.nowledge-mem/openclaw.json > pluginConfig > env vars > defaults
32
32
  hooks/
33
33
  behavioral.js - always-on behavioral guidance (~50 tokens/turn)
34
34
  recall.js - before_prompt_build: inject Working Memory + recalled memories
@@ -44,10 +44,10 @@ src/
44
44
  thread-search.js - search past conversations by keyword
45
45
  thread-fetch.js - fetch full messages from a thread with pagination
46
46
  openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema)
47
- ~/.nowledge-mem/openclaw.json - user config file (auto-created on first run)
47
+ ~/.nowledge-mem/openclaw.json - optional user config file (overrides OpenClaw settings when present)
48
48
  ```
49
49
 
50
- ## Tool Surface (9 tools)
50
+ ## Tool Surface (10 tools)
51
51
 
52
52
  ### OpenClaw Memory Slot (required for system prompt activation)
53
53
  - `memory_search` - multi-signal: BM25 + embedding + label + graph + decay. Returns `matchedVia` ("Text Match 100% + Semantic 69%"), `importance`, bi-temporal filters (`event_date_from/to`, `recorded_date_from/to`). Also returns `relatedThreads` (past conversation snippets matching the query) and `sourceThreadId` (link to source conversation). Mode: `"multi-signal"`.
@@ -64,6 +64,9 @@ openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema)
64
64
  - `nowledge_mem_thread_search` - search past conversations by keyword. Returns threads with matched message snippets, relevance scores, and message counts. Supports `source` filter.
65
65
  - `nowledge_mem_thread_fetch` - fetch full messages from a specific thread. Supports pagination via `offset` + `limit` for progressive retrieval of long conversations.
66
66
 
67
+ ### Diagnostics
68
+ - `nowledge_mem_status` - show effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. No parameters.
69
+
67
70
  ## Hook Surface
68
71
 
69
72
  - `before_prompt_build` (always-on) - behavioral guidance: brief note telling agent to save/search proactively. Adjusts when sessionContext is on to avoid redundant searches.
@@ -80,7 +83,7 @@ openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema)
80
83
 
81
84
  ## Config Keys
82
85
 
83
- Config file at `~/.nowledge-mem/openclaw.json` (auto-created on first run). Also supports env vars and pluginConfig.
86
+ Optional config file at `~/.nowledge-mem/openclaw.json`. Falls through to OpenClaw plugin settings when the file is absent.
84
87
 
85
88
  | Key | Type | Default | Env Var | Description |
86
89
  |-----|------|---------|---------|-------------|
@@ -91,7 +94,9 @@ Config file at `~/.nowledge-mem/openclaw.json` (auto-created on first run). Also
91
94
  | `apiUrl` | string | `""` | `NMEM_API_URL` | Remote server URL. Empty = local (127.0.0.1:14242) |
92
95
  | `apiKey` | string | `""` | `NMEM_API_KEY` | API key. Never logged. |
93
96
 
94
- **Priority**: pluginConfig > config file > env var > default.
97
+ **Priority**: config file > pluginConfig > env var > default.
98
+
99
+ `parseConfig()` returns `_sources` object tracking where each value came from (`"file"`, `"pluginConfig"`, `"env"`, `"default"`). Used by `nowledge_mem_status` tool.
95
100
 
96
101
  Legacy aliases accepted silently in all sources for backward compat:
97
102
  `autoRecall`→`sessionContext`, `autoCapture`→`sessionDigest`, `captureMinInterval`→`digestMinInterval`, `maxRecallResults`→`maxContextResults`.
@@ -99,7 +104,6 @@ Legacy aliases accepted silently in all sources for backward compat:
99
104
  ### Credential Handling Rules
100
105
  - `apiKey` → ONLY via child process env (`NMEM_API_KEY`). Never CLI arg, never logged.
101
106
  - `apiUrl` → passed as `--api-url` flag to CLI (not a secret).
102
- - Config values win over environment variables.
103
107
  - `_spawnEnv()` builds per-spawn env; `_apiUrlArgs()` adds `--api-url` when non-default.
104
108
 
105
109
  ## CLI Surface (nmem commands used by plugin)
package/README.md CHANGED
@@ -32,13 +32,15 @@ Start Nowledge Mem desktop app or run `nmem serve`, then enable the plugin:
32
32
  }
33
33
  ```
34
34
 
35
- That's it. The agent gets 9 tools and calls them on demand. No extra tokens wasted.
35
+ That's it. The agent gets 10 tools and calls them on demand. No extra tokens wasted.
36
36
 
37
37
  ### Remote mode
38
38
 
39
39
  Connect to a Nowledge Mem server running elsewhere (a VPS, a home server, or a shared team instance). See [remote access guide](https://mem.nowledge.co/docs/remote-access) for server setup.
40
40
 
41
- Enable the plugin the same way as local mode, then set your remote server in `~/.nowledge-mem/openclaw.json`:
41
+ Enable the plugin the same way as local mode, then set `apiUrl` and `apiKey` in the OpenClaw plugin settings.
42
+
43
+ Or use a config file at `~/.nowledge-mem/openclaw.json`:
42
44
 
43
45
  ```json
44
46
  {
@@ -62,7 +64,7 @@ flowchart TD
62
64
  B --> C["Behavioral guidance(always, ~110 tokens)"]
63
65
  B -.->|"sessionContext: true"| D["Working Memory +relevant memories (~1-2 KB)"]
64
66
 
65
- C --> E["Agent processes message(9 tools available)"]
67
+ C --> E["Agent processes message(10 tools available)"]
66
68
  D --> E
67
69
 
68
70
  E --> F{"Needs past context?"}
@@ -91,6 +93,7 @@ The behavioral hook nudges the agent to **search before answering** and **save a
91
93
  | Memory has `sourceThreadId` | `nowledge_mem_thread_fetch` | Fetch full source conversation. Pagination via `offset` + `limit`. |
92
94
  | "Find our discussion about X" | `nowledge_mem_thread_search` | Search past conversations by keyword. Returns matched snippets. |
93
95
  | "Forget X" | `nowledge_mem_forget` | Delete by ID or search query. |
96
+ | "Is my setup working?" | `nowledge_mem_status` | Show effective config, backend connectivity, and version. |
94
97
 
95
98
  ### Session Lifecycle (Automatic Capture)
96
99
 
@@ -153,21 +156,21 @@ flowchart LR
153
156
  subgraph "default" ["Default (recommended)"]
154
157
  direction TB
155
158
  D1["Every turn: behavioral guidance (~50 tokens)"]
156
- D2["Agent calls 9 tools on demand"]
159
+ D2["Agent calls 10 tools on demand"]
157
160
  D3["Session end: thread capture + LLM distillation"]
158
161
  end
159
162
 
160
163
  subgraph context ["Session Context"]
161
164
  direction TB
162
165
  C1["Every turn: guidance + Working Memory+ recalled memories (~1-2 KB)"]
163
- C2["Agent calls 9 tools on demand"]
166
+ C2["Agent calls 10 tools on demand"]
164
167
  C3["Session end: thread capture + LLM distillation"]
165
168
  end
166
169
 
167
170
  subgraph minimal ["Minimal"]
168
171
  direction TB
169
172
  M1["Every turn: behavioral guidance (~50 tokens)"]
170
- M2["Agent calls 9 tools on demand"]
173
+ M2["Agent calls 10 tools on demand"]
171
174
  M3["No automatic capture"]
172
175
  end
173
176
  ```
@@ -254,8 +257,8 @@ The plugin supports three modes. Choose based on how much you want to guarantee
254
257
 
255
258
  | Mode | Config | Behavior | Tradeoff |
256
259
  |------|--------|----------|----------|
257
- | **Default** (recommended) | `sessionContext: false, sessionDigest: true` | Agent calls 9 tools on demand. Behavioral guidance every turn (~50 tokens). Conversations captured + distilled at session end. | Lowest cost. Agent decides when to search, usually smart, occasionally forgets. |
258
- | **Session context** | `sessionContext: true` | Working Memory + relevant memories injected at prompt time, plus all 9 tools still available. | ~1-2 KB/turn. Guarantees context is always present. Best for short sessions or critical workflows. |
260
+ | **Default** (recommended) | `sessionContext: false, sessionDigest: true` | Agent calls 10 tools on demand. Behavioral guidance every turn (~50 tokens). Conversations captured + distilled at session end. | Lowest cost. Agent decides when to search, usually smart, occasionally forgets. |
261
+ | **Session context** | `sessionContext: true` | Working Memory + relevant memories injected at prompt time, plus all 10 tools still available. | ~1-2 KB/turn. Guarantees context is always present. Best for short sessions or critical workflows. |
259
262
  | **Minimal** | `sessionDigest: false` | Tool-only, no automatic capture. | Zero overhead beyond guidance. Use when you handle memory manually. |
260
263
 
261
264
  **Which mode should you use?**
@@ -319,7 +322,22 @@ openclaw nowledge-mem status
319
322
 
320
323
  ## Configuration
321
324
 
322
- On first startup, the plugin creates a config file at `~/.nowledge-mem/openclaw.json` with defaults. Edit it to customize:
325
+ No config needed to get started. The defaults work for local mode.
326
+
327
+ To change settings, use the OpenClaw plugin settings UI. Changes take effect on restart.
328
+
329
+ | Key | Type | Default | Description |
330
+ |-----|------|---------|-------------|
331
+ | `sessionContext` | boolean | `false` | Inject Working Memory + relevant memories at prompt time |
332
+ | `sessionDigest` | boolean | `true` | Thread capture + LLM distillation at session end |
333
+ | `digestMinInterval` | integer | `300` | Minimum seconds between session digests for the same thread (0-86400) |
334
+ | `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
335
+ | `apiUrl` | string | `""` | Remote server URL. Empty = local (`http://127.0.0.1:14242`) |
336
+ | `apiKey` | string | `""` | API key for remote access. Injected as `NMEM_API_KEY` env var, never logged |
337
+
338
+ ### Advanced: config file
339
+
340
+ For persistent or scripted config, create `~/.nowledge-mem/openclaw.json`:
323
341
 
324
342
  ```json
325
343
  {
@@ -332,16 +350,7 @@ On first startup, the plugin creates a config file at `~/.nowledge-mem/openclaw.
332
350
  }
333
351
  ```
334
352
 
335
- Edit the file and restart OpenClaw. The plugin reads it on every startup.
336
-
337
- | Key | Type | Default | Description |
338
- |-----|------|---------|-------------|
339
- | `sessionContext` | boolean | `false` | Inject Working Memory + relevant memories at prompt time |
340
- | `sessionDigest` | boolean | `true` | Thread capture + LLM distillation at session end |
341
- | `digestMinInterval` | integer | `300` | Minimum seconds between session digests for the same thread (0-86400) |
342
- | `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
343
- | `apiUrl` | string | `""` | Remote server URL. Empty = local (`http://127.0.0.1:14242`) |
344
- | `apiKey` | string | `""` | API key for remote access. Injected as `NMEM_API_KEY` env var, never logged |
353
+ The config file takes priority over OpenClaw settings. Only the keys you include are overridden; missing keys fall through to OpenClaw settings, then env vars, then defaults.
345
354
 
346
355
  ### Advanced: environment variables
347
356
 
@@ -356,7 +365,13 @@ NMEM_API_URL=https://...
356
365
  NMEM_API_KEY=your-key
357
366
  ```
358
367
 
359
- **Priority**: OpenClaw plugin config > config file > env vars > defaults.
368
+ ### Priority
369
+
370
+ ```
371
+ config file > OpenClaw settings > env vars > defaults
372
+ ```
373
+
374
+ Use `nowledge_mem_status` (or `openclaw nowledge-mem status`) to see where each value comes from.
360
375
 
361
376
  ## What Makes This Different
362
377
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-nowledge-mem",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "kind": "memory",
5
5
  "uiHints": {
6
6
  "sessionContext": {
@@ -66,7 +66,6 @@
66
66
  "default": "",
67
67
  "description": "API key for remote access. Stored in plugin config, injected as NMEM_API_KEY env var. Never logged."
68
68
  }
69
- },
70
- "additionalProperties": false
69
+ }
71
70
  }
72
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "type": "module",
5
5
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
6
6
  "author": {
package/src/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -42,69 +42,14 @@ const ALIAS_KEYS = {
42
42
  const CONFIG_DIR = join(homedir(), ".nowledge-mem");
43
43
  const CONFIG_PATH = join(CONFIG_DIR, "openclaw.json");
44
44
 
45
- const DEFAULT_CONFIG = {
46
- sessionContext: false,
47
- sessionDigest: true,
48
- digestMinInterval: 300,
49
- maxContextResults: 5,
50
- apiUrl: "",
51
- apiKey: "",
52
- };
53
-
54
45
  /**
55
- * Read ~/.nowledge-mem/openclaw.json.
56
- * On first run, creates the file seeded with values from pluginConfig
57
- * (if any) — this migrates settings before OpenClaw strips them.
58
- * Returns {} on any error — never crashes.
46
+ * Read ~/.nowledge-mem/openclaw.json if it exists.
47
+ * Returns {} when the file is missing or unreadable.
59
48
  */
60
- function readConfigFile(logger, pluginCfg) {
49
+ function readConfigFile(logger) {
61
50
  try {
62
51
  if (!existsSync(CONFIG_PATH)) {
63
- // Seed from pluginConfig + env vars for migration.
64
- // Users on older OpenClaw get their values written to the file
65
- // BEFORE a future OpenClaw update can strip them.
66
- const initial = { ...DEFAULT_CONFIG };
67
- if (pluginCfg && typeof pluginCfg === "object") {
68
- // Resolve aliases in pluginConfig for seeding
69
- const resolved = { ...pluginCfg };
70
- for (const [oldKey, newKey] of Object.entries(ALIAS_KEYS)) {
71
- if (oldKey in resolved) {
72
- if (!(newKey in resolved)) {
73
- resolved[newKey] = resolved[oldKey];
74
- }
75
- delete resolved[oldKey];
76
- }
77
- }
78
-
79
- if (typeof resolved.sessionContext === "boolean")
80
- initial.sessionContext = resolved.sessionContext;
81
- if (typeof resolved.sessionDigest === "boolean")
82
- initial.sessionDigest = resolved.sessionDigest;
83
- if (
84
- typeof resolved.digestMinInterval === "number" &&
85
- Number.isFinite(resolved.digestMinInterval)
86
- )
87
- initial.digestMinInterval = resolved.digestMinInterval;
88
- if (
89
- typeof resolved.maxContextResults === "number" &&
90
- Number.isFinite(resolved.maxContextResults)
91
- )
92
- initial.maxContextResults = resolved.maxContextResults;
93
- if (typeof resolved.apiUrl === "string" && resolved.apiUrl.trim())
94
- initial.apiUrl = resolved.apiUrl.trim();
95
- // apiKey intentionally NOT written to disk — keep in env/pluginConfig only
96
- }
97
-
98
- mkdirSync(CONFIG_DIR, { recursive: true });
99
- writeFileSync(
100
- CONFIG_PATH,
101
- JSON.stringify(initial, null, 2) + "\n",
102
- "utf-8",
103
- );
104
- logger?.info?.(
105
- `nowledge-mem: created config at ${CONFIG_PATH} — edit to customize`,
106
- );
107
- return { ...initial };
52
+ return {};
108
53
  }
109
54
  const content = readFileSync(CONFIG_PATH, "utf-8").trim();
110
55
  if (!content) return {};
@@ -148,7 +93,8 @@ function pickBool(obj, key) {
148
93
  }
149
94
 
150
95
  function pickNum(obj, key) {
151
- if (typeof obj[key] === "number" && Number.isFinite(obj[key])) return obj[key];
96
+ if (typeof obj[key] === "number" && Number.isFinite(obj[key]))
97
+ return obj[key];
152
98
  return undefined;
153
99
  }
154
100
 
@@ -166,11 +112,27 @@ function resolveAliases(obj) {
166
112
  return resolved;
167
113
  }
168
114
 
115
+ /**
116
+ * Return the first option whose value is not undefined.
117
+ * Each option is { value, source } where source is a label like
118
+ * "file", "pluginConfig", "env", or "default".
119
+ */
120
+ function firstDefined(...options) {
121
+ for (const opt of options) {
122
+ if (opt.value !== undefined) return opt;
123
+ }
124
+ return options[options.length - 1];
125
+ }
126
+
169
127
  // -------------------------------------------------------------------------
170
128
 
171
129
  /**
172
130
  * Parse plugin config with cascade:
173
- * pluginConfig > ~/.nowledge-mem/openclaw.json > env vars > defaults
131
+ * ~/.nowledge-mem/openclaw.json > pluginConfig > env vars > defaults
132
+ *
133
+ * The config file (if it exists) takes highest priority so users who
134
+ * create it get predictable behavior. Most users won't have one and
135
+ * will configure via OpenClaw's plugin settings UI (pluginConfig).
174
136
  *
175
137
  * Canonical keys: sessionContext, sessionDigest, digestMinInterval,
176
138
  * maxContextResults, apiUrl, apiKey
@@ -184,21 +146,20 @@ function resolveAliases(obj) {
184
146
  * Environment variables (all optional):
185
147
  * NMEM_SESSION_CONTEXT — true/1/yes to enable (alias: NMEM_AUTO_RECALL)
186
148
  * NMEM_SESSION_DIGEST — true/1/yes to enable (alias: NMEM_AUTO_CAPTURE)
187
- * NMEM_DIGEST_MIN_INTERVAL — seconds (0–86400)
188
- * NMEM_MAX_CONTEXT_RESULTS — integer (1–20)
149
+ * NMEM_DIGEST_MIN_INTERVAL — seconds (0-86400)
150
+ * NMEM_MAX_CONTEXT_RESULTS — integer (1-20)
189
151
  * NMEM_API_URL — remote server URL
190
152
  * NMEM_API_KEY — API key (never logged)
191
153
  */
192
154
  export function parseConfig(raw, logger) {
193
155
  const pluginCfg = raw && typeof raw === "object" ? raw : {};
194
- const fileCfg = readConfigFile(logger, pluginCfg);
156
+ const fileCfg = readConfigFile(logger);
195
157
 
196
158
  // Resolve aliases in both sources
197
159
  const resolvedPlugin = resolveAliases(pluginCfg);
198
160
  const resolvedFile = resolveAliases(fileCfg);
199
161
 
200
162
  // Strict: reject unknown keys in pluginConfig (typo catcher).
201
- // File config is our own — validated by shape, no strict check needed.
202
163
  const unknownKeys = Object.keys(resolvedPlugin).filter(
203
164
  (k) => !ALLOWED_KEYS.has(k),
204
165
  );
@@ -209,55 +170,97 @@ export function parseConfig(raw, logger) {
209
170
  );
210
171
  }
211
172
 
212
- // --- sessionContext ---
213
- const sessionContext =
214
- pickBool(resolvedPlugin, "sessionContext") ??
215
- pickBool(resolvedFile, "sessionContext") ??
216
- envBool("NMEM_SESSION_CONTEXT", "NMEM_AUTO_RECALL") ??
217
- false;
173
+ const _sources = {};
218
174
 
219
- // --- sessionDigest ---
220
- const sessionDigest =
221
- pickBool(resolvedPlugin, "sessionDigest") ??
222
- pickBool(resolvedFile, "sessionDigest") ??
223
- envBool("NMEM_SESSION_DIGEST", "NMEM_AUTO_CAPTURE") ??
224
- true;
175
+ // --- sessionContext: file > pluginConfig > env > default ---
176
+ const sc = firstDefined(
177
+ { value: pickBool(resolvedFile, "sessionContext"), source: "file" },
178
+ {
179
+ value: pickBool(resolvedPlugin, "sessionContext"),
180
+ source: "pluginConfig",
181
+ },
182
+ {
183
+ value: envBool("NMEM_SESSION_CONTEXT", "NMEM_AUTO_RECALL"),
184
+ source: "env",
185
+ },
186
+ { value: false, source: "default" },
187
+ );
188
+ const sessionContext = sc.value;
189
+ _sources.sessionContext = sc.source;
225
190
 
226
- // --- digestMinInterval ---
227
- const rawInterval =
228
- pickNum(resolvedPlugin, "digestMinInterval") ??
229
- pickNum(resolvedFile, "digestMinInterval") ??
230
- envInt("NMEM_DIGEST_MIN_INTERVAL") ??
231
- envInt("NMEM_CAPTURE_MIN_INTERVAL");
232
- const digestMinInterval =
233
- rawInterval !== undefined
234
- ? Math.min(86400, Math.max(0, Math.trunc(rawInterval)))
235
- : 300;
191
+ // --- sessionDigest: file > pluginConfig > env > default ---
192
+ const sd = firstDefined(
193
+ { value: pickBool(resolvedFile, "sessionDigest"), source: "file" },
194
+ {
195
+ value: pickBool(resolvedPlugin, "sessionDigest"),
196
+ source: "pluginConfig",
197
+ },
198
+ {
199
+ value: envBool("NMEM_SESSION_DIGEST", "NMEM_AUTO_CAPTURE"),
200
+ source: "env",
201
+ },
202
+ { value: true, source: "default" },
203
+ );
204
+ const sessionDigest = sd.value;
205
+ _sources.sessionDigest = sd.source;
236
206
 
237
- // --- maxContextResults ---
238
- const rawMax =
239
- pickNum(resolvedPlugin, "maxContextResults") ??
240
- pickNum(resolvedFile, "maxContextResults") ??
241
- envInt("NMEM_MAX_CONTEXT_RESULTS") ??
242
- envInt("NMEM_MAX_RECALL_RESULTS");
243
- const maxContextResults =
244
- rawMax !== undefined
245
- ? Math.min(20, Math.max(1, Math.trunc(rawMax)))
246
- : 5;
207
+ // --- digestMinInterval: file > pluginConfig > env > default ---
208
+ const dmiEnv =
209
+ envInt("NMEM_DIGEST_MIN_INTERVAL") ?? envInt("NMEM_CAPTURE_MIN_INTERVAL");
210
+ const dmi = firstDefined(
211
+ { value: pickNum(resolvedFile, "digestMinInterval"), source: "file" },
212
+ {
213
+ value: pickNum(resolvedPlugin, "digestMinInterval"),
214
+ source: "pluginConfig",
215
+ },
216
+ { value: dmiEnv, source: "env" },
217
+ { value: 300, source: "default" },
218
+ );
219
+ const digestMinInterval = Math.min(86400, Math.max(0, Math.trunc(dmi.value)));
220
+ _sources.digestMinInterval = dmi.source;
247
221
 
248
- // --- apiUrl: pluginConfig > file > env > "" ---
249
- const apiUrl =
250
- (typeof resolvedPlugin.apiUrl === "string" && resolvedPlugin.apiUrl.trim()) ||
251
- (typeof resolvedFile.apiUrl === "string" && resolvedFile.apiUrl.trim()) ||
252
- envStr("NMEM_API_URL") ||
253
- "";
222
+ // --- maxContextResults: file > pluginConfig > env > default ---
223
+ const mcrEnv =
224
+ envInt("NMEM_MAX_CONTEXT_RESULTS") ?? envInt("NMEM_MAX_RECALL_RESULTS");
225
+ const mcr = firstDefined(
226
+ { value: pickNum(resolvedFile, "maxContextResults"), source: "file" },
227
+ {
228
+ value: pickNum(resolvedPlugin, "maxContextResults"),
229
+ source: "pluginConfig",
230
+ },
231
+ { value: mcrEnv, source: "env" },
232
+ { value: 5, source: "default" },
233
+ );
234
+ const maxContextResults = Math.min(20, Math.max(1, Math.trunc(mcr.value)));
235
+ _sources.maxContextResults = mcr.source;
236
+
237
+ // --- apiUrl: file > pluginConfig > env > "" ---
238
+ const fileUrl =
239
+ typeof resolvedFile.apiUrl === "string" && resolvedFile.apiUrl.trim();
240
+ const pluginUrl =
241
+ typeof resolvedPlugin.apiUrl === "string" && resolvedPlugin.apiUrl.trim();
242
+ const au = firstDefined(
243
+ { value: fileUrl || undefined, source: "file" },
244
+ { value: pluginUrl || undefined, source: "pluginConfig" },
245
+ { value: envStr("NMEM_API_URL"), source: "env" },
246
+ { value: "", source: "default" },
247
+ );
248
+ const apiUrl = au.value;
249
+ _sources.apiUrl = au.source;
254
250
 
255
- // --- apiKey: pluginConfig > file > env > "" ---
256
- const apiKey =
257
- (typeof resolvedPlugin.apiKey === "string" && resolvedPlugin.apiKey.trim()) ||
258
- (typeof resolvedFile.apiKey === "string" && resolvedFile.apiKey.trim()) ||
259
- envStr("NMEM_API_KEY") ||
260
- "";
251
+ // --- apiKey: file > pluginConfig > env > "" ---
252
+ const fileKey =
253
+ typeof resolvedFile.apiKey === "string" && resolvedFile.apiKey.trim();
254
+ const pluginKey =
255
+ typeof resolvedPlugin.apiKey === "string" && resolvedPlugin.apiKey.trim();
256
+ const ak = firstDefined(
257
+ { value: fileKey || undefined, source: "file" },
258
+ { value: pluginKey || undefined, source: "pluginConfig" },
259
+ { value: envStr("NMEM_API_KEY"), source: "env" },
260
+ { value: "", source: "default" },
261
+ );
262
+ const apiKey = ak.value;
263
+ _sources.apiKey = ak.source;
261
264
 
262
265
  return {
263
266
  sessionContext,
@@ -266,5 +269,6 @@ export function parseConfig(raw, logger) {
266
269
  maxContextResults,
267
270
  apiUrl,
268
271
  apiKey,
272
+ _sources,
269
273
  };
270
274
  }
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ import { createForgetTool } from "./tools/forget.js";
18
18
  import { createMemoryGetTool } from "./tools/memory-get.js";
19
19
  import { createMemorySearchTool } from "./tools/memory-search.js";
20
20
  import { createSaveTool } from "./tools/save.js";
21
+ import { createStatusTool } from "./tools/status.js";
21
22
  import { createThreadFetchTool } from "./tools/thread-fetch.js";
22
23
  import { createThreadSearchTool } from "./tools/thread-search.js";
23
24
  import { createTimelineTool } from "./tools/timeline.js";
@@ -52,6 +53,9 @@ export default {
52
53
  api.registerTool(createThreadSearchTool(client, logger));
53
54
  api.registerTool(createThreadFetchTool(client, logger));
54
55
 
56
+ // Diagnostics
57
+ api.registerTool(createStatusTool(client, logger, cfg));
58
+
55
59
  // Always-on: behavioral guidance so the agent proactively saves and searches.
56
60
  // Fires every turn via before_prompt_build — ~50 tokens, negligible cost.
57
61
  // When sessionContext is on, guidance adjusts to avoid redundant searches.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * nowledge_mem_status - verify config, connectivity, and backend health.
3
+ *
4
+ * Lets the user (or agent) confirm that apiUrl, apiKey, and other
5
+ * settings are taking effect, and that the backend is reachable.
6
+ * Shows where each config value comes from (file, pluginConfig, env, default).
7
+ */
8
+ import { isDefaultApiUrl } from "../config.js";
9
+
10
+ export function createStatusTool(client, _logger, cfg) {
11
+ return {
12
+ name: "nowledge_mem_status",
13
+ description:
14
+ "Check Nowledge Mem status: effective configuration, backend connectivity, and version. " +
15
+ "Call this when the user wants to verify their setup is working, " +
16
+ "especially after changing apiUrl or apiKey for remote mode.",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {},
20
+ },
21
+ async execute() {
22
+ const lines = [];
23
+ const details = {};
24
+ const sources = cfg._sources || {};
25
+
26
+ // 1. Mode + connection target
27
+ const remote = !isDefaultApiUrl(cfg.apiUrl);
28
+ const mode = remote ? "remote" : "local";
29
+ details.mode = mode;
30
+ details.apiUrl = cfg.apiUrl || "http://127.0.0.1:14242";
31
+ details.apiKeySet = Boolean(cfg.apiKey);
32
+
33
+ lines.push(
34
+ `Mode: ${remote ? `Remote (${cfg.apiUrl})` : "Local (127.0.0.1:14242)"}`,
35
+ );
36
+ lines.push(`API key: ${cfg.apiKey ? "set" : "not set"}`);
37
+
38
+ // 2. CLI resolution
39
+ try {
40
+ const cmd = client.resolveCommand();
41
+ details.cli = cmd.join(" ");
42
+ lines.push(`CLI: ${cmd.join(" ")}`);
43
+ } catch {
44
+ details.cli = null;
45
+ lines.push("CLI: not found (install with: pip install nmem-cli)");
46
+ }
47
+
48
+ // 3. Backend health
49
+ let healthy = false;
50
+ try {
51
+ healthy = await client.checkHealth();
52
+ } catch {
53
+ // checkHealth already returns false on error
54
+ }
55
+ details.healthy = healthy;
56
+
57
+ if (healthy) {
58
+ lines.push("Backend: reachable");
59
+ // Fetch detailed health info
60
+ try {
61
+ const health = await client.apiJson(
62
+ "GET",
63
+ "/health",
64
+ undefined,
65
+ 5000,
66
+ );
67
+ if (health.version) {
68
+ details.version = health.version;
69
+ lines.push(`Version: ${health.version}`);
70
+ }
71
+ if (health.database_connected !== undefined) {
72
+ details.databaseConnected = health.database_connected;
73
+ lines.push(
74
+ `Database: ${health.database_connected ? "connected" : "disconnected"}`,
75
+ );
76
+ }
77
+ } catch {
78
+ // Health endpoint not available on older backends
79
+ }
80
+ } else {
81
+ lines.push("Backend: not reachable");
82
+ if (remote) {
83
+ lines.push(
84
+ " Check that apiUrl is correct and the server is running.",
85
+ );
86
+ if (!cfg.apiKey) {
87
+ lines.push(" Remote mode usually requires an API key.");
88
+ }
89
+ } else {
90
+ lines.push(" Ensure the Nowledge Mem desktop app is running.");
91
+ }
92
+ }
93
+
94
+ // 4. Effective config with sources
95
+ lines.push("");
96
+ lines.push("Config (source):");
97
+ lines.push(
98
+ ` sessionContext: ${cfg.sessionContext} (${sources.sessionContext || "?"})`,
99
+ );
100
+ lines.push(
101
+ ` sessionDigest: ${cfg.sessionDigest} (${sources.sessionDigest || "?"})`,
102
+ );
103
+ lines.push(
104
+ ` digestMinInterval: ${cfg.digestMinInterval}s (${sources.digestMinInterval || "?"})`,
105
+ );
106
+ lines.push(
107
+ ` maxContextResults: ${cfg.maxContextResults} (${sources.maxContextResults || "?"})`,
108
+ );
109
+
110
+ details.config = {
111
+ sessionContext: {
112
+ value: cfg.sessionContext,
113
+ source: sources.sessionContext,
114
+ },
115
+ sessionDigest: {
116
+ value: cfg.sessionDigest,
117
+ source: sources.sessionDigest,
118
+ },
119
+ digestMinInterval: {
120
+ value: cfg.digestMinInterval,
121
+ source: sources.digestMinInterval,
122
+ },
123
+ maxContextResults: {
124
+ value: cfg.maxContextResults,
125
+ source: sources.maxContextResults,
126
+ },
127
+ apiUrl: { value: cfg.apiUrl || "(local)", source: sources.apiUrl },
128
+ apiKey: {
129
+ value: cfg.apiKey ? "(set)" : "(not set)",
130
+ source: sources.apiKey,
131
+ },
132
+ };
133
+
134
+ lines.push("");
135
+ lines.push(
136
+ "Priority: config file > OpenClaw settings > env vars > defaults",
137
+ );
138
+
139
+ return {
140
+ content: [{ type: "text", text: lines.join("\n") }],
141
+ details,
142
+ };
143
+ },
144
+ };
145
+ }