@nowledge/openclaw-nowledge-mem 0.2.7

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.
@@ -0,0 +1,168 @@
1
+ function sliceLines(text, from, lines) {
2
+ const allLines = String(text || "").split(/\r?\n/u);
3
+ const start = Math.max(1, Math.trunc(Number(from) || 1));
4
+ const maxLines = Math.max(1, Math.trunc(Number(lines) || allLines.length));
5
+ const startIdx = start - 1;
6
+ const endIdx = Math.min(allLines.length, startIdx + maxLines);
7
+ const selected = allLines.slice(startIdx, endIdx);
8
+ return {
9
+ text: selected.join("\n"),
10
+ startLine: start,
11
+ endLine: Math.max(start, start + selected.length - 1),
12
+ totalLines: allLines.length,
13
+ };
14
+ }
15
+
16
+ function parseMemoryId(pathValue) {
17
+ const value = String(pathValue || "").trim();
18
+ if (!value) return null;
19
+
20
+ const fromDeeplink = value.match(
21
+ /^nowledgemem:\/\/memory\/([a-zA-Z0-9_-]+)$/u,
22
+ );
23
+ if (fromDeeplink) return fromDeeplink[1];
24
+
25
+ if (/^[a-zA-Z0-9_-]{8,}$/u.test(value)) {
26
+ return value;
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ export function createMemoryGetTool(client, logger) {
33
+ return {
34
+ name: "memory_get",
35
+ description:
36
+ "Read a specific memory snippet by path from memory_search (nowledgemem://memory/<id>) or by raw memory ID.",
37
+ parameters: {
38
+ type: "object",
39
+ properties: {
40
+ path: {
41
+ type: "string",
42
+ description:
43
+ "Memory path from memory_search (nowledgemem://memory/<id>) or raw memory ID",
44
+ },
45
+ from: {
46
+ type: "number",
47
+ description: "Optional 1-based starting line number",
48
+ },
49
+ lines: {
50
+ type: "number",
51
+ description: "Optional number of lines to return",
52
+ },
53
+ },
54
+ required: ["path"],
55
+ },
56
+ async execute(_toolCallId, params) {
57
+ const safeParams = params && typeof params === "object" ? params : {};
58
+ const path = String(safeParams.path ?? "").trim();
59
+ if (!path) {
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text",
64
+ text: JSON.stringify({
65
+ path: "",
66
+ text: "",
67
+ error: "path is required",
68
+ }),
69
+ },
70
+ ],
71
+ };
72
+ }
73
+
74
+ try {
75
+ const lowerPath = path.toLowerCase();
76
+ if (lowerPath === "memory.md" || lowerPath === "memory") {
77
+ const wm = await client.readWorkingMemory();
78
+ if (!wm.available) {
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: JSON.stringify({
84
+ path,
85
+ text: "",
86
+ error: "Working Memory not available",
87
+ }),
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ const snippet = sliceLines(
93
+ wm.content,
94
+ safeParams.from,
95
+ safeParams.lines,
96
+ );
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: JSON.stringify({
102
+ path,
103
+ text: snippet.text,
104
+ startLine: snippet.startLine,
105
+ endLine: snippet.endLine,
106
+ totalLines: snippet.totalLines,
107
+ source: "working-memory",
108
+ }),
109
+ },
110
+ ],
111
+ };
112
+ }
113
+
114
+ const memoryId = parseMemoryId(path);
115
+ if (!memoryId) {
116
+ return {
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: JSON.stringify({
121
+ path,
122
+ text: "",
123
+ error:
124
+ "Unsupported path. Use memory_search result path (nowledgemem://memory/<id>).",
125
+ }),
126
+ },
127
+ ],
128
+ };
129
+ }
130
+
131
+ const memory = await client.getMemory(memoryId);
132
+ const snippet = sliceLines(
133
+ memory.content ?? "",
134
+ safeParams.from,
135
+ safeParams.lines,
136
+ );
137
+
138
+ return {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text: JSON.stringify({
143
+ path,
144
+ memoryId,
145
+ title: memory.title || "(untitled)",
146
+ text: snippet.text,
147
+ startLine: snippet.startLine,
148
+ endLine: snippet.endLine,
149
+ totalLines: snippet.totalLines,
150
+ }),
151
+ },
152
+ ],
153
+ };
154
+ } catch (err) {
155
+ const message = err instanceof Error ? err.message : String(err);
156
+ logger.error(`memory_get failed: ${message}`);
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: JSON.stringify({ path, text: "", error: message }),
162
+ },
163
+ ],
164
+ };
165
+ }
166
+ },
167
+ };
168
+ }
@@ -0,0 +1,183 @@
1
+ function truncateSnippet(text, maxChars = 700) {
2
+ const value = String(text || "").trim();
3
+ if (!value) return "";
4
+ if (value.length <= maxChars) return value;
5
+ return `${value.slice(0, maxChars)}…`;
6
+ }
7
+
8
+ function toMemoryPath(memoryId) {
9
+ return `nowledgemem://memory/${memoryId}`;
10
+ }
11
+
12
+ export function createMemorySearchTool(client, logger) {
13
+ return {
14
+ name: "memory_search",
15
+ description:
16
+ "Search the user's knowledge graph using a multi-signal scoring pipeline: " +
17
+ "semantic (embedding), BM25 keyword, label match, graph & community signals, and recency/importance decay — " +
18
+ "not just simple vector similarity. " +
19
+ "Finds prior work, decisions, preferences, and facts. " +
20
+ "Returns snippets with memoryIds. " +
21
+ "Pass a memoryId to nowledge_mem_connections for cross-topic synthesis or source provenance. " +
22
+ "Supports bi-temporal filtering: event_date_from/to (when the fact HAPPENED) and " +
23
+ "recorded_date_from/to (when it was SAVED). Format: YYYY, YYYY-MM, or YYYY-MM-DD. " +
24
+ "For browsing recent activity by day use nowledge_mem_timeline instead.",
25
+ parameters: {
26
+ type: "object",
27
+ properties: {
28
+ query: {
29
+ type: "string",
30
+ description: "Natural language query",
31
+ },
32
+ maxResults: {
33
+ type: "number",
34
+ description: "Max result count (1-20, default 5)",
35
+ },
36
+ minScore: {
37
+ type: "number",
38
+ description: "Optional score threshold in [0, 1]",
39
+ },
40
+ event_date_from: {
41
+ type: "string",
42
+ description:
43
+ "Filter by when the fact/event HAPPENED — e.g. '2024', '2024-Q1', '2024-03', '2024-03-15'",
44
+ },
45
+ event_date_to: {
46
+ type: "string",
47
+ description:
48
+ "Upper bound for event date (YYYY, YYYY-MM, or YYYY-MM-DD)",
49
+ },
50
+ recorded_date_from: {
51
+ type: "string",
52
+ description:
53
+ "Filter by when this memory was SAVED to Nowledge Mem (YYYY-MM-DD)",
54
+ },
55
+ recorded_date_to: {
56
+ type: "string",
57
+ description: "Upper bound for record date (YYYY-MM-DD)",
58
+ },
59
+ },
60
+ required: ["query"],
61
+ },
62
+ async execute(_toolCallId, params) {
63
+ const safeParams = params && typeof params === "object" ? params : {};
64
+ const query = String(safeParams.query ?? "").trim();
65
+ const maxResults = Math.min(
66
+ 20,
67
+ Math.max(
68
+ 1,
69
+ Math.trunc(
70
+ Number(safeParams.maxResults ?? safeParams.limit ?? 5) || 5,
71
+ ),
72
+ ),
73
+ );
74
+ const minScore = Number(safeParams.minScore);
75
+ const hasMinScore = Number.isFinite(minScore);
76
+
77
+ const eventDateFrom = safeParams.event_date_from
78
+ ? String(safeParams.event_date_from).trim()
79
+ : undefined;
80
+ const eventDateTo = safeParams.event_date_to
81
+ ? String(safeParams.event_date_to).trim()
82
+ : undefined;
83
+ const recordedDateFrom = safeParams.recorded_date_from
84
+ ? String(safeParams.recorded_date_from).trim()
85
+ : undefined;
86
+ const recordedDateTo = safeParams.recorded_date_to
87
+ ? String(safeParams.recorded_date_to).trim()
88
+ : undefined;
89
+
90
+ const hasTemporalFilter =
91
+ eventDateFrom || eventDateTo || recordedDateFrom || recordedDateTo;
92
+
93
+ if (!query) {
94
+ return {
95
+ content: [{ type: "text", text: JSON.stringify({ results: [] }) }],
96
+ };
97
+ }
98
+
99
+ try {
100
+ let rawResults;
101
+
102
+ if (hasTemporalFilter) {
103
+ // Bi-temporal path: filter by event or record time
104
+ const { memories } = await client.searchTemporal(query, {
105
+ limit: maxResults,
106
+ eventDateFrom,
107
+ eventDateTo,
108
+ recordedDateFrom,
109
+ recordedDateTo,
110
+ });
111
+ rawResults = memories;
112
+ } else {
113
+ // Always use the rich API path to get relevance_reason + full metadata
114
+ rawResults = await client.searchRich(query, maxResults);
115
+ }
116
+
117
+ const filtered = hasMinScore
118
+ ? rawResults.filter((entry) => Number(entry.score ?? 0) >= minScore)
119
+ : rawResults;
120
+
121
+ const results = filtered.map((entry) => {
122
+ const path = toMemoryPath(entry.id);
123
+ const snippet = truncateSnippet(entry.content);
124
+ const lineCount = Math.max(1, snippet.split(/\r?\n/u).length);
125
+ const result = {
126
+ path,
127
+ startLine: 1,
128
+ endLine: lineCount,
129
+ score: Number(entry.score ?? 0),
130
+ title: entry.title || "(untitled)",
131
+ snippet,
132
+ memoryId: entry.id,
133
+ };
134
+ // Scoring transparency: show which signals fired
135
+ if (entry.relevanceReason)
136
+ result.matchedVia = entry.relevanceReason;
137
+ // Importance context
138
+ if (entry.importance !== undefined && entry.importance !== null)
139
+ result.importance = Number(entry.importance);
140
+ // Temporal metadata
141
+ if (entry.eventStart) result.eventStart = entry.eventStart;
142
+ if (entry.eventEnd) result.eventEnd = entry.eventEnd;
143
+ if (entry.temporalContext)
144
+ result.temporalContext = entry.temporalContext;
145
+ return result;
146
+ });
147
+
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: JSON.stringify(
153
+ {
154
+ results,
155
+ provider: "nmem",
156
+ mode: "multi-signal",
157
+ },
158
+ null,
159
+ 2,
160
+ ),
161
+ },
162
+ ],
163
+ details: { query, resultCount: results.length },
164
+ };
165
+ } catch (err) {
166
+ const message = err instanceof Error ? err.message : String(err);
167
+ logger.error(`memory_search failed: ${message}`);
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: JSON.stringify({
173
+ results: [],
174
+ disabled: true,
175
+ error: message,
176
+ }),
177
+ },
178
+ ],
179
+ };
180
+ }
181
+ },
182
+ };
183
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * nowledge_mem_save — structured knowledge capture.
3
+ *
4
+ * Unlike a generic "store text" tool, this captures knowledge with:
5
+ * - Type classification (8 unit types → typed nodes in the knowledge graph)
6
+ * - Labels (arbitrary tags for recall and filtering)
7
+ * - Temporal context (when the event happened, not just when it was saved)
8
+ *
9
+ * Reflects Nowledge Mem's v0.6 memory model where every memory is a
10
+ * rich typed node — not a flat text blob.
11
+ */
12
+
13
+ const VALID_UNIT_TYPES = new Set([
14
+ "fact",
15
+ "preference",
16
+ "decision",
17
+ "plan",
18
+ "procedure",
19
+ "learning",
20
+ "context",
21
+ "event",
22
+ ]);
23
+
24
+ export function createSaveTool(client, logger) {
25
+ return {
26
+ name: "nowledge_mem_save",
27
+ description:
28
+ "Save a new insight, decision, or fact to the user's permanent knowledge graph. " +
29
+ "Call this proactively — don't wait to be asked. If the conversation surfaces something worth keeping " +
30
+ "(a technical choice made, a preference stated, something learned, a plan formed, a tool discovered), save it. " +
31
+ "Specify unit_type to give the memory richer structure: " +
32
+ "decision (a choice made), learning (an insight gained), preference (user taste), " +
33
+ "fact (verified info), plan (future intent), procedure (how-to steps), event (something that happened). " +
34
+ "Use labels for topics/projects. Use event_start for when the event HAPPENED (not when it's saved).",
35
+ parameters: {
36
+ type: "object",
37
+ properties: {
38
+ text: {
39
+ type: "string",
40
+ description:
41
+ "The knowledge to save — be specific and self-contained. Write it as if explaining to your future self.",
42
+ },
43
+ title: {
44
+ type: "string",
45
+ description: "Short searchable title (50–60 chars ideal)",
46
+ },
47
+ unit_type: {
48
+ type: "string",
49
+ enum: [...VALID_UNIT_TYPES],
50
+ description:
51
+ "Type: fact (verified info) | preference (user taste) | decision (choice made) | " +
52
+ "plan (future intent) | procedure (how-to) | learning (insight gained) | " +
53
+ "context (background info) | event (something that happened)",
54
+ },
55
+ importance: {
56
+ type: "number",
57
+ description:
58
+ "0.8–1.0: critical decisions/breakthroughs. 0.5–0.7: useful insights. 0.3–0.4: minor notes.",
59
+ },
60
+ labels: {
61
+ type: "array",
62
+ items: { type: "string" },
63
+ description:
64
+ "Topic or project labels for this memory (e.g. [\"python\", \"infra\"]). Used in search filtering.",
65
+ },
66
+ event_start: {
67
+ type: "string",
68
+ description:
69
+ "When the event/fact HAPPENED — not when you're saving it. " +
70
+ "Format: YYYY, YYYY-MM, or YYYY-MM-DD. Example: '2024-03' for March 2024.",
71
+ },
72
+ event_end: {
73
+ type: "string",
74
+ description:
75
+ "End of the event period (for ranges). Same format as event_start.",
76
+ },
77
+ temporal_context: {
78
+ type: "string",
79
+ enum: ["past", "present", "future", "timeless"],
80
+ description:
81
+ "Temporal framing: past (already happened), present (ongoing), future (planned), timeless (always true).",
82
+ },
83
+ },
84
+ required: ["text"],
85
+ },
86
+ async execute(_toolCallId, params) {
87
+ const safeParams = params && typeof params === "object" ? params : {};
88
+ const text = String(safeParams.text ?? "").trim();
89
+ const title = safeParams.title ? String(safeParams.title).trim() : undefined;
90
+ const unitType =
91
+ typeof safeParams.unit_type === "string" &&
92
+ VALID_UNIT_TYPES.has(safeParams.unit_type)
93
+ ? safeParams.unit_type
94
+ : undefined;
95
+ const hasImportance =
96
+ safeParams.importance !== undefined && safeParams.importance !== null;
97
+ const importance = hasImportance
98
+ ? Math.min(1, Math.max(0, Number(safeParams.importance)))
99
+ : undefined;
100
+ const labels = Array.isArray(safeParams.labels)
101
+ ? safeParams.labels
102
+ .map((l) => String(l).trim())
103
+ .filter((l) => l.length > 0)
104
+ : [];
105
+ const eventStart = safeParams.event_start
106
+ ? String(safeParams.event_start).trim()
107
+ : undefined;
108
+ const eventEnd = safeParams.event_end
109
+ ? String(safeParams.event_end).trim()
110
+ : undefined;
111
+ const temporalContext =
112
+ typeof safeParams.temporal_context === "string" &&
113
+ ["past", "present", "future", "timeless"].includes(
114
+ safeParams.temporal_context,
115
+ )
116
+ ? safeParams.temporal_context
117
+ : undefined;
118
+
119
+ if (!text) {
120
+ return {
121
+ content: [{ type: "text", text: "Cannot save empty memory." }],
122
+ };
123
+ }
124
+
125
+ try {
126
+ const args = ["--json", "m", "add", text];
127
+ if (title) args.push("-t", title);
128
+ if (importance !== undefined && Number.isFinite(importance)) {
129
+ args.push("-i", String(importance));
130
+ }
131
+ if (unitType) args.push("--unit-type", unitType);
132
+ for (const label of labels) args.push("-l", label);
133
+ if (eventStart) args.push("--event-start", eventStart);
134
+ if (eventEnd) args.push("--event-end", eventEnd);
135
+ if (temporalContext) args.push("--when", temporalContext);
136
+
137
+ const data = client.execJson(args);
138
+ const id = String(
139
+ data.id ?? data.memory?.id ?? data.memory_id ?? "created",
140
+ );
141
+
142
+ // Build human-readable confirmation
143
+ const typeLabel = unitType ? ` [${unitType}]` : "";
144
+ const labelStr =
145
+ labels.length > 0 ? ` · labels: ${labels.join(", ")}` : "";
146
+ const timeStr = eventStart
147
+ ? ` · event: ${eventStart}${eventEnd ? `→${eventEnd}` : ""}`
148
+ : "";
149
+
150
+ logger.info(`save: stored memory ${id}${typeLabel}`);
151
+
152
+ return {
153
+ content: [
154
+ {
155
+ type: "text",
156
+ text: `Saved${title ? `: ${title}` : ""}${typeLabel} (id: ${id})${labelStr}${timeStr}`,
157
+ },
158
+ ],
159
+ details: {
160
+ id,
161
+ title,
162
+ unitType: data.unit_type || unitType,
163
+ importance,
164
+ labels: data.labels || labels,
165
+ eventStart,
166
+ eventEnd,
167
+ temporalContext,
168
+ },
169
+ };
170
+ } catch (err) {
171
+ const msg = err instanceof Error ? err.message : String(err);
172
+ logger.error(`save failed: ${msg}`);
173
+ return {
174
+ content: [{ type: "text", text: `Failed to save: ${msg}` }],
175
+ };
176
+ }
177
+ },
178
+ };
179
+ }