@getjack/jack 0.1.2 → 0.1.3
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 +54 -47
- package/src/commands/agents.ts +145 -10
- package/src/commands/down.ts +110 -102
- package/src/commands/feedback.ts +189 -0
- package/src/commands/init.ts +8 -12
- package/src/commands/login.ts +88 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +21 -0
- package/src/commands/mcp.ts +134 -7
- package/src/commands/new.ts +43 -17
- package/src/commands/open.ts +13 -6
- package/src/commands/projects.ts +269 -143
- package/src/commands/secrets.ts +413 -0
- package/src/commands/services.ts +96 -123
- package/src/commands/ship.ts +5 -1
- package/src/commands/whoami.ts +31 -0
- package/src/index.ts +218 -144
- package/src/lib/agent-files.ts +34 -0
- package/src/lib/agents.ts +390 -22
- package/src/lib/asset-hash.ts +50 -0
- package/src/lib/auth/client.ts +115 -0
- package/src/lib/auth/constants.ts +5 -0
- package/src/lib/auth/guard.ts +57 -0
- package/src/lib/auth/index.ts +18 -0
- package/src/lib/auth/store.ts +54 -0
- package/src/lib/binding-validator.ts +136 -0
- package/src/lib/build-helper.ts +211 -0
- package/src/lib/cloudflare-api.ts +24 -0
- package/src/lib/config.ts +5 -6
- package/src/lib/control-plane.ts +295 -0
- package/src/lib/debug.ts +3 -1
- package/src/lib/deploy-mode.ts +93 -0
- package/src/lib/deploy-upload.ts +92 -0
- package/src/lib/errors.ts +2 -0
- package/src/lib/github.ts +31 -1
- package/src/lib/hooks.ts +4 -12
- package/src/lib/intent.ts +88 -0
- package/src/lib/jsonc.ts +125 -0
- package/src/lib/local-paths.test.ts +902 -0
- package/src/lib/local-paths.ts +258 -0
- package/src/lib/managed-deploy.ts +175 -0
- package/src/lib/managed-down.ts +159 -0
- package/src/lib/mcp-config.ts +55 -34
- package/src/lib/names.ts +9 -29
- package/src/lib/project-operations.ts +676 -249
- package/src/lib/project-resolver.ts +476 -0
- package/src/lib/registry.ts +76 -37
- package/src/lib/resources.ts +196 -0
- package/src/lib/schema.ts +30 -1
- package/src/lib/storage/file-filter.ts +1 -0
- package/src/lib/storage/index.ts +5 -1
- package/src/lib/telemetry.ts +14 -0
- package/src/lib/tty.ts +15 -0
- package/src/lib/zip-packager.ts +255 -0
- package/src/mcp/resources/index.ts +8 -2
- package/src/mcp/server.ts +32 -4
- package/src/mcp/tools/index.ts +35 -13
- package/src/mcp/types.ts +6 -0
- package/src/mcp/utils.ts +1 -1
- package/src/templates/index.ts +42 -4
- package/src/templates/types.ts +13 -0
- package/templates/CLAUDE.md +166 -0
- package/templates/api/.jack.json +4 -0
- package/templates/api/bun.lock +1 -0
- package/templates/api/wrangler.jsonc +5 -0
- package/templates/hello/.jack.json +28 -0
- package/templates/hello/package.json +10 -0
- package/templates/hello/src/index.ts +11 -0
- package/templates/hello/tsconfig.json +11 -0
- package/templates/hello/wrangler.jsonc +5 -0
- package/templates/miniapp/.jack.json +15 -4
- package/templates/miniapp/bun.lock +135 -40
- package/templates/miniapp/index.html +1 -0
- package/templates/miniapp/package.json +3 -1
- package/templates/miniapp/public/.well-known/farcaster.json +7 -5
- package/templates/miniapp/public/icon.png +0 -0
- package/templates/miniapp/public/og.png +0 -0
- package/templates/miniapp/schema.sql +8 -0
- package/templates/miniapp/src/App.tsx +254 -3
- package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
- package/templates/miniapp/src/hooks/useAI.ts +35 -0
- package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
- package/templates/miniapp/src/hooks/useShare.ts +76 -0
- package/templates/miniapp/src/index.css +15 -0
- package/templates/miniapp/src/lib/api.ts +2 -1
- package/templates/miniapp/src/worker.ts +515 -1
- package/templates/miniapp/wrangler.jsonc +15 -3
- package/LICENSE +0 -190
- package/README.md +0 -55
- package/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
package/src/mcp/tools/index.ts
CHANGED
|
@@ -2,14 +2,10 @@ import type { Server as McpServer } from "@modelcontextprotocol/sdk/server/index
|
|
|
2
2
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { JackError, JackErrorCode } from "../../lib/errors.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
deployProject,
|
|
8
|
-
getProjectStatus,
|
|
9
|
-
listAllProjects,
|
|
10
|
-
} from "../../lib/project-operations.ts";
|
|
5
|
+
import { createProject, deployProject, getProjectStatus } from "../../lib/project-operations.ts";
|
|
6
|
+
import { listAllProjects } from "../../lib/project-resolver.ts";
|
|
11
7
|
import { Events, track, withTelemetry } from "../../lib/telemetry.ts";
|
|
12
|
-
import type { McpServerOptions } from "../types.ts";
|
|
8
|
+
import type { DebugLogger, McpServerOptions } from "../types.ts";
|
|
13
9
|
import { formatErrorResponse, formatSuccessResponse } from "../utils.ts";
|
|
14
10
|
|
|
15
11
|
// Tool schemas
|
|
@@ -40,9 +36,10 @@ const ListProjectsSchema = z.object({
|
|
|
40
36
|
.describe("Filter projects by status (defaults to 'all')"),
|
|
41
37
|
});
|
|
42
38
|
|
|
43
|
-
export function registerTools(server: McpServer, _options: McpServerOptions) {
|
|
39
|
+
export function registerTools(server: McpServer, _options: McpServerOptions, debug: DebugLogger) {
|
|
44
40
|
// Register tool list handler
|
|
45
41
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
42
|
+
debug("tools/list requested");
|
|
46
43
|
return {
|
|
47
44
|
tools: [
|
|
48
45
|
{
|
|
@@ -80,7 +77,7 @@ export function registerTools(server: McpServer, _options: McpServerOptions) {
|
|
|
80
77
|
{
|
|
81
78
|
name: "get_project_status",
|
|
82
79
|
description:
|
|
83
|
-
"Get detailed status information for a specific project, including deployment status, local path, and
|
|
80
|
+
"Get detailed status information for a specific project, including deployment status, local path, and backup status.",
|
|
84
81
|
inputSchema: {
|
|
85
82
|
type: "object",
|
|
86
83
|
properties: {
|
|
@@ -98,7 +95,7 @@ export function registerTools(server: McpServer, _options: McpServerOptions) {
|
|
|
98
95
|
{
|
|
99
96
|
name: "list_projects",
|
|
100
97
|
description:
|
|
101
|
-
"List all known projects with their status information. Can filter by local, deployed, or
|
|
98
|
+
"List all known projects with their status information. Can filter by local, deployed, or backup projects.",
|
|
102
99
|
inputSchema: {
|
|
103
100
|
type: "object",
|
|
104
101
|
properties: {
|
|
@@ -117,9 +114,12 @@ export function registerTools(server: McpServer, _options: McpServerOptions) {
|
|
|
117
114
|
// Register single tools/call handler that dispatches to individual tool implementations
|
|
118
115
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
119
116
|
const startTime = Date.now();
|
|
117
|
+
const toolName = request.params.name;
|
|
118
|
+
|
|
119
|
+
debug("tools/call requested", { tool: toolName, args: request.params.arguments });
|
|
120
120
|
|
|
121
121
|
try {
|
|
122
|
-
switch (
|
|
122
|
+
switch (toolName) {
|
|
123
123
|
case "create_project": {
|
|
124
124
|
const args = CreateProjectSchema.parse(request.params.arguments ?? {});
|
|
125
125
|
|
|
@@ -226,7 +226,23 @@ export function registerTools(server: McpServer, _options: McpServerOptions) {
|
|
|
226
226
|
const wrappedListProjects = withTelemetry(
|
|
227
227
|
"list_projects",
|
|
228
228
|
async (filter?: "all" | "local" | "deployed" | "cloud") => {
|
|
229
|
-
|
|
229
|
+
const allProjects = await listAllProjects();
|
|
230
|
+
|
|
231
|
+
// Apply filter if specified
|
|
232
|
+
if (!filter || filter === "all") {
|
|
233
|
+
return allProjects;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
switch (filter) {
|
|
237
|
+
case "local":
|
|
238
|
+
return allProjects.filter((p) => p.sources.filesystem);
|
|
239
|
+
case "deployed":
|
|
240
|
+
return allProjects.filter((p) => p.status === "live");
|
|
241
|
+
case "cloud":
|
|
242
|
+
return allProjects.filter((p) => p.sources.controlPlane);
|
|
243
|
+
default:
|
|
244
|
+
return allProjects;
|
|
245
|
+
}
|
|
230
246
|
},
|
|
231
247
|
{ platform: "mcp" },
|
|
232
248
|
);
|
|
@@ -244,9 +260,15 @@ export function registerTools(server: McpServer, _options: McpServerOptions) {
|
|
|
244
260
|
}
|
|
245
261
|
|
|
246
262
|
default:
|
|
247
|
-
throw new Error(`Unknown tool: ${
|
|
263
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
248
264
|
}
|
|
249
265
|
} catch (error) {
|
|
266
|
+
const duration = Date.now() - startTime;
|
|
267
|
+
debug("tools/call failed", {
|
|
268
|
+
tool: toolName,
|
|
269
|
+
duration_ms: duration,
|
|
270
|
+
error: error instanceof Error ? error.message : String(error),
|
|
271
|
+
});
|
|
250
272
|
return {
|
|
251
273
|
content: [
|
|
252
274
|
{
|
package/src/mcp/types.ts
CHANGED
|
@@ -26,4 +26,10 @@ export { JackErrorCode as McpErrorCode } from "../lib/errors.ts";
|
|
|
26
26
|
*/
|
|
27
27
|
export interface McpServerOptions {
|
|
28
28
|
projectPath?: string;
|
|
29
|
+
debug?: boolean;
|
|
29
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Debug logger function type
|
|
34
|
+
*/
|
|
35
|
+
export type DebugLogger = (message: string, data?: unknown) => void;
|
package/src/mcp/utils.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function formatErrorResponse(error: unknown, startTime: number): McpToolR
|
|
|
23
23
|
const message = error instanceof Error ? error.message : String(error);
|
|
24
24
|
const code = classifyMcpError(error);
|
|
25
25
|
const suggestion = isJackError(error)
|
|
26
|
-
? error.suggestion ?? getSuggestionForError(code)
|
|
26
|
+
? (error.suggestion ?? getSuggestionForError(code))
|
|
27
27
|
: getSuggestionForError(code);
|
|
28
28
|
|
|
29
29
|
return {
|
package/src/templates/index.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { parseJsonc } from "../lib/jsonc.ts";
|
|
5
|
+
import type { TemplateOrigin } from "../lib/registry.ts";
|
|
4
6
|
import type { Template } from "./types";
|
|
5
7
|
|
|
6
8
|
// Resolve templates directory relative to this file (src/templates -> templates)
|
|
7
9
|
const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates");
|
|
8
10
|
|
|
9
|
-
const BUILTIN_TEMPLATES = ["miniapp", "api"];
|
|
11
|
+
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api"];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolved template with origin tracking for lineage
|
|
15
|
+
*/
|
|
16
|
+
export interface ResolvedTemplate {
|
|
17
|
+
template: Template;
|
|
18
|
+
origin: TemplateOrigin;
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
/**
|
|
12
22
|
* Read all files in a directory recursively
|
|
@@ -59,12 +69,15 @@ async function loadTemplate(name: string): Promise<Template> {
|
|
|
59
69
|
name: string;
|
|
60
70
|
description: string;
|
|
61
71
|
secrets: string[];
|
|
72
|
+
optionalSecrets?: Template["optionalSecrets"];
|
|
62
73
|
capabilities?: Template["capabilities"];
|
|
63
74
|
requires?: Template["requires"];
|
|
64
75
|
hooks?: Template["hooks"];
|
|
76
|
+
agentContext?: Template["agentContext"];
|
|
77
|
+
intent?: Template["intent"];
|
|
65
78
|
} = { name, description: "", secrets: [] };
|
|
66
79
|
if (existsSync(metadataPath)) {
|
|
67
|
-
metadata =
|
|
80
|
+
metadata = parseJsonc(await readFile(metadataPath, "utf-8"));
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
// Read all template files
|
|
@@ -73,9 +86,12 @@ async function loadTemplate(name: string): Promise<Template> {
|
|
|
73
86
|
return {
|
|
74
87
|
description: metadata.description,
|
|
75
88
|
secrets: metadata.secrets,
|
|
89
|
+
optionalSecrets: metadata.optionalSecrets,
|
|
76
90
|
capabilities: metadata.capabilities,
|
|
77
91
|
requires: metadata.requires,
|
|
78
92
|
hooks: metadata.hooks,
|
|
93
|
+
agentContext: metadata.agentContext,
|
|
94
|
+
intent: metadata.intent,
|
|
79
95
|
files,
|
|
80
96
|
};
|
|
81
97
|
}
|
|
@@ -84,9 +100,9 @@ async function loadTemplate(name: string): Promise<Template> {
|
|
|
84
100
|
* Resolve template by name or GitHub URL
|
|
85
101
|
*/
|
|
86
102
|
export async function resolveTemplate(template?: string): Promise<Template> {
|
|
87
|
-
// No template →
|
|
103
|
+
// No template → hello (omakase default)
|
|
88
104
|
if (!template) {
|
|
89
|
-
return loadTemplate("
|
|
105
|
+
return loadTemplate("hello");
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
// Built-in template
|
|
@@ -104,6 +120,28 @@ export async function resolveTemplate(template?: string): Promise<Template> {
|
|
|
104
120
|
throw new Error(`Unknown template: ${template}\n\nAvailable: ${BUILTIN_TEMPLATES.join(", ")}`);
|
|
105
121
|
}
|
|
106
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Resolve template with origin tracking for lineage
|
|
125
|
+
* Used during project creation to record which template was used
|
|
126
|
+
*/
|
|
127
|
+
export async function resolveTemplateWithOrigin(
|
|
128
|
+
templateOption?: string,
|
|
129
|
+
): Promise<ResolvedTemplate> {
|
|
130
|
+
const templateName = templateOption || "hello";
|
|
131
|
+
|
|
132
|
+
// Determine origin type
|
|
133
|
+
const isGitHub = templateName.includes("/");
|
|
134
|
+
const origin: TemplateOrigin = {
|
|
135
|
+
type: isGitHub ? "github" : "builtin",
|
|
136
|
+
name: templateName,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Resolve the template
|
|
140
|
+
const template = await resolveTemplate(templateOption);
|
|
141
|
+
|
|
142
|
+
return { template, origin };
|
|
143
|
+
}
|
|
144
|
+
|
|
107
145
|
/**
|
|
108
146
|
* Replace template placeholders with project name
|
|
109
147
|
* All templates use "jack-template" as universal placeholder
|
package/src/templates/types.ts
CHANGED
|
@@ -30,12 +30,25 @@ export interface AgentContext {
|
|
|
30
30
|
full_text: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface OptionalSecret {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
setupUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IntentMetadata {
|
|
40
|
+
keywords: string[];
|
|
41
|
+
examples?: string[]; // For future telemetry/docs
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
export interface Template {
|
|
34
45
|
files: Record<string, string>; // path -> content
|
|
35
46
|
secrets?: string[]; // required secret keys (e.g., ["NEYNAR_API_KEY"])
|
|
47
|
+
optionalSecrets?: OptionalSecret[]; // optional secret configurations
|
|
36
48
|
capabilities?: Capability[]; // infrastructure requirements (deprecated, use requires)
|
|
37
49
|
requires?: ServiceTypeKey[]; // service requirements (DB, KV, CRON, QUEUE, STORAGE)
|
|
38
50
|
description?: string; // for help text
|
|
39
51
|
hooks?: TemplateHooks;
|
|
40
52
|
agentContext?: AgentContext;
|
|
53
|
+
intent?: IntentMetadata;
|
|
41
54
|
}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -185,6 +185,172 @@ These variables are substituted at runtime (different from template placeholders
|
|
|
185
185
|
}
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
## Farcaster Miniapp Embeds
|
|
189
|
+
|
|
190
|
+
When a cast includes a URL, Farcaster scrapes it for `fc:miniapp` meta tags to render a rich embed.
|
|
191
|
+
|
|
192
|
+
### fc:miniapp Meta Format
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<meta name="fc:miniapp" content='{"version":"1","imageUrl":"...","button":{...}}' />
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
{
|
|
200
|
+
version: "1",
|
|
201
|
+
imageUrl: "https://absolute-url/image.png", // MUST be absolute https
|
|
202
|
+
button: {
|
|
203
|
+
title: "Open App", // Max 32 chars
|
|
204
|
+
action: {
|
|
205
|
+
type: "launch_miniapp",
|
|
206
|
+
name: "app-name", // REQUIRED - app name shown in UI
|
|
207
|
+
url: "https://absolute-url" // MUST be absolute https
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Critical requirements:**
|
|
214
|
+
- All URLs must be absolute `https://` - no relative paths, no localhost
|
|
215
|
+
- `button.action.name` is **required** - omitting it breaks the embed
|
|
216
|
+
- Image must be 600×400 to 3000×2000 (3:2 ratio), <10MB, PNG/JPG/GIF/WebP
|
|
217
|
+
|
|
218
|
+
### Wrangler Assets + Dynamic Routes
|
|
219
|
+
|
|
220
|
+
To serve both static assets AND dynamic routes (like `/share` with meta tags):
|
|
221
|
+
|
|
222
|
+
```jsonc
|
|
223
|
+
// wrangler.jsonc (miniapp template)
|
|
224
|
+
"assets": {
|
|
225
|
+
"directory": "dist/client", // Cloudflare Vite plugin outputs client assets here
|
|
226
|
+
"binding": "ASSETS",
|
|
227
|
+
"not_found_handling": "single-page-application",
|
|
228
|
+
"run_worker_first": true // CRITICAL - without this, assets bypass the worker!
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Why `run_worker_first: true`?**
|
|
233
|
+
Without it, Cloudflare serves static files directly from the assets directory, completely bypassing your worker. This means:
|
|
234
|
+
- `/api/*` routes won't work if there's a matching file
|
|
235
|
+
- Dynamic routes like `/share` that need to inject meta tags won't work
|
|
236
|
+
- The worker only runs for truly non-existent paths
|
|
237
|
+
|
|
238
|
+
### External Fetch Timeout Pattern
|
|
239
|
+
|
|
240
|
+
When fetching external resources (like profile pictures for OG images), always use a timeout:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const controller = new AbortController();
|
|
244
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
248
|
+
clearTimeout(timeoutId);
|
|
249
|
+
|
|
250
|
+
if (response.ok) {
|
|
251
|
+
const buffer = await response.arrayBuffer();
|
|
252
|
+
// Also limit size to prevent memory issues
|
|
253
|
+
if (buffer.byteLength < 500_000) {
|
|
254
|
+
// process...
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// Handle timeout/network errors gracefully
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Without timeout, a slow or hanging external URL can cause your OG image generation to fail silently.
|
|
263
|
+
|
|
264
|
+
## URL Detection in Cloudflare Workers
|
|
265
|
+
|
|
266
|
+
When generating URLs for external services (like Farcaster embed URLs), you need reliable production URL detection. This is non-trivial because:
|
|
267
|
+
|
|
268
|
+
1. `new URL(request.url).origin` may not work correctly in all cases
|
|
269
|
+
2. Local development returns `localhost` which is invalid for embeds
|
|
270
|
+
3. Custom domains require explicit configuration
|
|
271
|
+
|
|
272
|
+
### The Pattern
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
function getBaseUrl(
|
|
276
|
+
env: Env,
|
|
277
|
+
c: { req: { header: (name: string) => string | undefined; url: string } },
|
|
278
|
+
): string | null {
|
|
279
|
+
// 1. Prefer explicit APP_URL (most reliable for custom domains)
|
|
280
|
+
if (env.APP_URL?.trim()) {
|
|
281
|
+
const url = env.APP_URL.replace(/\/$/, "");
|
|
282
|
+
if (url.startsWith("https://")) return url;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 2. Use Host header (always set by Cloudflare in production)
|
|
286
|
+
const host = c.req.header("host");
|
|
287
|
+
if (host) {
|
|
288
|
+
// Reject localhost - return null to signal "can't generate valid URLs"
|
|
289
|
+
if (host.startsWith("localhost") || host.startsWith("127.0.0.1")) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Get protocol from cf-visitor (Cloudflare-specific) or x-forwarded-proto
|
|
294
|
+
let proto = "https";
|
|
295
|
+
const cfVisitor = c.req.header("cf-visitor");
|
|
296
|
+
if (cfVisitor) {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(cfVisitor);
|
|
299
|
+
if (parsed.scheme) proto = parsed.scheme;
|
|
300
|
+
} catch {}
|
|
301
|
+
} else {
|
|
302
|
+
proto = c.req.header("x-forwarded-proto") || "https";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Workers.dev is always https
|
|
306
|
+
if (host.endsWith(".workers.dev")) proto = "https";
|
|
307
|
+
|
|
308
|
+
return `${proto}://${host}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 3. Fallback to URL origin (rarely needed)
|
|
312
|
+
try {
|
|
313
|
+
const url = new URL(c.req.url);
|
|
314
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return url.origin;
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Key Headers in Cloudflare Workers
|
|
325
|
+
|
|
326
|
+
| Header | Value | Notes |
|
|
327
|
+
|--------|-------|-------|
|
|
328
|
+
| `host` | `my-app.workers.dev` | Always set in production |
|
|
329
|
+
| `cf-visitor` | `{"scheme":"https"}` | Cloudflare-specific, most reliable for protocol |
|
|
330
|
+
| `x-forwarded-proto` | `https` | Standard header, less reliable |
|
|
331
|
+
|
|
332
|
+
### Handling Local Development
|
|
333
|
+
|
|
334
|
+
When `getBaseUrl()` returns `null`, show a helpful error instead of generating broken URLs:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
const baseUrl = getBaseUrl(env, c);
|
|
338
|
+
if (!baseUrl) {
|
|
339
|
+
return c.html(`
|
|
340
|
+
<h2>Share embeds require production deployment</h2>
|
|
341
|
+
<p>Deploy with <code>jack ship</code> to enable sharing.</p>
|
|
342
|
+
`);
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Why Not Just Use `new URL(request.url)`?
|
|
347
|
+
|
|
348
|
+
- In some edge cases, `request.url` may not have the expected origin
|
|
349
|
+
- Local development always returns localhost
|
|
350
|
+
- Doesn't help distinguish production from development
|
|
351
|
+
|
|
352
|
+
The Host header approach is reliable because Cloudflare always sets it to the actual domain being accessed.
|
|
353
|
+
|
|
188
354
|
## Adding New Templates
|
|
189
355
|
|
|
190
356
|
1. Create directory: `templates/my-template/`
|
package/templates/api/.jack.json
CHANGED
package/templates/api/bun.lock
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hello",
|
|
3
|
+
"description": "Minimal Worker (fetch)",
|
|
4
|
+
"secrets": [],
|
|
5
|
+
"intent": {
|
|
6
|
+
"keywords": ["hello", "starter", "worker"],
|
|
7
|
+
"examples": ["hello world worker"]
|
|
8
|
+
},
|
|
9
|
+
"hooks": {
|
|
10
|
+
"postDeploy": [
|
|
11
|
+
{
|
|
12
|
+
"action": "clipboard",
|
|
13
|
+
"text": "{{url}}",
|
|
14
|
+
"message": "Deploy URL copied to clipboard"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"action": "shell",
|
|
18
|
+
"command": "curl -s {{url}} | head -c 200",
|
|
19
|
+
"message": "Testing worker..."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"action": "box",
|
|
23
|
+
"title": "Deployed: {{name}}",
|
|
24
|
+
"lines": ["URL: {{url}}", "", "Worker is live!"]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -2,11 +2,22 @@
|
|
|
2
2
|
"name": "miniapp",
|
|
3
3
|
"description": "Farcaster Miniapp (React + Vite)",
|
|
4
4
|
"secrets": ["NEYNAR_API_KEY"],
|
|
5
|
-
"
|
|
6
|
-
|
|
5
|
+
"optionalSecrets": [
|
|
6
|
+
{
|
|
7
|
+
"name": "OPENAI_API_KEY",
|
|
8
|
+
"description": "For better AI (optional, falls back to free Workers AI)",
|
|
9
|
+
"setupUrl": "https://platform.openai.com/api-keys"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"capabilities": ["db", "ai"],
|
|
13
|
+
"requires": ["DB", "AI"],
|
|
14
|
+
"intent": {
|
|
15
|
+
"keywords": ["mini app", "miniapp", "frontend", "ui", "dashboard"],
|
|
16
|
+
"examples": ["simple dashboard", "farcaster miniapp"]
|
|
17
|
+
},
|
|
7
18
|
"agentContext": {
|
|
8
|
-
"summary": "A Farcaster miniapp using React + Vite frontend, Hono API on Cloudflare Workers, with D1 SQLite database.",
|
|
9
|
-
"full_text": "## Project Structure\n\n- `src/App.tsx` - React application entry point\n- `src/worker.ts` - Hono API routes for backend\n- `src/components/` - React components\n- `schema.sql` - D1 database schema\n- `wrangler.jsonc` - Cloudflare Workers configuration\n\n## Conventions\n\n- API routes are defined with Hono in `src/worker.ts`\n- Frontend uses Vite for bundling and is served as static assets\n- Database uses D1 prepared statements for queries\n- Secrets are managed via jack and pushed to Cloudflare\n- Wrangler is installed globally by jack, not in project dependencies\n\n## Resources\n\n- [Farcaster Miniapp Docs](https://miniapps.farcaster.xyz)\n- [Hono Documentation](https://hono.dev)\n- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1)"
|
|
19
|
+
"summary": "A Farcaster miniapp using React + Vite frontend, Hono API on Cloudflare Workers, with D1 SQLite database and AI capabilities.",
|
|
20
|
+
"full_text": "## Project Structure\n\n- `src/App.tsx` - React application entry point\n- `src/worker.ts` - Hono API routes for backend\n- `src/hooks/useAI.ts` - AI generation hook\n- `src/components/` - React components\n- `schema.sql` - D1 database schema\n- `wrangler.jsonc` - Cloudflare Workers configuration\n\n## Conventions\n\n- API routes are defined with Hono in `src/worker.ts`\n- Frontend uses Vite for bundling and is served as static assets\n- Database uses D1 prepared statements for queries\n- Secrets are managed via jack and pushed to Cloudflare\n- Wrangler is installed globally by jack, not in project dependencies\n\n## AI Feature\n\nThe template includes a `useAI()` hook for AI text generation.\n\n### Usage\n```tsx\nconst { generate, isLoading, error } = useAI();\nconst result = await generate('Your prompt here');\n// result.result = AI response text\n// result.provider = 'openai' | 'workers-ai'\n```\n\n### Providers\n- **Workers AI** (default): Free, uses Llama 3.1 8B. No API key needed.\n- **OpenAI** (optional): Better quality with gpt-4o-mini. Requires OPENAI_API_KEY.\n\n### Costs\n- **Workers AI**: Free (included in Cloudflare Workers)\n- **OpenAI gpt-4o-mini**: ~$0.0001 per request (~$0.15/1M input + $0.60/1M output tokens)\n- Typical usage (10 requests/day): ~$0.03/month\n\n### Rate Limiting\n- 10 requests per minute per IP address\n- Returns 429 with Retry-After header when exceeded\n- Rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining\n\n## Resources\n\n- [Farcaster Miniapp Docs](https://miniapps.farcaster.xyz)\n- [Hono Documentation](https://hono.dev)\n- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1)\n- [Workers AI Models](https://developers.cloudflare.com/workers-ai/models/)"
|
|
10
21
|
},
|
|
11
22
|
"hooks": {
|
|
12
23
|
"preDeploy": [
|