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,463 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import {homedir} from "node:os"
4
+ import {spawnSync} from "node:child_process"
5
+ import type {AgentConfig,ActionLog} from "./types"
6
+ import {ActionTracker} from "./action-tracker"
7
+
8
+ const TEXT_EXT=new Set([
9
+ ".ts",
10
+ ".tsx",
11
+ ".js",
12
+ ".jsx",
13
+ ".mjs",
14
+ ".cjs",
15
+ ".json",
16
+ ".md",
17
+ ".mdx",
18
+ ".css",
19
+ ".html",
20
+ ".yml",
21
+ ".yaml",
22
+ ".toml",
23
+ ".txt",
24
+ ])
25
+
26
+ function isTextFile(file_name: string): boolean{
27
+ const ext=path.extname(file_name).toLowerCase()
28
+
29
+ return TEXT_EXT.has(ext) || ext===""
30
+ }
31
+
32
+ export class ToolExecutor {
33
+ private overlay = new Map<string, string>();
34
+ private deleted = new Set<string>();
35
+ private readonly norm = (rel: string) =>
36
+ path.posix.normalize(rel.split(path.sep).join("/")).replace(/^\.\//, "");
37
+
38
+ constructor(
39
+ private readonly tracker: ActionTracker,
40
+ private readonly config: AgentConfig,
41
+ ) {}
42
+
43
+ private resolveSafe(rel: string): string {
44
+ const abs = path.resolve(this.config.codebasePath, rel);
45
+ const root = path.resolve(this.config.codebasePath);
46
+ const relCheck = path.relative(root, abs);
47
+ if (relCheck.startsWith("..") || path.isAbsolute(relCheck)) {
48
+ throw new Error(`Path escapes workspace: ${rel}`);
49
+ }
50
+ return abs;
51
+ }
52
+
53
+ private excluded(relPath: string): boolean {
54
+ const norm = this.norm(relPath);
55
+ const segments = norm.split("/");
56
+ const base = segments[segments.length - 1] ?? "";
57
+
58
+ for (const pat of this.config.excludePatterns) {
59
+ if (pat === "*.log" && base.endsWith(".log")) return true;
60
+ if (pat === ".env*" && base.startsWith(".env")) return true;
61
+ if (pat.includes("*")) continue;
62
+ if (segments.includes(pat) || norm === pat || norm.startsWith(`${pat}/`))
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ private assertNotExcluded(rel: string, op: string): void {
69
+ if (this.excluded(rel)) {
70
+ throw new Error(`${op}: path is excluded by policy: ${rel}`);
71
+ }
72
+ }
73
+
74
+ getEffectiveText(rel: string): string | undefined {
75
+ const key = this.norm(rel);
76
+ if (this.deleted.has(key)) return undefined;
77
+ if (this.overlay.has(key)) return this.overlay.get(key);
78
+ const abs = this.resolveSafe(rel);
79
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return undefined;
80
+ return fs.readFileSync(abs, "utf8");
81
+ }
82
+
83
+ readFile(rel: string): string {
84
+ this.assertNotExcluded(rel, "read_file");
85
+ const abs = this.resolveSafe(rel);
86
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
87
+ throw new Error(`File not found: ${rel}`);
88
+ }
89
+ const st = fs.statSync(abs);
90
+ if (st.size > this.config.maxFileSizeToRead) {
91
+ throw new Error(`File too large: ${rel}`);
92
+ }
93
+ const text = fs.readFileSync(abs, "utf8");
94
+ this.tracker.log({
95
+ type: "code_analysis",
96
+ path: this.norm(rel),
97
+ details: { after: text, toolName: "read_file" },
98
+ status: "executed",
99
+ });
100
+ return text;
101
+ }
102
+
103
+ createFile(rel: string, content: string): string {
104
+ if (!this.config.tools.allowFileCreation)
105
+ throw new Error("File creation disabled");
106
+ this.assertNotExcluded(rel, "create_file");
107
+ const key = this.norm(rel);
108
+ const abs = this.resolveSafe(rel);
109
+ if (fs.existsSync(abs) && !this.deleted.has(key)) {
110
+ throw new Error(`create_file: already exists: ${rel}`);
111
+ }
112
+ this.deleted.delete(key);
113
+ this.overlay.set(key, content);
114
+ this.tracker.log({
115
+ type: "file_create",
116
+ path: key,
117
+ details: { after: content },
118
+ status: "pending",
119
+ });
120
+ return `Staged new file: ${key}`;
121
+ }
122
+
123
+ modifyFile(rel: string, content: string): string {
124
+ if (!this.config.tools.allowFileModification)
125
+ throw new Error("File modification disabled");
126
+ this.assertNotExcluded(rel, "modify_file");
127
+ const before = this.getEffectiveText(rel);
128
+ if (before === undefined)
129
+ throw new Error(`modify_file: file not found: ${rel}`);
130
+ const key = this.norm(rel);
131
+ this.overlay.set(key, content);
132
+ this.tracker.log({
133
+ type: "file_modify",
134
+ path: key,
135
+ details: { before, after: content },
136
+ status: "pending",
137
+ });
138
+ return `Staged update: ${key}`;
139
+ }
140
+
141
+ deleteFile(rel: string): string {
142
+ if (!this.config.tools.allowFileModification)
143
+ throw new Error("File deletion disabled");
144
+ this.assertNotExcluded(rel, "delete_file");
145
+ const before = this.getEffectiveText(rel);
146
+ if (before === undefined)
147
+ throw new Error(`delete_file: file not found: ${rel}`);
148
+ const key = this.norm(rel);
149
+ this.overlay.delete(key);
150
+ this.deleted.add(key);
151
+ this.tracker.log({
152
+ type: "file_delete",
153
+ path: key,
154
+ details: { before },
155
+ status: "pending",
156
+ });
157
+ return `Staged delete: ${key}`;
158
+ }
159
+
160
+ createFolder(rel: string): string {
161
+ if (!this.config.tools.allowFolderCreation)
162
+ throw new Error("Folder creation disabled");
163
+ this.assertNotExcluded(rel, "create_folder");
164
+ const key = this.norm(rel);
165
+ this.tracker.log({
166
+ type: "folder_create",
167
+ path: key,
168
+ details: { after: key },
169
+ status: "pending",
170
+ });
171
+ return `Staged folder: ${key}`;
172
+ }
173
+
174
+ listFiles(rel: string, recursive: boolean): string {
175
+ this.assertNotExcluded(rel, "list_files");
176
+ const abs = this.resolveSafe(rel);
177
+ if (!fs.existsSync(abs)) throw new Error(`list_files: not found: ${rel}`);
178
+
179
+ const lines: string[] = [];
180
+ const walk = (dir: string, prefix: string) => {
181
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
182
+ for (const ent of entries) {
183
+ const full = path.join(dir, ent.name);
184
+ const relP = path.relative(this.config.codebasePath, full);
185
+ if (this.excluded(relP)) continue;
186
+ if (ent.isDirectory()) {
187
+ lines.push(`${prefix}${ent.name}/`);
188
+ if (recursive) walk(full, `${prefix}${ent.name}/`);
189
+ } else {
190
+ lines.push(`${prefix}${ent.name}`);
191
+ }
192
+ }
193
+ };
194
+
195
+ if (fs.statSync(abs).isDirectory()) walk(abs, "");
196
+ else lines.push(path.relative(this.config.codebasePath, abs));
197
+
198
+ const out = lines.sort().join("\n");
199
+ this.tracker.log({
200
+ type: "code_analysis",
201
+ path: this.norm(rel),
202
+ details: { after: out, toolName: "list_files" },
203
+ status: "executed",
204
+ });
205
+ return out || "(empty)";
206
+ }
207
+
208
+ searchFiles(
209
+ rootRel: string,
210
+ globPattern: string,
211
+ contentQuery?: string,
212
+ ): string {
213
+ this.assertNotExcluded(rootRel, "search_files");
214
+ const rootAbs = this.resolveSafe(rootRel);
215
+ if (!fs.existsSync(rootAbs))
216
+ throw new Error(`search_files: root not found: ${rootRel}`);
217
+
218
+ const results: string[] = [];
219
+ const regexFromGlob = (g: string): RegExp => {
220
+ const escaped = g
221
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
222
+ .replace(/\*\*/g, "§§")
223
+ .replace(/\*/g, "[^/\\\\]*")
224
+ .replace(/§§/g, ".*")
225
+ .replace(/\?/g, ".");
226
+ return new RegExp(`^${escaped}$`, "i");
227
+ };
228
+ const nameRe = regexFromGlob(globPattern.replace(/\\/g, "/"));
229
+
230
+ const walk = (dir: string) => {
231
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
232
+ const full = path.join(dir, ent.name);
233
+ const relP = path
234
+ .relative(this.config.codebasePath, full)
235
+ .split(path.sep)
236
+ .join("/");
237
+ if (this.excluded(relP)) continue;
238
+ if (ent.isDirectory()) walk(full);
239
+ else if (nameRe.test(relP) || nameRe.test(ent.name)) {
240
+ if (contentQuery) {
241
+ if (!isTextFile(full)) continue;
242
+ const text = fs.readFileSync(full, "utf8");
243
+ if (!text.includes(contentQuery)) continue;
244
+ }
245
+ results.push(relP);
246
+ }
247
+ }
248
+ };
249
+
250
+ if (fs.statSync(rootAbs).isDirectory()) walk(rootAbs);
251
+ else {
252
+ const relP = path
253
+ .relative(this.config.codebasePath, rootAbs)
254
+ .split(path.sep)
255
+ .join("/");
256
+ results.push(relP);
257
+ }
258
+
259
+ const out = [...new Set(results)].sort().join("\n");
260
+ this.tracker.log({
261
+ type: "code_analysis",
262
+ path: this.norm(rootRel),
263
+ details: { after: out || "(no matches)", toolName: "search_files" },
264
+ status: "executed",
265
+ });
266
+ return out || "(no matches)";
267
+ }
268
+
269
+ analyzeCodebase(rootRel: string): string {
270
+ const rootAbs = this.resolveSafe(rootRel);
271
+ if (!fs.existsSync(rootAbs))
272
+ throw new Error(`analyze_codebase: not found: ${rootRel}`);
273
+
274
+ let files = 0;
275
+ let dirs = 0;
276
+ const walk = (dir: string) => {
277
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
278
+ const full = path.join(dir, ent.name);
279
+ const relP = path.relative(this.config.codebasePath, full);
280
+ if (this.excluded(relP)) continue;
281
+ if (ent.isDirectory()) {
282
+ dirs++;
283
+ walk(full);
284
+ } else {
285
+ files++;
286
+ }
287
+ }
288
+ };
289
+ if (fs.statSync(rootAbs).isDirectory()) walk(rootAbs);
290
+ else files = 1;
291
+
292
+ const summary = `Files: ${files} | Directories: ${dirs}`;
293
+ this.tracker.log({
294
+ type: "code_analysis",
295
+ path: this.norm(rootRel),
296
+ details: { after: summary, toolName: "analyze_codebase" },
297
+ status: "executed",
298
+ });
299
+ return summary;
300
+ }
301
+
302
+ queueShell(command: string): string {
303
+ if (!this.config.tools.allowShellExecution)
304
+ throw new Error("Shell execution disabled");
305
+ this.tracker.log({
306
+ type: "tool_execute",
307
+ path: "shell",
308
+ details: { command, toolName: "execute_shell" },
309
+ status: "pending",
310
+ });
311
+ return `Shell queued: ${command}`;
312
+ }
313
+ skillRoots(): string[] {
314
+ const extra =
315
+ process.env.SKILLS_DIRS?.split(/[;]/)
316
+ .map((s) => s.trim())
317
+ .filter(Boolean) ?? [];
318
+ return [
319
+ ...extra,
320
+ path.join(homedir(), ".cursor/skills-cursor"),
321
+ path.join(homedir(), ".claude/skills"),
322
+ ];
323
+ }
324
+
325
+ listSkills(): string {
326
+ const lines: string[] = [];
327
+ for (const root of this.skillRoots()) {
328
+ if (!fs.existsSync(root)) continue;
329
+ const walk = (dir: string) => {
330
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
331
+ const full = path.join(dir, ent.name);
332
+ if (ent.isDirectory()) walk(full);
333
+ else if (ent.name === "SKILL.md") lines.push(full);
334
+ }
335
+ };
336
+ walk(root);
337
+ }
338
+ const out = lines.sort().join("\n");
339
+ this.tracker.log({
340
+ type: "code_analysis",
341
+ path: "skills",
342
+ details: { after: out || "(none)", toolName: "list_skills" },
343
+ status: "executed",
344
+ });
345
+ return out || "(none)";
346
+ }
347
+
348
+ readSkill(skillPath: string): string {
349
+ const abs = path.isAbsolute(skillPath)
350
+ ? path.normalize(skillPath)
351
+ : path.normalize(path.resolve(this.config.codebasePath, skillPath));
352
+ const allowed = this.skillRoots().some((root) => {
353
+ const r = path.resolve(root);
354
+ return abs === r || abs.startsWith(r + path.sep);
355
+ });
356
+ if (!allowed) throw new Error("read_skill: outside skill roots");
357
+ const text = fs.readFileSync(abs, "utf8");
358
+ this.tracker.log({
359
+ type: "code_analysis",
360
+ path: abs,
361
+ details: { after: text, toolName: "read_skill" },
362
+ status: "executed",
363
+ });
364
+ return text;
365
+ }
366
+
367
+ applyApprovedFromTracker(): { errors: string[]; newFiles: string[] } {
368
+ const errors: string[] = [];
369
+ const all = [...this.tracker.getActions()];
370
+ // Snapshot files before applying so we can detect newly created files (e.g., by scaffolders)
371
+ const snapshotFiles = (root: string): Set<string> => {
372
+ const s = new Set<string>();
373
+ if (!fs.existsSync(root)) return s;
374
+ const walk = (dir: string) => {
375
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
376
+ const full = path.join(dir, ent.name);
377
+ const rel = path.relative(this.config.codebasePath, full).split(path.sep).join("/");
378
+ if (ent.isDirectory()) walk(full);
379
+ else s.add(rel);
380
+ }
381
+ };
382
+ walk(root);
383
+ return s;
384
+ };
385
+
386
+ const preFiles = snapshotFiles(this.config.codebasePath);
387
+
388
+ for (const a of all.filter(
389
+ (x) => x.type === "folder_create" && x.status === "approved",
390
+ )) {
391
+ try {
392
+ fs.mkdirSync(this.resolveSafe(a.path), { recursive: true });
393
+ } catch (e) {
394
+ errors.push(String(e));
395
+ }
396
+ }
397
+
398
+ const fileOps = all
399
+ .filter(
400
+ (a) =>
401
+ (a.type === "file_create" ||
402
+ a.type === "file_modify" ||
403
+ a.type === "file_delete") &&
404
+ a.status === "approved",
405
+ )
406
+ .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
407
+
408
+ const lastByPath = new Map<string, ActionLog>();
409
+ for (const a of fileOps) lastByPath.set(this.norm(a.path), a);
410
+
411
+ for (const [p, a] of lastByPath) {
412
+ try {
413
+ if (a.type === "file_delete")
414
+ fs.rmSync(this.resolveSafe(p), { force: true });
415
+ else {
416
+ const target = this.resolveSafe(p);
417
+ fs.mkdirSync(path.dirname(target), { recursive: true });
418
+ fs.writeFileSync(target, a.details.after ?? "", "utf8");
419
+ }
420
+ } catch (e) {
421
+ errors.push(String(e));
422
+ }
423
+ }
424
+
425
+ // Run approved shell commands and capture output
426
+ for (const a of all.filter(
427
+ (x) => x.type === "tool_execute" && x.status === "approved",
428
+ )) {
429
+ const cmd = a.details.command;
430
+ if (!cmd) continue;
431
+ const r = spawnSync(cmd, {
432
+ shell: true,
433
+ cwd: this.config.codebasePath,
434
+ encoding: "utf8",
435
+ maxBuffer: 16 * 1024 * 1024,
436
+ });
437
+ const out = (r.stdout ?? "").toString();
438
+ const err = (r.stderr ?? "").toString();
439
+ if (out.trim()) console.log(`\n[Shell stdout] ${cmd}\n${out}`);
440
+ if (err.trim()) console.log(`\n[Shell stderr] ${cmd}\n${err}`);
441
+ if (r.status && r.status !== 0) errors.push(`shell exit ${r.status}: ${cmd}\n${err}`);
442
+ }
443
+
444
+ // Snapshot after apply to detect new files (scaffolders)
445
+ const postFiles = snapshotFiles(this.config.codebasePath);
446
+ const newFiles: string[] = [];
447
+ for (const f of postFiles) {
448
+ if (!preFiles.has(f)) newFiles.push(f);
449
+ }
450
+ if (newFiles.length) {
451
+ console.log("\nNew files created by approved actions/shells:");
452
+ for (const nf of newFiles.slice(0, 200)) console.log(` • ${nf}`);
453
+ if (newFiles.length > 200) console.log(` • ...and ${newFiles.length - 200} more`);
454
+ }
455
+
456
+ return { errors, newFiles };
457
+ }
458
+
459
+ clearStaging():void{
460
+ this.overlay.clear()
461
+ this.deleted.clear()
462
+ }
463
+ }
package/agent/types.ts ADDED
@@ -0,0 +1,69 @@
1
+ export type ActionType =
2
+ | 'file_create'
3
+ | 'file_modify'
4
+ | 'file_delete'
5
+ | 'folder_create'
6
+ | 'code_analysis'
7
+ | 'tool_execute';
8
+
9
+ export type ActionStatus = 'pending' | 'executed' | 'approved' | 'rejected';
10
+
11
+ export interface ActionLog {
12
+ id: string;
13
+ timestamp: Date;
14
+ type: ActionType;
15
+ path: string;
16
+ details: {
17
+ before?: string;
18
+ after?: string;
19
+ toolName?: string;
20
+ toolResult?: string;
21
+ error?: string;
22
+ command?: string;
23
+ };
24
+ status: ActionStatus;
25
+ userApproved?: boolean;
26
+ }
27
+
28
+ export interface AgentConfig {
29
+ codebasePath: string;
30
+ maxFileSizeToRead: number;
31
+ excludePatterns: string[];
32
+ tools: {
33
+ allowShellExecution: boolean;
34
+ allowFileModification: boolean;
35
+ allowFileCreation: boolean;
36
+ allowFolderCreation: boolean;
37
+ };
38
+ }
39
+
40
+ export const defaultAgentConfig = (): AgentConfig => ({
41
+ codebasePath: process.cwd(),
42
+ maxFileSizeToRead: 1024 * 1024 ,
43
+ excludePatterns: [
44
+ 'node_modules',
45
+ '.git',
46
+ 'dist',
47
+ 'build',
48
+ '.next',
49
+ '*.log',
50
+ '.env*',
51
+ ],
52
+ tools: {
53
+ allowShellExecution: true,
54
+ allowFileModification: true,
55
+ allowFileCreation: true,
56
+ allowFolderCreation: true,
57
+ },
58
+ });
59
+
60
+ export function isMutationType(t: ActionType): boolean {
61
+ return (
62
+ t === 'file_create' ||
63
+ t === 'file_modify' ||
64
+ t === 'file_delete' ||
65
+ t === 'folder_create' ||
66
+ t === 'tool_execute'
67
+ );
68
+ }
69
+