create-claudius 1.8.0 → 1.8.2
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/LICENSE +21 -21
- package/README.md +68 -68
- package/dist/index.js +0 -0
- package/package.json +57 -57
- package/templates/next/README.md +23 -23
- package/templates/next/_gitignore +7 -7
- package/templates/next/app/ClaudiusWidget.tsx +16 -16
- package/templates/next/app/layout.tsx +15 -15
- package/templates/next/app/page.tsx +24 -24
- package/templates/next/next.config.mjs +4 -4
- package/templates/next/package.json +22 -22
- package/templates/next/tsconfig.json +21 -21
- package/templates/react/README.md +23 -23
- package/templates/react/_gitignore +4 -4
- package/templates/react/index.html +12 -12
- package/templates/react/package.json +23 -23
- package/templates/react/src/App.tsx +31 -31
- package/templates/react/src/main.tsx +9 -9
- package/templates/react/tsconfig.json +17 -17
- package/templates/react/vite.config.ts +6 -6
- package/templates/vanilla/README.md +19 -19
- package/templates/vanilla/_gitignore +4 -4
- package/templates/vanilla/index.html +60 -60
- package/templates/vanilla/package.json +14 -14
- package/templates/worker/.dev.vars.example +3 -3
- package/templates/worker/README.md +34 -34
- package/templates/worker/_gitignore +5 -5
- package/templates/worker/package.json +18 -18
- package/templates/worker/src/index.ts +92 -92
- package/templates/worker/tsconfig.json +16 -16
- package/templates/worker/wrangler.toml +18 -18
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { cors } from "hono/cors";
|
|
3
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
4
|
-
|
|
5
|
-
interface Env {
|
|
6
|
-
ANTHROPIC_API_KEY: string;
|
|
7
|
-
ALLOWED_ORIGIN: string;
|
|
8
|
-
RATE_LIMIT: KVNamespace;
|
|
9
|
-
CLAUDE_MODEL?: string;
|
|
10
|
-
MAX_TOKENS?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface ChatMessage {
|
|
14
|
-
role: "user" | "assistant";
|
|
15
|
-
content: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
19
|
-
const MAX_MESSAGE_LENGTH = 2000;
|
|
20
|
-
const RATE_LIMIT_PER_MINUTE = 20;
|
|
21
|
-
|
|
22
|
-
// Customize your assistant's personality and knowledge here.
|
|
23
|
-
const SYSTEM_PROMPT =
|
|
24
|
-
"You are a helpful, concise assistant embedded on a website. " +
|
|
25
|
-
"Answer clearly and politely. If you don't know something, say so.";
|
|
26
|
-
|
|
27
|
-
const app = new Hono<{ Bindings: Env }>();
|
|
28
|
-
|
|
29
|
-
app.use(
|
|
30
|
-
"/api/*",
|
|
31
|
-
cors({
|
|
32
|
-
origin: (origin, c) => {
|
|
33
|
-
const allowed = (c.env.ALLOWED_ORIGIN || "http://localhost:5173")
|
|
34
|
-
.split(",")
|
|
35
|
-
.map((o: string) => o.trim())
|
|
36
|
-
.filter(Boolean);
|
|
37
|
-
if (origin?.startsWith("http://localhost:")) return origin;
|
|
38
|
-
return origin && allowed.includes(origin) ? origin : allowed[0];
|
|
39
|
-
},
|
|
40
|
-
allowMethods: ["POST", "OPTIONS"],
|
|
41
|
-
allowHeaders: ["Content-Type"],
|
|
42
|
-
maxAge: 86400,
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
47
|
-
|
|
48
|
-
app.post("/api/chat", async (c) => {
|
|
49
|
-
let body: { messages?: ChatMessage[] };
|
|
50
|
-
try {
|
|
51
|
-
body = await c.req.json();
|
|
52
|
-
} catch {
|
|
53
|
-
return c.json({ error: "Invalid JSON body." }, 400);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const messages = body.messages;
|
|
57
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
58
|
-
return c.json({ error: "A non-empty messages array is required." }, 400);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Minimal per-IP, per-minute rate limit backed by the RATE_LIMIT KV namespace.
|
|
62
|
-
const ip = c.req.header("cf-connecting-ip") ?? "unknown";
|
|
63
|
-
const bucket = `rl:${ip}:${Math.floor(Date.now() / 60000)}`;
|
|
64
|
-
const count = parseInt((await c.env.RATE_LIMIT.get(bucket)) ?? "0", 10);
|
|
65
|
-
if (count >= RATE_LIMIT_PER_MINUTE) {
|
|
66
|
-
return c.json({ error: "Too many requests. Please wait a minute." }, 429, {
|
|
67
|
-
"Retry-After": "60",
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
await c.env.RATE_LIMIT.put(bucket, String(count + 1), { expirationTtl: 120 });
|
|
71
|
-
|
|
72
|
-
const sanitized = messages.map((m) => ({
|
|
73
|
-
role: m.role,
|
|
74
|
-
content: String(m.content ?? "").slice(0, MAX_MESSAGE_LENGTH),
|
|
75
|
-
}));
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const client = new Anthropic({ apiKey: c.env.ANTHROPIC_API_KEY });
|
|
79
|
-
const response = await client.messages.create({
|
|
80
|
-
model: c.env.CLAUDE_MODEL ?? DEFAULT_MODEL,
|
|
81
|
-
max_tokens: c.env.MAX_TOKENS ? parseInt(c.env.MAX_TOKENS, 10) : 1024,
|
|
82
|
-
system: SYSTEM_PROMPT,
|
|
83
|
-
messages: sanitized,
|
|
84
|
-
});
|
|
85
|
-
const textBlock = response.content.find((block) => block.type === "text");
|
|
86
|
-
return c.json({ reply: textBlock && textBlock.type === "text" ? textBlock.text : "" });
|
|
87
|
-
} catch {
|
|
88
|
-
return c.json({ error: "AI service temporarily unavailable. Please try again." }, 502);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
export default app;
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
4
|
+
|
|
5
|
+
interface Env {
|
|
6
|
+
ANTHROPIC_API_KEY: string;
|
|
7
|
+
ALLOWED_ORIGIN: string;
|
|
8
|
+
RATE_LIMIT: KVNamespace;
|
|
9
|
+
CLAUDE_MODEL?: string;
|
|
10
|
+
MAX_TOKENS?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ChatMessage {
|
|
14
|
+
role: "user" | "assistant";
|
|
15
|
+
content: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
19
|
+
const MAX_MESSAGE_LENGTH = 2000;
|
|
20
|
+
const RATE_LIMIT_PER_MINUTE = 20;
|
|
21
|
+
|
|
22
|
+
// Customize your assistant's personality and knowledge here.
|
|
23
|
+
const SYSTEM_PROMPT =
|
|
24
|
+
"You are a helpful, concise assistant embedded on a website. " +
|
|
25
|
+
"Answer clearly and politely. If you don't know something, say so.";
|
|
26
|
+
|
|
27
|
+
const app = new Hono<{ Bindings: Env }>();
|
|
28
|
+
|
|
29
|
+
app.use(
|
|
30
|
+
"/api/*",
|
|
31
|
+
cors({
|
|
32
|
+
origin: (origin, c) => {
|
|
33
|
+
const allowed = (c.env.ALLOWED_ORIGIN || "http://localhost:5173")
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((o: string) => o.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
if (origin?.startsWith("http://localhost:")) return origin;
|
|
38
|
+
return origin && allowed.includes(origin) ? origin : allowed[0];
|
|
39
|
+
},
|
|
40
|
+
allowMethods: ["POST", "OPTIONS"],
|
|
41
|
+
allowHeaders: ["Content-Type"],
|
|
42
|
+
maxAge: 86400,
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
47
|
+
|
|
48
|
+
app.post("/api/chat", async (c) => {
|
|
49
|
+
let body: { messages?: ChatMessage[] };
|
|
50
|
+
try {
|
|
51
|
+
body = await c.req.json();
|
|
52
|
+
} catch {
|
|
53
|
+
return c.json({ error: "Invalid JSON body." }, 400);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const messages = body.messages;
|
|
57
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
58
|
+
return c.json({ error: "A non-empty messages array is required." }, 400);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Minimal per-IP, per-minute rate limit backed by the RATE_LIMIT KV namespace.
|
|
62
|
+
const ip = c.req.header("cf-connecting-ip") ?? "unknown";
|
|
63
|
+
const bucket = `rl:${ip}:${Math.floor(Date.now() / 60000)}`;
|
|
64
|
+
const count = parseInt((await c.env.RATE_LIMIT.get(bucket)) ?? "0", 10);
|
|
65
|
+
if (count >= RATE_LIMIT_PER_MINUTE) {
|
|
66
|
+
return c.json({ error: "Too many requests. Please wait a minute." }, 429, {
|
|
67
|
+
"Retry-After": "60",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
await c.env.RATE_LIMIT.put(bucket, String(count + 1), { expirationTtl: 120 });
|
|
71
|
+
|
|
72
|
+
const sanitized = messages.map((m) => ({
|
|
73
|
+
role: m.role,
|
|
74
|
+
content: String(m.content ?? "").slice(0, MAX_MESSAGE_LENGTH),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const client = new Anthropic({ apiKey: c.env.ANTHROPIC_API_KEY });
|
|
79
|
+
const response = await client.messages.create({
|
|
80
|
+
model: c.env.CLAUDE_MODEL ?? DEFAULT_MODEL,
|
|
81
|
+
max_tokens: c.env.MAX_TOKENS ? parseInt(c.env.MAX_TOKENS, 10) : 1024,
|
|
82
|
+
system: SYSTEM_PROMPT,
|
|
83
|
+
messages: sanitized,
|
|
84
|
+
});
|
|
85
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
86
|
+
return c.json({ reply: textBlock && textBlock.type === "text" ? textBlock.text : "" });
|
|
87
|
+
} catch {
|
|
88
|
+
return c.json({ error: "AI service temporarily unavailable. Please try again." }, 502);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export default app;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"types": ["@cloudflare/workers-types"],
|
|
8
|
-
"strict": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"noEmit": true,
|
|
11
|
-
"esModuleInterop": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"isolatedModules": true
|
|
14
|
-
},
|
|
15
|
-
"include": ["src"]
|
|
16
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["@cloudflare/workers-types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
name = "{{PROJECT_NAME}}-worker"
|
|
2
|
-
main = "src/index.ts"
|
|
3
|
-
compatibility_date = "2024-09-23"
|
|
4
|
-
|
|
5
|
-
[vars]
|
|
6
|
-
# Comma-separated list of origins allowed to call this worker.
|
|
7
|
-
ALLOWED_ORIGIN = "http://localhost:5173"
|
|
8
|
-
# Optional overrides:
|
|
9
|
-
# CLAUDE_MODEL = "claude-haiku-4-5-20251001"
|
|
10
|
-
# MAX_TOKENS = "1024"
|
|
11
|
-
|
|
12
|
-
# Per-IP rate limiting is backed by this KV namespace. Create it with:
|
|
13
|
-
# npx wrangler kv namespace create RATE_LIMIT
|
|
14
|
-
# then paste the returned id (and preview_id) below.
|
|
15
|
-
[[kv_namespaces]]
|
|
16
|
-
binding = "RATE_LIMIT"
|
|
17
|
-
id = "placeholder"
|
|
18
|
-
preview_id = "placeholder"
|
|
1
|
+
name = "{{PROJECT_NAME}}-worker"
|
|
2
|
+
main = "src/index.ts"
|
|
3
|
+
compatibility_date = "2024-09-23"
|
|
4
|
+
|
|
5
|
+
[vars]
|
|
6
|
+
# Comma-separated list of origins allowed to call this worker.
|
|
7
|
+
ALLOWED_ORIGIN = "http://localhost:5173"
|
|
8
|
+
# Optional overrides:
|
|
9
|
+
# CLAUDE_MODEL = "claude-haiku-4-5-20251001"
|
|
10
|
+
# MAX_TOKENS = "1024"
|
|
11
|
+
|
|
12
|
+
# Per-IP rate limiting is backed by this KV namespace. Create it with:
|
|
13
|
+
# npx wrangler kv namespace create RATE_LIMIT
|
|
14
|
+
# then paste the returned id (and preview_id) below.
|
|
15
|
+
[[kv_namespaces]]
|
|
16
|
+
binding = "RATE_LIMIT"
|
|
17
|
+
id = "placeholder"
|
|
18
|
+
preview_id = "placeholder"
|