ex-brain 0.1.0 → 0.2.0
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 +87 -37
- package/package.json +6 -5
- package/src/ai/compiler.ts +494 -0
- package/src/ai/embed-factory.ts +116 -0
- package/src/ai/entity-link.ts +195 -0
- package/src/ai/hash-embed.ts +30 -0
- package/src/ai/llm-client.ts +291 -0
- package/src/ai/timeline-extractor.ts +403 -0
- package/src/cli.ts +16 -0
- package/src/commands/compile-cmd.ts +208 -0
- package/src/commands/graph-cmd.ts +1070 -0
- package/src/commands/index.ts +1973 -0
- package/src/config.ts +80 -0
- package/src/db/client.ts +207 -0
- package/src/db/errors.ts +178 -0
- package/src/db/schema.ts +50 -0
- package/src/markdown/io.ts +61 -0
- package/src/markdown/parser.ts +72 -0
- package/src/mcp/server.ts +703 -0
- package/src/repositories/brain-repo.ts +990 -0
- package/src/settings.ts +235 -0
- package/src/types/index.ts +56 -0
- package/src/utils/cli-output.ts +569 -0
- package/src/utils/progress.ts +171 -0
- package/src/utils/query-sanitizer.ts +63 -0
- package/dist/cli.js +0 -93543
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { BrainDb } from "../db/client";
|
|
5
|
+
import { BrainRepository } from "../repositories/brain-repo";
|
|
6
|
+
import { loadSettings } from "../settings";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Error Handling Utilities
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
interface ToolError {
|
|
13
|
+
tool: string;
|
|
14
|
+
error: string;
|
|
15
|
+
message: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
recoverable: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatError(toolName: string, error: unknown): ToolError {
|
|
21
|
+
const err = error as Error;
|
|
22
|
+
const errorType = err?.name ?? "UnknownError";
|
|
23
|
+
const errorMessage = err?.message ?? String(error);
|
|
24
|
+
|
|
25
|
+
// 判断是否可恢复
|
|
26
|
+
const recoverablePatterns = [
|
|
27
|
+
"ECONNREFUSED",
|
|
28
|
+
"timeout",
|
|
29
|
+
"ETIMEDOUT",
|
|
30
|
+
"rate limit",
|
|
31
|
+
"429",
|
|
32
|
+
"503",
|
|
33
|
+
"502",
|
|
34
|
+
"timeout",
|
|
35
|
+
];
|
|
36
|
+
const isRecoverable = recoverablePatterns.some(p =>
|
|
37
|
+
errorMessage.toLowerCase().includes(p.toLowerCase())
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
tool: toolName,
|
|
42
|
+
error: errorType,
|
|
43
|
+
message: errorMessage,
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
recoverable: isRecoverable,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function logError(toolName: string, error: unknown, params?: Record<string, unknown>): void {
|
|
50
|
+
const errInfo = formatError(toolName, error);
|
|
51
|
+
console.error(`[MCP Error] Tool: ${toolName}`);
|
|
52
|
+
console.error(` Type: ${errInfo.error}`);
|
|
53
|
+
console.error(` Message: ${errInfo.message}`);
|
|
54
|
+
console.error(` Recoverable: ${errInfo.recoverable}`);
|
|
55
|
+
if (params) {
|
|
56
|
+
console.error(` Params: ${JSON.stringify(params)}`);
|
|
57
|
+
}
|
|
58
|
+
console.error(` Timestamp: ${errInfo.timestamp}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 包装工具 handler,添加错误处理
|
|
63
|
+
* 确保工具错误不会导致 MCP Server 崩溃,返回友好的 JSON 错误信息
|
|
64
|
+
*/
|
|
65
|
+
function withErrorHandling<T extends Record<string, unknown>>(
|
|
66
|
+
toolName: string,
|
|
67
|
+
handler: (params: T) => Promise<{ content: Array<{ type: string; text: string }> }>
|
|
68
|
+
) {
|
|
69
|
+
return async (params: T): Promise<{ content: Array<{ type: string; text: string }> }> => {
|
|
70
|
+
try {
|
|
71
|
+
return await handler(params);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logError(toolName, error, params);
|
|
74
|
+
const errInfo = formatError(toolName, error);
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text" as const,
|
|
79
|
+
text: JSON.stringify({
|
|
80
|
+
ok: false,
|
|
81
|
+
error: errInfo.error,
|
|
82
|
+
message: errInfo.message,
|
|
83
|
+
recoverable: errInfo.recoverable,
|
|
84
|
+
hint: errInfo.recoverable
|
|
85
|
+
? "This is a temporary error. Please try again later."
|
|
86
|
+
: "Please check the input parameters or system configuration.",
|
|
87
|
+
tool: toolName,
|
|
88
|
+
}, null, 2),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 资源错误处理包装
|
|
97
|
+
function withResourceErrorHandling<T extends Record<string, string>>(
|
|
98
|
+
resourceName: string,
|
|
99
|
+
handler: (uri: URL, vars: T) => Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }>
|
|
100
|
+
) {
|
|
101
|
+
return async (uri: URL, vars: T): Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }> => {
|
|
102
|
+
try {
|
|
103
|
+
return await handler(uri, vars);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logError(resourceName, error, vars as unknown as Record<string, unknown>);
|
|
106
|
+
const errInfo = formatError(resourceName, error);
|
|
107
|
+
return {
|
|
108
|
+
contents: [
|
|
109
|
+
{
|
|
110
|
+
uri: uri.href,
|
|
111
|
+
mimeType: "application/json",
|
|
112
|
+
text: JSON.stringify({
|
|
113
|
+
ok: false,
|
|
114
|
+
error: errInfo.error,
|
|
115
|
+
message: errInfo.message,
|
|
116
|
+
recoverable: errInfo.recoverable,
|
|
117
|
+
resource: resourceName,
|
|
118
|
+
}, null, 2),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const TOOL_MANIFEST = [
|
|
127
|
+
"brain_search",
|
|
128
|
+
"brain_query",
|
|
129
|
+
"brain_get",
|
|
130
|
+
"brain_put",
|
|
131
|
+
"brain_delete",
|
|
132
|
+
"brain_ingest",
|
|
133
|
+
"brain_link",
|
|
134
|
+
"brain_backlinks",
|
|
135
|
+
"brain_timeline",
|
|
136
|
+
"brain_timeline_add",
|
|
137
|
+
"brain_timeline_list",
|
|
138
|
+
"brain_timeline_delete",
|
|
139
|
+
"brain_timeline_extract",
|
|
140
|
+
"brain_compile",
|
|
141
|
+
"brain_smart_ingest",
|
|
142
|
+
"brain_tags",
|
|
143
|
+
"brain_tag",
|
|
144
|
+
"brain_list",
|
|
145
|
+
"brain_stats",
|
|
146
|
+
"brain_raw",
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
export async function startMcpServer(dbPath: string): Promise<void> {
|
|
150
|
+
const db = await BrainDb.connect(dbPath);
|
|
151
|
+
const repo = new BrainRepository(db);
|
|
152
|
+
const settings = await loadSettings();
|
|
153
|
+
const server = new McpServer({ name: "ebrain", version: "0.2.0" });
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Search & Query Tools
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
// Tool handler functions (wrapped with error handling below)
|
|
160
|
+
const brainSearchHandler = async ({ query, type, limit }: { query: string; type?: string; limit?: number }) => ({
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: JSON.stringify(
|
|
165
|
+
await repo.search(query, limit ?? 10, type),
|
|
166
|
+
null,
|
|
167
|
+
2,
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
server.registerTool(
|
|
174
|
+
"brain_search",
|
|
175
|
+
{
|
|
176
|
+
description: "Full-text search (hybridSearch without KNN)",
|
|
177
|
+
inputSchema: z.object({
|
|
178
|
+
query: z.string(),
|
|
179
|
+
type: z.string().optional(),
|
|
180
|
+
limit: z.number().int().positive().max(50).optional(),
|
|
181
|
+
}),
|
|
182
|
+
},
|
|
183
|
+
withErrorHandling("brain_search", brainSearchHandler),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const brainQueryHandler = async ({ question, limit }: { question: string; limit?: number }) => ({
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: "text",
|
|
190
|
+
text: JSON.stringify(await repo.query(question, limit ?? 10), null, 2),
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
server.registerTool(
|
|
196
|
+
"brain_query",
|
|
197
|
+
{
|
|
198
|
+
description: "Semantic query using vector embeddings",
|
|
199
|
+
inputSchema: z.object({
|
|
200
|
+
question: z.string(),
|
|
201
|
+
limit: z.number().int().positive().max(50).optional(),
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
withErrorHandling("brain_query", brainQueryHandler),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Page CRUD Tools
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
const brainGetHandler = async ({ slug }: { slug: string }) => ({
|
|
212
|
+
content: [
|
|
213
|
+
{ type: "text", text: JSON.stringify(await repo.getPage(slug), null, 2) },
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
server.registerTool(
|
|
218
|
+
"brain_get",
|
|
219
|
+
{
|
|
220
|
+
description: "Read a page and return its full content",
|
|
221
|
+
inputSchema: z.object({ slug: z.string() }),
|
|
222
|
+
},
|
|
223
|
+
withErrorHandling("brain_get", brainGetHandler),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const brainPutHandler = async ({ slug, content, type, title }: { slug: string; content: string; type?: string; title?: string }) => {
|
|
227
|
+
const page = await repo.putPage({
|
|
228
|
+
slug,
|
|
229
|
+
type: type ?? "note",
|
|
230
|
+
title: title ?? slug,
|
|
231
|
+
compiledTruth: content,
|
|
232
|
+
timeline: "",
|
|
233
|
+
});
|
|
234
|
+
return { content: [{ type: "text", text: JSON.stringify(page, null, 2) }] };
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
server.registerTool(
|
|
238
|
+
"brain_put",
|
|
239
|
+
{
|
|
240
|
+
description: "Write or update a page",
|
|
241
|
+
inputSchema: z.object({
|
|
242
|
+
slug: z.string(),
|
|
243
|
+
content: z.string(),
|
|
244
|
+
type: z.string().optional(),
|
|
245
|
+
title: z.string().optional(),
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
withErrorHandling("brain_put", brainPutHandler),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const brainDeleteHandler = async ({ slug }: { slug: string }) => {
|
|
252
|
+
await repo.deletePage(slug);
|
|
253
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, action: "delete", slug }) }] };
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
server.registerTool(
|
|
257
|
+
"brain_delete",
|
|
258
|
+
{
|
|
259
|
+
description: "Delete a page and all its related data (links, tags, timeline, raw)",
|
|
260
|
+
inputSchema: z.object({ slug: z.string() }),
|
|
261
|
+
},
|
|
262
|
+
withErrorHandling("brain_delete", brainDeleteHandler),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const brainIngestHandler = async ({ content, source_type, source_ref }: { content: string; source_type: string; source_ref: string }) => {
|
|
266
|
+
const safeRef = source_ref.replace(/[^a-zA-Z0-9/_-]+/g, "_").slice(0, 200);
|
|
267
|
+
const slug = `ingest/${safeRef || "untitled"}`;
|
|
268
|
+
const page = await repo.putPage({
|
|
269
|
+
slug,
|
|
270
|
+
type: source_type,
|
|
271
|
+
title: source_ref,
|
|
272
|
+
compiledTruth: content,
|
|
273
|
+
timeline: "",
|
|
274
|
+
frontmatter: { source_type, source_ref },
|
|
275
|
+
});
|
|
276
|
+
return { content: [{ type: "text", text: JSON.stringify(page, null, 2) }] };
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
server.registerTool(
|
|
280
|
+
"brain_ingest",
|
|
281
|
+
{
|
|
282
|
+
description: "Ingest source content as a new page (simple ingestion)",
|
|
283
|
+
inputSchema: z.object({
|
|
284
|
+
content: z.string(),
|
|
285
|
+
source_type: z.string(),
|
|
286
|
+
source_ref: z.string(),
|
|
287
|
+
}),
|
|
288
|
+
},
|
|
289
|
+
withErrorHandling("brain_ingest", brainIngestHandler),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Link Tools
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
const brainLinkHandler = async ({ from, to, context }: { from: string; to: string; context?: string }) => {
|
|
297
|
+
await repo.link(from, to, context ?? "");
|
|
298
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
server.registerTool(
|
|
302
|
+
"brain_link",
|
|
303
|
+
{
|
|
304
|
+
description: "Create a cross-link between two pages",
|
|
305
|
+
inputSchema: z.object({
|
|
306
|
+
from: z.string(),
|
|
307
|
+
to: z.string(),
|
|
308
|
+
context: z.string().optional(),
|
|
309
|
+
}),
|
|
310
|
+
},
|
|
311
|
+
withErrorHandling("brain_link", brainLinkHandler),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const brainBacklinksHandler = async ({ slug }: { slug: string }) => ({
|
|
315
|
+
content: [
|
|
316
|
+
{ type: "text", text: JSON.stringify(await repo.backlinks(slug), null, 2) },
|
|
317
|
+
],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
server.registerTool(
|
|
321
|
+
"brain_backlinks",
|
|
322
|
+
{
|
|
323
|
+
description: "List pages that link to this page",
|
|
324
|
+
inputSchema: z.object({ slug: z.string() }),
|
|
325
|
+
},
|
|
326
|
+
withErrorHandling("brain_backlinks", brainBacklinksHandler),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Timeline Tools (Enhanced)
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
const brainTimelineHandler = async ({ slug, limit }: { slug: string; limit?: number }) => ({
|
|
334
|
+
content: [
|
|
335
|
+
{
|
|
336
|
+
type: "text",
|
|
337
|
+
text: JSON.stringify(await repo.timeline(slug, limit ?? 50), null, 2),
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
server.registerTool(
|
|
343
|
+
"brain_timeline",
|
|
344
|
+
{
|
|
345
|
+
description: "List timeline entries for a specific page",
|
|
346
|
+
inputSchema: z.object({
|
|
347
|
+
slug: z.string(),
|
|
348
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
349
|
+
}),
|
|
350
|
+
},
|
|
351
|
+
withErrorHandling("brain_timeline", brainTimelineHandler),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const brainTimelineAddHandler = async ({ slug, date, summary, source, detail }: { slug: string; date: string; summary: string; source?: string; detail?: string }) => {
|
|
355
|
+
await repo.timelineAdd({
|
|
356
|
+
pageSlug: slug,
|
|
357
|
+
date,
|
|
358
|
+
summary,
|
|
359
|
+
source: source ?? "manual",
|
|
360
|
+
detail: detail ?? "",
|
|
361
|
+
});
|
|
362
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
server.registerTool(
|
|
366
|
+
"brain_timeline_add",
|
|
367
|
+
{
|
|
368
|
+
description: "Append a timeline entry to a page",
|
|
369
|
+
inputSchema: z.object({
|
|
370
|
+
slug: z.string().describe("Page slug"),
|
|
371
|
+
date: z.string().describe("Date in YYYY-MM-DD format"),
|
|
372
|
+
summary: z.string().describe("One-line summary (max 120 chars)"),
|
|
373
|
+
source: z.string().optional().describe("Source identifier"),
|
|
374
|
+
detail: z.string().optional().describe("Optional markdown detail"),
|
|
375
|
+
}),
|
|
376
|
+
},
|
|
377
|
+
withErrorHandling("brain_timeline_add", brainTimelineAddHandler),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const brainTimelineListHandler = async ({ limit }: { limit?: number }) => ({
|
|
381
|
+
content: [
|
|
382
|
+
{
|
|
383
|
+
type: "text",
|
|
384
|
+
text: JSON.stringify(await repo.timelineGlobal(limit ?? 100), null, 2),
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
server.registerTool(
|
|
390
|
+
"brain_timeline_list",
|
|
391
|
+
{
|
|
392
|
+
description: "List timeline entries across all pages (global timeline view)",
|
|
393
|
+
inputSchema: z.object({
|
|
394
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
395
|
+
}),
|
|
396
|
+
},
|
|
397
|
+
withErrorHandling("brain_timeline_list", brainTimelineListHandler),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const brainTimelineDeleteHandler = async ({ id }: { id: number }) => {
|
|
401
|
+
await repo.timelineDelete(id);
|
|
402
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, action: "timeline-delete", id }) }] };
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
server.registerTool(
|
|
406
|
+
"brain_timeline_delete",
|
|
407
|
+
{
|
|
408
|
+
description: "Delete a specific timeline entry by ID",
|
|
409
|
+
inputSchema: z.object({
|
|
410
|
+
id: z.number().int().positive().describe("Timeline entry ID to delete"),
|
|
411
|
+
}),
|
|
412
|
+
},
|
|
413
|
+
withErrorHandling("brain_timeline_delete", brainTimelineDeleteHandler),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const brainTimelineExtractHandler = async ({ slug, content, source, default_date }: { slug: string; content: string; source?: string; default_date?: string }) => {
|
|
417
|
+
const result = await repo.extractAndAddTimeline(
|
|
418
|
+
slug,
|
|
419
|
+
content,
|
|
420
|
+
source ?? "extracted",
|
|
421
|
+
default_date ?? new Date().toISOString().slice(0, 10),
|
|
422
|
+
settings.llm,
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: "text",
|
|
428
|
+
text: JSON.stringify({
|
|
429
|
+
ok: true,
|
|
430
|
+
entriesAdded: result.entries.length,
|
|
431
|
+
entries: result.entries,
|
|
432
|
+
confidence: result.confidence,
|
|
433
|
+
}, null, 2),
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
server.registerTool(
|
|
440
|
+
"brain_timeline_extract",
|
|
441
|
+
{
|
|
442
|
+
description: "Extract timeline events from content using AI. Adds extracted entries to page timeline.",
|
|
443
|
+
inputSchema: z.object({
|
|
444
|
+
slug: z.string().describe("Page slug to add timeline entries to"),
|
|
445
|
+
content: z.string().describe("Content to extract timeline events from"),
|
|
446
|
+
source: z.string().optional().describe("Source identifier"),
|
|
447
|
+
default_date: z.string().optional().describe("Default date (YYYY-MM-DD) for entries without explicit dates"),
|
|
448
|
+
}),
|
|
449
|
+
},
|
|
450
|
+
withErrorHandling("brain_timeline_extract", brainTimelineExtractHandler),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Smart Compilation Tools (Core Brain Function)
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
const brainCompileHandler = async ({ slug, new_info, source, date }: { slug: string; new_info: string; source?: string; date?: string }) => {
|
|
458
|
+
const result = await repo.compilePage(
|
|
459
|
+
slug,
|
|
460
|
+
new_info,
|
|
461
|
+
source ?? "user",
|
|
462
|
+
date ?? new Date().toISOString().slice(0, 10),
|
|
463
|
+
settings.llm,
|
|
464
|
+
);
|
|
465
|
+
return {
|
|
466
|
+
content: [
|
|
467
|
+
{
|
|
468
|
+
type: "text",
|
|
469
|
+
text: JSON.stringify({
|
|
470
|
+
ok: true,
|
|
471
|
+
slug,
|
|
472
|
+
changed: result.changed,
|
|
473
|
+
changeType: result.changeType,
|
|
474
|
+
changeSummary: result.changeSummary,
|
|
475
|
+
timelineEntriesAdded: result.timelineEntries.length,
|
|
476
|
+
confidence: result.confidence,
|
|
477
|
+
compiledTruthPreview: result.compiledTruth.slice(0, 500),
|
|
478
|
+
}, null, 2),
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
server.registerTool(
|
|
485
|
+
"brain_compile",
|
|
486
|
+
{
|
|
487
|
+
description: "Intelligently compile new information into a page's compiled truth. Analyzes semantic meaning, updates/replaces outdated info, adds source attribution, and extracts timeline events. This is the core 'brain' function.",
|
|
488
|
+
inputSchema: z.object({
|
|
489
|
+
slug: z.string().describe("Page slug to compile into"),
|
|
490
|
+
new_info: z.string().describe("New information to process (e.g., 'River AI closed Series A funding')"),
|
|
491
|
+
source: z.string().optional().describe("Source of information (e.g., 'meeting_notes', 'news', 'user')"),
|
|
492
|
+
date: z.string().optional().describe("Date of information (YYYY-MM-DD)"),
|
|
493
|
+
}),
|
|
494
|
+
},
|
|
495
|
+
withErrorHandling("brain_compile", brainCompileHandler),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const brainSmartIngestHandler = async ({ slug, content, source, type }: { slug: string; content: string; source?: string; type?: string }) => {
|
|
499
|
+
const result = await repo.ingestContent(
|
|
500
|
+
slug,
|
|
501
|
+
content,
|
|
502
|
+
source ?? "ingest",
|
|
503
|
+
type ?? "note",
|
|
504
|
+
settings.llm,
|
|
505
|
+
);
|
|
506
|
+
return {
|
|
507
|
+
content: [
|
|
508
|
+
{
|
|
509
|
+
type: "text",
|
|
510
|
+
text: JSON.stringify({
|
|
511
|
+
ok: true,
|
|
512
|
+
slug: result.page.slug,
|
|
513
|
+
compileResult: {
|
|
514
|
+
changed: result.compileResult.changed,
|
|
515
|
+
changeType: result.compileResult.changeType,
|
|
516
|
+
changeSummary: result.compileResult.changeSummary,
|
|
517
|
+
confidence: result.compileResult.confidence,
|
|
518
|
+
},
|
|
519
|
+
timelineResult: {
|
|
520
|
+
entriesAdded: result.timelineResult.entries.length,
|
|
521
|
+
confidence: result.timelineResult.confidence,
|
|
522
|
+
},
|
|
523
|
+
updatedAt: result.page.updatedAt,
|
|
524
|
+
}, null, 2),
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
server.registerTool(
|
|
531
|
+
"brain_smart_ingest",
|
|
532
|
+
{
|
|
533
|
+
description: "Full intelligent ingestion: compile truth, extract timeline, create entity links, sync to search. The complete pipeline for processing new content.",
|
|
534
|
+
inputSchema: z.object({
|
|
535
|
+
slug: z.string().describe("Page slug for the content"),
|
|
536
|
+
content: z.string().describe("Full content to ingest"),
|
|
537
|
+
source: z.string().optional().describe("Source identifier"),
|
|
538
|
+
type: z.string().optional().describe("Page type (person, company, project, note, etc.)"),
|
|
539
|
+
}),
|
|
540
|
+
},
|
|
541
|
+
withErrorHandling("brain_smart_ingest", brainSmartIngestHandler),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// Tag Tools
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
const brainTagsHandler = async ({ slug }: { slug: string }) => ({
|
|
549
|
+
content: [
|
|
550
|
+
{ type: "text", text: JSON.stringify(await repo.tags(slug), null, 2) },
|
|
551
|
+
],
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
server.registerTool(
|
|
555
|
+
"brain_tags",
|
|
556
|
+
{
|
|
557
|
+
description: "List tags on a page",
|
|
558
|
+
inputSchema: z.object({ slug: z.string() }),
|
|
559
|
+
},
|
|
560
|
+
withErrorHandling("brain_tags", brainTagsHandler),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const brainTagHandler = async ({ slug, tag, remove }: { slug: string; tag: string; remove?: boolean }) => {
|
|
564
|
+
if (remove) {
|
|
565
|
+
await repo.untag(slug, tag);
|
|
566
|
+
} else {
|
|
567
|
+
await repo.tag(slug, tag);
|
|
568
|
+
}
|
|
569
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
server.registerTool(
|
|
573
|
+
"brain_tag",
|
|
574
|
+
{
|
|
575
|
+
description: "Add or remove a tag from a page",
|
|
576
|
+
inputSchema: z.object({
|
|
577
|
+
slug: z.string(),
|
|
578
|
+
tag: z.string(),
|
|
579
|
+
remove: z.boolean().optional(),
|
|
580
|
+
}),
|
|
581
|
+
},
|
|
582
|
+
withErrorHandling("brain_tag", brainTagHandler),
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// Query & List Tools
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
const brainListHandler = async ({ type, tag, limit }: { type?: string; tag?: string; limit?: number }) => ({
|
|
590
|
+
content: [
|
|
591
|
+
{
|
|
592
|
+
type: "text",
|
|
593
|
+
text: JSON.stringify(
|
|
594
|
+
await repo.listPages({ type, tag, limit: limit ?? 50 }),
|
|
595
|
+
null,
|
|
596
|
+
2,
|
|
597
|
+
),
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
server.registerTool(
|
|
603
|
+
"brain_list",
|
|
604
|
+
{
|
|
605
|
+
description: "List pages with optional filters",
|
|
606
|
+
inputSchema: z.object({
|
|
607
|
+
type: z.string().optional(),
|
|
608
|
+
tag: z.string().optional(),
|
|
609
|
+
limit: z.number().int().positive().optional(),
|
|
610
|
+
}),
|
|
611
|
+
},
|
|
612
|
+
withErrorHandling("brain_list", brainListHandler),
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const brainStatsHandler = async () => ({
|
|
616
|
+
content: [{ type: "text", text: JSON.stringify(await repo.stats(), null, 2) }],
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
server.registerTool(
|
|
620
|
+
"brain_stats",
|
|
621
|
+
{ description: "Show knowledge base statistics", inputSchema: z.object({}) },
|
|
622
|
+
withErrorHandling("brain_stats", brainStatsHandler),
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
const brainRawHandler = async ({ slug, source, data }: { slug: string; source?: string; data?: unknown }) => {
|
|
626
|
+
if (data !== undefined) {
|
|
627
|
+
await repo.writeRaw(slug, source ?? "manual", data);
|
|
628
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
content: [
|
|
632
|
+
{
|
|
633
|
+
type: "text",
|
|
634
|
+
text: JSON.stringify(await repo.readRaw(slug, source), null, 2),
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
};
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
server.registerTool(
|
|
641
|
+
"brain_raw",
|
|
642
|
+
{
|
|
643
|
+
description: "Read or write raw source data for a page",
|
|
644
|
+
inputSchema: z.object({
|
|
645
|
+
slug: z.string(),
|
|
646
|
+
source: z.string().optional(),
|
|
647
|
+
data: z.unknown().optional(),
|
|
648
|
+
}),
|
|
649
|
+
},
|
|
650
|
+
withErrorHandling("brain_raw", brainRawHandler),
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
// Resources
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
const brainIndexHandler = async () => {
|
|
658
|
+
const slugs = await repo.allSlugs();
|
|
659
|
+
return {
|
|
660
|
+
contents: [
|
|
661
|
+
{
|
|
662
|
+
uri: "brain://index",
|
|
663
|
+
mimeType: "text/plain",
|
|
664
|
+
text: slugs.join("\n"),
|
|
665
|
+
},
|
|
666
|
+
],
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
server.registerResource(
|
|
671
|
+
"brain-index",
|
|
672
|
+
"brain://index",
|
|
673
|
+
{ title: "Brain Index", description: "All page slugs grouped in plain list." },
|
|
674
|
+
withResourceErrorHandling("brain-index", brainIndexHandler),
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
const brainPageHandler = async (uri: URL, vars: { slug?: string }) => {
|
|
678
|
+
const slug = String(vars.slug ?? "");
|
|
679
|
+
const page = await repo.getPage(slug);
|
|
680
|
+
return {
|
|
681
|
+
contents: [
|
|
682
|
+
{
|
|
683
|
+
uri: uri.href,
|
|
684
|
+
mimeType: "application/json",
|
|
685
|
+
text: JSON.stringify(page, null, 2),
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
};
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const pageTemplate = new ResourceTemplate("brain://pages/{slug}", {
|
|
692
|
+
list: undefined,
|
|
693
|
+
});
|
|
694
|
+
server.registerResource(
|
|
695
|
+
"brain-page",
|
|
696
|
+
pageTemplate,
|
|
697
|
+
{ title: "Brain Page", description: "Single page JSON resource." },
|
|
698
|
+
withResourceErrorHandling("brain-page", brainPageHandler),
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
const transport = new StdioServerTransport();
|
|
702
|
+
await server.connect(transport);
|
|
703
|
+
}
|