@getjack/jack 0.1.32 → 0.1.33

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 (193) hide show
  1. package/package.json +1 -1
  2. package/src/commands/deploys.ts +95 -0
  3. package/src/commands/link.ts +8 -0
  4. package/src/commands/mcp.ts +179 -4
  5. package/src/commands/rollback.ts +53 -0
  6. package/src/commands/services.ts +11 -1
  7. package/src/commands/ship.ts +3 -1
  8. package/src/commands/tokens.ts +16 -1
  9. package/src/commands/whoami.ts +43 -8
  10. package/src/index.ts +16 -0
  11. package/src/lib/agent-files.ts +54 -4
  12. package/src/lib/agent-integration.ts +4 -166
  13. package/src/lib/claude-hooks-installer.ts +55 -0
  14. package/src/lib/control-plane.ts +78 -40
  15. package/src/lib/debug.ts +2 -1
  16. package/src/lib/deploy-upload.ts +6 -0
  17. package/src/lib/hooks.ts +3 -1
  18. package/src/lib/managed-deploy.ts +12 -9
  19. package/src/lib/project-link.ts +6 -0
  20. package/src/lib/project-operations.ts +68 -22
  21. package/src/lib/telemetry.ts +2 -0
  22. package/src/mcp/README.md +1 -1
  23. package/src/mcp/resources/index.ts +1 -16
  24. package/src/mcp/server.ts +23 -0
  25. package/src/mcp/tools/index.ts +133 -17
  26. package/src/mcp/types.ts +1 -0
  27. package/src/mcp/utils.ts +2 -1
  28. package/src/templates/index.ts +25 -73
  29. package/templates/CLAUDE.md +41 -0
  30. package/templates/ai-chat/.jack.json +10 -5
  31. package/templates/ai-chat/bun.lock +50 -1
  32. package/templates/ai-chat/package.json +5 -0
  33. package/templates/ai-chat/public/app.js +73 -0
  34. package/templates/ai-chat/public/index.html +14 -197
  35. package/templates/ai-chat/schema.sql +14 -0
  36. package/templates/ai-chat/src/index.ts +86 -102
  37. package/templates/ai-chat/wrangler.jsonc +8 -1
  38. package/templates/cron/.jack.json +66 -0
  39. package/templates/cron/bun.lock +23 -0
  40. package/templates/cron/package.json +16 -0
  41. package/templates/cron/schema.sql +24 -0
  42. package/templates/cron/src/index.ts +117 -0
  43. package/templates/cron/src/jobs.ts +139 -0
  44. package/templates/cron/src/webhooks.ts +95 -0
  45. package/templates/cron/tsconfig.json +17 -0
  46. package/templates/cron/wrangler.jsonc +11 -0
  47. package/templates/miniapp/.jack.json +1 -1
  48. package/templates/nextjs/.jack.json +1 -1
  49. package/templates/nextjs-auth/.jack.json +44 -0
  50. package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
  51. package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
  52. package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
  53. package/templates/nextjs-auth/app/error.tsx +44 -0
  54. package/templates/nextjs-auth/app/globals.css +1 -0
  55. package/templates/nextjs-auth/app/health/route.ts +3 -0
  56. package/templates/nextjs-auth/app/layout.tsx +24 -0
  57. package/templates/nextjs-auth/app/login/page.tsx +10 -0
  58. package/templates/nextjs-auth/app/page.tsx +86 -0
  59. package/templates/nextjs-auth/app/signup/page.tsx +10 -0
  60. package/templates/nextjs-auth/bun.lock +1065 -0
  61. package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
  62. package/templates/nextjs-auth/components/auth-form.tsx +191 -0
  63. package/templates/nextjs-auth/components/header.tsx +50 -0
  64. package/templates/nextjs-auth/components/user-menu.tsx +23 -0
  65. package/templates/nextjs-auth/lib/auth-client.ts +3 -0
  66. package/templates/nextjs-auth/lib/auth.ts +43 -0
  67. package/templates/nextjs-auth/lib/utils.ts +6 -0
  68. package/templates/nextjs-auth/middleware.ts +33 -0
  69. package/templates/nextjs-auth/next.config.ts +8 -0
  70. package/templates/nextjs-auth/open-next.config.ts +6 -0
  71. package/templates/nextjs-auth/package.json +33 -0
  72. package/templates/nextjs-auth/postcss.config.mjs +8 -0
  73. package/templates/nextjs-auth/schema.sql +49 -0
  74. package/templates/nextjs-auth/tsconfig.json +28 -0
  75. package/templates/nextjs-auth/wrangler.jsonc +23 -0
  76. package/templates/nextjs-clerk/.jack.json +54 -0
  77. package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
  78. package/templates/nextjs-clerk/app/globals.css +1 -0
  79. package/templates/nextjs-clerk/app/health/route.ts +3 -0
  80. package/templates/nextjs-clerk/app/layout.tsx +26 -0
  81. package/templates/nextjs-clerk/app/page.tsx +86 -0
  82. package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
  83. package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
  84. package/templates/nextjs-clerk/bun.lock +1055 -0
  85. package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
  86. package/templates/nextjs-clerk/components/header.tsx +40 -0
  87. package/templates/nextjs-clerk/lib/utils.ts +6 -0
  88. package/templates/nextjs-clerk/middleware.ts +18 -0
  89. package/templates/nextjs-clerk/next.config.ts +8 -0
  90. package/templates/nextjs-clerk/open-next.config.ts +6 -0
  91. package/templates/nextjs-clerk/package.json +31 -0
  92. package/templates/nextjs-clerk/postcss.config.mjs +8 -0
  93. package/templates/nextjs-clerk/tsconfig.json +28 -0
  94. package/templates/nextjs-clerk/wrangler.jsonc +17 -0
  95. package/templates/nextjs-shadcn/.jack.json +34 -0
  96. package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
  97. package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
  98. package/templates/nextjs-shadcn/app/globals.css +126 -0
  99. package/templates/nextjs-shadcn/app/health/route.ts +3 -0
  100. package/templates/nextjs-shadcn/app/layout.tsx +24 -0
  101. package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
  102. package/templates/nextjs-shadcn/app/page.tsx +180 -0
  103. package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
  104. package/templates/nextjs-shadcn/bun.lock +1789 -0
  105. package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
  106. package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
  107. package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
  108. package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
  109. package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
  110. package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
  111. package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
  112. package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
  113. package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
  114. package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
  115. package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
  116. package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
  117. package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
  118. package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
  119. package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
  120. package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
  121. package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
  122. package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
  123. package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
  124. package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
  125. package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
  126. package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
  127. package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
  128. package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
  129. package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
  130. package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
  131. package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
  132. package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
  133. package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
  134. package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
  135. package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
  136. package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
  137. package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
  138. package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
  139. package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
  140. package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
  141. package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
  142. package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
  143. package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
  144. package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
  145. package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
  146. package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
  147. package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
  148. package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
  149. package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
  150. package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
  151. package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
  152. package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
  153. package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
  154. package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
  155. package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
  156. package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
  157. package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
  158. package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
  159. package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
  160. package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
  161. package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
  162. package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
  163. package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
  164. package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
  165. package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
  166. package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
  167. package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
  168. package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
  169. package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
  170. package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
  171. package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
  172. package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
  173. package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
  174. package/templates/nextjs-shadcn/components.json +23 -0
  175. package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
  176. package/templates/nextjs-shadcn/lib/utils.ts +6 -0
  177. package/templates/nextjs-shadcn/next-env.d.ts +6 -0
  178. package/templates/nextjs-shadcn/next.config.ts +8 -0
  179. package/templates/nextjs-shadcn/open-next.config.ts +6 -0
  180. package/templates/nextjs-shadcn/package.json +55 -0
  181. package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
  182. package/templates/nextjs-shadcn/tsconfig.json +28 -0
  183. package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
  184. package/templates/resend/.jack.json +64 -0
  185. package/templates/resend/bun.lock +23 -0
  186. package/templates/resend/package.json +16 -0
  187. package/templates/resend/schema.sql +13 -0
  188. package/templates/resend/src/email.ts +165 -0
  189. package/templates/resend/src/index.ts +108 -0
  190. package/templates/resend/tsconfig.json +17 -0
  191. package/templates/resend/wrangler.jsonc +11 -0
  192. package/templates/saas/.jack.json +1 -1
  193. package/templates/ai-chat/public/chat.js +0 -149
@@ -14,11 +14,15 @@ export const BUILTIN_TEMPLATES = [
14
14
  "hello",
15
15
  "miniapp",
16
16
  "api",
17
+ "cron",
18
+ "resend",
17
19
  "nextjs",
18
20
  "saas",
19
- "simple-api-starter",
20
21
  "ai-chat",
21
22
  "semantic-search",
23
+ "nextjs-shadcn",
24
+ "nextjs-clerk",
25
+ "nextjs-auth",
22
26
  ];
23
27
 
24
28
  /**
@@ -211,64 +215,32 @@ function extractMetadataFromZip(zipData: ArrayBuffer): Record<string, unknown> {
211
215
  }
212
216
 
213
217
  /**
214
- * Fetch a published template from jack cloud (public endpoint, no auth)
218
+ * Fetch a remote template (own project or published).
219
+ * Both paths require auth.
215
220
  */
216
- async function fetchPublishedTemplate(username: string, slug: string): Promise<Template> {
217
- const response = await fetch(
218
- `${getControlApiUrl()}/v1/projects/${encodeURIComponent(username)}/${encodeURIComponent(slug)}/source`,
219
- );
220
-
221
- if (!response.ok) {
222
- if (response.status === 404) {
223
- throw new Error(
224
- `Template not found: ${username}/${slug}\n\nMake sure the project exists and is published with: jack publish`,
225
- );
226
- }
227
- throw new Error(`Failed to fetch template: ${response.status}`);
228
- }
229
-
230
- const zipData = await response.arrayBuffer();
231
- const metadata = extractMetadataFromZip(zipData);
232
- const files = extractZipToFiles(zipData);
233
-
234
- return {
235
- description: (metadata.description as string) || `Fork of ${username}/${slug}`,
236
- secrets: (metadata.secrets as string[]) || [],
237
- optionalSecrets: metadata.optionalSecrets as Template["optionalSecrets"],
238
- envVars: metadata.envVars as Template["envVars"],
239
- capabilities: metadata.capabilities as Template["capabilities"],
240
- requires: metadata.requires as Template["requires"],
241
- hooks: metadata.hooks as Template["hooks"],
242
- agentContext: metadata.agentContext as Template["agentContext"],
243
- intent: metadata.intent as Template["intent"],
244
- files,
245
- };
246
- }
247
-
248
- /**
249
- * Fetch user's own project as a template (authenticated)
250
- */
251
- async function fetchUserTemplate(slug: string): Promise<Template | null> {
221
+ async function fetchRemoteTemplate(identifier: string): Promise<Template | null> {
252
222
  const { authFetch } = await import("../lib/auth/index.ts");
253
223
 
254
- const response = await authFetch(
255
- `${getControlApiUrl()}/v1/me/projects/${encodeURIComponent(slug)}/source`,
256
- );
257
-
258
- if (response.status === 404) {
259
- return null; // Not found, will try other sources
224
+ // Route to the correct endpoint based on format
225
+ let url: string;
226
+ if (identifier.includes("/")) {
227
+ const [owner, slug] = identifier.split("/", 2) as [string, string];
228
+ url = `${getControlApiUrl()}/v1/projects/${encodeURIComponent(owner)}/${encodeURIComponent(slug)}/source`;
229
+ } else {
230
+ url = `${getControlApiUrl()}/v1/me/projects/${encodeURIComponent(identifier)}/source`;
260
231
  }
261
232
 
262
- if (!response.ok) {
263
- throw new Error(`Failed to fetch your project: ${response.status}`);
264
- }
233
+ const response = await authFetch(url);
234
+
235
+ if (response.status === 404) return null;
236
+ if (!response.ok) throw new Error(`Failed to fetch template: ${response.status}`);
265
237
 
266
238
  const zipData = await response.arrayBuffer();
267
239
  const metadata = extractMetadataFromZip(zipData);
268
240
  const files = extractZipToFiles(zipData);
269
241
 
270
242
  return {
271
- description: (metadata.description as string) || `Your project: ${slug}`,
243
+ description: (metadata.description as string) || `Fork of ${identifier}`,
272
244
  secrets: (metadata.secrets as string[]) || [],
273
245
  optionalSecrets: metadata.optionalSecrets as Template["optionalSecrets"],
274
246
  envVars: metadata.envVars as Template["envVars"],
@@ -285,33 +257,13 @@ async function fetchUserTemplate(slug: string): Promise<Template | null> {
285
257
  * Resolve template by name
286
258
  */
287
259
  export async function resolveTemplate(template?: string): Promise<Template> {
288
- // No template hello (omakase default)
289
- if (!template) {
290
- return loadTemplate("hello");
291
- }
260
+ if (!template) return loadTemplate("hello");
261
+ if (BUILTIN_TEMPLATES.includes(template)) return loadTemplate(template);
292
262
 
293
- // Built-in template
294
- if (BUILTIN_TEMPLATES.includes(template)) {
295
- return loadTemplate(template);
296
- }
297
-
298
- // username/slug format - fetch from jack cloud
299
- if (template.includes("/")) {
300
- const [username, slug] = template.split("/", 2) as [string, string];
301
- return fetchPublishedTemplate(username, slug);
302
- }
303
-
304
- // Try as user's own project first
305
- try {
306
- const userTemplate = await fetchUserTemplate(template);
307
- if (userTemplate) {
308
- return userTemplate;
309
- }
310
- } catch (_err) {
311
- // If auth fails or project not found, fall through to error
312
- }
263
+ // Remote: "username/slug" or "my-own-project"
264
+ const result = await fetchRemoteTemplate(template);
265
+ if (result) return result;
313
266
 
314
- // Unknown template
315
267
  throw new Error(
316
268
  `Unknown template: ${template}\n\nAvailable built-in templates: ${BUILTIN_TEMPLATES.join(", ")}\nOr use username/slug format for published projects`,
317
269
  );
@@ -568,6 +568,47 @@ if (!baseUrl) {
568
568
 
569
569
  The Host header approach is reliable because Cloudflare always sets it to the actual domain being accessed.
570
570
 
571
+ ## OpenNext (Next.js on Cloudflare) Gotchas
572
+
573
+ Templates using Next.js via `@opennextjs/cloudflare` have platform-specific limitations. These apply to all Next.js templates (`nextjs-auth`, `nextjs-shadcn`, `nextjs-clerk`, etc.).
574
+
575
+ ### Client-side navigation after auth state changes: use window.location, not router.push
576
+
577
+ OpenNext's webpack runtime has an empty chunk URL resolver (`r.u=e=>{}`). This means `router.push()` to a page whose chunks aren't already loaded fails with `ChunkLoadError: Loading chunk X failed`.
578
+
579
+ **Rule:** After any auth state change (sign-in, sign-up, sign-out), always use `window.location.href` for a full page reload. This ensures middleware, server components, and cookies re-evaluate with the new session.
580
+
581
+ ```tsx
582
+ // BAD — ChunkLoadError on OpenNext
583
+ await authClient.signOut();
584
+ router.push("/");
585
+ router.refresh();
586
+
587
+ // GOOD — full reload, clean auth state
588
+ await authClient.signOut();
589
+ window.location.href = "/";
590
+ ```
591
+
592
+ `<Link>` components work fine for normal navigation because Next.js prefetches their chunks via `<script>` tags in the HTML.
593
+
594
+ ### Pages using getCloudflareContext() need `export const dynamic = 'force-dynamic'`
595
+
596
+ Without this, Next.js tries to statically prerender the page at build time, which fails because `getCloudflareContext()` is only available at runtime in the Workers environment.
597
+
598
+ ```tsx
599
+ import { getAuth } from "@/lib/auth";
600
+ export const dynamic = "force-dynamic";
601
+
602
+ export default async function DashboardPage() {
603
+ const auth = await getAuth(); // calls getCloudflareContext() internally
604
+ // ...
605
+ }
606
+ ```
607
+
608
+ ### Edge middleware cannot use Node.js APIs
609
+
610
+ Next.js middleware runs in the edge runtime. Don't call `auth.api.getSession()` (requires `perf_hooks`) — use cookie-only checks instead.
611
+
571
612
  ## Adding New Templates
572
613
 
573
614
  1. Create directory: `templates/my-template/`
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "ai-chat",
3
- "description": "AI chat with streaming UI (free Cloudflare AI)",
3
+ "description": "AI chat with streaming, persistence, and multi-model support",
4
4
  "secrets": [],
5
- "capabilities": ["ai"],
5
+ "capabilities": ["ai", "db"],
6
+ "requires": ["DB"],
6
7
  "intent": {
7
- "keywords": ["ai", "chat", "llm", "llama", "completion", "chatbot"],
8
- "examples": ["AI chatbot", "chat interface", "LLM chat app"]
8
+ "keywords": ["ai", "chat", "llm", "llama", "completion", "chatbot", "streaming"],
9
+ "examples": ["AI chatbot", "chat interface", "LLM chat app", "persistent chat"]
10
+ },
11
+ "agentContext": {
12
+ "summary": "An AI chat app with streaming UI, D1 chat persistence, and multi-model support via AI SDK.",
13
+ "full_text": "## Project Structure\n\n- `src/index.ts` - Hono API with chat endpoints and AI SDK integration\n- `src/jack-ai.ts` - Jack AI proxy wrapper (do not modify)\n- `public/index.html` - React app shell\n- `public/app.js` - Chat UI with useChat hook from AI SDK\n- `schema.sql` - D1 schema for chat and message persistence\n\n## AI Integration\n\nUses AI SDK with `workers-ai-provider` routed through Jack's metered AI proxy.\n\n### Changing Models\n\nEdit the model string in `src/index.ts`:\n```typescript\nmodel: provider('@cf/meta/llama-3.3-70b-instruct-fp8-fast')\n```\n\nAvailable models: https://developers.cloudflare.com/workers-ai/models/\n\n## Chat Persistence\n\nChats and messages are stored in D1:\n- `chats` table: chat metadata (id, created_at)\n- `messages` table: individual messages (role, content, chat_id)\n\nMessages persist across page refreshes. Each browser session creates a new chat.\n\n## API Endpoints\n\n- `POST /api/chat` - Send message and get streaming AI response\n- `GET /api/chat/:id` - Load chat history\n- `POST /api/chat/new` - Create a new chat\n\n## Resources\n\n- [AI SDK Docs](https://sdk.vercel.ai/docs)\n- [Workers AI Models](https://developers.cloudflare.com/workers-ai/models)"
9
14
  },
10
15
  "hooks": {
11
16
  "postDeploy": [
@@ -17,7 +22,7 @@
17
22
  {
18
23
  "action": "box",
19
24
  "title": "{{name}}",
20
- "lines": ["{{url}}", "", "jack open to view in browser"]
25
+ "lines": ["{{url}}", "", "Chat history saved to D1.", "jack open to start chatting"]
21
26
  }
22
27
  ]
23
28
  }
@@ -4,6 +4,11 @@
4
4
  "workspaces": {
5
5
  "": {
6
6
  "name": "jack-template",
7
+ "dependencies": {
8
+ "ai": "^4",
9
+ "hono": "^4.6.0",
10
+ "workers-ai-provider": "^0.7",
11
+ },
7
12
  "devDependencies": {
8
13
  "@cloudflare/workers-types": "^4.20241205.0",
9
14
  "typescript": "^5.0.0",
@@ -11,8 +16,52 @@
11
16
  },
12
17
  },
13
18
  "packages": {
14
- "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260120.0", "", {}, "sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw=="],
19
+ "@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
20
+
21
+ "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
22
+
23
+ "@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
24
+
25
+ "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
26
+
27
+ "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260212.0", "", {}, "sha512-ZK+e8T/2tWBCrE8PoAi9oqTxcBen9Apq+dxbsy1R5LFVdB6M4pY+oP49OFuHTTezrvNXbyvmzbf/vjtrCPGdNg=="],
28
+
29
+ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
30
+
31
+ "@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
32
+
33
+ "ai": ["ai@4.3.19", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q=="],
34
+
35
+ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
36
+
37
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
38
+
39
+ "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
40
+
41
+ "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
42
+
43
+ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
44
+
45
+ "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
46
+
47
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
48
+
49
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
50
+
51
+ "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
52
+
53
+ "swr": ["swr@2.4.0", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw=="],
54
+
55
+ "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
15
56
 
16
57
  "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
58
+
59
+ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
60
+
61
+ "workers-ai-provider": ["workers-ai-provider@0.7.5", "", { "dependencies": { "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8" } }, "sha512-dhCwgc3D65oDDTpH3k8Gf0Ek7KItzvaQidn2N5L5cqLo3WG8GM/4+Nr4rU56o8O3oZRsloB1gUCHYaRv2j7Y0A=="],
62
+
63
+ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
64
+
65
+ "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
17
66
  }
18
67
  }
@@ -7,6 +7,11 @@
7
7
  "dev": "wrangler dev",
8
8
  "deploy": "wrangler deploy"
9
9
  },
10
+ "dependencies": {
11
+ "ai": "^4",
12
+ "hono": "^4.6.0",
13
+ "workers-ai-provider": "^0.7"
14
+ },
10
15
  "devDependencies": {
11
16
  "@cloudflare/workers-types": "^4.20241205.0",
12
17
  "typescript": "^5.0.0"
@@ -0,0 +1,73 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { useChat } from "@ai-sdk/react";
4
+
5
+ const { useState, useEffect, useRef } = React;
6
+
7
+ function App() {
8
+ const [chatId, setChatId] = useState(null);
9
+ const messagesEndRef = useRef(null);
10
+
11
+ // Create a new chat on mount
12
+ useEffect(() => {
13
+ fetch("/api/chat/new", { method: "POST" })
14
+ .then((r) => r.json())
15
+ .then((data) => setChatId(data.id));
16
+ }, []);
17
+
18
+ const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
19
+ api: "/api/chat",
20
+ body: { chatId },
21
+ });
22
+
23
+ // Auto-scroll
24
+ useEffect(() => {
25
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
26
+ }, [messages]);
27
+
28
+ return React.createElement("div", { style: styles.container },
29
+ React.createElement("header", { style: styles.header },
30
+ React.createElement("h1", { style: styles.title }, "AI Chat"),
31
+ ),
32
+ React.createElement("div", { style: styles.messages },
33
+ messages.length === 0 && React.createElement("div", { style: styles.empty }, "Send a message to start chatting"),
34
+ messages.map((m) =>
35
+ React.createElement("div", { key: m.id, style: { ...styles.message, ...(m.role === "user" ? styles.userMessage : styles.assistantMessage) } },
36
+ React.createElement("div", { style: styles.messageRole }, m.role === "user" ? "You" : "AI"),
37
+ React.createElement("div", { style: styles.messageContent }, m.content || (m.parts?.map(p => p.text).join("") || "")),
38
+ )
39
+ ),
40
+ React.createElement("div", { ref: messagesEndRef }),
41
+ ),
42
+ React.createElement("form", { onSubmit: handleSubmit, style: styles.form },
43
+ React.createElement("input", {
44
+ value: input,
45
+ onChange: handleInputChange,
46
+ placeholder: "Type a message...",
47
+ style: styles.input,
48
+ disabled: isLoading,
49
+ }),
50
+ React.createElement("button", { type: "submit", style: styles.button, disabled: isLoading || !input.trim() },
51
+ isLoading ? "..." : "Send"
52
+ ),
53
+ ),
54
+ );
55
+ }
56
+
57
+ const styles = {
58
+ container: { display: "flex", flexDirection: "column", height: "100vh", maxWidth: "800px", margin: "0 auto" },
59
+ header: { padding: "16px 20px", borderBottom: "1px solid #262626" },
60
+ title: { fontSize: "18px", fontWeight: "600" },
61
+ messages: { flex: 1, overflow: "auto", padding: "20px", display: "flex", flexDirection: "column", gap: "16px" },
62
+ empty: { color: "#737373", textAlign: "center", marginTop: "40px" },
63
+ message: { padding: "12px 16px", borderRadius: "12px", maxWidth: "80%" },
64
+ userMessage: { alignSelf: "flex-end", background: "#2563eb", color: "white" },
65
+ assistantMessage: { alignSelf: "flex-start", background: "#1c1c1c", border: "1px solid #262626" },
66
+ messageRole: { fontSize: "11px", fontWeight: "600", marginBottom: "4px", opacity: 0.7, textTransform: "uppercase" },
67
+ messageContent: { fontSize: "14px", lineHeight: "1.5", whiteSpace: "pre-wrap" },
68
+ form: { padding: "16px 20px", borderTop: "1px solid #262626", display: "flex", gap: "8px" },
69
+ input: { flex: 1, padding: "10px 14px", borderRadius: "8px", border: "1px solid #333", background: "#1c1c1c", color: "#e5e5e5", fontSize: "14px", outline: "none" },
70
+ button: { padding: "10px 20px", borderRadius: "8px", border: "none", background: "#2563eb", color: "white", fontSize: "14px", fontWeight: "500", cursor: "pointer" },
71
+ };
72
+
73
+ createRoot(document.getElementById("root")).render(React.createElement(App));
@@ -5,205 +5,22 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>AI Chat</title>
7
7
  <style>
8
- * {
9
- box-sizing: border-box;
10
- margin: 0;
11
- padding: 0;
12
- }
13
-
14
- body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
16
- background: #f5f5f5;
17
- min-height: 100vh;
18
- display: flex;
19
- justify-content: center;
20
- padding: 1rem;
21
- }
22
-
23
- .chat-container {
24
- width: 100%;
25
- max-width: 700px;
26
- display: flex;
27
- flex-direction: column;
28
- height: calc(100vh - 2rem);
29
- }
30
-
31
- header {
32
- text-align: center;
33
- padding: 1rem;
34
- }
35
-
36
- header h1 {
37
- font-size: 1.5rem;
38
- color: #333;
39
- margin-bottom: 0.25rem;
40
- }
41
-
42
- header p {
43
- font-size: 0.875rem;
44
- color: #666;
45
- }
46
-
47
- .messages {
48
- flex: 1;
49
- overflow-y: auto;
50
- padding: 1rem;
51
- background: white;
52
- border-radius: 12px;
53
- margin-bottom: 1rem;
54
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
55
- }
56
-
57
- .message {
58
- padding: 0.75rem 1rem;
59
- margin-bottom: 0.75rem;
60
- border-radius: 12px;
61
- max-width: 85%;
62
- line-height: 1.5;
63
- word-wrap: break-word;
64
- }
65
-
66
- .message.user {
67
- background: #007bff;
68
- color: white;
69
- margin-left: auto;
70
- border-bottom-right-radius: 4px;
71
- }
72
-
73
- .message.assistant {
74
- background: #e9ecef;
75
- color: #333;
76
- margin-right: auto;
77
- border-bottom-left-radius: 4px;
78
- }
79
-
80
- .message.error {
81
- background: #fee;
82
- color: #c00;
83
- border: 1px solid #fcc;
84
- }
85
-
86
- .message.typing {
87
- color: #666;
88
- font-style: italic;
89
- }
90
-
91
- .input-area {
92
- display: flex;
93
- gap: 0.5rem;
94
- background: white;
95
- padding: 1rem;
96
- border-radius: 12px;
97
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
98
- }
99
-
100
- .input-area input {
101
- flex: 1;
102
- padding: 0.75rem 1rem;
103
- border: 1px solid #ddd;
104
- border-radius: 8px;
105
- font-size: 1rem;
106
- outline: none;
107
- transition: border-color 0.2s;
108
- }
109
-
110
- .input-area input:focus {
111
- border-color: #007bff;
112
- }
113
-
114
- .input-area input:disabled {
115
- background: #f5f5f5;
116
- cursor: not-allowed;
117
- }
118
-
119
- .input-area button {
120
- padding: 0.75rem 1.5rem;
121
- background: #007bff;
122
- color: white;
123
- border: none;
124
- border-radius: 8px;
125
- font-size: 1rem;
126
- cursor: pointer;
127
- transition: background 0.2s;
128
- }
129
-
130
- .input-area button:hover:not(:disabled) {
131
- background: #0056b3;
132
- }
133
-
134
- .input-area button:disabled {
135
- background: #ccc;
136
- cursor: not-allowed;
137
- }
138
-
139
- .empty-state {
140
- text-align: center;
141
- color: #999;
142
- padding: 3rem 1rem;
143
- }
144
-
145
- .empty-state p {
146
- font-size: 1.1rem;
147
- margin-bottom: 0.5rem;
148
- }
149
-
150
- .empty-state small {
151
- font-size: 0.875rem;
152
- }
153
-
154
- @media (max-width: 480px) {
155
- body {
156
- padding: 0.5rem;
157
- }
158
-
159
- .chat-container {
160
- height: calc(100vh - 1rem);
161
- }
162
-
163
- header h1 {
164
- font-size: 1.25rem;
165
- }
166
-
167
- .message {
168
- max-width: 90%;
169
- padding: 0.625rem 0.875rem;
170
- }
171
-
172
- .input-area {
173
- padding: 0.75rem;
174
- }
175
-
176
- .input-area button {
177
- padding: 0.75rem 1rem;
178
- }
179
- }
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0a0a; color: #e5e5e5; height: 100vh; }
10
+ #root { height: 100%; }
180
11
  </style>
181
12
  </head>
182
13
  <body>
183
- <div class="chat-container">
184
- <header>
185
- <h1>AI Chat</h1>
186
- <p>Powered by Cloudflare AI (Mistral 7B)</p>
187
- </header>
188
-
189
- <div id="messages" class="messages">
190
- <div class="empty-state">
191
- <p>Start a conversation</p>
192
- <small>Type a message below to begin</small>
193
- </div>
194
- </div>
195
-
196
- <div class="input-area">
197
- <input
198
- type="text"
199
- id="input"
200
- placeholder="Type your message..."
201
- autocomplete="off"
202
- />
203
- <button id="send">Send</button>
204
- </div>
205
- </div>
206
-
207
- <script src="/chat.js"></script>
14
+ <div id="root"></div>
15
+ <script type="importmap">
16
+ {
17
+ "imports": {
18
+ "react": "https://esm.sh/react@19",
19
+ "react-dom/client": "https://esm.sh/react-dom@19/client",
20
+ "@ai-sdk/react": "https://esm.sh/@ai-sdk/react@1?external=react"
21
+ }
22
+ }
23
+ </script>
24
+ <script type="module" src="/app.js"></script>
208
25
  </body>
209
26
  </html>
@@ -0,0 +1,14 @@
1
+ CREATE TABLE IF NOT EXISTS chats (
2
+ id TEXT PRIMARY KEY,
3
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
4
+ );
5
+
6
+ CREATE TABLE IF NOT EXISTS messages (
7
+ id TEXT PRIMARY KEY,
8
+ chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
9
+ role TEXT NOT NULL,
10
+ content TEXT NOT NULL,
11
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
12
+ );
13
+
14
+ CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id);