grok-telegram-bot 2.4.0 → 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 (78) hide show
  1. package/.env.example +38 -2
  2. package/CHANGELOG.md +119 -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/lifetime-flag.ts +20 -0
  8. package/src/app/settings-store.ts +47 -8
  9. package/src/app/types.ts +12 -1
  10. package/src/app/updater.ts +24 -3
  11. package/src/bot/auth.ts +96 -15
  12. package/src/bot/bot.ts +122 -15
  13. package/src/bot/chat-controller.ts +52 -18
  14. package/src/bot/commands.ts +69 -29
  15. package/src/bot/deps.ts +3 -0
  16. package/src/bot/group-memory.ts +159 -0
  17. package/src/bot/handlers/accounts.ts +7 -0
  18. package/src/bot/handlers/control.ts +85 -32
  19. package/src/bot/handlers/document.ts +31 -4
  20. package/src/bot/handlers/forum.ts +207 -0
  21. package/src/bot/handlers/menu.ts +86 -24
  22. package/src/bot/handlers/message.ts +101 -21
  23. package/src/bot/handlers/photo.ts +123 -16
  24. package/src/bot/handlers/running.ts +150 -24
  25. package/src/bot/handlers/session-card.ts +13 -5
  26. package/src/bot/handlers/sessions.ts +68 -18
  27. package/src/bot/handlers/voice.ts +52 -7
  28. package/src/bot/image-return.ts +8 -5
  29. package/src/bot/menu/ephemeral.ts +13 -3
  30. package/src/bot/menu/keyboard.ts +53 -14
  31. package/src/bot/menu/refresh.ts +3 -1
  32. package/src/bot/menu/status-panel.ts +12 -6
  33. package/src/bot/permission-service.ts +19 -0
  34. package/src/bot/prompt-anchor.ts +300 -0
  35. package/src/bot/prompt-content.ts +3 -0
  36. package/src/bot/registry.ts +94 -1
  37. package/src/bot/scope.ts +94 -0
  38. package/src/bot/session-runtime.ts +647 -158
  39. package/src/bot/suggestions.ts +91 -31
  40. package/src/bot/telegram-actions.ts +440 -0
  41. package/src/bot/telegram-bots.ts +495 -0
  42. package/src/bot/telegram-io.ts +94 -10
  43. package/src/cli.ts +2 -0
  44. package/src/config.ts +201 -2
  45. package/src/forum/bind-path.ts +146 -0
  46. package/src/forum/manager.ts +651 -0
  47. package/src/forum/project-icon.ts +142 -0
  48. package/src/forum/thread.ts +16 -0
  49. package/src/forum/topic-store.ts +114 -0
  50. package/src/forum/types.ts +29 -0
  51. package/src/grok/client.ts +130 -28
  52. package/src/index.ts +205 -75
  53. package/src/projects/manager.ts +16 -3
  54. package/src/render/chunk.ts +17 -10
  55. package/src/render/hashtags.ts +5 -1
  56. package/src/render/session-comment.ts +64 -7
  57. package/src/render/telegram-bridge.ts +360 -0
  58. package/src/render/tool-call.ts +56 -37
  59. package/src/service/platform.ts +44 -7
  60. package/src/service/windows.ts +16 -4
  61. package/src/sessions/history.ts +50 -9
  62. package/src/sessions/process.ts +7 -0
  63. package/src/sessions/types.ts +2 -2
  64. package/src/stream/streamer.ts +17 -6
  65. package/scripts/analyze-jsonl.ts +0 -33
  66. package/scripts/delayed-restart.ps1 +0 -29
  67. package/scripts/probe-exit-response-shape.py +0 -77
  68. package/scripts/probe-plan-exit.py +0 -60
  69. package/scripts/probe-plan-exit2.py +0 -48
  70. package/scripts/probe-plan-fields.py +0 -41
  71. package/scripts/probe-plan-fields2.py +0 -58
  72. package/scripts/probe-plan-response-path.py +0 -48
  73. package/scripts/sample-claude-tooluse.ts +0 -21
  74. package/scripts/sample-kiro-events.ts +0 -31
  75. package/scripts/smoke-exit-plan.ts +0 -274
  76. package/scripts/smoke-exit-shapes.ts +0 -252
  77. package/scripts/smoke-import.mjs +0 -82
  78. package/scripts/smoke-import.ts +0 -73
@@ -18,7 +18,7 @@ import type { AppConfig } from "../config.js";
18
18
  import { subagentSummary } from "../render/subagent.js";
19
19
  import type { SessionStore } from "../sessions/store.js";
20
20
  import type { AccountRotator } from "./account-rotator.js";
21
- import { ChatController } from "./chat-controller.js";
21
+ import { ChatController, type ChatBridgeServices } from "./chat-controller.js";
22
22
  import type { SessionRuntime } from "./session-runtime.js";
23
23
 
24
24
  export interface SessionDescription {
@@ -34,8 +34,14 @@ export interface SessionDescription {
34
34
 
35
35
  export class RuntimeRegistry {
36
36
  private readonly controllers = new Map<number, ChatController>();
37
+ /**
38
+ * Forum topic controllers keyed by `chatId:threadId`.
39
+ * Each topic has its own multi-session controller + settings (model/reasoning).
40
+ */
41
+ private readonly forumControllers = new Map<string, ChatController>();
37
42
  private refresher: ((chatId: number) => void) | undefined;
38
43
  private rotator: AccountRotator | undefined;
44
+ private bridge: ChatBridgeServices | undefined;
39
45
  /** Chat ids with a running turn, most-recently-started last. */
40
46
  private readonly activeChats: number[] = [];
41
47
  /** Subagent sessionId -> owner chat id. */
@@ -60,6 +66,13 @@ export class RuntimeRegistry {
60
66
  this.rotator = rotator;
61
67
  }
62
68
 
69
+ /** Telegram bridge services (forum, session store, sibling bots). */
70
+ setBridge(bridge: ChatBridgeServices): void {
71
+ this.bridge = bridge;
72
+ for (const c of this.controllers.values()) c.bridge = bridge;
73
+ for (const c of this.forumControllers.values()) c.bridge = bridge;
74
+ }
75
+
63
76
  controller(chatId: number): ChatController {
64
77
  let c = this.controllers.get(chatId);
65
78
  if (!c) {
@@ -74,6 +87,7 @@ export class RuntimeRegistry {
74
87
  (busy) => this.noteActivity(chatId, busy),
75
88
  () => this.rotator,
76
89
  );
90
+ c.bridge = this.bridge;
77
91
  this.controllers.set(chatId, c);
78
92
  }
79
93
  return c;
@@ -84,9 +98,78 @@ export class RuntimeRegistry {
84
98
  return this.controller(chatId).foreground();
85
99
  }
86
100
 
101
+ /**
102
+ * Multi-session controller for a forum topic (fixed project path).
103
+ * Settings key: `{chatId}:t{threadId}` — own model / reasoning / sessions.
104
+ */
105
+ forumController(
106
+ chatId: number,
107
+ threadId: number,
108
+ cwd: string,
109
+ projectName?: string,
110
+ ): ChatController {
111
+ const key = `${chatId}:${threadId}`;
112
+ let c = this.forumControllers.get(key);
113
+ if (c && c.fixedCwd && normPath(c.fixedCwd) !== normPath(cwd)) {
114
+ // Path re-bind — dispose and recreate.
115
+ c.dispose();
116
+ this.forumControllers.delete(key);
117
+ c = undefined;
118
+ }
119
+ if (!c) {
120
+ c = new ChatController(
121
+ this.api,
122
+ chatId,
123
+ this.acp,
124
+ this.cfg,
125
+ this.settings,
126
+ this.store,
127
+ (id) => this.refresher?.(id),
128
+ (busy) => this.noteActivity(chatId, busy),
129
+ () => this.rotator,
130
+ {
131
+ messageThreadId: threadId,
132
+ settingsKey: `${chatId}:t${threadId}`,
133
+ fixedCwd: cwd,
134
+ fixedProjectName: projectName,
135
+ },
136
+ );
137
+ c.bridge = this.bridge;
138
+ this.forumControllers.set(key, c);
139
+ }
140
+ return c;
141
+ }
142
+
143
+ /**
144
+ * Foreground runtime for a forum topic. Creates the topic controller lazily.
145
+ */
146
+ getForumTopic(
147
+ chatId: number,
148
+ threadId: number,
149
+ cwd: string,
150
+ projectName?: string,
151
+ ): SessionRuntime {
152
+ return this.forumController(chatId, threadId, cwd, projectName).foreground();
153
+ }
154
+
155
+ /** All forum topic controllers (for bidirectional session listing). */
156
+ allForumControllers(): ChatController[] {
157
+ return [...this.forumControllers.values()];
158
+ }
159
+
160
+ /** Forum controller that currently owns a session id, if any. */
161
+ forumControllerForSession(sessionId: string): ChatController | undefined {
162
+ for (const c of this.forumControllers.values()) {
163
+ if (c.findBySession(sessionId)) return c;
164
+ }
165
+ return undefined;
166
+ }
167
+
87
168
  disposeAll(): void {
88
169
  for (const c of this.controllers.values()) c.dispose();
89
170
  this.controllers.clear();
171
+ for (const c of this.forumControllers.values()) c.dispose();
172
+ this.forumControllers.clear();
90
173
  }
91
174
 
92
175
  /** Find the chat that currently controls a given session id. */
@@ -94,6 +177,12 @@ export class RuntimeRegistry {
94
177
  for (const [chatId, c] of this.controllers) {
95
178
  if (c.findBySession(sessionId)) return chatId;
96
179
  }
180
+ for (const [key, c] of this.forumControllers) {
181
+ if (c.findBySession(sessionId)) {
182
+ const chatId = Number(key.split(":")[0]);
183
+ return Number.isFinite(chatId) ? chatId : undefined;
184
+ }
185
+ }
97
186
  return undefined;
98
187
  }
99
188
 
@@ -184,3 +273,7 @@ export class RuntimeRegistry {
184
273
  }
185
274
  }
186
275
  }
276
+
277
+ function normPath(p: string): string {
278
+ return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
279
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Resolve private-chat vs forum-topic scope for handlers (menu, running, sessions).
3
+ * Forum topics use a dedicated ChatController with per-topic settings + sessions.
4
+ */
5
+ import type { Context } from "grammy";
6
+ import type { BotDeps } from "./deps.js";
7
+ import type { ChatController } from "./chat-controller.js";
8
+ import type { SessionRuntime } from "./session-runtime.js";
9
+ import { forumThreadId } from "../forum/thread.js";
10
+ import { FORUM_GENERAL_THREAD_ID } from "../forum/thread.js";
11
+
12
+ export interface HandlerScope {
13
+ chatId: number;
14
+ /** Forum message_thread_id when in the configured topic group. */
15
+ threadId?: number;
16
+ isForum: boolean;
17
+ /** Settings key for this scope (chat or chat:t{thread}). */
18
+ settingsKey: string;
19
+ controller: ChatController;
20
+ /** Foreground runtime for this scope. */
21
+ rt: SessionRuntime;
22
+ /** Extra fields so replies land in the same topic. */
23
+ threadExtra: { message_thread_id?: number };
24
+ /** Project path when forum topic is bound. */
25
+ projectPath?: string;
26
+ projectName?: string;
27
+ }
28
+
29
+ /** Build settings storage key. */
30
+ export function settingsKeyFor(chatId: number, threadId?: number): string {
31
+ if (threadId === undefined) return String(chatId);
32
+ return `${chatId}:t${threadId}`;
33
+ }
34
+
35
+ /** Extract thread id from message or callback message. */
36
+ export function threadIdFromContext(ctx: Context): number | undefined {
37
+ const msg = ctx.message ?? ctx.callbackQuery?.message;
38
+ if (!msg || !("message_thread_id" in msg)) return undefined;
39
+ const tid = (msg as { message_thread_id?: number }).message_thread_id;
40
+ return typeof tid === "number" ? tid : undefined;
41
+ }
42
+
43
+ /**
44
+ * Resolve handler scope. For the forum group, ensures topic binding (General →
45
+ * workspace) and returns the topic's ChatController + foreground runtime.
46
+ */
47
+ export function resolveScope(ctx: Context, deps: BotDeps): HandlerScope {
48
+ const chatId = ctx.chat!.id;
49
+ const rawThread = threadIdFromContext(ctx);
50
+ const isForum = Boolean(deps.forum?.isActiveForumChat(chatId));
51
+ const threadExtra =
52
+ isForum || rawThread !== undefined
53
+ ? { message_thread_id: isForum ? forumThreadId(rawThread) : rawThread }
54
+ : {};
55
+
56
+ if (!isForum || !deps.forum) {
57
+ const controller = deps.registry.controller(chatId);
58
+ return {
59
+ chatId,
60
+ isForum: false,
61
+ settingsKey: settingsKeyFor(chatId),
62
+ controller,
63
+ rt: controller.foreground(),
64
+ threadExtra: rawThread !== undefined ? { message_thread_id: rawThread } : {},
65
+ };
66
+ }
67
+
68
+ const tid = forumThreadId(rawThread);
69
+ // Ensure General / AI paths are bound before opening menus.
70
+ if (tid === FORUM_GENERAL_THREAD_ID) {
71
+ if (!deps.forum.store.get(tid)?.projectPath) {
72
+ deps.forum.store.bindProject(tid, deps.cfg.workspace, "General", "general");
73
+ }
74
+ }
75
+ const resolved = deps.forum.resolveCwd(tid);
76
+ const cwd = resolved?.cwd ?? deps.cfg.workspace;
77
+ const projectName = resolved?.projectName ?? (tid === FORUM_GENERAL_THREAD_ID ? "General" : `Topic ${tid}`);
78
+ if (!resolved && tid !== FORUM_GENERAL_THREAD_ID) {
79
+ // Unbound topic — still allow menu with workspace fallback for prefs; path
80
+ // bind happens on next text message via resolveForumRuntime.
81
+ }
82
+ const controller = deps.registry.forumController(chatId, tid, cwd, projectName);
83
+ return {
84
+ chatId,
85
+ threadId: tid,
86
+ isForum: true,
87
+ settingsKey: settingsKeyFor(chatId, tid),
88
+ controller,
89
+ rt: controller.foreground(),
90
+ threadExtra: { message_thread_id: tid },
91
+ projectPath: cwd,
92
+ projectName,
93
+ };
94
+ }