@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/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/dist/cli/init.js +387 -0
- package/dist/cli.js +184 -0
- package/dist/clients/telegram.js +148 -0
- package/dist/core/agent/factory.js +67 -0
- package/dist/core/agent/index.js +32 -0
- package/dist/core/agent/providers/claude-code.js +158 -0
- package/dist/core/agent/types.js +9 -0
- package/dist/core/approval.js +192 -0
- package/dist/core/executor.js +237 -0
- package/dist/core/logger.js +76 -0
- package/dist/core/platform.js +130 -0
- package/dist/gateway/gateway.js +435 -0
- package/dist/gateway/index.js +9 -0
- package/dist/gateway/security.js +35 -0
- package/dist/gateway/session.js +195 -0
- package/dist/gateway/types.js +8 -0
- package/dist/index.js +130 -0
- package/dist/mcp/server.js +156 -0
- package/dist/telegram/bot.js +333 -0
- package/install.sh +817 -0
- package/package.json +73 -0
- package/uninstall.sh +121 -0
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
|
+
}
|