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,82 @@
1
+ /**
2
+ * Writes/updates the project .env file from decrypted keys.
3
+ * Called after login — no user ever needs to manually create a .env file.
4
+ * Keys that are empty are omitted. Existing .env entries NOT in our key set are preserved.
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+
10
+ const ENV_PATH = path.resolve(process.cwd(), ".env");
11
+
12
+ const MANAGED_KEYS = new Set([
13
+ "OPENROUTER_KEY",
14
+ "OPENROUTER_MODEL",
15
+ "OPENROUTER_TIER",
16
+ "GOOGLE_GENERATIVE_AI_API_KEY",
17
+ "GROQ_API_KEY",
18
+ "ANTHROPIC_API_KEY",
19
+ "OPENAI_API_KEY",
20
+ "TELEGRAM_BOT_TOKEN",
21
+ "TELEGRAM_OWNER_ID",
22
+ "SUPABASE_URL",
23
+ "SUPABASE_SERVICE_ROLE_KEY",
24
+ "SUPABASE_ACCESS_TOKEN",
25
+ "GOOGLE_CLIENT_ID",
26
+ "GOOGLE_CLIENT_SECRET",
27
+ "FIRECRAWL_KEY",
28
+ "APIFY_API_KEY",
29
+ "BROWSERBASE_API_KEY",
30
+ "BROWSERBASE_PRODUCT_ID",
31
+ "PREFERRED_PROVIDERS",
32
+ "MODEL_OPENROUTER_PAID",
33
+ "MODEL_GEMINI",
34
+ "MODEL_CLAUDE",
35
+ "MODEL_OPENAI",
36
+ "MODEL_GROQ",
37
+ "PORT",
38
+ ]);
39
+
40
+ export function writeEnvFile(keys: Record<string, string>): void {
41
+ // PORT is always 8787
42
+ keys["PORT"] = "8787";
43
+
44
+ let existingLines: string[] = [];
45
+ if (fs.existsSync(ENV_PATH)) {
46
+ existingLines = fs.readFileSync(ENV_PATH, "utf8").split("\n");
47
+ }
48
+
49
+ const written = new Set<string>();
50
+ const seenKeys = new Set<string>();
51
+
52
+ const updatedLines = existingLines
53
+ .map((line) => {
54
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
55
+ if (!match) return line; // comment or blank — keep as-is
56
+ const key = match[1]!;
57
+
58
+ // Drop duplicate occurrences of the same key
59
+ if (seenKeys.has(key)) return null;
60
+ seenKeys.add(key);
61
+
62
+ if (!MANAGED_KEYS.has(key)) return line; // not ours — keep as-is
63
+
64
+ written.add(key);
65
+ const value = keys[key];
66
+ // Always write a value for PORT; omit blank non-PORT managed keys
67
+ if (!value) return key === "PORT" ? `PORT=8787` : `${key}=`;
68
+ return `${key}=${value}`;
69
+ })
70
+ .filter((line): line is string => line !== null);
71
+
72
+ // Append managed keys not yet in the file
73
+ const newLines: string[] = [];
74
+ for (const [key, value] of Object.entries(keys)) {
75
+ if (MANAGED_KEYS.has(key) && !written.has(key) && value) {
76
+ newLines.push(`${key}=${value}`);
77
+ }
78
+ }
79
+
80
+ const final = [...updatedLines, ...newLines].join("\n").trimEnd() + "\n";
81
+ fs.writeFileSync(ENV_PATH, final, "utf8");
82
+ }
package/bin/jerob.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npm entry point for jerob.
4
+ * Requires Bun to be installed: https://bun.sh
5
+ * Shells out to Bun so the TypeScript source runs natively.
6
+ */
7
+ const { execFileSync } = require("node:child_process");
8
+ const path = require("node:path");
9
+
10
+ // Check Bun is available
11
+ try {
12
+ execFileSync("bun", ["--version"], { stdio: "ignore" });
13
+ } catch {
14
+ console.error(
15
+ "\n[jerob] Bun is required but not installed.\n" +
16
+ "Install it from https://bun.sh and try again.\n" +
17
+ " Windows: powershell -c \"irm bun.sh/install.ps1 | iex\"\n" +
18
+ " macOS/Linux: curl -fsSL https://bun.sh/install | bash\n"
19
+ );
20
+ process.exit(1);
21
+ }
22
+
23
+ const entry = path.join(__dirname, "..", "index.ts");
24
+
25
+ execFileSync("bun", [entry, ...process.argv.slice(2)], {
26
+ stdio: "inherit",
27
+ cwd: process.cwd(),
28
+ });
@@ -0,0 +1,163 @@
1
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
2
+ import { createGroq } from "@ai-sdk/groq";
3
+ import { createAnthropic } from "@ai-sdk/anthropic";
4
+ import { createOpenAI } from "@ai-sdk/openai";
5
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
6
+ import type { LanguageModel } from "ai";
7
+
8
+ // ── Defaults — used when no MODEL_* override is set ──────────────────────────
9
+ export const DEFAULT_MODELS = {
10
+ openrouter_free: "openrouter/free",
11
+ openrouter_paid: "anthropic/claude-3.5-sonnet",
12
+ gemini: "gemini-3.1-flash-lite-preview",
13
+ claude: "claude-3-5-sonnet-20241022",
14
+ openai: "gpt-4o-mini",
15
+ groq: "llama-3.3-70b-versatile",
16
+ } as const;
17
+
18
+ // ── Active model per provider ─────────────────────────────────────────────────
19
+ // MODEL_* env vars are written by auth/env-writer.ts from stored modelOverrides.
20
+ function activeModel(provider: keyof typeof DEFAULT_MODELS): string {
21
+ const key = `MODEL_${provider.toUpperCase().replace("-", "_")}`;
22
+ return process.env[key]?.trim() || DEFAULT_MODELS[provider];
23
+ }
24
+
25
+ // ── Provider builders ─────────────────────────────────────────────────────────
26
+
27
+ function openrouterModel(modelId: string): LanguageModel | null {
28
+ const key = process.env.OPENROUTER_KEY?.trim();
29
+ if (!key || !key.startsWith("sk-or-v1-")) return null;
30
+ return createOpenRouter({ apiKey: key })(modelId);
31
+ }
32
+
33
+ function geminiModel(modelId?: string): LanguageModel | null {
34
+ const key = process.env.GOOGLE_GENERATIVE_AI_API_KEY?.trim();
35
+ if (!key) return null;
36
+ return createGoogleGenerativeAI({ apiKey: key })(modelId ?? activeModel("gemini"));
37
+ }
38
+
39
+ function claudeModel(modelId?: string): LanguageModel | null {
40
+ const key = process.env.ANTHROPIC_API_KEY?.trim();
41
+ if (!key) return null;
42
+ return createAnthropic({ apiKey: key })(modelId ?? activeModel("claude"));
43
+ }
44
+
45
+ function openaiModel(modelId?: string): LanguageModel | null {
46
+ const key = process.env.OPENAI_API_KEY?.trim();
47
+ if (!key) return null;
48
+ return createOpenAI({ apiKey: key })(modelId ?? activeModel("openai"));
49
+ }
50
+
51
+ function groqModel(modelId?: string): LanguageModel | null {
52
+ const key = process.env.GROQ_API_KEY?.trim();
53
+ if (!key) return null;
54
+ return createGroq({ apiKey: key })(modelId ?? activeModel("groq"));
55
+ }
56
+
57
+ // ── Helpers ───────────────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Returns providers in the exact order the user chose during setup:
61
+ * [primary, ...optionalFallbacks, groq]
62
+ * Written to PREFERRED_PROVIDERS by auth/env-writer.ts.
63
+ */
64
+ function preferredProviders(): string[] {
65
+ const raw = process.env.PREFERRED_PROVIDERS;
66
+ if (raw) return raw.split(",").map((s) => s.trim()).filter(Boolean);
67
+ return ["groq"];
68
+ }
69
+
70
+ function modelForProvider(provider: string): LanguageModel | null {
71
+ switch (provider) {
72
+ case "gemini": return geminiModel();
73
+ case "claude": return claudeModel();
74
+ case "openai": return openaiModel();
75
+ case "groq": return groqModel();
76
+ default: return null;
77
+ }
78
+ }
79
+
80
+ function firstAvailable(
81
+ candidates: Array<() => LanguageModel | null>,
82
+ name: string
83
+ ): LanguageModel {
84
+ for (const build of candidates) {
85
+ const m = build();
86
+ if (m) return m;
87
+ }
88
+ throw new Error(
89
+ `No AI model available for ${name}. ` +
90
+ `Run \`jerob set-key\` to add API keys or switch models.`
91
+ );
92
+ }
93
+
94
+ // ── Public API ────────────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Primary model — Agent, Plan, Ask modes.
98
+ *
99
+ * Order:
100
+ * 1. OpenRouter (paid model or free slug, if key present)
101
+ * 2. User's chosen providers in their selected order (PREFERRED_PROVIDERS)
102
+ * — primary first, optional fallbacks after, groq always last
103
+ */
104
+ export function getAgentModel(): LanguageModel {
105
+ const tier = process.env.OPENROUTER_TIER ?? "free";
106
+ const orKey = process.env.OPENROUTER_KEY?.trim();
107
+
108
+ const candidates: Array<() => LanguageModel | null> = [];
109
+
110
+ if (orKey) {
111
+ candidates.push(
112
+ tier === "paid"
113
+ ? () => openrouterModel(activeModel("openrouter_paid"))
114
+ : () => openrouterModel(DEFAULT_MODELS.openrouter_free)
115
+ );
116
+ }
117
+
118
+ // Respect user's chosen order exactly
119
+ for (const p of preferredProviders()) {
120
+ candidates.push(() => modelForProvider(p));
121
+ }
122
+
123
+ return firstAvailable(candidates, "Agent");
124
+ }
125
+
126
+ /**
127
+ * Secondary model — Scheduler planner, Browser Agent evaluator.
128
+ * Skips OpenRouter to avoid content refusals on structured prompts.
129
+ * Uses the same user-defined provider order as the primary model.
130
+ */
131
+ export function getAgentModel2(): LanguageModel {
132
+ const providers = preferredProviders();
133
+ const candidates = providers.map((p) => () => modelForProvider(p));
134
+ // Groq is already last in preferredProviders from setup, but ensure it's there
135
+ if (!providers.includes("groq")) {
136
+ candidates.push(() => groqModel());
137
+ }
138
+ return firstAvailable(candidates, "Model2");
139
+ }
140
+
141
+ /**
142
+ * Fallback model — when primary refuses or hits rate limits.
143
+ * Uses user's provider order, then falls back to openrouter/free → groq.
144
+ */
145
+ export function getAgentModel2Fallback(): LanguageModel {
146
+ const providers = preferredProviders();
147
+ const candidates: Array<() => LanguageModel | null> = providers.map(
148
+ (p) => () => modelForProvider(p)
149
+ );
150
+ // Final safety nets
151
+ candidates.push(() => openrouterModel(DEFAULT_MODELS.openrouter_free));
152
+ if (!providers.includes("groq")) candidates.push(() => groqModel());
153
+ return firstAvailable(candidates, "Fallback");
154
+ }
155
+
156
+ /**
157
+ * Explicit named model — for Stagehand/browser executor.
158
+ */
159
+ export function getNamedModel(
160
+ provider: "gemini" | "claude" | "openai" | "groq"
161
+ ): LanguageModel {
162
+ return firstAvailable([() => modelForProvider(provider)], provider);
163
+ }
@@ -0,0 +1,178 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import {
4
+ sendMail,
5
+ readMail,
6
+ searchMail,
7
+ summarizeMail,
8
+ replyMail,
9
+ draftMail,
10
+ deleteMail,
11
+ archiveMail,
12
+ labelMail,
13
+ classifyMail,
14
+ extractTasks,
15
+ bulkAction,
16
+ digest,
17
+ scheduleSend,
18
+ sendDraft,
19
+ getThread,
20
+ } from "./email_functions";
21
+
22
+ export function createEmailTools() {
23
+ return {
24
+ email_send: tool({
25
+ description: "Send an email to one or more recipients.",
26
+ inputSchema: z.object({
27
+ to: z.union([z.string(), z.array(z.string())]).describe("Recipient email(s)"),
28
+ subject: z.string(),
29
+ body: z.string().describe("Plain text body"),
30
+ cc: z.array(z.string()).optional(),
31
+ bcc: z.array(z.string()).optional(),
32
+ }),
33
+ execute: async (input) => sendMail(input),
34
+ }),
35
+
36
+ email_read: tool({
37
+ description: "Read a single email by its message ID.",
38
+ inputSchema: z.object({
39
+ messageId: z.string(),
40
+ }),
41
+ execute: async ({ messageId }) => readMail({ messageId }),
42
+ }),
43
+
44
+ email_search: tool({
45
+ description:
46
+ 'Search emails using Gmail query syntax (e.g. "from:foo@bar.com is:unread").',
47
+ inputSchema: z.object({
48
+ query: z.string(),
49
+ maxResults: z.number().optional().default(10),
50
+ }),
51
+ execute: async ({ query, maxResults }) => searchMail({ query, maxResults }),
52
+ }),
53
+
54
+ email_summarize: tool({
55
+ description: "Summarize an email using AI.",
56
+ inputSchema: z.object({
57
+ messageId: z.string(),
58
+ }),
59
+ execute: async ({ messageId }) => summarizeMail({ messageId }),
60
+ }),
61
+
62
+ email_reply: tool({
63
+ description: "Reply to an existing email, keeping the same thread.",
64
+ inputSchema: z.object({
65
+ messageId: z.string().describe("ID of the message to reply to"),
66
+ body: z.string().describe("Reply body text"),
67
+ }),
68
+ execute: async ({ messageId, body }) => replyMail({ messageId, body }),
69
+ }),
70
+
71
+ email_draft: tool({
72
+ description: "Save an email as a draft without sending.",
73
+ inputSchema: z.object({
74
+ to: z.union([z.string(), z.array(z.string())]),
75
+ subject: z.string(),
76
+ body: z.string(),
77
+ cc: z.array(z.string()).optional(),
78
+ bcc: z.array(z.string()).optional(),
79
+ }),
80
+ execute: async (input) => draftMail(input),
81
+ }),
82
+
83
+ email_delete: tool({
84
+ description: "Permanently delete an email by message ID.",
85
+ inputSchema: z.object({
86
+ messageId: z.string(),
87
+ }),
88
+ execute: async ({ messageId }) => deleteMail({ messageId }),
89
+ }),
90
+
91
+ email_archive: tool({
92
+ description: "Archive an email (removes it from Inbox).",
93
+ inputSchema: z.object({
94
+ messageId: z.string(),
95
+ }),
96
+ execute: async ({ messageId }) => archiveMail({ messageId }),
97
+ }),
98
+
99
+ email_label: tool({
100
+ description: "Add or remove labels on an email.",
101
+ inputSchema: z.object({
102
+ messageId: z.string(),
103
+ labelIds: z.array(z.string()).describe("Label IDs to add"),
104
+ removeLabelIds: z.array(z.string()).optional().describe("Label IDs to remove"),
105
+ }),
106
+ execute: async ({ messageId, labelIds, removeLabelIds }) =>
107
+ labelMail({ messageId, labelIds, removeLabelIds }),
108
+ }),
109
+
110
+ email_classify: tool({
111
+ description:
112
+ "Classify an email into a category (work, personal, newsletter, spam, finance, travel, support, social, other).",
113
+ inputSchema: z.object({
114
+ messageId: z.string(),
115
+ }),
116
+ execute: async ({ messageId }) => classifyMail({ messageId }),
117
+ }),
118
+
119
+ email_extract_tasks: tool({
120
+ description: "Extract action items and tasks from an email using AI.",
121
+ inputSchema: z.object({
122
+ messageId: z.string(),
123
+ }),
124
+ execute: async ({ messageId }) => extractTasks({ messageId }),
125
+ }),
126
+
127
+ email_bulk_action: tool({
128
+ description: "Perform an action on multiple emails at once.",
129
+ inputSchema: z.object({
130
+ messageIds: z.array(z.string()),
131
+ action: z.enum(["delete", "archive", "markRead", "markUnread", "label"]),
132
+ labelIds: z.array(z.string()).optional(),
133
+ }),
134
+ execute: async ({ messageIds, action, labelIds }) =>
135
+ bulkAction({ messageIds, action, labelIds }),
136
+ }),
137
+
138
+ email_digest: tool({
139
+ description: "Generate an AI digest summary of recent or filtered emails.",
140
+ inputSchema: z.object({
141
+ query: z.string().optional().default("is:unread in:inbox"),
142
+ maxResults: z.number().optional().default(10),
143
+ }),
144
+ execute: async ({ query, maxResults }) => digest({ query, maxResults }),
145
+ }),
146
+
147
+ email_schedule_send: tool({
148
+ description:
149
+ "Save an email as a draft scheduled for a future time. Returns draftId to send later.",
150
+ inputSchema: z.object({
151
+ to: z.union([z.string(), z.array(z.string())]),
152
+ subject: z.string(),
153
+ body: z.string(),
154
+ sendAt: z.string().describe("ISO 8601 datetime string for when to send"),
155
+ cc: z.array(z.string()).optional(),
156
+ bcc: z.array(z.string()).optional(),
157
+ }),
158
+ execute: async ({ sendAt, ...rest }) =>
159
+ scheduleSend({ ...rest, sendAt: new Date(sendAt) }),
160
+ }),
161
+
162
+ email_send_draft: tool({
163
+ description: "Send a previously saved draft by its draft ID.",
164
+ inputSchema: z.object({
165
+ draftId: z.string(),
166
+ }),
167
+ execute: async ({ draftId }) => sendDraft(draftId),
168
+ }),
169
+
170
+ email_thread: tool({
171
+ description: "Get all messages in an email thread.",
172
+ inputSchema: z.object({
173
+ threadId: z.string(),
174
+ }),
175
+ execute: async ({ threadId }) => getThread({ threadId }),
176
+ }),
177
+ };
178
+ }