agent-recall-mcp 3.2.3 → 3.3.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 (164) hide show
  1. package/README.md +17 -3
  2. package/dist/helpers/journal-files.d.ts +30 -0
  3. package/dist/helpers/journal-files.d.ts.map +1 -0
  4. package/dist/helpers/journal-files.js +96 -0
  5. package/dist/helpers/journal-files.js.map +1 -0
  6. package/dist/helpers/sections.d.ts +12 -0
  7. package/dist/helpers/sections.d.ts.map +1 -0
  8. package/dist/helpers/sections.js +84 -0
  9. package/dist/helpers/sections.js.map +1 -0
  10. package/dist/index.js +59 -1199
  11. package/dist/index.js.map +1 -1
  12. package/dist/palace/awareness.d.ts +67 -0
  13. package/dist/palace/awareness.d.ts.map +1 -0
  14. package/dist/palace/awareness.js +231 -0
  15. package/dist/palace/awareness.js.map +1 -0
  16. package/dist/palace/consolidate.d.ts +28 -0
  17. package/dist/palace/consolidate.d.ts.map +1 -0
  18. package/dist/palace/consolidate.js +129 -0
  19. package/dist/palace/consolidate.js.map +1 -0
  20. package/dist/palace/fan-out.d.ts +15 -0
  21. package/dist/palace/fan-out.d.ts.map +1 -0
  22. package/dist/palace/fan-out.js +78 -0
  23. package/dist/palace/fan-out.js.map +1 -0
  24. package/dist/palace/graph.d.ts +11 -0
  25. package/dist/palace/graph.d.ts.map +1 -0
  26. package/dist/palace/graph.js +59 -0
  27. package/dist/palace/graph.js.map +1 -0
  28. package/dist/palace/identity.d.ts +6 -0
  29. package/dist/palace/identity.d.ts.map +1 -0
  30. package/dist/palace/identity.js +18 -0
  31. package/dist/palace/identity.js.map +1 -0
  32. package/dist/palace/index-manager.d.ts +7 -0
  33. package/dist/palace/index-manager.d.ts.map +1 -0
  34. package/dist/palace/index-manager.js +46 -0
  35. package/dist/palace/index-manager.js.map +1 -0
  36. package/dist/palace/insights-index.d.ts +39 -0
  37. package/dist/palace/insights-index.d.ts.map +1 -0
  38. package/dist/palace/insights-index.js +101 -0
  39. package/dist/palace/insights-index.js.map +1 -0
  40. package/dist/palace/log.d.ts +9 -0
  41. package/dist/palace/log.d.ts.map +1 -0
  42. package/dist/palace/log.js +28 -0
  43. package/dist/palace/log.js.map +1 -0
  44. package/dist/palace/obsidian.d.ts +12 -0
  45. package/dist/palace/obsidian.d.ts.map +1 -0
  46. package/dist/palace/obsidian.js +76 -0
  47. package/dist/palace/obsidian.js.map +1 -0
  48. package/dist/palace/rooms.d.ts +14 -0
  49. package/dist/palace/rooms.d.ts.map +1 -0
  50. package/dist/palace/rooms.js +117 -0
  51. package/dist/palace/rooms.js.map +1 -0
  52. package/dist/palace/salience.d.ts +15 -0
  53. package/dist/palace/salience.d.ts.map +1 -0
  54. package/dist/palace/salience.js +33 -0
  55. package/dist/palace/salience.js.map +1 -0
  56. package/dist/resources/journal-resources.d.ts +3 -0
  57. package/dist/resources/journal-resources.d.ts.map +1 -0
  58. package/dist/resources/journal-resources.js +72 -0
  59. package/dist/resources/journal-resources.js.map +1 -0
  60. package/dist/server.d.ts +4 -0
  61. package/dist/server.d.ts.map +1 -0
  62. package/dist/server.js +7 -0
  63. package/dist/server.js.map +1 -0
  64. package/dist/storage/fs-utils.d.ts +8 -0
  65. package/dist/storage/fs-utils.d.ts.map +1 -0
  66. package/dist/storage/fs-utils.js +28 -0
  67. package/dist/storage/fs-utils.js.map +1 -0
  68. package/dist/storage/paths.d.ts +21 -0
  69. package/dist/storage/paths.d.ts.map +1 -0
  70. package/dist/storage/paths.js +59 -0
  71. package/dist/storage/paths.js.map +1 -0
  72. package/dist/storage/project.d.ts +17 -0
  73. package/dist/storage/project.d.ts.map +1 -0
  74. package/dist/storage/project.js +130 -0
  75. package/dist/storage/project.js.map +1 -0
  76. package/dist/tools/alignment-check.d.ts +3 -0
  77. package/dist/tools/alignment-check.d.ts.map +1 -0
  78. package/dist/tools/alignment-check.js +73 -0
  79. package/dist/tools/alignment-check.js.map +1 -0
  80. package/dist/tools/awareness-update.d.ts +9 -0
  81. package/dist/tools/awareness-update.d.ts.map +1 -0
  82. package/dist/tools/awareness-update.js +90 -0
  83. package/dist/tools/awareness-update.js.map +1 -0
  84. package/dist/tools/context-synthesize.d.ts +3 -0
  85. package/dist/tools/context-synthesize.d.ts.map +1 -0
  86. package/dist/tools/context-synthesize.js +204 -0
  87. package/dist/tools/context-synthesize.js.map +1 -0
  88. package/dist/tools/journal-archive.d.ts +3 -0
  89. package/dist/tools/journal-archive.d.ts.map +1 -0
  90. package/dist/tools/journal-archive.js +62 -0
  91. package/dist/tools/journal-archive.js.map +1 -0
  92. package/dist/tools/journal-capture.d.ts +3 -0
  93. package/dist/tools/journal-capture.d.ts.map +1 -0
  94. package/dist/tools/journal-capture.js +87 -0
  95. package/dist/tools/journal-capture.js.map +1 -0
  96. package/dist/tools/journal-cold-start.d.ts +3 -0
  97. package/dist/tools/journal-cold-start.d.ts.map +1 -0
  98. package/dist/tools/journal-cold-start.js +70 -0
  99. package/dist/tools/journal-cold-start.js.map +1 -0
  100. package/dist/tools/journal-list.d.ts +3 -0
  101. package/dist/tools/journal-list.d.ts.map +1 -0
  102. package/dist/tools/journal-list.js +43 -0
  103. package/dist/tools/journal-list.js.map +1 -0
  104. package/dist/tools/journal-projects.d.ts +3 -0
  105. package/dist/tools/journal-projects.d.ts.map +1 -0
  106. package/dist/tools/journal-projects.js +25 -0
  107. package/dist/tools/journal-projects.js.map +1 -0
  108. package/dist/tools/journal-read.d.ts +3 -0
  109. package/dist/tools/journal-read.d.ts.map +1 -0
  110. package/dist/tools/journal-read.js +72 -0
  111. package/dist/tools/journal-read.js.map +1 -0
  112. package/dist/tools/journal-search.d.ts +3 -0
  113. package/dist/tools/journal-search.d.ts.map +1 -0
  114. package/dist/tools/journal-search.js +113 -0
  115. package/dist/tools/journal-search.js.map +1 -0
  116. package/dist/tools/journal-state.d.ts +6 -0
  117. package/dist/tools/journal-state.d.ts.map +1 -0
  118. package/dist/tools/journal-state.js +111 -0
  119. package/dist/tools/journal-state.js.map +1 -0
  120. package/dist/tools/journal-write.d.ts +3 -0
  121. package/dist/tools/journal-write.d.ts.map +1 -0
  122. package/dist/tools/journal-write.js +88 -0
  123. package/dist/tools/journal-write.js.map +1 -0
  124. package/dist/tools/knowledge-read.d.ts +3 -0
  125. package/dist/tools/knowledge-read.d.ts.map +1 -0
  126. package/dist/tools/knowledge-read.js +118 -0
  127. package/dist/tools/knowledge-read.js.map +1 -0
  128. package/dist/tools/knowledge-write.d.ts +3 -0
  129. package/dist/tools/knowledge-write.d.ts.map +1 -0
  130. package/dist/tools/knowledge-write.js +89 -0
  131. package/dist/tools/knowledge-write.js.map +1 -0
  132. package/dist/tools/nudge.d.ts +3 -0
  133. package/dist/tools/nudge.d.ts.map +1 -0
  134. package/dist/tools/nudge.js +41 -0
  135. package/dist/tools/nudge.js.map +1 -0
  136. package/dist/tools/palace-lint.d.ts +7 -0
  137. package/dist/tools/palace-lint.d.ts.map +1 -0
  138. package/dist/tools/palace-lint.js +149 -0
  139. package/dist/tools/palace-lint.js.map +1 -0
  140. package/dist/tools/palace-read.d.ts +6 -0
  141. package/dist/tools/palace-read.d.ts.map +1 -0
  142. package/dist/tools/palace-read.js +78 -0
  143. package/dist/tools/palace-read.js.map +1 -0
  144. package/dist/tools/palace-search.d.ts +6 -0
  145. package/dist/tools/palace-search.d.ts.map +1 -0
  146. package/dist/tools/palace-search.js +81 -0
  147. package/dist/tools/palace-search.js.map +1 -0
  148. package/dist/tools/palace-walk.d.ts +12 -0
  149. package/dist/tools/palace-walk.d.ts.map +1 -0
  150. package/dist/tools/palace-walk.js +167 -0
  151. package/dist/tools/palace-walk.js.map +1 -0
  152. package/dist/tools/palace-write.d.ts +6 -0
  153. package/dist/tools/palace-write.d.ts.map +1 -0
  154. package/dist/tools/palace-write.js +108 -0
  155. package/dist/tools/palace-write.js.map +1 -0
  156. package/dist/tools/recall-insight.d.ts +9 -0
  157. package/dist/tools/recall-insight.d.ts.map +1 -0
  158. package/dist/tools/recall-insight.js +51 -0
  159. package/dist/tools/recall-insight.js.map +1 -0
  160. package/dist/types.d.ts +112 -0
  161. package/dist/types.d.ts.map +1 -0
  162. package/dist/types.js +31 -0
  163. package/dist/types.js.map +1 -0
  164. package/package.json +8 -4
package/dist/index.js CHANGED
@@ -1,32 +1,30 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import * as z from "zod/v4";
5
- import * as fs from "node:fs";
6
- import * as path from "node:path";
7
- import * as os from "node:os";
8
- import { execFile } from "node:child_process";
9
- import { promisify } from "node:util";
10
- const execFileAsync = promisify(execFile);
11
- // ---------------------------------------------------------------------------
12
- // Constants
13
- // ---------------------------------------------------------------------------
14
- const VERSION = "3.2.2";
15
- const JOURNAL_ROOT = process.env.AGENT_RECALL_ROOT ||
16
- path.join(os.homedir(), ".agent-recall");
17
- const LEGACY_ROOT = path.join(os.homedir(), ".claude", "projects");
18
- const SECTION_HEADERS = {
19
- brief: "## Brief",
20
- qa: "## Q&A Log",
21
- completed: "## Completed",
22
- status: "## Status",
23
- blockers: "## Blockers",
24
- next: "## Next",
25
- decisions: "## Decisions",
26
- reflection: "## Reflection",
27
- files: "## Files Changed",
28
- observations: "## Observations",
29
- };
3
+ import { VERSION, JOURNAL_ROOT, LEGACY_ROOT } from "./types.js";
4
+ import { server } from "./server.js";
5
+ // Import all tool registrations
6
+ import { register as registerJournalRead } from "./tools/journal-read.js";
7
+ import { register as registerJournalWrite } from "./tools/journal-write.js";
8
+ import { register as registerJournalCapture } from "./tools/journal-capture.js";
9
+ import { register as registerJournalList } from "./tools/journal-list.js";
10
+ import { register as registerJournalProjects } from "./tools/journal-projects.js";
11
+ import { register as registerJournalSearch } from "./tools/journal-search.js";
12
+ import { register as registerJournalState } from "./tools/journal-state.js";
13
+ import { register as registerJournalColdStart } from "./tools/journal-cold-start.js";
14
+ import { register as registerJournalArchive } from "./tools/journal-archive.js";
15
+ import { register as registerAlignmentCheck } from "./tools/alignment-check.js";
16
+ import { register as registerNudge } from "./tools/nudge.js";
17
+ import { register as registerContextSynthesize } from "./tools/context-synthesize.js";
18
+ import { register as registerKnowledgeWrite } from "./tools/knowledge-write.js";
19
+ import { register as registerKnowledgeRead } from "./tools/knowledge-read.js";
20
+ import { register as registerPalaceRead } from "./tools/palace-read.js";
21
+ import { register as registerPalaceWrite } from "./tools/palace-write.js";
22
+ import { register as registerPalaceWalk } from "./tools/palace-walk.js";
23
+ import { register as registerPalaceLint } from "./tools/palace-lint.js";
24
+ import { register as registerPalaceSearch } from "./tools/palace-search.js";
25
+ import { register as registerAwarenessUpdate } from "./tools/awareness-update.js";
26
+ import { register as registerRecallInsight } from "./tools/recall-insight.js";
27
+ import { register as registerJournalResources } from "./resources/journal-resources.js";
30
28
  // ---------------------------------------------------------------------------
31
29
  // CLI flags (handle before MCP starts)
32
30
  // ---------------------------------------------------------------------------
@@ -62,1183 +60,45 @@ if (args.includes("--list-tools")) {
62
60
  { name: "journal_state", description: "Layer 1 JSON state: read/write structured session data (v3)" },
63
61
  { name: "journal_cold_start", description: "Cache-aware cold start: hot/warm/cold entries (v3)" },
64
62
  { name: "journal_archive", description: "Archive old entries to cold storage (v3)" },
63
+ { name: "knowledge_write", description: "Write a structured lesson to a category-specific knowledge file" },
64
+ { name: "knowledge_read", description: "Read lessons from knowledge files, optionally filtered by project/category/query" },
65
+ { name: "palace_read", description: "Read a room or list all rooms in the Memory Palace" },
66
+ { name: "palace_write", description: "Write memory to a palace room with fan-out cross-referencing" },
67
+ { name: "palace_walk", description: "Progressive context loading: identity → active → relevant → full" },
68
+ { name: "palace_lint", description: "Health check: stale, orphans, low salience, missing refs" },
69
+ { name: "palace_search", description: "Search across palace rooms, ranked by salience" },
70
+ { name: "awareness_update", description: "Update awareness with new insights (call at session end)" },
71
+ { name: "recall_insight", description: "Recall cross-project insights relevant to current task" },
65
72
  ];
66
73
  process.stdout.write(JSON.stringify(tools, null, 2) + "\n");
67
74
  process.exit(0);
68
75
  }
69
76
  // ---------------------------------------------------------------------------
70
- // Utility functions
71
- // ---------------------------------------------------------------------------
72
- function ensureDir(dir) {
73
- if (!fs.existsSync(dir)) {
74
- fs.mkdirSync(dir, { recursive: true });
75
- }
76
- }
77
- function todayISO() {
78
- return new Date().toISOString().slice(0, 10);
79
- }
80
- /**
81
- * Auto-detect project slug from environment, git, or cwd.
82
- * Async to avoid blocking the event loop.
83
- */
84
- let _cachedProject = null;
85
- async function detectProject() {
86
- if (_cachedProject)
87
- return _cachedProject;
88
- // 1. Env var
89
- if (process.env.AGENT_RECALL_PROJECT) {
90
- _cachedProject = process.env.AGENT_RECALL_PROJECT;
91
- return _cachedProject;
92
- }
93
- // 2. Git repo name (async)
94
- try {
95
- const { stdout } = await execFileAsync("git", ["config", "--get", "remote.origin.url"], { timeout: 3000 });
96
- const remote = stdout.trim();
97
- if (remote) {
98
- const name = path.basename(remote, ".git");
99
- if (name) {
100
- _cachedProject = name;
101
- return name;
102
- }
103
- }
104
- }
105
- catch {
106
- try {
107
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { timeout: 3000 });
108
- const root = stdout.trim();
109
- if (root) {
110
- _cachedProject = path.basename(root);
111
- return _cachedProject;
112
- }
113
- }
114
- catch {
115
- // fall through
116
- }
117
- }
118
- // 3. package.json name
119
- const cwd = process.cwd();
120
- const pkgPath = path.join(cwd, "package.json");
121
- if (fs.existsSync(pkgPath)) {
122
- try {
123
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
124
- if (pkg.name) {
125
- _cachedProject = pkg.name.replace(/^@[^/]+\//, "");
126
- return _cachedProject;
127
- }
128
- }
129
- catch {
130
- // fall through
131
- }
132
- }
133
- // 4. Basename of cwd
134
- _cachedProject = path.basename(cwd);
135
- return _cachedProject;
136
- }
137
- /**
138
- * Resolve the journal directory for a project, checking both new and legacy locations.
139
- * For writes, always use the new location.
140
- */
141
- function journalDir(project) {
142
- // Sanitize: prevent path traversal (e.g. "../../etc")
143
- const safe = project.replace(/[^a-zA-Z0-9_\-\.]/g, "-");
144
- const resolved = path.join(JOURNAL_ROOT, "projects", safe, "journal");
145
- if (!resolved.startsWith(JOURNAL_ROOT)) {
146
- throw new Error(`Invalid project name: ${project}`);
147
- }
148
- return resolved;
149
- }
150
- /**
151
- * Find all journal directories for a project (new + legacy fallback).
152
- */
153
- function journalDirs(project) {
154
- const dirs = [];
155
- const primary = journalDir(project);
156
- if (fs.existsSync(primary))
157
- dirs.push(primary);
158
- // Legacy: ~/.claude/projects/*/memory/journal/
159
- // We try to match project slug in the directory name
160
- if (fs.existsSync(LEGACY_ROOT)) {
161
- try {
162
- const entries = fs.readdirSync(LEGACY_ROOT);
163
- for (const entry of entries) {
164
- if (entry.includes(project)) {
165
- const legacyJournal = path.join(LEGACY_ROOT, entry, "memory", "journal");
166
- if (fs.existsSync(legacyJournal)) {
167
- dirs.push(legacyJournal);
168
- }
169
- }
170
- }
171
- }
172
- catch {
173
- // ignore
174
- }
175
- }
176
- return dirs;
177
- }
178
- /**
179
- * List all .md journal files across all directories for a project.
180
- * Returns sorted array of { date, file, dir } with most recent first.
181
- */
182
- function listJournalFiles(project) {
183
- const dirs = journalDirs(project);
184
- const entries = [];
185
- const seen = new Set();
186
- for (const dir of dirs) {
187
- if (!fs.existsSync(dir))
188
- continue;
189
- const files = fs.readdirSync(dir);
190
- for (const file of files) {
191
- // Match YYYY-MM-DD.md (not log files)
192
- const match = file.match(/^(\d{4}-\d{2}-\d{2})\.md$/);
193
- if (match && !seen.has(match[1])) {
194
- seen.add(match[1]);
195
- entries.push({ date: match[1], file, dir });
196
- }
197
- }
198
- }
199
- entries.sort((a, b) => b.date.localeCompare(a.date));
200
- return entries;
201
- }
202
- /**
203
- * Read a journal file. Checks primary dir first, then legacy.
204
- */
205
- function readJournalFile(project, date) {
206
- const filename = `${date}.md`;
207
- const dirs = journalDirs(project);
208
- // Also check primary dir even if it wasn't in journalDirs (might not exist yet)
209
- const primaryDir = journalDir(project);
210
- const allDirs = [primaryDir, ...dirs.filter((d) => d !== primaryDir)];
211
- for (const dir of allDirs) {
212
- const filePath = path.join(dir, filename);
213
- if (fs.existsSync(filePath)) {
214
- return fs.readFileSync(filePath, "utf-8");
215
- }
216
- }
217
- return null;
218
- }
219
- /**
220
- * Extract a section from a markdown journal entry.
221
- */
222
- function extractSection(content, section) {
223
- if (section === "all")
224
- return content;
225
- if (section === "brief") {
226
- // Brief = first 3 non-empty lines after the title + momentum line if present
227
- const lines = content.split("\n");
228
- const nonEmpty = [];
229
- let pastTitle = false;
230
- for (const line of lines) {
231
- if (line.startsWith("# ")) {
232
- pastTitle = true;
233
- continue;
234
- }
235
- if (!pastTitle)
236
- continue;
237
- const trimmed = line.trim();
238
- if (trimmed === "")
239
- continue;
240
- nonEmpty.push(trimmed);
241
- if (nonEmpty.length >= 4)
242
- break; // 3 sentences + momentum
243
- }
244
- return nonEmpty.join("\n") || null;
245
- }
246
- const header = SECTION_HEADERS[section];
247
- if (!header)
248
- return null;
249
- const idx = content.indexOf(header);
250
- if (idx === -1)
251
- return null;
252
- // Find the next ## header (respecting code fences)
253
- const afterHeader = content.slice(idx);
254
- const lines = afterHeader.split("\n");
255
- const result = [lines[0]];
256
- let inCodeFence = false;
257
- for (let i = 1; i < lines.length; i++) {
258
- if (lines[i].startsWith("```"))
259
- inCodeFence = !inCodeFence;
260
- if (!inCodeFence && lines[i].startsWith("## "))
261
- break;
262
- result.push(lines[i]);
263
- }
264
- return result.join("\n").trimEnd();
265
- }
266
- /**
267
- * Extract title from journal file content.
268
- */
269
- function extractTitle(content) {
270
- const match = content.match(/^# (.+)$/m);
271
- return match ? match[1].trim() : "(untitled)";
272
- }
273
- /**
274
- * Extract momentum indicator from journal content.
275
- */
276
- function extractMomentum(content) {
277
- // Look for momentum patterns like 🟢 加速, 🟡 稳定, 🔴 减速
278
- const patterns = [/[🟢🟡🔴⚪]\s*\S+/];
279
- for (const pattern of patterns) {
280
- const match = content.match(pattern);
281
- if (match)
282
- return match[0];
283
- }
284
- return "";
285
- }
286
- /**
287
- * Append content to a specific section in a journal file, or to end of file.
288
- */
289
- function appendToSection(existingContent, newContent, section) {
290
- if (section === "replace_all") {
291
- return newContent;
292
- }
293
- if (!section) {
294
- // Append to end
295
- return existingContent.trimEnd() + "\n\n" + newContent + "\n";
296
- }
297
- const header = SECTION_HEADERS[section];
298
- if (!header) {
299
- // Unknown section — append to end
300
- return existingContent.trimEnd() + "\n\n" + newContent + "\n";
301
- }
302
- const idx = existingContent.indexOf(header);
303
- if (idx === -1) {
304
- // Section doesn't exist — append it
305
- return (existingContent.trimEnd() + "\n\n" + header + "\n\n" + newContent + "\n");
306
- }
307
- // Find the end of this section (next ## header or EOF, respecting code fences)
308
- const afterHeader = existingContent.slice(idx);
309
- const lines = afterHeader.split("\n");
310
- let insertAt = lines.length;
311
- let inCodeFence = false;
312
- for (let i = 1; i < lines.length; i++) {
313
- if (lines[i].startsWith("```"))
314
- inCodeFence = !inCodeFence;
315
- if (!inCodeFence && lines[i].startsWith("## ")) {
316
- insertAt = i;
317
- break;
318
- }
319
- }
320
- // Insert before the next section
321
- const before = existingContent.slice(0, idx + lines.slice(0, insertAt).join("\n").length);
322
- const after = existingContent.slice(idx + lines.slice(0, insertAt).join("\n").length);
323
- return before.trimEnd() + "\n\n" + newContent + "\n" + after;
324
- }
325
- /**
326
- * Update the index.md for a project.
327
- */
328
- function updateIndex(project) {
329
- const dir = journalDir(project);
330
- ensureDir(dir);
331
- const indexPath = path.join(dir, "index.md");
332
- const entries = listJournalFiles(project);
333
- let index = `# ${project} — Journal Index\n\n`;
334
- index += `> Auto-generated. ${entries.length} entries.\n\n`;
335
- index += `| Date | Title | Momentum |\n`;
336
- index += `|------|-------|----------|\n`;
337
- for (const entry of entries) {
338
- const content = fs.readFileSync(path.join(entry.dir, entry.file), "utf-8");
339
- const title = extractTitle(content);
340
- const momentum = extractMomentum(content);
341
- index += `| ${entry.date} | ${title} | ${momentum} |\n`;
342
- }
343
- fs.writeFileSync(indexPath, index, "utf-8");
344
- }
345
- /**
346
- * Count entries in a log file (for journal_capture entry numbering).
347
- */
348
- function countLogEntries(logPath) {
349
- if (!fs.existsSync(logPath))
350
- return 0;
351
- const content = fs.readFileSync(logPath, "utf-8");
352
- const matches = content.match(/^### Q\d+/gm);
353
- return matches ? matches.length : 0;
354
- }
355
- /**
356
- * List all projects (from both new and legacy locations).
357
- */
358
- function listAllProjects() {
359
- const projects = new Map();
360
- // New location
361
- const projectsDir = path.join(JOURNAL_ROOT, "projects");
362
- if (fs.existsSync(projectsDir)) {
363
- const dirs = fs.readdirSync(projectsDir);
364
- for (const slug of dirs) {
365
- const jDir = path.join(projectsDir, slug, "journal");
366
- if (fs.existsSync(jDir)) {
367
- const files = fs.readdirSync(jDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
368
- if (files.length > 0) {
369
- files.sort().reverse();
370
- projects.set(slug, {
371
- slug,
372
- lastEntry: files[0].replace(".md", ""),
373
- entryCount: files.length,
374
- });
375
- }
376
- }
377
- }
378
- }
379
- // Legacy location
380
- if (fs.existsSync(LEGACY_ROOT)) {
381
- try {
382
- const entries = fs.readdirSync(LEGACY_ROOT);
383
- for (const entry of entries) {
384
- const journalPath = path.join(LEGACY_ROOT, entry, "memory", "journal");
385
- if (fs.existsSync(journalPath)) {
386
- // Derive slug from directory name (e.g., "-Users-tongwu-some-project" -> "some-project")
387
- const parts = entry.split("-").filter(Boolean);
388
- const slug = parts[parts.length - 1] || entry;
389
- if (!projects.has(slug)) {
390
- const files = fs.readdirSync(journalPath).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
391
- if (files.length > 0) {
392
- files.sort().reverse();
393
- projects.set(slug, {
394
- slug,
395
- lastEntry: files[0].replace(".md", ""),
396
- entryCount: files.length,
397
- });
398
- }
399
- }
400
- }
401
- }
402
- }
403
- catch {
404
- // ignore
405
- }
406
- }
407
- const result = Array.from(projects.values());
408
- result.sort((a, b) => b.lastEntry.localeCompare(a.lastEntry));
409
- return result;
410
- }
411
- /**
412
- * Resolve "auto" project to actual slug.
413
- */
414
- async function resolveProject(project) {
415
- if (!project || project === "auto") {
416
- return await detectProject();
417
- }
418
- return project;
419
- }
420
- // ---------------------------------------------------------------------------
421
- // MCP Server
422
- // ---------------------------------------------------------------------------
423
- const server = new McpServer({
424
- name: "agent-recall",
425
- version: VERSION,
426
- });
427
- // ---------------------------------------------------------------------------
428
- // Tool: journal_read
429
- // ---------------------------------------------------------------------------
430
- server.registerTool("journal_read", {
431
- title: "Read Journal Entry",
432
- description: "Read a journal entry. Returns the full file content for agent cold-start. Use date='latest' for the most recent entry.",
433
- inputSchema: {
434
- date: z
435
- .string()
436
- .default("latest")
437
- .describe("ISO date string YYYY-MM-DD. Defaults to 'latest'. Use 'latest' for most recent entry."),
438
- project: z
439
- .string()
440
- .default("auto")
441
- .describe("Project slug (directory name under ~/.agent-recall/projects/). Defaults to current git repo name."),
442
- section: z
443
- .enum([
444
- "all",
445
- "brief",
446
- "qa",
447
- "completed",
448
- "status",
449
- "blockers",
450
- "next",
451
- "decisions",
452
- "reflection",
453
- "files",
454
- "observations",
455
- ])
456
- .default("all")
457
- .describe("Which section to return. 'brief' returns only the cold-start summary. 'all' returns full file."),
458
- },
459
- }, async ({ date, project, section }) => {
460
- const slug = await resolveProject(project);
461
- let targetDate = date;
462
- if (targetDate === "latest") {
463
- const entries = listJournalFiles(slug);
464
- if (entries.length === 0) {
465
- return {
466
- content: [
467
- {
468
- type: "text",
469
- text: JSON.stringify({
470
- error: `No journal entries found for project '${slug}'`,
471
- project: slug,
472
- }),
473
- },
474
- ],
475
- isError: true,
476
- };
477
- }
478
- targetDate = entries[0].date;
479
- }
480
- const fileContent = readJournalFile(slug, targetDate);
481
- if (!fileContent) {
482
- return {
483
- content: [
484
- {
485
- type: "text",
486
- text: JSON.stringify({
487
- error: `No journal entry found for ${targetDate} in project '${slug}'`,
488
- project: slug,
489
- date: targetDate,
490
- }),
491
- },
492
- ],
493
- isError: true,
494
- };
495
- }
496
- const extracted = extractSection(fileContent, section);
497
- return {
498
- content: [
499
- {
500
- type: "text",
501
- text: JSON.stringify({
502
- content: extracted || "",
503
- date: targetDate,
504
- project: slug,
505
- }),
506
- },
507
- ],
508
- };
509
- });
510
- // ---------------------------------------------------------------------------
511
- // Tool: journal_write
512
- // ---------------------------------------------------------------------------
513
- server.registerTool("journal_write", {
514
- title: "Write Journal Entry",
515
- description: "Append content to the current journal entry (creates today's file if absent). Use section='replace_all' to overwrite entire file.",
516
- inputSchema: {
517
- content: z.string().describe("Markdown content to append or write."),
518
- section: z
519
- .enum([
520
- "qa",
521
- "completed",
522
- "blockers",
523
- "next",
524
- "decisions",
525
- "observations",
526
- "replace_all",
527
- ])
528
- .optional()
529
- .describe("Target section. If omitted, appends to end of file. 'replace_all' overwrites entire file."),
530
- project: z
531
- .string()
532
- .default("auto")
533
- .describe("Project slug. Defaults to auto-detect."),
534
- },
535
- }, async ({ content, section, project }) => {
536
- const slug = await resolveProject(project);
537
- const date = todayISO();
538
- const dir = journalDir(slug);
539
- ensureDir(dir);
540
- const filePath = path.join(dir, `${date}.md`);
541
- let existing = "";
542
- if (fs.existsSync(filePath)) {
543
- existing = fs.readFileSync(filePath, "utf-8");
544
- }
545
- else if (!section || section !== "replace_all") {
546
- // Create a new file with a title
547
- existing = `# ${date} — ${slug}\n`;
548
- }
549
- const sectionArg = section ?? null;
550
- const updated = appendToSection(existing, content, sectionArg);
551
- fs.writeFileSync(filePath, updated, "utf-8");
552
- // Update index
553
- updateIndex(slug);
554
- return {
555
- content: [
556
- {
557
- type: "text",
558
- text: JSON.stringify({
559
- success: true,
560
- date,
561
- file: filePath,
562
- }),
563
- },
564
- ],
565
- };
566
- });
567
- // ---------------------------------------------------------------------------
568
- // Tool: journal_capture
569
- // ---------------------------------------------------------------------------
570
- server.registerTool("journal_capture", {
571
- title: "Capture Q&A",
572
- description: "Layer 1: lightweight Q&A capture. Appends to today's log file without loading the full journal.",
573
- inputSchema: {
574
- question: z
575
- .string()
576
- .describe("The human's question or request (summarized, 1 sentence)"),
577
- answer: z
578
- .string()
579
- .describe("The agent's key answer or decision (summarized, 1-2 sentences)"),
580
- tags: z
581
- .array(z.string())
582
- .optional()
583
- .describe("Optional tags for this entry (e.g. ['decision', 'bug-fix', 'architecture'])"),
584
- project: z
585
- .string()
586
- .default("auto")
587
- .describe("Project slug. Defaults to auto-detect."),
588
- },
589
- }, async ({ question, answer, tags, project }) => {
590
- const slug = await resolveProject(project);
591
- const date = todayISO();
592
- const dir = journalDir(slug);
593
- ensureDir(dir);
594
- const logPath = path.join(dir, `${date}-log.md`);
595
- const entryNum = countLogEntries(logPath) + 1;
596
- const tagStr = tags && tags.length > 0 ? ` [${tags.join(", ")}]` : "";
597
- const timestamp = new Date().toISOString().slice(11, 19);
598
- let entry = `### Q${entryNum} (${timestamp})${tagStr}\n\n`;
599
- entry += `**Q:** ${question}\n\n`;
600
- entry += `**A:** ${answer}\n\n`;
601
- if (!fs.existsSync(logPath)) {
602
- const header = `# ${date} — ${slug} — Session Log\n\n`;
603
- fs.writeFileSync(logPath, header + entry, "utf-8");
604
- }
605
- else {
606
- fs.appendFileSync(logPath, entry, "utf-8");
607
- }
608
- return {
609
- content: [
610
- {
611
- type: "text",
612
- text: JSON.stringify({
613
- success: true,
614
- entry_number: entryNum,
615
- }),
616
- },
617
- ],
618
- };
619
- });
620
- // ---------------------------------------------------------------------------
621
- // Tool: journal_list
622
- // ---------------------------------------------------------------------------
623
- server.registerTool("journal_list", {
624
- title: "List Journal Entries",
625
- description: "List available journal entries for a project.",
626
- inputSchema: {
627
- project: z
628
- .string()
629
- .default("auto")
630
- .describe("Project slug. Defaults to auto-detect."),
631
- limit: z
632
- .number()
633
- .int()
634
- .default(10)
635
- .describe("Return the N most recent entries. 0 = all."),
636
- },
637
- }, async ({ project, limit }) => {
638
- const slug = await resolveProject(project);
639
- let entries = listJournalFiles(slug);
640
- if (limit > 0) {
641
- entries = entries.slice(0, limit);
642
- }
643
- const result = entries.map((e) => {
644
- const content = fs.readFileSync(path.join(e.dir, e.file), "utf-8");
645
- return {
646
- date: e.date,
647
- title: extractTitle(content),
648
- momentum: extractMomentum(content),
649
- };
650
- });
651
- return {
652
- content: [
653
- {
654
- type: "text",
655
- text: JSON.stringify({
656
- project: slug,
657
- entries: result,
658
- }),
659
- },
660
- ],
661
- };
662
- });
663
- // ---------------------------------------------------------------------------
664
- // Tool: journal_projects
665
- // ---------------------------------------------------------------------------
666
- server.registerTool("journal_projects", {
667
- title: "List Projects",
668
- description: "List all projects tracked by agent-recall on this machine.",
669
- inputSchema: {},
670
- }, async () => {
671
- const projects = listAllProjects();
672
- return {
673
- content: [
674
- {
675
- type: "text",
676
- text: JSON.stringify({
677
- projects: projects.map((p) => ({
678
- slug: p.slug,
679
- last_entry: p.lastEntry,
680
- entry_count: p.entryCount,
681
- })),
682
- journal_root: JOURNAL_ROOT,
683
- }),
684
- },
685
- ],
686
- };
687
- });
688
- // ---------------------------------------------------------------------------
689
- // Tool: journal_search
690
- // ---------------------------------------------------------------------------
691
- server.registerTool("journal_search", {
692
- title: "Search Journals",
693
- description: "Full-text search across all journal entries for a project.",
694
- inputSchema: {
695
- query: z.string().describe("Search term (plain text, case-insensitive)"),
696
- project: z
697
- .string()
698
- .default("auto")
699
- .describe("Project slug. Defaults to auto-detect."),
700
- section: z
701
- .string()
702
- .optional()
703
- .describe("Limit search to a specific section type."),
704
- },
705
- }, async ({ query, project, section }) => {
706
- const slug = await resolveProject(project);
707
- const dirs = journalDirs(slug);
708
- const queryLower = query.toLowerCase();
709
- const results = [];
710
- for (const dir of dirs) {
711
- if (!fs.existsSync(dir))
712
- continue;
713
- const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
714
- for (const file of files) {
715
- const filePath = path.join(dir, file);
716
- const content = fs.readFileSync(filePath, "utf-8");
717
- const lines = content.split("\n");
718
- let currentSection = "top";
719
- for (let i = 0; i < lines.length; i++) {
720
- const line = lines[i];
721
- // Track current section
722
- if (line.startsWith("## ")) {
723
- currentSection = line
724
- .slice(3)
725
- .trim()
726
- .toLowerCase()
727
- .replace(/\s+/g, "_");
728
- }
729
- // Filter by section if specified
730
- if (section && currentSection !== section.toLowerCase()) {
731
- continue;
732
- }
733
- if (line.toLowerCase().includes(queryLower)) {
734
- const dateMatch = file.match(/^(\d{4}-\d{2}-\d{2})/);
735
- const date = dateMatch ? dateMatch[1] : file;
736
- // Build excerpt: line with surrounding context
737
- const start = Math.max(0, line.toLowerCase().indexOf(queryLower) - 40);
738
- const end = Math.min(line.length, line.toLowerCase().indexOf(queryLower) + query.length + 40);
739
- let excerpt = line.slice(start, end).trim();
740
- if (start > 0)
741
- excerpt = "..." + excerpt;
742
- if (end < line.length)
743
- excerpt = excerpt + "...";
744
- results.push({
745
- date,
746
- section: currentSection,
747
- excerpt,
748
- line: i + 1,
749
- });
750
- }
751
- }
752
- }
753
- }
754
- // Sort by date descending
755
- results.sort((a, b) => b.date.localeCompare(a.date));
756
- return {
757
- content: [
758
- {
759
- type: "text",
760
- text: JSON.stringify({ results }),
761
- },
762
- ],
763
- };
764
- });
765
- // ---------------------------------------------------------------------------
766
- // Resources
767
- // ---------------------------------------------------------------------------
768
- // Resource: project index
769
- server.registerResource("Journal Index", new ResourceTemplate("agent-recall://{project}/index", {
770
- list: async () => {
771
- const projects = listAllProjects();
772
- return {
773
- resources: projects.map((p) => ({
774
- uri: `agent-recall://${p.slug}/index`,
775
- name: `${p.slug} — Journal Index`,
776
- mimeType: "text/markdown",
777
- })),
778
- };
779
- },
780
- }), { description: "Journal index for a project", mimeType: "text/markdown" }, async (uri, { project }) => {
781
- const slug = Array.isArray(project) ? project[0] : (project || "unknown");
782
- const indexPath = path.join(journalDir(slug), "index.md");
783
- let content = "";
784
- if (fs.existsSync(indexPath)) {
785
- content = fs.readFileSync(indexPath, "utf-8");
786
- }
787
- else {
788
- content = `# ${slug} — No journal index found\n`;
789
- }
790
- return {
791
- contents: [
792
- {
793
- uri: uri.href,
794
- text: content,
795
- mimeType: "text/markdown",
796
- },
797
- ],
798
- };
799
- });
800
- // Resource: specific date entry
801
- server.registerResource("Journal Entry", new ResourceTemplate("agent-recall://{project}/{date}", {
802
- list: async () => {
803
- const projects = listAllProjects();
804
- const resources = [];
805
- for (const p of projects) {
806
- const entries = listJournalFiles(p.slug).slice(0, 5);
807
- for (const e of entries) {
808
- resources.push({
809
- uri: `agent-recall://${p.slug}/${e.date}`,
810
- name: `${p.slug} — ${e.date}`,
811
- mimeType: "text/markdown",
812
- });
813
- }
814
- }
815
- return { resources };
816
- },
817
- }), {
818
- description: "A specific journal entry by date",
819
- mimeType: "text/markdown",
820
- }, async (uri, { project, date }) => {
821
- const slug = Array.isArray(project) ? project[0] : (project || "unknown");
822
- const entryDate = Array.isArray(date) ? date[0] : (date || todayISO());
823
- const content = readJournalFile(slug, entryDate);
824
- return {
825
- contents: [
826
- {
827
- uri: uri.href,
828
- text: content || `# No entry for ${entryDate}\n`,
829
- mimeType: "text/markdown",
830
- },
831
- ],
832
- };
833
- });
834
- // ---------------------------------------------------------------------------
835
- // Tool: alignment_check (Intelligent Distance measurement)
836
- // ---------------------------------------------------------------------------
837
- server.registerTool("alignment_check", {
838
- title: "Alignment Check",
839
- description: "Record what the agent understood, its confidence, and any human correction. Measures the Intelligent Distance gap.",
840
- inputSchema: {
841
- goal: z.string().describe("Agent's understanding of the goal"),
842
- confidence: z.enum(["high", "medium", "low"]).describe("Agent's confidence"),
843
- assumptions: z.array(z.string()).optional().describe("What agent assumed"),
844
- unclear: z.string().optional().describe("What agent is unsure about"),
845
- human_correction: z.string().optional().describe("Human's correction or 'confirmed'"),
846
- delta: z.string().optional().describe("The gap, or 'none'"),
847
- category: z.enum(["goal", "scope", "priority", "technical", "aesthetic"]).default("goal"),
848
- project: z.string().default("auto"),
849
- },
850
- }, async ({ goal, confidence, assumptions, unclear, human_correction, delta, category, project }) => {
851
- const slug = await resolveProject(project);
852
- const date = todayISO();
853
- const dir = journalDir(slug);
854
- ensureDir(dir);
855
- const time = new Date().toISOString().slice(11, 19);
856
- const assumeStr = assumptions?.length ? assumptions.map(a => ` - ${a}`).join("\n") : " (none)";
857
- let entry = `### 🎯 Alignment (${time})\n`;
858
- entry += `**Goal**: ${goal}\n**Confidence**: ${confidence}\n**Category**: ${category}\n`;
859
- entry += `**Assumptions**:\n${assumeStr}\n`;
860
- if (unclear)
861
- entry += `**Unclear**: ${unclear}\n`;
862
- if (human_correction)
863
- entry += `**Human**: ${human_correction}\n**Delta**: ${delta || "not specified"}\n`;
864
- entry += "\n";
865
- const logPath = path.join(dir, `${date}-alignment.md`);
866
- if (!fs.existsSync(logPath)) {
867
- fs.writeFileSync(logPath, `# ${date} — Alignment Records\n\n---\n\n${entry}`, "utf-8");
868
- }
869
- else {
870
- fs.appendFileSync(logPath, entry, "utf-8");
871
- }
872
- return {
873
- content: [{ type: "text", text: JSON.stringify({ success: true, date, confidence, delta: delta || "pending", file: logPath }) }],
874
- };
875
- });
876
- // ---------------------------------------------------------------------------
877
- // Tool: nudge (surface human inconsistency)
878
- // ---------------------------------------------------------------------------
879
- server.registerTool("nudge", {
880
- title: "Nudge",
881
- description: "Surface a contradiction between the human's current input and a prior statement/decision. Helps the human clarify their own thinking.",
882
- inputSchema: {
883
- past_statement: z.string().describe("What the human said/decided before (with date if known)"),
884
- current_statement: z.string().describe("What the human is saying now"),
885
- question: z.string().describe("The clarifying question to ask"),
886
- category: z.enum(["goal", "scope", "priority", "technical", "aesthetic"]).default("goal"),
887
- project: z.string().default("auto"),
888
- },
889
- }, async ({ past_statement, current_statement, question, category, project }) => {
890
- const slug = await resolveProject(project);
891
- const date = todayISO();
892
- const dir = journalDir(slug);
893
- ensureDir(dir);
894
- const time = new Date().toISOString().slice(11, 19);
895
- let entry = `### 🔔 Nudge (${time})\n`;
896
- entry += `**Past**: ${past_statement}\n`;
897
- entry += `**Now**: ${current_statement}\n`;
898
- entry += `**Question**: ${question}\n`;
899
- entry += `**Category**: ${category}\n\n`;
900
- // Append to alignment log (nudges and alignment checks live together)
901
- const logPath = path.join(dir, `${date}-alignment.md`);
902
- if (!fs.existsSync(logPath)) {
903
- fs.writeFileSync(logPath, `# ${date} — Alignment Records\n\n---\n\n${entry}`, "utf-8");
904
- }
905
- else {
906
- fs.appendFileSync(logPath, entry, "utf-8");
907
- }
908
- return {
909
- content: [{ type: "text", text: JSON.stringify({ success: true, date, category, file: logPath }) }],
910
- };
911
- });
912
- // ---------------------------------------------------------------------------
913
- // Tool: context_synthesize (L3 semantic memory)
914
- // ---------------------------------------------------------------------------
915
- server.registerTool("context_synthesize", {
916
- title: "Synthesize Context",
917
- description: "Generate L3 semantic synthesis from recent journals. Extracts decisions, blockers, goal evolution, and detects contradictions across sessions.",
918
- inputSchema: {
919
- entries: z.number().int().default(5).describe("Number of recent entries to analyze"),
920
- focus: z.enum(["full", "decisions", "blockers", "goals"]).default("full"),
921
- project: z.string().default("auto"),
922
- },
923
- }, async ({ entries: count, focus, project }) => {
924
- const slug = await resolveProject(project);
925
- const journalEntries = listJournalFiles(slug);
926
- if (journalEntries.length === 0) {
927
- return { content: [{ type: "text", text: JSON.stringify({ error: `No entries for '${slug}'` }) }], isError: true };
928
- }
929
- const toRead = journalEntries.slice(0, count);
930
- const data = [];
931
- for (const entry of toRead) {
932
- const content = fs.readFileSync(path.join(entry.dir, entry.file), "utf-8");
933
- data.push({
934
- date: entry.date,
935
- brief: extractSection(content, "brief"),
936
- decisions: extractSection(content, "decisions"),
937
- blockers: extractSection(content, "blockers"),
938
- next: extractSection(content, "next"),
939
- observations: extractSection(content, "observations"),
940
- });
941
- }
942
- let syn = `# L3 Synthesis — ${slug}\n`;
943
- syn += `> ${toRead.length} entries: ${toRead[toRead.length - 1]?.date} → ${toRead[0]?.date}\n\n`;
944
- // Goal evolution
945
- if (focus === "full" || focus === "goals") {
946
- syn += `## Goal Evolution\n\n`;
947
- for (const e of data) {
948
- if (e.brief)
949
- syn += `**${e.date}**: ${e.brief.split("\n")[0]}\n`;
950
- }
951
- syn += "\n";
952
- }
953
- // Decisions with contradiction detection
954
- if (focus === "full" || focus === "decisions") {
955
- syn += `## Decisions\n\n`;
956
- const allDecisions = [];
957
- for (const e of data) {
958
- if (e.decisions)
959
- allDecisions.push(`**${e.date}**:\n${e.decisions}\n`);
960
- }
961
- syn += allDecisions.length > 0 ? allDecisions.join("\n") : "(none recorded)\n";
962
- // Simple contradiction check: find topics mentioned in multiple entries with different content
963
- if (allDecisions.length >= 2) {
964
- syn += "\n### Potential Contradictions\n\n";
965
- syn += "Review the decisions above. Flag if:\n";
966
- syn += "- A decision from an earlier date was reversed without explanation\n";
967
- syn += "- The same topic has conflicting approaches across dates\n";
968
- syn += "- A goal stated in one entry differs from another\n\n";
969
- }
970
- }
971
- // Current blockers
972
- if (focus === "full" || focus === "blockers") {
973
- syn += `## Active Blockers\n\n`;
974
- const latest = data.find(e => e.blockers);
975
- syn += latest ? `**${latest.date}**:\n${latest.blockers}\n\n` : "(none)\n\n";
976
- // Check if old blockers are still present
977
- const oldBlockers = data.filter(e => e.blockers && e !== latest);
978
- if (oldBlockers.length > 0) {
979
- syn += "### Recurring Blockers (appeared in older entries too)\n\n";
980
- for (const ob of oldBlockers.slice(0, 2)) {
981
- syn += `**${ob.date}**: ${ob.blockers?.split("\n")[0] || ""}\n`;
982
- }
983
- syn += "\n";
984
- }
985
- }
986
- // Cross-session observations
987
- if (focus === "full") {
988
- const obs = data.filter(e => e.observations);
989
- if (obs.length > 0) {
990
- syn += `## Patterns from Agent Observations\n\n`;
991
- for (const o of obs.slice(0, 3)) {
992
- syn += `**${o.date}**: ${o.observations?.split("\n").slice(0, 2).join(" ") || ""}\n`;
993
- }
994
- syn += "\n";
995
- }
996
- }
997
- // Alignment patterns (if alignment log exists for today)
998
- const alignPath = path.join(journalDir(slug), `${todayISO()}-alignment.md`);
999
- if (fs.existsSync(alignPath)) {
1000
- const alignContent = fs.readFileSync(alignPath, "utf-8");
1001
- const checks = (alignContent.match(/### 🎯/g) || []).length;
1002
- const nudges = (alignContent.match(/### 🔔/g) || []).length;
1003
- const low = (alignContent.match(/Confidence: low/g) || []).length;
1004
- if (checks > 0 || nudges > 0) {
1005
- syn += `## Today's Alignment\n\n`;
1006
- syn += `- Alignment checks: ${checks}\n- Nudges: ${nudges}\n- Low confidence: ${low}\n\n`;
1007
- }
1008
- }
1009
- return {
1010
- content: [{ type: "text", text: JSON.stringify({ project: slug, entries_analyzed: toRead.length, synthesis: syn }) }],
1011
- };
1012
- });
1013
- function stateFilePath(project, date) {
1014
- return path.join(journalDir(project), `${date}.state.json`);
1015
- }
1016
- function readState(project, date) {
1017
- const fp = stateFilePath(project, date);
1018
- if (!fs.existsSync(fp))
1019
- return null;
1020
- try {
1021
- return JSON.parse(fs.readFileSync(fp, "utf-8"));
1022
- }
1023
- catch {
1024
- return null;
1025
- }
1026
- }
1027
- server.registerTool("journal_state", {
1028
- title: "Read/Write Session State (JSON)",
1029
- description: "Layer 1: structured JSON session state. Faster than markdown for cold-start. " +
1030
- "Read mode: returns today's state as JSON. Write mode: merges new data into state. " +
1031
- "Use this for agent-to-agent handoffs — no prose parsing needed.",
1032
- inputSchema: {
1033
- action: z.enum(["read", "write"]).describe("'read' returns state, 'write' merges new data"),
1034
- data: z.string().optional().describe("JSON string to merge into state (write mode only)"),
1035
- date: z.string().default("latest").describe("ISO date or 'latest'"),
1036
- project: z.string().default("auto"),
1037
- },
1038
- }, async ({ action, data, date, project }) => {
1039
- const slug = await resolveProject(project);
1040
- let targetDate = date;
1041
- if (targetDate === "latest") {
1042
- // Find most recent state file
1043
- const dir = journalDir(slug);
1044
- if (fs.existsSync(dir)) {
1045
- const files = fs.readdirSync(dir)
1046
- .filter(f => f.endsWith(".state.json"))
1047
- .sort()
1048
- .reverse();
1049
- targetDate = files.length > 0 ? files[0].replace(".state.json", "") : todayISO();
1050
- }
1051
- else {
1052
- targetDate = todayISO();
1053
- }
1054
- }
1055
- if (action === "read") {
1056
- const state = readState(slug, targetDate);
1057
- return {
1058
- content: [{
1059
- type: "text",
1060
- text: JSON.stringify(state ?? { empty: true, date: targetDate, project: slug }),
1061
- }],
1062
- };
1063
- }
1064
- // Write: merge into existing state
1065
- const existing = readState(slug, todayISO()) ?? {
1066
- version: VERSION,
1067
- date: todayISO(),
1068
- project: slug,
1069
- timestamp: new Date().toISOString(),
1070
- completed: [],
1071
- failures: [],
1072
- state: {},
1073
- next_actions: [],
1074
- insights: [],
1075
- counts: {},
1076
- };
1077
- if (data) {
1078
- try {
1079
- const incoming = JSON.parse(data);
1080
- // Merge arrays by appending, objects by spreading
1081
- if (incoming.completed)
1082
- existing.completed.push(...incoming.completed);
1083
- if (incoming.failures)
1084
- existing.failures.push(...incoming.failures);
1085
- if (incoming.next_actions)
1086
- existing.next_actions = incoming.next_actions; // replace, not append
1087
- if (incoming.insights)
1088
- existing.insights.push(...incoming.insights);
1089
- if (incoming.state)
1090
- Object.assign(existing.state, incoming.state);
1091
- if (incoming.counts)
1092
- Object.assign(existing.counts, incoming.counts);
1093
- existing.timestamp = new Date().toISOString();
1094
- }
1095
- catch (e) {
1096
- return {
1097
- content: [{ type: "text", text: JSON.stringify({ error: `Invalid JSON: ${e}` }) }],
1098
- isError: true,
1099
- };
1100
- }
1101
- }
1102
- const fp = stateFilePath(slug, todayISO());
1103
- ensureDir(path.dirname(fp));
1104
- fs.writeFileSync(fp, JSON.stringify(existing, null, 2), "utf-8");
1105
- return {
1106
- content: [{
1107
- type: "text",
1108
- text: JSON.stringify({ success: true, date: todayISO(), entries: {
1109
- completed: existing.completed.length,
1110
- failures: existing.failures.length,
1111
- insights: existing.insights.length,
1112
- } }),
1113
- }],
1114
- };
1115
- });
1116
- // ---------------------------------------------------------------------------
1117
- // Tool: journal_cold_start — Cache-aware cold start (v3 architecture)
1118
- // ---------------------------------------------------------------------------
1119
- server.registerTool("journal_cold_start", {
1120
- title: "Cold Start Brief (Cache-Aware)",
1121
- description: "Returns a cache-aware cold-start package. HOT: today + yesterday (full). " +
1122
- "WARM: 2-7 days (summaries only). COLD: older (count only). " +
1123
- "Designed for minimal context consumption on session start.",
1124
- inputSchema: {
1125
- project: z.string().default("auto"),
1126
- },
1127
- }, async ({ project }) => {
1128
- const slug = await resolveProject(project);
1129
- const entries = listJournalFiles(slug);
1130
- const today = todayISO();
1131
- const hot = [];
1132
- const warm = [];
1133
- let coldCount = 0;
1134
- for (const entry of entries) {
1135
- const ageMs = Date.now() - new Date(entry.date).getTime();
1136
- const ageDays = ageMs / (1000 * 60 * 60 * 24);
1137
- if (ageDays <= 1.5) {
1138
- // HOT: state JSON (fast) + brief from markdown (capped at 5KB to save context)
1139
- const state = readState(slug, entry.date);
1140
- const fullPath = path.join(entry.dir, entry.file);
1141
- const stats = fs.statSync(fullPath);
1142
- const content = stats.size > 5120
1143
- ? fs.readFileSync(fullPath, "utf-8").slice(0, 5120) + "\n...(truncated, use journal_read for full)"
1144
- : fs.readFileSync(fullPath, "utf-8");
1145
- hot.push({
1146
- date: entry.date,
1147
- state,
1148
- brief: extractSection(content, "brief"),
1149
- });
1150
- }
1151
- else if (ageDays <= 7) {
1152
- // WARM: brief only (first 2KB of file to extract brief section)
1153
- const fullPath = path.join(entry.dir, entry.file);
1154
- const content = fs.readFileSync(fullPath, "utf-8").slice(0, 2048);
1155
- warm.push({
1156
- date: entry.date,
1157
- brief: extractSection(content, "brief"),
1158
- });
1159
- }
1160
- else {
1161
- // COLD: just count
1162
- coldCount++;
1163
- }
1164
- }
1165
- return {
1166
- content: [{
1167
- type: "text",
1168
- text: JSON.stringify({
1169
- project: slug,
1170
- cache: {
1171
- hot: { count: hot.length, entries: hot },
1172
- warm: { count: warm.length, entries: warm },
1173
- cold: { count: coldCount },
1174
- },
1175
- total_entries: entries.length,
1176
- tip: "HOT entries have full state. WARM have briefs only. Use journal_read for COLD entries.",
1177
- }),
1178
- }],
1179
- };
1180
- });
1181
- // ---------------------------------------------------------------------------
1182
- // Tool: journal_archive — Move completed sessions to cold storage
1183
- // ---------------------------------------------------------------------------
1184
- server.registerTool("journal_archive", {
1185
- title: "Archive Old Entries",
1186
- description: "Move entries older than N days to cold archive. Keeps a one-line summary per archived entry. " +
1187
- "Use after a project milestone or when journal count gets too high.",
1188
- inputSchema: {
1189
- older_than_days: z.number().int().default(7).describe("Archive entries older than this many days"),
1190
- project: z.string().default("auto"),
1191
- },
1192
- }, async ({ older_than_days, project }) => {
1193
- const slug = await resolveProject(project);
1194
- const entries = listJournalFiles(slug);
1195
- const dir = journalDir(slug);
1196
- const archiveDir = path.join(dir, "archive");
1197
- ensureDir(archiveDir);
1198
- let archived = 0;
1199
- const summaries = [];
1200
- for (const entry of entries) {
1201
- const ageMs = Date.now() - new Date(entry.date).getTime();
1202
- const ageDays = ageMs / (1000 * 60 * 60 * 24);
1203
- if (ageDays > older_than_days) {
1204
- const srcPath = path.join(entry.dir, entry.file);
1205
- const content = fs.readFileSync(srcPath, "utf-8");
1206
- const brief = extractSection(content, "brief");
1207
- const firstLine = brief?.split("\n").find(l => l.trim().length > 0) ?? "(no brief)";
1208
- // Move to archive (copy+delete for cross-device safety)
1209
- const destPath = path.join(archiveDir, entry.file);
1210
- fs.copyFileSync(srcPath, destPath);
1211
- fs.unlinkSync(srcPath);
1212
- // Also move state file if exists
1213
- const stateSrc = stateFilePath(slug, entry.date);
1214
- if (fs.existsSync(stateSrc)) {
1215
- const stateDest = path.join(archiveDir, `${entry.date}.state.json`);
1216
- fs.copyFileSync(stateSrc, stateDest);
1217
- fs.unlinkSync(stateSrc);
1218
- }
1219
- summaries.push(`${entry.date}: ${firstLine}`);
1220
- archived++;
1221
- }
1222
- }
1223
- // Write archive index
1224
- if (summaries.length > 0) {
1225
- const indexPath = path.join(archiveDir, "index.md");
1226
- const existing = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, "utf-8") : "# Archive\n\n";
1227
- fs.writeFileSync(indexPath, existing + summaries.join("\n") + "\n", "utf-8");
1228
- }
1229
- // Update main index
1230
- updateIndex(slug);
1231
- return {
1232
- content: [{
1233
- type: "text",
1234
- text: JSON.stringify({
1235
- archived,
1236
- summaries,
1237
- archive_dir: archiveDir,
1238
- }),
1239
- }],
1240
- };
1241
- });
77
+ // Register all tools
78
+ // ---------------------------------------------------------------------------
79
+ registerJournalRead(server);
80
+ registerJournalWrite(server);
81
+ registerJournalCapture(server);
82
+ registerJournalList(server);
83
+ registerJournalProjects(server);
84
+ registerJournalSearch(server);
85
+ registerJournalState(server);
86
+ registerJournalColdStart(server);
87
+ registerJournalArchive(server);
88
+ registerAlignmentCheck(server);
89
+ registerNudge(server);
90
+ registerContextSynthesize(server);
91
+ registerKnowledgeWrite(server);
92
+ registerKnowledgeRead(server);
93
+ registerPalaceRead(server);
94
+ registerPalaceWrite(server);
95
+ registerPalaceWalk(server);
96
+ registerPalaceLint(server);
97
+ registerPalaceSearch(server);
98
+ registerAwarenessUpdate(server);
99
+ registerRecallInsight(server);
100
+ // Register resources
101
+ registerJournalResources(server);
1242
102
  // ---------------------------------------------------------------------------
1243
103
  // Start
1244
104
  // ---------------------------------------------------------------------------