@getjack/jack 0.1.34 → 0.1.36

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 (90) hide show
  1. package/README.md +6 -6
  2. package/package.json +1 -1
  3. package/src/commands/down.ts +39 -7
  4. package/src/commands/link.ts +2 -4
  5. package/src/commands/logs.ts +2 -4
  6. package/src/commands/mcp.ts +12 -10
  7. package/src/commands/services.ts +4 -2
  8. package/src/commands/sync.ts +5 -6
  9. package/src/commands/update.ts +1 -0
  10. package/src/index.ts +8 -0
  11. package/src/lib/auth/client.ts +5 -2
  12. package/src/lib/binding-validator.ts +39 -3
  13. package/src/lib/build-helper.ts +18 -19
  14. package/src/lib/control-plane.ts +45 -0
  15. package/src/lib/do-config.ts +110 -0
  16. package/src/lib/do-export-validator.ts +26 -0
  17. package/src/lib/jsonc-edit.ts +292 -0
  18. package/src/lib/managed-deploy.ts +36 -1
  19. package/src/lib/project-link.ts +37 -0
  20. package/src/lib/project-operations.ts +31 -66
  21. package/src/lib/resources.ts +4 -5
  22. package/src/lib/schema.ts +8 -12
  23. package/src/lib/services/db-create.ts +2 -2
  24. package/src/lib/services/db-execute.ts +9 -6
  25. package/src/lib/services/db-list.ts +6 -4
  26. package/src/lib/services/endpoint-test.ts +275 -0
  27. package/src/lib/services/project-delete.ts +190 -0
  28. package/src/lib/services/project-environment.ts +579 -0
  29. package/src/lib/services/storage-config.ts +7 -309
  30. package/src/lib/services/storage-create.ts +2 -1
  31. package/src/lib/services/storage-delete.ts +3 -2
  32. package/src/lib/services/storage-info.ts +2 -1
  33. package/src/lib/services/storage-list.ts +6 -3
  34. package/src/lib/services/vectorize-config.ts +7 -264
  35. package/src/lib/services/vectorize-create.ts +2 -1
  36. package/src/lib/services/vectorize-delete.ts +6 -4
  37. package/src/lib/services/vectorize-list.ts +6 -3
  38. package/src/lib/storage/index.ts +21 -23
  39. package/src/lib/telemetry.ts +1 -0
  40. package/src/lib/wrangler-config.ts +43 -312
  41. package/src/lib/zip-packager.ts +28 -0
  42. package/src/mcp/test-utils.ts +31 -0
  43. package/src/mcp/tools/index.ts +280 -2
  44. package/src/templates/index.ts +5 -0
  45. package/src/templates/types.ts +4 -0
  46. package/templates/AI-BINDINGS.md +34 -76
  47. package/templates/CLAUDE.md +1 -1
  48. package/templates/ai-chat/src/index.ts +7 -14
  49. package/templates/ai-chat/src/jack-ai.ts +0 -6
  50. package/templates/chat/.jack.json +45 -0
  51. package/templates/chat/bun.lock +1584 -0
  52. package/templates/chat/components.json +23 -0
  53. package/templates/chat/index.html +12 -0
  54. package/templates/chat/package.json +41 -0
  55. package/templates/chat/src/chat-agent.ts +63 -0
  56. package/templates/chat/src/client/app.tsx +189 -0
  57. package/templates/chat/src/client/chat.tsx +222 -0
  58. package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
  59. package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
  60. package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
  61. package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
  62. package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
  63. package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
  64. package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
  65. package/templates/chat/src/client/components/ui/button.tsx +38 -0
  66. package/templates/chat/src/client/lib/utils.ts +6 -0
  67. package/templates/chat/src/client/main.tsx +11 -0
  68. package/templates/chat/src/client/styles.css +125 -0
  69. package/templates/chat/src/index.ts +25 -0
  70. package/templates/chat/src/jack-ai.ts +94 -0
  71. package/templates/chat/tsconfig.json +18 -0
  72. package/templates/chat/vite.config.ts +14 -0
  73. package/templates/chat/wrangler.jsonc +18 -0
  74. package/templates/cron/.jack.json +18 -28
  75. package/templates/cron/schema.sql +10 -20
  76. package/templates/cron/src/admin.ts +321 -0
  77. package/templates/cron/src/index.ts +151 -81
  78. package/templates/cron/src/monitor.ts +124 -0
  79. package/templates/semantic-search/src/index.ts +5 -43
  80. package/templates/semantic-search/src/jack-ai.ts +0 -6
  81. package/templates/telegram-bot/.jack.json +56 -0
  82. package/templates/telegram-bot/bun.lock +41 -0
  83. package/templates/telegram-bot/package.json +16 -0
  84. package/templates/telegram-bot/src/index.ts +236 -0
  85. package/templates/telegram-bot/src/jack-ai.ts +100 -0
  86. package/templates/telegram-bot/tsconfig.json +11 -0
  87. package/templates/telegram-bot/wrangler.jsonc +8 -0
  88. package/templates/cron/src/jobs.ts +0 -139
  89. package/templates/cron/src/webhooks.ts +0 -95
  90. package/templates/semantic-search/src/jack-vectorize.ts +0 -169
@@ -1,11 +1,11 @@
1
1
  import { Hono } from "hono";
2
2
  import { cors } from "hono/cors";
3
- import { createJob, getPendingJobs, processJob, retryFailedJobs } from "./jobs";
4
- import { logWebhookEvent, verifyWebhookSignature } from "./webhooks";
3
+ import { runChecks } from "./monitor";
4
+ import { adminHTML } from "./admin";
5
5
 
6
6
  type Bindings = {
7
7
  DB: D1Database;
8
- WEBHOOK_SECRET: string;
8
+ MONITOR_URLS?: string;
9
9
  };
10
10
 
11
11
  const app = new Hono<{ Bindings: Bindings }>();
@@ -13,105 +13,175 @@ const app = new Hono<{ Bindings: Bindings }>();
13
13
  app.use("/*", cors());
14
14
 
15
15
  app.get("/", (c) => {
16
- return c.json({
17
- message: "Background worker running",
18
- name: "jack-template",
19
- });
16
+ return c.html(adminHTML());
20
17
  });
21
18
 
22
19
  app.get("/health", (c) => {
23
20
  return c.json({ status: "ok", timestamp: Date.now() });
24
21
  });
25
22
 
26
- // Cron handler - called by scheduler on POST /__scheduled
27
- app.post("/__scheduled", async (c) => {
23
+ // Rich per-group uptime stats (URLs hidden)
24
+ app.get("/api/status", async (c) => {
28
25
  const db = c.env.DB;
26
+ const now = Math.floor(Date.now() / 1000);
27
+ const DAY = 86400;
29
28
 
30
- // Retry failed jobs that haven't exceeded max attempts
31
- const retried = await retryFailedJobs(db);
32
-
33
- // Get pending jobs that are ready to run
34
- const jobs = await getPendingJobs(db, 10);
35
- let processed = 0;
36
-
37
- for (const job of jobs) {
38
- await processJob(db, job);
39
- processed++;
40
- }
41
-
42
- return c.json({ processed, retried });
43
- });
29
+ const { results: groups } = await db
30
+ .prepare(
31
+ `SELECT
32
+ group_name,
33
+ COUNT(DISTINCT url) as endpoint_count,
34
+ MAX(created_at) as last_check_at,
35
+ SUM(CASE WHEN created_at > ?1 THEN 1 ELSE 0 END) as checks_24h,
36
+ SUM(CASE WHEN created_at > ?1 AND ok = 1 THEN 1 ELSE 0 END) as up_24h,
37
+ SUM(CASE WHEN created_at > ?2 THEN 1 ELSE 0 END) as checks_7d,
38
+ SUM(CASE WHEN created_at > ?2 AND ok = 1 THEN 1 ELSE 0 END) as up_7d,
39
+ COUNT(*) as checks_30d,
40
+ SUM(ok) as up_30d,
41
+ ROUND(AVG(CASE WHEN created_at > ?1 THEN latency_ms END)) as avg_latency,
42
+ MIN(CASE WHEN created_at > ?1 THEN latency_ms END) as min_latency,
43
+ MAX(CASE WHEN created_at > ?1 THEN latency_ms END) as max_latency
44
+ FROM checks
45
+ GROUP BY group_name
46
+ ORDER BY group_name`,
47
+ )
48
+ .bind(now - DAY, now - 7 * DAY)
49
+ .all();
44
50
 
45
- // Webhook ingestion endpoint
46
- app.post("/webhook", async (c) => {
47
- const db = c.env.DB;
48
- const body = await c.req.text();
49
- const signature = c.req.header("X-Signature") || "";
50
-
51
- // Verify webhook signature
52
- const valid = await verifyWebhookSignature(
53
- body,
54
- signature,
55
- c.env.WEBHOOK_SECRET,
51
+ const monitors = await Promise.all(
52
+ (groups as Array<Record<string, unknown>>).map(async (g) => {
53
+ const groupName = g.group_name as string;
54
+
55
+ // Recent checks for status bar + sparkline (newest first → reverse for display)
56
+ const { results: recentRaw } = await db
57
+ .prepare(
58
+ "SELECT ok, latency_ms FROM checks WHERE group_name = ? ORDER BY created_at DESC LIMIT 48",
59
+ )
60
+ .bind(groupName)
61
+ .all();
62
+ const recent = (
63
+ recentRaw as Array<{ ok: number; latency_ms: number }>
64
+ ).reverse();
65
+
66
+ // Uptime streak: time since last failure (or since first check if none)
67
+ const lastFail = (await db
68
+ .prepare(
69
+ "SELECT created_at FROM checks WHERE group_name = ? AND ok = 0 ORDER BY created_at DESC LIMIT 1",
70
+ )
71
+ .bind(groupName)
72
+ .first()) as { created_at: number } | null;
73
+
74
+ const firstCheck = (await db
75
+ .prepare(
76
+ "SELECT created_at FROM checks WHERE group_name = ? ORDER BY created_at ASC LIMIT 1",
77
+ )
78
+ .bind(groupName)
79
+ .first()) as { created_at: number } | null;
80
+
81
+ const streakStart = lastFail
82
+ ? lastFail.created_at
83
+ : firstCheck
84
+ ? firstCheck.created_at
85
+ : now;
86
+ const streakSeconds = Math.max(0, now - streakStart);
87
+
88
+ // Current status from latest check per URL
89
+ const { results: latestPerUrl } = await db
90
+ .prepare(
91
+ `SELECT c.ok FROM checks c
92
+ INNER JOIN (
93
+ SELECT url, MAX(created_at) as max_ts
94
+ FROM checks WHERE group_name = ? GROUP BY url
95
+ ) latest ON c.url = latest.url AND c.created_at = latest.max_ts
96
+ WHERE c.group_name = ?`,
97
+ )
98
+ .bind(groupName, groupName)
99
+ .all();
100
+
101
+ const allUp =
102
+ latestPerUrl.length > 0 &&
103
+ latestPerUrl.every((r: Record<string, unknown>) => r.ok);
104
+
105
+ function pct(up: number, total: number): number | null {
106
+ return total > 0 ? Math.round((1000 * up) / total) / 10 : null;
107
+ }
108
+
109
+ return {
110
+ group_name: groupName,
111
+ endpoint_count: g.endpoint_count,
112
+ all_up: allUp,
113
+ streak_seconds: streakSeconds,
114
+ last_check_at: g.last_check_at,
115
+ periods: {
116
+ "24h": {
117
+ uptime_pct: pct(g.up_24h as number, g.checks_24h as number),
118
+ failed_checks: (g.checks_24h as number) - (g.up_24h as number),
119
+ total_checks: g.checks_24h,
120
+ },
121
+ "7d": {
122
+ uptime_pct: pct(g.up_7d as number, g.checks_7d as number),
123
+ failed_checks: (g.checks_7d as number) - (g.up_7d as number),
124
+ total_checks: g.checks_7d,
125
+ },
126
+ "30d": {
127
+ uptime_pct: pct(g.up_30d as number, g.checks_30d as number),
128
+ failed_checks: (g.checks_30d as number) - (g.up_30d as number),
129
+ total_checks: g.checks_30d,
130
+ },
131
+ },
132
+ latency: {
133
+ avg: g.avg_latency || 0,
134
+ min: g.min_latency || 0,
135
+ max: g.max_latency || 0,
136
+ },
137
+ recent_checks: recent,
138
+ };
139
+ }),
56
140
  );
57
- if (!valid) {
58
- return c.json({ error: "Invalid signature" }, 401);
59
- }
60
-
61
- // Parse and log the event
62
- let parsed: Record<string, unknown>;
63
- try {
64
- parsed = JSON.parse(body);
65
- } catch {
66
- return c.json({ error: "Invalid JSON body" }, 400);
67
- }
68
-
69
- const eventType = (parsed.event as string) || "unknown";
70
- const source = (parsed.source as string) || "unknown";
71
-
72
- const eventId = await logWebhookEvent(db, {
73
- source,
74
- eventType,
75
- payload: body,
76
- });
77
141
 
78
- // Create a job from the webhook event for async processing
79
- await createJob(db, {
80
- type: `webhook.${eventType}`,
81
- payload: { webhookEventId: eventId, data: parsed.data || {} },
82
- });
83
-
84
- return c.json({ received: true, id: eventId });
142
+ return c.json({ monitors });
85
143
  });
86
144
 
87
- // List recent jobs
88
- app.get("/jobs", async (c) => {
145
+ // Recent checks (URLs hidden)
146
+ app.get("/api/checks", async (c) => {
89
147
  const db = c.env.DB;
90
-
91
148
  const { results } = await db
92
149
  .prepare(
93
- "SELECT * FROM jobs ORDER BY created_at DESC LIMIT 50",
150
+ "SELECT id, group_name, status_code, latency_ms, ok, source, error, created_at FROM checks ORDER BY created_at DESC LIMIT 100",
94
151
  )
95
152
  .all();
96
-
97
- return c.json({ jobs: results });
153
+ return c.json({ checks: results });
98
154
  });
99
155
 
100
- // Get a single job by ID
101
- app.get("/jobs/:id", async (c) => {
102
- const db = c.env.DB;
103
- const id = c.req.param("id");
104
-
105
- const job = await db
106
- .prepare("SELECT * FROM jobs WHERE id = ?")
107
- .bind(id)
108
- .first();
109
-
110
- if (!job) {
111
- return c.json({ error: "Job not found" }, 404);
112
- }
156
+ // Manual trigger
157
+ app.post("/api/trigger", async (c) => {
158
+ const results = await runChecks(
159
+ c.env.DB,
160
+ c.env as unknown as Record<string, unknown>,
161
+ "manual",
162
+ );
163
+ const allOk = results.every((r) => r.ok);
164
+ return c.json({
165
+ checked: results.length,
166
+ all_ok: allOk,
167
+ results: results.map((r) => ({
168
+ group: r.group_name,
169
+ ok: r.ok,
170
+ latency_ms: r.latency_ms,
171
+ })),
172
+ });
173
+ });
113
174
 
114
- return c.json({ job });
175
+ // Cron handler
176
+ app.post("/__scheduled", async (c) => {
177
+ const results = await runChecks(
178
+ c.env.DB,
179
+ c.env as unknown as Record<string, unknown>,
180
+ "cron",
181
+ );
182
+ const allOk = results.every((r) => r.ok);
183
+ console.log(`Uptime check: ${results.length} URLs, all_ok=${allOk}`);
184
+ return c.json({ checked: results.length, all_ok: allOk });
115
185
  });
116
186
 
117
187
  export default app;
@@ -0,0 +1,124 @@
1
+ export interface MonitorGroup {
2
+ name: string;
3
+ urls: string[];
4
+ }
5
+
6
+ export interface CheckResult {
7
+ url: string;
8
+ group_name: string;
9
+ status_code: number | null;
10
+ latency_ms: number;
11
+ ok: boolean;
12
+ error: string | null;
13
+ }
14
+
15
+ async function checkUrl(url: string): Promise<Omit<CheckResult, "group_name">> {
16
+ const start = Date.now();
17
+ try {
18
+ const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
19
+ return {
20
+ url,
21
+ status_code: res.status,
22
+ latency_ms: Date.now() - start,
23
+ ok: res.ok,
24
+ error: null,
25
+ };
26
+ } catch (err) {
27
+ return {
28
+ url,
29
+ status_code: null,
30
+ latency_ms: Date.now() - start,
31
+ ok: false,
32
+ error: err instanceof Error ? err.message : String(err),
33
+ };
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Parse MONITOR_URLS into named groups.
39
+ *
40
+ * Format: "GroupName=url1,url2;Other=url3,url4"
41
+ * Plain URLs without a group name go into "Default".
42
+ * If nothing is configured, monitors https://1.1.1.1 as "Default".
43
+ */
44
+ export function parseMonitorConfig(env: Record<string, unknown>): MonitorGroup[] {
45
+ const raw = env.MONITOR_URLS as string | undefined;
46
+ if (!raw?.trim()) {
47
+ return [{ name: "Default", urls: ["https://1.1.1.1"] }];
48
+ }
49
+
50
+ const groups: MonitorGroup[] = [];
51
+
52
+ for (const segment of raw.split(";")) {
53
+ const trimmed = segment.trim();
54
+ if (!trimmed) continue;
55
+
56
+ const eqIndex = trimmed.indexOf("=");
57
+ if (eqIndex > 0) {
58
+ const name = trimmed.slice(0, eqIndex).trim();
59
+ const urls = trimmed
60
+ .slice(eqIndex + 1)
61
+ .split(",")
62
+ .map((u) => u.trim())
63
+ .filter(Boolean);
64
+ if (urls.length) groups.push({ name, urls });
65
+ } else {
66
+ const urls = trimmed
67
+ .split(",")
68
+ .map((u) => u.trim())
69
+ .filter(Boolean);
70
+ const existing = groups.find((g) => g.name === "Default");
71
+ if (existing) {
72
+ existing.urls.push(...urls);
73
+ } else if (urls.length) {
74
+ groups.push({ name: "Default", urls });
75
+ }
76
+ }
77
+ }
78
+
79
+ if (groups.length === 0) {
80
+ groups.push({ name: "Default", urls: ["https://1.1.1.1"] });
81
+ }
82
+
83
+ return groups;
84
+ }
85
+
86
+ export async function runChecks(
87
+ db: D1Database,
88
+ env: Record<string, unknown>,
89
+ source: "cron" | "manual",
90
+ ): Promise<CheckResult[]> {
91
+ const groups = parseMonitorConfig(env);
92
+ const promises: Promise<CheckResult>[] = [];
93
+
94
+ for (const group of groups) {
95
+ for (const url of group.urls) {
96
+ promises.push(
97
+ checkUrl(url).then((r) => ({ ...r, group_name: group.name })),
98
+ );
99
+ }
100
+ }
101
+
102
+ const results = await Promise.all(promises);
103
+
104
+ for (const r of results) {
105
+ await db
106
+ .prepare(
107
+ "INSERT INTO checks (id, url, group_name, status_code, latency_ms, ok, source, error, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
108
+ )
109
+ .bind(
110
+ crypto.randomUUID(),
111
+ r.url,
112
+ r.group_name,
113
+ r.status_code,
114
+ r.latency_ms,
115
+ r.ok ? 1 : 0,
116
+ source,
117
+ r.error,
118
+ Math.floor(Date.now() / 1000),
119
+ )
120
+ .run();
121
+ }
122
+
123
+ return results;
124
+ }
@@ -1,23 +1,13 @@
1
1
  import { type JackAI, createJackAI } from "./jack-ai";
2
- import { type JackVectorize, createJackVectorize } from "./jack-vectorize";
3
2
 
4
3
  interface Env {
5
- // Direct bindings (for local dev with wrangler)
6
4
  AI?: Ai;
7
- VECTORS?: VectorizeIndex;
8
- // Jack proxy bindings (injected in jack cloud)
5
+ VECTORS: VectorizeIndex;
9
6
  __AI_PROXY?: Fetcher;
10
- __VECTORIZE_PROXY?: Fetcher;
11
- __JACK_PROJECT_ID?: string;
12
- __JACK_ORG_ID?: string;
13
- // Other bindings
14
7
  DB: D1Database;
15
8
  ASSETS: Fetcher;
16
9
  }
17
10
 
18
- // Index name must match wrangler.jsonc vectorize config
19
- const VECTORIZE_INDEX_NAME = "jack-template-vectors";
20
-
21
11
  // Minimal AI interface for embedding generation
22
12
  type AIClient = {
23
13
  run: (model: string, inputs: { text: string }) => Promise<{ data: number[][] } | unknown>;
@@ -25,10 +15,8 @@ type AIClient = {
25
15
 
26
16
  function getAI(env: Env): AIClient {
27
17
  // Prefer jack cloud proxy if available (for metering)
28
- if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
29
- return createJackAI(
30
- env as Required<Pick<Env, "__AI_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">>,
31
- ) as AIClient;
18
+ if (env.__AI_PROXY) {
19
+ return createJackAI(env as Pick<Env, "__AI_PROXY"> & { __AI_PROXY: Fetcher }) as AIClient;
32
20
  }
33
21
  // Fallback to direct binding for local dev
34
22
  if (env.AI) {
@@ -37,32 +25,6 @@ function getAI(env: Env): AIClient {
37
25
  throw new Error("No AI binding available");
38
26
  }
39
27
 
40
- // Minimal Vectorize interface
41
- type VectorizeClient = {
42
- insert: (
43
- vectors: { id: string; values: number[]; metadata?: Record<string, unknown> }[],
44
- ) => Promise<unknown>;
45
- query: (
46
- vector: number[],
47
- options?: { topK?: number; returnMetadata?: "none" | "indexed" | "all" },
48
- ) => Promise<{ matches: { id: string; score: number; metadata?: Record<string, unknown> }[] }>;
49
- };
50
-
51
- function getVectorize(env: Env): VectorizeClient {
52
- // Prefer jack cloud proxy if available (for metering)
53
- if (env.__VECTORIZE_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
54
- return createJackVectorize(
55
- env as Required<Pick<Env, "__VECTORIZE_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">>,
56
- VECTORIZE_INDEX_NAME,
57
- ) as VectorizeClient;
58
- }
59
- // Fallback to direct binding for local dev
60
- if (env.VECTORS) {
61
- return env.VECTORS as unknown as VectorizeClient;
62
- }
63
- throw new Error("No Vectorize binding available");
64
- }
65
-
66
28
  // Rate limiting: 10 requests per minute per IP
67
29
  const RATE_LIMIT = 10;
68
30
  const RATE_WINDOW_MS = 60_000;
@@ -142,7 +104,7 @@ export default {
142
104
  }
143
105
 
144
106
  // Store in Vectorize
145
- const vectors = getVectorize(env);
107
+ const vectors = env.VECTORS;
146
108
  await vectors.insert([
147
109
  {
148
110
  id,
@@ -188,7 +150,7 @@ export default {
188
150
  }
189
151
 
190
152
  // Search Vectorize
191
- const vectors = getVectorize(env);
153
+ const vectors = env.VECTORS;
192
154
  const results = await vectors.query(embeddingVector, {
193
155
  topK: limit,
194
156
  returnMetadata: "all",
@@ -10,8 +10,6 @@
10
10
  *
11
11
  * interface Env {
12
12
  * __AI_PROXY: Fetcher; // Service binding to binding-proxy worker
13
- * __JACK_PROJECT_ID: string; // Injected by control plane
14
- * __JACK_ORG_ID: string; // Injected by control plane
15
13
  * }
16
14
  *
17
15
  * export default {
@@ -29,8 +27,6 @@
29
27
 
30
28
  interface JackAIEnv {
31
29
  __AI_PROXY: Fetcher;
32
- __JACK_PROJECT_ID: string;
33
- __JACK_ORG_ID: string;
34
30
  }
35
31
 
36
32
  /**
@@ -56,8 +52,6 @@ export function createJackAI(env: JackAIEnv): {
56
52
  method: "POST",
57
53
  headers: {
58
54
  "Content-Type": "application/json",
59
- "X-Jack-Project-ID": env.__JACK_PROJECT_ID,
60
- "X-Jack-Org-ID": env.__JACK_ORG_ID,
61
55
  },
62
56
  body: JSON.stringify({ model, inputs, options }),
63
57
  });
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "telegram-bot",
3
+ "description": "Telegram bot with AI-powered responses",
4
+ "secrets": ["BOT_TOKEN", "WEBHOOK_SECRET"],
5
+ "requires": ["AI"],
6
+ "intent": {
7
+ "keywords": ["telegram", "bot", "chat", "messaging", "ai"],
8
+ "examples": ["telegram bot", "AI telegram assistant", "messaging bot", "chat bot"]
9
+ },
10
+ "agentContext": {
11
+ "summary": "A Telegram bot with grammY, AI-powered responses, and webhook delivery.",
12
+ "full_text": "## Project Structure\n\n- `src/index.ts` - grammY bot with webhook handler, commands, and AI integration\n- `src/jack-ai.ts` - Jack AI proxy wrapper (do not modify)\n\n## Commands\n\n- `/start` - Welcome message with available commands\n- `/help` - List all commands\n- `/ask <question>` - Ask the AI anything\n- `/status` - Bot info: region, timestamp, uptime\n\n## AI Responses\n\nThe bot responds with AI in two ways:\n1. `/ask <question>` - Explicit AI query\n2. Reply to any bot message - Treated as a follow-up question\n\nAI uses the jack metered proxy. Change the model in `src/index.ts`.\n\n## Webhook\n\nTelegram delivers updates via webhook POST to your deploy URL. The webhook is auto-registered during deployment. The `WEBHOOK_SECRET` header is verified on every request.\n\n## Environment Variables\n\n- `BOT_TOKEN` - Telegram bot token from @BotFather (secret)\n- `WEBHOOK_SECRET` - Auto-generated webhook verification token (secret)\n\n## Changing the AI Model\n\nEdit the model string in `src/index.ts`:\n```typescript\nconst MODEL = \"@cf/meta/llama-3.1-8b-instruct\";\n```\n\nAvailable models: https://developers.cloudflare.com/workers-ai/models/\n\n## Resources\n\n- [grammY Documentation](https://grammy.dev)\n- [Telegram Bot API](https://core.telegram.org/bots/api)\n- [AI Models](https://developers.cloudflare.com/workers-ai/models)"
13
+ },
14
+ "hooks": {
15
+ "preCreate": [
16
+ {
17
+ "action": "require",
18
+ "source": "secret",
19
+ "key": "BOT_TOKEN",
20
+ "message": "Telegram bot token from @BotFather",
21
+ "setupUrl": "https://t.me/BotFather",
22
+ "onMissing": "prompt",
23
+ "promptMessage": "Enter your Telegram Bot Token:",
24
+ "perProject": true,
25
+ "validateCommand": "curl -sf --max-time 5 https://api.telegram.org/bot{{value}}/getMe > /dev/null",
26
+ "validateError": "Invalid bot token. Make sure you copied it correctly from @BotFather."
27
+ },
28
+ {
29
+ "action": "require",
30
+ "source": "secret",
31
+ "key": "WEBHOOK_SECRET",
32
+ "message": "Generating webhook verification secret...",
33
+ "onMissing": "generate",
34
+ "generateCommand": "openssl rand -hex 32",
35
+ "perProject": true
36
+ }
37
+ ],
38
+ "postDeploy": [
39
+ {
40
+ "action": "shell",
41
+ "command": "curl -sf --max-time 10 {{url}}/register-webhook > /dev/null || true",
42
+ "message": "Registering Telegram webhook..."
43
+ },
44
+ {
45
+ "action": "shell",
46
+ "command": "BOT_LINK=$(curl -sf --max-time 10 {{url}}/bot-link) && [ -n \"$BOT_LINK\" ] && echo \"\" && echo \" Your bot is live! Open it here:\" && echo \" $BOT_LINK\" && echo \"\" && echo \" Send /start — it will respond.\" || true",
47
+ "message": ""
48
+ },
49
+ {
50
+ "action": "clipboard",
51
+ "text": "{{url}}",
52
+ "message": "Deploy URL copied to clipboard"
53
+ }
54
+ ]
55
+ }
56
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "jack-template",
7
+ "dependencies": {
8
+ "grammy": "^1.35.0",
9
+ },
10
+ "devDependencies": {
11
+ "@cloudflare/workers-types": "^4.20241205.0",
12
+ "typescript": "^5.0.0",
13
+ },
14
+ },
15
+ },
16
+ "packages": {
17
+ "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260213.0", "", {}, "sha512-dr905ft/1R0mnfdT9aun4vanLgIBN27ZyPxTCENKmhctSz6zNmBOvHbzDWAhGE0RBAKFf3X7ifMRcd0MkmBvgA=="],
18
+
19
+ "@grammyjs/types": ["@grammyjs/types@3.24.0", "", {}, "sha512-qQIEs4lN5WqUdr4aT8MeU6UFpMbGYAvcvYSW1A4OO1PABGJQHz/KLON6qvpf+5RxaNDQBxiY2k2otIhg/AG7RQ=="],
20
+
21
+ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
22
+
23
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
24
+
25
+ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
26
+
27
+ "grammy": ["grammy@1.40.0", "", { "dependencies": { "@grammyjs/types": "3.24.0", "abort-controller": "^3.0.0", "debug": "^4.4.3", "node-fetch": "^2.7.0" } }, "sha512-ssuE7fc1AwqlUxHr931OCVW3fU+oFDjHZGgvIedPKXfTdjXvzP19xifvVGCnPtYVUig1Kz+gwxe4A9M5WdkT4Q=="],
28
+
29
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
30
+
31
+ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
32
+
33
+ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
34
+
35
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
36
+
37
+ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
38
+
39
+ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
40
+ }
41
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "jack-template",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "wrangler dev",
7
+ "deploy": "wrangler deploy"
8
+ },
9
+ "dependencies": {
10
+ "grammy": "^1.35.0"
11
+ },
12
+ "devDependencies": {
13
+ "@cloudflare/workers-types": "^4.20241205.0",
14
+ "typescript": "^5.0.0"
15
+ }
16
+ }