@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.
- package/CHANGELOG.md +211 -0
- package/README.md +191 -0
- package/openclaw.plugin.json +61 -0
- package/package.json +54 -0
- package/src/client.js +673 -0
- package/src/commands/cli.js +45 -0
- package/src/commands/slash.js +109 -0
- package/src/config.js +43 -0
- package/src/hooks/capture.js +337 -0
- package/src/hooks/recall.js +109 -0
- package/src/index.js +81 -0
- package/src/tools/connections.js +324 -0
- package/src/tools/context.js +126 -0
- package/src/tools/forget.js +154 -0
- package/src/tools/memory-get.js +168 -0
- package/src/tools/memory-search.js +183 -0
- package/src/tools/save.js +179 -0
- package/src/tools/timeline.js +208 -0
|
@@ -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
|
+
}
|