@vheins/local-memory-mcp 0.13.2 → 0.14.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.id.md +94 -0
- package/README.md +19 -24
- package/dist/{chunk-O4B6AFYU.js → chunk-XSGQS2MU.js} +572 -61
- package/dist/dashboard/public/assets/index-C1OTQhII.js +87 -0
- package/dist/dashboard/public/assets/index-CUg8rZCA.css +1 -0
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/server.js +34 -15
- package/dist/mcp/server.js +102 -393
- package/package.json +3 -2
- package/dist/dashboard/public/assets/index-CbVS6V5d.js +0 -87
- package/dist/dashboard/public/assets/index-d4GwN8c5.css +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
CAPABILITIES,
|
|
4
|
-
ClaimListSchema,
|
|
5
|
-
ClaimReleaseSchema,
|
|
6
|
-
HandoffCreateSchema,
|
|
7
|
-
HandoffListSchema,
|
|
8
|
-
HandoffUpdateSchema,
|
|
9
4
|
LOG_LEVEL_VALUES,
|
|
10
5
|
MCP_PROTOCOL_VERSION,
|
|
11
6
|
MemoryAcknowledgeSchema,
|
|
@@ -25,18 +20,19 @@ import {
|
|
|
25
20
|
StandardStoreSchema,
|
|
26
21
|
StandardUpdateSchema,
|
|
27
22
|
TOOL_DEFINITIONS,
|
|
28
|
-
TaskClaimSchema,
|
|
29
23
|
TaskCreateInteractiveSchema,
|
|
30
24
|
TaskCreateSchema,
|
|
31
25
|
TaskDeleteSchema,
|
|
32
26
|
TaskGetSchema,
|
|
33
27
|
TaskListSchema,
|
|
28
|
+
TaskSearchSchema,
|
|
34
29
|
TaskUpdateSchema,
|
|
35
30
|
addLogSink,
|
|
36
31
|
buildStandardVectorText,
|
|
37
32
|
completePromptArgument,
|
|
38
33
|
completeResourceArgument,
|
|
39
34
|
createFileSink,
|
|
35
|
+
createMcpResponse,
|
|
40
36
|
createSessionContext,
|
|
41
37
|
decodeCursor,
|
|
42
38
|
encodeCursor,
|
|
@@ -44,7 +40,14 @@ import {
|
|
|
44
40
|
findContainingRoot,
|
|
45
41
|
getFilesystemRoots,
|
|
46
42
|
getLogLevel,
|
|
43
|
+
getPrimaryTextContent,
|
|
47
44
|
getPrompt,
|
|
45
|
+
handleClaimList,
|
|
46
|
+
handleClaimRelease,
|
|
47
|
+
handleHandoffCreate,
|
|
48
|
+
handleHandoffList,
|
|
49
|
+
handleHandoffUpdate,
|
|
50
|
+
handleTaskClaim,
|
|
48
51
|
inferRepoFromSession,
|
|
49
52
|
isPathWithinRoots,
|
|
50
53
|
listPrompts,
|
|
@@ -57,7 +60,7 @@ import {
|
|
|
57
60
|
toContextSlug,
|
|
58
61
|
updateSessionFromInitialize,
|
|
59
62
|
updateSessionRoots
|
|
60
|
-
} from "../chunk-
|
|
63
|
+
} from "../chunk-XSGQS2MU.js";
|
|
61
64
|
|
|
62
65
|
// src/mcp/server.ts
|
|
63
66
|
import readline from "readline";
|
|
@@ -186,111 +189,6 @@ function invalidCompletionParams(message) {
|
|
|
186
189
|
|
|
187
190
|
// src/mcp/tools/memory.store.ts
|
|
188
191
|
import { randomUUID } from "crypto";
|
|
189
|
-
|
|
190
|
-
// src/mcp/utils/mcp-response.ts
|
|
191
|
-
import { z } from "zod";
|
|
192
|
-
var McpAnnotationsSchema = z.object({
|
|
193
|
-
audience: z.array(z.enum(["user", "assistant"])).optional(),
|
|
194
|
-
priority: z.number().min(0).max(1).optional(),
|
|
195
|
-
lastModified: z.string().optional()
|
|
196
|
-
}).strict().optional();
|
|
197
|
-
var McpContentSchema = z.discriminatedUnion("type", [
|
|
198
|
-
z.object({
|
|
199
|
-
type: z.literal("text"),
|
|
200
|
-
text: z.string(),
|
|
201
|
-
annotations: McpAnnotationsSchema
|
|
202
|
-
}),
|
|
203
|
-
z.object({
|
|
204
|
-
type: z.literal("image"),
|
|
205
|
-
data: z.string(),
|
|
206
|
-
mimeType: z.string(),
|
|
207
|
-
annotations: McpAnnotationsSchema
|
|
208
|
-
}),
|
|
209
|
-
z.object({
|
|
210
|
-
type: z.literal("resource"),
|
|
211
|
-
resource: z.object({
|
|
212
|
-
uri: z.string(),
|
|
213
|
-
mimeType: z.string().optional(),
|
|
214
|
-
text: z.string().optional(),
|
|
215
|
-
annotations: McpAnnotationsSchema
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
]);
|
|
219
|
-
function createMcpResponse(data, summary, options) {
|
|
220
|
-
const { structuredContentPathHint, contentSummary, includeSerializedStructuredContent = false } = options || {};
|
|
221
|
-
void includeSerializedStructuredContent;
|
|
222
|
-
let finalData = data;
|
|
223
|
-
if (data && typeof data === "object") {
|
|
224
|
-
const cloned = JSON.parse(JSON.stringify(data));
|
|
225
|
-
finalData = cloned;
|
|
226
|
-
const arrayKeys = ["results", "tasks", "memories", "items"];
|
|
227
|
-
let foundArray = false;
|
|
228
|
-
for (const key of arrayKeys) {
|
|
229
|
-
const value = cloned[key];
|
|
230
|
-
if (Array.isArray(value)) {
|
|
231
|
-
cloned[key] = value.map(
|
|
232
|
-
(item) => pruneMetadata(item)
|
|
233
|
-
);
|
|
234
|
-
foundArray = true;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (Array.isArray(cloned)) {
|
|
238
|
-
finalData = cloned.map((item) => pruneMetadata(item));
|
|
239
|
-
} else if (!foundArray) {
|
|
240
|
-
finalData = pruneMetadata(cloned);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
const content = [];
|
|
244
|
-
if (contentSummary && contentSummary.trim().length > 0) {
|
|
245
|
-
content.push({
|
|
246
|
-
type: "text",
|
|
247
|
-
text: contentSummary.trim()
|
|
248
|
-
});
|
|
249
|
-
} else if (summary && summary.trim().length > 0) {
|
|
250
|
-
const pointerText = structuredContentPathHint ? `Read structuredContent.${structuredContentPathHint} for details.` : `Read structuredContent for machine-readable results.`;
|
|
251
|
-
content.push({
|
|
252
|
-
type: "text",
|
|
253
|
-
text: `${summary.trim()} ${pointerText}`
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
const response = {
|
|
257
|
-
structuredContent: finalData,
|
|
258
|
-
isError: false
|
|
259
|
-
};
|
|
260
|
-
if (includeSerializedStructuredContent === false) {
|
|
261
|
-
delete response.structuredContent;
|
|
262
|
-
}
|
|
263
|
-
response.content = content;
|
|
264
|
-
return response;
|
|
265
|
-
}
|
|
266
|
-
function pruneMetadata(item) {
|
|
267
|
-
if (!item || typeof item !== "object") return item;
|
|
268
|
-
const pruned = { ...item };
|
|
269
|
-
const toRemove = [
|
|
270
|
-
"hit_count",
|
|
271
|
-
"recall_count",
|
|
272
|
-
"last_used_at",
|
|
273
|
-
"expires_at",
|
|
274
|
-
"agent",
|
|
275
|
-
"role",
|
|
276
|
-
"model",
|
|
277
|
-
"recall_rate",
|
|
278
|
-
"vector_version",
|
|
279
|
-
"similarity"
|
|
280
|
-
// Similarity is useful but adds noise if many results
|
|
281
|
-
];
|
|
282
|
-
for (const field of toRemove) {
|
|
283
|
-
delete pruned[field];
|
|
284
|
-
}
|
|
285
|
-
return pruned;
|
|
286
|
-
}
|
|
287
|
-
function getPrimaryTextContent(response) {
|
|
288
|
-
if (!Array.isArray(response.content)) return "";
|
|
289
|
-
const textItem = response.content.find((item) => item.type === "text");
|
|
290
|
-
return textItem?.type === "text" ? textItem.text : "";
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// src/mcp/tools/memory.store.ts
|
|
294
192
|
function hasMetadataLikeTitle(title) {
|
|
295
193
|
const normalized = title.trim();
|
|
296
194
|
return /^\[[^\]]{0,200}(agent:|role:|model:|\d{4}-\d{2}-\d{2}|source_)[^\]]*\]/i.test(normalized);
|
|
@@ -311,9 +209,10 @@ async function handleMemoryStore(params, db2, vectors2) {
|
|
|
311
209
|
);
|
|
312
210
|
}
|
|
313
211
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
314
|
-
const
|
|
212
|
+
const createdAtTime = new Date(now).getTime();
|
|
213
|
+
const expires_at = validated.ttlDays != null ? new Date(createdAtTime + validated.ttlDays * 864e5).toISOString() : null;
|
|
315
214
|
if (!validated.supersedes && validated.type !== "task_archive") {
|
|
316
|
-
const conflict = await db2.
|
|
215
|
+
const conflict = await db2.memoryVectors.checkConflicts(
|
|
317
216
|
validated.content,
|
|
318
217
|
validated.scope.repo,
|
|
319
218
|
validated.type,
|
|
@@ -508,7 +407,7 @@ async function handleMemorySearch(params, db2, vectors2) {
|
|
|
508
407
|
const validated = MemorySearchSchema.parse(params);
|
|
509
408
|
const searchQuery = expandQuery(validated.query, validated.prompt);
|
|
510
409
|
const fetchLimit = (validated.offset + validated.limit) * 3;
|
|
511
|
-
const similarityResults = db2.
|
|
410
|
+
const similarityResults = db2.memoryVectors.searchBySimilarity(
|
|
512
411
|
searchQuery,
|
|
513
412
|
validated.repo,
|
|
514
413
|
fetchLimit,
|
|
@@ -824,7 +723,7 @@ function deriveTaskStatusTimestamps(status, now, existingTask) {
|
|
|
824
723
|
async function archiveTaskToMemory(taskId, repo, storage, vectors2) {
|
|
825
724
|
const task = storage.tasks.getTaskById(taskId);
|
|
826
725
|
if (!task) return;
|
|
827
|
-
const comments = storage.
|
|
726
|
+
const comments = storage.taskComments.getTaskCommentsByTaskId(taskId);
|
|
828
727
|
let content = `Task: [${task.task_code}] ${task.title}
|
|
829
728
|
`;
|
|
830
729
|
content += `Phase: ${task.phase}
|
|
@@ -940,7 +839,7 @@ async function handleTaskCreate(args, storage) {
|
|
|
940
839
|
const createdTasks = [];
|
|
941
840
|
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
942
841
|
const codesInRequest = /* @__PURE__ */ new Set();
|
|
943
|
-
const initialStats = storage.
|
|
842
|
+
const initialStats = storage.taskStats.getTaskStats(repo);
|
|
944
843
|
let pendingInRequestCount = 0;
|
|
945
844
|
for (const taskData of bulkTasks) {
|
|
946
845
|
if (codesInRequest.has(taskData.task_code)) {
|
|
@@ -988,6 +887,8 @@ async function handleTaskCreate(args, storage) {
|
|
|
988
887
|
canceled_at: statusTimestamps2.canceled_at,
|
|
989
888
|
est_tokens: taskData.est_tokens ?? 0,
|
|
990
889
|
tags,
|
|
890
|
+
commit_id: null,
|
|
891
|
+
changed_files: [],
|
|
991
892
|
metadata: taskData.metadata || {},
|
|
992
893
|
parent_id: resolveParentId(taskData.parent_id, repo, storage),
|
|
993
894
|
depends_on: taskData.depends_on || null
|
|
@@ -1030,9 +931,11 @@ async function handleTaskCreate(args, storage) {
|
|
|
1030
931
|
throw new Error("New tasks must be created with status 'backlog' or 'pending'.");
|
|
1031
932
|
}
|
|
1032
933
|
if (status === "pending") {
|
|
1033
|
-
const stats = storage.
|
|
934
|
+
const stats = storage.taskStats.getTaskStats(repo);
|
|
1034
935
|
if (stats.todo >= 10) {
|
|
1035
|
-
throw new Error(
|
|
936
|
+
throw new Error(
|
|
937
|
+
`Cannot create task as 'pending'. Maximum of 10 pending tasks reached. Please use status 'backlog' for new tasks instead.`
|
|
938
|
+
);
|
|
1036
939
|
}
|
|
1037
940
|
}
|
|
1038
941
|
const taskId = randomUUID2();
|
|
@@ -1062,6 +965,8 @@ async function handleTaskCreate(args, storage) {
|
|
|
1062
965
|
canceled_at: statusTimestamps.canceled_at,
|
|
1063
966
|
est_tokens: est_tokens ?? 0,
|
|
1064
967
|
tags: finalTags,
|
|
968
|
+
commit_id: null,
|
|
969
|
+
changed_files: [],
|
|
1065
970
|
metadata: metadata || {},
|
|
1066
971
|
parent_id: resolveParentId(parent_id, repo, storage),
|
|
1067
972
|
depends_on: depends_on || null
|
|
@@ -1255,7 +1160,7 @@ async function handleTaskUpdate(args, storage, vectors2) {
|
|
|
1255
1160
|
finalUpdates.in_progress_at = now;
|
|
1256
1161
|
storage.tasks.updateTask(targetId, finalUpdates);
|
|
1257
1162
|
if (comment !== void 0 || isStatusChanging) {
|
|
1258
|
-
storage.
|
|
1163
|
+
storage.taskComments.insertTaskComment({
|
|
1259
1164
|
id: randomUUID2(),
|
|
1260
1165
|
task_id: targetId,
|
|
1261
1166
|
repo,
|
|
@@ -1603,21 +1508,29 @@ async function handleMemoryDelete(params, db2, vectors2, onProgress) {
|
|
|
1603
1508
|
let lastRepo = repo || "unknown";
|
|
1604
1509
|
const total = targetIds.length;
|
|
1605
1510
|
let progress = 0;
|
|
1511
|
+
const existingMemories = db2.memories.getByIds(targetIds);
|
|
1512
|
+
const existingMap = new Map(existingMemories.map((m) => [m.id, m]));
|
|
1513
|
+
const validIdsToDelete = [];
|
|
1606
1514
|
for (const targetId of targetIds) {
|
|
1607
|
-
|
|
1608
|
-
onProgress(progress, total);
|
|
1609
|
-
}
|
|
1610
|
-
const existing = db2.memories.getById(targetId);
|
|
1515
|
+
const existing = existingMap.get(targetId);
|
|
1611
1516
|
if (existing) {
|
|
1612
1517
|
lastRepo = existing.scope.repo;
|
|
1613
1518
|
deletedCodes.push(existing.code || existing.id);
|
|
1614
|
-
|
|
1615
|
-
await vectors2.remove(targetId);
|
|
1616
|
-
deletedCount++;
|
|
1519
|
+
validIdsToDelete.push(targetId);
|
|
1617
1520
|
} else if (id) {
|
|
1618
1521
|
throw new Error(`Memory not found: ${targetId}`);
|
|
1619
1522
|
}
|
|
1620
|
-
|
|
1523
|
+
}
|
|
1524
|
+
if (validIdsToDelete.length > 0) {
|
|
1525
|
+
db2.memoryArchives.bulkDeleteMemories(validIdsToDelete);
|
|
1526
|
+
for (const validId of validIdsToDelete) {
|
|
1527
|
+
if (onProgress) {
|
|
1528
|
+
onProgress(progress, total);
|
|
1529
|
+
}
|
|
1530
|
+
await vectors2.remove(validId);
|
|
1531
|
+
progress++;
|
|
1532
|
+
}
|
|
1533
|
+
deletedCount = validIdsToDelete.length;
|
|
1621
1534
|
}
|
|
1622
1535
|
if (onProgress) {
|
|
1623
1536
|
onProgress(progress, total);
|
|
@@ -1714,266 +1627,6 @@ async function handleMemoryDetail(args, storage) {
|
|
|
1714
1627
|
});
|
|
1715
1628
|
}
|
|
1716
1629
|
|
|
1717
|
-
// src/mcp/tools/handoff.manage.ts
|
|
1718
|
-
function buildHandoffListSummary(repo, count, status, fromAgent, toAgent) {
|
|
1719
|
-
const parts = [`Found ${count} handoff${count === 1 ? "" : "s"} in repo "${repo}".`];
|
|
1720
|
-
if (status) {
|
|
1721
|
-
parts.push(`Status filter: ${status}.`);
|
|
1722
|
-
}
|
|
1723
|
-
if (fromAgent) {
|
|
1724
|
-
parts.push(`From agent: ${fromAgent}.`);
|
|
1725
|
-
}
|
|
1726
|
-
if (toAgent) {
|
|
1727
|
-
parts.push(`To agent: ${toAgent}.`);
|
|
1728
|
-
}
|
|
1729
|
-
return parts.join("\n");
|
|
1730
|
-
}
|
|
1731
|
-
function buildClaimListSummary(repo, count, agent, activeOnly) {
|
|
1732
|
-
const parts = [`Found ${count} claim${count === 1 ? "" : "s"} in repo "${repo}".`];
|
|
1733
|
-
if (agent) {
|
|
1734
|
-
parts.push(`Agent filter: ${agent}.`);
|
|
1735
|
-
}
|
|
1736
|
-
if (activeOnly) {
|
|
1737
|
-
parts.push("Showing active claims only.");
|
|
1738
|
-
}
|
|
1739
|
-
return parts.join("\n");
|
|
1740
|
-
}
|
|
1741
|
-
async function handleHandoffCreate(args, storage) {
|
|
1742
|
-
const validated = HandoffCreateSchema.parse(args);
|
|
1743
|
-
const { repo, from_agent, to_agent, task_id, task_code, summary, context, expires_at, structured } = validated;
|
|
1744
|
-
let resolvedTaskId = task_id ?? null;
|
|
1745
|
-
if (!resolvedTaskId && task_code) {
|
|
1746
|
-
const task = storage.tasks.getTaskByCode(repo, task_code);
|
|
1747
|
-
if (!task) {
|
|
1748
|
-
throw new Error(`Task not found: ${task_code} in repo ${repo}`);
|
|
1749
|
-
}
|
|
1750
|
-
resolvedTaskId = task.id;
|
|
1751
|
-
}
|
|
1752
|
-
const handoff = storage.handoffs.createHandoff({
|
|
1753
|
-
repo,
|
|
1754
|
-
from_agent,
|
|
1755
|
-
to_agent,
|
|
1756
|
-
task_id: resolvedTaskId,
|
|
1757
|
-
summary,
|
|
1758
|
-
context,
|
|
1759
|
-
expires_at
|
|
1760
|
-
});
|
|
1761
|
-
const contentSummary = [
|
|
1762
|
-
`Created handoff ${handoff.id}.`,
|
|
1763
|
-
`Repo: ${handoff.repo}`,
|
|
1764
|
-
`From: ${handoff.from_agent}`,
|
|
1765
|
-
`To: ${handoff.to_agent || "unassigned"}`,
|
|
1766
|
-
`Status: ${handoff.status}`,
|
|
1767
|
-
`Task ID: ${handoff.task_id || "-"}`,
|
|
1768
|
-
`Summary: ${handoff.summary}`
|
|
1769
|
-
].join("\n");
|
|
1770
|
-
return createMcpResponse(handoff, contentSummary, {
|
|
1771
|
-
contentSummary,
|
|
1772
|
-
includeSerializedStructuredContent: structured
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
async function handleHandoffList(args, storage) {
|
|
1776
|
-
const validated = HandoffListSchema.parse(args);
|
|
1777
|
-
const { repo, status, from_agent, to_agent, limit, offset, structured } = validated;
|
|
1778
|
-
const handoffs = storage.handoffs.listHandoffs({
|
|
1779
|
-
repo,
|
|
1780
|
-
status,
|
|
1781
|
-
from_agent,
|
|
1782
|
-
to_agent,
|
|
1783
|
-
limit,
|
|
1784
|
-
offset
|
|
1785
|
-
});
|
|
1786
|
-
const COLUMNS = [
|
|
1787
|
-
"id",
|
|
1788
|
-
"from_agent",
|
|
1789
|
-
"to_agent",
|
|
1790
|
-
"task_id",
|
|
1791
|
-
"task_code",
|
|
1792
|
-
"status",
|
|
1793
|
-
"created_at",
|
|
1794
|
-
"updated_at",
|
|
1795
|
-
"expires_at",
|
|
1796
|
-
"summary",
|
|
1797
|
-
"context"
|
|
1798
|
-
];
|
|
1799
|
-
const rows = handoffs.map((handoff) => [
|
|
1800
|
-
handoff.id,
|
|
1801
|
-
handoff.from_agent,
|
|
1802
|
-
handoff.to_agent,
|
|
1803
|
-
handoff.task_id,
|
|
1804
|
-
handoff.task_code ?? null,
|
|
1805
|
-
handoff.status,
|
|
1806
|
-
handoff.created_at,
|
|
1807
|
-
handoff.updated_at,
|
|
1808
|
-
handoff.expires_at,
|
|
1809
|
-
handoff.summary,
|
|
1810
|
-
handoff.context
|
|
1811
|
-
]);
|
|
1812
|
-
const structuredData = {
|
|
1813
|
-
schema: "handoff-list",
|
|
1814
|
-
handoffs: {
|
|
1815
|
-
columns: [...COLUMNS],
|
|
1816
|
-
rows
|
|
1817
|
-
},
|
|
1818
|
-
count: rows.length,
|
|
1819
|
-
offset
|
|
1820
|
-
};
|
|
1821
|
-
const contentSummary = buildHandoffListSummary(repo, rows.length, status, from_agent, to_agent);
|
|
1822
|
-
return createMcpResponse(structuredData, contentSummary, {
|
|
1823
|
-
contentSummary,
|
|
1824
|
-
includeSerializedStructuredContent: structured
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
async function handleHandoffUpdate(args, storage) {
|
|
1828
|
-
const validated = HandoffUpdateSchema.parse(args);
|
|
1829
|
-
const { id, status, structured } = validated;
|
|
1830
|
-
const existing = storage.handoffs.getHandoffById(id);
|
|
1831
|
-
if (!existing) {
|
|
1832
|
-
throw new Error(`Handoff not found: ${id}`);
|
|
1833
|
-
}
|
|
1834
|
-
const success = storage.handoffs.updateHandoffStatus(id, status);
|
|
1835
|
-
if (!success) {
|
|
1836
|
-
throw new Error(`Failed to update handoff: ${id}`);
|
|
1837
|
-
}
|
|
1838
|
-
const updated = storage.handoffs.getHandoffById(id);
|
|
1839
|
-
const result = {
|
|
1840
|
-
success,
|
|
1841
|
-
id,
|
|
1842
|
-
status,
|
|
1843
|
-
handoff: updated
|
|
1844
|
-
};
|
|
1845
|
-
const contentSummary = [`Updated handoff ${id}.`, `Status: ${status}`].join("\n");
|
|
1846
|
-
return createMcpResponse(result, contentSummary, {
|
|
1847
|
-
contentSummary,
|
|
1848
|
-
includeSerializedStructuredContent: structured
|
|
1849
|
-
});
|
|
1850
|
-
}
|
|
1851
|
-
async function handleTaskClaim(args, storage) {
|
|
1852
|
-
const validated = TaskClaimSchema.parse(args);
|
|
1853
|
-
const { repo, task_id, task_code, agent, role, metadata, structured } = validated;
|
|
1854
|
-
let taskId = task_id;
|
|
1855
|
-
let resolvedTaskCode;
|
|
1856
|
-
if (taskId) {
|
|
1857
|
-
const task = storage.tasks.getTaskById(taskId);
|
|
1858
|
-
if (!task || task.repo !== repo) {
|
|
1859
|
-
throw new Error(`Task not found: ${taskId} in repo ${repo}`);
|
|
1860
|
-
}
|
|
1861
|
-
resolvedTaskCode = task.task_code;
|
|
1862
|
-
} else if (task_code) {
|
|
1863
|
-
const task = storage.tasks.getTaskByCode(repo, task_code);
|
|
1864
|
-
if (!task) {
|
|
1865
|
-
throw new Error(`Task not found: ${task_code} in repo ${repo}`);
|
|
1866
|
-
}
|
|
1867
|
-
taskId = task.id;
|
|
1868
|
-
resolvedTaskCode = task.task_code;
|
|
1869
|
-
} else {
|
|
1870
|
-
throw new Error("Either task_id or task_code must be provided");
|
|
1871
|
-
}
|
|
1872
|
-
const claim = storage.handoffs.claimTask({
|
|
1873
|
-
repo,
|
|
1874
|
-
task_id: taskId,
|
|
1875
|
-
agent,
|
|
1876
|
-
role,
|
|
1877
|
-
metadata
|
|
1878
|
-
});
|
|
1879
|
-
const responseData = {
|
|
1880
|
-
...claim,
|
|
1881
|
-
task_code: resolvedTaskCode
|
|
1882
|
-
};
|
|
1883
|
-
const contentSummary = [
|
|
1884
|
-
`Claimed task ${resolvedTaskCode || claim.task_id}.`,
|
|
1885
|
-
`Repo: ${claim.repo}`,
|
|
1886
|
-
`Task ID: ${claim.task_id}`,
|
|
1887
|
-
`Agent: ${claim.agent}`,
|
|
1888
|
-
`Role: ${claim.role}`,
|
|
1889
|
-
`Claimed At: ${claim.claimed_at}`
|
|
1890
|
-
].join("\n");
|
|
1891
|
-
const response = createMcpResponse(responseData, contentSummary, {
|
|
1892
|
-
contentSummary,
|
|
1893
|
-
includeSerializedStructuredContent: structured
|
|
1894
|
-
});
|
|
1895
|
-
if (structured) {
|
|
1896
|
-
response.structuredContent = responseData;
|
|
1897
|
-
}
|
|
1898
|
-
return response;
|
|
1899
|
-
}
|
|
1900
|
-
async function handleClaimList(args, storage) {
|
|
1901
|
-
const validated = ClaimListSchema.parse(args);
|
|
1902
|
-
const { repo, agent, active_only, limit, offset, structured } = validated;
|
|
1903
|
-
const claims = storage.handoffs.listClaims({
|
|
1904
|
-
repo,
|
|
1905
|
-
agent,
|
|
1906
|
-
active_only,
|
|
1907
|
-
limit,
|
|
1908
|
-
offset
|
|
1909
|
-
});
|
|
1910
|
-
const COLUMNS = ["id", "task_id", "task_code", "agent", "role", "claimed_at", "released_at", "metadata"];
|
|
1911
|
-
const rows = claims.map((claim) => [
|
|
1912
|
-
claim.id,
|
|
1913
|
-
claim.task_id,
|
|
1914
|
-
claim.task_code ?? null,
|
|
1915
|
-
claim.agent,
|
|
1916
|
-
claim.role,
|
|
1917
|
-
claim.claimed_at,
|
|
1918
|
-
claim.released_at,
|
|
1919
|
-
claim.metadata
|
|
1920
|
-
]);
|
|
1921
|
-
const structuredData = {
|
|
1922
|
-
schema: "claim-list",
|
|
1923
|
-
claims: {
|
|
1924
|
-
columns: [...COLUMNS],
|
|
1925
|
-
rows
|
|
1926
|
-
},
|
|
1927
|
-
count: rows.length,
|
|
1928
|
-
offset
|
|
1929
|
-
};
|
|
1930
|
-
const contentSummary = buildClaimListSummary(repo, rows.length, agent, active_only);
|
|
1931
|
-
return createMcpResponse(structuredData, contentSummary, {
|
|
1932
|
-
contentSummary,
|
|
1933
|
-
includeSerializedStructuredContent: structured
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
1936
|
-
async function handleClaimRelease(args, storage) {
|
|
1937
|
-
const validated = ClaimReleaseSchema.parse(args);
|
|
1938
|
-
const { repo, task_id, task_code, agent, structured } = validated;
|
|
1939
|
-
let resolvedTaskId = task_id;
|
|
1940
|
-
let resolvedTaskCode = task_code ?? null;
|
|
1941
|
-
if (resolvedTaskId) {
|
|
1942
|
-
const task = storage.tasks.getTaskById(resolvedTaskId);
|
|
1943
|
-
if (!task || task.repo !== repo) {
|
|
1944
|
-
throw new Error(`Task not found: ${resolvedTaskId} in repo ${repo}`);
|
|
1945
|
-
}
|
|
1946
|
-
resolvedTaskCode = task.task_code;
|
|
1947
|
-
} else if (task_code) {
|
|
1948
|
-
const task = storage.tasks.getTaskByCode(repo, task_code);
|
|
1949
|
-
if (!task) {
|
|
1950
|
-
throw new Error(`Task not found: ${task_code} in repo ${repo}`);
|
|
1951
|
-
}
|
|
1952
|
-
resolvedTaskId = task.id;
|
|
1953
|
-
resolvedTaskCode = task.task_code;
|
|
1954
|
-
}
|
|
1955
|
-
const success = storage.handoffs.releaseClaim(resolvedTaskId, agent);
|
|
1956
|
-
if (!success) {
|
|
1957
|
-
throw new Error(`No active claim found for task ${resolvedTaskCode || resolvedTaskId}`);
|
|
1958
|
-
}
|
|
1959
|
-
const result = {
|
|
1960
|
-
success,
|
|
1961
|
-
repo,
|
|
1962
|
-
task_id: resolvedTaskId,
|
|
1963
|
-
task_code: resolvedTaskCode,
|
|
1964
|
-
agent: agent ?? null
|
|
1965
|
-
};
|
|
1966
|
-
const contentSummary = [
|
|
1967
|
-
`Released claim for task ${resolvedTaskCode || resolvedTaskId}.`,
|
|
1968
|
-
`Repo: ${repo}`,
|
|
1969
|
-
agent ? `Agent: ${agent}` : "Agent: any active claimant"
|
|
1970
|
-
].join("\n");
|
|
1971
|
-
return createMcpResponse(result, contentSummary, {
|
|
1972
|
-
contentSummary,
|
|
1973
|
-
includeSerializedStructuredContent: structured
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
1630
|
// src/mcp/tools/standard.store.ts
|
|
1978
1631
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1979
1632
|
function generateShortCode2() {
|
|
@@ -2451,7 +2104,7 @@ async function handleTaskGet(args, storage) {
|
|
|
2451
2104
|
if (!task) {
|
|
2452
2105
|
throw new Error(`Task not found: ${id || task_code} in repo ${repo}`);
|
|
2453
2106
|
}
|
|
2454
|
-
const comments = storage.
|
|
2107
|
+
const comments = storage.taskComments.getTaskCommentsByTaskId(task.id);
|
|
2455
2108
|
let contentSummary;
|
|
2456
2109
|
if (!isStructuredRequest) {
|
|
2457
2110
|
const lines = [
|
|
@@ -2488,6 +2141,62 @@ async function handleTaskGet(args, storage) {
|
|
|
2488
2141
|
});
|
|
2489
2142
|
}
|
|
2490
2143
|
|
|
2144
|
+
// src/mcp/tools/task.search.ts
|
|
2145
|
+
async function handleTaskSearch(args, storage) {
|
|
2146
|
+
const validated = TaskSearchSchema.parse(args);
|
|
2147
|
+
const { repo, query, status, limit, offset, structured: isStructuredRequest = false, phase, priority } = validated;
|
|
2148
|
+
let tasks;
|
|
2149
|
+
if (status) {
|
|
2150
|
+
const statuses = status.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2151
|
+
if (statuses.length > 1) {
|
|
2152
|
+
tasks = storage.tasks.getTasksByMultipleStatuses(repo, statuses, void 0, void 0, query);
|
|
2153
|
+
} else {
|
|
2154
|
+
tasks = storage.tasks.getTasksByRepo(repo, status, void 0, void 0, query);
|
|
2155
|
+
}
|
|
2156
|
+
} else {
|
|
2157
|
+
tasks = storage.tasks.getTasksByRepo(repo, void 0, void 0, void 0, query);
|
|
2158
|
+
}
|
|
2159
|
+
if (phase) {
|
|
2160
|
+
const phaseLower = phase.toLowerCase();
|
|
2161
|
+
tasks = tasks.filter((t) => t.phase && t.phase.toLowerCase() === phaseLower);
|
|
2162
|
+
}
|
|
2163
|
+
if (priority !== void 0) {
|
|
2164
|
+
tasks = tasks.filter((t) => t.priority === priority);
|
|
2165
|
+
}
|
|
2166
|
+
const total = tasks.length;
|
|
2167
|
+
const paginated = tasks.slice(offset, offset + limit);
|
|
2168
|
+
const COLUMNS = ["id", "task_code", "title", "status", "priority", "updated_at", "phase"];
|
|
2169
|
+
const rows = paginated.map((t) => [t.id, t.task_code, t.title, t.status, t.priority, t.updated_at, t.phase]);
|
|
2170
|
+
const structuredData = {
|
|
2171
|
+
schema: "task-search",
|
|
2172
|
+
query,
|
|
2173
|
+
count: paginated.length,
|
|
2174
|
+
total,
|
|
2175
|
+
offset,
|
|
2176
|
+
limit,
|
|
2177
|
+
results: {
|
|
2178
|
+
columns: [...COLUMNS],
|
|
2179
|
+
rows
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
let contentSummary;
|
|
2183
|
+
if (!isStructuredRequest) {
|
|
2184
|
+
contentSummary = paginated.length > 0 ? `Found ${total} tasks matching "${query}" in repo "${repo}". Use task-detail to fetch full task content.` : `No tasks found for "${query}" in repo "${repo}".`;
|
|
2185
|
+
}
|
|
2186
|
+
logger.info("[Tool] task.search", {
|
|
2187
|
+
repo,
|
|
2188
|
+
query,
|
|
2189
|
+
total,
|
|
2190
|
+
offset,
|
|
2191
|
+
returned: paginated.length
|
|
2192
|
+
});
|
|
2193
|
+
return createMcpResponse(structuredData, contentSummary || `Found ${total} tasks for "${query}".`, {
|
|
2194
|
+
contentSummary,
|
|
2195
|
+
structuredContentPathHint: "results",
|
|
2196
|
+
includeSerializedStructuredContent: isStructuredRequest
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2491
2200
|
// src/mcp/router.ts
|
|
2492
2201
|
function createRouter(db2, vectors2, options) {
|
|
2493
2202
|
const getSessionContext = options?.getSessionContext;
|
|
@@ -2639,6 +2348,8 @@ function createRouter(db2, vectors2, options) {
|
|
|
2639
2348
|
return await handleTaskDelete(args, db2);
|
|
2640
2349
|
case "task-list":
|
|
2641
2350
|
return await handleTaskList(args, db2);
|
|
2351
|
+
case "task-search":
|
|
2352
|
+
return await handleTaskSearch(args, db2);
|
|
2642
2353
|
case "task-detail":
|
|
2643
2354
|
return await handleTaskGet(args, db2);
|
|
2644
2355
|
default:
|
|
@@ -2813,8 +2524,8 @@ logger.info("[Server] startup", { pid: process.pid, version: CAPABILITIES.server
|
|
|
2813
2524
|
vectors.initialize().catch((err) => {
|
|
2814
2525
|
logger.warn("[Server] Initial vector model loading failed. Will retry on first use.", { error: String(err) });
|
|
2815
2526
|
});
|
|
2816
|
-
var expiredArchived = db.
|
|
2817
|
-
var lowScoreArchived = db.
|
|
2527
|
+
var expiredArchived = db.memoryArchives.archiveExpiredMemories();
|
|
2528
|
+
var lowScoreArchived = db.memoryArchives.archiveLowScoreMemories();
|
|
2818
2529
|
var totalArchived = (expiredArchived || 0) + (lowScoreArchived || 0);
|
|
2819
2530
|
if (totalArchived > 0) {
|
|
2820
2531
|
logger.info(
|
|
@@ -2835,9 +2546,7 @@ var logNotificationsEnabled = false;
|
|
|
2835
2546
|
var handleMethod = createRouter(db, vectors, {
|
|
2836
2547
|
getSessionContext: () => session,
|
|
2837
2548
|
sampleMessage: (params) => requestClient("sampling/createMessage", params),
|
|
2838
|
-
// eslint-disable-line @typescript-eslint/no-explicit-any
|
|
2839
2549
|
elicit: (params) => requestClient("elicitation/create", params),
|
|
2840
|
-
// eslint-disable-line @typescript-eslint/no-explicit-any
|
|
2841
2550
|
onResourcesMutated: (uris) => notifyUpdatedResources(uris)
|
|
2842
2551
|
});
|
|
2843
2552
|
addLogSink((payload) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vheins/local-memory-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "MCP Local Memory Service for coding copilot agents",
|
|
5
5
|
"mcpName": "io.github.vheins/local-memory-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@types/node": "^22.19.7",
|
|
69
69
|
"@typescript-eslint/eslint-plugin": "^8.58.1",
|
|
70
70
|
"@typescript-eslint/parser": "^8.58.1",
|
|
71
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
71
72
|
"eslint": "^10.2.0",
|
|
72
73
|
"eslint-config-prettier": "^10.1.8",
|
|
73
74
|
"eslint-plugin-prettier": "^5.5.5",
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
"typescript": "^5.7.3",
|
|
86
87
|
"typescript-eslint": "^8.58.1",
|
|
87
88
|
"vite": "^8.0.6",
|
|
88
|
-
"vitest": "^4.1.
|
|
89
|
+
"vitest": "^4.1.7"
|
|
89
90
|
},
|
|
90
91
|
"overrides": {
|
|
91
92
|
"protobufjs": "^7.5.5"
|