@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.
Files changed (46) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +152 -0
  3. package/USAGE.md +59 -0
  4. package/bin/detect-user.sh +53 -0
  5. package/bin/index-conversations.ts +34 -0
  6. package/bin/macrodata-daemon.ts +28 -0
  7. package/bin/macrodata-hook.sh +277 -0
  8. package/dist/bin/index-conversations.js +31 -0
  9. package/dist/bin/macrodata-daemon.js +30 -0
  10. package/dist/opencode/context.js +210 -0
  11. package/dist/opencode/conversations.js +367 -0
  12. package/dist/opencode/index.js +155 -0
  13. package/dist/opencode/journal.js +108 -0
  14. package/dist/opencode/logger.js +29 -0
  15. package/dist/opencode/search.js +210 -0
  16. package/dist/opencode/skills/macrodata-distill/SKILL.md +171 -0
  17. package/dist/opencode/skills/macrodata-dreamtime/SKILL.md +120 -0
  18. package/dist/opencode/skills/macrodata-memory-maintenance/SKILL.md +96 -0
  19. package/dist/opencode/skills/macrodata-onboarding/SKILL.md +346 -0
  20. package/dist/opencode/tools.js +367 -0
  21. package/dist/src/config.js +55 -0
  22. package/dist/src/conversations.js +513 -0
  23. package/dist/src/daemon.js +582 -0
  24. package/dist/src/detect-user.js +73 -0
  25. package/dist/src/embeddings.js +190 -0
  26. package/dist/src/index.js +413 -0
  27. package/dist/src/indexer.js +286 -0
  28. package/opencode/context.ts +322 -0
  29. package/opencode/conversations.ts +467 -0
  30. package/opencode/index.ts +208 -0
  31. package/opencode/journal.ts +153 -0
  32. package/opencode/logger.ts +32 -0
  33. package/opencode/search.ts +288 -0
  34. package/opencode/skills/macrodata-distill/SKILL.md +171 -0
  35. package/opencode/skills/macrodata-dreamtime/SKILL.md +120 -0
  36. package/opencode/skills/macrodata-memory-maintenance/SKILL.md +96 -0
  37. package/opencode/skills/macrodata-onboarding/SKILL.md +346 -0
  38. package/opencode/tools.ts +453 -0
  39. package/package.json +87 -0
  40. package/src/config.ts +66 -0
  41. package/src/conversations.ts +709 -0
  42. package/src/daemon.ts +785 -0
  43. package/src/detect-user.ts +97 -0
  44. package/src/embeddings.ts +262 -0
  45. package/src/index.ts +726 -0
  46. package/src/indexer.ts +394 -0
@@ -0,0 +1,367 @@
1
+ import { getRemindersDir } from "../src/config.js";
2
+ import { logger } from "./logger.js";
3
+ import { getMemoryIndexStats, rebuildMemoryIndex, searchMemory } from "./search.js";
4
+ import { getRecentJournal, getRecentSummaries, logJournal, saveConversationSummary } from "./journal.js";
5
+ import { getConversationIndexStats, rebuildConversationIndex, searchConversations } from "./conversations.js";
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
7
+ import { join } from "path";
8
+ import { tool } from "@opencode-ai/plugin";
9
+
10
+ //#region opencode/tools.ts
11
+ /**
12
+ * Macrodata tools for OpenCode
13
+ *
14
+ * Separate tools for memory operations
15
+ */
16
+ function loadAllSchedules() {
17
+ const remindersDir = getRemindersDir();
18
+ const schedules = [];
19
+ try {
20
+ if (!existsSync(remindersDir)) return schedules;
21
+ const files = readdirSync(remindersDir).filter((f) => f.endsWith(".json"));
22
+ for (const file of files) {
23
+ try {
24
+ const content = readFileSync(join(remindersDir, file), "utf-8");
25
+ schedules.push(JSON.parse(content));
26
+ } catch {}
27
+ }
28
+ } catch {}
29
+ return schedules;
30
+ }
31
+ function saveSchedule(schedule) {
32
+ const remindersDir = getRemindersDir();
33
+ if (!existsSync(remindersDir)) {
34
+ mkdirSync(remindersDir, { recursive: true });
35
+ }
36
+ const filePath = join(remindersDir, `${schedule.id}.json`);
37
+ writeFileSync(filePath, JSON.stringify(schedule, null, 2));
38
+ }
39
+ function deleteScheduleFile(id) {
40
+ const remindersDir = getRemindersDir();
41
+ const filePath = join(remindersDir, `${id}.json`);
42
+ try {
43
+ if (existsSync(filePath)) {
44
+ unlinkSync(filePath);
45
+ return true;
46
+ }
47
+ } catch {}
48
+ return false;
49
+ }
50
+ const logJournalTool = tool({
51
+ description: "Write a journal entry. Use this to record observations, decisions, or things to remember.",
52
+ args: {
53
+ topic: tool.schema.string().describe("Short topic/category for the entry"),
54
+ content: tool.schema.string().describe("The journal entry content"),
55
+ agentIntent: tool.schema.string().optional().describe("Optional: why you're logging this")
56
+ },
57
+ async execute(args) {
58
+ if (!args.topic || !args.content) {
59
+ return JSON.stringify({
60
+ success: false,
61
+ error: "Requires 'topic' and 'content'"
62
+ });
63
+ }
64
+ await logJournal(args.topic, args.content, {
65
+ source: "opencode-tool",
66
+ intent: args.agentIntent
67
+ });
68
+ return JSON.stringify({
69
+ success: true,
70
+ message: `Logged to journal: ${args.topic}`
71
+ });
72
+ }
73
+ });
74
+ const getRecentJournalTool = tool({
75
+ description: "Retrieve recent journal entries for context",
76
+ args: { count: tool.schema.number().optional().describe("Number of entries to retrieve (default: 40)") },
77
+ async execute(args) {
78
+ const entries = getRecentJournal(args.count || 40);
79
+ return JSON.stringify({
80
+ success: true,
81
+ entries
82
+ });
83
+ }
84
+ });
85
+ const saveConversationSummaryTool = tool({
86
+ description: "Save a summary of the current conversation for context recovery in future sessions",
87
+ args: {
88
+ summary: tool.schema.string().describe("Brief summary of what was discussed/accomplished"),
89
+ keyDecisions: tool.schema.array(tool.schema.string()).optional().describe("Important decisions made"),
90
+ openThreads: tool.schema.array(tool.schema.string()).optional().describe("Topics to follow up on"),
91
+ learnedPatterns: tool.schema.array(tool.schema.string()).optional().describe("New patterns learned about the user"),
92
+ notes: tool.schema.string().optional().describe("Freeform notes for anything that doesn't fit structured fields")
93
+ },
94
+ async execute(args) {
95
+ if (!args.summary) {
96
+ return JSON.stringify({
97
+ success: false,
98
+ error: "Requires 'summary'"
99
+ });
100
+ }
101
+ await saveConversationSummary({
102
+ summary: args.summary,
103
+ keyDecisions: args.keyDecisions,
104
+ openThreads: args.openThreads,
105
+ learnedPatterns: args.learnedPatterns,
106
+ notes: args.notes
107
+ });
108
+ return JSON.stringify({
109
+ success: true,
110
+ message: "Conversation summary saved"
111
+ });
112
+ }
113
+ });
114
+ const getRecentSummariesTool = tool({
115
+ description: "Get recent conversation summaries for context recovery",
116
+ args: { count: tool.schema.number().optional().describe("Number of summaries to retrieve (default: 7)") },
117
+ async execute(args) {
118
+ const summaries = getRecentSummaries(args.count || 7);
119
+ return JSON.stringify({
120
+ success: true,
121
+ summaries
122
+ });
123
+ }
124
+ });
125
+ const searchMemoryTool = tool({
126
+ description: "Semantic search over your history - journal, state files, projects, people. Use to find relevant context.",
127
+ args: {
128
+ query: tool.schema.string().describe("Natural language query to search for"),
129
+ type: tool.schema.enum([
130
+ "journal",
131
+ "state",
132
+ "project",
133
+ "person",
134
+ "meeting",
135
+ "topic"
136
+ ]).optional().describe("Filter by content type"),
137
+ limit: tool.schema.number().optional().describe("Maximum results to return (default: 5)"),
138
+ since: tool.schema.string().optional().describe("Only include items after this ISO date")
139
+ },
140
+ async execute(args) {
141
+ if (!args.query) {
142
+ return JSON.stringify({
143
+ success: false,
144
+ error: "Requires 'query'"
145
+ });
146
+ }
147
+ const results = await searchMemory(args.query, {
148
+ limit: args.limit || 5,
149
+ type: args.type,
150
+ since: args.since
151
+ });
152
+ if (results.length === 0) {
153
+ return JSON.stringify({
154
+ success: true,
155
+ message: "No matches found. Try rebuilding the index with rebuild_memory_index",
156
+ results: []
157
+ });
158
+ }
159
+ return JSON.stringify({
160
+ success: true,
161
+ count: results.length,
162
+ results: results.map((r) => ({
163
+ type: r.type,
164
+ source: r.source,
165
+ section: r.section,
166
+ score: Math.round(r.score * 100) / 100,
167
+ content: r.content.slice(0, 500)
168
+ }))
169
+ });
170
+ }
171
+ });
172
+ const searchConversationsTool = tool({
173
+ description: "Search past OpenCode sessions",
174
+ args: {
175
+ query: tool.schema.string().describe("Natural language query to search for"),
176
+ projectOnly: tool.schema.boolean().optional().describe("Only search current project"),
177
+ limit: tool.schema.number().optional().describe("Maximum results to return (default: 5)")
178
+ },
179
+ async execute(args) {
180
+ if (!args.query) {
181
+ return JSON.stringify({
182
+ success: false,
183
+ error: "Requires 'query'"
184
+ });
185
+ }
186
+ const results = await searchConversations(args.query, {
187
+ currentProject: process.cwd(),
188
+ limit: args.limit || 5,
189
+ projectOnly: args.projectOnly
190
+ });
191
+ if (results.length === 0) {
192
+ return JSON.stringify({
193
+ success: true,
194
+ message: "No matching conversations. Try rebuilding with rebuild_memory_index",
195
+ results: []
196
+ });
197
+ }
198
+ return JSON.stringify({
199
+ success: true,
200
+ count: results.length,
201
+ results: results.map((r) => ({
202
+ project: r.exchange.project,
203
+ timestamp: r.exchange.timestamp,
204
+ sessionId: r.exchange.sessionId,
205
+ score: Math.round(r.adjustedScore * 100) / 100,
206
+ userPrompt: r.exchange.userPrompt.slice(0, 200),
207
+ assistantSummary: r.exchange.assistantSummary.slice(0, 200)
208
+ }))
209
+ });
210
+ }
211
+ });
212
+ const rebuildMemoryIndexTool = tool({
213
+ description: "Rebuild the semantic search index from scratch. Use if index seems stale or corrupted.",
214
+ args: {},
215
+ async execute() {
216
+ await rebuildMemoryIndex();
217
+ const memoryStats = await getMemoryIndexStats();
218
+ rebuildConversationIndex().then((result) => logger.log(`Conversation index rebuilt: ${result.exchangeCount} exchanges`)).catch((err) => logger.error(`Conversation index rebuild failed: ${err}`));
219
+ return JSON.stringify({
220
+ success: true,
221
+ message: "Memory index rebuilt. Conversation index rebuilding in background.",
222
+ stats: { memoryItems: memoryStats.itemCount }
223
+ });
224
+ }
225
+ });
226
+ const getMemoryIndexStatsTool = tool({
227
+ description: "Get statistics about the memory index",
228
+ args: {},
229
+ async execute() {
230
+ const memoryStats = await getMemoryIndexStats();
231
+ const convStats = await getConversationIndexStats();
232
+ return JSON.stringify({
233
+ success: true,
234
+ memoryItems: memoryStats.itemCount,
235
+ conversationExchanges: convStats.exchangeCount
236
+ });
237
+ }
238
+ });
239
+ const scheduleReminderTool = tool({
240
+ description: "Schedule a recurring reminder using cron syntax. Examples: '0 9 * * *' = 9am daily, '0 */2 * * *' = every 2 hours. IMPORTANT: Check the current time before using this tool to ensure accurate scheduling.",
241
+ args: {
242
+ id: tool.schema.string().describe("Unique reminder identifier"),
243
+ cronExpression: tool.schema.string().describe("Cron expression (e.g., '0 9 * * *' for 9am daily)"),
244
+ description: tool.schema.string().describe("What this reminder is for"),
245
+ payload: tool.schema.string().describe("Message to process when reminder fires"),
246
+ model: tool.schema.string().optional().describe("Model to use for this reminder (see macrodata-models in context for available options)")
247
+ },
248
+ async execute(args) {
249
+ if (!args.id || !args.cronExpression || !args.description || !args.payload) {
250
+ return JSON.stringify({
251
+ success: false,
252
+ error: "Requires 'id', 'cronExpression', 'description', and 'payload'"
253
+ });
254
+ }
255
+ const schedule = {
256
+ id: args.id,
257
+ type: "cron",
258
+ expression: args.cronExpression,
259
+ description: args.description,
260
+ payload: args.payload,
261
+ agent: "opencode",
262
+ model: args.model,
263
+ createdAt: new Date().toISOString()
264
+ };
265
+ saveSchedule(schedule);
266
+ return JSON.stringify({
267
+ success: true,
268
+ message: `Created recurring reminder: ${args.id} (${args.cronExpression})${args.model ? ` with model ${args.model}` : ""}`
269
+ });
270
+ }
271
+ });
272
+ const scheduleOnceTool = tool({
273
+ description: "Schedule a one-shot reminder at a specific date/time. The reminder fires once and is automatically removed. IMPORTANT: Check the current time before using this tool to ensure accurate scheduling.",
274
+ args: {
275
+ id: tool.schema.string().describe("Unique reminder identifier"),
276
+ datetime: tool.schema.string().describe("ISO 8601 datetime (e.g., '2026-01-06T10:00:00')"),
277
+ description: tool.schema.string().describe("What this reminder is for"),
278
+ payload: tool.schema.string().describe("Message to process when reminder fires"),
279
+ model: tool.schema.string().optional().describe("Model to use for this reminder (see macrodata-models in context for available options)")
280
+ },
281
+ async execute(args) {
282
+ if (!args.id || !args.datetime || !args.description || !args.payload) {
283
+ return JSON.stringify({
284
+ success: false,
285
+ error: "Requires 'id', 'datetime', 'description', and 'payload'"
286
+ });
287
+ }
288
+ const schedule = {
289
+ id: args.id,
290
+ type: "once",
291
+ expression: args.datetime,
292
+ description: args.description,
293
+ payload: args.payload,
294
+ agent: "opencode",
295
+ model: args.model,
296
+ createdAt: new Date().toISOString()
297
+ };
298
+ saveSchedule(schedule);
299
+ return JSON.stringify({
300
+ success: true,
301
+ message: `Scheduled one-shot reminder: ${args.id} at ${args.datetime}${args.model ? ` with model ${args.model}` : ""}`
302
+ });
303
+ }
304
+ });
305
+ const removeReminderTool = tool({
306
+ description: "Remove a scheduled reminder",
307
+ args: { id: tool.schema.string().describe("Reminder ID to remove") },
308
+ async execute(args) {
309
+ if (!args.id) {
310
+ return JSON.stringify({
311
+ success: false,
312
+ error: "Requires 'id'"
313
+ });
314
+ }
315
+ const removed = deleteScheduleFile(args.id);
316
+ return JSON.stringify({
317
+ success: removed,
318
+ message: removed ? `Removed reminder: ${args.id}` : `Reminder not found: ${args.id}`
319
+ });
320
+ }
321
+ });
322
+ const listRemindersTool = tool({
323
+ description: "List all scheduled reminders",
324
+ args: {},
325
+ async execute() {
326
+ const schedules = loadAllSchedules();
327
+ return JSON.stringify({
328
+ success: true,
329
+ reminders: schedules
330
+ });
331
+ }
332
+ });
333
+ const getRelatedTool = tool({
334
+ description: "Get entries related to a specific memory item. Useful for exploring associative connections in your memory.",
335
+ args: { id: tool.schema.string().describe("The ID of the memory item to find related entries for") },
336
+ async execute(args) {
337
+ if (!args.id) {
338
+ return JSON.stringify({
339
+ success: false,
340
+ error: "Requires 'id'"
341
+ });
342
+ }
343
+ return JSON.stringify({
344
+ success: true,
345
+ message: "Related items feature not yet implemented",
346
+ related: []
347
+ });
348
+ }
349
+ });
350
+ const memoryTools = {
351
+ macrodata_log_journal: logJournalTool,
352
+ macrodata_get_recent_journal: getRecentJournalTool,
353
+ macrodata_save_conversation_summary: saveConversationSummaryTool,
354
+ macrodata_get_recent_summaries: getRecentSummariesTool,
355
+ macrodata_search_memory: searchMemoryTool,
356
+ macrodata_search_conversations: searchConversationsTool,
357
+ macrodata_rebuild_memory_index: rebuildMemoryIndexTool,
358
+ macrodata_get_memory_index_stats: getMemoryIndexStatsTool,
359
+ macrodata_schedule_reminder: scheduleReminderTool,
360
+ macrodata_schedule_once: scheduleOnceTool,
361
+ macrodata_remove_reminder: removeReminderTool,
362
+ macrodata_list_reminders: listRemindersTool,
363
+ macrodata_get_related: getRelatedTool
364
+ };
365
+
366
+ //#endregion
367
+ export { getMemoryIndexStatsTool, getRecentJournalTool, getRecentSummariesTool, getRelatedTool, listRemindersTool, logJournalTool, memoryTools, rebuildMemoryIndexTool, removeReminderTool, saveConversationSummaryTool, scheduleOnceTool, scheduleReminderTool, searchConversationsTool, searchMemoryTool };
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+
5
+ //#region src/config.ts
6
+ /**
7
+ * Shared configuration utilities
8
+ *
9
+ * All paths are resolved dynamically (not cached at module load)
10
+ * so that config changes take effect without restart.
11
+ */
12
+ const DEFAULT_ROOT = join(homedir(), ".config", "macrodata");
13
+ /**
14
+ * Get the macrodata state root directory.
15
+ * Priority: MACRODATA_ROOT env > ~/.config/macrodata/config.json > ~/.config/macrodata
16
+ *
17
+ * Resolved fresh each call so config changes take effect immediately.
18
+ */
19
+ function getStateRoot() {
20
+ if (process.env.MACRODATA_ROOT) {
21
+ return process.env.MACRODATA_ROOT;
22
+ }
23
+ const configPath = join(DEFAULT_ROOT, "config.json");
24
+ if (existsSync(configPath)) {
25
+ try {
26
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
27
+ if (config.root) return config.root;
28
+ } catch {}
29
+ }
30
+ return DEFAULT_ROOT;
31
+ }
32
+ function getStateDir() {
33
+ return join(getStateRoot(), "state");
34
+ }
35
+ function getEntitiesDir() {
36
+ return join(getStateRoot(), "entities");
37
+ }
38
+ function getJournalDir() {
39
+ return join(getStateRoot(), "journal");
40
+ }
41
+ function getSignalsDir() {
42
+ return join(getStateRoot(), "signals");
43
+ }
44
+ function getIndexDir() {
45
+ return join(getStateRoot(), ".index");
46
+ }
47
+ function getTopicsDir() {
48
+ return join(getStateRoot(), "topics");
49
+ }
50
+ function getRemindersDir() {
51
+ return join(getStateRoot(), "reminders");
52
+ }
53
+
54
+ //#endregion
55
+ export { getEntitiesDir, getIndexDir, getJournalDir, getRemindersDir, getStateDir, getStateRoot };