@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.
@@ -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-O4B6AFYU.js";
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 expires_at = validated.ttlDays != null ? new Date(Date.now() + validated.ttlDays * 864e5).toISOString() : null;
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.memories.checkConflicts(
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.memories.searchBySimilarity(
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.tasks.getTaskCommentsByTaskId(taskId);
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.tasks.getTaskStats(repo);
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.tasks.getTaskStats(repo);
934
+ const stats = storage.taskStats.getTaskStats(repo);
1034
935
  if (stats.todo >= 10) {
1035
- throw new Error(`Cannot create task as 'pending'. Maximum of 10 pending tasks reached. Please use status 'backlog' for new tasks instead.`);
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.tasks.insertTaskComment({
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
- if (onProgress) {
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
- db2.memories.delete(targetId);
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
- progress++;
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.tasks.getTaskCommentsByTaskId(task.id);
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.memories.archiveExpiredMemories();
2817
- var lowScoreArchived = db.memories.archiveLowScoreMemories();
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.13.2",
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.1"
89
+ "vitest": "^4.1.7"
89
90
  },
90
91
  "overrides": {
91
92
  "protobufjs": "^7.5.5"