@soleri/forge 5.11.0 → 5.12.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/README.md +3 -3
- package/dist/facades/forge.facade.js +3 -3
- package/dist/facades/forge.facade.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/scaffolder.js +137 -20
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/agents-md.d.ts +5 -0
- package/dist/templates/agents-md.js +33 -0
- package/dist/templates/agents-md.js.map +1 -0
- package/dist/templates/entry-point.js +15 -2
- package/dist/templates/entry-point.js.map +1 -1
- package/dist/templates/package-json.js +7 -0
- package/dist/templates/package-json.js.map +1 -1
- package/dist/templates/readme.js +80 -27
- package/dist/templates/readme.js.map +1 -1
- package/dist/templates/setup-script.d.ts +1 -1
- package/dist/templates/setup-script.js +135 -53
- package/dist/templates/setup-script.js.map +1 -1
- package/dist/templates/skills.d.ts +0 -7
- package/dist/templates/skills.js +0 -21
- package/dist/templates/skills.js.map +1 -1
- package/dist/templates/telegram-agent.d.ts +6 -0
- package/dist/templates/telegram-agent.js +212 -0
- package/dist/templates/telegram-agent.js.map +1 -0
- package/dist/templates/telegram-bot.d.ts +6 -0
- package/dist/templates/telegram-bot.js +450 -0
- package/dist/templates/telegram-bot.js.map +1 -0
- package/dist/templates/telegram-config.d.ts +6 -0
- package/dist/templates/telegram-config.js +81 -0
- package/dist/templates/telegram-config.js.map +1 -0
- package/dist/templates/telegram-supervisor.d.ts +6 -0
- package/dist/templates/telegram-supervisor.js +148 -0
- package/dist/templates/telegram-supervisor.js.map +1 -0
- package/dist/types.d.ts +15 -5
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/scaffolder.test.ts +62 -0
- package/src/facades/forge.facade.ts +3 -3
- package/src/lib.ts +3 -1
- package/src/scaffolder.ts +170 -28
- package/src/templates/agents-md.ts +35 -0
- package/src/templates/entry-point.ts +15 -2
- package/src/templates/package-json.ts +7 -0
- package/src/templates/readme.ts +89 -27
- package/src/templates/setup-script.ts +141 -54
- package/src/templates/skills.ts +0 -23
- package/src/templates/telegram-agent.ts +214 -0
- package/src/templates/telegram-bot.ts +452 -0
- package/src/templates/telegram-config.ts +83 -0
- package/src/templates/telegram-supervisor.ts +150 -0
- package/src/types.ts +12 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template: Telegram agent loop — wires @soleri/core agent loop to MCP tools.
|
|
3
|
+
* Generated by @soleri/forge for agents with Telegram transport.
|
|
4
|
+
*/
|
|
5
|
+
export function generateTelegramAgent(config) {
|
|
6
|
+
return `/**
|
|
7
|
+
* ${config.name} — Telegram Agent Loop.
|
|
8
|
+
* Generated by @soleri/forge.
|
|
9
|
+
*
|
|
10
|
+
* Wires the @soleri/core agent loop to the agent's MCP tools.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
runAgentLoop,
|
|
15
|
+
McpToolBridge,
|
|
16
|
+
createOutputCompressor,
|
|
17
|
+
} from '@soleri/core';
|
|
18
|
+
import type {
|
|
19
|
+
ChatMessage,
|
|
20
|
+
AgentLoopResult,
|
|
21
|
+
AgentCallbacks,
|
|
22
|
+
McpToolRegistration,
|
|
23
|
+
} from '@soleri/core';
|
|
24
|
+
import type { TelegramConfig } from './telegram-config.js';
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
27
|
+
import { dirname } from 'node:path';
|
|
28
|
+
|
|
29
|
+
// ─── Core Tools ──────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const CORE_TOOLS: McpToolRegistration[] = [
|
|
32
|
+
{
|
|
33
|
+
name: 'bash',
|
|
34
|
+
description: 'Execute a shell command. Returns stdout/stderr.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
command: { type: 'string', description: 'Shell command to execute.' },
|
|
39
|
+
timeout: { type: 'number', description: 'Timeout in ms. Default: 120000.' },
|
|
40
|
+
},
|
|
41
|
+
required: ['command'],
|
|
42
|
+
},
|
|
43
|
+
handler: async (input) => {
|
|
44
|
+
try {
|
|
45
|
+
const timeout = (input.timeout as number) ?? 120_000;
|
|
46
|
+
const output = execFileSync('/bin/sh', ['-c', input.command as string], {
|
|
47
|
+
timeout,
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
maxBuffer: 1024 * 1024,
|
|
50
|
+
});
|
|
51
|
+
return output;
|
|
52
|
+
} catch (err: unknown) {
|
|
53
|
+
const error = err as { stdout?: string; stderr?: string; message?: string };
|
|
54
|
+
return \`Error: \${error.stderr ?? error.stdout ?? error.message ?? 'Unknown error'}\`;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'read_file',
|
|
60
|
+
description: 'Read a file from the filesystem.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
path: { type: 'string', description: 'File path to read.' },
|
|
65
|
+
},
|
|
66
|
+
required: ['path'],
|
|
67
|
+
},
|
|
68
|
+
handler: async (input) => {
|
|
69
|
+
return readFileSync(input.path as string, 'utf-8');
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'write_file',
|
|
74
|
+
description: 'Write content to a file.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
path: { type: 'string', description: 'File path to write.' },
|
|
79
|
+
content: { type: 'string', description: 'Content to write.' },
|
|
80
|
+
},
|
|
81
|
+
required: ['path', 'content'],
|
|
82
|
+
},
|
|
83
|
+
handler: async (input) => {
|
|
84
|
+
mkdirSync(dirname(input.path as string), { recursive: true });
|
|
85
|
+
writeFileSync(input.path as string, input.content as string, 'utf-8');
|
|
86
|
+
return \`Wrote \${(input.content as string).length} bytes to \${input.path}\`;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'glob',
|
|
91
|
+
description: 'Find files matching a glob pattern.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
pattern: { type: 'string', description: 'Glob pattern.' },
|
|
96
|
+
cwd: { type: 'string', description: 'Working directory.' },
|
|
97
|
+
},
|
|
98
|
+
required: ['pattern'],
|
|
99
|
+
},
|
|
100
|
+
handler: async (input) => {
|
|
101
|
+
const cwd = (input.cwd as string) ?? process.cwd();
|
|
102
|
+
const output = execFileSync('find', ['.', '-path', input.pattern as string, '-type', 'f'], {
|
|
103
|
+
cwd,
|
|
104
|
+
encoding: 'utf-8',
|
|
105
|
+
timeout: 10_000,
|
|
106
|
+
});
|
|
107
|
+
const lines = output.trim().split('\\n').filter(Boolean).slice(0, 100);
|
|
108
|
+
return lines.length > 0 ? lines.join('\\n') : 'No files found.';
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'grep',
|
|
113
|
+
description: 'Search file contents with regex.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
pattern: { type: 'string', description: 'Regex pattern.' },
|
|
118
|
+
path: { type: 'string', description: 'File or directory to search.' },
|
|
119
|
+
type: { type: 'string', description: 'File type filter (e.g., ts, js).' },
|
|
120
|
+
},
|
|
121
|
+
required: ['pattern'],
|
|
122
|
+
},
|
|
123
|
+
handler: async (input) => {
|
|
124
|
+
const searchPath = (input.path as string) ?? '.';
|
|
125
|
+
const args = ['--no-heading', '-n', input.pattern as string, searchPath];
|
|
126
|
+
if (input.type) args.splice(2, 0, '--type', input.type as string);
|
|
127
|
+
try {
|
|
128
|
+
const output = execFileSync('rg', args, {
|
|
129
|
+
encoding: 'utf-8',
|
|
130
|
+
timeout: 10_000,
|
|
131
|
+
});
|
|
132
|
+
const lines = output.trim().split('\\n').filter(Boolean).slice(0, 50);
|
|
133
|
+
return lines.length > 0 ? lines.join('\\n') : 'No matches found.';
|
|
134
|
+
} catch {
|
|
135
|
+
return 'No matches found.';
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// ─── Agent Factory ───────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export interface TelegramAgentRunner {
|
|
144
|
+
run(messages: ChatMessage[], callbacks?: AgentCallbacks): Promise<AgentLoopResult>;
|
|
145
|
+
registerTool(tool: McpToolRegistration): void;
|
|
146
|
+
getToolCount(): number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createTelegramAgent(config: TelegramConfig): TelegramAgentRunner {
|
|
150
|
+
const bridge = new McpToolBridge({
|
|
151
|
+
compressor: createOutputCompressor({ maxLength: 4000 }),
|
|
152
|
+
maxOutput: 10_000,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Register core tools
|
|
156
|
+
bridge.registerAll(CORE_TOOLS);
|
|
157
|
+
|
|
158
|
+
const systemPrompt = buildSystemPrompt(config);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
run: async (messages: ChatMessage[], callbacks?: AgentCallbacks) => {
|
|
162
|
+
return runAgentLoop(messages, {
|
|
163
|
+
apiKey: config.apiKey,
|
|
164
|
+
model: config.model,
|
|
165
|
+
systemPrompt,
|
|
166
|
+
tools: bridge.getTools(),
|
|
167
|
+
executor: bridge.createExecutor(),
|
|
168
|
+
maxIterations: config.maxIterations,
|
|
169
|
+
maxTokens: config.maxTokens,
|
|
170
|
+
}, callbacks);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
registerTool: (tool: McpToolRegistration) => {
|
|
174
|
+
bridge.register(tool);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
getToolCount: () => bridge.size,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── System Prompt ───────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function buildSystemPrompt(config: TelegramConfig): string {
|
|
184
|
+
const parts: string[] = [
|
|
185
|
+
'# ${config.name}',
|
|
186
|
+
'',
|
|
187
|
+
'${config.role}',
|
|
188
|
+
'',
|
|
189
|
+
'## Environment',
|
|
190
|
+
\`- Platform: \${process.platform}\`,
|
|
191
|
+
\`- Working directory: \${config.workingDirectory ?? process.cwd()}\`,
|
|
192
|
+
\`- Date: \${new Date().toISOString().split('T')[0]}\`,
|
|
193
|
+
'',
|
|
194
|
+
'## Telegram Formatting',
|
|
195
|
+
'- Keep responses concise (under 2000 chars when possible)',
|
|
196
|
+
'- Use Markdown: **bold**, *italic*, \\\`code\\\`, \\\`\\\`\\\`code blocks\\\`\\\`\\\`',
|
|
197
|
+
'- Use bullet points for lists',
|
|
198
|
+
'- No raw JSON in responses — format for human reading',
|
|
199
|
+
'',
|
|
200
|
+
'## Available Tools',
|
|
201
|
+
'- bash: Execute shell commands',
|
|
202
|
+
'- read_file: Read file contents',
|
|
203
|
+
'- write_file: Write to files',
|
|
204
|
+
'- glob: Find files by pattern',
|
|
205
|
+
'- grep: Search file contents',
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
return parts.join('\\n');
|
|
209
|
+
}
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=telegram-agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-agent.js","sourceRoot":"","sources":["../../src/templates/telegram-agent.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,UAAU,qBAAqB,CAAC,MAAmB;IACvD,OAAO;KACJ,MAAM,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SAkLP,MAAM,CAAC,IAAI;;OAEb,MAAM,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;CAuBjB,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template: Telegram bot entry point with Grammy.
|
|
3
|
+
* Generated by @soleri/forge for agents with Telegram transport.
|
|
4
|
+
*/
|
|
5
|
+
export function generateTelegramBot(config) {
|
|
6
|
+
return `/**
|
|
7
|
+
* ${config.name} — Telegram Bot Entry Point.
|
|
8
|
+
* Generated by @soleri/forge.
|
|
9
|
+
*
|
|
10
|
+
* Grammy bot with middleware pipeline, session management, and agent dispatch.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Bot } from 'grammy';
|
|
14
|
+
import { loadTelegramConfig, type TelegramConfig } from './telegram-config.js';
|
|
15
|
+
import { createTelegramAgent } from './telegram-agent.js';
|
|
16
|
+
import {
|
|
17
|
+
ChatSessionManager,
|
|
18
|
+
ChatAuthManager,
|
|
19
|
+
TaskCancellationManager,
|
|
20
|
+
SelfUpdateManager,
|
|
21
|
+
RESTART_EXIT_CODE,
|
|
22
|
+
NotificationEngine,
|
|
23
|
+
FragmentBuffer,
|
|
24
|
+
chunkResponse,
|
|
25
|
+
detectFileIntent,
|
|
26
|
+
buildMultimodalContent,
|
|
27
|
+
saveTempFile,
|
|
28
|
+
cleanupTempFiles,
|
|
29
|
+
MAX_FILE_SIZE,
|
|
30
|
+
} from '@soleri/core';
|
|
31
|
+
import type { Fragment, FileInfo } from '@soleri/core';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import { homedir } from 'node:os';
|
|
34
|
+
import { mkdirSync, appendFileSync } from 'node:fs';
|
|
35
|
+
|
|
36
|
+
// ─── Config ──────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const CONFIG_DIR = join(homedir(), '.${config.id}');
|
|
39
|
+
const SESSIONS_DIR = join(CONFIG_DIR, 'telegram-sessions');
|
|
40
|
+
const AUTH_FILE = join(CONFIG_DIR, 'telegram-auth.json');
|
|
41
|
+
const RESTART_FILE = join(CONFIG_DIR, 'telegram-restart.json');
|
|
42
|
+
const UPLOAD_DIR = join(CONFIG_DIR, 'telegram-uploads');
|
|
43
|
+
const LOG_DIR = join(CONFIG_DIR, 'logs');
|
|
44
|
+
|
|
45
|
+
// ─── State ───────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const processingLocks = new Set<string>();
|
|
48
|
+
let telegramConfig: TelegramConfig;
|
|
49
|
+
let sessions: ChatSessionManager;
|
|
50
|
+
let auth: ChatAuthManager;
|
|
51
|
+
let fragmentBuffer: FragmentBuffer;
|
|
52
|
+
const cancellation = new TaskCancellationManager();
|
|
53
|
+
const updater = new SelfUpdateManager(RESTART_FILE);
|
|
54
|
+
let notifications: NotificationEngine;
|
|
55
|
+
|
|
56
|
+
// ─── Event Logger ────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function logEvent(type: string, chatId: number | string, data?: Record<string, unknown>): void {
|
|
59
|
+
try {
|
|
60
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
61
|
+
const date = new Date().toISOString().split('T')[0];
|
|
62
|
+
const entry = JSON.stringify({ ts: new Date().toISOString(), chatId, type, ...data });
|
|
63
|
+
appendFileSync(join(LOG_DIR, \`events-\${date}.jsonl\`), entry + '\\n', 'utf-8');
|
|
64
|
+
} catch {
|
|
65
|
+
// Logging failure is non-critical
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Bot Setup ───────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export async function startBot(): Promise<void> {
|
|
72
|
+
telegramConfig = loadTelegramConfig();
|
|
73
|
+
|
|
74
|
+
// Initialize infrastructure
|
|
75
|
+
sessions = new ChatSessionManager({
|
|
76
|
+
storageDir: SESSIONS_DIR,
|
|
77
|
+
ttlMs: 7_200_000, // 2 hours
|
|
78
|
+
compactionThreshold: 100,
|
|
79
|
+
compactionKeep: 40,
|
|
80
|
+
});
|
|
81
|
+
sessions.startReaper();
|
|
82
|
+
|
|
83
|
+
auth = new ChatAuthManager({
|
|
84
|
+
storagePath: AUTH_FILE,
|
|
85
|
+
passphrase: telegramConfig.passphrase,
|
|
86
|
+
allowedUsers: telegramConfig.allowedUsers,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
fragmentBuffer = new FragmentBuffer(
|
|
90
|
+
{ startThreshold: 4000, maxGapMs: 1500, maxParts: 12, maxTotalBytes: 50_000 },
|
|
91
|
+
(key, merged) => handleMergedMessage(key, merged),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Initialize notification engine
|
|
95
|
+
notifications = new NotificationEngine({
|
|
96
|
+
intervalMs: 30 * 60 * 1000, // 30 minutes
|
|
97
|
+
onNotify: async (_checkId, message) => {
|
|
98
|
+
try {
|
|
99
|
+
// Send to the most recently active chat
|
|
100
|
+
const activeSessions = sessions.listActive();
|
|
101
|
+
if (activeSessions.length > 0) {
|
|
102
|
+
await bot.api.sendMessage(activeSessions[0], message, { parse_mode: 'HTML' });
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Notification delivery failure is non-critical
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const bot = new Bot(telegramConfig.botToken);
|
|
111
|
+
|
|
112
|
+
// ─── Restart Context ───────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const restartCtx = updater.loadContext();
|
|
115
|
+
if (restartCtx) {
|
|
116
|
+
updater.clearContext();
|
|
117
|
+
try {
|
|
118
|
+
await bot.api.sendMessage(restartCtx.chatId, \`Back online! Restart reason: \${restartCtx.reason}.\`);
|
|
119
|
+
logEvent('restart_confirmed', restartCtx.chatId, { reason: restartCtx.reason });
|
|
120
|
+
} catch {
|
|
121
|
+
// Chat might not exist anymore
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Commands ──────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
bot.command('start', async (ctx) => {
|
|
128
|
+
if (!checkAuth(ctx)) return;
|
|
129
|
+
await ctx.reply('Welcome! I am ${config.name}, ${config.role}. How can I help?');
|
|
130
|
+
logEvent('command', ctx.chat.id, { command: 'start' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
bot.command('clear', async (ctx) => {
|
|
134
|
+
if (!checkAuth(ctx)) return;
|
|
135
|
+
sessions.clear(String(ctx.chat.id));
|
|
136
|
+
await ctx.reply('Session cleared. Starting fresh.');
|
|
137
|
+
logEvent('command', ctx.chat.id, { command: 'clear' });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
bot.command('health', async (ctx) => {
|
|
141
|
+
if (!checkAuth(ctx)) return;
|
|
142
|
+
const status = {
|
|
143
|
+
sessions: sessions.size,
|
|
144
|
+
authenticated: auth.authenticatedCount,
|
|
145
|
+
fragments: fragmentBuffer.pendingCount,
|
|
146
|
+
};
|
|
147
|
+
await ctx.reply(\`Health: \${JSON.stringify(status, null, 2)}\`);
|
|
148
|
+
logEvent('command', ctx.chat.id, { command: 'health' });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
bot.command('help', async (ctx) => {
|
|
152
|
+
if (!checkAuth(ctx)) return;
|
|
153
|
+
await ctx.reply(
|
|
154
|
+
[
|
|
155
|
+
'<b>Commands:</b>',
|
|
156
|
+
'/start — Initialize',
|
|
157
|
+
'/clear — Clear conversation',
|
|
158
|
+
'/health — System status',
|
|
159
|
+
'/help — This message',
|
|
160
|
+
'/stop — Cancel current task',
|
|
161
|
+
'/restart — Restart the bot',
|
|
162
|
+
].join('\\n'),
|
|
163
|
+
{ parse_mode: 'HTML' },
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
bot.command('stop', async (ctx) => {
|
|
168
|
+
const chatId = String(ctx.chat.id);
|
|
169
|
+
const info = cancellation.cancel(chatId);
|
|
170
|
+
if (info) {
|
|
171
|
+
const ranSec = ((Date.now() - info.startedAt) / 1000).toFixed(1);
|
|
172
|
+
await ctx.reply(\`Task cancelled after \${ranSec}s.\`);
|
|
173
|
+
logEvent('command', ctx.chat.id, { command: 'stop', ranMs: Date.now() - info.startedAt });
|
|
174
|
+
} else {
|
|
175
|
+
await ctx.reply('No task is currently running.');
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
bot.command('restart', async (ctx) => {
|
|
180
|
+
if (!checkAuth(ctx)) return;
|
|
181
|
+
const chatId = String(ctx.chat.id);
|
|
182
|
+
const result = updater.requestRestart(chatId, 'manual');
|
|
183
|
+
if (result.initiated) {
|
|
184
|
+
await ctx.reply('Restarting... I will be back shortly.');
|
|
185
|
+
logEvent('command', ctx.chat.id, { command: 'restart' });
|
|
186
|
+
setTimeout(() => process.exit(RESTART_EXIT_CODE), 1000);
|
|
187
|
+
} else {
|
|
188
|
+
await ctx.reply(\`Restart failed: \${result.error}\`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ─── Photo Messages ────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
bot.on('message:photo', async (ctx) => {
|
|
195
|
+
if (!checkAuth(ctx)) return;
|
|
196
|
+
const chatId = String(ctx.chat.id);
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const photo = ctx.message.photo[ctx.message.photo.length - 1]; // Largest size
|
|
200
|
+
const file = await ctx.api.getFile(photo.file_id);
|
|
201
|
+
if (!file.file_path) {
|
|
202
|
+
await ctx.reply('Could not download photo.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const url = \`https://api.telegram.org/file/bot\${telegramConfig.botToken}/\${file.file_path}\`;
|
|
207
|
+
const resp = await fetch(url);
|
|
208
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
209
|
+
|
|
210
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
211
|
+
await ctx.reply('File too large (max 20MB). Please compress and try again.');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const caption = ctx.message.caption ?? 'What do you see in this image?';
|
|
216
|
+
const fileInfo: FileInfo = {
|
|
217
|
+
name: file.file_path.split('/').pop() ?? 'photo.jpg',
|
|
218
|
+
mimeType: 'image/jpeg',
|
|
219
|
+
size: buffer.length,
|
|
220
|
+
data: buffer,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const intent = detectFileIntent(fileInfo.name, fileInfo.mimeType, caption);
|
|
224
|
+
const content = buildMultimodalContent(fileInfo, intent);
|
|
225
|
+
|
|
226
|
+
// Build multimodal user message for agent
|
|
227
|
+
const multimodalText = content.type === 'text'
|
|
228
|
+
? \`[File: \${fileInfo.name}]\\n\${content.content}\\n\\n\${caption}\`
|
|
229
|
+
: \`[Image: \${fileInfo.name}, \${Math.round(fileInfo.size / 1024)}KB]\\n\${caption}\`;
|
|
230
|
+
|
|
231
|
+
logEvent('photo_in', ctx.chat.id, { size: buffer.length, intent });
|
|
232
|
+
await dispatchToAgent(chatId, multimodalText, ctx);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
235
|
+
await ctx.reply('Failed to process photo.');
|
|
236
|
+
logEvent('error', ctx.chat.id, { error: msg, type: 'photo' });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ─── Document Messages ─────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
bot.on('message:document', async (ctx) => {
|
|
243
|
+
if (!checkAuth(ctx)) return;
|
|
244
|
+
const chatId = String(ctx.chat.id);
|
|
245
|
+
const doc = ctx.message.document;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
if (doc.file_size && doc.file_size > MAX_FILE_SIZE) {
|
|
249
|
+
await ctx.reply('File too large (max 20MB). Please compress and try again.');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const file = await ctx.api.getFile(doc.file_id);
|
|
254
|
+
if (!file.file_path) {
|
|
255
|
+
await ctx.reply('Could not download document.');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const url = \`https://api.telegram.org/file/bot\${telegramConfig.botToken}/\${file.file_path}\`;
|
|
260
|
+
const resp = await fetch(url);
|
|
261
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
262
|
+
|
|
263
|
+
const filename = doc.file_name ?? 'document';
|
|
264
|
+
const mimeType = doc.mime_type ?? 'application/octet-stream';
|
|
265
|
+
const caption = ctx.message.caption ?? '';
|
|
266
|
+
|
|
267
|
+
const intent = detectFileIntent(filename, mimeType, caption);
|
|
268
|
+
const fileInfo: FileInfo = { name: filename, mimeType, size: buffer.length, data: buffer };
|
|
269
|
+
|
|
270
|
+
if (intent === 'intake') {
|
|
271
|
+
saveTempFile(UPLOAD_DIR, filename, buffer);
|
|
272
|
+
await ctx.reply(\`Saved \${filename} for knowledge ingestion.\`);
|
|
273
|
+
logEvent('document_intake', ctx.chat.id, { filename, size: buffer.length });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const content = buildMultimodalContent(fileInfo, intent);
|
|
278
|
+
const userText = content.type === 'text'
|
|
279
|
+
? \`[File: \${filename}]\\n\${content.content}\${caption ? '\\n\\n' + caption : ''}\`
|
|
280
|
+
: \`[Document: \${filename}, \${Math.round(buffer.length / 1024)}KB]\${caption ? '\\n' + caption : ''}\`;
|
|
281
|
+
|
|
282
|
+
logEvent('document_in', ctx.chat.id, { filename, size: buffer.length, intent });
|
|
283
|
+
await dispatchToAgent(chatId, userText, ctx);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
286
|
+
await ctx.reply('Failed to process document.');
|
|
287
|
+
logEvent('error', ctx.chat.id, { error: msg, type: 'document' });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ─── Text Messages ────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
bot.on('message:text', async (ctx) => {
|
|
294
|
+
const text = ctx.message.text;
|
|
295
|
+
const chatId = String(ctx.chat.id);
|
|
296
|
+
const userId = ctx.from?.id;
|
|
297
|
+
|
|
298
|
+
// Auth check — if passphrase is set, check if user is authenticated
|
|
299
|
+
if (auth.enabled && !auth.isAuthenticated(String(userId))) {
|
|
300
|
+
if (telegramConfig.passphrase && text === telegramConfig.passphrase) {
|
|
301
|
+
auth.authenticate(String(userId), text);
|
|
302
|
+
await ctx.reply('Authenticated! How can I help?');
|
|
303
|
+
logEvent('auth', ctx.chat.id, { userId });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await ctx.reply('Please authenticate first.');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
logEvent('message_in', ctx.chat.id, { length: text.length });
|
|
311
|
+
|
|
312
|
+
// Fragment buffering
|
|
313
|
+
const fragment: Fragment = {
|
|
314
|
+
text,
|
|
315
|
+
messageId: ctx.message.message_id,
|
|
316
|
+
receivedAt: Date.now(),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const buffered = fragmentBuffer.receive(chatId, fragment);
|
|
320
|
+
if (buffered) return; // Wait for more fragments
|
|
321
|
+
|
|
322
|
+
// Process immediately
|
|
323
|
+
await dispatchToAgent(chatId, text, ctx);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ─── Error Handling ───────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
bot.catch((err) => {
|
|
329
|
+
console.error('[bot] Error:', err.message);
|
|
330
|
+
logEvent('error', 'global', { error: err.message });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ─── Start ────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
console.log('[${config.id}] Starting Telegram bot...');
|
|
336
|
+
logEvent('bot_start', 'system');
|
|
337
|
+
|
|
338
|
+
// Start notification polling and periodic temp cleanup
|
|
339
|
+
notifications.start();
|
|
340
|
+
setInterval(() => cleanupTempFiles(UPLOAD_DIR, 3_600_000), 3_600_000); // 1h
|
|
341
|
+
|
|
342
|
+
process.on('SIGINT', () => shutdown(bot));
|
|
343
|
+
process.on('SIGTERM', () => shutdown(bot));
|
|
344
|
+
|
|
345
|
+
await bot.start();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ─── Agent Dispatch ──────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
async function dispatchToAgent(
|
|
351
|
+
chatId: string,
|
|
352
|
+
text: string,
|
|
353
|
+
ctx: { reply: (text: string, options?: Record<string, unknown>) => Promise<unknown> },
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
if (processingLocks.has(chatId)) {
|
|
356
|
+
await ctx.reply('I am still working on your previous request. Please wait.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
processingLocks.add(chatId);
|
|
361
|
+
const signal = cancellation.create(chatId, 'Processing message');
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Append user message
|
|
365
|
+
sessions.appendMessage(chatId, {
|
|
366
|
+
role: 'user',
|
|
367
|
+
content: text,
|
|
368
|
+
timestamp: Date.now(),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Run agent with cancellation signal
|
|
372
|
+
const agent = createTelegramAgent(telegramConfig);
|
|
373
|
+
const session = sessions.getOrCreate(chatId);
|
|
374
|
+
const result = await agent.run(session.messages, { signal });
|
|
375
|
+
|
|
376
|
+
// Append agent response
|
|
377
|
+
sessions.appendMessages(chatId, result.newMessages);
|
|
378
|
+
|
|
379
|
+
// Send response in chunks
|
|
380
|
+
const chunks = chunkResponse(result.text, { maxChunkSize: 4000, format: 'html' });
|
|
381
|
+
for (const chunk of chunks) {
|
|
382
|
+
try {
|
|
383
|
+
await ctx.reply(chunk, { parse_mode: 'HTML' });
|
|
384
|
+
} catch {
|
|
385
|
+
// Fallback to plain text if HTML fails
|
|
386
|
+
await ctx.reply(chunk);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
logEvent('response_out', chatId, {
|
|
391
|
+
iterations: result.iterations,
|
|
392
|
+
toolCalls: result.toolCalls,
|
|
393
|
+
chunks: chunks.length,
|
|
394
|
+
});
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (signal.aborted) {
|
|
397
|
+
logEvent('cancelled', chatId);
|
|
398
|
+
return; // Already notified user via /stop handler
|
|
399
|
+
}
|
|
400
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
401
|
+
console.error(\`[agent] Error for chat \${chatId}:\`, msg);
|
|
402
|
+
await ctx.reply('Sorry, I encountered an error. Please try again.');
|
|
403
|
+
logEvent('error', chatId, { error: msg });
|
|
404
|
+
} finally {
|
|
405
|
+
cancellation.complete(chatId);
|
|
406
|
+
processingLocks.delete(chatId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Fragment Handler ────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
function handleMergedMessage(key: string, merged: string): void {
|
|
413
|
+
// key is the chatId — dispatch the merged text
|
|
414
|
+
// Note: we don't have the ctx here, so we use the bot API directly
|
|
415
|
+
// In a real implementation, you'd store the ctx reference
|
|
416
|
+
console.log(\`[fragment] Merged \${merged.length} chars for \${key}\`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ─── Auth Helper ─────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
function checkAuth(ctx: { from?: { id: number }; reply: (text: string) => Promise<unknown> }): boolean {
|
|
422
|
+
if (!auth.enabled) return true;
|
|
423
|
+
if (auth.isAuthenticated(String(ctx.from?.id))) return true;
|
|
424
|
+
ctx.reply('Please authenticate first.');
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─── Shutdown ────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
function shutdown(bot: Bot): void {
|
|
431
|
+
console.log('[${config.id}] Shutting down...');
|
|
432
|
+
logEvent('bot_stop', 'system');
|
|
433
|
+
cancellation.cancelAll();
|
|
434
|
+
notifications.stop();
|
|
435
|
+
cleanupTempFiles(UPLOAD_DIR);
|
|
436
|
+
fragmentBuffer.close();
|
|
437
|
+
sessions.close();
|
|
438
|
+
bot.stop();
|
|
439
|
+
process.exit(0);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ─── Main ────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
startBot().catch((err) => {
|
|
445
|
+
console.error('[${config.id}] Fatal:', err);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
});
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
//# sourceMappingURL=telegram-bot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-bot.js","sourceRoot":"","sources":["../../src/templates/telegram-bot.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,UAAU,mBAAmB,CAAC,MAAmB;IACrD,OAAO;KACJ,MAAM,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uCA+BuB,MAAM,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCA2FX,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA8M9C,MAAM,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgGT,MAAM,CAAC,EAAE;;;;;;;;;;;;;;oBAcP,MAAM,CAAC,EAAE;;;CAG5B,CAAC;AACF,CAAC"}
|