@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.
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: memory-tasks
3
+ description: "Task management via Basic Memory schemas: create, track, and resume structured tasks that survive context compaction. Uses BM's schema system for uniform notes queryable through the knowledge graph."
4
+ ---
5
+
6
+ # Memory Tasks
7
+
8
+ Manage work-in-progress using Basic Memory's schema system. Tasks are just notes with `type: Task` — they live in the knowledge graph, validate against a schema, and survive context compaction.
9
+
10
+ ## When to Use
11
+
12
+ - **Starting multi-step work** (3+ steps, or anything that might outlast the context window)
13
+ - **After compaction/restart** — search for active tasks to resume
14
+ - **Pre-compaction flush** — update all active tasks with current state
15
+ - **On demand** — user asks to create, check, or manage tasks
16
+
17
+ ## Task Schema
18
+
19
+ Tasks use the BM schema system (SPEC-SCHEMA). The schema note lives at `memory/schema/Task.md`:
20
+
21
+ ```yaml
22
+ ---
23
+ title: Task
24
+ type: schema
25
+ entity: Task
26
+ version: 1
27
+ schema:
28
+ description: string, what needs to be done
29
+ status?(enum): [active, blocked, done, abandoned], current state
30
+ assigned_to?: string, who is working on this
31
+ steps?(array): string, ordered steps to complete
32
+ current_step?: integer, which step number we're on (1-indexed)
33
+ context?: string, key context needed to resume after memory loss
34
+ started?: string, when work began
35
+ completed?: string, when work finished
36
+ blockers?(array): string, what's preventing progress
37
+ parent_task?: Task, parent task if this is a subtask
38
+ settings:
39
+ validation: warn
40
+ ---
41
+ ```
42
+
43
+ ## Creating a Task
44
+
45
+ When work qualifies, create a note in `memory/tasks/YYYY-MM-DD-short-name.md`:
46
+
47
+ ```markdown
48
+ ---
49
+ title: Descriptive task name
50
+ type: Task
51
+ status: active
52
+ created: YYYY-MM-DD
53
+ current_step: 1
54
+ ---
55
+
56
+ # Descriptive task name
57
+
58
+ ## Observations
59
+ - [description] What needs to be done, concisely
60
+ - [status] active
61
+ - [assigned_to] claw
62
+ - [started] YYYY-MM-DD
63
+ - [current_step] 1
64
+
65
+ ## Steps
66
+ 1. [ ] First concrete step
67
+ 2. [ ] Second concrete step
68
+ 3. [ ] Third concrete step
69
+
70
+ ## Context
71
+ What future-you needs to pick up this work. Include:
72
+ - Key file paths and repos involved
73
+ - Decisions already made and why
74
+ - What was tried and what worked/didn't
75
+ - Where to look for related context
76
+ ```
77
+
78
+ ### Key Principles
79
+
80
+ - **Steps are concrete and checkable** — "Implement X in file Y", not "figure out stuff"
81
+ - **Context is for post-amnesia resumption** — Write it as if explaining to a smart person who knows nothing about what you've been doing
82
+ - **Use observations for BM-queryable fields** — `[status]`, `[description]`, `[assigned_to]` etc. become searchable in the knowledge graph
83
+ - **Relations link to other entities** — `parent_task [[Other Task]]`, `related_to [[Some Note]]`
84
+
85
+ ## Resuming After Compaction
86
+
87
+ On session start or after compaction:
88
+
89
+ 1. **Search for active tasks:**
90
+ - Via BM: `search_notes("type:Task status:active")` or `search_notes("[status] active")`
91
+ - Via memory_search: query "active tasks" (composited search includes task scanning)
92
+
93
+ 2. **Read the task note** to get full context
94
+
95
+ 3. **Resume from `current_step`** using the `context` field
96
+
97
+ 4. **Update as you progress** — increment `current_step`, update context, check off steps
98
+
99
+ ## Updating Tasks
100
+
101
+ As work progresses, update the task note:
102
+
103
+ ```markdown
104
+ ## Steps
105
+ 1. [x] First step — done, resulted in X
106
+ 2. [x] Second step — done, changed approach because Y
107
+ 3. [ ] Third step — next up
108
+
109
+ ## Context
110
+ Updated context reflecting current state...
111
+ ```
112
+
113
+ Update frontmatter too:
114
+ ```yaml
115
+ current_step: 3
116
+ ```
117
+
118
+ ## Completing Tasks
119
+
120
+ When done:
121
+ ```yaml
122
+ status: done
123
+ completed: YYYY-MM-DD
124
+ ```
125
+
126
+ Add a brief summary of what was accomplished and any follow-up needed.
127
+
128
+ ## Pre-Compaction Flush
129
+
130
+ When a compaction event is imminent:
131
+
132
+ 1. Find all active tasks: `search_notes("type:Task status:active")`
133
+ 2. For each, update:
134
+ - `current_step` to reflect actual progress
135
+ - `context` with everything needed to resume
136
+ - Step checkboxes to show what's done
137
+ 3. This is **critical** — context not written down is context lost
138
+
139
+ ## Querying Tasks
140
+
141
+ With BM's schema system, tasks are fully queryable:
142
+
143
+ | Query | What it finds |
144
+ |-------|--------------|
145
+ | `search_notes("type:Task")` | All tasks |
146
+ | `search_notes("[status] active")` | Active tasks |
147
+ | `search_notes("[status] blocked")` | Blocked tasks |
148
+ | `search_notes("[assigned_to] claw")` | My tasks |
149
+ | `search_notes("type:Task [blockers]")` | Tasks with blockers |
150
+ | `schema_validate(noteType="Task")` | Validate all tasks against schema |
151
+ | `schema_diff(noteType="Task")` | Detect drift between schema and actual task notes |
152
+
153
+ > **Plugin tools vs BM CLI**: The `schema_validate`, `schema_infer`, and `schema_diff` tools are available when using the `@openclaw/basic-memory` plugin. The raw `search_notes(...)` queries also work and are useful for ad-hoc filtering. `memory_search` composites results from all sources including active task scanning.
154
+
155
+ ## Guidelines
156
+
157
+ - **One task per unit of work** — Don't cram multiple projects into one task
158
+ - **Externalize early** — If you think "I should remember this", write it down NOW
159
+ - **Context > steps** — Steps tell you what to do; context tells you why and how
160
+ - **Close finished tasks** — Don't leave completed work as `active`
161
+ - **Link related tasks** — Use `parent_task [[X]]` or relations to connect related work
162
+ - **Schema validation is your friend** — Run `bm schema validate Task` periodically to catch incomplete tasks
@@ -0,0 +1,123 @@
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 registerContextTool(
7
+ api: OpenClawPluginApi,
8
+ client: BmClient,
9
+ ): void {
10
+ api.registerTool(
11
+ {
12
+ name: "build_context",
13
+ label: "Build Context",
14
+ description:
15
+ "Navigate the Basic Memory knowledge graph via memory:// URLs. " +
16
+ "Returns the target note plus related notes connected through the graph. " +
17
+ "Use this to follow relations and discover connected concepts.",
18
+ parameters: Type.Object({
19
+ url: Type.String({
20
+ description:
21
+ 'Memory URL to navigate, e.g. "memory://agents/decisions" or "projects/my-project"',
22
+ }),
23
+ depth: Type.Optional(
24
+ Type.Number({
25
+ description: "How many relation hops to follow (default: 1)",
26
+ }),
27
+ ),
28
+ project: Type.Optional(
29
+ Type.String({
30
+ description: "Target project name (defaults to current project)",
31
+ }),
32
+ ),
33
+ }),
34
+ async execute(
35
+ _toolCallId: string,
36
+ params: { url: string; depth?: number; project?: string },
37
+ ) {
38
+ const depth = params.depth ?? 1
39
+ log.debug(`build_context: url="${params.url}" depth=${depth}`)
40
+
41
+ try {
42
+ const ctx = await client.buildContext(
43
+ params.url,
44
+ depth,
45
+ params.project,
46
+ )
47
+
48
+ if (!ctx.results || ctx.results.length === 0) {
49
+ return {
50
+ content: [
51
+ {
52
+ type: "text" as const,
53
+ text: `No context found for "${params.url}".`,
54
+ },
55
+ ],
56
+ details: {
57
+ url: params.url,
58
+ depth,
59
+ resultCount: 0,
60
+ },
61
+ }
62
+ }
63
+
64
+ const sections: string[] = []
65
+
66
+ for (const result of ctx.results) {
67
+ const primary = result.primary_result
68
+ sections.push(`## ${primary.title}\n${primary.content}`)
69
+
70
+ if (result.observations?.length > 0) {
71
+ const obs = result.observations
72
+ .map((o) => `- [${o.category}] ${o.content}`)
73
+ .join("\n")
74
+ sections.push(`### Observations\n${obs}`)
75
+ }
76
+
77
+ if (result.related_results?.length > 0) {
78
+ const rels = result.related_results
79
+ .map((r) => {
80
+ if (r.type === "relation") {
81
+ return `- ${r.relation_type} → **${r.to_entity}**`
82
+ }
83
+ const label = r.relation_type
84
+ ? `${r.relation_type} → **${r.title}**`
85
+ : `**${r.title}**`
86
+ return r.permalink
87
+ ? `- ${label} (${r.permalink})`
88
+ : `- ${label}`
89
+ })
90
+ .join("\n")
91
+ sections.push(`### Related\n${rels}`)
92
+ }
93
+ }
94
+
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text" as const,
99
+ text: sections.join("\n\n"),
100
+ },
101
+ ],
102
+ details: {
103
+ url: params.url,
104
+ depth,
105
+ resultCount: ctx.results.length,
106
+ },
107
+ }
108
+ } catch (err) {
109
+ log.error("build_context failed", err)
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text" as const,
114
+ text: `Failed to build context for "${params.url}". Check logs for details.`,
115
+ },
116
+ ],
117
+ }
118
+ }
119
+ },
120
+ },
121
+ { name: "build_context" },
122
+ )
123
+ }
@@ -0,0 +1,67 @@
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 registerDeleteTool(
7
+ api: OpenClawPluginApi,
8
+ client: BmClient,
9
+ ): void {
10
+ api.registerTool(
11
+ {
12
+ name: "delete_note",
13
+ label: "Delete Note",
14
+ description:
15
+ "Delete a note from the Basic Memory knowledge graph. " +
16
+ "The note is permanently removed from the filesystem and the search index.",
17
+ parameters: Type.Object({
18
+ identifier: Type.String({
19
+ description: "Note title, permalink, or memory:// URL to delete",
20
+ }),
21
+ project: Type.Optional(
22
+ Type.String({
23
+ description: "Target project name (defaults to current project)",
24
+ }),
25
+ ),
26
+ }),
27
+ async execute(
28
+ _toolCallId: string,
29
+ params: { identifier: string; project?: string },
30
+ ) {
31
+ log.debug(`delete_note: identifier="${params.identifier}"`)
32
+
33
+ try {
34
+ const result = await client.deleteNote(
35
+ params.identifier,
36
+ params.project,
37
+ )
38
+
39
+ return {
40
+ content: [
41
+ {
42
+ type: "text" as const,
43
+ text: `Deleted: ${result.title} (${result.permalink})`,
44
+ },
45
+ ],
46
+ details: {
47
+ title: result.title,
48
+ permalink: result.permalink,
49
+ file_path: result.file_path,
50
+ },
51
+ }
52
+ } catch (err) {
53
+ log.error("delete_note failed", err)
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text" as const,
58
+ text: `Failed to delete "${params.identifier}". It may not exist.`,
59
+ },
60
+ ],
61
+ }
62
+ }
63
+ },
64
+ },
65
+ { name: "delete_note" },
66
+ )
67
+ }
@@ -0,0 +1,118 @@
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 registerEditTool(
7
+ api: OpenClawPluginApi,
8
+ client: BmClient,
9
+ ): void {
10
+ api.registerTool(
11
+ {
12
+ name: "edit_note",
13
+ label: "Edit Note",
14
+ description:
15
+ "Incrementally edit an existing note in the Basic Memory knowledge graph. " +
16
+ "Supports append, prepend, find/replace, and section replacement " +
17
+ "without rewriting the entire note.",
18
+ parameters: Type.Object({
19
+ identifier: Type.String({
20
+ description: "Note title, permalink, or memory:// URL to edit",
21
+ }),
22
+ operation: Type.Union(
23
+ [
24
+ Type.Literal("append"),
25
+ Type.Literal("prepend"),
26
+ Type.Literal("find_replace"),
27
+ Type.Literal("replace_section"),
28
+ ],
29
+ {
30
+ description:
31
+ "Edit operation: append (add to end), prepend (add to start), " +
32
+ "find_replace (replace matching text), replace_section (replace a heading section)",
33
+ },
34
+ ),
35
+ content: Type.String({
36
+ description: "New content to add or replace with",
37
+ }),
38
+ find_text: Type.Optional(
39
+ Type.String({
40
+ description: "Text to find (required for find_replace)",
41
+ }),
42
+ ),
43
+ section: Type.Optional(
44
+ Type.String({
45
+ description:
46
+ "Section heading to replace (required for replace_section)",
47
+ }),
48
+ ),
49
+ expected_replacements: Type.Optional(
50
+ Type.Number({
51
+ description:
52
+ "Expected replacement count for find_replace (default: 1)",
53
+ }),
54
+ ),
55
+ project: Type.Optional(
56
+ Type.String({
57
+ description: "Target project name (defaults to current project)",
58
+ }),
59
+ ),
60
+ }),
61
+ async execute(
62
+ _toolCallId: string,
63
+ params: {
64
+ identifier: string
65
+ operation: "append" | "prepend" | "find_replace" | "replace_section"
66
+ content: string
67
+ find_text?: string
68
+ section?: string
69
+ expected_replacements?: number
70
+ project?: string
71
+ },
72
+ ) {
73
+ log.debug(`edit_note: id="${params.identifier}" op=${params.operation}`)
74
+
75
+ try {
76
+ const note = await client.editNote(
77
+ params.identifier,
78
+ params.operation,
79
+ params.content,
80
+ {
81
+ find_text: params.find_text,
82
+ section: params.section,
83
+ expected_replacements: params.expected_replacements,
84
+ },
85
+ params.project,
86
+ )
87
+
88
+ return {
89
+ content: [
90
+ {
91
+ type: "text" as const,
92
+ text: `Note updated: ${note.title} (${note.permalink})`,
93
+ },
94
+ ],
95
+ details: {
96
+ title: note.title,
97
+ permalink: note.permalink,
98
+ file_path: note.file_path,
99
+ operation: params.operation,
100
+ checksum: note.checksum ?? null,
101
+ },
102
+ }
103
+ } catch (err) {
104
+ log.error("edit_note failed", err)
105
+ return {
106
+ content: [
107
+ {
108
+ type: "text" as const,
109
+ text: `Failed to edit note "${params.identifier}". It may not exist.`,
110
+ },
111
+ ],
112
+ }
113
+ }
114
+ },
115
+ },
116
+ { name: "edit_note" },
117
+ )
118
+ }
@@ -0,0 +1,94 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { BmClient, ProjectListResult } from "../bm-client.ts"
4
+ import { log } from "../logger.ts"
5
+
6
+ function normalizeProject(project: ProjectListResult) {
7
+ return {
8
+ name: project.name,
9
+ path: project.path,
10
+ display_name: project.display_name ?? null,
11
+ is_private: project.is_private === true,
12
+ is_default: project.is_default === true || project.isDefault === true,
13
+ workspace_name: project.workspace_name ?? null,
14
+ workspace_type: project.workspace_type ?? null,
15
+ workspace_tenant_id: project.workspace_tenant_id ?? null,
16
+ }
17
+ }
18
+
19
+ export function registerProjectListTool(
20
+ api: OpenClawPluginApi,
21
+ client: BmClient,
22
+ ): void {
23
+ api.registerTool(
24
+ {
25
+ name: "list_memory_projects",
26
+ label: "List Projects",
27
+ description: "List all Basic Memory projects accessible to this agent",
28
+ parameters: Type.Object({
29
+ workspace: Type.Optional(
30
+ Type.String({
31
+ description: "Filter by workspace name or tenant_id",
32
+ }),
33
+ ),
34
+ }),
35
+ async execute(_toolCallId: string, params: { workspace?: string }) {
36
+ log.debug(`list_memory_projects: workspace="${params.workspace ?? ""}"`)
37
+
38
+ try {
39
+ const projects = await client.listProjects(params.workspace)
40
+ const normalized = projects.map(normalizeProject)
41
+
42
+ if (normalized.length === 0) {
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text" as const,
47
+ text: "No Basic Memory projects found.",
48
+ },
49
+ ],
50
+ details: {
51
+ count: 0,
52
+ projects: [],
53
+ },
54
+ }
55
+ }
56
+
57
+ const text = normalized
58
+ .map((project, idx) => {
59
+ const defaultSuffix = project.is_default ? " (default)" : ""
60
+ const displayLine = project.display_name
61
+ ? `\n Display Name: ${project.display_name}`
62
+ : ""
63
+ return `${idx + 1}. **${project.name}**${defaultSuffix}\n Path: ${project.path}\n Private: ${project.is_private}${displayLine}`
64
+ })
65
+ .join("\n\n")
66
+
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text" as const,
71
+ text: `Found ${normalized.length} project(s):\n\n${text}`,
72
+ },
73
+ ],
74
+ details: {
75
+ count: normalized.length,
76
+ projects: normalized,
77
+ },
78
+ }
79
+ } catch (err) {
80
+ log.error("list_memory_projects failed", err)
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text" as const,
85
+ text: "Failed to list Basic Memory projects. Is Basic Memory running? Check logs for details.",
86
+ },
87
+ ],
88
+ }
89
+ }
90
+ },
91
+ },
92
+ { name: "list_memory_projects" },
93
+ )
94
+ }
@@ -0,0 +1,75 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { BmClient, WorkspaceResult } from "../bm-client.ts"
4
+ import { log } from "../logger.ts"
5
+
6
+ function formatWorkspace(ws: WorkspaceResult, idx: number): string {
7
+ const subscription = ws.has_active_subscription ? "active" : "none"
8
+ return (
9
+ `${idx + 1}. **${ws.name}**\n` +
10
+ ` Type: ${ws.workspace_type} | Role: ${ws.role} | Subscription: ${subscription}`
11
+ )
12
+ }
13
+
14
+ export function registerWorkspaceListTool(
15
+ api: OpenClawPluginApi,
16
+ client: BmClient,
17
+ ): void {
18
+ api.registerTool(
19
+ {
20
+ name: "list_workspaces",
21
+ label: "List Workspaces",
22
+ description:
23
+ "List all Basic Memory workspaces (personal and organization) accessible to this user",
24
+ parameters: Type.Object({}),
25
+ async execute(_toolCallId: string, _params: Record<string, never>) {
26
+ log.debug("list_workspaces")
27
+
28
+ try {
29
+ const workspaces = await client.listWorkspaces()
30
+
31
+ if (workspaces.length === 0) {
32
+ return {
33
+ content: [
34
+ {
35
+ type: "text" as const,
36
+ text: "No workspaces found.",
37
+ },
38
+ ],
39
+ details: {
40
+ count: 0,
41
+ workspaces: [],
42
+ },
43
+ }
44
+ }
45
+
46
+ const text = workspaces.map(formatWorkspace).join("\n\n")
47
+
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text" as const,
52
+ text: `Found ${workspaces.length} workspace(s):\n\n${text}`,
53
+ },
54
+ ],
55
+ details: {
56
+ count: workspaces.length,
57
+ workspaces,
58
+ },
59
+ }
60
+ } catch (err) {
61
+ log.error("list_workspaces failed", err)
62
+ return {
63
+ content: [
64
+ {
65
+ type: "text" as const,
66
+ text: "Failed to list workspaces. Is Basic Memory running? Check logs for details.",
67
+ },
68
+ ],
69
+ }
70
+ }
71
+ },
72
+ },
73
+ { name: "list_workspaces" },
74
+ )
75
+ }