@basicmemory/openclaw-basic-memory 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +576 -0
- package/bm-client.ts +879 -0
- package/commands/cli.ts +176 -0
- package/commands/skills.ts +52 -0
- package/commands/slash.ts +73 -0
- package/config.ts +152 -0
- package/hooks/capture.ts +95 -0
- package/hooks/recall.ts +66 -0
- package/index.ts +120 -0
- package/logger.ts +47 -0
- package/openclaw.plugin.json +83 -0
- package/package.json +68 -0
- package/schema/task-schema.ts +34 -0
- package/scripts/setup-bm.sh +32 -0
- package/skills/memory-defrag/SKILL.md +87 -0
- package/skills/memory-metadata-search/SKILL.md +208 -0
- package/skills/memory-notes/SKILL.md +250 -0
- package/skills/memory-reflect/SKILL.md +63 -0
- package/skills/memory-schema/SKILL.md +237 -0
- package/skills/memory-tasks/SKILL.md +162 -0
- package/tools/build-context.ts +123 -0
- package/tools/delete-note.ts +67 -0
- package/tools/edit-note.ts +118 -0
- package/tools/list-memory-projects.ts +94 -0
- package/tools/list-workspaces.ts +75 -0
- package/tools/memory-provider.ts +327 -0
- package/tools/move-note.ts +74 -0
- package/tools/read-note.ts +79 -0
- package/tools/schema-diff.ts +104 -0
- package/tools/schema-infer.ts +103 -0
- package/tools/schema-validate.ts +100 -0
- package/tools/search-notes.ts +130 -0
- package/tools/write-note.ts +78 -0
- package/types/openclaw.d.ts +24 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { BmClient } from "../bm-client.ts"
|
|
4
|
+
import { log } from "../logger.ts"
|
|
5
|
+
|
|
6
|
+
export function registerSchemaValidateTool(
|
|
7
|
+
api: OpenClawPluginApi,
|
|
8
|
+
client: BmClient,
|
|
9
|
+
): void {
|
|
10
|
+
api.registerTool(
|
|
11
|
+
{
|
|
12
|
+
name: "schema_validate",
|
|
13
|
+
label: "Schema Validate",
|
|
14
|
+
description:
|
|
15
|
+
"Validate notes against their Picoschema definitions. " +
|
|
16
|
+
"Validates a specific note by identifier, or all notes of a given type.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
noteType: Type.Optional(
|
|
19
|
+
Type.String({
|
|
20
|
+
description:
|
|
21
|
+
'Note type to batch-validate (e.g., "person", "meeting")',
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
identifier: Type.Optional(
|
|
25
|
+
Type.String({
|
|
26
|
+
description:
|
|
27
|
+
"Specific note to validate (permalink, title, or path)",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
project: Type.Optional(
|
|
31
|
+
Type.String({
|
|
32
|
+
description: "Target project name (defaults to current project)",
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
}),
|
|
36
|
+
async execute(
|
|
37
|
+
_toolCallId: string,
|
|
38
|
+
params: { noteType?: string; identifier?: string; project?: string },
|
|
39
|
+
) {
|
|
40
|
+
log.debug(
|
|
41
|
+
`schema_validate: noteType="${params.noteType ?? ""}" identifier="${params.identifier ?? ""}"`,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await client.schemaValidate(
|
|
46
|
+
params.noteType,
|
|
47
|
+
params.identifier,
|
|
48
|
+
params.project,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Handle error responses from BM (e.g., no schema found)
|
|
52
|
+
const resultRecord = result as unknown as Record<string, unknown>
|
|
53
|
+
if ("error" in result && typeof resultRecord.error === "string") {
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text" as const, text: resultRecord.error }],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const lines: string[] = []
|
|
60
|
+
if (result.entity_type) {
|
|
61
|
+
lines.push(`**Type:** ${result.entity_type}`)
|
|
62
|
+
}
|
|
63
|
+
lines.push(
|
|
64
|
+
`**Notes:** ${result.total_notes ?? 0} | **Valid:** ${result.valid_count ?? 0} | **Warnings:** ${result.warning_count ?? 0} | **Errors:** ${result.error_count ?? 0}`,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if (result.results && result.results.length > 0) {
|
|
68
|
+
lines.push("")
|
|
69
|
+
for (const r of result.results) {
|
|
70
|
+
const status = r.valid ? "valid" : "invalid"
|
|
71
|
+
lines.push(`- **${r.identifier}** — ${status}`)
|
|
72
|
+
for (const w of r.warnings) {
|
|
73
|
+
lines.push(` - warning: ${w}`)
|
|
74
|
+
}
|
|
75
|
+
for (const e of r.errors) {
|
|
76
|
+
lines.push(` - error: ${e}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
83
|
+
details: result,
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
log.error("schema_validate failed", err)
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text" as const,
|
|
91
|
+
text: "Schema validation failed. Check logs for details.",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{ name: "schema_validate" },
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { BmClient } from "../bm-client.ts"
|
|
4
|
+
import { log } from "../logger.ts"
|
|
5
|
+
|
|
6
|
+
export function registerSearchTool(
|
|
7
|
+
api: OpenClawPluginApi,
|
|
8
|
+
client: BmClient,
|
|
9
|
+
): void {
|
|
10
|
+
api.registerTool(
|
|
11
|
+
{
|
|
12
|
+
name: "search_notes",
|
|
13
|
+
label: "Knowledge Search",
|
|
14
|
+
description:
|
|
15
|
+
"Search the Basic Memory knowledge graph for relevant notes, concepts, and connections. " +
|
|
16
|
+
"Returns matching notes with titles, content previews, and relevance scores. " +
|
|
17
|
+
"Optionally filter by frontmatter metadata fields, tags, or status.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
query: Type.String({ description: "Search query" }),
|
|
20
|
+
limit: Type.Optional(
|
|
21
|
+
Type.Number({ description: "Max results (default: 10)" }),
|
|
22
|
+
),
|
|
23
|
+
project: Type.Optional(
|
|
24
|
+
Type.String({
|
|
25
|
+
description: "Target project name (defaults to current project)",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
metadata_filters: Type.Optional(
|
|
29
|
+
Type.Record(Type.String(), Type.Unknown(), {
|
|
30
|
+
description:
|
|
31
|
+
"Filter by frontmatter fields. Supports equality, $in, $gt/$gte/$lt/$lte, $between, and array-contains operators.",
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
tags: Type.Optional(
|
|
35
|
+
Type.Array(Type.String(), {
|
|
36
|
+
description: "Filter by frontmatter tags (all must match)",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
status: Type.Optional(
|
|
40
|
+
Type.String({
|
|
41
|
+
description: "Filter by frontmatter status field",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
45
|
+
async execute(
|
|
46
|
+
_toolCallId: string,
|
|
47
|
+
params: {
|
|
48
|
+
query: string
|
|
49
|
+
limit?: number
|
|
50
|
+
project?: string
|
|
51
|
+
metadata_filters?: Record<string, unknown>
|
|
52
|
+
tags?: string[]
|
|
53
|
+
status?: string
|
|
54
|
+
},
|
|
55
|
+
) {
|
|
56
|
+
const limit = params.limit ?? 10
|
|
57
|
+
log.debug(
|
|
58
|
+
`search_notes: query="${params.query}" limit=${limit} project="${params.project ?? "default"}"`,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const metadata =
|
|
62
|
+
params.metadata_filters || params.tags || params.status
|
|
63
|
+
? {
|
|
64
|
+
filters: params.metadata_filters,
|
|
65
|
+
tags: params.tags,
|
|
66
|
+
status: params.status,
|
|
67
|
+
}
|
|
68
|
+
: undefined
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const results = await client.search(
|
|
72
|
+
params.query,
|
|
73
|
+
limit,
|
|
74
|
+
params.project,
|
|
75
|
+
metadata,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if (results.length === 0) {
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text" as const,
|
|
83
|
+
text: "No matching notes found in the knowledge graph.",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const text = results
|
|
90
|
+
.map((r, i) => {
|
|
91
|
+
const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : ""
|
|
92
|
+
const content = r.content ?? ""
|
|
93
|
+
const preview =
|
|
94
|
+
content.length > 200 ? `${content.slice(0, 200)}...` : content
|
|
95
|
+
return `${i + 1}. **${r.title}**${score}\n ${preview}`
|
|
96
|
+
})
|
|
97
|
+
.join("\n\n")
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text" as const,
|
|
103
|
+
text: `Found ${results.length} notes:\n\n${text}`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
details: {
|
|
107
|
+
count: results.length,
|
|
108
|
+
results: results.map((r) => ({
|
|
109
|
+
title: r.title,
|
|
110
|
+
permalink: r.permalink,
|
|
111
|
+
score: r.score,
|
|
112
|
+
})),
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
log.error("search_notes failed", err)
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text" as const,
|
|
121
|
+
text: "Search failed. Is Basic Memory running? Check logs for details.",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{ name: "search_notes" },
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { BmClient } from "../bm-client.ts"
|
|
4
|
+
import { log } from "../logger.ts"
|
|
5
|
+
|
|
6
|
+
export function registerWriteTool(
|
|
7
|
+
api: OpenClawPluginApi,
|
|
8
|
+
client: BmClient,
|
|
9
|
+
): void {
|
|
10
|
+
api.registerTool(
|
|
11
|
+
{
|
|
12
|
+
name: "write_note",
|
|
13
|
+
label: "Write Note",
|
|
14
|
+
description:
|
|
15
|
+
"Create or update a note in the Basic Memory knowledge graph. " +
|
|
16
|
+
"Notes are stored as Markdown files with semantic structure " +
|
|
17
|
+
"(observations, relations) that build a connected knowledge graph.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
title: Type.String({ description: "Note title" }),
|
|
20
|
+
content: Type.String({
|
|
21
|
+
description: "Note content in Markdown format",
|
|
22
|
+
}),
|
|
23
|
+
folder: Type.String({ description: "Folder to write the note in" }),
|
|
24
|
+
project: Type.Optional(
|
|
25
|
+
Type.String({
|
|
26
|
+
description: "Target project name (defaults to current project)",
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
async execute(
|
|
31
|
+
_toolCallId: string,
|
|
32
|
+
params: {
|
|
33
|
+
title: string
|
|
34
|
+
content: string
|
|
35
|
+
folder: string
|
|
36
|
+
project?: string
|
|
37
|
+
},
|
|
38
|
+
) {
|
|
39
|
+
log.debug(`write_note: title=${params.title} folder=${params.folder}`)
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const note = await client.writeNote(
|
|
43
|
+
params.title,
|
|
44
|
+
params.content,
|
|
45
|
+
params.folder,
|
|
46
|
+
params.project,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const msg = `Note saved: ${note.title} (${note.permalink})`
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text" as const,
|
|
54
|
+
text: msg,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
details: {
|
|
58
|
+
title: note.title,
|
|
59
|
+
permalink: note.permalink,
|
|
60
|
+
file_path: note.file_path,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
log.error("write_note failed", err)
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text" as const,
|
|
69
|
+
text: "Failed to write note. Is Basic Memory running? Check logs for details.",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{ name: "write_note" },
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare module "openclaw/plugin-sdk" {
|
|
2
|
+
export interface OpenClawPluginApi {
|
|
3
|
+
pluginConfig: unknown
|
|
4
|
+
logger: {
|
|
5
|
+
info: (msg: string) => void
|
|
6
|
+
warn: (msg: string) => void
|
|
7
|
+
error: (msg: string, ...args: unknown[]) => void
|
|
8
|
+
debug: (msg: string) => void
|
|
9
|
+
}
|
|
10
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
11
|
+
registerTool(tool: any, options: any): void
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
13
|
+
registerCommand(command: any): void
|
|
14
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
15
|
+
registerCli(handler: any, options?: any): void
|
|
16
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
17
|
+
registerService(service: any): void
|
|
18
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
19
|
+
on(event: string, handler: (...args: any[]) => any): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
23
|
+
export function stringEnum(values: readonly string[]): any
|
|
24
|
+
}
|