@basicmemory/openclaw-basic-memory 0.1.0-alpha.9 → 0.1.2

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/bm-client.ts CHANGED
@@ -1,14 +1,12 @@
1
1
  import { setTimeout as delay } from "node:timers/promises"
2
2
  import { Client } from "@modelcontextprotocol/sdk/client"
3
3
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
4
- import { trackToolCall } from "./analytics.ts"
5
4
  import { log } from "./logger.ts"
6
5
 
7
6
  const DEFAULT_RETRY_DELAYS_MS = [500, 1000, 2000]
8
7
 
9
8
  const REQUIRED_TOOLS = [
10
9
  "search_notes",
11
- "search_by_metadata",
12
10
  "read_note",
13
11
  "write_note",
14
12
  "edit_note",
@@ -154,12 +152,6 @@ export interface SchemaDiffResult {
154
152
  cardinality_changes: string[]
155
153
  }
156
154
 
157
- export interface MetadataSearchResult {
158
- results: SearchResult[]
159
- current_page: number
160
- page_size: number
161
- }
162
-
163
155
  function getErrorMessage(err: unknown): string {
164
156
  return err instanceof Error ? err.message : String(err)
165
157
  }
@@ -197,7 +189,7 @@ function isRecoverableConnectionError(err: unknown): boolean {
197
189
  )
198
190
  }
199
191
 
200
- function isNoteNotFoundError(err: unknown): boolean {
192
+ function _isNoteNotFoundError(err: unknown): boolean {
201
193
  const msg = getErrorMessage(err).toLowerCase()
202
194
  return (
203
195
  msg.includes("entity not found") ||
@@ -450,7 +442,6 @@ export class BmClient {
450
442
  name: string,
451
443
  args: Record<string, unknown>,
452
444
  ): Promise<unknown> {
453
- trackToolCall(name)
454
445
  const result = await this.callToolRaw(name, args)
455
446
 
456
447
  if (!isRecord(result) || result.structuredContent === undefined) {
@@ -504,25 +495,29 @@ export class BmClient {
504
495
  }
505
496
 
506
497
  async search(
507
- query: string,
498
+ query?: string,
508
499
  limit = 10,
509
500
  project?: string,
510
501
  metadata?: {
511
502
  filters?: Record<string, unknown>
512
503
  tags?: string[]
513
504
  status?: string
505
+ note_types?: string[]
506
+ entity_types?: string[]
514
507
  },
515
508
  ): Promise<SearchResult[]> {
516
509
  const args: Record<string, unknown> = {
517
- query,
518
510
  page: 1,
519
511
  page_size: limit,
520
512
  output_format: "json",
521
513
  }
514
+ if (query) args.query = query
522
515
  if (project) args.project = project
523
516
  if (metadata?.filters) args.metadata_filters = metadata.filters
524
517
  if (metadata?.tags) args.tags = metadata.tags
525
518
  if (metadata?.status) args.status = metadata.status
519
+ if (metadata?.note_types) args.note_types = metadata.note_types
520
+ if (metadata?.entity_types) args.entity_types = metadata.entity_types
526
521
 
527
522
  const payload = await this.callTool("search_notes", args)
528
523
 
@@ -661,11 +656,12 @@ export class BmClient {
661
656
  identifier,
662
657
  operation,
663
658
  content,
664
- find_text: options.find_text,
665
- section: options.section,
666
- expected_replacements: options.expected_replacements,
667
659
  output_format: "json",
668
660
  }
661
+ if (options.find_text) args.find_text = options.find_text
662
+ if (options.section) args.section = options.section
663
+ if (options.expected_replacements != null)
664
+ args.expected_replacements = options.expected_replacements
669
665
  if (project) args.project = project
670
666
 
671
667
  const payload = await this.callTool("edit_note", args)
@@ -810,31 +806,6 @@ export class BmClient {
810
806
  return payload as unknown as SchemaDiffResult
811
807
  }
812
808
 
813
- async searchByMetadata(
814
- filters: Record<string, unknown>,
815
- limit = 20,
816
- project?: string,
817
- ): Promise<MetadataSearchResult> {
818
- const args: Record<string, unknown> = {
819
- filters,
820
- limit,
821
- output_format: "json",
822
- }
823
- if (project) args.project = project
824
-
825
- const payload = await this.callTool("search_by_metadata", args)
826
-
827
- if (!isRecord(payload) || !Array.isArray(payload.results)) {
828
- throw new Error("invalid search_by_metadata response")
829
- }
830
-
831
- return payload as unknown as MetadataSearchResult
832
- }
833
-
834
- private isNoteNotFoundError(err: unknown): boolean {
835
- return isNoteNotFoundError(err)
836
- }
837
-
838
809
  async indexConversation(
839
810
  userMessage: string,
840
811
  assistantResponse: string,
@@ -856,22 +827,33 @@ export class BmClient {
856
827
  "---",
857
828
  ].join("\n")
858
829
 
830
+ // Try append first — if it fails for ANY reason, fall through to create
859
831
  try {
860
832
  await this.editNote(title, "append", entry)
861
833
  log.debug(`appended conversation to: ${title}`)
834
+ return
862
835
  } catch (err) {
863
- if (!this.isNoteNotFoundError(err)) {
864
- log.error("conversation append failed", err)
865
- return
866
- }
836
+ log.debug(`append failed, will create: ${getErrorMessage(err)}`)
837
+ }
867
838
 
868
- const content = [`# Conversations ${dateStr}`, "", entry].join("\n")
869
- try {
870
- await this.writeNote(title, content, "conversations")
871
- log.debug(`created conversation note: ${title}`)
872
- } catch (createErr) {
873
- log.error("conversation index failed", createErr)
874
- }
839
+ // Create the note with frontmatter and first entry
840
+ const content = [
841
+ "---",
842
+ `title: Conversations ${dateStr}`,
843
+ "type: Conversation",
844
+ `date: "${dateStr}"`,
845
+ "---",
846
+ "",
847
+ `# Conversations ${dateStr}`,
848
+ "",
849
+ entry,
850
+ ].join("\n")
851
+
852
+ try {
853
+ await this.writeNote(title, content, "conversations")
854
+ log.debug(`created conversation note: ${title}`)
855
+ } catch (err) {
856
+ log.error("conversation index failed", err)
875
857
  }
876
858
  }
877
859
 
package/config.ts CHANGED
@@ -6,11 +6,6 @@ export type CloudConfig = {
6
6
  api_key: string
7
7
  }
8
8
 
9
- export type DashboardConfig = {
10
- enabled: boolean
11
- port: number
12
- }
13
-
14
9
  export type BasicMemoryConfig = {
15
10
  project: string
16
11
  bmPath: string
@@ -23,7 +18,6 @@ export type BasicMemoryConfig = {
23
18
  recallPrompt: string
24
19
  debug: boolean
25
20
  cloud?: CloudConfig
26
- dashboard: DashboardConfig
27
21
  }
28
22
 
29
23
  const ALLOWED_KEYS = [
@@ -43,7 +37,6 @@ const ALLOWED_KEYS = [
43
37
  "recall_prompt",
44
38
  "debug",
45
39
  "cloud",
46
- "dashboard",
47
40
  ]
48
41
 
49
42
  function assertAllowedKeys(
@@ -113,22 +106,6 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
113
106
  }
114
107
  }
115
108
 
116
- let dashboard: DashboardConfig = { enabled: false, port: 3838 }
117
- if (
118
- cfg.dashboard &&
119
- typeof cfg.dashboard === "object" &&
120
- !Array.isArray(cfg.dashboard)
121
- ) {
122
- const d = cfg.dashboard as Record<string, unknown>
123
- dashboard = {
124
- enabled: typeof d.enabled === "boolean" ? d.enabled : false,
125
- port:
126
- typeof d.port === "number" && d.port > 0 && d.port < 65536
127
- ? d.port
128
- : 3838,
129
- }
130
- }
131
-
132
109
  return {
133
110
  project:
134
111
  typeof cfg.project === "string" && cfg.project.length > 0
@@ -167,7 +144,6 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
167
144
  : "Check for active tasks and recent activity. Summarize anything relevant to the current session.",
168
145
  debug: typeof cfg.debug === "boolean" ? cfg.debug : false,
169
146
  cloud,
170
- dashboard,
171
147
  }
172
148
  }
173
149
 
package/hooks/recall.ts CHANGED
@@ -1,8 +1,4 @@
1
- import type {
2
- BmClient,
3
- MetadataSearchResult,
4
- RecentResult,
5
- } from "../bm-client.ts"
1
+ import type { BmClient, RecentResult, SearchResult } from "../bm-client.ts"
6
2
  import type { BasicMemoryConfig } from "../config.ts"
7
3
  import { log } from "../logger.ts"
8
4
 
@@ -11,14 +7,14 @@ import { log } from "../logger.ts"
11
7
  * Returns empty string if nothing was found.
12
8
  */
13
9
  export function formatRecallContext(
14
- tasks: MetadataSearchResult,
10
+ tasks: SearchResult[],
15
11
  recent: RecentResult[],
16
12
  prompt: string,
17
13
  ): string {
18
14
  const sections: string[] = []
19
15
 
20
- if (tasks.results.length > 0) {
21
- const taskLines = tasks.results.map((t) => {
16
+ if (tasks.length > 0) {
17
+ const taskLines = tasks.map((t) => {
22
18
  const preview =
23
19
  t.content.length > 120 ? `${t.content.slice(0, 120)}...` : t.content
24
20
  return `- **${t.title}** — ${preview}`
@@ -46,7 +42,10 @@ export function buildRecallHandler(client: BmClient, cfg: BasicMemoryConfig) {
46
42
  return async (_event: Record<string, unknown>) => {
47
43
  try {
48
44
  const [tasks, recent] = await Promise.all([
49
- client.searchByMetadata({ entity_type: "Task", status: "active" }, 5),
45
+ client.search(undefined, 5, undefined, {
46
+ note_types: ["Task"],
47
+ status: "active",
48
+ }),
50
49
  client.recentActivity("1d"),
51
50
  ])
52
51
 
@@ -54,7 +53,7 @@ export function buildRecallHandler(client: BmClient, cfg: BasicMemoryConfig) {
54
53
  if (!context) return {}
55
54
 
56
55
  log.debug(
57
- `recall: ${tasks.results.length} active tasks, ${recent.length} recent items`,
56
+ `recall: ${tasks.length} active tasks, ${recent.length} recent items`,
58
57
  )
59
58
 
60
59
  return { context }
package/index.ts CHANGED
@@ -1,10 +1,6 @@
1
- import type { Server } from "node:http"
1
+ import { execSync } from "node:child_process"
2
+
2
3
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
- import {
4
- EVENT_PLUGIN_INSTALLED,
5
- EVENT_PLUGIN_STARTED,
6
- track,
7
- } from "./analytics.ts"
8
4
  import { BmClient } from "./bm-client.ts"
9
5
  import { registerCli } from "./commands/cli.ts"
10
6
  import { registerSkillCommands } from "./commands/skills.ts"
@@ -14,10 +10,11 @@ import {
14
10
  parseConfig,
15
11
  resolveProjectPath,
16
12
  } from "./config.ts"
17
- import { createDashboardServer } from "./dashboard/server.ts"
13
+
18
14
  import { buildCaptureHandler } from "./hooks/capture.ts"
19
15
  import { buildRecallHandler } from "./hooks/recall.ts"
20
16
  import { initLogger, log } from "./logger.ts"
17
+ import { CONVERSATION_SCHEMA_CONTENT } from "./schema/conversation-schema.ts"
21
18
  import { TASK_SCHEMA_CONTENT } from "./schema/task-schema.ts"
22
19
  import { registerContextTool } from "./tools/build-context.ts"
23
20
  import { registerDeleteTool } from "./tools/delete-note.ts"
@@ -53,8 +50,6 @@ export default {
53
50
  `project=${cfg.project} memoryDir=${cfg.memoryDir} memoryFile=${cfg.memoryFile}`,
54
51
  )
55
52
 
56
- track(EVENT_PLUGIN_INSTALLED)
57
-
58
53
  const client = new BmClient(cfg.bmPath, cfg.project)
59
54
 
60
55
  // --- BM Tools (always registered) ---
@@ -89,13 +84,47 @@ export default {
89
84
  registerCli(api, client, cfg)
90
85
 
91
86
  // --- Service lifecycle ---
92
- let dashboardServer: Server | undefined
93
-
94
87
  api.registerService({
95
88
  id: "openclaw-basic-memory",
96
89
  start: async (ctx: { config?: unknown; workspaceDir?: string }) => {
97
90
  log.info("starting...")
98
91
 
92
+ // Auto-install bm CLI if not found
93
+ const bmBin = cfg.bmPath || "bm"
94
+ try {
95
+ execSync(`command -v ${bmBin}`, { stdio: "ignore" })
96
+ } catch {
97
+ log.info("bm CLI not found on PATH — attempting auto-install...")
98
+ try {
99
+ execSync("command -v uv", { stdio: "ignore" })
100
+ log.info(
101
+ "installing basic-memory via uv (this may take a minute)...",
102
+ )
103
+ const result = execSync(
104
+ 'uv tool install "basic-memory @ git+https://github.com/basicmachines-co/basic-memory.git@main" --force',
105
+ { encoding: "utf-8", timeout: 120_000, stdio: "pipe" },
106
+ )
107
+ log.info(
108
+ `basic-memory installed: ${result.trim().split("\n").pop()}`,
109
+ )
110
+ // Verify it worked
111
+ try {
112
+ execSync(`command -v ${bmBin}`, { stdio: "ignore" })
113
+ log.info("bm CLI now available on PATH")
114
+ } catch {
115
+ log.error(
116
+ "bm installed but not found on PATH. You may need to add uv's bin directory to your PATH (typically ~/.local/bin).",
117
+ )
118
+ }
119
+ } catch (_uvErr) {
120
+ log.error(
121
+ "Cannot auto-install basic-memory: uv not found. " +
122
+ "Install uv first (brew install uv, or curl -LsSf https://astral.sh/uv/install.sh | sh), " +
123
+ "then restart the gateway.",
124
+ )
125
+ }
126
+ }
127
+
99
128
  const workspace = ctx.workspaceDir ?? process.cwd()
100
129
  const projectPath = resolveProjectPath(cfg.projectPath, workspace)
101
130
  cfg.projectPath = projectPath
@@ -104,46 +133,30 @@ export default {
104
133
  await client.ensureProject(projectPath)
105
134
  log.debug(`project "${cfg.project}" at ${projectPath}`)
106
135
 
107
- // Seed Task schema if not already present
108
- try {
109
- await client.readNote("schema/Task")
110
- log.debug("Task schema already exists, skipping seed")
111
- } catch {
136
+ // Seed schemas if not already present
137
+ for (const [name, content] of [
138
+ ["Task", TASK_SCHEMA_CONTENT],
139
+ ["Conversation", CONVERSATION_SCHEMA_CONTENT],
140
+ ] as const) {
112
141
  try {
113
- await client.writeNote("Task", TASK_SCHEMA_CONTENT, "schema")
114
- log.debug("seeded Task schema note")
115
- } catch (err) {
116
- log.debug("Task schema seed failed (non-fatal)", err)
142
+ await client.readNote(`schema/${name}`)
143
+ log.debug(`${name} schema already exists, skipping seed`)
144
+ } catch {
145
+ try {
146
+ await client.writeNote(name, content, "schema")
147
+ log.debug(`seeded ${name} schema note`)
148
+ } catch (err) {
149
+ log.debug(`${name} schema seed failed (non-fatal)`, err)
150
+ }
117
151
  }
118
152
  }
119
153
 
120
154
  setWorkspaceDir(workspace)
121
155
 
122
- // Start dashboard if enabled
123
- if (cfg.dashboard.enabled) {
124
- dashboardServer = createDashboardServer({
125
- port: cfg.dashboard.port,
126
- client,
127
- })
128
- dashboardServer.listen(cfg.dashboard.port, () => {
129
- log.info(
130
- `dashboard running at http://localhost:${cfg.dashboard.port}`,
131
- )
132
- })
133
- }
134
-
135
- track(EVENT_PLUGIN_STARTED, { project: cfg.project })
136
156
  log.info("connected — BM MCP stdio session running")
137
157
  },
138
158
  stop: async () => {
139
159
  log.info("stopping BM MCP session...")
140
- if (dashboardServer) {
141
- await new Promise<void>((resolve) =>
142
- dashboardServer?.close(() => resolve()),
143
- )
144
- dashboardServer = undefined
145
- log.info("dashboard stopped")
146
- }
147
160
  await client.stop()
148
161
  log.info("stopped")
149
162
  },
@@ -56,11 +56,6 @@
56
56
  "label": "Cloud Backend",
57
57
  "help": "Optional cloud backend config (url + api_key). If present, uses cloud instead of local BM.",
58
58
  "advanced": true
59
- },
60
- "dashboard": {
61
- "label": "Dashboard",
62
- "help": "Web dashboard for visualizing the knowledge graph (enabled, port)",
63
- "advanced": true
64
59
  }
65
60
  },
66
61
  "configSchema": {
@@ -81,13 +76,6 @@
81
76
  "url": { "type": "string" },
82
77
  "api_key": { "type": "string" }
83
78
  }
84
- },
85
- "dashboard": {
86
- "type": "object",
87
- "properties": {
88
- "enabled": { "type": "boolean" },
89
- "port": { "type": "number", "minimum": 1, "maximum": 65535 }
90
- }
91
79
  }
92
80
  },
93
81
  "required": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basicmemory/openclaw-basic-memory",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Basic Memory plugin for OpenClaw — local-first knowledge graph for agent memory",
6
6
  "license": "MIT",
@@ -9,7 +9,6 @@
9
9
  "url": "https://github.com/basicmachines-co/openclaw-basic-memory"
10
10
  },
11
11
  "files": [
12
- "analytics.ts",
13
12
  "index.ts",
14
13
  "bm-client.ts",
15
14
  "config.ts",
@@ -34,6 +33,7 @@
34
33
  "tools/write-note.ts",
35
34
  "types/openclaw.d.ts",
36
35
  "schema/task-schema.ts",
36
+ "schema/conversation-schema.ts",
37
37
  "skills/",
38
38
  "scripts/setup-bm.sh",
39
39
  "openclaw.plugin.json",
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Canonical Conversation schema note content, seeded into new projects on first startup.
3
+ * If the schema already exists (user may have customized it), seeding is skipped.
4
+ */
5
+ export const CONVERSATION_SCHEMA_CONTENT = `---
6
+ title: Conversation
7
+ type: schema
8
+ entity: Conversation
9
+ version: 1
10
+ schema:
11
+ date: "string, ISO date YYYY-MM-DD"
12
+ session_id?: "string, unique session identifier"
13
+ channel?: "string, where the conversation happened (webchat, telegram, discord, etc)"
14
+ participants?: "array, who was in the conversation"
15
+ topic?: "string, brief description of main topic"
16
+ summary?: "string, one-paragraph summary of key points"
17
+ key_decisions?: "array, decisions made during conversation"
18
+ action_items?: "array, things to do as a result"
19
+ settings:
20
+ validation: warn
21
+ ---
22
+
23
+ # Conversation
24
+
25
+ A record of a conversation session between the agent and user(s).
26
+
27
+ ## Observations
28
+ - [convention] Conversation files live in memory/conversations/ with format conversations-YYYY-MM-DD.md
29
+ - [convention] Messages are appended as the conversation progresses
30
+ - [convention] Summary and key_decisions populated at session end or by memory-reflect
31
+ - [convention] Skip routine greetings, heartbeat acks, and tool call details
32
+ - [convention] Focus on decisions, actions taken, and key context needed for continuity
33
+ `
@@ -60,8 +60,8 @@ async function searchMemoryFile(
60
60
 
61
61
  /**
62
62
  * Search for active tasks via BM knowledge graph.
63
- * Uses structured metadata filtering via search_by_metadata for precise querying.
64
- * Falls back to filesystem scan if metadata search fails.
63
+ * Uses search_notes with note_types and status filters for precise querying.
64
+ * Falls back to filesystem scan if search fails.
65
65
  */
66
66
  async function searchActiveTasks(
67
67
  query: string,
@@ -69,44 +69,29 @@ async function searchActiveTasks(
69
69
  workspaceDir: string,
70
70
  memoryDir: string,
71
71
  ): Promise<string> {
72
- // Try structured metadata search first
72
+ // Try structured search first
73
73
  try {
74
- const filters: Record<string, unknown> = { type: "task", status: "active" }
75
- const metaResults = await client.searchByMetadata(filters, 10)
76
-
77
- if (metaResults.results.length > 0) {
78
- // If the caller provided a query, filter results by keyword match
79
- const queryLower = query.toLowerCase()
80
- const terms = queryLower.split(/\s+/).filter((t) => t.length > 0)
81
-
82
- const filtered =
83
- terms.length > 0
84
- ? metaResults.results.filter((r) => {
85
- const text = `${r.title} ${r.content ?? ""}`.toLowerCase()
86
- return terms.some((term) => text.includes(term))
87
- })
88
- : metaResults.results
89
-
90
- if (filtered.length > 0) {
91
- const matches: string[] = []
92
- for (const r of filtered) {
93
- const score = r.score ? ` (${r.score.toFixed(2)})` : ""
94
- const preview =
95
- (r.content ?? "").length > 200
96
- ? `${(r.content ?? "").slice(0, 200)}…`
97
- : (r.content ?? "")
98
- matches.push(
99
- `- **${r.title}**${score} — ${r.file_path}\n > ${preview.replace(/\n/g, "\n > ")}`,
100
- )
101
- }
102
- return matches.join("\n\n")
74
+ const results = await client.search(query || undefined, 10, undefined, {
75
+ note_types: ["task"],
76
+ status: "active",
77
+ })
78
+
79
+ if (results.length > 0) {
80
+ const matches: string[] = []
81
+ for (const r of results) {
82
+ const score = r.score ? ` (${r.score.toFixed(2)})` : ""
83
+ const preview =
84
+ (r.content ?? "").length > 200
85
+ ? `${(r.content ?? "").slice(0, 200)}…`
86
+ : (r.content ?? "")
87
+ matches.push(
88
+ `- **${r.title}**${score} — ${r.file_path}\n > ${preview.replace(/\n/g, "\n > ")}`,
89
+ )
103
90
  }
91
+ return matches.join("\n\n")
104
92
  }
105
93
  } catch (err) {
106
- log.debug(
107
- "BM metadata task search failed, falling back to filesystem scan",
108
- err,
109
- )
94
+ log.debug("BM task search failed, falling back to filesystem scan", err)
110
95
  }
111
96
 
112
97
  // Fallback: filesystem scan for tasks not yet indexed