veryfront 0.1.13 → 0.1.14

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 (135) 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 +2 -1
  28. package/esm/deno.js +8 -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 +5 -1
  63. package/esm/src/platform/compat/http/deno-server.d.ts.map +1 -1
  64. package/esm/src/platform/compat/http/deno-server.js +3 -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/package.json +5 -1
  97. package/src/cli/app/data/slug-words.ts +225 -90
  98. package/src/cli/app/operations/project-creation.ts +3 -3
  99. package/src/cli/app/shell.ts +1 -1
  100. package/src/cli/app/utils.ts +0 -30
  101. package/src/cli/app/views/dashboard.ts +27 -4
  102. package/src/cli/auth/callback-server.ts +3 -2
  103. package/src/cli/commands/dev/handler.ts +2 -0
  104. package/src/cli/commands/init/init-command.ts +30 -3
  105. package/src/cli/commands/init/interactive-wizard.ts +62 -34
  106. package/src/cli/mcp/remote-file-tools.ts +50 -15
  107. package/src/cli/shared/reserve-slug.ts +9 -2
  108. package/src/cli/utils/env-prompt.ts +3 -0
  109. package/src/deno.js +8 -4
  110. package/src/src/agent/chat-handler.ts +57 -4
  111. package/src/src/agent/react/index.ts +2 -0
  112. package/src/src/agent/react/use-chat/browser-inference/browser-engine.ts +81 -0
  113. package/src/src/agent/react/use-chat/browser-inference/types.ts +52 -0
  114. package/src/src/agent/react/use-chat/browser-inference/worker-client.ts +89 -0
  115. package/src/src/agent/react/use-chat/browser-inference/worker-script.ts +98 -0
  116. package/src/src/agent/react/use-chat/index.ts +2 -0
  117. package/src/src/agent/react/use-chat/types.ts +20 -0
  118. package/src/src/agent/react/use-chat/use-chat.ts +148 -8
  119. package/src/src/agent/runtime/index.ts +72 -6
  120. package/src/src/build/production-build/templates.ts +2 -68
  121. package/src/src/chat/index.ts +2 -0
  122. package/src/src/errors/veryfront-error.ts +2 -1
  123. package/src/src/platform/adapters/runtime/deno/adapter.ts +5 -1
  124. package/src/src/platform/compat/http/deno-server.ts +3 -1
  125. package/src/src/provider/index.ts +1 -0
  126. package/src/src/provider/local/ai-sdk-adapter.ts +207 -0
  127. package/src/src/provider/local/env.ts +26 -0
  128. package/src/src/provider/local/local-engine.ts +288 -0
  129. package/src/src/provider/local/model-catalog.ts +73 -0
  130. package/src/src/provider/model-registry.ts +66 -2
  131. package/src/src/proxy/main.ts +41 -6
  132. package/src/src/proxy/server-resolver.ts +151 -0
  133. package/src/src/react/components/ai/chat/components/inference-badge.tsx +48 -0
  134. package/src/src/react/components/ai/chat/components/upgrade-cta.tsx +56 -0
  135. package/src/src/react/components/ai/chat/index.tsx +43 -6
@@ -3,13 +3,9 @@
3
3
  *
4
4
  * Helper functions for project creation and management.
5
5
  */
6
- import * as dntShim from "../../_dnt.shims.js";
7
-
8
6
 
9
7
  import { cwd } from "../../src/platform/index.js";
10
8
  import { join } from "../../src/platform/compat/path/index.js";
11
- import { getEnvironmentConfig } from "../../src/config/index.js";
12
- import { capitalizeSeparatedWords } from "../../src/utils/case-utils.js";
13
9
  import { readToken } from "../auth/token-store.js";
14
10
  import { pullCommand } from "../commands/pull/index.js";
15
11
  import { addLog, type AppState, type StateUpdater } from "./state.js";
@@ -46,32 +42,6 @@ export function normalizeSlug(projectName: string): string {
46
42
  return projectName.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
47
43
  }
48
44
 
49
- export function slugToName(slug: string): string {
50
- return capitalizeSeparatedWords(slug, "-", " ");
51
- }
52
-
53
- export async function createRemoteProject(
54
- token: string,
55
- slug: string,
56
- ): Promise<{ slug: string }> {
57
- const apiUrl = getEnvironmentConfig().apiUrl || "https://api.veryfront.com";
58
- const response = await dntShim.fetch(`${apiUrl}/projects`, {
59
- method: "POST",
60
- headers: {
61
- Authorization: `Bearer ${token}`,
62
- "Content-Type": "application/json",
63
- Accept: "application/json",
64
- },
65
- body: JSON.stringify({ slug, name: slugToName(slug) }),
66
- });
67
-
68
- if (response.ok) return (await response.json()) as { slug: string };
69
-
70
- const error = await response.json().catch(() => ({}));
71
- const msg = (error as { message?: string }).message || `HTTP ${response.status}`;
72
- throw new Error(msg);
73
- }
74
-
75
45
  export function getLocalProjectsFromState(
76
46
  appState: AppState,
77
47
  ): Array<{ slug: string; path: string }> {
@@ -154,18 +154,35 @@ function renderSection(title: string, isActive = true): string {
154
154
  * Render the help bar at the bottom
155
155
  */
156
156
  function renderHelpBar(state: AppState): string {
157
+ const hasItems = state.projects.items.length > 0 || state.examples.items.length > 0 ||
158
+ (!!state.remote.user && state.remote.projects.length > 0);
159
+
157
160
  if (!state.showHelp) {
158
161
  const userInfo = state.remote.user ? ` ${dim("-")} ${brand(state.remote.user.email)}` : "";
162
+ if (!hasItems) {
163
+ const authHint = state.remote.user
164
+ ? `${dim("x")} ${dim("logout")}`
165
+ : `${dim("a")} ${dim("login")}`;
166
+ return ` ${dim("n")} ${dim("new project")} ${authHint} ${dim("? more")} ${
167
+ dim("q quit")
168
+ }${userInfo}`;
169
+ }
159
170
  return ` ${dim("↑↓ select enter open ? more q quit")}${userInfo}`;
160
171
  }
161
172
 
162
173
  const lines: string[] = [];
163
- lines.push(` ${dim("o")} open ${dim("s")} studio ${dim("i")} ide`);
174
+
175
+ if (hasItems) {
176
+ lines.push(` ${dim("o")} open ${dim("s")} studio ${dim("i")} ide`);
177
+ }
164
178
 
165
179
  if (!state.remote.user) {
166
180
  lines.push(` ${dim("n")} new ${dim("a")} login`);
167
181
  } else {
168
- lines.push(` ${dim("n")} new ${dim("p")} pull ${dim("u")} push ${dim("x")} logout`);
182
+ const parts = [` ${dim("n")} new`];
183
+ if (hasItems) parts.push(`${dim("p")} pull ${dim("u")} push`);
184
+ parts.push(`${dim("x")} logout`);
185
+ lines.push(parts.join(" "));
169
186
  }
170
187
 
171
188
  lines.push(` ${dim("? hide q quit")}`);
@@ -193,6 +210,12 @@ export function renderDashboardBoxed(state: AppState): string {
193
210
  /**
194
211
  * Render empty state when no projects found
195
212
  */
196
- export function renderEmptyState(): string {
197
- return `\n ${dim("No projects.")} ${brand("n")} ${dim("to create")}\n`;
213
+ export function renderEmptyState(state: AppState): string {
214
+ const lines: string[] = [];
215
+
216
+ lines.push(renderBanner(state), "");
217
+ lines.push(` ${dim("No projects.")} ${brand("n")} ${dim("to create")}`, "");
218
+ lines.push(renderHelpBar(state));
219
+
220
+ return lines.join("\n");
198
221
  }
@@ -186,8 +186,9 @@ function tryStartDenoServer(port: number): CallbackServer {
186
186
  resolveCallback = resolve;
187
187
  });
188
188
 
189
- // @ts-ignore - Deno global
190
- const server = dntShim.Deno.serve(
189
+ // Access native Deno.serve via `self` to bypass dnt shim transform.
190
+ const nativeDeno = (self as unknown as Record<string, typeof dntShim.Deno>)["Deno"]!;
191
+ const server = nativeDeno.serve(
191
192
  { port, hostname: "127.0.0.1", onListen: () => {} },
192
193
  (request: dntShim.Request) => {
193
194
  const url = new URL(request.url);
@@ -66,5 +66,7 @@ export async function handleDevCommand(args: ParsedArgs): Promise<void> {
66
66
  hmr: opts.hmr,
67
67
  });
68
68
 
69
+ // Block until the dev server shuts down.
70
+ // Without this, main.ts reaches exitProcess(0) and terminates immediately.
69
71
  await done;
70
72
  }
@@ -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.14",
4
4
  "nodeModulesDir": "auto",
5
5
  "exclude": [
6
6
  "npm/",
@@ -252,7 +252,8 @@ export default {
252
252
  "@babel/parser": "npm:@babel/parser@7.26.3",
253
253
  "@babel/traverse": "npm:@babel/traverse@7.26.3",
254
254
  "@babel/generator": "npm:@babel/generator@7.26.3",
255
- "@babel/types": "npm:@babel/types@7.26.3"
255
+ "@babel/types": "npm:@babel/types@7.26.3",
256
+ "@huggingface/transformers": "npm:@huggingface/transformers@3.4.2"
256
257
  },
257
258
  "compilerOptions": {
258
259
  "jsx": "react-jsx",
@@ -278,7 +279,7 @@ export default {
278
279
  "proxy": "deno run --allow-all cli/main.ts serve --mode=proxy",
279
280
  "dev": "deno run --allow-all cli/main.ts dev",
280
281
  "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",
282
+ "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
283
  "build:npm": "deno run -A scripts/build/generate-integrations-module.ts && deno run -A scripts/build/build-npm-dnt.ts",
283
284
  "release": "deno run -A scripts/release.ts",
284
285
  "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 +384,10 @@ export default {
383
384
  "proseWrap": "preserve"
384
385
  },
385
386
  "allowScripts": {
386
- "allow": [],
387
+ "allow": [
388
+ "npm:sharp@0.33.5",
389
+ "npm:onnxruntime-node@1.20.1"
390
+ ],
387
391
  "deny": [
388
392
  "npm:esbuild@0.20.2",
389
393
  "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,