minutes-mcp 0.21.0 → 0.22.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,35 @@
1
+ export interface CaptureRelayCursor {
2
+ session_id?: string;
3
+ transcript_seq: number;
4
+ nudge_seq: number;
5
+ }
6
+ export interface CaptureRelayDiscovery {
7
+ v: number;
8
+ session_id: string;
9
+ transport: "unix_socket" | "windows_named_pipe";
10
+ endpoint: string;
11
+ owner_pid: number;
12
+ evidence_mode: string;
13
+ auth_token: string;
14
+ started_at: string;
15
+ heartbeat_at: string;
16
+ }
17
+ export interface CaptureRelayFrame {
18
+ type: string;
19
+ seq?: number;
20
+ session_id?: string;
21
+ message?: string;
22
+ [key: string]: unknown;
23
+ }
24
+ export interface CaptureRelaySnapshot {
25
+ discovery: Omit<CaptureRelayDiscovery, "auth_token">;
26
+ cursor: CaptureRelayCursor;
27
+ frames: CaptureRelayFrame[];
28
+ }
29
+ export interface CaptureRelayAttachOptions {
30
+ discoveryPath?: string;
31
+ timeoutMs?: number;
32
+ replayQuietMs?: number;
33
+ }
34
+ export declare function captureRelayDiscoveryPath(): string;
35
+ export declare function attachCaptureRelay(cursor?: CaptureRelayCursor, options?: CaptureRelayAttachOptions): Promise<CaptureRelaySnapshot>;
@@ -0,0 +1,181 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { createConnection } from "node:net";
5
+ const RELAY_PROTOCOL_VERSION = 1;
6
+ const HEARTBEAT_STALE_MS = 5_000;
7
+ export function captureRelayDiscoveryPath() {
8
+ return join(homedir(), ".minutes", "capture-relay.json");
9
+ }
10
+ export async function attachCaptureRelay(cursor = { transcript_seq: 0, nudge_seq: 0 }, options = {}) {
11
+ const discoveryPath = options.discoveryPath ?? captureRelayDiscoveryPath();
12
+ const discovery = await readAndValidateDiscovery(discoveryPath);
13
+ const timeoutMs = options.timeoutMs ?? 1_000;
14
+ const replayQuietMs = options.replayQuietMs ?? 40;
15
+ return await new Promise((resolvePromise, rejectPromise) => {
16
+ let socket;
17
+ let settled = false;
18
+ let acknowledged = false;
19
+ let buffer = "";
20
+ let quietTimer;
21
+ const frames = [];
22
+ const nextCursor = {
23
+ session_id: cursor.session_id,
24
+ transcript_seq: cursor.transcript_seq,
25
+ nudge_seq: cursor.nudge_seq,
26
+ };
27
+ const finish = (error) => {
28
+ if (settled)
29
+ return;
30
+ settled = true;
31
+ clearTimeout(timeoutTimer);
32
+ if (quietTimer)
33
+ clearTimeout(quietTimer);
34
+ socket?.destroy();
35
+ if (error) {
36
+ rejectPromise(error);
37
+ return;
38
+ }
39
+ resolvePromise({
40
+ discovery: redactDiscovery(discovery),
41
+ cursor: nextCursor,
42
+ frames,
43
+ });
44
+ };
45
+ const scheduleReplayComplete = () => {
46
+ if (!acknowledged)
47
+ return;
48
+ if (quietTimer)
49
+ clearTimeout(quietTimer);
50
+ quietTimer = setTimeout(() => finish(), replayQuietMs);
51
+ };
52
+ const timeoutTimer = setTimeout(() => finish(new Error("the capture attachment relay did not respond in time")), timeoutMs);
53
+ socket = createConnection(discovery.endpoint);
54
+ socket.setEncoding("utf8");
55
+ socket.once("connect", () => {
56
+ socket?.write(`${JSON.stringify({
57
+ v: RELAY_PROTOCOL_VERSION,
58
+ auth_token: discovery.auth_token,
59
+ action: { type: "observe", cursor },
60
+ })}\n`);
61
+ });
62
+ socket.on("data", (chunk) => {
63
+ buffer += chunk;
64
+ while (true) {
65
+ const newline = buffer.indexOf("\n");
66
+ if (newline < 0)
67
+ break;
68
+ const line = buffer.slice(0, newline).trim();
69
+ buffer = buffer.slice(newline + 1);
70
+ if (!line)
71
+ continue;
72
+ let frame;
73
+ try {
74
+ frame = JSON.parse(line);
75
+ }
76
+ catch {
77
+ finish(new Error("the capture attachment relay sent invalid data"));
78
+ return;
79
+ }
80
+ if (frame.type === "error") {
81
+ finish(new Error(String(frame.message ?? "capture attachment failed")));
82
+ return;
83
+ }
84
+ if (!acknowledged) {
85
+ if (frame.type !== "attached" && frame.type !== "cursor_reset") {
86
+ finish(new Error("the capture attachment relay did not acknowledge this client"));
87
+ return;
88
+ }
89
+ acknowledged = true;
90
+ nextCursor.session_id = discovery.session_id;
91
+ }
92
+ if (frame.type === "cursor_reset") {
93
+ nextCursor.session_id = String(frame.session_id ?? discovery.session_id);
94
+ nextCursor.transcript_seq = 0;
95
+ nextCursor.nudge_seq = 0;
96
+ }
97
+ else if (frame.type === "transcript" && typeof frame.seq === "number") {
98
+ nextCursor.transcript_seq = Math.max(nextCursor.transcript_seq, frame.seq);
99
+ }
100
+ else if (frame.type === "nudge" && typeof frame.seq === "number") {
101
+ nextCursor.nudge_seq = Math.max(nextCursor.nudge_seq, frame.seq);
102
+ }
103
+ frames.push(frame);
104
+ }
105
+ scheduleReplayComplete();
106
+ });
107
+ socket.once("error", (error) => finish(error));
108
+ socket.once("close", () => {
109
+ if (!settled && !acknowledged) {
110
+ finish(new Error("the capture owner closed the attachment relay"));
111
+ }
112
+ else if (!settled) {
113
+ finish();
114
+ }
115
+ });
116
+ });
117
+ }
118
+ async function readAndValidateDiscovery(path) {
119
+ let raw;
120
+ try {
121
+ raw = await readFile(path, "utf8");
122
+ }
123
+ catch {
124
+ throw new Error("no active capture attachment relay was found");
125
+ }
126
+ let discovery;
127
+ try {
128
+ discovery = JSON.parse(raw);
129
+ }
130
+ catch {
131
+ throw new Error("the capture attachment discovery file is invalid");
132
+ }
133
+ if (discovery.v !== RELAY_PROTOCOL_VERSION) {
134
+ throw new Error(`capture attachment protocol ${discovery.v} is not supported`);
135
+ }
136
+ if (!Number.isSafeInteger(discovery.owner_pid) || discovery.owner_pid <= 0) {
137
+ throw new Error("the capture attachment owner PID is invalid");
138
+ }
139
+ const heartbeat = Date.parse(discovery.heartbeat_at);
140
+ if (!Number.isFinite(heartbeat) || Date.now() - heartbeat > HEARTBEAT_STALE_MS) {
141
+ throw new Error("the capture attachment heartbeat is stale");
142
+ }
143
+ if (!discovery.auth_token || discovery.auth_token.length < 32) {
144
+ throw new Error("the capture attachment token is invalid");
145
+ }
146
+ validateLocalEndpoint(path, discovery);
147
+ if (process.platform !== "win32") {
148
+ const mode = (await stat(path)).mode & 0o777;
149
+ if ((mode & 0o077) !== 0) {
150
+ throw new Error("the capture attachment discovery file is not private to this user");
151
+ }
152
+ }
153
+ try {
154
+ process.kill(discovery.owner_pid, 0);
155
+ }
156
+ catch (error) {
157
+ if (error?.code === "ESRCH") {
158
+ throw new Error(`capture owner process ${discovery.owner_pid} is no longer running`);
159
+ }
160
+ }
161
+ return discovery;
162
+ }
163
+ function validateLocalEndpoint(discoveryPath, discovery) {
164
+ if (discovery.transport === "windows_named_pipe") {
165
+ if (!discovery.endpoint.toLowerCase().startsWith("\\\\.\\pipe\\minutes-capture-")) {
166
+ throw new Error("the capture attachment named pipe is not a Minutes local endpoint");
167
+ }
168
+ return;
169
+ }
170
+ if (discovery.transport !== "unix_socket") {
171
+ throw new Error("the capture attachment transport is invalid");
172
+ }
173
+ const expected = resolve(dirname(discoveryPath), "capture-relay.sock");
174
+ if (resolve(discovery.endpoint) !== expected) {
175
+ throw new Error("the capture attachment socket is outside the private Minutes directory");
176
+ }
177
+ }
178
+ function redactDiscovery(discovery) {
179
+ const { auth_token: _authToken, ...safe } = discovery;
180
+ return safe;
181
+ }
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  * - activity_summary: Summarize meeting-adjacent desktop context for a session/path/window
15
15
  * - search_context: Search app and captured window-title desktop context
16
16
  * - get_moment: Show the local rewind around a linked artifact, session, or timestamp
17
+ * - get_screen_context: Retrieve bounded, session-linked screen images
17
18
  * - consistency_report: Flag conflicting decisions and stale commitments
18
19
  * - get_person_profile: Rich relationship profile for a person (graph index)
19
20
  * - track_commitments: List open/stale commitments, filter by person
@@ -24,6 +25,8 @@
24
25
  * - list_voices: List enrolled voice profiles for speaker identification
25
26
  * - confirm_speaker: Confirm/correct speaker attribution in a meeting
26
27
  * - get_meeting_insights: Query structured insights (decisions, commitments, etc.) with confidence filtering
28
+ * - start_copilot / stop_copilot: Control the independent real-time copilot engine
29
+ * - copilot_status / read_copilot_nudges: Observe copilot health and cursor-based nudges
27
30
  *
28
31
  * All tools use execFile (not exec) to shell out to the `minutes` CLI binary.
29
32
  * No shell interpolation — safe from injection.
@@ -94,6 +97,61 @@ export declare function shouldRunMainEntry(argv1: string | null | undefined, mod
94
97
  export declare function parseKnowledgeConfig(configContent: string): KnowledgeConfigStatus | null;
95
98
  export declare const LIVE_EVENTS_RESOURCE_URI = "minutes://events/live";
96
99
  export declare const LIVE_EVENTS_URI_TEMPLATE = "minutes://events/live{?since_seq,limit}";
100
+ export declare const LIVE_COPILOT_RESOURCE_URI = "minutes://live/copilot";
101
+ type JsonObject = Record<string, unknown>;
102
+ export type CopilotStatusPayload = {
103
+ available: boolean;
104
+ active: boolean;
105
+ state: string;
106
+ pid: number | null;
107
+ goal: string | null;
108
+ surface: string | null;
109
+ provider: string | null;
110
+ model: string | null;
111
+ evidence_cursor: number;
112
+ capture_attachment: string | null;
113
+ last_error: string | null;
114
+ raw: string;
115
+ error?: string;
116
+ };
117
+ export type CopilotObserverSession = {
118
+ v: 1;
119
+ pid: number;
120
+ goal: string;
121
+ surface: "stdout" | "tui";
122
+ started_ts: string;
123
+ };
124
+ export type ObservedCopilotNudge = {
125
+ cursor: number;
126
+ format: "json" | "text" | "raw";
127
+ nudge: JsonObject;
128
+ raw: string;
129
+ observed_ts: string | null;
130
+ expires_at: string | null;
131
+ expired: boolean | null;
132
+ };
133
+ export type CopilotNudgeObservation = {
134
+ attached: boolean;
135
+ cursor: number;
136
+ session: CopilotObserverSession | null;
137
+ nudges: ObservedCopilotNudge[];
138
+ note: string;
139
+ };
140
+ export type LiveCopilotResourcePayload = {
141
+ v: 1;
142
+ resource: typeof LIVE_COPILOT_RESOURCE_URI;
143
+ available: boolean;
144
+ active: boolean;
145
+ state: string;
146
+ status: CopilotStatusPayload;
147
+ nudge_stream: {
148
+ attached: boolean;
149
+ cursor: number;
150
+ note: string;
151
+ };
152
+ latest_nudge: ObservedCopilotNudge | null;
153
+ current_nudge: ObservedCopilotNudge | null;
154
+ };
97
155
  export type LiveEventsResourceOptions = {
98
156
  uri: string;
99
157
  sinceSeq: number | null;
@@ -114,10 +172,30 @@ export type LiveEventsResourcePayload = {
114
172
  };
115
173
  export declare function parseLiveEventsResourceUri(rawUri: string): LiveEventsResourceOptions | null;
116
174
  export declare function buildLiveEventsResourcePayload(options: LiveEventsResourceOptions, events: unknown[], latestSeq: number): LiveEventsResourcePayload;
175
+ /** Parse the stable human-readable `minutes copilot status` surface. */
176
+ export declare function parseCopilotStatusOutput(raw: string): CopilotStatusPayload;
177
+ /** Parse the CLI's stdout presentation stream without invoking inference. */
178
+ export declare function parseCopilotNudgeLog(raw: string, nowMs?: number, latestObservedMs?: number): ObservedCopilotNudge[];
179
+ export declare function buildLiveCopilotResourcePayload(status: CopilotStatusPayload, observation: CopilotNudgeObservation): LiveCopilotResourcePayload;
180
+ export type CopilotNudgeReadPage = {
181
+ cursor: number;
182
+ next_cursor: number;
183
+ cursor_reset: boolean;
184
+ has_more: boolean;
185
+ nudges: ObservedCopilotNudge[];
186
+ };
187
+ export declare function selectCopilotNudges(observation: CopilotNudgeObservation, options: {
188
+ cursor?: number;
189
+ since?: string;
190
+ limit?: number;
191
+ }, nowMs?: number): CopilotNudgeReadPage;
117
192
  export type LiveEventsSubscriptionOptions = {
118
193
  pollIntervalMs?: number;
194
+ enableLiveEvents?: boolean;
195
+ enableCopilot?: boolean;
119
196
  latestEventSeq?: () => Promise<number>;
120
197
  readEventsSinceSeq?: (sinceSeq: number, limit: number) => Promise<unknown[]>;
198
+ copilotFingerprint?: () => Promise<string>;
121
199
  sendResourceUpdated?: (uri: string) => Promise<void>;
122
200
  onError?: (error: unknown) => void;
123
201
  };