@getjack/jack 0.1.33 → 0.1.35

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 (94) hide show
  1. package/README.md +6 -6
  2. package/package.json +1 -1
  3. package/src/commands/down.ts +39 -7
  4. package/src/commands/link.ts +2 -4
  5. package/src/commands/logs.ts +2 -4
  6. package/src/commands/mcp.ts +12 -10
  7. package/src/commands/secrets.ts +3 -1
  8. package/src/commands/services.ts +4 -2
  9. package/src/commands/sync.ts +5 -6
  10. package/src/lib/auth/client.ts +5 -2
  11. package/src/lib/binding-validator.ts +39 -3
  12. package/src/lib/build-helper.ts +18 -19
  13. package/src/lib/control-plane.ts +1 -0
  14. package/src/lib/crypto.ts +84 -0
  15. package/src/lib/deploy-upload.ts +7 -3
  16. package/src/lib/do-config.ts +110 -0
  17. package/src/lib/do-export-validator.ts +26 -0
  18. package/src/lib/hooks.ts +1 -2
  19. package/src/lib/jsonc-edit.ts +292 -0
  20. package/src/lib/managed-deploy.ts +36 -1
  21. package/src/lib/project-link.ts +37 -0
  22. package/src/lib/project-operations.ts +37 -46
  23. package/src/lib/prompts.ts +2 -2
  24. package/src/lib/resources.ts +4 -5
  25. package/src/lib/schema.ts +8 -12
  26. package/src/lib/services/db-create.ts +2 -2
  27. package/src/lib/services/db-execute.ts +9 -6
  28. package/src/lib/services/db-list.ts +6 -4
  29. package/src/lib/services/endpoint-test.ts +275 -0
  30. package/src/lib/services/project-delete.ts +190 -0
  31. package/src/lib/services/project-environment.ts +457 -0
  32. package/src/lib/services/storage-config.ts +7 -309
  33. package/src/lib/services/storage-create.ts +2 -1
  34. package/src/lib/services/storage-delete.ts +3 -2
  35. package/src/lib/services/storage-info.ts +2 -1
  36. package/src/lib/services/storage-list.ts +6 -3
  37. package/src/lib/services/vectorize-config.ts +7 -264
  38. package/src/lib/services/vectorize-create.ts +2 -1
  39. package/src/lib/services/vectorize-delete.ts +6 -4
  40. package/src/lib/services/vectorize-list.ts +6 -3
  41. package/src/lib/storage/index.ts +21 -23
  42. package/src/lib/telemetry.ts +1 -0
  43. package/src/lib/wrangler-config.ts +43 -312
  44. package/src/lib/zip-packager.ts +28 -0
  45. package/src/mcp/test-utils.ts +31 -0
  46. package/src/mcp/tools/index.ts +271 -0
  47. package/src/templates/index.ts +5 -0
  48. package/src/templates/types.ts +4 -0
  49. package/templates/AI-BINDINGS.md +34 -76
  50. package/templates/CLAUDE.md +22 -1
  51. package/templates/ai-chat/src/index.ts +7 -14
  52. package/templates/ai-chat/src/jack-ai.ts +0 -6
  53. package/templates/chat/.jack.json +45 -0
  54. package/templates/chat/bun.lock +1588 -0
  55. package/templates/chat/components.json +23 -0
  56. package/templates/chat/index.html +12 -0
  57. package/templates/chat/package.json +41 -0
  58. package/templates/chat/src/chat-agent.ts +61 -0
  59. package/templates/chat/src/client/app.tsx +189 -0
  60. package/templates/chat/src/client/chat.tsx +222 -0
  61. package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
  62. package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
  63. package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
  64. package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
  65. package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
  66. package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
  67. package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
  68. package/templates/chat/src/client/components/ui/button.tsx +38 -0
  69. package/templates/chat/src/client/lib/utils.ts +6 -0
  70. package/templates/chat/src/client/main.tsx +11 -0
  71. package/templates/chat/src/client/styles.css +125 -0
  72. package/templates/chat/src/index.ts +25 -0
  73. package/templates/chat/src/jack-ai.ts +94 -0
  74. package/templates/chat/tsconfig.json +18 -0
  75. package/templates/chat/vite.config.ts +14 -0
  76. package/templates/chat/wrangler.jsonc +18 -0
  77. package/templates/cron/.jack.json +18 -28
  78. package/templates/cron/schema.sql +10 -20
  79. package/templates/cron/src/admin.ts +321 -0
  80. package/templates/cron/src/index.ts +151 -81
  81. package/templates/cron/src/monitor.ts +124 -0
  82. package/templates/nextjs-clerk/app/layout.tsx +2 -0
  83. package/templates/semantic-search/src/index.ts +5 -43
  84. package/templates/semantic-search/src/jack-ai.ts +0 -6
  85. package/templates/telegram-bot/.jack.json +56 -0
  86. package/templates/telegram-bot/bun.lock +41 -0
  87. package/templates/telegram-bot/package.json +16 -0
  88. package/templates/telegram-bot/src/index.ts +236 -0
  89. package/templates/telegram-bot/src/jack-ai.ts +100 -0
  90. package/templates/telegram-bot/tsconfig.json +11 -0
  91. package/templates/telegram-bot/wrangler.jsonc +8 -0
  92. package/templates/cron/src/jobs.ts +0 -139
  93. package/templates/cron/src/webhooks.ts +0 -95
  94. package/templates/semantic-search/src/jack-vectorize.ts +0 -169
@@ -26,6 +26,9 @@ import {
26
26
  listDomains,
27
27
  unassignDomain,
28
28
  } from "../../lib/services/domain-operations.ts";
29
+ import { testEndpoint } from "../../lib/services/endpoint-test.ts";
30
+ import { deleteProject } from "../../lib/services/project-delete.ts";
31
+ import { getProjectEnvironment } from "../../lib/services/project-environment.ts";
29
32
  import { createStorageBucket } from "../../lib/services/storage-create.ts";
30
33
  import { deleteStorageBucket } from "../../lib/services/storage-delete.ts";
31
34
  import { getStorageBucketInfo } from "../../lib/services/storage-info.ts";
@@ -224,6 +227,18 @@ const RollbackProjectSchema = z.object({
224
227
  .describe("Path to project directory (defaults to current directory)"),
225
228
  });
226
229
 
230
+ const DeleteProjectSchema = z.object({
231
+ project_path: z
232
+ .string()
233
+ .optional()
234
+ .describe("Path to project directory (defaults to current directory)"),
235
+ export_database: z
236
+ .boolean()
237
+ .optional()
238
+ .default(false)
239
+ .describe("Export database to a SQL file before deletion"),
240
+ });
241
+
227
242
  const ListDomainsSchema = z.object({});
228
243
 
229
244
  const ConnectDomainSchema = z.object({
@@ -281,6 +296,33 @@ const TestCronSchema = z.object({
281
296
  .describe("Whether to trigger the cron handler on production (requires managed project)"),
282
297
  });
283
298
 
299
+ const GetProjectEnvironmentSchema = z.object({
300
+ project_path: z
301
+ .string()
302
+ .optional()
303
+ .describe("Path to project directory (defaults to current directory)"),
304
+ });
305
+
306
+ const TestEndpointSchema = z.object({
307
+ project_path: z
308
+ .string()
309
+ .optional()
310
+ .describe("Path to project directory (defaults to current directory)"),
311
+ path: z.string().describe("URL path to test, e.g. /api/todos"),
312
+ method: z
313
+ .enum(["GET", "POST", "PUT", "PATCH", "DELETE"])
314
+ .optional()
315
+ .default("GET")
316
+ .describe("HTTP method"),
317
+ headers: z.record(z.string()).optional().describe("Request headers"),
318
+ body: z.string().optional().describe("Request body (JSON string for POST/PUT/PATCH)"),
319
+ include_logs: z
320
+ .boolean()
321
+ .optional()
322
+ .default(true)
323
+ .describe("Capture runtime logs during the request (managed mode only)"),
324
+ });
325
+
284
326
  export function registerTools(server: McpServer, _options: McpServerOptions, debug: DebugLogger) {
285
327
  // Register tool list handler
286
328
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -423,6 +465,25 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
423
465
  },
424
466
  },
425
467
  },
468
+ {
469
+ name: "delete_project",
470
+ description:
471
+ "Permanently delete a project and all its resources (worker, database). This is irreversible. Optionally export the database to a SQL file before deletion.",
472
+ inputSchema: {
473
+ type: "object",
474
+ properties: {
475
+ project_path: {
476
+ type: "string",
477
+ description: "Path to project directory (defaults to current directory)",
478
+ },
479
+ export_database: {
480
+ type: "boolean",
481
+ default: false,
482
+ description: "Export database to a SQL file before deletion",
483
+ },
484
+ },
485
+ },
486
+ },
426
487
  {
427
488
  name: "create_database",
428
489
  description:
@@ -780,6 +841,63 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
780
841
  required: ["expression"],
781
842
  },
782
843
  },
844
+ {
845
+ name: "get_project_environment",
846
+ description:
847
+ "Get a consolidated snapshot of the project's runtime environment: deployed URL, bindings (D1, KV, R2, AI, Vectorize, Durable Objects), " +
848
+ "database schema with table definitions and row counts, cron schedules, environment variables, secret names, and configuration issues. " +
849
+ "Call this instead of making multiple separate calls to understand a project's full state.",
850
+ inputSchema: {
851
+ type: "object",
852
+ properties: {
853
+ project_path: {
854
+ type: "string",
855
+ description: "Path to project directory (defaults to current directory)",
856
+ },
857
+ },
858
+ },
859
+ },
860
+ {
861
+ name: "test_endpoint",
862
+ description:
863
+ "Test a deployed endpoint by making an HTTP request and capturing the response with optional runtime logs. " +
864
+ "Use after deploying to verify changes work, or to debug API behavior. " +
865
+ "Returns the full response (status, headers, body) and any console.log output from the worker.",
866
+ inputSchema: {
867
+ type: "object",
868
+ properties: {
869
+ project_path: {
870
+ type: "string",
871
+ description: "Path to project directory (defaults to current directory)",
872
+ },
873
+ path: {
874
+ type: "string",
875
+ description: "URL path to test, e.g. /api/todos",
876
+ },
877
+ method: {
878
+ type: "string",
879
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
880
+ default: "GET",
881
+ description: "HTTP method",
882
+ },
883
+ headers: {
884
+ type: "object",
885
+ additionalProperties: { type: "string" },
886
+ description: "Request headers",
887
+ },
888
+ body: {
889
+ type: "string",
890
+ description: "Request body (JSON string for POST/PUT/PATCH)",
891
+ },
892
+ include_logs: {
893
+ type: "boolean",
894
+ default: true,
895
+ description: "Capture runtime logs during the request (managed mode only)",
896
+ },
897
+ },
898
+ required: ["path"],
899
+ },
900
+ },
783
901
  ],
784
902
  };
785
903
  });
@@ -1156,6 +1274,48 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
1156
1274
  };
1157
1275
  }
1158
1276
 
1277
+ case "delete_project": {
1278
+ const args = DeleteProjectSchema.parse(request.params.arguments ?? {});
1279
+ const projectPath = args.project_path ?? process.cwd();
1280
+
1281
+ const wrappedDeleteProject = withTelemetry(
1282
+ "delete_project",
1283
+ async (projectDir: string, exportDatabase: boolean) => {
1284
+ const result = await deleteProject(projectDir, { exportDatabase });
1285
+
1286
+ track(Events.PROJECT_DELETED, {
1287
+ deploy_mode: result.deployMode,
1288
+ database_deleted: result.databaseDeleted,
1289
+ database_exported: result.databaseExportPath !== null,
1290
+ platform: "mcp",
1291
+ });
1292
+
1293
+ return {
1294
+ project_name: result.projectName,
1295
+ deploy_mode: result.deployMode,
1296
+ worker_deleted: result.workerDeleted,
1297
+ database_deleted: result.databaseDeleted,
1298
+ database_name: result.databaseName,
1299
+ database_export_path: result.databaseExportPath,
1300
+ resource_results: result.resourceResults,
1301
+ warnings: result.warnings,
1302
+ };
1303
+ },
1304
+ { platform: "mcp" },
1305
+ );
1306
+
1307
+ const result = await wrappedDeleteProject(projectPath, args.export_database ?? false);
1308
+
1309
+ return {
1310
+ content: [
1311
+ {
1312
+ type: "text",
1313
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1314
+ },
1315
+ ],
1316
+ };
1317
+ }
1318
+
1159
1319
  case "create_database": {
1160
1320
  const args = CreateDatabaseSchema.parse(request.params.arguments ?? {});
1161
1321
  const projectPath = args.project_path ?? process.cwd();
@@ -1961,6 +2121,117 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
1961
2121
  };
1962
2122
  }
1963
2123
 
2124
+ case "get_project_environment": {
2125
+ const args = GetProjectEnvironmentSchema.parse(request.params.arguments ?? {});
2126
+ const projectPath = args.project_path ?? process.cwd();
2127
+
2128
+ const wrappedGetProjectEnvironment = withTelemetry(
2129
+ "get_project_environment",
2130
+ async (projectDir: string) => {
2131
+ const env = await getProjectEnvironment(projectDir);
2132
+
2133
+ track(Events.COMMAND_COMPLETED, {
2134
+ command: "get_project_environment",
2135
+ has_database: !!env.database,
2136
+ binding_count:
2137
+ (env.bindings.d1 ? 1 : 0) +
2138
+ (env.bindings.r2?.length ?? 0) +
2139
+ (env.bindings.kv?.length ?? 0) +
2140
+ (env.bindings.ai ? 1 : 0) +
2141
+ (env.bindings.vectorize?.length ?? 0) +
2142
+ (env.bindings.durable_objects?.length ?? 0),
2143
+ issue_count: env.issues.length,
2144
+ platform: "mcp",
2145
+ });
2146
+
2147
+ return env;
2148
+ },
2149
+ { platform: "mcp" },
2150
+ );
2151
+
2152
+ const result = await wrappedGetProjectEnvironment(projectPath);
2153
+
2154
+ return {
2155
+ content: [
2156
+ {
2157
+ type: "text",
2158
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
2159
+ },
2160
+ ],
2161
+ };
2162
+ }
2163
+
2164
+ case "test_endpoint": {
2165
+ const args = TestEndpointSchema.parse(request.params.arguments ?? {});
2166
+ const projectPath = args.project_path ?? process.cwd();
2167
+
2168
+ const wrappedTestEndpoint = withTelemetry(
2169
+ "test_endpoint",
2170
+ async (
2171
+ projectDir: string,
2172
+ path: string,
2173
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
2174
+ headers?: Record<string, string>,
2175
+ body?: string,
2176
+ includeLogs?: boolean,
2177
+ ) => {
2178
+ const result = await testEndpoint({
2179
+ projectDir,
2180
+ path,
2181
+ method,
2182
+ headers,
2183
+ body,
2184
+ includeLogs,
2185
+ });
2186
+
2187
+ track(Events.COMMAND_COMPLETED, {
2188
+ command: "test_endpoint",
2189
+ method,
2190
+ status: result.response.status,
2191
+ duration_ms: result.response.duration_ms,
2192
+ log_count: result.logs.length,
2193
+ platform: "mcp",
2194
+ });
2195
+
2196
+ return result;
2197
+ },
2198
+ { platform: "mcp" },
2199
+ );
2200
+
2201
+ const result = await wrappedTestEndpoint(
2202
+ projectPath,
2203
+ args.path,
2204
+ args.method,
2205
+ args.headers,
2206
+ args.body,
2207
+ args.include_logs,
2208
+ );
2209
+
2210
+ // Add notes for agents about log availability
2211
+ const notes: string[] = [];
2212
+ if (args.include_logs && result.logs.length === 0) {
2213
+ const deployMode = await getDeployMode(projectPath);
2214
+ if (deployMode !== "managed") {
2215
+ notes.push(
2216
+ "Runtime logs are only available for managed (Jack Cloud) projects. Use tail_logs with a managed project.",
2217
+ );
2218
+ }
2219
+ }
2220
+
2221
+ return {
2222
+ content: [
2223
+ {
2224
+ type: "text",
2225
+ text: JSON.stringify(
2226
+ formatSuccessResponse(result, startTime, notes.length ? notes : undefined),
2227
+ null,
2228
+ 2,
2229
+ ),
2230
+ },
2231
+ ],
2232
+ };
2233
+ }
2234
+
1964
2235
  default:
1965
2236
  throw new Error(`Unknown tool: ${toolName}`);
1966
2237
  }
@@ -19,10 +19,12 @@ export const BUILTIN_TEMPLATES = [
19
19
  "nextjs",
20
20
  "saas",
21
21
  "ai-chat",
22
+ "chat",
22
23
  "semantic-search",
23
24
  "nextjs-shadcn",
24
25
  "nextjs-clerk",
25
26
  "nextjs-auth",
27
+ "telegram-bot",
26
28
  ];
27
29
 
28
30
  /**
@@ -89,6 +91,7 @@ async function loadTemplate(name: string): Promise<Template> {
89
91
  envVars?: Template["envVars"];
90
92
  capabilities?: Template["capabilities"];
91
93
  requires?: Template["requires"];
94
+ crons?: Template["crons"];
92
95
  hooks?: Template["hooks"];
93
96
  agentContext?: Template["agentContext"];
94
97
  intent?: Template["intent"];
@@ -107,6 +110,7 @@ async function loadTemplate(name: string): Promise<Template> {
107
110
  envVars: metadata.envVars,
108
111
  capabilities: metadata.capabilities,
109
112
  requires: metadata.requires,
113
+ crons: metadata.crons,
110
114
  hooks: metadata.hooks,
111
115
  agentContext: metadata.agentContext,
112
116
  intent: metadata.intent,
@@ -246,6 +250,7 @@ async function fetchRemoteTemplate(identifier: string): Promise<Template | null>
246
250
  envVars: metadata.envVars as Template["envVars"],
247
251
  capabilities: metadata.capabilities as Template["capabilities"],
248
252
  requires: metadata.requires as Template["requires"],
253
+ crons: metadata.crons as Template["crons"],
249
254
  hooks: metadata.hooks as Template["hooks"],
250
255
  agentContext: metadata.agentContext as Template["agentContext"],
251
256
  intent: metadata.intent as Template["intent"],
@@ -35,6 +35,9 @@ export type HookAction =
35
35
  onMissing?: "fail" | "prompt" | "generate";
36
36
  promptMessage?: string;
37
37
  generateCommand?: string;
38
+ perProject?: boolean; // Skip global secrets store — always prompt or generate fresh
39
+ validateCommand?: string; // Shell command to validate prompted value ({{value}} is substituted)
40
+ validateError?: string; // Error message shown when validation fails
38
41
  }
39
42
  | {
40
43
  action: "stripe-setup";
@@ -92,6 +95,7 @@ export interface Template {
92
95
  envVars?: EnvVar[]; // environment variables (non-secret config)
93
96
  capabilities?: Capability[]; // infrastructure requirements (deprecated, use requires)
94
97
  requires?: ServiceTypeKey[]; // service requirements (DB, KV, CRON, QUEUE, STORAGE)
98
+ crons?: string[]; // cron expressions to auto-create (managed mode only)
95
99
  description?: string; // for help text
96
100
  hooks?: TemplateHooks;
97
101
  agentContext?: AgentContext;
@@ -2,17 +2,15 @@
2
2
 
3
3
  ## Quick Reference for Coding Agents
4
4
 
5
- **DO NOT add `[ai]` or `[[vectorize]]` bindings directly for jack cloud projects.**
5
+ ### AI Uses Proxy
6
6
 
7
- Jack cloud uses metered proxies for AI and Vectorize calls. Use the patterns below:
8
-
9
- ### AI Pattern
7
+ AI uses a metered proxy (`__AI_PROXY`). Templates need a `jack-ai.ts` wrapper:
10
8
 
11
9
  ```typescript
12
10
  import { createJackAI } from "./jack-ai";
13
11
 
14
12
  function getAI(env: Env) {
15
- if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
13
+ if (env.__AI_PROXY) {
16
14
  return createJackAI(env);
17
15
  }
18
16
  if (env.AI) {
@@ -26,81 +24,60 @@ const ai = getAI(env);
26
24
  const result = await ai.run("@cf/meta/llama-3.2-1b-instruct", { messages });
27
25
  ```
28
26
 
29
- ### Vectorize Pattern
30
-
31
- ```typescript
32
- import { createJackVectorize } from "./jack-vectorize";
27
+ ### Vectorize — Direct Binding (Metered via Code Injection)
33
28
 
34
- const INDEX_NAME = "my-vectors";
29
+ Vectorize uses `env.VECTORS` directly. No wrapper needed. The control plane injects a metering
30
+ layer at deploy time that wraps the binding before your code runs.
35
31
 
36
- function getVectorize(env: Env) {
37
- if (env.__VECTORIZE_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
38
- return createJackVectorize(env, INDEX_NAME);
39
- }
40
- if (env.VECTORS) {
41
- return env.VECTORS;
42
- }
43
- throw new Error("No Vectorize binding available");
44
- }
45
-
46
- // Usage:
47
- const vectors = getVectorize(env);
48
- const results = await vectors.query(embedding, { topK: 10 });
49
- await vectors.insert([{ id: "doc1", values: embedding, metadata: {} }]);
32
+ ```typescript
33
+ // Just use env.VECTORS metering happens automatically
34
+ const results = await env.VECTORS.query(embedding, { topK: 10 });
35
+ await env.VECTORS.insert([{ id: "doc1", values: embedding, metadata: {} }]);
50
36
  ```
51
37
 
52
- ## Why This Pattern?
38
+ **Do NOT create a vectorize wrapper.** The old `jack-vectorize.ts` proxy pattern is deprecated.
53
39
 
54
- Jack cloud is multi-tenant. All users share Cloudflare's AI quota (10k neurons/day). Without metering:
55
- - One user could exhaust the daily quota for everyone
56
- - No visibility into per-project usage
57
- - No ability to bill for AI usage
40
+ ## Why Different Patterns?
58
41
 
59
- The proxy:
60
- 1. Checks per-project quota
61
- 2. Logs usage to Analytics Engine
62
- 3. Forwards to real AI binding
63
- 4. Returns response unchanged
42
+ - **AI** is a global shared service. The proxy enforces per-project quota (sync check before forwarding).
43
+ - **Vectorize** is per-tenant (each project gets its own index). The control plane wraps `env.VECTORS` via code injection (`__jack_meter.mjs`) for async metering. No proxy needed.
64
44
 
65
45
  ## Environment Bindings
66
46
 
67
47
  ### Jack Cloud (Managed Deploy)
68
48
 
69
- Control plane injects these bindings:
70
- - `__AI_PROXY` - Service binding to jack-binding-proxy for AI
71
- - `__VECTORIZE_PROXY` - Service binding to jack-binding-proxy for Vectorize
72
- - `__JACK_PROJECT_ID` - Project ID for metering
73
- - `__JACK_ORG_ID` - Organization ID for billing
49
+ Control plane injects:
50
+ - `__AI_PROXY` Service binding to jack-binding-proxy for AI (metering + quota)
51
+ - `VECTORS` Direct Vectorize binding to the project's own index
52
+ - `__JACK_VECTORIZE_USAGE` Analytics Engine binding for vectorize metering (injected, not visible to user code)
74
53
 
75
- `env.AI` and `env.VECTORS` are **NOT available** in jack cloud. Direct calls will fail.
54
+ `env.AI` is **NOT available** in jack cloud (use `getAI()` helper). `env.VECTORS` **IS available** directly.
76
55
 
77
56
  ### Local Development
78
57
 
79
58
  wrangler.jsonc provides:
80
- - `AI` - Direct Cloudflare AI binding for local testing
81
- - `VECTORS` - Direct Vectorize binding for local testing
59
+ - `AI` Direct Cloudflare AI binding for local testing
60
+ - `VECTORS` Direct Vectorize binding for local testing
82
61
 
83
- The helper functions automatically use the right binding based on environment.
62
+ The `getAI()` helper automatically uses the right binding. For vectorize, `env.VECTORS` works in both environments.
84
63
 
85
64
  ## Template Pattern
86
65
 
87
66
  ### AI Templates
88
67
 
89
- 1. **src/jack-ai.ts** - Client wrapper (copy from ai-chat or semantic-search template)
68
+ 1. **src/jack-ai.ts** Client wrapper (copy from ai-chat or semantic-search template)
90
69
 
91
70
  2. **Env interface** with optional bindings:
92
71
  ```typescript
93
72
  interface Env {
94
73
  AI?: Ai; // Local dev
95
74
  __AI_PROXY?: Fetcher; // Jack cloud
96
- __JACK_PROJECT_ID?: string; // Jack cloud
97
- __JACK_ORG_ID?: string; // Jack cloud
98
75
  }
99
76
  ```
100
77
 
101
78
  3. **getAI() helper** that handles both environments
102
79
 
103
- 4. **wrangler.jsonc** with AI binding for local dev only:
80
+ 4. **wrangler.jsonc** with AI binding:
104
81
  ```jsonc
105
82
  {
106
83
  "ai": { "binding": "AI" }
@@ -109,21 +86,16 @@ interface Env {
109
86
 
110
87
  ### Vectorize Templates
111
88
 
112
- 1. **src/jack-vectorize.ts** - Client wrapper (copy from semantic-search template)
113
-
114
- 2. **Env interface** with optional bindings:
89
+ 1. **Env interface** with direct binding:
115
90
  ```typescript
116
91
  interface Env {
117
- VECTORS?: VectorizeIndex; // Local dev
118
- __VECTORIZE_PROXY?: Fetcher; // Jack cloud
119
- __JACK_PROJECT_ID?: string; // Jack cloud
120
- __JACK_ORG_ID?: string; // Jack cloud
92
+ VECTORS: VectorizeIndex;
121
93
  }
122
94
  ```
123
95
 
124
- 3. **getVectorize() helper** that handles both environments
96
+ 2. **Use `env.VECTORS` directly** no wrapper, no helper needed
125
97
 
126
- 4. **wrangler.jsonc** with Vectorize binding for local dev only:
98
+ 3. **wrangler.jsonc** with Vectorize binding:
127
99
  ```jsonc
128
100
  {
129
101
  "vectorize": [{
@@ -136,8 +108,6 @@ interface Env {
136
108
 
137
109
  ## Error Handling
138
110
 
139
- Quota exceeded returns 429:
140
-
141
111
  ### AI Quota
142
112
  ```typescript
143
113
  try {
@@ -150,21 +120,9 @@ try {
150
120
  }
151
121
  ```
152
122
 
153
- ### Vectorize Quota
154
- ```typescript
155
- try {
156
- const results = await vectors.query(embedding, { topK: 10 });
157
- } catch (error) {
158
- if (error.code === "VECTORIZE_QUERY_QUOTA_EXCEEDED") {
159
- // Query limit (33,000/day) reached
160
- console.log(`Retry in ${error.resetIn} seconds`);
161
- }
162
- if (error.code === "VECTORIZE_MUTATION_QUOTA_EXCEEDED") {
163
- // Mutation limit (10,000/day) reached
164
- console.log(`Retry in ${error.resetIn} seconds`);
165
- }
166
- }
167
- ```
123
+ ### Vectorize
124
+
125
+ Vectorize calls go directly to the binding. There is no sync quota enforcement — metering is async via Analytics Engine. If Cloudflare's own rate limits are hit, the binding returns standard errors.
168
126
 
169
127
  ## BYOC Mode
170
128
 
@@ -176,6 +134,6 @@ For Bring Your Own Cloud deployments:
176
134
 
177
135
  ## See Also
178
136
 
179
- - `/docs/internal/specs/binding-proxy-worker.md` - Full architecture spec
180
- - `apps/binding-proxy-worker/` - Proxy implementation
181
- - `apps/control-plane/src/deployment-service.ts` - Binding injection logic
137
+ - `apps/binding-proxy-worker/` AI proxy implementation
138
+ - `apps/control-plane/src/metering-wrapper.ts` Vectorize metering code injection
139
+ - `apps/control-plane/src/deployment-service.ts` Binding injection logic
@@ -26,7 +26,7 @@ const result = await env.AI.run(model, inputs);
26
26
  import { createJackAI } from "./jack-ai";
27
27
 
28
28
  function getAI(env: Env) {
29
- if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
29
+ if (env.__AI_PROXY) {
30
30
  return createJackAI(env);
31
31
  }
32
32
  return env.AI;
@@ -114,6 +114,27 @@ Before shipping a new template:
114
114
  - [ ] Install time < 5s (cold cache + lockfile)
115
115
  - [ ] All scripts work without global tools except wrangler
116
116
  - [ ] `.gitignore` includes `.env`, `.dev.vars`, `.secrets.json`
117
+ - [ ] Prebuilt builds without env vars: `bun run build` succeeds with zero secrets set
118
+ - [ ] If template has `schema.sql`, verify it's included in prebuild output
119
+
120
+ ## Prebuild Compatibility (IMPORTANT)
121
+
122
+ Templates are prebuilt at release time **without any env vars or secrets**. The prebuild runs `bun run build` in a clean environment. If the build depends on runtime env vars, it will fail silently (only discovered when someone runs `prebuild-templates.ts`).
123
+
124
+ **Common failure:** A provider component (e.g., `ClerkProvider`, `AuthProvider`) in the root layout requires an API key at import time. Next.js tries to statically prerender pages and the provider throws because the key is missing.
125
+
126
+ **Fix:** Add `export const dynamic = "force-dynamic"` to the root layout. This tells Next.js to skip static prerendering for all pages, deferring to runtime where env vars are available.
127
+
128
+ ```tsx
129
+ // app/layout.tsx
130
+ export const dynamic = "force-dynamic"; // Required: provider needs runtime env vars
131
+
132
+ export default function RootLayout({ children }) {
133
+ return <SomeProvider>{children}</SomeProvider>;
134
+ }
135
+ ```
136
+
137
+ **General rule:** If a template imports any SDK/provider that reads `process.env` or `env.*` at initialization, that template's layout or pages must be force-dynamic to survive a zero-env-var prebuild.
117
138
 
118
139
  ## Placeholder System
119
140
 
@@ -1,30 +1,25 @@
1
+ import { type Message, convertToCoreMessages, streamText } from "ai";
1
2
  import { Hono } from "hono";
2
- import { streamText, convertToCoreMessages, type Message } from "ai";
3
3
  import { createWorkersAI } from "workers-ai-provider";
4
4
  import { createJackAI } from "./jack-ai";
5
5
 
6
6
  interface Env {
7
7
  AI?: Ai;
8
8
  __AI_PROXY?: Fetcher;
9
- __JACK_PROJECT_ID?: string;
10
- __JACK_ORG_ID?: string;
11
9
  ASSETS: Fetcher;
12
10
  DB: D1Database;
13
11
  }
14
12
 
15
13
  function getAI(env: Env) {
16
- if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
17
- return createJackAI(
18
- env as Required<
19
- Pick<Env, "__AI_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">
20
- >,
21
- );
14
+ if (env.__AI_PROXY) {
15
+ return createJackAI(env as Pick<Env, "__AI_PROXY"> & { __AI_PROXY: Fetcher });
22
16
  }
23
17
  if (env.AI) return env.AI;
24
18
  throw new Error("No AI binding available");
25
19
  }
26
20
 
27
- const SYSTEM_PROMPT = `You are a helpful AI assistant. Be concise, friendly, and helpful. Keep responses short unless detail is needed.`;
21
+ const SYSTEM_PROMPT =
22
+ "You are a helpful AI assistant. Be concise, friendly, and helpful. Keep responses short unless detail is needed.";
28
23
 
29
24
  // Rate limiting
30
25
  const RATE_LIMIT = 10;
@@ -74,16 +69,14 @@ app.post("/api/chat", async (c) => {
74
69
  messages: Message[];
75
70
  chatId?: string;
76
71
  }>();
77
- if (!messages || !Array.isArray(messages)) {
72
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
78
73
  return c.json({ error: "Invalid request." }, 400);
79
74
  }
80
75
 
81
76
  // Save user message to DB if we have a chatId
82
77
  const lastUserMsg = messages.findLast((m: Message) => m.role === "user");
83
78
  if (chatId && lastUserMsg) {
84
- await c.env.DB.prepare(
85
- "INSERT INTO messages (id, chat_id, role, content) VALUES (?, ?, ?, ?)",
86
- )
79
+ await c.env.DB.prepare("INSERT INTO messages (id, chat_id, role, content) VALUES (?, ?, ?, ?)")
87
80
  .bind(
88
81
  crypto.randomUUID(),
89
82
  chatId,
@@ -10,8 +10,6 @@
10
10
  *
11
11
  * interface Env {
12
12
  * __AI_PROXY: Fetcher; // Service binding to binding-proxy worker
13
- * __JACK_PROJECT_ID: string; // Injected by control plane
14
- * __JACK_ORG_ID: string; // Injected by control plane
15
13
  * }
16
14
  *
17
15
  * export default {
@@ -29,8 +27,6 @@
29
27
 
30
28
  interface JackAIEnv {
31
29
  __AI_PROXY: Fetcher;
32
- __JACK_PROJECT_ID: string;
33
- __JACK_ORG_ID: string;
34
30
  }
35
31
 
36
32
  /**
@@ -56,8 +52,6 @@ export function createJackAI(env: JackAIEnv): {
56
52
  method: "POST",
57
53
  headers: {
58
54
  "Content-Type": "application/json",
59
- "X-Jack-Project-ID": env.__JACK_PROJECT_ID,
60
- "X-Jack-Org-ID": env.__JACK_ORG_ID,
61
55
  },
62
56
  body: JSON.stringify({ model, inputs, options }),
63
57
  });