@basicmemory/openclaw-basic-memory 0.1.0-alpha.12 → 0.1.0-alpha.14

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
@@ -196,7 +196,7 @@ function isRecoverableConnectionError(err: unknown): boolean {
196
196
  )
197
197
  }
198
198
 
199
- function isNoteNotFoundError(err: unknown): boolean {
199
+ function _isNoteNotFoundError(err: unknown): boolean {
200
200
  const msg = getErrorMessage(err).toLowerCase()
201
201
  return (
202
202
  msg.includes("entity not found") ||
@@ -659,11 +659,12 @@ export class BmClient {
659
659
  identifier,
660
660
  operation,
661
661
  content,
662
- find_text: options.find_text,
663
- section: options.section,
664
- expected_replacements: options.expected_replacements,
665
662
  output_format: "json",
666
663
  }
664
+ if (options.find_text) args.find_text = options.find_text
665
+ if (options.section) args.section = options.section
666
+ if (options.expected_replacements != null)
667
+ args.expected_replacements = options.expected_replacements
667
668
  if (project) args.project = project
668
669
 
669
670
  const payload = await this.callTool("edit_note", args)
@@ -829,10 +830,6 @@ export class BmClient {
829
830
  return payload as unknown as MetadataSearchResult
830
831
  }
831
832
 
832
- private isNoteNotFoundError(err: unknown): boolean {
833
- return isNoteNotFoundError(err)
834
- }
835
-
836
833
  async indexConversation(
837
834
  userMessage: string,
838
835
  assistantResponse: string,
@@ -854,22 +851,33 @@ export class BmClient {
854
851
  "---",
855
852
  ].join("\n")
856
853
 
854
+ // Try append first — if it fails for ANY reason, fall through to create
857
855
  try {
858
856
  await this.editNote(title, "append", entry)
859
857
  log.debug(`appended conversation to: ${title}`)
858
+ return
860
859
  } catch (err) {
861
- if (!this.isNoteNotFoundError(err)) {
862
- log.error("conversation append failed", err)
863
- return
864
- }
860
+ log.debug(`append failed, will create: ${getErrorMessage(err)}`)
861
+ }
865
862
 
866
- const content = [`# Conversations ${dateStr}`, "", entry].join("\n")
867
- try {
868
- await this.writeNote(title, content, "conversations")
869
- log.debug(`created conversation note: ${title}`)
870
- } catch (createErr) {
871
- log.error("conversation index failed", createErr)
872
- }
863
+ // Create the note with frontmatter and first entry
864
+ const content = [
865
+ "---",
866
+ `title: Conversations ${dateStr}`,
867
+ "type: Conversation",
868
+ `date: "${dateStr}"`,
869
+ "---",
870
+ "",
871
+ `# Conversations ${dateStr}`,
872
+ "",
873
+ entry,
874
+ ].join("\n")
875
+
876
+ try {
877
+ await this.writeNote(title, content, "conversations")
878
+ log.debug(`created conversation note: ${title}`)
879
+ } catch (err) {
880
+ log.error("conversation index failed", err)
873
881
  }
874
882
  }
875
883
 
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/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "node:child_process"
2
- import type { Server } from "node:http"
2
+
3
3
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
4
4
  import { BmClient } from "./bm-client.ts"
5
5
  import { registerCli } from "./commands/cli.ts"
@@ -10,10 +10,11 @@ import {
10
10
  parseConfig,
11
11
  resolveProjectPath,
12
12
  } from "./config.ts"
13
- import { createDashboardServer } from "./dashboard/server.ts"
13
+
14
14
  import { buildCaptureHandler } from "./hooks/capture.ts"
15
15
  import { buildRecallHandler } from "./hooks/recall.ts"
16
16
  import { initLogger, log } from "./logger.ts"
17
+ import { CONVERSATION_SCHEMA_CONTENT } from "./schema/conversation-schema.ts"
17
18
  import { TASK_SCHEMA_CONTENT } from "./schema/task-schema.ts"
18
19
  import { registerContextTool } from "./tools/build-context.ts"
19
20
  import { registerDeleteTool } from "./tools/delete-note.ts"
@@ -83,8 +84,6 @@ export default {
83
84
  registerCli(api, client, cfg)
84
85
 
85
86
  // --- Service lifecycle ---
86
- let dashboardServer: Server | undefined
87
-
88
87
  api.registerService({
89
88
  id: "openclaw-basic-memory",
90
89
  start: async (ctx: { config?: unknown; workspaceDir?: string }) => {
@@ -105,7 +104,9 @@ export default {
105
104
  'uv tool install "basic-memory @ git+https://github.com/basicmachines-co/basic-memory.git@main" --force',
106
105
  { encoding: "utf-8", timeout: 120_000, stdio: "pipe" },
107
106
  )
108
- log.info(`basic-memory installed: ${result.trim().split("\n").pop()}`)
107
+ log.info(
108
+ `basic-memory installed: ${result.trim().split("\n").pop()}`,
109
+ )
109
110
  // Verify it worked
110
111
  try {
111
112
  execSync(`command -v ${bmBin}`, { stdio: "ignore" })
@@ -115,7 +116,7 @@ export default {
115
116
  "bm installed but not found on PATH. You may need to add uv's bin directory to your PATH (typically ~/.local/bin).",
116
117
  )
117
118
  }
118
- } catch (uvErr) {
119
+ } catch (_uvErr) {
119
120
  log.error(
120
121
  "Cannot auto-install basic-memory: uv not found. " +
121
122
  "Install uv first (brew install uv, or curl -LsSf https://astral.sh/uv/install.sh | sh), " +
@@ -132,45 +133,30 @@ export default {
132
133
  await client.ensureProject(projectPath)
133
134
  log.debug(`project "${cfg.project}" at ${projectPath}`)
134
135
 
135
- // Seed Task schema if not already present
136
- try {
137
- await client.readNote("schema/Task")
138
- log.debug("Task schema already exists, skipping seed")
139
- } 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) {
140
141
  try {
141
- await client.writeNote("Task", TASK_SCHEMA_CONTENT, "schema")
142
- log.debug("seeded Task schema note")
143
- } catch (err) {
144
- 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
+ }
145
151
  }
146
152
  }
147
153
 
148
154
  setWorkspaceDir(workspace)
149
155
 
150
- // Start dashboard if enabled
151
- if (cfg.dashboard.enabled) {
152
- dashboardServer = createDashboardServer({
153
- port: cfg.dashboard.port,
154
- client,
155
- })
156
- dashboardServer.listen(cfg.dashboard.port, () => {
157
- log.info(
158
- `dashboard running at http://localhost:${cfg.dashboard.port}`,
159
- )
160
- })
161
- }
162
-
163
156
  log.info("connected — BM MCP stdio session running")
164
157
  },
165
158
  stop: async () => {
166
159
  log.info("stopping BM MCP session...")
167
- if (dashboardServer) {
168
- await new Promise<void>((resolve) =>
169
- dashboardServer?.close(() => resolve()),
170
- )
171
- dashboardServer = undefined
172
- log.info("dashboard stopped")
173
- }
174
160
  await client.stop()
175
161
  log.info("stopped")
176
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.12",
3
+ "version": "0.1.0-alpha.14",
4
4
  "type": "module",
5
5
  "description": "Basic Memory plugin for OpenClaw — local-first knowledge graph for agent memory",
6
6
  "license": "MIT",
@@ -33,8 +33,7 @@
33
33
  "tools/write-note.ts",
34
34
  "types/openclaw.d.ts",
35
35
  "schema/task-schema.ts",
36
- "dashboard/server.ts",
37
- "dashboard/index.html",
36
+ "schema/conversation-schema.ts",
38
37
  "skills/",
39
38
  "scripts/setup-bm.sh",
40
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
+ `
@@ -1,182 +0,0 @@
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>
@@ -1,146 +0,0 @@
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
- }