@geminixiang/mama 0.1.10 → 0.2.0-beta.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 +80 -23
- package/dist/adapter.d.ts +11 -9
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +2 -2
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +33 -21
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +20 -13
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +13 -4
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +98 -43
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +25 -20
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +143 -58
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +124 -29
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts +7 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +303 -89
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +63 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +94 -0
- package/dist/bindings.js.map +1 -0
- package/dist/config.d.ts +34 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +98 -38
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -14
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +20 -5
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +20 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +51 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/instrument.d.ts +2 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +14 -0
- package/dist/instrument.js.map +1 -0
- package/dist/link-server.d.ts +16 -0
- package/dist/link-server.d.ts.map +1 -0
- package/dist/link-server.js +839 -0
- package/dist/link-server.js.map +1 -0
- package/dist/link-token.d.ts +32 -0
- package/dist/link-token.d.ts.map +1 -0
- package/dist/link-token.js +68 -0
- package/dist/link-token.js.map +1 -0
- package/dist/log.d.ts +3 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +10 -9
- package/dist/log.js.map +1 -1
- package/dist/login.d.ts +29 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +164 -0
- package/dist/login.js.map +1 -0
- package/dist/main.d.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +322 -82
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +93 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +336 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/sandbox/container.d.ts +15 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +122 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +16 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +206 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +12 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +89 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +20 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +51 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +51 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +31 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +205 -0
- package/dist/sentry.js.map +1 -0
- package/dist/session-store.d.ts +72 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +186 -0
- package/dist/session-store.js.map +1 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +8 -8
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +21 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +103 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +6 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +11 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +33 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +10 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +58 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +106 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +389 -0
- package/dist/vault.js.map +1 -0
- package/dist/vault.test.d.ts +2 -0
- package/dist/vault.test.d.ts.map +1 -0
- package/dist/vault.test.js +67 -0
- package/dist/vault.test.js.map +1 -0
- package/package.json +13 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/adapters/slack/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAErD,eAAO,MAAM,sBAAsB,wLAEmB,CAAC;AAEvD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,QAAQ,EACf,OAAO,CAAC,EAAE,OAAO,GAChB;IACD,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,mBAAmB,CAAC;IACjC,QAAQ,EAAE,YAAY,CAAC;CACxB,
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/adapters/slack/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAErD,eAAO,MAAM,sBAAsB,wLAEmB,CAAC;AAEvD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,QAAQ,EACf,OAAO,CAAC,EAAE,OAAO,GAChB;IACD,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,mBAAmB,CAAC;IACjC,QAAQ,EAAE,YAAY,CAAC;CACxB,CAkOA","sourcesContent":["import type { ChatMessage, ChatResponseContext, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport type { SlackBot, SlackEvent } from \"./bot.js\";\n\nexport const SLACK_FORMATTING_GUIDE = `## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).`;\n\nexport function createSlackAdapters(\n event: SlackEvent,\n slack: SlackBot,\n isEvent?: boolean,\n): {\n message: ChatMessage;\n responseCtx: ChatResponseContext;\n platform: PlatformInfo;\n} {\n let messageTs: string | null = null;\n const threadMessageTs: string[] = [];\n let accumulatedText = \"\";\n let isWorking = true;\n const workingIndicator = \" ...\";\n let updatePromise = Promise.resolve();\n\n const user = slack.getUser(event.user);\n\n // Extract event filename for status message\n const eventFilename = isEvent ? event.text.match(/^\\[EVENT:([^:]+):/)?.[1] : undefined;\n\n const rootTs = event.thread_ts || (!isEvent ? event.ts : undefined);\n const isThreaded = !!event.thread_ts;\n\n /** Post first reply in-thread under the user's message, creating a thread if none exists */\n const postFirstMessage = async (text: string): Promise<string> => {\n return rootTs\n ? slack.postInThread(event.channel, rootTs, text)\n : slack.postMessage(event.channel, text);\n };\n\n const message: ChatMessage = {\n id: event.ts,\n sessionKey:\n event.sessionKey ?? (event.thread_ts ? `${event.channel}:${event.thread_ts}` : event.channel),\n userId: event.user,\n userName: user?.userName,\n text: event.text,\n attachments: (event.attachments || []).map((a) => ({\n name: a.original,\n localPath: a.localPath,\n })),\n threadTs: event.thread_ts,\n };\n\n const platform: PlatformInfo = {\n name: \"slack\",\n formattingGuide: SLACK_FORMATTING_GUIDE,\n channels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),\n users: slack\n .getAllUsers()\n .map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),\n };\n\n const responseCtx = {\n respond: async (text: string) => {\n updatePromise = updatePromise.then(async () => {\n try {\n accumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n\n // Truncate accumulated text if too long (Slack limit is 40K, we use 35K for safety)\n const MAX_MAIN_LENGTH = 35000;\n const truncationNote = \"\\n\\n_(message truncated, ask me to elaborate on specific parts)_\";\n if (accumulatedText.length > MAX_MAIN_LENGTH) {\n accumulatedText =\n accumulatedText.substring(0, MAX_MAIN_LENGTH - truncationNote.length) +\n truncationNote;\n }\n\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n if (messageTs) {\n await slack.updateMessage(event.channel, messageTs, displayText);\n } else if (isThreaded) {\n // Reply within the user's thread\n messageTs = await slack.postInThread(event.channel, event.thread_ts!, displayText);\n } else {\n messageTs = await postFirstMessage(displayText);\n }\n\n if (messageTs) {\n slack.logBotResponse(event.channel, text, messageTs, isThreaded ? rootTs : undefined);\n }\n } catch (err) {\n log.logWarning(\"Slack respond error\", err instanceof Error ? err.message : String(err));\n }\n });\n await updatePromise;\n },\n\n replaceResponse: async (text: string) => {\n updatePromise = updatePromise.then(async () => {\n try {\n // Replace the accumulated text entirely, with truncation\n const MAX_MAIN_LENGTH = 35000;\n const truncationNote = \"\\n\\n_(message truncated, ask me to elaborate on specific parts)_\";\n if (text.length > MAX_MAIN_LENGTH) {\n accumulatedText =\n text.substring(0, MAX_MAIN_LENGTH - truncationNote.length) + truncationNote;\n } else {\n accumulatedText = text;\n }\n\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n if (messageTs) {\n await slack.updateMessage(event.channel, messageTs, displayText);\n } else if (isThreaded) {\n messageTs = await slack.postInThread(event.channel, event.thread_ts!, displayText);\n } else {\n messageTs = await postFirstMessage(displayText);\n }\n } catch (err) {\n log.logWarning(\n \"Slack replaceResponse error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n respondInThread: async (text: string, options?: { style?: \"muted\" }) => {\n updatePromise = updatePromise.then(async () => {\n try {\n // Always anchor to the thread root (event.thread_ts ?? event.ts)\n const threadAnchor = rootTs ?? messageTs;\n if (threadAnchor) {\n // Truncate thread messages if too long (20K limit for safety)\n const MAX_THREAD_LENGTH = 20000;\n let threadText = text;\n if (threadText.length > MAX_THREAD_LENGTH) {\n threadText = `${threadText.substring(0, MAX_THREAD_LENGTH - 50)}\\n\\n_(truncated)_`;\n }\n\n // Use context block for muted style (small gray text like Slack's Home tab)\n if (options?.style === \"muted\") {\n const CONTEXT_TEXT_LIMIT = 3000;\n const blockText =\n threadText.length > CONTEXT_TEXT_LIMIT\n ? threadText.substring(0, CONTEXT_TEXT_LIMIT - 20) + \"\\n_(truncated)_\"\n : threadText;\n const ts = await slack.postInThreadBlocks(event.channel, threadAnchor, threadText, [\n { type: \"context\", elements: [{ type: \"mrkdwn\", text: blockText }] },\n ]);\n threadMessageTs.push(ts);\n } else {\n const ts = await slack.postInThread(event.channel, threadAnchor, threadText);\n threadMessageTs.push(ts);\n }\n }\n } catch (err) {\n log.logWarning(\n \"Slack respondInThread error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n setTyping: async (isTyping: boolean) => {\n if (isTyping && !messageTs) {\n if (!rootTs) {\n return;\n }\n try {\n const statusText = eventFilename ? `Starting event: ${eventFilename}` : \"Thinking\";\n await slack.setAssistantStatus(event.channel, rootTs, statusText);\n } catch {\n // Assistant API not available — first respond() call will create the message\n }\n }\n },\n\n uploadFile: async (filePath: string, title?: string) => {\n await slack.uploadFile(event.channel, filePath, title, rootTs);\n },\n\n setWorking: async (working: boolean) => {\n updatePromise = updatePromise.then(async () => {\n try {\n isWorking = working;\n if (messageTs) {\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n const updates: Promise<void>[] = [\n slack.updateMessage(event.channel, messageTs, displayText),\n ];\n if (!working && rootTs) {\n updates.push(slack.setAssistantStatus(event.channel, rootTs, \"\").catch(() => {}));\n }\n await Promise.all(updates);\n }\n } catch (err) {\n log.logWarning(\n \"Slack setWorking error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n deleteResponse: async () => {\n updatePromise = updatePromise.then(async () => {\n // Clear assistant status first\n if (rootTs) {\n try {\n await slack.setAssistantStatus(event.channel, rootTs, \"\");\n } catch {\n // Ignore errors clearing status\n }\n }\n\n // Delete thread messages first (in reverse order)\n for (let i = threadMessageTs.length - 1; i >= 0; i--) {\n try {\n await slack.deleteMessage(event.channel, threadMessageTs[i]);\n } catch {\n // Ignore errors deleting thread messages\n }\n }\n threadMessageTs.length = 0;\n // Then delete main message\n if (messageTs) {\n await slack.deleteMessage(event.channel, messageTs);\n messageTs = null;\n }\n });\n await updatePromise;\n },\n };\n\n return { message, responseCtx, platform };\n}\n"]}
|
|
@@ -12,23 +12,24 @@ export function createSlackAdapters(event, slack, isEvent) {
|
|
|
12
12
|
const user = slack.getUser(event.user);
|
|
13
13
|
// Extract event filename for status message
|
|
14
14
|
const eventFilename = isEvent ? event.text.match(/^\[EVENT:([^:]+):/)?.[1] : undefined;
|
|
15
|
-
const rootTs = event.thread_ts
|
|
15
|
+
const rootTs = event.thread_ts || (!isEvent ? event.ts : undefined);
|
|
16
16
|
const isThreaded = !!event.thread_ts;
|
|
17
|
-
/** Post first
|
|
17
|
+
/** Post first reply in-thread under the user's message, creating a thread if none exists */
|
|
18
18
|
const postFirstMessage = async (text) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
slack.
|
|
22
|
-
}
|
|
23
|
-
return ts;
|
|
19
|
+
return rootTs
|
|
20
|
+
? slack.postInThread(event.channel, rootTs, text)
|
|
21
|
+
: slack.postMessage(event.channel, text);
|
|
24
22
|
};
|
|
25
23
|
const message = {
|
|
26
24
|
id: event.ts,
|
|
27
|
-
sessionKey: `${event.channel}:${
|
|
25
|
+
sessionKey: event.sessionKey ?? (event.thread_ts ? `${event.channel}:${event.thread_ts}` : event.channel),
|
|
28
26
|
userId: event.user,
|
|
29
27
|
userName: user?.userName,
|
|
30
28
|
text: event.text,
|
|
31
|
-
attachments: (event.attachments || []).map((a) => ({
|
|
29
|
+
attachments: (event.attachments || []).map((a) => ({
|
|
30
|
+
name: a.original,
|
|
31
|
+
localPath: a.localPath,
|
|
32
|
+
})),
|
|
32
33
|
threadTs: event.thread_ts,
|
|
33
34
|
};
|
|
34
35
|
const platform = {
|
|
@@ -58,7 +59,7 @@ export function createSlackAdapters(event, slack, isEvent) {
|
|
|
58
59
|
}
|
|
59
60
|
else if (isThreaded) {
|
|
60
61
|
// Reply within the user's thread
|
|
61
|
-
messageTs = await slack.postInThread(event.channel,
|
|
62
|
+
messageTs = await slack.postInThread(event.channel, event.thread_ts, displayText);
|
|
62
63
|
}
|
|
63
64
|
else {
|
|
64
65
|
messageTs = await postFirstMessage(displayText);
|
|
@@ -91,7 +92,7 @@ export function createSlackAdapters(event, slack, isEvent) {
|
|
|
91
92
|
await slack.updateMessage(event.channel, messageTs, displayText);
|
|
92
93
|
}
|
|
93
94
|
else if (isThreaded) {
|
|
94
|
-
messageTs = await slack.postInThread(event.channel,
|
|
95
|
+
messageTs = await slack.postInThread(event.channel, event.thread_ts, displayText);
|
|
95
96
|
}
|
|
96
97
|
else {
|
|
97
98
|
messageTs = await postFirstMessage(displayText);
|
|
@@ -106,9 +107,8 @@ export function createSlackAdapters(event, slack, isEvent) {
|
|
|
106
107
|
respondInThread: async (text, options) => {
|
|
107
108
|
updatePromise = updatePromise.then(async () => {
|
|
108
109
|
try {
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
const threadAnchor = isThreaded ? rootTs : messageTs;
|
|
110
|
+
// Always anchor to the thread root (event.thread_ts ?? event.ts)
|
|
111
|
+
const threadAnchor = rootTs ?? messageTs;
|
|
112
112
|
if (threadAnchor) {
|
|
113
113
|
// Truncate thread messages if too long (20K limit for safety)
|
|
114
114
|
const MAX_THREAD_LENGTH = 20000;
|
|
@@ -141,6 +141,9 @@ export function createSlackAdapters(event, slack, isEvent) {
|
|
|
141
141
|
},
|
|
142
142
|
setTyping: async (isTyping) => {
|
|
143
143
|
if (isTyping && !messageTs) {
|
|
144
|
+
if (!rootTs) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
144
147
|
try {
|
|
145
148
|
const statusText = eventFilename ? `Starting event: ${eventFilename}` : "Thinking";
|
|
146
149
|
await slack.setAssistantStatus(event.channel, rootTs, statusText);
|
|
@@ -162,7 +165,7 @@ export function createSlackAdapters(event, slack, isEvent) {
|
|
|
162
165
|
const updates = [
|
|
163
166
|
slack.updateMessage(event.channel, messageTs, displayText),
|
|
164
167
|
];
|
|
165
|
-
if (!working) {
|
|
168
|
+
if (!working && rootTs) {
|
|
166
169
|
updates.push(slack.setAssistantStatus(event.channel, rootTs, "").catch(() => { }));
|
|
167
170
|
}
|
|
168
171
|
await Promise.all(updates);
|
|
@@ -177,11 +180,13 @@ export function createSlackAdapters(event, slack, isEvent) {
|
|
|
177
180
|
deleteResponse: async () => {
|
|
178
181
|
updatePromise = updatePromise.then(async () => {
|
|
179
182
|
// Clear assistant status first
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
if (rootTs) {
|
|
184
|
+
try {
|
|
185
|
+
await slack.setAssistantStatus(event.channel, rootTs, "");
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Ignore errors clearing status
|
|
189
|
+
}
|
|
185
190
|
}
|
|
186
191
|
// Delete thread messages first (in reverse order)
|
|
187
192
|
for (let i = threadMessageTs.length - 1; i >= 0; i--) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/adapters/slack/context.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChatMessage, ChatResponseContext, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport type { SlackBot, SlackEvent } from \"./bot.js\";\n\nexport const SLACK_FORMATTING_GUIDE = `## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).`;\n\nexport function createSlackAdapters(\n event: SlackEvent,\n slack: SlackBot,\n isEvent?: boolean,\n): {\n message: ChatMessage;\n responseCtx: ChatResponseContext;\n platform: PlatformInfo;\n} {\n let messageTs: string | null = null;\n const threadMessageTs: string[] = [];\n let accumulatedText = \"\";\n let isWorking = true;\n const workingIndicator = \" ...\";\n let updatePromise = Promise.resolve();\n\n const user = slack.getUser(event.user);\n\n // Extract event filename for status message\n const eventFilename = isEvent ? event.text.match(/^\\[EVENT:([^:]+):/)?.[1] : undefined;\n\n const rootTs = event.thread_ts ?? event.ts;\n const isThreaded = !!event.thread_ts;\n\n /** Post first top-level message and register thread alias so stop commands can find this session */\n const postFirstMessage = async (text: string): Promise<string> => {\n const ts = await slack.postMessage(event.channel, text);\n if (ts) {\n slack.registerThreadAlias(`${event.channel}:${ts}`, `${event.channel}:${rootTs}`);\n }\n return ts;\n };\n\n const message: ChatMessage = {\n id: event.ts,\n sessionKey: `${event.channel}:${rootTs}`,\n userId: event.user,\n userName: user?.userName,\n text: event.text,\n attachments: (event.attachments || []).map((a) => ({ name: a.local, localPath: a.local })),\n threadTs: event.thread_ts,\n };\n\n const platform: PlatformInfo = {\n name: \"slack\",\n formattingGuide: SLACK_FORMATTING_GUIDE,\n channels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),\n users: slack\n .getAllUsers()\n .map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),\n };\n\n const responseCtx = {\n respond: async (text: string) => {\n updatePromise = updatePromise.then(async () => {\n try {\n accumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n\n // Truncate accumulated text if too long (Slack limit is 40K, we use 35K for safety)\n const MAX_MAIN_LENGTH = 35000;\n const truncationNote = \"\\n\\n_(message truncated, ask me to elaborate on specific parts)_\";\n if (accumulatedText.length > MAX_MAIN_LENGTH) {\n accumulatedText =\n accumulatedText.substring(0, MAX_MAIN_LENGTH - truncationNote.length) +\n truncationNote;\n }\n\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n if (messageTs) {\n await slack.updateMessage(event.channel, messageTs, displayText);\n } else if (isThreaded) {\n // Reply within the user's thread\n messageTs = await slack.postInThread(event.channel, rootTs, displayText);\n } else {\n messageTs = await postFirstMessage(displayText);\n }\n\n if (messageTs) {\n slack.logBotResponse(event.channel, text, messageTs, isThreaded ? rootTs : undefined);\n }\n } catch (err) {\n log.logWarning(\"Slack respond error\", err instanceof Error ? err.message : String(err));\n }\n });\n await updatePromise;\n },\n\n replaceResponse: async (text: string) => {\n updatePromise = updatePromise.then(async () => {\n try {\n // Replace the accumulated text entirely, with truncation\n const MAX_MAIN_LENGTH = 35000;\n const truncationNote = \"\\n\\n_(message truncated, ask me to elaborate on specific parts)_\";\n if (text.length > MAX_MAIN_LENGTH) {\n accumulatedText =\n text.substring(0, MAX_MAIN_LENGTH - truncationNote.length) + truncationNote;\n } else {\n accumulatedText = text;\n }\n\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n if (messageTs) {\n await slack.updateMessage(event.channel, messageTs, displayText);\n } else if (isThreaded) {\n messageTs = await slack.postInThread(event.channel, rootTs, displayText);\n } else {\n messageTs = await postFirstMessage(displayText);\n }\n } catch (err) {\n log.logWarning(\n \"Slack replaceResponse error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n respondInThread: async (text: string, options?: { style?: \"muted\" }) => {\n updatePromise = updatePromise.then(async () => {\n try {\n // For threaded sessions, anchor to the user's root thread\n // For channel sessions, anchor to the main bot message\n const threadAnchor = isThreaded ? rootTs : messageTs;\n if (threadAnchor) {\n // Truncate thread messages if too long (20K limit for safety)\n const MAX_THREAD_LENGTH = 20000;\n let threadText = text;\n if (threadText.length > MAX_THREAD_LENGTH) {\n threadText = `${threadText.substring(0, MAX_THREAD_LENGTH - 50)}\\n\\n_(truncated)_`;\n }\n\n // Use context block for muted style (small gray text like Slack's Home tab)\n if (options?.style === \"muted\") {\n const CONTEXT_TEXT_LIMIT = 3000;\n const blockText =\n threadText.length > CONTEXT_TEXT_LIMIT\n ? threadText.substring(0, CONTEXT_TEXT_LIMIT - 20) + \"\\n_(truncated)_\"\n : threadText;\n const ts = await slack.postInThreadBlocks(event.channel, threadAnchor, threadText, [\n { type: \"context\", elements: [{ type: \"mrkdwn\", text: blockText }] },\n ]);\n threadMessageTs.push(ts);\n } else {\n const ts = await slack.postInThread(event.channel, threadAnchor, threadText);\n threadMessageTs.push(ts);\n }\n }\n } catch (err) {\n log.logWarning(\n \"Slack respondInThread error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n setTyping: async (isTyping: boolean) => {\n if (isTyping && !messageTs) {\n try {\n const statusText = eventFilename ? `Starting event: ${eventFilename}` : \"Thinking\";\n await slack.setAssistantStatus(event.channel, rootTs, statusText);\n } catch {\n // Assistant API not available — first respond() call will create the message\n }\n }\n },\n\n uploadFile: async (filePath: string, title?: string) => {\n await slack.uploadFile(event.channel, filePath, title, rootTs);\n },\n\n setWorking: async (working: boolean) => {\n updatePromise = updatePromise.then(async () => {\n try {\n isWorking = working;\n if (messageTs) {\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n const updates: Promise<void>[] = [\n slack.updateMessage(event.channel, messageTs, displayText),\n ];\n if (!working) {\n updates.push(slack.setAssistantStatus(event.channel, rootTs, \"\").catch(() => {}));\n }\n await Promise.all(updates);\n }\n } catch (err) {\n log.logWarning(\n \"Slack setWorking error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n deleteResponse: async () => {\n updatePromise = updatePromise.then(async () => {\n // Clear assistant status first\n try {\n await slack.setAssistantStatus(event.channel, rootTs, \"\");\n } catch {\n // Ignore errors clearing status\n }\n\n // Delete thread messages first (in reverse order)\n for (let i = threadMessageTs.length - 1; i >= 0; i--) {\n try {\n await slack.deleteMessage(event.channel, threadMessageTs[i]);\n } catch {\n // Ignore errors deleting thread messages\n }\n }\n threadMessageTs.length = 0;\n // Then delete main message\n if (messageTs) {\n await slack.deleteMessage(event.channel, messageTs);\n messageTs = null;\n }\n });\n await updatePromise;\n },\n };\n\n return { message, responseCtx, platform };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/adapters/slack/context.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChatMessage, ChatResponseContext, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport type { SlackBot, SlackEvent } from \"./bot.js\";\n\nexport const SLACK_FORMATTING_GUIDE = `## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).`;\n\nexport function createSlackAdapters(\n event: SlackEvent,\n slack: SlackBot,\n isEvent?: boolean,\n): {\n message: ChatMessage;\n responseCtx: ChatResponseContext;\n platform: PlatformInfo;\n} {\n let messageTs: string | null = null;\n const threadMessageTs: string[] = [];\n let accumulatedText = \"\";\n let isWorking = true;\n const workingIndicator = \" ...\";\n let updatePromise = Promise.resolve();\n\n const user = slack.getUser(event.user);\n\n // Extract event filename for status message\n const eventFilename = isEvent ? event.text.match(/^\\[EVENT:([^:]+):/)?.[1] : undefined;\n\n const rootTs = event.thread_ts || (!isEvent ? event.ts : undefined);\n const isThreaded = !!event.thread_ts;\n\n /** Post first reply in-thread under the user's message, creating a thread if none exists */\n const postFirstMessage = async (text: string): Promise<string> => {\n return rootTs\n ? slack.postInThread(event.channel, rootTs, text)\n : slack.postMessage(event.channel, text);\n };\n\n const message: ChatMessage = {\n id: event.ts,\n sessionKey:\n event.sessionKey ?? (event.thread_ts ? `${event.channel}:${event.thread_ts}` : event.channel),\n userId: event.user,\n userName: user?.userName,\n text: event.text,\n attachments: (event.attachments || []).map((a) => ({\n name: a.original,\n localPath: a.localPath,\n })),\n threadTs: event.thread_ts,\n };\n\n const platform: PlatformInfo = {\n name: \"slack\",\n formattingGuide: SLACK_FORMATTING_GUIDE,\n channels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),\n users: slack\n .getAllUsers()\n .map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),\n };\n\n const responseCtx = {\n respond: async (text: string) => {\n updatePromise = updatePromise.then(async () => {\n try {\n accumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n\n // Truncate accumulated text if too long (Slack limit is 40K, we use 35K for safety)\n const MAX_MAIN_LENGTH = 35000;\n const truncationNote = \"\\n\\n_(message truncated, ask me to elaborate on specific parts)_\";\n if (accumulatedText.length > MAX_MAIN_LENGTH) {\n accumulatedText =\n accumulatedText.substring(0, MAX_MAIN_LENGTH - truncationNote.length) +\n truncationNote;\n }\n\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n if (messageTs) {\n await slack.updateMessage(event.channel, messageTs, displayText);\n } else if (isThreaded) {\n // Reply within the user's thread\n messageTs = await slack.postInThread(event.channel, event.thread_ts!, displayText);\n } else {\n messageTs = await postFirstMessage(displayText);\n }\n\n if (messageTs) {\n slack.logBotResponse(event.channel, text, messageTs, isThreaded ? rootTs : undefined);\n }\n } catch (err) {\n log.logWarning(\"Slack respond error\", err instanceof Error ? err.message : String(err));\n }\n });\n await updatePromise;\n },\n\n replaceResponse: async (text: string) => {\n updatePromise = updatePromise.then(async () => {\n try {\n // Replace the accumulated text entirely, with truncation\n const MAX_MAIN_LENGTH = 35000;\n const truncationNote = \"\\n\\n_(message truncated, ask me to elaborate on specific parts)_\";\n if (text.length > MAX_MAIN_LENGTH) {\n accumulatedText =\n text.substring(0, MAX_MAIN_LENGTH - truncationNote.length) + truncationNote;\n } else {\n accumulatedText = text;\n }\n\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n if (messageTs) {\n await slack.updateMessage(event.channel, messageTs, displayText);\n } else if (isThreaded) {\n messageTs = await slack.postInThread(event.channel, event.thread_ts!, displayText);\n } else {\n messageTs = await postFirstMessage(displayText);\n }\n } catch (err) {\n log.logWarning(\n \"Slack replaceResponse error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n respondInThread: async (text: string, options?: { style?: \"muted\" }) => {\n updatePromise = updatePromise.then(async () => {\n try {\n // Always anchor to the thread root (event.thread_ts ?? event.ts)\n const threadAnchor = rootTs ?? messageTs;\n if (threadAnchor) {\n // Truncate thread messages if too long (20K limit for safety)\n const MAX_THREAD_LENGTH = 20000;\n let threadText = text;\n if (threadText.length > MAX_THREAD_LENGTH) {\n threadText = `${threadText.substring(0, MAX_THREAD_LENGTH - 50)}\\n\\n_(truncated)_`;\n }\n\n // Use context block for muted style (small gray text like Slack's Home tab)\n if (options?.style === \"muted\") {\n const CONTEXT_TEXT_LIMIT = 3000;\n const blockText =\n threadText.length > CONTEXT_TEXT_LIMIT\n ? threadText.substring(0, CONTEXT_TEXT_LIMIT - 20) + \"\\n_(truncated)_\"\n : threadText;\n const ts = await slack.postInThreadBlocks(event.channel, threadAnchor, threadText, [\n { type: \"context\", elements: [{ type: \"mrkdwn\", text: blockText }] },\n ]);\n threadMessageTs.push(ts);\n } else {\n const ts = await slack.postInThread(event.channel, threadAnchor, threadText);\n threadMessageTs.push(ts);\n }\n }\n } catch (err) {\n log.logWarning(\n \"Slack respondInThread error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n setTyping: async (isTyping: boolean) => {\n if (isTyping && !messageTs) {\n if (!rootTs) {\n return;\n }\n try {\n const statusText = eventFilename ? `Starting event: ${eventFilename}` : \"Thinking\";\n await slack.setAssistantStatus(event.channel, rootTs, statusText);\n } catch {\n // Assistant API not available — first respond() call will create the message\n }\n }\n },\n\n uploadFile: async (filePath: string, title?: string) => {\n await slack.uploadFile(event.channel, filePath, title, rootTs);\n },\n\n setWorking: async (working: boolean) => {\n updatePromise = updatePromise.then(async () => {\n try {\n isWorking = working;\n if (messageTs) {\n const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n const updates: Promise<void>[] = [\n slack.updateMessage(event.channel, messageTs, displayText),\n ];\n if (!working && rootTs) {\n updates.push(slack.setAssistantStatus(event.channel, rootTs, \"\").catch(() => {}));\n }\n await Promise.all(updates);\n }\n } catch (err) {\n log.logWarning(\n \"Slack setWorking error\",\n err instanceof Error ? err.message : String(err),\n );\n }\n });\n await updatePromise;\n },\n\n deleteResponse: async () => {\n updatePromise = updatePromise.then(async () => {\n // Clear assistant status first\n if (rootTs) {\n try {\n await slack.setAssistantStatus(event.channel, rootTs, \"\");\n } catch {\n // Ignore errors clearing status\n }\n }\n\n // Delete thread messages first (in reverse order)\n for (let i = threadMessageTs.length - 1; i >= 0; i--) {\n try {\n await slack.deleteMessage(event.channel, threadMessageTs[i]);\n } catch {\n // Ignore errors deleting thread messages\n }\n }\n threadMessageTs.length = 0;\n // Then delete main message\n if (messageTs) {\n await slack.deleteMessage(event.channel, messageTs);\n messageTs = null;\n }\n });\n await updatePromise;\n },\n };\n\n return { message, responseCtx, platform };\n}\n"]}
|
|
@@ -17,11 +17,12 @@ export declare class TelegramBot implements Bot {
|
|
|
17
17
|
workingDir: string;
|
|
18
18
|
});
|
|
19
19
|
start(): Promise<void>;
|
|
20
|
-
postMessage(
|
|
21
|
-
updateMessage(
|
|
20
|
+
postMessage(conversationId: string, text: string): Promise<string>;
|
|
21
|
+
updateMessage(conversationId: string, ts: string, text: string): Promise<void>;
|
|
22
22
|
enqueueEvent(event: BotEvent): boolean;
|
|
23
23
|
getPlatformInfo(): PlatformInfo;
|
|
24
24
|
postMessageRaw(chatId: number, text: string): Promise<number>;
|
|
25
|
+
postPlainMessage(chatId: number, text: string): Promise<void>;
|
|
25
26
|
postReply(chatId: number, replyToMessageId: number, text: string): Promise<number>;
|
|
26
27
|
deleteMessageRaw(chatId: number, messageId: number): Promise<void>;
|
|
27
28
|
sendTyping(chatId: number): Promise<void>;
|
|
@@ -39,6 +40,7 @@ export declare class TelegramBot implements Bot {
|
|
|
39
40
|
}[]>;
|
|
40
41
|
private processTelegramFile;
|
|
41
42
|
private getQueue;
|
|
43
|
+
private extractMessageContext;
|
|
42
44
|
private isAddressedToBot;
|
|
43
45
|
private cleanText;
|
|
44
46
|
private setupEventHandlers;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAQhF,MAAM,WAAW,aAAc,SAAQ,QAAQ;IAC7C,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAuCD,qBAAa,WAAY,YAAW,GAAG;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAa;IAEhC,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAQ7E;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAe3B;IAEK,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhE;IAEK,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5E;IAED,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAcrC;IAED,eAAe,IAAI,YAAY,CAQ9B;IAMK,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMvF;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;IAEK,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9C;IAEK,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjF;IAED,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAI9C;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAS9D;IAED;;;;OAIG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,GAAG,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAyBhD;YAKa,mBAAmB;IAgDjC,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,kBAAkB;CAyE3B","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { createTelegramAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n type: \"message\" | \"command\";\n userName?: string;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Telegram queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nexport class TelegramBot implements Bot {\n private client: GrammyBot;\n private handler: BotHandler;\n private botToken: string;\n private workingDir: string;\n private botUserId: string | null = null;\n private botUsername: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.botToken = config.token;\n this.workingDir = config.workingDir;\n this.client = new GrammyBot(config.token);\n this.client.catch((err) => {\n log.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n const me = await this.client.api.getMe();\n this.botUserId = String(me.id);\n this.botUsername = me.username ?? null;\n this.startupTime = Date.now();\n\n this.setupEventHandlers();\n\n // Start polling in background (bot.start() runs indefinitely)\n this.client.start().catch((err) => {\n log.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n });\n\n log.logConnected();\n log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n }\n\n async postMessage(channel: string, text: string): Promise<string> {\n const result = await this.postMessageRaw(parseInt(channel), text);\n return String(result);\n }\n\n async updateMessage(channel: string, ts: string, text: string): Promise<void> {\n try {\n await this.client.api.editMessageText(parseInt(channel), parseInt(ts), text, {\n parse_mode: \"HTML\",\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"message is not modified\")) {\n throw err;\n }\n }\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.channel);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const adapters = createTelegramAdapters(event as TelegramEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"telegram\",\n formattingGuide:\n '## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\"url\">text</a>',\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postMessageRaw(chatId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n return result.message_id;\n }\n\n async postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n }\n\n async deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n await this.client.api.deleteMessage(chatId, messageId);\n }\n\n async sendTyping(chatId: number): Promise<void> {\n await this.client.api.sendChatAction(chatId, \"typing\");\n }\n\n async uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n }\n\n logToFile(channel: string, entry: object): void {\n const dir = join(this.workingDir, channel);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n appendFileSync(join(dir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channel: string, text: string, ts: string): void {\n this.logToFile(channel, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Telegram message\n * Downloads files before returning metadata so the agent can read them immediately\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n async processAttachments(\n chatId: string,\n message: any,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Handle photos (take the largest size for best quality)\n if (message.photo && message.photo.length > 0) {\n const photos = message.photo;\n const photo = photos[photos.length - 1]; // Largest photo\n const fileId = photo.file_id;\n\n downloads.push(this.processTelegramFile(chatId, fileId, `photo_${message.message_id}.jpg`));\n }\n\n // Handle documents\n if (message.document) {\n const doc = message.document;\n const fileId = doc.file_id;\n const fileName = doc.file_name ?? `document_${message.message_id}`;\n\n downloads.push(this.processTelegramFile(chatId, fileId, fileName));\n }\n\n const attachments = await Promise.all(downloads);\n return attachments.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download a file from Telegram and return attachment metadata\n */\n private async processTelegramFile(\n chatId: string,\n fileId: string,\n originalName: string,\n ): Promise<{ name: string; localPath: string } | null> {\n try {\n // Get file info from Telegram\n const file = await this.client.api.getFile(fileId);\n if (!file.file_path) {\n log.logWarning(\"Telegram file has no path\", fileId);\n return null;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${chatId}/attachments/${filename}`;\n const fullDir = join(this.workingDir, chatId, \"attachments\");\n\n if (!existsSync(fullDir)) mkdirSync(fullDir, { recursive: true });\n\n // Construct download URL\n const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;\n\n // Download the file\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(fullDir, filename), Buffer.from(buffer));\n\n return {\n name: originalName,\n localPath: localPath,\n };\n } catch (err) {\n log.logWarning(`Failed to process Telegram file`, `${originalName}: ${err}`);\n return null;\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private isAddressedToBot(text: string, chatType: string): boolean {\n if (chatType === \"private\") return true;\n if (!this.botUsername) return false;\n return text.includes(`@${this.botUsername}`);\n }\n\n private cleanText(text: string): string {\n if (!this.botUsername) return text.trim();\n return text.replace(new RegExp(`@${this.botUsername}`, \"g\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n this.client.on(\"message\", async (ctx) => {\n const msg = ctx.message;\n\n // Skip messages from before startup (Telegram replays recent messages on poll start)\n if (msg.date * 1000 < this.startupTime) return;\n // Skip bot messages\n if (msg.from?.is_bot) return;\n\n const text = msg.text ?? msg.caption ?? \"\";\n if (!text && !msg.document && !msg.photo) return;\n\n const chatId = String(msg.chat.id);\n const chatType = msg.chat.type;\n const userId = String(msg.from?.id ?? \"unknown\");\n const userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n const msgId = String(msg.message_id);\n\n // Determine thread: if this is a reply, use parent message_id as thread_ts\n const replyToId = msg.reply_to_message?.message_id;\n const threadTs = replyToId ? String(replyToId) : undefined;\n\n // Check if addressed to bot\n if (!this.isAddressedToBot(text, chatType)) return;\n\n const cleanedText = this.cleanText(text);\n const sessionKey = `${chatId}:${threadTs ?? msgId}`;\n\n // Process attachments (starts download in background)\n const processedAttachments = await this.processAttachments(chatId, msg);\n\n const event: TelegramEvent = {\n type: \"message\",\n channel: chatId,\n ts: msgId,\n thread_ts: threadTs,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log the message\n this.logToFile(chatId, {\n date: new Date(msg.date * 1000).toISOString(),\n ts: msgId,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle /stop command\n if (cleanedText.toLowerCase() === \"/stop\" || cleanedText.toLowerCase() === \"stop\") {\n if (this.handler.isRunning(sessionKey)) {\n this.handler.handleStop(sessionKey, chatId, this);\n } else {\n this.postMessage(chatId, \"Nothing running.\");\n }\n return;\n }\n\n if (this.handler.isRunning(sessionKey)) {\n this.postMessage(chatId, \"Already working. Say <code>stop</code> to cancel.\");\n } else {\n this.getQueue(sessionKey).enqueue(() => {\n const adapters = createTelegramAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAUhF,MAAM,WAAW,aAAc,SAAQ,QAAQ;IAC7C,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA2DD,qBAAa,WAAY,YAAW,GAAG;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAa;IAEhC,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAQ7E;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB3B;IAEK,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGvE;IAEK,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWnF;IAED,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAmBrC;IAED,eAAe,IAAI,YAAY,CAQ9B;IAMK,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMvF;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;IAEK,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9C;IAEK,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjF;IAED,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAI9C;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAS9D;IAED;;;;OAIG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,GAAG,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAyBhD;YAKa,mBAAmB;IAgDjC,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,kBAAkB;CA0I3B","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport { parseLoginCommand } from \"../../login.js\";\nimport * as log from \"../../log.js\";\nimport { formatAlreadyWorking, formatNothingRunning } from \"../../ui-copy.js\";\nimport { createTelegramAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n type: \"message\" | \"command\";\n userName?: string;\n}\n\ninterface MessageContext {\n msg: any;\n text: string;\n chatId: string;\n chatType: string;\n userId: string;\n userName: string;\n msgId: string;\n threadTs: string | undefined;\n sessionKey: string;\n}\n\nconst TELEGRAM_COMMANDS = [\n { command: \"start\", description: \"Show the welcome message\" },\n { command: \"help\", description: \"Show help\" },\n { command: \"stop\", description: \"Stop the current task\" },\n { command: \"new\", description: \"Start a new conversation\" },\n { command: \"login\", description: \"Open your secure login page\" },\n] as const;\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Telegram queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nexport class TelegramBot implements Bot {\n private client: GrammyBot;\n private handler: BotHandler;\n private botToken: string;\n private workingDir: string;\n private botUserId: string | null = null;\n private botUsername: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.botToken = config.token;\n this.workingDir = config.workingDir;\n this.client = new GrammyBot(config.token);\n this.client.catch((err) => {\n log.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n const me = await this.client.api.getMe();\n this.botUserId = String(me.id);\n this.botUsername = me.username ?? null;\n this.startupTime = Date.now();\n\n await this.client.api.setMyCommands([...TELEGRAM_COMMANDS]);\n\n this.setupEventHandlers();\n\n // Start polling in background (bot.start() runs indefinitely)\n this.client.start().catch((err) => {\n log.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n });\n\n log.logConnected();\n log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n }\n\n async postMessage(conversationId: string, text: string): Promise<string> {\n const result = await this.postMessageRaw(parseInt(conversationId), text);\n return String(result);\n }\n\n async updateMessage(conversationId: string, ts: string, text: string): Promise<void> {\n try {\n await this.client.api.editMessageText(parseInt(conversationId), parseInt(ts), text, {\n parse_mode: \"HTML\",\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"message is not modified\")) {\n throw err;\n }\n }\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const telegramEvent: TelegramEvent = {\n ...event,\n type: \"message\",\n conversationId: event.conversationId,\n };\n const adapters = createTelegramAdapters(telegramEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"telegram\",\n formattingGuide:\n '## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\"url\">text</a>',\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postMessageRaw(chatId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n return result.message_id;\n }\n\n async postPlainMessage(chatId: number, text: string): Promise<void> {\n await this.client.api.sendMessage(chatId, text);\n }\n\n async postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n }\n\n async deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n await this.client.api.deleteMessage(chatId, messageId);\n }\n\n async sendTyping(chatId: number): Promise<void> {\n await this.client.api.sendChatAction(chatId, \"typing\");\n }\n\n async uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n }\n\n logToFile(channel: string, entry: object): void {\n const channelDir = join(this.workingDir, channel);\n if (!existsSync(channelDir)) mkdirSync(channelDir, { recursive: true });\n appendFileSync(join(channelDir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channel: string, text: string, ts: string): void {\n this.logToFile(channel, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Telegram message\n * Downloads files before returning metadata so the agent can read them immediately\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n async processAttachments(\n chatId: string,\n message: any,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Handle photos (take the largest size for best quality)\n if (message.photo && message.photo.length > 0) {\n const photos = message.photo;\n const photo = photos[photos.length - 1]; // Largest photo\n const fileId = photo.file_id;\n\n downloads.push(this.processTelegramFile(chatId, fileId, `photo_${message.message_id}.jpg`));\n }\n\n // Handle documents\n if (message.document) {\n const doc = message.document;\n const fileId = doc.file_id;\n const fileName = doc.file_name ?? `document_${message.message_id}`;\n\n downloads.push(this.processTelegramFile(chatId, fileId, fileName));\n }\n\n const attachments = await Promise.all(downloads);\n return attachments.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download a file from Telegram and return attachment metadata\n */\n private async processTelegramFile(\n chatId: string,\n fileId: string,\n originalName: string,\n ): Promise<{ name: string; localPath: string } | null> {\n try {\n // Get file info from Telegram\n const file = await this.client.api.getFile(fileId);\n if (!file.file_path) {\n log.logWarning(\"Telegram file has no path\", fileId);\n return null;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${chatId}/attachments/${filename}`;\n const attachmentsDir = join(this.workingDir, chatId, \"attachments\");\n\n if (!existsSync(attachmentsDir)) mkdirSync(attachmentsDir, { recursive: true });\n\n // Construct download URL\n const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;\n\n // Download the file\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));\n\n return {\n name: originalName,\n localPath: localPath,\n };\n } catch (err) {\n log.logWarning(`Failed to process Telegram file`, `${originalName}: ${err}`);\n return null;\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private extractMessageContext(msg: any): MessageContext | null {\n if (!msg) return null;\n if (msg.date * 1000 < this.startupTime) return null;\n if (msg.from?.is_bot) return null;\n\n const text = msg.text ?? msg.caption ?? \"\";\n if (!text && !msg.document && !msg.photo) return null;\n\n const chatId = String(msg.chat.id);\n const chatType = msg.chat.type;\n const userId = String(msg.from?.id ?? \"unknown\");\n const userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n const msgId = String(msg.message_id);\n const replyToId = msg.reply_to_message?.message_id;\n const threadTs = replyToId ? String(replyToId) : undefined;\n\n // Private chats: single session per chat (no per-message splitting)\n // Groups: per-thread sessions (use reply chain or unique message id)\n const sessionKey = chatType === \"private\" ? chatId : `${chatId}:${threadTs ?? msgId}`;\n\n return { msg, text, chatId, chatType, userId, userName, msgId, threadTs, sessionKey };\n }\n\n private isAddressedToBot(text: string, chatType: string): boolean {\n if (chatType === \"private\") return true;\n if (!this.botUsername) return false;\n return text.toLowerCase().includes(`@${this.botUsername.toLowerCase()}`);\n }\n\n private cleanText(text: string): string {\n if (!this.botUsername) return text.trim();\n return text.replace(new RegExp(`@${this.botUsername}`, \"gi\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n // --- Slash commands (registered before catch-all so grammY intercepts them) ---\n\n this.client.command(\"start\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Welcome!</b>\",\n \"\",\n \"I'm an AI coding agent. Send me a message or use these commands:\",\n \"\",\n \"/new — Start a new conversation\",\n \"/stop — Stop the current task\",\n \"/help — Show help\",\n \"/login — Open your secure login page\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"help\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Available commands:</b>\",\n \"\",\n \"/start — Show the welcome message\",\n \"/help — Show help\",\n \"/stop — Stop the current task\",\n \"/new — Start a new conversation\",\n \"/login — Open your secure login page\",\n \"\",\n \"You can also send a regular message to chat with the agent.\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"stop\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n });\n\n this.client.command(\"new\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleNew(mc.sessionKey, mc.chatId, this);\n });\n\n this.client.command(\"login\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleLogin(\n \"telegram\",\n mc.userId,\n mc.chatId,\n this,\n mc.text,\n mc.chatType === \"private\",\n );\n });\n\n // --- Catch-all for regular (non-command) messages ---\n\n this.client.on(\"message\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n\n // In groups, only respond when addressed to bot\n if (!this.isAddressedToBot(mc.text, mc.chatType)) return;\n\n const cleanedText = this.cleanText(mc.text);\n\n if (parseLoginCommand(cleanedText)) {\n await this.handler.handleLogin(\n \"telegram\",\n mc.userId,\n mc.chatId,\n this,\n cleanedText,\n mc.chatType === \"private\",\n );\n return;\n }\n\n // Process attachments\n const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);\n\n const event: TelegramEvent = {\n type: \"message\",\n conversationId: mc.chatId,\n ts: mc.msgId,\n thread_ts: mc.threadTs,\n sessionKey: mc.sessionKey,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log the message\n this.logToFile(mc.chatId, {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle bare \"stop\" text (backward compat)\n if (cleanedText.toLowerCase() === \"stop\") {\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n return;\n }\n\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.postMessage(mc.chatId, formatAlreadyWorking(\"telegram\", \"/stop\"));\n } else {\n this.getQueue(mc.sessionKey).enqueue(() => {\n const adapters = createTelegramAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n}\n"]}
|