@geoql/mdr 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +152 -0
- package/USAGE.md +59 -0
- package/bin/detect-user.sh +53 -0
- package/bin/index-conversations.ts +34 -0
- package/bin/macrodata-daemon.ts +28 -0
- package/bin/macrodata-hook.sh +277 -0
- package/dist/bin/index-conversations.js +31 -0
- package/dist/bin/macrodata-daemon.js +30 -0
- package/dist/opencode/context.js +210 -0
- package/dist/opencode/conversations.js +367 -0
- package/dist/opencode/index.js +155 -0
- package/dist/opencode/journal.js +108 -0
- package/dist/opencode/logger.js +29 -0
- package/dist/opencode/search.js +210 -0
- package/dist/opencode/skills/macrodata-distill/SKILL.md +171 -0
- package/dist/opencode/skills/macrodata-dreamtime/SKILL.md +120 -0
- package/dist/opencode/skills/macrodata-memory-maintenance/SKILL.md +96 -0
- package/dist/opencode/skills/macrodata-onboarding/SKILL.md +346 -0
- package/dist/opencode/tools.js +367 -0
- package/dist/src/config.js +55 -0
- package/dist/src/conversations.js +513 -0
- package/dist/src/daemon.js +582 -0
- package/dist/src/detect-user.js +73 -0
- package/dist/src/embeddings.js +190 -0
- package/dist/src/index.js +413 -0
- package/dist/src/indexer.js +286 -0
- package/opencode/context.ts +322 -0
- package/opencode/conversations.ts +467 -0
- package/opencode/index.ts +208 -0
- package/opencode/journal.ts +153 -0
- package/opencode/logger.ts +32 -0
- package/opencode/search.ts +288 -0
- package/opencode/skills/macrodata-distill/SKILL.md +171 -0
- package/opencode/skills/macrodata-dreamtime/SKILL.md +120 -0
- package/opencode/skills/macrodata-memory-maintenance/SKILL.md +96 -0
- package/opencode/skills/macrodata-onboarding/SKILL.md +346 -0
- package/opencode/tools.ts +453 -0
- package/package.json +87 -0
- package/src/config.ts +66 -0
- package/src/conversations.ts +709 -0
- package/src/daemon.ts +785 -0
- package/src/detect-user.ts +97 -0
- package/src/embeddings.ts +262 -0
- package/src/index.ts +726 -0
- package/src/indexer.ts +394 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macrodata Local MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for local file-based memory:
|
|
5
|
+
* - log_journal: Append timestamped entries (with auto-indexing)
|
|
6
|
+
* - get_recent_journal: Get recent entries
|
|
7
|
+
* - search_memory: Semantic search using Transformers.js
|
|
8
|
+
* - manage_index: Rebuild or get stats for memory/conversation indexes
|
|
9
|
+
* - schedule: Create cron or one-shot reminders
|
|
10
|
+
* - list_reminders: List active schedules
|
|
11
|
+
* - remove_reminder: Delete a reminder
|
|
12
|
+
* - save_conversation_summary: Save session summaries
|
|
13
|
+
* - get_recent_summaries: Get past summaries
|
|
14
|
+
* - search_conversations: Search past Claude Code sessions
|
|
15
|
+
* - expand_conversation: Load full context from a session
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import {
|
|
22
|
+
existsSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
writeFileSync,
|
|
25
|
+
appendFileSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readdirSync,
|
|
28
|
+
} from "fs";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
import {
|
|
31
|
+
searchMemory as doSearchMemory,
|
|
32
|
+
indexJournalEntry,
|
|
33
|
+
rebuildIndex,
|
|
34
|
+
getIndexStats,
|
|
35
|
+
type MemoryItemType,
|
|
36
|
+
} from "./indexer.js";
|
|
37
|
+
import {
|
|
38
|
+
searchConversations,
|
|
39
|
+
expandConversation,
|
|
40
|
+
rebuildConversationIndex,
|
|
41
|
+
updateConversationIndex,
|
|
42
|
+
getConversationIndexStats,
|
|
43
|
+
} from "./conversations.js";
|
|
44
|
+
import {
|
|
45
|
+
getStateRoot,
|
|
46
|
+
getStateDir,
|
|
47
|
+
getEntitiesDir,
|
|
48
|
+
getJournalDir,
|
|
49
|
+
getIndexDir,
|
|
50
|
+
getRemindersDir,
|
|
51
|
+
} from "./config.js";
|
|
52
|
+
import { unlinkSync } from "fs";
|
|
53
|
+
|
|
54
|
+
// Types
|
|
55
|
+
interface JournalEntry {
|
|
56
|
+
timestamp: string;
|
|
57
|
+
topic: string;
|
|
58
|
+
content: string;
|
|
59
|
+
metadata?: {
|
|
60
|
+
source?: string;
|
|
61
|
+
intent?: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Schedule {
|
|
66
|
+
id: string;
|
|
67
|
+
type: "cron" | "once";
|
|
68
|
+
expression: string;
|
|
69
|
+
description: string;
|
|
70
|
+
payload: string;
|
|
71
|
+
agent?: "opencode" | "claude"; // Which agent CLI to trigger
|
|
72
|
+
model?: string; // Optional model override (e.g., "anthropic/claude-opus-4-6")
|
|
73
|
+
createdAt: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Helpers
|
|
77
|
+
function ensureDirectories() {
|
|
78
|
+
const entitiesDir = getEntitiesDir();
|
|
79
|
+
const dirs = [
|
|
80
|
+
getStateRoot(),
|
|
81
|
+
getStateDir(),
|
|
82
|
+
entitiesDir,
|
|
83
|
+
join(entitiesDir, "people"),
|
|
84
|
+
join(entitiesDir, "projects"),
|
|
85
|
+
getJournalDir(),
|
|
86
|
+
getIndexDir(),
|
|
87
|
+
];
|
|
88
|
+
for (const dir of dirs) {
|
|
89
|
+
if (!existsSync(dir)) {
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function loadAllSchedules(): Schedule[] {
|
|
96
|
+
const remindersDir = getRemindersDir();
|
|
97
|
+
const schedules: Schedule[] = [];
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
if (!existsSync(remindersDir)) return schedules;
|
|
101
|
+
|
|
102
|
+
const files = readdirSync(remindersDir).filter((f) => f.endsWith(".json"));
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
try {
|
|
105
|
+
const content = readFileSync(join(remindersDir, file), "utf-8");
|
|
106
|
+
schedules.push(JSON.parse(content));
|
|
107
|
+
} catch {
|
|
108
|
+
// Skip malformed files
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return schedules;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function saveSchedule(schedule: Schedule) {
|
|
119
|
+
const remindersDir = getRemindersDir();
|
|
120
|
+
if (!existsSync(remindersDir)) {
|
|
121
|
+
mkdirSync(remindersDir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
const filePath = join(remindersDir, `${schedule.id}.json`);
|
|
124
|
+
writeFileSync(filePath, JSON.stringify(schedule, null, 2));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function deleteScheduleFile(id: string) {
|
|
128
|
+
const remindersDir = getRemindersDir();
|
|
129
|
+
const filePath = join(remindersDir, `${id}.json`);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
if (existsSync(filePath)) {
|
|
133
|
+
unlinkSync(filePath);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getTodayJournalPath(): string {
|
|
143
|
+
const today = new Date().toISOString().split("T")[0];
|
|
144
|
+
return join(getJournalDir(), `${today}.jsonl`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getRecentJournalEntries(count: number): JournalEntry[] {
|
|
148
|
+
const entries: JournalEntry[] = [];
|
|
149
|
+
const journalDir = getJournalDir();
|
|
150
|
+
|
|
151
|
+
// Get all journal files, sorted by name (date) descending
|
|
152
|
+
if (!existsSync(journalDir)) return entries;
|
|
153
|
+
|
|
154
|
+
const files = readdirSync(journalDir)
|
|
155
|
+
.filter((f: string) => f.endsWith(".jsonl"))
|
|
156
|
+
.sort()
|
|
157
|
+
.reverse();
|
|
158
|
+
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
if (entries.length >= count) break;
|
|
161
|
+
|
|
162
|
+
const content = readFileSync(join(journalDir, file), "utf-8");
|
|
163
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
164
|
+
|
|
165
|
+
for (const line of lines.reverse()) {
|
|
166
|
+
if (entries.length >= count) break;
|
|
167
|
+
try {
|
|
168
|
+
entries.push(JSON.parse(line));
|
|
169
|
+
} catch {
|
|
170
|
+
// Skip malformed lines
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Create MCP server with every tool registered.
|
|
179
|
+
export function createServer(): McpServer {
|
|
180
|
+
const server = new McpServer({
|
|
181
|
+
name: "macrodata-local",
|
|
182
|
+
version: "0.1.0",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Tool: log_journal
|
|
186
|
+
server.tool(
|
|
187
|
+
"log_journal",
|
|
188
|
+
"Append a timestamped entry to the journal",
|
|
189
|
+
{
|
|
190
|
+
topic: z.string().describe("Category or tag for this entry"),
|
|
191
|
+
content: z.string().describe("The actual note or observation"),
|
|
192
|
+
source: z.string().optional().describe("Where this came from (conversation, cron, etc.)"),
|
|
193
|
+
intent: z.string().optional().describe("What you were doing when logging this"),
|
|
194
|
+
},
|
|
195
|
+
async ({ topic, content, source, intent }) => {
|
|
196
|
+
ensureDirectories();
|
|
197
|
+
|
|
198
|
+
const entry: JournalEntry = {
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
topic,
|
|
201
|
+
content,
|
|
202
|
+
metadata: {
|
|
203
|
+
...(source && { source }),
|
|
204
|
+
...(intent && { intent }),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const journalPath = getTodayJournalPath();
|
|
209
|
+
appendFileSync(journalPath, JSON.stringify(entry) + "\n");
|
|
210
|
+
|
|
211
|
+
// Index the entry for semantic search
|
|
212
|
+
try {
|
|
213
|
+
await indexJournalEntry(entry);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error("[log_journal] Failed to index entry:", err);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: "text" as const,
|
|
222
|
+
text: `Logged to journal: ${topic}`,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Tool: get_recent_journal
|
|
230
|
+
server.tool(
|
|
231
|
+
"get_recent_journal",
|
|
232
|
+
"Get the N most recent journal entries, optionally filtered by topic",
|
|
233
|
+
{
|
|
234
|
+
count: z.number().default(10).describe("Number of entries to retrieve"),
|
|
235
|
+
topic: z.string().optional().describe("Filter by specific topic"),
|
|
236
|
+
},
|
|
237
|
+
async ({ count, topic }) => {
|
|
238
|
+
let entries = getRecentJournalEntries(Math.min(count * 2, 100)); // Get more to filter
|
|
239
|
+
|
|
240
|
+
if (topic) {
|
|
241
|
+
entries = entries.filter((e) => e.topic === topic);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
entries = entries.slice(0, count);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "text" as const,
|
|
250
|
+
text: JSON.stringify(entries, null, 2),
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Tool: search_memory
|
|
258
|
+
server.tool(
|
|
259
|
+
"search_memory",
|
|
260
|
+
"Semantic search across journal entries and entity files. Returns ranked results.",
|
|
261
|
+
{
|
|
262
|
+
query: z.string().describe("Natural language search query"),
|
|
263
|
+
type: z
|
|
264
|
+
.enum(["journal", "person", "project", "all"])
|
|
265
|
+
.default("all")
|
|
266
|
+
.describe("Filter by content type"),
|
|
267
|
+
since: z.string().optional().describe("Only include items after this ISO date"),
|
|
268
|
+
limit: z.number().default(5).describe("Maximum results to return"),
|
|
269
|
+
},
|
|
270
|
+
async ({ query, type, since, limit }) => {
|
|
271
|
+
try {
|
|
272
|
+
const results = await doSearchMemory(query, {
|
|
273
|
+
limit,
|
|
274
|
+
type: type === "all" ? undefined : (type as MemoryItemType),
|
|
275
|
+
since,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (results.length === 0) {
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text" as const,
|
|
283
|
+
text: "(no matches found)",
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const formatted = results
|
|
290
|
+
.map((r, i) => {
|
|
291
|
+
const header = `[${i + 1}] ${r.type}${r.section ? ` / ${r.section}` : ""} (score: ${r.score.toFixed(3)})`;
|
|
292
|
+
const meta = r.timestamp ? ` Date: ${r.timestamp}` : "";
|
|
293
|
+
const source = ` Source: ${r.source}`;
|
|
294
|
+
const content = r.content.slice(0, 500) + (r.content.length > 500 ? "..." : "");
|
|
295
|
+
return [header, meta, source, "", content].filter(Boolean).join("\n");
|
|
296
|
+
})
|
|
297
|
+
.join("\n\n---\n\n");
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "text" as const,
|
|
303
|
+
text: formatted,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return {
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: "text" as const,
|
|
312
|
+
text: `Search error: ${String(err)}`,
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Tool: manage_index
|
|
321
|
+
server.tool(
|
|
322
|
+
"manage_index",
|
|
323
|
+
"Manage search indexes. Target 'memory' for journal/entities, 'conversations' for past Claude Code sessions.",
|
|
324
|
+
{
|
|
325
|
+
target: z.enum(["memory", "conversations"]).describe("Which index to manage"),
|
|
326
|
+
action: z
|
|
327
|
+
.enum(["rebuild", "update", "stats"])
|
|
328
|
+
.describe(
|
|
329
|
+
"'rebuild' to reindex from scratch, 'update' for incremental (conversations only), 'stats' to get counts",
|
|
330
|
+
),
|
|
331
|
+
},
|
|
332
|
+
async ({ target, action }) => {
|
|
333
|
+
try {
|
|
334
|
+
if (target === "memory") {
|
|
335
|
+
if (action === "rebuild" || action === "update") {
|
|
336
|
+
const result = await rebuildIndex();
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: "text" as const,
|
|
341
|
+
text: `Memory index rebuilt. Indexed ${result.itemCount} items.`,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
} else {
|
|
346
|
+
const stats = await getIndexStats();
|
|
347
|
+
return {
|
|
348
|
+
content: [
|
|
349
|
+
{ type: "text" as const, text: `Memory index contains ${stats.itemCount} items.` },
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
if (action === "rebuild") {
|
|
355
|
+
// Run in background - don't wait
|
|
356
|
+
rebuildConversationIndex()
|
|
357
|
+
.then((result) =>
|
|
358
|
+
console.log(
|
|
359
|
+
`[Macrodata] Conversation index rebuilt: ${result.exchangeCount} exchanges`,
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
.catch((err) =>
|
|
363
|
+
console.error(`[Macrodata] Conversation index rebuild failed: ${err}`),
|
|
364
|
+
);
|
|
365
|
+
return {
|
|
366
|
+
content: [
|
|
367
|
+
{
|
|
368
|
+
type: "text" as const,
|
|
369
|
+
text: `Conversation index rebuild started in background.`,
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
} else if (action === "update") {
|
|
374
|
+
// Incremental update - also background
|
|
375
|
+
updateConversationIndex()
|
|
376
|
+
.then((result) =>
|
|
377
|
+
console.log(
|
|
378
|
+
`[Macrodata] Conversation index updated: ${result.filesUpdated} files (${result.skipped} skipped, total: ${result.exchangeCount})`,
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
.catch((err) =>
|
|
382
|
+
console.error(`[Macrodata] Conversation index update failed: ${err}`),
|
|
383
|
+
);
|
|
384
|
+
return {
|
|
385
|
+
content: [
|
|
386
|
+
{ type: "text" as const, text: `Conversation index update started in background.` },
|
|
387
|
+
],
|
|
388
|
+
};
|
|
389
|
+
} else {
|
|
390
|
+
const stats = await getConversationIndexStats();
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: "text" as const,
|
|
395
|
+
text: `Conversation index contains ${stats.exchangeCount} exchanges.`,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (err) {
|
|
402
|
+
return {
|
|
403
|
+
content: [
|
|
404
|
+
{ type: "text" as const, text: `Failed to ${action} ${target} index: ${String(err)}` },
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Tool: schedule
|
|
412
|
+
server.tool(
|
|
413
|
+
"schedule",
|
|
414
|
+
"Create a reminder. Use type 'cron' for recurring (expression is cron syntax) or 'once' for one-shot (expression is ISO datetime).",
|
|
415
|
+
{
|
|
416
|
+
type: z.enum(["cron", "once"]).describe("'cron' for recurring, 'once' for one-shot"),
|
|
417
|
+
id: z.string().describe("Unique identifier for this reminder"),
|
|
418
|
+
expression: z
|
|
419
|
+
.string()
|
|
420
|
+
.describe(
|
|
421
|
+
"Cron expression (e.g., '0 9 * * *') or ISO datetime (e.g., '2026-01-31T10:00:00')",
|
|
422
|
+
),
|
|
423
|
+
description: z.string().describe("What this reminder is for"),
|
|
424
|
+
payload: z.string().describe("Message to process when reminder fires"),
|
|
425
|
+
model: z
|
|
426
|
+
.string()
|
|
427
|
+
.optional()
|
|
428
|
+
.describe("Model to use (e.g., 'anthropic/claude-opus-4-6' for deep thinking tasks)"),
|
|
429
|
+
},
|
|
430
|
+
async ({ type, id, expression, description, payload, model }) => {
|
|
431
|
+
const schedule: Schedule = {
|
|
432
|
+
id,
|
|
433
|
+
type,
|
|
434
|
+
expression,
|
|
435
|
+
description,
|
|
436
|
+
payload,
|
|
437
|
+
agent: "claude",
|
|
438
|
+
model,
|
|
439
|
+
createdAt: new Date().toISOString(),
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// Save to individual file (overwrites if exists)
|
|
443
|
+
saveSchedule(schedule);
|
|
444
|
+
|
|
445
|
+
const typeLabel = type === "cron" ? "recurring" : "one-shot";
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "text" as const,
|
|
450
|
+
text: `Created ${typeLabel} reminder: ${id} (${expression})${model ? ` with model ${model}` : ""}`,
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
},
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Tool: list_reminders
|
|
458
|
+
server.tool("list_reminders", "List all active scheduled reminders", {}, async () => {
|
|
459
|
+
const schedules = loadAllSchedules();
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
content: [
|
|
463
|
+
{
|
|
464
|
+
type: "text" as const,
|
|
465
|
+
text: JSON.stringify(schedules, null, 2),
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Tool: remove_reminder
|
|
472
|
+
server.tool(
|
|
473
|
+
"remove_reminder",
|
|
474
|
+
"Remove a scheduled reminder",
|
|
475
|
+
{
|
|
476
|
+
id: z.string().describe("ID of the reminder to remove"),
|
|
477
|
+
},
|
|
478
|
+
async ({ id }) => {
|
|
479
|
+
const removed = deleteScheduleFile(id);
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
content: [
|
|
483
|
+
{
|
|
484
|
+
type: "text" as const,
|
|
485
|
+
text: removed ? `Removed reminder: ${id}` : `Reminder not found: ${id}`,
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
// Tool: save_conversation_summary
|
|
493
|
+
server.tool(
|
|
494
|
+
"save_conversation_summary",
|
|
495
|
+
"Save a summary of the current conversation for context recovery in future sessions",
|
|
496
|
+
{
|
|
497
|
+
summary: z.string().describe("Brief summary of what was discussed/accomplished"),
|
|
498
|
+
keyDecisions: z.array(z.string()).optional().describe("Important decisions made"),
|
|
499
|
+
openThreads: z.array(z.string()).optional().describe("Topics to follow up on"),
|
|
500
|
+
learnedPatterns: z
|
|
501
|
+
.array(z.string())
|
|
502
|
+
.optional()
|
|
503
|
+
.describe("New patterns learned about the user"),
|
|
504
|
+
notes: z.string().optional().describe("Freeform notes"),
|
|
505
|
+
},
|
|
506
|
+
async ({ summary, keyDecisions, openThreads, learnedPatterns, notes }) => {
|
|
507
|
+
ensureDirectories();
|
|
508
|
+
|
|
509
|
+
const parts = [summary];
|
|
510
|
+
if (keyDecisions?.length) parts.push(`Decisions: ${keyDecisions.join(", ")}`);
|
|
511
|
+
if (openThreads?.length) parts.push(`Open threads: ${openThreads.join(", ")}`);
|
|
512
|
+
if (learnedPatterns?.length) parts.push(`Learned: ${learnedPatterns.join(", ")}`);
|
|
513
|
+
if (notes) parts.push(`Notes: ${notes}`);
|
|
514
|
+
|
|
515
|
+
const entry: JournalEntry = {
|
|
516
|
+
timestamp: new Date().toISOString(),
|
|
517
|
+
topic: "conversation-summary",
|
|
518
|
+
content: parts.join("\n"),
|
|
519
|
+
metadata: { source: "conversation" },
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const journalPath = getTodayJournalPath();
|
|
523
|
+
appendFileSync(journalPath, JSON.stringify(entry) + "\n");
|
|
524
|
+
|
|
525
|
+
// Index for semantic search
|
|
526
|
+
try {
|
|
527
|
+
await indexJournalEntry(entry);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.error("[save_conversation_summary] Failed to index:", err);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
content: [
|
|
534
|
+
{
|
|
535
|
+
type: "text" as const,
|
|
536
|
+
text: "Conversation summary saved.",
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// Tool: get_recent_summaries
|
|
544
|
+
server.tool(
|
|
545
|
+
"get_recent_summaries",
|
|
546
|
+
"Get recent conversation summaries for context recovery",
|
|
547
|
+
{
|
|
548
|
+
count: z.number().default(7).describe("Number of summaries to retrieve"),
|
|
549
|
+
},
|
|
550
|
+
async ({ count }) => {
|
|
551
|
+
// Get recent journal entries filtered by topic
|
|
552
|
+
let entries = getRecentJournalEntries(count * 3);
|
|
553
|
+
entries = entries.filter((e) => e.topic === "conversation-summary").slice(0, count);
|
|
554
|
+
|
|
555
|
+
if (entries.length === 0) {
|
|
556
|
+
return {
|
|
557
|
+
content: [
|
|
558
|
+
{
|
|
559
|
+
type: "text" as const,
|
|
560
|
+
text: "No conversation summaries yet.",
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
content: [
|
|
568
|
+
{
|
|
569
|
+
type: "text" as const,
|
|
570
|
+
text: JSON.stringify(entries, null, 2),
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
};
|
|
574
|
+
},
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
// Tool: search_conversations
|
|
578
|
+
server.tool(
|
|
579
|
+
"search_conversations",
|
|
580
|
+
"Search past Claude Code conversations for similar problems/solutions. By default searches current project first, with recent conversations weighted higher.",
|
|
581
|
+
{
|
|
582
|
+
query: z
|
|
583
|
+
.string()
|
|
584
|
+
.describe(
|
|
585
|
+
"What to search for (e.g., 'fixing TypeScript errors', 'performance optimization')",
|
|
586
|
+
),
|
|
587
|
+
projectOnly: z
|
|
588
|
+
.boolean()
|
|
589
|
+
.default(false)
|
|
590
|
+
.describe("Only search current project (default: search all but boost current)"),
|
|
591
|
+
limit: z.number().default(5).describe("Maximum results to return"),
|
|
592
|
+
},
|
|
593
|
+
async ({ query, projectOnly, limit }) => {
|
|
594
|
+
try {
|
|
595
|
+
// Get current project from CWD environment (set by hook)
|
|
596
|
+
const currentProject = process.env.CLAUDE_PROJECT_DIR;
|
|
597
|
+
|
|
598
|
+
const results = await searchConversations(query, {
|
|
599
|
+
currentProject,
|
|
600
|
+
projectOnly,
|
|
601
|
+
limit,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (results.length === 0) {
|
|
605
|
+
return {
|
|
606
|
+
content: [
|
|
607
|
+
{
|
|
608
|
+
type: "text" as const,
|
|
609
|
+
text: "No matching conversations found. Try manage_index(target: 'conversations', action: 'update').",
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Format results: metadata + user prompt only (not full response)
|
|
616
|
+
const formatted = results
|
|
617
|
+
.map((r, i) => {
|
|
618
|
+
const date = new Date(r.exchange.timestamp).toLocaleDateString();
|
|
619
|
+
const branch = r.exchange.branch ? ` (${r.exchange.branch})` : "";
|
|
620
|
+
return `[${i + 1}] ${r.exchange.project}${branch} - ${date}
|
|
621
|
+
"${r.exchange.userPrompt.slice(0, 200)}${r.exchange.userPrompt.length > 200 ? "..." : ""}"
|
|
622
|
+
Session: ${r.exchange.sessionId}`;
|
|
623
|
+
})
|
|
624
|
+
.join("\n\n");
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
content: [
|
|
628
|
+
{
|
|
629
|
+
type: "text" as const,
|
|
630
|
+
text: `Found ${results.length} relevant conversation(s):\n\n${formatted}\n\nUse expand_conversation to see full context.`,
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
};
|
|
634
|
+
} catch (err) {
|
|
635
|
+
return {
|
|
636
|
+
content: [
|
|
637
|
+
{
|
|
638
|
+
type: "text" as const,
|
|
639
|
+
text: `Search error: ${String(err)}`,
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// Tool: expand_conversation
|
|
648
|
+
server.tool(
|
|
649
|
+
"expand_conversation",
|
|
650
|
+
"Load full context from a past conversation. Use after search_conversations to see the complete exchange.",
|
|
651
|
+
{
|
|
652
|
+
sessionPath: z.string().describe("Session file path from search results"),
|
|
653
|
+
messageUuid: z.string().optional().describe("Specific message UUID to center on"),
|
|
654
|
+
contextMessages: z.number().default(10).describe("Number of messages to include"),
|
|
655
|
+
},
|
|
656
|
+
async ({ sessionPath, messageUuid, contextMessages }) => {
|
|
657
|
+
try {
|
|
658
|
+
// Resolve session path if only ID given
|
|
659
|
+
let fullPath = sessionPath;
|
|
660
|
+
if (!sessionPath.startsWith("/")) {
|
|
661
|
+
// Assume it's a session ID, need to find the file
|
|
662
|
+
// For now, require full path
|
|
663
|
+
return {
|
|
664
|
+
content: [
|
|
665
|
+
{
|
|
666
|
+
type: "text" as const,
|
|
667
|
+
text: "Please provide the full session path from search results.",
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const result = await expandConversation(fullPath, messageUuid || "", contextMessages);
|
|
674
|
+
|
|
675
|
+
const formatted = result.messages
|
|
676
|
+
.map((m) => {
|
|
677
|
+
const prefix = m.role === "user" ? "User" : "Assistant";
|
|
678
|
+
return `**${prefix}**: ${m.content}`;
|
|
679
|
+
})
|
|
680
|
+
.join("\n\n---\n\n");
|
|
681
|
+
|
|
682
|
+
const header = `Project: ${result.project}${result.branch ? ` (${result.branch})` : ""}\n\n`;
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
content: [
|
|
686
|
+
{
|
|
687
|
+
type: "text" as const,
|
|
688
|
+
text: header + formatted,
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
};
|
|
692
|
+
} catch (err) {
|
|
693
|
+
return {
|
|
694
|
+
content: [
|
|
695
|
+
{
|
|
696
|
+
type: "text" as const,
|
|
697
|
+
text: `Failed to expand conversation: ${String(err)}`,
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
return server;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Start server
|
|
709
|
+
export async function main(): Promise<void> {
|
|
710
|
+
const server = createServer();
|
|
711
|
+
const transport = new StdioServerTransport();
|
|
712
|
+
await server.connect(transport);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Auto-start only when run directly, so importing this module for tests does
|
|
716
|
+
// not open a stdio transport.
|
|
717
|
+
export function isRunAsMain(argv1: string | undefined, moduleUrl: string): boolean {
|
|
718
|
+
return Boolean(argv1) && moduleUrl === `file://${argv1}`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/* v8 ignore next 3 -- entry-point glue: only runs when this file is the process
|
|
722
|
+
entry (node dist/src/index.js); that path is a subprocess vitest cannot
|
|
723
|
+
instrument. main() and isRunAsMain() are each covered directly above. */
|
|
724
|
+
if (isRunAsMain(process.argv[1], import.meta.url)) {
|
|
725
|
+
main().catch(console.error);
|
|
726
|
+
}
|