@getjack/jack 0.1.32 → 0.1.34
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.
- package/package.json +1 -1
- package/src/commands/deploys.ts +95 -0
- package/src/commands/link.ts +8 -0
- package/src/commands/mcp.ts +179 -4
- package/src/commands/rollback.ts +53 -0
- package/src/commands/secrets.ts +3 -1
- package/src/commands/services.ts +11 -1
- package/src/commands/ship.ts +3 -1
- package/src/commands/tokens.ts +16 -1
- package/src/commands/whoami.ts +43 -8
- package/src/index.ts +16 -0
- package/src/lib/agent-files.ts +54 -4
- package/src/lib/agent-integration.ts +4 -166
- package/src/lib/claude-hooks-installer.ts +55 -0
- package/src/lib/control-plane.ts +78 -40
- package/src/lib/crypto.ts +84 -0
- package/src/lib/debug.ts +2 -1
- package/src/lib/deploy-upload.ts +13 -3
- package/src/lib/hooks.ts +4 -3
- package/src/lib/managed-deploy.ts +12 -9
- package/src/lib/project-link.ts +6 -0
- package/src/lib/project-operations.ts +92 -30
- package/src/lib/prompts.ts +2 -2
- package/src/lib/telemetry.ts +2 -0
- package/src/mcp/README.md +1 -1
- package/src/mcp/resources/index.ts +1 -16
- package/src/mcp/server.ts +23 -0
- package/src/mcp/tools/index.ts +133 -17
- package/src/mcp/types.ts +1 -0
- package/src/mcp/utils.ts +2 -1
- package/src/templates/index.ts +25 -73
- package/templates/CLAUDE.md +62 -0
- package/templates/ai-chat/.jack.json +10 -5
- package/templates/ai-chat/bun.lock +50 -1
- package/templates/ai-chat/package.json +5 -0
- package/templates/ai-chat/public/app.js +73 -0
- package/templates/ai-chat/public/index.html +14 -197
- package/templates/ai-chat/schema.sql +14 -0
- package/templates/ai-chat/src/index.ts +86 -102
- package/templates/ai-chat/wrangler.jsonc +8 -1
- package/templates/cron/.jack.json +66 -0
- package/templates/cron/bun.lock +23 -0
- package/templates/cron/package.json +16 -0
- package/templates/cron/schema.sql +24 -0
- package/templates/cron/src/index.ts +117 -0
- package/templates/cron/src/jobs.ts +139 -0
- package/templates/cron/src/webhooks.ts +95 -0
- package/templates/cron/tsconfig.json +17 -0
- package/templates/cron/wrangler.jsonc +11 -0
- package/templates/miniapp/.jack.json +1 -1
- package/templates/nextjs/.jack.json +1 -1
- package/templates/nextjs-auth/.jack.json +44 -0
- package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
- package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
- package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
- package/templates/nextjs-auth/app/error.tsx +44 -0
- package/templates/nextjs-auth/app/globals.css +1 -0
- package/templates/nextjs-auth/app/health/route.ts +3 -0
- package/templates/nextjs-auth/app/layout.tsx +24 -0
- package/templates/nextjs-auth/app/login/page.tsx +10 -0
- package/templates/nextjs-auth/app/page.tsx +86 -0
- package/templates/nextjs-auth/app/signup/page.tsx +10 -0
- package/templates/nextjs-auth/bun.lock +1065 -0
- package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
- package/templates/nextjs-auth/components/auth-form.tsx +191 -0
- package/templates/nextjs-auth/components/header.tsx +50 -0
- package/templates/nextjs-auth/components/user-menu.tsx +23 -0
- package/templates/nextjs-auth/lib/auth-client.ts +3 -0
- package/templates/nextjs-auth/lib/auth.ts +43 -0
- package/templates/nextjs-auth/lib/utils.ts +6 -0
- package/templates/nextjs-auth/middleware.ts +33 -0
- package/templates/nextjs-auth/next.config.ts +8 -0
- package/templates/nextjs-auth/open-next.config.ts +6 -0
- package/templates/nextjs-auth/package.json +33 -0
- package/templates/nextjs-auth/postcss.config.mjs +8 -0
- package/templates/nextjs-auth/schema.sql +49 -0
- package/templates/nextjs-auth/tsconfig.json +28 -0
- package/templates/nextjs-auth/wrangler.jsonc +23 -0
- package/templates/nextjs-clerk/.jack.json +54 -0
- package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
- package/templates/nextjs-clerk/app/globals.css +1 -0
- package/templates/nextjs-clerk/app/health/route.ts +3 -0
- package/templates/nextjs-clerk/app/layout.tsx +28 -0
- package/templates/nextjs-clerk/app/page.tsx +86 -0
- package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/templates/nextjs-clerk/bun.lock +1055 -0
- package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
- package/templates/nextjs-clerk/components/header.tsx +40 -0
- package/templates/nextjs-clerk/lib/utils.ts +6 -0
- package/templates/nextjs-clerk/middleware.ts +18 -0
- package/templates/nextjs-clerk/next.config.ts +8 -0
- package/templates/nextjs-clerk/open-next.config.ts +6 -0
- package/templates/nextjs-clerk/package.json +31 -0
- package/templates/nextjs-clerk/postcss.config.mjs +8 -0
- package/templates/nextjs-clerk/tsconfig.json +28 -0
- package/templates/nextjs-clerk/wrangler.jsonc +17 -0
- package/templates/nextjs-shadcn/.jack.json +34 -0
- package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
- package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
- package/templates/nextjs-shadcn/app/globals.css +126 -0
- package/templates/nextjs-shadcn/app/health/route.ts +3 -0
- package/templates/nextjs-shadcn/app/layout.tsx +24 -0
- package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
- package/templates/nextjs-shadcn/app/page.tsx +180 -0
- package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
- package/templates/nextjs-shadcn/bun.lock +1789 -0
- package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
- package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
- package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
- package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
- package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
- package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
- package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
- package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
- package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
- package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
- package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
- package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
- package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
- package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
- package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
- package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
- package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
- package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
- package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
- package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
- package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
- package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
- package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
- package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
- package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
- package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
- package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
- package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
- package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
- package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
- package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
- package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
- package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
- package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
- package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
- package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
- package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
- package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
- package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
- package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
- package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
- package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
- package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
- package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
- package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
- package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
- package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
- package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-shadcn/components.json +23 -0
- package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-shadcn/lib/utils.ts +6 -0
- package/templates/nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/nextjs-shadcn/next.config.ts +8 -0
- package/templates/nextjs-shadcn/open-next.config.ts +6 -0
- package/templates/nextjs-shadcn/package.json +55 -0
- package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
- package/templates/nextjs-shadcn/tsconfig.json +28 -0
- package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
- package/templates/resend/.jack.json +64 -0
- package/templates/resend/bun.lock +23 -0
- package/templates/resend/package.json +16 -0
- package/templates/resend/schema.sql +13 -0
- package/templates/resend/src/email.ts +165 -0
- package/templates/resend/src/index.ts +108 -0
- package/templates/resend/tsconfig.json +17 -0
- package/templates/resend/wrangler.jsonc +11 -0
- package/templates/saas/.jack.json +1 -1
- package/templates/ai-chat/public/chat.js +0 -149
|
@@ -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
|
-
|
|
10
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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);
|
|
@@ -1,49 +1,32 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { streamText, convertToCoreMessages, type Message } from "ai";
|
|
3
|
+
import { createWorkersAI } from "workers-ai-provider";
|
|
1
4
|
import { createJackAI } from "./jack-ai";
|
|
2
5
|
|
|
3
6
|
interface Env {
|
|
4
|
-
// Direct AI binding (for local dev with wrangler)
|
|
5
7
|
AI?: Ai;
|
|
6
|
-
// Jack proxy bindings (injected in jack cloud)
|
|
7
8
|
__AI_PROXY?: Fetcher;
|
|
8
9
|
__JACK_PROJECT_ID?: string;
|
|
9
10
|
__JACK_ORG_ID?: string;
|
|
10
|
-
// Assets binding
|
|
11
11
|
ASSETS: Fetcher;
|
|
12
|
+
DB: D1Database;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function getAI(env: Env) {
|
|
15
|
-
// Prefer jack cloud proxy if available (for metering)
|
|
16
16
|
if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
|
|
17
17
|
return createJackAI(
|
|
18
|
-
env as Required<
|
|
18
|
+
env as Required<
|
|
19
|
+
Pick<Env, "__AI_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">
|
|
20
|
+
>,
|
|
19
21
|
);
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
if (env.AI) {
|
|
23
|
-
return env.AI;
|
|
24
|
-
}
|
|
23
|
+
if (env.AI) return env.AI;
|
|
25
24
|
throw new Error("No AI binding available");
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
role: "user" | "assistant" | "system";
|
|
30
|
-
content: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// System prompt - customize this to change the AI's personality
|
|
34
|
-
const SYSTEM_PROMPT = `You are a helpful AI assistant built with jack (getjack.sh).
|
|
35
|
-
|
|
36
|
-
jack helps developers ship ideas fast - from "what if" to a live URL in seconds. You're running on Cloudflare's edge network, close to users worldwide.
|
|
37
|
-
|
|
38
|
-
Be concise, friendly, and helpful. If asked about jack:
|
|
39
|
-
- jack new creates projects from templates
|
|
40
|
-
- jack ship deploys to production
|
|
41
|
-
- jack open opens your app in browser
|
|
42
|
-
- Docs: https://docs.getjack.sh
|
|
27
|
+
const SYSTEM_PROMPT = `You are a helpful AI assistant. Be concise, friendly, and helpful. Keep responses short unless detail is needed.`;
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Rate limiting: 10 requests per minute per IP
|
|
29
|
+
// Rate limiting
|
|
47
30
|
const RATE_LIMIT = 10;
|
|
48
31
|
const RATE_WINDOW_MS = 60_000;
|
|
49
32
|
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
|
@@ -51,91 +34,92 @@ const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
|
|
51
34
|
function checkRateLimit(ip: string): boolean {
|
|
52
35
|
const now = Date.now();
|
|
53
36
|
const entry = rateLimitMap.get(ip);
|
|
54
|
-
|
|
55
37
|
if (!entry || now >= entry.resetAt) {
|
|
56
38
|
rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
|
|
57
39
|
return true;
|
|
58
40
|
}
|
|
59
|
-
|
|
60
|
-
if (entry.count >= RATE_LIMIT) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
41
|
+
if (entry.count >= RATE_LIMIT) return false;
|
|
64
42
|
entry.count++;
|
|
65
43
|
return true;
|
|
66
44
|
}
|
|
67
45
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
46
|
+
const app = new Hono<{ Bindings: Env }>();
|
|
47
|
+
|
|
48
|
+
// Create new chat
|
|
49
|
+
app.post("/api/chat/new", async (c) => {
|
|
50
|
+
const id = crypto.randomUUID();
|
|
51
|
+
await c.env.DB.prepare("INSERT INTO chats (id) VALUES (?)").bind(id).run();
|
|
52
|
+
return c.json({ id });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Load chat history
|
|
56
|
+
app.get("/api/chat/:id", async (c) => {
|
|
57
|
+
const chatId = c.req.param("id");
|
|
58
|
+
const { results } = await c.env.DB.prepare(
|
|
59
|
+
"SELECT id, role, content, created_at FROM messages WHERE chat_id = ? ORDER BY created_at ASC",
|
|
60
|
+
)
|
|
61
|
+
.bind(chatId)
|
|
62
|
+
.all();
|
|
63
|
+
return c.json({ messages: results || [] });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Chat endpoint with streaming
|
|
67
|
+
app.post("/api/chat", async (c) => {
|
|
68
|
+
const ip = c.req.header("cf-connecting-ip") || "unknown";
|
|
69
|
+
if (!checkRateLimit(ip)) {
|
|
70
|
+
return c.json({ error: "Too many requests. Please wait a moment." }, 429);
|
|
75
71
|
}
|
|
76
|
-
}
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// POST /api/chat - Streaming chat endpoint
|
|
88
|
-
if (request.method === "POST" && url.pathname === "/api/chat") {
|
|
89
|
-
const ip = request.headers.get("cf-connecting-ip") || "unknown";
|
|
90
|
-
|
|
91
|
-
// Check rate limit
|
|
92
|
-
if (!checkRateLimit(ip)) {
|
|
93
|
-
// Cleanup old entries occasionally
|
|
94
|
-
cleanupRateLimitMap();
|
|
95
|
-
return Response.json(
|
|
96
|
-
{ error: "Too many requests. Please wait a moment and try again." },
|
|
97
|
-
{ status: 429 },
|
|
98
|
-
);
|
|
99
|
-
}
|
|
73
|
+
const { messages, chatId } = await c.req.json<{
|
|
74
|
+
messages: Message[];
|
|
75
|
+
chatId?: string;
|
|
76
|
+
}>();
|
|
77
|
+
if (!messages || !Array.isArray(messages)) {
|
|
78
|
+
return c.json({ error: "Invalid request." }, 400);
|
|
79
|
+
}
|
|
100
80
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error("Chat error:", err);
|
|
135
|
-
return Response.json({ error: "Something went wrong. Please try again." }, { status: 500 });
|
|
81
|
+
// Save user message to DB if we have a chatId
|
|
82
|
+
const lastUserMsg = messages.findLast((m: Message) => m.role === "user");
|
|
83
|
+
if (chatId && lastUserMsg) {
|
|
84
|
+
await c.env.DB.prepare(
|
|
85
|
+
"INSERT INTO messages (id, chat_id, role, content) VALUES (?, ?, ?, ?)",
|
|
86
|
+
)
|
|
87
|
+
.bind(
|
|
88
|
+
crypto.randomUUID(),
|
|
89
|
+
chatId,
|
|
90
|
+
"user",
|
|
91
|
+
typeof lastUserMsg.content === "string"
|
|
92
|
+
? lastUserMsg.content
|
|
93
|
+
: JSON.stringify(lastUserMsg.content),
|
|
94
|
+
)
|
|
95
|
+
.run();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ai = getAI(c.env);
|
|
99
|
+
const provider = createWorkersAI({ binding: ai as Ai });
|
|
100
|
+
|
|
101
|
+
const result = streamText({
|
|
102
|
+
model: provider("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
|
|
103
|
+
system: SYSTEM_PROMPT,
|
|
104
|
+
messages: convertToCoreMessages(messages),
|
|
105
|
+
onFinish: async ({ text }) => {
|
|
106
|
+
// Save assistant response to DB
|
|
107
|
+
if (chatId && text) {
|
|
108
|
+
await c.env.DB.prepare(
|
|
109
|
+
"INSERT INTO messages (id, chat_id, role, content) VALUES (?, ?, ?, ?)",
|
|
110
|
+
)
|
|
111
|
+
.bind(crypto.randomUUID(), chatId, "assistant", text)
|
|
112
|
+
.run();
|
|
136
113
|
}
|
|
137
|
-
}
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return result.toDataStreamResponse();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Serve static assets for non-API routes
|
|
121
|
+
app.get("*", async (c) => {
|
|
122
|
+
return c.env.ASSETS.fetch(c.req.raw);
|
|
123
|
+
});
|
|
138
124
|
|
|
139
|
-
|
|
140
|
-
},
|
|
141
|
-
};
|
|
125
|
+
export default app;
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
"name": "jack-template",
|
|
3
3
|
"main": "src/index.ts",
|
|
4
4
|
"compatibility_date": "2024-12-01",
|
|
5
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
5
6
|
"ai": {
|
|
6
7
|
"binding": "AI"
|
|
7
8
|
},
|
|
8
9
|
"assets": {
|
|
9
10
|
"directory": "public",
|
|
10
11
|
"binding": "ASSETS"
|
|
11
|
-
}
|
|
12
|
+
},
|
|
13
|
+
"d1_databases": [
|
|
14
|
+
{
|
|
15
|
+
"binding": "DB",
|
|
16
|
+
"database_name": "jack-template-db"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
12
19
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cron",
|
|
3
|
+
"description": "Background tasks with cron scheduling and webhook ingestion",
|
|
4
|
+
"secrets": [],
|
|
5
|
+
"capabilities": ["db"],
|
|
6
|
+
"requires": ["DB", "CRON"],
|
|
7
|
+
"intent": {
|
|
8
|
+
"keywords": [
|
|
9
|
+
"cron",
|
|
10
|
+
"background",
|
|
11
|
+
"jobs",
|
|
12
|
+
"webhook",
|
|
13
|
+
"scheduled",
|
|
14
|
+
"queue",
|
|
15
|
+
"worker",
|
|
16
|
+
"tasks"
|
|
17
|
+
],
|
|
18
|
+
"examples": [
|
|
19
|
+
"background job processor",
|
|
20
|
+
"scheduled tasks",
|
|
21
|
+
"webhook handler",
|
|
22
|
+
"job queue"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"agentContext": {
|
|
26
|
+
"summary": "A background task worker with cron scheduling, D1 job queue, and webhook ingestion.",
|
|
27
|
+
"full_text": "## Project Structure\n\n- `src/index.ts` - Hono API with cron handler, webhook endpoint, and job status routes\n- `src/jobs.ts` - Job queue: create, process, retry with D1 backend\n- `src/webhooks.ts` - Webhook ingestion with HMAC-SHA256 signature verification\n- `schema.sql` - D1 schema (jobs, webhook_events)\n\n## Cron\n\nThe `POST /__scheduled` route runs on a cron schedule (default: every 5 minutes). It processes pending jobs and retries failed ones.\n\n## Jobs\n\n```typescript\n// Create a job\nawait createJob(db, { type: 'process-order', payload: { orderId: '123' } });\n\n// Jobs are processed automatically by cron\n// Failed jobs retry up to 3 times with exponential backoff\n```\n\n## Webhooks\n\n```\nPOST /webhook\nX-Signature: sha256=abc123...\nContent-Type: application/json\n\n{ \"event\": \"order.completed\", \"data\": { ... } }\n```\n\nWebhooks are verified using HMAC-SHA256, logged to D1, and can create jobs for async processing.\n\n## Endpoints\n\n- `POST /__scheduled` - Cron handler (called by scheduler)\n- `POST /webhook` - Inbound webhook receiver with signature verification\n- `GET /jobs` - List recent jobs with status\n- `GET /health` - Health check\n\n## Environment Variables\n\n- `WEBHOOK_SECRET` - HMAC signing secret for webhook verification (auto-generated)\n\n## Resources\n\n- [Hono Documentation](https://hono.dev)"
|
|
28
|
+
},
|
|
29
|
+
"hooks": {
|
|
30
|
+
"preCreate": [
|
|
31
|
+
{
|
|
32
|
+
"action": "require",
|
|
33
|
+
"source": "secret",
|
|
34
|
+
"key": "WEBHOOK_SECRET",
|
|
35
|
+
"message": "Generating webhook signing secret...",
|
|
36
|
+
"onMissing": "generate",
|
|
37
|
+
"generateCommand": "openssl rand -hex 32"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"postDeploy": [
|
|
41
|
+
{
|
|
42
|
+
"action": "clipboard",
|
|
43
|
+
"text": "{{url}}",
|
|
44
|
+
"message": "Deploy URL copied to clipboard"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"action": "shell",
|
|
48
|
+
"command": "curl -s {{url}}/health | head -c 200",
|
|
49
|
+
"message": "Testing worker health..."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"action": "box",
|
|
53
|
+
"title": "Background worker live: {{name}}",
|
|
54
|
+
"lines": [
|
|
55
|
+
"{{url}}",
|
|
56
|
+
"",
|
|
57
|
+
"Endpoints:",
|
|
58
|
+
" POST {{url}}/webhook — inbound webhook receiver",
|
|
59
|
+
" GET {{url}}/jobs — job queue status",
|
|
60
|
+
"",
|
|
61
|
+
"Cron: runs every 5 minutes"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "jack-template",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"hono": "^4.6.0",
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
12
|
+
"typescript": "^5.0.0",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"packages": {
|
|
17
|
+
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260212.0", "", {}, "sha512-ZK+e8T/2tWBCrE8PoAi9oqTxcBen9Apq+dxbsy1R5LFVdB6M4pY+oP49OFuHTTezrvNXbyvmzbf/vjtrCPGdNg=="],
|
|
18
|
+
|
|
19
|
+
"hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
|
|
20
|
+
|
|
21
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jack-template",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "wrangler dev",
|
|
7
|
+
"deploy": "wrangler deploy"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"hono": "^4.6.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
14
|
+
"typescript": "^5.0.0"
|
|
15
|
+
}
|
|
16
|
+
}
|