Skip to content

Commit 20b3017

Browse files
committed
feat: add project list tool and lint fix workflow
1 parent 84b320a commit 20b3017

26 files changed

Lines changed: 693 additions & 132 deletions

README.md

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,43 @@ For a practical runbook, see [Memory + Task Flow](./MEMORY_TASK_FLOW.md).
3030

3131
## Installation
3232

33-
Clone and install:
33+
### Install in OpenClaw (local directory)
34+
3435
```bash
3536
git clone https://github.com/basicmachines-co/openclaw-basic-memory.git
3637
cd openclaw-basic-memory
3738
bun install
39+
openclaw plugins install -l "$PWD"
3840
```
3941

40-
### Optional: Install Companion Skills
41-
42-
You can pair this plugin with skills from
43-
[`basic-memory-skills`](https://github.com/basicmachines-co/basic-memory-skills):
42+
Then set this plugin as the memory slot owner in your OpenClaw config:
4443

45-
- `memory-tasks` — structured task tracking that survives compaction
46-
- `memory-reflect` — periodic consolidation of recent notes into durable memory
47-
- `memory-defrag` — periodic cleanup/reorganization of memory files
44+
```json5
45+
{
46+
plugins: {
47+
entries: {
48+
"basic-memory": {
49+
enabled: true
50+
}
51+
},
52+
slots: {
53+
memory: "basic-memory"
54+
}
55+
}
56+
}
57+
```
4858

49-
Install (workspace-local):
59+
Restart OpenClaw gateway after install/config changes, then verify:
5060

5161
```bash
52-
git clone https://github.com/basicmachines-co/basic-memory-skills.git
53-
cp -r basic-memory-skills/memory-tasks ~/.openclaw/workspace/skills/
54-
cp -r basic-memory-skills/memory-reflect ~/.openclaw/workspace/skills/
55-
cp -r basic-memory-skills/memory-defrag ~/.openclaw/workspace/skills/
62+
openclaw plugins list
63+
openclaw plugins info basic-memory
5664
```
5765

58-
If you want these skills available to multiple workspaces/agents on the same machine,
59-
install to `~/.openclaw/skills/` instead.
66+
### Manual Load Path (Dev Alternative)
6067

61-
After installation, start a new OpenClaw session so the refreshed skill set is loaded.
68+
If you prefer loading directly from a path in config instead of `plugins install`:
6269

63-
Add to your OpenClaw config:
6470
```json5
6571
{
6672
plugins: {
@@ -71,11 +77,37 @@ Add to your OpenClaw config:
7177
"basic-memory": {
7278
enabled: true
7379
}
80+
},
81+
slots: {
82+
memory: "basic-memory"
7483
}
7584
}
7685
}
7786
```
7887

88+
### Optional: Install Companion Skills
89+
90+
You can pair this plugin with skills from
91+
[`basic-memory-skills`](https://github.com/basicmachines-co/basic-memory-skills):
92+
93+
- `memory-tasks` — structured task tracking that survives compaction
94+
- `memory-reflect` — periodic consolidation of recent notes into durable memory
95+
- `memory-defrag` — periodic cleanup/reorganization of memory files
96+
97+
Install (workspace-local):
98+
99+
```bash
100+
git clone https://github.com/basicmachines-co/basic-memory-skills.git
101+
cp -r basic-memory-skills/memory-tasks ~/.openclaw/workspace/skills/
102+
cp -r basic-memory-skills/memory-reflect ~/.openclaw/workspace/skills/
103+
cp -r basic-memory-skills/memory-defrag ~/.openclaw/workspace/skills/
104+
```
105+
106+
If you want these skills available to multiple workspaces/agents on the same machine,
107+
install to `~/.openclaw/skills/` instead.
108+
109+
After installation, start a new OpenClaw session so the refreshed skill set is loaded.
110+
79111
## Configuration
80112

81113
### Minimal (zero-config)
@@ -344,6 +376,30 @@ bun run lint # Linting
344376
bun test # Run tests (156 tests)
345377
```
346378

379+
## Publish to npm
380+
381+
This package is published as `@openclaw/basic-memory`.
382+
383+
```bash
384+
# 1) Verify release readiness (types + tests + npm pack dry run)
385+
just release-check
386+
387+
# 2) Inspect publish payload
388+
just release-pack
389+
390+
# 3) Authenticate once (if needed)
391+
npm login
392+
393+
# 4) Publish current version from package.json
394+
just release-publish
395+
```
396+
397+
For a full release (version bump + publish + push tag):
398+
399+
```bash
400+
just release patch # or: minor, major, 0.2.0, etc.
401+
```
402+
347403
### Project Structure
348404
```
349405
openclaw-basic-memory/

biome.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@
3131
"semicolons": "asNeeded"
3232
}
3333
},
34+
"overrides": [
35+
{
36+
"includes": ["**/*.test.ts"],
37+
"linter": {
38+
"rules": {
39+
"style": {
40+
"noNonNullAssertion": "off"
41+
},
42+
"complexity": {
43+
"noBannedTypes": "off"
44+
},
45+
"suspicious": {
46+
"noExplicitAny": "off"
47+
}
48+
}
49+
}
50+
}
51+
],
3452
"linter": {
3553
"domains": {
3654
"project": "none"

bm-client.test.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { beforeEach, describe, expect, it, jest } from "bun:test"
22
import {
33
BmClient,
4-
isProjectAlreadyExistsError,
54
isMissingEditNoteCommandError,
65
isNoteNotFoundError,
6+
isProjectAlreadyExistsError,
77
isUnsupportedStripFrontmatterError,
88
parseJsonOutput,
99
stripFrontmatter,
@@ -47,18 +47,18 @@ Warning: something happened
4747
new Error("No such option: --strip-frontmatter"),
4848
),
4949
).toBe(true)
50-
expect(isUnsupportedStripFrontmatterError(new Error("different error"))).toBe(
51-
false,
52-
)
50+
expect(
51+
isUnsupportedStripFrontmatterError(new Error("different error")),
52+
).toBe(false)
5353
})
5454

5555
it("detects missing edit-note command errors", () => {
5656
expect(
5757
isMissingEditNoteCommandError(new Error("No such command 'edit-note'")),
5858
).toBe(true)
59-
expect(isMissingEditNoteCommandError(new Error("validation failed"))).toBe(
60-
false,
61-
)
59+
expect(
60+
isMissingEditNoteCommandError(new Error("validation failed")),
61+
).toBe(false)
6262
})
6363

6464
it("detects note-not-found errors and excludes missing command errors", () => {
@@ -199,9 +199,7 @@ describe("BmClient behavior", () => {
199199
})
200200

201201
it("ensureProject throws when project creation fails for other reasons", async () => {
202-
const execRaw = jest
203-
.fn()
204-
.mockRejectedValue(new Error("permission denied"))
202+
const execRaw = jest.fn().mockRejectedValue(new Error("permission denied"))
205203
;(client as any).execRaw = execRaw
206204

207205
await expect(client.ensureProject("/tmp/memory")).rejects.toThrow(
@@ -230,13 +228,49 @@ describe("BmClient behavior", () => {
230228
])
231229
})
232230

231+
it("listProjects runs bm project list --format json", async () => {
232+
const execRaw = jest.fn().mockResolvedValue(
233+
JSON.stringify([
234+
{
235+
name: "alpha",
236+
path: "/tmp/alpha",
237+
display_name: "Alpha Project",
238+
is_private: true,
239+
is_default: true,
240+
},
241+
]),
242+
)
243+
;(client as any).execRaw = execRaw
244+
245+
const projects = await client.listProjects()
246+
247+
expect(execRaw).toHaveBeenCalledWith([
248+
"project",
249+
"list",
250+
"--format",
251+
"json",
252+
])
253+
expect(projects).toEqual([
254+
{
255+
name: "alpha",
256+
path: "/tmp/alpha",
257+
display_name: "Alpha Project",
258+
is_private: true,
259+
is_default: true,
260+
},
261+
])
262+
})
263+
233264
it("indexConversation does not create fallback note on non-not-found edit errors", async () => {
234265
;(client as any).editNote = jest
235266
.fn()
236267
.mockRejectedValue(new Error("No such command 'edit-note'"))
237268
;(client as any).writeNote = jest.fn()
238269

239-
await client.indexConversation("user message long enough", "assistant reply long enough")
270+
await client.indexConversation(
271+
"user message long enough",
272+
"assistant reply long enough",
273+
)
240274

241275
expect((client as any).writeNote).not.toHaveBeenCalled()
242276
})
@@ -252,7 +286,10 @@ describe("BmClient behavior", () => {
252286
file_path: "conversations/x.md",
253287
})
254288

255-
await client.indexConversation("user message long enough", "assistant reply long enough")
289+
await client.indexConversation(
290+
"user message long enough",
291+
"assistant reply long enough",
292+
)
256293

257294
expect((client as any).writeNote).toHaveBeenCalledTimes(1)
258295
const [title, content, folder] = (client as any).writeNote.mock.calls[0]

bm-client.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ export interface RecentResult {
6969
created_at: string
7070
}
7171

72+
export interface ProjectListResult {
73+
name: string
74+
path: string
75+
display_name?: string | null
76+
is_private?: boolean
77+
is_default?: boolean
78+
isDefault?: boolean
79+
}
80+
7281
/**
7382
* Extract JSON from CLI output that may contain non-JSON prefix lines
7483
* (warnings, log messages, etc). Finds the first line starting with
@@ -219,6 +228,27 @@ export class BmClient {
219228
}
220229
}
221230

231+
async listProjects(): Promise<ProjectListResult[]> {
232+
const out = await this.execRaw(["project", "list", "--format", "json"])
233+
const parsed = parseJsonOutput(out)
234+
235+
if (Array.isArray(parsed)) {
236+
return parsed as ProjectListResult[]
237+
}
238+
239+
if (parsed && typeof parsed === "object") {
240+
const asRecord = parsed as Record<string, unknown>
241+
if (Array.isArray(asRecord.projects)) {
242+
return asRecord.projects as ProjectListResult[]
243+
}
244+
if (Array.isArray(asRecord.items)) {
245+
return asRecord.items as ProjectListResult[]
246+
}
247+
}
248+
249+
throw new Error("invalid bm project list response")
250+
}
251+
222252
async search(query: string, limit = 10): Promise<SearchResult[]> {
223253
// search-notes outputs JSON natively (no --format flag needed)
224254
// Try hybrid search (FTS + vector) first, fall back to FTS if semantic is disabled
@@ -268,7 +298,11 @@ export class BmClient {
268298
if (!isUnsupportedStripFrontmatterError(err)) {
269299
throw err
270300
}
271-
const fallbackOut = await this.execTool(["tool", "read-note", identifier])
301+
const fallbackOut = await this.execTool([
302+
"tool",
303+
"read-note",
304+
identifier,
305+
])
272306
const fallbackResult = parseJsonOutput(fallbackOut) as NoteResult
273307
fallbackResult.content = stripFrontmatter(fallbackResult.content)
274308
return fallbackResult
@@ -349,7 +383,10 @@ export class BmClient {
349383
}
350384

351385
if (options.expected_replacements !== undefined) {
352-
args.push("--expected-replacements", String(options.expected_replacements))
386+
args.push(
387+
"--expected-replacements",
388+
String(options.expected_replacements),
389+
)
353390
}
354391

355392
try {
@@ -419,7 +456,11 @@ export class BmClient {
419456
const oldPath = resolve(projectPath, existing.file_path)
420457

421458
// Write to new folder (this creates the note at the new location)
422-
const result = await this.writeNote(existing.title, existing.content, newFolder)
459+
const result = await this.writeNote(
460+
existing.title,
461+
existing.content,
462+
newFolder,
463+
)
423464

424465
// Delete old file
425466
try {

commands/cli.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ export function registerCli(
7979
async (
8080
identifier: string,
8181
opts: {
82-
operation: "append" | "prepend" | "find_replace" | "replace_section"
82+
operation:
83+
| "append"
84+
| "prepend"
85+
| "find_replace"
86+
| "replace_section"
8387
content: string
8488
findText?: string
8589
section?: string

config.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ describe("config", () => {
103103
})
104104

105105
it("should throw error for unknown config keys", () => {
106-
expect(() =>
107-
parseConfig({ unknownKey: "value" }),
108-
).toThrow("basic-memory config has unknown keys: unknownKey")
106+
expect(() => parseConfig({ unknownKey: "value" })).toThrow(
107+
"basic-memory config has unknown keys: unknownKey",
108+
)
109109
})
110110

111111
it("should handle complete config object", () => {

0 commit comments

Comments
 (0)