@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 +44 -28
- package/config.ts +25 -1
- package/dashboard/index.html +182 -0
- package/dashboard/server.ts +146 -0
- package/index.ts +26 -2
- package/openclaw.plugin.json +15 -3
- package/package.json +7 -1
- package/scripts/setup-bm.sh +2 -3
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
|
|
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: "
|
|
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 | `"
|
|
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
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -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": "
|
|
52
|
-
"help": "Filesystem path for Basic Memory project data (relative paths resolve from workspace)
|
|
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.
|
|
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",
|
package/scripts/setup-bm.sh
CHANGED
|
@@ -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
|
|
18
|
+
# ── install basic-memory ────────────────────────────────
|
|
19
19
|
echo "Installing basic-memory from ${BM_REPO}@${BM_REF} ..."
|
|
20
20
|
uv tool install \
|
|
21
|
-
"basic-memory
|
|
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 ────────────────────────────────────────────────────────
|