@basicmemory/openclaw-basic-memory 0.24.1 → 1.0.0
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/README.md +24 -12
- package/bm-client.ts +302 -112
- package/commands/cli.ts +5 -6
- package/commands/slash.ts +4 -4
- package/context-engine/basic-memory-context-engine.ts +74 -9
- package/context-engine/types.ts +21 -0
- package/dist/bm-client.d.ts +61 -12
- package/dist/bm-client.js +250 -101
- package/dist/commands/cli.d.ts +1 -1
- package/dist/commands/cli.js +5 -5
- package/dist/commands/slash.d.ts +1 -1
- package/dist/commands/slash.js +4 -3
- package/dist/context-engine/basic-memory-context-engine.d.ts +20 -1
- package/dist/context-engine/basic-memory-context-engine.js +50 -2
- package/dist/context-engine/types.d.ts +9 -0
- package/dist/context-engine/types.js +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +79 -69
- package/dist/memory/basic-memory-capability.d.ts +19 -0
- package/dist/memory/basic-memory-capability.js +190 -0
- package/dist/tools/build-context.d.ts +1 -1
- package/dist/tools/build-context.js +12 -4
- package/dist/tools/delete-note.d.ts +1 -1
- package/dist/tools/delete-note.js +2 -1
- package/dist/tools/edit-note.d.ts +1 -1
- package/dist/tools/edit-note.js +22 -12
- package/dist/tools/list-memory-projects.d.ts +1 -1
- package/dist/tools/list-memory-projects.js +2 -1
- package/dist/tools/list-workspaces.d.ts +1 -1
- package/dist/tools/list-workspaces.js +4 -4
- package/dist/tools/move-note.d.ts +1 -1
- package/dist/tools/move-note.js +2 -1
- package/dist/tools/read-note.d.ts +1 -1
- package/dist/tools/read-note.js +2 -1
- package/dist/tools/schema-diff.d.ts +1 -1
- package/dist/tools/schema-diff.js +2 -1
- package/dist/tools/schema-infer.d.ts +1 -1
- package/dist/tools/schema-infer.js +2 -1
- package/dist/tools/schema-validate.d.ts +1 -1
- package/dist/tools/schema-validate.js +2 -1
- package/dist/tools/search-notes.d.ts +1 -1
- package/dist/tools/search-notes.js +13 -6
- package/dist/tools/write-note.d.ts +1 -1
- package/dist/tools/write-note.js +11 -12
- package/index.ts +93 -84
- package/memory/basic-memory-capability.ts +253 -0
- package/openclaw.plugin.json +0 -2
- package/package.json +6 -5
- package/skills/manifest.json +5 -0
- package/skills/memory-agents/SKILL.md +23 -10
- package/skills/memory-capture/SKILL.md +29 -18
- package/skills/memory-comark/SKILL.md +17 -6
- package/skills/memory-continue/SKILL.md +47 -27
- package/skills/memory-curate/SKILL.md +40 -24
- package/skills/memory-defrag/SKILL.md +18 -7
- package/skills/memory-doctor/SKILL.md +93 -0
- package/skills/memory-ingest/SKILL.md +33 -17
- package/skills/memory-lifecycle/SKILL.md +35 -22
- package/skills/memory-literary-analysis/SKILL.md +112 -75
- package/skills/memory-metadata-search/SKILL.md +39 -28
- package/skills/memory-notes/SKILL.md +75 -50
- package/skills/memory-okf/SKILL.md +27 -15
- package/skills/memory-onboarding/SKILL.md +20 -9
- package/skills/memory-onboarding/references/assistant-setup.md +1 -1
- package/skills/memory-onboarding/references/conventions.md +4 -4
- package/skills/memory-onboarding/references/domain-playbooks.md +1 -1
- package/skills/memory-onboarding/references/schema-guide.md +9 -8
- package/skills/memory-quest/SKILL.md +18 -7
- package/skills/memory-reflect/SKILL.md +25 -13
- package/skills/memory-research/SKILL.md +28 -13
- package/skills/memory-schema/SKILL.md +36 -24
- package/skills/memory-tasks/SKILL.md +34 -22
- package/tools/build-context.ts +14 -5
- package/tools/delete-note.ts +2 -2
- package/tools/edit-note.ts +27 -14
- package/tools/list-memory-projects.ts +6 -2
- package/tools/list-workspaces.ts +8 -5
- package/tools/move-note.ts +2 -2
- package/tools/read-note.ts +2 -2
- package/tools/schema-diff.ts +2 -2
- package/tools/schema-infer.ts +2 -2
- package/tools/schema-validate.ts +2 -2
- package/tools/search-notes.ts +18 -7
- package/tools/write-note.ts +12 -12
- package/dist/tools/memory-provider.d.ts +0 -15
- package/dist/tools/memory-provider.js +0 -275
- package/tools/memory-provider.ts +0 -336
package/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { promisify } from "node:util"
|
|
2
4
|
|
|
3
5
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"
|
|
4
6
|
import { BmClient } from "./bm-client.ts"
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
} from "./config.ts"
|
|
13
15
|
import { BasicMemoryContextEngine } from "./context-engine/basic-memory-context-engine.ts"
|
|
14
16
|
import { initLogger, log } from "./logger.ts"
|
|
17
|
+
import { buildBasicMemoryCapability } from "./memory/basic-memory-capability.ts"
|
|
15
18
|
import { CONVERSATION_SCHEMA_CONTENT } from "./schema/conversation-schema.ts"
|
|
16
19
|
import { TASK_SCHEMA_CONTENT } from "./schema/task-schema.ts"
|
|
17
20
|
import { registerContextTool } from "./tools/build-context.ts"
|
|
@@ -19,10 +22,6 @@ import { registerDeleteTool } from "./tools/delete-note.ts"
|
|
|
19
22
|
import { registerEditTool } from "./tools/edit-note.ts"
|
|
20
23
|
import { registerProjectListTool } from "./tools/list-memory-projects.ts"
|
|
21
24
|
import { registerWorkspaceListTool } from "./tools/list-workspaces.ts"
|
|
22
|
-
import {
|
|
23
|
-
registerMemoryProvider,
|
|
24
|
-
setWorkspaceDir,
|
|
25
|
-
} from "./tools/memory-provider.ts"
|
|
26
25
|
import { registerMoveTool } from "./tools/move-note.ts"
|
|
27
26
|
import { registerReadTool } from "./tools/read-note.ts"
|
|
28
27
|
import { registerSchemaDiffTool } from "./tools/schema-diff.ts"
|
|
@@ -31,20 +30,71 @@ import { registerSchemaValidateTool } from "./tools/schema-validate.ts"
|
|
|
31
30
|
import { registerSearchTool } from "./tools/search-notes.ts"
|
|
32
31
|
import { registerWriteTool } from "./tools/write-note.ts"
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
const execFileAsync = promisify(execFile)
|
|
34
|
+
|
|
35
|
+
// Async so a probe or an install never blocks OpenClaw's event loop during
|
|
36
|
+
// plugin startup.
|
|
37
|
+
export async function isCommandAvailable(command: string): Promise<boolean> {
|
|
35
38
|
try {
|
|
36
|
-
|
|
39
|
+
await execFileAsync(command, ["--version"])
|
|
37
40
|
return true
|
|
38
41
|
} catch {
|
|
39
42
|
return false
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
async function installBasicMemoryIfMissing(bmBin: string): Promise<void> {
|
|
47
|
+
if (await isCommandAvailable(bmBin)) return
|
|
48
|
+
|
|
49
|
+
log.info("bm CLI not found on PATH — attempting auto-install...")
|
|
50
|
+
if (!(await isCommandAvailable("uv"))) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Cannot auto-install basic-memory: uv not found. " +
|
|
53
|
+
"Install uv first (brew install uv, or curl -LsSf https://astral.sh/uv/install.sh | sh), " +
|
|
54
|
+
"then restart the gateway.",
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
log.info("installing basic-memory via uv (this may take a minute)...")
|
|
59
|
+
// basic-memory pins a FastMCP pre-release; without this flag uv silently
|
|
60
|
+
// installs the last release that had none (#1338).
|
|
61
|
+
const { stdout } = await execFileAsync(
|
|
62
|
+
"uv",
|
|
63
|
+
["tool", "install", "basic-memory", "--prerelease=allow", "--force"],
|
|
64
|
+
{ encoding: "utf-8", timeout: 120_000 },
|
|
65
|
+
)
|
|
66
|
+
log.info(`basic-memory installed: ${stdout.trim().split("\n").pop()}`)
|
|
67
|
+
if (!(await isCommandAvailable(bmBin))) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
"bm installed but not found on PATH. You may need to add uv's bin directory to your PATH (typically ~/.local/bin).",
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function seedSchemas(client: BmClient): Promise<void> {
|
|
75
|
+
for (const [name, content] of [
|
|
76
|
+
["Task", TASK_SCHEMA_CONTENT],
|
|
77
|
+
["Conversation", CONVERSATION_SCHEMA_CONTENT],
|
|
78
|
+
] as const) {
|
|
79
|
+
try {
|
|
80
|
+
await client.readNote(`schema/${name}`)
|
|
81
|
+
log.debug(`${name} schema already exists, skipping seed`)
|
|
82
|
+
} catch {
|
|
83
|
+
try {
|
|
84
|
+
await client.writeNote(name, content, "schema")
|
|
85
|
+
log.debug(`seeded ${name} schema note`)
|
|
86
|
+
} catch (err) {
|
|
87
|
+
log.debug(`${name} schema seed failed (non-fatal)`, err)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
43
93
|
export default definePluginEntry({
|
|
44
94
|
id: "openclaw-basic-memory",
|
|
45
95
|
name: "Basic Memory",
|
|
46
96
|
description:
|
|
47
|
-
"Local-first knowledge graph for OpenClaw — persistent memory with graph search and
|
|
97
|
+
"Local-first knowledge graph for OpenClaw — persistent memory with graph search, recall and conversation capture",
|
|
48
98
|
configSchema: basicMemoryConfigSchema,
|
|
49
99
|
|
|
50
100
|
register(api) {
|
|
@@ -72,9 +122,14 @@ export default definePluginEntry({
|
|
|
72
122
|
registerSchemaInferTool(api, client)
|
|
73
123
|
registerSchemaDiffTool(api, client)
|
|
74
124
|
|
|
75
|
-
// ---
|
|
76
|
-
|
|
77
|
-
|
|
125
|
+
// --- Memory capability ---
|
|
126
|
+
// OpenClaw 2026.10 owns the memory_search and memory_get tool names
|
|
127
|
+
// (memory-core registers them even when another plugin holds the memory
|
|
128
|
+
// slot), so the plugin serves the host through registerMemoryCapability:
|
|
129
|
+
// gateway memory search, status and doctor, Active Memory recall, and the
|
|
130
|
+
// pre-compaction flush.
|
|
131
|
+
api.registerMemoryCapability(buildBasicMemoryCapability(client))
|
|
132
|
+
log.info("registered Basic Memory memory capability")
|
|
78
133
|
api.registerContextEngine(
|
|
79
134
|
"openclaw-basic-memory",
|
|
80
135
|
() => new BasicMemoryContextEngine(client, cfg),
|
|
@@ -92,83 +147,37 @@ export default definePluginEntry({
|
|
|
92
147
|
start: async (ctx: { config?: unknown; workspaceDir?: string }) => {
|
|
93
148
|
log.info("starting...")
|
|
94
149
|
|
|
95
|
-
// Auto-install bm CLI if not found
|
|
96
|
-
const bmBin = cfg.bmPath || "bm"
|
|
97
|
-
if (!isCommandAvailable(bmBin)) {
|
|
98
|
-
log.info("bm CLI not found on PATH — attempting auto-install...")
|
|
99
|
-
try {
|
|
100
|
-
if (!isCommandAvailable("uv")) {
|
|
101
|
-
throw new Error("uv not found")
|
|
102
|
-
}
|
|
103
|
-
log.info(
|
|
104
|
-
"installing basic-memory via uv (this may take a minute)...",
|
|
105
|
-
)
|
|
106
|
-
// basic-memory pins a FastMCP pre-release; without this flag uv
|
|
107
|
-
// silently installs the last release that had none (#1338).
|
|
108
|
-
const result = execFileSync(
|
|
109
|
-
"uv",
|
|
110
|
-
[
|
|
111
|
-
"tool",
|
|
112
|
-
"install",
|
|
113
|
-
"basic-memory",
|
|
114
|
-
"--prerelease=allow",
|
|
115
|
-
"--force",
|
|
116
|
-
],
|
|
117
|
-
{
|
|
118
|
-
encoding: "utf-8",
|
|
119
|
-
timeout: 120_000,
|
|
120
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
121
|
-
},
|
|
122
|
-
)
|
|
123
|
-
log.info(
|
|
124
|
-
`basic-memory installed: ${result.trim().split("\n").pop()}`,
|
|
125
|
-
)
|
|
126
|
-
// Verify it worked
|
|
127
|
-
if (isCommandAvailable(bmBin)) {
|
|
128
|
-
log.info("bm CLI now available on PATH")
|
|
129
|
-
} else {
|
|
130
|
-
log.error(
|
|
131
|
-
"bm installed but not found on PATH. You may need to add uv's bin directory to your PATH (typically ~/.local/bin).",
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
} catch (_uvErr) {
|
|
135
|
-
log.error(
|
|
136
|
-
"Cannot auto-install basic-memory: uv not found. " +
|
|
137
|
-
"Install uv first (brew install uv, or curl -LsSf https://astral.sh/uv/install.sh | sh), " +
|
|
138
|
-
"then restart the gateway.",
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
150
|
const workspace = ctx.workspaceDir ?? process.cwd()
|
|
144
151
|
const projectPath = resolveProjectPath(cfg.projectPath, workspace)
|
|
145
152
|
cfg.projectPath = projectPath
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
154
|
+
// bm runs with the workspace as its working directory. OpenClaw
|
|
155
|
+
// creates its default workspace only on the first agent run, and
|
|
156
|
+
// spawning into a missing directory fails with an ENOENT that names
|
|
157
|
+
// the bm binary instead of the directory.
|
|
158
|
+
await mkdir(workspace, { recursive: true })
|
|
159
|
+
|
|
160
|
+
// OpenClaw gives a service about 5 s to start. A first start (bm
|
|
161
|
+
// install, cold imports, first indexing) takes far longer, and a
|
|
162
|
+
// timed-out start fails the plugin. So startup runs in the
|
|
163
|
+
// background; tool calls wait for it inside BmClient, and a failed
|
|
164
|
+
// startup is retried by the next tool call instead of failing here.
|
|
165
|
+
client
|
|
166
|
+
.start({
|
|
167
|
+
cwd: workspace,
|
|
168
|
+
projectPath,
|
|
169
|
+
prepare: () => installBasicMemoryIfMissing(cfg.bmPath || "bm"),
|
|
170
|
+
})
|
|
171
|
+
.then(async () => {
|
|
172
|
+
log.info("connected — BM MCP stdio session running")
|
|
173
|
+
await seedSchemas(client)
|
|
174
|
+
})
|
|
175
|
+
.catch((err) => {
|
|
176
|
+
log.error(
|
|
177
|
+
"Basic Memory startup failed; the next tool call will retry",
|
|
178
|
+
err,
|
|
179
|
+
)
|
|
180
|
+
})
|
|
172
181
|
},
|
|
173
182
|
stop: async () => {
|
|
174
183
|
log.info("stopping BM MCP session...")
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { MemoryPluginCapability } from "openclaw/plugin-sdk/memory-host-core"
|
|
2
|
+
import {
|
|
3
|
+
type BmClient,
|
|
4
|
+
isNoteNotFoundError,
|
|
5
|
+
searchResultText,
|
|
6
|
+
} from "../bm-client.ts"
|
|
7
|
+
import { log } from "../logger.ts"
|
|
8
|
+
|
|
9
|
+
// OpenClaw documents deriving the provider types from the public capability
|
|
10
|
+
// type rather than importing its private memory-host-search facade.
|
|
11
|
+
type MemoryProviderRuntime = NonNullable<
|
|
12
|
+
MemoryPluginCapability["providerRuntime"]
|
|
13
|
+
>
|
|
14
|
+
type MemoryProviderHandle = NonNullable<
|
|
15
|
+
Awaited<ReturnType<MemoryProviderRuntime["open"]>>["provider"]
|
|
16
|
+
>
|
|
17
|
+
type MemorySearchRequest = Parameters<MemoryProviderHandle["search"]>[0]
|
|
18
|
+
type MemorySearchPage = Awaited<ReturnType<MemoryProviderHandle["search"]>>
|
|
19
|
+
type MemoryGetRequest = Parameters<MemoryProviderHandle["get"]>[0]
|
|
20
|
+
type MemoryGetResult = Awaited<ReturnType<MemoryProviderHandle["get"]>>
|
|
21
|
+
type MemoryHealth = Awaited<ReturnType<MemoryProviderHandle["health"]>>
|
|
22
|
+
type MemoryCallerContext = Parameters<
|
|
23
|
+
MemoryProviderRuntime["open"]
|
|
24
|
+
>[0]["context"]
|
|
25
|
+
type MemoryProviderFlushPlanResolver = NonNullable<
|
|
26
|
+
MemoryPluginCapability["providerFlushPlanResolver"]
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
export const MEMORY_PROVIDER_ID = "openclaw-basic-memory"
|
|
30
|
+
|
|
31
|
+
// Agent tools this plugin registers that Active Memory may use for deep
|
|
32
|
+
// recall, and that the pre-compaction flush may use to look before writing.
|
|
33
|
+
export const RECALL_TOOL_NAMES = [
|
|
34
|
+
"search_notes",
|
|
35
|
+
"read_note",
|
|
36
|
+
"build_context",
|
|
37
|
+
] as const
|
|
38
|
+
const FLUSH_PERSISTENCE_TOOL_NAMES = ["write_note", "edit_note"] as const
|
|
39
|
+
const FLUSH_LOOKUP_TOOL_NAMES = ["search_notes", "read_note"] as const
|
|
40
|
+
|
|
41
|
+
const DEFAULT_MAX_RESULTS = 10
|
|
42
|
+
const MAX_EXCERPT_CHARS = 700
|
|
43
|
+
// memory-core's flush prompts use the same token for "nothing to save".
|
|
44
|
+
const SILENT_REPLY_TOKEN = "NO_REPLY"
|
|
45
|
+
|
|
46
|
+
// --- Search and retrieval ---
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Basic Memory fuses keyword and vector scores into [0, 1.3] (see
|
|
50
|
+
* search_reader.py FUSION_BONUS). OpenClaw compares provider scores against a
|
|
51
|
+
* caller's minScore on a 0–1 scale, so a fused score above 1 is capped at 1.
|
|
52
|
+
*/
|
|
53
|
+
function hostScore(score: number | undefined): number | undefined {
|
|
54
|
+
return score === undefined ? undefined : Math.min(score, 1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function excerptOf(content: string): string {
|
|
58
|
+
return content.length > MAX_EXCERPT_CHARS
|
|
59
|
+
? `${content.slice(0, MAX_EXCERPT_CHARS)}…`
|
|
60
|
+
: content
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Apply the host's optional line window to a note body. `from` is 1-based,
|
|
65
|
+
* matching the host's memory_get contract.
|
|
66
|
+
*/
|
|
67
|
+
function sliceLines(
|
|
68
|
+
text: string,
|
|
69
|
+
from: number | undefined,
|
|
70
|
+
lines: number | undefined,
|
|
71
|
+
): Pick<
|
|
72
|
+
Extract<MemoryGetResult, { status: "ok" }>,
|
|
73
|
+
"text" | "truncated" | "from" | "lines" | "nextFrom"
|
|
74
|
+
> {
|
|
75
|
+
if (from === undefined && lines === undefined) return { text }
|
|
76
|
+
|
|
77
|
+
const all = text.split("\n")
|
|
78
|
+
const start = Math.max(1, from ?? 1)
|
|
79
|
+
const count = lines ?? all.length - start + 1
|
|
80
|
+
const window = all.slice(start - 1, start - 1 + count)
|
|
81
|
+
const end = start - 1 + window.length
|
|
82
|
+
const truncated = end < all.length
|
|
83
|
+
return {
|
|
84
|
+
text: window.join("\n"),
|
|
85
|
+
from: start,
|
|
86
|
+
lines: window.length,
|
|
87
|
+
truncated,
|
|
88
|
+
...(truncated ? { nextFrom: end + 1 } : {}),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Open a caller-bound reader over the plugin's shared BM MCP session.
|
|
94
|
+
*
|
|
95
|
+
* The host rechecks the caller's authority around every provider call;
|
|
96
|
+
* context.assertCurrent() is also called before and after each BM request so
|
|
97
|
+
* a revoked caller is refused rather than served. Every note in the project
|
|
98
|
+
* is visible to every caller, which matches what the plugin's own
|
|
99
|
+
* search_notes and read_note tools already expose.
|
|
100
|
+
*/
|
|
101
|
+
export function openBasicMemoryReader(
|
|
102
|
+
client: BmClient,
|
|
103
|
+
context: MemoryCallerContext,
|
|
104
|
+
): MemoryProviderHandle {
|
|
105
|
+
const guarded = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
106
|
+
context.assertCurrent()
|
|
107
|
+
const result = await run()
|
|
108
|
+
context.assertCurrent()
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Trigger: the reader opens while the plugin service is not running, as in
|
|
113
|
+
// one-shot CLI commands like `openclaw memory status`.
|
|
114
|
+
// Why: the first BM call then spawns a `bm mcp` child that nothing else
|
|
115
|
+
// will stop, and its open pipes keep the CLI process from exiting.
|
|
116
|
+
// Outcome: this reader owns that connection and stops it on close().
|
|
117
|
+
const ownsConnection = !client.isServiceRunning()
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
capabilities: {
|
|
121
|
+
sources: ["memory"],
|
|
122
|
+
pagination: false,
|
|
123
|
+
candidates: [],
|
|
124
|
+
projectFilter: false,
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async search(request: MemorySearchRequest): Promise<MemorySearchPage> {
|
|
128
|
+
const results = await guarded(() =>
|
|
129
|
+
client.search(request.query, request.maxResults ?? DEFAULT_MAX_RESULTS),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const hits = results
|
|
133
|
+
.map((result) => ({ result, score: hostScore(result.score) }))
|
|
134
|
+
// Trigger: the caller set a score floor.
|
|
135
|
+
// Why: the host contract requires every provider to enforce minScore.
|
|
136
|
+
// Outcome: unscored or lower-scored hits are dropped.
|
|
137
|
+
.filter(
|
|
138
|
+
({ score }) =>
|
|
139
|
+
request.minScore === undefined ||
|
|
140
|
+
(score !== undefined && score >= request.minScore),
|
|
141
|
+
)
|
|
142
|
+
.map(({ result, score }) => ({
|
|
143
|
+
reference: { providerId: MEMORY_PROVIDER_ID, id: result.permalink },
|
|
144
|
+
excerpt: excerptOf(searchResultText(result)),
|
|
145
|
+
source: result.file_path,
|
|
146
|
+
...(score === undefined ? {} : { score }),
|
|
147
|
+
}))
|
|
148
|
+
|
|
149
|
+
return { hits }
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async get(request: MemoryGetRequest): Promise<MemoryGetResult> {
|
|
153
|
+
try {
|
|
154
|
+
const note = await guarded(() => client.readNote(request.reference.id))
|
|
155
|
+
return {
|
|
156
|
+
status: "ok",
|
|
157
|
+
reference: { providerId: MEMORY_PROVIDER_ID, id: note.permalink },
|
|
158
|
+
...sliceLines(note.content, request.from, request.lines),
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// A missing note is an answer; any other failure must surface so the
|
|
162
|
+
// host does not report it as an empty success.
|
|
163
|
+
if (isNoteNotFoundError(err)) return { status: "not_found" }
|
|
164
|
+
throw err
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async health(): Promise<MemoryHealth> {
|
|
169
|
+
// Listing projects proves the BM MCP session answers without reading
|
|
170
|
+
// any note content, so it is safe for status-only callers.
|
|
171
|
+
try {
|
|
172
|
+
await guarded(() => client.listProjects())
|
|
173
|
+
return { status: "ready" }
|
|
174
|
+
} catch (err) {
|
|
175
|
+
log.warn(
|
|
176
|
+
`memory health check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
177
|
+
)
|
|
178
|
+
return {
|
|
179
|
+
status: "unavailable",
|
|
180
|
+
message: "Basic Memory is not reachable",
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// In the gateway the BM MCP session is shared by the whole plugin and
|
|
186
|
+
// owned by its service, so closing one caller's reader releases nothing.
|
|
187
|
+
async close(): Promise<void> {
|
|
188
|
+
if (ownsConnection) await client.stop()
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- Pre-compaction flush ---
|
|
194
|
+
|
|
195
|
+
const FLUSH_PROMPT = [
|
|
196
|
+
"Pre-compaction memory flush.",
|
|
197
|
+
"Save durable facts, decisions and open tasks from this session to Basic Memory.",
|
|
198
|
+
"Use search_notes or read_note first to find an existing note, then edit_note to update it;",
|
|
199
|
+
"use write_note only for a new topic.",
|
|
200
|
+
`If nothing is worth saving, reply with ${SILENT_REPLY_TOKEN}.`,
|
|
201
|
+
].join(" ")
|
|
202
|
+
|
|
203
|
+
const FLUSH_SYSTEM_PROMPT = [
|
|
204
|
+
"Pre-compaction memory flush turn.",
|
|
205
|
+
"The session is near auto-compaction; capture durable memories in Basic Memory with write_note or edit_note.",
|
|
206
|
+
`You may reply, but usually ${SILENT_REPLY_TOKEN} is correct.`,
|
|
207
|
+
].join(" ")
|
|
208
|
+
|
|
209
|
+
// Returning a tools-arm plan makes the host persist through this plugin's own
|
|
210
|
+
// tools instead of appending to a workspace memory file. The host has already
|
|
211
|
+
// honored agents.defaults.compaction.memoryFlush.enabled before calling this.
|
|
212
|
+
const resolveFlushPlan: MemoryProviderFlushPlanResolver = () => ({
|
|
213
|
+
prompt: FLUSH_PROMPT,
|
|
214
|
+
systemPrompt: FLUSH_SYSTEM_PROMPT,
|
|
215
|
+
persistenceToolNames: FLUSH_PERSISTENCE_TOOL_NAMES,
|
|
216
|
+
lookupToolNames: FLUSH_LOOKUP_TOOL_NAMES,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// --- Prompt guidance ---
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* OpenClaw fills any capability field the slot owner leaves empty from other
|
|
223
|
+
* memory registrations, including memory-core's prompt section, which points
|
|
224
|
+
* agents at its file-only memory_search. Supplying our own section keeps the
|
|
225
|
+
* agent's guidance on the Basic Memory tools.
|
|
226
|
+
*/
|
|
227
|
+
function buildPromptSection(params: { availableTools: Set<string> }): string[] {
|
|
228
|
+
if (!params.availableTools.has("search_notes")) return []
|
|
229
|
+
return [
|
|
230
|
+
"## Basic Memory",
|
|
231
|
+
"Long-term memory lives in Basic Memory, a knowledge graph of Markdown notes.",
|
|
232
|
+
"Before answering questions about prior work, decisions, people or preferences, search it with search_notes and read the best match with read_note.",
|
|
233
|
+
"Earlier conversations are saved automatically in daily conversation notes.",
|
|
234
|
+
"memory_search and memory_get cover only the workspace MEMORY.md and memory/ files, not Basic Memory.",
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- Capability ---
|
|
239
|
+
|
|
240
|
+
export function buildBasicMemoryCapability(
|
|
241
|
+
client: BmClient,
|
|
242
|
+
): MemoryPluginCapability {
|
|
243
|
+
return {
|
|
244
|
+
providerRuntime: {
|
|
245
|
+
async open({ context }) {
|
|
246
|
+
return { provider: openBasicMemoryReader(client, context) }
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
recallToolNames: RECALL_TOOL_NAMES,
|
|
250
|
+
providerFlushPlanResolver: resolveFlushPlan,
|
|
251
|
+
promptBuilder: buildPromptSection,
|
|
252
|
+
}
|
|
253
|
+
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basicmemory/openclaw-basic-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"index.ts",
|
|
16
16
|
"bm-client.ts",
|
|
17
17
|
"context-engine/basic-memory-context-engine.ts",
|
|
18
|
+
"context-engine/types.ts",
|
|
19
|
+
"memory/basic-memory-capability.ts",
|
|
18
20
|
"config.ts",
|
|
19
21
|
"logger.ts",
|
|
20
22
|
"commands/cli.ts",
|
|
@@ -27,7 +29,6 @@
|
|
|
27
29
|
"tools/edit-note.ts",
|
|
28
30
|
"tools/list-memory-projects.ts",
|
|
29
31
|
"tools/list-workspaces.ts",
|
|
30
|
-
"tools/memory-provider.ts",
|
|
31
32
|
"tools/move-note.ts",
|
|
32
33
|
"tools/read-note.ts",
|
|
33
34
|
"tools/schema-diff.ts",
|
|
@@ -80,8 +81,8 @@
|
|
|
80
81
|
"minGatewayVersion": "2026.5.2"
|
|
81
82
|
},
|
|
82
83
|
"build": {
|
|
83
|
-
"openclawVersion": "2026.
|
|
84
|
-
"pluginSdkVersion": "2026.
|
|
84
|
+
"openclawVersion": "2026.10.9",
|
|
85
|
+
"pluginSdkVersion": "2026.10.9"
|
|
85
86
|
},
|
|
86
87
|
"install": {
|
|
87
88
|
"minHostVersion": ">=2026.5.2"
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
"devDependencies": {
|
|
91
92
|
"@biomejs/biome": "^2.4.6",
|
|
92
93
|
"@types/node": "^20.0.0",
|
|
93
|
-
"openclaw": "^2026.
|
|
94
|
+
"openclaw": "^2026.10.6",
|
|
94
95
|
"typescript": "^5.9.3"
|
|
95
96
|
}
|
|
96
97
|
}
|
package/skills/manifest.json
CHANGED
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"name": "memory-defrag",
|
|
30
30
|
"description": "Defragment a Basic Memory project: split bloated notes, consolidate scattered facts, resolve contradictions, retire stale content. Plans first; deletes only when told. Run weekly or on demand."
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"dir": "memory-doctor",
|
|
34
|
+
"name": "memory-doctor",
|
|
35
|
+
"description": "Check local Basic Memory projects with bm doctor, explain the findings, and run the suggested fixes once the user agrees. Use when search misses or repeats notes, or a save looks lost."
|
|
36
|
+
},
|
|
32
37
|
{
|
|
33
38
|
"dir": "memory-ingest",
|
|
34
39
|
"name": "memory-ingest",
|
|
@@ -11,7 +11,19 @@ short-lived agent in Cloud that reads the target, works through the Basic Memory
|
|
|
11
11
|
tools, and changes notes as its instructions say.
|
|
12
12
|
|
|
13
13
|
Agents run in Basic Memory Cloud only, on plans that include them. A local
|
|
14
|
-
Basic Memory has no agent tools.
|
|
14
|
+
Basic Memory has no agent tools. Confirm the connected server exposes `bm_agent`
|
|
15
|
+
before starting this workflow.
|
|
16
|
+
|
|
17
|
+
## Project scope
|
|
18
|
+
|
|
19
|
+
If the project is unknown, discover it with `bm_project(command="list")`.
|
|
20
|
+
Pass a nonempty `project` on every note, folder, context and schema call, even
|
|
21
|
+
with one project or a qualified `memory://` URL. Use the chosen project's name,
|
|
22
|
+
`workspace/project`, or external ID; `<project>` below is a placeholder for it.
|
|
23
|
+
Recent activity also requires `project`. Search accepts `project` for one
|
|
24
|
+
project, `projects` for selected projects, or `all_projects=True` explicitly.
|
|
25
|
+
Discovery and bundled manuals need no project. Keep the same scope when following
|
|
26
|
+
returned paths, paginating, or recovering from a refusal.
|
|
15
27
|
|
|
16
28
|
## When to Use
|
|
17
29
|
|
|
@@ -24,12 +36,13 @@ Basic Memory has no agent tools.
|
|
|
24
36
|
|
|
25
37
|
| Tool | Does |
|
|
26
38
|
|---|---|
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
39
|
+
| `bm_agent(command="list", project="<project>")` | The project's agents and how their recent runs went |
|
|
40
|
+
| `bm_agent(command="run", project="<project>")` | Starts an agent on a note; returns the run id |
|
|
41
|
+
| `bm_agent(command="get", project="<project>")` | One run: status, notes changed, and the agent's last reply |
|
|
42
|
+
| `bm_agent(command="runs", project="<project>")` | Recent runs, newest first, optionally for one agent |
|
|
43
|
+
| `bm_agent(command="mark", project="<project>")` | Mark a run `kept` or `dismissed` with `run_id` and `decision` |
|
|
31
44
|
|
|
32
|
-
Create and edit agents with `
|
|
45
|
+
Create and edit agents with `bm_write` and `bm_edit`. There is no separate
|
|
33
46
|
agent tool for that.
|
|
34
47
|
|
|
35
48
|
## Write an Agent
|
|
@@ -112,10 +125,10 @@ Point the user to these before writing an agent from scratch.
|
|
|
112
125
|
## Run an Agent
|
|
113
126
|
|
|
114
127
|
```python
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
bm_agent(command="list", project="work")
|
|
129
|
+
bm_agent(command="run", agent="Curator", identifier="projects/Launch", project="work")
|
|
117
130
|
# -> run id. Runs usually take one to a few minutes.
|
|
118
|
-
|
|
131
|
+
bm_agent(command="get", run_id="<run id>", project="work")
|
|
119
132
|
```
|
|
120
133
|
|
|
121
134
|
An agent changes the user's notes. Ask before running one, unless the user has
|
|
@@ -128,7 +141,7 @@ on the version the person ended with. Notes written by agents, imports, or
|
|
|
128
141
|
|
|
129
142
|
## Check a Run
|
|
130
143
|
|
|
131
|
-
`
|
|
144
|
+
`bm_agent(command="get", project="<project>")` shows the status, the notes the run created, edited, moved, or
|
|
132
145
|
deleted, and the agent's last reply. Every reply is also kept in a run log note, `agents/runs/<agent> run <id>`. Read it for the whole
|
|
133
146
|
conversation; do not edit it.
|
|
134
147
|
|