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.
@@ -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
- // ── Expected CLI version (must match this MCP server release) ──
202
- const MCP_SERVER_VERSION = "0.13.2";
203
- const EXPECTED_CLI_VERSION = MCP_SERVER_VERSION;
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/${RELEASE_TAG}/${binaryName}`;
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 ${RELEASE_TAG} release...`);
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
- const installedVersion = match[1];
317
- if (installedVersion !== EXPECTED_CLI_VERSION) {
318
- console.error(`[Minutes] CLI version mismatch: installed ${installedVersion}, server expects ${EXPECTED_CLI_VERSION}. ` +
319
- `Update with: brew upgrade minutes (or cargo install minutes-cli)`);
320
- }
321
- else {
322
- console.error(`[Minutes] CLI v${installedVersion} up to date`);
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 — don't block on failure
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: process.argv[1] ? resolve(process.argv[1]) === __filename : false,
2377
+ match: shouldRunMainEntry(process.argv[1], __filename),
2138
2378
  });
2139
- if (process.argv[1] && resolve(process.argv[1]) === __filename) {
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;