@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,960 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TaskGraphManager = exports.createTaskGraph = void 0;
7
+ exports.proxyId = proxyId;
8
+ exports.isProxy = isProxy;
9
+ exports.cleanupProxies = cleanupProxies;
10
+ exports.createTask = createTask;
11
+ exports.updateTask = updateTask;
12
+ exports.moveTask = moveTask;
13
+ exports.deleteTask = deleteTask;
14
+ exports.getTask = getTask;
15
+ exports.listTasks = listTasks;
16
+ exports.createTaskRelation = createTaskRelation;
17
+ exports.deleteTaskRelation = deleteTaskRelation;
18
+ exports.listTaskRelations = listTaskRelations;
19
+ exports.findLinkedTasks = findLinkedTasks;
20
+ exports.createCrossRelation = createCrossRelation;
21
+ exports.deleteCrossRelation = deleteCrossRelation;
22
+ exports.saveTaskGraph = saveTaskGraph;
23
+ exports.loadTaskGraph = loadTaskGraph;
24
+ const fs_1 = __importDefault(require("fs"));
25
+ const path_1 = __importDefault(require("path"));
26
+ const task_types_1 = require("../graphs/task-types");
27
+ Object.defineProperty(exports, "createTaskGraph", { enumerable: true, get: function () { return task_types_1.createTaskGraph; } });
28
+ const knowledge_types_1 = require("../graphs/knowledge-types");
29
+ const manager_types_1 = require("../graphs/manager-types");
30
+ const tasks_1 = require("../lib/search/tasks");
31
+ const bm25_1 = require("../lib/search/bm25");
32
+ const file_mirror_1 = require("../lib/file-mirror");
33
+ const attachment_types_1 = require("../graphs/attachment-types");
34
+ const file_import_1 = require("../lib/file-import");
35
+ // ---------------------------------------------------------------------------
36
+ // Proxy helpers
37
+ // ---------------------------------------------------------------------------
38
+ /** Build the proxy node ID. With projectId: `@docs::frontend::guide.md::Setup`, without: `@docs::guide.md::Setup` */
39
+ function proxyId(targetGraph, nodeId, projectId) {
40
+ return projectId ? `@${targetGraph}::${projectId}::${nodeId}` : `@${targetGraph}::${nodeId}`;
41
+ }
42
+ /** Check whether a node is a cross-graph proxy. */
43
+ function isProxy(graph, nodeId) {
44
+ if (!graph.hasNode(nodeId))
45
+ return false;
46
+ return graph.getNodeAttribute(nodeId, 'proxyFor') !== undefined;
47
+ }
48
+ /** Ensure a proxy node exists for the given external target. Returns its ID. */
49
+ function ensureProxyNode(graph, targetGraph, nodeId, projectId) {
50
+ const id = proxyId(targetGraph, nodeId, projectId);
51
+ if (!graph.hasNode(id)) {
52
+ graph.addNode(id, {
53
+ title: '',
54
+ description: '',
55
+ status: 'backlog',
56
+ priority: 'low',
57
+ tags: [],
58
+ dueDate: null,
59
+ estimate: null,
60
+ completedAt: null,
61
+ embedding: [],
62
+ attachments: [],
63
+ createdAt: 0,
64
+ updatedAt: 0,
65
+ version: 0,
66
+ proxyFor: { graph: targetGraph, nodeId, projectId },
67
+ });
68
+ }
69
+ return id;
70
+ }
71
+ /** Remove a proxy node if it has zero incident edges. */
72
+ function cleanupProxy(graph, nodeId) {
73
+ if (!graph.hasNode(nodeId))
74
+ return;
75
+ if (!isProxy(graph, nodeId))
76
+ return;
77
+ if (graph.degree(nodeId) === 0) {
78
+ graph.dropNode(nodeId);
79
+ }
80
+ }
81
+ /**
82
+ * Remove all proxy nodes whose target no longer exists in the external graph.
83
+ * Called after doc/code/file removal in the indexer.
84
+ */
85
+ function cleanupProxies(graph, targetGraph, externalGraph) {
86
+ const toRemove = [];
87
+ graph.forEachNode((id, attrs) => {
88
+ if (attrs.proxyFor && attrs.proxyFor.graph === targetGraph) {
89
+ if (!externalGraph.hasNode(attrs.proxyFor.nodeId)) {
90
+ toRemove.push(id);
91
+ }
92
+ }
93
+ });
94
+ for (const id of toRemove) {
95
+ graph.dropNode(id); // also drops incident edges
96
+ }
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // CRUD — Tasks
100
+ // ---------------------------------------------------------------------------
101
+ /** Create a task, return its slug ID. */
102
+ function createTask(graph, title, description, status, priority, tags, embedding, dueDate = null, estimate = null, author = '') {
103
+ const id = (0, knowledge_types_1.slugify)(title, graph);
104
+ const now = Date.now();
105
+ graph.addNode(id, {
106
+ title,
107
+ description,
108
+ status,
109
+ priority,
110
+ tags,
111
+ dueDate,
112
+ estimate,
113
+ completedAt: null,
114
+ embedding,
115
+ attachments: [],
116
+ createdAt: now,
117
+ updatedAt: now,
118
+ version: 1,
119
+ createdBy: author || undefined,
120
+ updatedBy: author || undefined,
121
+ });
122
+ return id;
123
+ }
124
+ /** Partial update of a task. Returns true if found and updated. Throws VersionConflictError if expectedVersion is provided and doesn't match. */
125
+ function updateTask(graph, taskId, patch, embedding, author = '', expectedVersion) {
126
+ if (!graph.hasNode(taskId))
127
+ return false;
128
+ if (isProxy(graph, taskId))
129
+ return false;
130
+ if (expectedVersion !== undefined) {
131
+ const current = graph.getNodeAttribute(taskId, 'version');
132
+ if (current !== expectedVersion)
133
+ throw new manager_types_1.VersionConflictError(current, expectedVersion);
134
+ }
135
+ if (patch.title !== undefined)
136
+ graph.setNodeAttribute(taskId, 'title', patch.title);
137
+ if (patch.description !== undefined)
138
+ graph.setNodeAttribute(taskId, 'description', patch.description);
139
+ if (patch.priority !== undefined)
140
+ graph.setNodeAttribute(taskId, 'priority', patch.priority);
141
+ if (patch.tags !== undefined)
142
+ graph.setNodeAttribute(taskId, 'tags', patch.tags);
143
+ if (patch.dueDate !== undefined)
144
+ graph.setNodeAttribute(taskId, 'dueDate', patch.dueDate);
145
+ if (patch.estimate !== undefined)
146
+ graph.setNodeAttribute(taskId, 'estimate', patch.estimate);
147
+ if (embedding !== undefined)
148
+ graph.setNodeAttribute(taskId, 'embedding', embedding);
149
+ if (author)
150
+ graph.setNodeAttribute(taskId, 'updatedBy', author);
151
+ // Handle status change with completedAt auto-logic
152
+ if (patch.status !== undefined) {
153
+ const oldStatus = graph.getNodeAttribute(taskId, 'status');
154
+ graph.setNodeAttribute(taskId, 'status', patch.status);
155
+ if ((patch.status === 'done' || patch.status === 'cancelled') && oldStatus !== 'done' && oldStatus !== 'cancelled') {
156
+ graph.setNodeAttribute(taskId, 'completedAt', Date.now());
157
+ }
158
+ else if (patch.status !== 'done' && patch.status !== 'cancelled' && (oldStatus === 'done' || oldStatus === 'cancelled')) {
159
+ graph.setNodeAttribute(taskId, 'completedAt', null);
160
+ }
161
+ }
162
+ graph.setNodeAttribute(taskId, 'version', graph.getNodeAttribute(taskId, 'version') + 1);
163
+ graph.setNodeAttribute(taskId, 'updatedAt', Date.now());
164
+ return true;
165
+ }
166
+ /** Move a task to a new status. Handles completedAt auto-logic. Returns true if found. Throws VersionConflictError if expectedVersion is provided and doesn't match. */
167
+ function moveTask(graph, taskId, newStatus, expectedVersion) {
168
+ if (!graph.hasNode(taskId))
169
+ return false;
170
+ if (isProxy(graph, taskId))
171
+ return false;
172
+ if (expectedVersion !== undefined) {
173
+ const current = graph.getNodeAttribute(taskId, 'version');
174
+ if (current !== expectedVersion)
175
+ throw new manager_types_1.VersionConflictError(current, expectedVersion);
176
+ }
177
+ const oldStatus = graph.getNodeAttribute(taskId, 'status');
178
+ graph.setNodeAttribute(taskId, 'status', newStatus);
179
+ if ((newStatus === 'done' || newStatus === 'cancelled') && oldStatus !== 'done' && oldStatus !== 'cancelled') {
180
+ graph.setNodeAttribute(taskId, 'completedAt', Date.now());
181
+ }
182
+ else if (newStatus !== 'done' && newStatus !== 'cancelled' && (oldStatus === 'done' || oldStatus === 'cancelled')) {
183
+ graph.setNodeAttribute(taskId, 'completedAt', null);
184
+ }
185
+ graph.setNodeAttribute(taskId, 'version', graph.getNodeAttribute(taskId, 'version') + 1);
186
+ graph.setNodeAttribute(taskId, 'updatedAt', Date.now());
187
+ return true;
188
+ }
189
+ /** Delete a task and all its incident edges. Also cleans up orphaned proxy nodes. */
190
+ function deleteTask(graph, taskId) {
191
+ if (!graph.hasNode(taskId))
192
+ return false;
193
+ if (isProxy(graph, taskId))
194
+ return false;
195
+ const proxyNeighbors = [];
196
+ graph.forEachNeighbor(taskId, (neighbor) => {
197
+ if (isProxy(graph, neighbor))
198
+ proxyNeighbors.push(neighbor);
199
+ });
200
+ graph.dropNode(taskId);
201
+ for (const p of proxyNeighbors) {
202
+ cleanupProxy(graph, p);
203
+ }
204
+ return true;
205
+ }
206
+ /** Get a task by ID, or null if not found. Excludes proxy nodes. */
207
+ function getTask(graph, taskId) {
208
+ if (!graph.hasNode(taskId))
209
+ return null;
210
+ if (isProxy(graph, taskId))
211
+ return null;
212
+ const attrs = graph.getNodeAttributes(taskId);
213
+ const subtasks = [];
214
+ const blockedBy = [];
215
+ const blocks = [];
216
+ const related = [];
217
+ const crossLinks = [];
218
+ // Incoming edges: subtask_of (child → this) means child is a subtask
219
+ graph.forEachInEdge(taskId, (_edge, edgeAttrs, source) => {
220
+ if (isProxy(graph, source)) {
221
+ // Incoming cross-graph link (e.g. note → this task via mirror proxy)
222
+ const proxyFor = graph.getNodeAttribute(source, 'proxyFor');
223
+ if (proxyFor) {
224
+ crossLinks.push({
225
+ nodeId: proxyFor.nodeId,
226
+ targetGraph: proxyFor.graph,
227
+ kind: edgeAttrs.kind,
228
+ direction: 'incoming',
229
+ });
230
+ }
231
+ return;
232
+ }
233
+ const srcAttrs = graph.getNodeAttributes(source);
234
+ if (edgeAttrs.kind === 'subtask_of') {
235
+ subtasks.push({ id: source, title: srcAttrs.title, status: srcAttrs.status });
236
+ }
237
+ else if (edgeAttrs.kind === 'blocks') {
238
+ // source blocks this task
239
+ blockedBy.push({ id: source, title: srcAttrs.title });
240
+ }
241
+ else if (edgeAttrs.kind === 'related_to') {
242
+ related.push({ id: source, title: srcAttrs.title });
243
+ }
244
+ });
245
+ // Outgoing edges
246
+ graph.forEachOutEdge(taskId, (_edge, edgeAttrs, _source, target) => {
247
+ if (isProxy(graph, target)) {
248
+ // Outgoing cross-graph link (task → external node via proxy)
249
+ const proxyFor = graph.getNodeAttribute(target, 'proxyFor');
250
+ if (proxyFor) {
251
+ crossLinks.push({
252
+ nodeId: proxyFor.nodeId,
253
+ targetGraph: proxyFor.graph,
254
+ kind: edgeAttrs.kind,
255
+ direction: 'outgoing',
256
+ });
257
+ }
258
+ return;
259
+ }
260
+ const tgtAttrs = graph.getNodeAttributes(target);
261
+ if (edgeAttrs.kind === 'subtask_of') {
262
+ // this task is a subtask of target — skip, handled via parent lookup
263
+ }
264
+ else if (edgeAttrs.kind === 'blocks') {
265
+ blocks.push({ id: target, title: tgtAttrs.title });
266
+ }
267
+ else if (edgeAttrs.kind === 'related_to') {
268
+ if (!related.some(r => r.id === target)) {
269
+ related.push({ id: target, title: tgtAttrs.title });
270
+ }
271
+ }
272
+ });
273
+ return {
274
+ id: taskId,
275
+ title: attrs.title,
276
+ description: attrs.description,
277
+ status: attrs.status,
278
+ priority: attrs.priority,
279
+ tags: attrs.tags,
280
+ dueDate: attrs.dueDate,
281
+ estimate: attrs.estimate,
282
+ completedAt: attrs.completedAt,
283
+ createdAt: attrs.createdAt,
284
+ updatedAt: attrs.updatedAt,
285
+ version: attrs.version,
286
+ attachments: attrs.attachments ?? [],
287
+ subtasks,
288
+ blockedBy,
289
+ blocks,
290
+ related,
291
+ crossLinks,
292
+ };
293
+ }
294
+ /** List tasks with optional filters. Excludes proxy nodes. */
295
+ function listTasks(graph, opts = {}) {
296
+ const { status, priority, tag, filter, limit = 50 } = opts;
297
+ const lowerFilter = filter?.toLowerCase();
298
+ const lowerTag = tag?.toLowerCase();
299
+ const results = [];
300
+ graph.forEachNode((id, attrs) => {
301
+ if (attrs.proxyFor)
302
+ return;
303
+ if (status && attrs.status !== status)
304
+ return;
305
+ if (priority && attrs.priority !== priority)
306
+ return;
307
+ if (lowerTag && !attrs.tags.some(t => t.toLowerCase() === lowerTag))
308
+ return;
309
+ if (lowerFilter) {
310
+ const match = id.toLowerCase().includes(lowerFilter) ||
311
+ attrs.title.toLowerCase().includes(lowerFilter);
312
+ if (!match)
313
+ return;
314
+ }
315
+ results.push({
316
+ id,
317
+ title: attrs.title,
318
+ description: attrs.description?.slice(0, 500),
319
+ status: attrs.status,
320
+ priority: attrs.priority,
321
+ tags: attrs.tags,
322
+ dueDate: attrs.dueDate,
323
+ estimate: attrs.estimate,
324
+ completedAt: attrs.completedAt,
325
+ version: attrs.version,
326
+ createdAt: attrs.createdAt,
327
+ updatedAt: attrs.updatedAt,
328
+ attachments: attrs.attachments ?? [],
329
+ });
330
+ });
331
+ return results
332
+ .sort((a, b) => {
333
+ // Sort by priority (critical first), then dueDate (earliest first, nulls last)
334
+ const pDiff = task_types_1.PRIORITY_ORDER[a.priority] - task_types_1.PRIORITY_ORDER[b.priority];
335
+ if (pDiff !== 0)
336
+ return pDiff;
337
+ if (a.dueDate === null && b.dueDate === null)
338
+ return 0;
339
+ if (a.dueDate === null)
340
+ return 1;
341
+ if (b.dueDate === null)
342
+ return -1;
343
+ return a.dueDate - b.dueDate;
344
+ })
345
+ .slice(0, limit);
346
+ }
347
+ // ---------------------------------------------------------------------------
348
+ // CRUD — Task Relations (task ↔ task)
349
+ // ---------------------------------------------------------------------------
350
+ /** Create a directed relation between two tasks. Returns true if created. */
351
+ function createTaskRelation(graph, fromId, toId, kind) {
352
+ if (!graph.hasNode(fromId) || !graph.hasNode(toId))
353
+ return false;
354
+ if (isProxy(graph, fromId) || isProxy(graph, toId))
355
+ return false;
356
+ if (graph.hasEdge(fromId, toId))
357
+ return false;
358
+ graph.addEdgeWithKey(`${fromId}→${toId}`, fromId, toId, { kind });
359
+ return true;
360
+ }
361
+ /** Delete a task relation. Returns true if it existed. */
362
+ function deleteTaskRelation(graph, fromId, toId) {
363
+ if (!graph.hasEdge(fromId, toId))
364
+ return false;
365
+ graph.dropEdge(fromId, toId);
366
+ return true;
367
+ }
368
+ /** List all relations for a task (both incoming and outgoing). Resolves proxy IDs and titles. */
369
+ function listTaskRelations(graph, taskId, externalGraphs) {
370
+ if (!graph.hasNode(taskId))
371
+ return [];
372
+ const results = [];
373
+ function resolveTitle(nodeId, targetGraph) {
374
+ if (!targetGraph) {
375
+ // Same-graph task
376
+ if (graph.hasNode(nodeId) && !isProxy(graph, nodeId)) {
377
+ return graph.getNodeAttribute(nodeId, 'title') || undefined;
378
+ }
379
+ return undefined;
380
+ }
381
+ if (!externalGraphs)
382
+ return undefined;
383
+ const extGraph = (0, manager_types_1.resolveExternalGraph)(externalGraphs, targetGraph);
384
+ if (!extGraph || !extGraph.hasNode(nodeId))
385
+ return undefined;
386
+ const attrs = extGraph.getNodeAttributes(nodeId);
387
+ return attrs.title || attrs.name || undefined;
388
+ }
389
+ function resolveEntry(source, target, kind) {
390
+ const sourceProxy = graph.hasNode(source) ? graph.getNodeAttribute(source, 'proxyFor') : undefined;
391
+ const targetProxy = graph.hasNode(target) ? graph.getNodeAttribute(target, 'proxyFor') : undefined;
392
+ if (targetProxy) {
393
+ const title = resolveTitle(targetProxy.nodeId, targetProxy.graph);
394
+ return { fromId: source, toId: targetProxy.nodeId, kind, targetGraph: targetProxy.graph, ...(title ? { title } : {}) };
395
+ }
396
+ if (sourceProxy) {
397
+ const title = resolveTitle(sourceProxy.nodeId, sourceProxy.graph);
398
+ return { fromId: sourceProxy.nodeId, toId: target, kind, targetGraph: sourceProxy.graph, ...(title ? { title } : {}) };
399
+ }
400
+ const otherId = source === taskId ? target : source;
401
+ const title = resolveTitle(otherId);
402
+ return { fromId: source, toId: target, kind, ...(title ? { title } : {}) };
403
+ }
404
+ graph.forEachOutEdge(taskId, (_edge, attrs, source, target) => {
405
+ results.push(resolveEntry(source, target, attrs.kind));
406
+ });
407
+ graph.forEachInEdge(taskId, (_edge, attrs, source, target) => {
408
+ results.push(resolveEntry(source, target, attrs.kind));
409
+ });
410
+ return results;
411
+ }
412
+ /**
413
+ * Find all tasks that have a cross-graph relation to the given target node.
414
+ * Optionally filter by relation kind.
415
+ */
416
+ function findLinkedTasks(graph, targetGraph, targetNodeId, kind, projectId) {
417
+ const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
418
+ if (projectId)
419
+ candidates.push(proxyId(targetGraph, targetNodeId));
420
+ const results = [];
421
+ const seen = new Set();
422
+ for (const pId of candidates) {
423
+ if (!graph.hasNode(pId))
424
+ continue;
425
+ graph.forEachInEdge(pId, (_edge, attrs, source) => {
426
+ if (seen.has(source))
427
+ return;
428
+ if (isProxy(graph, source))
429
+ return;
430
+ if (kind && attrs.kind !== kind)
431
+ return;
432
+ const taskAttrs = graph.getNodeAttributes(source);
433
+ seen.add(source);
434
+ results.push({
435
+ taskId: source,
436
+ title: taskAttrs.title,
437
+ kind: attrs.kind,
438
+ status: taskAttrs.status,
439
+ priority: taskAttrs.priority,
440
+ tags: taskAttrs.tags,
441
+ });
442
+ });
443
+ }
444
+ return results;
445
+ }
446
+ // ---------------------------------------------------------------------------
447
+ // Cross-graph relations (task → doc/code/file/knowledge node)
448
+ // ---------------------------------------------------------------------------
449
+ /**
450
+ * Create a cross-graph relation from a task to a node in an external graph.
451
+ * Optionally validates that the target exists in the external graph.
452
+ */
453
+ function createCrossRelation(graph, fromTaskId, targetGraph, targetNodeId, kind, externalGraph, projectId) {
454
+ if (!graph.hasNode(fromTaskId) || isProxy(graph, fromTaskId))
455
+ return false;
456
+ if (externalGraph && !externalGraph.hasNode(targetNodeId))
457
+ return false;
458
+ const pId = ensureProxyNode(graph, targetGraph, targetNodeId, projectId);
459
+ if (graph.hasEdge(fromTaskId, pId))
460
+ return false;
461
+ graph.addEdgeWithKey(`${fromTaskId}→${pId}`, fromTaskId, pId, { kind });
462
+ return true;
463
+ }
464
+ /**
465
+ * Delete a cross-graph relation. Cleans up orphaned proxy node.
466
+ */
467
+ function deleteCrossRelation(graph, fromTaskId, targetGraph, targetNodeId, projectId) {
468
+ const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
469
+ if (projectId)
470
+ candidates.push(proxyId(targetGraph, targetNodeId));
471
+ for (const pId of candidates) {
472
+ if (graph.hasEdge(fromTaskId, pId)) {
473
+ graph.dropEdge(fromTaskId, pId);
474
+ cleanupProxy(graph, pId);
475
+ return true;
476
+ }
477
+ }
478
+ return false;
479
+ }
480
+ // ---------------------------------------------------------------------------
481
+ // Persistence
482
+ // ---------------------------------------------------------------------------
483
+ function saveTaskGraph(graph, graphMemory, embeddingFingerprint) {
484
+ fs_1.default.mkdirSync(graphMemory, { recursive: true });
485
+ const file = path_1.default.join(graphMemory, 'tasks.json');
486
+ const tmp = file + '.tmp';
487
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
488
+ fs_1.default.renameSync(tmp, file);
489
+ }
490
+ function loadTaskGraph(graphMemory, fresh = false, embeddingFingerprint) {
491
+ const graph = (0, task_types_1.createTaskGraph)();
492
+ if (fresh)
493
+ return graph;
494
+ const file = path_1.default.join(graphMemory, 'tasks.json');
495
+ if (!fs_1.default.existsSync(file))
496
+ return graph;
497
+ try {
498
+ const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
499
+ const stored = data.embeddingModel;
500
+ if (embeddingFingerprint && stored !== embeddingFingerprint) {
501
+ process.stderr.write(`[task-graph] Embedding config changed, re-indexing task graph\n`);
502
+ return graph;
503
+ }
504
+ graph.import(data.graph);
505
+ process.stderr.write(`[task-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
506
+ }
507
+ catch (err) {
508
+ process.stderr.write(`[task-graph] Failed to load graph, starting fresh: ${err}\n`);
509
+ }
510
+ return graph;
511
+ }
512
+ // ---------------------------------------------------------------------------
513
+ // Bidirectional mirror helpers (Task ↔ Knowledge)
514
+ // ---------------------------------------------------------------------------
515
+ /**
516
+ * Create a mirror proxy in KnowledgeGraph when a task links to a note.
517
+ * Creates `@tasks::taskId` proxy node + edge proxy→noteId in KnowledgeGraph.
518
+ */
519
+ function createMirrorInKnowledgeGraph(knowledgeGraph, taskId, noteId, kind) {
520
+ const mirrorProxyId = `@tasks::${taskId}`;
521
+ if (!knowledgeGraph.hasNode(mirrorProxyId)) {
522
+ knowledgeGraph.addNode(mirrorProxyId, {
523
+ title: '',
524
+ content: '',
525
+ tags: [],
526
+ embedding: [],
527
+ attachments: [],
528
+ createdAt: 0,
529
+ updatedAt: 0,
530
+ version: 0,
531
+ proxyFor: { graph: 'tasks', nodeId: taskId },
532
+ });
533
+ }
534
+ if (!knowledgeGraph.hasNode(noteId))
535
+ return;
536
+ const edgeKey = `${mirrorProxyId}→${noteId}`;
537
+ if (!knowledgeGraph.hasEdge(edgeKey)) {
538
+ knowledgeGraph.addEdgeWithKey(edgeKey, mirrorProxyId, noteId, { kind });
539
+ }
540
+ }
541
+ /**
542
+ * Remove the mirror proxy edge/node from KnowledgeGraph when a task→knowledge relation is deleted.
543
+ */
544
+ function deleteMirrorFromKnowledgeGraph(knowledgeGraph, taskId, noteId) {
545
+ const mirrorProxyId = `@tasks::${taskId}`;
546
+ const edgeKey = `${mirrorProxyId}→${noteId}`;
547
+ if (knowledgeGraph.hasEdge(edgeKey)) {
548
+ knowledgeGraph.dropEdge(edgeKey);
549
+ }
550
+ // Cleanup orphan proxy
551
+ if (knowledgeGraph.hasNode(mirrorProxyId)) {
552
+ const proxyFor = knowledgeGraph.getNodeAttribute(mirrorProxyId, 'proxyFor');
553
+ if (proxyFor && knowledgeGraph.degree(mirrorProxyId) === 0) {
554
+ knowledgeGraph.dropNode(mirrorProxyId);
555
+ }
556
+ }
557
+ }
558
+ // ---------------------------------------------------------------------------
559
+ // TaskGraphManager — unified API for task graph operations
560
+ // ---------------------------------------------------------------------------
561
+ class TaskGraphManager {
562
+ _graph;
563
+ embedFns;
564
+ ctx;
565
+ ext;
566
+ knowledgeGraph;
567
+ mirrorTracker;
568
+ _bm25Index;
569
+ constructor(_graph, embedFns, ctx, ext = {}) {
570
+ this._graph = _graph;
571
+ this.embedFns = embedFns;
572
+ this.ctx = ctx;
573
+ this.ext = ext;
574
+ this.knowledgeGraph = ext.knowledgeGraph;
575
+ this._bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.title} ${attrs.description} ${attrs.tags.join(' ')}`);
576
+ this._graph.forEachNode((id, attrs) => {
577
+ if (!attrs.proxyFor)
578
+ this._bm25Index.addDocument(id, attrs);
579
+ });
580
+ }
581
+ get graph() { return this._graph; }
582
+ get bm25Index() { return this._bm25Index; }
583
+ rebuildBm25Index() {
584
+ this._bm25Index.clear();
585
+ this._graph.forEachNode((id, attrs) => {
586
+ if (!attrs.proxyFor)
587
+ this._bm25Index.addDocument(id, attrs);
588
+ });
589
+ }
590
+ setMirrorTracker(tracker) {
591
+ this.mirrorTracker = tracker;
592
+ }
593
+ /** Returns updatedAt for a node, or null if not found. Used by startup scan. */
594
+ getNodeUpdatedAt(taskId) {
595
+ if (!this._graph.hasNode(taskId))
596
+ return null;
597
+ if (isProxy(this._graph, taskId))
598
+ return null;
599
+ return this._graph.getNodeAttribute(taskId, 'updatedAt') ?? null;
600
+ }
601
+ get tasksDir() {
602
+ const base = this.ctx.mirrorDir ?? this.ctx.projectDir;
603
+ return base ? path_1.default.join(base, '.tasks') : undefined;
604
+ }
605
+ recordMirrorWrites(taskId) {
606
+ const dir = this.tasksDir;
607
+ if (!dir || !this.mirrorTracker)
608
+ return;
609
+ const entityDir = path_1.default.join(dir, taskId);
610
+ this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
611
+ this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'task.md'));
612
+ this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'description.md'));
613
+ }
614
+ // -- Write (mutations with embed + dirty + emit + cross-graph cleanup) --
615
+ async createTask(title, description, status = 'backlog', priority = 'medium', tags = [], dueDate = null, estimate = null) {
616
+ const embedding = await this.embedFns.document(`${title} ${description}`);
617
+ const taskId = createTask(this._graph, title, description, status, priority, tags, embedding, dueDate, estimate, this.ctx.author);
618
+ this._bm25Index.addDocument(taskId, this._graph.getNodeAttributes(taskId));
619
+ this.ctx.markDirty();
620
+ this.ctx.emit('task:created', { projectId: this.ctx.projectId, taskId });
621
+ const dir = this.tasksDir;
622
+ if (dir) {
623
+ const attrs = this._graph.getNodeAttributes(taskId);
624
+ (0, file_mirror_1.mirrorTaskCreate)(dir, taskId, attrs, []);
625
+ this.recordMirrorWrites(taskId);
626
+ }
627
+ return taskId;
628
+ }
629
+ async updateTask(taskId, patch, expectedVersion) {
630
+ const existing = getTask(this._graph, taskId);
631
+ if (!existing)
632
+ return false;
633
+ const embedText = `${patch.title ?? existing.title} ${patch.description ?? existing.description}`;
634
+ const embedding = await this.embedFns.document(embedText);
635
+ updateTask(this._graph, taskId, patch, embedding, this.ctx.author, expectedVersion);
636
+ this._bm25Index.updateDocument(taskId, this._graph.getNodeAttributes(taskId));
637
+ this.ctx.markDirty();
638
+ this.ctx.emit('task:updated', { projectId: this.ctx.projectId, taskId });
639
+ const dir = this.tasksDir;
640
+ if (dir) {
641
+ const attrs = this._graph.getNodeAttributes(taskId);
642
+ const relations = listTaskRelations(this._graph, taskId, this.ext);
643
+ (0, file_mirror_1.mirrorTaskUpdate)(dir, taskId, { ...patch, by: this.ctx.author }, attrs, relations);
644
+ this.recordMirrorWrites(taskId);
645
+ }
646
+ return true;
647
+ }
648
+ deleteTask(taskId) {
649
+ if (this.tasksDir)
650
+ (0, file_mirror_1.deleteMirrorDir)(this.tasksDir, taskId);
651
+ this._bm25Index.removeDocument(taskId);
652
+ const ok = deleteTask(this._graph, taskId);
653
+ if (!ok)
654
+ return false;
655
+ // Clean up proxy in KnowledgeGraph if any note links to this task
656
+ if (this.knowledgeGraph) {
657
+ const toRemove = [];
658
+ this.knowledgeGraph.forEachNode((id, attrs) => {
659
+ if (attrs.proxyFor?.graph === 'tasks' && attrs.proxyFor.nodeId === taskId) {
660
+ toRemove.push(id);
661
+ }
662
+ });
663
+ for (const id of toRemove)
664
+ this.knowledgeGraph.dropNode(id);
665
+ }
666
+ this.ctx.markDirty();
667
+ this.ctx.emit('task:deleted', { projectId: this.ctx.projectId, taskId });
668
+ return true;
669
+ }
670
+ moveTask(taskId, status, expectedVersion) {
671
+ const ok = moveTask(this._graph, taskId, status, expectedVersion);
672
+ if (!ok)
673
+ return false;
674
+ this.ctx.markDirty();
675
+ this.ctx.emit('task:moved', { projectId: this.ctx.projectId, taskId, status });
676
+ const dir = this.tasksDir;
677
+ if (dir) {
678
+ const attrs = this._graph.getNodeAttributes(taskId);
679
+ const relations = listTaskRelations(this._graph, taskId, this.ext);
680
+ (0, file_mirror_1.mirrorTaskUpdate)(dir, taskId, { status, completedAt: attrs.completedAt, by: this.ctx.author }, attrs, relations);
681
+ this.recordMirrorWrites(taskId);
682
+ }
683
+ return true;
684
+ }
685
+ linkTasks(fromId, toId, kind) {
686
+ const ok = createTaskRelation(this._graph, fromId, toId, kind);
687
+ if (ok) {
688
+ this.ctx.markDirty();
689
+ const dir = this.tasksDir;
690
+ if (dir) {
691
+ const fromAttrs = this._graph.getNodeAttributes(fromId);
692
+ const fromRels = listTaskRelations(this._graph, fromId, this.ext);
693
+ (0, file_mirror_1.mirrorTaskRelation)(dir, fromId, 'add', kind, toId, fromAttrs, fromRels);
694
+ this.recordMirrorWrites(fromId);
695
+ }
696
+ }
697
+ return ok;
698
+ }
699
+ createCrossLink(taskId, targetId, targetGraph, kind, projectId) {
700
+ const pid = projectId || this.ctx.projectId;
701
+ const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, targetGraph, pid);
702
+ const ok = createCrossRelation(this._graph, taskId, targetGraph, targetId, kind, extGraph, pid);
703
+ // Bidirectional: create mirror proxy in KnowledgeGraph
704
+ if (ok && targetGraph === 'knowledge' && this.knowledgeGraph) {
705
+ createMirrorInKnowledgeGraph(this.knowledgeGraph, taskId, targetId, kind);
706
+ }
707
+ if (ok) {
708
+ this.ctx.markDirty();
709
+ const dir = this.tasksDir;
710
+ if (dir) {
711
+ const attrs = this._graph.getNodeAttributes(taskId);
712
+ const relations = listTaskRelations(this._graph, taskId, this.ext);
713
+ (0, file_mirror_1.mirrorTaskRelation)(dir, taskId, 'add', kind, targetId, attrs, relations, targetGraph);
714
+ this.recordMirrorWrites(taskId);
715
+ }
716
+ }
717
+ return ok;
718
+ }
719
+ deleteCrossLink(taskId, targetId, targetGraph, projectId) {
720
+ const pid = projectId || this.ctx.projectId;
721
+ // Read edge kind before deleting
722
+ let kind = '';
723
+ try {
724
+ const proxyNodeId = proxyId(targetGraph, targetId, pid);
725
+ if (this._graph.hasEdge(taskId, proxyNodeId)) {
726
+ kind = this._graph.getEdgeAttribute(this._graph.edge(taskId, proxyNodeId), 'kind') ?? '';
727
+ }
728
+ }
729
+ catch { /* ignore */ }
730
+ const ok = deleteCrossRelation(this._graph, taskId, targetGraph, targetId, pid);
731
+ // Bidirectional: remove mirror proxy from KnowledgeGraph
732
+ if (ok && targetGraph === 'knowledge' && this.knowledgeGraph) {
733
+ deleteMirrorFromKnowledgeGraph(this.knowledgeGraph, taskId, targetId);
734
+ }
735
+ if (ok) {
736
+ this.ctx.markDirty();
737
+ const dir = this.tasksDir;
738
+ if (dir) {
739
+ const attrs = this._graph.getNodeAttributes(taskId);
740
+ const relations = listTaskRelations(this._graph, taskId, this.ext);
741
+ (0, file_mirror_1.mirrorTaskRelation)(dir, taskId, 'remove', kind, targetId, attrs, relations, targetGraph);
742
+ this.recordMirrorWrites(taskId);
743
+ }
744
+ }
745
+ return ok;
746
+ }
747
+ deleteTaskLink(fromId, toId) {
748
+ // Read edge kind before deleting
749
+ let kind = '';
750
+ try {
751
+ if (this._graph.hasEdge(fromId, toId)) {
752
+ kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, toId), 'kind') ?? '';
753
+ }
754
+ }
755
+ catch { /* ignore */ }
756
+ const ok = deleteTaskRelation(this._graph, fromId, toId);
757
+ if (ok) {
758
+ this.ctx.markDirty();
759
+ const dir = this.tasksDir;
760
+ if (dir) {
761
+ const fromAttrs = this._graph.getNodeAttributes(fromId);
762
+ const fromRels = listTaskRelations(this._graph, fromId, this.ext);
763
+ (0, file_mirror_1.mirrorTaskRelation)(dir, fromId, 'remove', kind, toId, fromAttrs, fromRels);
764
+ this.recordMirrorWrites(fromId);
765
+ }
766
+ }
767
+ return ok;
768
+ }
769
+ // -- Attachments --
770
+ addAttachment(taskId, filename, data) {
771
+ const dir = this.tasksDir;
772
+ if (!dir)
773
+ return null;
774
+ if (!this._graph.hasNode(taskId) || isProxy(this._graph, taskId))
775
+ return null;
776
+ const safe = (0, file_mirror_1.sanitizeFilename)(filename);
777
+ if (!safe)
778
+ return null;
779
+ const entityDir = path_1.default.join(dir, taskId);
780
+ (0, file_mirror_1.writeAttachment)(dir, taskId, safe, data);
781
+ this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
782
+ (0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'add', safe);
783
+ this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
784
+ const attachments = (0, attachment_types_1.scanAttachments)(entityDir);
785
+ this._graph.setNodeAttribute(taskId, 'attachments', attachments);
786
+ this._graph.setNodeAttribute(taskId, 'updatedAt', Date.now());
787
+ this.ctx.markDirty();
788
+ this.ctx.emit('task:attachment:added', { projectId: this.ctx.projectId, taskId, filename: safe });
789
+ return attachments.find(a => a.filename === safe) ?? null;
790
+ }
791
+ removeAttachment(taskId, filename) {
792
+ const dir = this.tasksDir;
793
+ if (!dir)
794
+ return false;
795
+ if (!this._graph.hasNode(taskId) || isProxy(this._graph, taskId))
796
+ return false;
797
+ const safe = (0, file_mirror_1.sanitizeFilename)(filename);
798
+ const entityDir = path_1.default.join(dir, taskId);
799
+ const deleted = (0, file_mirror_1.deleteAttachment)(dir, taskId, safe);
800
+ if (!deleted)
801
+ return false;
802
+ this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
803
+ (0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'remove', safe);
804
+ this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
805
+ const attachments = (0, attachment_types_1.scanAttachments)(entityDir);
806
+ this._graph.setNodeAttribute(taskId, 'attachments', attachments);
807
+ this._graph.setNodeAttribute(taskId, 'updatedAt', Date.now());
808
+ this.ctx.markDirty();
809
+ this.ctx.emit('task:attachment:deleted', { projectId: this.ctx.projectId, taskId, filename: safe });
810
+ return true;
811
+ }
812
+ syncAttachments(taskId) {
813
+ const dir = this.tasksDir;
814
+ if (!dir)
815
+ return;
816
+ if (!this._graph.hasNode(taskId) || isProxy(this._graph, taskId))
817
+ return;
818
+ const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, taskId));
819
+ this._graph.setNodeAttribute(taskId, 'attachments', attachments);
820
+ this.ctx.markDirty();
821
+ }
822
+ listAttachments(taskId) {
823
+ if (!this._graph.hasNode(taskId) || isProxy(this._graph, taskId))
824
+ return [];
825
+ return this._graph.getNodeAttribute(taskId, 'attachments') ?? [];
826
+ }
827
+ getAttachmentPath(taskId, filename) {
828
+ const dir = this.tasksDir;
829
+ if (!dir)
830
+ return null;
831
+ return (0, file_mirror_1.getAttachmentPath)(dir, taskId, filename);
832
+ }
833
+ // -- Import from file (reverse mirror — does NOT write back to file) --
834
+ async importFromFile(parsed) {
835
+ const exists = this._graph.hasNode(parsed.id) && !isProxy(this._graph, parsed.id);
836
+ const embedding = await this.embedFns.document(`${parsed.title} ${parsed.description}`);
837
+ const now = Date.now();
838
+ if (exists) {
839
+ const existing = this._graph.getNodeAttributes(parsed.id);
840
+ this._graph.mergeNodeAttributes(parsed.id, {
841
+ title: parsed.title,
842
+ description: parsed.description,
843
+ status: parsed.status,
844
+ priority: parsed.priority,
845
+ tags: parsed.tags,
846
+ dueDate: parsed.dueDate,
847
+ estimate: parsed.estimate,
848
+ completedAt: parsed.completedAt,
849
+ embedding,
850
+ attachments: parsed.attachments,
851
+ updatedAt: now,
852
+ createdAt: existing.createdAt,
853
+ version: parsed.version ?? existing.version + 1,
854
+ ...(parsed.createdBy != null ? { createdBy: parsed.createdBy } : {}),
855
+ ...(parsed.updatedBy != null ? { updatedBy: parsed.updatedBy } : {}),
856
+ });
857
+ }
858
+ else {
859
+ this._graph.addNode(parsed.id, {
860
+ title: parsed.title,
861
+ description: parsed.description,
862
+ status: parsed.status,
863
+ priority: parsed.priority,
864
+ tags: parsed.tags,
865
+ dueDate: parsed.dueDate,
866
+ estimate: parsed.estimate,
867
+ completedAt: parsed.completedAt,
868
+ embedding,
869
+ attachments: parsed.attachments ?? [],
870
+ createdAt: parsed.createdAt ?? now,
871
+ updatedAt: now,
872
+ version: parsed.version ?? 1,
873
+ createdBy: parsed.createdBy ?? undefined,
874
+ updatedBy: parsed.updatedBy ?? undefined,
875
+ });
876
+ }
877
+ this._bm25Index.updateDocument(parsed.id, this._graph.getNodeAttributes(parsed.id));
878
+ this.syncRelationsFromFile(parsed.id, parsed.relations);
879
+ this.ctx.markDirty();
880
+ this.ctx.emit(exists ? 'task:updated' : 'task:created', { projectId: this.ctx.projectId, taskId: parsed.id });
881
+ }
882
+ updateDescriptionFromFile(taskId, description) {
883
+ if (!this._graph.hasNode(taskId) || isProxy(this._graph, taskId))
884
+ return;
885
+ this._graph.setNodeAttribute(taskId, 'description', description);
886
+ this._graph.setNodeAttribute(taskId, 'updatedAt', Date.now());
887
+ this._graph.setNodeAttribute(taskId, 'version', (this._graph.getNodeAttribute(taskId, 'version') ?? 0) + 1);
888
+ this.ctx.markDirty();
889
+ this.ctx.emit('task:updated', { projectId: this.ctx.projectId, taskId });
890
+ }
891
+ deleteFromFile(taskId) {
892
+ if (!this._graph.hasNode(taskId))
893
+ return;
894
+ if (isProxy(this._graph, taskId))
895
+ return;
896
+ this._bm25Index.removeDocument(taskId);
897
+ deleteTask(this._graph, taskId);
898
+ if (this.knowledgeGraph) {
899
+ const pId = `@tasks::${taskId}`;
900
+ if (this.knowledgeGraph.hasNode(pId))
901
+ this.knowledgeGraph.dropNode(pId);
902
+ }
903
+ this.ctx.markDirty();
904
+ this.ctx.emit('task:deleted', { projectId: this.ctx.projectId, taskId });
905
+ }
906
+ syncRelationsFromFile(taskId, desired) {
907
+ const current = [];
908
+ this._graph.forEachOutEdge(taskId, (_edge, attrs, _src, target) => {
909
+ const proxy = this._graph.hasNode(target) ? this._graph.getNodeAttribute(target, 'proxyFor') : undefined;
910
+ if (proxy) {
911
+ current.push({ to: proxy.nodeId, kind: attrs.kind, graph: proxy.graph });
912
+ }
913
+ else {
914
+ current.push({ to: target, kind: attrs.kind });
915
+ }
916
+ });
917
+ const diff = (0, file_import_1.diffRelations)(current, desired);
918
+ for (const rel of diff.toRemove) {
919
+ if (rel.graph) {
920
+ deleteCrossRelation(this._graph, taskId, rel.graph, rel.to);
921
+ if (rel.graph === 'knowledge' && this.knowledgeGraph) {
922
+ deleteMirrorFromKnowledgeGraph(this.knowledgeGraph, taskId, rel.to);
923
+ }
924
+ }
925
+ else {
926
+ deleteTaskRelation(this._graph, taskId, rel.to);
927
+ }
928
+ }
929
+ for (const rel of diff.toAdd) {
930
+ if (rel.graph) {
931
+ const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, rel.graph);
932
+ createCrossRelation(this._graph, taskId, rel.graph, rel.to, rel.kind, extGraph);
933
+ if (rel.graph === 'knowledge' && this.knowledgeGraph) {
934
+ createMirrorInKnowledgeGraph(this.knowledgeGraph, taskId, rel.to, rel.kind);
935
+ }
936
+ }
937
+ else {
938
+ createTaskRelation(this._graph, taskId, rel.to, rel.kind);
939
+ }
940
+ }
941
+ }
942
+ // -- Read --
943
+ getTask(taskId) {
944
+ return getTask(this._graph, taskId);
945
+ }
946
+ listTasks(opts) {
947
+ return listTasks(this._graph, opts);
948
+ }
949
+ async searchTasks(query, opts) {
950
+ const embedding = opts?.searchMode === 'keyword' ? [] : await this.embedFns.query(query);
951
+ return (0, tasks_1.searchTasks)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
952
+ }
953
+ listRelations(taskId) {
954
+ return listTaskRelations(this._graph, taskId, this.ext);
955
+ }
956
+ findLinkedTasks(targetGraph, targetNodeId, kind, projectId) {
957
+ return findLinkedTasks(this._graph, targetGraph, targetNodeId, kind, projectId || this.ctx.projectId);
958
+ }
959
+ }
960
+ exports.TaskGraphManager = TaskGraphManager;