minutes-mcp 0.25.3 → 0.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -315,6 +315,30 @@ export declare function liveMeetingSnippet(body: string, query?: string): string
315
315
  export declare function shouldRunMainEntry(argv1: string | null | undefined, moduleFilename: string): boolean;
316
316
  export declare function parseKnowledgeConfig(configContent: string): KnowledgeConfigStatus | null;
317
317
  export declare function createCapabilityRepairCoordinator(repair: () => Promise<boolean>, maximumAttempts?: number): () => Promise<boolean>;
318
+ export type HealthItem = {
319
+ label?: unknown;
320
+ state?: unknown;
321
+ [key: string]: unknown;
322
+ };
323
+ export type HealthOutput = {
324
+ ok: boolean;
325
+ items: HealthItem[] | null;
326
+ engine?: string;
327
+ effectiveEngine?: string;
328
+ model?: string;
329
+ };
330
+ export declare function parseHealthOutput(stdout: string): HealthOutput;
331
+ type WhisperModelCheckState = {
332
+ done: boolean;
333
+ };
334
+ export type EnsureWhisperModelOptions = {
335
+ checkState?: WhisperModelCheckState;
336
+ health?: () => Promise<string>;
337
+ configFileExists?: () => Promise<boolean>;
338
+ setup?: (model: string) => Promise<void>;
339
+ log?: (message: string) => void;
340
+ };
341
+ export declare function ensureWhisperModel(options?: EnsureWhisperModelOptions): Promise<void>;
318
342
  declare function isCliAvailable(): Promise<boolean>;
319
343
  export declare function repairCliCapabilities(required: readonly string[], capabilities: CapabilityProbeResult, repair: () => Promise<boolean>, reprobe: () => CapabilityProbeResult): Promise<CapabilityProbeResult>;
320
344
  export declare function mcpCliChildEnv(extra?: Record<string, string>): Record<string, string | undefined>;
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ import { execFile, spawn, spawnSync } from "child_process";
44
44
  import { createHash } from "crypto";
45
45
  import { promisify } from "util";
46
46
  import { closeSync, constants, copyFileSync, existsSync, fstatSync, mkdirSync, openSync, readdirSync, realpathSync, statSync, } from "fs";
47
- import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
47
+ import { access, mkdir, readFile, rm, stat, writeFile } from "fs/promises";
48
48
  import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "path";
49
49
  import { fileURLToPath } from "url";
50
50
  import { homedir } from "os";
@@ -1570,7 +1570,7 @@ const COPILOT_SUPPORTED = hasFeature(CLI_CAPABILITIES, "copilot_realtime");
1570
1570
  // `./version.ts` (see issue #183). Hosted `.mcpb` bundles will run
1571
1571
  // against CLIs with different minor/patch numbers within the same
1572
1572
  // major; that is explicitly supported.
1573
- const MCP_SERVER_VERSION = "0.25.3";
1573
+ const MCP_SERVER_VERSION = "0.25.4";
1574
1574
  export function parseKnowledgeConfig(configContent) {
1575
1575
  const knowledgeMatch = configContent.match(/\[knowledge\][\s\S]*?(?=\n\[|$)/);
1576
1576
  if (!knowledgeMatch) {
@@ -1777,38 +1777,150 @@ async function checkCliVersion() {
1777
1777
  // Version check is best-effort. Don't block on failure.
1778
1778
  }
1779
1779
  }
1780
- // ── Auto-setup: download whisper model if missing ───────────
1781
- // Recording needs a whisper model (~75MB for tiny). If the CLI is
1782
- // available but the model isn't downloaded, trigger setup automatically
1783
- // in the background so the first "start recording" just works.
1780
+ // ── Auto-setup: download configured whisper model if missing ───────────
1781
+ // Whisper recording needs a model. If Whisper is configured but its model
1782
+ // isn't downloaded, trigger setup automatically in the background so the
1783
+ // first "start recording" just works without changing the user's choice.
1784
1784
  let modelCheckDone = false;
1785
- async function ensureWhisperModel() {
1786
- if (modelCheckDone)
1785
+ function isHealthItem(value) {
1786
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1787
+ }
1788
+ export function parseHealthOutput(stdout) {
1789
+ let parsed;
1790
+ try {
1791
+ parsed = JSON.parse(stdout);
1792
+ }
1793
+ catch {
1794
+ return { ok: false, items: null };
1795
+ }
1796
+ if (Array.isArray(parsed)) {
1797
+ return parsed.every(isHealthItem)
1798
+ ? { ok: true, items: parsed }
1799
+ : { ok: false, items: null };
1800
+ }
1801
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1802
+ return { ok: false, items: null };
1803
+ }
1804
+ const envelope = parsed;
1805
+ if (envelope.ok !== true) {
1806
+ return { ok: false, items: null };
1807
+ }
1808
+ const data = envelope.data;
1809
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1810
+ return { ok: false, items: null };
1811
+ }
1812
+ const dataRecord = data;
1813
+ const items = dataRecord.items;
1814
+ if (!Array.isArray(items) || !items.every(isHealthItem)) {
1815
+ return { ok: false, items: null };
1816
+ }
1817
+ return {
1818
+ ok: true,
1819
+ items,
1820
+ ...(typeof dataRecord.engine === "string" ? { engine: dataRecord.engine } : {}),
1821
+ ...(typeof dataRecord.effective_engine === "string"
1822
+ ? { effectiveEngine: dataRecord.effective_engine }
1823
+ : {}),
1824
+ ...(typeof dataRecord.model === "string" ? { model: dataRecord.model } : {}),
1825
+ };
1826
+ }
1827
+ async function configFileExists() {
1828
+ const configRoot = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
1829
+ try {
1830
+ await access(join(configRoot, "minutes", "config.toml"));
1831
+ return true;
1832
+ }
1833
+ catch (error) {
1834
+ if (error.code === "ENOENT")
1835
+ return false;
1836
+ throw error;
1837
+ }
1838
+ }
1839
+ export async function ensureWhisperModel(options = {}) {
1840
+ const checkState = options.checkState;
1841
+ if (checkState ? checkState.done : modelCheckDone)
1787
1842
  return;
1788
- modelCheckDone = true;
1843
+ if (checkState)
1844
+ checkState.done = true;
1845
+ else
1846
+ modelCheckDone = true;
1847
+ const log = options.log ?? ((message) => console.error(message));
1848
+ const health = options.health ?? (async () => {
1849
+ const { stdout } = await execFileAsync(MINUTES_BIN, ["health", "--json"], {
1850
+ timeout: 10000,
1851
+ env: mcpCliChildEnv(),
1852
+ });
1853
+ return stdout;
1854
+ });
1855
+ const setup = options.setup ?? (async (model) => {
1856
+ await execFileAsync(MINUTES_BIN, ["setup", "--model", model], {
1857
+ timeout: 300000,
1858
+ env: mcpCliChildEnv(),
1859
+ });
1860
+ });
1861
+ const doesConfigFileExist = options.configFileExists ?? configFileExists;
1862
+ let healthOutput;
1789
1863
  try {
1790
- // health --json returns an array of { label, state, detail, optional } items.
1791
- // The "Speech model" item has state "ready" when downloaded.
1792
- const { stdout } = await execFileAsync(MINUTES_BIN, ["health", "--json"], { timeout: 10000, env: mcpCliChildEnv() });
1793
- const items = JSON.parse(stdout);
1794
- const modelItem = Array.isArray(items) && items.find((i) => i.label === "Speech model");
1795
- if (modelItem && modelItem.state === "ready") {
1796
- console.error("[Minutes] Whisper model ready");
1864
+ healthOutput = parseHealthOutput(await health());
1865
+ }
1866
+ catch (error) {
1867
+ const message = error instanceof Error ? error.message : String(error);
1868
+ log(`[Minutes] Unable to check speech model status — skipping Whisper auto-setup: ${message}`);
1869
+ return;
1870
+ }
1871
+ if (!healthOutput.ok || healthOutput.items === null) {
1872
+ log("[Minutes] Unrecognized health --json output — skipping Whisper auto-setup");
1873
+ return;
1874
+ }
1875
+ const modelItem = healthOutput.items.find((item) => item.label === "Speech model");
1876
+ if (!modelItem) {
1877
+ log("[Minutes] unrecognized health items — skipping Whisper auto-setup");
1878
+ return;
1879
+ }
1880
+ if (modelItem.state === "ready") {
1881
+ log("[Minutes] Whisper model ready");
1882
+ return;
1883
+ }
1884
+ if (modelItem.state !== "attention") {
1885
+ log(`[Minutes] Speech model health state is ${String(modelItem.state)} — skipping Whisper auto-setup`);
1886
+ return;
1887
+ }
1888
+ const engineForDecision = healthOutput.effectiveEngine ?? healthOutput.engine;
1889
+ if (engineForDecision !== undefined && engineForDecision.toLowerCase() !== "whisper") {
1890
+ const upgradeGuidance = healthOutput.effectiveEngine === undefined
1891
+ ? "; upgrade the CLI to let auto-setup resolve the effective engine"
1892
+ : "";
1893
+ log(`[Minutes] Transcription engine is ${engineForDecision} — skipping Whisper auto-setup${upgradeGuidance}`);
1894
+ return;
1895
+ }
1896
+ let model = healthOutput.model;
1897
+ if (model !== undefined) {
1898
+ log(`[Minutes] Whisper model ${model} is configured but not ready — downloading it...`);
1899
+ }
1900
+ else {
1901
+ let configExists;
1902
+ try {
1903
+ configExists = await doesConfigFileExist();
1904
+ }
1905
+ catch (error) {
1906
+ const message = error instanceof Error ? error.message : String(error);
1907
+ log(`[Minutes] Unable to check for an existing config — skipping Whisper auto-setup: ${message}`);
1797
1908
  return;
1798
1909
  }
1910
+ if (configExists) {
1911
+ log("[Minutes] config exists but this CLI does not report the configured model; run `minutes setup --model <your model>` or upgrade the CLI");
1912
+ return;
1913
+ }
1914
+ model = "tiny";
1915
+ log("[Minutes] No Whisper model is configured — downloading tiny model (~75MB)...");
1799
1916
  }
1800
- catch {
1801
- // health command may not exist in older CLI versions — fall through to setup
1802
- }
1803
- // Model not found — download tiny model in background
1804
- console.error("[Minutes] Whisper model not found — downloading tiny model (~75MB)...");
1805
1917
  try {
1806
- await execFileAsync(MINUTES_BIN, ["setup", "--model", "tiny"], { timeout: 300000, env: mcpCliChildEnv() });
1807
- console.error("[Minutes] ✓ Whisper tiny model downloaded — recording is ready");
1918
+ await setup(model);
1919
+ log(`[Minutes] ✓ Whisper ${model} model downloaded — recording is ready`);
1808
1920
  }
1809
- catch (e) {
1810
- console.error(`[Minutes] Model download failed: ${e.message || e}. ` +
1811
- `Run manually: minutes setup --model tiny`);
1921
+ catch (error) {
1922
+ const message = error instanceof Error ? error.message : String(error);
1923
+ log(`[Minutes] Model download failed: ${message}. Run manually: minutes setup --model ${model}`);
1812
1924
  }
1813
1925
  }
1814
1926
  // ── CLI availability detection ──────────────────────────────
@@ -6760,15 +6872,16 @@ registerTool("start_live_transcript", "Start real-time transcription. If a recor
6760
6872
  };
6761
6873
  });
6762
6874
  // ── Tool: read_live_transcript ──────────────────────────────
6763
- registerTool("read_live_transcript", "Read the live transcript — works during both recordings and live transcript sessions. Use 'since' to get new lines after a cursor (line number) or time window (e.g., '5m', '30s'). Use 'status' mode to check if a session is active.", {
6875
+ registerTool("read_live_transcript", "Read the live transcript — works during both recordings and live transcript sessions. Use 'since' to get new finalized lines after a cursor (line number) or time window (e.g., '5m', '30s'). Set include_current for one fresh, clearly provisional current-speech draft. Use 'status' mode to check if a session is active.", {
6764
6876
  since: z.string().optional().describe("Line number (e.g., '42') or duration (e.g., '5m', '30s'). Omit to get all lines."),
6765
6877
  status_only: z.boolean().optional().default(false).describe("If true, return session status instead of transcript lines"),
6878
+ include_current: z.boolean().optional().default(false).describe("If true, return finalized lines plus at most one fresh provisional current-speech draft and its replacement identity"),
6766
6879
  relay_cursor: z.object({
6767
6880
  session_id: z.string().optional(),
6768
6881
  transcript_seq: z.number().int().nonnegative(),
6769
6882
  nudge_seq: z.number().int().nonnegative(),
6770
6883
  }).optional().describe("Transient relay cursor returned by a previous read. Reconnects without replaying already-seen transcript or nudge frames."),
6771
- }, { title: "Read Live Transcript", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ since, status_only, relay_cursor }) => {
6884
+ }, { title: "Read Live Transcript", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ since, status_only, include_current, relay_cursor }) => {
6772
6885
  if (!(await isCliAvailable())) {
6773
6886
  return { content: [{ type: "text", text: CLI_INSTALL_MSG }] };
6774
6887
  }
@@ -6779,18 +6892,30 @@ registerTool("read_live_transcript", "Read the live transcript — works during
6779
6892
  else if (since) {
6780
6893
  args.push("--since", since);
6781
6894
  }
6782
- let relay;
6783
- try {
6784
- relay = await attachCaptureRelay(relay_cursor);
6895
+ if (include_current && !status_only) {
6896
+ args.push("--include-current");
6785
6897
  }
6786
- catch {
6787
- // A durable-only or inactive session is still readable. start_live_transcript
6788
- // reports attachment failures explicitly when another process owns capture.
6898
+ let relay;
6899
+ if (!include_current) {
6900
+ try {
6901
+ relay = await attachCaptureRelay(relay_cursor);
6902
+ }
6903
+ catch {
6904
+ // A durable-only or inactive session is still readable. start_live_transcript
6905
+ // reports attachment failures explicitly when another process owns capture.
6906
+ }
6789
6907
  }
6790
6908
  try {
6791
6909
  const { stdout } = await runMinutes(args, 10000);
6792
6910
  // For status queries, a message is helpful. For transcript reads, empty = no new lines.
6793
6911
  const fallback = status_only ? "No transcript data available." : "";
6912
+ if (include_current && !status_only) {
6913
+ const snapshot = parseJsonOutput(stdout || fallback);
6914
+ return {
6915
+ content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }],
6916
+ structuredContent: snapshot,
6917
+ };
6918
+ }
6794
6919
  if (relay) {
6795
6920
  const payload = {
6796
6921
  durable: parseJsonOutput(stdout || fallback),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minutes-mcp",
3
- "version": "0.25.3",
3
+ "version": "0.25.4",
4
4
  "description": "MCP server for minutes \u2014 conversation memory for AI assistants. Works with Claude Desktop, Mistral Vibe, Cursor, Windsurf, and any MCP client.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -45,12 +45,12 @@
45
45
  "dependencies": {
46
46
  "@modelcontextprotocol/ext-apps": "^1.6.0",
47
47
  "@modelcontextprotocol/sdk": "^1.29.0",
48
- "minutes-sdk": "0.25.3",
48
+ "minutes-sdk": "0.25.4",
49
49
  "yaml": "^2.8.3",
50
50
  "zod": "^3.25 || ^4.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^22.19.17",
53
+ "@types/node": "^26.2.0",
54
54
  "tsx": "^4.0.0",
55
55
  "typescript": "^5.5.0",
56
56
  "vite": "^7.3.2",