@soleri/forge 5.11.0 → 5.12.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.
Files changed (53) hide show
  1. package/dist/facades/forge.facade.js +3 -3
  2. package/dist/facades/forge.facade.js.map +1 -1
  3. package/dist/lib.d.ts +2 -2
  4. package/dist/lib.js +1 -1
  5. package/dist/lib.js.map +1 -1
  6. package/dist/scaffolder.js +137 -20
  7. package/dist/scaffolder.js.map +1 -1
  8. package/dist/templates/agents-md.d.ts +5 -0
  9. package/dist/templates/agents-md.js +33 -0
  10. package/dist/templates/agents-md.js.map +1 -0
  11. package/dist/templates/entry-point.js +15 -2
  12. package/dist/templates/entry-point.js.map +1 -1
  13. package/dist/templates/package-json.js +7 -0
  14. package/dist/templates/package-json.js.map +1 -1
  15. package/dist/templates/readme.js +80 -27
  16. package/dist/templates/readme.js.map +1 -1
  17. package/dist/templates/setup-script.d.ts +1 -1
  18. package/dist/templates/setup-script.js +135 -53
  19. package/dist/templates/setup-script.js.map +1 -1
  20. package/dist/templates/skills.d.ts +0 -7
  21. package/dist/templates/skills.js +0 -21
  22. package/dist/templates/skills.js.map +1 -1
  23. package/dist/templates/telegram-agent.d.ts +6 -0
  24. package/dist/templates/telegram-agent.js +212 -0
  25. package/dist/templates/telegram-agent.js.map +1 -0
  26. package/dist/templates/telegram-bot.d.ts +6 -0
  27. package/dist/templates/telegram-bot.js +450 -0
  28. package/dist/templates/telegram-bot.js.map +1 -0
  29. package/dist/templates/telegram-config.d.ts +6 -0
  30. package/dist/templates/telegram-config.js +81 -0
  31. package/dist/templates/telegram-config.js.map +1 -0
  32. package/dist/templates/telegram-supervisor.d.ts +6 -0
  33. package/dist/templates/telegram-supervisor.js +148 -0
  34. package/dist/templates/telegram-supervisor.js.map +1 -0
  35. package/dist/types.d.ts +13 -5
  36. package/dist/types.js +7 -1
  37. package/dist/types.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/__tests__/scaffolder.test.ts +62 -0
  40. package/src/facades/forge.facade.ts +3 -3
  41. package/src/lib.ts +2 -1
  42. package/src/scaffolder.ts +170 -28
  43. package/src/templates/agents-md.ts +35 -0
  44. package/src/templates/entry-point.ts +15 -2
  45. package/src/templates/package-json.ts +7 -0
  46. package/src/templates/readme.ts +89 -27
  47. package/src/templates/setup-script.ts +141 -54
  48. package/src/templates/skills.ts +0 -23
  49. package/src/templates/telegram-agent.ts +214 -0
  50. package/src/templates/telegram-bot.ts +452 -0
  51. package/src/templates/telegram-config.ts +83 -0
  52. package/src/templates/telegram-supervisor.ts +150 -0
  53. package/src/types.ts +9 -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,6 @@
1
+ /**
2
+ * Template: Telegram bot entry point with Grammy.
3
+ * Generated by @soleri/forge for agents with Telegram transport.
4
+ */
5
+ import type { AgentConfig } from '../types.js';
6
+ export declare function generateTelegramBot(config: AgentConfig): string;
@@ -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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Template: Telegram configuration module.
3
+ * Generated by @soleri/forge for agents with Telegram transport.
4
+ */
5
+ import type { AgentConfig } from '../types.js';
6
+ export declare function generateTelegramConfig(config: AgentConfig): string;