jerob 1.1.0 → 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.
- package/LICENSE +21 -0
- package/Telegram/agent-run.ts +174 -0
- package/Telegram/approval-session.ts +82 -0
- package/Telegram/auth.ts +1 -0
- package/Telegram/constants.ts +6 -0
- package/Telegram/handlers.ts +161 -0
- package/Telegram/index.ts +28 -0
- package/Telegram/plan-session.ts +51 -0
- package/Telegram/text.ts +16 -0
- package/bin/jerob.js +4 -8
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abhishek Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -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
|
+
}
|
package/Telegram/auth.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isOwner=(id: Number)=>String(id)===process.env.TELEGRAM_OWNER_ID
|
|
@@ -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
|
+
}
|
package/Telegram/text.ts
ADDED
|
@@ -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, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>');
|
|
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/bin/jerob.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
import { execFileSync } from "node:child_process";
|
|
4
|
-
import
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
* npm entry point for jerob.
|
|
8
|
-
* Requires Bun to be installed: https://bun.sh
|
|
9
|
-
* Shells out to Bun so the TypeScript source runs natively.
|
|
10
|
-
*/
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
7
|
|
|
12
8
|
// Check Bun is available
|
|
13
9
|
try {
|
|
@@ -22,7 +18,7 @@ try {
|
|
|
22
18
|
process.exit(1);
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
const entry =
|
|
21
|
+
const entry = join(__dirname, "..", "index.ts");
|
|
26
22
|
|
|
27
23
|
execFileSync("bun", [entry, ...process.argv.slice(2)], {
|
|
28
24
|
stdio: "inherit",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jerob",
|
|
3
|
-
"version": "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",
|