minutes-mcp 0.6.0 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +207 -59
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* - process_audio: Process an audio file through the pipeline
|
|
13
13
|
* - add_note: Add a timestamped note to a recording or meeting
|
|
14
14
|
* - consistency_report: Flag conflicting decisions and stale commitments
|
|
15
|
-
* - get_person_profile:
|
|
15
|
+
* - get_person_profile: Rich relationship profile for a person (graph index)
|
|
16
|
+
* - track_commitments: List open/stale commitments, filter by person
|
|
17
|
+
* - relationship_map: All contacts with scores and losing-touch alerts
|
|
16
18
|
* - research_topic: Cross-meeting topic research
|
|
17
19
|
* - qmd_collection_status: Check QMD collection registration
|
|
18
20
|
* - register_qmd_collection: Register Minutes output as QMD collection
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* - process_audio: Process an audio file through the pipeline
|
|
13
13
|
* - add_note: Add a timestamped note to a recording or meeting
|
|
14
14
|
* - consistency_report: Flag conflicting decisions and stale commitments
|
|
15
|
-
* - get_person_profile:
|
|
15
|
+
* - get_person_profile: Rich relationship profile for a person (graph index)
|
|
16
|
+
* - track_commitments: List open/stale commitments, filter by person
|
|
17
|
+
* - relationship_map: All contacts with scores and losing-touch alerts
|
|
16
18
|
* - research_topic: Cross-meeting topic research
|
|
17
19
|
* - qmd_collection_status: Check QMD collection registration
|
|
18
20
|
* - register_qmd_collection: Register Minutes output as QMD collection
|
|
@@ -237,7 +239,7 @@ function validatePathInDirectories(path, roots, allowedExts) {
|
|
|
237
239
|
// ── MCP Server ──────────────────────────────────────────────
|
|
238
240
|
const server = new McpServer({
|
|
239
241
|
name: "minutes",
|
|
240
|
-
version: "0.
|
|
242
|
+
version: "0.7.3",
|
|
241
243
|
});
|
|
242
244
|
// Configurable directories — override via env vars in Claude Desktop extension settings
|
|
243
245
|
const MEETINGS_DIR = process.env.MEETINGS_DIR || join(homedir(), "meetings");
|
|
@@ -583,74 +585,84 @@ registerAppTool(server, "consistency_report", {
|
|
|
583
585
|
});
|
|
584
586
|
// ── Tool: get_person_profile ───────────────────────────────
|
|
585
587
|
registerAppTool(server, "get_person_profile", {
|
|
586
|
-
description: "
|
|
588
|
+
description: "Get a rich relationship profile for a person: meetings, commitments, topics, relationship score, and trend. Uses the conversation graph index for instant results.",
|
|
587
589
|
inputSchema: {
|
|
588
590
|
name: z.string().describe("Person / attendee name to profile"),
|
|
589
591
|
},
|
|
590
592
|
annotations: { title: "Person Profile", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
591
593
|
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
592
594
|
}, async ({ name }) => {
|
|
593
|
-
//
|
|
594
|
-
if (
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
595
|
+
// Try graph index first (via CLI `minutes people --json`)
|
|
596
|
+
if (await isCliAvailable()) {
|
|
597
|
+
const { stdout } = await runMinutes(["people", "--json"]);
|
|
598
|
+
const people = parseJsonOutput(stdout);
|
|
599
|
+
if (Array.isArray(people)) {
|
|
600
|
+
const nameLower = name.toLowerCase();
|
|
601
|
+
const match = people.find((p) => p.name?.toLowerCase().includes(nameLower) ||
|
|
602
|
+
p.slug?.toLowerCase().includes(nameLower));
|
|
603
|
+
if (match) {
|
|
604
|
+
const daysSince = Math.round(match.days_since || 0);
|
|
605
|
+
const last = daysSince < 1 ? "today" : daysSince < 2 ? "yesterday" : `${daysSince}d ago`;
|
|
606
|
+
const sections = [];
|
|
607
|
+
sections.push(`Relationship score: ${(match.score || 0).toFixed(1)} | ${match.meeting_count} meetings | last: ${last}`);
|
|
608
|
+
if (match.losing_touch) {
|
|
609
|
+
sections.push("⚠ LOSING TOUCH — meeting frequency has declined");
|
|
610
|
+
}
|
|
611
|
+
if (match.top_topics?.length > 0) {
|
|
612
|
+
sections.push("Top topics: " + match.top_topics.join(", "));
|
|
613
|
+
}
|
|
614
|
+
if (match.open_commitments > 0) {
|
|
615
|
+
sections.push(`Open commitments: ${match.open_commitments}`);
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
content: [{ type: "text", text: `Profile for ${match.name}:\n\n${sections.join("\n")}` }],
|
|
619
|
+
structuredContent: { ...match, view: "person" },
|
|
620
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
621
|
+
};
|
|
622
|
+
}
|
|
601
623
|
}
|
|
602
|
-
|
|
603
|
-
|
|
624
|
+
// Fall back to legacy CLI person command for richer meeting-level data
|
|
625
|
+
const { stdout: legacyOut, stderr } = await runMinutes(["person", name]);
|
|
626
|
+
const profile = parseJsonOutput(legacyOut);
|
|
627
|
+
if (profile && typeof profile === "object") {
|
|
628
|
+
const topics = Array.isArray(profile.top_topics) ? profile.top_topics : [];
|
|
629
|
+
const openIntents = Array.isArray(profile.open_intents) ? profile.open_intents : [];
|
|
630
|
+
const recentMeetings = Array.isArray(profile.recent_meetings) ? profile.recent_meetings : [];
|
|
631
|
+
if (topics.length === 0 && openIntents.length === 0 && recentMeetings.length === 0) {
|
|
632
|
+
return {
|
|
633
|
+
content: [{ type: "text", text: `No profile data found for ${name}.` }],
|
|
634
|
+
structuredContent: { name, top_topics: [], open_intents: [], recent_meetings: [], view: "person" },
|
|
635
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
const sections = [];
|
|
639
|
+
if (topics.length > 0)
|
|
640
|
+
sections.push("Top topics:\n" + topics.map((t) => `- ${t.topic} (${t.count})`).join("\n"));
|
|
641
|
+
if (openIntents.length > 0)
|
|
642
|
+
sections.push("Open commitments:\n" + openIntents.map((i) => `- ${i.kind}: ${i.what}${i.by_date ? ` by ${i.by_date}` : ""}`).join("\n"));
|
|
643
|
+
if (recentMeetings.length > 0)
|
|
644
|
+
sections.push("Recent meetings:\n" + recentMeetings.map((m) => `- ${m.date} — ${m.title}`).join("\n"));
|
|
645
|
+
return {
|
|
646
|
+
content: [{ type: "text", text: `Profile for ${profile.name}:\n\n${sections.join("\n\n")}` }],
|
|
647
|
+
structuredContent: { ...profile, view: "person" },
|
|
648
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
649
|
+
};
|
|
604
650
|
}
|
|
605
|
-
|
|
606
|
-
return {
|
|
607
|
-
content: [{ type: "text", text }],
|
|
608
|
-
structuredContent: { name, top_topics: profile.topics.map((t) => ({ topic: t, count: 1 })), open_intents: profile.openActions, recent_meetings: profile.meetings, view: "person" },
|
|
609
|
-
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
const { stdout, stderr } = await runMinutes(["person", name]);
|
|
613
|
-
const profile = parseJsonOutput(stdout);
|
|
614
|
-
if (!profile || typeof profile !== "object") {
|
|
615
|
-
return { content: [{ type: "text", text: stderr || stdout }] };
|
|
616
|
-
}
|
|
617
|
-
const topics = Array.isArray(profile.top_topics) ? profile.top_topics : [];
|
|
618
|
-
const openIntents = Array.isArray(profile.open_intents) ? profile.open_intents : [];
|
|
619
|
-
const recentMeetings = Array.isArray(profile.recent_meetings)
|
|
620
|
-
? profile.recent_meetings
|
|
621
|
-
: [];
|
|
622
|
-
if (topics.length === 0 && openIntents.length === 0 && recentMeetings.length === 0) {
|
|
623
|
-
return {
|
|
624
|
-
content: [{ type: "text", text: `No profile data found for ${name}.` }],
|
|
625
|
-
structuredContent: { name, top_topics: [], open_intents: [], recent_meetings: [], view: "person" },
|
|
626
|
-
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
627
|
-
};
|
|
651
|
+
return { content: [{ type: "text", text: stderr || legacyOut || `No data found for ${name}.` }] };
|
|
628
652
|
}
|
|
653
|
+
// Pure-TS fallback when CLI is not available
|
|
654
|
+
const profile = await reader.getPersonProfile(MEETINGS_DIR, name);
|
|
629
655
|
const sections = [];
|
|
630
|
-
if (topics.length > 0)
|
|
631
|
-
sections.push("
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (
|
|
635
|
-
sections.push("Open
|
|
636
|
-
|
|
637
|
-
.map((intent) => `- ${intent.kind}: ${intent.what}${intent.by_date ? ` by ${intent.by_date}` : ""}`)
|
|
638
|
-
.join("\n"));
|
|
639
|
-
}
|
|
640
|
-
if (recentMeetings.length > 0) {
|
|
641
|
-
sections.push("Recent meetings:\n" +
|
|
642
|
-
recentMeetings
|
|
643
|
-
.map((meeting) => `- ${meeting.date} — ${meeting.title}`)
|
|
644
|
-
.join("\n"));
|
|
645
|
-
}
|
|
656
|
+
if (profile.topics.length > 0)
|
|
657
|
+
sections.push("Topics: " + profile.topics.join(", "));
|
|
658
|
+
if (profile.meetings.length > 0)
|
|
659
|
+
sections.push("Meetings:\n" + profile.meetings.map((m) => `- ${m.date} — ${m.title}`).join("\n"));
|
|
660
|
+
if (profile.openActions.length > 0)
|
|
661
|
+
sections.push("Open actions:\n" + profile.openActions.map((a) => `- ${a.task} (${a.status})`).join("\n"));
|
|
662
|
+
const text = sections.length > 0 ? sections.join("\n\n") : `No profile data found for ${name}.`;
|
|
646
663
|
return {
|
|
647
|
-
content: [
|
|
648
|
-
|
|
649
|
-
type: "text",
|
|
650
|
-
text: `Profile for ${profile.name}:\n\n${sections.join("\n\n")}`,
|
|
651
|
-
},
|
|
652
|
-
],
|
|
653
|
-
structuredContent: { ...profile, view: "person" },
|
|
664
|
+
content: [{ type: "text", text }],
|
|
665
|
+
structuredContent: { name, top_topics: profile.topics.map((t) => ({ topic: t, count: 1 })), open_intents: profile.openActions, recent_meetings: profile.meetings, view: "person" },
|
|
654
666
|
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
655
667
|
};
|
|
656
668
|
});
|
|
@@ -912,6 +924,102 @@ server.tool("register_qmd_collection", "Register the Minutes output directory as
|
|
|
912
924
|
],
|
|
913
925
|
};
|
|
914
926
|
});
|
|
927
|
+
// ── Tool: track_commitments ─────────────────────────────────
|
|
928
|
+
registerAppTool(server, "track_commitments", {
|
|
929
|
+
description: "List open and stale commitments (action items, intents, decisions) across all meetings. Optionally filter by person. Answers: 'What did I promise Sarah?' or 'What's overdue?'",
|
|
930
|
+
inputSchema: {
|
|
931
|
+
person: z.string().optional().describe("Filter by person name or slug (optional — omit for all commitments)"),
|
|
932
|
+
},
|
|
933
|
+
annotations: { title: "Track Commitments", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
934
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
935
|
+
}, async ({ person }) => {
|
|
936
|
+
// Use CLI: minutes people --json (rebuild if needed, read from SQLite)
|
|
937
|
+
const args = ["people", "--json"];
|
|
938
|
+
if (!(await isCliAvailable())) {
|
|
939
|
+
return { content: [{ type: "text", text: "Minutes CLI not available. Install with: cargo install minutes-cli" }] };
|
|
940
|
+
}
|
|
941
|
+
// Get full people data and extract commitments
|
|
942
|
+
const { stdout } = await runMinutes(args);
|
|
943
|
+
const people = parseJsonOutput(stdout);
|
|
944
|
+
if (!Array.isArray(people)) {
|
|
945
|
+
return { content: [{ type: "text", text: "No relationship data found. Run: minutes people --rebuild" }] };
|
|
946
|
+
}
|
|
947
|
+
// Filter to the requested person if specified
|
|
948
|
+
let relevantPeople = people;
|
|
949
|
+
if (person) {
|
|
950
|
+
const personLower = person.toLowerCase();
|
|
951
|
+
relevantPeople = people.filter((p) => p.name?.toLowerCase().includes(personLower) ||
|
|
952
|
+
p.slug?.toLowerCase().includes(personLower));
|
|
953
|
+
}
|
|
954
|
+
// Build commitment summary from open_commitments counts
|
|
955
|
+
const sections = [];
|
|
956
|
+
const withCommitments = relevantPeople.filter((p) => p.open_commitments > 0);
|
|
957
|
+
if (withCommitments.length === 0) {
|
|
958
|
+
const scope = person ? ` for ${person}` : "";
|
|
959
|
+
return {
|
|
960
|
+
content: [{ type: "text", text: `No open commitments found${scope}.` }],
|
|
961
|
+
structuredContent: { commitments: [], person: person || null, view: "commitments" },
|
|
962
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "commitments" },
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
for (const p of withCommitments) {
|
|
966
|
+
sections.push(`${p.name}: ${p.open_commitments} open commitment${p.open_commitments !== 1 ? "s" : ""} (last seen: ${p.last_seen?.split("T")[0] || "unknown"})`);
|
|
967
|
+
}
|
|
968
|
+
const text = `Open commitments${person ? ` for ${person}` : ""}:\n\n${sections.join("\n")}`;
|
|
969
|
+
return {
|
|
970
|
+
content: [{ type: "text", text }],
|
|
971
|
+
structuredContent: { people: withCommitments, person: person || null, view: "commitments" },
|
|
972
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "commitments" },
|
|
973
|
+
};
|
|
974
|
+
});
|
|
975
|
+
// ── Tool: relationship_map ──────────────────────────────────
|
|
976
|
+
registerAppTool(server, "relationship_map", {
|
|
977
|
+
description: "Show all contacts with relationship scores, meeting frequency, and 'losing touch' alerts. Overview of your entire conversation network.",
|
|
978
|
+
inputSchema: {
|
|
979
|
+
limit: z.number().optional().describe("Max people to return (default: 15)"),
|
|
980
|
+
},
|
|
981
|
+
annotations: { title: "Relationship Map", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
982
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
983
|
+
}, async ({ limit }) => {
|
|
984
|
+
if (!(await isCliAvailable())) {
|
|
985
|
+
return { content: [{ type: "text", text: "Minutes CLI not available. Install with: cargo install minutes-cli" }] };
|
|
986
|
+
}
|
|
987
|
+
const maxPeople = limit || 15;
|
|
988
|
+
const { stdout } = await runMinutes(["people", "--json", "--limit", String(maxPeople)]);
|
|
989
|
+
const people = parseJsonOutput(stdout);
|
|
990
|
+
if (!Array.isArray(people) || people.length === 0) {
|
|
991
|
+
return {
|
|
992
|
+
content: [{ type: "text", text: "No relationship data found. Run: minutes people --rebuild" }],
|
|
993
|
+
structuredContent: { people: [], view: "relationship_map" },
|
|
994
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "relationship_map" },
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
// Format human-readable output
|
|
998
|
+
const lines = [];
|
|
999
|
+
const losingTouch = [];
|
|
1000
|
+
for (const p of people) {
|
|
1001
|
+
const daysSince = Math.round(p.days_since || 0);
|
|
1002
|
+
const last = daysSince < 1 ? "today" : daysSince < 2 ? "yesterday" : `${daysSince}d ago`;
|
|
1003
|
+
const status = p.losing_touch
|
|
1004
|
+
? "⚠ losing touch"
|
|
1005
|
+
: p.open_commitments > 0
|
|
1006
|
+
? `${p.open_commitments} open commitment${p.open_commitments !== 1 ? "s" : ""}`
|
|
1007
|
+
: "✓ all clear";
|
|
1008
|
+
lines.push(`${p.name} — ${p.meeting_count} meetings, last: ${last}, ${status} (score: ${(p.score || 0).toFixed(1)})`);
|
|
1009
|
+
if (p.losing_touch) {
|
|
1010
|
+
losingTouch.push(`${p.name} — ${p.meeting_count} meetings total, last seen ${daysSince}d ago`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
let text = `Relationship Map (${people.length} contacts):\n\n${lines.join("\n")}`;
|
|
1014
|
+
if (losingTouch.length > 0) {
|
|
1015
|
+
text += `\n\nLosing Touch:\n${losingTouch.join("\n")}`;
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
content: [{ type: "text", text }],
|
|
1019
|
+
structuredContent: { people, view: "relationship_map" },
|
|
1020
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "relationship_map" },
|
|
1021
|
+
};
|
|
1022
|
+
});
|
|
915
1023
|
// ── Resources ───────────────────────────────────────────────
|
|
916
1024
|
server.resource("recent_meetings", "minutes://meetings/recent", { description: "List of recent meetings and memos" }, async () => {
|
|
917
1025
|
if (!(await isCliAvailable())) {
|
|
@@ -968,6 +1076,46 @@ server.resource("meeting", new ResourceTemplate("minutes://meetings/{slug}", { l
|
|
|
968
1076
|
}
|
|
969
1077
|
return { contents: [{ uri: uri.href, mimeType: "text/plain", text: `Meeting not found: ${slug}` }] };
|
|
970
1078
|
});
|
|
1079
|
+
// ── Resource: recent_ideas (voice memos from last N days) ──
|
|
1080
|
+
server.resource("recent-ideas", "minutes://ideas/recent", { description: "Recent voice memos and ideas captured from any device (last 14 days)" }, async (uri) => {
|
|
1081
|
+
const meetings = await reader.listMeetings(MEETINGS_DIR, 200);
|
|
1082
|
+
const cutoff = new Date();
|
|
1083
|
+
cutoff.setDate(cutoff.getDate() - 14);
|
|
1084
|
+
const memos = meetings.filter((m) => {
|
|
1085
|
+
if (m.frontmatter.type !== "memo")
|
|
1086
|
+
return false;
|
|
1087
|
+
const date = new Date(m.frontmatter.date);
|
|
1088
|
+
return date >= cutoff;
|
|
1089
|
+
});
|
|
1090
|
+
if (memos.length === 0) {
|
|
1091
|
+
return {
|
|
1092
|
+
contents: [{
|
|
1093
|
+
uri: uri.href,
|
|
1094
|
+
mimeType: "text/plain",
|
|
1095
|
+
text: "No voice memos in the last 14 days.",
|
|
1096
|
+
}],
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
const lines = memos
|
|
1100
|
+
.sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime())
|
|
1101
|
+
.slice(0, 20)
|
|
1102
|
+
.map((m) => {
|
|
1103
|
+
const date = new Date(m.frontmatter.date).toLocaleDateString("en-US", {
|
|
1104
|
+
month: "short",
|
|
1105
|
+
day: "numeric",
|
|
1106
|
+
});
|
|
1107
|
+
const device = m.frontmatter.device ? ` (${m.frontmatter.device})` : "";
|
|
1108
|
+
return `- [${date}] ${m.frontmatter.title}${device} — ${m.frontmatter.duration}`;
|
|
1109
|
+
})
|
|
1110
|
+
.join("\n");
|
|
1111
|
+
return {
|
|
1112
|
+
contents: [{
|
|
1113
|
+
uri: uri.href,
|
|
1114
|
+
mimeType: "text/plain",
|
|
1115
|
+
text: `Recent voice memos (${memos.length} in last 14 days):\n\n${lines}`,
|
|
1116
|
+
}],
|
|
1117
|
+
};
|
|
1118
|
+
});
|
|
971
1119
|
// ── Tool: start_dictation ──────────────────────────────────
|
|
972
1120
|
server.tool("start_dictation", "Start dictation mode. Speak naturally — text goes to clipboard and daily note after each pause. Runs until stop_dictation is called or silence timeout.", {}, { title: "Start Dictation", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async () => {
|
|
973
1121
|
if (!(await isCliAvailable())) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minutes-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "MCP server for minutes — conversation memory for AI assistants. Works with Claude Desktop, Cursor, Windsurf, and any MCP client.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"dev": "tsx src/index.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
42
41
|
"@modelcontextprotocol/ext-apps": "^1.2.2",
|
|
43
|
-
"
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
43
|
+
"minutes-sdk": "^0.7.0",
|
|
44
44
|
"yaml": "^2.8.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|