@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/README.md +140 -484
- package/bm-client.ts +27 -19
- package/config.ts +0 -24
- package/index.ts +21 -35
- package/openclaw.plugin.json +0 -12
- package/package.json +2 -3
- package/schema/conversation-schema.ts +33 -0
- package/dashboard/index.html +0 -182
- package/dashboard/server.ts +0 -146
package/bm-client.ts
CHANGED
|
@@ -196,7 +196,7 @@ function isRecoverableConnectionError(err: unknown): boolean {
|
|
|
196
196
|
)
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
function
|
|
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
|
-
|
|
862
|
-
|
|
863
|
-
return
|
|
864
|
-
}
|
|
860
|
+
log.debug(`append failed, will create: ${getErrorMessage(err)}`)
|
|
861
|
+
}
|
|
865
862
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
142
|
-
log.debug(
|
|
143
|
-
} catch
|
|
144
|
-
|
|
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
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
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
|
+
`
|
package/dashboard/index.html
DELETED
|
@@ -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>
|
package/dashboard/server.ts
DELETED
|
@@ -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
|
-
}
|