@nguyentamdat/mempalace 1.0.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 +127 -0
- package/hooks/README.md +133 -0
- package/hooks/mempal_precompact_hook.sh +35 -0
- package/hooks/mempal_save_hook.sh +80 -0
- package/package.json +36 -0
- package/src/cli.ts +50 -0
- package/src/commands/compress.ts +161 -0
- package/src/commands/init.ts +40 -0
- package/src/commands/mine.ts +51 -0
- package/src/commands/search.ts +23 -0
- package/src/commands/split.ts +20 -0
- package/src/commands/status.ts +12 -0
- package/src/commands/wake-up.ts +20 -0
- package/src/config.ts +111 -0
- package/src/convo-miner.ts +373 -0
- package/src/dialect.ts +921 -0
- package/src/entity-detector.d.ts +25 -0
- package/src/entity-detector.ts +674 -0
- package/src/entity-registry.ts +806 -0
- package/src/general-extractor.ts +487 -0
- package/src/index.ts +5 -0
- package/src/knowledge-graph.ts +461 -0
- package/src/layers.ts +512 -0
- package/src/mcp-server.ts +1034 -0
- package/src/miner.ts +612 -0
- package/src/missing-modules.d.ts +43 -0
- package/src/normalize.ts +374 -0
- package/src/onboarding.ts +485 -0
- package/src/palace-graph.ts +310 -0
- package/src/room-detector-local.ts +415 -0
- package/src/room-detector.d.ts +1 -0
- package/src/room-detector.ts +6 -0
- package/src/searcher.ts +181 -0
- package/src/spellcheck.ts +200 -0
- package/src/split-mega-files.d.ts +8 -0
- package/src/split-mega-files.ts +297 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import {
|
|
6
|
+
ChromaClient,
|
|
7
|
+
type Collection,
|
|
8
|
+
DefaultEmbeddingFunction,
|
|
9
|
+
type GetParams,
|
|
10
|
+
IncludeEnum,
|
|
11
|
+
type Metadata,
|
|
12
|
+
type QueryResponse,
|
|
13
|
+
} from "chromadb";
|
|
14
|
+
import { MempalaceConfig } from "./config";
|
|
15
|
+
import { KnowledgeGraph } from "./knowledge-graph";
|
|
16
|
+
import { findTunnels, graphStats, traverse } from "./palace-graph";
|
|
17
|
+
import { searchMemories } from "./searcher";
|
|
18
|
+
|
|
19
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
20
|
+
type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
21
|
+
|
|
22
|
+
interface JsonObject {
|
|
23
|
+
[key: string]: JsonValue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JsonSchemaProperty {
|
|
27
|
+
type: "string" | "number" | "integer" | "boolean" | "object" | "array";
|
|
28
|
+
description?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface InputSchema {
|
|
32
|
+
type: "object";
|
|
33
|
+
properties: Record<string, JsonSchemaProperty>;
|
|
34
|
+
required?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type ToolHandler = (args: Record<string, unknown>) => Promise<JsonObject>;
|
|
38
|
+
|
|
39
|
+
interface ToolDefinition {
|
|
40
|
+
description: string;
|
|
41
|
+
input_schema: InputSchema;
|
|
42
|
+
handler: ToolHandler;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface JsonRpcRequest {
|
|
46
|
+
jsonrpc?: string;
|
|
47
|
+
id?: JsonValue;
|
|
48
|
+
method?: string;
|
|
49
|
+
params?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface JsonRpcSuccessResponse {
|
|
53
|
+
jsonrpc: "2.0";
|
|
54
|
+
id?: JsonValue;
|
|
55
|
+
result: JsonObject;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface JsonRpcErrorResponse {
|
|
59
|
+
jsonrpc: "2.0";
|
|
60
|
+
id?: JsonValue;
|
|
61
|
+
error: {
|
|
62
|
+
code: number;
|
|
63
|
+
message: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
|
|
68
|
+
type DrawerCollection = Collection;
|
|
69
|
+
type DiaryEntry = { date: string; timestamp: string; topic: string; content: string };
|
|
70
|
+
type KgDirection = "outgoing" | "incoming" | "both";
|
|
71
|
+
|
|
72
|
+
interface PalaceGraphCollection {
|
|
73
|
+
count: () => Promise<number>;
|
|
74
|
+
get: (args: {
|
|
75
|
+
limit: number;
|
|
76
|
+
offset: number;
|
|
77
|
+
include: string[];
|
|
78
|
+
}) => Promise<{
|
|
79
|
+
metadatas?: Array<Record<string, unknown> | null>;
|
|
80
|
+
ids?: string[];
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const kg = new KnowledgeGraph();
|
|
85
|
+
const config = new MempalaceConfig();
|
|
86
|
+
const embeddingFunction = new DefaultEmbeddingFunction();
|
|
87
|
+
|
|
88
|
+
export const PALACE_PROTOCOL = `IMPORTANT — MemPalace Memory Protocol:
|
|
89
|
+
1. ON WAKE-UP: Call mempalace_status to load palace overview + AAAK spec.
|
|
90
|
+
2. BEFORE RESPONDING about any person, project, or past event: call mempalace_kg_query or mempalace_search FIRST. Never guess — verify.
|
|
91
|
+
3. IF UNSURE about a fact (name, gender, age, relationship): say "let me check" and query the palace. Wrong is worse than slow.
|
|
92
|
+
4. AFTER EACH SESSION: call mempalace_diary_write to record what happened, what you learned, what matters.
|
|
93
|
+
5. WHEN FACTS CHANGE: call mempalace_kg_invalidate on the old fact, mempalace_kg_add for the new one.
|
|
94
|
+
|
|
95
|
+
This protocol ensures the AI KNOWS before it speaks. Storage is not memory — but storage + this protocol = memory.`;
|
|
96
|
+
|
|
97
|
+
export const AAAK_SPEC = `AAAK is a compressed memory dialect that MemPalace uses for efficient storage.
|
|
98
|
+
It is designed to be readable by both humans and LLMs without decoding.
|
|
99
|
+
|
|
100
|
+
FORMAT:
|
|
101
|
+
ENTITIES: 3-letter uppercase codes. ALC=Alice, JOR=Jordan, RIL=Riley, MAX=Max, BEN=Ben.
|
|
102
|
+
EMOTIONS: *action markers* before/during text. *warm*=joy, *fierce*=determined, *raw*=vulnerable, *bloom*=tenderness.
|
|
103
|
+
STRUCTURE: Pipe-separated fields. FAM: family | PROJ: projects | ⚠: warnings/reminders.
|
|
104
|
+
DATES: ISO format (2026-03-31). COUNTS: Nx = N mentions (e.g., 570x).
|
|
105
|
+
IMPORTANCE: ★ to ★★★★★ (1-5 scale).
|
|
106
|
+
HALLS: hall_facts, hall_events, hall_discoveries, hall_preferences, hall_advice.
|
|
107
|
+
WINGS: wing_user, wing_agent, wing_team, wing_code, wing_myproject, wing_hardware, wing_ue5, wing_ai_research.
|
|
108
|
+
ROOMS: Hyphenated slugs representing named ideas (e.g., chromadb-setup, gpu-pricing).
|
|
109
|
+
|
|
110
|
+
EXAMPLE:
|
|
111
|
+
FAM: ALC→♡JOR | 2D(kids): RIL(18,sports) MAX(11,chess+swimming) | BEN(contributor)
|
|
112
|
+
|
|
113
|
+
Read AAAK naturally — expand codes mentally, treat *markers* as emotional context.
|
|
114
|
+
When WRITING AAAK: use entity codes, mark emotions, keep structure tight.`;
|
|
115
|
+
|
|
116
|
+
function logInfo(message: string): void {
|
|
117
|
+
process.stderr.write(`${message}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function logError(message: string): void {
|
|
121
|
+
process.stderr.write(`${message}\n`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function noPalace(): JsonObject {
|
|
125
|
+
return {
|
|
126
|
+
error: "No palace found",
|
|
127
|
+
palace_path: config.palacePath,
|
|
128
|
+
hint: "Run: mempalace init <dir> && mempalace mine <dir>",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
133
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function toOptionalString(value: unknown): string | null {
|
|
137
|
+
return typeof value === "string" ? value : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireString(value: unknown, name: string): string {
|
|
141
|
+
if (typeof value !== "string") {
|
|
142
|
+
throw new Error(`Expected '${name}' to be a string`);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function toInteger(value: unknown, fallback: number): number {
|
|
148
|
+
if (value === undefined) {
|
|
149
|
+
return fallback;
|
|
150
|
+
}
|
|
151
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
152
|
+
throw new Error("Expected an integer value");
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toNumber(value: unknown, fallback: number): number {
|
|
158
|
+
if (value === undefined) {
|
|
159
|
+
return fallback;
|
|
160
|
+
}
|
|
161
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
162
|
+
throw new Error("Expected a numeric value");
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function toDirection(value: unknown): KgDirection {
|
|
168
|
+
if (value === undefined) {
|
|
169
|
+
return "both";
|
|
170
|
+
}
|
|
171
|
+
if (value === "outgoing" || value === "incoming" || value === "both") {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
throw new Error("Expected direction to be 'outgoing', 'incoming', or 'both'");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function getCollection(create = false): Promise<DrawerCollection | null> {
|
|
178
|
+
try {
|
|
179
|
+
const client = new ChromaClient();
|
|
180
|
+
if (create) {
|
|
181
|
+
return await client.getOrCreateCollection({
|
|
182
|
+
name: config.collectionName,
|
|
183
|
+
embeddingFunction,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return await client.getCollection({
|
|
187
|
+
name: config.collectionName,
|
|
188
|
+
embeddingFunction,
|
|
189
|
+
});
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function getAllMetadatas(col: DrawerCollection): Promise<Metadata[]> {
|
|
196
|
+
const total = await col.count();
|
|
197
|
+
if (total === 0) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const metadatas: Metadata[] = [];
|
|
202
|
+
let offset = 0;
|
|
203
|
+
|
|
204
|
+
while (offset < total) {
|
|
205
|
+
const batch = await col.get({
|
|
206
|
+
limit: Math.min(1000, total - offset),
|
|
207
|
+
offset,
|
|
208
|
+
include: [IncludeEnum.Metadatas],
|
|
209
|
+
});
|
|
210
|
+
for (const meta of batch.metadatas) {
|
|
211
|
+
if (meta) {
|
|
212
|
+
metadatas.push(meta);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (batch.ids.length === 0) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
offset += batch.ids.length;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return metadatas;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isMultiQueryResponse(response: QueryResponse): response is QueryResponse & {
|
|
225
|
+
ids: string[][];
|
|
226
|
+
documents: (string | null)[][] | null;
|
|
227
|
+
metadatas: (Metadata | null)[][] | null;
|
|
228
|
+
distances: number[][] | null;
|
|
229
|
+
} {
|
|
230
|
+
return Array.isArray(response.ids[0]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function toPalaceGraphCollection(col: DrawerCollection): PalaceGraphCollection {
|
|
234
|
+
return {
|
|
235
|
+
count: async () => col.count(),
|
|
236
|
+
get: async ({ limit, offset, include }) => {
|
|
237
|
+
const mappedInclude = include.flatMap((value) => {
|
|
238
|
+
if (value === IncludeEnum.Metadatas) {
|
|
239
|
+
return [IncludeEnum.Metadatas];
|
|
240
|
+
}
|
|
241
|
+
if (value === IncludeEnum.Documents) {
|
|
242
|
+
return [IncludeEnum.Documents];
|
|
243
|
+
}
|
|
244
|
+
if (value === IncludeEnum.Distances) {
|
|
245
|
+
return [IncludeEnum.Distances];
|
|
246
|
+
}
|
|
247
|
+
return [];
|
|
248
|
+
});
|
|
249
|
+
const response = await col.get({ limit, offset, include: mappedInclude });
|
|
250
|
+
return {
|
|
251
|
+
metadatas: response.metadatas.map((meta) => (meta ? { ...meta } : null)),
|
|
252
|
+
ids: response.ids,
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function toolStatus(): Promise<JsonObject> {
|
|
259
|
+
const col = await getCollection();
|
|
260
|
+
if (!col) {
|
|
261
|
+
return noPalace();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const count = await col.count();
|
|
265
|
+
const wings: Record<string, number> = {};
|
|
266
|
+
const rooms: Record<string, number> = {};
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const allMeta = await getAllMetadatas(col);
|
|
270
|
+
for (const meta of allMeta) {
|
|
271
|
+
const wing = String(meta.wing ?? "unknown");
|
|
272
|
+
const room = String(meta.room ?? "unknown");
|
|
273
|
+
wings[wing] = (wings[wing] ?? 0) + 1;
|
|
274
|
+
rooms[room] = (rooms[room] ?? 0) + 1;
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
total_drawers: count,
|
|
281
|
+
wings,
|
|
282
|
+
rooms,
|
|
283
|
+
palace_path: config.palacePath,
|
|
284
|
+
protocol: PALACE_PROTOCOL,
|
|
285
|
+
aaak_dialect: AAAK_SPEC,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function toolListWings(): Promise<JsonObject> {
|
|
290
|
+
const col = await getCollection();
|
|
291
|
+
if (!col) {
|
|
292
|
+
return noPalace();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const wings: Record<string, number> = {};
|
|
296
|
+
try {
|
|
297
|
+
const allMeta = await getAllMetadatas(col);
|
|
298
|
+
for (const meta of allMeta) {
|
|
299
|
+
const wing = String(meta.wing ?? "unknown");
|
|
300
|
+
wings[wing] = (wings[wing] ?? 0) + 1;
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { wings };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function toolListRooms(wing: string | null = null): Promise<JsonObject> {
|
|
309
|
+
const col = await getCollection();
|
|
310
|
+
if (!col) {
|
|
311
|
+
return noPalace();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const rooms: Record<string, number> = {};
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const params: GetParams = {
|
|
318
|
+
include: [IncludeEnum.Metadatas],
|
|
319
|
+
};
|
|
320
|
+
if (wing) {
|
|
321
|
+
params.where = { wing };
|
|
322
|
+
}
|
|
323
|
+
const results = await col.get(params);
|
|
324
|
+
for (const meta of results.metadatas) {
|
|
325
|
+
if (!meta) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const room = String(meta.room ?? "unknown");
|
|
329
|
+
rooms[room] = (rooms[room] ?? 0) + 1;
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { wing: wing ?? "all", rooms };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function toolGetTaxonomy(): Promise<JsonObject> {
|
|
338
|
+
const col = await getCollection();
|
|
339
|
+
if (!col) {
|
|
340
|
+
return noPalace();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const taxonomy: Record<string, Record<string, number>> = {};
|
|
344
|
+
try {
|
|
345
|
+
const allMeta = await getAllMetadatas(col);
|
|
346
|
+
for (const meta of allMeta) {
|
|
347
|
+
const wing = String(meta.wing ?? "unknown");
|
|
348
|
+
const room = String(meta.room ?? "unknown");
|
|
349
|
+
taxonomy[wing] ??= {};
|
|
350
|
+
taxonomy[wing][room] = (taxonomy[wing][room] ?? 0) + 1;
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { taxonomy };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function toolSearch(
|
|
359
|
+
query: string,
|
|
360
|
+
limit = 5,
|
|
361
|
+
wing: string | null = null,
|
|
362
|
+
room: string | null = null,
|
|
363
|
+
): Promise<JsonObject> {
|
|
364
|
+
const result = await searchMemories({
|
|
365
|
+
query,
|
|
366
|
+
palacePath: config.palacePath,
|
|
367
|
+
wing: wing ?? undefined,
|
|
368
|
+
room: room ?? undefined,
|
|
369
|
+
nResults: limit,
|
|
370
|
+
});
|
|
371
|
+
return result as unknown as JsonObject;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function toolCheckDuplicate(content: string, threshold = 0.9): Promise<JsonObject> {
|
|
375
|
+
const col = await getCollection();
|
|
376
|
+
if (!col) {
|
|
377
|
+
return noPalace();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const results = await col.query({
|
|
382
|
+
queryTexts: [content],
|
|
383
|
+
nResults: 5,
|
|
384
|
+
include: [IncludeEnum.Metadatas, IncludeEnum.Documents, IncludeEnum.Distances],
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (!isMultiQueryResponse(results)) {
|
|
388
|
+
return { is_duplicate: false, matches: [] };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const ids = results.ids[0] ?? [];
|
|
392
|
+
const distances = results.distances?.[0] ?? [];
|
|
393
|
+
const metadatas = results.metadatas?.[0] ?? [];
|
|
394
|
+
const documents = results.documents?.[0] ?? [];
|
|
395
|
+
|
|
396
|
+
const duplicates: JsonObject[] = [];
|
|
397
|
+
for (let index = 0; index < ids.length; index += 1) {
|
|
398
|
+
const drawerId = ids[index];
|
|
399
|
+
const distance = distances[index] ?? 1;
|
|
400
|
+
const similarity = Math.round((1 - distance) * 1000) / 1000;
|
|
401
|
+
if (similarity < threshold) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const meta = metadatas[index] ?? {};
|
|
406
|
+
const document = String(documents[index] ?? "");
|
|
407
|
+
duplicates.push({
|
|
408
|
+
id: drawerId,
|
|
409
|
+
wing: String(meta.wing ?? "?"),
|
|
410
|
+
room: String(meta.room ?? "?"),
|
|
411
|
+
similarity,
|
|
412
|
+
content: document.length > 200 ? `${document.slice(0, 200)}...` : document,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
is_duplicate: duplicates.length > 0,
|
|
418
|
+
matches: duplicates,
|
|
419
|
+
};
|
|
420
|
+
} catch (error) {
|
|
421
|
+
return { error: String(error) };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function toolGetAaakSpec(): Promise<JsonObject> {
|
|
426
|
+
return { aaak_spec: AAAK_SPEC };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function toolTraverseGraph(startRoom: string, maxHops = 2): Promise<JsonObject> {
|
|
430
|
+
const col = await getCollection();
|
|
431
|
+
if (!col) {
|
|
432
|
+
return noPalace();
|
|
433
|
+
}
|
|
434
|
+
return (await traverse(startRoom, toPalaceGraphCollection(col), undefined, maxHops)) as unknown as JsonObject;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function toolFindTunnels(wingA: string | null = null, wingB: string | null = null): Promise<JsonObject> {
|
|
438
|
+
const col = await getCollection();
|
|
439
|
+
if (!col) {
|
|
440
|
+
return noPalace();
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
tunnels: await findTunnels(wingA, wingB, toPalaceGraphCollection(col)),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function toolGraphStats(): Promise<JsonObject> {
|
|
448
|
+
const col = await getCollection();
|
|
449
|
+
if (!col) {
|
|
450
|
+
return noPalace();
|
|
451
|
+
}
|
|
452
|
+
return (await graphStats(toPalaceGraphCollection(col))) as unknown as JsonObject;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function toolAddDrawer(
|
|
456
|
+
wing: string,
|
|
457
|
+
room: string,
|
|
458
|
+
content: string,
|
|
459
|
+
sourceFile: string | null = null,
|
|
460
|
+
addedBy = "mcp",
|
|
461
|
+
): Promise<JsonObject> {
|
|
462
|
+
const col = await getCollection(true);
|
|
463
|
+
if (!col) {
|
|
464
|
+
return noPalace();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const dup = await toolCheckDuplicate(content, 0.9);
|
|
468
|
+
if (dup.is_duplicate === true) {
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
reason: "duplicate",
|
|
472
|
+
matches: Array.isArray(dup.matches) ? dup.matches : [],
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const now = new Date();
|
|
477
|
+
const hash = createHash("md5")
|
|
478
|
+
.update(`${content.slice(0, 100)}${now.toISOString()}`)
|
|
479
|
+
.digest("hex")
|
|
480
|
+
.slice(0, 16);
|
|
481
|
+
const drawerId = `drawer_${wing}_${room}_${hash}`;
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
await col.add({
|
|
485
|
+
ids: [drawerId],
|
|
486
|
+
documents: [content],
|
|
487
|
+
metadatas: [
|
|
488
|
+
{
|
|
489
|
+
wing,
|
|
490
|
+
room,
|
|
491
|
+
source_file: sourceFile ?? "",
|
|
492
|
+
chunk_index: 0,
|
|
493
|
+
added_by: addedBy,
|
|
494
|
+
filed_at: now.toISOString(),
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
});
|
|
498
|
+
logInfo(`Filed drawer: ${drawerId} → ${wing}/${room}`);
|
|
499
|
+
return { success: true, drawer_id: drawerId, wing, room };
|
|
500
|
+
} catch (error) {
|
|
501
|
+
return { success: false, error: String(error) };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function toolDeleteDrawer(drawerId: string): Promise<JsonObject> {
|
|
506
|
+
const col = await getCollection();
|
|
507
|
+
if (!col) {
|
|
508
|
+
return noPalace();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const existing = await col.get({ ids: [drawerId] });
|
|
512
|
+
if (existing.ids.length === 0) {
|
|
513
|
+
return { success: false, error: `Drawer not found: ${drawerId}` };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
await col.delete({ ids: [drawerId] });
|
|
518
|
+
logInfo(`Deleted drawer: ${drawerId}`);
|
|
519
|
+
return { success: true, drawer_id: drawerId };
|
|
520
|
+
} catch (error) {
|
|
521
|
+
return { success: false, error: String(error) };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function toolKgQuery(
|
|
526
|
+
entity: string,
|
|
527
|
+
asOf: string | null = null,
|
|
528
|
+
direction: KgDirection = "both",
|
|
529
|
+
): Promise<JsonObject> {
|
|
530
|
+
const facts = kg.queryEntity(entity, asOf, direction);
|
|
531
|
+
return {
|
|
532
|
+
entity,
|
|
533
|
+
as_of: asOf,
|
|
534
|
+
facts: facts as unknown as JsonValue[],
|
|
535
|
+
count: facts.length,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function toolKgAdd(
|
|
540
|
+
subject: string,
|
|
541
|
+
predicate: string,
|
|
542
|
+
object: string,
|
|
543
|
+
validFrom: string | null = null,
|
|
544
|
+
sourceCloset: string | null = null,
|
|
545
|
+
): Promise<JsonObject> {
|
|
546
|
+
const tripleId = kg.addTriple(subject, predicate, object, validFrom, null, 1.0, sourceCloset, null);
|
|
547
|
+
return {
|
|
548
|
+
success: true,
|
|
549
|
+
triple_id: tripleId,
|
|
550
|
+
fact: `${subject} → ${predicate} → ${object}`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function toolKgInvalidate(
|
|
555
|
+
subject: string,
|
|
556
|
+
predicate: string,
|
|
557
|
+
object: string,
|
|
558
|
+
ended: string | null = null,
|
|
559
|
+
): Promise<JsonObject> {
|
|
560
|
+
kg.invalidateTriple(subject, predicate, object, ended);
|
|
561
|
+
return {
|
|
562
|
+
success: true,
|
|
563
|
+
fact: `${subject} → ${predicate} → ${object}`,
|
|
564
|
+
ended: ended ?? "today",
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function toolKgTimeline(entity: string | null = null): Promise<JsonObject> {
|
|
569
|
+
const timeline = kg.getTimeline(entity);
|
|
570
|
+
return {
|
|
571
|
+
entity: entity ?? "all",
|
|
572
|
+
timeline: timeline as unknown as JsonValue[],
|
|
573
|
+
count: timeline.length,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function toolKgStats(): Promise<JsonObject> {
|
|
578
|
+
return kg.getStats() as unknown as JsonObject;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function toolDiaryWrite(agentName: string, entry: string, topic = "general"): Promise<JsonObject> {
|
|
582
|
+
const wing = `wing_${agentName.toLowerCase().replaceAll(" ", "_")}`;
|
|
583
|
+
const room = "diary";
|
|
584
|
+
const col = await getCollection(true);
|
|
585
|
+
if (!col) {
|
|
586
|
+
return noPalace();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const now = new Date();
|
|
590
|
+
const dateStamp = now.toISOString().slice(0, 10).replaceAll("-", "");
|
|
591
|
+
const timeStamp = now.toISOString().slice(11, 19).replaceAll(":", "");
|
|
592
|
+
const hash = createHash("md5").update(entry.slice(0, 50)).digest("hex").slice(0, 8);
|
|
593
|
+
const entryId = `diary_${wing}_${dateStamp}_${timeStamp}_${hash}`;
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
await col.add({
|
|
597
|
+
ids: [entryId],
|
|
598
|
+
documents: [entry],
|
|
599
|
+
metadatas: [
|
|
600
|
+
{
|
|
601
|
+
wing,
|
|
602
|
+
room,
|
|
603
|
+
hall: "hall_diary",
|
|
604
|
+
topic,
|
|
605
|
+
type: "diary_entry",
|
|
606
|
+
agent: agentName,
|
|
607
|
+
filed_at: now.toISOString(),
|
|
608
|
+
date: now.toISOString().slice(0, 10),
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
logInfo(`Diary entry: ${entryId} → ${wing}/diary/${topic}`);
|
|
613
|
+
return {
|
|
614
|
+
success: true,
|
|
615
|
+
entry_id: entryId,
|
|
616
|
+
agent: agentName,
|
|
617
|
+
topic,
|
|
618
|
+
timestamp: now.toISOString(),
|
|
619
|
+
};
|
|
620
|
+
} catch (error) {
|
|
621
|
+
return { success: false, error: String(error) };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function toolDiaryRead(agentName: string, lastN = 10): Promise<JsonObject> {
|
|
626
|
+
const wing = `wing_${agentName.toLowerCase().replaceAll(" ", "_")}`;
|
|
627
|
+
const col = await getCollection();
|
|
628
|
+
if (!col) {
|
|
629
|
+
return noPalace();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
const results = await col.get({
|
|
634
|
+
where: { $and: [{ wing }, { room: "diary" }] },
|
|
635
|
+
include: [IncludeEnum.Documents, IncludeEnum.Metadatas],
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
if (results.ids.length === 0) {
|
|
639
|
+
return { agent: agentName, entries: [], message: "No diary entries yet." };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const entries: DiaryEntry[] = [];
|
|
643
|
+
for (let index = 0; index < results.ids.length; index += 1) {
|
|
644
|
+
const meta = results.metadatas[index];
|
|
645
|
+
const document = results.documents[index];
|
|
646
|
+
entries.push({
|
|
647
|
+
date: String(meta?.date ?? ""),
|
|
648
|
+
timestamp: String(meta?.filed_at ?? ""),
|
|
649
|
+
topic: String(meta?.topic ?? ""),
|
|
650
|
+
content: String(document ?? ""),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
entries.sort((left, right) => right.timestamp.localeCompare(left.timestamp));
|
|
655
|
+
const sliced = entries.slice(0, lastN);
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
agent: agentName,
|
|
659
|
+
entries: sliced as unknown as JsonValue[],
|
|
660
|
+
total: results.ids.length,
|
|
661
|
+
showing: sliced.length,
|
|
662
|
+
};
|
|
663
|
+
} catch (error) {
|
|
664
|
+
return { error: String(error) };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export const TOOLS: Record<string, ToolDefinition> = {
|
|
669
|
+
mempalace_status: {
|
|
670
|
+
description: "Palace overview — total drawers, wing and room counts",
|
|
671
|
+
input_schema: { type: "object", properties: {} },
|
|
672
|
+
handler: async () => toolStatus(),
|
|
673
|
+
},
|
|
674
|
+
mempalace_list_wings: {
|
|
675
|
+
description: "List all wings with drawer counts",
|
|
676
|
+
input_schema: { type: "object", properties: {} },
|
|
677
|
+
handler: async () => toolListWings(),
|
|
678
|
+
},
|
|
679
|
+
mempalace_list_rooms: {
|
|
680
|
+
description: "List rooms within a wing (or all rooms if no wing given)",
|
|
681
|
+
input_schema: {
|
|
682
|
+
type: "object",
|
|
683
|
+
properties: {
|
|
684
|
+
wing: { type: "string", description: "Wing to list rooms for (optional)" },
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
handler: async (args) => toolListRooms(toOptionalString(args.wing)),
|
|
688
|
+
},
|
|
689
|
+
mempalace_get_taxonomy: {
|
|
690
|
+
description: "Full taxonomy: wing → room → drawer count",
|
|
691
|
+
input_schema: { type: "object", properties: {} },
|
|
692
|
+
handler: async () => toolGetTaxonomy(),
|
|
693
|
+
},
|
|
694
|
+
mempalace_get_aaak_spec: {
|
|
695
|
+
description:
|
|
696
|
+
"Get the AAAK dialect specification — the compressed memory format MemPalace uses. Call this if you need to read or write AAAK-compressed memories.",
|
|
697
|
+
input_schema: { type: "object", properties: {} },
|
|
698
|
+
handler: async () => toolGetAaakSpec(),
|
|
699
|
+
},
|
|
700
|
+
mempalace_kg_query: {
|
|
701
|
+
description:
|
|
702
|
+
"Query the knowledge graph for an entity's relationships. Returns typed facts with temporal validity. E.g. 'Max' → child_of Alice, loves chess, does swimming. Filter by date with as_of to see what was true at a point in time.",
|
|
703
|
+
input_schema: {
|
|
704
|
+
type: "object",
|
|
705
|
+
properties: {
|
|
706
|
+
entity: { type: "string", description: "Entity to query (e.g. 'Max', 'MyProject', 'Alice')" },
|
|
707
|
+
as_of: {
|
|
708
|
+
type: "string",
|
|
709
|
+
description: "Date filter — only facts valid at this date (YYYY-MM-DD, optional)",
|
|
710
|
+
},
|
|
711
|
+
direction: {
|
|
712
|
+
type: "string",
|
|
713
|
+
description: "outgoing (entity→?), incoming (?→entity), or both (default: both)",
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
required: ["entity"],
|
|
717
|
+
},
|
|
718
|
+
handler: async (args) =>
|
|
719
|
+
toolKgQuery(requireString(args.entity, "entity"), toOptionalString(args.as_of), toDirection(args.direction)),
|
|
720
|
+
},
|
|
721
|
+
mempalace_kg_add: {
|
|
722
|
+
description:
|
|
723
|
+
"Add a fact to the knowledge graph. Subject → predicate → object with optional time window. E.g. ('Max', 'started_school', 'Year 7', valid_from='2026-09-01').",
|
|
724
|
+
input_schema: {
|
|
725
|
+
type: "object",
|
|
726
|
+
properties: {
|
|
727
|
+
subject: { type: "string", description: "The entity doing/being something" },
|
|
728
|
+
predicate: {
|
|
729
|
+
type: "string",
|
|
730
|
+
description: "The relationship type (e.g. 'loves', 'works_on', 'daughter_of')",
|
|
731
|
+
},
|
|
732
|
+
object: { type: "string", description: "The entity being connected to" },
|
|
733
|
+
valid_from: { type: "string", description: "When this became true (YYYY-MM-DD, optional)" },
|
|
734
|
+
source_closet: { type: "string", description: "Closet ID where this fact appears (optional)" },
|
|
735
|
+
},
|
|
736
|
+
required: ["subject", "predicate", "object"],
|
|
737
|
+
},
|
|
738
|
+
handler: async (args) =>
|
|
739
|
+
toolKgAdd(
|
|
740
|
+
requireString(args.subject, "subject"),
|
|
741
|
+
requireString(args.predicate, "predicate"),
|
|
742
|
+
requireString(args.object, "object"),
|
|
743
|
+
toOptionalString(args.valid_from),
|
|
744
|
+
toOptionalString(args.source_closet),
|
|
745
|
+
),
|
|
746
|
+
},
|
|
747
|
+
mempalace_kg_invalidate: {
|
|
748
|
+
description: "Mark a fact as no longer true. E.g. ankle injury resolved, job ended, moved house.",
|
|
749
|
+
input_schema: {
|
|
750
|
+
type: "object",
|
|
751
|
+
properties: {
|
|
752
|
+
subject: { type: "string", description: "Entity" },
|
|
753
|
+
predicate: { type: "string", description: "Relationship" },
|
|
754
|
+
object: { type: "string", description: "Connected entity" },
|
|
755
|
+
ended: { type: "string", description: "When it stopped being true (YYYY-MM-DD, default: today)" },
|
|
756
|
+
},
|
|
757
|
+
required: ["subject", "predicate", "object"],
|
|
758
|
+
},
|
|
759
|
+
handler: async (args) =>
|
|
760
|
+
toolKgInvalidate(
|
|
761
|
+
requireString(args.subject, "subject"),
|
|
762
|
+
requireString(args.predicate, "predicate"),
|
|
763
|
+
requireString(args.object, "object"),
|
|
764
|
+
toOptionalString(args.ended),
|
|
765
|
+
),
|
|
766
|
+
},
|
|
767
|
+
mempalace_kg_timeline: {
|
|
768
|
+
description: "Chronological timeline of facts. Shows the story of an entity (or everything) in order.",
|
|
769
|
+
input_schema: {
|
|
770
|
+
type: "object",
|
|
771
|
+
properties: {
|
|
772
|
+
entity: { type: "string", description: "Entity to get timeline for (optional — omit for full timeline)" },
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
handler: async (args) => toolKgTimeline(toOptionalString(args.entity)),
|
|
776
|
+
},
|
|
777
|
+
mempalace_kg_stats: {
|
|
778
|
+
description: "Knowledge graph overview: entities, triples, current vs expired facts, relationship types.",
|
|
779
|
+
input_schema: { type: "object", properties: {} },
|
|
780
|
+
handler: async () => toolKgStats(),
|
|
781
|
+
},
|
|
782
|
+
mempalace_traverse: {
|
|
783
|
+
description:
|
|
784
|
+
"Walk the palace graph from a room. Shows connected ideas across wings — the tunnels. Like following a thread through the palace: start at 'chromadb-setup' in wing_code, discover it connects to wing_myproject (planning) and wing_user (feelings about it).",
|
|
785
|
+
input_schema: {
|
|
786
|
+
type: "object",
|
|
787
|
+
properties: {
|
|
788
|
+
start_room: {
|
|
789
|
+
type: "string",
|
|
790
|
+
description: "Room to start from (e.g. 'chromadb-setup', 'riley-school')",
|
|
791
|
+
},
|
|
792
|
+
max_hops: { type: "integer", description: "How many connections to follow (default: 2)" },
|
|
793
|
+
},
|
|
794
|
+
required: ["start_room"],
|
|
795
|
+
},
|
|
796
|
+
handler: async (args) => toolTraverseGraph(requireString(args.start_room, "start_room"), toInteger(args.max_hops, 2)),
|
|
797
|
+
},
|
|
798
|
+
mempalace_find_tunnels: {
|
|
799
|
+
description:
|
|
800
|
+
"Find rooms that bridge two wings — the hallways connecting different domains. E.g. what topics connect wing_code to wing_team?",
|
|
801
|
+
input_schema: {
|
|
802
|
+
type: "object",
|
|
803
|
+
properties: {
|
|
804
|
+
wing_a: { type: "string", description: "First wing (optional)" },
|
|
805
|
+
wing_b: { type: "string", description: "Second wing (optional)" },
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
handler: async (args) => toolFindTunnels(toOptionalString(args.wing_a), toOptionalString(args.wing_b)),
|
|
809
|
+
},
|
|
810
|
+
mempalace_graph_stats: {
|
|
811
|
+
description: "Palace graph overview: total rooms, tunnel connections, edges between wings.",
|
|
812
|
+
input_schema: { type: "object", properties: {} },
|
|
813
|
+
handler: async () => toolGraphStats(),
|
|
814
|
+
},
|
|
815
|
+
mempalace_search: {
|
|
816
|
+
description: "Semantic search. Returns verbatim drawer content with similarity scores.",
|
|
817
|
+
input_schema: {
|
|
818
|
+
type: "object",
|
|
819
|
+
properties: {
|
|
820
|
+
query: { type: "string", description: "What to search for" },
|
|
821
|
+
limit: { type: "integer", description: "Max results (default 5)" },
|
|
822
|
+
wing: { type: "string", description: "Filter by wing (optional)" },
|
|
823
|
+
room: { type: "string", description: "Filter by room (optional)" },
|
|
824
|
+
},
|
|
825
|
+
required: ["query"],
|
|
826
|
+
},
|
|
827
|
+
handler: async (args) =>
|
|
828
|
+
toolSearch(
|
|
829
|
+
requireString(args.query, "query"),
|
|
830
|
+
toInteger(args.limit, 5),
|
|
831
|
+
toOptionalString(args.wing),
|
|
832
|
+
toOptionalString(args.room),
|
|
833
|
+
),
|
|
834
|
+
},
|
|
835
|
+
mempalace_check_duplicate: {
|
|
836
|
+
description: "Check if content already exists in the palace before filing",
|
|
837
|
+
input_schema: {
|
|
838
|
+
type: "object",
|
|
839
|
+
properties: {
|
|
840
|
+
content: { type: "string", description: "Content to check" },
|
|
841
|
+
threshold: { type: "number", description: "Similarity threshold 0-1 (default 0.9)" },
|
|
842
|
+
},
|
|
843
|
+
required: ["content"],
|
|
844
|
+
},
|
|
845
|
+
handler: async (args) => toolCheckDuplicate(requireString(args.content, "content"), toNumber(args.threshold, 0.9)),
|
|
846
|
+
},
|
|
847
|
+
mempalace_add_drawer: {
|
|
848
|
+
description: "File verbatim content into the palace. Checks for duplicates first.",
|
|
849
|
+
input_schema: {
|
|
850
|
+
type: "object",
|
|
851
|
+
properties: {
|
|
852
|
+
wing: { type: "string", description: "Wing (project name)" },
|
|
853
|
+
room: { type: "string", description: "Room (aspect: backend, decisions, meetings...)" },
|
|
854
|
+
content: {
|
|
855
|
+
type: "string",
|
|
856
|
+
description: "Verbatim content to store — exact words, never summarized",
|
|
857
|
+
},
|
|
858
|
+
source_file: { type: "string", description: "Where this came from (optional)" },
|
|
859
|
+
added_by: { type: "string", description: "Who is filing this (default: mcp)" },
|
|
860
|
+
},
|
|
861
|
+
required: ["wing", "room", "content"],
|
|
862
|
+
},
|
|
863
|
+
handler: async (args) =>
|
|
864
|
+
toolAddDrawer(
|
|
865
|
+
requireString(args.wing, "wing"),
|
|
866
|
+
requireString(args.room, "room"),
|
|
867
|
+
requireString(args.content, "content"),
|
|
868
|
+
toOptionalString(args.source_file),
|
|
869
|
+
toOptionalString(args.added_by) ?? "mcp",
|
|
870
|
+
),
|
|
871
|
+
},
|
|
872
|
+
mempalace_delete_drawer: {
|
|
873
|
+
description: "Delete a drawer by ID. Irreversible.",
|
|
874
|
+
input_schema: {
|
|
875
|
+
type: "object",
|
|
876
|
+
properties: {
|
|
877
|
+
drawer_id: { type: "string", description: "ID of the drawer to delete" },
|
|
878
|
+
},
|
|
879
|
+
required: ["drawer_id"],
|
|
880
|
+
},
|
|
881
|
+
handler: async (args) => toolDeleteDrawer(requireString(args.drawer_id, "drawer_id")),
|
|
882
|
+
},
|
|
883
|
+
mempalace_diary_write: {
|
|
884
|
+
description:
|
|
885
|
+
"Write to your personal agent diary in AAAK format. Your observations, thoughts, what you worked on, what matters. Each agent has their own diary with full history. Write in AAAK for compression — e.g. 'SESSION:2026-04-04|built.palace.graph+diary.tools|ALC.req:agent.diaries.in.aaak|★★★'. Use entity codes from the AAAK spec.",
|
|
886
|
+
input_schema: {
|
|
887
|
+
type: "object",
|
|
888
|
+
properties: {
|
|
889
|
+
agent_name: { type: "string", description: "Your name — each agent gets their own diary wing" },
|
|
890
|
+
entry: {
|
|
891
|
+
type: "string",
|
|
892
|
+
description: "Your diary entry in AAAK format — compressed, entity-coded, emotion-marked",
|
|
893
|
+
},
|
|
894
|
+
topic: { type: "string", description: "Topic tag (optional, default: general)" },
|
|
895
|
+
},
|
|
896
|
+
required: ["agent_name", "entry"],
|
|
897
|
+
},
|
|
898
|
+
handler: async (args) =>
|
|
899
|
+
toolDiaryWrite(
|
|
900
|
+
requireString(args.agent_name, "agent_name"),
|
|
901
|
+
requireString(args.entry, "entry"),
|
|
902
|
+
toOptionalString(args.topic) ?? "general",
|
|
903
|
+
),
|
|
904
|
+
},
|
|
905
|
+
mempalace_diary_read: {
|
|
906
|
+
description:
|
|
907
|
+
"Read your recent diary entries (in AAAK). See what past versions of yourself recorded — your journal across sessions.",
|
|
908
|
+
input_schema: {
|
|
909
|
+
type: "object",
|
|
910
|
+
properties: {
|
|
911
|
+
agent_name: { type: "string", description: "Your name — each agent gets their own diary wing" },
|
|
912
|
+
last_n: { type: "integer", description: "Number of recent entries to read (default: 10)" },
|
|
913
|
+
},
|
|
914
|
+
required: ["agent_name"],
|
|
915
|
+
},
|
|
916
|
+
handler: async (args) => toolDiaryRead(requireString(args.agent_name, "agent_name"), toInteger(args.last_n, 10)),
|
|
917
|
+
},
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
export async function handleRequest(rawRequest: unknown): Promise<JsonRpcResponse | null> {
|
|
921
|
+
const request = isRecord(rawRequest) ? (rawRequest as JsonRpcRequest) : {};
|
|
922
|
+
const method = typeof request.method === "string" ? request.method : "";
|
|
923
|
+
const params = isRecord(request.params) ? request.params : {};
|
|
924
|
+
const reqId = request.id;
|
|
925
|
+
|
|
926
|
+
if (method === "initialize") {
|
|
927
|
+
return {
|
|
928
|
+
jsonrpc: "2.0",
|
|
929
|
+
id: reqId,
|
|
930
|
+
result: {
|
|
931
|
+
protocolVersion: "2024-11-05",
|
|
932
|
+
capabilities: { tools: {} },
|
|
933
|
+
serverInfo: { name: "mempalace", version: "2.0.0" },
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (method === "notifications/initialized") {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (method === "tools/list") {
|
|
943
|
+
return {
|
|
944
|
+
jsonrpc: "2.0",
|
|
945
|
+
id: reqId,
|
|
946
|
+
result: {
|
|
947
|
+
tools: Object.entries(TOOLS).map(([name, tool]) => ({
|
|
948
|
+
name,
|
|
949
|
+
description: tool.description,
|
|
950
|
+
inputSchema: tool.input_schema as unknown as JsonValue,
|
|
951
|
+
})) as JsonValue[],
|
|
952
|
+
},
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (method === "tools/call") {
|
|
957
|
+
const toolName = typeof params.name === "string" ? params.name : "";
|
|
958
|
+
const toolArgs = isRecord(params.arguments) ? params.arguments : {};
|
|
959
|
+
|
|
960
|
+
if (!(toolName in TOOLS)) {
|
|
961
|
+
return {
|
|
962
|
+
jsonrpc: "2.0",
|
|
963
|
+
id: reqId,
|
|
964
|
+
error: { code: -32601, message: `Unknown tool: ${toolName}` },
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
try {
|
|
969
|
+
const result = await TOOLS[toolName].handler(toolArgs);
|
|
970
|
+
return {
|
|
971
|
+
jsonrpc: "2.0",
|
|
972
|
+
id: reqId,
|
|
973
|
+
result: {
|
|
974
|
+
content: [
|
|
975
|
+
{
|
|
976
|
+
type: "text",
|
|
977
|
+
text: JSON.stringify(result, null, 2),
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
} catch (error) {
|
|
983
|
+
logError(`Tool error in ${toolName}: ${String(error)}`);
|
|
984
|
+
return {
|
|
985
|
+
jsonrpc: "2.0",
|
|
986
|
+
id: reqId,
|
|
987
|
+
error: { code: -32000, message: String(error) },
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return {
|
|
993
|
+
jsonrpc: "2.0",
|
|
994
|
+
id: reqId,
|
|
995
|
+
error: { code: -32601, message: `Unknown method: ${method}` },
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export async function main(): Promise<void> {
|
|
1000
|
+
logInfo("MemPalace MCP Server starting...");
|
|
1001
|
+
const rl = createInterface({
|
|
1002
|
+
input: process.stdin,
|
|
1003
|
+
crlfDelay: Infinity,
|
|
1004
|
+
terminal: false,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
try {
|
|
1008
|
+
for await (const rawLine of rl) {
|
|
1009
|
+
const line = rawLine.trim();
|
|
1010
|
+
if (!line) {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
const request = JSON.parse(line) as unknown;
|
|
1016
|
+
const response = await handleRequest(request);
|
|
1017
|
+
if (response !== null) {
|
|
1018
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1019
|
+
}
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
logError(`Server error: ${String(error)}`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
} finally {
|
|
1025
|
+
rl.close();
|
|
1026
|
+
kg.close();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (import.meta.main) {
|
|
1031
|
+
void main();
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export default main;
|