veryfront 0.1.209 → 0.1.211

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.
@@ -99,6 +99,59 @@ export function captureStreamedToolCallInput(toolCall) {
99
99
  ...(error ? { parseError: error } : {}),
100
100
  };
101
101
  }
102
+ /**
103
+ * A streamed tool call is "incomplete" when the provider stream terminated
104
+ * (abort, stall, timeout, transport error) before the SDK emitted the
105
+ * finalizing `tool-call` event that sets `inputAvailable: true`. In that state
106
+ * `arguments` only holds partial JSON fragments from `tool-input-delta` events,
107
+ * so the tool call is NOT a committed model choice and must not be parsed or
108
+ * executed. This is semantically distinct from a parse failure on a finalized
109
+ * tool call (`inputAvailable: true` but malformed JSON — which only happens on
110
+ * genuine provider bugs) and needs to be reported as a stream-termination
111
+ * error rather than a tool-argument error.
112
+ */
113
+ export function isStreamedToolCallIncomplete(toolCall) {
114
+ return toolCall.inputAvailable !== true;
115
+ }
116
+ /**
117
+ * Classify and build the persisted `MessagePart` for a single streamed tool
118
+ * call. Pure function — no logging, no SSE, no memory. Callers decide what to
119
+ * do with the result so this stays unit-testable.
120
+ *
121
+ * The resulting `part` is always pushed into the assistant message so the
122
+ * conversation history is transparent: even incomplete tool calls leave a
123
+ * visible trace with their partial `inputText`. What differs is the caller's
124
+ * error-surfacing behavior (log warning, SSE event, tool-result error).
125
+ */
126
+ export function materializeStreamedToolCall(tc) {
127
+ const basePart = {
128
+ type: `tool-${tc.name}`,
129
+ toolCallId: tc.id,
130
+ toolName: tc.name,
131
+ args: {},
132
+ ...(tc.arguments.length > 0 ? { inputText: tc.arguments } : {}),
133
+ };
134
+ if (isStreamedToolCallIncomplete(tc)) {
135
+ return {
136
+ kind: "incomplete",
137
+ part: basePart,
138
+ partialArgumentsLength: tc.arguments.length,
139
+ partialArgumentsPreview: tc.arguments.slice(0, 200),
140
+ };
141
+ }
142
+ const capturedInput = captureStreamedToolCallInput(tc);
143
+ const part = {
144
+ type: `tool-${tc.name}`,
145
+ toolCallId: tc.id,
146
+ toolName: tc.name,
147
+ args: capturedInput.args,
148
+ ...(capturedInput.inputText ? { inputText: capturedInput.inputText } : {}),
149
+ };
150
+ if (capturedInput.parseError) {
151
+ return { kind: "parse-error", part, parseError: capturedInput.parseError };
152
+ }
153
+ return { kind: "complete", part };
154
+ }
102
155
  function isToolResultPart(part) {
103
156
  return part.type === "tool-result" && "result" in part;
104
157
  }
@@ -649,20 +702,35 @@ export class AgentRuntime {
649
702
  if (state.accumulatedText)
650
703
  streamParts.push({ type: "text", text: state.accumulatedText });
651
704
  for (const tc of state.toolCalls.values()) {
652
- const capturedInput = captureStreamedToolCallInput(tc);
653
- if (capturedInput.parseError) {
705
+ const materialized = materializeStreamedToolCall(tc);
706
+ streamParts.push(materialized.part);
707
+ if (materialized.kind === "incomplete") {
708
+ // Stream terminated before the provider emitted the finalizing
709
+ // `tool-call` event for this block. The model never committed this
710
+ // tool use. Surface the failure via SSE so the live client can
711
+ // react, and leave the partial fragment under `inputText` in the
712
+ // persisted part above so the history is replayable and transparent.
713
+ logger.warn("Streamed tool call terminated before tool-call event", {
714
+ toolCallId: tc.id,
715
+ toolName: tc.name,
716
+ partialArgumentsLength: materialized.partialArgumentsLength,
717
+ partialArgumentsPreview: materialized.partialArgumentsPreview,
718
+ });
719
+ const dynamicIncomplete = isDynamicTool(tc.name);
720
+ sendSSE(controller, encoder, {
721
+ type: "tool-input-error",
722
+ toolCallId: tc.id,
723
+ errorText: `Stream terminated before tool-call event fired for "${tc.name}". ` +
724
+ `Received ${materialized.partialArgumentsLength} chars of partial tool-input deltas.`,
725
+ ...(dynamicIncomplete ? { dynamic: true } : {}),
726
+ });
727
+ }
728
+ else if (materialized.kind === "parse-error") {
654
729
  logger.warn("Failed to parse streamed tool arguments", {
655
730
  toolCallId: tc.id,
656
- error: capturedInput.parseError,
731
+ error: materialized.parseError,
657
732
  });
658
733
  }
659
- streamParts.push({
660
- type: `tool-${tc.name}`,
661
- toolCallId: tc.id,
662
- toolName: tc.name,
663
- args: capturedInput.args,
664
- ...(capturedInput.inputText ? { inputText: capturedInput.inputText } : {}),
665
- });
666
734
  }
667
735
  const assistantMessage = {
668
736
  id: `msg_${Date.now()}_${step}`,
@@ -711,6 +779,23 @@ export class AgentRuntime {
711
779
  streamedToolCalls.some((tc) => tc.name === LOAD_SKILL_TOOL_ID);
712
780
  for (const tc of streamedToolCalls) {
713
781
  throwIfAborted(abortSignal);
782
+ if (isStreamedToolCallIncomplete(tc)) {
783
+ // Stream ended before the provider finalized this tool call. We
784
+ // cannot execute it — record a distinct stream-termination error
785
+ // (not a tool-argument parse error) so the parent step and any
786
+ // upstream orchestrator (e.g. the child-fork watchdog) see a
787
+ // completed step with a clearly-labelled failure and can recover.
788
+ const incompleteToolCall = {
789
+ id: tc.id,
790
+ name: tc.name,
791
+ args: {},
792
+ ...(tc.arguments.length > 0 ? { inputText: tc.arguments } : {}),
793
+ status: "pending",
794
+ };
795
+ await this.recordToolError(incompleteToolCall, `Stream terminated before tool-call event fired for "${tc.name}". ` +
796
+ `Received ${tc.arguments.length} chars of partial tool-input deltas.`, controller, encoder, currentMessages, toolCalls);
797
+ continue;
798
+ }
714
799
  const capturedInput = captureStreamedToolCallInput(tc);
715
800
  const toolCall = {
716
801
  id: tc.id,
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.209";
1
+ export declare const VERSION = "0.1.211";
2
2
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.209";
3
+ export const VERSION = "0.1.211";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.209",
3
+ "version": "0.1.211",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
@@ -21,6 +21,9 @@ import {
21
21
  vfListLocalProjects,
22
22
  vfListRoutes,
23
23
  } from "./tools/project-tools.js";
24
+ import { vfBuild } from "./tools/build-tool.js";
25
+ import { vfRunLint } from "./tools/run-lint-tool.js";
26
+ import { vfRunTests } from "./tools/run-tests-tool.js";
24
27
  import { vfGetConventions, vfScaffold } from "./tools/scaffold-tools.js";
25
28
  import { vfGetSkillReference, vfGetSkills } from "./tools/skill-tools.js";
26
29
  import { cicdTools } from "./tools/cicd-tools.js";
@@ -44,8 +47,11 @@ export const advancedTools: MCPTool[] = [
44
47
  vfPreviewRoute,
45
48
  vfGetDebugContext,
46
49
  vfGetComponentTree,
50
+ vfBuild,
47
51
  vfHotReload,
48
52
  vfTriggerHmr,
49
53
  vfWaitForReady,
54
+ vfRunLint,
50
55
  vfGetFlywheelStatus,
56
+ vfRunTests,
51
57
  ];
@@ -411,6 +411,62 @@ export class StandaloneMCPServer {
411
411
  }
412
412
  },
413
413
  },
414
+ {
415
+ name: "vf_run_tests",
416
+ description: "Run the project's test suite and get structured pass/fail results. " +
417
+ "Returns a summary with total, passed, failed, skipped counts and failure details " +
418
+ "including file path, test name, error message, and line number. " +
419
+ "Do not use for lint checks — use vf_run_lint instead.",
420
+ inputSchema: {
421
+ type: "object",
422
+ properties: {
423
+ filter: {
424
+ type: "string",
425
+ description: "Filter tests by name pattern",
426
+ },
427
+ parallel: {
428
+ type: "boolean",
429
+ description: "Run tests in parallel",
430
+ },
431
+ timeout: {
432
+ type: "number",
433
+ description:
434
+ "Maximum time to wait for test completion in milliseconds (default: 300000)",
435
+ },
436
+ },
437
+ },
438
+ async execute(args) {
439
+ const { executeTests } = await import("./tools/run-tests-tool.js");
440
+ return executeTests({
441
+ filter: args.filter as string | undefined,
442
+ parallel: args.parallel as boolean | undefined,
443
+ timeout: args.timeout as number | undefined,
444
+ });
445
+ },
446
+ },
447
+ {
448
+ name: "vf_run_lint",
449
+ description:
450
+ "Run the linter. Returns structured diagnostics with file, line, column, rule code, and message. " +
451
+ "Do not use for test results — use vf_run_tests instead. " +
452
+ "Do not use for compile/runtime errors — use vf_get_errors instead.",
453
+ inputSchema: {
454
+ type: "object",
455
+ properties: {
456
+ timeout: {
457
+ type: "number",
458
+ description:
459
+ "Maximum time to wait for lint completion in milliseconds (default: 120000)",
460
+ },
461
+ },
462
+ },
463
+ async execute(args) {
464
+ const { executeLint } = await import("./tools/run-lint-tool.js");
465
+ return executeLint({
466
+ timeout: args.timeout as number | undefined,
467
+ });
468
+ },
469
+ },
414
470
  ...this.createContext7Tools(),
415
471
  ];
416
472
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * MCP tool for production builds.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { cwd } from "../../../src/platform/index.js";
7
+ import { join } from "../../../src/platform/compat/path/index.js";
8
+ import { buildProduction } from "../../../src/build/index.js";
9
+ import { withSpan } from "../../../src/observability/tracing/otlp-setup.js";
10
+ import type { MCPTool } from "../tools.js";
11
+
12
+ // ============================================================================
13
+ // Tool: vf_build
14
+ // ============================================================================
15
+
16
+ const buildInput = z.object({
17
+ outputDir: z
18
+ .string()
19
+ .optional()
20
+ .describe("Output directory for the build. Defaults to '<projectDir>/dist'."),
21
+ splitting: z
22
+ .boolean()
23
+ .optional()
24
+ .default(true)
25
+ .describe("Enable code splitting. Defaults to true."),
26
+ compress: z
27
+ .boolean()
28
+ .optional()
29
+ .default(true)
30
+ .describe("Enable compression (gzip/brotli). Defaults to true."),
31
+ ssg: z
32
+ .boolean()
33
+ .optional()
34
+ .default(true)
35
+ .describe("Enable static site generation. Defaults to true."),
36
+ dryRun: z
37
+ .boolean()
38
+ .optional()
39
+ .default(false)
40
+ .describe("Preview the build without writing files to disk. Defaults to false."),
41
+ });
42
+
43
+ type BuildInput = z.infer<typeof buildInput>;
44
+
45
+ interface BuildResult {
46
+ success: boolean;
47
+ pages?: number;
48
+ chunks?: number;
49
+ assets?: number;
50
+ totalSize?: number;
51
+ duration_ms?: number;
52
+ outputDir?: string;
53
+ dryRun?: boolean;
54
+ ssgPaths?: string[];
55
+ error?: string;
56
+ }
57
+
58
+ export const vfBuild: MCPTool<BuildInput, BuildResult> = {
59
+ name: "vf_build",
60
+ title: "Production Build",
61
+ annotations: {
62
+ readOnlyHint: false,
63
+ destructiveHint: false,
64
+ idempotentHint: true,
65
+ openWorldHint: false,
66
+ },
67
+ description: "Use this when you need to run a production build for the current project. " +
68
+ "Bundles, optimises, and writes output to the dist directory. " +
69
+ "Use dryRun=true to preview the build without writing files. " +
70
+ "Do not use for development — use the dev server tools instead. " +
71
+ "Do not use for lint checks — use vf_run_lint instead.",
72
+ inputSchema: buildInput,
73
+ execute: (input) =>
74
+ withSpan(
75
+ "cli.mcp.tool.vf_build",
76
+ async () => {
77
+ const projectDir = cwd();
78
+ const outputDir = input.outputDir ?? join(projectDir, "dist");
79
+ const startTime = Date.now();
80
+
81
+ try {
82
+ const stats = await buildProduction({
83
+ projectDir,
84
+ outputDir,
85
+ enableSplitting: input.splitting,
86
+ enableCompression: input.compress,
87
+ ssg: input.ssg,
88
+ dryRun: input.dryRun,
89
+ });
90
+
91
+ const duration_ms = Date.now() - startTime;
92
+
93
+ return {
94
+ success: true,
95
+ pages: stats.pages,
96
+ chunks: stats.chunks,
97
+ assets: stats.assets,
98
+ totalSize: stats.totalSize,
99
+ duration_ms,
100
+ outputDir,
101
+ dryRun: input.dryRun,
102
+ ssgPaths: stats.ssgPaths,
103
+ };
104
+ } catch (error) {
105
+ const duration_ms = Date.now() - startTime;
106
+ return {
107
+ success: false,
108
+ error: error instanceof Error ? error.message : String(error),
109
+ duration_ms,
110
+ };
111
+ }
112
+ },
113
+ { "tool.dryRun": input.dryRun },
114
+ ),
115
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * MCP tool: vf_run_lint
3
+ *
4
+ * Runs the linter via subprocess and returns structured diagnostics.
5
+ * Reuses parseLintJsonOutput from the CLI lint command.
6
+ */
7
+ import * as dntShim from "../../../_dnt.shims.js";
8
+
9
+
10
+ import { z } from "zod";
11
+ import type { MCPTool } from "../tools.js";
12
+ import { type LintResult, parseLintJsonOutput } from "../../commands/lint/command.js";
13
+
14
+ const runLintInput = z.object({
15
+ timeout: z.number().optional().default(120000).describe(
16
+ "Maximum time to wait for lint completion in milliseconds. Defaults to 120000 (2 minutes).",
17
+ ),
18
+ });
19
+
20
+ type RunLintInput = z.infer<typeof runLintInput>;
21
+
22
+ /** Spawn deno lint and return structured results. Exported for standalone reuse. */
23
+ export async function executeLint(
24
+ input: { timeout?: number } = {},
25
+ ): Promise<LintResult> {
26
+ const cmd = new dntShim.Deno.Command("deno", {
27
+ args: ["lint", "--json"],
28
+ stdout: "piped",
29
+ stderr: "piped",
30
+ });
31
+
32
+ const child = cmd.spawn();
33
+ const timeoutMs = input.timeout ?? 120000;
34
+
35
+ const result = await Promise.race([
36
+ child.output(),
37
+ new Promise<never>((_, reject) => {
38
+ const timer = dntShim.setTimeout(() => {
39
+ try {
40
+ child.kill();
41
+ } catch {
42
+ // Process may have already exited
43
+ }
44
+ reject(new Error(`Lint execution timed out after ${timeoutMs}ms`));
45
+ }, timeoutMs);
46
+ dntShim.Deno.unrefTimer(timer);
47
+ }),
48
+ ]);
49
+
50
+ const stdout = new TextDecoder().decode(result.stdout);
51
+ return parseLintJsonOutput(stdout, result.code);
52
+ }
53
+
54
+ export const vfRunLint: MCPTool<RunLintInput, LintResult> = {
55
+ name: "vf_run_lint",
56
+ title: "Run Lint",
57
+ annotations: {
58
+ readOnlyHint: true,
59
+ destructiveHint: false,
60
+ idempotentHint: true,
61
+ openWorldHint: false,
62
+ },
63
+ description: "Use this when you need to check for lint issues in the project. " +
64
+ "Returns structured diagnostics with file path, line, column, rule code, and message for each issue. " +
65
+ "Do not use for test results — use vf_run_tests instead. " +
66
+ "Do not use for compile/runtime errors — use vf_get_errors instead.",
67
+ inputSchema: runLintInput,
68
+ execute: (input) => executeLint(input),
69
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * MCP tool: vf_run_tests
3
+ *
4
+ * Runs the project test suite via subprocess and returns structured results.
5
+ * Reuses parseTestOutput from the CLI test command.
6
+ */
7
+ import * as dntShim from "../../../_dnt.shims.js";
8
+
9
+
10
+ import { z } from "zod";
11
+ import type { MCPTool } from "../../../src/mcp/index.js";
12
+ import { parseTestOutput, type TestResult } from "../../commands/test/command.js";
13
+
14
+ const runTestsInput = z.object({
15
+ filter: z.string().optional().describe(
16
+ "Filter tests by name pattern. Example: 'router' to run only tests matching 'router'.",
17
+ ),
18
+ parallel: z.boolean().optional().default(false).describe(
19
+ "Run tests in parallel. Defaults to false.",
20
+ ),
21
+ timeout: z.number().optional().default(300000).describe(
22
+ "Maximum time to wait for test completion in milliseconds. Defaults to 300000 (5 minutes).",
23
+ ),
24
+ });
25
+
26
+ type RunTestsInput = z.infer<typeof runTestsInput>;
27
+
28
+ /** Build the deno test command args from input options. */
29
+ export function buildTestArgs(input: { filter?: string; parallel?: boolean }): string[] {
30
+ return [
31
+ "test",
32
+ "--no-check",
33
+ "--allow-all",
34
+ "--unstable-worker-options",
35
+ "--unstable-net",
36
+ ...(input.parallel ? ["--parallel"] : []),
37
+ ...(input.filter ? [`--filter=${input.filter}`] : []),
38
+ ];
39
+ }
40
+
41
+ /** Env vars required for deterministic test runs. */
42
+ export const TEST_ENV: Record<string, string> = {
43
+ VF_DISABLE_LRU_INTERVAL: "1",
44
+ SSR_TRANSFORM_PER_PROJECT_LIMIT: "0",
45
+ REVALIDATION_PER_PROJECT_LIMIT: "0",
46
+ NODE_ENV: "production",
47
+ LOG_FORMAT: "text",
48
+ };
49
+
50
+ /** Spawn deno test and return structured results. Exported for standalone reuse. */
51
+ export async function executeTests(
52
+ input: { filter?: string; parallel?: boolean; timeout?: number },
53
+ ): Promise<TestResult> {
54
+ const cmd = new dntShim.Deno.Command("deno", {
55
+ args: buildTestArgs(input),
56
+ stdout: "piped",
57
+ stderr: "piped",
58
+ env: TEST_ENV,
59
+ });
60
+
61
+ const child = cmd.spawn();
62
+ const timeoutMs = input.timeout ?? 300000;
63
+
64
+ const result = await Promise.race([
65
+ child.output(),
66
+ new Promise<never>((_, reject) => {
67
+ const timer = dntShim.setTimeout(() => {
68
+ try {
69
+ child.kill();
70
+ } catch {
71
+ // Process may have already exited
72
+ }
73
+ reject(new Error(`Test execution timed out after ${timeoutMs}ms`));
74
+ }, timeoutMs);
75
+ // Don't prevent process exit while waiting
76
+ dntShim.Deno.unrefTimer(timer);
77
+ }),
78
+ ]);
79
+
80
+ const stdout = new TextDecoder().decode(result.stdout);
81
+ const stderr = new TextDecoder().decode(result.stderr);
82
+ return parseTestOutput(stdout + "\n" + stderr, result.code);
83
+ }
84
+
85
+ export const vfRunTests: MCPTool<RunTestsInput, TestResult> = {
86
+ name: "vf_run_tests",
87
+ title: "Run Tests",
88
+ annotations: {
89
+ readOnlyHint: false,
90
+ destructiveHint: false,
91
+ idempotentHint: true,
92
+ openWorldHint: false,
93
+ },
94
+ description:
95
+ "Use this when you need to run the project's test suite and get structured pass/fail results. " +
96
+ "Returns a summary with total, passed, failed, skipped counts and failure details including " +
97
+ "file path, test name, error message, and line number. " +
98
+ "Do not use for lint checks — use vf_run_lint instead.",
99
+ inputSchema: runTestsInput,
100
+ execute: (input) => executeTests(input),
101
+ };
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.209",
3
+ "version": "0.1.211",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -26,14 +26,41 @@ export function stripLeadingEmptyObjectPlaceholder(rawArgs: string): string {
26
26
  }
27
27
 
28
28
  export function mergeToolInputDelta(currentArguments: string, nextDelta: string): string {
29
+ const normalizedDelta = nextDelta.trimStart();
30
+ const candidateDeltas = normalizedDelta.startsWith('"')
31
+ ? [normalizedDelta, `{${normalizedDelta}`]
32
+ : [normalizedDelta];
33
+
29
34
  if (currentArguments === "{}") {
30
- const normalizedDelta = nextDelta.trimStart();
31
- if (normalizedDelta.startsWith("{")) {
32
- return normalizedDelta;
35
+ for (const candidate of candidateDeltas) {
36
+ if (candidate.startsWith("{")) {
37
+ return candidate;
38
+ }
39
+ }
40
+ }
41
+
42
+ if (nextDelta.length === 0) {
43
+ return currentArguments;
44
+ }
45
+
46
+ if (currentArguments.length === 0) {
47
+ return nextDelta;
48
+ }
49
+
50
+ for (const candidate of candidateDeltas) {
51
+ if (candidate === currentArguments || currentArguments.includes(candidate)) {
52
+ return currentArguments;
33
53
  }
34
54
 
35
- if (normalizedDelta.startsWith('"')) {
36
- return `{${normalizedDelta}`;
55
+ if (candidate.startsWith(currentArguments)) {
56
+ return candidate;
57
+ }
58
+
59
+ const maxOverlap = Math.min(currentArguments.length, candidate.length);
60
+ for (let overlap = maxOverlap; overlap > 0; overlap--) {
61
+ if (currentArguments.endsWith(candidate.slice(0, overlap))) {
62
+ return currentArguments + candidate.slice(overlap);
63
+ }
37
64
  }
38
65
  }
39
66