jerob 1.0.0

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 (69) hide show
  1. package/CLI/cli.ts +42 -0
  2. package/README.md +137 -0
  3. package/SETUP.md +584 -0
  4. package/agent/action-tracker.ts +45 -0
  5. package/agent/agent-tools.ts +111 -0
  6. package/agent/approval.ts +137 -0
  7. package/agent/diff-view.ts +26 -0
  8. package/agent/orchestrator.ts +186 -0
  9. package/agent/tool-executor.ts +463 -0
  10. package/agent/types.ts +69 -0
  11. package/ask/orchestrator.ts +244 -0
  12. package/auth/auth.ts +567 -0
  13. package/auth/config-store.ts +77 -0
  14. package/auth/crypto.ts +51 -0
  15. package/auth/env-writer.ts +82 -0
  16. package/bin/jerob.js +28 -0
  17. package/config/ai.config.ts +163 -0
  18. package/email_ops/email-tools.ts +178 -0
  19. package/email_ops/email_functions.ts +443 -0
  20. package/email_ops/email_init.ts +92 -0
  21. package/email_ops/email_pass_store.ts +61 -0
  22. package/email_ops/email_server.ts +29 -0
  23. package/email_ops/types.ts +88 -0
  24. package/index.ts +176 -0
  25. package/package.json +88 -0
  26. package/plan/browser-agent/README.md +118 -0
  27. package/plan/browser-agent/USAGE.md +308 -0
  28. package/plan/browser-agent/evaluator.ts +353 -0
  29. package/plan/browser-agent/executor.ts +372 -0
  30. package/plan/browser-agent/index.ts +13 -0
  31. package/plan/browser-agent/orchestrator.ts +323 -0
  32. package/plan/browser-agent/planner.ts +200 -0
  33. package/plan/browser-agent/types.ts +62 -0
  34. package/plan/browser-tool.ts +128 -0
  35. package/plan/index.ts +12 -0
  36. package/plan/orchestrator.ts +214 -0
  37. package/plan/planner.ts +183 -0
  38. package/plan/selection.ts +50 -0
  39. package/plan/types.ts +13 -0
  40. package/plan/web-tools.ts +119 -0
  41. package/scheduler/ARCHITECTURE.md +263 -0
  42. package/scheduler/README.md +200 -0
  43. package/scheduler/SETUP-READY.sql +84 -0
  44. package/scheduler/check-status.sql +124 -0
  45. package/scheduler/config-sync.ts +91 -0
  46. package/scheduler/db-migrate.ts +271 -0
  47. package/scheduler/db.ts +162 -0
  48. package/scheduler/debug.ts +184 -0
  49. package/scheduler/orchestrator.ts +438 -0
  50. package/scheduler/planner.ts +170 -0
  51. package/scheduler/update-task-email.ts +70 -0
  52. package/supabase/.temp/cli-latest +1 -0
  53. package/supabase/.temp/gotrue-version +1 -0
  54. package/supabase/.temp/linked-project.json +1 -0
  55. package/supabase/.temp/pooler-url +1 -0
  56. package/supabase/.temp/postgres-version +1 -0
  57. package/supabase/.temp/project-ref +1 -0
  58. package/supabase/.temp/rest-version +1 -0
  59. package/supabase/.temp/storage-migration +1 -0
  60. package/supabase/.temp/storage-version +1 -0
  61. package/supabase/deploy.ps1 +50 -0
  62. package/supabase/functions/scheduler-tick/index.ts +496 -0
  63. package/supabase/supabase/.temp/linked-project.json +1 -0
  64. package/tsconfig.json +33 -0
  65. package/tui/spinner.ts +33 -0
  66. package/tui/spinup.ts +67 -0
  67. package/tui/terminal-render.ts +16 -0
  68. package/utils/llm-error.ts +185 -0
  69. package/utils/model-validator.ts +247 -0
@@ -0,0 +1,438 @@
1
+ import chalk from "chalk";
2
+ import { select, text, confirm, isCancel } from "@clack/prompts";
3
+ import { withSpinner } from "../tui/spinner";
4
+ import {
5
+ getAllTasks,
6
+ getTaskById,
7
+ createTask,
8
+ updateTask,
9
+ deleteTask,
10
+ getRunsForTask,
11
+ } from "./db";
12
+ import { planScheduledTask, computeNextRun } from "./planner";
13
+ import { printLLMError } from "../utils/llm-error";
14
+
15
+ // ── Helpers ───────────────────────────────────────────────────────────────────
16
+
17
+ /**
18
+ * Accepts either:
19
+ * - A plain time like "11:06" or "11.06" → converts to daily cron in UTC
20
+ * - A full 5-field cron expression → used as-is (assumed UTC)
21
+ */
22
+ function parseCronInput(input: string): string {
23
+ const trimmed = input.trim();
24
+
25
+ // Match HH:MM or HH.MM (12 or 24 hour, with optional AM/PM)
26
+ const timeMatch = trimmed.match(/^(\d{1,2})[:.h](\d{2})\s*(am|pm)?$/i);
27
+ if (timeMatch) {
28
+ let hours = parseInt(timeMatch[1]!, 10);
29
+ const minutes = parseInt(timeMatch[2]!, 10);
30
+ const meridiem = timeMatch[3]?.toLowerCase();
31
+
32
+ if (meridiem === "pm" && hours < 12) hours += 12;
33
+ if (meridiem === "am" && hours === 12) hours = 0;
34
+
35
+ // Convert local time → UTC
36
+ const localOffsetMinutes = new Date().getTimezoneOffset(); // e.g. -330 for IST
37
+ const totalLocalMinutes = hours * 60 + minutes;
38
+ const totalUtcMinutes = totalLocalMinutes + localOffsetMinutes; // getTimezoneOffset is negated
39
+
40
+ // Wrap around 24 hours
41
+ const utcMinutes = ((totalUtcMinutes % 1440) + 1440) % 1440;
42
+ const utcHour = Math.floor(utcMinutes / 60);
43
+ const utcMin = utcMinutes % 60;
44
+
45
+ const cron = `${utcMin} ${utcHour} * * *`;
46
+ console.log(chalk.dim(` → Converted to UTC cron: ${cron} (runs daily at ${trimmed} local time)`));
47
+ return cron;
48
+ }
49
+
50
+ // Otherwise treat as raw cron expression
51
+ return trimmed;
52
+ }
53
+
54
+ function fmtDate(iso: string | null) {
55
+ if (!iso) return chalk.dim("never");
56
+ return new Date(iso).toLocaleString();
57
+ }
58
+
59
+ function fmtCron(cron: string) {
60
+ // Show UTC cron + local time equivalent if it's a daily schedule
61
+ const daily = cron.match(/^(\d+)\s+(\d+)\s+\*\s+\*\s+\*$/);
62
+ if (daily) {
63
+ const utcMin = parseInt(daily[1]!, 10);
64
+ const utcHour = parseInt(daily[2]!, 10);
65
+ const localDate = new Date();
66
+ localDate.setUTCHours(utcHour, utcMin, 0, 0);
67
+ return `${cron} ${chalk.dim(`(daily ${localDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} local)`)}`;
68
+ }
69
+ return cron;
70
+ }
71
+
72
+ function fmtEnabled(v: boolean) {
73
+ return v ? chalk.green("enabled") : chalk.red("disabled");
74
+ }
75
+
76
+ // ── Sub-screens ───────────────────────────────────────────────────────────────
77
+
78
+ async function listTasks() {
79
+ const tasks = await getAllTasks();
80
+ if (tasks.length === 0) {
81
+ console.log(chalk.yellow("\nNo scheduled tasks yet.\n"));
82
+ return;
83
+ }
84
+ console.log(chalk.bold(`\n${"#".padEnd(4)} ${"Name".padEnd(28)} ${"Cron".padEnd(16)} ${"Status".padEnd(10)} ${"Runs".padEnd(6)} Last Run`));
85
+ console.log("─".repeat(90));
86
+ tasks.forEach((t, i) => {
87
+ console.log(
88
+ `${String(i + 1).padEnd(4)} ${t.name.slice(0, 27).padEnd(28)} ${t.cron.padEnd(16)} ${fmtEnabled(t.enabled).padEnd(18)} ${String(t.run_count).padEnd(6)} ${fmtDate(t.last_run_at)}`
89
+ ); });
90
+ console.log();
91
+ }
92
+
93
+ async function addTask() {
94
+ const desc = await text({
95
+ message: "Describe the repetitive task (what should happen and how often):",
96
+ placeholder: "e.g. Every morning search top AI news and email me a summary",
97
+ });
98
+ if (isCancel(desc) || !desc?.trim()) return;
99
+
100
+ let plan;
101
+ try {
102
+ plan = await withSpinner("Planning task with AI…", () =>
103
+ planScheduledTask(desc.trim())
104
+ );
105
+ } catch (err) {
106
+ printLLMError(err, "Scheduler planner");
107
+ return;
108
+ }
109
+
110
+ console.log(chalk.bold(`\nTask name: ${plan.name}`));
111
+ console.log(chalk.bold(`Cron: ${plan.cron}`));
112
+ console.log(chalk.bold(`Steps (${plan.steps.length}):`));
113
+ plan.steps.forEach((s:any) => console.log(` ${s.order}. [${s.type}] ${s.instruction}`));
114
+
115
+
116
+ // Only ask for email if there's an email_send step AND no email already in the instructions
117
+ const hasEmailStep = plan.steps.some((s: any) => s.type === "email_send");
118
+ const emailAlreadyInSteps = plan.steps.some(
119
+ (s: any) => s.type === "email_send" && /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/.test(s.instruction)
120
+ );
121
+ let recipientEmail: string | null = null;
122
+
123
+ if (hasEmailStep && !emailAlreadyInSteps) {
124
+ const emailPrompt = await text({
125
+ message: "📧 This task sends emails. Enter recipient email address:",
126
+ placeholder: "your@email.com",
127
+ validate: (v) => {
128
+ if (!v || !v.trim()) return "Email is required for email_send steps";
129
+ if (!v.includes("@")) return "Please enter a valid email";
130
+ return undefined;
131
+ },
132
+ });
133
+ if (isCancel(emailPrompt)) return;
134
+ recipientEmail = emailPrompt?.trim() || null;
135
+ }
136
+
137
+ const editCron = await text({
138
+ message: "Schedule — enter time like '11:06' (daily, your local time) or a full cron e.g. '0 9 * * 1':",
139
+ initialValue: plan.cron,
140
+ });
141
+ if (isCancel(editCron)) return;
142
+ const finalCron = parseCronInput(editCron?.trim() || plan.cron);
143
+
144
+ const emailInput = await text({
145
+ message: "Send summary email after each run? (leave blank to skip):",
146
+ placeholder: "you@example.com",
147
+ });
148
+ if (isCancel(emailInput)) return;
149
+ const summaryEmail = emailInput?.trim() || null;
150
+
151
+ // Replace USER_EMAIL placeholder AND update any email_send step instructions to include the actual email
152
+ const finalSteps = plan.steps.map((step:any) => {
153
+ if (step.type === "email_send" && recipientEmail) {
154
+ // If instruction has USER_EMAIL, replace it
155
+ if (step.instruction.includes("USER_EMAIL")) {
156
+ return {
157
+ ...step,
158
+ instruction: step.instruction.replace(/USER_EMAIL/g, recipientEmail),
159
+ };
160
+ }
161
+ // Otherwise, append the email to the instruction
162
+ return {
163
+ ...step,
164
+ instruction: `${step.instruction} to ${recipientEmail}`,
165
+ };
166
+ }
167
+ return step;
168
+ });
169
+
170
+ const ok = await confirm({ message: "Create this task?" });
171
+ if (isCancel(ok) || !ok) return;
172
+
173
+ const task = await createTask({
174
+ name: plan.name,
175
+ description: desc.trim(),
176
+ cron: finalCron,
177
+ enabled: true,
178
+ steps: finalSteps,
179
+ summary_email: summaryEmail,
180
+ next_run_at: computeNextRun(finalCron),
181
+ });
182
+
183
+ console.log(chalk.green(`\n✓ Task created: ${task.id}\n`));
184
+ }
185
+
186
+ async function manageTask() {
187
+ const tasks = await getAllTasks();
188
+ if (tasks.length === 0) {
189
+ console.log(chalk.yellow("\nNo tasks to manage.\n"));
190
+ return;
191
+ }
192
+
193
+ const choice = await select({
194
+ message: "Select a task:",
195
+ options: tasks.map((t) => ({
196
+ value: t.id,
197
+ label: `${t.name} [${fmtEnabled(t.enabled)}] — ${t.cron}`,
198
+ })),
199
+ });
200
+ if (isCancel(choice)) return;
201
+
202
+ const task = await getTaskById(choice as string);
203
+ if (!task) return;
204
+
205
+ while (true) {
206
+ console.log(chalk.bold(`\n── ${task.name} ──`));
207
+ console.log(` ID: ${task.id}`);
208
+ console.log(` Cron: ${fmtCron(task.cron)}`);
209
+ console.log(` Status: ${fmtEnabled(task.enabled)}`);
210
+ console.log(` Runs: ${task.run_count}`);
211
+ console.log(` Last run: ${fmtDate(task.last_run_at)}`);
212
+ console.log(` Next run: ${fmtDate(task.next_run_at)}`);
213
+ console.log(` Summary to: ${task.summary_email ?? chalk.dim("(none)")}`);
214
+ console.log(` Steps:`);
215
+ task.steps.forEach((s) => console.log(` ${s.order}. [${s.type}] ${s.instruction}`));
216
+ console.log();
217
+
218
+ const action = await select({
219
+ message: "Action:",
220
+ options: [
221
+ { value: "toggle", label: task.enabled ? "Disable task" : "Enable task" },
222
+ { value: "edit_cron", label: "Edit cron schedule" },
223
+ { value: "edit_email", label: "Edit summary email" },
224
+ { value: "run_now", label: "Run now (manual trigger)" },
225
+ { value: "history", label: "View run history" },
226
+ { value: "delete", label: chalk.red("Delete task") },
227
+ { value: "back", label: "Back" },
228
+ ],
229
+ });
230
+ if (isCancel(action) || action === "back") break;
231
+
232
+ if (action === "toggle") {
233
+ await updateTask(task.id, { enabled: !task.enabled });
234
+ task.enabled = !task.enabled;
235
+ console.log(chalk.green(`✓ Task ${task.enabled ? "enabled" : "disabled"}\n`));
236
+ }
237
+
238
+ if (action === "edit_cron") {
239
+ const newCron = await text({ message: "New schedule — time like '11:06' (daily, local) or full cron:", initialValue: task.cron });
240
+ if (!isCancel(newCron) && newCron?.trim()) {
241
+ const parsed = parseCronInput(newCron.trim());
242
+ await updateTask(task.id, { cron: parsed, next_run_at: computeNextRun(parsed) });
243
+ task.cron = parsed;
244
+ console.log(chalk.green(`✓ Schedule updated to: ${parsed}\n`));
245
+ }
246
+ }
247
+
248
+ if (action === "edit_email") {
249
+ const newEmail = await text({ message: "Summary email (blank to remove):", initialValue: task.summary_email ?? "" });
250
+ if (!isCancel(newEmail)) {
251
+ const val = newEmail?.trim() || null;
252
+ await updateTask(task.id, { summary_email: val });
253
+ task.summary_email = val;
254
+ console.log(chalk.green("✓ Email updated\n"));
255
+ }
256
+ }
257
+
258
+ if (action === "run_now") {
259
+ const confirmed = await confirm({ message: "Run this task now?" });
260
+ if (!isCancel(confirmed) && confirmed) {
261
+ let error: string | null = null;
262
+ let ranCount = 0;
263
+ let taskStatus = "unknown";
264
+
265
+ await withSpinner(`Running ${task.name}…`, async () => {
266
+ const url = process.env.SUPABASE_URL;
267
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_KEY;
268
+
269
+ if (!url || !key) {
270
+ error = "SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY not set in .env";
271
+ return;
272
+ }
273
+
274
+ // Set next_run_at far in the past so it's definitely "due"
275
+ await updateTask(task.id, {
276
+ next_run_at: new Date(Date.now() - 60000).toISOString(),
277
+ });
278
+
279
+ // Small delay to ensure DB write is committed
280
+ await new Promise((r) => setTimeout(r, 500));
281
+
282
+ const res = await fetch(`${url}/functions/v1/scheduler-tick`, {
283
+ method: "POST",
284
+ headers: {
285
+ "Content-Type": "application/json",
286
+ Authorization: `Bearer ${key}`,
287
+ },
288
+ body: JSON.stringify({}),
289
+ });
290
+
291
+ const body = await res.text();
292
+
293
+ if (!res.ok) {
294
+ error = `Edge Function returned ${res.status}: ${body}`;
295
+ return;
296
+ }
297
+
298
+ try {
299
+ const data = JSON.parse(body);
300
+ ranCount = data?.ran ?? 0;
301
+ const match = (data?.results ?? []).find((r: any) => r.taskId === task.id);
302
+ taskStatus = match?.status ?? (ranCount > 0 ? "success" : "not_picked_up");
303
+ } catch {
304
+ error = `Invalid response: ${body.slice(0, 100)}`;
305
+ }
306
+ });
307
+
308
+ if (error) {
309
+ console.log(chalk.red(`\n✖ ${error}\n`));
310
+ } else if (taskStatus === "not_picked_up") {
311
+ console.log(chalk.yellow("\n⚠ Edge Function ran but did not pick up this task."));
312
+ console.log(chalk.dim("This can happen if next_run_at wasn't updated in time. Try again.\n"));
313
+ } else {
314
+ const icon = taskStatus === "success" ? chalk.green("✓")
315
+ : taskStatus === "partial" ? chalk.yellow("⚠")
316
+ : chalk.red("✖");
317
+ console.log(`\n${icon} Done — status: ${chalk.bold(taskStatus)}`);
318
+ console.log(chalk.dim("View full results: Manage → View run history\n"));
319
+ }
320
+ }
321
+ }
322
+
323
+ if (action === "history") {
324
+ const runs = await getRunsForTask(task.id, 10);
325
+ if (runs.length === 0) {
326
+ console.log(chalk.yellow("\nNo runs yet.\n"));
327
+ } else {
328
+ console.log(chalk.bold("\nRun History (last 10):\n"));
329
+ runs.forEach((r, i) => {
330
+ const icon =
331
+ r.status === "success" ? chalk.green("✓") :
332
+ r.status === "failed" ? chalk.red("✖") :
333
+ chalk.yellow("⟳");
334
+ console.log(`${i + 1}. ${icon} ${fmtDate(r.started_at)} — ${chalk.bold(r.status)}`);
335
+
336
+ // Show step-level results
337
+ if (Array.isArray(r.step_results) && r.step_results.length > 0) {
338
+ r.step_results.forEach((s: any) => {
339
+ const stepIcon = s.success ? chalk.green(" ✓") : chalk.red(" ✖");
340
+ console.log(`${stepIcon} Step ${s.order} [${s.type ?? "?"}]: ${String(s.output ?? "").slice(0, 180)}`);
341
+ });
342
+ }
343
+
344
+ // Top-level error — classify it for the user
345
+ if (r.error) {
346
+ const err = r.error;
347
+ let category = chalk.red("Error");
348
+ let hint = "";
349
+
350
+ if (/invalid.?api.?key|api key|unauthorized|401/i.test(err)) {
351
+ category = chalk.red("LLM API Key Error");
352
+ hint = " → Run `jerob set-key` to update your API keys, then `jerob sync-credentials`";
353
+ } else if (/quota|rate.?limit|429|too many requests/i.test(err)) {
354
+ category = chalk.yellow("Rate Limit / Quota");
355
+ hint = " → Your API key hit its limit. Try a different provider or wait.";
356
+ } else if (/no llm keys|no api key|all llm providers failed/i.test(err)) {
357
+ category = chalk.red("No LLM Keys Configured");
358
+ hint = " → Run `jerob sync-credentials` to push your keys to Supabase.";
359
+ } else if (/gmail|refresh.?token|oauth/i.test(err)) {
360
+ category = chalk.red("Gmail Auth Error");
361
+ hint = " → Re-authenticate Gmail: run `jerob jet` and use an email operation.";
362
+ } else if (/firecrawl|search failed|crawl failed/i.test(err)) {
363
+ category = chalk.yellow("Web Search/Crawl Error");
364
+ hint = " → Check your FIRECRAWL_KEY or the target URL.";
365
+ } else if (/supabase|database|relation|table/i.test(err)) {
366
+ category = chalk.red("Database Error");
367
+ hint = " → Run `jerob setup-db` to verify your schema is up to date.";
368
+ } else if (/fetch|network|ECONNREFUSED|timeout/i.test(err)) {
369
+ category = chalk.yellow("Network Error");
370
+ hint = " → Transient network issue. The task will retry next scheduled run.";
371
+ }
372
+
373
+ console.log(chalk.red(` ${category}: ${err.slice(0, 300)}`));
374
+ if (hint) console.log(chalk.dim(hint));
375
+ }
376
+
377
+ if (r.output && r.status !== "failed") {
378
+ console.log(chalk.dim(` Output: ${r.output.slice(0, 200)}`));
379
+ }
380
+ console.log();
381
+ });
382
+ }
383
+ }
384
+
385
+ if (action === "delete") {
386
+ const sure = await confirm({ message: chalk.red(`Delete "${task.name}"? This cannot be undone.`) });
387
+ if (!isCancel(sure) && sure) {
388
+ await deleteTask(task.id);
389
+ console.log(chalk.green("✓ Task deleted\n"));
390
+ break;
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ // ── Main entry ────────────────────────────────────────────────────────────────
397
+
398
+ function showDeployInstructions() {
399
+ console.log(chalk.bold("\n🚀 Serverless Deploy (Supabase Edge Function)\n"));
400
+ console.log("1. Install Supabase CLI: npm i -g supabase");
401
+ console.log("2. Login: supabase login");
402
+ console.log("3. Link project: supabase link --project-ref <YOUR_PROJECT_REF>");
403
+ console.log("4. Deploy + sync keys: .\\supabase\\deploy.ps1");
404
+ console.log("5. Run setup SQL once:");
405
+ console.log(chalk.cyan(" Open scheduler/SETUP-READY.sql in Supabase SQL Editor and click RUN"));
406
+ console.log(chalk.green("\nAfter deploy, tasks run every minute in Supabase — no local process needed.\n"));
407
+ }
408
+
409
+ export async function runSchedulerMode() {
410
+ console.log(chalk.bold("\n⏰ Scheduler Mode\n"));
411
+ console.log(chalk.dim("Tip: run 'jimmy daemon' in a separate terminal to execute tasks autonomously.\n"));
412
+
413
+ while (true) {
414
+ const option = await select({
415
+ message: "Scheduler:",
416
+ options: [
417
+ { value: "list", label: "List all tasks" },
418
+ { value: "add", label: "Add new task (AI plans it)" },
419
+ { value: "manage", label: "Manage / edit / delete a task" },
420
+ { value: "deploy", label: "Show serverless deploy instructions" },
421
+ { value: "back", label: "Back" },
422
+ ],
423
+ });
424
+
425
+ if (isCancel(option) || option === "back") break;
426
+
427
+ try {
428
+ if (option === "list") await listTasks();
429
+ if (option === "add") await addTask();
430
+ if (option === "manage") await manageTask();
431
+ if (option === "deploy") showDeployInstructions();
432
+ } catch (err) {
433
+ // DB errors, network errors — print clearly
434
+ const msg = err instanceof Error ? err.message : String(err);
435
+ console.log(chalk.red(`\n✖ ${msg}\n`));
436
+ }
437
+ }
438
+ }
@@ -0,0 +1,170 @@
1
+ import { generateText } from "ai";
2
+ import { getAgentModel, getAgentModel2, getAgentModel2Fallback } from "../config/ai.config";
3
+ import type { TaskStep } from "./db";
4
+ import { parseLLMError } from "../utils/llm-error";
5
+
6
+ interface ParsedPlan {
7
+ steps: TaskStep[];
8
+ cron: string;
9
+ name: string;
10
+ }
11
+
12
+ function extractJson(text: string): string | null {
13
+ let s = text.trim();
14
+ if (s.startsWith("```json")) s = s.slice(7);
15
+ if (s.startsWith("```")) s = s.slice(3);
16
+ if (s.endsWith("```")) s = s.slice(0, -3);
17
+ s = s.trim();
18
+ const i = s.indexOf("{");
19
+ if (i === -1) return null;
20
+ let depth = 0;
21
+ for (let j = i; j < s.length; j++) {
22
+ if (s[j] === "{") depth++;
23
+ else if (s[j] === "}") {
24
+ depth--;
25
+ if (depth === 0) return s.slice(i, j + 1);
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+
31
+ const SYSTEM_PROMPT = `You are a task automation planner. Given a user description of a repetitive task, produce a structured execution plan.
32
+
33
+ Step types:
34
+ - "web_search": search the web for information
35
+ - "web_crawl": scrape a specific URL for content
36
+ - "email_send": send an email with accumulated results
37
+ - "custom": any other AI-driven text/data task
38
+
39
+ IMPORTANT for email_send steps:
40
+ - If the user includes one or more email addresses in their description, embed them directly in the instruction, e.g. "send summary to alice@gmail.com, bob@work.com with subject 'Daily Report'"
41
+ - If the user says "email me" without specifying an address, use the placeholder USER_EMAIL so the system can ask for it
42
+ - NEVER invent or guess email addresses
43
+
44
+ You MUST respond with ONLY valid JSON — no markdown, no explanation, just the JSON object:
45
+ {
46
+ "name": "short descriptive task name",
47
+ "cron": "standard 5-field cron expression e.g. 0 9 * * *",
48
+ "steps": [
49
+ { "order": 1, "type": "web_search", "instruction": "exact instruction for this step" },
50
+ { "order": 2, "type": "email_send", "instruction": "send results to alice@gmail.com with subject 'Results'" }
51
+ ]
52
+ }`;
53
+
54
+ const models = [
55
+ () => getAgentModel2(), // Groq llama — most reliable for structured output
56
+ () => getAgentModel2Fallback(), // OpenRouter fallback
57
+ () => getAgentModel(), // Primary (may be filtered)
58
+ ];
59
+
60
+ export async function planScheduledTask(description: string): Promise<ParsedPlan> {
61
+ let lastError: Error | null = null;
62
+
63
+ for (const getModel of models) {
64
+ let model;
65
+ try { model = getModel(); } catch { continue; }
66
+
67
+ for (let attempt = 0; attempt < 2; attempt++) {
68
+ let text = "";
69
+ try {
70
+ const res = await generateText({
71
+ model,
72
+ messages: [
73
+ { role: "system", content: SYSTEM_PROMPT },
74
+ { role: "user", content: `Plan this task: ${description}` },
75
+ ],
76
+ temperature: attempt === 0 ? 0.3 : 0.1,
77
+ });
78
+ text = res.text?.trim() ?? "";
79
+ } catch (e) {
80
+ lastError = e instanceof Error ? e : new Error(String(e));
81
+ const parsed = parseLLMError(e);
82
+ // Auth/quota errors won't be fixed by trying another model — bail immediately
83
+ if (parsed.type === "auth" || parsed.type === "quota") {
84
+ throw new Error(`Scheduler planner: ${parsed.message}`);
85
+ }
86
+ break; // try next model
87
+ }
88
+
89
+ // Detect safety refusals
90
+ const lower = text.toLowerCase();
91
+ if (
92
+ lower.includes("i'm sorry") ||
93
+ lower.includes("i cannot") ||
94
+ lower.includes("i can't") ||
95
+ lower.startsWith("user safety") ||
96
+ lower.includes("content policy") ||
97
+ text.length < 20
98
+ ) {
99
+ lastError = new Error(`Model refused: ${text.slice(0, 100)}`);
100
+ break; // try next model
101
+ }
102
+
103
+ const jsonStr = extractJson(text);
104
+ if (!jsonStr) {
105
+ lastError = new Error(`No JSON found in response: ${text.slice(0, 200)}`);
106
+ continue; // retry same model with lower temp
107
+ }
108
+
109
+ try {
110
+ const parsed = JSON.parse(jsonStr);
111
+ if (!Array.isArray(parsed.steps) || parsed.steps.length === 0) {
112
+ lastError = new Error("Plan missing steps");
113
+ continue;
114
+ }
115
+ return {
116
+ name: parsed.name ?? "Unnamed Task",
117
+ cron: parsed.cron ?? "0 9 * * *",
118
+ steps: parsed.steps as TaskStep[],
119
+ };
120
+ } catch (e) {
121
+ lastError = new Error(`JSON parse failed: ${e}`);
122
+ continue;
123
+ }
124
+ }
125
+ }
126
+
127
+ throw new Error(
128
+ `Failed to plan task after trying all models. Last error: ${lastError?.message ?? "unknown"}`
129
+ );
130
+ }
131
+
132
+ export function computeNextRun(cronExpr: string): string {
133
+ // Parse the 5-field cron expression and find the next matching UTC time
134
+ const [min, hour, dom, month, dow] = cronExpr.trim().split(/\s+/);
135
+
136
+ const matchField = (value: number, field: string): boolean => {
137
+ if (field === "*") return true;
138
+ if (field.includes("/")) {
139
+ const [base, step] = field.split("/");
140
+ const start = base === "*" ? 0 : parseInt(base as string, 10);
141
+ return (value - start) % parseInt(step!, 10) === 0 && value >= start;
142
+ }
143
+ if (field.includes(",")) return field.split(",").map(Number).includes(value);
144
+ if (field.includes("-")) {
145
+ const [lo, hi] = field.split("-").map(Number);
146
+ return value >= lo! && value <= hi!;
147
+ }
148
+ return parseInt(field, 10) === value;
149
+ };
150
+
151
+ // Scan forward from next minute in UTC (Supabase pg_cron runs in UTC)
152
+ const now = new Date();
153
+ now.setSeconds(0, 0);
154
+
155
+ for (let offset = 1; offset <= 60 * 24 * 7; offset++) {
156
+ const c = new Date(now.getTime() + offset * 60_000);
157
+ if (
158
+ matchField(c.getUTCMinutes(), min!) &&
159
+ matchField(c.getUTCHours(), hour!) &&
160
+ matchField(c.getUTCDate(), dom!) &&
161
+ matchField(c.getUTCMonth() + 1, month!) &&
162
+ matchField(c.getUTCDay(), dow!)
163
+ ) {
164
+ return c.toISOString();
165
+ }
166
+ }
167
+
168
+ // Fallback: 1 hour from now
169
+ return new Date(now.getTime() + 3_600_000).toISOString();
170
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Update an existing task's email_send steps with correct recipient
3
+ * Usage: bun run scheduler/update-task-email.ts "task-id" "your@email.com"
4
+ */
5
+
6
+ import { getTaskById, updateTask } from "./db";
7
+ import chalk from "chalk";
8
+
9
+ const taskId = process.argv[2];
10
+ const email = process.argv[3];
11
+
12
+ if (!taskId || !email) {
13
+ console.log(chalk.red("\n❌ Usage:\n"));
14
+ console.log(chalk.cyan('bun run scheduler/update-task-email.ts "task-id" "your@email.com"\n'));
15
+ console.log(chalk.dim("To find task IDs, run: jimmy jet → Scheduler → List all tasks\n"));
16
+ process.exit(1);
17
+ }
18
+
19
+ if (!email.includes("@")) {
20
+ console.log(chalk.red("\n❌ Invalid email address\n"));
21
+ process.exit(1);
22
+ }
23
+
24
+ async function updateTaskEmail() {
25
+ const task = await getTaskById(taskId as string);
26
+
27
+ if (!task) {
28
+ console.log(chalk.red(`\n❌ Task not found: ${taskId}\n`));
29
+ process.exit(1);
30
+ }
31
+
32
+ console.log(chalk.bold(`\nUpdating task: ${task.name}`));
33
+ console.log(chalk.dim(`Current steps:`));
34
+ task.steps.forEach((s) => console.log(` ${s.order}. [${s.type}] ${s.instruction}`));
35
+
36
+ // Update all email_send steps
37
+ const updatedSteps = task.steps.map((step) => {
38
+ if (step.type === "email_send") {
39
+ // Replace any existing email or USER_EMAIL with the new one
40
+ let newInstruction = step.instruction
41
+ .replace(/USER_EMAIL/g, email as string)
42
+ .replace(/recipient@example\.com/g, email as string)
43
+ .replace(/to [^\s@]+@[^\s@]+/g, `to ${email}`);
44
+
45
+ // If no "to X" pattern found, append it
46
+ if (!newInstruction.includes("to ") || !newInstruction.includes("@")) {
47
+ newInstruction = `${newInstruction} to ${email}`;
48
+ }
49
+
50
+ return { ...step, instruction: newInstruction };
51
+ }
52
+ return step;
53
+ });
54
+
55
+ await updateTask(taskId as string, { steps: updatedSteps });
56
+
57
+ console.log(chalk.green(`\n✓ Updated email steps to send to: ${email}\n`));
58
+ console.log(chalk.dim("Updated steps:"));
59
+ updatedSteps.forEach((s) => {
60
+ if (s.type === "email_send") {
61
+ console.log(chalk.cyan(` ${s.order}. [${s.type}] ${s.instruction}`));
62
+ }
63
+ });
64
+ console.log();
65
+ }
66
+
67
+ updateTaskEmail().catch((err) => {
68
+ console.error(chalk.red(`\n✖ ${err instanceof Error ? err.message : String(err)}\n`));
69
+ process.exit(1);
70
+ });
@@ -0,0 +1 @@
1
+ v2.105.0
@@ -0,0 +1 @@
1
+ v2.189.0
@@ -0,0 +1 @@
1
+ {"ref":"ebqpfhzeswycagvzgtik","name":"Jimmy AI","organization_id":"rqgvjmsbeivsipzjozzh","organization_slug":"rqgvjmsbeivsipzjozzh"}
@@ -0,0 +1 @@
1
+ postgresql://postgres.ebqpfhzeswycagvzgtik@aws-1-ap-southeast-2.pooler.supabase.com:5432/postgres
@@ -0,0 +1 @@
1
+ 17.6.1.127