@getjack/jack 0.1.22 → 0.1.23

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/commands/clone.ts +5 -5
  3. package/src/commands/down.ts +44 -69
  4. package/src/commands/link.ts +9 -6
  5. package/src/commands/new.ts +54 -76
  6. package/src/commands/publish.ts +7 -2
  7. package/src/commands/secrets.ts +2 -1
  8. package/src/commands/services.ts +41 -15
  9. package/src/commands/update.ts +2 -2
  10. package/src/index.ts +43 -2
  11. package/src/lib/agent-integration.ts +217 -0
  12. package/src/lib/auth/login-flow.ts +2 -1
  13. package/src/lib/binding-validator.ts +2 -3
  14. package/src/lib/build-helper.ts +7 -1
  15. package/src/lib/hooks.ts +101 -21
  16. package/src/lib/managed-down.ts +32 -55
  17. package/src/lib/project-detection.ts +48 -21
  18. package/src/lib/project-operations.ts +31 -13
  19. package/src/lib/prompts.ts +16 -23
  20. package/src/lib/services/db-execute.ts +39 -0
  21. package/src/lib/services/sql-classifier.test.ts +2 -2
  22. package/src/lib/services/sql-classifier.ts +5 -4
  23. package/src/lib/version-check.ts +15 -10
  24. package/src/lib/zip-packager.ts +16 -0
  25. package/src/mcp/resources/index.ts +42 -2
  26. package/src/templates/index.ts +63 -3
  27. package/templates/ai-chat/.jack.json +29 -0
  28. package/templates/ai-chat/bun.lock +18 -0
  29. package/templates/ai-chat/package.json +14 -0
  30. package/templates/ai-chat/public/chat.js +149 -0
  31. package/templates/ai-chat/public/index.html +209 -0
  32. package/templates/ai-chat/src/index.ts +105 -0
  33. package/templates/ai-chat/wrangler.jsonc +12 -0
  34. package/templates/semantic-search/.jack.json +26 -0
  35. package/templates/semantic-search/bun.lock +18 -0
  36. package/templates/semantic-search/package.json +12 -0
  37. package/templates/semantic-search/public/app.js +120 -0
  38. package/templates/semantic-search/public/index.html +210 -0
  39. package/templates/semantic-search/schema.sql +5 -0
  40. package/templates/semantic-search/src/index.ts +144 -0
  41. package/templates/semantic-search/tsconfig.json +13 -0
  42. package/templates/semantic-search/wrangler.jsonc +27 -0
@@ -16,6 +16,7 @@ import {
16
16
  } from "../templates/index.ts";
17
17
  import type { EnvVar, Template } from "../templates/types.ts";
18
18
  import { generateAgentFiles } from "./agent-files.ts";
19
+ import { ensureAgentIntegration } from "./agent-integration.ts";
19
20
  import {
20
21
  getActiveAgents,
21
22
  getAgentDefinition,
@@ -535,14 +536,17 @@ async function runAutoDetectFlow(
535
536
  dryRun = false,
536
537
  ): Promise<AutoDetectResult> {
537
538
  // Step 1: Validate project (file count, size limits)
539
+ const validationSpin = reporter.spinner("Scanning project...");
538
540
  const validation = await validateProject(projectPath);
539
541
  if (!validation.valid) {
542
+ validationSpin.error("Project too large or invalid");
540
543
  track(Events.AUTO_DETECT_REJECTED, { reason: "validation_failed" });
541
544
  throw new JackError(
542
545
  JackErrorCode.VALIDATION_ERROR,
543
546
  validation.error || "Project validation failed",
544
547
  );
545
548
  }
549
+ validationSpin.success(`Scanned ${validation.fileCount} files`);
546
550
 
547
551
  // Step 2: Detect project type
548
552
  const detection = detectProjectType(projectPath);
@@ -565,9 +569,11 @@ async function runAutoDetectFlow(
565
569
  if (detection.type === "unknown") {
566
570
  track(Events.AUTO_DETECT_FAILED, { reason: "unknown_type" });
567
571
 
572
+ // Use detection.error if available (e.g., monorepo message), otherwise generic
568
573
  throw new JackError(
569
574
  JackErrorCode.VALIDATION_ERROR,
570
- "Could not detect project type\n\nSupported types:\n - Vite (React, Vue, etc.)\n - Hono API\n - SvelteKit (with @sveltejs/adapter-cloudflare)\n\nTo deploy manually, create a wrangler.jsonc file.\nDocs: https://docs.getjack.org/guides/manual-setup",
575
+ detection.error ||
576
+ "Could not detect project type\n\nSupported types:\n - Vite (React, Vue, etc.)\n - Hono API\n - SvelteKit (with @sveltejs/adapter-cloudflare)\n\nTo deploy manually, create a wrangler.jsonc file.\nDocs: https://docs.getjack.org/guides/manual-setup",
571
577
  );
572
578
  }
573
579
 
@@ -875,17 +881,17 @@ export async function createProject(
875
881
 
876
882
  // No match - prompt user to choose
877
883
  if (interactive) {
878
- const { select } = await import("@clack/prompts");
884
+ const { promptSelectValue, isCancel } = await import("./hooks.ts");
879
885
  console.error("");
880
886
  console.error(` No template matched for: "${intentPhrase}"`);
881
887
  console.error("");
882
888
 
883
- const choice = await select({
884
- message: "Select a template:",
885
- options: BUILTIN_TEMPLATES.map((t, i) => ({ value: t, label: `${i + 1}. ${t}` })),
886
- });
889
+ const choice = await promptSelectValue(
890
+ "Select a template:",
891
+ BUILTIN_TEMPLATES.map((t) => ({ value: t, label: t })),
892
+ );
887
893
 
888
- if (typeof choice !== "string") {
894
+ if (isCancel(choice) || typeof choice !== "string") {
889
895
  throw new JackError(JackErrorCode.VALIDATION_ERROR, "No template selected", undefined, {
890
896
  exitCode: 0,
891
897
  reported: true,
@@ -917,18 +923,18 @@ export async function createProject(
917
923
 
918
924
  // Multiple matches
919
925
  if (interactive) {
920
- const { select } = await import("@clack/prompts");
926
+ const { promptSelectValue, isCancel } = await import("./hooks.ts");
921
927
  console.error("");
922
928
  console.error(` Multiple templates matched: "${intentPhrase}"`);
923
929
  console.error("");
924
930
 
925
931
  const matchedNames = matches.map((m) => m.template);
926
- const choice = await select({
927
- message: "Select a template:",
928
- options: matchedNames.map((t, i) => ({ value: t, label: `${i + 1}. ${t}` })),
929
- });
932
+ const choice = await promptSelectValue(
933
+ "Select a template:",
934
+ matchedNames.map((t) => ({ value: t, label: t })),
935
+ );
930
936
 
931
- if (typeof choice !== "string") {
937
+ if (isCancel(choice) || typeof choice !== "string") {
932
938
  throw new JackError(JackErrorCode.VALIDATION_ERROR, "No template selected", undefined, {
933
939
  exitCode: 0,
934
940
  reported: true,
@@ -1609,6 +1615,18 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1609
1615
  deployMode = link?.deploy_mode ?? "byo";
1610
1616
  }
1611
1617
 
1618
+ // Ensure agent integration is set up (JACK.md, MCP config)
1619
+ // This is idempotent and runs silently
1620
+ try {
1621
+ await ensureAgentIntegration(projectPath, {
1622
+ projectName,
1623
+ silent: true,
1624
+ });
1625
+ } catch (err) {
1626
+ // Don't fail deploy if agent integration fails
1627
+ debug("Agent integration setup failed:", err);
1628
+ }
1629
+
1612
1630
  // Ensure wrangler is installed (auto-install if needed)
1613
1631
  if (!dryRun) {
1614
1632
  let installSpinner: OperationSpinner | null = null;
@@ -1,5 +1,7 @@
1
- import { isCancel, select, text } from "@clack/prompts";
1
+ import { text } from "@clack/prompts";
2
+ import { isCancel } from "./hooks.ts";
2
3
  import type { DetectedSecret } from "./env-parser.ts";
4
+ import { promptSelectValue } from "./hooks.ts";
3
5
  import { info, success, warn } from "./output.ts";
4
6
  import { getSavedSecrets, getSecretsPath, maskSecret, saveSecrets } from "./secrets.ts";
5
7
 
@@ -23,14 +25,11 @@ export async function promptSaveSecrets(detected: DetectedSecret[]): Promise<voi
23
25
  }
24
26
  console.error("");
25
27
 
26
- const action = await select({
27
- message: "What would you like to do?",
28
- options: [
29
- { value: "save", label: "Save to jack for future projects" },
30
- { value: "paste", label: "Paste additional secrets" },
31
- { value: "skip", label: "Skip for now" },
32
- ],
33
- });
28
+ const action = await promptSelectValue("What would you like to do?", [
29
+ { value: "save", label: "Save to jack for future projects" },
30
+ { value: "paste", label: "Paste additional secrets" },
31
+ { value: "skip", label: "Skip for now" },
32
+ ]);
34
33
 
35
34
  if (isCancel(action) || action === "skip") {
36
35
  return;
@@ -111,13 +110,10 @@ export async function promptUseSecrets(): Promise<Record<string, string> | null>
111
110
  }
112
111
  console.error("");
113
112
 
114
- const action = await select({
115
- message: "Use them for this project?",
116
- options: [
117
- { label: "Yes", value: "yes" },
118
- { label: "No", value: "no" },
119
- ],
120
- });
113
+ const action = await promptSelectValue("Use them for this project?", [
114
+ { value: "yes", label: "Yes" },
115
+ { value: "no", label: "No" },
116
+ ]);
121
117
 
122
118
  if (isCancel(action) || action !== "yes") {
123
119
  return null;
@@ -154,13 +150,10 @@ export async function promptUseSecretsFromList(
154
150
  return false;
155
151
  }
156
152
 
157
- const action = await select({
158
- message: "Use saved secrets for this project?",
159
- options: [
160
- { label: "Yes", value: "yes" },
161
- { label: "No", value: "no" },
162
- ],
163
- });
153
+ const action = await promptSelectValue("Use saved secrets for this project?", [
154
+ { value: "yes", label: "Yes" },
155
+ { value: "no", label: "No" },
156
+ ]);
164
157
 
165
158
  return !isCancel(action) && action === "yes";
166
159
  }
@@ -147,7 +147,26 @@ async function executeViaWrangler(
147
147
  .quiet();
148
148
 
149
149
  if (result.exitCode !== 0) {
150
+ // Wrangler outputs errors to stdout as JSON, not stderr
151
+ const stdout = result.stdout.toString().trim();
150
152
  const stderr = result.stderr.toString().trim();
153
+
154
+ // Try to parse JSON error from stdout first
155
+ try {
156
+ const data = JSON.parse(stdout);
157
+ if (data.error) {
158
+ // Wrangler error format: { error: { text: "...", notes: [{ text: "..." }] } }
159
+ const errorText = data.error.text || data.error.message || "Unknown error";
160
+ const notes = data.error.notes?.map((n: { text: string }) => n.text).join("; ");
161
+ return {
162
+ success: false,
163
+ error: notes ? `${errorText} (${notes})` : errorText,
164
+ };
165
+ }
166
+ } catch {
167
+ // Not JSON, fall through to stderr
168
+ }
169
+
151
170
  return {
152
171
  success: false,
153
172
  error: stderr || `Failed to execute SQL on ${databaseName}`,
@@ -172,6 +191,8 @@ async function executeViaWrangler(
172
191
  last_row_id: firstResult.meta.last_row_id,
173
192
  }
174
193
  : undefined,
194
+ // Capture error details from wrangler response
195
+ error: firstResult.error || firstResult.message,
175
196
  };
176
197
  }
177
198
 
@@ -198,7 +219,25 @@ async function executeFileViaWrangler(
198
219
  .quiet();
199
220
 
200
221
  if (result.exitCode !== 0) {
222
+ // Wrangler outputs errors to stdout as JSON, not stderr
223
+ const stdout = result.stdout.toString().trim();
201
224
  const stderr = result.stderr.toString().trim();
225
+
226
+ // Try to parse JSON error from stdout first
227
+ try {
228
+ const data = JSON.parse(stdout);
229
+ if (data.error) {
230
+ const errorText = data.error.text || data.error.message || "Unknown error";
231
+ const notes = data.error.notes?.map((n: { text: string }) => n.text).join("; ");
232
+ return {
233
+ success: false,
234
+ error: notes ? `${errorText} (${notes})` : errorText,
235
+ };
236
+ }
237
+ } catch {
238
+ // Not JSON, fall through to stderr
239
+ }
240
+
202
241
  return {
203
242
  success: false,
204
243
  error: stderr || `Failed to execute SQL file on ${databaseName}`,
@@ -229,9 +229,9 @@ describe("sql-classifier", () => {
229
229
  expect(result.operation).toBe("SELECT");
230
230
  });
231
231
 
232
- it("treats unknown operations as write for safety", () => {
232
+ it("treats unknown operations as read (let SQLite handle errors)", () => {
233
233
  const result = classifyStatement("VACUUM");
234
- expect(result.risk).toBe("write");
234
+ expect(result.risk).toBe("read");
235
235
  expect(result.operation).toBe("VACUUM");
236
236
  });
237
237
 
@@ -143,8 +143,8 @@ export function classifyStatement(sql: string): ClassifiedStatement {
143
143
  case "SELECT":
144
144
  return { sql: trimmed, risk: "read", operation: "SELECT" };
145
145
  default:
146
- // Unknown CTE operation - default to write for safety
147
- return { sql: trimmed, risk: "write", operation };
146
+ // Unknown CTE operation - let SQLite handle it
147
+ return { sql: trimmed, risk: "read", operation };
148
148
  }
149
149
  }
150
150
 
@@ -205,8 +205,9 @@ export function classifyStatement(sql: string): ClassifiedStatement {
205
205
  return { sql: trimmed, risk: "read", operation: "PRAGMA" };
206
206
  }
207
207
 
208
- // Unknown operations default to write for safety
209
- return { sql: trimmed, risk: "write", operation };
208
+ // Unknown operations default to read - let SQLite handle syntax errors
209
+ // Invalid SQL can't modify data, so no point requiring --write for gibberish
210
+ return { sql: trimmed, risk: "read", operation };
210
211
  }
211
212
 
212
213
  /**
@@ -85,20 +85,25 @@ function isNewerVersion(latest: string, current: string): boolean {
85
85
  /**
86
86
  * Check if an update is available (uses cache, non-blocking)
87
87
  * Returns the latest version if newer, null otherwise
88
+ * @param skipCache - If true, bypass the cache and always fetch from npm
88
89
  */
89
- export async function checkForUpdate(): Promise<string | null> {
90
+ export async function checkForUpdate(
91
+ skipCache = false,
92
+ ): Promise<string | null> {
90
93
  const currentVersion = getCurrentVersion();
91
94
 
92
- // Check cache first
93
- const cache = await readVersionCache();
94
- const now = Date.now();
95
-
96
- if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
97
- // Use cached value
98
- if (isNewerVersion(cache.latestVersion, currentVersion)) {
99
- return cache.latestVersion;
95
+ // Check cache first (unless skipCache is true)
96
+ if (!skipCache) {
97
+ const cache = await readVersionCache();
98
+ const now = Date.now();
99
+
100
+ if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
101
+ // Use cached value
102
+ if (isNewerVersion(cache.latestVersion, currentVersion)) {
103
+ return cache.latestVersion;
104
+ }
105
+ return null;
100
106
  }
101
- return null;
102
107
  }
103
108
 
104
109
  // Fetch fresh version (don't await in caller for non-blocking)
@@ -45,6 +45,12 @@ export interface ManifestData {
45
45
  vars?: Record<string, string>;
46
46
  r2?: Array<{ binding: string; bucket_name: string }>;
47
47
  kv?: Array<{ binding: string }>;
48
+ vectorize?: Array<{
49
+ binding: string;
50
+ preset?: string;
51
+ dimensions?: number;
52
+ metric?: string;
53
+ }>;
48
54
  };
49
55
  }
50
56
 
@@ -224,6 +230,16 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
224
230
  }));
225
231
  }
226
232
 
233
+ // Extract Vectorize bindings
234
+ if (config.vectorize && config.vectorize.length > 0) {
235
+ bindings.vectorize = config.vectorize.map((vec) => ({
236
+ binding: vec.binding,
237
+ preset: vec.preset,
238
+ dimensions: vec.dimensions,
239
+ metric: vec.metric,
240
+ }));
241
+ }
242
+
227
243
  // Return undefined if no bindings were extracted
228
244
  return Object.keys(bindings).length > 0 ? bindings : undefined;
229
245
  }
@@ -67,15 +67,30 @@ export function registerResources(
67
67
 
68
68
  if (uri === "agents://context") {
69
69
  const projectPath = options.projectPath ?? process.cwd();
70
+ const jackPath = join(projectPath, "JACK.md");
70
71
  const agentsPath = join(projectPath, "AGENTS.md");
71
72
  const claudePath = join(projectPath, "CLAUDE.md");
72
73
 
73
74
  const contents: string[] = [];
74
75
 
76
+ // Try to read JACK.md first (jack-specific context)
77
+ if (existsSync(jackPath)) {
78
+ try {
79
+ const jackContent = await Bun.file(jackPath).text();
80
+ contents.push("# JACK.md\n\n");
81
+ contents.push(jackContent);
82
+ } catch {
83
+ // Ignore read errors
84
+ }
85
+ }
86
+
75
87
  // Try to read AGENTS.md
76
88
  if (existsSync(agentsPath)) {
77
89
  try {
78
90
  const agentsContent = await Bun.file(agentsPath).text();
91
+ if (contents.length > 0) {
92
+ contents.push("\n\n---\n\n");
93
+ }
79
94
  contents.push("# AGENTS.md\n\n");
80
95
  contents.push(agentsContent);
81
96
  } catch {
@@ -97,13 +112,38 @@ export function registerResources(
97
112
  }
98
113
  }
99
114
 
115
+ // If no agent files found, return jack guidance as fallback
100
116
  if (contents.length === 0) {
117
+ const fallbackGuidance = `# Jack Project
118
+
119
+ This project is managed by jack.
120
+
121
+ ## Commands
122
+ - \`jack ship\` - Deploy changes
123
+ - \`jack logs\` - Stream production logs
124
+ - \`jack services\` - Manage databases and other services
125
+
126
+ **Always use jack commands. Never use wrangler directly.**
127
+
128
+ ## MCP Tools Available
129
+
130
+ If connected, prefer \`mcp__jack__*\` tools over CLI:
131
+ - \`mcp__jack__deploy_project\` - Deploy changes
132
+ - \`mcp__jack__execute_sql\` - Query databases
133
+ - \`mcp__jack__get_project_status\` - Check status
134
+
135
+ ## Documentation
136
+
137
+ Full docs: https://docs.getjack.org/llms-full.txt
138
+
139
+ Check for JACK.md in the project root for project-specific instructions.
140
+ `;
101
141
  return {
102
142
  contents: [
103
143
  {
104
144
  uri,
105
- mimeType: "text/plain",
106
- text: "No agent context files found in project directory",
145
+ mimeType: "text/markdown",
146
+ text: fallbackGuidance,
107
147
  },
108
148
  ],
109
149
  };
@@ -10,7 +10,7 @@ import type { Template } from "./types";
10
10
  // Resolve templates directory relative to this file (src/templates -> templates)
11
11
  const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates");
12
12
 
13
- export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs", "saas"];
13
+ export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs", "saas", "simple-api-starter", "ai-chat", "semantic-search"];
14
14
 
15
15
  /**
16
16
  * Resolved template with origin tracking for lineage
@@ -104,8 +104,57 @@ async function loadTemplate(name: string): Promise<Template> {
104
104
  // Internal files that should be excluded from templates
105
105
  const INTERNAL_FILES = [".jack.json", ".jack/template.json"];
106
106
 
107
+ // Wrangler config files that need sanitization when forking (JSONC only, no TOML support)
108
+ const WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json"];
109
+
107
110
  /**
108
- * Extract zip buffer to file map, excluding internal files
111
+ * Strip provider-specific IDs from wrangler config bindings.
112
+ * When forking a template, the original author's resource IDs won't work
113
+ * for the new user - wrangler 4.45.0+ will auto-provision new resources.
114
+ *
115
+ * Stripped fields:
116
+ * - D1: database_id (author's database)
117
+ * - KV: id (author's namespace)
118
+ * - R2: nothing (bucket_name is just a name, not a provider ID)
119
+ */
120
+ function sanitizeWranglerConfig(content: string, filename: string): string {
121
+ // Only handle JSON/JSONC files
122
+ if (!filename.endsWith(".json") && !filename.endsWith(".jsonc")) {
123
+ return content;
124
+ }
125
+
126
+ try {
127
+ const config = parseJsonc(content);
128
+
129
+ // D1: strip database_id
130
+ if (Array.isArray(config.d1_databases)) {
131
+ for (const db of config.d1_databases) {
132
+ if (db && typeof db === "object" && "database_id" in db) {
133
+ delete db.database_id;
134
+ }
135
+ }
136
+ }
137
+
138
+ // KV: strip id
139
+ if (Array.isArray(config.kv_namespaces)) {
140
+ for (const kv of config.kv_namespaces) {
141
+ if (kv && typeof kv === "object" && "id" in kv) {
142
+ delete kv.id;
143
+ }
144
+ }
145
+ }
146
+
147
+ // Re-serialize with formatting
148
+ return JSON.stringify(config, null, "\t");
149
+ } catch {
150
+ // If parsing fails, return original content
151
+ return content;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Extract zip buffer to file map, excluding internal files.
157
+ * Sanitizes wrangler config to remove provider IDs (D1 database_id, KV id).
109
158
  */
110
159
  function extractZipToFiles(zipData: ArrayBuffer): Record<string, string> {
111
160
  const files: Record<string, string> = {};
@@ -117,7 +166,17 @@ function extractZipToFiles(zipData: ArrayBuffer): Record<string, string> {
117
166
 
118
167
  // Skip internal files
119
168
  if (path && !INTERNAL_FILES.includes(path)) {
120
- files[path] = new TextDecoder().decode(content);
169
+ let fileContent = new TextDecoder().decode(content);
170
+
171
+ // Sanitize wrangler config files to strip provider IDs
172
+ // This ensures forked projects create new resources instead of
173
+ // trying to use the original author's resources
174
+ const filename = path.split("/").pop() || path;
175
+ if (filename === "wrangler.jsonc" || filename === "wrangler.json") {
176
+ fileContent = sanitizeWranglerConfig(fileContent, filename);
177
+ }
178
+
179
+ files[path] = fileContent;
121
180
  }
122
181
  }
123
182
 
@@ -288,6 +347,7 @@ export function renderTemplate(template: Template, vars: { name: string }): Temp
288
347
  rendered[path] = content
289
348
  .replace(/jack-template-db/g, `${vars.name}-db`)
290
349
  .replace(/jack-template-cache/g, `${vars.name}-cache`)
350
+ .replace(/jack-template-vectors/g, `${vars.name}-vectors`)
291
351
  .replace(/jack-template/g, vars.name);
292
352
  }
293
353
  return { ...template, files: rendered };
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "ai-chat",
3
+ "description": "AI chat with streaming UI (free Cloudflare AI)",
4
+ "secrets": [],
5
+ "capabilities": ["ai"],
6
+ "intent": {
7
+ "keywords": ["ai", "chat", "llm", "mistral", "completion", "chatbot"],
8
+ "examples": ["AI chatbot", "chat interface", "LLM chat app"]
9
+ },
10
+ "hooks": {
11
+ "postDeploy": [
12
+ {
13
+ "action": "clipboard",
14
+ "text": "{{url}}",
15
+ "message": "URL copied to clipboard"
16
+ },
17
+ {
18
+ "action": "box",
19
+ "title": "AI Chat: {{name}}",
20
+ "lines": [
21
+ "{{url}}",
22
+ "",
23
+ "Open in browser to start chatting!",
24
+ "Rate limit: 10 requests/minute"
25
+ ]
26
+ }
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "jack-template",
7
+ "devDependencies": {
8
+ "@cloudflare/workers-types": "^4.20241205.0",
9
+ "typescript": "^5.0.0",
10
+ },
11
+ },
12
+ },
13
+ "packages": {
14
+ "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260120.0", "", {}, "sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw=="],
15
+
16
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
17
+ }
18
+ }
@@ -0,0 +1,14 @@
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
+ "devDependencies": {
11
+ "@cloudflare/workers-types": "^4.20241205.0",
12
+ "typescript": "^5.0.0"
13
+ }
14
+ }