@basicmemory/openclaw-basic-memory 0.1.5 → 0.2.1
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/context-engine/basic-memory-context-engine.test.ts +398 -0
- package/context-engine/basic-memory-context-engine.ts +330 -0
- package/hooks/capture.ts +18 -6
- package/hooks/recall.ts +28 -12
- package/index.ts +9 -12
- package/package.json +5 -4
- package/scripts/setup-bm.sh +2 -2
- package/skills/manifest.json +5 -0
- package/skills/memory-literary-analysis/SKILL.md +511 -0
- package/skills/memory-metadata-search/SKILL.md +2 -0
- package/tools/build-context.ts +5 -0
- package/tools/delete-note.ts +4 -0
- package/tools/edit-note.ts +5 -0
- package/tools/list-memory-projects.ts +6 -0
- package/tools/list-workspaces.ts +5 -0
- package/tools/memory-provider.ts +23 -0
- package/tools/move-note.ts +5 -0
- package/tools/read-note.ts +5 -0
- package/tools/schema-diff.ts +5 -0
- package/tools/schema-infer.ts +5 -0
- package/tools/schema-validate.ts +6 -0
- package/tools/search-notes.ts +10 -0
- package/tools/write-note.ts +10 -0
- package/types/openclaw.d.ts +0 -24
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { createRequire } from "node:module"
|
|
2
|
+
import { dirname, resolve } from "node:path"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core"
|
|
5
|
+
import type {
|
|
6
|
+
AssembleResult,
|
|
7
|
+
BootstrapResult,
|
|
8
|
+
CompactResult,
|
|
9
|
+
ContextEngine,
|
|
10
|
+
} from "openclaw/plugin-sdk"
|
|
11
|
+
import type { BmClient } from "../bm-client.ts"
|
|
12
|
+
import type { BasicMemoryConfig } from "../config.ts"
|
|
13
|
+
import { selectCaptureTurn } from "../hooks/capture.ts"
|
|
14
|
+
import { loadRecallState } from "../hooks/recall.ts"
|
|
15
|
+
import { log } from "../logger.ts"
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url)
|
|
18
|
+
export const MAX_ASSEMBLE_RECALL_CHARS = 1200
|
|
19
|
+
const TRUNCATED_RECALL_SUFFIX = "\n\n[Basic Memory recall truncated]"
|
|
20
|
+
const SUBAGENT_HANDOFF_FOLDER = "agent/subagents"
|
|
21
|
+
const MAX_SUBAGENT_RECALL_CHARS = 800
|
|
22
|
+
|
|
23
|
+
interface SessionMemoryState {
|
|
24
|
+
recallContext: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SubagentHandoffState {
|
|
28
|
+
noteIdentifier: string
|
|
29
|
+
noteTitle: string
|
|
30
|
+
parentSessionKey: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function boundRecallContext(context: string): string {
|
|
34
|
+
if (context.length <= MAX_ASSEMBLE_RECALL_CHARS) {
|
|
35
|
+
return context
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const trimmed = context
|
|
39
|
+
.slice(
|
|
40
|
+
0,
|
|
41
|
+
Math.max(0, MAX_ASSEMBLE_RECALL_CHARS - TRUNCATED_RECALL_SUFFIX.length),
|
|
42
|
+
)
|
|
43
|
+
.trimEnd()
|
|
44
|
+
|
|
45
|
+
return `${trimmed}${TRUNCATED_RECALL_SUFFIX}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function slugifySessionKey(sessionKey: string): string {
|
|
49
|
+
return sessionKey
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
52
|
+
.replace(/^-+|-+$/g, "")
|
|
53
|
+
.slice(0, 80)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildSubagentNoteTitle(childSessionKey: string): string {
|
|
57
|
+
return `subagent-handoff-${slugifySessionKey(childSessionKey)}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildSubagentHandoffContent(params: {
|
|
61
|
+
parentSessionKey: string
|
|
62
|
+
childSessionKey: string
|
|
63
|
+
recallContext?: string
|
|
64
|
+
}): string {
|
|
65
|
+
const sections = [
|
|
66
|
+
"# Subagent Handoff",
|
|
67
|
+
"",
|
|
68
|
+
"## Sessions",
|
|
69
|
+
`- Parent: ${params.parentSessionKey}`,
|
|
70
|
+
`- Child: ${params.childSessionKey}`,
|
|
71
|
+
"",
|
|
72
|
+
"## Lifecycle",
|
|
73
|
+
`- Spawned: ${new Date().toISOString()}`,
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
if (params.recallContext) {
|
|
77
|
+
sections.push(
|
|
78
|
+
"",
|
|
79
|
+
"## Parent Basic Memory Context",
|
|
80
|
+
params.recallContext.slice(0, MAX_SUBAGENT_RECALL_CHARS).trimEnd(),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return sections.join("\n")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildSubagentCompletionUpdate(params: {
|
|
88
|
+
childSessionKey: string
|
|
89
|
+
reason: "deleted" | "completed" | "swept" | "released"
|
|
90
|
+
}): string {
|
|
91
|
+
const statusLine =
|
|
92
|
+
params.reason === "completed"
|
|
93
|
+
? "Child run completed. Durable conversation capture continues through the normal afterTurn path."
|
|
94
|
+
: `Child run ended with reason: ${params.reason}.`
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
"",
|
|
98
|
+
"## Completion",
|
|
99
|
+
`- Child: ${params.childSessionKey}`,
|
|
100
|
+
`- Ended: ${new Date().toISOString()}`,
|
|
101
|
+
`- Reason: ${params.reason}`,
|
|
102
|
+
"",
|
|
103
|
+
statusLine,
|
|
104
|
+
].join("\n")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type LegacyContextEngineModule = {
|
|
108
|
+
LegacyContextEngine: new () => {
|
|
109
|
+
compact(params: {
|
|
110
|
+
sessionId: string
|
|
111
|
+
sessionFile: string
|
|
112
|
+
tokenBudget?: number
|
|
113
|
+
force?: boolean
|
|
114
|
+
currentTokenCount?: number
|
|
115
|
+
compactionTarget?: "budget" | "threshold"
|
|
116
|
+
customInstructions?: string
|
|
117
|
+
runtimeContext?: Record<string, unknown>
|
|
118
|
+
}): Promise<CompactResult>
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function loadLegacyContextEngine(): Promise<
|
|
123
|
+
LegacyContextEngineModule["LegacyContextEngine"]
|
|
124
|
+
> {
|
|
125
|
+
const pluginSdkPath = require.resolve("openclaw/plugin-sdk")
|
|
126
|
+
const legacyPath = resolve(
|
|
127
|
+
dirname(pluginSdkPath),
|
|
128
|
+
"context-engine",
|
|
129
|
+
"legacy.js",
|
|
130
|
+
)
|
|
131
|
+
const module = (await import(
|
|
132
|
+
pathToFileURL(legacyPath).href
|
|
133
|
+
)) as LegacyContextEngineModule
|
|
134
|
+
return module.LegacyContextEngine
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class BasicMemoryContextEngine implements ContextEngine {
|
|
138
|
+
readonly info = {
|
|
139
|
+
id: "openclaw-basic-memory",
|
|
140
|
+
name: "Basic Memory Context Engine",
|
|
141
|
+
version: "0.1.5",
|
|
142
|
+
ownsCompaction: false,
|
|
143
|
+
} as const
|
|
144
|
+
|
|
145
|
+
private readonly sessionState = new Map<string, SessionMemoryState>()
|
|
146
|
+
private readonly subagentState = new Map<string, SubagentHandoffState>()
|
|
147
|
+
private legacyContextEnginePromise: Promise<
|
|
148
|
+
InstanceType<LegacyContextEngineModule["LegacyContextEngine"]>
|
|
149
|
+
> | null = null
|
|
150
|
+
|
|
151
|
+
constructor(
|
|
152
|
+
private readonly client: BmClient,
|
|
153
|
+
private readonly cfg: BasicMemoryConfig,
|
|
154
|
+
) {}
|
|
155
|
+
|
|
156
|
+
async bootstrap(params: {
|
|
157
|
+
sessionId: string
|
|
158
|
+
sessionFile: string
|
|
159
|
+
}): Promise<BootstrapResult> {
|
|
160
|
+
if (!this.cfg.autoRecall) {
|
|
161
|
+
this.sessionState.delete(params.sessionId)
|
|
162
|
+
return { bootstrapped: false, reason: "autoRecall disabled" }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const recall = await loadRecallState(this.client, this.cfg)
|
|
167
|
+
if (!recall) {
|
|
168
|
+
this.sessionState.delete(params.sessionId)
|
|
169
|
+
return { bootstrapped: false, reason: "no recall context found" }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.sessionState.set(params.sessionId, {
|
|
173
|
+
recallContext: boundRecallContext(recall.context),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
log.debug(
|
|
177
|
+
`context-engine bootstrap: session=${params.sessionId} tasks=${recall.tasks.length} recent=${recall.recent.length}`,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return { bootstrapped: true }
|
|
181
|
+
} catch (err) {
|
|
182
|
+
this.sessionState.delete(params.sessionId)
|
|
183
|
+
log.error("context-engine bootstrap failed", err)
|
|
184
|
+
return { bootstrapped: false, reason: "recall failed" }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async ingest(): Promise<{ ingested: boolean }> {
|
|
189
|
+
return { ingested: false }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async assemble(params: {
|
|
193
|
+
sessionId: string
|
|
194
|
+
messages: AgentMessage[]
|
|
195
|
+
tokenBudget?: number
|
|
196
|
+
}): Promise<AssembleResult> {
|
|
197
|
+
const state = this.sessionState.get(params.sessionId)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
messages: params.messages,
|
|
201
|
+
estimatedTokens: 0,
|
|
202
|
+
systemPromptAddition: state?.recallContext,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async afterTurn(params: {
|
|
207
|
+
sessionId: string
|
|
208
|
+
sessionFile: string
|
|
209
|
+
messages: AgentMessage[]
|
|
210
|
+
prePromptMessageCount: number
|
|
211
|
+
autoCompactionSummary?: string
|
|
212
|
+
isHeartbeat?: boolean
|
|
213
|
+
tokenBudget?: number
|
|
214
|
+
runtimeContext?: Record<string, unknown>
|
|
215
|
+
}): Promise<void> {
|
|
216
|
+
if (!this.cfg.autoCapture) return
|
|
217
|
+
|
|
218
|
+
const newMessages = params.messages.slice(params.prePromptMessageCount)
|
|
219
|
+
const turn =
|
|
220
|
+
selectCaptureTurn(newMessages, this.cfg.captureMinChars) ??
|
|
221
|
+
selectCaptureTurn(params.messages, this.cfg.captureMinChars)
|
|
222
|
+
|
|
223
|
+
if (!turn) return
|
|
224
|
+
|
|
225
|
+
log.debug(
|
|
226
|
+
`context-engine afterTurn: session=${params.sessionId} user=${turn.userText.length} assistant=${turn.assistantText.length}`,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await this.client.indexConversation(turn.userText, turn.assistantText)
|
|
231
|
+
} catch (err) {
|
|
232
|
+
log.error("context-engine capture failed", err)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async compact(params: {
|
|
237
|
+
sessionId: string
|
|
238
|
+
sessionFile: string
|
|
239
|
+
tokenBudget?: number
|
|
240
|
+
force?: boolean
|
|
241
|
+
currentTokenCount?: number
|
|
242
|
+
compactionTarget?: "budget" | "threshold"
|
|
243
|
+
customInstructions?: string
|
|
244
|
+
runtimeContext?: Record<string, unknown>
|
|
245
|
+
}): Promise<CompactResult> {
|
|
246
|
+
const legacy = await this.getLegacyContextEngine()
|
|
247
|
+
return legacy.compact(params)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async prepareSubagentSpawn(params: {
|
|
251
|
+
parentSessionKey: string
|
|
252
|
+
childSessionKey: string
|
|
253
|
+
ttlMs?: number
|
|
254
|
+
}): Promise<{ rollback: () => Promise<void> } | undefined> {
|
|
255
|
+
const parentState = this.sessionState.get(params.parentSessionKey)
|
|
256
|
+
const noteTitle = buildSubagentNoteTitle(params.childSessionKey)
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const note = await this.client.writeNote(
|
|
260
|
+
noteTitle,
|
|
261
|
+
buildSubagentHandoffContent({
|
|
262
|
+
parentSessionKey: params.parentSessionKey,
|
|
263
|
+
childSessionKey: params.childSessionKey,
|
|
264
|
+
recallContext: parentState?.recallContext,
|
|
265
|
+
}),
|
|
266
|
+
SUBAGENT_HANDOFF_FOLDER,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
this.subagentState.set(params.childSessionKey, {
|
|
270
|
+
noteIdentifier: note.permalink,
|
|
271
|
+
noteTitle: note.title,
|
|
272
|
+
parentSessionKey: params.parentSessionKey,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
rollback: async () => {
|
|
277
|
+
const handoff = this.subagentState.get(params.childSessionKey)
|
|
278
|
+
this.subagentState.delete(params.childSessionKey)
|
|
279
|
+
if (!handoff) return
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await this.client.deleteNote(handoff.noteIdentifier)
|
|
283
|
+
} catch (err) {
|
|
284
|
+
log.error("context-engine subagent rollback failed", err)
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
log.error("context-engine prepareSubagentSpawn failed", err)
|
|
290
|
+
return undefined
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async onSubagentEnded(params: {
|
|
295
|
+
childSessionKey: string
|
|
296
|
+
reason: "deleted" | "completed" | "swept" | "released"
|
|
297
|
+
}): Promise<void> {
|
|
298
|
+
const handoff = this.subagentState.get(params.childSessionKey)
|
|
299
|
+
if (!handoff) return
|
|
300
|
+
|
|
301
|
+
this.subagentState.delete(params.childSessionKey)
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
await this.client.editNote(
|
|
305
|
+
handoff.noteIdentifier,
|
|
306
|
+
"append",
|
|
307
|
+
buildSubagentCompletionUpdate(params),
|
|
308
|
+
)
|
|
309
|
+
} catch (err) {
|
|
310
|
+
log.error("context-engine onSubagentEnded failed", err)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async dispose(): Promise<void> {
|
|
315
|
+
this.sessionState.clear()
|
|
316
|
+
this.subagentState.clear()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private async getLegacyContextEngine(): Promise<
|
|
320
|
+
InstanceType<LegacyContextEngineModule["LegacyContextEngine"]>
|
|
321
|
+
> {
|
|
322
|
+
if (!this.legacyContextEnginePromise) {
|
|
323
|
+
this.legacyContextEnginePromise = loadLegacyContextEngine().then(
|
|
324
|
+
(LegacyContextEngine) => new LegacyContextEngine(),
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return this.legacyContextEnginePromise
|
|
329
|
+
}
|
|
330
|
+
}
|
package/hooks/capture.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { log } from "../logger.ts"
|
|
|
5
5
|
/**
|
|
6
6
|
* Extract text content from a message object.
|
|
7
7
|
*/
|
|
8
|
-
function extractText(msg: Record<string, unknown>): string {
|
|
8
|
+
export function extractText(msg: Record<string, unknown>): string {
|
|
9
9
|
const content = msg.content
|
|
10
10
|
if (typeof content === "string") return content
|
|
11
11
|
|
|
@@ -27,7 +27,7 @@ function extractText(msg: Record<string, unknown>): string {
|
|
|
27
27
|
/**
|
|
28
28
|
* Find the last user+assistant turn from the messages array.
|
|
29
29
|
*/
|
|
30
|
-
function getLastTurn(messages: unknown[]): {
|
|
30
|
+
export function getLastTurn(messages: unknown[]): {
|
|
31
31
|
userText: string
|
|
32
32
|
assistantText: string
|
|
33
33
|
} {
|
|
@@ -60,6 +60,19 @@ function getLastTurn(messages: unknown[]): {
|
|
|
60
60
|
return { userText, assistantText }
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export function selectCaptureTurn(
|
|
64
|
+
messages: unknown[],
|
|
65
|
+
minChars: number,
|
|
66
|
+
): { userText: string; assistantText: string } | null {
|
|
67
|
+
const turn = getLastTurn(messages)
|
|
68
|
+
if (!turn.userText && !turn.assistantText) return null
|
|
69
|
+
if (turn.userText.length < minChars && turn.assistantText.length < minChars) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return turn
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
/**
|
|
64
77
|
* Build the post-turn capture handler for Mode B.
|
|
65
78
|
*
|
|
@@ -77,10 +90,9 @@ export function buildCaptureHandler(client: BmClient, cfg: BasicMemoryConfig) {
|
|
|
77
90
|
return
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (userText.length < minChars && assistantText.length < minChars) return
|
|
93
|
+
const turn = selectCaptureTurn(event.messages, minChars)
|
|
94
|
+
if (!turn) return
|
|
95
|
+
const { userText, assistantText } = turn
|
|
84
96
|
|
|
85
97
|
log.debug(
|
|
86
98
|
`capturing conversation: user=${userText.length} chars, assistant=${assistantText.length} chars`,
|
package/hooks/recall.ts
CHANGED
|
@@ -2,6 +2,12 @@ import type { BmClient, RecentResult, SearchResult } from "../bm-client.ts"
|
|
|
2
2
|
import type { BasicMemoryConfig } from "../config.ts"
|
|
3
3
|
import { log } from "../logger.ts"
|
|
4
4
|
|
|
5
|
+
export interface RecallState {
|
|
6
|
+
tasks: SearchResult[]
|
|
7
|
+
recent: RecentResult[]
|
|
8
|
+
context: string
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
/**
|
|
6
12
|
* Format recalled context from active tasks and recent activity.
|
|
7
13
|
* Returns empty string if nothing was found.
|
|
@@ -32,6 +38,24 @@ export function formatRecallContext(
|
|
|
32
38
|
return `${sections.join("\n\n")}\n\n---\n${prompt}`
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
export async function loadRecallState(
|
|
42
|
+
client: BmClient,
|
|
43
|
+
cfg: BasicMemoryConfig,
|
|
44
|
+
): Promise<RecallState | null> {
|
|
45
|
+
const [tasks, recent] = await Promise.all([
|
|
46
|
+
client.search(undefined, 5, undefined, {
|
|
47
|
+
note_types: ["Task"],
|
|
48
|
+
status: "active",
|
|
49
|
+
}),
|
|
50
|
+
client.recentActivity("1d"),
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
const context = formatRecallContext(tasks, recent, cfg.recallPrompt)
|
|
54
|
+
if (!context) return null
|
|
55
|
+
|
|
56
|
+
return { tasks, recent, context }
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
/**
|
|
36
60
|
* Build the pre-turn recall handler.
|
|
37
61
|
*
|
|
@@ -41,22 +65,14 @@ export function formatRecallContext(
|
|
|
41
65
|
export function buildRecallHandler(client: BmClient, cfg: BasicMemoryConfig) {
|
|
42
66
|
return async (_event: Record<string, unknown>) => {
|
|
43
67
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
note_types: ["Task"],
|
|
47
|
-
status: "active",
|
|
48
|
-
}),
|
|
49
|
-
client.recentActivity("1d"),
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
const context = formatRecallContext(tasks, recent, cfg.recallPrompt)
|
|
53
|
-
if (!context) return {}
|
|
68
|
+
const recall = await loadRecallState(client, cfg)
|
|
69
|
+
if (!recall) return {}
|
|
54
70
|
|
|
55
71
|
log.debug(
|
|
56
|
-
`recall: ${tasks.length} active tasks, ${recent.length} recent items`,
|
|
72
|
+
`recall: ${recall.tasks.length} active tasks, ${recall.recent.length} recent items`,
|
|
57
73
|
)
|
|
58
74
|
|
|
59
|
-
return { context }
|
|
75
|
+
return { context: recall.context }
|
|
60
76
|
} catch (err) {
|
|
61
77
|
log.error("recall failed", err)
|
|
62
78
|
return {}
|
package/index.ts
CHANGED
|
@@ -10,9 +10,7 @@ import {
|
|
|
10
10
|
parseConfig,
|
|
11
11
|
resolveProjectPath,
|
|
12
12
|
} from "./config.ts"
|
|
13
|
-
|
|
14
|
-
import { buildCaptureHandler } from "./hooks/capture.ts"
|
|
15
|
-
import { buildRecallHandler } from "./hooks/recall.ts"
|
|
13
|
+
import { BasicMemoryContextEngine } from "./context-engine/basic-memory-context-engine.ts"
|
|
16
14
|
import { initLogger, log } from "./logger.ts"
|
|
17
15
|
import { CONVERSATION_SCHEMA_CONTENT } from "./schema/conversation-schema.ts"
|
|
18
16
|
import { TASK_SCHEMA_CONTENT } from "./schema/task-schema.ts"
|
|
@@ -33,6 +31,8 @@ import { registerSchemaValidateTool } from "./tools/schema-validate.ts"
|
|
|
33
31
|
import { registerSearchTool } from "./tools/search-notes.ts"
|
|
34
32
|
import { registerWriteTool } from "./tools/write-note.ts"
|
|
35
33
|
|
|
34
|
+
const BASIC_MEMORY_RELEASE_TAG = "v0.20.2"
|
|
35
|
+
|
|
36
36
|
export default {
|
|
37
37
|
id: "openclaw-basic-memory",
|
|
38
38
|
name: "Basic Memory",
|
|
@@ -69,14 +69,11 @@ export default {
|
|
|
69
69
|
// --- Composited memory_search + memory_get (always registered) ---
|
|
70
70
|
registerMemoryProvider(api, client, cfg)
|
|
71
71
|
log.info("registered composited memory_search + memory_get")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (cfg.autoRecall) {
|
|
78
|
-
api.on("agent_start", buildRecallHandler(client, cfg))
|
|
79
|
-
}
|
|
72
|
+
api.registerContextEngine(
|
|
73
|
+
"openclaw-basic-memory",
|
|
74
|
+
() => new BasicMemoryContextEngine(client, cfg),
|
|
75
|
+
)
|
|
76
|
+
log.info("registered Basic Memory context engine")
|
|
80
77
|
|
|
81
78
|
// --- Commands ---
|
|
82
79
|
registerCommands(api, client)
|
|
@@ -101,7 +98,7 @@ export default {
|
|
|
101
98
|
"installing basic-memory via uv (this may take a minute)...",
|
|
102
99
|
)
|
|
103
100
|
const result = execSync(
|
|
104
|
-
|
|
101
|
+
`uv tool install "basic-memory @ git+https://github.com/basicmachines-co/basic-memory.git@${BASIC_MEMORY_RELEASE_TAG}" --force`,
|
|
105
102
|
{ encoding: "utf-8", timeout: 120_000, stdio: "pipe" },
|
|
106
103
|
)
|
|
107
104
|
log.info(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basicmemory/openclaw-basic-memory",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Basic Memory plugin for OpenClaw — local-first knowledge graph for agent memory",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"index.ts",
|
|
13
13
|
"bm-client.ts",
|
|
14
|
+
"context-engine/",
|
|
14
15
|
"config.ts",
|
|
15
16
|
"logger.ts",
|
|
16
17
|
"commands/cli.ts",
|
|
@@ -31,7 +32,6 @@
|
|
|
31
32
|
"tools/schema-validate.ts",
|
|
32
33
|
"tools/search-notes.ts",
|
|
33
34
|
"tools/write-note.ts",
|
|
34
|
-
"types/openclaw.d.ts",
|
|
35
35
|
"schema/task-schema.ts",
|
|
36
36
|
"schema/conversation-schema.ts",
|
|
37
37
|
"skills/",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@sinclair/typebox": "0.34.47"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"openclaw": ">=2026.
|
|
52
|
+
"openclaw": ">=2026.3.7"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"postinstall": "bash scripts/setup-bm.sh || true",
|
|
@@ -69,8 +69,9 @@
|
|
|
69
69
|
]
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@biomejs/biome": "^2.
|
|
72
|
+
"@biomejs/biome": "^2.4.6",
|
|
73
73
|
"@types/node": "^20.0.0",
|
|
74
|
+
"openclaw": "^2026.3.8",
|
|
74
75
|
"typescript": "^5.9.3"
|
|
75
76
|
}
|
|
76
77
|
}
|
package/scripts/setup-bm.sh
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Install basic-memory CLI via uv from
|
|
2
|
+
# Install basic-memory CLI via uv from a pinned release tag.
|
|
3
3
|
# Idempotent — safe to re-run. Fails gracefully when uv is absent.
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
6
6
|
BM_REPO="https://github.com/basicmachines-co/basic-memory.git"
|
|
7
|
-
BM_REF="${BM_REF:-
|
|
7
|
+
BM_REF="${BM_REF:-v0.20.2}"
|
|
8
8
|
|
|
9
9
|
# ── check for uv ──────────────────────────────────────────────────
|
|
10
10
|
if ! command -v uv >/dev/null 2>&1; then
|
package/skills/manifest.json
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"name": "memory-lifecycle",
|
|
15
15
|
"description": "Manage entity status transitions in Basic Memory: archive completed work, move notes between status folders, update frontmatter, and handle edge cases. Use when marking items complete, archiving old entities, or managing any folder-based status workflow."
|
|
16
16
|
},
|
|
17
|
+
{
|
|
18
|
+
"dir": "memory-literary-analysis",
|
|
19
|
+
"name": "memory-literary-analysis",
|
|
20
|
+
"description": "Analyze a complete literary work into a structured Basic Memory knowledge graph. Covers schema design, entity seeding, chapter-by-chapter processing, cross-referencing, validation, and visualization."
|
|
21
|
+
},
|
|
17
22
|
{
|
|
18
23
|
"dir": "memory-metadata-search",
|
|
19
24
|
"name": "memory-metadata-search",
|