@vibeframe/cli 0.106.8 → 0.107.1

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,51 @@
1
+ /**
2
+ * @module tools/_shared/long-poll
3
+ * @description Long-poll promotion for long-running MCP tool calls.
4
+ *
5
+ * MCP hosts (notably Claude Desktop) time out tool calls after a minute or
6
+ * two, while `build`/`render` legitimately run for many minutes. The fix:
7
+ * run the work normally, and if it finishes inside the synchronous window
8
+ * return its result untouched — byte-identical to the unwrapped call, and no
9
+ * job record is ever written. If the window elapses first, persist a local
10
+ * {@link JobRecord} (jobType "build"/"render", provider "local"), return a
11
+ * `promoted` envelope immediately, and let the work continue in-process; the
12
+ * settle handlers write the terminal record that `status_job` then serves.
13
+ *
14
+ * CLI surfaces never reach this module — callers gate on
15
+ * `ctx.surface === "mcp"`.
16
+ */
17
+ import type { ToolExecuteResult } from "../define-tool.js";
18
+ export interface LocalJobUpdate {
19
+ /** 0-100 when a percentage is known; otherwise omit. */
20
+ progress?: number;
21
+ /** Pipeline stage label, e.g. "assets" | "compose" | "render". */
22
+ stage?: string;
23
+ message?: string;
24
+ }
25
+ export interface PromotionOptions {
26
+ jobType: "build" | "render";
27
+ /** Absolute project dir — the job record lands in `<projectDir>/.vibeframe/jobs/`. */
28
+ projectDir: string;
29
+ /** CLI equivalent of this invocation, stored on the record for retryWith. */
30
+ command: string;
31
+ /** Synchronous window before promotion. Default 45s; see resolveWindowMs. */
32
+ windowMs?: number;
33
+ /** Heartbeat cadence once promoted. Default 10s. */
34
+ heartbeatMs?: number;
35
+ now?: () => Date;
36
+ }
37
+ /**
38
+ * Resolve the promotion window. `VIBE_MCP_PROMOTE_AFTER_MS` overrides the
39
+ * default; `"off"` or `"0"` disables promotion entirely (pure synchronous
40
+ * behavior — the escape hatch for hosts with generous timeouts).
41
+ */
42
+ export declare function resolveWindowMs(optionMs?: number): number | null;
43
+ /**
44
+ * Run `work`; promote to a polled background job if it outlives the window.
45
+ *
46
+ * The `report` callback passed to `work` is safe to call at any time: before
47
+ * promotion it only buffers the latest update (callers typically also feed
48
+ * `ctx.onProgress` for MCP progress notifications); after promotion it
49
+ * coalesces updates into the job record on disk.
50
+ */
51
+ export declare function runWithMcpPromotion(work: (report: (update: LocalJobUpdate) => void) => Promise<ToolExecuteResult>, opts: PromotionOptions): Promise<ToolExecuteResult>;
@@ -0,0 +1 @@
1
+ export {};
@@ -22,12 +22,41 @@ export interface McpTool {
22
22
  description: string;
23
23
  inputSchema: McpInputSchema;
24
24
  }
25
- export type McpDispatcher = (name: string, args: Record<string, unknown>) => Promise<{
25
+ /** Per-call extras forwarded by the MCP server request handler. */
26
+ export interface McpCallExtra {
27
+ /**
28
+ * Raw progress sink — typically sends an MCP `notifications/progress` for
29
+ * the request's progressToken. The dispatcher wraps it with throttling,
30
+ * monotonicity, and post-completion cutoff before exposing it to tools.
31
+ */
32
+ onProgress?: (update: {
33
+ progress: number;
34
+ total?: number;
35
+ message?: string;
36
+ }) => void;
37
+ }
38
+ export type McpDispatcher = (name: string, args: Record<string, unknown>, extra?: McpCallExtra) => Promise<{
26
39
  content: Array<{
27
40
  type: "text";
28
41
  text: string;
29
42
  }>;
30
43
  }>;
44
+ export interface ProgressSink {
45
+ send: (update: {
46
+ progress?: number;
47
+ total?: number;
48
+ message?: string;
49
+ }) => void;
50
+ close: () => void;
51
+ }
52
+ /**
53
+ * Wrap a raw progress callback with the guarantees the MCP spec expects:
54
+ * `progress` increases monotonically per token, updates are throttled to at
55
+ * most one per `minIntervalMs` (the first always passes), and nothing is
56
+ * emitted after `close()` — progress notifications must stop once the
57
+ * request's result has been sent.
58
+ */
59
+ export declare function makeProgressSink(raw: McpCallExtra["onProgress"], minIntervalMs?: number): ProgressSink;
31
60
  /** Filter manifest by `surfaces.mcp` (default: included) and project to MCP tool shape. */
32
61
  export declare function manifestToMcpTools(manifest: readonly ToolDefinition[]): McpTool[];
33
62
  /** Build the dispatcher used by `handleToolCall` in the MCP server. */
@@ -140,7 +140,16 @@ async function withCapturedStdout(fn) {
140
140
  `;
141
141
  };
142
142
  process.stdout.write = (chunk, encodingOrCallback, callback) => {
143
- captured += formatStdoutChunk(chunk, encodingOrCallback);
143
+ const text = formatStdoutChunk(chunk, encodingOrCallback);
144
+ if (text.startsWith("{") && text.includes('"jsonrpc":"2.0"')) {
145
+ return originalWrite.call(
146
+ process.stdout,
147
+ chunk,
148
+ encodingOrCallback,
149
+ callback
150
+ );
151
+ }
152
+ captured += text;
144
153
  invokeWriteCallback(encodingOrCallback, callback);
145
154
  return true;
146
155
  };
@@ -159,6 +168,32 @@ async function withCapturedStdout(fn) {
159
168
  }
160
169
  }
161
170
  }
171
+ function makeProgressSink(raw, minIntervalMs = 1e3) {
172
+ if (!raw) {
173
+ return { send: () => void 0, close: () => void 0 };
174
+ }
175
+ let closed = false;
176
+ let lastSentAt = 0;
177
+ let lastProgress = 0;
178
+ return {
179
+ send: (update) => {
180
+ if (closed) return;
181
+ const now = Date.now();
182
+ if (lastSentAt !== 0 && now - lastSentAt < minIntervalMs) return;
183
+ lastSentAt = now;
184
+ const progress = update.progress !== void 0 && update.progress > lastProgress ? update.progress : lastProgress + 1;
185
+ lastProgress = progress;
186
+ raw({
187
+ progress,
188
+ ...update.total !== void 0 ? { total: update.total } : {},
189
+ ...update.message ? { message: update.message } : {}
190
+ });
191
+ },
192
+ close: () => {
193
+ closed = true;
194
+ }
195
+ };
196
+ }
162
197
  function formatZodError(err) {
163
198
  const missing = err.issues.filter((i) => {
164
199
  if (i.code !== "invalid_type") return false;
@@ -193,7 +228,7 @@ function buildMcpDispatcher(manifest) {
193
228
  byName.set(t.name, t);
194
229
  }
195
230
  }
196
- return async (name, args) => {
231
+ return async (name, args, extra) => {
197
232
  const tool = byName.get(name);
198
233
  if (!tool) {
199
234
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
@@ -204,11 +239,13 @@ function buildMcpDispatcher(manifest) {
204
239
  content: [{ type: "text", text: `${name} failed: ${formatZodError(parsed.error)}` }]
205
240
  };
206
241
  }
242
+ const sink = makeProgressSink(extra?.onProgress);
207
243
  try {
208
244
  const result = await withCapturedStdout(
209
245
  () => tool.execute(parsed.data, {
210
246
  workingDirectory: process.cwd(),
211
- surface: "mcp"
247
+ surface: "mcp",
248
+ ...extra?.onProgress ? { onProgress: sink.send } : {}
212
249
  })
213
250
  );
214
251
  const text = result.success ? JSON.stringify({ success: true, ...result.data }) : result.data ? JSON.stringify({
@@ -220,10 +257,13 @@ function buildMcpDispatcher(manifest) {
220
257
  } catch (error) {
221
258
  const msg = error instanceof Error ? error.message : String(error);
222
259
  return { content: [{ type: "text", text: `${name} threw: ${msg}` }] };
260
+ } finally {
261
+ sink.close();
223
262
  }
224
263
  };
225
264
  }
226
265
  export {
227
266
  buildMcpDispatcher,
267
+ makeProgressSink,
228
268
  manifestToMcpTools
229
269
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -37,6 +37,18 @@ export interface ExecuteContext {
37
37
  projectPath: string | null;
38
38
  setProjectPath(path: string): void;
39
39
  };
40
+ /**
41
+ * Optional progress sink. The MCP adapter wires this to MCP
42
+ * `notifications/progress` when the client supplied a progressToken;
43
+ * CLI/Agent surfaces leave it undefined. Omitting `progress` advances an
44
+ * internal monotonic counter; the adapter also throttles and enforces
45
+ * monotonicity defensively.
46
+ */
47
+ onProgress?: (update: {
48
+ progress?: number;
49
+ total?: number;
50
+ message?: string;
51
+ }) => void;
40
52
  }
41
53
  export interface ToolExecuteResult {
42
54
  success: boolean;