minutes-mcp 0.7.0 → 0.8.0
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 +5 -1
- package/dist/index.js +228 -60
- package/dist-ui/index.html +95 -72
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -12,10 +12,14 @@
|
|
|
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
|
|
21
|
+
* - list_voices: List enrolled voice profiles for speaker identification
|
|
22
|
+
* - confirm_speaker: Confirm/correct speaker attribution in a meeting
|
|
19
23
|
*
|
|
20
24
|
* All tools use execFile (not exec) to shell out to the `minutes` CLI binary.
|
|
21
25
|
* No shell interpolation — safe from injection.
|
package/dist/index.js
CHANGED
|
@@ -12,17 +12,21 @@
|
|
|
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
|
|
21
|
+
* - list_voices: List enrolled voice profiles for speaker identification
|
|
22
|
+
* - confirm_speaker: Confirm/correct speaker attribution in a meeting
|
|
19
23
|
*
|
|
20
24
|
* All tools use execFile (not exec) to shell out to the `minutes` CLI binary.
|
|
21
25
|
* No shell interpolation — safe from injection.
|
|
22
26
|
*/
|
|
23
27
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
28
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
|
-
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
|
|
29
|
+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, EXTENSION_ID, } from "@modelcontextprotocol/ext-apps/server";
|
|
26
30
|
import { z } from "zod";
|
|
27
31
|
import { execFile, spawn } from "child_process";
|
|
28
32
|
import { promisify } from "util";
|
|
@@ -237,7 +241,13 @@ function validatePathInDirectories(path, roots, allowedExts) {
|
|
|
237
241
|
// ── MCP Server ──────────────────────────────────────────────
|
|
238
242
|
const server = new McpServer({
|
|
239
243
|
name: "minutes",
|
|
240
|
-
version: "0.
|
|
244
|
+
version: "0.8.0",
|
|
245
|
+
});
|
|
246
|
+
// Declare MCP Apps extension support so hosts classify this server as interactive.
|
|
247
|
+
// The `extensions` field is part of the draft MCP spec (SEP-1724) — not yet in the
|
|
248
|
+
// stable SDK types, so we cast through `any`.
|
|
249
|
+
server.server.registerCapabilities({
|
|
250
|
+
extensions: { [EXTENSION_ID]: {} },
|
|
241
251
|
});
|
|
242
252
|
// Configurable directories — override via env vars in Claude Desktop extension settings
|
|
243
253
|
const MEETINGS_DIR = process.env.MEETINGS_DIR || join(homedir(), "meetings");
|
|
@@ -583,74 +593,84 @@ registerAppTool(server, "consistency_report", {
|
|
|
583
593
|
});
|
|
584
594
|
// ── Tool: get_person_profile ───────────────────────────────
|
|
585
595
|
registerAppTool(server, "get_person_profile", {
|
|
586
|
-
description: "
|
|
596
|
+
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
597
|
inputSchema: {
|
|
588
598
|
name: z.string().describe("Person / attendee name to profile"),
|
|
589
599
|
},
|
|
590
600
|
annotations: { title: "Person Profile", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
591
601
|
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
592
602
|
}, async ({ name }) => {
|
|
593
|
-
//
|
|
594
|
-
if (
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
603
|
+
// Try graph index first (via CLI `minutes people --json`)
|
|
604
|
+
if (await isCliAvailable()) {
|
|
605
|
+
const { stdout } = await runMinutes(["people", "--json"]);
|
|
606
|
+
const people = parseJsonOutput(stdout);
|
|
607
|
+
if (Array.isArray(people)) {
|
|
608
|
+
const nameLower = name.toLowerCase();
|
|
609
|
+
const match = people.find((p) => p.name?.toLowerCase().includes(nameLower) ||
|
|
610
|
+
p.slug?.toLowerCase().includes(nameLower));
|
|
611
|
+
if (match) {
|
|
612
|
+
const daysSince = Math.round(match.days_since || 0);
|
|
613
|
+
const last = daysSince < 1 ? "today" : daysSince < 2 ? "yesterday" : `${daysSince}d ago`;
|
|
614
|
+
const sections = [];
|
|
615
|
+
sections.push(`Relationship score: ${(match.score || 0).toFixed(1)} | ${match.meeting_count} meetings | last: ${last}`);
|
|
616
|
+
if (match.losing_touch) {
|
|
617
|
+
sections.push("⚠ LOSING TOUCH — meeting frequency has declined");
|
|
618
|
+
}
|
|
619
|
+
if (match.top_topics?.length > 0) {
|
|
620
|
+
sections.push("Top topics: " + match.top_topics.join(", "));
|
|
621
|
+
}
|
|
622
|
+
if (match.open_commitments > 0) {
|
|
623
|
+
sections.push(`Open commitments: ${match.open_commitments}`);
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
content: [{ type: "text", text: `Profile for ${match.name}:\n\n${sections.join("\n")}` }],
|
|
627
|
+
structuredContent: { ...match, view: "person" },
|
|
628
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
629
|
+
};
|
|
630
|
+
}
|
|
601
631
|
}
|
|
602
|
-
|
|
603
|
-
|
|
632
|
+
// Fall back to legacy CLI person command for richer meeting-level data
|
|
633
|
+
const { stdout: legacyOut, stderr } = await runMinutes(["person", name]);
|
|
634
|
+
const profile = parseJsonOutput(legacyOut);
|
|
635
|
+
if (profile && typeof profile === "object") {
|
|
636
|
+
const topics = Array.isArray(profile.top_topics) ? profile.top_topics : [];
|
|
637
|
+
const openIntents = Array.isArray(profile.open_intents) ? profile.open_intents : [];
|
|
638
|
+
const recentMeetings = Array.isArray(profile.recent_meetings) ? profile.recent_meetings : [];
|
|
639
|
+
if (topics.length === 0 && openIntents.length === 0 && recentMeetings.length === 0) {
|
|
640
|
+
return {
|
|
641
|
+
content: [{ type: "text", text: `No profile data found for ${name}.` }],
|
|
642
|
+
structuredContent: { name, top_topics: [], open_intents: [], recent_meetings: [], view: "person" },
|
|
643
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const sections = [];
|
|
647
|
+
if (topics.length > 0)
|
|
648
|
+
sections.push("Top topics:\n" + topics.map((t) => `- ${t.topic} (${t.count})`).join("\n"));
|
|
649
|
+
if (openIntents.length > 0)
|
|
650
|
+
sections.push("Open commitments:\n" + openIntents.map((i) => `- ${i.kind}: ${i.what}${i.by_date ? ` by ${i.by_date}` : ""}`).join("\n"));
|
|
651
|
+
if (recentMeetings.length > 0)
|
|
652
|
+
sections.push("Recent meetings:\n" + recentMeetings.map((m) => `- ${m.date} — ${m.title}`).join("\n"));
|
|
653
|
+
return {
|
|
654
|
+
content: [{ type: "text", text: `Profile for ${profile.name}:\n\n${sections.join("\n\n")}` }],
|
|
655
|
+
structuredContent: { ...profile, view: "person" },
|
|
656
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
657
|
+
};
|
|
604
658
|
}
|
|
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
|
-
};
|
|
659
|
+
return { content: [{ type: "text", text: stderr || legacyOut || `No data found for ${name}.` }] };
|
|
628
660
|
}
|
|
661
|
+
// Pure-TS fallback when CLI is not available
|
|
662
|
+
const profile = await reader.getPersonProfile(MEETINGS_DIR, name);
|
|
629
663
|
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
|
-
}
|
|
664
|
+
if (profile.topics.length > 0)
|
|
665
|
+
sections.push("Topics: " + profile.topics.join(", "));
|
|
666
|
+
if (profile.meetings.length > 0)
|
|
667
|
+
sections.push("Meetings:\n" + profile.meetings.map((m) => `- ${m.date} — ${m.title}`).join("\n"));
|
|
668
|
+
if (profile.openActions.length > 0)
|
|
669
|
+
sections.push("Open actions:\n" + profile.openActions.map((a) => `- ${a.task} (${a.status})`).join("\n"));
|
|
670
|
+
const text = sections.length > 0 ? sections.join("\n\n") : `No profile data found for ${name}.`;
|
|
646
671
|
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" },
|
|
672
|
+
content: [{ type: "text", text }],
|
|
673
|
+
structuredContent: { name, top_topics: profile.topics.map((t) => ({ topic: t, count: 1 })), open_intents: profile.openActions, recent_meetings: profile.meetings, view: "person" },
|
|
654
674
|
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
|
|
655
675
|
};
|
|
656
676
|
});
|
|
@@ -912,6 +932,107 @@ server.tool("register_qmd_collection", "Register the Minutes output directory as
|
|
|
912
932
|
],
|
|
913
933
|
};
|
|
914
934
|
});
|
|
935
|
+
// ── Tool: track_commitments ─────────────────────────────────
|
|
936
|
+
registerAppTool(server, "track_commitments", {
|
|
937
|
+
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?'",
|
|
938
|
+
inputSchema: {
|
|
939
|
+
person: z.string().optional().describe("Filter by person name or slug (optional — omit for all commitments)"),
|
|
940
|
+
},
|
|
941
|
+
annotations: { title: "Track Commitments", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
942
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
943
|
+
}, async ({ person }) => {
|
|
944
|
+
if (!(await isCliAvailable())) {
|
|
945
|
+
return { content: [{ type: "text", text: "Minutes CLI not available. Install with: cargo install minutes-cli" }] };
|
|
946
|
+
}
|
|
947
|
+
// Use dedicated commitments command for full text detail
|
|
948
|
+
const args = ["commitments", "--json"];
|
|
949
|
+
if (person)
|
|
950
|
+
args.push("--person", person);
|
|
951
|
+
const { stdout } = await runMinutes(args);
|
|
952
|
+
const commitments = parseJsonOutput(stdout);
|
|
953
|
+
if (!Array.isArray(commitments) || commitments.length === 0) {
|
|
954
|
+
const scope = person ? ` for ${person}` : "";
|
|
955
|
+
return {
|
|
956
|
+
content: [{ type: "text", text: `No open commitments found${scope}.` }],
|
|
957
|
+
structuredContent: { commitments: [], person: person || null, view: "commitments" },
|
|
958
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "commitments" },
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
// Group by status
|
|
962
|
+
const stale = commitments.filter((c) => c.status === "stale");
|
|
963
|
+
const open = commitments.filter((c) => c.status === "open");
|
|
964
|
+
const lines = [];
|
|
965
|
+
if (stale.length > 0) {
|
|
966
|
+
lines.push(`STALE (${stale.length} overdue):`);
|
|
967
|
+
for (const c of stale) {
|
|
968
|
+
const who = c.person_name || "unassigned";
|
|
969
|
+
lines.push(` ⚠ ${c.text} (${who}; due: ${c.due_date || "no date"}; from: ${c.meeting_title})`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (open.length > 0) {
|
|
973
|
+
if (stale.length > 0)
|
|
974
|
+
lines.push("");
|
|
975
|
+
lines.push(`OPEN (${open.length}):`);
|
|
976
|
+
for (const c of open) {
|
|
977
|
+
const who = c.person_name || "unassigned";
|
|
978
|
+
lines.push(` · ${c.text} (${who}; from: ${c.meeting_title})`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const text = `Commitments${person ? ` for ${person}` : ""}:\n\n${lines.join("\n")}`;
|
|
982
|
+
return {
|
|
983
|
+
content: [{ type: "text", text }],
|
|
984
|
+
structuredContent: { commitments, person: person || null, stale_count: stale.length, open_count: open.length, view: "commitments" },
|
|
985
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "commitments" },
|
|
986
|
+
};
|
|
987
|
+
});
|
|
988
|
+
// ── Tool: relationship_map ──────────────────────────────────
|
|
989
|
+
registerAppTool(server, "relationship_map", {
|
|
990
|
+
description: "Show all contacts with relationship scores, meeting frequency, and 'losing touch' alerts. Overview of your entire conversation network.",
|
|
991
|
+
inputSchema: {
|
|
992
|
+
limit: z.number().optional().describe("Max people to return (default: 15)"),
|
|
993
|
+
},
|
|
994
|
+
annotations: { title: "Relationship Map", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
995
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
996
|
+
}, async ({ limit }) => {
|
|
997
|
+
if (!(await isCliAvailable())) {
|
|
998
|
+
return { content: [{ type: "text", text: "Minutes CLI not available. Install with: cargo install minutes-cli" }] };
|
|
999
|
+
}
|
|
1000
|
+
const maxPeople = limit || 15;
|
|
1001
|
+
const { stdout } = await runMinutes(["people", "--json", "--limit", String(maxPeople)]);
|
|
1002
|
+
const people = parseJsonOutput(stdout);
|
|
1003
|
+
if (!Array.isArray(people) || people.length === 0) {
|
|
1004
|
+
return {
|
|
1005
|
+
content: [{ type: "text", text: "No relationship data found. Run: minutes people --rebuild" }],
|
|
1006
|
+
structuredContent: { people: [], view: "relationship_map" },
|
|
1007
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "relationship_map" },
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
// Format human-readable output
|
|
1011
|
+
const lines = [];
|
|
1012
|
+
const losingTouch = [];
|
|
1013
|
+
for (const p of people) {
|
|
1014
|
+
const daysSince = Math.round(p.days_since || 0);
|
|
1015
|
+
const last = daysSince < 1 ? "today" : daysSince < 2 ? "yesterday" : `${daysSince}d ago`;
|
|
1016
|
+
const status = p.losing_touch
|
|
1017
|
+
? "⚠ losing touch"
|
|
1018
|
+
: p.open_commitments > 0
|
|
1019
|
+
? `${p.open_commitments} open commitment${p.open_commitments !== 1 ? "s" : ""}`
|
|
1020
|
+
: "✓ all clear";
|
|
1021
|
+
lines.push(`${p.name} — ${p.meeting_count} meetings, last: ${last}, ${status} (score: ${(p.score || 0).toFixed(1)})`);
|
|
1022
|
+
if (p.losing_touch) {
|
|
1023
|
+
losingTouch.push(`${p.name} — ${p.meeting_count} meetings total, last seen ${daysSince}d ago`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
let text = `Relationship Map (${people.length} contacts):\n\n${lines.join("\n")}`;
|
|
1027
|
+
if (losingTouch.length > 0) {
|
|
1028
|
+
text += `\n\nLosing Touch:\n${losingTouch.join("\n")}`;
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
content: [{ type: "text", text }],
|
|
1032
|
+
structuredContent: { people, view: "relationship_map" },
|
|
1033
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "relationship_map" },
|
|
1034
|
+
};
|
|
1035
|
+
});
|
|
915
1036
|
// ── Resources ───────────────────────────────────────────────
|
|
916
1037
|
server.resource("recent_meetings", "minutes://meetings/recent", { description: "List of recent meetings and memos" }, async () => {
|
|
917
1038
|
if (!(await isCliAvailable())) {
|
|
@@ -1069,6 +1190,53 @@ server.tool("stop_dictation", "Stop the current dictation session.", {}, { title
|
|
|
1069
1190
|
],
|
|
1070
1191
|
};
|
|
1071
1192
|
});
|
|
1193
|
+
// ── Tool: list_voices ────────────────────────────────────────
|
|
1194
|
+
server.tool("list_voices", "List enrolled voice profiles for speaker identification. Shows who has been enrolled, sample count, and model version.", {}, { title: "Voice Profiles", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => {
|
|
1195
|
+
if (!(await isCliAvailable())) {
|
|
1196
|
+
return { content: [{ type: "text", text: "Minutes CLI not available." }] };
|
|
1197
|
+
}
|
|
1198
|
+
const { stdout, stderr } = await runMinutes(["voices", "--json"]);
|
|
1199
|
+
const profiles = parseJsonOutput(stdout);
|
|
1200
|
+
if (!Array.isArray(profiles) || profiles.length === 0) {
|
|
1201
|
+
return {
|
|
1202
|
+
content: [{ type: "text", text: "No voice profiles enrolled. The user can enroll with: minutes enroll" }],
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
const lines = profiles.map((p) => `${p.name} — ${p.sample_count} samples, ${p.source} (${p.model_version})`);
|
|
1206
|
+
return {
|
|
1207
|
+
content: [{ type: "text", text: `Voice profiles (${profiles.length}):\n\n${lines.join("\n")}` }],
|
|
1208
|
+
structuredContent: { profiles, view: "voices" },
|
|
1209
|
+
};
|
|
1210
|
+
});
|
|
1211
|
+
// ── Tool: confirm_speaker ────────────────────────────────────
|
|
1212
|
+
server.tool("confirm_speaker", "Confirm or correct a speaker attribution in a meeting. Promotes the attribution to High confidence and rewrites the transcript label. Optionally saves the speaker's voice profile for future meetings.", {
|
|
1213
|
+
meeting: z.string().describe("Path to the meeting markdown file"),
|
|
1214
|
+
speaker_label: z.string().describe("Speaker label to confirm (e.g., SPEAKER_1)"),
|
|
1215
|
+
name: z.string().describe("Real name to assign to this speaker"),
|
|
1216
|
+
save_voice: z.boolean().optional().default(false).describe("Save this speaker's voice profile for future automatic identification"),
|
|
1217
|
+
}, { title: "Confirm Speaker", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ meeting, speaker_label, name, save_voice }) => {
|
|
1218
|
+
if (!(await isCliAvailable())) {
|
|
1219
|
+
return { content: [{ type: "text", text: "Minutes CLI not available." }] };
|
|
1220
|
+
}
|
|
1221
|
+
const args = ["confirm", "--meeting", meeting, "--speaker", speaker_label, "--name", name];
|
|
1222
|
+
if (save_voice)
|
|
1223
|
+
args.push("--save-voice");
|
|
1224
|
+
try {
|
|
1225
|
+
const { stdout, stderr } = await runMinutes(args);
|
|
1226
|
+
const output = (stderr || stdout || "").trim();
|
|
1227
|
+
return {
|
|
1228
|
+
content: [{ type: "text", text: output || `Confirmed: ${speaker_label} = ${name}` }],
|
|
1229
|
+
structuredContent: { meeting, speaker_label, name, save_voice, confirmed: true },
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
const msg = error?.stderr || error?.message || String(error);
|
|
1234
|
+
return {
|
|
1235
|
+
content: [{ type: "text", text: `Failed to confirm speaker: ${msg}` }],
|
|
1236
|
+
isError: true,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1072
1240
|
// ── Start server ────────────────────────────────────────────
|
|
1073
1241
|
async function main() {
|
|
1074
1242
|
const transport = new StdioServerTransport();
|