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.
- package/README.md +17 -3
- package/dist/helpers/journal-files.d.ts +30 -0
- package/dist/helpers/journal-files.d.ts.map +1 -0
- package/dist/helpers/journal-files.js +96 -0
- package/dist/helpers/journal-files.js.map +1 -0
- package/dist/helpers/sections.d.ts +12 -0
- package/dist/helpers/sections.d.ts.map +1 -0
- package/dist/helpers/sections.js +84 -0
- package/dist/helpers/sections.js.map +1 -0
- package/dist/index.js +59 -1199
- package/dist/index.js.map +1 -1
- package/dist/palace/awareness.d.ts +67 -0
- package/dist/palace/awareness.d.ts.map +1 -0
- package/dist/palace/awareness.js +231 -0
- package/dist/palace/awareness.js.map +1 -0
- package/dist/palace/consolidate.d.ts +28 -0
- package/dist/palace/consolidate.d.ts.map +1 -0
- package/dist/palace/consolidate.js +129 -0
- package/dist/palace/consolidate.js.map +1 -0
- package/dist/palace/fan-out.d.ts +15 -0
- package/dist/palace/fan-out.d.ts.map +1 -0
- package/dist/palace/fan-out.js +78 -0
- package/dist/palace/fan-out.js.map +1 -0
- package/dist/palace/graph.d.ts +11 -0
- package/dist/palace/graph.d.ts.map +1 -0
- package/dist/palace/graph.js +59 -0
- package/dist/palace/graph.js.map +1 -0
- package/dist/palace/identity.d.ts +6 -0
- package/dist/palace/identity.d.ts.map +1 -0
- package/dist/palace/identity.js +18 -0
- package/dist/palace/identity.js.map +1 -0
- package/dist/palace/index-manager.d.ts +7 -0
- package/dist/palace/index-manager.d.ts.map +1 -0
- package/dist/palace/index-manager.js +46 -0
- package/dist/palace/index-manager.js.map +1 -0
- package/dist/palace/insights-index.d.ts +39 -0
- package/dist/palace/insights-index.d.ts.map +1 -0
- package/dist/palace/insights-index.js +101 -0
- package/dist/palace/insights-index.js.map +1 -0
- package/dist/palace/log.d.ts +9 -0
- package/dist/palace/log.d.ts.map +1 -0
- package/dist/palace/log.js +28 -0
- package/dist/palace/log.js.map +1 -0
- package/dist/palace/obsidian.d.ts +12 -0
- package/dist/palace/obsidian.d.ts.map +1 -0
- package/dist/palace/obsidian.js +76 -0
- package/dist/palace/obsidian.js.map +1 -0
- package/dist/palace/rooms.d.ts +14 -0
- package/dist/palace/rooms.d.ts.map +1 -0
- package/dist/palace/rooms.js +117 -0
- package/dist/palace/rooms.js.map +1 -0
- package/dist/palace/salience.d.ts +15 -0
- package/dist/palace/salience.d.ts.map +1 -0
- package/dist/palace/salience.js +33 -0
- package/dist/palace/salience.js.map +1 -0
- package/dist/resources/journal-resources.d.ts +3 -0
- package/dist/resources/journal-resources.d.ts.map +1 -0
- package/dist/resources/journal-resources.js +72 -0
- package/dist/resources/journal-resources.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +7 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/fs-utils.d.ts +8 -0
- package/dist/storage/fs-utils.d.ts.map +1 -0
- package/dist/storage/fs-utils.js +28 -0
- package/dist/storage/fs-utils.js.map +1 -0
- package/dist/storage/paths.d.ts +21 -0
- package/dist/storage/paths.d.ts.map +1 -0
- package/dist/storage/paths.js +59 -0
- package/dist/storage/paths.js.map +1 -0
- package/dist/storage/project.d.ts +17 -0
- package/dist/storage/project.d.ts.map +1 -0
- package/dist/storage/project.js +130 -0
- package/dist/storage/project.js.map +1 -0
- package/dist/tools/alignment-check.d.ts +3 -0
- package/dist/tools/alignment-check.d.ts.map +1 -0
- package/dist/tools/alignment-check.js +73 -0
- package/dist/tools/alignment-check.js.map +1 -0
- package/dist/tools/awareness-update.d.ts +9 -0
- package/dist/tools/awareness-update.d.ts.map +1 -0
- package/dist/tools/awareness-update.js +90 -0
- package/dist/tools/awareness-update.js.map +1 -0
- package/dist/tools/context-synthesize.d.ts +3 -0
- package/dist/tools/context-synthesize.d.ts.map +1 -0
- package/dist/tools/context-synthesize.js +204 -0
- package/dist/tools/context-synthesize.js.map +1 -0
- package/dist/tools/journal-archive.d.ts +3 -0
- package/dist/tools/journal-archive.d.ts.map +1 -0
- package/dist/tools/journal-archive.js +62 -0
- package/dist/tools/journal-archive.js.map +1 -0
- package/dist/tools/journal-capture.d.ts +3 -0
- package/dist/tools/journal-capture.d.ts.map +1 -0
- package/dist/tools/journal-capture.js +87 -0
- package/dist/tools/journal-capture.js.map +1 -0
- package/dist/tools/journal-cold-start.d.ts +3 -0
- package/dist/tools/journal-cold-start.d.ts.map +1 -0
- package/dist/tools/journal-cold-start.js +70 -0
- package/dist/tools/journal-cold-start.js.map +1 -0
- package/dist/tools/journal-list.d.ts +3 -0
- package/dist/tools/journal-list.d.ts.map +1 -0
- package/dist/tools/journal-list.js +43 -0
- package/dist/tools/journal-list.js.map +1 -0
- package/dist/tools/journal-projects.d.ts +3 -0
- package/dist/tools/journal-projects.d.ts.map +1 -0
- package/dist/tools/journal-projects.js +25 -0
- package/dist/tools/journal-projects.js.map +1 -0
- package/dist/tools/journal-read.d.ts +3 -0
- package/dist/tools/journal-read.d.ts.map +1 -0
- package/dist/tools/journal-read.js +72 -0
- package/dist/tools/journal-read.js.map +1 -0
- package/dist/tools/journal-search.d.ts +3 -0
- package/dist/tools/journal-search.d.ts.map +1 -0
- package/dist/tools/journal-search.js +113 -0
- package/dist/tools/journal-search.js.map +1 -0
- package/dist/tools/journal-state.d.ts +6 -0
- package/dist/tools/journal-state.d.ts.map +1 -0
- package/dist/tools/journal-state.js +111 -0
- package/dist/tools/journal-state.js.map +1 -0
- package/dist/tools/journal-write.d.ts +3 -0
- package/dist/tools/journal-write.d.ts.map +1 -0
- package/dist/tools/journal-write.js +88 -0
- package/dist/tools/journal-write.js.map +1 -0
- package/dist/tools/knowledge-read.d.ts +3 -0
- package/dist/tools/knowledge-read.d.ts.map +1 -0
- package/dist/tools/knowledge-read.js +118 -0
- package/dist/tools/knowledge-read.js.map +1 -0
- package/dist/tools/knowledge-write.d.ts +3 -0
- package/dist/tools/knowledge-write.d.ts.map +1 -0
- package/dist/tools/knowledge-write.js +89 -0
- package/dist/tools/knowledge-write.js.map +1 -0
- package/dist/tools/nudge.d.ts +3 -0
- package/dist/tools/nudge.d.ts.map +1 -0
- package/dist/tools/nudge.js +41 -0
- package/dist/tools/nudge.js.map +1 -0
- package/dist/tools/palace-lint.d.ts +7 -0
- package/dist/tools/palace-lint.d.ts.map +1 -0
- package/dist/tools/palace-lint.js +149 -0
- package/dist/tools/palace-lint.js.map +1 -0
- package/dist/tools/palace-read.d.ts +6 -0
- package/dist/tools/palace-read.d.ts.map +1 -0
- package/dist/tools/palace-read.js +78 -0
- package/dist/tools/palace-read.js.map +1 -0
- package/dist/tools/palace-search.d.ts +6 -0
- package/dist/tools/palace-search.d.ts.map +1 -0
- package/dist/tools/palace-search.js +81 -0
- package/dist/tools/palace-search.js.map +1 -0
- package/dist/tools/palace-walk.d.ts +12 -0
- package/dist/tools/palace-walk.d.ts.map +1 -0
- package/dist/tools/palace-walk.js +167 -0
- package/dist/tools/palace-walk.js.map +1 -0
- package/dist/tools/palace-write.d.ts +6 -0
- package/dist/tools/palace-write.d.ts.map +1 -0
- package/dist/tools/palace-write.js +108 -0
- package/dist/tools/palace-write.js.map +1 -0
- package/dist/tools/recall-insight.d.ts +9 -0
- package/dist/tools/recall-insight.d.ts.map +1 -0
- package/dist/tools/recall-insight.js +51 -0
- package/dist/tools/recall-insight.js.map +1 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +31 -0
- package/dist/types.js.map +1 -0
- 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
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
//
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|