@sarkar-ai/deskmate 0.2.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.
package/dist/index.js ADDED
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ require("dotenv/config");
37
+ const mode = process.argv[2] || "telegram";
38
+ const BOT_NAME = process.env.BOT_NAME || "Deskmate";
39
+ /** Parse ALLOWED_USERS (multi-client) and ALLOWED_USER_ID (legacy) into UserIdentity[] */
40
+ function buildAllowedUsers() {
41
+ const users = [];
42
+ // New multi-client format: "telegram:123,discord:456,slack:U789"
43
+ const multiClient = process.env.ALLOWED_USERS;
44
+ if (multiClient) {
45
+ for (const entry of multiClient.split(",").map((s) => s.trim()).filter(Boolean)) {
46
+ const colonIdx = entry.indexOf(":");
47
+ if (colonIdx > 0) {
48
+ users.push({
49
+ clientType: entry.slice(0, colonIdx),
50
+ platformUserId: entry.slice(colonIdx + 1),
51
+ });
52
+ }
53
+ }
54
+ }
55
+ // Legacy single-user format
56
+ const legacyId = process.env.ALLOWED_USER_ID;
57
+ if (legacyId && legacyId !== "0") {
58
+ // Only add if not already covered by ALLOWED_USERS
59
+ const alreadyHas = users.some((u) => u.clientType === "telegram" && u.platformUserId === legacyId);
60
+ if (!alreadyHas) {
61
+ users.push({ clientType: "telegram", platformUserId: legacyId });
62
+ }
63
+ }
64
+ return users;
65
+ }
66
+ async function main() {
67
+ console.log(`🚀 Starting ${BOT_NAME} in ${mode} mode...`);
68
+ switch (mode) {
69
+ case "telegram":
70
+ // Telegram bot mode - natural language interface
71
+ const { startTelegramBot } = await Promise.resolve().then(() => __importStar(require("./telegram/bot")));
72
+ await startTelegramBot();
73
+ break;
74
+ case "mcp":
75
+ // MCP server mode - for Claude.ai / MCP clients
76
+ const { startMcpServer } = await Promise.resolve().then(() => __importStar(require("./mcp/server")));
77
+ await startMcpServer();
78
+ break;
79
+ case "both":
80
+ // Run both (MCP on stdio, Telegram in background)
81
+ console.log("Starting both Telegram bot and MCP server...");
82
+ const [{ startTelegramBot: startBot }, { startMcpServer: startMcp }] = await Promise.all([
83
+ Promise.resolve().then(() => __importStar(require("./telegram/bot"))),
84
+ Promise.resolve().then(() => __importStar(require("./mcp/server"))),
85
+ ]);
86
+ // Start Telegram in background (for approval notifications)
87
+ startBot().catch(console.error);
88
+ // Start MCP (this blocks on stdio)
89
+ await startMcp();
90
+ break;
91
+ case "gateway": {
92
+ const { Gateway } = await Promise.resolve().then(() => __importStar(require("./gateway")));
93
+ const { TelegramClient } = await Promise.resolve().then(() => __importStar(require("./clients/telegram")));
94
+ const allowedUsers = buildAllowedUsers();
95
+ if (allowedUsers.length === 0) {
96
+ throw new Error("No allowed users configured. Set ALLOWED_USERS or ALLOWED_USER_ID in your .env");
97
+ }
98
+ const gateway = new Gateway({
99
+ botName: BOT_NAME,
100
+ workingDir: process.env.WORKING_DIR || process.env.HOME || "/",
101
+ allowedUsers,
102
+ maxTurns: 10,
103
+ });
104
+ // Auto-register clients based on available env vars
105
+ if (process.env.TELEGRAM_BOT_TOKEN) {
106
+ gateway.registerClient(new TelegramClient(process.env.TELEGRAM_BOT_TOKEN));
107
+ }
108
+ // Future: Discord, Slack, etc.
109
+ await gateway.start();
110
+ break;
111
+ }
112
+ default:
113
+ console.error(`Unknown mode: ${mode}`);
114
+ console.error("Usage: npm start [telegram|mcp|both|gateway]");
115
+ process.exit(1);
116
+ }
117
+ }
118
+ main().catch((error) => {
119
+ console.error("Fatal error:", error);
120
+ process.exit(1);
121
+ });
122
+ // Graceful shutdown
123
+ process.on("SIGINT", () => {
124
+ console.log("\n👋 Shutting down...");
125
+ process.exit(0);
126
+ });
127
+ process.on("SIGTERM", () => {
128
+ console.log("\n👋 Shutting down...");
129
+ process.exit(0);
130
+ });
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startMcpServer = startMcpServer;
4
+ // @ts-nocheck - Required due to MCP SDK type inference issue (TS2589)
5
+ // See: https://github.com/modelcontextprotocol/sdk/issues - deep type instantiation with Zod schemas
6
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
7
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
8
+ const zod_1 = require("zod");
9
+ const executor_1 = require("../core/executor");
10
+ const approval_1 = require("../core/approval");
11
+ const logger_1 = require("../core/logger");
12
+ const log = (0, logger_1.createStderrLogger)("MCP");
13
+ async function startMcpServer() {
14
+ const executor = new executor_1.Executor();
15
+ const server = new mcp_js_1.McpServer({
16
+ name: "deskmate",
17
+ version: "1.0.0",
18
+ });
19
+ // Tool: Execute shell command
20
+ server.tool("execute_command", "Execute a shell command on the local machine. Requires approval for potentially dangerous commands.", {
21
+ command: zod_1.z.string().describe("The shell command to execute"),
22
+ working_dir: zod_1.z.string().optional().describe("Working directory (defaults to configured dir)"),
23
+ }, async ({ command, working_dir }) => {
24
+ log.info("Tool invoked: execute_command", { command, working_dir });
25
+ const approved = await approval_1.approvalManager.requestApproval("command", `Execute: ${command}`, { command, workingDir: working_dir || executor.getWorkingDir() }, { autoApprove: true });
26
+ if (!approved) {
27
+ log.warn("Command not approved", { command });
28
+ return {
29
+ content: [{ type: "text", text: "Command was not approved or timed out." }],
30
+ };
31
+ }
32
+ const result = await executor.executeCommand(command, working_dir);
33
+ log.debug("Tool completed: execute_command", { success: result.success, exitCode: result.exitCode });
34
+ return {
35
+ content: [{ type: "text", text: `Exit code: ${result.exitCode}\n\n${result.output}` }],
36
+ isError: !result.success,
37
+ };
38
+ });
39
+ // Tool: Read file
40
+ server.tool("read_file", "Read the contents of a file", {
41
+ path: zod_1.z.string().describe("Path to the file (absolute or relative to working dir)"),
42
+ }, async ({ path }) => {
43
+ log.info("Tool invoked: read_file", { path });
44
+ try {
45
+ const content = await executor.readFile(path);
46
+ log.debug("Tool completed: read_file", { path, size: content.length });
47
+ return {
48
+ content: [{ type: "text", text: content }],
49
+ };
50
+ }
51
+ catch (error) {
52
+ log.error("Tool failed: read_file", { path, error: error.message });
53
+ return {
54
+ content: [{ type: "text", text: `Error reading file: ${error.message}` }],
55
+ isError: true,
56
+ };
57
+ }
58
+ });
59
+ // Tool: Write file
60
+ server.tool("write_file", "Write content to a file. Requires approval.", {
61
+ path: zod_1.z.string().describe("Path to the file (absolute or relative to working dir)"),
62
+ file_content: zod_1.z.string().describe("Content to write to the file"),
63
+ }, async ({ path, file_content }) => {
64
+ log.info("Tool invoked: write_file", { path, contentLength: file_content.length });
65
+ const approved = await approval_1.approvalManager.requestApproval("write_file", `Write to: ${path}`, { path, contentPreview: file_content.slice(0, 200) });
66
+ if (!approved) {
67
+ log.warn("File write not approved", { path });
68
+ return {
69
+ content: [{ type: "text", text: "File write was not approved or timed out." }],
70
+ };
71
+ }
72
+ try {
73
+ await executor.writeFile(path, file_content);
74
+ log.debug("Tool completed: write_file", { path, size: file_content.length });
75
+ return {
76
+ content: [{ type: "text", text: `Successfully wrote ${file_content.length} bytes to ${path}` }],
77
+ };
78
+ }
79
+ catch (error) {
80
+ log.error("Tool failed: write_file", { path, error: error.message });
81
+ return {
82
+ content: [{ type: "text", text: `Error writing file: ${error.message}` }],
83
+ isError: true,
84
+ };
85
+ }
86
+ });
87
+ // Tool: List directory
88
+ server.tool("list_directory", "List files and directories in a path", {
89
+ path: zod_1.z.string().optional().describe("Path to list (defaults to working directory)"),
90
+ }, async ({ path }) => {
91
+ log.info("Tool invoked: list_directory", { path });
92
+ try {
93
+ const files = await executor.listDirectory(path);
94
+ const output = files
95
+ .map((f) => {
96
+ const type = f.isDirectory ? "dir" : "file";
97
+ const size = f.isDirectory ? "" : ` (${formatBytes(f.size)})`;
98
+ return `[${type}] ${f.name}${size}`;
99
+ })
100
+ .join("\n");
101
+ log.debug("Tool completed: list_directory", { path, itemCount: files.length });
102
+ return {
103
+ content: [{ type: "text", text: output || "(empty directory)" }],
104
+ };
105
+ }
106
+ catch (error) {
107
+ log.error("Tool failed: list_directory", { path, error: error.message });
108
+ return {
109
+ content: [{ type: "text", text: `Error listing directory: ${error.message}` }],
110
+ isError: true,
111
+ };
112
+ }
113
+ });
114
+ // Tool: Get system info
115
+ server.tool("get_system_info", "Get information about the local system", {}, async () => {
116
+ log.info("Tool invoked: get_system_info");
117
+ const info = await executor.getSystemInfo();
118
+ const output = Object.entries(info)
119
+ .map(([key, value]) => `${key}: ${value}`)
120
+ .join("\n");
121
+ log.debug("Tool completed: get_system_info");
122
+ return {
123
+ content: [{ type: "text", text: output }],
124
+ };
125
+ });
126
+ // Tool: List pending approvals
127
+ server.tool("list_pending_approvals", "List actions waiting for approval", {}, async () => {
128
+ log.info("Tool invoked: list_pending_approvals");
129
+ const pending = approval_1.approvalManager.getPendingActions();
130
+ log.debug("Tool completed: list_pending_approvals", { pendingCount: pending.length });
131
+ if (pending.length === 0) {
132
+ return {
133
+ content: [{ type: "text", text: "No pending approvals" }],
134
+ };
135
+ }
136
+ const output = pending
137
+ .map((a) => `[${a.id}] ${a.type}: ${a.description}`)
138
+ .join("\n");
139
+ return {
140
+ content: [{ type: "text", text: output }],
141
+ };
142
+ });
143
+ // Connect via stdio
144
+ const transport = new stdio_js_1.StdioServerTransport();
145
+ await server.connect(transport);
146
+ log.info("MCP Server started", { transport: "stdio" });
147
+ }
148
+ function formatBytes(bytes) {
149
+ if (bytes < 1024)
150
+ return `${bytes} B`;
151
+ if (bytes < 1024 * 1024)
152
+ return `${(bytes / 1024).toFixed(1)} KB`;
153
+ if (bytes < 1024 * 1024 * 1024)
154
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
155
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
156
+ }
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.startTelegramBot = startTelegramBot;
37
+ const path = __importStar(require("path"));
38
+ const grammy_1 = require("grammy");
39
+ const executor_1 = require("../core/executor");
40
+ const approval_1 = require("../core/approval");
41
+ const agent_1 = require("../core/agent");
42
+ const logger_1 = require("../core/logger");
43
+ const platform_1 = require("../core/platform");
44
+ const session_1 = require("../gateway/session");
45
+ const log = (0, logger_1.createLogger)("TelegramBot");
46
+ const BOT_NAME = process.env.BOT_NAME || "Deskmate";
47
+ const SCREENSHOT_DIR = process.env.TMPDIR ? `${process.env.TMPDIR}deskmate-screenshots` : "/tmp/deskmate-screenshots";
48
+ const SYSTEM_PROMPT = `You are a local machine assistant named ${BOT_NAME}. Users will ask you to perform tasks on their computer via Telegram.
49
+
50
+ You have access to tools to execute commands, read/write files, and explore the filesystem. Use them to help users accomplish their tasks.
51
+
52
+ SCREENSHOT CAPABILITY:
53
+ When the user asks to see the screen, take a screenshot, or wants visual feedback, use this command:
54
+ ${(0, platform_1.getScreenshotHint)(SCREENSHOT_DIR)}
55
+ The screenshot will automatically be sent to the user via Telegram after your response.
56
+
57
+ IMPORTANT RULES:
58
+ - Be concise in your responses (Telegram messages should be brief)
59
+ - Use the available tools to accomplish tasks
60
+ - For dangerous operations, explain what you're about to do before doing it
61
+ - Never use sudo unless explicitly asked
62
+ - Keep responses under 4000 characters (Telegram limit)
63
+ - When asked for screenshots, always use the screenshot command above`;
64
+ async function startTelegramBot() {
65
+ const token = process.env.TELEGRAM_BOT_TOKEN;
66
+ const allowedUserId = parseInt(process.env.ALLOWED_USER_ID || "0", 10);
67
+ if (!token || !allowedUserId) {
68
+ throw new Error("TELEGRAM_BOT_TOKEN and ALLOWED_USER_ID required for Telegram bot");
69
+ }
70
+ const bot = new grammy_1.Bot(token);
71
+ const executor = new executor_1.Executor();
72
+ const workingDir = process.env.WORKING_DIR || process.env.HOME || "/";
73
+ // Create the agent provider (abstracted - can be swapped)
74
+ const agentProvider = (0, agent_1.createAgentProvider)();
75
+ log.info("Using agent provider", { name: agentProvider.name, version: agentProvider.version });
76
+ // Verify provider is available
77
+ const providerAvailable = await agentProvider.isAvailable();
78
+ if (!providerAvailable) {
79
+ log.warn("Agent provider may not be fully available", { provider: agentProvider.name });
80
+ }
81
+ // Store session IDs per chat for context/memory (persisted to disk)
82
+ const chatSessions = new session_1.SessionManager({
83
+ storagePath: path.join(workingDir, "data", "sessions-telegram.json"),
84
+ });
85
+ // Register Telegram as an approval notifier
86
+ approval_1.approvalManager.addNotifier(async (action) => {
87
+ // This will be called when approval is needed
88
+ // We send a Telegram message for the user to approve/reject
89
+ try {
90
+ const keyboard = new grammy_1.InlineKeyboard()
91
+ .text("✅ Approve", `approve:${action.id}`)
92
+ .text("❌ Reject", `reject:${action.id}`);
93
+ let emoji = "🔐";
94
+ let details = "";
95
+ switch (action.type) {
96
+ case "command":
97
+ emoji = "⚡";
98
+ details = `\`\`\`bash\n${action.details.command}\n\`\`\``;
99
+ break;
100
+ case "write_file":
101
+ emoji = "📝";
102
+ details = `Path: \`${action.details.path}\`\nPreview: ${(action.details.contentPreview || "").slice(0, 100)}...`;
103
+ break;
104
+ case "folder_access":
105
+ emoji = "📁";
106
+ details = `Folder: \`${action.details.baseFolder}\`\nFile: \`${action.details.path}\``;
107
+ break;
108
+ case "read_file":
109
+ emoji = "đŸ‘ī¸";
110
+ details = `Path: \`${action.details.path}\``;
111
+ break;
112
+ default:
113
+ details = JSON.stringify(action.details, null, 2);
114
+ }
115
+ const timeLeft = Math.ceil((action.expiresAt.getTime() - Date.now()) / 1000);
116
+ await bot.api.sendMessage(allowedUserId, `${emoji} *Approval Required*\n\n` +
117
+ `Type: \`${action.type}\`\n` +
118
+ `${action.description}\n\n` +
119
+ `${details}\n\n` +
120
+ `âąī¸ Expires in ${timeLeft}s`, { parse_mode: "Markdown", reply_markup: keyboard });
121
+ log.info("Approval notification sent to Telegram", { actionId: action.id, type: action.type });
122
+ }
123
+ catch (error) {
124
+ log.error("Failed to send Telegram notification", { error: error.message });
125
+ }
126
+ });
127
+ // Auth middleware
128
+ bot.use(async (ctx, next) => {
129
+ if (ctx.from?.id !== allowedUserId) {
130
+ console.log(`Unauthorized: ${ctx.from?.id}`);
131
+ return;
132
+ }
133
+ await next();
134
+ });
135
+ // Commands
136
+ bot.command("start", (ctx) => ctx.reply(`👋 *${BOT_NAME} Ready*\n\n` +
137
+ "Send me any task and I'll execute it on your local machine.\n\n" +
138
+ "I remember our conversation, so you can ask follow-up questions!\n\n" +
139
+ "*Examples:*\n" +
140
+ "â€ĸ `list all docker containers`\n" +
141
+ "â€ĸ `what's using port 3000?`\n" +
142
+ "â€ĸ `show disk usage`\n" +
143
+ "â€ĸ `take a screenshot`\n\n" +
144
+ "*Commands:*\n" +
145
+ "â€ĸ /screenshot - Take a screenshot\n" +
146
+ "â€ĸ /status - System info\n" +
147
+ "â€ĸ /reset - Clear memory & start fresh", { parse_mode: "Markdown" }));
148
+ // Quick screenshot command
149
+ bot.command("screenshot", async (ctx) => {
150
+ const chatId = ctx.chat.id;
151
+ await ctx.reply("📸 Taking screenshot...");
152
+ try {
153
+ const { exec } = await Promise.resolve().then(() => __importStar(require("child_process")));
154
+ const { promisify } = await Promise.resolve().then(() => __importStar(require("util")));
155
+ const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
156
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
157
+ const execAsync = promisify(exec);
158
+ await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
159
+ const filename = `screenshot-${Date.now()}.png`;
160
+ const filepath = path.join(SCREENSHOT_DIR, filename);
161
+ await execAsync((0, platform_1.getScreenshotCommand)(filepath));
162
+ await bot.api.sendPhoto(chatId, new grammy_1.InputFile(filepath), {
163
+ caption: "📸 Screenshot",
164
+ });
165
+ await fs.unlink(filepath).catch(() => { });
166
+ log.info("Screenshot command completed", { chatId });
167
+ }
168
+ catch (error) {
169
+ log.error("Screenshot command failed", { error: error.message });
170
+ await ctx.reply(`❌ Screenshot failed: ${error.message}`);
171
+ }
172
+ });
173
+ bot.command("status", async (ctx) => {
174
+ const pending = approval_1.approvalManager.getPendingActions();
175
+ const info = await executor.getSystemInfo();
176
+ const hasSession = chatSessions.has("telegram", String(ctx.chat.id));
177
+ await ctx.reply(`đŸ–Ĩī¸ *System Status*\n\n` +
178
+ `â€ĸ Host: ${info.hostname || "unknown"}\n` +
179
+ `â€ĸ Platform: ${info.platform}\n` +
180
+ `â€ĸ Agent: ${agentProvider.name} v${agentProvider.version}\n` +
181
+ `â€ĸ Pending approvals: ${pending.length}\n` +
182
+ `â€ĸ Working dir: \`${executor.getWorkingDir()}\`\n` +
183
+ `â€ĸ Session active: ${hasSession ? "Yes ✅" : "No"}`, { parse_mode: "Markdown" });
184
+ });
185
+ // Reset session - start fresh conversation
186
+ bot.command("reset", async (ctx) => {
187
+ const chatId = ctx.chat.id;
188
+ const hadSession = chatSessions.has("telegram", String(chatId));
189
+ chatSessions.delete("telegram", String(chatId));
190
+ log.info("Session reset", { chatId, hadSession });
191
+ await ctx.reply(hadSession
192
+ ? "🔄 Session cleared! Starting fresh conversation."
193
+ : "â„šī¸ No active session to clear.");
194
+ });
195
+ // Approval callbacks
196
+ bot.callbackQuery(/^approve:(.+)$/, async (ctx) => {
197
+ const actionId = ctx.match[1];
198
+ const success = approval_1.approvalManager.approve(actionId);
199
+ await ctx.answerCallbackQuery({ text: success ? "Approved!" : "Action not found" });
200
+ if (success) {
201
+ await ctx.editMessageText("✅ *Approved* - executing...", { parse_mode: "Markdown" });
202
+ }
203
+ });
204
+ bot.callbackQuery(/^reject:(.+)$/, async (ctx) => {
205
+ const actionId = ctx.match[1];
206
+ approval_1.approvalManager.reject(actionId);
207
+ await ctx.answerCallbackQuery({ text: "Rejected" });
208
+ await ctx.editMessageText("❌ *Rejected*", { parse_mode: "Markdown" });
209
+ });
210
+ // Helper to send screenshots
211
+ async function sendScreenshots(chatId, since) {
212
+ try {
213
+ const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
214
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
215
+ await fs.mkdir(SCREENSHOT_DIR, { recursive: true }).catch(() => { });
216
+ const files = await fs.readdir(SCREENSHOT_DIR).catch(() => []);
217
+ let sent = 0;
218
+ for (const file of files) {
219
+ if (!file.endsWith(".png"))
220
+ continue;
221
+ const filepath = path.join(SCREENSHOT_DIR, file);
222
+ const stats = await fs.stat(filepath).catch(() => null);
223
+ if (stats && stats.mtime >= since) {
224
+ log.info("Sending screenshot", { filepath });
225
+ await bot.api.sendPhoto(chatId, new grammy_1.InputFile(filepath), {
226
+ caption: "📸 Screenshot",
227
+ });
228
+ // Clean up after sending
229
+ await fs.unlink(filepath).catch(() => { });
230
+ sent++;
231
+ }
232
+ }
233
+ return sent;
234
+ }
235
+ catch (error) {
236
+ log.error("Failed to send screenshots", { error: error.message });
237
+ return 0;
238
+ }
239
+ }
240
+ // Main message handler - uses abstract agent provider
241
+ bot.on("message:text", async (ctx) => {
242
+ const userMessage = ctx.message.text;
243
+ const chatId = ctx.chat.id;
244
+ const thinkingMsg = await ctx.reply("🤔 Thinking...");
245
+ // Record time before execution for screenshot detection
246
+ const executionStartTime = new Date();
247
+ // Get existing session for this chat (if any)
248
+ const existingSessionId = chatSessions.get("telegram", String(chatId));
249
+ log.info("Received message", {
250
+ userId: ctx.from?.id,
251
+ chatId,
252
+ hasSession: !!existingSessionId,
253
+ message: userMessage.slice(0, 100),
254
+ });
255
+ try {
256
+ let result = "";
257
+ let lastUpdate = Date.now();
258
+ let newSessionId;
259
+ // Use the abstract agent provider
260
+ for await (const event of agentProvider.queryStream(userMessage, {
261
+ systemPrompt: SYSTEM_PROMPT,
262
+ workingDir,
263
+ sessionId: existingSessionId,
264
+ maxTurns: 10,
265
+ })) {
266
+ log.debug("Agent event", { type: event.type });
267
+ switch (event.type) {
268
+ case "text":
269
+ if (event.text) {
270
+ result = event.text;
271
+ }
272
+ break;
273
+ case "tool_use":
274
+ log.debug("Tool use", { tool: event.toolName });
275
+ break;
276
+ case "done":
277
+ if (event.response) {
278
+ result = event.response.text;
279
+ newSessionId = event.response.sessionId;
280
+ }
281
+ break;
282
+ case "error":
283
+ throw new Error(event.error || "Unknown agent error");
284
+ }
285
+ // Update thinking message periodically to show progress
286
+ if (Date.now() - lastUpdate > 3000) {
287
+ try {
288
+ await ctx.api.editMessageText(chatId, thinkingMsg.message_id, "âŗ Working...");
289
+ lastUpdate = Date.now();
290
+ }
291
+ catch {
292
+ // Ignore edit errors
293
+ }
294
+ }
295
+ }
296
+ // Store the session ID for future messages
297
+ if (newSessionId) {
298
+ chatSessions.set("telegram", String(chatId), newSessionId);
299
+ log.info("Session stored", { chatId, sessionId: newSessionId });
300
+ }
301
+ // Send final result
302
+ const finalMessage = result || "Task completed (no output)";
303
+ const truncated = finalMessage.slice(0, 4000); // Telegram limit
304
+ log.info("Agent completed", { resultLength: finalMessage.length, hasSession: !!newSessionId });
305
+ await ctx.api.editMessageText(chatId, thinkingMsg.message_id, truncated, {
306
+ parse_mode: "Markdown",
307
+ }).catch(async () => {
308
+ // If Markdown fails, try without
309
+ await ctx.api.editMessageText(chatId, thinkingMsg.message_id, truncated);
310
+ });
311
+ // Check for and send any screenshots taken during execution
312
+ const screenshotsSent = await sendScreenshots(chatId, executionStartTime);
313
+ if (screenshotsSent > 0) {
314
+ log.info("Screenshots sent", { count: screenshotsSent });
315
+ }
316
+ }
317
+ catch (error) {
318
+ log.error("Agent error", { error: error.message });
319
+ // If session error, clear the session and retry might help
320
+ if (error.message?.includes("session")) {
321
+ chatSessions.delete("telegram", String(chatId));
322
+ log.warn("Cleared invalid session", { chatId });
323
+ }
324
+ await ctx.api.editMessageText(chatId, thinkingMsg.message_id, `❌ Error: ${error.message}`);
325
+ }
326
+ });
327
+ // Start polling
328
+ console.log("🤖 Starting Telegram bot...");
329
+ bot.start({
330
+ drop_pending_updates: true, // Don't process old updates on restart
331
+ onStart: (info) => console.log(`✅ Telegram bot @${info.username} running (${agentProvider.name})`),
332
+ });
333
+ }