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.
- package/CLI/cli.ts +42 -0
- package/README.md +137 -0
- package/SETUP.md +584 -0
- package/agent/action-tracker.ts +45 -0
- package/agent/agent-tools.ts +111 -0
- package/agent/approval.ts +137 -0
- package/agent/diff-view.ts +26 -0
- package/agent/orchestrator.ts +186 -0
- package/agent/tool-executor.ts +463 -0
- package/agent/types.ts +69 -0
- package/ask/orchestrator.ts +244 -0
- package/auth/auth.ts +567 -0
- package/auth/config-store.ts +77 -0
- package/auth/crypto.ts +51 -0
- package/auth/env-writer.ts +82 -0
- package/bin/jerob.js +28 -0
- package/config/ai.config.ts +163 -0
- package/email_ops/email-tools.ts +178 -0
- package/email_ops/email_functions.ts +443 -0
- package/email_ops/email_init.ts +92 -0
- package/email_ops/email_pass_store.ts +61 -0
- package/email_ops/email_server.ts +29 -0
- package/email_ops/types.ts +88 -0
- package/index.ts +176 -0
- package/package.json +88 -0
- package/plan/browser-agent/README.md +118 -0
- package/plan/browser-agent/USAGE.md +308 -0
- package/plan/browser-agent/evaluator.ts +353 -0
- package/plan/browser-agent/executor.ts +372 -0
- package/plan/browser-agent/index.ts +13 -0
- package/plan/browser-agent/orchestrator.ts +323 -0
- package/plan/browser-agent/planner.ts +200 -0
- package/plan/browser-agent/types.ts +62 -0
- package/plan/browser-tool.ts +128 -0
- package/plan/index.ts +12 -0
- package/plan/orchestrator.ts +214 -0
- package/plan/planner.ts +183 -0
- package/plan/selection.ts +50 -0
- package/plan/types.ts +13 -0
- package/plan/web-tools.ts +119 -0
- package/scheduler/ARCHITECTURE.md +263 -0
- package/scheduler/README.md +200 -0
- package/scheduler/SETUP-READY.sql +84 -0
- package/scheduler/check-status.sql +124 -0
- package/scheduler/config-sync.ts +91 -0
- package/scheduler/db-migrate.ts +271 -0
- package/scheduler/db.ts +162 -0
- package/scheduler/debug.ts +184 -0
- package/scheduler/orchestrator.ts +438 -0
- package/scheduler/planner.ts +170 -0
- package/scheduler/update-task-email.ts +70 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/linked-project.json +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/deploy.ps1 +50 -0
- package/supabase/functions/scheduler-tick/index.ts +496 -0
- package/supabase/supabase/.temp/linked-project.json +1 -0
- package/tsconfig.json +33 -0
- package/tui/spinner.ts +33 -0
- package/tui/spinup.ts +67 -0
- package/tui/terminal-render.ts +16 -0
- package/utils/llm-error.ts +185 -0
- package/utils/model-validator.ts +247 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the one-time Supabase schema setup automatically.
|
|
3
|
+
*
|
|
4
|
+
* SQL migrations + Edge Function deployment use the Supabase Management API
|
|
5
|
+
* which requires a PERSONAL ACCESS TOKEN (not the service role key).
|
|
6
|
+
* Get one at: https://supabase.com/dashboard/account/tokens
|
|
7
|
+
*
|
|
8
|
+
* The service role key is only used at runtime (stored in user_config / Edge Function secrets).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
|
|
13
|
+
// ─── SQL Migrations ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const MIGRATIONS: { label: string; sql: string }[] = [
|
|
16
|
+
{
|
|
17
|
+
label: "Enable pg_cron extension",
|
|
18
|
+
sql: `CREATE EXTENSION IF NOT EXISTS pg_cron; GRANT USAGE ON SCHEMA cron TO postgres;`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: "Create scheduler_tasks table",
|
|
22
|
+
sql: `
|
|
23
|
+
CREATE TABLE IF NOT EXISTS scheduler_tasks (
|
|
24
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
25
|
+
name TEXT NOT NULL,
|
|
26
|
+
description TEXT NOT NULL DEFAULT '',
|
|
27
|
+
cron TEXT NOT NULL,
|
|
28
|
+
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
29
|
+
steps JSONB NOT NULL DEFAULT '[]',
|
|
30
|
+
summary_email TEXT,
|
|
31
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
32
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
33
|
+
last_run_at TIMESTAMPTZ,
|
|
34
|
+
next_run_at TIMESTAMPTZ,
|
|
35
|
+
run_count INTEGER NOT NULL DEFAULT 0
|
|
36
|
+
);`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: "Create scheduler_runs table",
|
|
40
|
+
sql: `
|
|
41
|
+
CREATE TABLE IF NOT EXISTS scheduler_runs (
|
|
42
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
43
|
+
task_id UUID NOT NULL REFERENCES scheduler_tasks(id) ON DELETE CASCADE,
|
|
44
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
45
|
+
finished_at TIMESTAMPTZ,
|
|
46
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running','success','failed')),
|
|
47
|
+
output TEXT,
|
|
48
|
+
error TEXT,
|
|
49
|
+
step_results JSONB NOT NULL DEFAULT '[]'
|
|
50
|
+
);`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Create user_config table",
|
|
54
|
+
sql: `
|
|
55
|
+
CREATE TABLE IF NOT EXISTS user_config (
|
|
56
|
+
key TEXT PRIMARY KEY,
|
|
57
|
+
value TEXT NOT NULL,
|
|
58
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
59
|
+
);`,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: "Enable Row Level Security",
|
|
63
|
+
sql: `
|
|
64
|
+
ALTER TABLE scheduler_tasks ENABLE ROW LEVEL SECURITY;
|
|
65
|
+
ALTER TABLE scheduler_runs ENABLE ROW LEVEL SECURITY;
|
|
66
|
+
ALTER TABLE user_config ENABLE ROW LEVEL SECURITY;`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: "Create RLS policies",
|
|
70
|
+
sql: `
|
|
71
|
+
DROP POLICY IF EXISTS "Service role full access" ON scheduler_tasks;
|
|
72
|
+
DROP POLICY IF EXISTS "Service role full access" ON scheduler_runs;
|
|
73
|
+
DROP POLICY IF EXISTS "Service role full access" ON user_config;
|
|
74
|
+
CREATE POLICY "Service role full access" ON scheduler_tasks FOR ALL USING (auth.role() = 'service_role');
|
|
75
|
+
CREATE POLICY "Service role full access" ON scheduler_runs FOR ALL USING (auth.role() = 'service_role');
|
|
76
|
+
CREATE POLICY "Service role full access" ON user_config FOR ALL USING (auth.role() = 'service_role');`,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** "https://abcdefgh.supabase.co" → "abcdefgh" */
|
|
83
|
+
function extractProjectRef(supabaseUrl: string): string | null {
|
|
84
|
+
const match = supabaseUrl.match(/https:\/\/([^.]+)\.supabase\.co/);
|
|
85
|
+
return match?.[1] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Execute raw SQL via the Supabase Management API (requires personal access token). */
|
|
89
|
+
async function execSql(projectRef: string, accessToken: string, sql: string): Promise<void> {
|
|
90
|
+
const res = await fetch(
|
|
91
|
+
`https://api.supabase.com/v1/projects/${projectRef}/database/query`,
|
|
92
|
+
{
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: {
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
Authorization: `Bearer ${accessToken}`,
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({ query: sql }),
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const body = await res.text();
|
|
103
|
+
throw new Error(`SQL failed (${res.status}): ${body}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── pg_cron ──────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
async function runCronSchedule(
|
|
110
|
+
projectRef: string,
|
|
111
|
+
accessToken: string,
|
|
112
|
+
serviceRoleKey: string
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const sql = `
|
|
115
|
+
SELECT cron.unschedule('jerob-scheduler-tick')
|
|
116
|
+
WHERE EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'jerob-scheduler-tick');
|
|
117
|
+
|
|
118
|
+
SELECT cron.schedule(
|
|
119
|
+
'jerob-scheduler-tick',
|
|
120
|
+
'* * * * *',
|
|
121
|
+
$sql$
|
|
122
|
+
SELECT net.http_post(
|
|
123
|
+
url := 'https://${projectRef}.supabase.co/functions/v1/scheduler-tick',
|
|
124
|
+
headers := jsonb_build_object(
|
|
125
|
+
'Content-Type', 'application/json',
|
|
126
|
+
'Authorization', 'Bearer ${serviceRoleKey}'
|
|
127
|
+
),
|
|
128
|
+
body := '{}'::jsonb
|
|
129
|
+
);
|
|
130
|
+
$sql$
|
|
131
|
+
);
|
|
132
|
+
`;
|
|
133
|
+
await execSql(projectRef, accessToken, sql);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Edge Function deployment ─────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Deploys the Edge Function using the Supabase CLI if installed.
|
|
140
|
+
* The Management REST API cannot bundle Deno TypeScript — only the CLI can.
|
|
141
|
+
*/
|
|
142
|
+
async function deployEdgeFunction(projectRef: string, accessToken: string): Promise<void> {
|
|
143
|
+
// Check if supabase CLI is available
|
|
144
|
+
const { execSync } = await import("node:child_process");
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
execSync("supabase --version", { stdio: "ignore" });
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"Supabase CLI not installed. Install it from https://supabase.com/docs/guides/cli then run `jerob setup-db` again, or deploy manually with: supabase functions deploy scheduler-tick"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Login with access token non-interactively, then link project, then deploy
|
|
155
|
+
try {
|
|
156
|
+
execSync(`supabase login --token ${accessToken}`, { stdio: "pipe" });
|
|
157
|
+
} catch {
|
|
158
|
+
// login may fail if already logged in — continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
execSync(`supabase link --project-ref ${projectRef}`, { stdio: "pipe" });
|
|
163
|
+
} catch {
|
|
164
|
+
// link may fail if already linked — continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Deploy — throws if it fails
|
|
168
|
+
execSync("supabase functions deploy scheduler-tick --no-verify-jwt", {
|
|
169
|
+
stdio: "pipe",
|
|
170
|
+
cwd: process.cwd(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Sets secrets on the Edge Function.
|
|
176
|
+
* NOTE: Supabase blocks the "SUPABASE_" prefix — use APP_ prefix instead.
|
|
177
|
+
* The Edge Function reads APP_DB_URL and APP_SERVICE_KEY.
|
|
178
|
+
*/
|
|
179
|
+
async function setEdgeFunctionSecrets(
|
|
180
|
+
projectRef: string,
|
|
181
|
+
accessToken: string,
|
|
182
|
+
supabaseUrl: string,
|
|
183
|
+
serviceRoleKey: string
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
const res = await fetch(`https://api.supabase.com/v1/projects/${projectRef}/secrets`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
Authorization: `Bearer ${accessToken}`,
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify([
|
|
192
|
+
{ name: "APP_DB_URL", value: supabaseUrl },
|
|
193
|
+
{ name: "APP_SERVICE_KEY", value: serviceRoleKey },
|
|
194
|
+
]),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const body = await res.text();
|
|
199
|
+
throw new Error(`Secret upload failed (${res.status}): ${body}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Main entry point ─────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param supabaseUrl https://xxx.supabase.co
|
|
207
|
+
* @param serviceRoleKey Runtime service role key (stored in secrets/user_config)
|
|
208
|
+
* @param accessToken Personal access token for Management API (setup only)
|
|
209
|
+
*/
|
|
210
|
+
export async function runDbMigrations(
|
|
211
|
+
supabaseUrl: string,
|
|
212
|
+
serviceRoleKey: string,
|
|
213
|
+
accessToken: string
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
const projectRef = extractProjectRef(supabaseUrl);
|
|
216
|
+
if (!projectRef) {
|
|
217
|
+
console.log(chalk.yellow("⚠ Could not parse project ref from Supabase URL — skipping."));
|
|
218
|
+
console.log(chalk.dim(" Expected format: https://YOUR_REF.supabase.co"));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(chalk.bold("\n🗄 Setting up Supabase...\n"));
|
|
223
|
+
|
|
224
|
+
// 1. SQL migrations
|
|
225
|
+
for (const migration of MIGRATIONS) {
|
|
226
|
+
process.stdout.write(chalk.dim(` ${migration.label}... `));
|
|
227
|
+
try {
|
|
228
|
+
await execSql(projectRef, accessToken, migration.sql);
|
|
229
|
+
console.log(chalk.green("✓"));
|
|
230
|
+
} catch (err: any) {
|
|
231
|
+
const msg: string = err?.message ?? String(err);
|
|
232
|
+
if (msg.includes("already exists")) {
|
|
233
|
+
console.log(chalk.dim("already exists, skipped"));
|
|
234
|
+
} else {
|
|
235
|
+
console.log(chalk.red(`✗ ${msg}`));
|
|
236
|
+
console.log(chalk.yellow(" Run scheduler/SETUP-READY.sql manually if needed."));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2. Deploy Edge Function
|
|
242
|
+
process.stdout.write(chalk.dim(` Deploy scheduler-tick Edge Function... `));
|
|
243
|
+
try {
|
|
244
|
+
await deployEdgeFunction(projectRef, accessToken);
|
|
245
|
+
console.log(chalk.green("✓"));
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
console.log(chalk.yellow(`⚠ ${err?.message ?? err}`));
|
|
248
|
+
console.log(chalk.dim(" Deploy manually: supabase functions deploy scheduler-tick"));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 3. Set Edge Function secrets
|
|
252
|
+
process.stdout.write(chalk.dim(` Set Edge Function secrets... `));
|
|
253
|
+
try {
|
|
254
|
+
await setEdgeFunctionSecrets(projectRef, accessToken, supabaseUrl, serviceRoleKey);
|
|
255
|
+
console.log(chalk.green("✓"));
|
|
256
|
+
} catch (err: any) {
|
|
257
|
+
console.log(chalk.yellow(`⚠ ${err?.message ?? err}`));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 4. pg_cron schedule
|
|
261
|
+
process.stdout.write(chalk.dim(` Schedule Edge Function via pg_cron... `));
|
|
262
|
+
try {
|
|
263
|
+
await runCronSchedule(projectRef, accessToken, serviceRoleKey);
|
|
264
|
+
console.log(chalk.green("✓"));
|
|
265
|
+
} catch (err: any) {
|
|
266
|
+
console.log(chalk.yellow(`⚠ pg_cron: ${err?.message ?? err}`));
|
|
267
|
+
console.log(chalk.dim(" Enable pg_cron: Supabase Dashboard → Database → Extensions"));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(chalk.green.bold("\n✅ Supabase fully set up — scheduler is live!\n"));
|
|
271
|
+
}
|
package/scheduler/db.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
// Lazy init — don't crash at import time if env vars are missing.
|
|
4
|
+
// getDb() is called at the first actual DB operation, giving .env time to load.
|
|
5
|
+
let _db: SupabaseClient | null = null;
|
|
6
|
+
|
|
7
|
+
export function getDb(): SupabaseClient {
|
|
8
|
+
if (_db) return _db;
|
|
9
|
+
const url = process.env.SUPABASE_URL;
|
|
10
|
+
const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_KEY;
|
|
11
|
+
if (!url || !key) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"Supabase is not configured. Add SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY to your .env file."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
_db = createClient(url, key);
|
|
17
|
+
return _db;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Types mirroring the DB schema ─────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface SchedulerTask {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
cron: string;
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
steps: TaskStep[];
|
|
29
|
+
summary_email: string | null;
|
|
30
|
+
created_at: string;
|
|
31
|
+
updated_at: string;
|
|
32
|
+
last_run_at: string | null;
|
|
33
|
+
next_run_at: string | null;
|
|
34
|
+
run_count: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TaskStep {
|
|
38
|
+
order: number;
|
|
39
|
+
type: "web_search" | "web_crawl" | "email_send" | "custom";
|
|
40
|
+
instruction: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SchedulerRun {
|
|
44
|
+
id: string;
|
|
45
|
+
task_id: string;
|
|
46
|
+
started_at: string;
|
|
47
|
+
finished_at: string | null;
|
|
48
|
+
status: "running" | "success" | "failed";
|
|
49
|
+
output: string | null;
|
|
50
|
+
error: string | null;
|
|
51
|
+
step_results: StepResult[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface StepResult {
|
|
55
|
+
order: number;
|
|
56
|
+
instruction: string;
|
|
57
|
+
output: string;
|
|
58
|
+
success: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Convenience typed query builder — avoids repeating `as any` everywhere.
|
|
62
|
+
// The untyped Supabase client infers `never` for table columns without a
|
|
63
|
+
// generated schema, so we go through `any` at the `.from()` boundary only.
|
|
64
|
+
function table(name: string) {
|
|
65
|
+
return getDb().from(name) as any;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── DB helpers ────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export async function createTask(
|
|
71
|
+
data: Omit<SchedulerTask, "id" | "created_at" | "updated_at" | "last_run_at" | "run_count">
|
|
72
|
+
): Promise<SchedulerTask> {
|
|
73
|
+
const { data: row, error } = await table("scheduler_tasks")
|
|
74
|
+
.insert({ ...data, next_run_at: data.next_run_at ?? null })
|
|
75
|
+
.select()
|
|
76
|
+
.single();
|
|
77
|
+
if (error) throw new Error(`Failed to create task: ${error.message}`);
|
|
78
|
+
return row as SchedulerTask;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function getAllTasks(): Promise<SchedulerTask[]> {
|
|
82
|
+
const { data, error } = await table("scheduler_tasks")
|
|
83
|
+
.select("*")
|
|
84
|
+
.order("created_at", { ascending: false });
|
|
85
|
+
if (error) throw new Error(`Failed to fetch tasks: ${error.message}`);
|
|
86
|
+
return (data ?? []) as SchedulerTask[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function getTaskById(id: string): Promise<SchedulerTask | null> {
|
|
90
|
+
const { data, error } = await table("scheduler_tasks")
|
|
91
|
+
.select("*")
|
|
92
|
+
.eq("id", id)
|
|
93
|
+
.single();
|
|
94
|
+
if (error) return null;
|
|
95
|
+
return data as SchedulerTask;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function updateTask(
|
|
99
|
+
id: string,
|
|
100
|
+
patch: Partial<Omit<SchedulerTask, "id" | "created_at">>
|
|
101
|
+
): Promise<SchedulerTask> {
|
|
102
|
+
const { data, error } = await table("scheduler_tasks")
|
|
103
|
+
.update({ ...patch, updated_at: new Date().toISOString() })
|
|
104
|
+
.eq("id", id)
|
|
105
|
+
.select()
|
|
106
|
+
.single();
|
|
107
|
+
if (error) throw new Error(`Failed to update task: ${error.message}`);
|
|
108
|
+
return data as SchedulerTask;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function deleteTask(id: string): Promise<void> {
|
|
112
|
+
const { error } = await table("scheduler_tasks").delete().eq("id", id);
|
|
113
|
+
if (error) throw new Error(`Failed to delete task: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function getEnabledDueTasks(): Promise<SchedulerTask[]> {
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
const { data, error } = await table("scheduler_tasks")
|
|
119
|
+
.select("*")
|
|
120
|
+
.eq("enabled", true)
|
|
121
|
+
.lte("next_run_at", now);
|
|
122
|
+
if (error) throw new Error(`Failed to fetch due tasks: ${error.message}`);
|
|
123
|
+
return (data ?? []) as SchedulerTask[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function createRun(taskId: string): Promise<SchedulerRun> {
|
|
127
|
+
const { data, error } = await table("scheduler_runs")
|
|
128
|
+
.insert({ task_id: taskId, status: "running", step_results: [] })
|
|
129
|
+
.select()
|
|
130
|
+
.single();
|
|
131
|
+
if (error) throw new Error(`Failed to create run record: ${error.message}`);
|
|
132
|
+
return data as SchedulerRun;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function finishRun(
|
|
136
|
+
runId: string,
|
|
137
|
+
status: "success" | "failed",
|
|
138
|
+
output: string,
|
|
139
|
+
stepResults: StepResult[],
|
|
140
|
+
errorMsg?: string
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const { error } = await table("scheduler_runs")
|
|
143
|
+
.update({
|
|
144
|
+
status,
|
|
145
|
+
output,
|
|
146
|
+
error: errorMsg ?? null,
|
|
147
|
+
step_results: stepResults,
|
|
148
|
+
finished_at: new Date().toISOString(),
|
|
149
|
+
})
|
|
150
|
+
.eq("id", runId);
|
|
151
|
+
if (error) throw new Error(`Failed to finish run record: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function getRunsForTask(taskId: string, limit = 10): Promise<SchedulerRun[]> {
|
|
155
|
+
const { data, error } = await table("scheduler_runs")
|
|
156
|
+
.select("*")
|
|
157
|
+
.eq("task_id", taskId)
|
|
158
|
+
.order("started_at", { ascending: false })
|
|
159
|
+
.limit(limit);
|
|
160
|
+
if (error) throw new Error(`Failed to fetch runs: ${error.message}`);
|
|
161
|
+
return (data ?? []) as SchedulerRun[];
|
|
162
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler Debug Helper
|
|
3
|
+
* Run: bun run scheduler/debug.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getDb, getAllTasks, getRunsForTask } from "./db";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
|
|
9
|
+
async function checkCredentials() {
|
|
10
|
+
console.log(chalk.bold("\n🔑 Checking Credentials in user_config...\n"));
|
|
11
|
+
|
|
12
|
+
const { data, error } = await getDb().from("user_config").select("key,updated_at");
|
|
13
|
+
|
|
14
|
+
if (error) {
|
|
15
|
+
console.log(chalk.red(`❌ Error: ${error.message}`));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!data || data.length === 0) {
|
|
20
|
+
console.log(chalk.red("❌ No credentials found in user_config table!"));
|
|
21
|
+
console.log(chalk.yellow("\nRun this to sync: bun run index.ts sync-credentials\n"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const keys = [
|
|
26
|
+
"openrouter_key",
|
|
27
|
+
"groq_api_key",
|
|
28
|
+
"firecrawl_key",
|
|
29
|
+
"google_client_id",
|
|
30
|
+
"google_client_secret",
|
|
31
|
+
"google_refresh_token",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
console.log("Credential Status:");
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
const found = data.find((r: any) => r.key === key);
|
|
37
|
+
if (found) {
|
|
38
|
+
console.log(` ${chalk.green("✓")} ${key} (updated: ${new Date((found as any).updated_at).toLocaleString()})`);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(` ${chalk.red("✖")} ${key} ${chalk.dim("(missing)")}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function checkTasks() {
|
|
47
|
+
console.log(chalk.bold("📋 Tasks:\n"));
|
|
48
|
+
|
|
49
|
+
const tasks = await getAllTasks();
|
|
50
|
+
|
|
51
|
+
if (tasks.length === 0) {
|
|
52
|
+
console.log(chalk.yellow("No tasks found.\n"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const t of tasks) {
|
|
57
|
+
console.log(chalk.bold(`${t.name}`));
|
|
58
|
+
console.log(` ID: ${t.id}`);
|
|
59
|
+
console.log(` Status: ${t.enabled ? chalk.green("enabled") : chalk.red("disabled")}`);
|
|
60
|
+
console.log(` Cron: ${t.cron}`);
|
|
61
|
+
console.log(` Next run: ${t.next_run_at ? new Date(t.next_run_at).toLocaleString() : chalk.dim("not scheduled")}`);
|
|
62
|
+
console.log(` Last run: ${t.last_run_at ? new Date(t.last_run_at).toLocaleString() : chalk.dim("never")}`);
|
|
63
|
+
console.log(` Run count: ${t.run_count}`);
|
|
64
|
+
console.log(` Steps: ${t.steps.length}`);
|
|
65
|
+
t.steps.forEach((s) => console.log(` ${s.order}. [${s.type}] ${s.instruction}`));
|
|
66
|
+
|
|
67
|
+
// Check last 3 runs
|
|
68
|
+
const runs = await getRunsForTask(t.id, 3);
|
|
69
|
+
if (runs.length > 0) {
|
|
70
|
+
console.log(chalk.dim(` Recent runs (${runs.length}):`));
|
|
71
|
+
runs.forEach((r, i) => {
|
|
72
|
+
const icon = r.status === "success" ? chalk.green("✓") : r.status === "failed" ? chalk.red("✖") : chalk.yellow("⟳");
|
|
73
|
+
console.log(` ${icon} ${new Date(r.started_at).toLocaleString()} — ${r.status}`);
|
|
74
|
+
if (r.error) console.log(chalk.red(` Error: ${r.error.slice(0, 100)}`));
|
|
75
|
+
if (r.output) console.log(chalk.dim(` ${r.output.slice(0, 100)}`));
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
console.log(chalk.dim(" No runs yet"));
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function checkCronStatus() {
|
|
85
|
+
console.log(chalk.bold("⏰ Checking pg_cron Status...\n"));
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Check if cron job exists
|
|
89
|
+
const { data: cronJobs, error: cronError } = await getDb().rpc("pg_cron_job_status" as any);
|
|
90
|
+
|
|
91
|
+
if (cronError && cronError.message.includes("does not exist")) {
|
|
92
|
+
console.log(chalk.yellow("⚠️ Cannot check pg_cron status via RPC (normal for some Supabase setups)"));
|
|
93
|
+
console.log(chalk.dim("Run this SQL in Supabase SQL Editor to check:"));
|
|
94
|
+
console.log(chalk.cyan(" select * from cron.job;"));
|
|
95
|
+
console.log(chalk.cyan(" select * from cron.job_run_details where jobname = 'jimmy-scheduler-tick' order by start_time desc limit 5;"));
|
|
96
|
+
console.log();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (cronError) {
|
|
101
|
+
console.log(chalk.red(`❌ Error: ${cronError.message}`));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log("Cron jobs:", cronJobs);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.log(chalk.yellow("⚠️ Could not check cron status directly"));
|
|
108
|
+
console.log(chalk.dim("This is normal — check manually in Supabase SQL Editor"));
|
|
109
|
+
}
|
|
110
|
+
console.log();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function testEdgeFunctionManually() {
|
|
114
|
+
console.log(chalk.bold("🧪 Manual Edge Function Trigger...\n"));
|
|
115
|
+
|
|
116
|
+
const url = process.env.SUPABASE_URL;
|
|
117
|
+
const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_KEY;
|
|
118
|
+
|
|
119
|
+
if (!url || !key) {
|
|
120
|
+
console.log(chalk.red("❌ SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY not set in .env"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const functionUrl = `${url}/functions/v1/scheduler-tick`;
|
|
125
|
+
console.log(`Calling: ${functionUrl}\n`);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(functionUrl, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
131
|
+
body: JSON.stringify({}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const text = await res.text();
|
|
135
|
+
|
|
136
|
+
if (res.ok) {
|
|
137
|
+
console.log(chalk.green(`✓ Success (${res.status})`));
|
|
138
|
+
try {
|
|
139
|
+
const data = JSON.parse(text);
|
|
140
|
+
console.log(chalk.bold("\nResponse:"));
|
|
141
|
+
console.log(JSON.stringify(data, null, 2));
|
|
142
|
+
} catch {
|
|
143
|
+
console.log(text);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
console.log(chalk.red(`✖ Failed (${res.status}): ${text}`));
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.log(chalk.red(`✖ Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
150
|
+
}
|
|
151
|
+
console.log();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function main() {
|
|
155
|
+
console.log(chalk.bold.cyan("\n════════════════════════════════════════════════"));
|
|
156
|
+
console.log(chalk.bold.cyan(" Jimmy Scheduler Debug Tool"));
|
|
157
|
+
console.log(chalk.bold.cyan("════════════════════════════════════════════════"));
|
|
158
|
+
|
|
159
|
+
await checkCredentials();
|
|
160
|
+
await checkTasks();
|
|
161
|
+
await checkCronStatus();
|
|
162
|
+
|
|
163
|
+
console.log(chalk.bold("────────────────────────────────────────────────"));
|
|
164
|
+
console.log(chalk.bold("Manual Test (optional)"));
|
|
165
|
+
console.log(chalk.bold("────────────────────────────────────────────────\n"));
|
|
166
|
+
console.log("This will manually trigger the Edge Function to test if it works:");
|
|
167
|
+
console.log(chalk.dim("(Press Ctrl+C to skip, or wait 3 seconds...)\n"));
|
|
168
|
+
|
|
169
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
170
|
+
|
|
171
|
+
await testEdgeFunctionManually();
|
|
172
|
+
|
|
173
|
+
console.log(chalk.bold.green("✓ Debug complete\n"));
|
|
174
|
+
console.log(chalk.dim("Next steps:"));
|
|
175
|
+
console.log(" • Check Edge Function logs: supabase functions logs scheduler-tick");
|
|
176
|
+
console.log(" • View runs in Supabase: select * from scheduler_runs order by started_at desc;");
|
|
177
|
+
console.log(" • Check cron job: select * from cron.job_run_details where jobname = 'jimmy-scheduler-tick';");
|
|
178
|
+
console.log();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch((err) => {
|
|
182
|
+
console.error(chalk.red(`\n✖ Debug tool crashed: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|