@getjack/jack 0.1.2 → 0.1.4

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 (91) hide show
  1. package/README.md +77 -29
  2. package/package.json +54 -47
  3. package/src/commands/agents.ts +145 -10
  4. package/src/commands/down.ts +110 -102
  5. package/src/commands/feedback.ts +189 -0
  6. package/src/commands/init.ts +8 -12
  7. package/src/commands/login.ts +88 -0
  8. package/src/commands/logout.ts +14 -0
  9. package/src/commands/logs.ts +21 -0
  10. package/src/commands/mcp.ts +134 -7
  11. package/src/commands/new.ts +43 -17
  12. package/src/commands/open.ts +13 -6
  13. package/src/commands/projects.ts +269 -143
  14. package/src/commands/secrets.ts +413 -0
  15. package/src/commands/services.ts +96 -123
  16. package/src/commands/ship.ts +5 -1
  17. package/src/commands/whoami.ts +31 -0
  18. package/src/index.ts +218 -144
  19. package/src/lib/agent-files.ts +34 -0
  20. package/src/lib/agents.ts +390 -22
  21. package/src/lib/asset-hash.ts +50 -0
  22. package/src/lib/auth/client.ts +115 -0
  23. package/src/lib/auth/constants.ts +5 -0
  24. package/src/lib/auth/guard.ts +57 -0
  25. package/src/lib/auth/index.ts +18 -0
  26. package/src/lib/auth/store.ts +54 -0
  27. package/src/lib/binding-validator.ts +136 -0
  28. package/src/lib/build-helper.ts +211 -0
  29. package/src/lib/cloudflare-api.ts +24 -0
  30. package/src/lib/config.ts +5 -6
  31. package/src/lib/control-plane.ts +295 -0
  32. package/src/lib/debug.ts +3 -1
  33. package/src/lib/deploy-mode.ts +93 -0
  34. package/src/lib/deploy-upload.ts +92 -0
  35. package/src/lib/errors.ts +2 -0
  36. package/src/lib/github.ts +31 -1
  37. package/src/lib/hooks.ts +4 -12
  38. package/src/lib/intent.ts +88 -0
  39. package/src/lib/jsonc.ts +125 -0
  40. package/src/lib/local-paths.test.ts +902 -0
  41. package/src/lib/local-paths.ts +258 -0
  42. package/src/lib/managed-deploy.ts +175 -0
  43. package/src/lib/managed-down.ts +159 -0
  44. package/src/lib/mcp-config.ts +55 -34
  45. package/src/lib/names.ts +9 -29
  46. package/src/lib/project-operations.ts +676 -249
  47. package/src/lib/project-resolver.ts +476 -0
  48. package/src/lib/registry.ts +76 -37
  49. package/src/lib/resources.ts +196 -0
  50. package/src/lib/schema.ts +30 -1
  51. package/src/lib/storage/file-filter.ts +1 -0
  52. package/src/lib/storage/index.ts +5 -1
  53. package/src/lib/telemetry.ts +14 -0
  54. package/src/lib/tty.ts +15 -0
  55. package/src/lib/zip-packager.ts +255 -0
  56. package/src/mcp/resources/index.ts +8 -2
  57. package/src/mcp/server.ts +32 -4
  58. package/src/mcp/tools/index.ts +35 -13
  59. package/src/mcp/types.ts +6 -0
  60. package/src/mcp/utils.ts +1 -1
  61. package/src/templates/index.ts +42 -4
  62. package/src/templates/types.ts +13 -0
  63. package/templates/CLAUDE.md +166 -0
  64. package/templates/api/.jack.json +4 -0
  65. package/templates/api/bun.lock +1 -0
  66. package/templates/api/wrangler.jsonc +5 -0
  67. package/templates/hello/.jack.json +28 -0
  68. package/templates/hello/package.json +10 -0
  69. package/templates/hello/src/index.ts +11 -0
  70. package/templates/hello/tsconfig.json +11 -0
  71. package/templates/hello/wrangler.jsonc +5 -0
  72. package/templates/miniapp/.jack.json +15 -4
  73. package/templates/miniapp/bun.lock +135 -40
  74. package/templates/miniapp/index.html +1 -0
  75. package/templates/miniapp/package.json +3 -1
  76. package/templates/miniapp/public/.well-known/farcaster.json +7 -5
  77. package/templates/miniapp/public/icon.png +0 -0
  78. package/templates/miniapp/public/og.png +0 -0
  79. package/templates/miniapp/schema.sql +8 -0
  80. package/templates/miniapp/src/App.tsx +254 -3
  81. package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
  82. package/templates/miniapp/src/hooks/useAI.ts +35 -0
  83. package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
  84. package/templates/miniapp/src/hooks/useShare.ts +76 -0
  85. package/templates/miniapp/src/index.css +15 -0
  86. package/templates/miniapp/src/lib/api.ts +2 -1
  87. package/templates/miniapp/src/worker.ts +515 -1
  88. package/templates/miniapp/wrangler.jsonc +15 -3
  89. package/LICENSE +0 -190
  90. package/src/commands/cloud.ts +0 -230
  91. package/templates/api/wrangler.toml +0 -3
@@ -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
- createProject,
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 cloud backup status.",
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 cloud-backed projects.",
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 (request.params.name) {
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
- return await listAllProjects(filter);
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: ${request.params.name}`);
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 {
@@ -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 = JSON.parse(await readFile(metadataPath, "utf-8"));
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 → miniapp (omakase default)
103
+ // No template → hello (omakase default)
88
104
  if (!template) {
89
- return loadTemplate("miniapp");
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
@@ -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
  }
@@ -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/`
@@ -2,6 +2,10 @@
2
2
  "name": "api",
3
3
  "description": "Hono API with routing",
4
4
  "secrets": [],
5
+ "intent": {
6
+ "keywords": ["api", "endpoint", "webhook", "worker"],
7
+ "examples": ["stripe webhook API", "REST API endpoint"]
8
+ },
5
9
  "hooks": {
6
10
  "postDeploy": [
7
11
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
+ "configVersion": 0,
3
4
  "workspaces": {
4
5
  "": {
5
6
  "name": "jack-template",
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "jack-template",
3
+ "main": "src/index.ts",
4
+ "compatibility_date": "2024-12-01"
5
+ }
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "jack-template",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "wrangler dev",
8
+ "deploy": "wrangler deploy"
9
+ }
10
+ }
@@ -0,0 +1,11 @@
1
+ export default {
2
+ async fetch() {
3
+ return new Response(
4
+ JSON.stringify({
5
+ message: "Hello from jack!",
6
+ timestamp: new Date().toISOString(),
7
+ }),
8
+ { headers: { "Content-Type": "application/json" } },
9
+ );
10
+ },
11
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["@cloudflare/workers-types"]
9
+ },
10
+ "include": ["src/**/*"]
11
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "jack-template",
3
+ "main": "src/index.ts",
4
+ "compatibility_date": "2024-12-01"
5
+ }
@@ -2,11 +2,22 @@
2
2
  "name": "miniapp",
3
3
  "description": "Farcaster Miniapp (React + Vite)",
4
4
  "secrets": ["NEYNAR_API_KEY"],
5
- "capabilities": ["db"],
6
- "requires": ["DB"],
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": [