@basicmemory/openclaw-basic-memory 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +576 -0
- package/bm-client.ts +879 -0
- package/commands/cli.ts +176 -0
- package/commands/skills.ts +52 -0
- package/commands/slash.ts +73 -0
- package/config.ts +152 -0
- package/hooks/capture.ts +95 -0
- package/hooks/recall.ts +66 -0
- package/index.ts +120 -0
- package/logger.ts +47 -0
- package/openclaw.plugin.json +83 -0
- package/package.json +68 -0
- package/schema/task-schema.ts +34 -0
- package/scripts/setup-bm.sh +32 -0
- package/skills/memory-defrag/SKILL.md +87 -0
- package/skills/memory-metadata-search/SKILL.md +208 -0
- package/skills/memory-notes/SKILL.md +250 -0
- package/skills/memory-reflect/SKILL.md +63 -0
- package/skills/memory-schema/SKILL.md +237 -0
- package/skills/memory-tasks/SKILL.md +162 -0
- package/tools/build-context.ts +123 -0
- package/tools/delete-note.ts +67 -0
- package/tools/edit-note.ts +118 -0
- package/tools/list-memory-projects.ts +94 -0
- package/tools/list-workspaces.ts +75 -0
- package/tools/memory-provider.ts +327 -0
- package/tools/move-note.ts +74 -0
- package/tools/read-note.ts +79 -0
- package/tools/schema-diff.ts +104 -0
- package/tools/schema-infer.ts +103 -0
- package/tools/schema-validate.ts +100 -0
- package/tools/search-notes.ts +130 -0
- package/tools/write-note.ts +78 -0
- package/types/openclaw.d.ts +24 -0
package/bm-client.ts
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises"
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client"
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
4
|
+
import { log } from "./logger.ts"
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RETRY_DELAYS_MS = [500, 1000, 2000]
|
|
7
|
+
|
|
8
|
+
const REQUIRED_TOOLS = [
|
|
9
|
+
"search_notes",
|
|
10
|
+
"search_by_metadata",
|
|
11
|
+
"read_note",
|
|
12
|
+
"write_note",
|
|
13
|
+
"edit_note",
|
|
14
|
+
"build_context",
|
|
15
|
+
"recent_activity",
|
|
16
|
+
"list_memory_projects",
|
|
17
|
+
"list_workspaces",
|
|
18
|
+
"create_memory_project",
|
|
19
|
+
"delete_note",
|
|
20
|
+
"move_note",
|
|
21
|
+
"schema_validate",
|
|
22
|
+
"schema_infer",
|
|
23
|
+
"schema_diff",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export interface SearchResult {
|
|
27
|
+
title: string
|
|
28
|
+
permalink: string
|
|
29
|
+
content: string
|
|
30
|
+
score?: number
|
|
31
|
+
file_path: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface NoteResult {
|
|
35
|
+
title: string
|
|
36
|
+
permalink: string
|
|
37
|
+
content: string
|
|
38
|
+
file_path: string
|
|
39
|
+
frontmatter?: Record<string, unknown> | null
|
|
40
|
+
checksum?: string | null
|
|
41
|
+
action?: "created" | "updated"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EditNoteResult {
|
|
45
|
+
title: string
|
|
46
|
+
permalink: string
|
|
47
|
+
file_path: string
|
|
48
|
+
operation: "append" | "prepend" | "find_replace" | "replace_section"
|
|
49
|
+
checksum?: string | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface ReadNoteOptions {
|
|
53
|
+
includeFrontmatter?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface EditNoteOptions {
|
|
57
|
+
find_text?: string
|
|
58
|
+
section?: string
|
|
59
|
+
expected_replacements?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ContextResult {
|
|
63
|
+
results: Array<{
|
|
64
|
+
primary_result: NoteResult
|
|
65
|
+
observations: Array<{
|
|
66
|
+
category: string
|
|
67
|
+
content: string
|
|
68
|
+
}>
|
|
69
|
+
related_results: Array<{
|
|
70
|
+
type: "relation" | "entity"
|
|
71
|
+
title?: string
|
|
72
|
+
permalink: string
|
|
73
|
+
relation_type?: string
|
|
74
|
+
from_entity?: string
|
|
75
|
+
to_entity?: string
|
|
76
|
+
}>
|
|
77
|
+
}>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RecentResult {
|
|
81
|
+
title: string
|
|
82
|
+
permalink: string
|
|
83
|
+
file_path: string
|
|
84
|
+
created_at: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ProjectListResult {
|
|
88
|
+
name: string
|
|
89
|
+
path: string
|
|
90
|
+
display_name?: string | null
|
|
91
|
+
is_private?: boolean
|
|
92
|
+
is_default?: boolean
|
|
93
|
+
isDefault?: boolean
|
|
94
|
+
workspace_name?: string | null
|
|
95
|
+
workspace_type?: string | null
|
|
96
|
+
workspace_tenant_id?: string | null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface WorkspaceResult {
|
|
100
|
+
tenant_id: string
|
|
101
|
+
name: string
|
|
102
|
+
workspace_type: string
|
|
103
|
+
role: string
|
|
104
|
+
organization_id?: string | null
|
|
105
|
+
has_active_subscription: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SchemaValidationResult {
|
|
109
|
+
entity_type: string | null
|
|
110
|
+
total_notes: number
|
|
111
|
+
total_entities: number
|
|
112
|
+
valid_count: number
|
|
113
|
+
warning_count: number
|
|
114
|
+
error_count: number
|
|
115
|
+
results: Array<{
|
|
116
|
+
identifier: string
|
|
117
|
+
valid: boolean
|
|
118
|
+
warnings: string[]
|
|
119
|
+
errors: string[]
|
|
120
|
+
}>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface SchemaInferResult {
|
|
124
|
+
entity_type: string
|
|
125
|
+
notes_analyzed: number
|
|
126
|
+
field_frequencies: Array<{
|
|
127
|
+
name: string
|
|
128
|
+
percentage: number
|
|
129
|
+
count: number
|
|
130
|
+
total: number
|
|
131
|
+
source: string
|
|
132
|
+
sample_values?: string[]
|
|
133
|
+
is_array?: boolean
|
|
134
|
+
target_type?: string | null
|
|
135
|
+
}>
|
|
136
|
+
suggested_schema: Record<string, unknown>
|
|
137
|
+
suggested_required: string[]
|
|
138
|
+
suggested_optional: string[]
|
|
139
|
+
excluded: string[]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SchemaDiffResult {
|
|
143
|
+
entity_type: string
|
|
144
|
+
schema_found: boolean
|
|
145
|
+
new_fields: Array<{
|
|
146
|
+
name: string
|
|
147
|
+
source: string
|
|
148
|
+
count: number
|
|
149
|
+
total: number
|
|
150
|
+
percentage: number
|
|
151
|
+
}>
|
|
152
|
+
dropped_fields: Array<{ name: string; source: string; declared_in?: string }>
|
|
153
|
+
cardinality_changes: string[]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface MetadataSearchResult {
|
|
157
|
+
results: SearchResult[]
|
|
158
|
+
current_page: number
|
|
159
|
+
page_size: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getErrorMessage(err: unknown): string {
|
|
163
|
+
return err instanceof Error ? err.message : String(err)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
167
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function extractTextFromContent(content: unknown): string {
|
|
171
|
+
if (!Array.isArray(content)) return ""
|
|
172
|
+
|
|
173
|
+
const textBlocks = content
|
|
174
|
+
.filter(
|
|
175
|
+
(block): block is { type: "text"; text: string } =>
|
|
176
|
+
isRecord(block) &&
|
|
177
|
+
block.type === "text" &&
|
|
178
|
+
typeof block.text === "string",
|
|
179
|
+
)
|
|
180
|
+
.map((block) => block.text)
|
|
181
|
+
|
|
182
|
+
return textBlocks.join("\n").trim()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isRecoverableConnectionError(err: unknown): boolean {
|
|
186
|
+
const msg = getErrorMessage(err).toLowerCase()
|
|
187
|
+
return (
|
|
188
|
+
msg.includes("connection closed") ||
|
|
189
|
+
msg.includes("not connected") ||
|
|
190
|
+
msg.includes("transport") ||
|
|
191
|
+
msg.includes("broken pipe") ||
|
|
192
|
+
msg.includes("econnreset") ||
|
|
193
|
+
msg.includes("epipe") ||
|
|
194
|
+
msg.includes("failed to start bm mcp stdio") ||
|
|
195
|
+
msg.includes("client is closed")
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isNoteNotFoundError(err: unknown): boolean {
|
|
200
|
+
const msg = getErrorMessage(err).toLowerCase()
|
|
201
|
+
return (
|
|
202
|
+
msg.includes("entity not found") ||
|
|
203
|
+
msg.includes("note not found") ||
|
|
204
|
+
msg.includes("resource not found") ||
|
|
205
|
+
msg.includes("could not find note matching") ||
|
|
206
|
+
msg.includes("404")
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function asString(value: unknown): string | null {
|
|
211
|
+
return typeof value === "string" ? value : null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export class BmClient {
|
|
215
|
+
private bmPath: string
|
|
216
|
+
private project: string
|
|
217
|
+
private cwd?: string
|
|
218
|
+
private env?: Record<string, string>
|
|
219
|
+
private shouldRun = false
|
|
220
|
+
|
|
221
|
+
private client: Client | null = null
|
|
222
|
+
private transport: StdioClientTransport | null = null
|
|
223
|
+
private connectPromise: Promise<void> | null = null
|
|
224
|
+
private retryDelaysMs = [...DEFAULT_RETRY_DELAYS_MS]
|
|
225
|
+
|
|
226
|
+
constructor(bmPath: string, project: string) {
|
|
227
|
+
this.bmPath = bmPath
|
|
228
|
+
this.project = project
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async start(options?: {
|
|
232
|
+
cwd?: string
|
|
233
|
+
env?: Record<string, string>
|
|
234
|
+
}): Promise<void> {
|
|
235
|
+
this.shouldRun = true
|
|
236
|
+
if (options?.cwd) {
|
|
237
|
+
this.cwd = options.cwd
|
|
238
|
+
}
|
|
239
|
+
if (options?.env) {
|
|
240
|
+
this.env = options.env
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await this.connectWithRetries()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async stop(): Promise<void> {
|
|
247
|
+
this.shouldRun = false
|
|
248
|
+
await this.disconnectCurrent(this.client, this.transport)
|
|
249
|
+
this.client = null
|
|
250
|
+
this.transport = null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async connectWithRetries(): Promise<void> {
|
|
254
|
+
let lastErr: unknown
|
|
255
|
+
|
|
256
|
+
for (let attempt = 0; attempt <= this.retryDelaysMs.length; attempt++) {
|
|
257
|
+
try {
|
|
258
|
+
await this.ensureConnected()
|
|
259
|
+
return
|
|
260
|
+
} catch (err) {
|
|
261
|
+
lastErr = err
|
|
262
|
+
await this.disconnectCurrent(this.client, this.transport)
|
|
263
|
+
this.client = null
|
|
264
|
+
this.transport = null
|
|
265
|
+
|
|
266
|
+
if (attempt === this.retryDelaysMs.length) {
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const waitMs = this.retryDelaysMs[attempt]
|
|
271
|
+
log.warn(
|
|
272
|
+
`BM MCP connect failed (attempt ${attempt + 1}/${this.retryDelaysMs.length + 1}): ${getErrorMessage(err)}; retrying in ${waitMs}ms`,
|
|
273
|
+
)
|
|
274
|
+
await delay(waitMs)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
throw new Error(`BM MCP unavailable: ${getErrorMessage(lastErr)}`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async ensureConnected(): Promise<Client> {
|
|
282
|
+
if (!this.shouldRun) {
|
|
283
|
+
this.shouldRun = true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (this.client && this.transport) {
|
|
287
|
+
return this.client
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!this.connectPromise) {
|
|
291
|
+
this.connectPromise = this.connectFresh()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
await this.connectPromise
|
|
296
|
+
} finally {
|
|
297
|
+
this.connectPromise = null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!this.client) {
|
|
301
|
+
throw new Error("BM MCP client was not initialized")
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return this.client
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private async connectFresh(): Promise<void> {
|
|
308
|
+
const transport = new StdioClientTransport({
|
|
309
|
+
command: this.bmPath,
|
|
310
|
+
args: ["mcp", "--transport", "stdio"],
|
|
311
|
+
cwd: this.cwd,
|
|
312
|
+
env: this.env,
|
|
313
|
+
stderr: "pipe",
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const client = new Client(
|
|
317
|
+
{
|
|
318
|
+
name: "openclaw-basic-memory",
|
|
319
|
+
version: "0.1.0",
|
|
320
|
+
},
|
|
321
|
+
{ capabilities: {} },
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
const stderr = transport.stderr
|
|
325
|
+
if (stderr) {
|
|
326
|
+
stderr.on("data", (data: Buffer) => {
|
|
327
|
+
const msg = data.toString().trim()
|
|
328
|
+
if (msg.length > 0) {
|
|
329
|
+
log.debug(`[bm mcp] ${msg}`)
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
transport.onclose = () => {
|
|
335
|
+
if (this.transport !== transport) return
|
|
336
|
+
log.warn("BM MCP stdio session closed")
|
|
337
|
+
this.client = null
|
|
338
|
+
this.transport = null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
transport.onerror = (err: unknown) => {
|
|
342
|
+
if (this.transport !== transport) return
|
|
343
|
+
log.warn(`BM MCP transport error: ${getErrorMessage(err)}`)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.client = client
|
|
347
|
+
this.transport = transport
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
await client.connect(transport)
|
|
351
|
+
const tools = await client.listTools()
|
|
352
|
+
this.assertRequiredTools(tools.tools.map((tool) => tool.name))
|
|
353
|
+
|
|
354
|
+
log.info(
|
|
355
|
+
`connected to BM MCP stdio (project=${this.project}, pid=${transport.pid ?? "unknown"})`,
|
|
356
|
+
)
|
|
357
|
+
} catch (err) {
|
|
358
|
+
await this.disconnectCurrent(client, transport)
|
|
359
|
+
if (this.client === client) {
|
|
360
|
+
this.client = null
|
|
361
|
+
}
|
|
362
|
+
if (this.transport === transport) {
|
|
363
|
+
this.transport = null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
throw new Error(`failed to start BM MCP stdio: ${getErrorMessage(err)}`)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private assertRequiredTools(toolNames: string[]): void {
|
|
371
|
+
const available = new Set(toolNames)
|
|
372
|
+
const missing = REQUIRED_TOOLS.filter((name) => !available.has(name))
|
|
373
|
+
if (missing.length > 0) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`BM MCP server missing required tools: ${missing.join(", ")}`,
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private async disconnectCurrent(
|
|
381
|
+
client: Client | null,
|
|
382
|
+
transport: StdioClientTransport | null,
|
|
383
|
+
): Promise<void> {
|
|
384
|
+
if (client) {
|
|
385
|
+
try {
|
|
386
|
+
await client.close()
|
|
387
|
+
} catch {
|
|
388
|
+
// ignore shutdown errors
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (transport) {
|
|
393
|
+
try {
|
|
394
|
+
await transport.close()
|
|
395
|
+
} catch {
|
|
396
|
+
// ignore shutdown errors
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async callToolRaw(
|
|
402
|
+
name: string,
|
|
403
|
+
args: Record<string, unknown>,
|
|
404
|
+
): Promise<unknown> {
|
|
405
|
+
let lastErr: unknown
|
|
406
|
+
|
|
407
|
+
for (let attempt = 0; attempt <= this.retryDelaysMs.length; attempt++) {
|
|
408
|
+
try {
|
|
409
|
+
const client = await this.ensureConnected()
|
|
410
|
+
const result = await client.callTool({
|
|
411
|
+
name,
|
|
412
|
+
arguments: args,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
if (isRecord(result) && result.isError === true) {
|
|
416
|
+
const message = extractTextFromContent(result.content)
|
|
417
|
+
throw new Error(
|
|
418
|
+
`BM MCP tool ${name} failed${message ? `: ${message}` : ""}`,
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return result
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (!isRecoverableConnectionError(err)) {
|
|
425
|
+
throw err
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
lastErr = err
|
|
429
|
+
await this.disconnectCurrent(this.client, this.transport)
|
|
430
|
+
this.client = null
|
|
431
|
+
this.transport = null
|
|
432
|
+
|
|
433
|
+
if (attempt === this.retryDelaysMs.length) {
|
|
434
|
+
break
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const waitMs = this.retryDelaysMs[attempt]
|
|
438
|
+
log.warn(
|
|
439
|
+
`BM MCP call ${name} failed (attempt ${attempt + 1}/${this.retryDelaysMs.length + 1}): ${getErrorMessage(err)}; retrying in ${waitMs}ms`,
|
|
440
|
+
)
|
|
441
|
+
await delay(waitMs)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
throw new Error(`BM MCP unavailable: ${getErrorMessage(lastErr)}`)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private async callTool(
|
|
449
|
+
name: string,
|
|
450
|
+
args: Record<string, unknown>,
|
|
451
|
+
): Promise<unknown> {
|
|
452
|
+
const result = await this.callToolRaw(name, args)
|
|
453
|
+
|
|
454
|
+
if (!isRecord(result) || result.structuredContent === undefined) {
|
|
455
|
+
throw new Error(`BM MCP tool ${name} returned no structured payload`)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const structuredPayload = result.structuredContent
|
|
459
|
+
if (isRecord(structuredPayload) && structuredPayload.result !== undefined) {
|
|
460
|
+
return structuredPayload.result
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return structuredPayload
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async ensureProject(projectPath: string): Promise<void> {
|
|
467
|
+
const payload = await this.callTool("create_memory_project", {
|
|
468
|
+
project_name: this.project,
|
|
469
|
+
project_path: projectPath,
|
|
470
|
+
set_default: true,
|
|
471
|
+
output_format: "json",
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
if (!isRecord(payload)) {
|
|
475
|
+
throw new Error("invalid create_memory_project response")
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async listWorkspaces(): Promise<WorkspaceResult[]> {
|
|
480
|
+
const payload = await this.callTool("list_workspaces", {
|
|
481
|
+
output_format: "json",
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
if (isRecord(payload) && Array.isArray(payload.workspaces)) {
|
|
485
|
+
return payload.workspaces as WorkspaceResult[]
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
throw new Error("invalid list_workspaces response")
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async listProjects(workspace?: string): Promise<ProjectListResult[]> {
|
|
492
|
+
const args: Record<string, unknown> = { output_format: "json" }
|
|
493
|
+
if (workspace) args.workspace = workspace
|
|
494
|
+
|
|
495
|
+
const payload = await this.callTool("list_memory_projects", args)
|
|
496
|
+
|
|
497
|
+
if (isRecord(payload) && Array.isArray(payload.projects)) {
|
|
498
|
+
return payload.projects as ProjectListResult[]
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
throw new Error("invalid list_memory_projects response")
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async search(
|
|
505
|
+
query: string,
|
|
506
|
+
limit = 10,
|
|
507
|
+
project?: string,
|
|
508
|
+
metadata?: {
|
|
509
|
+
filters?: Record<string, unknown>
|
|
510
|
+
tags?: string[]
|
|
511
|
+
status?: string
|
|
512
|
+
},
|
|
513
|
+
): Promise<SearchResult[]> {
|
|
514
|
+
const args: Record<string, unknown> = {
|
|
515
|
+
query,
|
|
516
|
+
page: 1,
|
|
517
|
+
page_size: limit,
|
|
518
|
+
output_format: "json",
|
|
519
|
+
}
|
|
520
|
+
if (project) args.project = project
|
|
521
|
+
if (metadata?.filters) args.metadata_filters = metadata.filters
|
|
522
|
+
if (metadata?.tags) args.tags = metadata.tags
|
|
523
|
+
if (metadata?.status) args.status = metadata.status
|
|
524
|
+
|
|
525
|
+
const payload = await this.callTool("search_notes", args)
|
|
526
|
+
|
|
527
|
+
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
528
|
+
throw new Error("invalid search_notes response")
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return payload.results as SearchResult[]
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async readNote(
|
|
535
|
+
identifier: string,
|
|
536
|
+
options: ReadNoteOptions = {},
|
|
537
|
+
project?: string,
|
|
538
|
+
): Promise<NoteResult> {
|
|
539
|
+
const args: Record<string, unknown> = {
|
|
540
|
+
identifier,
|
|
541
|
+
include_frontmatter: options.includeFrontmatter === true,
|
|
542
|
+
output_format: "json",
|
|
543
|
+
}
|
|
544
|
+
if (project) args.project = project
|
|
545
|
+
|
|
546
|
+
const payload = await this.callTool("read_note", args)
|
|
547
|
+
|
|
548
|
+
if (!isRecord(payload)) {
|
|
549
|
+
throw new Error("invalid read_note response")
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const title = asString(payload.title)
|
|
553
|
+
const permalink = asString(payload.permalink)
|
|
554
|
+
const content = asString(payload.content)
|
|
555
|
+
const filePath = asString(payload.file_path)
|
|
556
|
+
|
|
557
|
+
if (!title || !permalink || content === null || !filePath) {
|
|
558
|
+
throw new Error("invalid read_note payload")
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
title,
|
|
563
|
+
permalink,
|
|
564
|
+
content,
|
|
565
|
+
file_path: filePath,
|
|
566
|
+
frontmatter: isRecord(payload.frontmatter) ? payload.frontmatter : null,
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async writeNote(
|
|
571
|
+
title: string,
|
|
572
|
+
content: string,
|
|
573
|
+
folder: string,
|
|
574
|
+
project?: string,
|
|
575
|
+
): Promise<NoteResult> {
|
|
576
|
+
const args: Record<string, unknown> = {
|
|
577
|
+
title,
|
|
578
|
+
content,
|
|
579
|
+
directory: folder,
|
|
580
|
+
output_format: "json",
|
|
581
|
+
}
|
|
582
|
+
if (project) args.project = project
|
|
583
|
+
|
|
584
|
+
const payload = await this.callTool("write_note", args)
|
|
585
|
+
|
|
586
|
+
if (!isRecord(payload)) {
|
|
587
|
+
throw new Error("invalid write_note response")
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const resultTitle = asString(payload.title)
|
|
591
|
+
const permalink = asString(payload.permalink)
|
|
592
|
+
const filePath = asString(payload.file_path)
|
|
593
|
+
|
|
594
|
+
if (!resultTitle || !permalink || !filePath) {
|
|
595
|
+
throw new Error("invalid write_note payload")
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
title: resultTitle,
|
|
600
|
+
permalink,
|
|
601
|
+
content,
|
|
602
|
+
file_path: filePath,
|
|
603
|
+
checksum: asString(payload.checksum),
|
|
604
|
+
action:
|
|
605
|
+
payload.action === "created" || payload.action === "updated"
|
|
606
|
+
? payload.action
|
|
607
|
+
: undefined,
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async buildContext(
|
|
612
|
+
url: string,
|
|
613
|
+
depth = 1,
|
|
614
|
+
project?: string,
|
|
615
|
+
): Promise<ContextResult> {
|
|
616
|
+
const args: Record<string, unknown> = {
|
|
617
|
+
url,
|
|
618
|
+
depth,
|
|
619
|
+
output_format: "json",
|
|
620
|
+
}
|
|
621
|
+
if (project) args.project = project
|
|
622
|
+
|
|
623
|
+
const payload = await this.callTool("build_context", args)
|
|
624
|
+
|
|
625
|
+
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
626
|
+
throw new Error("invalid build_context response")
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return payload as unknown as ContextResult
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async recentActivity(
|
|
633
|
+
timeframe = "24h",
|
|
634
|
+
project?: string,
|
|
635
|
+
): Promise<RecentResult[]> {
|
|
636
|
+
const args: Record<string, unknown> = {
|
|
637
|
+
timeframe,
|
|
638
|
+
output_format: "json",
|
|
639
|
+
}
|
|
640
|
+
if (project) args.project = project
|
|
641
|
+
|
|
642
|
+
const payload = await this.callTool("recent_activity", args)
|
|
643
|
+
|
|
644
|
+
if (Array.isArray(payload)) {
|
|
645
|
+
return payload as RecentResult[]
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
throw new Error("invalid recent_activity response")
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async editNote(
|
|
652
|
+
identifier: string,
|
|
653
|
+
operation: "append" | "prepend" | "find_replace" | "replace_section",
|
|
654
|
+
content: string,
|
|
655
|
+
options: EditNoteOptions = {},
|
|
656
|
+
project?: string,
|
|
657
|
+
): Promise<EditNoteResult> {
|
|
658
|
+
const args: Record<string, unknown> = {
|
|
659
|
+
identifier,
|
|
660
|
+
operation,
|
|
661
|
+
content,
|
|
662
|
+
find_text: options.find_text,
|
|
663
|
+
section: options.section,
|
|
664
|
+
expected_replacements: options.expected_replacements,
|
|
665
|
+
output_format: "json",
|
|
666
|
+
}
|
|
667
|
+
if (project) args.project = project
|
|
668
|
+
|
|
669
|
+
const payload = await this.callTool("edit_note", args)
|
|
670
|
+
|
|
671
|
+
if (!isRecord(payload)) {
|
|
672
|
+
throw new Error("invalid edit_note response")
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const title = asString(payload.title)
|
|
676
|
+
const permalink = asString(payload.permalink)
|
|
677
|
+
const filePath = asString(payload.file_path)
|
|
678
|
+
|
|
679
|
+
if (!title || !permalink || !filePath) {
|
|
680
|
+
throw new Error("invalid edit_note payload")
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
title,
|
|
685
|
+
permalink,
|
|
686
|
+
file_path: filePath,
|
|
687
|
+
operation,
|
|
688
|
+
checksum: asString(payload.checksum),
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async deleteNote(
|
|
693
|
+
identifier: string,
|
|
694
|
+
project?: string,
|
|
695
|
+
): Promise<{ title: string; permalink: string; file_path: string }> {
|
|
696
|
+
const args: Record<string, unknown> = {
|
|
697
|
+
identifier,
|
|
698
|
+
output_format: "json",
|
|
699
|
+
}
|
|
700
|
+
if (project) args.project = project
|
|
701
|
+
|
|
702
|
+
const payload = await this.callTool("delete_note", args)
|
|
703
|
+
|
|
704
|
+
if (!isRecord(payload)) {
|
|
705
|
+
throw new Error("invalid delete_note response")
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (payload.deleted !== true) {
|
|
709
|
+
throw new Error(`delete_note did not delete "${identifier}"`)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
title: asString(payload.title) ?? identifier,
|
|
714
|
+
permalink: asString(payload.permalink) ?? identifier,
|
|
715
|
+
file_path: asString(payload.file_path) ?? identifier,
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async moveNote(
|
|
720
|
+
identifier: string,
|
|
721
|
+
newFolder: string,
|
|
722
|
+
project?: string,
|
|
723
|
+
): Promise<NoteResult> {
|
|
724
|
+
const args: Record<string, unknown> = {
|
|
725
|
+
identifier,
|
|
726
|
+
destination_folder: newFolder,
|
|
727
|
+
output_format: "json",
|
|
728
|
+
}
|
|
729
|
+
if (project) args.project = project
|
|
730
|
+
|
|
731
|
+
const payload = await this.callTool("move_note", args)
|
|
732
|
+
|
|
733
|
+
if (!isRecord(payload)) {
|
|
734
|
+
throw new Error("invalid move_note response")
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (payload.moved !== true) {
|
|
738
|
+
throw new Error(
|
|
739
|
+
asString(payload.error) ??
|
|
740
|
+
`move_note did not move "${identifier}" to "${newFolder}"`,
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
title: asString(payload.title) ?? identifier,
|
|
746
|
+
permalink: asString(payload.permalink) ?? identifier,
|
|
747
|
+
content: "",
|
|
748
|
+
file_path: asString(payload.file_path) ?? "",
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async schemaValidate(
|
|
753
|
+
noteType?: string,
|
|
754
|
+
identifier?: string,
|
|
755
|
+
project?: string,
|
|
756
|
+
): Promise<SchemaValidationResult> {
|
|
757
|
+
const args: Record<string, unknown> = { output_format: "json" }
|
|
758
|
+
if (noteType) args.note_type = noteType
|
|
759
|
+
if (identifier) args.identifier = identifier
|
|
760
|
+
if (project) args.project = project
|
|
761
|
+
|
|
762
|
+
const payload = await this.callTool("schema_validate", args)
|
|
763
|
+
|
|
764
|
+
if (!isRecord(payload)) {
|
|
765
|
+
throw new Error("invalid schema_validate response")
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return payload as unknown as SchemaValidationResult
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async schemaInfer(
|
|
772
|
+
noteType: string,
|
|
773
|
+
threshold = 0.25,
|
|
774
|
+
project?: string,
|
|
775
|
+
): Promise<SchemaInferResult> {
|
|
776
|
+
const args: Record<string, unknown> = {
|
|
777
|
+
note_type: noteType,
|
|
778
|
+
threshold,
|
|
779
|
+
output_format: "json",
|
|
780
|
+
}
|
|
781
|
+
if (project) args.project = project
|
|
782
|
+
|
|
783
|
+
const payload = await this.callTool("schema_infer", args)
|
|
784
|
+
|
|
785
|
+
if (!isRecord(payload)) {
|
|
786
|
+
throw new Error("invalid schema_infer response")
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return payload as unknown as SchemaInferResult
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async schemaDiff(
|
|
793
|
+
noteType: string,
|
|
794
|
+
project?: string,
|
|
795
|
+
): Promise<SchemaDiffResult> {
|
|
796
|
+
const args: Record<string, unknown> = {
|
|
797
|
+
note_type: noteType,
|
|
798
|
+
output_format: "json",
|
|
799
|
+
}
|
|
800
|
+
if (project) args.project = project
|
|
801
|
+
|
|
802
|
+
const payload = await this.callTool("schema_diff", args)
|
|
803
|
+
|
|
804
|
+
if (!isRecord(payload)) {
|
|
805
|
+
throw new Error("invalid schema_diff response")
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return payload as unknown as SchemaDiffResult
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async searchByMetadata(
|
|
812
|
+
filters: Record<string, unknown>,
|
|
813
|
+
limit = 20,
|
|
814
|
+
project?: string,
|
|
815
|
+
): Promise<MetadataSearchResult> {
|
|
816
|
+
const args: Record<string, unknown> = {
|
|
817
|
+
filters,
|
|
818
|
+
limit,
|
|
819
|
+
output_format: "json",
|
|
820
|
+
}
|
|
821
|
+
if (project) args.project = project
|
|
822
|
+
|
|
823
|
+
const payload = await this.callTool("search_by_metadata", args)
|
|
824
|
+
|
|
825
|
+
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
826
|
+
throw new Error("invalid search_by_metadata response")
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return payload as unknown as MetadataSearchResult
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private isNoteNotFoundError(err: unknown): boolean {
|
|
833
|
+
return isNoteNotFoundError(err)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async indexConversation(
|
|
837
|
+
userMessage: string,
|
|
838
|
+
assistantResponse: string,
|
|
839
|
+
): Promise<void> {
|
|
840
|
+
const now = new Date()
|
|
841
|
+
const dateStr = now.toISOString().split("T")[0]
|
|
842
|
+
const timeStr = now.toTimeString().slice(0, 5)
|
|
843
|
+
const title = `conversations-${dateStr}`
|
|
844
|
+
|
|
845
|
+
const entry = [
|
|
846
|
+
`### ${timeStr}`,
|
|
847
|
+
"",
|
|
848
|
+
"**User:**",
|
|
849
|
+
userMessage,
|
|
850
|
+
"",
|
|
851
|
+
"**Assistant:**",
|
|
852
|
+
assistantResponse,
|
|
853
|
+
"",
|
|
854
|
+
"---",
|
|
855
|
+
].join("\n")
|
|
856
|
+
|
|
857
|
+
try {
|
|
858
|
+
await this.editNote(title, "append", entry)
|
|
859
|
+
log.debug(`appended conversation to: ${title}`)
|
|
860
|
+
} catch (err) {
|
|
861
|
+
if (!this.isNoteNotFoundError(err)) {
|
|
862
|
+
log.error("conversation append failed", err)
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
|
|
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
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
getProject(): string {
|
|
877
|
+
return this.project
|
|
878
|
+
}
|
|
879
|
+
}
|