minutes-mcp 0.13.2 → 0.14.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/capabilities.d.ts +74 -0
- package/dist/capabilities.js +131 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +260 -20
- package/dist/version.d.ts +35 -0
- package/dist/version.js +67 -0
- package/dist-ui/index.html +92 -59
- package/fixtures/demo/2026-02-28-pricing-strategy.md +47 -0
- package/fixtures/demo/2026-03-04-northwind-call.md +43 -0
- package/fixtures/demo/2026-03-11-eng-standup.md +37 -0
- package/fixtures/demo/2026-03-25-pricing-reversal.md +44 -0
- package/fixtures/demo/2026-04-17-prioritization.md +52 -0
- package/package.json +3 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI capabilities feature-detection probe.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 of #183: instead of guessing which MCP tools to expose based on
|
|
5
|
+
* version strings, ask the CLI directly via `minutes capabilities --json`.
|
|
6
|
+
* Tools whose backing CLI subcommand the report confirms get registered;
|
|
7
|
+
* tools whose subcommand is missing (or whose feature key is absent, or
|
|
8
|
+
* whose probe failed entirely) are hidden from the MCP tool list.
|
|
9
|
+
*
|
|
10
|
+
* The probe is synchronous so tool registration at module load can consult
|
|
11
|
+
* it. If the CLI is missing, older, or crashes, the probe returns `null`
|
|
12
|
+
* and `hasFeature(null, ...)` returns `false` — fail-closed. For the
|
|
13
|
+
* specific features this module gates (new in 0.14.0), that matches
|
|
14
|
+
* ground truth: a CLI too old to respond to `capabilities` is also too
|
|
15
|
+
* old to have the backing subcommand, so hiding the tool is correct.
|
|
16
|
+
*
|
|
17
|
+
* The alternative (fail-open) was rejected because it produced tools that
|
|
18
|
+
* fail at call time with "unknown subcommand" errors on older CLIs — the
|
|
19
|
+
* exact UX problem #183 Phase 2 is meant to eliminate.
|
|
20
|
+
*/
|
|
21
|
+
export type CapabilityReport = {
|
|
22
|
+
/** Semver of the CLI, e.g. "0.14.0". */
|
|
23
|
+
version: string;
|
|
24
|
+
/** Wire-contract version. Bumps only on breaking changes. */
|
|
25
|
+
api_version: number;
|
|
26
|
+
/** Feature name to whether the CLI supports it. */
|
|
27
|
+
features: Record<string, boolean>;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Probe the installed CLI for its capability report. Synchronous so it
|
|
31
|
+
* runs before tool registrations at module load.
|
|
32
|
+
*
|
|
33
|
+
* Returns `null` if:
|
|
34
|
+
* - the binary does not exist on PATH or at the resolved path,
|
|
35
|
+
* - the CLI is too old to have a `capabilities` subcommand,
|
|
36
|
+
* - the output is not valid JSON,
|
|
37
|
+
* - the output does not match the expected shape.
|
|
38
|
+
*
|
|
39
|
+
* A `null` return is a soft signal meaning "proceed optimistically"; the
|
|
40
|
+
* caller should register all tools as if every feature is supported.
|
|
41
|
+
*/
|
|
42
|
+
export declare function probeCapabilitiesSync(binPath: string, options?: {
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}): CapabilityReport | null;
|
|
45
|
+
/**
|
|
46
|
+
* The newest wire-contract version this MCP server understands. A report
|
|
47
|
+
* whose `api_version` exceeds this value is rejected (treated as null by
|
|
48
|
+
* the caller) so a future breaking CLI schema cannot be silently trusted
|
|
49
|
+
* by an older MCP.
|
|
50
|
+
*
|
|
51
|
+
* Add a new compatibility branch here, don't just bump this number, when
|
|
52
|
+
* the CLI schema changes in a non-additive way.
|
|
53
|
+
*/
|
|
54
|
+
export declare const MAX_SUPPORTED_API_VERSION = 1;
|
|
55
|
+
/**
|
|
56
|
+
* Parse a capability report JSON payload with shape validation.
|
|
57
|
+
*
|
|
58
|
+
* Exposed separately from the probe so unit tests can exercise the
|
|
59
|
+
* parser without spawning a subprocess.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseCapabilityReport(raw: string): CapabilityReport | null;
|
|
62
|
+
/**
|
|
63
|
+
* Decide whether to expose a feature-gated MCP tool.
|
|
64
|
+
*
|
|
65
|
+
* Fail-closed contract:
|
|
66
|
+
* - `report === null`: probe failed or CLI is old/missing. Return `false`.
|
|
67
|
+
* The gated tool is hidden. For the Phase 2 gate set (features new in
|
|
68
|
+
* the same CLI release that introduced the `capabilities` subcommand),
|
|
69
|
+
* this matches ground truth: an old CLI is missing both the probe and
|
|
70
|
+
* the backing subcommand.
|
|
71
|
+
* - `report !== null` and feature key is present and `true`: Return `true`.
|
|
72
|
+
* - `report !== null` and feature key is `false` or missing: Return `false`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function hasFeature(report: CapabilityReport | null, name: string): boolean;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI capabilities feature-detection probe.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 of #183: instead of guessing which MCP tools to expose based on
|
|
5
|
+
* version strings, ask the CLI directly via `minutes capabilities --json`.
|
|
6
|
+
* Tools whose backing CLI subcommand the report confirms get registered;
|
|
7
|
+
* tools whose subcommand is missing (or whose feature key is absent, or
|
|
8
|
+
* whose probe failed entirely) are hidden from the MCP tool list.
|
|
9
|
+
*
|
|
10
|
+
* The probe is synchronous so tool registration at module load can consult
|
|
11
|
+
* it. If the CLI is missing, older, or crashes, the probe returns `null`
|
|
12
|
+
* and `hasFeature(null, ...)` returns `false` — fail-closed. For the
|
|
13
|
+
* specific features this module gates (new in 0.14.0), that matches
|
|
14
|
+
* ground truth: a CLI too old to respond to `capabilities` is also too
|
|
15
|
+
* old to have the backing subcommand, so hiding the tool is correct.
|
|
16
|
+
*
|
|
17
|
+
* The alternative (fail-open) was rejected because it produced tools that
|
|
18
|
+
* fail at call time with "unknown subcommand" errors on older CLIs — the
|
|
19
|
+
* exact UX problem #183 Phase 2 is meant to eliminate.
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from "child_process";
|
|
22
|
+
/**
|
|
23
|
+
* Probe the installed CLI for its capability report. Synchronous so it
|
|
24
|
+
* runs before tool registrations at module load.
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` if:
|
|
27
|
+
* - the binary does not exist on PATH or at the resolved path,
|
|
28
|
+
* - the CLI is too old to have a `capabilities` subcommand,
|
|
29
|
+
* - the output is not valid JSON,
|
|
30
|
+
* - the output does not match the expected shape.
|
|
31
|
+
*
|
|
32
|
+
* A `null` return is a soft signal meaning "proceed optimistically"; the
|
|
33
|
+
* caller should register all tools as if every feature is supported.
|
|
34
|
+
*/
|
|
35
|
+
export function probeCapabilitiesSync(binPath, options = {}) {
|
|
36
|
+
const timeoutMs = options.timeoutMs ?? 2000;
|
|
37
|
+
let stdout;
|
|
38
|
+
try {
|
|
39
|
+
stdout = execFileSync(binPath, ["capabilities", "--json"], {
|
|
40
|
+
timeout: timeoutMs,
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
// Silence stderr so the MCP console stays quiet when the CLI is
|
|
43
|
+
// old (and prints an unknown-subcommand error to stderr).
|
|
44
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return parseCapabilityReport(stdout);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The newest wire-contract version this MCP server understands. A report
|
|
54
|
+
* whose `api_version` exceeds this value is rejected (treated as null by
|
|
55
|
+
* the caller) so a future breaking CLI schema cannot be silently trusted
|
|
56
|
+
* by an older MCP.
|
|
57
|
+
*
|
|
58
|
+
* Add a new compatibility branch here, don't just bump this number, when
|
|
59
|
+
* the CLI schema changes in a non-additive way.
|
|
60
|
+
*/
|
|
61
|
+
export const MAX_SUPPORTED_API_VERSION = 1;
|
|
62
|
+
/**
|
|
63
|
+
* Parse a capability report JSON payload with shape validation.
|
|
64
|
+
*
|
|
65
|
+
* Exposed separately from the probe so unit tests can exercise the
|
|
66
|
+
* parser without spawning a subprocess.
|
|
67
|
+
*/
|
|
68
|
+
export function parseCapabilityReport(raw) {
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(raw.trim());
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (!parsed || typeof parsed !== "object")
|
|
77
|
+
return null;
|
|
78
|
+
// Object.create(null) would be ideal for the target map; we guard
|
|
79
|
+
// against `__proto__`/`constructor`/`prototype` pollution below.
|
|
80
|
+
const obj = parsed;
|
|
81
|
+
if (typeof obj.version !== "string")
|
|
82
|
+
return null;
|
|
83
|
+
if (typeof obj.api_version !== "number")
|
|
84
|
+
return null;
|
|
85
|
+
// Reject reports from a future CLI with a wire contract we do not
|
|
86
|
+
// understand. Treating this as null triggers the fail-closed path so
|
|
87
|
+
// no tools get silently enabled based on a schema we cannot verify.
|
|
88
|
+
if (!Number.isInteger(obj.api_version) ||
|
|
89
|
+
obj.api_version < 1 ||
|
|
90
|
+
obj.api_version > MAX_SUPPORTED_API_VERSION) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (!obj.features || typeof obj.features !== "object")
|
|
94
|
+
return null;
|
|
95
|
+
// Coerce feature map values to booleans; drop non-boolean entries so
|
|
96
|
+
// a misformed payload never accidentally enables a tool. Use a
|
|
97
|
+
// null-prototype object so polluted keys (__proto__, constructor,
|
|
98
|
+
// prototype) cannot reach anything via the prototype chain.
|
|
99
|
+
const rawFeatures = obj.features;
|
|
100
|
+
const features = Object.create(null);
|
|
101
|
+
for (const [name, value] of Object.entries(rawFeatures)) {
|
|
102
|
+
if (name === "__proto__" || name === "constructor" || name === "prototype") {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === "boolean") {
|
|
106
|
+
features[name] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
version: obj.version,
|
|
111
|
+
api_version: obj.api_version,
|
|
112
|
+
features,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Decide whether to expose a feature-gated MCP tool.
|
|
117
|
+
*
|
|
118
|
+
* Fail-closed contract:
|
|
119
|
+
* - `report === null`: probe failed or CLI is old/missing. Return `false`.
|
|
120
|
+
* The gated tool is hidden. For the Phase 2 gate set (features new in
|
|
121
|
+
* the same CLI release that introduced the `capabilities` subcommand),
|
|
122
|
+
* this matches ground truth: an old CLI is missing both the probe and
|
|
123
|
+
* the backing subcommand.
|
|
124
|
+
* - `report !== null` and feature key is present and `true`: Return `true`.
|
|
125
|
+
* - `report !== null` and feature key is `false` or missing: Return `false`.
|
|
126
|
+
*/
|
|
127
|
+
export function hasFeature(report, name) {
|
|
128
|
+
if (report === null)
|
|
129
|
+
return false;
|
|
130
|
+
return report.features[name] === true;
|
|
131
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* - get_meeting: Get full transcript of a specific meeting
|
|
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
|
+
* - activity_summary: Summarize meeting-adjacent desktop context for a session/path/window
|
|
15
|
+
* - search_context: Search app and captured window-title desktop context
|
|
16
|
+
* - get_moment: Show the local rewind around a linked artifact, session, or timestamp
|
|
14
17
|
* - consistency_report: Flag conflicting decisions and stale commitments
|
|
15
18
|
* - get_person_profile: Rich relationship profile for a person (graph index)
|
|
16
19
|
* - track_commitments: List open/stale commitments, filter by person
|
|
@@ -32,4 +35,5 @@ export type KnowledgeConfigStatus = {
|
|
|
32
35
|
adapter: string;
|
|
33
36
|
engine: string;
|
|
34
37
|
};
|
|
38
|
+
export declare function shouldRunMainEntry(argv1: string | null | undefined, moduleFilename: string): boolean;
|
|
35
39
|
export declare function parseKnowledgeConfig(configContent: string): KnowledgeConfigStatus | null;
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* - get_meeting: Get full transcript of a specific meeting
|
|
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
|
+
* - activity_summary: Summarize meeting-adjacent desktop context for a session/path/window
|
|
15
|
+
* - search_context: Search app and captured window-title desktop context
|
|
16
|
+
* - get_moment: Show the local rewind around a linked artifact, session, or timestamp
|
|
14
17
|
* - consistency_report: Flag conflicting decisions and stale commitments
|
|
15
18
|
* - get_person_profile: Rich relationship profile for a person (graph index)
|
|
16
19
|
* - track_commitments: List open/stale commitments, filter by person
|
|
@@ -37,14 +40,83 @@ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, EXTENSION_ID,
|
|
|
37
40
|
import { z } from "zod";
|
|
38
41
|
import { execFile, spawn } from "child_process";
|
|
39
42
|
import { promisify } from "util";
|
|
40
|
-
import { existsSync } from "fs";
|
|
43
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, realpathSync } from "fs";
|
|
41
44
|
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
42
45
|
import { delimiter, dirname, join, resolve } from "path";
|
|
43
46
|
import { fileURLToPath } from "url";
|
|
44
47
|
import { homedir } from "os";
|
|
45
48
|
import * as reader from "minutes-sdk";
|
|
46
49
|
import { canonicalizeRoot, expandHomeLikePath, validatePathInDirectories, validatePathInDirectory, } from "./paths.js";
|
|
50
|
+
import { isCliCompatible } from "./version.js";
|
|
51
|
+
import { hasFeature, probeCapabilitiesSync, } from "./capabilities.js";
|
|
47
52
|
crashTrace("imports-complete");
|
|
53
|
+
// ── Demo mode (--demo flag) ────────────────────────────────
|
|
54
|
+
// `npx minutes-mcp --demo` is a one-shot setup: copies bundled fixture
|
|
55
|
+
// meetings to ~/.minutes/demo/, prints the MCP config snippet with an explicit
|
|
56
|
+
// MEETINGS_DIR env override, prints suggested questions, and exits 0.
|
|
57
|
+
//
|
|
58
|
+
// The printed config uses env:{ MEETINGS_DIR } pointing at the demo dir. No
|
|
59
|
+
// separate --demo flag at runtime. The MCP host just launches standard
|
|
60
|
+
// `minutes-mcp`; the env override is what routes it at the demo corpus. This
|
|
61
|
+
// avoids the TTY-detection ambiguity that an earlier dual-mode design had.
|
|
62
|
+
//
|
|
63
|
+
// Guarded on `--demo` AND on being the actual entry point so importers don't
|
|
64
|
+
// trigger disk side effects by mistake. Use the same realpath-aware guard as
|
|
65
|
+
// `main()` so npm/.bin shims and symlinked entrypoints still execute demo mode.
|
|
66
|
+
if (process.argv.includes("--demo") && shouldRunMainEntry(process.argv[1], fileURLToPath(import.meta.url))) {
|
|
67
|
+
handleDemoSetup();
|
|
68
|
+
}
|
|
69
|
+
function handleDemoSetup() {
|
|
70
|
+
const demoDir = join(homedir(), ".minutes", "demo");
|
|
71
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
72
|
+
// Package layout after build: dist/index.js; fixtures live at
|
|
73
|
+
// <pkg>/fixtures/demo/ next to dist/.
|
|
74
|
+
const fixturesSrc = resolve(here, "..", "fixtures", "demo");
|
|
75
|
+
if (!existsSync(fixturesSrc)) {
|
|
76
|
+
console.error(`[minutes-mcp --demo] bundled fixtures not found at ${fixturesSrc}. ` +
|
|
77
|
+
`This build of minutes-mcp is missing the demo corpus. ` +
|
|
78
|
+
`Try upgrading with: npm install -g minutes-mcp@latest`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
mkdirSync(demoDir, { recursive: true });
|
|
82
|
+
for (const entry of readdirSync(fixturesSrc)) {
|
|
83
|
+
if (!entry.endsWith(".md"))
|
|
84
|
+
continue;
|
|
85
|
+
copyFileSync(join(fixturesSrc, entry), join(demoDir, entry));
|
|
86
|
+
}
|
|
87
|
+
// The config snippet embeds the fully-resolved demoDir so users don't have
|
|
88
|
+
// to fill it in manually. MCP hosts inject this env when launching the
|
|
89
|
+
// server; the server's existing MEETINGS_DIR logic (line ~800) picks it up.
|
|
90
|
+
const configSnippet = JSON.stringify({
|
|
91
|
+
mcpServers: {
|
|
92
|
+
"minutes-demo": {
|
|
93
|
+
command: "npx",
|
|
94
|
+
args: ["minutes-mcp"],
|
|
95
|
+
env: {
|
|
96
|
+
MEETINGS_DIR: demoDir,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}, null, 2);
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log("Demo corpus ready at: " + demoDir);
|
|
103
|
+
console.log("5 fixture meetings with a pricing reversal, a customer commitment that slips, and a feature cut.");
|
|
104
|
+
console.log("");
|
|
105
|
+
console.log("═══ MCP config (paste into Claude Desktop, Cursor, Claude Code, or any MCP client) ═══");
|
|
106
|
+
console.log(configSnippet);
|
|
107
|
+
console.log("");
|
|
108
|
+
console.log("═══ Try asking your agent ═══");
|
|
109
|
+
console.log(" • List the meetings in this corpus.");
|
|
110
|
+
console.log(" • What did we decide about pricing? Which decision is current?");
|
|
111
|
+
console.log(" • What got killed in the last product prioritization meeting?");
|
|
112
|
+
console.log(" • What action items are still open, and who owns each?");
|
|
113
|
+
console.log(" • Summarize the Northwind customer thread.");
|
|
114
|
+
console.log("");
|
|
115
|
+
console.log("Note: some structured tools (consistency report, person profile) auto-install the Minutes CLI on first use.");
|
|
116
|
+
console.log("Full setup (real audio capture, transcription, real meetings): https://useminutes.app");
|
|
117
|
+
console.log("");
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
48
120
|
const UI_RESOURCE_URI = "ui://minutes/dashboard";
|
|
49
121
|
const MCP_TOOLS_DOCS_BASE_URL = "https://useminutes.app/docs/mcp/tools";
|
|
50
122
|
export const MEETING_INSIGHT_KINDS = ["decision", "commitment", "question"];
|
|
@@ -146,6 +218,22 @@ async function triggerQmdIndex() {
|
|
|
146
218
|
// ESM-compatible __dirname
|
|
147
219
|
const __filename = fileURLToPath(import.meta.url);
|
|
148
220
|
const __dirname = dirname(__filename);
|
|
221
|
+
function canonicalEntrypointPath(filePath) {
|
|
222
|
+
if (!filePath)
|
|
223
|
+
return null;
|
|
224
|
+
const resolved = resolve(filePath);
|
|
225
|
+
try {
|
|
226
|
+
return realpathSync(resolved);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return resolved;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export function shouldRunMainEntry(argv1, moduleFilename) {
|
|
233
|
+
const entryPath = canonicalEntrypointPath(argv1);
|
|
234
|
+
const modulePath = canonicalEntrypointPath(moduleFilename);
|
|
235
|
+
return !!entryPath && !!modulePath && entryPath === modulePath;
|
|
236
|
+
}
|
|
149
237
|
// ── Extension runtime detection ───────────────────────────────
|
|
150
238
|
// When running as a Claude Desktop extension (.mcpb), Claude uses its built-in
|
|
151
239
|
// Node.js runtime. Child processes spawned from that runtime land in a
|
|
@@ -198,9 +286,31 @@ function findMinutesBinary() {
|
|
|
198
286
|
return "minutes";
|
|
199
287
|
}
|
|
200
288
|
let MINUTES_BIN = findMinutesBinary();
|
|
201
|
-
// ──
|
|
202
|
-
|
|
203
|
-
|
|
289
|
+
// ── Capability probe (Phase 2 of #183) ────────────────────────
|
|
290
|
+
// Ask the CLI what it supports instead of inferring from version strings.
|
|
291
|
+
// Synchronous so it can run before tool registrations at module load.
|
|
292
|
+
// A `null` report means we could not probe (missing or old CLI): the
|
|
293
|
+
// `hasFeature` helper treats that as "optimistic yes" so we preserve
|
|
294
|
+
// backward compatibility with pre-0.14.0 CLIs that have no `capabilities`
|
|
295
|
+
// subcommand.
|
|
296
|
+
const CLI_CAPABILITIES = probeCapabilitiesSync(MINUTES_BIN);
|
|
297
|
+
if (CLI_CAPABILITIES) {
|
|
298
|
+
crashTrace("cli-capabilities-probed", {
|
|
299
|
+
cliVersion: CLI_CAPABILITIES.version,
|
|
300
|
+
apiVersion: CLI_CAPABILITIES.api_version,
|
|
301
|
+
featureCount: Object.keys(CLI_CAPABILITIES.features).length,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
crashTrace("cli-capabilities-unknown");
|
|
306
|
+
}
|
|
307
|
+
// ── MCP server version ────────────────────────────────────────
|
|
308
|
+
// Kept for capabilities handshake and user-facing log messages.
|
|
309
|
+
// The compatibility decision against the installed CLI lives in
|
|
310
|
+
// `./version.ts` (see issue #183). Hosted `.mcpb` bundles will run
|
|
311
|
+
// against CLIs with different minor/patch numbers within the same
|
|
312
|
+
// major; that is explicitly supported.
|
|
313
|
+
const MCP_SERVER_VERSION = "0.14.0";
|
|
204
314
|
export function parseKnowledgeConfig(configContent) {
|
|
205
315
|
const knowledgeMatch = configContent.match(/\[knowledge\][\s\S]*?(?=\n\[|$)/);
|
|
206
316
|
if (!knowledgeMatch) {
|
|
@@ -218,8 +328,10 @@ export function parseKnowledgeConfig(configContent) {
|
|
|
218
328
|
engine: engineMatch?.[1] || "none",
|
|
219
329
|
};
|
|
220
330
|
}
|
|
221
|
-
const RELEASE_TAG = `v${EXPECTED_CLI_VERSION}`;
|
|
222
331
|
// ── CLI auto-install ────────────────────────────────────────
|
|
332
|
+
// Auto-install fetches from the GitHub `releases/latest/download/` redirect,
|
|
333
|
+
// not a pinned tag, so hosted `.mcpb` bundles self-heal across our release
|
|
334
|
+
// cadence. See issue #183 for context.
|
|
223
335
|
// When installed via MCPB or `npx minutes-mcp`, the Rust CLI binary
|
|
224
336
|
// may not be present. We attempt to install it automatically so
|
|
225
337
|
// non-technical users don't hit a "binary not found" dead end.
|
|
@@ -253,12 +365,12 @@ async function tryAutoInstall() {
|
|
|
253
365
|
const binaryName = getReleaseBinaryName();
|
|
254
366
|
if (binaryName) {
|
|
255
367
|
try {
|
|
256
|
-
const url = `https://github.com/silverstein/minutes/releases/download/${
|
|
368
|
+
const url = `https://github.com/silverstein/minutes/releases/latest/download/${binaryName}`;
|
|
257
369
|
const installDir = getInstallDir();
|
|
258
370
|
const isWindows = process.platform === "win32";
|
|
259
371
|
const targetName = isWindows ? "minutes.exe" : "minutes";
|
|
260
372
|
const targetPath = join(installDir, targetName);
|
|
261
|
-
console.error(`[Minutes] Downloading ${binaryName} from
|
|
373
|
+
console.error(`[Minutes] Downloading ${binaryName} from latest release...`);
|
|
262
374
|
// Ensure install directory exists
|
|
263
375
|
await execFileAsync("mkdir", ["-p", installDir], { timeout: 5000 }).catch(() => { });
|
|
264
376
|
// Download with curl (available on macOS, Linux, and modern Windows)
|
|
@@ -310,21 +422,26 @@ async function tryAutoInstall() {
|
|
|
310
422
|
async function checkCliVersion() {
|
|
311
423
|
try {
|
|
312
424
|
const { stdout } = await execFileAsync(MINUTES_BIN, ["--version"], { timeout: 5000, env: augmentedEnv() });
|
|
313
|
-
// Output is like "minutes 0.8.0" or just "0.8.0"
|
|
425
|
+
// Output is like "minutes 0.8.0" or just "0.8.0".
|
|
314
426
|
const match = stdout.trim().match(/(\d+\.\d+\.\d+)/);
|
|
315
|
-
if (match)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
427
|
+
if (!match)
|
|
428
|
+
return;
|
|
429
|
+
const installedVersion = match[1];
|
|
430
|
+
const result = isCliCompatible(installedVersion, MCP_SERVER_VERSION);
|
|
431
|
+
// Only surface logs the user should see. Same-major skew is silent-
|
|
432
|
+
// compatible, which is the whole point of issue #183 fix: hosted `.mcpb`
|
|
433
|
+
// bundles frequently run against a CLI with a different minor/patch
|
|
434
|
+
// and that is fine.
|
|
435
|
+
if (result.severity === "error") {
|
|
436
|
+
console.error(`[Minutes] ${result.message}`);
|
|
324
437
|
}
|
|
438
|
+
else if (result.severity === "ok") {
|
|
439
|
+
console.error(`[Minutes] ${result.message}`);
|
|
440
|
+
}
|
|
441
|
+
// "info" severity (compatible skew, unparseable version) stays silent.
|
|
325
442
|
}
|
|
326
443
|
catch {
|
|
327
|
-
// Version check is best-effort
|
|
444
|
+
// Version check is best-effort. Don't block on failure.
|
|
328
445
|
}
|
|
329
446
|
}
|
|
330
447
|
// ── Auto-setup: download whisper model if missing ───────────
|
|
@@ -1086,6 +1203,129 @@ registerDocsAppTool(server, "search_meetings", {
|
|
|
1086
1203
|
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "dashboard" },
|
|
1087
1204
|
};
|
|
1088
1205
|
});
|
|
1206
|
+
// ── Tool: activity_summary ──────────────────────────────────
|
|
1207
|
+
// Feature-gated (#183 phase 2). Hidden when the CLI does not report
|
|
1208
|
+
// activity_summary in its capability probe. On an older CLI that has no
|
|
1209
|
+
// `capabilities` subcommand, the probe returns null and `hasFeature`
|
|
1210
|
+
// resolves to true, so the tool is still exposed and any runtime call
|
|
1211
|
+
// surfaces a clear CLI error rather than silently disappearing.
|
|
1212
|
+
if (hasFeature(CLI_CAPABILITIES, "activity_summary"))
|
|
1213
|
+
registerDocsAppTool(server, "activity_summary", {
|
|
1214
|
+
description: "Summarize meeting-adjacent desktop context for a linked artifact, context session, or explicit time window.",
|
|
1215
|
+
inputSchema: {
|
|
1216
|
+
session_id: z.string().optional().describe("Explicit desktop-context session id"),
|
|
1217
|
+
path: z.string().optional().describe("Linked artifact path, such as a meeting markdown file or live transcript JSONL"),
|
|
1218
|
+
start: z.string().optional().describe("Window start (RFC3339); use with end when no session/path is provided"),
|
|
1219
|
+
end: z.string().optional().describe("Window end (RFC3339); use with start when no session/path is provided"),
|
|
1220
|
+
},
|
|
1221
|
+
annotations: { title: "Activity Summary", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1222
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
1223
|
+
}, async ({ session_id, path, start, end }) => {
|
|
1224
|
+
if (!(await isCliAvailable())) {
|
|
1225
|
+
return { content: [{ type: "text", text: `Desktop-context summaries require the full CLI.\n\n${CLI_INSTALL_MSG}` }] };
|
|
1226
|
+
}
|
|
1227
|
+
const args = ["context", "activity-summary", "--json"];
|
|
1228
|
+
if (session_id)
|
|
1229
|
+
args.push("--session", session_id);
|
|
1230
|
+
if (path)
|
|
1231
|
+
args.push("--path", path);
|
|
1232
|
+
if (start)
|
|
1233
|
+
args.push("--start", start);
|
|
1234
|
+
if (end)
|
|
1235
|
+
args.push("--end", end);
|
|
1236
|
+
const { stdout, stderr } = await runMinutes(args);
|
|
1237
|
+
const parsed = parseJsonOutput(stdout);
|
|
1238
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1239
|
+
return { content: [{ type: "text", text: stderr || stdout }] };
|
|
1240
|
+
}
|
|
1241
|
+
const apps = Array.isArray(parsed.top_apps) ? parsed.top_apps : [];
|
|
1242
|
+
const windows = Array.isArray(parsed.top_windows) ? parsed.top_windows : [];
|
|
1243
|
+
const events = Array.isArray(parsed.events) ? parsed.events : [];
|
|
1244
|
+
const lines = [
|
|
1245
|
+
`Desktop context summary: ${parsed.window?.start || "?"} -> ${parsed.window?.end || "?"}`,
|
|
1246
|
+
apps.length ? `Top apps: ${apps.map((entry) => `${entry.name} (${entry.count})`).join(", ")}` : "",
|
|
1247
|
+
windows.length ? `Top windows: ${windows.map((entry) => `${entry.name} (${entry.count})`).join(", ")}` : "",
|
|
1248
|
+
events.length ? `Events: ${events.length}` : "Events: 0",
|
|
1249
|
+
].filter(Boolean);
|
|
1250
|
+
return {
|
|
1251
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1252
|
+
structuredContent: { ...parsed, kind: "activity_summary", view: "context" },
|
|
1253
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "context", kind: "activity_summary" },
|
|
1254
|
+
};
|
|
1255
|
+
});
|
|
1256
|
+
// ── Tool: search_context ────────────────────────────────────
|
|
1257
|
+
// Feature-gated (#183 phase 2). See activity_summary comment above.
|
|
1258
|
+
if (hasFeature(CLI_CAPABILITIES, "search_context"))
|
|
1259
|
+
registerDocsAppTool(server, "search_context", {
|
|
1260
|
+
description: "Search desktop-context events across app focus and captured window titles, including opted-in browser titles.",
|
|
1261
|
+
inputSchema: {
|
|
1262
|
+
query: z.string().describe("Text query for app names, bundle ids, or captured window titles"),
|
|
1263
|
+
limit: z.number().optional().default(20).describe("Maximum results"),
|
|
1264
|
+
},
|
|
1265
|
+
annotations: { title: "Search Context", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1266
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
1267
|
+
}, async ({ query, limit }) => {
|
|
1268
|
+
if (!(await isCliAvailable())) {
|
|
1269
|
+
return { content: [{ type: "text", text: `Desktop-context search requires the full CLI.\n\n${CLI_INSTALL_MSG}` }] };
|
|
1270
|
+
}
|
|
1271
|
+
const { stdout, stderr } = await runMinutes(["context", "search", query, "--limit", String(limit), "--json"]);
|
|
1272
|
+
const parsed = parseJsonOutput(stdout);
|
|
1273
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1274
|
+
return { content: [{ type: "text", text: stderr || stdout }] };
|
|
1275
|
+
}
|
|
1276
|
+
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
1277
|
+
const text = results.length === 0
|
|
1278
|
+
? `No desktop-context events found for "${query}".`
|
|
1279
|
+
: results
|
|
1280
|
+
.map((event) => `${event.observed_at} — ${event.app_name || event.bundle_id || "unknown"}${event.window_title ? ` :: ${event.window_title}` : ""}`)
|
|
1281
|
+
.join("\n");
|
|
1282
|
+
return {
|
|
1283
|
+
content: [{ type: "text", text }],
|
|
1284
|
+
structuredContent: { query, results, view: "context", kind: "search_context" },
|
|
1285
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "context", kind: "search_context" },
|
|
1286
|
+
};
|
|
1287
|
+
});
|
|
1288
|
+
// ── Tool: get_moment ────────────────────────────────────────
|
|
1289
|
+
// Feature-gated (#183 phase 2). See activity_summary comment above.
|
|
1290
|
+
if (hasFeature(CLI_CAPABILITIES, "get_moment"))
|
|
1291
|
+
registerDocsAppTool(server, "get_moment", {
|
|
1292
|
+
description: "Show the local rewind around a linked artifact, context session, or explicit timestamp.",
|
|
1293
|
+
inputSchema: {
|
|
1294
|
+
session_id: z.string().optional().describe("Explicit desktop-context session id"),
|
|
1295
|
+
path: z.string().optional().describe("Linked artifact path, such as a meeting markdown file or live transcript JSONL"),
|
|
1296
|
+
at: z.string().optional().describe("Explicit anchor timestamp (RFC3339)"),
|
|
1297
|
+
before_minutes: z.number().optional().default(10).describe("Minutes before the anchor"),
|
|
1298
|
+
after_minutes: z.number().optional().default(10).describe("Minutes after the anchor"),
|
|
1299
|
+
},
|
|
1300
|
+
annotations: { title: "Get Moment", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1301
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI } },
|
|
1302
|
+
}, async ({ session_id, path, at, before_minutes, after_minutes }) => {
|
|
1303
|
+
if (!(await isCliAvailable())) {
|
|
1304
|
+
return { content: [{ type: "text", text: `Desktop-context rewind requires the full CLI.\n\n${CLI_INSTALL_MSG}` }] };
|
|
1305
|
+
}
|
|
1306
|
+
const args = ["context", "get-moment", "--json", "--before-minutes", String(before_minutes), "--after-minutes", String(after_minutes)];
|
|
1307
|
+
if (session_id)
|
|
1308
|
+
args.push("--session", session_id);
|
|
1309
|
+
if (path)
|
|
1310
|
+
args.push("--path", path);
|
|
1311
|
+
if (at)
|
|
1312
|
+
args.push("--at", at);
|
|
1313
|
+
const { stdout, stderr } = await runMinutes(args);
|
|
1314
|
+
const parsed = parseJsonOutput(stdout);
|
|
1315
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1316
|
+
return { content: [{ type: "text", text: stderr || stdout }] };
|
|
1317
|
+
}
|
|
1318
|
+
const events = Array.isArray(parsed.events) ? parsed.events : [];
|
|
1319
|
+
const text = [
|
|
1320
|
+
`Moment window: ${parsed.window?.start || "?"} -> ${parsed.window?.end || "?"}`,
|
|
1321
|
+
...events.map((event) => `${event.observed_at} — ${event.app_name || event.bundle_id || "unknown"}${event.window_title ? ` :: ${event.window_title}` : ""}`),
|
|
1322
|
+
].join("\n");
|
|
1323
|
+
return {
|
|
1324
|
+
content: [{ type: "text", text }],
|
|
1325
|
+
structuredContent: { ...parsed, view: "context", kind: "get_moment" },
|
|
1326
|
+
_meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "context", kind: "get_moment" },
|
|
1327
|
+
};
|
|
1328
|
+
});
|
|
1089
1329
|
// ── Tool: consistency_report ───────────────────────────────
|
|
1090
1330
|
registerDocsAppTool(server, "consistency_report", {
|
|
1091
1331
|
description: "Flag conflicting decisions and stale commitments across meetings using structured intent data.",
|
|
@@ -2134,9 +2374,9 @@ crashTrace("pre-main-guard", {
|
|
|
2134
2374
|
argv1: process.argv[1] ?? null,
|
|
2135
2375
|
resolvedArgv1: process.argv[1] ? resolve(process.argv[1]) : null,
|
|
2136
2376
|
__filename,
|
|
2137
|
-
match:
|
|
2377
|
+
match: shouldRunMainEntry(process.argv[1], __filename),
|
|
2138
2378
|
});
|
|
2139
|
-
if (
|
|
2379
|
+
if (shouldRunMainEntry(process.argv[1], __filename)) {
|
|
2140
2380
|
main().catch((error) => {
|
|
2141
2381
|
crashTrace("main-rejected", error);
|
|
2142
2382
|
console.error("Fatal error:", error);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI/MCP version compatibility.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the historical strict-equality check with same-major semver
|
|
5
|
+
* compatibility. See issue #183 for the rationale: hosted `.mcpb` bundles
|
|
6
|
+
* ship a frozen MCP server version, while users' CLI versions advance
|
|
7
|
+
* independently via brew/cargo/auto-install. Strict equality turned every
|
|
8
|
+
* version skew into scary user-facing warnings and broke auto-install when
|
|
9
|
+
* the pinned GitHub release tag no longer matched.
|
|
10
|
+
*/
|
|
11
|
+
export type VersionParts = {
|
|
12
|
+
major: number;
|
|
13
|
+
minor: number;
|
|
14
|
+
patch: number;
|
|
15
|
+
};
|
|
16
|
+
export type CompatibilitySeverity = "ok" | "info" | "error";
|
|
17
|
+
export type CompatibilityResult = {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
severity: CompatibilitySeverity;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function parseVersion(raw: string): VersionParts | null;
|
|
23
|
+
/**
|
|
24
|
+
* Decide whether a CLI version is compatible with the running MCP server.
|
|
25
|
+
*
|
|
26
|
+
* Rules (Phase 1 of #183):
|
|
27
|
+
* - Unparseable version string: proceed, log informationally. Old CLIs may
|
|
28
|
+
* not emit a parseable `--version` but usually still work.
|
|
29
|
+
* - Major-version mismatch: not compatible. Emit one clear error with an
|
|
30
|
+
* upgrade command.
|
|
31
|
+
* - Same major, same version: ok, one-line info log.
|
|
32
|
+
* - Same major, different minor/patch: ok. Older CLI with newer MCP, or
|
|
33
|
+
* vice-versa, is backward-compatible within a major per our contract.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isCliCompatible(cliVersion: string, serverVersion: string): CompatibilityResult;
|