grok-telegram-bot 2.3.1 → 2.5.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 (88) hide show
  1. package/.env.example +64 -2
  2. package/CHANGELOG.md +156 -1
  3. package/README.md +58 -15
  4. package/docs/GROUP.md +225 -0
  5. package/docs/INSTALL.md +3 -0
  6. package/package.json +1 -1
  7. package/src/app/accounts.ts +84 -0
  8. package/src/app/instance-lock.ts +6 -0
  9. package/src/app/lifetime-flag.ts +20 -0
  10. package/src/app/settings-store.ts +47 -8
  11. package/src/app/types.ts +30 -2
  12. package/src/app/updater.ts +38 -6
  13. package/src/app/usage.ts +204 -7
  14. package/src/bot/account-rotator.ts +10 -0
  15. package/src/bot/auth.ts +96 -15
  16. package/src/bot/bot.ts +154 -11
  17. package/src/bot/chat-controller.ts +82 -13
  18. package/src/bot/commands.ts +69 -27
  19. package/src/bot/complexity-gate.ts +69 -0
  20. package/src/bot/deps.ts +22 -0
  21. package/src/bot/group-memory.ts +159 -0
  22. package/src/bot/handlers/accounts.ts +58 -1
  23. package/src/bot/handlers/control.ts +85 -32
  24. package/src/bot/handlers/document.ts +31 -4
  25. package/src/bot/handlers/forum.ts +207 -0
  26. package/src/bot/handlers/import-session.ts +290 -0
  27. package/src/bot/handlers/menu.ts +102 -61
  28. package/src/bot/handlers/message.ts +102 -21
  29. package/src/bot/handlers/photo.ts +123 -16
  30. package/src/bot/handlers/running.ts +172 -16
  31. package/src/bot/handlers/session-card.ts +20 -0
  32. package/src/bot/handlers/sessions.ts +76 -15
  33. package/src/bot/handlers/usage.ts +118 -16
  34. package/src/bot/handlers/voice.ts +52 -7
  35. package/src/bot/image-return.ts +8 -5
  36. package/src/bot/menu/ephemeral.ts +13 -3
  37. package/src/bot/menu/keyboard.ts +54 -14
  38. package/src/bot/menu/refresh.ts +3 -1
  39. package/src/bot/menu/status-panel.ts +25 -6
  40. package/src/bot/permission-service.ts +19 -0
  41. package/src/bot/prompt-anchor.ts +300 -0
  42. package/src/bot/prompt-content.ts +7 -0
  43. package/src/bot/registry.ts +94 -1
  44. package/src/bot/scope.ts +94 -0
  45. package/src/bot/session-fork.ts +11 -0
  46. package/src/bot/session-runtime.ts +1254 -83
  47. package/src/bot/suggestions.ts +489 -0
  48. package/src/bot/telegram-actions.ts +440 -0
  49. package/src/bot/telegram-bots.ts +495 -0
  50. package/src/bot/telegram-io.ts +94 -10
  51. package/src/cli.ts +2 -0
  52. package/src/config.ts +242 -2
  53. package/src/forum/bind-path.ts +146 -0
  54. package/src/forum/manager.ts +651 -0
  55. package/src/forum/project-icon.ts +142 -0
  56. package/src/forum/thread.ts +16 -0
  57. package/src/forum/topic-store.ts +114 -0
  58. package/src/forum/types.ts +29 -0
  59. package/src/grok/client.ts +214 -37
  60. package/src/grok/plan-approval.ts +72 -0
  61. package/src/grok/session-log.ts +16 -0
  62. package/src/grok/types.ts +21 -2
  63. package/src/import/build-import.ts +132 -0
  64. package/src/import/history-readers.ts +681 -0
  65. package/src/import/list-running.ts +100 -0
  66. package/src/import/sources.ts +78 -0
  67. package/src/index.ts +315 -30
  68. package/src/projects/manager.ts +16 -3
  69. package/src/render/chunk.ts +17 -10
  70. package/src/render/diff.ts +11 -2
  71. package/src/render/file-summary.ts +31 -1
  72. package/src/render/hashtags.ts +5 -1
  73. package/src/render/markdown.ts +293 -35
  74. package/src/render/plan.ts +127 -0
  75. package/src/render/session-comment.ts +318 -0
  76. package/src/render/telegram-bridge.ts +360 -0
  77. package/src/render/tool-call-detail.ts +400 -19
  78. package/src/render/tool-call-merge.ts +115 -0
  79. package/src/render/tool-call.ts +444 -162
  80. package/src/render/truncate.ts +85 -0
  81. package/src/service/platform.ts +44 -7
  82. package/src/service/windows.ts +30 -6
  83. package/src/sessions/history.ts +98 -0
  84. package/src/sessions/process.ts +7 -0
  85. package/src/sessions/store.ts +3 -0
  86. package/src/sessions/types.ts +5 -0
  87. package/src/stream/streamer.ts +90 -15
  88. package/src/tasks/runner.ts +4 -3
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Execute agent-requested Telegram bridge actions and produce results +
3
+ * user-facing status lines.
4
+ */
5
+ import type { Api } from "grammy";
6
+ import type { AppConfig } from "../config.js";
7
+ import type { ForumManager } from "../forum/manager.js";
8
+ import type { ForumTopicBinding } from "../forum/types.js";
9
+ import { FORUM_GENERAL_THREAD_ID } from "../forum/thread.js";
10
+ import type { SessionStore } from "../sessions/store.js";
11
+ import { createLogger } from "../logger.js";
12
+ import { searchGroupMemory } from "./group-memory.js";
13
+ import type { TelegramBotService } from "./telegram-bots.js";
14
+ import type { TelegramAction } from "../render/telegram-bridge.js";
15
+
16
+ const log = createLogger("telegram-actions");
17
+
18
+ /** Cross-topic prompt injection (implemented by bot.ts via registry). */
19
+ export type SubmitTopicPromptFn = (opts: {
20
+ threadId: number;
21
+ cwd: string;
22
+ projectName: string;
23
+ prompt: string;
24
+ newSession?: boolean;
25
+ }) => Promise<{ outcome: "ran" | "queued"; sessionId?: string }>;
26
+
27
+ export interface TelegramActionContext {
28
+ api: Api;
29
+ cfg: AppConfig;
30
+ chatId: number;
31
+ messageThreadId?: number;
32
+ forum?: ForumManager;
33
+ store: SessionStore;
34
+ bots: TelegramBotService;
35
+ /** Dispatch a prompt into another forum topic's session. */
36
+ submitTopicPrompt?: SubmitTopicPromptFn;
37
+ }
38
+
39
+ export interface TelegramActionResult {
40
+ action: string;
41
+ ok: boolean;
42
+ /** Machine-readable payload for the agent. */
43
+ data?: unknown;
44
+ error?: string;
45
+ /** Short line to show the user (optional). */
46
+ userNote?: string;
47
+ }
48
+
49
+ /** Run actions sequentially; bot_command may await a sibling bot. */
50
+ export async function executeTelegramActions(
51
+ actions: TelegramAction[],
52
+ ctx: TelegramActionContext,
53
+ ): Promise<TelegramActionResult[]> {
54
+ const results: TelegramActionResult[] = [];
55
+ for (const action of actions) {
56
+ try {
57
+ results.push(await runOne(action, ctx));
58
+ } catch (e) {
59
+ const msg = (e as Error).message ?? String(e);
60
+ log.warn(`action ${action.action} failed: ${msg}`);
61
+ results.push({ action: action.action, ok: false, error: msg });
62
+ }
63
+ }
64
+ return results;
65
+ }
66
+
67
+ async function runOne(
68
+ action: TelegramAction,
69
+ ctx: TelegramActionContext,
70
+ ): Promise<TelegramActionResult> {
71
+ switch (action.action) {
72
+ case "create_topic":
73
+ return createTopic(action, ctx);
74
+ case "set_path":
75
+ return setPath(action, ctx);
76
+ case "send_prompt":
77
+ return sendPrompt(action, ctx);
78
+ case "search_memory":
79
+ return searchMemory(action, ctx);
80
+ case "list_bots":
81
+ return listBots(ctx);
82
+ case "bot_command":
83
+ return botCommand(action, ctx);
84
+ default:
85
+ return { action: "unknown", ok: false, error: "unsupported action" };
86
+ }
87
+ }
88
+
89
+ async function createTopic(
90
+ action: Extract<TelegramAction, { action: "create_topic" }>,
91
+ ctx: TelegramActionContext,
92
+ ): Promise<TelegramActionResult> {
93
+ const forum = ctx.forum;
94
+ if (!forum?.isReady) {
95
+ return {
96
+ action: "create_topic",
97
+ ok: false,
98
+ error:
99
+ "Forum topics not available (TOPIC_GROUP_ID unset, bot not admin, or Topics disabled)",
100
+ };
101
+ }
102
+ const created = await forum.createBoundTopic(action.name, action.path);
103
+ if (!created.ok) {
104
+ return { action: "create_topic", ok: false, error: created.error };
105
+ }
106
+ const b = created.binding;
107
+ return {
108
+ action: "create_topic",
109
+ ok: true,
110
+ data: {
111
+ threadId: b.threadId,
112
+ name: b.name,
113
+ projectPath: b.projectPath,
114
+ kind: b.kind,
115
+ },
116
+ userNote: b.projectPath
117
+ ? `\u{1F4CC} Created topic **${b.name}** (#${b.threadId}) \u2192 \`${b.projectPath}\``
118
+ : `\u{1F4CC} Created topic **${b.name}** (#${b.threadId}) (unbound — use set_path)`,
119
+ };
120
+ }
121
+
122
+ function setPath(
123
+ action: Extract<TelegramAction, { action: "set_path" }>,
124
+ ctx: TelegramActionContext,
125
+ ): TelegramActionResult {
126
+ const forum = ctx.forum;
127
+ if (!forum?.isReady) {
128
+ return {
129
+ action: "set_path",
130
+ ok: false,
131
+ error: "Forum topics not available",
132
+ };
133
+ }
134
+ const resolved = resolveTopicRef(forum, action.topic, ctx.cfg);
135
+ if (!resolved.ok) {
136
+ return { action: "set_path", ok: false, error: resolved.error };
137
+ }
138
+ const { binding } = resolved;
139
+ if (binding.threadId === FORUM_GENERAL_THREAD_ID) {
140
+ return {
141
+ action: "set_path",
142
+ ok: false,
143
+ error: "Cannot rebind the General topic (always GROK_WORKSPACE)",
144
+ };
145
+ }
146
+ if (binding.kind === "ai_chat") {
147
+ return {
148
+ action: "set_path",
149
+ ok: false,
150
+ error: "Cannot rebind the AI Chat topic (always GROK_WORKSPACE)",
151
+ };
152
+ }
153
+
154
+ // Agent set_path: create missing absolute folders (new project flow).
155
+ const result = forum.tryBindPath(binding.threadId, action.path, { createIfMissing: true });
156
+ if (!result.ok) {
157
+ return { action: "set_path", ok: false, error: result.error };
158
+ }
159
+ const b = result.binding;
160
+ const createdNote = result.created ? " (folder created)" : "";
161
+ return {
162
+ action: "set_path",
163
+ ok: true,
164
+ data: {
165
+ threadId: b.threadId,
166
+ name: b.name,
167
+ projectPath: b.projectPath,
168
+ kind: b.kind,
169
+ created: !!result.created,
170
+ },
171
+ userNote: `\u{1F4C1} Topic **${b.name}** (#${b.threadId}) \u2192 \`${b.projectPath}\`${createdNote}`,
172
+ };
173
+ }
174
+
175
+ async function sendPrompt(
176
+ action: Extract<TelegramAction, { action: "send_prompt" }>,
177
+ ctx: TelegramActionContext,
178
+ ): Promise<TelegramActionResult> {
179
+ const forum = ctx.forum;
180
+ if (!forum?.isReady) {
181
+ return {
182
+ action: "send_prompt",
183
+ ok: false,
184
+ error: "Forum topics not available",
185
+ };
186
+ }
187
+ if (!ctx.submitTopicPrompt) {
188
+ return {
189
+ action: "send_prompt",
190
+ ok: false,
191
+ error: "Cross-topic prompt dispatch is not wired",
192
+ };
193
+ }
194
+
195
+ const resolved = resolveTopicRef(forum, action.topic, ctx.cfg);
196
+ if (!resolved.ok) {
197
+ return { action: "send_prompt", ok: false, error: resolved.error };
198
+ }
199
+ const { binding } = resolved;
200
+ const cwd = binding.projectPath;
201
+ if (!cwd) {
202
+ return {
203
+ action: "send_prompt",
204
+ ok: false,
205
+ error: `Topic **${binding.name}** (#${binding.threadId}) has no project path — use set_path or create_topic with path first`,
206
+ };
207
+ }
208
+
209
+ const projectName =
210
+ binding.kind === "ai_chat"
211
+ ? ctx.cfg.topicAiChatName
212
+ : binding.kind === "general"
213
+ ? "General"
214
+ : binding.name;
215
+
216
+ // Visible injection in the *target* topic so humans see the orchestrated prompt.
217
+ // Plain text (no Markdown) so agent prompts with * _ ` cannot break the send.
218
+ const preview =
219
+ action.prompt.length > 1500 ? action.prompt.slice(0, 1499) + "\u2026" : action.prompt;
220
+ await ctx.api
221
+ .sendMessage(
222
+ forum.groupId,
223
+ `\u{1F4E8} Prompt from bridge${action.newSession ? " (new session)" : ""}\n\n${preview}`,
224
+ { message_thread_id: binding.threadId },
225
+ )
226
+ .catch((e) => {
227
+ log.debug(`send_prompt announce failed: ${(e as Error).message}`);
228
+ });
229
+
230
+ try {
231
+ const res = await ctx.submitTopicPrompt({
232
+ threadId: binding.threadId,
233
+ cwd,
234
+ projectName,
235
+ prompt: action.prompt,
236
+ newSession: action.newSession,
237
+ });
238
+ return {
239
+ action: "send_prompt",
240
+ ok: true,
241
+ data: {
242
+ threadId: binding.threadId,
243
+ name: binding.name,
244
+ projectPath: cwd,
245
+ outcome: res.outcome,
246
+ sessionId: res.sessionId,
247
+ newSession: !!action.newSession,
248
+ promptPreview: action.prompt.slice(0, 200),
249
+ },
250
+ userNote: `\u{1F4E8} Prompt ${res.outcome} in **${binding.name}** (#${binding.threadId})`,
251
+ };
252
+ } catch (e) {
253
+ return {
254
+ action: "send_prompt",
255
+ ok: false,
256
+ error: (e as Error).message ?? String(e),
257
+ data: { threadId: binding.threadId, name: binding.name, projectPath: cwd },
258
+ };
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Resolve a topic ref: numeric / #id, "general", "ai chat", or exact title.
264
+ */
265
+ export function resolveTopicRef(
266
+ forum: ForumManager,
267
+ ref: string,
268
+ cfg: AppConfig,
269
+ ): { ok: true; binding: ForumTopicBinding } | { ok: false; error: string } {
270
+ const raw = ref.trim();
271
+ if (!raw) return { ok: false, error: "Empty topic reference" };
272
+
273
+ // #123 or plain digits
274
+ const idMatch = /^#?(\d+)$/.exec(raw);
275
+ if (idMatch) {
276
+ const threadId = Number(idMatch[1]);
277
+ const b = forum.store.get(threadId);
278
+ if (!b) {
279
+ // General (1) may not be in store yet — synthesize workspace bind.
280
+ if (threadId === FORUM_GENERAL_THREAD_ID) {
281
+ const bound = forum.store.bindProject(threadId, cfg.workspace, "General", "general");
282
+ return { ok: true, binding: bound };
283
+ }
284
+ return { ok: false, error: `No topic mapped for thread #${threadId}` };
285
+ }
286
+ return { ok: true, binding: b };
287
+ }
288
+
289
+ const key = raw.toLowerCase();
290
+ if (key === "general" || key === "general topic") {
291
+ let b = forum.store.get(FORUM_GENERAL_THREAD_ID);
292
+ if (!b?.projectPath) {
293
+ b = forum.store.bindProject(FORUM_GENERAL_THREAD_ID, cfg.workspace, "General", "general");
294
+ }
295
+ return { ok: true, binding: b };
296
+ }
297
+
298
+ const aiName = (cfg.topicAiChatName || "AI Chat").toLowerCase();
299
+ if (key === "ai chat" || key === "ai_chat" || key === aiName) {
300
+ const ai = forum.store.findAiChat();
301
+ if (ai) return { ok: true, binding: ai };
302
+ // Fallback: ensure workspace topic by name match
303
+ const byName = forum.store.all().find((t) => t.name.toLowerCase() === aiName);
304
+ if (byName) return { ok: true, binding: byName };
305
+ return { ok: false, error: "AI Chat topic not found — run /forum_setup" };
306
+ }
307
+
308
+ // Exact title match (case-insensitive)
309
+ const hits = forum.store.all().filter((t) => t.name.toLowerCase() === key);
310
+ if (hits.length === 1) return { ok: true, binding: hits[0]! };
311
+ if (hits.length > 1) {
312
+ return {
313
+ ok: false,
314
+ error: `Multiple topics named "${raw}" (${hits.map((h) => "#" + h.threadId).join(", ")}). Use #threadId.`,
315
+ };
316
+ }
317
+ return {
318
+ ok: false,
319
+ error: `Topic not found: "${raw}". Use exact title, #threadId, "general", or "ai chat".`,
320
+ };
321
+ }
322
+
323
+ function searchMemory(
324
+ action: Extract<TelegramAction, { action: "search_memory" }>,
325
+ ctx: TelegramActionContext,
326
+ ): TelegramActionResult {
327
+ const topics = ctx.forum?.isReady ? ctx.forum.store.all() : undefined;
328
+ const hits = searchGroupMemory({
329
+ query: action.query,
330
+ limit: action.limit,
331
+ sessionsDir: ctx.cfg.sessionsDir,
332
+ store: ctx.store,
333
+ topics,
334
+ });
335
+ return {
336
+ action: "search_memory",
337
+ ok: true,
338
+ data: { query: action.query, hits },
339
+ userNote:
340
+ hits.length > 0
341
+ ? `\u{1F50D} Memory search: ${hits.length} hit(s) for \u201c${action.query.slice(0, 60)}\u201d`
342
+ : `\u{1F50D} Memory search: no hits for \u201c${action.query.slice(0, 60)}\u201d`,
343
+ };
344
+ }
345
+
346
+ async function listBots(ctx: TelegramActionContext): Promise<TelegramActionResult> {
347
+ if (ctx.cfg.allowedTelegramBots.length === 0) {
348
+ return {
349
+ action: "list_bots",
350
+ ok: true,
351
+ data: { bots: [], note: "ALLOWED_TELEGRAM_BOTS is empty" },
352
+ userNote: "\u{1F916} No sibling bots configured (ALLOWED_TELEGRAM_BOTS)",
353
+ };
354
+ }
355
+ const bots = await ctx.bots.listBots(true);
356
+ const catalog = Object.fromEntries(
357
+ bots.map((b) => [
358
+ b.username,
359
+ b.commands.map((c) =>
360
+ c.description ? `/${c.command} — ${c.description}` : `/${c.command}`,
361
+ ),
362
+ ]),
363
+ );
364
+ return {
365
+ action: "list_bots",
366
+ ok: true,
367
+ data: {
368
+ bots,
369
+ commands: catalog,
370
+ usage:
371
+ "Call bot_command with bot=username (no @), command without leading slash, optional args. " +
372
+ "Treat replies like MCP tool results. Unknown/timeout commands return ok=false — continue the session; do not treat them as Done.",
373
+ },
374
+ userNote: `\u{1F916} Sibling bots: ${bots
375
+ .map((b) => {
376
+ const cmds = b.commands.length
377
+ ? ` (${b.commands.map((c) => "/" + c.command).join(", ")})`
378
+ : "";
379
+ return "@" + b.username + cmds;
380
+ })
381
+ .join(", ") || "(none)"}`,
382
+ };
383
+ }
384
+
385
+ async function botCommand(
386
+ action: Extract<TelegramAction, { action: "bot_command" }>,
387
+ ctx: TelegramActionContext,
388
+ ): Promise<TelegramActionResult> {
389
+ // Prefer the configured forum group so sibling bots (group-scoped) see the command.
390
+ const chatId = ctx.cfg.topicGroupId ?? ctx.chatId;
391
+ const messageThreadId =
392
+ ctx.cfg.topicGroupId !== undefined && chatId === ctx.cfg.topicGroupId
393
+ ? ctx.messageThreadId
394
+ : undefined;
395
+
396
+ const res = await ctx.bots.invokeCommand({
397
+ bot: action.bot,
398
+ command: action.command,
399
+ args: action.args,
400
+ chatId,
401
+ messageThreadId,
402
+ });
403
+
404
+ if (!res.ok) {
405
+ // Timeout / dead bot / bad command: session continues via bridge results.
406
+ // Never surface as a successful Done.
407
+ const kind = res.kind;
408
+ const icon = kind === "timeout" ? "\u23F1\uFE0F" : "\u26A0\uFE0F";
409
+ return {
410
+ action: "bot_command",
411
+ ok: false,
412
+ error: res.error,
413
+ data: {
414
+ bot: action.bot,
415
+ command: action.command,
416
+ args: action.args,
417
+ kind,
418
+ sessionContinues: true,
419
+ partialReply: res.partialReply,
420
+ },
421
+ userNote: `${icon} Waiting for @${action.bot} /${action.command} failed: ${res.error} (session continues)`,
422
+ };
423
+ }
424
+
425
+ const partialNote = res.partial
426
+ ? " \u2014 partial (hard timeout; stream may be incomplete)"
427
+ : "";
428
+ return {
429
+ action: "bot_command",
430
+ ok: true,
431
+ data: {
432
+ bot: action.bot,
433
+ command: action.command,
434
+ args: action.args,
435
+ reply: res.reply,
436
+ partial: res.partial,
437
+ },
438
+ userNote: `\u{1F916} @${action.bot} /${action.command} \u2014 reply ready (${res.reply.length} chars)${partialNote}`,
439
+ };
440
+ }