@nowledge/openclaw-nowledge-mem 0.6.5 → 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/src/config.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
1
5
  export const API_DEFAULT_URL = "http://127.0.0.1:14242";
2
6
 
3
7
  /**
@@ -17,6 +21,11 @@ const ALLOWED_KEYS = new Set([
17
21
  "maxContextResults",
18
22
  "apiUrl",
19
23
  "apiKey",
24
+ // Legacy aliases — accepted but not advertised
25
+ "autoRecall",
26
+ "autoCapture",
27
+ "captureMinInterval",
28
+ "maxRecallResults",
20
29
  ]);
21
30
 
22
31
  // Backward-compatible aliases (old names → new names).
@@ -28,11 +37,69 @@ const ALIAS_KEYS = {
28
37
  maxRecallResults: "maxContextResults",
29
38
  };
30
39
 
31
- export function parseConfig(raw) {
32
- const obj = raw && typeof raw === "object" ? raw : {};
40
+ // --- config file ---------------------------------------------------------
41
+
42
+ const CONFIG_DIR = join(homedir(), ".nowledge-mem");
43
+ const CONFIG_PATH = join(CONFIG_DIR, "openclaw.json");
44
+
45
+ /**
46
+ * Read ~/.nowledge-mem/openclaw.json if it exists.
47
+ * Returns {} when the file is missing or unreadable.
48
+ */
49
+ function readConfigFile(logger) {
50
+ try {
51
+ if (!existsSync(CONFIG_PATH)) {
52
+ return {};
53
+ }
54
+ const content = readFileSync(CONFIG_PATH, "utf-8").trim();
55
+ if (!content) return {};
56
+ return JSON.parse(content);
57
+ } catch (err) {
58
+ logger?.warn?.(
59
+ `nowledge-mem: could not read ${CONFIG_PATH}: ${err instanceof Error ? err.message : err}`,
60
+ );
61
+ return {};
62
+ }
63
+ }
64
+
65
+ /** Exported for tests / CLI tooling. */
66
+ export { CONFIG_PATH };
67
+
68
+ // --- env var helpers -----------------------------------------------------
69
+
70
+ function envStr(name) {
71
+ const v = process.env[name];
72
+ return typeof v === "string" ? v.trim() : undefined;
73
+ }
74
+
75
+ function envBool(name, fallbackName) {
76
+ const v = envStr(name) ?? envStr(fallbackName);
77
+ if (v === undefined) return undefined;
78
+ return v === "true" || v === "1" || v === "yes";
79
+ }
80
+
81
+ function envInt(name) {
82
+ const v = envStr(name);
83
+ if (v === undefined) return undefined;
84
+ const n = Number.parseInt(v, 10);
85
+ return Number.isFinite(n) ? n : undefined;
86
+ }
87
+
88
+ // --- helpers to read a boolean/number from resolved sources --------------
89
+
90
+ function pickBool(obj, key) {
91
+ if (typeof obj[key] === "boolean") return obj[key];
92
+ return undefined;
93
+ }
94
+
95
+ function pickNum(obj, key) {
96
+ if (typeof obj[key] === "number" && Number.isFinite(obj[key]))
97
+ return obj[key];
98
+ return undefined;
99
+ }
33
100
 
34
- // Resolve aliases: copy old-name values to new-name slots
35
- // (new name takes precedence if both are present).
101
+ /** Resolve legacy aliases in an object: old names → new names. */
102
+ function resolveAliases(obj) {
36
103
  const resolved = { ...obj };
37
104
  for (const [oldKey, newKey] of Object.entries(ALIAS_KEYS)) {
38
105
  if (oldKey in resolved) {
@@ -42,50 +109,166 @@ export function parseConfig(raw) {
42
109
  delete resolved[oldKey];
43
110
  }
44
111
  }
112
+ return resolved;
113
+ }
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
+
127
+ // -------------------------------------------------------------------------
45
128
 
46
- // Strict: reject unknown keys so users catch typos immediately.
47
- const unknownKeys = Object.keys(resolved).filter((k) => !ALLOWED_KEYS.has(k));
129
+ /**
130
+ * Parse plugin config with cascade:
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).
136
+ *
137
+ * Canonical keys: sessionContext, sessionDigest, digestMinInterval,
138
+ * maxContextResults, apiUrl, apiKey
139
+ *
140
+ * Legacy aliases (accepted from all sources; never shown in docs):
141
+ * autoRecall → sessionContext
142
+ * autoCapture → sessionDigest
143
+ * captureMinInterval → digestMinInterval
144
+ * maxRecallResults → maxContextResults
145
+ *
146
+ * Environment variables (all optional):
147
+ * NMEM_SESSION_CONTEXT — true/1/yes to enable (alias: NMEM_AUTO_RECALL)
148
+ * NMEM_SESSION_DIGEST — true/1/yes to enable (alias: NMEM_AUTO_CAPTURE)
149
+ * NMEM_DIGEST_MIN_INTERVAL — seconds (0-86400)
150
+ * NMEM_MAX_CONTEXT_RESULTS — integer (1-20)
151
+ * NMEM_API_URL — remote server URL
152
+ * NMEM_API_KEY — API key (never logged)
153
+ */
154
+ export function parseConfig(raw, logger) {
155
+ const pluginCfg = raw && typeof raw === "object" ? raw : {};
156
+ const fileCfg = readConfigFile(logger);
157
+
158
+ // Resolve aliases in both sources
159
+ const resolvedPlugin = resolveAliases(pluginCfg);
160
+ const resolvedFile = resolveAliases(fileCfg);
161
+
162
+ // Strict: reject unknown keys in pluginConfig (typo catcher).
163
+ const unknownKeys = Object.keys(resolvedPlugin).filter(
164
+ (k) => !ALLOWED_KEYS.has(k),
165
+ );
48
166
  if (unknownKeys.length > 0) {
49
167
  throw new Error(
50
168
  `nowledge-mem: unknown config key${unknownKeys.length > 1 ? "s" : ""}: ${unknownKeys.join(", ")}. ` +
51
- `Allowed keys: ${[...ALLOWED_KEYS].join(", ")}`,
169
+ `Allowed keys: ${[...ALLOWED_KEYS].filter((k) => !Object.keys(ALIAS_KEYS).includes(k)).join(", ")}`,
52
170
  );
53
171
  }
54
172
 
55
- // apiUrl: config wins, then env var, then local default
56
- const apiUrl =
57
- (typeof resolved.apiUrl === "string" && resolved.apiUrl.trim()) ||
58
- (typeof process.env.NMEM_API_URL === "string" &&
59
- process.env.NMEM_API_URL.trim()) ||
60
- "";
173
+ const _sources = {};
174
+
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;
190
+
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;
206
+
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;
221
+
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;
61
250
 
62
- // apiKey: config wins, then env var — never logged, never in CLI args
63
- const apiKey =
64
- (typeof resolved.apiKey === "string" && resolved.apiKey.trim()) ||
65
- (typeof process.env.NMEM_API_KEY === "string" &&
66
- process.env.NMEM_API_KEY.trim()) ||
67
- "";
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;
68
264
 
69
265
  return {
70
- sessionContext:
71
- typeof resolved.sessionContext === "boolean"
72
- ? resolved.sessionContext
73
- : false,
74
- sessionDigest:
75
- typeof resolved.sessionDigest === "boolean"
76
- ? resolved.sessionDigest
77
- : true,
78
- digestMinInterval:
79
- typeof resolved.digestMinInterval === "number" &&
80
- Number.isFinite(resolved.digestMinInterval)
81
- ? Math.min(86400, Math.max(0, Math.trunc(resolved.digestMinInterval)))
82
- : 300,
83
- maxContextResults:
84
- typeof resolved.maxContextResults === "number" &&
85
- Number.isFinite(resolved.maxContextResults)
86
- ? Math.min(20, Math.max(1, Math.trunc(resolved.maxContextResults)))
87
- : 5,
266
+ sessionContext,
267
+ sessionDigest,
268
+ digestMinInterval,
269
+ maxContextResults,
88
270
  apiUrl,
89
271
  apiKey,
272
+ _sources,
90
273
  };
91
274
  }
@@ -291,7 +291,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
291
291
  });
292
292
 
293
293
  // 2. Triage + distill: language-agnostic LLM-based capture.
294
- // Defensive guard — registration in index.js already gates on autoCapture,
294
+ // Defensive guard - registration in index.js already gates on sessionDigest,
295
295
  // but check here too so the handler is safe if called directly.
296
296
  if (!cfg.sessionDigest) return;
297
297
 
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";
@@ -30,8 +31,8 @@ export default {
30
31
  kind: "memory",
31
32
 
32
33
  register(api) {
33
- const cfg = parseConfig(api.pluginConfig);
34
34
  const logger = api.logger;
35
+ const cfg = parseConfig(api.pluginConfig, logger);
35
36
  const client = new NowledgeMemClient(logger, {
36
37
  apiUrl: cfg.apiUrl,
37
38
  apiKey: cfg.apiKey,
@@ -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
+ }