@vheins/local-memory-mcp 0.9.6 → 0.9.9

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.
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
- <script type="module" crossorigin src="/assets/index-C9M1BD7U.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-CNE1bKgp.css">
11
+ <script type="module" crossorigin src="/assets/index-D-xKLFFr.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-C2cM-UXM.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="app"></div>
@@ -10,7 +10,7 @@ import {
10
10
  createFileSink,
11
11
  listResources,
12
12
  logger
13
- } from "../chunk-PWNNK75T.js";
13
+ } from "../chunk-UB4KAJYE.js";
14
14
 
15
15
  // src/dashboard/server.ts
16
16
  import express from "express";
@@ -215,7 +215,7 @@ vectors.initialize().catch((err) => {
215
215
  });
216
216
 
217
217
  // src/dashboard/routes/index.ts
218
- import { Router as Router5 } from "express";
218
+ import { Router as Router6 } from "express";
219
219
 
220
220
  // src/dashboard/routes/system.routes.ts
221
221
  import { Router } from "express";
@@ -297,6 +297,7 @@ var SystemController = class {
297
297
  uptime: Math.floor((Date.now() - startTime) / 1e3),
298
298
  version: pkg.version,
299
299
  memoryCount: stats.totalMemories,
300
+ repoCount: stats.totalRepos,
300
301
  pendingRequests: mcpClient.getPendingCount(),
301
302
  dbPath: db.getDbPath()
302
303
  };
@@ -321,8 +322,7 @@ var SystemController = class {
321
322
  try {
322
323
  await db.refresh();
323
324
  const repo = req.query.repo;
324
- if (!repo) return res.status(400).json(jsonApiError("repo is required", 400));
325
- const stats = db.system.getDashboardStats(repo);
325
+ const stats = repo ? db.system.getDashboardStats(repo) : db.system.getGlobalDashboardStats();
326
326
  res.json(jsonApiRes(stats, "system-stats"));
327
327
  } catch (err) {
328
328
  const message = err instanceof Error ? err.message : "Internal server error";
@@ -723,41 +723,33 @@ var TasksController = class {
723
723
  const attributes = getAttributes(req);
724
724
  const existingTask = db.tasks.getTaskById(id);
725
725
  if (!existingTask) return res.status(404).json(jsonApiError("Task not found", 404));
726
- await db.withWrite(() => {
727
- db.tasks.updateTask(id, attributes);
728
- if (attributes.status && attributes.status !== existingTask.status) {
729
- db.actions.logAction("update", existingTask.repo, {
730
- taskId: id,
731
- query: `Status changed to ${attributes.status}`
732
- });
733
- db.tasks.insertTaskComment({
734
- id: randomUUID2(),
735
- task_id: id,
736
- repo: existingTask.repo,
737
- comment: attributes.comment || `Status updated via dashboard`,
738
- agent: "dashboard",
739
- role: "user",
740
- model: "web-ui",
741
- previous_status: existingTask.status,
742
- next_status: attributes.status,
743
- created_at: (/* @__PURE__ */ new Date()).toISOString()
744
- });
745
- } else if (attributes.comment) {
746
- db.tasks.insertTaskComment({
747
- id: randomUUID2(),
748
- task_id: id,
749
- repo: existingTask.repo,
750
- comment: attributes.comment,
751
- agent: "dashboard",
752
- role: "user",
753
- model: "web-ui",
754
- previous_status: null,
755
- next_status: null,
756
- created_at: (/* @__PURE__ */ new Date()).toISOString()
757
- });
726
+ if (!mcpClient.isConnected()) await mcpClient.start();
727
+ const toolArgs = {
728
+ repo: existingTask.repo,
729
+ id,
730
+ agent: "dashboard",
731
+ role: "user",
732
+ model: "web-ui",
733
+ structured: true
734
+ };
735
+ for (const [key, value] of Object.entries(attributes)) {
736
+ if (value !== void 0) {
737
+ toolArgs[key] = value;
758
738
  }
759
- });
760
- res.json(jsonApiRes({ message: "Updated" }, "status"));
739
+ }
740
+ if (attributes.status && attributes.status !== existingTask.status && !toolArgs.comment) {
741
+ toolArgs.comment = `Status updated via dashboard to ${attributes.status}`;
742
+ }
743
+ if (attributes.status === "completed" && toolArgs.est_tokens === void 0) {
744
+ toolArgs.est_tokens = existingTask.est_tokens || 0;
745
+ }
746
+ await mcpClient.callTool("task-update", toolArgs);
747
+ await db.refresh();
748
+ const updatedTask = db.tasks.getTaskById(id);
749
+ if (!updatedTask) {
750
+ return res.status(500).json(jsonApiError("Task updated but could not be reloaded", 500));
751
+ }
752
+ res.json(jsonApiRes(updatedTask, "task"));
761
753
  } catch (err) {
762
754
  const message = err instanceof Error ? err.message : "Internal server error";
763
755
  res.status(500).json(jsonApiError(message));
@@ -808,23 +800,23 @@ var TasksController = class {
808
800
  try {
809
801
  await db.refresh();
810
802
  const { repo } = req.query;
811
- if (!repo) return res.status(400).json(jsonApiError("repo is required", 400));
803
+ const targetRepo = typeof repo === "string" && repo.length > 0 ? repo : null;
812
804
  const stats = {
813
805
  daily: {
814
- ...db.tasks.getTaskTimeStats(repo, "daily"),
815
- history: db.tasks.getTaskComparisonSeries(repo, "daily")
806
+ ...db.tasks.getTaskTimeStats(targetRepo, "daily"),
807
+ history: db.tasks.getTaskComparisonSeries(targetRepo, "daily")
816
808
  },
817
809
  weekly: {
818
- ...db.tasks.getTaskTimeStats(repo, "weekly"),
819
- history: db.tasks.getTaskComparisonSeries(repo, "weekly")
810
+ ...db.tasks.getTaskTimeStats(targetRepo, "weekly"),
811
+ history: db.tasks.getTaskComparisonSeries(targetRepo, "weekly")
820
812
  },
821
813
  monthly: {
822
- ...db.tasks.getTaskTimeStats(repo, "monthly"),
823
- history: db.tasks.getTaskComparisonSeries(repo, "monthly")
814
+ ...db.tasks.getTaskTimeStats(targetRepo, "monthly"),
815
+ history: db.tasks.getTaskComparisonSeries(targetRepo, "monthly")
824
816
  },
825
817
  overall: {
826
- ...db.tasks.getTaskTimeStats(repo, "overall"),
827
- history: db.tasks.getTaskComparisonSeries(repo, "overall")
818
+ ...db.tasks.getTaskTimeStats(targetRepo, "overall"),
819
+ history: db.tasks.getTaskComparisonSeries(targetRepo, "overall")
828
820
  }
829
821
  };
830
822
  res.json(jsonApiRes(stats, "performance-stats"));
@@ -1039,13 +1031,77 @@ router4.put("/:id", StandardsController.update);
1039
1031
  router4.delete("/:id", StandardsController.delete);
1040
1032
  var standard_routes_default = router4;
1041
1033
 
1042
- // src/dashboard/routes/index.ts
1034
+ // src/dashboard/routes/coordination.routes.ts
1035
+ import { Router as Router5 } from "express";
1036
+
1037
+ // src/dashboard/controllers/CoordinationController.ts
1038
+ var CoordinationController = class {
1039
+ static async listClaims(req, res) {
1040
+ try {
1041
+ await db.refresh();
1042
+ const { repo, agent, active_only } = req.query;
1043
+ const page = Math.max(1, parseInt(req.query.page || "1", 10));
1044
+ const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize || "20", 10)));
1045
+ if (!repo) return res.status(400).json(jsonApiError("repo is required", 400));
1046
+ const claims = db.handoffs.listClaims({
1047
+ repo,
1048
+ agent: typeof agent === "string" ? agent : void 0,
1049
+ active_only: active_only === void 0 ? true : String(active_only) === "true",
1050
+ limit: pageSize,
1051
+ offset: (page - 1) * pageSize
1052
+ });
1053
+ const total = db.handoffs.listClaims({
1054
+ repo,
1055
+ agent: typeof agent === "string" ? agent : void 0,
1056
+ active_only: active_only === void 0 ? true : String(active_only) === "true",
1057
+ limit: 1e5,
1058
+ offset: 0
1059
+ }).length;
1060
+ res.json(
1061
+ jsonApiRes(claims, "claim", {
1062
+ meta: {
1063
+ page,
1064
+ pageSize,
1065
+ totalItems: total,
1066
+ totalPages: Math.ceil(total / pageSize)
1067
+ }
1068
+ })
1069
+ );
1070
+ } catch (err) {
1071
+ const message = err instanceof Error ? err.message : "Internal server error";
1072
+ res.status(500).json(jsonApiError(message));
1073
+ }
1074
+ }
1075
+ static async releaseClaim(req, res) {
1076
+ try {
1077
+ const attributes = getAttributes(req);
1078
+ if (!mcpClient.isConnected()) await mcpClient.start();
1079
+ const result = await mcpClient.callTool("claim-release", {
1080
+ ...attributes,
1081
+ structured: true
1082
+ });
1083
+ res.json(jsonApiRes(result.structuredContent || result, "claim-release"));
1084
+ } catch (err) {
1085
+ const message = err instanceof Error ? err.message : "Internal server error";
1086
+ res.status(500).json(jsonApiError(message));
1087
+ }
1088
+ }
1089
+ };
1090
+
1091
+ // src/dashboard/routes/coordination.routes.ts
1043
1092
  var router5 = Router5();
1044
- router5.use("/", system_routes_default);
1045
- router5.use("/memories", memory_routes_default);
1046
- router5.use("/tasks", task_routes_default);
1047
- router5.use("/standards", standard_routes_default);
1048
- var routes_default = router5;
1093
+ router5.get("/claims", CoordinationController.listClaims);
1094
+ router5.post("/claims/release", CoordinationController.releaseClaim);
1095
+ var coordination_routes_default = router5;
1096
+
1097
+ // src/dashboard/routes/index.ts
1098
+ var router6 = Router6();
1099
+ router6.use("/", system_routes_default);
1100
+ router6.use("/memories", memory_routes_default);
1101
+ router6.use("/tasks", task_routes_default);
1102
+ router6.use("/standards", standard_routes_default);
1103
+ router6.use("/coordination", coordination_routes_default);
1104
+ var routes_default = router6;
1049
1105
 
1050
1106
  // src/dashboard/server.ts
1051
1107
  var __dirname3 = path3.dirname(fileURLToPath3(import.meta.url));
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CAPABILITIES,
4
+ ClaimListSchema,
5
+ ClaimReleaseSchema,
4
6
  HandoffCreateSchema,
5
7
  HandoffListSchema,
6
8
  HandoffUpdateSchema,
@@ -55,7 +57,7 @@ import {
55
57
  toContextSlug,
56
58
  updateSessionFromInitialize,
57
59
  updateSessionRoots
58
- } from "../chunk-PWNNK75T.js";
60
+ } from "../chunk-UB4KAJYE.js";
59
61
 
60
62
  // src/mcp/server.ts
61
63
  import readline from "readline";
@@ -1663,6 +1665,16 @@ function buildHandoffListSummary(repo, count, status, fromAgent, toAgent) {
1663
1665
  }
1664
1666
  return parts.join("\n");
1665
1667
  }
1668
+ function buildClaimListSummary(repo, count, agent, activeOnly) {
1669
+ const parts = [`Found ${count} claim${count === 1 ? "" : "s"} in repo "${repo}".`];
1670
+ if (agent) {
1671
+ parts.push(`Agent filter: ${agent}.`);
1672
+ }
1673
+ if (activeOnly) {
1674
+ parts.push("Showing active claims only.");
1675
+ }
1676
+ return parts.join("\n");
1677
+ }
1666
1678
  async function handleHandoffCreate(args, storage) {
1667
1679
  const validated = HandoffCreateSchema.parse(args);
1668
1680
  const { repo, from_agent, to_agent, task_id, task_code, summary, context, expires_at, structured } = validated;
@@ -1708,15 +1720,31 @@ async function handleHandoffList(args, storage) {
1708
1720
  limit,
1709
1721
  offset
1710
1722
  });
1711
- const COLUMNS = ["id", "from_agent", "to_agent", "task_id", "status", "created_at", "summary"];
1723
+ const COLUMNS = [
1724
+ "id",
1725
+ "from_agent",
1726
+ "to_agent",
1727
+ "task_id",
1728
+ "task_code",
1729
+ "status",
1730
+ "created_at",
1731
+ "updated_at",
1732
+ "expires_at",
1733
+ "summary",
1734
+ "context"
1735
+ ];
1712
1736
  const rows = handoffs.map((handoff) => [
1713
1737
  handoff.id,
1714
1738
  handoff.from_agent,
1715
1739
  handoff.to_agent,
1716
1740
  handoff.task_id,
1741
+ handoff.task_code ?? null,
1717
1742
  handoff.status,
1718
1743
  handoff.created_at,
1719
- handoff.summary
1744
+ handoff.updated_at,
1745
+ handoff.expires_at,
1746
+ handoff.summary,
1747
+ handoff.context
1720
1748
  ]);
1721
1749
  const structuredData = {
1722
1750
  schema: "handoff-list",
@@ -1806,6 +1834,82 @@ async function handleTaskClaim(args, storage) {
1806
1834
  }
1807
1835
  return response;
1808
1836
  }
1837
+ async function handleClaimList(args, storage) {
1838
+ const validated = ClaimListSchema.parse(args);
1839
+ const { repo, agent, active_only, limit, offset, structured } = validated;
1840
+ const claims = storage.handoffs.listClaims({
1841
+ repo,
1842
+ agent,
1843
+ active_only,
1844
+ limit,
1845
+ offset
1846
+ });
1847
+ const COLUMNS = ["id", "task_id", "task_code", "agent", "role", "claimed_at", "released_at", "metadata"];
1848
+ const rows = claims.map((claim) => [
1849
+ claim.id,
1850
+ claim.task_id,
1851
+ claim.task_code ?? null,
1852
+ claim.agent,
1853
+ claim.role,
1854
+ claim.claimed_at,
1855
+ claim.released_at,
1856
+ claim.metadata
1857
+ ]);
1858
+ const structuredData = {
1859
+ schema: "claim-list",
1860
+ claims: {
1861
+ columns: [...COLUMNS],
1862
+ rows
1863
+ },
1864
+ count: rows.length,
1865
+ offset
1866
+ };
1867
+ const contentSummary = buildClaimListSummary(repo, rows.length, agent, active_only);
1868
+ return createMcpResponse(structuredData, contentSummary, {
1869
+ contentSummary,
1870
+ includeSerializedStructuredContent: structured
1871
+ });
1872
+ }
1873
+ async function handleClaimRelease(args, storage) {
1874
+ const validated = ClaimReleaseSchema.parse(args);
1875
+ const { repo, task_id, task_code, agent, structured } = validated;
1876
+ let resolvedTaskId = task_id;
1877
+ let resolvedTaskCode = task_code ?? null;
1878
+ if (resolvedTaskId) {
1879
+ const task = storage.tasks.getTaskById(resolvedTaskId);
1880
+ if (!task || task.repo !== repo) {
1881
+ throw new Error(`Task not found: ${resolvedTaskId} in repo ${repo}`);
1882
+ }
1883
+ resolvedTaskCode = task.task_code;
1884
+ } else if (task_code) {
1885
+ const task = storage.tasks.getTaskByCode(repo, task_code);
1886
+ if (!task) {
1887
+ throw new Error(`Task not found: ${task_code} in repo ${repo}`);
1888
+ }
1889
+ resolvedTaskId = task.id;
1890
+ resolvedTaskCode = task.task_code;
1891
+ }
1892
+ const success = storage.handoffs.releaseClaim(resolvedTaskId, agent);
1893
+ if (!success) {
1894
+ throw new Error(`No active claim found for task ${resolvedTaskCode || resolvedTaskId}`);
1895
+ }
1896
+ const result = {
1897
+ success,
1898
+ repo,
1899
+ task_id: resolvedTaskId,
1900
+ task_code: resolvedTaskCode,
1901
+ agent: agent ?? null
1902
+ };
1903
+ const contentSummary = [
1904
+ `Released claim for task ${resolvedTaskCode || resolvedTaskId}.`,
1905
+ `Repo: ${repo}`,
1906
+ agent ? `Agent: ${agent}` : "Agent: any active claimant"
1907
+ ].join("\n");
1908
+ return createMcpResponse(result, contentSummary, {
1909
+ contentSummary,
1910
+ includeSerializedStructuredContent: structured
1911
+ });
1912
+ }
1809
1913
 
1810
1914
  // src/mcp/tools/standard.store.ts
1811
1915
  import { randomUUID as randomUUID3 } from "crypto";
@@ -1901,10 +2005,63 @@ async function handleStandardStore(params, db2, vectors2) {
1901
2005
 
1902
2006
  // src/mcp/tools/standard.search.ts
1903
2007
  var HYBRID_WEIGHTS_STANDARD = {
1904
- similarity: 0.55,
1905
- vector: 0.35,
1906
- usage: 0.1
2008
+ similarity: 0.4,
2009
+ vector: 0.25,
2010
+ keyword: 0.3,
2011
+ usage: 0.05
1907
2012
  };
2013
+ function tokenizeSearchText(value) {
2014
+ return value.toLowerCase().split(/[^a-z0-9]+/g).map((token) => token.trim()).filter((token) => token.length >= 2);
2015
+ }
2016
+ function scoreKeywordRelevance(query, standard) {
2017
+ const queryTokens = Array.from(new Set(tokenizeSearchText(query)));
2018
+ if (queryTokens.length === 0) return 0;
2019
+ const titleText = standard.title.toLowerCase();
2020
+ const contextText = standard.context.toLowerCase();
2021
+ const tagText = standard.tags.join(" ").toLowerCase();
2022
+ const stackText = standard.stack.join(" ").toLowerCase();
2023
+ const contentText = standard.content.toLowerCase();
2024
+ const queryPhrase = query.trim().toLowerCase();
2025
+ let titleMatches = 0;
2026
+ let contextMatches = 0;
2027
+ let tagMatches = 0;
2028
+ let stackMatches = 0;
2029
+ let contentMatches = 0;
2030
+ for (const token of queryTokens) {
2031
+ if (titleText.includes(token)) titleMatches += 1;
2032
+ if (contextText.includes(token)) contextMatches += 1;
2033
+ if (tagText.includes(token)) tagMatches += 1;
2034
+ if (stackText.includes(token)) stackMatches += 1;
2035
+ if (contentText.includes(token)) contentMatches += 1;
2036
+ }
2037
+ const titleCoverage = titleMatches / queryTokens.length;
2038
+ const contextCoverage = contextMatches / queryTokens.length;
2039
+ const tagCoverage = tagMatches / queryTokens.length;
2040
+ const stackCoverage = stackMatches / queryTokens.length;
2041
+ const contentCoverage = contentMatches / queryTokens.length;
2042
+ const exactPhraseBonus = queryPhrase.length >= 6 && (titleText.includes(queryPhrase) || contentText.includes(queryPhrase) || tagText.includes(queryPhrase)) ? 0.2 : 0;
2043
+ return Math.min(
2044
+ 1,
2045
+ titleCoverage * 0.45 + contextCoverage * 0.15 + tagCoverage * 0.15 + stackCoverage * 0.05 + contentCoverage * 0.2 + exactPhraseBonus
2046
+ );
2047
+ }
2048
+ function collectMatchedTerms(query, standard) {
2049
+ const queryTokens = Array.from(new Set(tokenizeSearchText(query)));
2050
+ if (queryTokens.length === 0) return [];
2051
+ const searchableFields = [
2052
+ standard.title.toLowerCase(),
2053
+ standard.context.toLowerCase(),
2054
+ standard.tags.join(" ").toLowerCase(),
2055
+ standard.stack.join(" ").toLowerCase(),
2056
+ standard.content.toLowerCase()
2057
+ ];
2058
+ return queryTokens.filter((token) => searchableFields.some((field) => field.includes(token)));
2059
+ }
2060
+ function toConfidence(finalScore, keywordScore) {
2061
+ if (finalScore >= 0.72 || keywordScore >= 0.85) return "high";
2062
+ if (finalScore >= 0.42 || keywordScore >= 0.45) return "medium";
2063
+ return "low";
2064
+ }
1908
2065
  async function handleStandardSearch(params, db2, vectors2) {
1909
2066
  const validated = StandardSearchSchema.parse(params);
1910
2067
  const searchQuery = expandQuery(validated.query || "", void 0);
@@ -1936,13 +2093,18 @@ async function handleStandardSearch(params, db2, vectors2) {
1936
2093
  if (similarityResults.length > 0) {
1937
2094
  scoredStandards = similarityResults.map((candidate) => {
1938
2095
  const vectorScore = vectorScoreMap.get(candidate.id) ?? 0;
2096
+ const keywordScore = scoreKeywordRelevance(validated.query || "", candidate);
2097
+ const matchedTerms = collectMatchedTerms(validated.query || "", candidate);
1939
2098
  const usageScore = Math.min(1, candidate.hit_count / 10);
1940
- const finalScore = candidate.similarity * HYBRID_WEIGHTS_STANDARD.similarity + vectorScore * HYBRID_WEIGHTS_STANDARD.vector + usageScore * HYBRID_WEIGHTS_STANDARD.usage;
2099
+ const finalScore = candidate.similarity * HYBRID_WEIGHTS_STANDARD.similarity + vectorScore * HYBRID_WEIGHTS_STANDARD.vector + keywordScore * HYBRID_WEIGHTS_STANDARD.keyword + usageScore * HYBRID_WEIGHTS_STANDARD.usage;
1941
2100
  return {
1942
2101
  standard: candidate,
1943
2102
  similarityScore: candidate.similarity,
1944
2103
  vectorScore,
1945
- finalScore
2104
+ keywordScore,
2105
+ matchedTerms,
2106
+ finalScore,
2107
+ confidence: toConfidence(finalScore, keywordScore)
1946
2108
  };
1947
2109
  });
1948
2110
  } else if (vectorResults.length > 0) {
@@ -1951,43 +2113,58 @@ async function handleStandardSearch(params, db2, vectors2) {
1951
2113
  scoredStandards = vectorResults.flatMap((row) => {
1952
2114
  const standard = standardMap.get(row.id);
1953
2115
  if (!standard) return [];
2116
+ const keywordScore = scoreKeywordRelevance(validated.query || "", standard);
2117
+ const matchedTerms = collectMatchedTerms(validated.query || "", standard);
1954
2118
  const usageScore = Math.min(1, standard.hit_count / 10);
2119
+ const finalScore = row.score * HYBRID_WEIGHTS_STANDARD.vector + keywordScore * HYBRID_WEIGHTS_STANDARD.keyword + usageScore * (1 - HYBRID_WEIGHTS_STANDARD.vector - HYBRID_WEIGHTS_STANDARD.keyword);
1955
2120
  return [
1956
2121
  {
1957
2122
  standard,
1958
2123
  similarityScore: 0,
1959
2124
  vectorScore: row.score,
1960
- finalScore: row.score * 0.9 + usageScore * 0.1
2125
+ keywordScore,
2126
+ matchedTerms,
2127
+ finalScore,
2128
+ confidence: toConfidence(finalScore, keywordScore)
1961
2129
  }
1962
2130
  ];
1963
2131
  });
1964
2132
  }
1965
2133
  } catch (error) {
1966
2134
  logger.warn("Standard vector search failed, using similarity only", { error: String(error) });
1967
- scoredStandards = similarityResults.map((candidate) => ({
1968
- standard: candidate,
1969
- similarityScore: candidate.similarity,
1970
- vectorScore: 0,
1971
- finalScore: candidate.similarity * 0.9 + Math.min(1, candidate.hit_count / 10) * 0.1
1972
- }));
2135
+ scoredStandards = similarityResults.map((candidate) => {
2136
+ const keywordScore = scoreKeywordRelevance(validated.query || "", candidate);
2137
+ const matchedTerms = collectMatchedTerms(validated.query || "", candidate);
2138
+ const finalScore = candidate.similarity * (HYBRID_WEIGHTS_STANDARD.similarity + HYBRID_WEIGHTS_STANDARD.vector * 0.5) + keywordScore * HYBRID_WEIGHTS_STANDARD.keyword + Math.min(1, candidate.hit_count / 10) * HYBRID_WEIGHTS_STANDARD.usage;
2139
+ return {
2140
+ standard: candidate,
2141
+ similarityScore: candidate.similarity,
2142
+ vectorScore: 0,
2143
+ keywordScore,
2144
+ matchedTerms,
2145
+ finalScore,
2146
+ confidence: toConfidence(finalScore, keywordScore)
2147
+ };
2148
+ });
1973
2149
  }
1974
2150
  scoredStandards.sort((a, b) => b.finalScore - a.finalScore);
1975
2151
  const threshold = scoredStandards.length <= 5 ? 0.08 : 0.2;
1976
- let results = scoredStandards.filter((candidate) => candidate.finalScore >= threshold).map((candidate) => candidate.standard);
2152
+ let results = scoredStandards.filter((candidate) => candidate.finalScore >= threshold);
1977
2153
  if (results.length === 0 && scoredStandards.length > 0) {
1978
- results = [scoredStandards[0].standard];
2154
+ results = [scoredStandards[0]];
1979
2155
  }
1980
2156
  const paginatedResults = results.slice(validated.offset, validated.offset + validated.limit);
1981
- db2.standards.incrementHitCounts(paginatedResults.map((standard) => standard.id));
2157
+ db2.standards.incrementHitCounts(paginatedResults.map(({ standard }) => standard.id));
1982
2158
  logger.info("[Tool] standard.search - searched coding standards", {
1983
2159
  resultCount: paginatedResults.length,
1984
2160
  stack: validated.stack,
1985
2161
  language: validated.language,
1986
2162
  context: validated.context,
1987
- version: validated.version
2163
+ version: validated.version,
2164
+ topConfidence: paginatedResults[0]?.confidence
1988
2165
  });
1989
- const COLUMNS = ["code", "id", "title", "context", "language", "scope", "tags", "updated_at"];
1990
- const rows = paginatedResults.map((standard) => [
2166
+ const COLUMNS = ["code", "id", "title", "context", "language", "scope", "tags", "confidence", "score", "matched_terms", "updated_at"];
2167
+ const rows = paginatedResults.map(({ standard, confidence, finalScore, matchedTerms }) => [
1991
2168
  standard.code ?? "-",
1992
2169
  standard.id,
1993
2170
  standard.title,
@@ -1995,15 +2172,18 @@ async function handleStandardSearch(params, db2, vectors2) {
1995
2172
  standard.language || "-",
1996
2173
  standard.is_global ? "global" : standard.repo || "-",
1997
2174
  standard.tags.join(", "),
2175
+ confidence,
2176
+ Number(finalScore.toFixed(3)),
2177
+ matchedTerms.join(", "),
1998
2178
  standard.updated_at
1999
2179
  ]);
2000
2180
  let contentSummary;
2001
2181
  if (paginatedResults.length > 0) {
2002
2182
  const parts = [
2003
2183
  "Standards:",
2004
- "- code|title|context|language|scope",
2184
+ "- code|confidence|matched_terms|title|context|language|scope",
2005
2185
  ...paginatedResults.map(
2006
- (standard) => `- ${standard.code ?? "-"}|${standard.title}|${standard.context}|${standard.language || "-"}|${standard.is_global ? "global" : standard.repo || "-"}`
2186
+ ({ standard, confidence, matchedTerms }) => `- ${standard.code ?? "-"}|${confidence}|${matchedTerms.join(", ")}|${standard.title}|${standard.context}|${standard.language || "-"}|${standard.is_global ? "global" : standard.repo || "-"}`
2007
2187
  ),
2008
2188
  "",
2009
2189
  "Use standard-detail with code for full content."
@@ -2283,6 +2463,7 @@ function createRouter(db2, vectors2, options) {
2283
2463
  "task-create",
2284
2464
  "task-create-interactive",
2285
2465
  "task-claim",
2466
+ "claim-release",
2286
2467
  "task-update",
2287
2468
  "task-delete"
2288
2469
  ]);
@@ -2327,6 +2508,10 @@ function createRouter(db2, vectors2, options) {
2327
2508
  return await handleHandoffUpdate(args, db2);
2328
2509
  case "task-claim":
2329
2510
  return await handleTaskClaim(args, db2);
2511
+ case "claim-list":
2512
+ return await handleClaimList(args, db2);
2513
+ case "claim-release":
2514
+ return await handleClaimRelease(args, db2);
2330
2515
  case "standard-store":
2331
2516
  return await handleStandardStore(args, db2, vectors2);
2332
2517
  case "standard-update":
@@ -18,7 +18,7 @@ You are a memory-aware agent. Memory is project truth, not a suggestion.
18
18
 
19
19
  ## Execution Policy
20
20
  1. **Orient**: Call `task-list` for active work and `handoff-list` for pending transfers when starting a repository session. Close stale pending handoffs with `handoff-update` when they no longer describe unfinished work.
21
- 2. **Claim**: Use `task-claim` before taking ownership of a concrete task.
21
+ 2. **Claim**: Use `task-claim` before taking ownership of a concrete task. Use `claim-list` when ownership is unclear and `claim-release` to clear stale claims during reassignment.
22
22
  3. **Search**: Call `memory-search` with `current_file_path` and `current_tags` before coding.
23
23
  4. **Standards**: Call `standard-search` when implementation may be governed by coding standards.
24
24
  5. **Retrieve**: Use `memory-detail` for full content if search pointer rows are insufficient.
@@ -9,8 +9,8 @@ agent: Project Manager
9
9
  ## 1. NAVIGATION (`task-list`)
10
10
  - **Sync**: Call `task-list` at every session start (default: `in_progress,pending`).
11
11
  - **Format**: Compact pointer table: `id`, `task_code`, `title`, `status`, `priority`, `updated_at`, `comments_count`. Use `query` for keyword search.
12
- - **Retrieve**: Fetch full context via `task-detail` AFTER selecting a task.
13
- - **Coordination**: Check active ownership with task metadata and `task-claim`. NEVER work on tasks claimed by others. Focus on ONE task at a time.
12
+ - **Retrieve**: Fetch full context via `task-detail` AFTER selecting a task. The hydrated task includes current coordination state such as active claims and pending handoffs.
13
+ - **Coordination**: Check active ownership with task coordination metadata, `task-claim`, and `claim-list`. NEVER work on tasks claimed by others. Focus on ONE task at a time.
14
14
 
15
15
  ## 2. DETAIL TOOLS
16
16
  - **Tasks**: Call `task-detail` for history/comments (ID or `task_code`).
@@ -23,6 +23,8 @@ agent: Project Manager
23
23
  - **Transition Safety**: MUST move from `backlog/pending` → `in_progress` → `completed`. Skipping `in_progress` is forbidden.
24
24
  - **Automatic Cleanup**: `task-update` to `completed` or `canceled` automatically releases active claims and expires pending handoffs linked to that task.
25
25
  - **Claiming**: Use `task-claim` when taking ownership of a task, with `task_code` when working from human-visible queues.
26
+ - **Claim Inspection**: Use `claim-list` when ownership is unclear or when triaging stale work.
27
+ - **Claim Release**: Use `claim-release` to clear stale ownership explicitly when a task is being handed back or reassigned.
26
28
  - **Handoff**: Use `handoff-create` only when pausing or transferring unfinished work. Do not use pending handoffs for completion summaries; close consumed/stale handoffs with `handoff-update`.
27
29
  - **Validation**: Only mark `completed` after passing tests or explicitly documenting why verification could not run.
28
30
  - **Archiving**: Completion triggers auto-archive to `task_archive` memory with token reporting.
@@ -18,7 +18,7 @@ Use the tools in the same flow exposed by the dashboard: navigate with compact l
18
18
 
19
19
  ## 2. Task Flow
20
20
  - **Navigate**: Start with `task-list` using active statuses (`in_progress,pending`) or explicit filters.
21
- - **Hydrate**: Use `task-detail` after selecting a task from the list. Do not treat list rows as full task context.
21
+ - **Hydrate**: Use `task-detail` after selecting a task from the list. Full task detail includes comments plus current coordination state such as active claims and pending handoffs.
22
22
  - **Mutate**: Use `task-create`, `task-update`, and `task-delete` for lifecycle changes.
23
23
  - **Transitions**: Move `backlog/pending/blocked` to `in_progress` before `completed`; include validation evidence and token estimate on completion.
24
24
  - **Automatic cleanup**: Moving a task to `completed` or `canceled` automatically releases active claims and expires pending handoffs linked to that task.
@@ -30,11 +30,13 @@ Use the tools in the same flow exposed by the dashboard: navigate with compact l
30
30
  - **Scope**: Prefer repo-specific standards for local conventions; use global standards only for cross-repo rules.
31
31
 
32
32
  ## 4. Handoff & Claim Flow
33
- - **Queue**: Use `handoff-list` as the handoff navigation layer. Filter by `status`, `from_agent`, or `to_agent`.
33
+ - **Queue**: Use `handoff-list` as the handoff navigation layer. Filter by `status`, `from_agent`, or `to_agent`. Hydrated handoff rows include `task_code`, `updated_at`, `expires_at`, and structured `context`.
34
34
  - **Create handoff**: Use `handoff-create` only when unfinished work needs context transfer. Include `from_agent`, optional `to_agent`, optional `task_code` or `task_id`, concise `summary`, and structured `context` with `next_steps`, `blockers`, or `remaining_work`.
35
35
  - **Close handoff**: Use `handoff-update` to move stale or consumed handoffs out of `pending`. Use `accepted` when consumed, `rejected` when declined, and `expired` when obsolete or only a completed-work summary.
36
36
  - **No completion handoff**: Do not create pending handoffs for completed-work summaries, release notes, validation notes, or archive records. Put those on `task-update` comments or durable memory.
37
37
  - **Claim task**: Use `task-claim` for task ownership. Do not encode claims in memory metadata.
38
+ - **Inspect claims**: Use `claim-list` to inspect active ownership by repo or agent before reassigning work.
39
+ - **Release claims**: Use `claim-release` to explicitly clear stale ownership when work is transferred or abandoned.
38
40
  - **Linkage**: Prefer `task_code` for human workflows and `task_id` when already hydrated.
39
41
 
40
42
  ## 5. Reference Flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vheins/local-memory-mcp",
3
- "version": "0.9.6",
3
+ "version": "0.9.9",
4
4
  "description": "MCP Local Memory Service for coding copilot agents",
5
5
  "mcpName": "io.github.vheins/local-memory-mcp",
6
6
  "type": "module",