@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,453 @@
1
+ /**
2
+ * Macrodata tools for OpenCode
3
+ *
4
+ * Separate tools for memory operations
5
+ */
6
+
7
+ import { tool } from "@opencode-ai/plugin";
8
+ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, unlinkSync } from "fs";
9
+ import { join } from "path";
10
+ import { getRemindersDir } from "../src/config.js";
11
+ import {
12
+ logJournal,
13
+ getRecentJournal,
14
+ getRecentSummaries,
15
+ saveConversationSummary,
16
+ } from "./journal.js";
17
+ import { searchMemory, rebuildMemoryIndex, getMemoryIndexStats } from "./search.js";
18
+ import {
19
+ searchConversations,
20
+ rebuildConversationIndex,
21
+ getConversationIndexStats,
22
+ } from "./conversations.js";
23
+ import { logger } from "./logger.js";
24
+
25
+ interface Schedule {
26
+ id: string;
27
+ type: "cron" | "once";
28
+ expression: string;
29
+ description: string;
30
+ payload: string;
31
+ agent?: "opencode" | "claude";
32
+ model?: string;
33
+ createdAt: string;
34
+ }
35
+
36
+ function loadAllSchedules(): Schedule[] {
37
+ const remindersDir = getRemindersDir();
38
+ const schedules: Schedule[] = [];
39
+
40
+ try {
41
+ if (!existsSync(remindersDir)) return schedules;
42
+
43
+ const files = readdirSync(remindersDir).filter((f) => f.endsWith(".json"));
44
+ for (const file of files) {
45
+ try {
46
+ const content = readFileSync(join(remindersDir, file), "utf-8");
47
+ schedules.push(JSON.parse(content));
48
+ } catch {
49
+ // Skip malformed files
50
+ }
51
+ }
52
+ } catch {
53
+ // Ignore
54
+ }
55
+
56
+ return schedules;
57
+ }
58
+
59
+ function saveSchedule(schedule: Schedule): void {
60
+ const remindersDir = getRemindersDir();
61
+ if (!existsSync(remindersDir)) {
62
+ mkdirSync(remindersDir, { recursive: true });
63
+ }
64
+ const filePath = join(remindersDir, `${schedule.id}.json`);
65
+ writeFileSync(filePath, JSON.stringify(schedule, null, 2));
66
+ }
67
+
68
+ function deleteScheduleFile(id: string): boolean {
69
+ const remindersDir = getRemindersDir();
70
+ const filePath = join(remindersDir, `${id}.json`);
71
+
72
+ try {
73
+ if (existsSync(filePath)) {
74
+ unlinkSync(filePath);
75
+ return true;
76
+ }
77
+ } catch {
78
+ // Ignore
79
+ }
80
+ return false;
81
+ }
82
+
83
+ // --- Journal Tools ---
84
+
85
+ export const logJournalTool = tool({
86
+ description:
87
+ "Write a journal entry. Use this to record observations, decisions, or things to remember.",
88
+ args: {
89
+ topic: tool.schema.string().describe("Short topic/category for the entry"),
90
+ content: tool.schema.string().describe("The journal entry content"),
91
+ agentIntent: tool.schema.string().optional().describe("Optional: why you're logging this"),
92
+ },
93
+ async execute(args) {
94
+ if (!args.topic || !args.content) {
95
+ return JSON.stringify({ success: false, error: "Requires 'topic' and 'content'" });
96
+ }
97
+
98
+ await logJournal(args.topic, args.content, {
99
+ source: "opencode-tool",
100
+ intent: args.agentIntent,
101
+ });
102
+
103
+ return JSON.stringify({ success: true, message: `Logged to journal: ${args.topic}` });
104
+ },
105
+ });
106
+
107
+ export const getRecentJournalTool = tool({
108
+ description: "Retrieve recent journal entries for context",
109
+ args: {
110
+ count: tool.schema.number().optional().describe("Number of entries to retrieve (default: 40)"),
111
+ },
112
+ async execute(args) {
113
+ const entries = getRecentJournal(args.count || 40);
114
+ return JSON.stringify({ success: true, entries });
115
+ },
116
+ });
117
+
118
+ // --- Summary Tools ---
119
+
120
+ export const saveConversationSummaryTool = tool({
121
+ description: "Save a summary of the current conversation for context recovery in future sessions",
122
+ args: {
123
+ summary: tool.schema.string().describe("Brief summary of what was discussed/accomplished"),
124
+ keyDecisions: tool.schema
125
+ .array(tool.schema.string())
126
+ .optional()
127
+ .describe("Important decisions made"),
128
+ openThreads: tool.schema
129
+ .array(tool.schema.string())
130
+ .optional()
131
+ .describe("Topics to follow up on"),
132
+ learnedPatterns: tool.schema
133
+ .array(tool.schema.string())
134
+ .optional()
135
+ .describe("New patterns learned about the user"),
136
+ notes: tool.schema
137
+ .string()
138
+ .optional()
139
+ .describe("Freeform notes for anything that doesn't fit structured fields"),
140
+ },
141
+ async execute(args) {
142
+ if (!args.summary) {
143
+ return JSON.stringify({ success: false, error: "Requires 'summary'" });
144
+ }
145
+
146
+ await saveConversationSummary({
147
+ summary: args.summary,
148
+ keyDecisions: args.keyDecisions,
149
+ openThreads: args.openThreads,
150
+ learnedPatterns: args.learnedPatterns,
151
+ notes: args.notes,
152
+ });
153
+
154
+ return JSON.stringify({ success: true, message: "Conversation summary saved" });
155
+ },
156
+ });
157
+
158
+ export const getRecentSummariesTool = tool({
159
+ description: "Get recent conversation summaries for context recovery",
160
+ args: {
161
+ count: tool.schema.number().optional().describe("Number of summaries to retrieve (default: 7)"),
162
+ },
163
+ async execute(args) {
164
+ const summaries = getRecentSummaries(args.count || 7);
165
+ return JSON.stringify({ success: true, summaries });
166
+ },
167
+ });
168
+
169
+ // --- Search Tools ---
170
+
171
+ export const searchMemoryTool = tool({
172
+ description:
173
+ "Semantic search over your history - journal, state files, projects, people. Use to find relevant context.",
174
+ args: {
175
+ query: tool.schema.string().describe("Natural language query to search for"),
176
+ type: tool.schema
177
+ .enum(["journal", "state", "project", "person", "meeting", "topic"])
178
+ .optional()
179
+ .describe("Filter by content type"),
180
+ limit: tool.schema.number().optional().describe("Maximum results to return (default: 5)"),
181
+ since: tool.schema.string().optional().describe("Only include items after this ISO date"),
182
+ },
183
+ async execute(args) {
184
+ if (!args.query) {
185
+ return JSON.stringify({ success: false, error: "Requires 'query'" });
186
+ }
187
+
188
+ const results = await searchMemory(args.query, {
189
+ limit: args.limit || 5,
190
+ type: args.type as any,
191
+ since: args.since,
192
+ });
193
+
194
+ if (results.length === 0) {
195
+ return JSON.stringify({
196
+ success: true,
197
+ message: "No matches found. Try rebuilding the index with rebuild_memory_index",
198
+ results: [],
199
+ });
200
+ }
201
+
202
+ return JSON.stringify({
203
+ success: true,
204
+ count: results.length,
205
+ results: results.map((r) => ({
206
+ type: r.type,
207
+ source: r.source,
208
+ section: r.section,
209
+ score: Math.round(r.score * 100) / 100,
210
+ content: r.content.slice(0, 500),
211
+ })),
212
+ });
213
+ },
214
+ });
215
+
216
+ export const searchConversationsTool = tool({
217
+ description: "Search past OpenCode sessions",
218
+ args: {
219
+ query: tool.schema.string().describe("Natural language query to search for"),
220
+ projectOnly: tool.schema.boolean().optional().describe("Only search current project"),
221
+ limit: tool.schema.number().optional().describe("Maximum results to return (default: 5)"),
222
+ },
223
+ async execute(args) {
224
+ if (!args.query) {
225
+ return JSON.stringify({ success: false, error: "Requires 'query'" });
226
+ }
227
+
228
+ const results = await searchConversations(args.query, {
229
+ currentProject: process.cwd(),
230
+ limit: args.limit || 5,
231
+ projectOnly: args.projectOnly,
232
+ });
233
+
234
+ if (results.length === 0) {
235
+ return JSON.stringify({
236
+ success: true,
237
+ message: "No matching conversations. Try rebuilding with rebuild_memory_index",
238
+ results: [],
239
+ });
240
+ }
241
+
242
+ return JSON.stringify({
243
+ success: true,
244
+ count: results.length,
245
+ results: results.map((r) => ({
246
+ project: r.exchange.project,
247
+ timestamp: r.exchange.timestamp,
248
+ sessionId: r.exchange.sessionId,
249
+ score: Math.round(r.adjustedScore * 100) / 100,
250
+ userPrompt: r.exchange.userPrompt.slice(0, 200),
251
+ assistantSummary: r.exchange.assistantSummary.slice(0, 200),
252
+ })),
253
+ });
254
+ },
255
+ });
256
+
257
+ // --- Index Tools ---
258
+
259
+ export const rebuildMemoryIndexTool = tool({
260
+ description:
261
+ "Rebuild the semantic search index from scratch. Use if index seems stale or corrupted.",
262
+ args: {},
263
+ async execute() {
264
+ // Rebuild memory index synchronously (fast)
265
+ await rebuildMemoryIndex();
266
+ const memoryStats = await getMemoryIndexStats();
267
+
268
+ // Rebuild conversation index in background (slow - thousands of exchanges)
269
+ rebuildConversationIndex()
270
+ .then((result) => logger.log(`Conversation index rebuilt: ${result.exchangeCount} exchanges`))
271
+ .catch((err) => logger.error(`Conversation index rebuild failed: ${err}`));
272
+
273
+ return JSON.stringify({
274
+ success: true,
275
+ message: "Memory index rebuilt. Conversation index rebuilding in background.",
276
+ stats: {
277
+ memoryItems: memoryStats.itemCount,
278
+ },
279
+ });
280
+ },
281
+ });
282
+
283
+ export const getMemoryIndexStatsTool = tool({
284
+ description: "Get statistics about the memory index",
285
+ args: {},
286
+ async execute() {
287
+ const memoryStats = await getMemoryIndexStats();
288
+ const convStats = await getConversationIndexStats();
289
+
290
+ return JSON.stringify({
291
+ success: true,
292
+ memoryItems: memoryStats.itemCount,
293
+ conversationExchanges: convStats.exchangeCount,
294
+ });
295
+ },
296
+ });
297
+
298
+ // --- Reminder Tools ---
299
+
300
+ export const scheduleReminderTool = tool({
301
+ description:
302
+ "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.",
303
+ args: {
304
+ id: tool.schema.string().describe("Unique reminder identifier"),
305
+ cronExpression: tool.schema
306
+ .string()
307
+ .describe("Cron expression (e.g., '0 9 * * *' for 9am daily)"),
308
+ description: tool.schema.string().describe("What this reminder is for"),
309
+ payload: tool.schema.string().describe("Message to process when reminder fires"),
310
+ model: tool.schema
311
+ .string()
312
+ .optional()
313
+ .describe(
314
+ "Model to use for this reminder (see macrodata-models in context for available options)",
315
+ ),
316
+ },
317
+ async execute(args) {
318
+ if (!args.id || !args.cronExpression || !args.description || !args.payload) {
319
+ return JSON.stringify({
320
+ success: false,
321
+ error: "Requires 'id', 'cronExpression', 'description', and 'payload'",
322
+ });
323
+ }
324
+
325
+ const schedule: Schedule = {
326
+ id: args.id,
327
+ type: "cron",
328
+ expression: args.cronExpression,
329
+ description: args.description,
330
+ payload: args.payload,
331
+ agent: "opencode",
332
+ model: args.model,
333
+ createdAt: new Date().toISOString(),
334
+ };
335
+
336
+ saveSchedule(schedule);
337
+
338
+ return JSON.stringify({
339
+ success: true,
340
+ message: `Created recurring reminder: ${args.id} (${args.cronExpression})${args.model ? ` with model ${args.model}` : ""}`,
341
+ });
342
+ },
343
+ });
344
+
345
+ export const scheduleOnceTool = tool({
346
+ description:
347
+ "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.",
348
+ args: {
349
+ id: tool.schema.string().describe("Unique reminder identifier"),
350
+ datetime: tool.schema.string().describe("ISO 8601 datetime (e.g., '2026-01-06T10:00:00')"),
351
+ description: tool.schema.string().describe("What this reminder is for"),
352
+ payload: tool.schema.string().describe("Message to process when reminder fires"),
353
+ model: tool.schema
354
+ .string()
355
+ .optional()
356
+ .describe(
357
+ "Model to use for this reminder (see macrodata-models in context for available options)",
358
+ ),
359
+ },
360
+ async execute(args) {
361
+ if (!args.id || !args.datetime || !args.description || !args.payload) {
362
+ return JSON.stringify({
363
+ success: false,
364
+ error: "Requires 'id', 'datetime', 'description', and 'payload'",
365
+ });
366
+ }
367
+
368
+ const schedule: Schedule = {
369
+ id: args.id,
370
+ type: "once",
371
+ expression: args.datetime,
372
+ description: args.description,
373
+ payload: args.payload,
374
+ agent: "opencode",
375
+ model: args.model,
376
+ createdAt: new Date().toISOString(),
377
+ };
378
+
379
+ saveSchedule(schedule);
380
+
381
+ return JSON.stringify({
382
+ success: true,
383
+ message: `Scheduled one-shot reminder: ${args.id} at ${args.datetime}${args.model ? ` with model ${args.model}` : ""}`,
384
+ });
385
+ },
386
+ });
387
+
388
+ export const removeReminderTool = tool({
389
+ description: "Remove a scheduled reminder",
390
+ args: {
391
+ id: tool.schema.string().describe("Reminder ID to remove"),
392
+ },
393
+ async execute(args) {
394
+ if (!args.id) {
395
+ return JSON.stringify({ success: false, error: "Requires 'id'" });
396
+ }
397
+
398
+ const removed = deleteScheduleFile(args.id);
399
+
400
+ return JSON.stringify({
401
+ success: removed,
402
+ message: removed ? `Removed reminder: ${args.id}` : `Reminder not found: ${args.id}`,
403
+ });
404
+ },
405
+ });
406
+
407
+ export const listRemindersTool = tool({
408
+ description: "List all scheduled reminders",
409
+ args: {},
410
+ async execute() {
411
+ const schedules = loadAllSchedules();
412
+ return JSON.stringify({ success: true, reminders: schedules });
413
+ },
414
+ });
415
+
416
+ // --- Related Items Tool ---
417
+
418
+ export const getRelatedTool = tool({
419
+ description:
420
+ "Get entries related to a specific memory item. Useful for exploring associative connections in your memory.",
421
+ args: {
422
+ id: tool.schema.string().describe("The ID of the memory item to find related entries for"),
423
+ },
424
+ async execute(args) {
425
+ if (!args.id) {
426
+ return JSON.stringify({ success: false, error: "Requires 'id'" });
427
+ }
428
+
429
+ // TODO: Implement related items lookup
430
+ return JSON.stringify({
431
+ success: true,
432
+ message: "Related items feature not yet implemented",
433
+ related: [],
434
+ });
435
+ },
436
+ });
437
+
438
+ // Export all tools as a collection
439
+ export const memoryTools = {
440
+ macrodata_log_journal: logJournalTool,
441
+ macrodata_get_recent_journal: getRecentJournalTool,
442
+ macrodata_save_conversation_summary: saveConversationSummaryTool,
443
+ macrodata_get_recent_summaries: getRecentSummariesTool,
444
+ macrodata_search_memory: searchMemoryTool,
445
+ macrodata_search_conversations: searchConversationsTool,
446
+ macrodata_rebuild_memory_index: rebuildMemoryIndexTool,
447
+ macrodata_get_memory_index_stats: getMemoryIndexStatsTool,
448
+ macrodata_schedule_reminder: scheduleReminderTool,
449
+ macrodata_schedule_once: scheduleOnceTool,
450
+ macrodata_remove_reminder: removeReminderTool,
451
+ macrodata_list_reminders: listRemindersTool,
452
+ macrodata_get_related: getRelatedTool,
453
+ };
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@geoql/mdr",
3
+ "version": "0.0.1",
4
+ "description": "Persistent layered memory for coding agents (OpenCode, Claude Code) — journal, entities, state files, semantic search, daemon, scheduled reminders. A hard fork of ascorbic/macrodata by Matt Kane.",
5
+ "keywords": [
6
+ "agent",
7
+ "autonomous",
8
+ "claude-code",
9
+ "journal",
10
+ "local",
11
+ "memory",
12
+ "opencode",
13
+ "plugin",
14
+ "semantic-search"
15
+ ],
16
+ "homepage": "https://mdr.geoql.in",
17
+ "bugs": {
18
+ "url": "https://github.com/geoql/mdr/issues"
19
+ },
20
+ "license": "MIT",
21
+ "author": {
22
+ "name": "Vinayak Kulkarni",
23
+ "email": "inbox.vinayak@gmail.com",
24
+ "url": "https://vinayakkulkarni.dev"
25
+ },
26
+ "contributors": [
27
+ "Matt Kane (https://github.com/ascorbic) — original author of ascorbic/macrodata"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/geoql/mdr.git",
32
+ "directory": "plugins/macrodata"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "opencode",
38
+ "bin",
39
+ "USAGE.md",
40
+ "LICENSE"
41
+ ],
42
+ "type": "module",
43
+ "main": "./dist/opencode/index.js",
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "prepublishOnly": "pnpm build",
50
+ "start": "node dist/src/index.js",
51
+ "daemon": "node dist/bin/macrodata-daemon.js",
52
+ "pretest": "pnpm build",
53
+ "test": "vitest run",
54
+ "check": "oxlint --type-aware --type-check",
55
+ "lint": "oxlint",
56
+ "lint:fix": "oxlint --fix",
57
+ "format": "oxfmt --write .",
58
+ "format:check": "oxfmt --check ."
59
+ },
60
+ "dependencies": {
61
+ "@huggingface/transformers": "^4.2.0",
62
+ "@modelcontextprotocol/sdk": "^1.29.0",
63
+ "@opencode-ai/plugin": "^1.17.15",
64
+ "@opencode-ai/sdk": "^1.17.15",
65
+ "chokidar": "^5.0.0",
66
+ "croner": "^10.0.1",
67
+ "vectra": "^0.15.0",
68
+ "zod": "^4.4.3"
69
+ },
70
+ "devDependencies": {
71
+ "@types/node": "^26.1.1",
72
+ "oxfmt": "catalog:",
73
+ "oxlint": "^1.73.0",
74
+ "oxlint-tsgolint": "latest",
75
+ "tsdown": "latest"
76
+ },
77
+ "engines": {
78
+ "node": ">=24.11.0"
79
+ },
80
+ "opencode": {
81
+ "type": "plugin",
82
+ "hooks": [
83
+ "chat.message",
84
+ "experimental.session.compacting"
85
+ ]
86
+ }
87
+ }
package/src/config.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Shared configuration utilities
3
+ *
4
+ * All paths are resolved dynamically (not cached at module load)
5
+ * so that config changes take effect without restart.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+
12
+ const DEFAULT_ROOT = join(homedir(), ".config", "macrodata");
13
+
14
+ /**
15
+ * Get the macrodata state root directory.
16
+ * Priority: MACRODATA_ROOT env > ~/.config/macrodata/config.json > ~/.config/macrodata
17
+ *
18
+ * Resolved fresh each call so config changes take effect immediately.
19
+ */
20
+ export function getStateRoot(): string {
21
+ // Env var takes precedence (useful for testing/overrides)
22
+ if (process.env.MACRODATA_ROOT) {
23
+ return process.env.MACRODATA_ROOT;
24
+ }
25
+
26
+ // Check config file in default location
27
+ const configPath = join(DEFAULT_ROOT, "config.json");
28
+ if (existsSync(configPath)) {
29
+ try {
30
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
31
+ if (config.root) return config.root;
32
+ } catch {
33
+ // Ignore parse errors
34
+ }
35
+ }
36
+
37
+ return DEFAULT_ROOT;
38
+ }
39
+
40
+ export function getStateDir(): string {
41
+ return join(getStateRoot(), "state");
42
+ }
43
+
44
+ export function getEntitiesDir(): string {
45
+ return join(getStateRoot(), "entities");
46
+ }
47
+
48
+ export function getJournalDir(): string {
49
+ return join(getStateRoot(), "journal");
50
+ }
51
+
52
+ export function getSignalsDir(): string {
53
+ return join(getStateRoot(), "signals");
54
+ }
55
+
56
+ export function getIndexDir(): string {
57
+ return join(getStateRoot(), ".index");
58
+ }
59
+
60
+ export function getTopicsDir(): string {
61
+ return join(getStateRoot(), "topics");
62
+ }
63
+
64
+ export function getRemindersDir(): string {
65
+ return join(getStateRoot(), "reminders");
66
+ }