minutes-mcp 0.28.2 → 0.29.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/dist/index.d.ts CHANGED
@@ -640,6 +640,8 @@ export declare function verifiedStopRecordingSummary(snapshot: {
640
640
  };
641
641
  };
642
642
  }): string;
643
+ /** Temporal projection consumes only the exact authorized corpus snapshot. */
644
+ export declare function policyDecisionHistory(snapshot: StableCorpusSnapshot): reader.DecisionHistoryEntry[];
643
645
  export declare function readVerifiedScreenImage(imagePath: string, expectedByteSize: number, expectedSha256: string, screenRoot?: string, hooks?: BoundReadHooks): Promise<Buffer>;
644
646
  export declare function restrictedMeetingStubResult(meeting: PolicyVerifiedMeeting): {
645
647
  content: {
package/dist/index.js CHANGED
@@ -841,11 +841,11 @@ export async function policySearchMeetings(dir, query, limit, includeRestricted,
841
841
  const boundedLimit = normalizeMcpResultLimit(limit, MCP_POLICY_MEETING_RESULT_MAX, "policy search");
842
842
  if (!query)
843
843
  return [];
844
- const needle = query.toLowerCase();
845
- return policyMatchingSnapshotOperation(dir, includeRestricted, (meeting) => meeting.frontmatter.title.toLowerCase().includes(needle) ||
846
- meeting.body.toLowerCase().includes(needle), (snapshots) => {
844
+ return policyMatchingSnapshotOperation(dir, includeRestricted, (meeting) => reader.lexicalMatch(meeting.frontmatter.title, meeting.body, query) !== null, (snapshots) => {
847
845
  const results = [];
848
- for (const snapshot of newestPolicySnapshots(snapshots)) {
846
+ const ranked = snapshots.map((snapshot) => ({ snapshot, score: reader.lexicalMatch(snapshot.meeting.frontmatter.title, snapshot.meeting.body, query)?.score ?? 0 }));
847
+ ranked.sort((a, b) => reader.compareSearchMatches({ score: a.score, date: a.snapshot.meeting.frontmatter.date, path: a.snapshot.path }, { score: b.score, date: b.snapshot.meeting.frontmatter.date, path: b.snapshot.path }));
848
+ for (const { snapshot } of ranked) {
849
849
  results.push(snapshot.meeting);
850
850
  if (results.length >= boundedLimit)
851
851
  break;
@@ -867,17 +867,10 @@ function meetingMatchesSince(meeting, since) {
867
867
  return Number.isFinite(boundary) && Number.isFinite(meetingDate) && meetingDate >= boundary;
868
868
  }
869
869
  function meetingMatchesPerson(meeting, person) {
870
- if (!person)
870
+ if (person === undefined)
871
871
  return true;
872
- const needle = person.trim().toLowerCase();
873
- if (!needle)
874
- return true;
875
- const people = [
876
- ...meeting.frontmatter.attendees,
877
- ...meeting.frontmatter.people,
878
- ...(meeting.frontmatter.attendees_raw || "").split(/[,;\n]/),
879
- ];
880
- return people.some((entry) => entry.trim().toLowerCase().includes(needle));
872
+ const identity = reader.resolvePersonIdentity([meeting], person);
873
+ return !!identity && reader.meetingMentionsPerson(meeting, identity);
881
874
  }
882
875
  function normalizedPersonSelector(value) {
883
876
  return (value ?? "").trim().replace(/\s+/g, " ").toLowerCase();
@@ -910,7 +903,7 @@ export function policyIntentResults(meetings, query, intentKind, owner, limit =
910
903
  const queryFields = includeMeetingTitleInQuery
911
904
  ? [what, who || "", meeting.frontmatter.title]
912
905
  : [what, who || ""];
913
- if (needle && !queryFields.some((value) => value.toLowerCase().includes(needle)))
906
+ if (needle && !reader.lexicalMatch("", queryFields.join(" "), query))
914
907
  return false;
915
908
  if (ownerNeedle && !ownerMatchesExactSelector(who, ownerNeedle))
916
909
  return false;
@@ -1037,17 +1030,30 @@ function meetingMatchesToolSearch(meeting, filter) {
1037
1030
  if (intentMode) {
1038
1031
  return policyIntentResults([meeting], filter.query, filter.intentKind, filter.owner, 1).length > 0;
1039
1032
  }
1040
- const needle = filter.query.trim().toLowerCase();
1041
- return needle.length > 0 &&
1042
- (meeting.frontmatter.title.toLowerCase().includes(needle) ||
1043
- meeting.body.toLowerCase().includes(needle));
1033
+ return reader.lexicalMatch(meeting.frontmatter.title, meeting.body, filter.query) !== null;
1044
1034
  }
1045
1035
  export function collectPolicyToolSearchSnapshots(snapshot, includeRestricted, filter) {
1046
- return newestPolicySnapshots(collectPolicyVerifiedMeetingSnapshots(snapshot, includeRestricted, (meeting) => meetingMatchesToolSearch(meeting, filter)));
1036
+ const matches = [];
1037
+ for (const file of snapshot.files) {
1038
+ if (!isActiveCorpusMeetingPath(file.path, snapshot.canonicalRoot))
1039
+ continue;
1040
+ const meeting = parsePolicyVerifiedMeeting(file.content, file.path);
1041
+ if (!meeting || (!includeRestricted && meetingSensitivity(meeting) === "restricted"))
1042
+ continue;
1043
+ if (!meetingMatchesToolSearch(meeting, filter))
1044
+ continue;
1045
+ meeting.retrieval = reader.lexicalMatch(meeting.frontmatter.title, meeting.body, filter.query) ?? undefined;
1046
+ matches.push({ path: file.path, content: file.content, meeting });
1047
+ }
1048
+ return matches.sort((a, b) => reader.compareSearchMatches({ score: a.meeting.retrieval?.score ?? 0, date: a.meeting.frontmatter.date, path: a.path }, { score: b.meeting.retrieval?.score ?? 0, date: b.meeting.frontmatter.date, path: b.path })).slice(0, MCP_POLICY_MEETING_RESULT_MAX);
1047
1049
  }
1048
- async function policyToolSearchMeetings(dir, includeRestricted, filter) {
1050
+ async function policyToolSearchMeetings(dir, includeRestricted, filter, limit = MCP_MEETING_RESULT_MAX) {
1049
1051
  return withStableCorpusLease(dir, (snapshot) => collectPolicyToolSearchSnapshots(snapshot, includeRestricted, filter)
1050
- .map((entry) => entry.meeting));
1052
+ .slice(0, limit)
1053
+ .map((entry) => ({ ...entry.meeting, evidence: reader.buildSourceEvidence({
1054
+ path: entry.path, content: entry.content, body: entry.meeting.body,
1055
+ speakerMap: entry.meeting.frontmatter.speaker_map,
1056
+ }, { query: filter.query, limit: 3 }) })));
1051
1057
  }
1052
1058
  export function openActionsFromMeetings(meetings, limit = MCP_ACTION_RESULT_MAX) {
1053
1059
  const boundedLimit = normalizeMcpResultLimit(limit, MCP_ACTION_RESULT_MAX, "open action");
@@ -1336,7 +1342,7 @@ export function researchTopicProjection(sourceMeetings, query) {
1336
1342
  for (const meeting of meetings) {
1337
1343
  for (const decision of meeting.frontmatter.decisions) {
1338
1344
  if (!needle ||
1339
- ![decision.text, decision.topic || ""].some((value) => value.toLowerCase().includes(needle))) {
1345
+ !reader.lexicalMatch(decision.topic || "", decision.text, query)) {
1340
1346
  continue;
1341
1347
  }
1342
1348
  const line = `- ${boundedMcpField(meeting.frontmatter.date) ?? ""} — ${boundedMcpField(decision.text) ?? ""} (${boundedMcpField(meeting.frontmatter.title) ?? ""})`;
@@ -1351,7 +1357,7 @@ export function researchTopicProjection(sourceMeetings, query) {
1351
1357
  const topicCounts = new Map();
1352
1358
  for (const meeting of meetings) {
1353
1359
  for (const rawTag of meeting.frontmatter.tags) {
1354
- if (!needle || !rawTag.toLowerCase().includes(needle))
1360
+ if (!needle || !reader.lexicalMatch(rawTag, "", query))
1355
1361
  continue;
1356
1362
  const topic = boundedMcpField(rawTag) ?? "";
1357
1363
  if (!topic)
@@ -1591,7 +1597,7 @@ const COPILOT_SUPPORTED = hasFeature(CLI_CAPABILITIES, "copilot_realtime");
1591
1597
  // `./version.ts` (see issue #183). Hosted `.mcpb` bundles will run
1592
1598
  // against CLIs with different minor/patch numbers within the same
1593
1599
  // major; that is explicitly supported.
1594
- const MCP_SERVER_VERSION = "0.28.2";
1600
+ const MCP_SERVER_VERSION = "0.29.1";
1595
1601
  export function parseKnowledgeConfig(configContent) {
1596
1602
  const knowledgeMatch = configContent.match(/\[knowledge\][\s\S]*?(?=\n\[|$)/);
1597
1603
  if (!knowledgeMatch) {
@@ -4109,7 +4115,7 @@ registerDocsAppTool("search_meetings", {
4109
4115
  intentKind: intent_kind,
4110
4116
  owner,
4111
4117
  intentsOnly: intents_only,
4112
- });
4118
+ }, intentMode ? MCP_POLICY_MEETING_RESULT_MAX : boundedLimit);
4113
4119
  let results;
4114
4120
  if (intentMode) {
4115
4121
  results = policyIntentResults(meetings, query, intent_kind, owner, boundedLimit);
@@ -4119,7 +4125,9 @@ registerDocsAppTool("search_meetings", {
4119
4125
  for (const meeting of meetings) {
4120
4126
  matches.push({
4121
4127
  ...meetingSearchItem(meeting),
4122
- snippet: liveMeetingSnippet(meeting.body, query),
4128
+ snippet: meeting.evidence?.segments[0]?.quote ?? liveMeetingSnippet(meeting.body, query),
4129
+ retrieval: meeting.retrieval,
4130
+ evidence: meeting.evidence,
4123
4131
  });
4124
4132
  if (matches.length >= boundedLimit)
4125
4133
  break;
@@ -4311,6 +4319,17 @@ if (hasFeature(CLI_CAPABILITIES, "get_moment"))
4311
4319
  _meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "context", kind: "get_moment" },
4312
4320
  };
4313
4321
  });
4322
+ /** Temporal projection consumes only the exact authorized corpus snapshot. */
4323
+ export function policyDecisionHistory(snapshot) {
4324
+ const sources = snapshot.files.flatMap((file) => {
4325
+ if (!isActiveCorpusMeetingPath(file.path, snapshot.canonicalRoot))
4326
+ return [];
4327
+ const meeting = parsePolicyVerifiedMeeting(file.content, file.path);
4328
+ return !meeting || meetingSensitivity(meeting) === "restricted"
4329
+ ? [] : [{ ...meeting, content: file.content }];
4330
+ });
4331
+ return reader.buildDecisionHistory(sources);
4332
+ }
4314
4333
  // ── Tool: consistency_report ───────────────────────────────
4315
4334
  // ── Tool: get_screen_context ───────────────────────────────
4316
4335
  // Direct image content is returned only for paths that the CLI resolved from
@@ -4445,14 +4464,18 @@ registerDocsAppTool("consistency_report", {
4445
4464
  annotations: { title: "Consistency Report", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4446
4465
  _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
4447
4466
  }, async ({ owner, stale_after_days }) => {
4467
+ const meetingsDir = await getEffectiveMeetingsDir();
4448
4468
  if (!(await isCliAvailable())) {
4449
- return { content: [{ type: "text", text: `Consistency reports require the full CLI for structured intent analysis.\n\n${CLI_INSTALL_MSG}` }] };
4469
+ const decisionHistory = await withStableCorpusLease(meetingsDir, policyDecisionHistory);
4470
+ return {
4471
+ content: [{ type: "text", text: `Decision history: ${decisionHistory.length} sourced records. Full stale-commitment analysis requires the Minutes CLI.` }],
4472
+ structuredContent: { decision_conflicts: [], stale_commitments: [], decision_history: decisionHistory, analysis_available: false, view: "report" },
4473
+ };
4450
4474
  }
4451
4475
  const capabilities = await ensureCliCapabilities(["consistency_corpus_root_v1"]);
4452
4476
  if (capabilities.kind !== "report" || !hasFeature(capabilities, "consistency_corpus_root_v1")) {
4453
4477
  throw new Error("Consistency reports require a Minutes CLI that supports the selected meeting folder. Update Minutes, then try again.");
4454
4478
  }
4455
- const meetingsDir = await getEffectiveMeetingsDir();
4456
4479
  const args = ["consistency", "--dir", meetingsDir, "--stale-after-days", String(stale_after_days)];
4457
4480
  if (owner)
4458
4481
  args.push("--owner", owner);
@@ -4461,6 +4484,7 @@ registerDocsAppTool("consistency_report", {
4461
4484
  if (!report || typeof report !== "object") {
4462
4485
  return { content: [{ type: "text", text: stderr || stdout }] };
4463
4486
  }
4487
+ const decisionHistory = await withStableCorpusLease(meetingsDir, policyDecisionHistory);
4464
4488
  const decisionConflicts = Array.isArray(report.decision_conflicts)
4465
4489
  ? report.decision_conflicts
4466
4490
  : [];
@@ -4470,7 +4494,7 @@ registerDocsAppTool("consistency_report", {
4470
4494
  if (decisionConflicts.length === 0 && staleCommitments.length === 0) {
4471
4495
  return {
4472
4496
  content: [{ type: "text", text: "No consistency issues found." }],
4473
- structuredContent: { decision_conflicts: [], stale_commitments: [], view: "report" },
4497
+ structuredContent: { decision_conflicts: [], stale_commitments: [], decision_history: decisionHistory, view: "report" },
4474
4498
  _meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "report" },
4475
4499
  };
4476
4500
  }
@@ -4489,7 +4513,7 @@ registerDocsAppTool("consistency_report", {
4489
4513
  }
4490
4514
  return {
4491
4515
  content: [{ type: "text", text: sections.join("\n\n") }],
4492
- structuredContent: { decision_conflicts: decisionConflicts, stale_commitments: staleCommitments, view: "report" },
4516
+ structuredContent: { decision_conflicts: decisionConflicts, stale_commitments: staleCommitments, decision_history: decisionHistory, view: "report" },
4493
4517
  _meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "report" },
4494
4518
  };
4495
4519
  });
@@ -4585,14 +4609,14 @@ registerTool("research_topic", "Research a topic across policy-authorized meetin
4585
4609
  const needle = query.trim().toLowerCase();
4586
4610
  const meetingsDir = await getEffectiveMeetingsDir();
4587
4611
  const sourceMeetings = await policyListMeetings(meetingsDir, MCP_POLICY_MEETING_RESULT_MAX, include_restricted);
4612
+ const identity = attendee === undefined ? undefined : reader.resolvePersonIdentity(sourceMeetings, attendee);
4588
4613
  const meetings = [];
4589
4614
  if (needle.length > 0) {
4590
4615
  for (const meeting of sourceMeetings) {
4591
4616
  if ((contentType && meeting.frontmatter.type !== contentType) ||
4592
4617
  !meetingMatchesSince(meeting, since) ||
4593
- !meetingMatchesPerson(meeting, attendee) ||
4594
- (!meeting.frontmatter.title.toLowerCase().includes(needle) &&
4595
- !meeting.body.toLowerCase().includes(needle))) {
4618
+ (attendee !== undefined && (!identity || !reader.meetingMentionsPerson(meeting, identity))) ||
4619
+ !reader.lexicalMatch(meeting.frontmatter.title, meeting.body, query)) {
4596
4620
  continue;
4597
4621
  }
4598
4622
  meetings.push(meeting);
@@ -4699,10 +4723,13 @@ registerDocsAppTool("get_meeting", {
4699
4723
  // Non-fatal: fall through to raw content with no speaker_map enrichment.
4700
4724
  }
4701
4725
  }
4702
- let structuredOut = structured;
4726
+ let structuredOut = {
4727
+ ...structured,
4728
+ evidence: reader.buildSourceEvidence({ path: resolved, content: rawContent, body: rawParsed.body, speakerMap: rawParsed.frontmatter.speaker_map }),
4729
+ };
4703
4730
  if (include_restricted && meetingSensitivity(sensitivityParsed) === "restricted") {
4704
4731
  structuredOut = {
4705
- ...structured,
4732
+ ...structuredOut,
4706
4733
  sensitivity: "restricted",
4707
4734
  sensitivity_override: { applied: true, logged: "durable-jsonl" },
4708
4735
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minutes-mcp",
3
- "version": "0.28.2",
3
+ "version": "0.29.1",
4
4
  "mcpName": "io.github.silverstein/minutes",
5
5
  "description": "MCP server for minutes \u2014 conversation memory for AI assistants. Works with Claude Desktop, Mistral Vibe, Cursor, Windsurf, and any MCP client.",
6
6
  "main": "dist/index.js",
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/ext-apps": "^2.0.0",
49
49
  "@modelcontextprotocol/sdk": "^1.31.0",
50
- "minutes-sdk": "0.28.2",
50
+ "minutes-sdk": "0.29.1",
51
51
  "yaml": "^2.8.3",
52
52
  "zod": "^3.25 || ^4.0"
53
53
  },