Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion integrations/openclaw/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,12 @@ read_note(identifier="decisions/auth-strategy", project="backend")
### `write_note`
**Purpose**: Create new notes in the knowledge graph
**When to use**: When users share important information, make decisions, or want to save insights for later
**Best practices**: Use clear titles, organize in appropriate folders, structure with headings
**Best practices**: Use clear titles, organize in appropriate folders, structure the Markdown body with headings, and pass frontmatter through the dedicated parameters instead of embedding YAML in `content`

**Frontmatter parameters**:
- `tags`: A string array or comma-separated string
- `note_type`: The note type stored as `type`; defaults to `note`
- `metadata`: Additional frontmatter fields, including nested objects

**Examples**:
```
Expand Down Expand Up @@ -231,6 +236,12 @@ We chose JWT tokens with refresh token rotation.
write_note(
title="Client Meeting - February 8, 2024",
folder="meetings",
tags=["client", "planning"],
note_type="Meeting",
metadata={
"status": "complete",
"attendees": ["John", "Sarah"],
},
content="""
# Client Meeting - February 8, 2024

Expand All @@ -256,6 +267,7 @@ write_note(
**Purpose**: Modify existing notes incrementally
**When to use**: To add updates, fix information, or organize existing content
**Operations**: append, prepend, find_replace, replace_section
**Frontmatter updates**: Pass `metadata` to merge custom frontmatter fields in the same call. Provided keys replace existing values, unrelated fields remain unchanged, and nested objects are supported.

**Examples**:
```
Expand All @@ -276,6 +288,7 @@ edit_note(
identifier="weekly-review",
operation="replace_section",
section="## This Week",
metadata={"status": "review", "progress": {"completed": 4, "total": 6}},
content="""## This Week
- Completed API authentication
- Client meeting went well
Expand Down
41 changes: 30 additions & 11 deletions integrations/openclaw/MEMORY_TASK_FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,10 @@ This fallback behavior keeps tasks discoverable even if the graph index is stale
write_note(
title="migrate-auth-routes",
folder="tasks",
content="""---
title: migrate-auth-routes
type: Task
status: active
current_step: 1
---

## Context
note_type="Task",
tags=["auth", "migration"],
metadata={"status": "active", "current_step": 1},
content="""## Context
Starting auth route migration.

## Plan
Expand All @@ -122,12 +118,35 @@ Starting auth route migration.

### Advance a task

- Update plan checkboxes with `edit_note` + `replace_section`
- Bump step with `edit_note` + `find_replace` (for `current_step`)
Update the plan and its frontmatter state together:

```
edit_note(
identifier="tasks/migrate-auth-routes",
operation="replace_section",
section="## Plan",
metadata={"current_step": 2},
content="""## Plan
- [x] Implement middleware
- [ ] Add tests"""
)
```

### Complete a task

Use `edit_note` (`find_replace`) to change `status: active` to `status: done`.
Record the outcome while setting the structured status field:

```
edit_note(
identifier="tasks/migrate-auth-routes",
operation="append",
metadata={"status": "done"},
content="""

## Outcome
Migration completed and verified."""
)
```

## Operational Tips

Expand Down
20 changes: 18 additions & 2 deletions integrations/openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ All tools accept an optional `project` parameter for cross-project operations.
| `memory_get` | Read a specific note by title or path |
| `search_notes` | Search the knowledge graph directly |
| `read_note` | Read a note by title, permalink, or `memory://` URL |
| `write_note` | Create or update a note |
| `edit_note` | Append, prepend, find/replace, or replace a section |
| `write_note` | Create or overwrite a note with optional tags, note type, and custom frontmatter metadata |
| `edit_note` | Append, prepend, replace content, and merge custom frontmatter metadata |
| `delete_note` | Delete a note |
| `move_note` | Move a note to a different folder |
| `build_context` | Navigate the knowledge graph — follow relations and connections |
Expand All @@ -172,6 +172,22 @@ All tools accept an optional `project` parameter for cross-project operations.
| `schema_infer` | Analyze notes and suggest a schema |
| `schema_diff` | Detect drift between schema and actual usage |

Use the dedicated frontmatter parameters when creating structured notes:

```
write_note(
title="auth-middleware-rollout",
folder="tasks",
note_type="Task",
tags=["auth", "rollout"],
metadata={"status": "active", "current_step": 2},
content="""## Context
Rolling JWT middleware to all API routes."""
)
```

`edit_note` accepts `metadata` alongside any edit operation, so content and frontmatter state can be updated in one call. Metadata keys supplied by the caller are merged into existing frontmatter; unrelated fields remain unchanged.

## Slash commands

| Command | Description |
Expand Down
73 changes: 73 additions & 0 deletions integrations/openclaw/bm-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,51 @@ describe("BmClient MCP behavior", () => {
})
})

it("writeNote passes metadata, tags, and note_type when provided", async () => {
const callTool = jest.fn().mockResolvedValue(
mcpResult({
title: "Task Note",
permalink: "tasks/task-note",
file_path: "tasks/task-note.md",
action: "created",
}),
)
setConnected(client, callTool)

await client.writeNote(
"Task Note",
"hello",
"tasks",
undefined,
undefined,
{
metadata: {
status: "active",
scheduling: { due: "2026-09-02" },
},
tags: "work,priority",
noteType: "Task",
},
)

expect(callTool).toHaveBeenCalledWith({
name: "write_note",
arguments: {
title: "Task Note",
content: "hello",
directory: "tasks",
output_format: "json",
project: DEFAULT_PROJECT,
metadata: {
status: "active",
scheduling: { due: "2026-09-02" },
},
tags: "work,priority",
note_type: "Task",
},
})
})

it("writeNote throws NoteAlreadyExistsError on conflict response", async () => {
const callTool = jest.fn().mockResolvedValue(
mcpResult({
Expand Down Expand Up @@ -191,6 +236,34 @@ describe("BmClient MCP behavior", () => {
expect(result.checksum).toBe("abc")
})

it("editNote passes metadata to edit_note", async () => {
const callTool = jest.fn().mockResolvedValue(
mcpResult({
title: "t",
permalink: "p",
file_path: "notes/t.md",
operation: "append",
}),
)
setConnected(client, callTool)

await client.editNote("t", "append", "new", {
metadata: { status: "complete", review: { approved: true } },
})

expect(callTool).toHaveBeenCalledWith({
name: "edit_note",
arguments: {
identifier: "t",
operation: "append",
content: "new",
metadata: { status: "complete", review: { approved: true } },
output_format: "json",
project: DEFAULT_PROJECT,
},
})
})

it("search calls search_notes with paging params", async () => {
const callTool = jest.fn().mockResolvedValue(
mcpResult({
Expand Down
12 changes: 12 additions & 0 deletions integrations/openclaw/bm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,17 @@ interface ReadNoteOptions {
includeFrontmatter?: boolean
}

interface WriteNoteOptions {
metadata?: Record<string, unknown>
tags?: string[] | string
noteType?: string
}

interface EditNoteOptions {
find_text?: string
section?: string
expected_replacements?: number
metadata?: Record<string, unknown>
}

export interface ContextResult {
Expand Down Expand Up @@ -605,6 +612,7 @@ export class BmClient {
folder: string,
project?: string,
overwrite?: boolean,
options: WriteNoteOptions = {},
): Promise<NoteResult> {
const args: Record<string, unknown> = {
title,
Expand All @@ -614,6 +622,9 @@ export class BmClient {
project: this.routedProject(project),
}
if (overwrite !== undefined) args.overwrite = overwrite
if (options.metadata !== undefined) args.metadata = options.metadata
if (options.tags !== undefined) args.tags = options.tags
if (options.noteType !== undefined) args.note_type = options.noteType

const payload = await this.callTool("write_note", args)

Expand Down Expand Up @@ -707,6 +718,7 @@ export class BmClient {
if (options.section) args.section = options.section
if (options.expected_replacements != null)
args.expected_replacements = options.expected_replacements
if (options.metadata !== undefined) args.metadata = options.metadata

const payload = await this.callTool("edit_note", args)

Expand Down
43 changes: 43 additions & 0 deletions integrations/openclaw/tools/edit-note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ describe("edit tool", () => {
type: "number",
}),
project: expect.objectContaining({ type: "string" }),
metadata: expect.objectContaining({
type: "object",
additionalProperties: true,
}),
}),
}),
execute: expect.any(Function),
Expand Down Expand Up @@ -89,6 +93,7 @@ describe("edit tool", () => {
find_text: undefined,
section: undefined,
expected_replacements: undefined,
metadata: undefined,
},
undefined,
)
Expand Down Expand Up @@ -126,6 +131,7 @@ describe("edit tool", () => {
find_text: "old text",
section: undefined,
expected_replacements: 3,
metadata: undefined,
},
undefined,
)
Expand Down Expand Up @@ -154,6 +160,7 @@ describe("edit tool", () => {
find_text: undefined,
section: "## This Week",
expected_replacements: undefined,
metadata: undefined,
},
undefined,
)
Expand Down Expand Up @@ -182,11 +189,47 @@ describe("edit tool", () => {
find_text: undefined,
section: undefined,
expected_replacements: undefined,
metadata: undefined,
},
"other-project",
)
})

it("passes metadata through independently of the edit operation", async () => {
;(mockClient.editNote as jest.MockedFunction<any>).mockResolvedValue({
title: "Test Note",
permalink: "test-note",
file_path: "notes/test-note.md",
operation: "append",
})

await executeFunction("tool-call-id", {
identifier: "test-note",
operation: "append",
content: "new content",
metadata: {
status: "complete",
review: { approved: true },
},
})

expect(mockClient.editNote).toHaveBeenCalledWith(
"test-note",
"append",
"new content",
{
find_text: undefined,
section: undefined,
expected_replacements: undefined,
metadata: {
status: "complete",
review: { approved: true },
},
},
undefined,
)
})

it("returns friendly error when edit fails", async () => {
;(mockClient.editNote as jest.MockedFunction<any>).mockRejectedValue(
new Error("edit failed"),
Expand Down
12 changes: 12 additions & 0 deletions integrations/openclaw/tools/edit-note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ export function registerEditTool(
description: "Target project name (defaults to current project)",
}),
),
metadata: Type.Optional(
Type.Object(
{},
{
additionalProperties: true,
description:
"Frontmatter fields to merge independently of the edit operation. Nested objects are supported.",
},
),
),
}),
async execute(
_toolCallId: string,
Expand All @@ -70,6 +80,7 @@ export function registerEditTool(
section?: string
expected_replacements?: number
project?: string
metadata?: Record<string, unknown>
},
) {
log.debug(`edit_note: id="${params.identifier}" op=${params.operation}`)
Expand All @@ -83,6 +94,7 @@ export function registerEditTool(
find_text: params.find_text,
section: params.section,
expected_replacements: params.expected_replacements,
metadata: params.metadata,
},
params.project,
)
Expand Down
Loading