@getjack/jack 0.1.23 → 0.1.25
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/services.ts +156 -36
- package/src/lib/control-plane.ts +86 -0
- package/src/lib/services/db-execute.ts +100 -1
- package/src/lib/services/db-list.ts +34 -7
- package/src/lib/services/vectorize-config.ts +569 -0
- package/src/lib/services/vectorize-create.ts +166 -0
- package/src/lib/services/vectorize-delete.ts +54 -0
- package/src/lib/services/vectorize-info.ts +52 -0
- package/src/lib/services/vectorize-list.ts +56 -0
- package/src/lib/version-check.ts +2 -2
- package/src/mcp/test-utils.ts +47 -2
- package/src/mcp/tools/index.ts +282 -0
- package/templates/AI-BINDINGS.md +181 -0
- package/templates/CLAUDE.md +30 -0
- package/templates/ai-chat/.jack.json +3 -4
- package/templates/ai-chat/src/index.ts +45 -5
- package/templates/ai-chat/src/jack-ai.ts +96 -0
- package/templates/semantic-search/.jack.json +3 -4
- package/templates/semantic-search/src/index.ts +70 -12
- package/templates/semantic-search/src/jack-ai.ts +96 -0
- package/templates/semantic-search/src/jack-vectorize.ts +165 -0
package/src/mcp/tools/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
wrapResultsForMcp,
|
|
13
13
|
} from "../../lib/services/db-execute.ts";
|
|
14
14
|
import { listDatabases } from "../../lib/services/db-list.ts";
|
|
15
|
+
import { createVectorizeIndex } from "../../lib/services/vectorize-create.ts";
|
|
16
|
+
import { deleteVectorizeIndex } from "../../lib/services/vectorize-delete.ts";
|
|
17
|
+
import { getVectorizeInfo } from "../../lib/services/vectorize-info.ts";
|
|
18
|
+
import { listVectorizeIndexes } from "../../lib/services/vectorize-list.ts";
|
|
15
19
|
import { Events, track, withTelemetry } from "../../lib/telemetry.ts";
|
|
16
20
|
import type { DebugLogger, McpServerOptions } from "../types.ts";
|
|
17
21
|
import { formatErrorResponse, formatSuccessResponse } from "../utils.ts";
|
|
@@ -78,6 +82,43 @@ const ExecuteSqlSchema = z.object({
|
|
|
78
82
|
.describe("Database name (auto-detect from wrangler.jsonc if not provided)"),
|
|
79
83
|
});
|
|
80
84
|
|
|
85
|
+
const CreateVectorizeIndexSchema = z.object({
|
|
86
|
+
name: z.string().optional().describe("Index name (auto-generated if not provided)"),
|
|
87
|
+
dimensions: z.number().optional().default(768).describe("Vector dimensions (default: 768)"),
|
|
88
|
+
metric: z
|
|
89
|
+
.enum(["cosine", "euclidean", "dot-product"])
|
|
90
|
+
.optional()
|
|
91
|
+
.default("cosine")
|
|
92
|
+
.describe("Distance metric (default: cosine)"),
|
|
93
|
+
project_path: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Path to project directory (defaults to current directory)"),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const ListVectorizeIndexesSchema = z.object({
|
|
100
|
+
project_path: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Path to project directory (defaults to current directory)"),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const DeleteVectorizeIndexSchema = z.object({
|
|
107
|
+
name: z.string().describe("Index name to delete"),
|
|
108
|
+
project_path: z
|
|
109
|
+
.string()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Path to project directory (defaults to current directory)"),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const GetVectorizeInfoSchema = z.object({
|
|
115
|
+
name: z.string().describe("Index name"),
|
|
116
|
+
project_path: z
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("Path to project directory (defaults to current directory)"),
|
|
120
|
+
});
|
|
121
|
+
|
|
81
122
|
export function registerTools(server: McpServer, _options: McpServerOptions, debug: DebugLogger) {
|
|
82
123
|
// Register tool list handler
|
|
83
124
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -212,6 +253,85 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
|
|
|
212
253
|
required: ["sql"],
|
|
213
254
|
},
|
|
214
255
|
},
|
|
256
|
+
{
|
|
257
|
+
name: "create_vectorize_index",
|
|
258
|
+
description:
|
|
259
|
+
"Create a new Vectorize index for vector similarity search. Returns deploy_required=true since binding needs deploy to activate.",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {
|
|
263
|
+
name: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: "Index name (auto-generated if not provided)",
|
|
266
|
+
},
|
|
267
|
+
dimensions: {
|
|
268
|
+
type: "number",
|
|
269
|
+
default: 768,
|
|
270
|
+
description: "Vector dimensions (default: 768 for bge-base-en-v1.5)",
|
|
271
|
+
},
|
|
272
|
+
metric: {
|
|
273
|
+
type: "string",
|
|
274
|
+
enum: ["cosine", "euclidean", "dot-product"],
|
|
275
|
+
default: "cosine",
|
|
276
|
+
description: "Distance metric (default: cosine)",
|
|
277
|
+
},
|
|
278
|
+
project_path: {
|
|
279
|
+
type: "string",
|
|
280
|
+
description: "Path to project directory (defaults to current directory)",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "list_vectorize_indexes",
|
|
287
|
+
description: "List all Vectorize indexes for a project.",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
project_path: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "Path to project directory (defaults to current directory)",
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "delete_vectorize_index",
|
|
300
|
+
description: "Delete a Vectorize index.",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {
|
|
304
|
+
name: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "Index name to delete",
|
|
307
|
+
},
|
|
308
|
+
project_path: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Path to project directory (defaults to current directory)",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
required: ["name"],
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "get_vectorize_info",
|
|
318
|
+
description:
|
|
319
|
+
"Get information about a Vectorize index (dimensions, metric, vector count).",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: "object",
|
|
322
|
+
properties: {
|
|
323
|
+
name: {
|
|
324
|
+
type: "string",
|
|
325
|
+
description: "Index name",
|
|
326
|
+
},
|
|
327
|
+
project_path: {
|
|
328
|
+
type: "string",
|
|
329
|
+
description: "Path to project directory (defaults to current directory)",
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
required: ["name"],
|
|
333
|
+
},
|
|
334
|
+
},
|
|
215
335
|
],
|
|
216
336
|
};
|
|
217
337
|
});
|
|
@@ -550,6 +670,168 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
|
|
|
550
670
|
};
|
|
551
671
|
}
|
|
552
672
|
|
|
673
|
+
case "create_vectorize_index": {
|
|
674
|
+
const args = CreateVectorizeIndexSchema.parse(request.params.arguments ?? {});
|
|
675
|
+
const projectPath = args.project_path ?? process.cwd();
|
|
676
|
+
|
|
677
|
+
const wrappedCreateVectorizeIndex = withTelemetry(
|
|
678
|
+
"create_vectorize_index",
|
|
679
|
+
async (
|
|
680
|
+
projectDir: string,
|
|
681
|
+
name?: string,
|
|
682
|
+
dimensions?: number,
|
|
683
|
+
metric?: "cosine" | "euclidean" | "dot-product",
|
|
684
|
+
) => {
|
|
685
|
+
const result = await createVectorizeIndex(projectDir, {
|
|
686
|
+
name,
|
|
687
|
+
dimensions,
|
|
688
|
+
metric,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Track business event
|
|
692
|
+
track(Events.SERVICE_CREATED, {
|
|
693
|
+
service_type: "vectorize",
|
|
694
|
+
platform: "mcp",
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
index_name: result.indexName,
|
|
699
|
+
binding_name: result.bindingName,
|
|
700
|
+
dimensions: result.dimensions,
|
|
701
|
+
metric: result.metric,
|
|
702
|
+
created: result.created,
|
|
703
|
+
deploy_required: true,
|
|
704
|
+
};
|
|
705
|
+
},
|
|
706
|
+
{ platform: "mcp" },
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
const result = await wrappedCreateVectorizeIndex(
|
|
710
|
+
projectPath,
|
|
711
|
+
args.name,
|
|
712
|
+
args.dimensions,
|
|
713
|
+
args.metric,
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
content: [
|
|
718
|
+
{
|
|
719
|
+
type: "text",
|
|
720
|
+
text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
|
|
721
|
+
},
|
|
722
|
+
],
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
case "list_vectorize_indexes": {
|
|
727
|
+
const args = ListVectorizeIndexesSchema.parse(request.params.arguments ?? {});
|
|
728
|
+
const projectPath = args.project_path ?? process.cwd();
|
|
729
|
+
|
|
730
|
+
const wrappedListVectorizeIndexes = withTelemetry(
|
|
731
|
+
"list_vectorize_indexes",
|
|
732
|
+
async (projectDir: string) => {
|
|
733
|
+
const indexes = await listVectorizeIndexes(projectDir);
|
|
734
|
+
return {
|
|
735
|
+
indexes: indexes.map((idx) => ({
|
|
736
|
+
name: idx.name,
|
|
737
|
+
binding: idx.binding,
|
|
738
|
+
dimensions: idx.dimensions,
|
|
739
|
+
metric: idx.metric,
|
|
740
|
+
vector_count: idx.vectorCount,
|
|
741
|
+
})),
|
|
742
|
+
};
|
|
743
|
+
},
|
|
744
|
+
{ platform: "mcp" },
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
const result = await wrappedListVectorizeIndexes(projectPath);
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
content: [
|
|
751
|
+
{
|
|
752
|
+
type: "text",
|
|
753
|
+
text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
case "delete_vectorize_index": {
|
|
760
|
+
const args = DeleteVectorizeIndexSchema.parse(request.params.arguments ?? {});
|
|
761
|
+
const projectPath = args.project_path ?? process.cwd();
|
|
762
|
+
|
|
763
|
+
const wrappedDeleteVectorizeIndex = withTelemetry(
|
|
764
|
+
"delete_vectorize_index",
|
|
765
|
+
async (projectDir: string, indexName: string) => {
|
|
766
|
+
const result = await deleteVectorizeIndex(projectDir, indexName);
|
|
767
|
+
|
|
768
|
+
// Track business event
|
|
769
|
+
track(Events.SERVICE_DELETED, {
|
|
770
|
+
service_type: "vectorize",
|
|
771
|
+
platform: "mcp",
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
index_name: result.indexName,
|
|
776
|
+
deleted: result.deleted,
|
|
777
|
+
binding_removed: result.bindingRemoved,
|
|
778
|
+
};
|
|
779
|
+
},
|
|
780
|
+
{ platform: "mcp" },
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
const result = await wrappedDeleteVectorizeIndex(projectPath, args.name);
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
content: [
|
|
787
|
+
{
|
|
788
|
+
type: "text",
|
|
789
|
+
text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
case "get_vectorize_info": {
|
|
796
|
+
const args = GetVectorizeInfoSchema.parse(request.params.arguments ?? {});
|
|
797
|
+
|
|
798
|
+
const wrappedGetVectorizeInfo = withTelemetry(
|
|
799
|
+
"get_vectorize_info",
|
|
800
|
+
async (indexName: string) => {
|
|
801
|
+
const info = await getVectorizeInfo(indexName);
|
|
802
|
+
|
|
803
|
+
if (!info) {
|
|
804
|
+
throw new JackError(
|
|
805
|
+
JackErrorCode.RESOURCE_NOT_FOUND,
|
|
806
|
+
`Vectorize index '${indexName}' not found`,
|
|
807
|
+
"Use list_vectorize_indexes to see available indexes",
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
name: info.name,
|
|
813
|
+
dimensions: info.dimensions,
|
|
814
|
+
metric: info.metric,
|
|
815
|
+
vector_count: info.vectorCount,
|
|
816
|
+
created_on: info.createdOn,
|
|
817
|
+
modified_on: info.modifiedOn,
|
|
818
|
+
};
|
|
819
|
+
},
|
|
820
|
+
{ platform: "mcp" },
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
const result = await wrappedGetVectorizeInfo(args.name);
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
content: [
|
|
827
|
+
{
|
|
828
|
+
type: "text",
|
|
829
|
+
text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
|
|
830
|
+
},
|
|
831
|
+
],
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
553
835
|
default:
|
|
554
836
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
555
837
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# AI and Vectorize Bindings in Jack Cloud
|
|
2
|
+
|
|
3
|
+
## Quick Reference for Coding Agents
|
|
4
|
+
|
|
5
|
+
**DO NOT add `[ai]` or `[[vectorize]]` bindings directly for jack cloud projects.**
|
|
6
|
+
|
|
7
|
+
Jack cloud uses metered proxies for AI and Vectorize calls. Use the patterns below:
|
|
8
|
+
|
|
9
|
+
### AI Pattern
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { createJackAI } from "./jack-ai";
|
|
13
|
+
|
|
14
|
+
function getAI(env: Env) {
|
|
15
|
+
if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
|
|
16
|
+
return createJackAI(env);
|
|
17
|
+
}
|
|
18
|
+
if (env.AI) {
|
|
19
|
+
return env.AI;
|
|
20
|
+
}
|
|
21
|
+
throw new Error("No AI binding available");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Usage:
|
|
25
|
+
const ai = getAI(env);
|
|
26
|
+
const result = await ai.run("@cf/meta/llama-3.2-1b-instruct", { messages });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Vectorize Pattern
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { createJackVectorize } from "./jack-vectorize";
|
|
33
|
+
|
|
34
|
+
const INDEX_NAME = "my-vectors";
|
|
35
|
+
|
|
36
|
+
function getVectorize(env: Env) {
|
|
37
|
+
if (env.__VECTORIZE_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
|
|
38
|
+
return createJackVectorize(env, INDEX_NAME);
|
|
39
|
+
}
|
|
40
|
+
if (env.VECTORS) {
|
|
41
|
+
return env.VECTORS;
|
|
42
|
+
}
|
|
43
|
+
throw new Error("No Vectorize binding available");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Usage:
|
|
47
|
+
const vectors = getVectorize(env);
|
|
48
|
+
const results = await vectors.query(embedding, { topK: 10 });
|
|
49
|
+
await vectors.insert([{ id: "doc1", values: embedding, metadata: {} }]);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Why This Pattern?
|
|
53
|
+
|
|
54
|
+
Jack cloud is multi-tenant. All users share Cloudflare's AI quota (10k neurons/day). Without metering:
|
|
55
|
+
- One user could exhaust the daily quota for everyone
|
|
56
|
+
- No visibility into per-project usage
|
|
57
|
+
- No ability to bill for AI usage
|
|
58
|
+
|
|
59
|
+
The proxy:
|
|
60
|
+
1. Checks per-project quota
|
|
61
|
+
2. Logs usage to Analytics Engine
|
|
62
|
+
3. Forwards to real AI binding
|
|
63
|
+
4. Returns response unchanged
|
|
64
|
+
|
|
65
|
+
## Environment Bindings
|
|
66
|
+
|
|
67
|
+
### Jack Cloud (Managed Deploy)
|
|
68
|
+
|
|
69
|
+
Control plane injects these bindings:
|
|
70
|
+
- `__AI_PROXY` - Service binding to jack-binding-proxy for AI
|
|
71
|
+
- `__VECTORIZE_PROXY` - Service binding to jack-binding-proxy for Vectorize
|
|
72
|
+
- `__JACK_PROJECT_ID` - Project ID for metering
|
|
73
|
+
- `__JACK_ORG_ID` - Organization ID for billing
|
|
74
|
+
|
|
75
|
+
`env.AI` and `env.VECTORS` are **NOT available** in jack cloud. Direct calls will fail.
|
|
76
|
+
|
|
77
|
+
### Local Development
|
|
78
|
+
|
|
79
|
+
wrangler.jsonc provides:
|
|
80
|
+
- `AI` - Direct Cloudflare AI binding for local testing
|
|
81
|
+
- `VECTORS` - Direct Vectorize binding for local testing
|
|
82
|
+
|
|
83
|
+
The helper functions automatically use the right binding based on environment.
|
|
84
|
+
|
|
85
|
+
## Template Pattern
|
|
86
|
+
|
|
87
|
+
### AI Templates
|
|
88
|
+
|
|
89
|
+
1. **src/jack-ai.ts** - Client wrapper (copy from ai-chat or semantic-search template)
|
|
90
|
+
|
|
91
|
+
2. **Env interface** with optional bindings:
|
|
92
|
+
```typescript
|
|
93
|
+
interface Env {
|
|
94
|
+
AI?: Ai; // Local dev
|
|
95
|
+
__AI_PROXY?: Fetcher; // Jack cloud
|
|
96
|
+
__JACK_PROJECT_ID?: string; // Jack cloud
|
|
97
|
+
__JACK_ORG_ID?: string; // Jack cloud
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
3. **getAI() helper** that handles both environments
|
|
102
|
+
|
|
103
|
+
4. **wrangler.jsonc** with AI binding for local dev only:
|
|
104
|
+
```jsonc
|
|
105
|
+
{
|
|
106
|
+
"ai": { "binding": "AI" }
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Vectorize Templates
|
|
111
|
+
|
|
112
|
+
1. **src/jack-vectorize.ts** - Client wrapper (copy from semantic-search template)
|
|
113
|
+
|
|
114
|
+
2. **Env interface** with optional bindings:
|
|
115
|
+
```typescript
|
|
116
|
+
interface Env {
|
|
117
|
+
VECTORS?: VectorizeIndex; // Local dev
|
|
118
|
+
__VECTORIZE_PROXY?: Fetcher; // Jack cloud
|
|
119
|
+
__JACK_PROJECT_ID?: string; // Jack cloud
|
|
120
|
+
__JACK_ORG_ID?: string; // Jack cloud
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
3. **getVectorize() helper** that handles both environments
|
|
125
|
+
|
|
126
|
+
4. **wrangler.jsonc** with Vectorize binding for local dev only:
|
|
127
|
+
```jsonc
|
|
128
|
+
{
|
|
129
|
+
"vectorize": [{
|
|
130
|
+
"binding": "VECTORS",
|
|
131
|
+
"index_name": "my-vectors",
|
|
132
|
+
"preset": "cloudflare"
|
|
133
|
+
}]
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Error Handling
|
|
138
|
+
|
|
139
|
+
Quota exceeded returns 429:
|
|
140
|
+
|
|
141
|
+
### AI Quota
|
|
142
|
+
```typescript
|
|
143
|
+
try {
|
|
144
|
+
const result = await ai.run(model, inputs);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error.code === "AI_QUOTA_EXCEEDED") {
|
|
147
|
+
// Daily limit (1000 requests) reached, resets at midnight UTC
|
|
148
|
+
console.log(`Retry in ${error.resetIn} seconds`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Vectorize Quota
|
|
154
|
+
```typescript
|
|
155
|
+
try {
|
|
156
|
+
const results = await vectors.query(embedding, { topK: 10 });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.code === "VECTORIZE_QUERY_QUOTA_EXCEEDED") {
|
|
159
|
+
// Query limit (33,000/day) reached
|
|
160
|
+
console.log(`Retry in ${error.resetIn} seconds`);
|
|
161
|
+
}
|
|
162
|
+
if (error.code === "VECTORIZE_MUTATION_QUOTA_EXCEEDED") {
|
|
163
|
+
// Mutation limit (10,000/day) reached
|
|
164
|
+
console.log(`Retry in ${error.resetIn} seconds`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## BYOC Mode
|
|
170
|
+
|
|
171
|
+
For Bring Your Own Cloud deployments:
|
|
172
|
+
- User configures their own Cloudflare account
|
|
173
|
+
- Direct AI binding is used (no proxy)
|
|
174
|
+
- No metering (it's their account)
|
|
175
|
+
- Standard Cloudflare docs apply
|
|
176
|
+
|
|
177
|
+
## See Also
|
|
178
|
+
|
|
179
|
+
- `/docs/internal/specs/binding-proxy-worker.md` - Full architecture spec
|
|
180
|
+
- `apps/binding-proxy-worker/` - Proxy implementation
|
|
181
|
+
- `apps/control-plane/src/deployment-service.ts` - Binding injection logic
|
package/templates/CLAUDE.md
CHANGED
|
@@ -6,6 +6,36 @@
|
|
|
6
6
|
2. **Templates are for user code, not jack's tools** - wrangler is jack's responsibility (installed globally), not the template's
|
|
7
7
|
3. **Ship a lockfile** - `bun.lock` provides 70% faster installs on cold cache
|
|
8
8
|
|
|
9
|
+
## AI Bindings (IMPORTANT)
|
|
10
|
+
|
|
11
|
+
**For jack cloud projects, DO NOT use direct AI bindings.**
|
|
12
|
+
|
|
13
|
+
See [AI-BINDINGS.md](./AI-BINDINGS.md) for the full pattern.
|
|
14
|
+
|
|
15
|
+
Quick summary:
|
|
16
|
+
- Jack cloud uses a metered proxy (`__AI_PROXY`) instead of direct `env.AI`
|
|
17
|
+
- Templates must include `src/jack-ai.ts` wrapper
|
|
18
|
+
- Use `getAI(env)` helper instead of `env.AI` directly
|
|
19
|
+
- wrangler.jsonc still declares `ai: { binding: "AI" }` for local dev only
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// WRONG for jack cloud:
|
|
23
|
+
const result = await env.AI.run(model, inputs);
|
|
24
|
+
|
|
25
|
+
// CORRECT (works for both local dev and jack cloud):
|
|
26
|
+
import { createJackAI } from "./jack-ai";
|
|
27
|
+
|
|
28
|
+
function getAI(env: Env) {
|
|
29
|
+
if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
|
|
30
|
+
return createJackAI(env);
|
|
31
|
+
}
|
|
32
|
+
return env.AI;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ai = getAI(env);
|
|
36
|
+
const result = await ai.run(model, inputs);
|
|
37
|
+
```
|
|
38
|
+
|
|
9
39
|
## Dependency Rules
|
|
10
40
|
|
|
11
41
|
### DO include in templates:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"secrets": [],
|
|
5
5
|
"capabilities": ["ai"],
|
|
6
6
|
"intent": {
|
|
7
|
-
"keywords": ["ai", "chat", "llm", "
|
|
7
|
+
"keywords": ["ai", "chat", "llm", "llama", "completion", "chatbot"],
|
|
8
8
|
"examples": ["AI chatbot", "chat interface", "LLM chat app"]
|
|
9
9
|
},
|
|
10
10
|
"hooks": {
|
|
@@ -16,12 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"action": "box",
|
|
19
|
-
"title": "
|
|
19
|
+
"title": "{{name}}",
|
|
20
20
|
"lines": [
|
|
21
21
|
"{{url}}",
|
|
22
22
|
"",
|
|
23
|
-
"
|
|
24
|
-
"Rate limit: 10 requests/minute"
|
|
23
|
+
"jack open to view in browser"
|
|
25
24
|
]
|
|
26
25
|
}
|
|
27
26
|
]
|
|
@@ -1,13 +1,46 @@
|
|
|
1
|
+
import { createJackAI } from "./jack-ai";
|
|
2
|
+
|
|
1
3
|
interface Env {
|
|
2
|
-
AI
|
|
4
|
+
// Direct AI binding (for local dev with wrangler)
|
|
5
|
+
AI?: Ai;
|
|
6
|
+
// Jack proxy bindings (injected in jack cloud)
|
|
7
|
+
__AI_PROXY?: Fetcher;
|
|
8
|
+
__JACK_PROJECT_ID?: string;
|
|
9
|
+
__JACK_ORG_ID?: string;
|
|
10
|
+
// Assets binding
|
|
3
11
|
ASSETS: Fetcher;
|
|
4
12
|
}
|
|
5
13
|
|
|
14
|
+
function getAI(env: Env) {
|
|
15
|
+
// Prefer jack cloud proxy if available (for metering)
|
|
16
|
+
if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
|
|
17
|
+
return createJackAI(env as Required<Pick<Env, "__AI_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">>);
|
|
18
|
+
}
|
|
19
|
+
// Fallback to direct binding for local dev
|
|
20
|
+
if (env.AI) {
|
|
21
|
+
return env.AI;
|
|
22
|
+
}
|
|
23
|
+
throw new Error("No AI binding available");
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
interface ChatMessage {
|
|
7
27
|
role: "user" | "assistant" | "system";
|
|
8
28
|
content: string;
|
|
9
29
|
}
|
|
10
30
|
|
|
31
|
+
// System prompt - customize this to change the AI's personality
|
|
32
|
+
const SYSTEM_PROMPT = `You are a helpful AI assistant built with jack (getjack.sh).
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
Be concise, friendly, and helpful. If asked about jack:
|
|
37
|
+
- jack new creates projects from templates
|
|
38
|
+
- jack ship deploys to production
|
|
39
|
+
- jack open opens your app in browser
|
|
40
|
+
- Docs: https://docs.getjack.sh
|
|
41
|
+
|
|
42
|
+
Focus on being useful. Keep responses short unless detail is needed.`;
|
|
43
|
+
|
|
11
44
|
// Rate limiting: 10 requests per minute per IP
|
|
12
45
|
const RATE_LIMIT = 10;
|
|
13
46
|
const RATE_WINDOW_MS = 60_000;
|
|
@@ -65,7 +98,7 @@ export default {
|
|
|
65
98
|
|
|
66
99
|
try {
|
|
67
100
|
const body = (await request.json()) as { messages?: ChatMessage[] };
|
|
68
|
-
|
|
101
|
+
let messages = body.messages;
|
|
69
102
|
|
|
70
103
|
if (!messages || !Array.isArray(messages)) {
|
|
71
104
|
return Response.json(
|
|
@@ -74,9 +107,16 @@ export default {
|
|
|
74
107
|
);
|
|
75
108
|
}
|
|
76
109
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
"
|
|
110
|
+
// Prepend system prompt if not already present
|
|
111
|
+
if (messages.length === 0 || messages[0].role !== "system") {
|
|
112
|
+
messages = [{ role: "system", content: SYSTEM_PROMPT }, ...messages];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Stream response using Llama 3.2 1B - cheapest model with good quality
|
|
116
|
+
// See: https://developers.cloudflare.com/workers-ai/models/
|
|
117
|
+
const ai = getAI(env);
|
|
118
|
+
const stream = await ai.run(
|
|
119
|
+
"@cf/meta/llama-3.2-1b-instruct",
|
|
80
120
|
{
|
|
81
121
|
messages,
|
|
82
122
|
stream: true,
|