veryfront 0.1.13 → 0.1.15

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 (153) hide show
  1. package/esm/cli/app/data/slug-words.d.ts.map +1 -1
  2. package/esm/cli/app/data/slug-words.js +225 -90
  3. package/esm/cli/app/operations/project-creation.js +4 -3
  4. package/esm/cli/app/shell.js +1 -1
  5. package/esm/cli/app/utils.d.ts +5 -4
  6. package/esm/cli/app/utils.d.ts.map +1 -1
  7. package/esm/cli/app/utils.js +0 -23
  8. package/esm/cli/app/views/dashboard.d.ts +1 -1
  9. package/esm/cli/app/views/dashboard.d.ts.map +1 -1
  10. package/esm/cli/app/views/dashboard.js +22 -4
  11. package/esm/cli/auth/callback-server.d.ts.map +1 -1
  12. package/esm/cli/auth/callback-server.js +3 -2
  13. package/esm/cli/commands/dev/handler.d.ts.map +1 -1
  14. package/esm/cli/commands/dev/handler.js +2 -0
  15. package/esm/cli/commands/init/init-command.d.ts.map +1 -1
  16. package/esm/cli/commands/init/init-command.js +20 -3
  17. package/esm/cli/commands/init/interactive-wizard.d.ts +3 -2
  18. package/esm/cli/commands/init/interactive-wizard.d.ts.map +1 -1
  19. package/esm/cli/commands/init/interactive-wizard.js +55 -27
  20. package/esm/cli/mcp/remote-file-tools.d.ts +0 -6
  21. package/esm/cli/mcp/remote-file-tools.d.ts.map +1 -1
  22. package/esm/cli/mcp/remote-file-tools.js +37 -15
  23. package/esm/cli/shared/reserve-slug.d.ts.map +1 -1
  24. package/esm/cli/shared/reserve-slug.js +8 -3
  25. package/esm/cli/utils/env-prompt.d.ts.map +1 -1
  26. package/esm/cli/utils/env-prompt.js +3 -0
  27. package/esm/deno.d.ts +5 -1
  28. package/esm/deno.js +11 -4
  29. package/esm/src/agent/chat-handler.d.ts +4 -3
  30. package/esm/src/agent/chat-handler.d.ts.map +1 -1
  31. package/esm/src/agent/chat-handler.js +55 -4
  32. package/esm/src/agent/react/index.d.ts +1 -1
  33. package/esm/src/agent/react/index.d.ts.map +1 -1
  34. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.d.ts +18 -0
  35. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.d.ts.map +1 -0
  36. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.js +54 -0
  37. package/esm/src/agent/react/use-chat/browser-inference/types.d.ts +43 -0
  38. package/esm/src/agent/react/use-chat/browser-inference/types.d.ts.map +1 -0
  39. package/esm/src/agent/react/use-chat/browser-inference/types.js +4 -0
  40. package/esm/src/agent/react/use-chat/browser-inference/worker-client.d.ts +23 -0
  41. package/esm/src/agent/react/use-chat/browser-inference/worker-client.d.ts.map +1 -0
  42. package/esm/src/agent/react/use-chat/browser-inference/worker-client.js +67 -0
  43. package/esm/src/agent/react/use-chat/browser-inference/worker-script.d.ts +8 -0
  44. package/esm/src/agent/react/use-chat/browser-inference/worker-script.d.ts.map +1 -0
  45. package/esm/src/agent/react/use-chat/browser-inference/worker-script.js +97 -0
  46. package/esm/src/agent/react/use-chat/index.d.ts +1 -1
  47. package/esm/src/agent/react/use-chat/index.d.ts.map +1 -1
  48. package/esm/src/agent/react/use-chat/types.d.ts +12 -0
  49. package/esm/src/agent/react/use-chat/types.d.ts.map +1 -1
  50. package/esm/src/agent/react/use-chat/use-chat.d.ts.map +1 -1
  51. package/esm/src/agent/react/use-chat/use-chat.js +120 -6
  52. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  53. package/esm/src/agent/runtime/index.js +59 -7
  54. package/esm/src/build/production-build/templates.d.ts +2 -2
  55. package/esm/src/build/production-build/templates.d.ts.map +1 -1
  56. package/esm/src/build/production-build/templates.js +2 -68
  57. package/esm/src/chat/index.d.ts +1 -1
  58. package/esm/src/chat/index.d.ts.map +1 -1
  59. package/esm/src/errors/veryfront-error.d.ts +3 -0
  60. package/esm/src/errors/veryfront-error.d.ts.map +1 -1
  61. package/esm/src/platform/adapters/runtime/deno/adapter.d.ts.map +1 -1
  62. package/esm/src/platform/adapters/runtime/deno/adapter.js +24 -3
  63. package/esm/src/platform/compat/http/deno-server.d.ts.map +1 -1
  64. package/esm/src/platform/compat/http/deno-server.js +23 -2
  65. package/esm/src/provider/index.d.ts +1 -1
  66. package/esm/src/provider/index.d.ts.map +1 -1
  67. package/esm/src/provider/index.js +1 -1
  68. package/esm/src/provider/local/ai-sdk-adapter.d.ts +19 -0
  69. package/esm/src/provider/local/ai-sdk-adapter.d.ts.map +1 -0
  70. package/esm/src/provider/local/ai-sdk-adapter.js +164 -0
  71. package/esm/src/provider/local/env.d.ts +10 -0
  72. package/esm/src/provider/local/env.d.ts.map +1 -0
  73. package/esm/src/provider/local/env.js +23 -0
  74. package/esm/src/provider/local/local-engine.d.ts +61 -0
  75. package/esm/src/provider/local/local-engine.d.ts.map +1 -0
  76. package/esm/src/provider/local/local-engine.js +211 -0
  77. package/esm/src/provider/local/model-catalog.d.ts +30 -0
  78. package/esm/src/provider/local/model-catalog.d.ts.map +1 -0
  79. package/esm/src/provider/local/model-catalog.js +58 -0
  80. package/esm/src/provider/model-registry.d.ts +14 -0
  81. package/esm/src/provider/model-registry.d.ts.map +1 -1
  82. package/esm/src/provider/model-registry.js +58 -2
  83. package/esm/src/proxy/main.js +34 -6
  84. package/esm/src/proxy/server-resolver.d.ts +23 -0
  85. package/esm/src/proxy/server-resolver.d.ts.map +1 -0
  86. package/esm/src/proxy/server-resolver.js +124 -0
  87. package/esm/src/react/components/ai/chat/components/inference-badge.d.ts +8 -0
  88. package/esm/src/react/components/ai/chat/components/inference-badge.d.ts.map +1 -0
  89. package/esm/src/react/components/ai/chat/components/inference-badge.js +36 -0
  90. package/esm/src/react/components/ai/chat/components/upgrade-cta.d.ts +7 -0
  91. package/esm/src/react/components/ai/chat/components/upgrade-cta.d.ts.map +1 -0
  92. package/esm/src/react/components/ai/chat/components/upgrade-cta.js +33 -0
  93. package/esm/src/react/components/ai/chat/index.d.ts +7 -1
  94. package/esm/src/react/components/ai/chat/index.d.ts.map +1 -1
  95. package/esm/src/react/components/ai/chat/index.js +16 -4
  96. package/esm/src/sandbox/index.d.ts +31 -0
  97. package/esm/src/sandbox/index.d.ts.map +1 -0
  98. package/esm/src/sandbox/index.js +30 -0
  99. package/esm/src/sandbox/sandbox.d.ts +48 -0
  100. package/esm/src/sandbox/sandbox.d.ts.map +1 -0
  101. package/esm/src/sandbox/sandbox.js +178 -0
  102. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.d.ts.map +1 -1
  103. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.js +8 -2
  104. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.d.ts +1 -0
  105. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.d.ts.map +1 -1
  106. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.js +1 -0
  107. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/transform.d.ts.map +1 -1
  108. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/transform.js +15 -1
  109. package/package.json +8 -1
  110. package/src/cli/app/data/slug-words.ts +225 -90
  111. package/src/cli/app/operations/project-creation.ts +3 -3
  112. package/src/cli/app/shell.ts +1 -1
  113. package/src/cli/app/utils.ts +0 -30
  114. package/src/cli/app/views/dashboard.ts +27 -4
  115. package/src/cli/auth/callback-server.ts +3 -2
  116. package/src/cli/commands/dev/handler.ts +2 -0
  117. package/src/cli/commands/init/init-command.ts +30 -3
  118. package/src/cli/commands/init/interactive-wizard.ts +62 -34
  119. package/src/cli/mcp/remote-file-tools.ts +50 -15
  120. package/src/cli/shared/reserve-slug.ts +9 -2
  121. package/src/cli/utils/env-prompt.ts +3 -0
  122. package/src/deno.js +11 -4
  123. package/src/src/agent/chat-handler.ts +57 -4
  124. package/src/src/agent/react/index.ts +2 -0
  125. package/src/src/agent/react/use-chat/browser-inference/browser-engine.ts +81 -0
  126. package/src/src/agent/react/use-chat/browser-inference/types.ts +52 -0
  127. package/src/src/agent/react/use-chat/browser-inference/worker-client.ts +89 -0
  128. package/src/src/agent/react/use-chat/browser-inference/worker-script.ts +98 -0
  129. package/src/src/agent/react/use-chat/index.ts +2 -0
  130. package/src/src/agent/react/use-chat/types.ts +20 -0
  131. package/src/src/agent/react/use-chat/use-chat.ts +148 -8
  132. package/src/src/agent/runtime/index.ts +72 -6
  133. package/src/src/build/production-build/templates.ts +2 -68
  134. package/src/src/chat/index.ts +2 -0
  135. package/src/src/errors/veryfront-error.ts +2 -1
  136. package/src/src/platform/adapters/runtime/deno/adapter.ts +25 -3
  137. package/src/src/platform/compat/http/deno-server.ts +28 -1
  138. package/src/src/provider/index.ts +1 -0
  139. package/src/src/provider/local/ai-sdk-adapter.ts +207 -0
  140. package/src/src/provider/local/env.ts +26 -0
  141. package/src/src/provider/local/local-engine.ts +288 -0
  142. package/src/src/provider/local/model-catalog.ts +73 -0
  143. package/src/src/provider/model-registry.ts +66 -2
  144. package/src/src/proxy/main.ts +41 -6
  145. package/src/src/proxy/server-resolver.ts +151 -0
  146. package/src/src/react/components/ai/chat/components/inference-badge.tsx +48 -0
  147. package/src/src/react/components/ai/chat/components/upgrade-cta.tsx +56 -0
  148. package/src/src/react/components/ai/chat/index.tsx +43 -6
  149. package/src/src/sandbox/index.ts +32 -0
  150. package/src/src/sandbox/sandbox.ts +236 -0
  151. package/src/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.ts +9 -2
  152. package/src/src/transforms/pipeline/stages/ssr-vf-modules/index.ts +1 -0
  153. package/src/src/transforms/pipeline/stages/ssr-vf-modules/transform.ts +17 -0
@@ -4,7 +4,7 @@
4
4
  *******************************/
5
5
 
6
6
  import { cliLogger as logger } from "../../utils/index.js";
7
- import { brand, dim, green } from "../../ui/index.js";
7
+ import { brand, dim, green, red } from "../../ui/index.js";
8
8
  import { createSpinner } from "../../ui/progress.js";
9
9
  import { box } from "../../ui/box.js";
10
10
  import { ensureDir } from "../../../deps/jsr.io/@std/fs/1.0.22/mod.js";
@@ -35,7 +35,11 @@ import {
35
35
  loadIntegrations,
36
36
  validateIntegrations,
37
37
  } from "../../templates/integration-loader.js";
38
- import { runInteractiveWizard, shouldRunWizard } from "./interactive-wizard.js";
38
+ import {
39
+ runInteractiveWizard,
40
+ shouldRunWizard,
41
+ validateProjectName,
42
+ } from "./interactive-wizard.js";
39
43
 
40
44
  /**
41
45
  * Icon mapping for integrations based on category/name
@@ -185,8 +189,31 @@ export async function initCommand(options: InitOptions): Promise<void> {
185
189
  let projectName = name;
186
190
  let initGit = false;
187
191
 
192
+ // Validate project name before doing anything else
193
+ if (name) {
194
+ const nameError = validateProjectName(name);
195
+ if (nameError) {
196
+ console.error(red(nameError));
197
+ return;
198
+ }
199
+ }
200
+
201
+ // Check if directory already exists before entering the wizard
202
+ if (name && !options.force) {
203
+ const fs = createFileSystem();
204
+ const targetDir = join(cwd(), name);
205
+ if (await fs.exists(targetDir)) {
206
+ console.error(
207
+ red(
208
+ `Directory "${name}" already exists. Choose a different name or use --force to overwrite.`,
209
+ ),
210
+ );
211
+ return;
212
+ }
213
+ }
214
+
188
215
  if (shouldRunWizard(options)) {
189
- const wizardResult = await runInteractiveWizard();
216
+ const wizardResult = await runInteractiveWizard(name);
190
217
  if (wizardResult.cancelled) {
191
218
  return;
192
219
  }
@@ -6,6 +6,13 @@ import { select, textInput } from "../../utils/terminal-select.js";
6
6
  import { getTemplateSelectOptions, TEMPLATES } from "./catalog.js";
7
7
  import type { InitTemplate } from "./types.js";
8
8
 
9
+ /** Reject path separators and traversal so the name stays a single directory. */
10
+ export function validateProjectName(name: string): string | null {
11
+ if (/[/\\]/.test(name)) return 'Project name cannot contain "/" or "\\"';
12
+ if (name === "." || name === "..") return 'Project name cannot be "." or ".."';
13
+ return null;
14
+ }
15
+
9
16
  export interface WizardResult {
10
17
  projectName: string | null; // null = use current directory
11
18
  template: InitTemplate;
@@ -18,10 +25,10 @@ function canRunWizard(): boolean {
18
25
  return !(isCiEnv() || isDenoTestingEnv()) && checkIsInteractive();
19
26
  }
20
27
 
21
- export async function runInteractiveWizard(): Promise<WizardResult> {
28
+ export async function runInteractiveWizard(existingName?: string): Promise<WizardResult> {
22
29
  if (!canRunWizard()) {
23
30
  return {
24
- projectName: null,
31
+ projectName: existingName ?? null,
25
32
  template: "minimal",
26
33
  initGit: false,
27
34
  skipped: true,
@@ -37,31 +44,28 @@ export async function runInteractiveWizard(): Promise<WizardResult> {
37
44
  console.log(`│ Let's set up your project.`);
38
45
  console.log("│");
39
46
 
40
- // Step 1: Location prompt
41
- const locationChoice = await select(
42
- "Where should we create your project?",
43
- [
44
- { value: "current", label: "Current folder", description: "Use this directory" },
45
- { value: "new", label: "New folder", description: "Create a new directory" },
46
- ],
47
- 0,
48
- );
49
-
50
- if (locationChoice === null) {
51
- console.log(muted("\n Cancelled.\n"));
52
- return {
53
- projectName: null,
54
- template: "minimal",
55
- initGit: false,
56
- skipped: false,
57
- cancelled: true,
58
- };
59
- }
60
-
61
- let projectName: string | null = null;
62
- if (locationChoice === "new") {
63
- const name = await textInput("Project name", "my-app");
64
- if (name === null) {
47
+ let projectName: string | null = existingName ?? null;
48
+
49
+ // Location prompt (skip when name was provided via CLI)
50
+ if (!existingName) {
51
+ const locationChoice = await select(
52
+ "Where should we create your project?",
53
+ [
54
+ {
55
+ value: "current",
56
+ label: "Current folder",
57
+ description: "Use this directory",
58
+ },
59
+ {
60
+ value: "new",
61
+ label: "New folder",
62
+ description: "Create a new directory",
63
+ },
64
+ ],
65
+ 0,
66
+ );
67
+
68
+ if (locationChoice === null) {
65
69
  console.log(muted("\n Cancelled.\n"));
66
70
  return {
67
71
  projectName: null,
@@ -71,12 +75,36 @@ export async function runInteractiveWizard(): Promise<WizardResult> {
71
75
  cancelled: true,
72
76
  };
73
77
  }
74
- if (name) {
75
- projectName = name;
78
+
79
+ if (locationChoice === "new") {
80
+ const name = await textInput("Project name", "my-app");
81
+ if (name === null) {
82
+ console.log(muted("\n Cancelled.\n"));
83
+ return {
84
+ projectName: null,
85
+ template: "minimal",
86
+ initGit: false,
87
+ skipped: false,
88
+ cancelled: true,
89
+ };
90
+ }
91
+ const validName = name || "my-app";
92
+ const nameError = validateProjectName(validName);
93
+ if (nameError) {
94
+ console.log(muted(`\n ${nameError}\n`));
95
+ return {
96
+ projectName: null,
97
+ template: "minimal",
98
+ initGit: false,
99
+ skipped: false,
100
+ cancelled: true,
101
+ };
102
+ }
103
+ projectName = validName;
76
104
  }
77
105
  }
78
106
 
79
- // Step 2: Template selection
107
+ // Template selection
80
108
  const templateChoice = await select(
81
109
  "What would you like to build?",
82
110
  getTemplateSelectOptions(),
@@ -96,7 +124,7 @@ export async function runInteractiveWizard(): Promise<WizardResult> {
96
124
 
97
125
  const template = templateChoice as InitTemplate;
98
126
 
99
- // Step 3: Git init prompt
127
+ // Git init prompt
100
128
  const gitChoice = await select(
101
129
  "Initialize a git repository?",
102
130
  [
@@ -136,7 +164,7 @@ export async function runInteractiveWizard(): Promise<WizardResult> {
136
164
  return { projectName, template, initGit, skipped: false, cancelled: false };
137
165
  }
138
166
 
139
- export function shouldRunWizard(options: { template?: string; name?: string }): boolean {
140
- // Run wizard if no template and no name specified via CLI
141
- return !options.template && !options.name;
167
+ export function shouldRunWizard(options: { template?: string }): boolean {
168
+ // Always run wizard unless template is explicitly specified via --template
169
+ return !options.template;
142
170
  }
@@ -15,6 +15,7 @@ import { z } from "zod";
15
15
  import type { MCPTool } from "./tools.js";
16
16
  import { getEnvironmentConfig } from "../../src/config/index.js";
17
17
  import { withSpan } from "../../src/observability/tracing/otlp-setup.js";
18
+ import { randomSuffix } from "../shared/slug.js";
18
19
 
19
20
  import { DEFAULT_LOCAL_API_URL } from "../shared/constants.js";
20
21
 
@@ -135,6 +136,46 @@ interface Project {
135
136
  created_at?: string;
136
137
  }
137
138
 
139
+ const MAX_SLUG_ATTEMPTS = 10;
140
+
141
+ function slugToName(slug: string): string {
142
+ return slug
143
+ .split("-")
144
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
145
+ .join(" ");
146
+ }
147
+
148
+ /**
149
+ * Create a project with slug conflict retry.
150
+ * On 409, appends a random suffix and retries (matches reserveProjectSlug behavior).
151
+ * Name is derived from the base slug (without random suffix) for readability.
152
+ */
153
+ async function createProjectWithRetry(
154
+ slug: string,
155
+ body: Record<string, unknown>,
156
+ ): Promise<{ ok: true; data: Project; slug: string } | { ok: false; error: string }> {
157
+ const name = slugToName(slug);
158
+ let currentSlug = slug;
159
+
160
+ for (let attempt = 1; attempt <= MAX_SLUG_ATTEMPTS; attempt++) {
161
+ const result = await apiRequest<Project>("POST", "/projects", {
162
+ body: { ...body, slug: currentSlug, name },
163
+ });
164
+
165
+ if (result.ok && result.data) {
166
+ return { ok: true, data: result.data, slug: currentSlug };
167
+ }
168
+
169
+ if (result.status !== 409) {
170
+ return { ok: false, error: result.error ?? "Failed to create project" };
171
+ }
172
+
173
+ currentSlug = `${slug}-${randomSuffix()}`;
174
+ }
175
+
176
+ return { ok: false, error: `Could not find available slug after ${MAX_SLUG_ATTEMPTS} attempts` };
177
+ }
178
+
138
179
  // ============================================================================
139
180
  // Tool: vf_remote_list_files
140
181
  // ============================================================================
@@ -573,8 +614,9 @@ export const vfRemoteDeleteBranch: MCPTool<RemoteDeleteBranchInput, RemoteDelete
573
614
  // ============================================================================
574
615
 
575
616
  const remoteCreateProjectInput = z.object({
576
- name: z.string().describe("Project name"),
577
- slug: z.string().describe("Project slug (lowercase letters, numbers, hyphens only)"),
617
+ slug: z.string().describe(
618
+ "Project slug (lowercase letters, numbers, hyphens only). A random suffix is appended if the slug is already taken.",
619
+ ),
578
620
  template: z.string().optional().describe("Template to use (e.g., 'chat', 'rag', 'minimal')"),
579
621
  is_public: z.boolean().optional().describe("Whether the project is public (default: false)"),
580
622
  });
@@ -592,13 +634,9 @@ export const vfRemoteCreateProject: MCPTool<RemoteCreateProjectInput, RemoteCrea
592
634
  description: "Create a new Veryfront project. Returns the project details including ID and slug.",
593
635
  inputSchema: remoteCreateProjectInput,
594
636
  execute: async (input) => {
595
- const result = await apiRequest<Project>("POST", "/projects", {
596
- body: {
597
- name: input.name,
598
- slug: input.slug,
599
- template: input.template,
600
- isPublic: input.is_public,
601
- },
637
+ const result = await createProjectWithRetry(input.slug, {
638
+ template: input.template,
639
+ isPublic: input.is_public,
602
640
  });
603
641
 
604
642
  if (!result.ok) return { success: false, error: result.error };
@@ -612,9 +650,8 @@ export const vfRemoteCreateProject: MCPTool<RemoteCreateProjectInput, RemoteCrea
612
650
 
613
651
  const remoteCloneProjectInput = z.object({
614
652
  source_project: z.string().describe("Source project slug or ID to clone from"),
615
- target_name: z.string().describe("Name for the new project"),
616
653
  target_slug: z.string().describe(
617
- "Slug for the new project (lowercase letters, numbers, hyphens only)",
654
+ "Slug for the new project (lowercase letters, numbers, hyphens only). A random suffix is appended if the slug is already taken.",
618
655
  ),
619
656
  file_pattern: z.string().optional().describe(
620
657
  "Optional file pattern to filter which files to clone (e.g., '*.tsx')",
@@ -639,9 +676,7 @@ export const vfRemoteCloneProject: MCPTool<RemoteCloneProjectInput, RemoteCloneP
639
676
  withSpan(
640
677
  "cli.mcp.tool.vf_remote_clone_project",
641
678
  async () => {
642
- const createResult = await apiRequest<Project>("POST", "/projects", {
643
- body: { name: input.target_name, slug: input.target_slug },
644
- });
679
+ const createResult = await createProjectWithRetry(input.target_slug, {});
645
680
 
646
681
  if (!createResult.ok) {
647
682
  return { success: false, error: `Failed to create project: ${createResult.error}` };
@@ -684,7 +719,7 @@ export const vfRemoteCloneProject: MCPTool<RemoteCloneProjectInput, RemoteCloneP
684
719
 
685
720
  const createFileResult = await apiRequest<{ id: string; path: string }>(
686
721
  "PUT",
687
- `/${input.target_slug}/files/${encodeFilePath(file.path)}`,
722
+ `/${createResult.slug}/files/${encodeFilePath(file.path)}`,
688
723
  { body: { content: getResult.data.content } },
689
724
  );
690
725
 
@@ -9,8 +9,13 @@ import * as dntShim from "../../_dnt.shims.js";
9
9
 
10
10
 
11
11
  import { type EnvironmentConfig, getEnvironmentConfig } from "../../src/config/index.js";
12
+ import { capitalizeSeparatedWords } from "../../src/utils/case-utils.js";
12
13
  import { randomSuffix } from "./slug.js";
13
14
 
15
+ function slugToName(slug: string): string {
16
+ return capitalizeSeparatedWords(slug, "-", " ");
17
+ }
18
+
14
19
  export interface ReserveResult {
15
20
  slug: string;
16
21
  projectId: string;
@@ -39,10 +44,11 @@ export async function reserveProjectSlug(
39
44
  token: string,
40
45
  env: EnvironmentConfig = getEnvironmentConfig(),
41
46
  ): Promise<ReserveResult> {
47
+ const name = slugToName(slug);
42
48
  let currentSlug = slug;
43
49
 
44
50
  for (let attempt = 1; attempt <= MAX_SLUG_ATTEMPTS; attempt++) {
45
- const result = await tryCreateProject(currentSlug, token, env);
51
+ const result = await tryCreateProject(currentSlug, name, token, env);
46
52
 
47
53
  if (result.success) {
48
54
  return {
@@ -64,6 +70,7 @@ export async function reserveProjectSlug(
64
70
 
65
71
  async function tryCreateProject(
66
72
  slug: string,
73
+ name: string,
67
74
  token: string,
68
75
  env: EnvironmentConfig = getEnvironmentConfig(),
69
76
  ): Promise<CreateProjectResult> {
@@ -75,7 +82,7 @@ async function tryCreateProject(
75
82
  "Content-Type": "application/json",
76
83
  Accept: "application/json",
77
84
  },
78
- body: JSON.stringify({ slug, name: slug }),
85
+ body: JSON.stringify({ slug, name }),
79
86
  });
80
87
 
81
88
  if (response.ok) {
@@ -174,6 +174,9 @@ export function generateGitignoreContent(existingContent?: string): string {
174
174
  "dist/",
175
175
  ".veryfront/",
176
176
  "",
177
+ "# Local AI model cache",
178
+ ".cache/",
179
+ "",
177
180
  "# IDE",
178
181
  ".vscode/",
179
182
  ".idea/",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "nodeModulesDir": "auto",
5
5
  "exclude": [
6
6
  "npm/",
@@ -37,6 +37,7 @@ export default {
37
37
  "./provider": "./src/provider/index.ts",
38
38
  "./fs": "./src/fs/index.ts",
39
39
  "./integrations": "./src/integrations/index.ts",
40
+ "./sandbox": "./src/sandbox/index.ts",
40
41
  "./cli": "./cli/main.ts"
41
42
  },
42
43
  "imports": {
@@ -55,6 +56,7 @@ export default {
55
56
  "veryfront/react/fonts": "./src/react/fonts/index.ts",
56
57
  "veryfront/react/components/ai": "./src/react/components/ai/index.ts",
57
58
  "veryfront/components/ai": "./src/react/components/ai/index.ts",
59
+ "veryfront/sandbox": "./src/sandbox/index.ts",
58
60
  "veryfront/agent/react": "./src/agent/react/index.ts",
59
61
  "veryfront/agent/testing": "./src/agent/testing/index.ts",
60
62
  "veryfront/agent/middleware": "./src/agent/middleware/index.ts",
@@ -137,6 +139,7 @@ export default {
137
139
  "#veryfront/testing/bdd.ts": "./src/testing/bdd.ts",
138
140
  "#veryfront/testing/deno-compat": "./src/testing/deno-compat.ts",
139
141
  "#veryfront/testing/utils": "./src/testing/utils.ts",
142
+ "#veryfront/sandbox": "./src/sandbox/index.ts",
140
143
  "#veryfront/tool": "./src/tool/index.ts",
141
144
  "#veryfront/tool/schema": "./src/tool/schema/index.ts",
142
145
  "#veryfront/transforms": "./src/transforms/index.ts",
@@ -252,7 +255,8 @@ export default {
252
255
  "@babel/parser": "npm:@babel/parser@7.26.3",
253
256
  "@babel/traverse": "npm:@babel/traverse@7.26.3",
254
257
  "@babel/generator": "npm:@babel/generator@7.26.3",
255
- "@babel/types": "npm:@babel/types@7.26.3"
258
+ "@babel/types": "npm:@babel/types@7.26.3",
259
+ "@huggingface/transformers": "npm:@huggingface/transformers@3.4.2"
256
260
  },
257
261
  "compilerOptions": {
258
262
  "jsx": "react-jsx",
@@ -278,7 +282,7 @@ export default {
278
282
  "proxy": "deno run --allow-all cli/main.ts serve --mode=proxy",
279
283
  "dev": "deno run --allow-all cli/main.ts dev",
280
284
  "production": "deno run --allow-all cli/main.ts serve --mode=production",
281
- "build": "deno run -A scripts/build/generate-integrations-module.ts && deno run -A scripts/build/generate-templates-manifest.ts && deno run -A scripts/build/generate-dev-ui-manifest.ts && deno run -A scripts/build/prepare-framework-sources.ts && deno compile --allow-all --include src/platform/polyfills --include src/proxy/main.ts --include dist/framework-src --output ./bin/veryfront cli/main.ts",
285
+ "build": "deno run -A scripts/build/generate-integrations-module.ts && deno run -A scripts/build/generate-templates-manifest.ts && deno run -A scripts/build/generate-dev-ui-manifest.ts && deno run -A scripts/build/prepare-framework-sources.ts && deno run -A scripts/build/prebundle-client-scripts.ts && deno compile --allow-all --include src/platform/polyfills --include src/proxy/main.ts --include dist/framework-src --output ./bin/veryfront cli/main.ts",
282
286
  "build:npm": "deno run -A scripts/build/generate-integrations-module.ts && deno run -A scripts/build/build-npm-dnt.ts",
283
287
  "release": "deno run -A scripts/release.ts",
284
288
  "test": "VF_DISABLE_LRU_INTERVAL=1 SSR_TRANSFORM_PER_PROJECT_LIMIT=0 REVALIDATION_PER_PROJECT_LIMIT=0 NODE_ENV=production LOG_FORMAT=text deno test --no-check --parallel --allow-all '--ignore=tests/e2e,tests/integration/compiled-binary-e2e.test.ts' --unstable-worker-options --unstable-net",
@@ -383,7 +387,10 @@ export default {
383
387
  "proseWrap": "preserve"
384
388
  },
385
389
  "allowScripts": {
386
- "allow": [],
390
+ "allow": [
391
+ "npm:sharp@0.33.5",
392
+ "npm:onnxruntime-node@1.20.1"
393
+ ],
387
394
  "deny": [
388
395
  "npm:esbuild@0.20.2",
389
396
  "npm:protobufjs@7.5.4"
@@ -2,6 +2,8 @@ import * as dntShim from "../../_dnt.shims.js";
2
2
  import { z } from "zod";
3
3
  import { getAgent } from "./composition/index.js";
4
4
  import type { Message } from "./types.js";
5
+ import { fromError } from "../errors/veryfront-error.js";
6
+ import { DEFAULT_LOCAL_MODEL } from "../provider/local/model-catalog.js";
5
7
 
6
8
  // ---------------------------------------------------------------------------
7
9
  // Zod schemas for validating AI SDK v5 chat UI messages
@@ -129,11 +131,25 @@ export interface ChatHandlerOptions {
129
131
  ) => Record<string, unknown> | Promise<Record<string, unknown>>);
130
132
  }
131
133
 
134
+ /**
135
+ * Extract the raw Request from either a raw Request or a Pages Router APIContext.
136
+ * Pages Router handlers receive `(ctx)` where `ctx.request` is the Request.
137
+ * App Router handlers receive `(request, context)` where `request` IS the Request.
138
+ */
139
+ function extractRequest(requestOrCtx: unknown): dntShim.Request {
140
+ if (requestOrCtx instanceof dntShim.Request) return requestOrCtx;
141
+ // Pages Router APIContext — has a .request property
142
+ const ctx = requestOrCtx as { request?: dntShim.Request };
143
+ if (ctx.request instanceof dntShim.Request) return ctx.request;
144
+ throw new Error("Invalid handler argument: expected Request or APIContext");
145
+ }
146
+
132
147
  /**
133
148
  * Create a POST handler for a chat API route.
134
149
  *
135
- * Encapsulates request validation, message transformation, agent streaming,
136
- * and error handling so that template routes stay one-liners.
150
+ * Works with both App Router and Pages Router:
151
+ * - App Router: `app/api/chat/route.ts` handler receives `(request, context)`
152
+ * - Pages Router: `pages/api/chat.ts` — handler receives `(ctx)`
137
153
  *
138
154
  * @example
139
155
  * ```ts
@@ -145,12 +161,22 @@ export function createChatHandler(
145
161
  agentId: string,
146
162
  options?: ChatHandlerOptions,
147
163
  ) {
148
- return async function POST(request: dntShim.Request): Promise<dntShim.Response> {
164
+ // deno-lint-ignore no-explicit-any
165
+ return async function POST(requestOrCtx: any): Promise<dntShim.Response> {
166
+ const request = extractRequest(requestOrCtx);
167
+ // Resolve agent outside try so it's available in the catch block for
168
+ // extracting the system prompt in the 503 fallback response.
169
+ let agent: ReturnType<typeof getAgent> | undefined;
170
+ try {
171
+ agent = getAgent(agentId);
172
+ } catch {
173
+ return dntShim.Response.json({ error: "Agent not found" }, { status: 404 });
174
+ }
175
+
149
176
  try {
150
177
  const body = await request.json();
151
178
  const { messages: rawMessages } = chatRequestSchema.parse(body);
152
179
 
153
- const agent = getAgent(agentId);
154
180
  if (!agent) {
155
181
  return dntShim.Response.json({ error: "Agent not found" }, { status: 404 });
156
182
  }
@@ -177,6 +203,33 @@ export function createChatHandler(
177
203
  );
178
204
  }
179
205
 
206
+ // Detect structured "no_ai_available" errors from local engine
207
+ const vfError = fromError(error);
208
+ if (vfError?.type === "no_ai_available") {
209
+ // Resolve the agent's system prompt so the browser can use it for inference
210
+ const systemConfig = agent?.config?.system;
211
+ let systemPrompt = "You are a helpful AI assistant.";
212
+ if (typeof systemConfig === "string") {
213
+ systemPrompt = systemConfig;
214
+ } else if (typeof systemConfig === "function") {
215
+ try {
216
+ systemPrompt = await systemConfig();
217
+ } catch {
218
+ // Fall back to default
219
+ }
220
+ }
221
+
222
+ return dntShim.Response.json(
223
+ {
224
+ code: "NO_AI_AVAILABLE",
225
+ fallback: "browser",
226
+ model: DEFAULT_LOCAL_MODEL,
227
+ systemPrompt,
228
+ },
229
+ { status: 503 },
230
+ );
231
+ }
232
+
180
233
  return dntShim.Response.json(
181
234
  { error: "Internal server error" },
182
235
  { status: 500 },
@@ -6,7 +6,9 @@
6
6
 
7
7
  export { useChat } from "./use-chat/index.js";
8
8
  export type {
9
+ BrowserInferenceStatus,
9
10
  DynamicToolUIPart,
11
+ InferenceMode,
10
12
  OnToolCallArg,
11
13
  ReasoningUIPart,
12
14
  TextUIPart,
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Bridge between BrowserInferenceClient (Worker) and useChat's streaming interface.
3
+ *
4
+ * Converts UIMessage[] to simple {role, content}[] for the Worker,
5
+ * and synthesizes the same streaming updates that handleStreamingResponse produces.
6
+ */
7
+
8
+ import type { BrowserInferenceStatus } from "../types.js";
9
+ import type { UIMessage, UIMessagePart } from "../types.js";
10
+ import { generateClientId } from "../utils.js";
11
+ import { BrowserInferenceClient } from "./worker-client.js";
12
+
13
+ export interface BrowserInferenceCallbacks {
14
+ onUpdate: (parts: UIMessagePart[], messageId: string) => void;
15
+ onMessage: (message: UIMessage) => void;
16
+ onStatusChange: (status: BrowserInferenceStatus) => void;
17
+ onDownloadProgress?: (progress: number) => void;
18
+ onError: (error: Error) => void;
19
+ }
20
+
21
+ function extractTextFromMessages(
22
+ messages: UIMessage[],
23
+ ): Array<{ role: string; content: string }> {
24
+ return messages
25
+ .filter((m) => m.role === "user" || m.role === "assistant")
26
+ .map((m) => {
27
+ const textPart = m.parts.find((p) => p.type === "text");
28
+ const content = textPart && "text" in textPart ? textPart.text : "";
29
+ return { role: m.role, content };
30
+ })
31
+ .filter((m) => m.content.length > 0);
32
+ }
33
+
34
+ export function runBrowserInference(
35
+ messages: UIMessage[],
36
+ systemPrompt: string,
37
+ callbacks: BrowserInferenceCallbacks,
38
+ ): void {
39
+ const client = BrowserInferenceClient.getInstance();
40
+ const messageId = generateClientId("msg");
41
+ const simpleMessages = extractTextFromMessages(messages);
42
+
43
+ let accumulated = "";
44
+
45
+ client.generate(
46
+ messageId,
47
+ simpleMessages,
48
+ { systemPrompt, maxNewTokens: 512, temperature: 0.7 },
49
+ {
50
+ onStatus: (status) => callbacks.onStatusChange(status),
51
+ onDownloadProgress: (progress) => callbacks.onDownloadProgress?.(progress),
52
+ onToken: (token) => {
53
+ accumulated = token; // transformers.js callback_function sends full text each time
54
+ const parts: UIMessagePart[] = [
55
+ { type: "text", text: accumulated, state: "streaming" },
56
+ ];
57
+ callbacks.onUpdate(parts, messageId);
58
+ },
59
+ onDone: (text) => {
60
+ const finalParts: UIMessagePart[] = [
61
+ { type: "text", text: text || accumulated, state: "done" },
62
+ ];
63
+ const assistantMessage: UIMessage = {
64
+ id: messageId,
65
+ role: "assistant",
66
+ parts: finalParts,
67
+ };
68
+ callbacks.onMessage(assistantMessage);
69
+ callbacks.onStatusChange("ready");
70
+ },
71
+ onError: (errorMsg) => {
72
+ callbacks.onStatusChange("error");
73
+ callbacks.onError(new Error(errorMsg));
74
+ },
75
+ },
76
+ );
77
+ }
78
+
79
+ export function stopBrowserInference(): void {
80
+ BrowserInferenceClient.getInstance().stop();
81
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Worker ↔ main thread message protocol for browser-side inference.
3
+ */
4
+
5
+ export interface WorkerGenerateRequest {
6
+ type: "generate";
7
+ id: string;
8
+ messages: Array<{ role: string; content: string }>;
9
+ options?: {
10
+ maxNewTokens?: number;
11
+ temperature?: number;
12
+ systemPrompt?: string;
13
+ };
14
+ }
15
+
16
+ export type WorkerRequest = WorkerGenerateRequest;
17
+
18
+ export interface WorkerStatusResponse {
19
+ type: "status";
20
+ status: "loading-runtime" | "downloading-model" | "ready" | "generating";
21
+ }
22
+
23
+ export interface WorkerDownloadProgressResponse {
24
+ type: "download-progress";
25
+ progress: number; // 0–100
26
+ file?: string;
27
+ }
28
+
29
+ export interface WorkerTokenResponse {
30
+ type: "token";
31
+ id: string;
32
+ token: string;
33
+ }
34
+
35
+ export interface WorkerDoneResponse {
36
+ type: "done";
37
+ id: string;
38
+ text: string;
39
+ }
40
+
41
+ export interface WorkerErrorResponse {
42
+ type: "error";
43
+ id: string;
44
+ error: string;
45
+ }
46
+
47
+ export type WorkerResponse =
48
+ | WorkerStatusResponse
49
+ | WorkerDownloadProgressResponse
50
+ | WorkerTokenResponse
51
+ | WorkerDoneResponse
52
+ | WorkerErrorResponse;