jerob 1.1.1 → 1.2.1

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.
@@ -0,0 +1,174 @@
1
+ import { tool, ToolLoopAgent, stepCountIs } from "ai";
2
+ import { z } from "zod";
3
+ import { getAgentModel } from "../config/ai.config";
4
+ import { ActionTracker } from "../agent/action-tracker.ts";
5
+ import { ToolExecutor } from "../agent/tool-executor.ts";
6
+ import { createAgentTools } from "../agent/agent-tools.ts";
7
+ import { defaultAgentConfig, type AgentConfig } from "../agent/types.ts";
8
+ import { createWebTools } from "../plan/web-tools.ts";
9
+ import type { Plan, PlanStep } from "../plan/types.ts";
10
+ import { clip, replyMd, escapeHtml } from "./text.ts";
11
+ import { finishOrApprove } from "./approval-session.ts";
12
+
13
+ function readOnlyConfig(): AgentConfig {
14
+ const c = defaultAgentConfig();
15
+ c.tools.allowFileCreation = false;
16
+ c.tools.allowFileModification = false;
17
+ c.tools.allowFolderCreation = false;
18
+ c.tools.allowShellExecution = false;
19
+ return c;
20
+ }
21
+
22
+ function agentOptions(config: AgentConfig, maxSteps: number) {
23
+ return {
24
+ model: getAgentModel(),
25
+ stopWhen: stepCountIs(maxSteps),
26
+ instructions: `Workspace root: ${config.codebasePath}`,
27
+ };
28
+ }
29
+
30
+ function formatErrorMessage(err: unknown) {
31
+ if (!err) return "Unknown error";
32
+ if (typeof err === "string") return err;
33
+ if (err instanceof Error) {
34
+ if (err.message) return err.message;
35
+ return err.name || "Error";
36
+ }
37
+ const anyErr = err as Record<string, unknown>;
38
+ if (anyErr?.vercel && typeof anyErr?.vercel === "object") {
39
+ const aiError = (anyErr as any).vercel.ai?.error;
40
+ if (aiError) {
41
+ const statusCode = (anyErr as any).statusCode ?? (aiError as any).statusCode;
42
+ const message = (anyErr as any).message ?? (aiError as any).message ?? JSON.stringify((anyErr as any).data ?? aiError, null, 2);
43
+ return statusCode ? `AI request failed (status ${statusCode}): ${message}` : `AI request failed: ${message}`;
44
+ }
45
+ }
46
+ if (Array.isArray((anyErr as any).errors)) {
47
+ const last = ((anyErr as any).errors as any[]).slice(-1)[0];
48
+ if (last?.message) return String(last.message);
49
+ }
50
+ try {
51
+ return JSON.stringify(err, null, 2).slice(0, 2000);
52
+ } catch {
53
+ return String(err);
54
+ }
55
+ }
56
+
57
+ async function safeReplyError(
58
+ ctx: { reply: (t: string, o?: object) => Promise<unknown> },
59
+ subject: string,
60
+ err: unknown,
61
+ ) {
62
+ const message = `❌ ${subject} failed.\n\n${clip(formatErrorMessage(err), 3500)}`;
63
+ await ctx.reply(message).catch(console.error);
64
+ }
65
+
66
+ function createReadOnlyTools(executor: ToolExecutor) {
67
+ return {
68
+ read_file: tool({
69
+ description: "Read a workspace file (relative path).",
70
+ inputSchema: z.object({ path: z.string() }),
71
+ execute: async ({ path: p }) => executor.readFile(p),
72
+ }),
73
+ list_files: tool({
74
+ description: "List files/dirs at a path.",
75
+ inputSchema: z.object({
76
+ path: z.string(),
77
+ recursive: z.boolean().optional().default(false),
78
+ }),
79
+ execute: async ({ path: p, recursive }) =>
80
+ executor.listFiles(p, recursive),
81
+ }),
82
+ search_files: tool({
83
+ description:
84
+ "Find files matching a glob pattern; optional content filter.",
85
+ inputSchema: z.object({
86
+ root: z.string(),
87
+ pattern: z.string(),
88
+ content_contains: z.string().optional(),
89
+ }),
90
+ execute: async ({ root, pattern, content_contains }) =>
91
+ executor.searchFiles(root, pattern, content_contains),
92
+ }),
93
+ analyze_codebase: tool({
94
+ description: "Summarize the codebase structure.",
95
+ inputSchema: z.object({ path: z.string().default(".") }),
96
+ execute: async ({ path: p }) => executor.analyzeCodebase(p),
97
+ }),
98
+ };
99
+ }
100
+
101
+ function extraWebTools(tracker: ActionTracker) {
102
+ return process.env.FIRECRAWL_API_KEY ? createWebTools(tracker) : {};
103
+ }
104
+
105
+ export async function runAsk(ctx:{reply:(t:string , o?:object)=>Promise<unknown>} , question:string){
106
+ const config = readOnlyConfig();
107
+ const tracker = new ActionTracker();
108
+ const executor = new ToolExecutor(tracker, config);
109
+ const tools = { ...createReadOnlyTools(executor), ...extraWebTools(tracker) };
110
+ const agent = new ToolLoopAgent({
111
+ ...agentOptions(config, 20),
112
+ tools,
113
+ });
114
+
115
+ try {
116
+ const { text } = await agent.generate({ prompt: question });
117
+ console.log(text);
118
+ await replyMd(ctx, text.trim() || "no answer");
119
+ } catch (err) {
120
+ console.error("runAsk error:", err);
121
+ await safeReplyError(ctx, "Ask", err);
122
+ }
123
+ }
124
+
125
+ export async function runAgent(ctx: { reply: (t: string, o?: object) => Promise<unknown> }, chatId: number, goal: string) {
126
+ const config = defaultAgentConfig();
127
+ const tracker = new ActionTracker();
128
+ const executor = new ToolExecutor(tracker, config);
129
+ const tools = createAgentTools(executor);
130
+ const agent = new ToolLoopAgent({
131
+ ...agentOptions(config, 40),
132
+ tools,
133
+ });
134
+
135
+ try {
136
+ const { text } = await agent.generate({ prompt: goal });
137
+ if (text?.trim()) await replyMd(ctx, text.trim());
138
+ } catch (err) {
139
+ console.error("runAgent error:", err);
140
+ await safeReplyError(ctx, "Agent", err);
141
+ }
142
+ await finishOrApprove(ctx, chatId, tracker, executor, '✅ Done. No file changes were needed.');
143
+ }
144
+
145
+ export async function runPlanSteps(
146
+ ctx: { reply: (t: string, o?: object) => Promise<unknown> },
147
+ chatId: number,
148
+ plan: Plan,
149
+ steps: PlanStep[],
150
+ ) {
151
+ const config = defaultAgentConfig();
152
+ const tracker = new ActionTracker();
153
+ const executor = new ToolExecutor(tracker, config);
154
+ const tools = { ...createAgentTools(executor), ...extraWebTools(tracker) };
155
+
156
+ for (const step of steps) {
157
+ await ctx.reply(`🔧 Executing: <b>${escapeHtml(step.title)}</b>`, { parse_mode: 'HTML' });
158
+ const prompt = [`Goal: ${plan.goal}`, `Step: ${step.title}`, step.description].join('\n');
159
+ const agent = new ToolLoopAgent({
160
+ ...agentOptions(config, 30),
161
+ tools,
162
+ });
163
+ try {
164
+ const { text } = await agent.generate({ prompt });
165
+ if (text?.trim()) await replyMd(ctx, text.trim());
166
+ } catch (err) {
167
+ console.error(`runPlanSteps error on step ${step.title}:`, err);
168
+ await safeReplyError(ctx, `Step ${step.title}`, err);
169
+ return;
170
+ }
171
+ }
172
+
173
+ await finishOrApprove(ctx, chatId, tracker, executor, '✅ All steps done. No file changes needed.');
174
+ }
@@ -0,0 +1,82 @@
1
+ import { Markup } from 'telegraf';
2
+ import type { ActionTracker } from '../agent/action-tracker.ts';
3
+ import type { ToolExecutor } from '../agent/tool-executor.ts';
4
+ import type { ActionLog } from '../agent/types.ts';
5
+ import { composeBeforeAfter, formatPatch } from '../agent/diff-view.ts';
6
+ import { clip } from './text.ts';
7
+
8
+ export interface ApprovalSession {
9
+ tracker: ActionTracker;
10
+ executor: ToolExecutor;
11
+ pending: ActionLog[];
12
+ }
13
+
14
+ export const approvalSessions = new Map<number, ApprovalSession>();
15
+
16
+ function groupPending(pending: ActionLog[]) {
17
+ const files = new Map<string, ActionLog[]>();
18
+ const shells: ActionLog[] = [];
19
+ for (const a of pending) {
20
+ if (a.type === 'tool_execute') shells.push(a);
21
+ else {
22
+ if (!files.has(a.path)) files.set(a.path, []);
23
+ files.get(a.path)!.push(a);
24
+ }
25
+ }
26
+ return { files, shells };
27
+ }
28
+
29
+ export function approvalSummary(pending: ActionLog[]): string {
30
+ const { files, shells } = groupPending(pending);
31
+ const fileLines = [...files].map(([path, actions]) => {
32
+ const types = [...new Set(actions.map((a) => a.type.replace(/_/g, ' ')))].join(', ');
33
+ return `📄 ${path} (${types})`;
34
+ });
35
+ const shellLines = shells.map((s) => `🖥 Shell: ${s.details.command}`);
36
+ return ['Staged changes — review before applying', '', ...fileLines, ...shellLines, '', `Total: ${pending.length} change(s)`].join('\n');
37
+ }
38
+
39
+ export function approvalDiff(pending: ActionLog[]): string {
40
+ const { files, shells } = groupPending(pending);
41
+ const parts: string[] = [];
42
+ for (const [filePath, actions] of files) {
43
+ const sorted = [...actions].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
44
+ const { before, after } = composeBeforeAfter(sorted);
45
+ parts.push(clip(formatPatch(filePath, before, after), 1500));
46
+ }
47
+ for (const s of shells) parts.push(`🖥 Shell: ${s.details.command}`);
48
+ return parts.join('\n\n').trim();
49
+ }
50
+
51
+ async function promptApproval(
52
+ ctx: { reply: (t: string, o?: object) => Promise<unknown> },
53
+ chatId: number,
54
+ session: ApprovalSession,
55
+ ) {
56
+ approvalSessions.set(chatId, session);
57
+ await ctx.reply(approvalSummary(session.pending), {
58
+ parse_mode: 'Markdown',
59
+ ...Markup.inlineKeyboard([
60
+ [Markup.button.callback('📋 Show Diff', 'approval_diff')],
61
+ [
62
+ Markup.button.callback('✅ Accept All', 'approval_accept'),
63
+ Markup.button.callback('❌ Reject All', 'approval_reject'),
64
+ ],
65
+ ]),
66
+ });
67
+ }
68
+
69
+ export async function finishOrApprove(
70
+ ctx: { reply: (t: string, o?: object) => Promise<unknown> },
71
+ chatId: number,
72
+ tracker: ActionTracker,
73
+ executor: ToolExecutor,
74
+ noChangesMsg: string,
75
+ ) {
76
+ const pending = tracker.getPendingMutations();
77
+ if (pending.length === 0) {
78
+ await ctx.reply(noChangesMsg);
79
+ return;
80
+ }
81
+ await promptApproval(ctx, chatId, { tracker, executor, pending });
82
+ }
@@ -0,0 +1 @@
1
+ export const isOwner=(id: Number)=>String(id)===process.env.TELEGRAM_OWNER_ID
@@ -0,0 +1,6 @@
1
+ export const WELCOME = [
2
+ '👋 Hi! I am Jimmy your AI Operator bot. Here is what I can do:\n',
3
+ '/ask — Ask a question about the codebase',
4
+ '/agent — Let the agent modify your codebase',
5
+ '/plan — Generate a step-by-step plan for a goal',
6
+ ].join('\n');
@@ -0,0 +1,161 @@
1
+ import type{ Telegraf} from "telegraf"
2
+ import { isOwner } from "./auth"
3
+ import { WELCOME } from "./constants"
4
+ import { clip, commandArg } from "./text"
5
+ import { runAgent, runAsk, runPlanSteps } from "./agent-run"
6
+ import { generatePlan } from "../plan/planner"
7
+ import { planKeyboard, planMessage, planSessions, refreshPlanUi, type PlanSession } from "./plan-session"
8
+ import { approvalDiff, approvalSessions } from "./approval-session"
9
+
10
+ async function notifyTelegramError(
11
+ ctx: { reply: (t: string, o?: object) => Promise<unknown> },
12
+ err: unknown,
13
+ action: string,
14
+ ) {
15
+ console.error(`${action} error:`, err);
16
+ const message = typeof err === 'string'
17
+ ? err
18
+ : err instanceof Error
19
+ ? err.message || err.name
20
+ : JSON.stringify(err, null, 2);
21
+ await ctx.reply(`❌ ${action} failed.\n\n${clip(message, 3500)}`)
22
+ .catch(console.error);
23
+ }
24
+
25
+ export function registerHandlers(bot: Telegraf){
26
+ bot.command("start",async(ctx)=>{
27
+ if(!isOwner(ctx.chat.id)) return
28
+ await ctx.reply(WELCOME,{parse_mode:"Markdown"})
29
+ })
30
+
31
+ bot.command("ask",async(ctx)=>{
32
+ if(!isOwner(ctx.chat.id)) return
33
+ const q=commandArg(ctx.message.text,"ask")
34
+
35
+ if(!q){
36
+ return ctx.reply("Usage /ask your question",{parse_mode:"Markdown"})
37
+ }
38
+
39
+ await ctx.reply("🥁Researching your Question")
40
+ void runAsk(ctx,q).catch((err)=>notifyTelegramError(ctx, err, 'Ask'))
41
+ })
42
+
43
+
44
+ bot.command("agent",async(ctx)=>{
45
+ if(!isOwner(ctx.chat.id)) return
46
+ const goal=commandArg(ctx.message.text,"agent")
47
+
48
+ if(!goal){
49
+ return ctx.reply("Usage /agent your description",{parse_mode:"Markdown"})
50
+ }
51
+
52
+ await ctx.reply("🤖 Agent is Working on your task")
53
+ void runAgent(ctx,ctx.chat.id,goal).catch((err)=>notifyTelegramError(ctx, err, 'Agent'))
54
+ })
55
+
56
+ bot.command("plan", async (ctx) => {
57
+ if (!isOwner(ctx.chat.id)) return;
58
+ const goal = commandArg(ctx.message.text, "plan");
59
+
60
+ if (!goal)
61
+ return ctx.reply("Usage: `/plan <your goal>`", {
62
+ parse_mode: "Markdown",
63
+ });
64
+
65
+ await ctx.reply("🧭 Generating a plan…");
66
+
67
+ void (async ()=>{
68
+ const plan = await generatePlan(goal)
69
+ const session:PlanSession = {plan , selected:new Set(plan.steps.map((s)=>s.id))}
70
+ await ctx.reply(planMessage(session) , {parse_mode:"HTML", ...planKeyboard(session)});
71
+ planSessions.set(ctx.chat.id, session);
72
+ })().catch((err)=>notifyTelegramError(ctx, err, 'Plan generation'))
73
+ });
74
+
75
+ bot.action(/^plan_toggle:(.+)$/, async (ctx) => {
76
+ if (!isOwner(ctx.chat!.id)) return ctx.answerCbQuery();
77
+ const s = planSessions.get(ctx.chat!.id);
78
+ if (!s) return ctx.answerCbQuery();
79
+
80
+ const id = ctx.match[1]!;
81
+ if (s.selected.has(id)) s.selected.delete(id);
82
+ else s.selected.add(id);
83
+
84
+ await refreshPlanUi(ctx, s);
85
+ await ctx.answerCbQuery();
86
+ });
87
+
88
+
89
+ bot.action('plan_all', async (ctx) => {
90
+ if (!isOwner(ctx.chat!.id)) return ctx.answerCbQuery();
91
+ const s = planSessions.get(ctx.chat!.id);
92
+ if (!s) return ctx.answerCbQuery();
93
+ for (const step of s.plan.steps) s.selected.add(step.id);
94
+ await refreshPlanUi(ctx, s);
95
+ await ctx.answerCbQuery();
96
+ });
97
+
98
+ bot.action('plan_none', async (ctx) => {
99
+ if (!isOwner(ctx.chat!.id)) return ctx.answerCbQuery();
100
+ const s = planSessions.get(ctx.chat!.id);
101
+ if (!s) return ctx.answerCbQuery();
102
+ s.selected.clear();
103
+ await refreshPlanUi(ctx, s);
104
+ await ctx.answerCbQuery();
105
+ });
106
+
107
+ bot.action('plan_proceed', async (ctx) => {
108
+ if (!isOwner(ctx.chat!.id)) return ctx.answerCbQuery();
109
+ const s = planSessions.get(ctx.chat!.id);
110
+ if (!s) return ctx.answerCbQuery();
111
+
112
+ const steps = s.plan.steps.filter((step) => s.selected.has(step.id));
113
+ if (steps.length === 0) return ctx.answerCbQuery();
114
+
115
+ const { plan } = s;
116
+ planSessions.delete(ctx.chat!.id);
117
+ const list = steps.map((step, i) => `${i + 1}. ${step.title}`).join('\n');
118
+ await ctx.editMessageText(`🚀 Executing ${steps.length} step(s)…\n\n${list}`);
119
+ await ctx.answerCbQuery();
120
+
121
+ void runPlanSteps(ctx, ctx.chat!.id, plan, steps).catch((err)=>notifyTelegramError(ctx, err, 'Plan execution'));
122
+ });
123
+
124
+ bot.action('approval_diff', async (ctx) => {
125
+ if (!isOwner(ctx.chat!.id)) return ctx.answerCbQuery();
126
+ const s = approvalSessions.get(ctx.chat!.id);
127
+ if (!s) return ctx.answerCbQuery();
128
+ await ctx.answerCbQuery();
129
+ await ctx.reply(clip(approvalDiff(s.pending)));
130
+ });
131
+
132
+ bot.action('approval_accept', async (ctx) => {
133
+ if (!isOwner(ctx.chat!.id)) return ctx.answerCbQuery();
134
+ const s = approvalSessions.get(ctx.chat!.id);
135
+ if (!s) return ctx.answerCbQuery();
136
+
137
+ approvalSessions.delete(ctx.chat!.id);
138
+ for (const a of s.pending) s.tracker.updateStatus(a.id, 'approved', true);
139
+ const { errors } = s.executor.applyApprovedFromTracker();
140
+ s.executor.clearStaging();
141
+
142
+ await ctx.editMessageText('✅ All changes applied.');
143
+ await ctx.answerCbQuery('Applied!');
144
+ if (errors.length) console.error(errors);
145
+ });
146
+
147
+ bot.action('approval_reject', async (ctx) => {
148
+ if (!isOwner(ctx.chat!.id)) return ctx.answerCbQuery();
149
+ const s = approvalSessions.get(ctx.chat!.id);
150
+ if (!s) return ctx.answerCbQuery();
151
+
152
+ approvalSessions.delete(ctx.chat!.id);
153
+ for (const a of s.pending) s.tracker.updateStatus(a.id, 'rejected', false);
154
+ s.executor.clearStaging();
155
+
156
+ await ctx.editMessageText('❌ All changes rejected. Nothing was applied.');
157
+ await ctx.answerCbQuery('Rejected');
158
+ });
159
+
160
+
161
+ }
@@ -0,0 +1,28 @@
1
+ import { Telegraf } from "telegraf";
2
+ import chalk from "chalk";
3
+ import { WELCOME } from "./constants";
4
+ import { resolve } from "node:dns";
5
+ import { registerHandlers } from "./handlers";
6
+
7
+ export async function runTelegramMode() {
8
+ const token = process.env.TELEGRAM_BOT_TOKEN;
9
+ const ownerId = process.env.TELEGRAM_OWNER_ID;
10
+
11
+ const bot = new Telegraf(token!);
12
+ registerHandlers(bot)
13
+
14
+ await bot.telegram.sendMessage(ownerId!, WELCOME, { parse_mode: "Markdown" });
15
+ console.log(chalk.green("Sent welcome message to Telegram.\n"));
16
+
17
+ bot.launch();
18
+ console.log(chalk.green("Telegram bot is running. Press Ctrl+C to stop.\n"));
19
+
20
+ await new Promise<void>((resolve) => {
21
+ const stop = () => {
22
+ bot.stop("SIGINT");
23
+ resolve();
24
+ };
25
+ process.once("SIGINT", stop);
26
+ process.once("SIGTERM", stop);
27
+ });
28
+ }
@@ -0,0 +1,51 @@
1
+ import { Markup } from 'telegraf';
2
+ import type { Plan } from '../plan/types.ts';
3
+ import { escapeHtml } from './text.ts';
4
+
5
+ export interface PlanSession {
6
+ plan: Plan;
7
+ selected: Set<string>;
8
+ }
9
+
10
+ export const planSessions = new Map<number, PlanSession>();
11
+
12
+ export function planMessage(session: PlanSession): string {
13
+ const lines = session.plan.steps.map((step, i) => {
14
+ const mark = session.selected.has(step.id) ? '✅' : '⬜';
15
+ const tag = step.complexity ? ` [${step.complexity}]` : '';
16
+ return `${mark} ${i + 1}. <b>${escapeHtml(step.title)}</b>${tag}`;
17
+ });
18
+ return [
19
+ `📋 <b>Plan for:</b> ${escapeHtml(session.plan.goal)}`,
20
+ '',
21
+ ...lines,
22
+ '',
23
+ '<i>Tap steps to toggle, then hit Proceed.</i>',
24
+ ].join('\n');
25
+ }
26
+
27
+ export function planKeyboard(session: PlanSession) {
28
+ const rows = session.plan.steps.map((step, i) => {
29
+ const mark = session.selected.has(step.id) ? '✅' : '⬜';
30
+ const label = `${mark} Step ${i + 1}: ${step.title}`;
31
+ return [Markup.button.callback(label, `plan_toggle:${step.id}`)];
32
+ });
33
+ return Markup.inlineKeyboard([
34
+ ...rows,
35
+ [
36
+ Markup.button.callback('✅ Select All', 'plan_all'),
37
+ Markup.button.callback('⬜ Deselect All', 'plan_none'),
38
+ ],
39
+ [Markup.button.callback('🚀 Proceed', 'plan_proceed')],
40
+ ]);
41
+ }
42
+
43
+ export async function refreshPlanUi(
44
+ ctx: { editMessageText: (t: string, o: object) => Promise<unknown> },
45
+ s: PlanSession,
46
+ ) {
47
+ await ctx.editMessageText(planMessage(s), {
48
+ parse_mode: 'HTML',
49
+ reply_markup: planKeyboard(s).reply_markup,
50
+ });
51
+ }
@@ -0,0 +1,16 @@
1
+ export const clip = (text: string, max = 4000) =>
2
+ text.length <= max ? text : text.slice(0, max) + '\n…[truncated]';
3
+
4
+ export const replyMd = (ctx: { reply: (t: string, o?: object) => Promise<unknown> }, text: string) =>
5
+ ctx.reply(clip(text));
6
+
7
+ export const escapeHtml = (text: string) =>
8
+ text
9
+ .replace(/&/g, '&amp;')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;');
12
+
13
+ /** Text after `/name …` */
14
+ export function commandArg(fullText: string, name: string): string {
15
+ return fullText.replace(new RegExp(`^/${name}\\s*`, 'i'), '').trim();
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jerob",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Personal AI agent CLI — autonomous code agent, planner, browser automation, Q&A, and serverless scheduler",
5
5
  "module": "index.ts",
6
6
  "private": false,
@@ -20,6 +20,7 @@
20
20
  "plan/",
21
21
  "scheduler/",
22
22
  "supabase/",
23
+ "Telegram/",
23
24
  "tui/",
24
25
  "utils/",
25
26
  "tsconfig.json",