@prih/mcp-graph-memory 1.0.3

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 (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +512 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/files.js +64 -0
  7. package/dist/api/rest/graph.js +56 -0
  8. package/dist/api/rest/index.js +117 -0
  9. package/dist/api/rest/knowledge.js +238 -0
  10. package/dist/api/rest/skills.js +284 -0
  11. package/dist/api/rest/tasks.js +272 -0
  12. package/dist/api/rest/tools.js +126 -0
  13. package/dist/api/rest/validation.js +191 -0
  14. package/dist/api/rest/websocket.js +65 -0
  15. package/dist/api/tools/code/get-file-symbols.js +30 -0
  16. package/dist/api/tools/code/get-symbol.js +22 -0
  17. package/dist/api/tools/code/list-files.js +18 -0
  18. package/dist/api/tools/code/search-code.js +27 -0
  19. package/dist/api/tools/code/search-files.js +22 -0
  20. package/dist/api/tools/context/get-context.js +19 -0
  21. package/dist/api/tools/docs/cross-references.js +76 -0
  22. package/dist/api/tools/docs/explain-symbol.js +55 -0
  23. package/dist/api/tools/docs/find-examples.js +52 -0
  24. package/dist/api/tools/docs/get-node.js +24 -0
  25. package/dist/api/tools/docs/get-toc.js +22 -0
  26. package/dist/api/tools/docs/list-snippets.js +46 -0
  27. package/dist/api/tools/docs/list-topics.js +18 -0
  28. package/dist/api/tools/docs/search-files.js +22 -0
  29. package/dist/api/tools/docs/search-snippets.js +43 -0
  30. package/dist/api/tools/docs/search.js +27 -0
  31. package/dist/api/tools/file-index/get-file-info.js +21 -0
  32. package/dist/api/tools/file-index/list-all-files.js +28 -0
  33. package/dist/api/tools/file-index/search-all-files.js +24 -0
  34. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  35. package/dist/api/tools/knowledge/create-note.js +20 -0
  36. package/dist/api/tools/knowledge/create-relation.js +29 -0
  37. package/dist/api/tools/knowledge/delete-note.js +19 -0
  38. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  39. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  40. package/dist/api/tools/knowledge/get-note.js +20 -0
  41. package/dist/api/tools/knowledge/list-notes.js +18 -0
  42. package/dist/api/tools/knowledge/list-relations.js +17 -0
  43. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  44. package/dist/api/tools/knowledge/search-notes.js +25 -0
  45. package/dist/api/tools/knowledge/update-note.js +34 -0
  46. package/dist/api/tools/skills/add-attachment.js +31 -0
  47. package/dist/api/tools/skills/bump-usage.js +19 -0
  48. package/dist/api/tools/skills/create-skill-link.js +25 -0
  49. package/dist/api/tools/skills/create-skill.js +26 -0
  50. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  51. package/dist/api/tools/skills/delete-skill.js +20 -0
  52. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  53. package/dist/api/tools/skills/get-skill.js +21 -0
  54. package/dist/api/tools/skills/link-skill.js +23 -0
  55. package/dist/api/tools/skills/list-skills.js +20 -0
  56. package/dist/api/tools/skills/recall-skills.js +18 -0
  57. package/dist/api/tools/skills/remove-attachment.js +19 -0
  58. package/dist/api/tools/skills/search-skills.js +25 -0
  59. package/dist/api/tools/skills/update-skill.js +58 -0
  60. package/dist/api/tools/tasks/add-attachment.js +31 -0
  61. package/dist/api/tools/tasks/create-task-link.js +25 -0
  62. package/dist/api/tools/tasks/create-task.js +25 -0
  63. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  64. package/dist/api/tools/tasks/delete-task.js +20 -0
  65. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  66. package/dist/api/tools/tasks/get-task.js +20 -0
  67. package/dist/api/tools/tasks/link-task.js +23 -0
  68. package/dist/api/tools/tasks/list-tasks.js +24 -0
  69. package/dist/api/tools/tasks/move-task.js +38 -0
  70. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  71. package/dist/api/tools/tasks/search-tasks.js +25 -0
  72. package/dist/api/tools/tasks/update-task.js +55 -0
  73. package/dist/cli/index.js +451 -0
  74. package/dist/cli/indexer.js +277 -0
  75. package/dist/graphs/attachment-types.js +74 -0
  76. package/dist/graphs/code-types.js +10 -0
  77. package/dist/graphs/code.js +172 -0
  78. package/dist/graphs/docs.js +198 -0
  79. package/dist/graphs/file-index-types.js +10 -0
  80. package/dist/graphs/file-index.js +310 -0
  81. package/dist/graphs/file-lang.js +119 -0
  82. package/dist/graphs/knowledge-types.js +32 -0
  83. package/dist/graphs/knowledge.js +764 -0
  84. package/dist/graphs/manager-types.js +87 -0
  85. package/dist/graphs/skill-types.js +10 -0
  86. package/dist/graphs/skill.js +1013 -0
  87. package/dist/graphs/task-types.js +17 -0
  88. package/dist/graphs/task.js +960 -0
  89. package/dist/lib/embedder.js +101 -0
  90. package/dist/lib/events-log.js +400 -0
  91. package/dist/lib/file-import.js +327 -0
  92. package/dist/lib/file-mirror.js +446 -0
  93. package/dist/lib/frontmatter.js +17 -0
  94. package/dist/lib/mirror-watcher.js +637 -0
  95. package/dist/lib/multi-config.js +254 -0
  96. package/dist/lib/parsers/code.js +246 -0
  97. package/dist/lib/parsers/codeblock.js +66 -0
  98. package/dist/lib/parsers/docs.js +196 -0
  99. package/dist/lib/project-manager.js +418 -0
  100. package/dist/lib/promise-queue.js +22 -0
  101. package/dist/lib/search/bm25.js +167 -0
  102. package/dist/lib/search/code.js +103 -0
  103. package/dist/lib/search/docs.js +108 -0
  104. package/dist/lib/search/file-index.js +31 -0
  105. package/dist/lib/search/files.js +61 -0
  106. package/dist/lib/search/knowledge.js +101 -0
  107. package/dist/lib/search/skills.js +104 -0
  108. package/dist/lib/search/tasks.js +103 -0
  109. package/dist/lib/watcher.js +67 -0
  110. package/package.json +83 -0
  111. package/ui/README.md +54 -0
@@ -0,0 +1,637 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.MirrorWriteTracker = void 0;
40
+ exports.startMirrorWatcher = startMirrorWatcher;
41
+ exports.scanMirrorDirs = scanMirrorDirs;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const chokidar_1 = __importDefault(require("chokidar"));
45
+ const file_import_1 = require("./file-import");
46
+ const events_log_1 = require("./events-log");
47
+ const frontmatter_1 = require("./frontmatter");
48
+ /**
49
+ * Tracks recent mirror writes to suppress re-import (feedback loop prevention).
50
+ * When mirrorNote/mirrorTask writes a file, the watcher will fire —
51
+ * this tracker lets us detect our own writes and skip them.
52
+ */
53
+ class MirrorWriteTracker {
54
+ recentWrites = new Map();
55
+ static STALE_MS = 10_000; // entries older than 10s are stale
56
+ static MAX_ENTRIES = 10_000;
57
+ /** Called by mirrorNote/mirrorTask after writing a file. */
58
+ recordWrite(filePath) {
59
+ try {
60
+ const stat = fs.statSync(filePath, { throwIfNoEntry: false });
61
+ if (stat)
62
+ this.recentWrites.set(filePath, stat.mtimeMs);
63
+ }
64
+ catch { /* ignore */ }
65
+ // Prevent unbounded growth — evict stale entries periodically
66
+ if (this.recentWrites.size > MirrorWriteTracker.MAX_ENTRIES)
67
+ this.evictStale();
68
+ }
69
+ /** Called by watcher before importing. Returns true if this was our own write. */
70
+ isOwnWrite(filePath) {
71
+ const recorded = this.recentWrites.get(filePath);
72
+ if (recorded == null)
73
+ return false;
74
+ try {
75
+ const stat = fs.statSync(filePath, { throwIfNoEntry: false });
76
+ if (!stat)
77
+ return false;
78
+ if (Math.abs(stat.mtimeMs - recorded) < 100) {
79
+ this.recentWrites.delete(filePath);
80
+ return true;
81
+ }
82
+ }
83
+ catch { /* ignore */ }
84
+ this.recentWrites.delete(filePath);
85
+ return false;
86
+ }
87
+ evictStale() {
88
+ const now = Date.now();
89
+ for (const [filePath, mtime] of this.recentWrites) {
90
+ if (now - mtime > MirrorWriteTracker.STALE_MS)
91
+ this.recentWrites.delete(filePath);
92
+ }
93
+ }
94
+ }
95
+ exports.MirrorWriteTracker = MirrorWriteTracker;
96
+ function classifyFile(projectDir, filePath) {
97
+ const rel = path.relative(projectDir, filePath);
98
+ const parts = rel.split(path.sep);
99
+ if (parts.length === 3) {
100
+ const [dir, id, file] = parts;
101
+ // Snapshot files (gitignored, regenerated)
102
+ if (dir === '.notes' && file === 'note.md')
103
+ return { type: 'note-snapshot', id, entityDir: path.join(projectDir, dir, id) };
104
+ if (dir === '.tasks' && file === 'task.md')
105
+ return { type: 'task-snapshot', id, entityDir: path.join(projectDir, dir, id) };
106
+ if (dir === '.skills' && file === 'skill.md')
107
+ return { type: 'skill-snapshot', id, entityDir: path.join(projectDir, dir, id) };
108
+ // Event log (source of truth)
109
+ if (dir === '.notes' && file === 'events.jsonl')
110
+ return { type: 'note-events', id, entityDir: path.join(projectDir, dir, id) };
111
+ if (dir === '.tasks' && file === 'events.jsonl')
112
+ return { type: 'task-events', id, entityDir: path.join(projectDir, dir, id) };
113
+ if (dir === '.skills' && file === 'events.jsonl')
114
+ return { type: 'skill-events', id, entityDir: path.join(projectDir, dir, id) };
115
+ // Content files (git-tracked human-editable)
116
+ if (dir === '.notes' && file === 'content.md')
117
+ return { type: 'note-content', id, entityDir: path.join(projectDir, dir, id) };
118
+ if (dir === '.tasks' && file === 'description.md')
119
+ return { type: 'task-content', id, entityDir: path.join(projectDir, dir, id) };
120
+ if (dir === '.skills' && file === 'description.md')
121
+ return { type: 'skill-content', id, entityDir: path.join(projectDir, dir, id) };
122
+ }
123
+ if (parts.length === 4) {
124
+ const [dir, id, sub] = parts;
125
+ // Attachment files in {id}/attachments/
126
+ if (sub === 'attachments') {
127
+ if (dir === '.notes')
128
+ return { type: 'note-attachment', id, entityDir: path.join(projectDir, dir, id) };
129
+ if (dir === '.tasks')
130
+ return { type: 'task-attachment', id, entityDir: path.join(projectDir, dir, id) };
131
+ if (dir === '.skills')
132
+ return { type: 'skill-attachment', id, entityDir: path.join(projectDir, dir, id) };
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ // ---------------------------------------------------------------------------
138
+ // Snapshot diff helpers — detect user edits to task.md/note.md/skill.md
139
+ // ---------------------------------------------------------------------------
140
+ function isoToMs(value) {
141
+ if (value == null)
142
+ return null;
143
+ const d = new Date(value);
144
+ return isNaN(d.getTime()) ? null : d.getTime();
145
+ }
146
+ const VALID_STATUSES = new Set(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']);
147
+ const VALID_PRIORITIES = new Set(['critical', 'high', 'medium', 'low']);
148
+ const VALID_SOURCES = new Set(['user', 'learned']);
149
+ /** Parse a task snapshot file and extract structural fields from frontmatter. */
150
+ function parseTaskSnapshot(filePath) {
151
+ try {
152
+ const raw = fs.readFileSync(filePath, 'utf-8');
153
+ const { frontmatter: fm, body } = (0, frontmatter_1.parseMarkdown)(raw);
154
+ const lines = body.split('\n');
155
+ const headingMatch = lines[0]?.match(/^#\s+(.+)/);
156
+ const title = headingMatch?.[1]?.trim();
157
+ let start = 1;
158
+ if (lines[start] === '')
159
+ start++;
160
+ const description = lines.slice(start).join('\n').trim();
161
+ const result = {};
162
+ if (title)
163
+ result.title = title;
164
+ if (description)
165
+ result.description = description;
166
+ if (VALID_STATUSES.has(fm.status))
167
+ result.status = fm.status;
168
+ if (VALID_PRIORITIES.has(fm.priority))
169
+ result.priority = fm.priority;
170
+ if (Array.isArray(fm.tags))
171
+ result.tags = fm.tags.filter((t) => typeof t === 'string');
172
+ if ('dueDate' in fm)
173
+ result.dueDate = isoToMs(fm.dueDate);
174
+ if ('estimate' in fm)
175
+ result.estimate = typeof fm.estimate === 'number' ? fm.estimate : null;
176
+ return result;
177
+ }
178
+ catch {
179
+ return null;
180
+ }
181
+ }
182
+ /** Parse a note snapshot file. */
183
+ function parseNoteSnapshot(filePath) {
184
+ try {
185
+ const raw = fs.readFileSync(filePath, 'utf-8');
186
+ const { frontmatter: fm, body } = (0, frontmatter_1.parseMarkdown)(raw);
187
+ const lines = body.split('\n');
188
+ const headingMatch = lines[0]?.match(/^#\s+(.+)/);
189
+ const title = headingMatch?.[1]?.trim();
190
+ let start = 1;
191
+ if (lines[start] === '')
192
+ start++;
193
+ const content = lines.slice(start).join('\n').trim();
194
+ const result = {};
195
+ if (title)
196
+ result.title = title;
197
+ if (content !== undefined)
198
+ result.content = content;
199
+ if (Array.isArray(fm.tags))
200
+ result.tags = fm.tags.filter((t) => typeof t === 'string');
201
+ return result;
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ }
207
+ /** Parse a skill snapshot file. */
208
+ function parseSkillSnapshot(filePath) {
209
+ try {
210
+ const raw = fs.readFileSync(filePath, 'utf-8');
211
+ const { frontmatter: fm, body } = (0, frontmatter_1.parseMarkdown)(raw);
212
+ const lines = body.split('\n');
213
+ const headingMatch = lines[0]?.match(/^#\s+(.+)/);
214
+ const title = headingMatch?.[1]?.trim();
215
+ let start = 1;
216
+ if (lines[start] === '')
217
+ start++;
218
+ // Find ## Steps section to extract description
219
+ const stepsIdx = lines.findIndex((l, i) => i >= start && /^##\s+Steps/i.test(l));
220
+ const description = stepsIdx === -1
221
+ ? lines.slice(start).join('\n').trim()
222
+ : lines.slice(start, stepsIdx).join('\n').trim();
223
+ const result = {};
224
+ if (title)
225
+ result.title = title;
226
+ if (description)
227
+ result.description = description;
228
+ if (Array.isArray(fm.tags))
229
+ result.tags = fm.tags.filter((t) => typeof t === 'string');
230
+ if (VALID_SOURCES.has(fm.source))
231
+ result.source = fm.source;
232
+ if (typeof fm.confidence === 'number')
233
+ result.confidence = fm.confidence;
234
+ if (Array.isArray(fm.triggers))
235
+ result.triggers = fm.triggers.filter((t) => typeof t === 'string');
236
+ return result;
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ }
242
+ // ---------------------------------------------------------------------------
243
+ // startMirrorWatcher
244
+ // ---------------------------------------------------------------------------
245
+ /**
246
+ * Start watching .notes/, .tasks/, and .skills/ for external file edits.
247
+ *
248
+ * File types watched:
249
+ * - events.jsonl changes → replay all events → importFromFile (e.g., after git pull)
250
+ * - description.md / content.md changes → update description in graph (no new event)
251
+ * - task.md / note.md / skill.md user edits → detect delta → append update events → importFromFile
252
+ * - attachments/* → syncAttachments
253
+ * - directory removal → deleteFromFile
254
+ *
255
+ * Returns a handle to close the watcher.
256
+ */
257
+ function startMirrorWatcher(config) {
258
+ const notesDir = path.join(config.projectDir, '.notes');
259
+ const tasksDir = path.join(config.projectDir, '.tasks');
260
+ const skillsDir = path.join(config.projectDir, '.skills');
261
+ let resolveReady;
262
+ const whenReady = new Promise(resolve => { resolveReady = resolve; });
263
+ const watchPaths = [notesDir, tasksDir];
264
+ if (config.skillManager)
265
+ watchPaths.push(skillsDir);
266
+ const watcher = chokidar_1.default.watch(watchPaths, {
267
+ ignoreInitial: true,
268
+ persistent: true,
269
+ depth: 3, // increased to catch attachments/{filename}
270
+ });
271
+ const handleAddOrChange = (filePath) => {
272
+ if (config.tracker.isOwnWrite(filePath))
273
+ return;
274
+ const classified = classifyFile(config.projectDir, filePath);
275
+ if (!classified)
276
+ return;
277
+ const { type, id, entityDir } = classified;
278
+ // --- events.jsonl changed (e.g., git pull merged new events) ---
279
+ if (type === 'note-events') {
280
+ config.mutationQueue.enqueue(async () => {
281
+ const parsed = (0, file_import_1.parseNoteDir)(entityDir);
282
+ if (parsed)
283
+ await config.knowledgeManager.importFromFile(parsed);
284
+ }).catch(err => process.stderr.write(`[mirror-watcher] note events import error: ${err}\n`));
285
+ return;
286
+ }
287
+ if (type === 'task-events') {
288
+ config.mutationQueue.enqueue(async () => {
289
+ const parsed = (0, file_import_1.parseTaskDir)(entityDir);
290
+ if (parsed)
291
+ await config.taskManager.importFromFile(parsed);
292
+ }).catch(err => process.stderr.write(`[mirror-watcher] task events import error: ${err}\n`));
293
+ return;
294
+ }
295
+ if (type === 'skill-events' && config.skillManager) {
296
+ const mgr = config.skillManager;
297
+ config.mutationQueue.enqueue(async () => {
298
+ const parsed = (0, file_import_1.parseSkillDir)(entityDir);
299
+ if (parsed)
300
+ await mgr.importFromFile(parsed);
301
+ }).catch(err => process.stderr.write(`[mirror-watcher] skill events import error: ${err}\n`));
302
+ return;
303
+ }
304
+ // --- content.md / description.md changed (human-edited in IDE) ---
305
+ if (type === 'note-content') {
306
+ config.mutationQueue.enqueue(async () => {
307
+ // Re-parse from directory to pick up new content + existing events
308
+ const parsed = (0, file_import_1.parseNoteDir)(entityDir);
309
+ if (parsed)
310
+ await config.knowledgeManager.importFromFile(parsed);
311
+ }).catch(err => process.stderr.write(`[mirror-watcher] note content import error: ${err}\n`));
312
+ return;
313
+ }
314
+ if (type === 'task-content') {
315
+ config.mutationQueue.enqueue(async () => {
316
+ const parsed = (0, file_import_1.parseTaskDir)(entityDir);
317
+ if (parsed)
318
+ await config.taskManager.importFromFile(parsed);
319
+ }).catch(err => process.stderr.write(`[mirror-watcher] task content import error: ${err}\n`));
320
+ return;
321
+ }
322
+ if (type === 'skill-content' && config.skillManager) {
323
+ const mgr = config.skillManager;
324
+ config.mutationQueue.enqueue(async () => {
325
+ const parsed = (0, file_import_1.parseSkillDir)(entityDir);
326
+ if (parsed)
327
+ await mgr.importFromFile(parsed);
328
+ }).catch(err => process.stderr.write(`[mirror-watcher] skill content import error: ${err}\n`));
329
+ return;
330
+ }
331
+ // --- snapshot (task.md / note.md / skill.md) edited by user ---
332
+ // Detect delta vs current graph state, append update events, then re-import from dir
333
+ if (type === 'task-snapshot') {
334
+ config.mutationQueue.enqueue(async () => {
335
+ const current = config.taskManager.getTask(id);
336
+ const snapshot = parseTaskSnapshot(filePath);
337
+ if (!current || !snapshot)
338
+ return;
339
+ const eventsPath = path.join(entityDir, 'events.jsonl');
340
+ const delta = {};
341
+ if (snapshot.title !== undefined && snapshot.title !== current.title)
342
+ delta.title = snapshot.title;
343
+ if (snapshot.status !== undefined && snapshot.status !== current.status)
344
+ delta.status = snapshot.status;
345
+ if (snapshot.priority !== undefined && snapshot.priority !== current.priority)
346
+ delta.priority = snapshot.priority;
347
+ if (snapshot.tags !== undefined && JSON.stringify(snapshot.tags) !== JSON.stringify(current.tags))
348
+ delta.tags = snapshot.tags;
349
+ if ('dueDate' in snapshot && snapshot.dueDate !== current.dueDate)
350
+ delta.dueDate = snapshot.dueDate;
351
+ if ('estimate' in snapshot && snapshot.estimate !== current.estimate)
352
+ delta.estimate = snapshot.estimate;
353
+ if (Object.keys(delta).length > 0) {
354
+ (0, events_log_1.appendEvent)(eventsPath, { op: 'update', ...delta });
355
+ config.tracker.recordWrite(eventsPath);
356
+ }
357
+ // If description changed, write to description.md
358
+ if (snapshot.description !== undefined && snapshot.description !== current.description) {
359
+ const descPath = path.join(entityDir, 'description.md');
360
+ fs.writeFileSync(descPath, snapshot.description, 'utf-8');
361
+ config.tracker.recordWrite(descPath);
362
+ }
363
+ const parsed = (0, file_import_1.parseTaskDir)(entityDir);
364
+ if (parsed)
365
+ await config.taskManager.importFromFile(parsed);
366
+ }).catch(err => process.stderr.write(`[mirror-watcher] task snapshot edit error: ${err}\n`));
367
+ return;
368
+ }
369
+ if (type === 'note-snapshot') {
370
+ config.mutationQueue.enqueue(async () => {
371
+ const current = config.knowledgeManager.getNote(id);
372
+ const snapshot = parseNoteSnapshot(filePath);
373
+ if (!current || !snapshot)
374
+ return;
375
+ const eventsPath = path.join(entityDir, 'events.jsonl');
376
+ const delta = {};
377
+ if (snapshot.title !== undefined && snapshot.title !== current.title)
378
+ delta.title = snapshot.title;
379
+ if (snapshot.tags !== undefined && JSON.stringify(snapshot.tags) !== JSON.stringify(current.tags))
380
+ delta.tags = snapshot.tags;
381
+ if (Object.keys(delta).length > 0) {
382
+ (0, events_log_1.appendEvent)(eventsPath, { op: 'update', ...delta });
383
+ config.tracker.recordWrite(eventsPath);
384
+ }
385
+ // If content changed, write to content.md
386
+ if (snapshot.content !== undefined && snapshot.content !== current.content) {
387
+ const contentPath = path.join(entityDir, 'content.md');
388
+ fs.writeFileSync(contentPath, snapshot.content, 'utf-8');
389
+ config.tracker.recordWrite(contentPath);
390
+ }
391
+ const parsed = (0, file_import_1.parseNoteDir)(entityDir);
392
+ if (parsed)
393
+ await config.knowledgeManager.importFromFile(parsed);
394
+ }).catch(err => process.stderr.write(`[mirror-watcher] note snapshot edit error: ${err}\n`));
395
+ return;
396
+ }
397
+ if (type === 'skill-snapshot' && config.skillManager) {
398
+ const mgr = config.skillManager;
399
+ config.mutationQueue.enqueue(async () => {
400
+ const current = mgr.getSkill(id);
401
+ const snapshot = parseSkillSnapshot(filePath);
402
+ if (!current || !snapshot)
403
+ return;
404
+ const eventsPath = path.join(entityDir, 'events.jsonl');
405
+ const delta = {};
406
+ if (snapshot.title !== undefined && snapshot.title !== current.title)
407
+ delta.title = snapshot.title;
408
+ if (snapshot.tags !== undefined && JSON.stringify(snapshot.tags) !== JSON.stringify(current.tags))
409
+ delta.tags = snapshot.tags;
410
+ if (snapshot.source !== undefined && snapshot.source !== current.source)
411
+ delta.source = snapshot.source;
412
+ if (snapshot.confidence !== undefined && snapshot.confidence !== current.confidence)
413
+ delta.confidence = snapshot.confidence;
414
+ if (snapshot.triggers !== undefined && JSON.stringify(snapshot.triggers) !== JSON.stringify(current.triggers))
415
+ delta.triggers = snapshot.triggers;
416
+ if (Object.keys(delta).length > 0) {
417
+ (0, events_log_1.appendEvent)(eventsPath, { op: 'update', ...delta });
418
+ config.tracker.recordWrite(eventsPath);
419
+ }
420
+ // If description changed, write to description.md
421
+ if (snapshot.description !== undefined && snapshot.description !== current.description) {
422
+ const descPath = path.join(entityDir, 'description.md');
423
+ fs.writeFileSync(descPath, snapshot.description, 'utf-8');
424
+ config.tracker.recordWrite(descPath);
425
+ }
426
+ const parsed = (0, file_import_1.parseSkillDir)(entityDir);
427
+ if (parsed)
428
+ await mgr.importFromFile(parsed);
429
+ }).catch(err => process.stderr.write(`[mirror-watcher] skill snapshot edit error: ${err}\n`));
430
+ return;
431
+ }
432
+ // --- attachment file added ---
433
+ if (type === 'note-attachment') {
434
+ config.mutationQueue.enqueue(async () => {
435
+ config.knowledgeManager.syncAttachments(id);
436
+ }).catch(err => process.stderr.write(`[mirror-watcher] note attachment sync error: ${err}\n`));
437
+ return;
438
+ }
439
+ if (type === 'task-attachment') {
440
+ config.mutationQueue.enqueue(async () => {
441
+ config.taskManager.syncAttachments(id);
442
+ }).catch(err => process.stderr.write(`[mirror-watcher] task attachment sync error: ${err}\n`));
443
+ return;
444
+ }
445
+ if (type === 'skill-attachment' && config.skillManager) {
446
+ const mgr = config.skillManager;
447
+ config.mutationQueue.enqueue(async () => {
448
+ mgr.syncAttachments(id);
449
+ }).catch(err => process.stderr.write(`[mirror-watcher] skill attachment sync error: ${err}\n`));
450
+ return;
451
+ }
452
+ };
453
+ const handleUnlink = (filePath) => {
454
+ const classified = classifyFile(config.projectDir, filePath);
455
+ if (!classified)
456
+ return;
457
+ const { type, id, entityDir } = classified;
458
+ // events.jsonl deleted → delete entity from graph
459
+ if (type === 'note-events') {
460
+ config.mutationQueue.enqueue(async () => {
461
+ config.knowledgeManager.deleteFromFile(id);
462
+ }).catch(err => process.stderr.write(`[mirror-watcher] note delete error: ${err}\n`));
463
+ return;
464
+ }
465
+ if (type === 'task-events') {
466
+ config.mutationQueue.enqueue(async () => {
467
+ config.taskManager.deleteFromFile(id);
468
+ }).catch(err => process.stderr.write(`[mirror-watcher] task delete error: ${err}\n`));
469
+ return;
470
+ }
471
+ if (type === 'skill-events' && config.skillManager) {
472
+ const mgr = config.skillManager;
473
+ config.mutationQueue.enqueue(async () => {
474
+ mgr.deleteFromFile(id);
475
+ }).catch(err => process.stderr.write(`[mirror-watcher] skill delete error: ${err}\n`));
476
+ return;
477
+ }
478
+ // Snapshot deleted → ignore (gitignored, server regenerates it)
479
+ if (type === 'note-snapshot' || type === 'task-snapshot' || type === 'skill-snapshot') {
480
+ return;
481
+ }
482
+ // content.md deleted → re-import with empty content
483
+ if (type === 'note-content') {
484
+ config.mutationQueue.enqueue(async () => {
485
+ const parsed = (0, file_import_1.parseNoteDir)(entityDir);
486
+ if (parsed)
487
+ await config.knowledgeManager.importFromFile(parsed);
488
+ }).catch(err => process.stderr.write(`[mirror-watcher] note content delete sync error: ${err}\n`));
489
+ return;
490
+ }
491
+ if (type === 'task-content') {
492
+ config.mutationQueue.enqueue(async () => {
493
+ const parsed = (0, file_import_1.parseTaskDir)(entityDir);
494
+ if (parsed)
495
+ await config.taskManager.importFromFile(parsed);
496
+ }).catch(err => process.stderr.write(`[mirror-watcher] task content delete sync error: ${err}\n`));
497
+ return;
498
+ }
499
+ if (type === 'skill-content' && config.skillManager) {
500
+ const mgr = config.skillManager;
501
+ config.mutationQueue.enqueue(async () => {
502
+ const parsed = (0, file_import_1.parseSkillDir)(entityDir);
503
+ if (parsed)
504
+ await mgr.importFromFile(parsed);
505
+ }).catch(err => process.stderr.write(`[mirror-watcher] skill content delete sync error: ${err}\n`));
506
+ return;
507
+ }
508
+ // Attachment deleted → sync attachments metadata
509
+ if (type === 'note-attachment') {
510
+ config.mutationQueue.enqueue(async () => {
511
+ config.knowledgeManager.syncAttachments(id);
512
+ }).catch(err => process.stderr.write(`[mirror-watcher] note attachment sync error: ${err}\n`));
513
+ return;
514
+ }
515
+ if (type === 'task-attachment') {
516
+ config.mutationQueue.enqueue(async () => {
517
+ config.taskManager.syncAttachments(id);
518
+ }).catch(err => process.stderr.write(`[mirror-watcher] task attachment sync error: ${err}\n`));
519
+ return;
520
+ }
521
+ if (type === 'skill-attachment' && config.skillManager) {
522
+ const mgr = config.skillManager;
523
+ config.mutationQueue.enqueue(async () => {
524
+ mgr.syncAttachments(id);
525
+ }).catch(err => process.stderr.write(`[mirror-watcher] skill attachment sync error: ${err}\n`));
526
+ return;
527
+ }
528
+ };
529
+ watcher.on('add', handleAddOrChange);
530
+ watcher.on('change', handleAddOrChange);
531
+ watcher.on('unlink', handleUnlink);
532
+ watcher.once('ready', () => {
533
+ const watched = ['.notes/', '.tasks/'];
534
+ if (config.skillManager)
535
+ watched.push('.skills/');
536
+ process.stderr.write(`[mirror-watcher] Watching ${watched.join(', ')} in ${config.projectDir}\n`);
537
+ resolveReady();
538
+ });
539
+ watcher.on('error', (err) => {
540
+ process.stderr.write(`[mirror-watcher] Error: ${err}\n`);
541
+ });
542
+ return { whenReady, close: () => watcher.close() };
543
+ }
544
+ /**
545
+ * Scan .notes/, .tasks/, and .skills/ directories once on startup.
546
+ * Imports any entity dirs where events.jsonl is newer than the graph's updatedAt.
547
+ */
548
+ async function scanMirrorDirs(config) {
549
+ const notesDir = path.join(config.projectDir, '.notes');
550
+ const tasksDir = path.join(config.projectDir, '.tasks');
551
+ const skillsDir = path.join(config.projectDir, '.skills');
552
+ let noteCount = 0;
553
+ let taskCount = 0;
554
+ let skillCount = 0;
555
+ // Scan notes (directory-based: .notes/{id}/events.jsonl)
556
+ if (fs.existsSync(notesDir)) {
557
+ const entries = fs.readdirSync(notesDir, { withFileTypes: true });
558
+ for (const entry of entries) {
559
+ if (!entry.isDirectory())
560
+ continue;
561
+ const entityDir = path.join(notesDir, entry.name);
562
+ const eventsPath = path.join(entityDir, 'events.jsonl');
563
+ if (!fs.existsSync(eventsPath))
564
+ continue;
565
+ noteCount++;
566
+ const parsed = (0, file_import_1.parseNoteDir)(entityDir);
567
+ if (!parsed)
568
+ continue;
569
+ const existingUpdatedAt = config.knowledgeManager.getNodeUpdatedAt(parsed.id);
570
+ const evMtime = fs.statSync(eventsPath).mtimeMs;
571
+ const contentPath = path.join(entityDir, 'content.md');
572
+ const contentMtime = fs.existsSync(contentPath) ? fs.statSync(contentPath).mtimeMs : 0;
573
+ const fileMtime = Math.max(evMtime, contentMtime);
574
+ if (existingUpdatedAt == null || fileMtime > existingUpdatedAt) {
575
+ await config.mutationQueue.enqueue(async () => {
576
+ await config.knowledgeManager.importFromFile(parsed);
577
+ });
578
+ }
579
+ }
580
+ }
581
+ // Scan tasks (directory-based: .tasks/{id}/events.jsonl)
582
+ if (fs.existsSync(tasksDir)) {
583
+ const entries = fs.readdirSync(tasksDir, { withFileTypes: true });
584
+ for (const entry of entries) {
585
+ if (!entry.isDirectory())
586
+ continue;
587
+ const entityDir = path.join(tasksDir, entry.name);
588
+ const eventsPath = path.join(entityDir, 'events.jsonl');
589
+ if (!fs.existsSync(eventsPath))
590
+ continue;
591
+ taskCount++;
592
+ const parsed = (0, file_import_1.parseTaskDir)(entityDir);
593
+ if (!parsed)
594
+ continue;
595
+ const existingUpdatedAt = config.taskManager.getNodeUpdatedAt(parsed.id);
596
+ const evMtime = fs.statSync(eventsPath).mtimeMs;
597
+ const descPath = path.join(entityDir, 'description.md');
598
+ const descMtime = fs.existsSync(descPath) ? fs.statSync(descPath).mtimeMs : 0;
599
+ const fileMtime = Math.max(evMtime, descMtime);
600
+ if (existingUpdatedAt == null || fileMtime > existingUpdatedAt) {
601
+ await config.mutationQueue.enqueue(async () => {
602
+ await config.taskManager.importFromFile(parsed);
603
+ });
604
+ }
605
+ }
606
+ }
607
+ // Scan skills (directory-based: .skills/{id}/events.jsonl)
608
+ if (config.skillManager && fs.existsSync(skillsDir)) {
609
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
610
+ for (const entry of entries) {
611
+ if (!entry.isDirectory())
612
+ continue;
613
+ const entityDir = path.join(skillsDir, entry.name);
614
+ const eventsPath = path.join(entityDir, 'events.jsonl');
615
+ if (!fs.existsSync(eventsPath))
616
+ continue;
617
+ skillCount++;
618
+ const parsed = (0, file_import_1.parseSkillDir)(entityDir);
619
+ if (!parsed)
620
+ continue;
621
+ const existingUpdatedAt = config.skillManager.getNodeUpdatedAt(parsed.id);
622
+ const evMtime = fs.statSync(eventsPath).mtimeMs;
623
+ const descPath = path.join(entityDir, 'description.md');
624
+ const descMtime = fs.existsSync(descPath) ? fs.statSync(descPath).mtimeMs : 0;
625
+ const fileMtime = Math.max(evMtime, descMtime);
626
+ if (existingUpdatedAt == null || fileMtime > existingUpdatedAt) {
627
+ const mgr = config.skillManager;
628
+ await config.mutationQueue.enqueue(async () => {
629
+ await mgr.importFromFile(parsed);
630
+ });
631
+ }
632
+ }
633
+ }
634
+ if (noteCount > 0 || taskCount > 0 || skillCount > 0) {
635
+ process.stderr.write(`[mirror-watcher] Scanned ${noteCount} note(s), ${taskCount} task(s), ${skillCount} skill(s)\n`);
636
+ }
637
+ }