veryfront 0.1.131 → 0.1.136

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.
Files changed (140) hide show
  1. package/esm/_dnt.polyfills.d.ts +11 -0
  2. package/esm/_dnt.polyfills.d.ts.map +1 -1
  3. package/esm/_dnt.polyfills.js +14 -0
  4. package/esm/cli/commands/build/handler.js +2 -0
  5. package/esm/cli/commands/deploy/command.d.ts.map +1 -1
  6. package/esm/cli/commands/deploy/command.js +2 -0
  7. package/esm/cli/commands/files/command.d.ts.map +1 -1
  8. package/esm/cli/commands/files/command.js +1 -0
  9. package/esm/cli/commands/uploads/command.d.ts.map +1 -1
  10. package/esm/cli/commands/uploads/command.js +1 -0
  11. package/esm/cli/help/tips.d.ts +3 -0
  12. package/esm/cli/help/tips.d.ts.map +1 -1
  13. package/esm/cli/help/tips.js +15 -1
  14. package/esm/cli/router.d.ts.map +1 -1
  15. package/esm/cli/router.js +4 -0
  16. package/esm/cli/shared/animation.d.ts +3 -0
  17. package/esm/cli/shared/animation.d.ts.map +1 -0
  18. package/esm/cli/shared/animation.js +23 -0
  19. package/esm/cli/templates/manifest.d.ts +6 -0
  20. package/esm/cli/templates/manifest.js +12 -6
  21. package/esm/cli/ui/progress.d.ts.map +1 -1
  22. package/esm/cli/ui/progress.js +13 -1
  23. package/esm/deno.js +1 -1
  24. package/esm/src/agent/index.d.ts +1 -1
  25. package/esm/src/agent/index.d.ts.map +1 -1
  26. package/esm/src/agent/runtime/ai-stream-handler.d.ts.map +1 -1
  27. package/esm/src/agent/runtime/ai-stream-handler.js +56 -5
  28. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  29. package/esm/src/agent/runtime/index.js +21 -3
  30. package/esm/src/agent/runtime/model-tool-converter.d.ts +5 -1
  31. package/esm/src/agent/runtime/model-tool-converter.d.ts.map +1 -1
  32. package/esm/src/agent/runtime/model-tool-converter.js +35 -4
  33. package/esm/src/agent/runtime/tool-helpers.d.ts +2 -1
  34. package/esm/src/agent/runtime/tool-helpers.d.ts.map +1 -1
  35. package/esm/src/agent/runtime/tool-helpers.js +6 -3
  36. package/esm/src/agent/types.d.ts +19 -0
  37. package/esm/src/agent/types.d.ts.map +1 -1
  38. package/esm/src/channels/control-plane.d.ts +67 -0
  39. package/esm/src/channels/control-plane.d.ts.map +1 -1
  40. package/esm/src/channels/control-plane.js +27 -0
  41. package/esm/src/discovery/handlers/tool-handler.d.ts.map +1 -1
  42. package/esm/src/discovery/handlers/tool-handler.js +12 -2
  43. package/esm/src/html/html-injection.d.ts +2 -0
  44. package/esm/src/html/html-injection.d.ts.map +1 -1
  45. package/esm/src/html/html-injection.js +10 -5
  46. package/esm/src/html/nonce-injection.d.ts +3 -0
  47. package/esm/src/html/nonce-injection.d.ts.map +1 -0
  48. package/esm/src/html/nonce-injection.js +249 -0
  49. package/esm/src/internal-agents/ag-ui-sse.d.ts +1 -0
  50. package/esm/src/internal-agents/ag-ui-sse.d.ts.map +1 -1
  51. package/esm/src/internal-agents/ag-ui-sse.js +18 -0
  52. package/esm/src/internal-agents/run-stream.d.ts.map +1 -1
  53. package/esm/src/internal-agents/run-stream.js +26 -4
  54. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.d.ts +1 -0
  55. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.d.ts.map +1 -1
  56. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.js +15 -1
  57. package/esm/src/platform/adapters/fs/veryfront/types.d.ts +2 -0
  58. package/esm/src/platform/adapters/fs/veryfront/types.d.ts.map +1 -1
  59. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
  60. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +2 -0
  61. package/esm/src/proxy/handler.d.ts.map +1 -1
  62. package/esm/src/proxy/handler.js +25 -5
  63. package/esm/src/react/components/Head.d.ts +9 -0
  64. package/esm/src/react/components/Head.d.ts.map +1 -1
  65. package/esm/src/react/components/Head.js +9 -0
  66. package/esm/src/react/context/index.d.ts +9 -0
  67. package/esm/src/react/context/index.d.ts.map +1 -1
  68. package/esm/src/react/context/index.js +9 -0
  69. package/esm/src/react/router/index.d.ts +9 -0
  70. package/esm/src/react/router/index.d.ts.map +1 -1
  71. package/esm/src/react/router/index.js +9 -0
  72. package/esm/src/rendering/orchestrator/html.d.ts +1 -0
  73. package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
  74. package/esm/src/rendering/orchestrator/html.js +81 -89
  75. package/esm/src/rendering/script-page-handling.js +1 -0
  76. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  77. package/esm/src/server/handlers/dev/framework-candidates.generated.js +9 -0
  78. package/esm/src/server/handlers/request/ssr/ssr-response-builder.d.ts.map +1 -1
  79. package/esm/src/server/handlers/request/ssr/ssr-response-builder.js +3 -7
  80. package/esm/src/server/handlers/request/static.handler.d.ts.map +1 -1
  81. package/esm/src/server/handlers/request/static.handler.js +18 -10
  82. package/esm/src/tool/factory.d.ts.map +1 -1
  83. package/esm/src/tool/factory.js +14 -4
  84. package/esm/src/tool/types.d.ts +2 -0
  85. package/esm/src/tool/types.d.ts.map +1 -1
  86. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/constants.d.ts +1 -0
  87. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/constants.d.ts.map +1 -1
  88. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/constants.js +3 -0
  89. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.d.ts.map +1 -1
  90. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.js +4 -2
  91. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.d.ts +1 -1
  92. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.d.ts.map +1 -1
  93. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.js +10 -9
  94. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
  95. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +3 -1
  96. package/esm/src/utils/version-constant.d.ts +1 -1
  97. package/esm/src/utils/version-constant.js +1 -1
  98. package/package.json +1 -1
  99. package/src/_dnt.polyfills.ts +27 -0
  100. package/src/cli/commands/build/handler.ts +3 -0
  101. package/src/cli/commands/deploy/command.ts +3 -0
  102. package/src/cli/commands/files/command.ts +1 -0
  103. package/src/cli/commands/uploads/command.ts +3 -0
  104. package/src/cli/help/tips.ts +18 -1
  105. package/src/cli/router.ts +5 -0
  106. package/src/cli/shared/animation.ts +25 -0
  107. package/src/cli/templates/manifest.js +12 -6
  108. package/src/cli/ui/progress.ts +13 -1
  109. package/src/deno.js +1 -1
  110. package/src/src/agent/index.ts +2 -0
  111. package/src/src/agent/runtime/ai-stream-handler.ts +64 -6
  112. package/src/src/agent/runtime/index.ts +26 -1
  113. package/src/src/agent/runtime/model-tool-converter.ts +47 -3
  114. package/src/src/agent/runtime/tool-helpers.ts +15 -3
  115. package/src/src/agent/types.ts +23 -0
  116. package/src/src/channels/control-plane.ts +31 -0
  117. package/src/src/discovery/handlers/tool-handler.ts +13 -2
  118. package/src/src/html/html-injection.ts +16 -5
  119. package/src/src/html/nonce-injection.ts +300 -0
  120. package/src/src/internal-agents/ag-ui-sse.ts +20 -0
  121. package/src/src/internal-agents/run-stream.ts +35 -4
  122. package/src/src/platform/adapters/fs/veryfront/proxy-manager.ts +29 -3
  123. package/src/src/platform/adapters/fs/veryfront/types.ts +2 -0
  124. package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +2 -0
  125. package/src/src/proxy/handler.ts +43 -5
  126. package/src/src/react/components/Head.tsx +10 -0
  127. package/src/src/react/context/index.tsx +10 -0
  128. package/src/src/react/router/index.tsx +10 -0
  129. package/src/src/rendering/orchestrator/html.ts +125 -100
  130. package/src/src/rendering/script-page-handling.ts +1 -0
  131. package/src/src/server/handlers/dev/framework-candidates.generated.ts +9 -0
  132. package/src/src/server/handlers/request/ssr/ssr-response-builder.ts +7 -11
  133. package/src/src/server/handlers/request/static.handler.ts +22 -10
  134. package/src/src/tool/factory.ts +17 -4
  135. package/src/src/tool/types.ts +2 -0
  136. package/src/src/transforms/pipeline/stages/ssr-vf-modules/constants.ts +4 -0
  137. package/src/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.ts +4 -2
  138. package/src/src/transforms/pipeline/stages/ssr-vf-modules/index.ts +18 -15
  139. package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +4 -1
  140. package/src/src/utils/version-constant.ts +1 -1
@@ -77,6 +77,10 @@ import { resolveConfiguredAgentModel, resolveRuntimeModel } from "./model-resolu
77
77
  const logger = serverLogger.component("agent");
78
78
  const LOAD_SKILL_TOOL_ID = "load-skill";
79
79
 
80
+ type RuntimeToolFilterConfig = AgentConfig & {
81
+ __vfAllowedRemoteTools?: string[];
82
+ };
83
+
80
84
  function createAbortError(reason?: unknown): Error {
81
85
  if (reason instanceof Error) {
82
86
  return reason;
@@ -187,6 +191,18 @@ function isLocalModel(model: LanguageModel): boolean {
187
191
  (typeof m.modelId === "string" && m.modelId.startsWith("local/"));
188
192
  }
189
193
 
194
+ function getRuntimeAllowedRemoteTools(config: AgentConfig): string[] | undefined {
195
+ const configWithRuntimeFilters = config as RuntimeToolFilterConfig;
196
+ if (!Object.hasOwn(configWithRuntimeFilters, "__vfAllowedRemoteTools")) {
197
+ return undefined;
198
+ }
199
+ const raw = configWithRuntimeFilters.__vfAllowedRemoteTools;
200
+ if (!Array.isArray(raw)) {
201
+ return [];
202
+ }
203
+ return raw.every((toolName) => typeof toolName === "string") ? raw : [];
204
+ }
205
+
190
206
  export class AgentRuntime {
191
207
  private id: string;
192
208
  private config: AgentConfig;
@@ -418,6 +434,7 @@ export class AgentRuntime {
418
434
 
419
435
  // Request-scoped skill policy (not class-level mutable state)
420
436
  let activeSkillPolicy: string[] | undefined;
437
+ const allowedRemoteToolNames = getRuntimeAllowedRemoteTools(this.config);
421
438
 
422
439
  for (let step = 0; step < maxSteps; step++) {
423
440
  this.status = "thinking";
@@ -425,6 +442,7 @@ export class AgentRuntime {
425
442
 
426
443
  let tools = isLocal ? [] : await getAvailableTools(this.config.tools, {
427
444
  includeSkillTools: Boolean(this.config.skills),
445
+ allowedRemoteToolNames,
428
446
  });
429
447
 
430
448
  // Layer 1: Filter tools based on active skill policy (planning-time)
@@ -548,6 +566,7 @@ export class AgentRuntime {
548
566
  toolCallId: tc.toolCallId,
549
567
  projectId: cacheCtx?.projectId,
550
568
  },
569
+ allowedRemoteToolNames,
551
570
  );
552
571
 
553
572
  toolCall.status = "completed";
@@ -662,6 +681,7 @@ export class AgentRuntime {
662
681
  // Request-scoped skill policy (not class-level mutable state)
663
682
  let activeSkillPolicy: string[] | undefined;
664
683
  let finalFinishReason: string | undefined;
684
+ const allowedRemoteToolNames = getRuntimeAllowedRemoteTools(this.config);
665
685
 
666
686
  for (let step = 0; step < maxSteps; step++) {
667
687
  throwIfAborted(abortSignal);
@@ -669,6 +689,7 @@ export class AgentRuntime {
669
689
 
670
690
  let tools = isLocalStreaming ? [] : await getAvailableTools(this.config.tools, {
671
691
  includeSkillTools: Boolean(this.config.skills),
692
+ allowedRemoteToolNames,
672
693
  });
673
694
 
674
695
  // Layer 1: Filter tools based on active skill policy (planning-time)
@@ -680,7 +701,10 @@ export class AgentRuntime {
680
701
  model: languageModel,
681
702
  system: systemPrompt,
682
703
  messages: convertToModelMessages(currentMessages),
683
- tools: convertToolsToAISDK(tools),
704
+ tools: convertToolsToAISDK(tools, {
705
+ model: effectiveModel,
706
+ allowedToolNames: allowedRemoteToolNames,
707
+ }),
684
708
  maxOutputTokens: this.resolveMaxOutputTokens(maxOutputTokensOverride),
685
709
  temperature: DEFAULT_TEMPERATURE,
686
710
  abortSignal,
@@ -791,6 +815,7 @@ export class AgentRuntime {
791
815
  toolCallId: tc.id,
792
816
  ...toolContext,
793
817
  },
818
+ allowedRemoteToolNames,
794
819
  );
795
820
  throwIfAborted(abortSignal);
796
821
 
@@ -10,6 +10,42 @@
10
10
  import { jsonSchema, tool } from "ai";
11
11
  import type { ToolSet } from "ai";
12
12
  import type { ToolDefinition } from "../../tool/index.js";
13
+ import { anthropic } from "@ai-sdk/anthropic";
14
+
15
+ export interface ConvertToolsToAISDKOptions {
16
+ model?: string;
17
+ allowedToolNames?: string[];
18
+ }
19
+
20
+ function resolveHostedProvider(model?: string): string | undefined {
21
+ if (!model) return undefined;
22
+
23
+ const [provider, second] = model.split("/", 3);
24
+ if (!provider) return undefined;
25
+ if (provider === "veryfront-cloud") {
26
+ return second || undefined;
27
+ }
28
+
29
+ return provider;
30
+ }
31
+
32
+ function resolveProviderNativeTools(
33
+ options?: ConvertToolsToAISDKOptions,
34
+ ): ToolSet | undefined {
35
+ if (!options?.allowedToolNames?.includes("web_search")) {
36
+ return undefined;
37
+ }
38
+
39
+ if (resolveHostedProvider(options.model) !== "anthropic") {
40
+ return undefined;
41
+ }
42
+
43
+ return {
44
+ web_search: anthropic.tools.webSearch_20250305({
45
+ maxUses: 5,
46
+ }),
47
+ };
48
+ }
13
49
 
14
50
  /**
15
51
  * Convert veryfront tool definitions to AI SDK ToolSet.
@@ -19,9 +55,8 @@ import type { ToolDefinition } from "../../tool/index.js";
19
55
  */
20
56
  export function convertToolsToAISDK(
21
57
  tools: ToolDefinition[],
58
+ options?: ConvertToolsToAISDKOptions,
22
59
  ): ToolSet | undefined {
23
- if (!tools.length) return undefined;
24
-
25
60
  const toolSet: ToolSet = {};
26
61
 
27
62
  for (const def of tools) {
@@ -31,5 +66,14 @@ export function convertToolsToAISDK(
31
66
  });
32
67
  }
33
68
 
34
- return toolSet;
69
+ const providerNativeTools = resolveProviderNativeTools(options);
70
+ if (providerNativeTools) {
71
+ for (const [name, providerTool] of Object.entries(providerNativeTools)) {
72
+ if (!Object.hasOwn(toolSet, name)) {
73
+ toolSet[name] = providerTool;
74
+ }
75
+ }
76
+ }
77
+
78
+ return Object.keys(toolSet).length > 0 ? toolSet : undefined;
35
79
  }
@@ -100,6 +100,7 @@ export async function executeConfiguredTool(
100
100
  input: Record<string, unknown>,
101
101
  toolsConfig: true | Record<string, ToolConfigEntry> | undefined,
102
102
  context?: ToolExecutionContext,
103
+ allowedRemoteToolNames?: string[],
103
104
  ): Promise<unknown> {
104
105
  const configuredTool = resolveConfiguredTool(toolsConfig, toolName);
105
106
  if (configuredTool) {
@@ -114,6 +115,9 @@ export async function executeConfiguredTool(
114
115
 
115
116
  // Fall back to remote execution for integration tools (e.g., github:list-repos)
116
117
  if (isRemoteIntegrationTool(toolName)) {
118
+ if (allowedRemoteToolNames && !allowedRemoteToolNames.includes(toolName)) {
119
+ throw new Error(`Tool "${toolName}" is not allowed for this run`);
120
+ }
117
121
  return await executeRemoteIntegrationTool(
118
122
  toolName,
119
123
  input,
@@ -152,7 +156,11 @@ function addToolDefinition(
152
156
  */
153
157
  export async function getAvailableTools(
154
158
  toolsConfig: true | Record<string, ToolConfigEntry> | undefined,
155
- options?: { includeSkillTools?: boolean; includeIntegrationTools?: boolean },
159
+ options?: {
160
+ includeSkillTools?: boolean;
161
+ includeIntegrationTools?: boolean;
162
+ allowedRemoteToolNames?: string[];
163
+ },
156
164
  ): Promise<ToolDefinition[]> {
157
165
  if (!toolsConfig) return [];
158
166
 
@@ -176,7 +184,9 @@ export async function getAvailableTools(
176
184
  const { getRemoteIntegrationToolDefinitions } = await import(
177
185
  "../../integrations/remote-tools.js"
178
186
  );
179
- const remoteDefs = await getRemoteIntegrationToolDefinitions();
187
+ const remoteDefs = (await getRemoteIntegrationToolDefinitions()).filter((def) =>
188
+ !options?.allowedRemoteToolNames || options.allowedRemoteToolNames.includes(def.name)
189
+ );
180
190
  for (const def of remoteDefs) {
181
191
  logToolDefinition(def.name, def);
182
192
  }
@@ -211,7 +221,9 @@ export async function getAvailableTools(
211
221
  const { getRemoteIntegrationToolDefinitions } = await import(
212
222
  "../../integrations/remote-tools.js"
213
223
  );
214
- const remoteDefs = await getRemoteIntegrationToolDefinitions();
224
+ const remoteDefs = (await getRemoteIntegrationToolDefinitions()).filter((def) =>
225
+ !options?.allowedRemoteToolNames || options.allowedRemoteToolNames.includes(def.name)
226
+ );
215
227
  for (const def of remoteDefs) {
216
228
  // Skip if already present (e.g., explicitly configured by name)
217
229
  if (!tools.some((t) => t.name === def.name)) {
@@ -45,6 +45,28 @@ export type ModelString = string;
45
45
  // Import for use in AgentConfig
46
46
  import type { EdgeConfig, MemoryConfig } from "./schemas/index.js";
47
47
 
48
+ export type AgentSuggestion =
49
+ | {
50
+ id: string;
51
+ type: "prompt";
52
+ title: string;
53
+ description?: string;
54
+ prompt: string;
55
+ }
56
+ | {
57
+ id: string;
58
+ type: "task";
59
+ title: string;
60
+ description?: string;
61
+ task: string;
62
+ prompt?: string;
63
+ };
64
+
65
+ export interface AgentSuggestions {
66
+ welcomeMessage?: string;
67
+ suggestions: AgentSuggestion[];
68
+ }
69
+
48
70
  export interface AgentConfig {
49
71
  id?: string;
50
72
  /**
@@ -78,6 +100,7 @@ export interface AgentConfig {
78
100
  * and registers the skill tools.
79
101
  */
80
102
  skills?: true | string[];
103
+ suggestions?: AgentSuggestions;
81
104
  /** Set to false to disable the default security middleware */
82
105
  security?: false;
83
106
  }
@@ -29,6 +29,29 @@ export const RuntimeAgentSkillSchema = z.object({
29
29
  examples: z.array(z.string()).optional(),
30
30
  });
31
31
 
32
+ export const RuntimeAgentSuggestionSchema = z.discriminatedUnion("type", [
33
+ z.object({
34
+ id: z.string().min(1),
35
+ type: z.literal("prompt"),
36
+ title: z.string().min(1),
37
+ description: z.string().optional(),
38
+ prompt: z.string().min(1),
39
+ }),
40
+ z.object({
41
+ id: z.string().min(1),
42
+ type: z.literal("task"),
43
+ title: z.string().min(1),
44
+ description: z.string().optional(),
45
+ task: z.string().min(1),
46
+ prompt: z.string().min(1).optional(),
47
+ }),
48
+ ]);
49
+
50
+ export const RuntimeAgentSuggestionsSchema = z.object({
51
+ welcomeMessage: z.string().min(1).optional(),
52
+ suggestions: z.array(RuntimeAgentSuggestionSchema),
53
+ });
54
+
32
55
  export const RuntimeAgentSchema = z.object({
33
56
  id: z.string().min(1),
34
57
  name: z.string().min(1),
@@ -36,6 +59,7 @@ export const RuntimeAgentSchema = z.object({
36
59
  model: z.string().nullable().optional(),
37
60
  version: z.string().nullable().optional(),
38
61
  skills: z.array(RuntimeAgentSkillSchema).optional(),
62
+ suggestions: RuntimeAgentSuggestionsSchema.optional(),
39
63
  });
40
64
 
41
65
  export const RuntimeAgentListResponseSchema = z.object({
@@ -67,6 +91,8 @@ const controlPlaneClaimsSchema = z.object({
67
91
  export type ControlPlaneSurface = z.infer<typeof ControlPlaneSurfaceSchema>;
68
92
  export type ControlPlaneAgentsListRequest = z.infer<typeof ControlPlaneAgentsListRequestSchema>;
69
93
  export type RuntimeAgentSkill = z.infer<typeof RuntimeAgentSkillSchema>;
94
+ export type RuntimeAgentSuggestion = z.infer<typeof RuntimeAgentSuggestionSchema>;
95
+ export type RuntimeAgentSuggestions = z.infer<typeof RuntimeAgentSuggestionsSchema>;
70
96
  export type RuntimeAgent = z.infer<typeof RuntimeAgentSchema>;
71
97
  export type RuntimeAgentListResponse = z.infer<typeof RuntimeAgentListResponseSchema>;
72
98
  export type DispatchClaims = z.infer<typeof dispatchClaimsSchema>;
@@ -235,6 +261,10 @@ function resolveAgentSkills(agent: Agent): RuntimeAgentSkill[] {
235
261
 
236
262
  function getRuntimeAgentMetadata(id: string, agent: Agent): RuntimeAgent {
237
263
  const rawConfig = agent.config as unknown as Record<string, unknown>;
264
+ const suggestionsParseResult = rawConfig.suggestions === undefined
265
+ ? null
266
+ : RuntimeAgentSuggestionsSchema.safeParse(rawConfig.suggestions);
267
+ const suggestions = suggestionsParseResult?.success ? suggestionsParseResult.data : undefined;
238
268
 
239
269
  return RuntimeAgentSchema.parse({
240
270
  id,
@@ -245,6 +275,7 @@ function getRuntimeAgentMetadata(id: string, agent: Agent): RuntimeAgent {
245
275
  model: agent.config.model ?? null,
246
276
  version: typeof rawConfig.version === "string" ? rawConfig.version : null,
247
277
  skills: resolveAgentSkills(agent),
278
+ ...(suggestions === undefined ? {} : { suggestions }),
248
279
  });
249
280
  }
250
281
 
@@ -7,13 +7,24 @@ import { registerTool } from "../../mcp/index.js";
7
7
  import type { DiscoveryHandler } from "../types.js";
8
8
  import { filenameToId } from "../discovery-utils.js";
9
9
 
10
+ function hasGeneratedToolId(tool: Tool): boolean {
11
+ return typeof tool.__veryfrontGeneratedId === "string" &&
12
+ tool.id === tool.__veryfrontGeneratedId;
13
+ }
14
+
10
15
  export const toolHandler: DiscoveryHandler<Tool> = {
11
16
  typeName: "tool",
12
17
  validate: (item): item is Tool =>
13
18
  item !== null && typeof item === "object" && typeof (item as Tool).execute === "function",
14
- getId: (_item, file) => filenameToId(file),
19
+ getId: (tool, file) => {
20
+ return typeof tool.id === "string" &&
21
+ tool.id.trim().length > 0 &&
22
+ !hasGeneratedToolId(tool)
23
+ ? tool.id
24
+ : filenameToId(file);
25
+ },
15
26
  register: (id, tool) => {
16
- const toolWithId = { ...tool, id };
27
+ const toolWithId = tool.id === id ? tool : { ...tool, id };
17
28
  registerTool(id, toolWithId);
18
29
  return toolWithId;
19
30
  },
@@ -44,6 +44,8 @@ export interface InjectHTMLContentOptions {
44
44
  yjsGuid?: string;
45
45
  /** Pre-built import map JSON for ESM module resolution (injected into <head>) */
46
46
  importMapJson?: string;
47
+ /** Framework-generated project stylesheet for production shells */
48
+ projectStylesheetHref?: string;
47
49
  }
48
50
 
49
51
  function toProjectRelativePath(absolutePath: string, projectDir?: string): string {
@@ -96,6 +98,11 @@ export function injectHTMLContent(
96
98
  html = html.replace(/<\/head>/i, `${importMapTag}\n</head>`);
97
99
  }
98
100
 
101
+ if (options.projectStylesheetHref && /<\/head>/i.test(html) && !hasProjectStylesheet(html)) {
102
+ const projectStylesheetTag = `<link rel="stylesheet" href="${options.projectStylesheetHref}">`;
103
+ html = html.replace(/<\/head>/i, `${projectStylesheetTag}\n</head>`);
104
+ }
105
+
99
106
  const shouldUsePreviewStylesheet = options.mode === "development" ||
100
107
  options.environment === "preview";
101
108
 
@@ -116,8 +123,9 @@ export function injectHTMLContent(
116
123
  environment: options.environment,
117
124
  }),
118
125
  });
126
+ const nonceAttr = buildNonceAttribute(options.nonce);
119
127
  const hydrationScript =
120
- `<script id="veryfront-hydration-data" type="application/json">${hydrationData}</script>`;
128
+ `<script id="veryfront-hydration-data" type="application/json"${nonceAttr}>${hydrationData}</script>`;
121
129
  html = html.replace(/<\/body>/i, `${hydrationScript}</body>`);
122
130
  }
123
131
 
@@ -125,19 +133,22 @@ export function injectHTMLContent(
125
133
  const hasDevScriptsPlaceholder = /{{\s*devScripts\s*}}/i.test(html);
126
134
 
127
135
  if (hasDevScriptsPlaceholder) {
128
- html = html.replace(/{{\s*devScripts\s*}}/gi, getDevScripts());
136
+ html = html.replace(/{{\s*devScripts\s*}}/gi, getDevScripts(options.devPort, options.nonce));
129
137
  }
130
138
 
131
- html = html.replace(/{{\s*devStyles\s*}}/gi, getDevStyles());
139
+ html = html.replace(/{{\s*devStyles\s*}}/gi, getDevStyles(options.nonce));
132
140
 
133
141
  if (!hasDevScriptsPlaceholder && hasBodyClose) {
134
- html = html.replace(/<\/body>/i, `${getDevStyles()}${getDevScripts()}</body>`);
142
+ html = html.replace(
143
+ /<\/body>/i,
144
+ `${getDevStyles(options.nonce)}${getDevScripts(options.devPort, options.nonce)}</body>`,
145
+ );
135
146
  }
136
147
  } else {
137
148
  html = html.replace(/{{\s*devScripts\s*}}/gi, "");
138
149
  html = html.replace(/{{\s*devStyles\s*}}/gi, "");
139
150
 
140
- const prodScripts = getProdScripts(options.slug);
151
+ const prodScripts = getProdScripts(options.slug, options.nonce);
141
152
  const hasProdScriptsPlaceholder = /{{\s*prodScripts\s*}}/i.test(html);
142
153
 
143
154
  if (hasProdScriptsPlaceholder) {
@@ -0,0 +1,300 @@
1
+ import { escapeHtml } from "../utils/html-escape.js";
2
+
3
+ function findTagEnd(html: string, start: number): number {
4
+ let activeQuote: '"' | "'" | null = null;
5
+
6
+ for (let index = start + 1; index < html.length; index++) {
7
+ const char = html[index];
8
+
9
+ if (activeQuote) {
10
+ if (char === activeQuote) activeQuote = null;
11
+ continue;
12
+ }
13
+
14
+ if (char === '"' || char === "'") {
15
+ activeQuote = char;
16
+ continue;
17
+ }
18
+
19
+ if (char === ">") return index;
20
+ }
21
+
22
+ return -1;
23
+ }
24
+
25
+ function getOpeningTagName(tag: string): "script" | "style" | undefined {
26
+ const match = /^<\s*([a-zA-Z][\w:-]*)/u.exec(tag);
27
+ const tagName = match?.[1]?.toLowerCase();
28
+ if (tagName === "script" || tagName === "style") return tagName;
29
+ return undefined;
30
+ }
31
+
32
+ function isTagBoundary(char: string | undefined): boolean {
33
+ return char === undefined || /\s|\/|>/u.test(char);
34
+ }
35
+
36
+ function findRawTextClosingTagStart(
37
+ lowerHtml: string,
38
+ tagName: "script" | "style",
39
+ fromIndex: number,
40
+ ): number {
41
+ const needle = `</${tagName}`;
42
+ let searchIndex = fromIndex;
43
+
44
+ while (searchIndex < lowerHtml.length) {
45
+ const closingIndex = lowerHtml.indexOf(needle, searchIndex);
46
+ if (closingIndex === -1) return -1;
47
+
48
+ if (isTagBoundary(lowerHtml[closingIndex + needle.length])) {
49
+ return closingIndex;
50
+ }
51
+
52
+ searchIndex = closingIndex + needle.length;
53
+ }
54
+
55
+ return -1;
56
+ }
57
+
58
+ interface ParsedAttribute {
59
+ name: string;
60
+ start: number;
61
+ end: number;
62
+ value: string | null;
63
+ }
64
+
65
+ function findAttribute(tag: string, attributeName: string): ParsedAttribute | undefined {
66
+ const closeIndex = tag.lastIndexOf(">");
67
+ if (closeIndex <= 0) return undefined;
68
+
69
+ let index = 1;
70
+ while (index < closeIndex && !/\s|\/|>/u.test(tag[index] ?? "")) index++;
71
+
72
+ while (index < closeIndex) {
73
+ while (index < closeIndex && /\s/u.test(tag[index] ?? "")) index++;
74
+ if (index >= closeIndex) break;
75
+
76
+ const char = tag[index];
77
+ if (char === "/" || char === ">") break;
78
+
79
+ const start = index;
80
+ while (index < closeIndex && !/[\s=/>]/u.test(tag[index] ?? "")) index++;
81
+ const name = tag.slice(start, index);
82
+
83
+ while (index < closeIndex && /\s/u.test(tag[index] ?? "")) index++;
84
+
85
+ let value: string | null = null;
86
+ if (tag[index] === "=") {
87
+ index++;
88
+ while (index < closeIndex && /\s/u.test(tag[index] ?? "")) index++;
89
+
90
+ const quote = tag[index];
91
+ if (quote === '"' || quote === "'") {
92
+ index++;
93
+ const valueStart = index;
94
+ while (index < closeIndex && tag[index] !== quote) index++;
95
+ value = tag.slice(valueStart, index);
96
+ if (index < closeIndex) index++;
97
+ } else {
98
+ const valueStart = index;
99
+ while (index < closeIndex && !/[\s>]/u.test(tag[index] ?? "")) index++;
100
+ value = tag.slice(valueStart, index);
101
+ }
102
+ }
103
+
104
+ if (name.toLowerCase() === attributeName) {
105
+ return { name, start, end: index, value };
106
+ }
107
+ }
108
+
109
+ return undefined;
110
+ }
111
+
112
+ function injectNonceIntoOpeningTag(tag: string, escapedNonce: string): string {
113
+ const existingNonce = findAttribute(tag, "nonce");
114
+ if (existingNonce) {
115
+ if (existingNonce.value?.trim()) return tag;
116
+
117
+ return `${tag.slice(0, existingNonce.start)}nonce="${escapedNonce}"${
118
+ tag.slice(existingNonce.end)
119
+ }`;
120
+ }
121
+
122
+ const closeIndex = tag.lastIndexOf(">");
123
+ if (closeIndex === -1) return tag;
124
+
125
+ const insertAt = /\/\s*>$/u.test(tag) ? closeIndex - 1 : closeIndex;
126
+ return `${tag.slice(0, insertAt)} nonce="${escapedNonce}"${tag.slice(insertAt)}`;
127
+ }
128
+
129
+ export function addNonceToHtmlTags(html: string, nonce?: string): string {
130
+ if (!nonce) return html;
131
+
132
+ const escapedNonce = escapeHtml(nonce);
133
+ const lowerHtml = html.toLowerCase();
134
+ let result = "";
135
+ let index = 0;
136
+ let rawTextTag: "script" | "style" | null = null;
137
+
138
+ while (index < html.length) {
139
+ if (rawTextTag) {
140
+ const closingIndex = findRawTextClosingTagStart(lowerHtml, rawTextTag, index);
141
+ if (closingIndex === -1) {
142
+ result += html.slice(index);
143
+ break;
144
+ }
145
+
146
+ result += html.slice(index, closingIndex);
147
+ index = closingIndex;
148
+ rawTextTag = null;
149
+ continue;
150
+ }
151
+
152
+ if (html.startsWith("<!--", index)) {
153
+ const commentEnd = html.indexOf("-->", index + 4);
154
+ const endIndex = commentEnd === -1 ? html.length : commentEnd + 3;
155
+ result += html.slice(index, endIndex);
156
+ index = endIndex;
157
+ continue;
158
+ }
159
+
160
+ if (html[index] !== "<") {
161
+ result += html[index];
162
+ index++;
163
+ continue;
164
+ }
165
+
166
+ const tagEnd = findTagEnd(html, index);
167
+ if (tagEnd === -1) {
168
+ result += html.slice(index);
169
+ break;
170
+ }
171
+
172
+ const tag = html.slice(index, tagEnd + 1);
173
+ const tagName = getOpeningTagName(tag);
174
+
175
+ if (!tagName) {
176
+ result += tag;
177
+ index = tagEnd + 1;
178
+ continue;
179
+ }
180
+
181
+ result += injectNonceIntoOpeningTag(tag, escapedNonce);
182
+ index = tagEnd + 1;
183
+
184
+ if (!/\/\s*>$/u.test(tag)) {
185
+ rawTextTag = tagName;
186
+ }
187
+ }
188
+
189
+ return result;
190
+ }
191
+
192
+ export function addNonceToHtmlStream(
193
+ stream: ReadableStream<Uint8Array>,
194
+ nonce?: string,
195
+ ): ReadableStream<Uint8Array> {
196
+ if (!nonce) return stream;
197
+
198
+ const escapedNonce = escapeHtml(nonce);
199
+ const decoder = new TextDecoder();
200
+ const encoder = new TextEncoder();
201
+ let buffer = "";
202
+ let rawTextTag: "script" | "style" | null = null;
203
+
204
+ function transformBuffer(flush: boolean): string {
205
+ let result = "";
206
+ let index = 0;
207
+ const lowerBuffer = buffer.toLowerCase();
208
+
209
+ while (index < buffer.length) {
210
+ if (rawTextTag) {
211
+ const closingIndex = findRawTextClosingTagStart(lowerBuffer, rawTextTag, index);
212
+ if (closingIndex === -1) {
213
+ if (flush) {
214
+ result += buffer.slice(index);
215
+ index = buffer.length;
216
+ break;
217
+ }
218
+
219
+ const retainLength = `</${rawTextTag}`.length;
220
+ const safeEnd = Math.max(index, buffer.length - retainLength);
221
+ result += buffer.slice(index, safeEnd);
222
+ index = safeEnd;
223
+ break;
224
+ }
225
+
226
+ result += buffer.slice(index, closingIndex);
227
+ index = closingIndex;
228
+ rawTextTag = null;
229
+ continue;
230
+ }
231
+
232
+ if (buffer.startsWith("<!--", index)) {
233
+ const commentEnd = buffer.indexOf("-->", index + 4);
234
+ if (commentEnd === -1) {
235
+ if (flush) {
236
+ result += buffer.slice(index);
237
+ index = buffer.length;
238
+ }
239
+ break;
240
+ }
241
+
242
+ const endIndex = commentEnd + 3;
243
+ result += buffer.slice(index, endIndex);
244
+ index = endIndex;
245
+ continue;
246
+ }
247
+
248
+ if (buffer[index] !== "<") {
249
+ const nextTagIndex = buffer.indexOf("<", index);
250
+ const endIndex = nextTagIndex === -1 ? buffer.length : nextTagIndex;
251
+ result += buffer.slice(index, endIndex);
252
+ index = endIndex;
253
+ continue;
254
+ }
255
+
256
+ const tagEnd = findTagEnd(buffer, index);
257
+ if (tagEnd === -1) {
258
+ if (flush) {
259
+ result += buffer.slice(index);
260
+ index = buffer.length;
261
+ }
262
+ break;
263
+ }
264
+
265
+ const tag = buffer.slice(index, tagEnd + 1);
266
+ const tagName = getOpeningTagName(tag);
267
+
268
+ if (!tagName) {
269
+ result += tag;
270
+ index = tagEnd + 1;
271
+ continue;
272
+ }
273
+
274
+ result += injectNonceIntoOpeningTag(tag, escapedNonce);
275
+ index = tagEnd + 1;
276
+
277
+ if (!/\/\s*>$/u.test(tag)) {
278
+ rawTextTag = tagName;
279
+ }
280
+ }
281
+
282
+ buffer = buffer.slice(index);
283
+ return result;
284
+ }
285
+
286
+ return stream.pipeThrough(
287
+ new TransformStream<Uint8Array, Uint8Array>({
288
+ transform(chunk, controller) {
289
+ buffer += decoder.decode(chunk, { stream: true });
290
+ const transformed = transformBuffer(false);
291
+ if (transformed) controller.enqueue(encoder.encode(transformed));
292
+ },
293
+ flush(controller) {
294
+ buffer += decoder.decode();
295
+ const transformed = transformBuffer(true);
296
+ if (transformed) controller.enqueue(encoder.encode(transformed));
297
+ },
298
+ }),
299
+ );
300
+ }