@getjack/jack 0.1.34 → 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 (88) 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/services.ts +4 -2
  8. package/src/commands/sync.ts +5 -6
  9. package/src/lib/auth/client.ts +5 -2
  10. package/src/lib/binding-validator.ts +39 -3
  11. package/src/lib/build-helper.ts +18 -19
  12. package/src/lib/control-plane.ts +1 -0
  13. package/src/lib/do-config.ts +110 -0
  14. package/src/lib/do-export-validator.ts +26 -0
  15. package/src/lib/jsonc-edit.ts +292 -0
  16. package/src/lib/managed-deploy.ts +36 -1
  17. package/src/lib/project-link.ts +37 -0
  18. package/src/lib/project-operations.ts +13 -38
  19. package/src/lib/resources.ts +4 -5
  20. package/src/lib/schema.ts +8 -12
  21. package/src/lib/services/db-create.ts +2 -2
  22. package/src/lib/services/db-execute.ts +9 -6
  23. package/src/lib/services/db-list.ts +6 -4
  24. package/src/lib/services/endpoint-test.ts +275 -0
  25. package/src/lib/services/project-delete.ts +190 -0
  26. package/src/lib/services/project-environment.ts +457 -0
  27. package/src/lib/services/storage-config.ts +7 -309
  28. package/src/lib/services/storage-create.ts +2 -1
  29. package/src/lib/services/storage-delete.ts +3 -2
  30. package/src/lib/services/storage-info.ts +2 -1
  31. package/src/lib/services/storage-list.ts +6 -3
  32. package/src/lib/services/vectorize-config.ts +7 -264
  33. package/src/lib/services/vectorize-create.ts +2 -1
  34. package/src/lib/services/vectorize-delete.ts +6 -4
  35. package/src/lib/services/vectorize-list.ts +6 -3
  36. package/src/lib/storage/index.ts +21 -23
  37. package/src/lib/telemetry.ts +1 -0
  38. package/src/lib/wrangler-config.ts +43 -312
  39. package/src/lib/zip-packager.ts +28 -0
  40. package/src/mcp/test-utils.ts +31 -0
  41. package/src/mcp/tools/index.ts +271 -0
  42. package/src/templates/index.ts +5 -0
  43. package/src/templates/types.ts +4 -0
  44. package/templates/AI-BINDINGS.md +34 -76
  45. package/templates/CLAUDE.md +1 -1
  46. package/templates/ai-chat/src/index.ts +7 -14
  47. package/templates/ai-chat/src/jack-ai.ts +0 -6
  48. package/templates/chat/.jack.json +45 -0
  49. package/templates/chat/bun.lock +1588 -0
  50. package/templates/chat/components.json +23 -0
  51. package/templates/chat/index.html +12 -0
  52. package/templates/chat/package.json +41 -0
  53. package/templates/chat/src/chat-agent.ts +61 -0
  54. package/templates/chat/src/client/app.tsx +189 -0
  55. package/templates/chat/src/client/chat.tsx +222 -0
  56. package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
  57. package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
  58. package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
  59. package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
  60. package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
  61. package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
  62. package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
  63. package/templates/chat/src/client/components/ui/button.tsx +38 -0
  64. package/templates/chat/src/client/lib/utils.ts +6 -0
  65. package/templates/chat/src/client/main.tsx +11 -0
  66. package/templates/chat/src/client/styles.css +125 -0
  67. package/templates/chat/src/index.ts +25 -0
  68. package/templates/chat/src/jack-ai.ts +94 -0
  69. package/templates/chat/tsconfig.json +18 -0
  70. package/templates/chat/vite.config.ts +14 -0
  71. package/templates/chat/wrangler.jsonc +18 -0
  72. package/templates/cron/.jack.json +18 -28
  73. package/templates/cron/schema.sql +10 -20
  74. package/templates/cron/src/admin.ts +321 -0
  75. package/templates/cron/src/index.ts +151 -81
  76. package/templates/cron/src/monitor.ts +124 -0
  77. package/templates/semantic-search/src/index.ts +5 -43
  78. package/templates/semantic-search/src/jack-ai.ts +0 -6
  79. package/templates/telegram-bot/.jack.json +56 -0
  80. package/templates/telegram-bot/bun.lock +41 -0
  81. package/templates/telegram-bot/package.json +16 -0
  82. package/templates/telegram-bot/src/index.ts +236 -0
  83. package/templates/telegram-bot/src/jack-ai.ts +100 -0
  84. package/templates/telegram-bot/tsconfig.json +11 -0
  85. package/templates/telegram-bot/wrangler.jsonc +8 -0
  86. package/templates/cron/src/jobs.ts +0 -139
  87. package/templates/cron/src/webhooks.ts +0 -95
  88. package/templates/semantic-search/src/jack-vectorize.ts +0 -169
@@ -0,0 +1,84 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { memo, useId, useMemo } from "react";
3
+ import ReactMarkdown, { type Components } from "react-markdown";
4
+ import remarkBreaks from "remark-breaks";
5
+ import remarkGfm from "remark-gfm";
6
+
7
+ function extractLanguage(className?: string): string {
8
+ if (!className) return "plaintext";
9
+ const match = className.match(/language-(\w+)/);
10
+ return match ? match[1] : "plaintext";
11
+ }
12
+
13
+ const components: Partial<Components> = {
14
+ code: function CodeComponent({ className, children, ...props }) {
15
+ const isInline =
16
+ !props.node?.position?.start.line ||
17
+ props.node?.position?.start.line === props.node?.position?.end.line;
18
+
19
+ if (isInline) {
20
+ return (
21
+ <code
22
+ className={cn("rounded bg-secondary px-1.5 py-0.5 font-mono text-[0.85em]", className)}
23
+ {...props}
24
+ >
25
+ {children}
26
+ </code>
27
+ );
28
+ }
29
+
30
+ const language = extractLanguage(className);
31
+
32
+ return (
33
+ <div className="not-prose my-3 overflow-clip rounded-lg border border-border bg-card">
34
+ {language !== "plaintext" && (
35
+ <div className="flex items-center justify-between border-b border-border bg-secondary/50 px-3 py-1.5">
36
+ <span className="text-xs text-muted-foreground">{language}</span>
37
+ </div>
38
+ )}
39
+ <pre className="overflow-x-auto p-3">
40
+ <code className="text-[13px] leading-relaxed">{children}</code>
41
+ </pre>
42
+ </div>
43
+ );
44
+ },
45
+ pre: function PreComponent({ children }) {
46
+ return <>{children}</>;
47
+ },
48
+ };
49
+
50
+ const MemoizedMarkdownBlock = memo(
51
+ function MarkdownBlock({ content }: { content: string }) {
52
+ return (
53
+ <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} components={components}>
54
+ {content}
55
+ </ReactMarkdown>
56
+ );
57
+ },
58
+ (prev, next) => prev.content === next.content,
59
+ );
60
+ MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock";
61
+
62
+ function MarkdownComponent({
63
+ children,
64
+ id,
65
+ className,
66
+ }: { children: string; id?: string; className?: string }) {
67
+ const generatedId = useId();
68
+ const blockId = id ?? generatedId;
69
+ const blocks = useMemo(() => children.split(/\n\n+/), [children]);
70
+
71
+ return (
72
+ <div className={className}>
73
+ {blocks.map((block, index) => (
74
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable blockId prefix makes this safe
75
+ <MemoizedMarkdownBlock key={`${blockId}-${index}`} content={block} />
76
+ ))}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ const Markdown = memo(MarkdownComponent);
82
+ Markdown.displayName = "Markdown";
83
+
84
+ export { Markdown };
@@ -0,0 +1,54 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { Markdown } from "./markdown";
3
+
4
+ function Message({
5
+ children,
6
+ className,
7
+ ...props
8
+ }: { children: React.ReactNode; className?: string } & React.HTMLAttributes<HTMLDivElement>) {
9
+ return (
10
+ <div className={cn("flex gap-3", className)} {...props}>
11
+ {children}
12
+ </div>
13
+ );
14
+ }
15
+
16
+ function MessageAvatar({ initials, className }: { initials: string; className?: string }) {
17
+ return (
18
+ <div
19
+ className={cn(
20
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground",
21
+ className,
22
+ )}
23
+ >
24
+ {initials}
25
+ </div>
26
+ );
27
+ }
28
+
29
+ function MessageContent({
30
+ children,
31
+ markdown = false,
32
+ className,
33
+ ...props
34
+ }: {
35
+ children: React.ReactNode;
36
+ markdown?: boolean;
37
+ className?: string;
38
+ } & React.HTMLAttributes<HTMLDivElement>) {
39
+ if (markdown) {
40
+ return (
41
+ <div className={cn("prose prose-sm dark:prose-invert min-w-0", className)} {...props}>
42
+ <Markdown>{children as string}</Markdown>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <div className={cn("text-sm", className)} {...props}>
49
+ {children}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export { Message, MessageAvatar, MessageContent };
@@ -0,0 +1,20 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ function PromptSuggestion({
5
+ children,
6
+ className,
7
+ ...props
8
+ }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
9
+ return (
10
+ <Button
11
+ variant="outline"
12
+ className={cn("h-auto rounded-full px-4 py-2 text-sm font-normal", className)}
13
+ {...props}
14
+ >
15
+ {children}
16
+ </Button>
17
+ );
18
+ }
19
+
20
+ export { PromptSuggestion };
@@ -0,0 +1,134 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { ChevronDown } from "lucide-react";
3
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
4
+ import { Markdown } from "./markdown";
5
+
6
+ type ReasoningContextType = {
7
+ isOpen: boolean;
8
+ onOpenChange: (open: boolean) => void;
9
+ };
10
+
11
+ const ReasoningContext = createContext<ReasoningContextType | undefined>(undefined);
12
+
13
+ function useReasoningContext() {
14
+ const context = useContext(ReasoningContext);
15
+ if (!context) {
16
+ throw new Error("useReasoningContext must be used within a Reasoning provider");
17
+ }
18
+ return context;
19
+ }
20
+
21
+ function Reasoning({
22
+ children,
23
+ className,
24
+ open,
25
+ onOpenChange,
26
+ isStreaming,
27
+ }: {
28
+ children: React.ReactNode;
29
+ className?: string;
30
+ open?: boolean;
31
+ onOpenChange?: (open: boolean) => void;
32
+ isStreaming?: boolean;
33
+ }) {
34
+ const [internalOpen, setInternalOpen] = useState(false);
35
+ const [wasAutoOpened, setWasAutoOpened] = useState(false);
36
+
37
+ const isControlled = open !== undefined;
38
+ const isOpen = isControlled ? open : internalOpen;
39
+
40
+ const handleOpenChange = (newOpen: boolean) => {
41
+ if (!isControlled) {
42
+ setInternalOpen(newOpen);
43
+ }
44
+ onOpenChange?.(newOpen);
45
+ };
46
+
47
+ useEffect(() => {
48
+ if (isStreaming && !wasAutoOpened) {
49
+ if (!isControlled) setInternalOpen(true);
50
+ setWasAutoOpened(true);
51
+ }
52
+
53
+ if (!isStreaming && wasAutoOpened) {
54
+ if (!isControlled) setInternalOpen(false);
55
+ setWasAutoOpened(false);
56
+ }
57
+ }, [isStreaming, wasAutoOpened, isControlled]);
58
+
59
+ return (
60
+ <ReasoningContext.Provider value={{ isOpen, onOpenChange: handleOpenChange }}>
61
+ <div className={className}>{children}</div>
62
+ </ReasoningContext.Provider>
63
+ );
64
+ }
65
+
66
+ function ReasoningTrigger({
67
+ children,
68
+ className,
69
+ ...props
70
+ }: { children: React.ReactNode; className?: string } & React.HTMLAttributes<HTMLButtonElement>) {
71
+ const { isOpen, onOpenChange } = useReasoningContext();
72
+
73
+ return (
74
+ <button
75
+ type="button"
76
+ className={cn("flex cursor-pointer items-center gap-1.5 text-xs", className)}
77
+ onClick={() => onOpenChange(!isOpen)}
78
+ {...props}
79
+ >
80
+ <span className="text-muted-foreground">{children}</span>
81
+ <ChevronDown
82
+ className={cn("h-3 w-3 text-muted-foreground transition-transform", isOpen && "rotate-180")}
83
+ />
84
+ </button>
85
+ );
86
+ }
87
+
88
+ function ReasoningContent({
89
+ children,
90
+ className,
91
+ markdown = false,
92
+ ...props
93
+ }: {
94
+ children: React.ReactNode;
95
+ className?: string;
96
+ markdown?: boolean;
97
+ } & React.HTMLAttributes<HTMLDivElement>) {
98
+ const contentRef = useRef<HTMLDivElement>(null);
99
+ const innerRef = useRef<HTMLDivElement>(null);
100
+ const { isOpen } = useReasoningContext();
101
+
102
+ useEffect(() => {
103
+ if (!contentRef.current || !innerRef.current) return;
104
+
105
+ const observer = new ResizeObserver(() => {
106
+ if (contentRef.current && innerRef.current && isOpen) {
107
+ contentRef.current.style.maxHeight = `${innerRef.current.scrollHeight}px`;
108
+ }
109
+ });
110
+
111
+ observer.observe(innerRef.current);
112
+
113
+ if (isOpen) {
114
+ contentRef.current.style.maxHeight = `${innerRef.current.scrollHeight}px`;
115
+ }
116
+
117
+ return () => observer.disconnect();
118
+ }, [isOpen]);
119
+
120
+ return (
121
+ <div
122
+ ref={contentRef}
123
+ className={cn("overflow-hidden transition-[max-height] duration-150 ease-out", className)}
124
+ style={{ maxHeight: isOpen ? contentRef.current?.scrollHeight : "0px" }}
125
+ {...props}
126
+ >
127
+ <div ref={innerRef} className="prose prose-sm dark:prose-invert text-muted-foreground">
128
+ {markdown ? <Markdown>{children as string}</Markdown> : children}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ export { Reasoning, ReasoningTrigger, ReasoningContent };
@@ -0,0 +1,28 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { cn } from "@/lib/utils";
3
+ import { ChevronDown } from "lucide-react";
4
+ import { useStickToBottomContext } from "use-stick-to-bottom";
5
+
6
+ function ScrollButton({ className, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
7
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
8
+
9
+ return (
10
+ <Button
11
+ variant="outline"
12
+ size="icon"
13
+ className={cn(
14
+ "h-8 w-8 rounded-full transition-all duration-150 ease-out",
15
+ !isAtBottom
16
+ ? "translate-y-0 scale-100 opacity-100"
17
+ : "pointer-events-none translate-y-4 scale-95 opacity-0",
18
+ className,
19
+ )}
20
+ onClick={() => scrollToBottom()}
21
+ {...props}
22
+ >
23
+ <ChevronDown className="h-4 w-4" />
24
+ </Button>
25
+ );
26
+ }
27
+
28
+ export { ScrollButton };
@@ -0,0 +1,38 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { type VariantProps, cva } from "class-variance-authority";
3
+ import { Slot } from "radix-ui";
4
+
5
+ const buttonVariants = cva(
6
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
11
+ outline:
12
+ "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
13
+ secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
14
+ ghost: "hover:bg-accent hover:text-accent-foreground",
15
+ },
16
+ size: {
17
+ default: "h-9 px-4 py-2",
18
+ sm: "h-8 rounded-md px-3",
19
+ lg: "h-10 rounded-md px-6",
20
+ icon: "size-9",
21
+ },
22
+ },
23
+ defaultVariants: { variant: "default", size: "default" },
24
+ },
25
+ );
26
+
27
+ function Button({
28
+ className,
29
+ variant,
30
+ size,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
34
+ const Comp = asChild ? Slot.Root : "button";
35
+ return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
36
+ }
37
+
38
+ export { Button, buttonVariants };
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,11 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./app";
4
+ import "./styles.css";
5
+
6
+ // biome-ignore lint/style/noNonNullAssertion: root element always exists in index.html
7
+ createRoot(document.getElementById("root")!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>,
11
+ );
@@ -0,0 +1,125 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+ @plugin "@tailwindcss/typography";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ @theme inline {
9
+ --radius-sm: calc(var(--radius) - 4px);
10
+ --radius-md: calc(var(--radius) - 2px);
11
+ --radius-lg: var(--radius);
12
+ --radius-xl: calc(var(--radius) + 4px);
13
+ --radius-2xl: calc(var(--radius) + 8px);
14
+ --radius-3xl: calc(var(--radius) + 12px);
15
+ --radius-4xl: calc(var(--radius) + 16px);
16
+ --color-background: var(--background);
17
+ --color-foreground: var(--foreground);
18
+ --color-card: var(--card);
19
+ --color-card-foreground: var(--card-foreground);
20
+ --color-popover: var(--popover);
21
+ --color-popover-foreground: var(--popover-foreground);
22
+ --color-primary: var(--primary);
23
+ --color-primary-foreground: var(--primary-foreground);
24
+ --color-secondary: var(--secondary);
25
+ --color-secondary-foreground: var(--secondary-foreground);
26
+ --color-muted: var(--muted);
27
+ --color-muted-foreground: var(--muted-foreground);
28
+ --color-accent: var(--accent);
29
+ --color-accent-foreground: var(--accent-foreground);
30
+ --color-destructive: var(--destructive);
31
+ --color-border: var(--border);
32
+ --color-input: var(--input);
33
+ --color-ring: var(--ring);
34
+ --color-chart-1: var(--chart-1);
35
+ --color-chart-2: var(--chart-2);
36
+ --color-chart-3: var(--chart-3);
37
+ --color-chart-4: var(--chart-4);
38
+ --color-chart-5: var(--chart-5);
39
+ --color-sidebar: var(--sidebar);
40
+ --color-sidebar-foreground: var(--sidebar-foreground);
41
+ --color-sidebar-primary: var(--sidebar-primary);
42
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
43
+ --color-sidebar-accent: var(--sidebar-accent);
44
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
45
+ --color-sidebar-border: var(--sidebar-border);
46
+ --color-sidebar-ring: var(--sidebar-ring);
47
+ }
48
+
49
+ :root {
50
+ --radius: 0.625rem;
51
+ --background: oklch(1 0 0);
52
+ --foreground: oklch(0.145 0 0);
53
+ --card: oklch(1 0 0);
54
+ --card-foreground: oklch(0.145 0 0);
55
+ --popover: oklch(1 0 0);
56
+ --popover-foreground: oklch(0.145 0 0);
57
+ --primary: oklch(0.205 0 0);
58
+ --primary-foreground: oklch(0.985 0 0);
59
+ --secondary: oklch(0.97 0 0);
60
+ --secondary-foreground: oklch(0.205 0 0);
61
+ --muted: oklch(0.97 0 0);
62
+ --muted-foreground: oklch(0.556 0 0);
63
+ --accent: oklch(0.97 0 0);
64
+ --accent-foreground: oklch(0.205 0 0);
65
+ --destructive: oklch(0.577 0.245 27.325);
66
+ --border: oklch(0.922 0 0);
67
+ --input: oklch(0.922 0 0);
68
+ --ring: oklch(0.708 0 0);
69
+ --chart-1: oklch(0.646 0.222 41.116);
70
+ --chart-2: oklch(0.6 0.118 184.704);
71
+ --chart-3: oklch(0.398 0.07 227.392);
72
+ --chart-4: oklch(0.828 0.189 84.429);
73
+ --chart-5: oklch(0.769 0.188 70.08);
74
+ --sidebar: oklch(0.985 0 0);
75
+ --sidebar-foreground: oklch(0.145 0 0);
76
+ --sidebar-primary: oklch(0.205 0 0);
77
+ --sidebar-primary-foreground: oklch(0.985 0 0);
78
+ --sidebar-accent: oklch(0.97 0 0);
79
+ --sidebar-accent-foreground: oklch(0.205 0 0);
80
+ --sidebar-border: oklch(0.922 0 0);
81
+ --sidebar-ring: oklch(0.708 0 0);
82
+ }
83
+
84
+ .dark {
85
+ --background: oklch(0.145 0 0);
86
+ --foreground: oklch(0.985 0 0);
87
+ --card: oklch(0.205 0 0);
88
+ --card-foreground: oklch(0.985 0 0);
89
+ --popover: oklch(0.205 0 0);
90
+ --popover-foreground: oklch(0.985 0 0);
91
+ --primary: oklch(0.922 0 0);
92
+ --primary-foreground: oklch(0.205 0 0);
93
+ --secondary: oklch(0.269 0 0);
94
+ --secondary-foreground: oklch(0.985 0 0);
95
+ --muted: oklch(0.269 0 0);
96
+ --muted-foreground: oklch(0.708 0 0);
97
+ --accent: oklch(0.269 0 0);
98
+ --accent-foreground: oklch(0.985 0 0);
99
+ --destructive: oklch(0.704 0.191 22.216);
100
+ --border: oklch(1 0 0 / 10%);
101
+ --input: oklch(1 0 0 / 15%);
102
+ --ring: oklch(0.556 0 0);
103
+ --chart-1: oklch(0.488 0.243 264.376);
104
+ --chart-2: oklch(0.696 0.17 162.48);
105
+ --chart-3: oklch(0.769 0.188 70.08);
106
+ --chart-4: oklch(0.627 0.265 303.9);
107
+ --chart-5: oklch(0.645 0.246 16.439);
108
+ --sidebar: oklch(0.205 0 0);
109
+ --sidebar-foreground: oklch(0.985 0 0);
110
+ --sidebar-primary: oklch(0.488 0.243 264.376);
111
+ --sidebar-primary-foreground: oklch(0.985 0 0);
112
+ --sidebar-accent: oklch(0.269 0 0);
113
+ --sidebar-accent-foreground: oklch(0.985 0 0);
114
+ --sidebar-border: oklch(1 0 0 / 10%);
115
+ --sidebar-ring: oklch(0.556 0 0);
116
+ }
117
+
118
+ @layer base {
119
+ * {
120
+ @apply border-border outline-ring/50;
121
+ }
122
+ body {
123
+ @apply bg-background text-foreground;
124
+ }
125
+ }
@@ -0,0 +1,25 @@
1
+ import { routeAgentRequest } from "agents";
2
+
3
+ export { Chat } from "./chat-agent";
4
+
5
+ interface Env {
6
+ AI?: Ai;
7
+ __AI_PROXY?: Fetcher;
8
+ ASSETS: Fetcher;
9
+ Chat: DurableObjectNamespace;
10
+ }
11
+
12
+ export default {
13
+ async fetch(request: Request, env: Env): Promise<Response> {
14
+ const url = new URL(request.url);
15
+
16
+ if (url.pathname === "/health") {
17
+ return Response.json({ status: "ok", timestamp: Date.now() });
18
+ }
19
+
20
+ const agentResponse = await routeAgentRequest(request, env);
21
+ if (agentResponse !== null) return agentResponse;
22
+
23
+ return env.ASSETS.fetch(request);
24
+ },
25
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Jack AI Client - Drop-in replacement for Cloudflare AI binding.
3
+ *
4
+ * This wrapper provides the same interface as env.AI but routes calls
5
+ * through jack's binding proxy for metering and quota enforcement.
6
+ *
7
+ * Usage in templates:
8
+ * ```typescript
9
+ * import { createJackAI } from "./jack-ai";
10
+ *
11
+ * interface Env {
12
+ * __AI_PROXY: Fetcher; // Service binding to binding-proxy worker
13
+ * }
14
+ *
15
+ * export default {
16
+ * async fetch(request: Request, env: Env) {
17
+ * const AI = createJackAI(env);
18
+ * const result = await AI.run("@cf/meta/llama-3.2-1b-instruct", { messages });
19
+ * // Works exactly like env.AI.run()
20
+ * }
21
+ * };
22
+ * ```
23
+ *
24
+ * The wrapper is transparent - it accepts the same parameters as env.AI.run()
25
+ * and returns the same response types, including streaming.
26
+ */
27
+
28
+ interface JackAIEnv {
29
+ __AI_PROXY: Fetcher;
30
+ }
31
+
32
+ /**
33
+ * Creates a Jack AI client that mirrors the Cloudflare AI binding interface.
34
+ *
35
+ * @param env - Worker environment with jack proxy bindings
36
+ * @returns AI-compatible object with run() method
37
+ */
38
+ export function createJackAI(env: JackAIEnv): {
39
+ run: <T = unknown>(
40
+ model: string,
41
+ inputs: unknown,
42
+ options?: unknown,
43
+ ) => Promise<T | ReadableStream>;
44
+ } {
45
+ return {
46
+ async run<T = unknown>(
47
+ model: string,
48
+ inputs: unknown,
49
+ options?: unknown,
50
+ ): Promise<T | ReadableStream> {
51
+ const response = await env.__AI_PROXY.fetch("http://internal/ai/run", {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ },
56
+ body: JSON.stringify({ model, inputs, options }),
57
+ });
58
+
59
+ // Handle quota exceeded
60
+ if (response.status === 429) {
61
+ const error = await response.json();
62
+ const quotaError = new Error(
63
+ (error as { message?: string }).message || "AI quota exceeded",
64
+ );
65
+ (quotaError as Error & { code: string }).code = "AI_QUOTA_EXCEEDED";
66
+ (quotaError as Error & { resetIn?: number }).resetIn = (
67
+ error as { resetIn?: number }
68
+ ).resetIn;
69
+ throw quotaError;
70
+ }
71
+
72
+ // Handle other errors
73
+ if (!response.ok) {
74
+ const error = await response.json();
75
+ throw new Error((error as { error?: string }).error || "AI request failed");
76
+ }
77
+
78
+ // Handle streaming response
79
+ const contentType = response.headers.get("Content-Type");
80
+ if (contentType?.includes("text/event-stream")) {
81
+ return response.body as ReadableStream;
82
+ }
83
+
84
+ // Handle JSON response
85
+ return response.json() as Promise<T>;
86
+ },
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Type-safe wrapper that infers return types based on model.
92
+ * For advanced users who want full type safety.
93
+ */
94
+ export type JackAI = ReturnType<typeof createJackAI>;
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["@cloudflare/workers-types"],
12
+ "baseUrl": ".",
13
+ "paths": {
14
+ "@/*": ["./src/client/*"]
15
+ }
16
+ },
17
+ "include": ["src"]
18
+ }
@@ -0,0 +1,14 @@
1
+ import { resolve } from "node:path";
2
+ import { cloudflare } from "@cloudflare/vite-plugin";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+ import react from "@vitejs/plugin-react";
5
+ import { defineConfig } from "vite";
6
+
7
+ export default defineConfig({
8
+ plugins: [react(), tailwindcss(), cloudflare()],
9
+ resolve: {
10
+ alias: {
11
+ "@": resolve(__dirname, "./src/client"),
12
+ },
13
+ },
14
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "jack-template",
3
+ "main": "src/index.ts",
4
+ "compatibility_date": "2024-12-01",
5
+ "compatibility_flags": ["nodejs_compat"],
6
+ "ai": {
7
+ "binding": "AI"
8
+ },
9
+ "durable_objects": {
10
+ "bindings": [{ "name": "Chat", "class_name": "Chat" }]
11
+ },
12
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Chat"] }],
13
+ "assets": {
14
+ "directory": "dist/client",
15
+ "binding": "ASSETS",
16
+ "not_found_handling": "single-page-application"
17
+ }
18
+ }