@basicmemory/openclaw-basic-memory 0.2.4 → 0.23.0

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.
Files changed (37) hide show
  1. package/README.md +11 -1
  2. package/bm-client.ts +55 -21
  3. package/commands/cli.ts +0 -1
  4. package/commands/skills.ts +51 -2
  5. package/config.ts +0 -17
  6. package/context-engine/basic-memory-context-engine.ts +25 -74
  7. package/dist/bm-client.d.ts +2 -0
  8. package/dist/bm-client.js +42 -32
  9. package/dist/commands/cli.js +0 -1
  10. package/dist/commands/skills.js +41 -2
  11. package/dist/config.d.ts +0 -5
  12. package/dist/config.js +0 -10
  13. package/dist/context-engine/basic-memory-context-engine.d.ts +13 -73
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.js +21 -11
  16. package/dist/tools/list-memory-projects.js +2 -1
  17. package/index.ts +23 -12
  18. package/openclaw.plugin.json +2 -15
  19. package/package.json +5 -4
  20. package/scripts/fetch-skills.ts +62 -67
  21. package/scripts/setup-bm.sh +27 -10
  22. package/skills/manifest.json +25 -0
  23. package/skills/memory-capture/SKILL.md +275 -0
  24. package/skills/memory-ci-capture/SKILL.md +56 -0
  25. package/skills/memory-continue/SKILL.md +148 -0
  26. package/skills/memory-curate/SKILL.md +243 -0
  27. package/skills/memory-literary-analysis/SKILL.md +15 -12
  28. package/skills/memory-notes/SKILL.md +22 -6
  29. package/skills/memory-onboarding/SKILL.md +142 -0
  30. package/skills/memory-onboarding/evals/evals.json +29 -0
  31. package/skills/memory-onboarding/references/assistant-setup.md +64 -0
  32. package/skills/memory-onboarding/references/conventions.md +118 -0
  33. package/skills/memory-onboarding/references/domain-playbooks.md +159 -0
  34. package/skills/memory-onboarding/references/schema-guide.md +136 -0
  35. package/skills/memory-schema/SKILL.md +7 -5
  36. package/skills/memory-tasks/SKILL.md +1 -1
  37. package/tools/list-memory-projects.ts +2 -1
package/README.md CHANGED
@@ -18,6 +18,14 @@ Over time, your agent builds a knowledge base that grows with you. Context that
18
18
 
19
19
  Learn more: [basicmemory.com](https://basicmemory.com) · [GitHub](https://github.com/basicmachines-co/basic-memory) · [Docs](https://docs.basicmemory.com)
20
20
 
21
+ Source now lives in the canonical [`basic-memory`](https://github.com/basicmachines-co/basic-memory) repository under `integrations/openclaw/`. The npm package remains `@basicmemory/openclaw-basic-memory`.
22
+
23
+ Maintainer check from the monorepo root:
24
+
25
+ ```bash
26
+ just package-check-openclaw
27
+ ```
28
+
21
29
  ## What this plugin does
22
30
 
23
31
  This plugin connects Basic Memory to OpenClaw so your agent can:
@@ -207,6 +215,8 @@ Ten skills ship with the plugin — no installation needed:
207
215
  bun run fetch-skills
208
216
  ```
209
217
 
218
+ In the monorepo, this copies from the canonical top-level [`skills/`](../../skills) directory into the generated `integrations/openclaw/skills/` package bundle.
219
+
210
220
  ## Task notes
211
221
 
212
222
  The plugin works well with structured task notes in `memory/tasks/`:
@@ -258,7 +268,7 @@ Cloud extends local-first — still plain Markdown, still yours. [Start a free t
258
268
  - [Security](./SECURITY.md) — how auto-installation and data handling work
259
269
  - [Development](./DEVELOPMENT.md) — contributing, tests, publishing
260
270
  - [Basic Memory docs](https://docs.basicmemory.com)
261
- - [Issues](https://github.com/basicmachines-co/openclaw-basic-memory/issues)
271
+ - [Issues](https://github.com/basicmachines-co/basic-memory/issues)
262
272
 
263
273
  ## Telemetry
264
274
 
package/bm-client.ts CHANGED
@@ -100,6 +100,7 @@ export interface ProjectListResult {
100
100
  is_default?: boolean
101
101
  isDefault?: boolean
102
102
  workspace_name?: string | null
103
+ workspace_slug?: string | null
103
104
  workspace_type?: string | null
104
105
  workspace_tenant_id?: string | null
105
106
  }
@@ -198,7 +199,7 @@ function isRecoverableConnectionError(err: unknown): boolean {
198
199
  )
199
200
  }
200
201
 
201
- function _isNoteNotFoundError(err: unknown): boolean {
202
+ function isNoteNotFoundError(err: unknown): boolean {
202
203
  const msg = getErrorMessage(err).toLowerCase()
203
204
  return (
204
205
  msg.includes("entity not found") ||
@@ -213,6 +214,22 @@ function asString(value: unknown): string | null {
213
214
  return typeof value === "string" ? value : null
214
215
  }
215
216
 
217
+ function projectMatchesWorkspace(
218
+ project: ProjectListResult,
219
+ workspace: string,
220
+ ): boolean {
221
+ const requested = workspace.trim().toLowerCase()
222
+ if (!requested) return true
223
+
224
+ return [
225
+ project.workspace_name,
226
+ project.workspace_slug,
227
+ project.workspace_tenant_id,
228
+ ].some(
229
+ (value) => typeof value === "string" && value.toLowerCase() === requested,
230
+ )
231
+ }
232
+
216
233
  export class BmClient {
217
234
  private bmPath: string
218
235
  private project: string
@@ -465,6 +482,10 @@ export class BmClient {
465
482
  return structuredPayload
466
483
  }
467
484
 
485
+ private routedProject(project?: string): string {
486
+ return project ?? this.project
487
+ }
488
+
468
489
  async ensureProject(projectPath: string): Promise<void> {
469
490
  const payload = await this.callTool("create_memory_project", {
470
491
  project_name: this.project,
@@ -491,13 +512,18 @@ export class BmClient {
491
512
  }
492
513
 
493
514
  async listProjects(workspace?: string): Promise<ProjectListResult[]> {
494
- const args: Record<string, unknown> = { output_format: "json" }
495
- if (workspace) args.workspace = workspace
496
-
497
- const payload = await this.callTool("list_memory_projects", args)
515
+ const payload = await this.callTool("list_memory_projects", {
516
+ output_format: "json",
517
+ })
498
518
 
499
519
  if (isRecord(payload) && Array.isArray(payload.projects)) {
500
- return payload.projects as ProjectListResult[]
520
+ const projects = payload.projects as ProjectListResult[]
521
+ if (workspace) {
522
+ return projects.filter((project) =>
523
+ projectMatchesWorkspace(project, workspace),
524
+ )
525
+ }
526
+ return projects
501
527
  }
502
528
 
503
529
  throw new Error("invalid list_memory_projects response")
@@ -519,9 +545,9 @@ export class BmClient {
519
545
  page: 1,
520
546
  page_size: limit,
521
547
  output_format: "json",
548
+ project: this.routedProject(project),
522
549
  }
523
550
  if (query) args.query = query
524
- if (project) args.project = project
525
551
  if (metadata?.filters) args.metadata_filters = metadata.filters
526
552
  if (metadata?.tags) args.tags = metadata.tags
527
553
  if (metadata?.status) args.status = metadata.status
@@ -546,8 +572,8 @@ export class BmClient {
546
572
  identifier,
547
573
  include_frontmatter: options.includeFrontmatter === true,
548
574
  output_format: "json",
575
+ project: this.routedProject(project),
549
576
  }
550
- if (project) args.project = project
551
577
 
552
578
  const payload = await this.callTool("read_note", args)
553
579
 
@@ -585,8 +611,8 @@ export class BmClient {
585
611
  content,
586
612
  directory: folder,
587
613
  output_format: "json",
614
+ project: this.routedProject(project),
588
615
  }
589
- if (project) args.project = project
590
616
  if (overwrite !== undefined) args.overwrite = overwrite
591
617
 
592
618
  const payload = await this.callTool("write_note", args)
@@ -632,8 +658,8 @@ export class BmClient {
632
658
  url,
633
659
  depth,
634
660
  output_format: "json",
661
+ project: this.routedProject(project),
635
662
  }
636
- if (project) args.project = project
637
663
 
638
664
  const payload = await this.callTool("build_context", args)
639
665
 
@@ -651,8 +677,8 @@ export class BmClient {
651
677
  const args: Record<string, unknown> = {
652
678
  timeframe,
653
679
  output_format: "json",
680
+ project: this.routedProject(project),
654
681
  }
655
- if (project) args.project = project
656
682
 
657
683
  const payload = await this.callTool("recent_activity", args)
658
684
 
@@ -675,12 +701,12 @@ export class BmClient {
675
701
  operation,
676
702
  content,
677
703
  output_format: "json",
704
+ project: this.routedProject(project),
678
705
  }
679
706
  if (options.find_text) args.find_text = options.find_text
680
707
  if (options.section) args.section = options.section
681
708
  if (options.expected_replacements != null)
682
709
  args.expected_replacements = options.expected_replacements
683
- if (project) args.project = project
684
710
 
685
711
  const payload = await this.callTool("edit_note", args)
686
712
 
@@ -712,8 +738,8 @@ export class BmClient {
712
738
  const args: Record<string, unknown> = {
713
739
  identifier,
714
740
  output_format: "json",
741
+ project: this.routedProject(project),
715
742
  }
716
- if (project) args.project = project
717
743
 
718
744
  const payload = await this.callTool("delete_note", args)
719
745
 
@@ -741,8 +767,8 @@ export class BmClient {
741
767
  identifier,
742
768
  destination_folder: newFolder,
743
769
  output_format: "json",
770
+ project: this.routedProject(project),
744
771
  }
745
- if (project) args.project = project
746
772
 
747
773
  const payload = await this.callTool("move_note", args)
748
774
 
@@ -770,10 +796,12 @@ export class BmClient {
770
796
  identifier?: string,
771
797
  project?: string,
772
798
  ): Promise<SchemaValidationResult> {
773
- const args: Record<string, unknown> = { output_format: "json" }
799
+ const args: Record<string, unknown> = {
800
+ output_format: "json",
801
+ project: this.routedProject(project),
802
+ }
774
803
  if (noteType) args.note_type = noteType
775
804
  if (identifier) args.identifier = identifier
776
- if (project) args.project = project
777
805
 
778
806
  const payload = await this.callTool("schema_validate", args)
779
807
 
@@ -793,8 +821,8 @@ export class BmClient {
793
821
  note_type: noteType,
794
822
  threshold,
795
823
  output_format: "json",
824
+ project: this.routedProject(project),
796
825
  }
797
- if (project) args.project = project
798
826
 
799
827
  const payload = await this.callTool("schema_infer", args)
800
828
 
@@ -812,8 +840,8 @@ export class BmClient {
812
840
  const args: Record<string, unknown> = {
813
841
  note_type: noteType,
814
842
  output_format: "json",
843
+ project: this.routedProject(project),
815
844
  }
816
- if (project) args.project = project
817
845
 
818
846
  const payload = await this.callTool("schema_diff", args)
819
847
 
@@ -845,13 +873,19 @@ export class BmClient {
845
873
  "---",
846
874
  ].join("\n")
847
875
 
848
- // Try append first — if it fails for ANY reason, fall through to create
849
876
  try {
850
877
  await this.editNote(title, "append", entry)
851
878
  log.debug(`appended conversation to: ${title}`)
852
879
  return
853
880
  } catch (err) {
854
- log.debug(`append failed, will create: ${getErrorMessage(err)}`)
881
+ if (!isNoteNotFoundError(err)) {
882
+ log.error(`conversation append failed: ${getErrorMessage(err)}`, err)
883
+ throw err
884
+ }
885
+
886
+ log.debug(
887
+ `conversation note missing, will create: ${getErrorMessage(err)}`,
888
+ )
855
889
  }
856
890
 
857
891
  // Create the note with frontmatter and first entry
@@ -868,7 +902,7 @@ export class BmClient {
868
902
  ].join("\n")
869
903
 
870
904
  try {
871
- await this.writeNote(title, content, "conversations", undefined, true)
905
+ await this.writeNote(title, content, "conversations")
872
906
  log.debug(`created conversation note: ${title}`)
873
907
  } catch (err) {
874
908
  log.error("conversation index failed", err)
package/commands/cli.ts CHANGED
@@ -167,7 +167,6 @@ export function registerCli(
167
167
  console.log(`Memory dir: ${cfg.memoryDir}`)
168
168
  console.log(`Memory file: ${cfg.memoryFile}`)
169
169
  console.log(`Auto-capture: ${cfg.autoCapture}`)
170
- console.log(`Cloud: ${cfg.cloud ? cfg.cloud.url : "disabled"}`)
171
170
  console.log(`Debug: ${cfg.debug}`)
172
171
  })
173
172
  },
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs"
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs"
2
2
  import { dirname, resolve } from "node:path"
3
3
  import { fileURLToPath } from "node:url"
4
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
@@ -39,13 +39,62 @@ function loadSkill(skillsDir: string, dir: string): string {
39
39
  return readFileSync(resolve(skillsDir, dir, "SKILL.md"), "utf-8")
40
40
  }
41
41
 
42
+ /**
43
+ * List a skill's bundled resource files (references/, assets/, ...) so the
44
+ * command can tell the assistant where they live on disk. SKILL.md workflows
45
+ * reference these by relative path; without the mapping, bundled commands
46
+ * would direct the assistant to files it was never given. evals/ is excluded
47
+ * as test fixtures.
48
+ */
49
+ function listResourceFiles(skillsDir: string, dir: string): string[] {
50
+ const root = resolve(skillsDir, dir)
51
+ const files: string[] = []
52
+ const walk = (rel: string) => {
53
+ for (const entry of readdirSync(resolve(root, rel), {
54
+ withFileTypes: true,
55
+ })) {
56
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name
57
+ if (entry.isDirectory()) {
58
+ if (relPath !== "evals") walk(relPath)
59
+ } else if (relPath !== "SKILL.md") {
60
+ files.push(relPath)
61
+ }
62
+ }
63
+ }
64
+ walk("")
65
+ return files.sort()
66
+ }
67
+
68
+ function resourceSection(
69
+ skillsDir: string,
70
+ dir: string,
71
+ files: string[],
72
+ ): string {
73
+ if (files.length === 0) return ""
74
+ const lines = files
75
+ .map((f) => `- \`${f}\` → ${resolve(skillsDir, dir, f)}`)
76
+ .join("\n")
77
+ return (
78
+ "\n\n## Bundled resource files\n\n" +
79
+ "This skill references companion files by relative path. They are installed at:\n\n" +
80
+ `${lines}\n\n` +
81
+ "When the workflow directs you to read one of these files, read it from the absolute path listed above."
82
+ )
83
+ }
84
+
42
85
  export function registerSkillCommands(api: OpenClawPluginApi): void {
43
86
  const skillsDir = resolveSkillsDir(api)
44
87
  const manifest = loadManifest(skillsDir)
45
88
 
46
89
  for (const entry of manifest) {
47
90
  const commandName = entry.dir.replace(/^memory-/, "")
48
- const content = loadSkill(skillsDir, entry.dir)
91
+ const content =
92
+ loadSkill(skillsDir, entry.dir) +
93
+ resourceSection(
94
+ skillsDir,
95
+ entry.dir,
96
+ listResourceFiles(skillsDir, entry.dir),
97
+ )
49
98
 
50
99
  api.registerCommand({
51
100
  name: commandName,
package/config.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  import { homedir, hostname } from "node:os"
2
2
  import { isAbsolute, resolve } from "node:path"
3
3
 
4
- export type CloudConfig = {
5
- url: string
6
- api_key: string
7
- }
8
-
9
4
  export type BasicMemoryConfig = {
10
5
  project: string
11
6
  bmPath: string
@@ -17,7 +12,6 @@ export type BasicMemoryConfig = {
17
12
  autoRecall: boolean
18
13
  recallPrompt: string
19
14
  debug: boolean
20
- cloud?: CloudConfig
21
15
  }
22
16
 
23
17
  const ALLOWED_KEYS = [
@@ -36,7 +30,6 @@ const ALLOWED_KEYS = [
36
30
  "recallPrompt",
37
31
  "recall_prompt",
38
32
  "debug",
39
- "cloud",
40
33
  ]
41
34
 
42
35
  function assertAllowedKeys(
@@ -98,15 +91,6 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
98
91
  ? (cfg.memory_file as string)
99
92
  : "MEMORY.md"
100
93
 
101
- let cloud: CloudConfig | undefined
102
- if (cfg.cloud && typeof cfg.cloud === "object" && !Array.isArray(cfg.cloud)) {
103
- const c = cfg.cloud as Record<string, unknown>
104
- assertAllowedKeys(c, ["url", "api_key"], "basic-memory cloud config")
105
- if (typeof c.url === "string" && typeof c.api_key === "string") {
106
- cloud = { url: c.url, api_key: c.api_key }
107
- }
108
- }
109
-
110
94
  return {
111
95
  project:
112
96
  typeof cfg.project === "string" && cfg.project.length > 0
@@ -144,7 +128,6 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
144
128
  ? (cfg.recall_prompt as string)
145
129
  : "Check for active tasks and recent activity. Summarize anything relevant to the current session.",
146
130
  debug: typeof cfg.debug === "boolean" ? cfg.debug : false,
147
- cloud,
148
131
  }
149
132
  }
150
133
 
@@ -1,4 +1,10 @@
1
- import type { AgentMessage } from "@mariozechner/pi-agent-core"
1
+ import type {
2
+ AssembleResult,
3
+ BootstrapResult,
4
+ CompactResult,
5
+ ContextEngine,
6
+ SubagentSpawnPreparation,
7
+ } from "openclaw/plugin-sdk"
2
8
  import { delegateCompactionToRuntime } from "openclaw/plugin-sdk/core"
3
9
  import type { BmClient } from "../bm-client.ts"
4
10
  import type { BasicMemoryConfig } from "../config.ts"
@@ -11,41 +17,16 @@ const TRUNCATED_RECALL_SUFFIX = "\n\n[Basic Memory recall truncated]"
11
17
  const SUBAGENT_HANDOFF_FOLDER = "agent/subagents"
12
18
  const MAX_SUBAGENT_RECALL_CHARS = 800
13
19
 
14
- type AssembleResult = {
15
- messages: AgentMessage[]
16
- estimatedTokens: number
17
- systemPromptAddition?: string
18
- }
19
-
20
- type BootstrapResult = {
21
- bootstrapped: boolean
22
- importedMessages?: number
23
- reason?: string
24
- }
25
-
26
- type CompactResult = {
27
- ok: boolean
28
- compacted: boolean
29
- reason?: string
30
- result?: {
31
- summary?: string
32
- firstKeptEntryId?: string
33
- tokensBefore: number
34
- tokensAfter?: number
35
- details?: unknown
36
- sessionId?: string
37
- sessionFile?: string
38
- }
39
- }
40
-
41
- interface ContextEngine {
42
- readonly info: {
43
- id: string
44
- name: string
45
- version?: string
46
- ownsCompaction?: boolean
47
- }
48
- }
20
+ type BootstrapParams = Parameters<NonNullable<ContextEngine["bootstrap"]>>[0]
21
+ type AssembleParams = Parameters<ContextEngine["assemble"]>[0]
22
+ type AfterTurnParams = Parameters<NonNullable<ContextEngine["afterTurn"]>>[0]
23
+ type CompactParams = Parameters<ContextEngine["compact"]>[0]
24
+ type PrepareSubagentSpawnParams = Parameters<
25
+ NonNullable<ContextEngine["prepareSubagentSpawn"]>
26
+ >[0]
27
+ type OnSubagentEndedParams = Parameters<
28
+ NonNullable<ContextEngine["onSubagentEnded"]>
29
+ >[0]
49
30
 
50
31
  interface SessionMemoryState {
51
32
  recallContext: string
@@ -147,10 +128,7 @@ export class BasicMemoryContextEngine implements ContextEngine {
147
128
  private readonly cfg: BasicMemoryConfig,
148
129
  ) {}
149
130
 
150
- async bootstrap(params: {
151
- sessionId: string
152
- sessionFile: string
153
- }): Promise<BootstrapResult> {
131
+ async bootstrap(params: BootstrapParams): Promise<BootstrapResult> {
154
132
  if (!this.cfg.autoRecall) {
155
133
  this.sessionState.delete(params.sessionId)
156
134
  return { bootstrapped: false, reason: "autoRecall disabled" }
@@ -183,11 +161,7 @@ export class BasicMemoryContextEngine implements ContextEngine {
183
161
  return { ingested: false }
184
162
  }
185
163
 
186
- async assemble(params: {
187
- sessionId: string
188
- messages: AgentMessage[]
189
- tokenBudget?: number
190
- }): Promise<AssembleResult> {
164
+ async assemble(params: AssembleParams): Promise<AssembleResult> {
191
165
  const state = this.sessionState.get(params.sessionId)
192
166
 
193
167
  return {
@@ -197,16 +171,7 @@ export class BasicMemoryContextEngine implements ContextEngine {
197
171
  }
198
172
  }
199
173
 
200
- async afterTurn(params: {
201
- sessionId: string
202
- sessionFile: string
203
- messages: AgentMessage[]
204
- prePromptMessageCount: number
205
- autoCompactionSummary?: string
206
- isHeartbeat?: boolean
207
- tokenBudget?: number
208
- runtimeContext?: Record<string, unknown>
209
- }): Promise<void> {
174
+ async afterTurn(params: AfterTurnParams): Promise<void> {
210
175
  if (!this.cfg.autoCapture) return
211
176
 
212
177
  const newMessages = params.messages.slice(params.prePromptMessageCount)
@@ -227,24 +192,13 @@ export class BasicMemoryContextEngine implements ContextEngine {
227
192
  }
228
193
  }
229
194
 
230
- async compact(params: {
231
- sessionId: string
232
- sessionFile: string
233
- tokenBudget?: number
234
- force?: boolean
235
- currentTokenCount?: number
236
- compactionTarget?: "budget" | "threshold"
237
- customInstructions?: string
238
- runtimeContext?: Record<string, unknown>
239
- }): Promise<CompactResult> {
195
+ async compact(params: CompactParams): Promise<CompactResult> {
240
196
  return delegateCompactionToRuntime(params)
241
197
  }
242
198
 
243
- async prepareSubagentSpawn(params: {
244
- parentSessionKey: string
245
- childSessionKey: string
246
- ttlMs?: number
247
- }): Promise<{ rollback: () => Promise<void> } | undefined> {
199
+ async prepareSubagentSpawn(
200
+ params: PrepareSubagentSpawnParams,
201
+ ): Promise<SubagentSpawnPreparation | undefined> {
248
202
  const parentState = this.sessionState.get(params.parentSessionKey)
249
203
  const noteTitle = buildSubagentNoteTitle(params.childSessionKey)
250
204
 
@@ -284,10 +238,7 @@ export class BasicMemoryContextEngine implements ContextEngine {
284
238
  }
285
239
  }
286
240
 
287
- async onSubagentEnded(params: {
288
- childSessionKey: string
289
- reason: "deleted" | "completed" | "swept" | "released"
290
- }): Promise<void> {
241
+ async onSubagentEnded(params: OnSubagentEndedParams): Promise<void> {
291
242
  const handoff = this.subagentState.get(params.childSessionKey)
292
243
  if (!handoff) return
293
244
 
@@ -64,6 +64,7 @@ export interface ProjectListResult {
64
64
  is_default?: boolean;
65
65
  isDefault?: boolean;
66
66
  workspace_name?: string | null;
67
+ workspace_slug?: string | null;
67
68
  workspace_type?: string | null;
68
69
  workspace_tenant_id?: string | null;
69
70
  }
@@ -147,6 +148,7 @@ export declare class BmClient {
147
148
  private disconnectCurrent;
148
149
  private callToolRaw;
149
150
  private callTool;
151
+ private routedProject;
150
152
  ensureProject(projectPath: string): Promise<void>;
151
153
  listWorkspaces(): Promise<WorkspaceResult[]>;
152
154
  listProjects(workspace?: string): Promise<ProjectListResult[]>;