@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,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
|
+
}
|