baby-daemon 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/.env.example +3 -0
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/bin/baby-daemon.js +189 -0
- package/bin/memory-watch.js +98 -0
- package/bin/memory.js +399 -0
- package/mcp-server.js +553 -0
- package/package.json +63 -0
- package/src/config.js +18 -0
- package/src/idempotency.js +159 -0
- package/src/memoryStore.js +95 -0
- package/src/summarizer.js +151 -0
- package/src/vectorStore.js +410 -0
- package/src/watcher.js +263 -0
package/mcp-server.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mcp-server.js
|
|
5
|
+
* ─────────────
|
|
6
|
+
* Phase 5 — Baby Daemon MCP Server
|
|
7
|
+
*
|
|
8
|
+
* WHAT THIS FILE DOES:
|
|
9
|
+
* Wraps all existing Baby Daemon functionality (search, store, dump, archive)
|
|
10
|
+
* as an MCP (Model Context Protocol) server. Any MCP-compatible AI host
|
|
11
|
+
* (Claude Desktop, Cursor, Cline, Windsurf, etc.) can spawn this as a child
|
|
12
|
+
* process and use Baby Daemon's memory tools natively.
|
|
13
|
+
*
|
|
14
|
+
* HOW IT WORKS:
|
|
15
|
+
* 1. The AI host (e.g. Claude Desktop) spawns: node mcp-server.js
|
|
16
|
+
* 2. Host and server do a JSON-RPC handshake over stdin/stdout
|
|
17
|
+
* 3. Host discovers available tools via tools/list
|
|
18
|
+
* 4. LLM calls tools like memory_search when the user asks about past context
|
|
19
|
+
* 5. Server executes using existing Baby Daemon modules and returns results
|
|
20
|
+
*
|
|
21
|
+
* CONCEPT: stdio transport
|
|
22
|
+
* No HTTP, no ports, no network. The host writes JSON-RPC messages to our
|
|
23
|
+
* stdin pipe, we read them, execute, and write responses to stdout.
|
|
24
|
+
* stderr is reserved for debug logs (never send JSON-RPC on stderr).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
28
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
import fs from 'fs';
|
|
31
|
+
import path from 'path';
|
|
32
|
+
import { fileURLToPath } from 'url';
|
|
33
|
+
|
|
34
|
+
// ── Import existing Baby Daemon modules ─────────────────────────
|
|
35
|
+
import { searchMemories, archiveMemories } from './src/vectorStore.js';
|
|
36
|
+
import { readAllMemories } from './src/memoryStore.js';
|
|
37
|
+
import { summarizeChatLog } from './src/summarizer.js';
|
|
38
|
+
import { saveMemoriesForFile } from './src/memoryStore.js';
|
|
39
|
+
import { syncMemoriesToVectorStore } from './src/vectorStore.js';
|
|
40
|
+
|
|
41
|
+
// ── Paths ───────────────────────────────────────────────────────
|
|
42
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const MEMORY_FILE = path.join(__dirname, 'memory.jsonl');
|
|
44
|
+
const LOGS_DIR = path.join(__dirname, 'logs');
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────
|
|
47
|
+
// 1. CREATE THE MCP SERVER
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* CONCEPT: McpServer
|
|
52
|
+
* The SDK provides a high-level McpServer class that handles:
|
|
53
|
+
* - JSON-RPC 2.0 message parsing and framing
|
|
54
|
+
* - The initialization handshake (capability negotiation)
|
|
55
|
+
* - Tool/resource/prompt registration and dispatch
|
|
56
|
+
* - Error formatting
|
|
57
|
+
*
|
|
58
|
+
* We just register our tools and connect — the SDK does the rest.
|
|
59
|
+
*/
|
|
60
|
+
const server = new McpServer({
|
|
61
|
+
name: 'baby-daemon',
|
|
62
|
+
version: '1.0.0',
|
|
63
|
+
description: 'AI memory system — stores, searches, and retrieves project context across coding sessions and agents.',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────
|
|
67
|
+
// 2. REGISTER TOOLS
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* CONCEPT: Tool registration
|
|
72
|
+
* server.tool(name, description, inputSchema, handler)
|
|
73
|
+
*
|
|
74
|
+
* - name: What the LLM sees and calls (e.g. "memory_search")
|
|
75
|
+
* - description: CRITICAL — the LLM reads this to decide WHEN to use the tool.
|
|
76
|
+
* A bad description = the AI never calls your tool or calls it wrong.
|
|
77
|
+
* - inputSchema: Zod schema that validates inputs. The SDK converts this to
|
|
78
|
+
* JSON Schema for the LLM to understand the expected arguments.
|
|
79
|
+
* - handler: Your function. Receives validated args, returns { content: [...] }
|
|
80
|
+
*
|
|
81
|
+
* The return format is always:
|
|
82
|
+
* { content: [{ type: "text", text: "..." }] }
|
|
83
|
+
* This is part of the MCP spec — all tool results are arrays of content blocks.
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
// ── TOOL 1: memory_search ────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
server.tool(
|
|
89
|
+
'memory_search',
|
|
90
|
+
'Search through project memories using semantic vector similarity and keyword fallback. ' +
|
|
91
|
+
'Use this when the user asks about past decisions, bugs, fixes, architecture choices, or any historical project context. ' +
|
|
92
|
+
'Supports natural language queries like "What was the auth bug yesterday?" or "How did we fix the payment timeout?". ' +
|
|
93
|
+
'Returns ranked results with confidence scores, source references, and evidence quotes.',
|
|
94
|
+
{
|
|
95
|
+
query: z.string().describe('Natural language search query (e.g. "authentication issue", "database migration decision")'),
|
|
96
|
+
type: z.string().optional().describe('Filter by memory type: decision, proposed_idea, rejected_idea, open_question, bug, resolved_bug, architecture_note, file_change'),
|
|
97
|
+
since: z.string().optional().describe('ISO 8601 date string to filter memories created after this date (e.g. "2026-05-29T00:00:00Z")'),
|
|
98
|
+
file: z.string().optional().describe('Filter by related file name or source chat file'),
|
|
99
|
+
limit: z.number().optional().describe('Maximum number of results to return (default: 5)'),
|
|
100
|
+
},
|
|
101
|
+
async ({ query, type, since, file, limit = 5 }) => {
|
|
102
|
+
try {
|
|
103
|
+
const result = await searchMemories(query, { since, type, file, limit });
|
|
104
|
+
|
|
105
|
+
if (result.results.length === 0) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: 'No matching memories found. Try a different query, broaden your search, or check if memories have been stored yet.',
|
|
110
|
+
}],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Format results for the LLM in a clear, structured way
|
|
115
|
+
const formatted = result.results.map((item, i) => {
|
|
116
|
+
const score = item.score ? ` (similarity: ${(item.score * 100).toFixed(0)}%)` : '';
|
|
117
|
+
const relatedFiles = item.related_files?.length > 0 ? `\n Related files: ${item.related_files.join(', ')}` : '';
|
|
118
|
+
const evidence = item.original_text ? `\n Evidence: "${item.original_text}"` : '';
|
|
119
|
+
return `${i + 1}. [${item.type?.toUpperCase()}]${score}\n ${item.content}${relatedFiles}${evidence}\n Source: ${item.chat_file} | Date: ${item.timestamp}`;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: `Found ${result.results.length} memories (via ${result.method} search):\n\n${formatted.join('\n\n')}`,
|
|
126
|
+
}],
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: 'text', text: `Search failed: ${error.message}` }],
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// ── TOOL 2: memory_store ─────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
server.tool(
|
|
140
|
+
'memory_store',
|
|
141
|
+
'Process raw text (conversation log, code review, meeting notes, etc.) through the AI summarization pipeline ' +
|
|
142
|
+
'and store the extracted memories. The text is sent to Gemini for structured extraction — it identifies decisions, ' +
|
|
143
|
+
'bugs, architecture notes, proposed ideas, etc. — then saves them to memory.jsonl and syncs with the vector database. ' +
|
|
144
|
+
'Use this when you want to preserve important context from the current session for future agents.',
|
|
145
|
+
{
|
|
146
|
+
content: z.string().describe('The raw text content to process and extract memories from (e.g. a conversation log, code review notes, or any text with technical context)'),
|
|
147
|
+
source_name: z.string().optional().describe('A descriptive name for the source of this content (default: "mcp-session-<timestamp>.md")'),
|
|
148
|
+
},
|
|
149
|
+
async ({ content, source_name }) => {
|
|
150
|
+
try {
|
|
151
|
+
const fileName = source_name || `mcp-session-${Date.now()}.md`;
|
|
152
|
+
|
|
153
|
+
// Run through the full summarization pipeline
|
|
154
|
+
const memories = await summarizeChatLog(content, fileName);
|
|
155
|
+
|
|
156
|
+
if (memories.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: 'No significant memories were extracted from the provided content. The text may not contain technical decisions, bugs, or architecture-relevant information.',
|
|
161
|
+
}],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Save to memory.jsonl
|
|
166
|
+
const savedCount = saveMemoriesForFile(fileName, memories);
|
|
167
|
+
|
|
168
|
+
// Sync to vector store (LanceDB)
|
|
169
|
+
await syncMemoriesToVectorStore(fileName, memories);
|
|
170
|
+
|
|
171
|
+
// Format summary of what was stored
|
|
172
|
+
const summary = memories.map((mem, i) =>
|
|
173
|
+
`${i + 1}. [${mem.type.toUpperCase()}] (confidence: ${mem.confidence.toFixed(2)}) ${mem.content}`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
content: [{
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: `Successfully extracted and stored ${memories.length} memories from "${fileName}":\n\n${summary.join('\n')}\n\nMemories are now searchable via memory_search.`,
|
|
180
|
+
}],
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: 'text', text: `Memory storage failed: ${error.message}` }],
|
|
185
|
+
isError: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// ── TOOL 3: memory_dump ──────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
server.tool(
|
|
194
|
+
'memory_dump',
|
|
195
|
+
'Dump all stored memories as raw data, optionally filtered by date. This is a fallback tool — use it when ' +
|
|
196
|
+
'semantic search is not finding what you need, or when you want to see ALL stored memories without ranking. ' +
|
|
197
|
+
'Also useful for debugging or verifying what the memory system has stored.',
|
|
198
|
+
{
|
|
199
|
+
since: z.string().optional().describe('ISO 8601 date string — only return memories created after this date'),
|
|
200
|
+
type: z.string().optional().describe('Filter by memory type: decision, proposed_idea, rejected_idea, open_question, bug, resolved_bug, architecture_note, file_change'),
|
|
201
|
+
},
|
|
202
|
+
async ({ since, type }) => {
|
|
203
|
+
try {
|
|
204
|
+
let memories = readAllMemories();
|
|
205
|
+
|
|
206
|
+
// Apply date filter
|
|
207
|
+
if (since) {
|
|
208
|
+
const cutoff = new Date(since);
|
|
209
|
+
if (!isNaN(cutoff.getTime())) {
|
|
210
|
+
memories = memories.filter(m => new Date(m.timestamp) >= cutoff);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Apply type filter
|
|
215
|
+
if (type) {
|
|
216
|
+
const targetType = type.trim().toLowerCase();
|
|
217
|
+
memories = memories.filter(m => m.type?.toLowerCase() === targetType);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (memories.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
content: [{
|
|
223
|
+
type: 'text',
|
|
224
|
+
text: 'No memories found matching the given filters.',
|
|
225
|
+
}],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Format as readable output
|
|
230
|
+
const formatted = memories.map((mem, i) => {
|
|
231
|
+
const files = mem.related_files?.length > 0 ? ` | Files: ${mem.related_files.join(', ')}` : '';
|
|
232
|
+
return `${i + 1}. [${mem.type?.toUpperCase()}] ${mem.content}\n Confidence: ${mem.confidence} | Status: ${mem.status} | Source: ${mem.source?.chat_file}${files}\n Date: ${mem.timestamp}`;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
content: [{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: `Dumping ${memories.length} memories:\n\n${formatted.join('\n\n')}`,
|
|
239
|
+
}],
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: 'text', text: `Memory dump failed: ${error.message}` }],
|
|
244
|
+
isError: true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// ── TOOL 4: memory_archive ───────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
server.tool(
|
|
253
|
+
'memory_archive',
|
|
254
|
+
'Move old memories from the active table to an archive table in LanceDB. ' +
|
|
255
|
+
'This keeps the active search pool small and fast. Archived memories are still stored but won\'t appear in regular searches. ' +
|
|
256
|
+
'Use this for maintenance when the memory database grows large.',
|
|
257
|
+
{
|
|
258
|
+
age_days: z.number().optional().describe('Archive memories older than this many days (default: 30)'),
|
|
259
|
+
},
|
|
260
|
+
async ({ age_days = 30 }) => {
|
|
261
|
+
try {
|
|
262
|
+
const result = await archiveMemories({ ageDays: age_days });
|
|
263
|
+
return {
|
|
264
|
+
content: [{
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: result.msg,
|
|
267
|
+
}],
|
|
268
|
+
};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
content: [{ type: 'text', text: `Archival failed: ${error.message}` }],
|
|
272
|
+
isError: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// ── TOOL 5: memory_read ──────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
server.tool(
|
|
281
|
+
'memory_read',
|
|
282
|
+
'Load a complete project knowledge briefing — all active memories grouped by category ' +
|
|
283
|
+
'(decisions, architecture notes, active bugs, resolved bugs, file changes, proposed ideas, open questions). ' +
|
|
284
|
+
'Use this at the start of a new session to get full project context, or when you need a comprehensive overview ' +
|
|
285
|
+
'of everything the memory system knows about the project.',
|
|
286
|
+
{},
|
|
287
|
+
async () => {
|
|
288
|
+
try {
|
|
289
|
+
const allMemories = readAllMemories();
|
|
290
|
+
const activeMemories = allMemories.filter(m => m.status === 'active');
|
|
291
|
+
|
|
292
|
+
if (activeMemories.length === 0) {
|
|
293
|
+
return {
|
|
294
|
+
content: [{
|
|
295
|
+
type: 'text',
|
|
296
|
+
text: 'No active memories found in this project. Memories are created when the file watcher processes chat logs, or when content is stored via the memory_store tool.',
|
|
297
|
+
}],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Group by type
|
|
302
|
+
const groups = {};
|
|
303
|
+
activeMemories.forEach(mem => {
|
|
304
|
+
const type = mem.type || 'other';
|
|
305
|
+
if (!groups[type]) groups[type] = [];
|
|
306
|
+
groups[type].push(mem);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Format each group
|
|
310
|
+
const displayOrder = [
|
|
311
|
+
{ key: 'decision', title: '📢 DECISIONS' },
|
|
312
|
+
{ key: 'architecture_note', title: '🏗️ ARCHITECTURE NOTES' },
|
|
313
|
+
{ key: 'bug', title: '🐛 ACTIVE BUGS' },
|
|
314
|
+
{ key: 'resolved_bug', title: '✅ RESOLVED BUGS' },
|
|
315
|
+
{ key: 'file_change', title: '📝 FILE CHANGES' },
|
|
316
|
+
{ key: 'proposed_idea', title: '💡 PROPOSED IDEAS' },
|
|
317
|
+
{ key: 'open_question', title: '❓ OPEN QUESTIONS' },
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const sections = [];
|
|
321
|
+
for (const { key, title } of displayOrder) {
|
|
322
|
+
const items = groups[key];
|
|
323
|
+
if (!items || items.length === 0) continue;
|
|
324
|
+
|
|
325
|
+
const lines = items.map((item, i) => {
|
|
326
|
+
const files = item.related_files?.length > 0 ? ` (files: ${item.related_files.join(', ')})` : '';
|
|
327
|
+
return ` ${i + 1}. ${item.content}${files}`;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
sections.push(`${title} (${items.length}):\n${lines.join('\n')}`);
|
|
331
|
+
delete groups[key]; // Remove processed group
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Handle any remaining types not in displayOrder
|
|
335
|
+
for (const [type, items] of Object.entries(groups)) {
|
|
336
|
+
const lines = items.map((item, i) => ` ${i + 1}. ${item.content}`);
|
|
337
|
+
sections.push(`🔮 ${type.toUpperCase()} (${items.length}):\n${lines.join('\n')}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
content: [{
|
|
342
|
+
type: 'text',
|
|
343
|
+
text: `Project Knowledge Briefing (${activeMemories.length} active memories):\n\n${sections.join('\n\n')}`,
|
|
344
|
+
}],
|
|
345
|
+
};
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return {
|
|
348
|
+
content: [{ type: 'text', text: `Read failed: ${error.message}` }],
|
|
349
|
+
isError: true,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// ─────────────────────────────────────────────────────────────────
|
|
356
|
+
// 3. REGISTER RESOURCES
|
|
357
|
+
// ─────────────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* CONCEPT: Resources
|
|
361
|
+
* Resources are read-only data the AI can access. Unlike tools (which the LLM
|
|
362
|
+
* decides to call), resources are typically pulled by the client/user.
|
|
363
|
+
* Think of them as "files" or "views" the AI can read.
|
|
364
|
+
*/
|
|
365
|
+
|
|
366
|
+
server.resource(
|
|
367
|
+
'memory-status',
|
|
368
|
+
'memory://status',
|
|
369
|
+
async (uri) => {
|
|
370
|
+
try {
|
|
371
|
+
const allMemories = readAllMemories();
|
|
372
|
+
const active = allMemories.filter(m => m.status === 'active');
|
|
373
|
+
const outdated = allMemories.filter(m => m.status === 'outdated');
|
|
374
|
+
|
|
375
|
+
// Count by type
|
|
376
|
+
const typeCounts = {};
|
|
377
|
+
active.forEach(m => {
|
|
378
|
+
const t = m.type || 'unknown';
|
|
379
|
+
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Find latest memory timestamp
|
|
383
|
+
const timestamps = allMemories.map(m => new Date(m.timestamp).getTime()).filter(t => !isNaN(t));
|
|
384
|
+
const lastMemory = timestamps.length > 0 ? new Date(Math.max(...timestamps)).toISOString() : 'none';
|
|
385
|
+
|
|
386
|
+
// Check if LanceDB is available
|
|
387
|
+
let lanceDbStatus = 'unknown';
|
|
388
|
+
try {
|
|
389
|
+
await import('@lancedb/lancedb');
|
|
390
|
+
lanceDbStatus = 'available';
|
|
391
|
+
} catch {
|
|
392
|
+
lanceDbStatus = 'unavailable (using MiniSearch fallback)';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const status = {
|
|
396
|
+
total_memories: allMemories.length,
|
|
397
|
+
active_memories: active.length,
|
|
398
|
+
outdated_memories: outdated.length,
|
|
399
|
+
memories_by_type: typeCounts,
|
|
400
|
+
last_memory_timestamp: lastMemory,
|
|
401
|
+
memory_file: MEMORY_FILE,
|
|
402
|
+
memory_file_exists: fs.existsSync(MEMORY_FILE),
|
|
403
|
+
lancedb_status: lanceDbStatus,
|
|
404
|
+
logs_directory: LOGS_DIR,
|
|
405
|
+
logs_directory_exists: fs.existsSync(LOGS_DIR),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
contents: [{
|
|
410
|
+
uri: uri.href,
|
|
411
|
+
mimeType: 'application/json',
|
|
412
|
+
text: JSON.stringify(status, null, 2),
|
|
413
|
+
}],
|
|
414
|
+
};
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return {
|
|
417
|
+
contents: [{
|
|
418
|
+
uri: uri.href,
|
|
419
|
+
mimeType: 'text/plain',
|
|
420
|
+
text: `Error reading status: ${error.message}`,
|
|
421
|
+
}],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// ─────────────────────────────────────────────────────────────────
|
|
428
|
+
// 4. REGISTER PROMPTS
|
|
429
|
+
// ─────────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* CONCEPT: Prompts
|
|
433
|
+
* Pre-written prompt templates that help the AI behave in a specific way.
|
|
434
|
+
* When invoked, the server returns a crafted prompt that includes memory context.
|
|
435
|
+
* The AI host can use this to "bootstrap" a new session with project knowledge.
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
server.prompt(
|
|
439
|
+
'continue_from_memory',
|
|
440
|
+
'Load relevant project memories and generate a context-rich prompt to continue coding from where the last agent or session left off. ' +
|
|
441
|
+
'Use this at the start of a new coding session to inherit all previous context.',
|
|
442
|
+
{
|
|
443
|
+
task: z.string().describe('What you want to work on (e.g. "implement payment flow", "fix the auth bug", "refactor database layer")'),
|
|
444
|
+
},
|
|
445
|
+
async ({ task }) => {
|
|
446
|
+
try {
|
|
447
|
+
// Get all active memories for context
|
|
448
|
+
const allMemories = readAllMemories();
|
|
449
|
+
const activeMemories = allMemories.filter(m => m.status === 'active');
|
|
450
|
+
|
|
451
|
+
// Also try to find task-specific memories via search
|
|
452
|
+
let relevantMemories = [];
|
|
453
|
+
try {
|
|
454
|
+
const searchResult = await searchMemories(task, { limit: 5 });
|
|
455
|
+
relevantMemories = searchResult.results || [];
|
|
456
|
+
} catch {
|
|
457
|
+
// Search might fail if no memories exist yet — that's OK
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Build the context sections
|
|
461
|
+
let contextBlock = '';
|
|
462
|
+
|
|
463
|
+
if (relevantMemories.length > 0) {
|
|
464
|
+
const relevant = relevantMemories.map((m, i) =>
|
|
465
|
+
`${i + 1}. [${m.type?.toUpperCase()}] ${m.content}`
|
|
466
|
+
).join('\n');
|
|
467
|
+
contextBlock += `\n\nMost relevant memories for your task:\n${relevant}`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (activeMemories.length > 0) {
|
|
471
|
+
// Group key info
|
|
472
|
+
const decisions = activeMemories.filter(m => m.type === 'decision');
|
|
473
|
+
const bugs = activeMemories.filter(m => m.type === 'bug');
|
|
474
|
+
const architecture = activeMemories.filter(m => m.type === 'architecture_note');
|
|
475
|
+
|
|
476
|
+
if (decisions.length > 0) {
|
|
477
|
+
contextBlock += `\n\nKey decisions made:\n${decisions.map(d => `• ${d.content}`).join('\n')}`;
|
|
478
|
+
}
|
|
479
|
+
if (bugs.length > 0) {
|
|
480
|
+
contextBlock += `\n\nActive bugs:\n${bugs.map(b => `• ${b.content}`).join('\n')}`;
|
|
481
|
+
}
|
|
482
|
+
if (architecture.length > 0) {
|
|
483
|
+
contextBlock += `\n\nArchitecture notes:\n${architecture.map(a => `• ${a.content}`).join('\n')}`;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!contextBlock) {
|
|
488
|
+
contextBlock = '\n\nNo previous memories found. This appears to be a fresh project or the memory system hasn\'t ingested any chat logs yet.';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
messages: [
|
|
493
|
+
{
|
|
494
|
+
role: 'user',
|
|
495
|
+
content: {
|
|
496
|
+
type: 'text',
|
|
497
|
+
text: `You are continuing work on a coding project. Here is the context from previous sessions, automatically retrieved from the Baby Daemon memory system.
|
|
498
|
+
|
|
499
|
+
IMPORTANT: Treat these memories as helpful hints, NOT absolute truth. They were auto-generated from past conversations. Always verify against the actual codebase before making changes. If a memory seems uncertain or contradictory, ask the user for clarification.
|
|
500
|
+
${contextBlock}
|
|
501
|
+
|
|
502
|
+
Your task: ${task}
|
|
503
|
+
|
|
504
|
+
Please review the context above, then proceed with the task. Start by confirming your understanding of the current state and any assumptions you're making.`,
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return {
|
|
511
|
+
messages: [
|
|
512
|
+
{
|
|
513
|
+
role: 'user',
|
|
514
|
+
content: {
|
|
515
|
+
type: 'text',
|
|
516
|
+
text: `Failed to load project memories: ${error.message}\n\nPlease proceed with the task "${task}" without historical context. Ask the user for any needed background.`,
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
// ─────────────────────────────────────────────────────────────────
|
|
526
|
+
// 5. CONNECT TO STDIO TRANSPORT AND START
|
|
527
|
+
// ─────────────────────────────────────────────────────────────────
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* CONCEPT: StdioServerTransport
|
|
531
|
+
* This connects the MCP server to stdin/stdout pipes.
|
|
532
|
+
* The host (Claude Desktop, Cursor, etc.) spawns this process and communicates
|
|
533
|
+
* via those pipes. No HTTP server, no ports, no network — just OS-level IPC.
|
|
534
|
+
*
|
|
535
|
+
* Under the hood:
|
|
536
|
+
* - Reads line-delimited JSON-RPC messages from process.stdin
|
|
537
|
+
* - Writes JSON-RPC responses to process.stdout
|
|
538
|
+
* - Debug logs go to process.stderr (safe, won't interfere with protocol)
|
|
539
|
+
*/
|
|
540
|
+
|
|
541
|
+
async function main() {
|
|
542
|
+
const transport = new StdioServerTransport();
|
|
543
|
+
await server.connect(transport);
|
|
544
|
+
// Server is now running and listening on stdin.
|
|
545
|
+
// It will stay alive as long as the host keeps the pipe open.
|
|
546
|
+
// The SDK handles the full lifecycle: init handshake → operation → shutdown.
|
|
547
|
+
console.error('Baby Daemon MCP server started and listening on stdio.');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
main().catch((error) => {
|
|
551
|
+
console.error('Fatal: Failed to start MCP server:', error);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "baby-daemon",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI memory system for coding agents — file watcher, LLM summarizer, vector search, and MCP server. Give your AI persistent memory across sessions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"baby-daemon": "./bin/baby-daemon.js",
|
|
8
|
+
"memory-watch": "./bin/memory-watch.js",
|
|
9
|
+
"memory": "./bin/memory.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"watch": "node bin/memory-watch.js",
|
|
13
|
+
"mcp": "node mcp-server.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/",
|
|
18
|
+
"mcp-server.js",
|
|
19
|
+
".env.example",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ai",
|
|
25
|
+
"memory",
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"llm",
|
|
29
|
+
"coding-agent",
|
|
30
|
+
"vector-search",
|
|
31
|
+
"lancedb",
|
|
32
|
+
"gemini",
|
|
33
|
+
"daemon",
|
|
34
|
+
"context",
|
|
35
|
+
"claude",
|
|
36
|
+
"cursor",
|
|
37
|
+
"cline"
|
|
38
|
+
],
|
|
39
|
+
"author": "sankolte",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/sankolte/Baby-Daemon.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/sankolte/Baby-Daemon#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/sankolte/Baby-Daemon/issues"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@google/genai": "^2.6.0",
|
|
54
|
+
"@lancedb/lancedb": "^0.29.0",
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
56
|
+
"chokidar": "^5.0.0",
|
|
57
|
+
"chrono-node": "^2.9.1",
|
|
58
|
+
"dotenv": "^17.4.2",
|
|
59
|
+
"minisearch": "^7.2.0",
|
|
60
|
+
"zod": "^4.4.3"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
package/src/config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GoogleGenAI } from '@google/genai';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
|
|
4
|
+
// Load environment variables from .env file
|
|
5
|
+
dotenv.config();
|
|
6
|
+
|
|
7
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
8
|
+
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
console.warn(
|
|
11
|
+
'\n ⚠️ Warning: GEMINI_API_KEY is not set in your environment or .env file.\n' +
|
|
12
|
+
' Please create a .env file in the project root with your API key:\n' +
|
|
13
|
+
' GEMINI_API_KEY=your_free_gemini_api_key_here\n'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Initialize the Gemini API client
|
|
18
|
+
export const ai = new GoogleGenAI({ apiKey });
|