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
package/tui/spinup.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { select, isCancel } from "@clack/prompts";
2
+ import chalk from "chalk";
3
+ import figlet from "figlet";
4
+ import { runCLIMode } from "../CLI/cli";
5
+ import { runTelegramMode } from "../Telegram";
6
+
7
+ const HF = "ANSI Shadow";
8
+ const SHADOW = chalk.hex("#5941de");
9
+ const FACE = chalk.hex("#eeeffff").bold;
10
+
11
+ function printBannerWithShadow(ascii: string) {
12
+ const bannerLines = ascii.replace(/\s+$/, "").split("\n");
13
+ const maxLen = Math.max(...bannerLines.map((l) => l.length), 0);
14
+ const rowWidth = maxLen + 2;
15
+
16
+ for (const line of bannerLines) {
17
+ console.log(SHADOW((" " + line).padEnd(rowWidth)));
18
+ }
19
+ process.stdout.write(`\x1b[${bannerLines.length}A`);
20
+ for (const line of bannerLines) {
21
+ console.log(FACE(line.padEnd(rowWidth)));
22
+ }
23
+ console.log();
24
+ }
25
+
26
+ export const startArena = async () => {
27
+ let ascii: string;
28
+
29
+ try {
30
+ ascii = figlet.textSync("Jerob", { font: HF });
31
+ } catch (e) {
32
+ ascii = figlet.textSync("Jerob", { font: "Standard" });
33
+ }
34
+
35
+ printBannerWithShadow(ascii);
36
+
37
+ while (true) {
38
+ const option = await select({
39
+ message: chalk.green("choose way to communicate"),
40
+ options: [
41
+ { value: "CLI", label: "CLI" },
42
+ { value: "Telegram", label: "Telegram" },
43
+ { value: "Exit", label: "Exit" },
44
+ ],
45
+ });
46
+
47
+ if (isCancel(option) || option == "Exit") {
48
+ console.log(chalk.green.bold("Goodbye !!!"));
49
+ return "END";
50
+ }
51
+ if (option == "CLI") {
52
+ try {
53
+ await runCLIMode();
54
+ } catch (err) {
55
+ console.log(chalk.red(`\n✖ CLI error: ${err instanceof Error ? err.message : String(err)}\n`));
56
+ }
57
+ continue;
58
+ } else if (option == "Telegram") {
59
+ try {
60
+ await runTelegramMode();
61
+ } catch (err) {
62
+ console.log(chalk.red(`\n✖ Telegram error: ${err instanceof Error ? err.message : String(err)}\n`));
63
+ }
64
+ continue;
65
+ }
66
+ }
67
+ };
@@ -0,0 +1,16 @@
1
+ import { marked } from 'marked';
2
+ import { markedTerminal } from 'marked-terminal';
3
+
4
+ let ready=false;
5
+
6
+ function ensureMarked():void{
7
+ if(ready) return;
8
+ const w=Math.max(40,Math.min(process.stdout.columns || 80,120));
9
+ // @ts-ignore
10
+ marked.use(markedTerminal({width:w,reflowText: true},{}));
11
+ ready=true;
12
+ }
13
+ export const renderHTMLMarkdown=(content:string): string=>{
14
+ ensureMarked()
15
+ return marked.parse(content.trimEnd(),{async:false}) as string
16
+ }
@@ -0,0 +1,185 @@
1
+ import chalk from "chalk";
2
+
3
+ export interface LLMError {
4
+ type: "rate_limit" | "quota" | "auth" | "network" | "refusal" | "timeout" | "unknown";
5
+ message: string;
6
+ retryable: boolean;
7
+ retryAfterMs?: number;
8
+ }
9
+
10
+ /**
11
+ * Parse any LLM/API error into a structured, user-friendly form.
12
+ */
13
+ export function parseLLMError(err: unknown): LLMError {
14
+ const raw = err instanceof Error ? err.message : String(err);
15
+ // Log the raw provider error so we can see exactly what the API returned
16
+ console.error(chalk.dim(`[LLM raw error] ${raw}`));
17
+ const lower = raw.toLowerCase();
18
+
19
+ // Rate limit / quota
20
+ if (
21
+ lower.includes("rate limit") ||
22
+ lower.includes("ratelimit") ||
23
+ lower.includes("rate_limit") ||
24
+ lower.includes("429") ||
25
+ lower.includes("too many requests")
26
+ ) {
27
+ // Try to extract retry-after seconds from message
28
+ const retryMatch = raw.match(/retry after (\d+)/i) || raw.match(/(\d+)\s*second/i);
29
+ const retryAfterMs = retryMatch ? parseInt(retryMatch[1]!) * 1000 : 10_000;
30
+ return {
31
+ type: "rate_limit",
32
+ message: "Rate limit reached. The model is temporarily throttled.",
33
+ retryable: true,
34
+ retryAfterMs,
35
+ };
36
+ }
37
+
38
+ // Quota exceeded
39
+ if (
40
+ lower.includes("quota") ||
41
+ lower.includes("insufficient_quota") ||
42
+ lower.includes("billing") ||
43
+ lower.includes("credits") ||
44
+ lower.includes("402")
45
+ ) {
46
+ return {
47
+ type: "quota",
48
+ message: "API quota or credits exhausted. Check your billing at openrouter.ai or groq.com.",
49
+ retryable: false,
50
+ };
51
+ }
52
+
53
+ // Auth
54
+ if (
55
+ lower.includes("401") ||
56
+ lower.includes("403") ||
57
+ lower.includes("unauthorized") ||
58
+ lower.includes("invalid api key") ||
59
+ lower.includes("invalid_api_key") ||
60
+ lower.includes("authentication")
61
+ ) {
62
+ return {
63
+ type: "auth",
64
+ message: "Invalid or missing API key. Run `jimmy set-key` to update it.",
65
+ retryable: false,
66
+ };
67
+ }
68
+
69
+ // Timeout / network
70
+ if (
71
+ lower.includes("timeout") ||
72
+ lower.includes("timed out") ||
73
+ lower.includes("econnrefused") ||
74
+ lower.includes("enotfound") ||
75
+ lower.includes("network") ||
76
+ lower.includes("fetch failed") ||
77
+ lower.includes("504") ||
78
+ lower.includes("503") ||
79
+ lower.includes("502")
80
+ ) {
81
+ return {
82
+ type: "timeout",
83
+ message: "Network or server timeout. Check your internet connection and try again.",
84
+ retryable: true,
85
+ retryAfterMs: 3_000,
86
+ };
87
+ }
88
+
89
+ // Model refusal / content policy
90
+ if (
91
+ lower.includes("content policy") ||
92
+ lower.includes("safety") ||
93
+ lower.includes("i'm sorry") ||
94
+ lower.includes("i cannot") ||
95
+ lower.includes("i can't help") ||
96
+ lower.includes("cannot assist")
97
+ ) {
98
+ return {
99
+ type: "refusal",
100
+ message: "The model refused this request due to content policy. Try rephrasing your prompt.",
101
+ retryable: false,
102
+ };
103
+ }
104
+
105
+ return {
106
+ type: "unknown",
107
+ message: raw.slice(0, 300),
108
+ retryable: false,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Print a formatted, user-friendly LLM error to the console.
114
+ */
115
+ export function printLLMError(err: unknown, context?: string): void {
116
+ const parsed = parseLLMError(err);
117
+
118
+ const prefix = context ? chalk.dim(`[${context}] `) : "";
119
+
120
+ switch (parsed.type) {
121
+ case "rate_limit":
122
+ console.log(chalk.yellow(`\n${prefix}⏱ Rate limited — ${parsed.message}`));
123
+ if (parsed.retryAfterMs) {
124
+ console.log(chalk.dim(` Retry in ~${Math.ceil(parsed.retryAfterMs / 1000)}s`));
125
+ }
126
+ break;
127
+ case "quota":
128
+ console.log(chalk.red(`\n${prefix}💳 Quota exceeded — ${parsed.message}`));
129
+ break;
130
+ case "auth":
131
+ console.log(chalk.red(`\n${prefix}🔑 Auth error — ${parsed.message}`));
132
+ break;
133
+ case "timeout":
134
+ console.log(chalk.yellow(`\n${prefix}🌐 Network error — ${parsed.message}`));
135
+ break;
136
+ case "refusal":
137
+ console.log(chalk.yellow(`\n${prefix}🚫 Model refusal — ${parsed.message}`));
138
+ break;
139
+ default:
140
+ console.log(chalk.red(`\n${prefix}❌ LLM error — ${parsed.message}`));
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Retry an LLM operation with exponential backoff.
146
+ * Stops immediately on non-retryable errors (auth, quota, refusal).
147
+ */
148
+ export async function withLLMRetry<T>(
149
+ operation: () => Promise<T>,
150
+ options: {
151
+ maxRetries?: number;
152
+ baseDelayMs?: number;
153
+ context?: string;
154
+ } = {}
155
+ ): Promise<T> {
156
+ const { maxRetries = 3, baseDelayMs = 1_500, context } = options;
157
+ let lastError: unknown;
158
+
159
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
160
+ try {
161
+ return await operation();
162
+ } catch (err) {
163
+ lastError = err;
164
+ const parsed = parseLLMError(err);
165
+
166
+ if (!parsed.retryable) {
167
+ printLLMError(err, context);
168
+ throw err;
169
+ }
170
+
171
+ if (attempt === maxRetries) break;
172
+
173
+ const delay = parsed.retryAfterMs ?? baseDelayMs * Math.pow(2, attempt);
174
+ console.log(
175
+ chalk.dim(
176
+ ` ${context ? `[${context}] ` : ""}Retrying in ${Math.ceil(delay / 1000)}s (attempt ${attempt + 1}/${maxRetries})…`
177
+ )
178
+ );
179
+ await new Promise((r) => setTimeout(r, delay));
180
+ }
181
+ }
182
+
183
+ printLLMError(lastError, context);
184
+ throw lastError;
185
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Model ID validation for all supported providers.
3
+ *
4
+ * Strategy:
5
+ * - Each provider has a curated list of known-valid models.
6
+ * - If the input matches a known model → valid.
7
+ * - If the input passes the provider's format rules → warn but allow (future models).
8
+ * - If neither → error with suggestions.
9
+ *
10
+ * No live API calls are made — this is intentional to keep setup fast and offline-capable.
11
+ */
12
+
13
+ export type Provider = "openrouter" | "gemini" | "claude" | "openai" | "groq";
14
+
15
+ // ── Known valid models per provider ──────────────────────────────────────────
16
+
17
+ export const KNOWN_MODELS: Record<Provider, string[]> = {
18
+ openrouter: [
19
+ // Anthropic via OpenRouter
20
+ "anthropic/claude-3.5-sonnet",
21
+ "anthropic/claude-3.5-haiku",
22
+ "anthropic/claude-3-opus",
23
+ "anthropic/claude-3-haiku",
24
+ // OpenAI via OpenRouter
25
+ "openai/gpt-4o",
26
+ "openai/gpt-4o-mini",
27
+ "openai/gpt-4-turbo",
28
+ "openai/o1-preview",
29
+ "openai/o1-mini",
30
+ // Google via OpenRouter
31
+ "google/gemini-pro-1.5",
32
+ "google/gemini-flash-1.5",
33
+ "google/gemini-2.0-flash-exp:free",
34
+ // Meta via OpenRouter
35
+ "meta-llama/llama-3.1-8b-instruct:free",
36
+ "meta-llama/llama-3.1-70b-instruct",
37
+ "meta-llama/llama-3.3-70b-instruct",
38
+ // Mistral via OpenRouter
39
+ "mistralai/mistral-7b-instruct:free",
40
+ "mistralai/mistral-large",
41
+ "mistralai/mixtral-8x7b-instruct",
42
+ // Qwen
43
+ "qwen/qwen-2.5-72b-instruct",
44
+ "qwen/qwen-2.5-7b-instruct:free",
45
+ // Free routing
46
+ "openrouter/free",
47
+ ],
48
+
49
+ gemini: [
50
+ "gemini-2.0-flash",
51
+ "gemini-2.0-flash-exp",
52
+ "gemini-1.5-pro",
53
+ "gemini-1.5-flash",
54
+ "gemini-1.5-flash-8b",
55
+ "gemini-1.0-pro",
56
+ "gemini-pro",
57
+ "gemini-3.1-flash-lite-preview",
58
+ "gemini-2.0-flash-thinking-exp",
59
+ ],
60
+
61
+ claude: [
62
+ "claude-3-5-sonnet-20241022",
63
+ "claude-3-5-haiku-20241022",
64
+ "claude-3-opus-20240229",
65
+ "claude-3-sonnet-20240229",
66
+ "claude-3-haiku-20240307",
67
+ "claude-2.1",
68
+ "claude-instant-1.2",
69
+ ],
70
+
71
+ openai: [
72
+ "gpt-4o",
73
+ "gpt-4o-mini",
74
+ "gpt-4-turbo",
75
+ "gpt-4-turbo-preview",
76
+ "gpt-4",
77
+ "gpt-3.5-turbo",
78
+ "gpt-3.5-turbo-16k",
79
+ "o1-preview",
80
+ "o1-mini",
81
+ "o3-mini",
82
+ ],
83
+
84
+ groq: [
85
+ "llama-3.3-70b-versatile",
86
+ "llama-3.1-8b-instant",
87
+ "llama-3.1-70b-versatile",
88
+ "llama3-8b-8192",
89
+ "llama3-70b-8192",
90
+ "mixtral-8x7b-32768",
91
+ "gemma2-9b-it",
92
+ "gemma-7b-it",
93
+ "whisper-large-v3",
94
+ ],
95
+ };
96
+
97
+ // ── Format rules per provider ─────────────────────────────────────────────────
98
+
99
+ const FORMAT_RULES: Record<Provider, { pattern: RegExp; hint: string }> = {
100
+ openrouter: {
101
+ pattern: /^[a-z0-9_-]+\/[a-z0-9._:-]+$/i,
102
+ hint: "Must be in format provider/model-name (e.g. openai/gpt-4o, meta-llama/llama-3.1-8b-instruct:free)",
103
+ },
104
+ gemini: {
105
+ pattern: /^gemini[-\w.]+$/i,
106
+ hint: "Must start with 'gemini' (e.g. gemini-2.0-flash, gemini-1.5-pro)",
107
+ },
108
+ claude: {
109
+ pattern: /^claude[-\w.]+$/i,
110
+ hint: "Must start with 'claude' (e.g. claude-3-5-sonnet-20241022)",
111
+ },
112
+ openai: {
113
+ pattern: /^(gpt|o[0-9])[-\w.]+$/i,
114
+ hint: "Must start with 'gpt-' or 'o1/'o3' (e.g. gpt-4o, o1-mini)",
115
+ },
116
+ groq: {
117
+ pattern: /^[a-z0-9][-\w.]+$/i,
118
+ hint: "e.g. llama-3.3-70b-versatile, mixtral-8x7b-32768",
119
+ },
120
+ };
121
+
122
+ // ── Public validator ──────────────────────────────────────────────────────────
123
+
124
+ export interface ValidationResult {
125
+ valid: boolean;
126
+ error?: string; // hard error — don't allow
127
+ warning?: string; // soft warning — allow but inform
128
+ suggestions?: string[]; // closest known models
129
+ }
130
+
131
+ /**
132
+ * Validates a model ID for a given provider.
133
+ * Returns { valid: true } if known, { valid: true, warning } if format-valid but unknown,
134
+ * { valid: false, error } if format is wrong.
135
+ */
136
+ export function validateModel(provider: Provider, modelId: string): ValidationResult {
137
+ const s = modelId.trim();
138
+
139
+ if (!s) {
140
+ return { valid: false, error: "Model ID cannot be empty" };
141
+ }
142
+
143
+ if (s.length > 150) {
144
+ return { valid: false, error: "Model ID is too long (max 150 characters)" };
145
+ }
146
+
147
+ // Check known list first
148
+ const known = KNOWN_MODELS[provider];
149
+ if (known.includes(s)) {
150
+ return { valid: true };
151
+ }
152
+
153
+ // Check format
154
+ const rule = FORMAT_RULES[provider];
155
+ if (!rule.pattern.test(s)) {
156
+ const suggestions = closestMatches(s, known);
157
+ return {
158
+ valid: false,
159
+ error: `Invalid format for ${provider}. ${rule.hint}`,
160
+ suggestions,
161
+ };
162
+ }
163
+
164
+ // Format valid but not in known list — allow with warning
165
+ const suggestions = closestMatches(s, known);
166
+ return {
167
+ valid: true,
168
+ warning: `"${s}" is not in the known model list for ${provider}. It may work if it's a new model.`,
169
+ suggestions,
170
+ };
171
+ }
172
+
173
+ /** Find up to 3 closest known models by string similarity */
174
+ function closestMatches(input: string, known: string[]): string[] {
175
+ const lower = input.toLowerCase();
176
+ return known
177
+ .map((k) => ({ k, score: similarity(lower, k.toLowerCase()) }))
178
+ .sort((a, b) => b.score - a.score)
179
+ .slice(0, 3)
180
+ .filter((x) => x.score > 0.2)
181
+ .map((x) => x.k);
182
+ }
183
+
184
+ /** Simple bigram similarity (Sørensen–Dice coefficient) */
185
+ function similarity(a: string, b: string): number {
186
+ if (a === b) return 1;
187
+ if (a.length < 2 || b.length < 2) return 0;
188
+
189
+ const bigrams = (s: string) => {
190
+ const set = new Map<string, number>();
191
+ for (let i = 0; i < s.length - 1; i++) {
192
+ const bg = s.slice(i, i + 2);
193
+ set.set(bg, (set.get(bg) ?? 0) + 1);
194
+ }
195
+ return set;
196
+ };
197
+
198
+ const aGrams = bigrams(a);
199
+ const bGrams = bigrams(b);
200
+ let intersection = 0;
201
+
202
+ for (const [bg, count] of aGrams) {
203
+ const bCount = bGrams.get(bg) ?? 0;
204
+ intersection += Math.min(count, bCount);
205
+ }
206
+
207
+ return (2 * intersection) / (a.length - 1 + (b.length - 1));
208
+ }
209
+
210
+ /**
211
+ * Returns a validate function compatible with @clack/prompts `text.validate`.
212
+ * Shows hard errors, shows warnings inline, allows the user to proceed.
213
+ */
214
+ export function clackModelValidator(
215
+ provider: Provider
216
+ ): (value: string | undefined) => string | undefined {
217
+ return (value) => {
218
+ const v = value?.trim() ?? "";
219
+ if (!v) return "Model ID required";
220
+ const result = validateModel(provider, v);
221
+ if (!result.valid) {
222
+ const base = result.error!;
223
+ const hint = result.suggestions?.length
224
+ ? `\n Did you mean: ${result.suggestions.join(", ")}?`
225
+ : "";
226
+ return base + hint;
227
+ }
228
+ // warnings are shown after the prompt, not as validation errors
229
+ return undefined;
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Print a validation warning to the console (called after text() resolves).
235
+ */
236
+ export function printModelWarning(provider: Provider, modelId: string): void {
237
+ const result = validateModel(provider, modelId.trim());
238
+ if (result.warning) {
239
+ console.log(chalk.yellow(` ⚠ ${result.warning}`));
240
+ if (result.suggestions?.length) {
241
+ console.log(chalk.dim(` Known models: ${result.suggestions.join(", ")}`));
242
+ }
243
+ }
244
+ }
245
+
246
+ // chalk is used only in printModelWarning — import it here to avoid circular deps
247
+ import chalk from "chalk";