@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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/commands/down.ts +39 -7
- package/src/commands/link.ts +2 -4
- package/src/commands/logs.ts +2 -4
- package/src/commands/mcp.ts +12 -10
- package/src/commands/services.ts +4 -2
- package/src/commands/sync.ts +5 -6
- package/src/lib/auth/client.ts +5 -2
- package/src/lib/binding-validator.ts +39 -3
- package/src/lib/build-helper.ts +18 -19
- package/src/lib/control-plane.ts +1 -0
- package/src/lib/do-config.ts +110 -0
- package/src/lib/do-export-validator.ts +26 -0
- package/src/lib/jsonc-edit.ts +292 -0
- package/src/lib/managed-deploy.ts +36 -1
- package/src/lib/project-link.ts +37 -0
- package/src/lib/project-operations.ts +13 -38
- package/src/lib/resources.ts +4 -5
- package/src/lib/schema.ts +8 -12
- package/src/lib/services/db-create.ts +2 -2
- package/src/lib/services/db-execute.ts +9 -6
- package/src/lib/services/db-list.ts +6 -4
- package/src/lib/services/endpoint-test.ts +275 -0
- package/src/lib/services/project-delete.ts +190 -0
- package/src/lib/services/project-environment.ts +457 -0
- package/src/lib/services/storage-config.ts +7 -309
- package/src/lib/services/storage-create.ts +2 -1
- package/src/lib/services/storage-delete.ts +3 -2
- package/src/lib/services/storage-info.ts +2 -1
- package/src/lib/services/storage-list.ts +6 -3
- package/src/lib/services/vectorize-config.ts +7 -264
- package/src/lib/services/vectorize-create.ts +2 -1
- package/src/lib/services/vectorize-delete.ts +6 -4
- package/src/lib/services/vectorize-list.ts +6 -3
- package/src/lib/storage/index.ts +21 -23
- package/src/lib/telemetry.ts +1 -0
- package/src/lib/wrangler-config.ts +43 -312
- package/src/lib/zip-packager.ts +28 -0
- package/src/mcp/test-utils.ts +31 -0
- package/src/mcp/tools/index.ts +271 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/types.ts +4 -0
- package/templates/AI-BINDINGS.md +34 -76
- package/templates/CLAUDE.md +1 -1
- package/templates/ai-chat/src/index.ts +7 -14
- package/templates/ai-chat/src/jack-ai.ts +0 -6
- package/templates/chat/.jack.json +45 -0
- package/templates/chat/bun.lock +1588 -0
- package/templates/chat/components.json +23 -0
- package/templates/chat/index.html +12 -0
- package/templates/chat/package.json +41 -0
- package/templates/chat/src/chat-agent.ts +61 -0
- package/templates/chat/src/client/app.tsx +189 -0
- package/templates/chat/src/client/chat.tsx +222 -0
- package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
- package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
- package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
- package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
- package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
- package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
- package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
- package/templates/chat/src/client/components/ui/button.tsx +38 -0
- package/templates/chat/src/client/lib/utils.ts +6 -0
- package/templates/chat/src/client/main.tsx +11 -0
- package/templates/chat/src/client/styles.css +125 -0
- package/templates/chat/src/index.ts +25 -0
- package/templates/chat/src/jack-ai.ts +94 -0
- package/templates/chat/tsconfig.json +18 -0
- package/templates/chat/vite.config.ts +14 -0
- package/templates/chat/wrangler.jsonc +18 -0
- package/templates/cron/.jack.json +18 -28
- package/templates/cron/schema.sql +10 -20
- package/templates/cron/src/admin.ts +321 -0
- package/templates/cron/src/index.ts +151 -81
- package/templates/cron/src/monitor.ts +124 -0
- package/templates/semantic-search/src/index.ts +5 -43
- package/templates/semantic-search/src/jack-ai.ts +0 -6
- package/templates/telegram-bot/.jack.json +56 -0
- package/templates/telegram-bot/bun.lock +41 -0
- package/templates/telegram-bot/package.json +16 -0
- package/templates/telegram-bot/src/index.ts +236 -0
- package/templates/telegram-bot/src/jack-ai.ts +100 -0
- package/templates/telegram-bot/tsconfig.json +11 -0
- package/templates/telegram-bot/wrangler.jsonc +8 -0
- package/templates/cron/src/jobs.ts +0 -139
- package/templates/cron/src/webhooks.ts +0 -95
- 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,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
|
+
}
|