@basicmemory/openclaw-basic-memory 0.1.0-alpha.1 → 0.1.0-alpha.11

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/README.md CHANGED
@@ -36,9 +36,6 @@ For a practical runbook, see [Memory + Task Flow](./MEMORY_TASK_FLOW.md).
36
36
  # Install the plugin (automatically installs the bm CLI via uv)
37
37
  openclaw plugins install @basicmemory/openclaw-basic-memory
38
38
 
39
- # Enable and assign to the memory slot
40
- openclaw plugins enable basic-memory --slot memory
41
-
42
39
  # Restart the gateway
43
40
  openclaw gateway restart
44
41
  ```
@@ -46,7 +43,7 @@ openclaw gateway restart
46
43
  Verify:
47
44
  ```bash
48
45
  openclaw plugins list
49
- openclaw plugins info basic-memory
46
+ openclaw plugins info openclaw-basic-memory
50
47
  ```
51
48
 
52
49
  If `uv` is not installed, the `bm` CLI setup is skipped gracefully during install. Install `uv` first, then re-run the postinstall script:
@@ -55,6 +52,17 @@ If `uv` is not installed, the `bm` CLI setup is skipped gracefully during instal
55
52
  bash ~/.openclaw/extensions/openclaw-basic-memory/scripts/setup-bm.sh
56
53
  ```
57
54
 
55
+ ### Basic Memory Cloud
56
+
57
+ Everything works locally — cloud adds cross-device, team, and production capabilities:
58
+
59
+ - **Your agent's memory travels with you** — same knowledge graph on laptop, desktop, and hosted environments
60
+ - **Team knowledge sharing** — org workspaces let multiple agents and team members build on a shared knowledge base
61
+ - **Durable memory for production agents** — persistent memory that survives CI teardowns and container restarts
62
+ - **Multi-agent coordination** — multiple agents can read and write to the same graph
63
+
64
+ Cloud extends local-first — still plain markdown, still yours. Start with a [7-day free trial](https://basicmemory.com) and use code `BMCLAW` for 20% off for 3 months. See [BASIC_MEMORY.md](./BASIC_MEMORY.md) for cloud setup.
65
+
58
66
  ### Development (local directory)
59
67
 
60
68
  For plugin development, clone and link locally:
@@ -64,7 +72,7 @@ git clone https://github.com/basicmachines-co/openclaw-basic-memory.git
64
72
  cd openclaw-basic-memory
65
73
  bun install
66
74
  openclaw plugins install -l "$PWD"
67
- openclaw plugins enable basic-memory --slot memory
75
+ openclaw plugins enable openclaw-basic-memory --slot memory
68
76
  openclaw gateway restart
69
77
  ```
70
78
 
@@ -77,12 +85,12 @@ Or load directly from a path in your OpenClaw config:
77
85
  paths: ["~/dev/openclaw-basic-memory"]
78
86
  },
79
87
  entries: {
80
- "basic-memory": {
88
+ "openclaw-basic-memory": {
81
89
  enabled: true
82
90
  }
83
91
  },
84
92
  slots: {
85
- memory: "basic-memory"
93
+ memory: "openclaw-basic-memory"
86
94
  }
87
95
  }
88
96
  }
@@ -120,23 +128,23 @@ This installs to the same `skills/` directory the plugin reads from, so updated
120
128
  ### Minimal (zero-config)
121
129
  ```json5
122
130
  {
123
- "basic-memory": {
131
+ "openclaw-basic-memory": {
124
132
  enabled: true
125
133
  }
126
134
  }
127
135
  ```
128
136
 
129
- This uses sensible defaults: auto-generated project name, maps Basic Memory to your workspace `memory/` directory, and captures conversations.
137
+ This uses sensible defaults: auto-generated project name, maps Basic Memory to your workspace root, sets it as the default BM project, and captures conversations.
130
138
 
131
139
  ### Full configuration
132
140
  ```json5
133
141
  {
134
- "basic-memory": {
142
+ "openclaw-basic-memory": {
135
143
  enabled: true,
136
144
  config: {
137
145
  project: "my-agent", // BM project name (default: "openclaw-{hostname}")
138
146
  bmPath: "bm", // Path to BM CLI binary
139
- projectPath: "~/.openclaw/workspace/memory/", // Optional override; supports absolute, ~/..., or workspace-relative paths
147
+ projectPath: ".", // Defaults to workspace root; supports absolute, ~/..., or workspace-relative paths
140
148
  memoryDir: "memory/", // Relative memory dir for task scanning
141
149
  memoryFile: "MEMORY.md", // Working memory file for grep search
142
150
  autoCapture: true, // Index conversations automatically
@@ -155,7 +163,7 @@ This uses sensible defaults: auto-generated project name, maps Basic Memory to y
155
163
  |--------|------|---------|-------------|
156
164
  | `project` | string | `"openclaw-{hostname}"` | Basic Memory project name |
157
165
  | `bmPath` | string | `"bm"` | Path to Basic Memory CLI binary |
158
- | `projectPath` | string | `"memory/"` | Directory for BM project data (resolved from workspace unless absolute) |
166
+ | `projectPath` | string | `"."` | Directory for BM project data (defaults to workspace root; resolved from workspace unless absolute) |
159
167
  | `memoryDir` | string | `"memory/"` | Relative path for task scanning |
160
168
  | `memoryFile` | string | `"MEMORY.md"` | Working memory file (grep-searched) |
161
169
  | `autoCapture` | boolean | `true` | Auto-index agent conversations |
@@ -168,7 +176,7 @@ Snake_case aliases (`memory_dir`, `memory_file`, `auto_recall`, `recall_prompt`,
168
176
 
169
177
  Cloud sync is optional — see [BASIC_MEMORY.md](./BASIC_MEMORY.md) for cloud configuration.
170
178
 
171
- On startup, the plugin ensures the configured BM project exists at `projectPath` via MCP `create_memory_project` in idempotent mode.
179
+ On startup, the plugin ensures the configured BM project exists at `projectPath` via MCP `create_memory_project` in idempotent mode, and sets it as the default Basic Memory project.
172
180
 
173
181
  ## How It Works
174
182
 
@@ -287,17 +295,6 @@ After each agent turn (when `autoCapture: true`), the plugin:
287
295
  2. Appends them as timestamped entries to a daily conversation note (`conversations-YYYY-MM-DD`)
288
296
  3. Skips very short exchanges (< `captureMinChars` chars each, default 10)
289
297
 
290
- ### Basic Memory Cloud
291
-
292
- Everything works locally — cloud adds cross-device, team, and production capabilities:
293
-
294
- - **Your agent's memory travels with you** — same knowledge graph on laptop, desktop, and hosted environments
295
- - **Team knowledge sharing** — org workspaces let multiple agents and team members build on a shared knowledge base
296
- - **Durable memory for production agents** — persistent memory that survives CI teardowns and container restarts
297
- - **Multi-agent coordination** — multiple agents can read and write to the same graph
298
-
299
- Cloud extends local-first — still plain markdown, still yours. Start with a [7-day free trial](https://basicmemory.com) and use code `BMCLAW` for 20% off for 3 months. See [BASIC_MEMORY.md](./BASIC_MEMORY.md) for setup, or visit [basicmemory.com](https://basicmemory.com) for more info.
300
-
301
298
  ## Agent Tools
302
299
 
303
300
  All content tools accept an optional `project` parameter to operate on a different project than the default (cross-project operations).
@@ -453,6 +450,17 @@ rm -rf /tmp/jiti/ "$TMPDIR/jiti/"
453
450
  openclaw gateway stop && openclaw gateway start
454
451
  ```
455
452
 
453
+ ### Disabling semantic search
454
+ If you want to run without vector/embedding dependencies (faster startup, less memory), set the environment variable before launching:
455
+ ```bash
456
+ BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=false
457
+ ```
458
+ Or in `~/.basic-memory/config.json`:
459
+ ```json
460
+ { "semantic_search_enabled": false }
461
+ ```
462
+ Search will fall back to full-text search only.
463
+
456
464
  ### Search returns no results
457
465
  1. Check that the MCP session is connected (look for `connected to BM MCP stdio` in logs)
458
466
  2. Verify files exist in the project directory
@@ -499,7 +507,7 @@ bun run test:int # Real BM MCP integration tests
499
507
 
500
508
  ## Publish to npm
501
509
 
502
- This package is published as `@openclaw/basic-memory`.
510
+ This package is published as `@basicmemory/openclaw-basic-memory`.
503
511
 
504
512
  ```bash
505
513
  # 1) Verify release readiness (types + tests + npm pack dry run)
@@ -531,9 +539,7 @@ just release patch # or: minor, major, 0.2.0, etc.
531
539
  4. publish to npm
532
540
  5. create a GitHub release
533
541
 
534
- Repository secret required:
535
-
536
- - `NPM_TOKEN` (npm publish token with package publish permissions)
542
+ Publishing uses npm OIDC trusted publishing — no secrets required. The trusted publisher is configured on npmjs.com to accept provenance from this repo's `release.yml` workflow.
537
543
 
538
544
  ### Project Structure
539
545
  ```
@@ -564,6 +570,16 @@ openclaw-basic-memory/
564
570
  └── recall.ts # Auto-recall (active tasks + recent activity)
565
571
  ```
566
572
 
573
+ ## Telemetry
574
+
575
+ This plugin itself does not collect any telemetry. However, the **Basic Memory CLI** (`bm`) that the plugin spawns may send anonymous usage analytics. See the [Basic Memory documentation](https://github.com/basicmachines-co/basic-memory) for details.
576
+
577
+ To opt out of Basic Memory CLI telemetry:
578
+
579
+ ```bash
580
+ export BASIC_MEMORY_NO_PROMOS=1
581
+ ```
582
+
567
583
  ## License
568
584
 
569
585
  MIT — see LICENSE file.
package/config.ts CHANGED
@@ -6,6 +6,11 @@ export type CloudConfig = {
6
6
  api_key: string
7
7
  }
8
8
 
9
+ export type DashboardConfig = {
10
+ enabled: boolean
11
+ port: number
12
+ }
13
+
9
14
  export type BasicMemoryConfig = {
10
15
  project: string
11
16
  bmPath: string
@@ -18,6 +23,7 @@ export type BasicMemoryConfig = {
18
23
  recallPrompt: string
19
24
  debug: boolean
20
25
  cloud?: CloudConfig
26
+ dashboard: DashboardConfig
21
27
  }
22
28
 
23
29
  const ALLOWED_KEYS = [
@@ -37,6 +43,7 @@ const ALLOWED_KEYS = [
37
43
  "recall_prompt",
38
44
  "debug",
39
45
  "cloud",
46
+ "dashboard",
40
47
  ]
41
48
 
42
49
  function assertAllowedKeys(
@@ -106,6 +113,22 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
106
113
  }
107
114
  }
108
115
 
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
+
109
132
  return {
110
133
  project:
111
134
  typeof cfg.project === "string" && cfg.project.length > 0
@@ -114,7 +137,7 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
114
137
  projectPath:
115
138
  typeof cfg.projectPath === "string" && cfg.projectPath.length > 0
116
139
  ? cfg.projectPath
117
- : memoryDir,
140
+ : ".",
118
141
  bmPath:
119
142
  typeof cfg.bmPath === "string" && cfg.bmPath.length > 0
120
143
  ? cfg.bmPath
@@ -144,6 +167,7 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
144
167
  : "Check for active tasks and recent activity. Summarize anything relevant to the current session.",
145
168
  debug: typeof cfg.debug === "boolean" ? cfg.debug : false,
146
169
  cloud,
170
+ dashboard,
147
171
  }
148
172
  }
149
173
 
@@ -0,0 +1,182 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Memory Dashboard</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { background: #0a0a0a; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
10
+ code, .mono { font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; }
11
+
12
+ /* Stats Bar */
13
+ .stats-bar {
14
+ display: flex; gap: 24px; padding: 16px 24px;
15
+ background: #111; border-bottom: 1px solid #222;
16
+ }
17
+ .stat { text-align: center; }
18
+ .stat-value { font-size: 28px; font-weight: 700; }
19
+ .stat-label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
20
+ .stat-active .stat-value { color: #3b82f6; }
21
+ .stat-done .stat-value { color: #22c55e; }
22
+ .stat-explore .stat-value { color: #a855f7; }
23
+ .stat-total .stat-value { color: #e0e0e0; }
24
+
25
+ /* Layout */
26
+ .main { display: flex; height: calc(100vh - 70px); }
27
+ .kanban-area { flex: 1; overflow-x: auto; padding: 16px; }
28
+ .sidebar { width: 320px; border-left: 1px solid #222; padding: 16px; overflow-y: auto; flex-shrink: 0; }
29
+
30
+ /* Kanban */
31
+ .kanban { display: flex; gap: 12px; height: 100%; }
32
+ .column { flex: 1; min-width: 220px; background: #111; border-radius: 8px; display: flex; flex-direction: column; }
33
+ .column-header { padding: 12px; font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid; }
34
+ .col-active .column-header { border-color: #3b82f6; color: #3b82f6; }
35
+ .col-blocked .column-header { border-color: #ef4444; color: #ef4444; }
36
+ .col-done .column-header { border-color: #22c55e; color: #22c55e; }
37
+ .col-abandoned .column-header { border-color: #6b7280; color: #6b7280; }
38
+ .column-body { padding: 8px; overflow-y: auto; flex: 1; }
39
+
40
+ /* Cards */
41
+ .card {
42
+ background: #1a1a1a; border: 1px solid #333; border-radius: 6px;
43
+ padding: 10px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.15s;
44
+ }
45
+ .card:hover { border-color: #555; }
46
+ .card-title { font-size: 13px; font-weight: 600; margin-bottom: 6px; word-break: break-word; }
47
+ .card-meta { font-size: 11px; color: #888; font-family: 'SF Mono', monospace; }
48
+ .card-meta span { margin-right: 10px; }
49
+ .card-detail { display: none; margin-top: 8px; font-size: 12px; color: #aaa; border-top: 1px solid #333; padding-top: 8px; }
50
+ .card.expanded .card-detail { display: block; }
51
+
52
+ /* Sidebar */
53
+ .sidebar h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; margin-bottom: 12px; }
54
+ .activity-item {
55
+ padding: 8px 0; border-bottom: 1px solid #1a1a1a; font-size: 13px;
56
+ }
57
+ .activity-title { font-weight: 500; }
58
+ .activity-time { font-size: 11px; color: #666; font-family: 'SF Mono', monospace; }
59
+
60
+ /* Refresh indicator */
61
+ .refresh { position: fixed; top: 8px; right: 8px; font-size: 11px; color: #444; }
62
+
63
+ @media (max-width: 900px) {
64
+ .main { flex-direction: column; }
65
+ .sidebar { width: 100%; border-left: none; border-top: 1px solid #222; max-height: 300px; }
66
+ .kanban { flex-wrap: wrap; }
67
+ .column { min-width: 180px; }
68
+ }
69
+ </style>
70
+ </head>
71
+ <body>
72
+
73
+ <div class="stats-bar" id="stats-bar">
74
+ <div class="stat stat-total"><div class="stat-value" id="stat-total">-</div><div class="stat-label">Total Notes</div></div>
75
+ <div class="stat stat-active"><div class="stat-value" id="stat-active">-</div><div class="stat-label">Active Tasks</div></div>
76
+ <div class="stat stat-done"><div class="stat-value" id="stat-done">-</div><div class="stat-label">Completed</div></div>
77
+ <div class="stat stat-explore"><div class="stat-value" id="stat-explore">-</div><div class="stat-label">Explorations</div></div>
78
+ </div>
79
+
80
+ <div class="main">
81
+ <div class="kanban-area">
82
+ <div class="kanban">
83
+ <div class="column col-active"><div class="column-header">Active <span class="col-count"></span></div><div class="column-body" id="col-active"></div></div>
84
+ <div class="column col-blocked"><div class="column-header">Blocked <span class="col-count"></span></div><div class="column-body" id="col-blocked"></div></div>
85
+ <div class="column col-done"><div class="column-header">Done <span class="col-count"></span></div><div class="column-body" id="col-done"></div></div>
86
+ <div class="column col-abandoned"><div class="column-header">Abandoned <span class="col-count"></span></div><div class="column-body" id="col-abandoned"></div></div>
87
+ </div>
88
+ </div>
89
+ <div class="sidebar">
90
+ <h2>Activity Feed</h2>
91
+ <div id="activity-feed"></div>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="refresh" id="refresh">⏳</div>
96
+
97
+ <script>
98
+ const API = '';
99
+
100
+ function makeCard(task) {
101
+ const fm = task.frontmatter || {};
102
+ const status = (fm.status || 'active').toLowerCase();
103
+ const step = fm.current_step || '?';
104
+ const total = fm.total_steps || '?';
105
+ const assigned = fm.assigned_to || '';
106
+ const div = document.createElement('div');
107
+ div.className = 'card';
108
+ div.innerHTML = `
109
+ <div class="card-title">${esc(task.title)}</div>
110
+ <div class="card-meta">
111
+ ${assigned ? `<span>👤 ${esc(assigned)}</span>` : ''}
112
+ <span>📊 ${esc(String(step))}/${esc(String(total))}</span>
113
+ </div>
114
+ <div class="card-detail mono">${esc(task.content || '').slice(0, 300)}</div>
115
+ `;
116
+ div.onclick = () => div.classList.toggle('expanded');
117
+ return { el: div, status };
118
+ }
119
+
120
+ function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
121
+
122
+ async function fetchJson(url) {
123
+ try { const r = await fetch(url); return r.ok ? r.json() : null; } catch { return null; }
124
+ }
125
+
126
+ async function refresh() {
127
+ document.getElementById('refresh').textContent = '🔄';
128
+
129
+ const [tasks, activity, stats] = await Promise.all([
130
+ fetchJson(`${API}/api/tasks`),
131
+ fetchJson(`${API}/api/activity`),
132
+ fetchJson(`${API}/api/stats`),
133
+ ]);
134
+
135
+ // Stats
136
+ if (stats) {
137
+ document.getElementById('stat-total').textContent = stats.totalNotes;
138
+ document.getElementById('stat-active').textContent = stats.activeTasks;
139
+ document.getElementById('stat-done').textContent = stats.completedTasks;
140
+ document.getElementById('stat-explore').textContent = stats.explorations;
141
+ }
142
+
143
+ // Kanban
144
+ const cols = { active: [], blocked: [], done: [], abandoned: [] };
145
+ if (tasks) {
146
+ for (const t of tasks) {
147
+ const { el, status } = makeCard(t);
148
+ const bucket = cols[status] ? status : 'active';
149
+ cols[bucket].push(el);
150
+ }
151
+ }
152
+ for (const [key, items] of Object.entries(cols)) {
153
+ const container = document.getElementById(`col-${key}`);
154
+ container.innerHTML = '';
155
+ for (const el of items) container.appendChild(el);
156
+ container.parentElement.querySelector('.col-count').textContent = `(${items.length})`;
157
+ }
158
+
159
+ // Activity
160
+ const feed = document.getElementById('activity-feed');
161
+ feed.innerHTML = '';
162
+ if (activity?.length) {
163
+ for (const a of activity.slice(0, 30)) {
164
+ const div = document.createElement('div');
165
+ div.className = 'activity-item';
166
+ const time = a.created_at ? new Date(a.created_at).toLocaleTimeString() : '';
167
+ div.innerHTML = `<div class="activity-title">${esc(a.title)}</div><div class="activity-time">${time}</div>`;
168
+ feed.appendChild(div);
169
+ }
170
+ } else {
171
+ feed.innerHTML = '<div style="color:#555">No recent activity</div>';
172
+ }
173
+
174
+ document.getElementById('refresh').textContent = '✓';
175
+ setTimeout(() => { document.getElementById('refresh').textContent = ''; }, 2000);
176
+ }
177
+
178
+ refresh();
179
+ setInterval(refresh, 30000);
180
+ </script>
181
+ </body>
182
+ </html>
@@ -0,0 +1,146 @@
1
+ import { readFileSync } from "node:fs"
2
+ import {
3
+ createServer,
4
+ type IncomingMessage,
5
+ type Server,
6
+ type ServerResponse,
7
+ } from "node:http"
8
+ import { join } from "node:path"
9
+ import type { BmClient, SearchResult } from "../bm-client.ts"
10
+
11
+ export interface DashboardServerOptions {
12
+ port: number
13
+ client: BmClient
14
+ }
15
+
16
+ export function createDashboardServer(options: DashboardServerOptions): Server {
17
+ const { client, port } = options
18
+
19
+ const indexHtml = readFileSync(
20
+ join(import.meta.dirname ?? __dirname, "index.html"),
21
+ "utf-8",
22
+ )
23
+
24
+ const server = createServer(
25
+ async (req: IncomingMessage, res: ServerResponse) => {
26
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`)
27
+ const path = url.pathname
28
+
29
+ try {
30
+ if (path === "/" && req.method === "GET") {
31
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
32
+ res.end(indexHtml)
33
+ return
34
+ }
35
+
36
+ if (path === "/api/tasks" && req.method === "GET") {
37
+ const results = await client.search("type:Task", 50, undefined, {
38
+ filters: { type: "Task" },
39
+ })
40
+ const tasks = await enrichWithFrontmatter(client, results)
41
+ json(res, tasks)
42
+ return
43
+ }
44
+
45
+ if (path === "/api/activity" && req.method === "GET") {
46
+ const results = await client.recentActivity("24h")
47
+ json(res, results)
48
+ return
49
+ }
50
+
51
+ if (path === "/api/explorations" && req.method === "GET") {
52
+ const results = await client.search(
53
+ "type:Exploration",
54
+ 50,
55
+ undefined,
56
+ {
57
+ filters: { type: "Exploration" },
58
+ },
59
+ )
60
+ json(res, results)
61
+ return
62
+ }
63
+
64
+ if (path === "/api/notes/daily" && req.method === "GET") {
65
+ const today = new Date().toISOString().split("T")[0]
66
+ const results = await client.search(today, 5)
67
+ const daily = results.filter((r) => r.title.includes(today))
68
+ json(res, daily)
69
+ return
70
+ }
71
+
72
+ if (path === "/api/stats" && req.method === "GET") {
73
+ const [allNotes, tasks, explorations] = await Promise.all([
74
+ client.recentActivity("720h").catch(() => []),
75
+ client
76
+ .search("type:Task", 100, undefined, {
77
+ filters: { type: "Task" },
78
+ })
79
+ .catch(() => []),
80
+ client
81
+ .search("type:Exploration", 100, undefined, {
82
+ filters: { type: "Exploration" },
83
+ })
84
+ .catch(() => []),
85
+ ])
86
+
87
+ const tasksWithFm = await enrichWithFrontmatter(client, tasks)
88
+ const active = tasksWithFm.filter(
89
+ (t) => t.frontmatter?.status === "active",
90
+ ).length
91
+ const completed = tasksWithFm.filter(
92
+ (t) =>
93
+ t.frontmatter?.status === "done" ||
94
+ t.frontmatter?.status === "completed",
95
+ ).length
96
+
97
+ json(res, {
98
+ totalNotes: allNotes.length,
99
+ activeTasks: active,
100
+ completedTasks: completed,
101
+ explorations: explorations.length,
102
+ })
103
+ return
104
+ }
105
+
106
+ res.writeHead(404, { "Content-Type": "application/json" })
107
+ res.end(JSON.stringify({ error: "not found" }))
108
+ } catch (err: unknown) {
109
+ const message = err instanceof Error ? err.message : String(err)
110
+ res.writeHead(500, { "Content-Type": "application/json" })
111
+ res.end(JSON.stringify({ error: message }))
112
+ }
113
+ },
114
+ )
115
+
116
+ return server
117
+ }
118
+
119
+ function json(res: ServerResponse, data: unknown): void {
120
+ res.writeHead(200, {
121
+ "Content-Type": "application/json",
122
+ "Access-Control-Allow-Origin": "*",
123
+ })
124
+ res.end(JSON.stringify(data))
125
+ }
126
+
127
+ async function enrichWithFrontmatter(
128
+ client: BmClient,
129
+ results: SearchResult[],
130
+ ): Promise<
131
+ Array<SearchResult & { frontmatter?: Record<string, unknown> | null }>
132
+ > {
133
+ const enriched = await Promise.all(
134
+ results.map(async (r) => {
135
+ try {
136
+ const note = await client.readNote(r.permalink, {
137
+ includeFrontmatter: true,
138
+ })
139
+ return { ...r, frontmatter: note.frontmatter ?? null }
140
+ } catch {
141
+ return { ...r, frontmatter: null }
142
+ }
143
+ }),
144
+ )
145
+ return enriched
146
+ }
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Server } from "node:http"
1
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
3
  import { BmClient } from "./bm-client.ts"
3
4
  import { registerCli } from "./commands/cli.ts"
@@ -8,6 +9,7 @@ import {
8
9
  parseConfig,
9
10
  resolveProjectPath,
10
11
  } from "./config.ts"
12
+ import { createDashboardServer } from "./dashboard/server.ts"
11
13
  import { buildCaptureHandler } from "./hooks/capture.ts"
12
14
  import { buildRecallHandler } from "./hooks/recall.ts"
13
15
  import { initLogger, log } from "./logger.ts"
@@ -30,7 +32,7 @@ import { registerSearchTool } from "./tools/search-notes.ts"
30
32
  import { registerWriteTool } from "./tools/write-note.ts"
31
33
 
32
34
  export default {
33
- id: "basic-memory",
35
+ id: "openclaw-basic-memory",
34
36
  name: "Basic Memory",
35
37
  description:
36
38
  "Local-first knowledge graph for OpenClaw — persistent memory with graph search and composited memory_search",
@@ -80,8 +82,10 @@ export default {
80
82
  registerCli(api, client, cfg)
81
83
 
82
84
  // --- Service lifecycle ---
85
+ let dashboardServer: Server | undefined
86
+
83
87
  api.registerService({
84
- id: "basic-memory",
88
+ id: "openclaw-basic-memory",
85
89
  start: async (ctx: { config?: unknown; workspaceDir?: string }) => {
86
90
  log.info("starting...")
87
91
 
@@ -108,10 +112,30 @@ export default {
108
112
 
109
113
  setWorkspaceDir(workspace)
110
114
 
115
+ // Start dashboard if enabled
116
+ if (cfg.dashboard.enabled) {
117
+ dashboardServer = createDashboardServer({
118
+ port: cfg.dashboard.port,
119
+ client,
120
+ })
121
+ dashboardServer.listen(cfg.dashboard.port, () => {
122
+ log.info(
123
+ `dashboard running at http://localhost:${cfg.dashboard.port}`,
124
+ )
125
+ })
126
+ }
127
+
111
128
  log.info("connected — BM MCP stdio session running")
112
129
  },
113
130
  stop: async () => {
114
131
  log.info("stopping BM MCP session...")
132
+ if (dashboardServer) {
133
+ await new Promise<void>((resolve) =>
134
+ dashboardServer?.close(() => resolve()),
135
+ )
136
+ dashboardServer = undefined
137
+ log.info("dashboard stopped")
138
+ }
115
139
  await client.stop()
116
140
  log.info("stopped")
117
141
  },
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "basic-memory",
2
+ "id": "openclaw-basic-memory",
3
3
  "kind": "memory",
4
4
  "skills": [
5
5
  "skills/memory-tasks",
@@ -48,14 +48,19 @@
48
48
  },
49
49
  "projectPath": {
50
50
  "label": "Project Path",
51
- "placeholder": "~/.openclaw/workspace/memory/",
52
- "help": "Filesystem path for Basic Memory project data (relative paths resolve from workspace); created automatically if missing",
51
+ "placeholder": ".",
52
+ "help": "Filesystem path for Basic Memory project data (defaults to workspace root; relative paths resolve from workspace)",
53
53
  "advanced": true
54
54
  },
55
55
  "cloud": {
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
59
64
  }
60
65
  },
61
66
  "configSchema": {
@@ -76,6 +81,13 @@
76
81
  "url": { "type": "string" },
77
82
  "api_key": { "type": "string" }
78
83
  }
84
+ },
85
+ "dashboard": {
86
+ "type": "object",
87
+ "properties": {
88
+ "enabled": { "type": "boolean" },
89
+ "port": { "type": "number", "minimum": 1, "maximum": 65535 }
90
+ }
79
91
  }
80
92
  },
81
93
  "required": []
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@basicmemory/openclaw-basic-memory",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.11",
4
4
  "type": "module",
5
5
  "description": "Basic Memory plugin for OpenClaw — local-first knowledge graph for agent memory",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/basicmachines-co/openclaw-basic-memory"
10
+ },
7
11
  "files": [
8
12
  "index.ts",
9
13
  "bm-client.ts",
@@ -29,6 +33,8 @@
29
33
  "tools/write-note.ts",
30
34
  "types/openclaw.d.ts",
31
35
  "schema/task-schema.ts",
36
+ "dashboard/server.ts",
37
+ "dashboard/index.html",
32
38
  "skills/",
33
39
  "scripts/setup-bm.sh",
34
40
  "openclaw.plugin.json",
@@ -15,11 +15,10 @@ if ! command -v uv >/dev/null 2>&1; then
15
15
  exit 0
16
16
  fi
17
17
 
18
- # ── install basic-memory[semantic] ────────────────────────────────
18
+ # ── install basic-memory ────────────────────────────────
19
19
  echo "Installing basic-memory from ${BM_REPO}@${BM_REF} ..."
20
20
  uv tool install \
21
- "basic-memory[semantic] @ git+${BM_REPO}@${BM_REF}" \
22
- --with 'onnxruntime<1.24; platform_system == "Darwin" and platform_machine == "x86_64"' \
21
+ "basic-memory @ git+${BM_REPO}@${BM_REF}" \
23
22
  --force
24
23
 
25
24
  # ── verify ────────────────────────────────────────────────────────