@stage-labs/metro 0.1.0-beta.21 → 0.1.0-beta.23

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 (72) hide show
  1. package/dist/cli.js +18 -12
  2. package/dist/control.js +68 -12
  3. package/dist/prompt.js +51 -0
  4. package/package.json +1 -1
  5. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +1 -1
  6. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +10 -10
  7. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +1 -1
  8. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +1 -1
  9. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +7 -7
  10. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +1 -1
  11. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +10 -10
  12. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/station.ts +2 -2
  13. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-transcribe.ts +6 -6
  14. package/runtime/node_modules/@metro-labs/mcp/package.json +2 -2
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +14 -8
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +2 -4
  17. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +49 -27
  18. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -2
  19. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +6 -6
  20. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +6 -6
  21. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +6 -6
  22. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +2 -2
  23. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +4 -4
  24. package/runtime/node_modules/@metro-labs/telegram/package.json +5 -2
  25. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +47 -86
  26. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +210 -251
  27. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +66 -107
  28. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +4 -4
  29. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +110 -141
  30. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +1 -1
  31. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
  32. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/inbound.ts +6 -6
  33. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +35 -92
  34. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
  35. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +65 -151
  36. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +60 -32
  37. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +1 -1
  38. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +8 -1
  39. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -47
  40. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -3
  41. package/runtime/node_modules/@metro-labs/{telegram-user → telegram-bot}/package.json +3 -6
  42. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  43. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  44. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  45. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  46. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  47. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  48. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  49. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  50. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  51. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  52. package/runtime/runtime.json +1 -1
  53. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
  54. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
  55. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
  56. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
  57. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
  58. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
  59. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
  60. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
  61. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
  62. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
  63. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
  64. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +0 -0
  65. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +0 -0
  66. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +0 -0
  67. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
  68. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-speak.ts +0 -0
  69. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice.ts +0 -0
  70. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/wire.ts +0 -0
  71. /package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/normalize.ts +0 -0
  72. /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
@@ -0,0 +1,172 @@
1
+ import { errMsg } from '@metro-labs/mcp/log';
2
+ import { accounts, lineOf } from './accounts.js';
3
+ import { mintId, SELF_URI } from './wire.js';
4
+ import { mediaRefOf, saveTelegramMedia } from './attachments.js';
5
+ import type { TgMsg, TgReaction, TgReactionCount } from './types.js';
6
+
7
+ export type { TgMsg, TgReaction, TgReactionCount };
8
+
9
+ function lineForMsg(
10
+ accountId: string,
11
+ m: TgMsg,
12
+ ): { line: string; topicId?: number } {
13
+ const topicId = m.is_topic_message ? m.message_thread_id : undefined;
14
+ return { line: lineOf(accountId, m.chat.id, topicId), topicId };
15
+ }
16
+
17
+ function stickerTag(s: NonNullable<TgMsg['sticker']>): string {
18
+ const set = s.set_name ? ` · ${s.set_name}` : '';
19
+ return `[sticker${s.emoji ? ` ${s.emoji}` : ''}${set}]`;
20
+ }
21
+
22
+ const TAG_EXTRACTORS: ((m: TgMsg) => string | null)[] = [
23
+ (m) => (m.photo?.length ? '[image]' : null),
24
+ (m) => (m.voice ? '[voice]' : null),
25
+ (m) => (m.audio ? `[audio: ${m.audio.file_name ?? 'audio'}]` : null),
26
+ (m) => (m.video ? `[video: ${m.video.file_name ?? 'video'}]` : null),
27
+ (m) => (m.animation ? `[gif: ${m.animation.file_name ?? 'gif'}]` : null),
28
+ (m) => (m.sticker ? stickerTag(m.sticker) : null),
29
+ (m) =>
30
+ m.document && !m.animation
31
+ ? `[file: ${m.document.file_name ?? 'doc'}]`
32
+ : null,
33
+ (m) =>
34
+ m.location
35
+ ? `[location: ${m.location.latitude}, ${m.location.longitude}]`
36
+ : null,
37
+ (m) => (m.dice ? `[dice ${m.dice.emoji} = ${m.dice.value}]` : null),
38
+ ];
39
+
40
+ function projectText(m: TgMsg): string {
41
+ const tags = TAG_EXTRACTORS.map((f) => f(m)).filter(
42
+ (t): t is string => t !== null,
43
+ );
44
+ return [m.text ?? m.caption, ...tags].filter(Boolean).join(' ');
45
+ }
46
+
47
+ export function envelope(accountId: string, m: TgMsg): Record<string, unknown> {
48
+ const { line } = lineForMsg(accountId, m);
49
+ return {
50
+ kind: 'inbound',
51
+ id: mintId(),
52
+ ts: new Date(m.date * 1000).toISOString(),
53
+ station: 'telegram-bot',
54
+ line,
55
+ line_name: m.chat.title ?? m.chat.first_name ?? undefined,
56
+ from: `metro://telegram-bot/${accountId}/user/${m.from?.id ?? 'unknown'}`,
57
+ from_name: m.from?.username ? `@${m.from.username}` : m.from?.first_name,
58
+ from_display_name: m.from?.first_name,
59
+ message_id: String(m.message_id),
60
+ text: projectText(m),
61
+ payload: m,
62
+ is_private: m.chat.type === 'private',
63
+ };
64
+ }
65
+
66
+ export function reactionEnvelope(
67
+ accountId: string,
68
+ r: TgReaction,
69
+ ): Record<string, unknown> | null {
70
+ if (r.user?.is_bot) return null;
71
+ const newEmojis = r.new_reaction
72
+ .filter((x) => x.type === 'emoji')
73
+ .map((x) => x.emoji ?? '');
74
+ const oldEmojis = r.old_reaction
75
+ .filter((x) => x.type === 'emoji')
76
+ .map((x) => x.emoji ?? '');
77
+ const added = newEmojis.filter((e) => !oldEmojis.includes(e));
78
+ if (!added.length) return null;
79
+ return {
80
+ kind: 'react',
81
+ id: mintId(),
82
+ ts: new Date(r.date * 1000).toISOString(),
83
+ station: 'telegram-bot',
84
+ line: lineOf(accountId, r.chat.id),
85
+ from: `metro://telegram-bot/${accountId}/user/${r.user?.id ?? 'unknown'}`,
86
+ from_name: r.user?.username ? `@${r.user.username}` : r.user?.first_name,
87
+ from_display_name: r.user?.first_name,
88
+ message_id: String(r.message_id),
89
+ emoji: added[0],
90
+ event: { type: 'react', emoji: added[0], targetId: String(r.message_id) },
91
+ is_private: r.chat.type === 'private',
92
+ payload: r,
93
+ };
94
+ }
95
+
96
+ export function reactionCountEnvelope(
97
+ accountId: string,
98
+ rc: TgReactionCount,
99
+ ): Record<string, unknown> | null {
100
+ const emojiReactions = rc.reactions.filter(
101
+ (r) => r.type.type === 'emoji' && r.type.emoji,
102
+ );
103
+ if (!emojiReactions.length) return null;
104
+ const top = emojiReactions.reduce((a, b) =>
105
+ b.total_count > a.total_count ? b : a,
106
+ );
107
+ const emoji = top.type.emoji ?? '';
108
+ if (!emoji) return null;
109
+ return {
110
+ kind: 'react',
111
+ id: mintId(),
112
+ ts: new Date(rc.date * 1000).toISOString(),
113
+ station: 'telegram-bot',
114
+ line: lineOf(accountId, rc.chat.id),
115
+ from: `metro://telegram-bot/${accountId}/user/unknown`,
116
+ message_id: String(rc.message_id),
117
+ emoji,
118
+ event: { type: 'react', emoji, targetId: String(rc.message_id) },
119
+ is_private: rc.chat.type === 'private',
120
+ payload: rc,
121
+ };
122
+ }
123
+
124
+ export function emitInbound(
125
+ emit: (e: unknown) => void,
126
+ accountId: string,
127
+ e: Record<string, unknown>,
128
+ ): void {
129
+ const owner = accounts.get(accountId)?.cfg.owner;
130
+ const payload = {
131
+ ...(e.payload as Record<string, unknown> | undefined),
132
+ account: accountId,
133
+ };
134
+ emit({ ...e, ...(owner ? { to: owner } : {}), account: accountId, payload });
135
+ }
136
+
137
+ export function saveMediaAndEmit(
138
+ emit: (e: unknown) => void,
139
+ accountId: string,
140
+ m: TgMsg,
141
+ sourceEnvId: string,
142
+ ): void {
143
+ const ref = mediaRefOf(m);
144
+ if (!ref) return;
145
+ const line = lineForMsg(accountId, m).line;
146
+ void saveTelegramMedia(accountId, ref, String(m.message_id), 0)
147
+ .then((saved) => {
148
+ emitInbound(emit, accountId, {
149
+ kind: 'inbound',
150
+ id: mintId(),
151
+ ts: new Date().toISOString(),
152
+ station: 'telegram-bot',
153
+ line,
154
+ from: SELF_URI || `metro://telegram-bot/${accountId}/self`,
155
+ text: `📎 saved: ${saved.path}`,
156
+ payload: {
157
+ contentType: 'attachmentSaved',
158
+ attachmentFor: sourceEnvId,
159
+ index: 0,
160
+ attachmentPath: saved.path,
161
+ localPath: saved.path,
162
+ mime: saved.mime,
163
+ name: saved.name,
164
+ },
165
+ });
166
+ })
167
+ .catch((err: unknown) =>
168
+ process.stderr.write(
169
+ `telegram-bot media save failed: ${errMsg(err)}\n`,
170
+ ),
171
+ );
172
+ }
@@ -0,0 +1,103 @@
1
+ import { errMsg } from '@metro-labs/mcp/log';
2
+ import { accounts, loadAccounts, tg, type Account } from './accounts.js';
3
+ import { emit } from './wire.js';
4
+ import {
5
+ emitInbound,
6
+ envelope,
7
+ reactionCountEnvelope,
8
+ reactionEnvelope,
9
+ saveMediaAndEmit,
10
+ type TgMsg,
11
+ type TgReaction,
12
+ type TgReactionCount,
13
+ } from './format.js';
14
+ import { readCalls } from '@metro-labs/mcp/trains/protocol';
15
+ import { handleCall } from './actions.js';
16
+
17
+ readCalls('telegram-bot', handleCall);
18
+
19
+ interface Update {
20
+ update_id: number;
21
+ message?: TgMsg;
22
+ message_reaction?: TgReaction;
23
+ message_reaction_count?: TgReactionCount;
24
+ }
25
+
26
+ function handleUpdate(id: string, u: Update): void {
27
+ if (u.message && !u.message.from?.is_bot) {
28
+ const env = envelope(id, u.message);
29
+ emitInbound(emit, id, env);
30
+ saveMediaAndEmit(emit, id, u.message, env.id as string);
31
+ }
32
+ if (u.message_reaction) {
33
+ const env = reactionEnvelope(id, u.message_reaction);
34
+ if (env) emitInbound(emit, id, env);
35
+ }
36
+ if (u.message_reaction_count) {
37
+ const env = reactionCountEnvelope(id, u.message_reaction_count);
38
+ if (env) emitInbound(emit, id, env);
39
+ }
40
+ }
41
+
42
+ async function runAccount(acct: Account): Promise<void> {
43
+ const { id } = acct.cfg;
44
+ try {
45
+ await tg(id, 'deleteWebhook', { drop_pending_updates: false });
46
+ } catch (err) {
47
+ process.stderr.write(
48
+ `telegram-bot[${id}] deleteWebhook: ${errMsg(err)}\n`,
49
+ );
50
+ }
51
+
52
+ for (;;) {
53
+ try {
54
+ const updates = await tg<Update[]>(
55
+ id,
56
+ 'getUpdates',
57
+ {
58
+ offset: acct.offset,
59
+ timeout: 25,
60
+ allowed_updates: [
61
+ 'message',
62
+ 'message_reaction',
63
+ 'message_reaction_count',
64
+ ],
65
+ },
66
+ 60_000,
67
+ );
68
+ for (const u of updates) {
69
+ acct.offset = u.update_id + 1;
70
+ handleUpdate(id, u);
71
+ }
72
+ } catch (err) {
73
+ process.stderr.write(
74
+ `telegram-bot[${id}] poll error: ${errMsg(err)}\n`,
75
+ );
76
+ await new Promise((r) => setTimeout(r, 2_000));
77
+ }
78
+ }
79
+ }
80
+
81
+ const cfgs = loadAccounts();
82
+ for (const cfg of cfgs) {
83
+ accounts.set(cfg.id, {
84
+ cfg,
85
+ api: `https://api.telegram.org/bot${cfg.token}`,
86
+ fileApi: `https://api.telegram.org/file/bot${cfg.token}`,
87
+ offset: 0,
88
+ });
89
+ }
90
+ if (accounts.size === 0) {
91
+ process.stderr.write('telegram-bot: no accounts booted, exiting\n');
92
+ process.exit(2);
93
+ }
94
+ process.stderr.write(
95
+ `telegram-bot train ready (multi) — ${accounts.size} account(s): ${[...accounts.keys()].join(', ')}\n`,
96
+ );
97
+
98
+ for (const acct of accounts.values())
99
+ runAccount(acct).catch((err: unknown) => {
100
+ process.stderr.write(
101
+ `telegram-bot[${acct.cfg.id}] account loop failed: ${errMsg(err)}\n`,
102
+ );
103
+ });
@@ -0,0 +1,166 @@
1
+ import { tg, tgForm, targetOf } from './accounts.js';
2
+ import { emit, mintId, respond, SELF_URI } from './wire.js';
3
+ import { appendFile } from '@metro-labs/mcp/stations/attachments';
4
+
5
+ export function emitOutbound(
6
+ accountId: string,
7
+ line: string,
8
+ messageId: string,
9
+ text: string,
10
+ replyTo?: string,
11
+ ): void {
12
+ emit({
13
+ kind: 'outbound',
14
+ id: mintId(),
15
+ ts: new Date().toISOString(),
16
+ station: 'telegram-bot',
17
+ line,
18
+ from: SELF_URI,
19
+ to: line,
20
+ message_id: messageId,
21
+ text,
22
+ reply_to: replyTo,
23
+ ...(replyTo ? { event: { type: 'reply', replyTo } } : {}),
24
+ account: accountId,
25
+ payload: { account: accountId },
26
+ });
27
+ }
28
+
29
+ export function finishSend(
30
+ id: string,
31
+ accountId: string,
32
+ line: string,
33
+ messageId: string,
34
+ label: string,
35
+ replyTo?: string,
36
+ extra?: Record<string, unknown>,
37
+ ): void {
38
+ emitOutbound(accountId, line, messageId, label, replyTo);
39
+ respond(id, {
40
+ result: { messageId, account: accountId, ...extra },
41
+ });
42
+ }
43
+
44
+ interface MediaArgs {
45
+ line: string;
46
+ path: string;
47
+ caption?: string;
48
+ replyTo?: string;
49
+ parseMode?: string;
50
+ account?: string;
51
+ name?: string;
52
+ }
53
+
54
+ export async function sendMedia(
55
+ method: string,
56
+ fieldName: string,
57
+ args: Record<string, unknown>,
58
+ ): Promise<{ accountId: string; message_id: number }> {
59
+ const {
60
+ line,
61
+ path,
62
+ caption,
63
+ replyTo,
64
+ parseMode,
65
+ account,
66
+ name: fileName,
67
+ } = args as unknown as MediaArgs;
68
+ const { accountId, chatId, topicId } = targetOf(line, account);
69
+ const form = new FormData();
70
+ form.append('chat_id', String(chatId));
71
+ if (topicId !== undefined) form.append('message_thread_id', String(topicId));
72
+ if (caption) form.append('caption', caption);
73
+ if (parseMode) form.append('parse_mode', parseMode);
74
+ if (replyTo)
75
+ form.append(
76
+ 'reply_parameters',
77
+ JSON.stringify({ message_id: Number(replyTo) }),
78
+ );
79
+ const name = fileName ?? path.split('/').pop() ?? fieldName;
80
+ await appendFile(form, fieldName, path, name);
81
+ const r = await tgForm<{ message_id: number }>(accountId, method, form);
82
+ return { accountId, message_id: r.message_id };
83
+ }
84
+
85
+ export async function media(
86
+ id: string,
87
+ method: string,
88
+ field: string,
89
+ label: string,
90
+ args: Record<string, unknown>,
91
+ ): Promise<void> {
92
+ const { accountId, message_id } = await sendMedia(method, field, args);
93
+ const line = (args as { line: string }).line;
94
+ finishSend(
95
+ id,
96
+ accountId,
97
+ line,
98
+ String(message_id),
99
+ label,
100
+ args.replyTo as string | undefined,
101
+ );
102
+ }
103
+
104
+ export const MEDIA_METHOD_FIELD: Record<
105
+ string,
106
+ { method: string; field: string }
107
+ > = {
108
+ image: { method: 'sendPhoto', field: 'photo' },
109
+ voice: { method: 'sendVoice', field: 'voice' },
110
+ audio: { method: 'sendAudio', field: 'audio' },
111
+ video: { method: 'sendVideo', field: 'video' },
112
+ document: { method: 'sendDocument', field: 'document' },
113
+ };
114
+
115
+ export async function sendDice(
116
+ id: string,
117
+ args: Record<string, unknown>,
118
+ ): Promise<void> {
119
+ const {
120
+ line,
121
+ emoji = '\U0001F3B2',
122
+ account,
123
+ } = args as { line: string; emoji?: string; account?: string };
124
+ const { accountId, chatId, topicId } = targetOf(line, account);
125
+ const body: Record<string, unknown> = { chat_id: chatId, emoji };
126
+ if (topicId !== undefined) body.message_thread_id = topicId;
127
+ const r = await tg<{ message_id: number; dice?: { value: number } }>(
128
+ accountId,
129
+ 'sendDice',
130
+ body,
131
+ );
132
+ finishSend(
133
+ id,
134
+ accountId,
135
+ line,
136
+ String(r.message_id),
137
+ `[dice ${emoji} = ${r.dice?.value ?? '?'}]`,
138
+ undefined,
139
+ { value: r.dice?.value },
140
+ );
141
+ }
142
+
143
+ export async function sendLocation(
144
+ id: string,
145
+ args: Record<string, unknown>,
146
+ ): Promise<void> {
147
+ const { line, latitude, longitude, account } = args as {
148
+ line: string;
149
+ latitude: number;
150
+ longitude: number;
151
+ account?: string;
152
+ };
153
+ const { accountId, chatId } = targetOf(line, account);
154
+ const r = await tg<{ message_id: number }>(accountId, 'sendLocation', {
155
+ chat_id: chatId,
156
+ latitude,
157
+ longitude,
158
+ });
159
+ finishSend(
160
+ id,
161
+ accountId,
162
+ line,
163
+ String(r.message_id),
164
+ `[location: ${latitude}, ${longitude}]`,
165
+ );
166
+ }
@@ -0,0 +1,47 @@
1
+ import type { MemberList, MetroMember } from '@metro-labs/mcp/stations/types';
2
+
3
+ export interface TgUser {
4
+ id: number;
5
+ username?: string;
6
+ first_name?: string;
7
+ last_name?: string;
8
+ is_bot?: boolean;
9
+ }
10
+
11
+ export interface TgChatMember {
12
+ user: TgUser;
13
+ status: string;
14
+ }
15
+
16
+ export function mapTgMember(m: TgChatMember): MetroMember {
17
+ const u = m.user;
18
+ const member: MetroMember = { id: String(u.id) };
19
+ if (u.username) member.name = u.username;
20
+ const display = [u.first_name, u.last_name].filter(Boolean).join(' ');
21
+ if (display) member.display_name = display;
22
+ member.is_admin = m.status === 'creator' || m.status === 'administrator';
23
+ member.is_bot = Boolean(u.is_bot);
24
+ return member;
25
+ }
26
+
27
+ const BOT_LIMIT_REASON =
28
+ 'Telegram Bot API cannot enumerate full membership; returning administrators only.';
29
+
30
+ export function adminMemberList(
31
+ admins: TgChatMember[],
32
+ total: number | null,
33
+ ): MemberList {
34
+ return {
35
+ members: admins.map(mapTgMember),
36
+ capability: {
37
+ supported: true,
38
+ complete: false,
39
+ reason: BOT_LIMIT_REASON,
40
+ ...(total != null ? { total } : {}),
41
+ },
42
+ };
43
+ }
44
+
45
+ export function inaccessibleMemberList(reason: string): MemberList {
46
+ return { members: [], capability: { supported: false, complete: false, reason } };
47
+ }
@@ -0,0 +1,17 @@
1
+ import type { Station, Verb } from '@metro-labs/mcp/stations/types';
2
+
3
+ export const telegramBotStation: Station = {
4
+ name: 'telegram-bot',
5
+ hasAccounts: true,
6
+ hasTrain: true,
7
+ messageVerbs: new Set<Verb>([
8
+ 'send',
9
+ 'reply',
10
+ 'react',
11
+ 'unreact',
12
+ 'edit',
13
+ 'delete',
14
+ ]),
15
+ attachmentMode: 'canonical',
16
+ tools: [],
17
+ };
@@ -0,0 +1,48 @@
1
+ export interface TgMsg {
2
+ message_id: number;
3
+ date: number;
4
+ chat: { id: number; type: string; title?: string; first_name?: string };
5
+ from?: {
6
+ id: number;
7
+ username?: string;
8
+ first_name?: string;
9
+ is_bot?: boolean;
10
+ };
11
+ text?: string;
12
+ caption?: string;
13
+ message_thread_id?: number;
14
+ is_topic_message?: boolean;
15
+ photo?: { file_id: string; file_size?: number }[];
16
+ document?: { file_name?: string; file_id?: string; mime_type?: string };
17
+ voice?: { file_id?: string; duration?: number; mime_type?: string };
18
+ audio?: { file_id?: string; file_name?: string; mime_type?: string };
19
+ video?: { file_id?: string; file_name?: string; mime_type?: string };
20
+ animation?: { file_id?: string; file_name?: string; mime_type?: string };
21
+ sticker?: { file_id?: string; emoji?: string; set_name?: string };
22
+ location?: { latitude: number; longitude: number };
23
+ dice?: { emoji: string; value: number };
24
+ }
25
+
26
+ export interface TgReaction {
27
+ chat: { id: number; type: string };
28
+ message_id: number;
29
+ user?: {
30
+ id: number;
31
+ username?: string;
32
+ first_name?: string;
33
+ is_bot?: boolean;
34
+ };
35
+ date: number;
36
+ old_reaction: { type: string; emoji?: string }[];
37
+ new_reaction: { type: string; emoji?: string }[];
38
+ }
39
+
40
+ export interface TgReactionCount {
41
+ chat: { id: number; type: string };
42
+ message_id: number;
43
+ date: number;
44
+ reactions: {
45
+ type: { type: string; emoji?: string };
46
+ total_count: number;
47
+ }[];
48
+ }
@@ -0,0 +1,3 @@
1
+ export { emit, respond, mintId } from '@metro-labs/mcp/stations/station-runtime';
2
+
3
+ export const SELF_URI = process.env.METRO_SELF_URI ?? '';
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.21"
2
+ "version": "0.1.0-beta.23"
3
3
  }
@@ -1,81 +0,0 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
- import {
4
- makeAccountStore,
5
- resolveAccountId,
6
- type Die,
7
- } from '@metro-labs/mcp/stations/account-store';
8
- import type { UserAccount } from './types.js';
9
-
10
- const ACCOUNTS_FILE =
11
- process.env.TELEGRAM_USER_ACCOUNTS_FILE ??
12
- join(homedir(), '.metro', 'telegram-user-accounts.json');
13
-
14
- const isSignedInt = (s: string): boolean => /^-?\d+$/.test(s);
15
- const isTopic = (s: string): boolean => /^\d+$/.test(s);
16
-
17
- function validateAccount(a: UserAccount, seen: Set<string>, die: Die): void {
18
- if (!a.id) die('account missing id');
19
- if (!a.session || typeof a.session !== 'string')
20
- die(`account '${a.id}' missing session`);
21
- if (!Number.isInteger(a.apiId) || (a.apiId ?? 0) <= 0)
22
- die(`account '${a.id}' missing apiId`);
23
- if (!a.apiHash || typeof a.apiHash !== 'string')
24
- die(`account '${a.id}' missing apiHash`);
25
- if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
26
- seen.add(a.id);
27
- }
28
-
29
- export const { loadAccounts } = makeAccountStore<UserAccount>({
30
- prefix: 'telegram-user',
31
- file: ACCOUNTS_FILE,
32
- allowlistEnv: ['TELEGRAM_USER_ONLY_ACCOUNTS', 'TELEGRAM_USER_ACCOUNTS'],
33
- validate(raw, die) {
34
- const seen = new Set<string>();
35
- for (const a of raw) validateAccount(a, seen, die);
36
- },
37
- });
38
-
39
- export const accounts = new Map<string, UserAccount>();
40
-
41
- export function accountFor(args: { account?: string; line?: string }): string {
42
- return resolveAccountId(accounts, args, (line) => targetOf(line)?.accountId);
43
- }
44
-
45
- export function lineOf(
46
- accountId: string,
47
- chatId: number | string,
48
- topicId?: number,
49
- ): string {
50
- const tail = topicId !== undefined ? `${chatId}/${topicId}` : `${chatId}`;
51
- return `metro://telegram-user/${accountId}/${tail}`;
52
- }
53
-
54
- interface Target {
55
- accountId: string;
56
- chatId: number;
57
- topicId?: number;
58
- }
59
-
60
- function splitScoped(path: string[]): { accountId: string; rest: string[] } {
61
- const first = path[0];
62
- if (path.length >= 2 && first !== undefined && !isSignedInt(first))
63
- return { accountId: first, rest: path.slice(1) };
64
- return { accountId: 'default', rest: path };
65
- }
66
-
67
- export function targetOf(line: string): Target | undefined {
68
- const prefix = 'metro://telegram-user/';
69
- if (!line.startsWith(prefix)) return undefined;
70
- const path = line.slice(prefix.length).split('/').filter(Boolean);
71
- const { accountId, rest } = splitScoped(path);
72
- const [chatId, topicId] = rest;
73
- if (rest.length < 1 || rest.length > 2 || chatId === undefined) return undefined;
74
- if (!isSignedInt(chatId)) return undefined;
75
- if (topicId !== undefined && !isTopic(topicId)) return undefined;
76
- return {
77
- accountId,
78
- chatId: Number(chatId),
79
- ...(topicId !== undefined ? { topicId: Number(topicId) } : {}),
80
- };
81
- }