@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.20

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 (225) hide show
  1. package/dist/api.js +103 -0
  2. package/dist/cli.js +134 -565
  3. package/dist/control.js +102 -0
  4. package/dist/prompt.js +10 -0
  5. package/dist/runtime.js +94 -0
  6. package/dist/store.js +70 -0
  7. package/dist/update.js +33 -0
  8. package/dist/version.js +97 -0
  9. package/package.json +34 -22
  10. package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
  11. package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
  12. package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
  13. package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
  14. package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
  15. package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
  16. package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
  17. package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
  18. package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
  19. package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
  20. package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
  21. package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
  22. package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
  23. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
  24. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
  25. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
  26. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
  27. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  29. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  30. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  31. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  32. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  33. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +340 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +28 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +115 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  133. package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
  134. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
  135. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
  136. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
  137. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
  138. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
  139. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
  140. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
  141. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
  145. package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
  146. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
  147. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
  148. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
  149. package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
  150. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
  151. package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
  152. package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
  153. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
  154. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
  155. package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
  156. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
  157. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
  158. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
  159. package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
  160. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
  161. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
  162. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
  163. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  164. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  165. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  166. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  167. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  168. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  169. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  170. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  171. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  172. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  187. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  188. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  189. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  190. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  191. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  192. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  193. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  194. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  213. package/runtime/runtime.json +3 -0
  214. package/runtime/trains/.keep +0 -0
  215. package/LICENSE +0 -21
  216. package/README.md +0 -78
  217. package/dist/channels/discord.js +0 -148
  218. package/dist/channels/telegram.js +0 -137
  219. package/dist/lib/address.js +0 -21
  220. package/dist/lib/codex-rc.js +0 -274
  221. package/dist/lib/dotenv.js +0 -31
  222. package/dist/log.js +0 -12
  223. package/dist/paths.js +0 -45
  224. package/dist/tail.js +0 -161
  225. package/skills/metro/SKILL.md +0 -122
@@ -0,0 +1,87 @@
1
+ import type { Message } from '@mtcute/bun';
2
+ import { FileLocation } from '@mtcute/bun';
3
+ import {
4
+ saveBufferToCache,
5
+ type SavedAttachment,
6
+ } from '@metro-labs/mcp/stations/attachments';
7
+ import type { UserClient } from './client.js';
8
+
9
+ type Media = NonNullable<Message['media']>;
10
+ export type DownloadableMedia = Media & FileLocation;
11
+
12
+ export function isDownloadable(media: Media): media is DownloadableMedia {
13
+ return media instanceof FileLocation;
14
+ }
15
+
16
+ interface MediaMeta {
17
+ mime?: string;
18
+ name?: string;
19
+ }
20
+
21
+ function fallbackMime(media: DownloadableMedia): string | undefined {
22
+ const type = (media as { type?: unknown }).type;
23
+ if (type === 'photo') return 'image/jpeg';
24
+ if (type === 'sticker') return 'image/webp';
25
+ return undefined;
26
+ }
27
+
28
+ function metaOf(media: DownloadableMedia): MediaMeta {
29
+ const withMime = media as { mimeType?: unknown };
30
+ const withName = media as { fileName?: unknown };
31
+ const mime =
32
+ (typeof withMime.mimeType === 'string' ? withMime.mimeType : undefined) ??
33
+ fallbackMime(media);
34
+ const name =
35
+ typeof withName.fileName === 'string' ? withName.fileName : undefined;
36
+ return {
37
+ ...(mime ? { mime } : {}),
38
+ ...(name ? { name } : {}),
39
+ };
40
+ }
41
+
42
+ export interface PendingDescriptor {
43
+ kind: string;
44
+ name?: string;
45
+ }
46
+
47
+ const KIND_BY_TYPE: Record<string, string> = {
48
+ photo: 'image',
49
+ sticker: 'image',
50
+ voice: 'audio',
51
+ audio: 'audio',
52
+ video: 'video',
53
+ video_note: 'video',
54
+ animation: 'video',
55
+ };
56
+
57
+ function mediaType(media: DownloadableMedia): string {
58
+ const type = (media as { type?: unknown }).type;
59
+ return typeof type === 'string' ? type : '';
60
+ }
61
+
62
+ function kindOf(media: DownloadableMedia, mime: string | undefined): string {
63
+ const byType = KIND_BY_TYPE[mediaType(media)];
64
+ if (byType) return byType;
65
+ if (mime?.startsWith('image/')) return 'image';
66
+ if (mime?.startsWith('audio/')) return 'audio';
67
+ if (mime?.startsWith('video/')) return 'video';
68
+ return 'file';
69
+ }
70
+
71
+ export function pendingDescriptorOf(media: DownloadableMedia): PendingDescriptor {
72
+ const meta = metaOf(media);
73
+ return {
74
+ kind: kindOf(media, meta.mime),
75
+ ...(meta.name ? { name: meta.name } : {}),
76
+ };
77
+ }
78
+
79
+ export async function downloadMedia(
80
+ client: UserClient,
81
+ media: DownloadableMedia,
82
+ messageId: string,
83
+ index = 0,
84
+ ): Promise<SavedAttachment> {
85
+ const buf = await client.tg.downloadAsBuffer(media);
86
+ return saveBufferToCache(buf, messageId, index, metaOf(media));
87
+ }
@@ -0,0 +1,77 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { TelegramClient } from '@mtcute/bun';
5
+ import { TrainError } from '@metro-labs/mcp/train-error';
6
+ import { errMsg } from '@metro-labs/mcp/log';
7
+ import type { UserAccount } from './types.js';
8
+
9
+ const STATE_DIR =
10
+ process.env.METRO_STATE_DIR ?? join(homedir(), '.cache', 'metro');
11
+
12
+ export interface Self {
13
+ username: string | null;
14
+ phone: string | null;
15
+ }
16
+
17
+ export interface UserClient {
18
+ account: UserAccount;
19
+ tg: TelegramClient;
20
+ self: () => Self | null;
21
+ connect: () => Promise<void>;
22
+ startUpdates: () => Promise<void>;
23
+ disconnect: () => Promise<void>;
24
+ }
25
+
26
+ function storagePath(accountId: string): string {
27
+ const dir = join(STATE_DIR, 'telegram-user');
28
+ mkdirSync(dir, { recursive: true });
29
+ return join(dir, `${accountId}.session`);
30
+ }
31
+
32
+ export function createClient(account: UserAccount): UserClient {
33
+ const apiId = account.apiId ?? 0;
34
+ const { apiHash } = account;
35
+ if (!Number.isInteger(apiId) || apiId <= 0 || !apiHash)
36
+ throw new TrainError(
37
+ 'telegram_user_auth',
38
+ `account '${account.id}' missing apiId/apiHash`,
39
+ );
40
+ const tg = new TelegramClient({
41
+ apiId,
42
+ apiHash,
43
+ storage: storagePath(account.id),
44
+ });
45
+
46
+ let me: Self | null = null;
47
+
48
+ const connect = async (): Promise<void> => {
49
+ try {
50
+ await tg.importSession(account.session);
51
+ await tg.connect();
52
+ } catch (e) {
53
+ throw new TrainError(
54
+ 'telegram_user_auth',
55
+ `account '${account.id}' connect failed: ${errMsg(e)}`,
56
+ );
57
+ }
58
+ try {
59
+ const user = await tg.getMe();
60
+ me = { username: user.username ?? null, phone: user.phoneNumber ?? null };
61
+ } catch (e) {
62
+ process.stderr.write(
63
+ `telegram-user[${account.id}] identity unavailable: ${errMsg(e)}\n`,
64
+ );
65
+ }
66
+ };
67
+
68
+ const startUpdates = async (): Promise<void> => {
69
+ await tg.startUpdatesLoop();
70
+ };
71
+
72
+ const disconnect = async (): Promise<void> => {
73
+ await tg.disconnect();
74
+ };
75
+
76
+ return { account, tg, self: () => me, connect, startUpdates, disconnect };
77
+ }
@@ -0,0 +1,141 @@
1
+ import type { Chat, Message, Peer, User } from '@mtcute/bun';
2
+ import type { SavedAttachment } from '@metro-labs/mcp/stations/attachments';
3
+ import { lineOf } from './accounts.js';
4
+ import { mintId } from './wire.js';
5
+ import { isDownloadable, pendingDescriptorOf } from './attachments.js';
6
+
7
+ const isUser = (peer: Peer): peer is User => peer.type === 'user';
8
+ const isChat = (peer: Peer): peer is Chat => peer.type === 'chat';
9
+
10
+ export function senderName(peer: Peer): string | undefined {
11
+ if (isUser(peer)) return peer.username ? `@${peer.username}` : peer.firstName;
12
+ return peer.title;
13
+ }
14
+
15
+ export function displayName(peer: Peer): string | undefined {
16
+ if (isUser(peer)) return peer.firstName;
17
+ return peer.title;
18
+ }
19
+
20
+ function chatName(peer: Peer): string | undefined {
21
+ if (isChat(peer)) return peer.title;
22
+ if (isUser(peer)) return peer.username ? `@${peer.username}` : peer.firstName;
23
+ return undefined;
24
+ }
25
+
26
+ function topicOf(m: Message): number | undefined {
27
+ if (!m.isTopicMessage) return undefined;
28
+ const threadId = m.replyToMessage?.threadId;
29
+ return threadId ?? undefined;
30
+ }
31
+
32
+ function mediaTag(m: Message): string | undefined {
33
+ if (m.media === null) return undefined;
34
+ return `[${m.media.type}]`;
35
+ }
36
+
37
+ function projectText(m: Message): string {
38
+ const tag = mediaTag(m);
39
+ return [m.text, tag].filter(Boolean).join(' ');
40
+ }
41
+
42
+ export function isOwnEcho(m: Message): boolean {
43
+ if (m.isOutgoing) return true;
44
+ const { sender } = m;
45
+ return isUser(sender) && sender.isSelf;
46
+ }
47
+
48
+ export function envelope(
49
+ accountId: string,
50
+ m: Message,
51
+ ): Record<string, unknown> {
52
+ const topicId = topicOf(m);
53
+ const line = lineOf(accountId, m.chat.id, topicId);
54
+ const isPrivate = isUser(m.chat);
55
+ const { media } = m;
56
+ const attachments =
57
+ media !== null && isDownloadable(media)
58
+ ? [pendingDescriptorOf(media)]
59
+ : undefined;
60
+ return {
61
+ kind: 'inbound',
62
+ id: mintId(),
63
+ ts: m.date.toISOString(),
64
+ station: 'telegram-user',
65
+ line,
66
+ line_name: chatName(m.chat),
67
+ from: `metro://telegram-user/${accountId}/user/${m.sender.id}`,
68
+ from_name: senderName(m.sender),
69
+ from_display_name: displayName(m.sender),
70
+ message_id: String(m.id),
71
+ text: projectText(m),
72
+ is_private: isPrivate,
73
+ has_media: media !== null,
74
+ payload: {
75
+ account: accountId,
76
+ message_id: String(m.id),
77
+ ...(attachments ? { attachments } : {}),
78
+ },
79
+ };
80
+ }
81
+
82
+ export function attachmentSavedEnvelope(
83
+ accountId: string,
84
+ line: string,
85
+ sourceEnvId: string,
86
+ saved: SavedAttachment,
87
+ index = 0,
88
+ ): Record<string, unknown> {
89
+ return {
90
+ kind: 'inbound',
91
+ id: mintId(),
92
+ ts: new Date().toISOString(),
93
+ station: 'telegram-user',
94
+ line,
95
+ from: `metro://telegram-user/${accountId}/self`,
96
+ text: `📎 saved: ${saved.path}`,
97
+ payload: {
98
+ account: accountId,
99
+ contentType: 'attachmentSaved',
100
+ attachmentFor: sourceEnvId,
101
+ index,
102
+ attachmentPath: saved.path,
103
+ localPath: saved.path,
104
+ mime: saved.mime,
105
+ name: saved.name,
106
+ },
107
+ };
108
+ }
109
+
110
+ export interface ReactionInput {
111
+ accountId: string;
112
+ chatId: number;
113
+ messageId: number;
114
+ emoji: string;
115
+ senderId: number;
116
+ date: Date;
117
+ isPrivate: boolean;
118
+ senderName?: string;
119
+ removed?: boolean;
120
+ }
121
+
122
+ export function reactionEnvelope(r: ReactionInput): Record<string, unknown> {
123
+ return {
124
+ kind: 'react',
125
+ id: mintId(),
126
+ ts: r.date.toISOString(),
127
+ station: 'telegram-user',
128
+ line: lineOf(r.accountId, r.chatId),
129
+ from: `metro://telegram-user/${r.accountId}/user/${r.senderId}`,
130
+ from_name: r.senderName,
131
+ message_id: String(r.messageId),
132
+ emoji: r.emoji,
133
+ event: { type: 'react', emoji: r.emoji, targetId: String(r.messageId) },
134
+ is_private: r.isPrivate,
135
+ payload: {
136
+ account: r.accountId,
137
+ message_id: String(r.messageId),
138
+ removed: r.removed === true,
139
+ },
140
+ };
141
+ }
@@ -0,0 +1,261 @@
1
+ import type {
2
+ GroupResult,
3
+ MemberOutcome,
4
+ } from '@metro-labs/mcp/stations/types';
5
+ import { TrainError } from '@metro-labs/mcp/train-error';
6
+ import { errMsg } from '@metro-labs/mcp/log';
7
+ import type { UserClient } from './client.js';
8
+ import { lineOf } from './accounts.js';
9
+
10
+ export interface MissingInvitee {
11
+ userId: number;
12
+ }
13
+
14
+ interface CreateGroupResponse {
15
+ chat: { id: number };
16
+ missing: MissingInvitee[];
17
+ }
18
+
19
+ interface InviteLink {
20
+ link: string;
21
+ }
22
+
23
+ interface TgGroup {
24
+ createGroup: (p: {
25
+ title: string;
26
+ users: string[];
27
+ }) => Promise<CreateGroupResponse>;
28
+ addChatMembers: (
29
+ chatId: number,
30
+ users: string[],
31
+ params: { forwardCount: number },
32
+ ) => Promise<MissingInvitee[]>;
33
+ kickChatMember: (p: { chatId: number; userId: string }) => Promise<unknown>;
34
+ exportInviteLink: (chatId: number) => Promise<InviteLink>;
35
+ resolvePeer: (peer: string) => Promise<{
36
+ userId?: number;
37
+ channelId?: number;
38
+ chatId?: number;
39
+ }>;
40
+ }
41
+
42
+ const tgGroup = (client: UserClient): TgGroup => {
43
+ const tg: unknown = client.tg;
44
+ return tg as TgGroup;
45
+ };
46
+
47
+ const INVITE_REASON =
48
+ 'could not be direct-added (not a mutual contact or privacy-restricted); share the invite link';
49
+
50
+ const ADD_FAILURE_CODES = [
51
+ 'CHAT_MEMBER_ADD_FAILED',
52
+ 'USER_PRIVACY_RESTRICTED',
53
+ 'USER_NOT_MUTUAL_CONTACT',
54
+ 'USER_CHANNELS_TOO_MUCH',
55
+ 'USER_ALREADY_PARTICIPANT',
56
+ 'USER_BLOCKED',
57
+ 'USER_BOT',
58
+ 'USER_KICKED',
59
+ 'PEER_FLOOD',
60
+ 'USERS_TOO_MUCH',
61
+ ];
62
+
63
+ function isAddFailure(e: unknown): boolean {
64
+ const msg = errMsg(e).toUpperCase();
65
+ return ADD_FAILURE_CODES.some((code) => msg.includes(code));
66
+ }
67
+
68
+ const added = (id: string): MemberOutcome => ({ id, status: 'added' });
69
+ const invited = (id: string): MemberOutcome => ({
70
+ id,
71
+ status: 'invited',
72
+ reason: INVITE_REASON,
73
+ });
74
+
75
+ function membersOf(args: Record<string, unknown>): string[] {
76
+ const raw = args.members;
77
+ if (!Array.isArray(raw)) return [];
78
+ return raw.filter((v) => typeof v === 'string' && v.length > 0) as string[];
79
+ }
80
+
81
+ async function addOne(
82
+ client: UserClient,
83
+ chatId: number,
84
+ member: string,
85
+ ): Promise<MemberOutcome> {
86
+ try {
87
+ const missing = await tgGroup(client).addChatMembers(chatId, [member], {
88
+ forwardCount: 0,
89
+ });
90
+ return missing.length ? invited(member) : added(member);
91
+ } catch (e) {
92
+ if (isAddFailure(e)) return invited(member);
93
+ throw e;
94
+ }
95
+ }
96
+
97
+ interface CreatedChat {
98
+ chatId: number;
99
+ outcomes: MemberOutcome[];
100
+ }
101
+
102
+ async function createWithAll(
103
+ client: UserClient,
104
+ title: string,
105
+ members: string[],
106
+ ): Promise<CreatedChat> {
107
+ const res = await tgGroup(client).createGroup({ title, users: members });
108
+ const missing = new Set(res.missing.map((m) => m.userId));
109
+ const outcomes: MemberOutcome[] = [];
110
+ for (const m of members) {
111
+ const uid = await idOf(client, m);
112
+ outcomes.push(
113
+ uid !== null && missing.has(Number(uid)) ? invited(m) : added(m),
114
+ );
115
+ }
116
+ return { chatId: res.chat.id, outcomes };
117
+ }
118
+
119
+ async function createPerMember(
120
+ client: UserClient,
121
+ title: string,
122
+ members: string[],
123
+ ): Promise<CreatedChat> {
124
+ let chatId: number | null = null;
125
+ const outcomes: MemberOutcome[] = [];
126
+ const pending: string[] = [];
127
+ for (const m of members) {
128
+ if (chatId !== null) {
129
+ pending.push(m);
130
+ continue;
131
+ }
132
+ try {
133
+ const res = await tgGroup(client).createGroup({ title, users: [m] });
134
+ chatId = res.chat.id;
135
+ outcomes.push(res.missing.length ? invited(m) : added(m));
136
+ } catch (e) {
137
+ if (!isAddFailure(e)) throw e;
138
+ outcomes.push(invited(m));
139
+ }
140
+ }
141
+ if (chatId === null)
142
+ throw new TrainError(
143
+ 'telegram_user_call',
144
+ 'could not create group: no requested member could be added',
145
+ );
146
+ for (const m of pending) outcomes.push(await addOne(client, chatId, m));
147
+ return { chatId, outcomes };
148
+ }
149
+
150
+ async function createChat(
151
+ client: UserClient,
152
+ title: string,
153
+ members: string[],
154
+ ): Promise<CreatedChat> {
155
+ try {
156
+ return await createWithAll(client, title, members);
157
+ } catch (e) {
158
+ if (!isAddFailure(e)) throw e;
159
+ return createPerMember(client, title, members);
160
+ }
161
+ }
162
+
163
+ async function finalize(
164
+ client: UserClient,
165
+ base: GroupResult,
166
+ chatId: number,
167
+ outcomes: MemberOutcome[],
168
+ ): Promise<GroupResult> {
169
+ const result: GroupResult = { ...base, members: outcomes };
170
+ if (outcomes.some((o) => o.status === 'invited'))
171
+ result.inviteLink = (await tgGroup(client).exportInviteLink(chatId)).link;
172
+ return result;
173
+ }
174
+
175
+ async function idOf(client: UserClient, member: string): Promise<string | null> {
176
+ const trimmed = member.replace(/^@/, '');
177
+ if (/^-?\d+$/.test(trimmed)) return trimmed;
178
+ try {
179
+ const peer = await tgGroup(client).resolvePeer(member);
180
+ const uid = peer.userId ?? peer.channelId ?? peer.chatId;
181
+ return uid !== undefined ? String(uid) : null;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ export async function groupCreate(
188
+ client: UserClient,
189
+ accountId: string,
190
+ args: Record<string, unknown>,
191
+ ): Promise<GroupResult> {
192
+ const name = typeof args.name === 'string' ? args.name : '';
193
+ if (!name) throw new TrainError('bad_request', 'groupCreate requires a `name`');
194
+ const members = membersOf(args);
195
+ if (!members.length)
196
+ throw new TrainError('bad_request', 'groupCreate requires `members`');
197
+ const { chatId, outcomes } = await createChat(client, name, members);
198
+ return finalize(
199
+ client,
200
+ {
201
+ capability: { supported: true },
202
+ line: lineOf(accountId, chatId),
203
+ id: String(chatId),
204
+ name,
205
+ },
206
+ chatId,
207
+ outcomes,
208
+ );
209
+ }
210
+
211
+ export async function groupAddMembers(
212
+ client: UserClient,
213
+ chatId: number,
214
+ line: string,
215
+ args: Record<string, unknown>,
216
+ ): Promise<GroupResult> {
217
+ const members = membersOf(args);
218
+ if (!members.length)
219
+ throw new TrainError('bad_request', 'groupAddMembers requires `members`');
220
+ const outcomes: MemberOutcome[] = [];
221
+ for (const m of members) outcomes.push(await addOne(client, chatId, m));
222
+ return finalize(
223
+ client,
224
+ { capability: { supported: true }, line, id: String(chatId) },
225
+ chatId,
226
+ outcomes,
227
+ );
228
+ }
229
+
230
+ export async function groupRemoveMembers(
231
+ client: UserClient,
232
+ chatId: number,
233
+ line: string,
234
+ args: Record<string, unknown>,
235
+ ): Promise<GroupResult> {
236
+ const members = membersOf(args);
237
+ if (!members.length)
238
+ throw new TrainError('bad_request', 'groupRemoveMembers requires `members`');
239
+ for (const m of members)
240
+ await tgGroup(client).kickChatMember({ chatId, userId: m });
241
+ return {
242
+ capability: { supported: true },
243
+ line,
244
+ id: String(chatId),
245
+ members: members.map((id) => ({ id, status: 'removed' as const })),
246
+ };
247
+ }
248
+
249
+ export async function groupInviteLink(
250
+ client: UserClient,
251
+ chatId: number,
252
+ line: string,
253
+ ): Promise<GroupResult> {
254
+ const link = await tgGroup(client).exportInviteLink(chatId);
255
+ return {
256
+ capability: { supported: true },
257
+ line,
258
+ id: String(chatId),
259
+ inviteLink: link.link,
260
+ };
261
+ }
@@ -0,0 +1,52 @@
1
+ import type { Message } from '@mtcute/bun';
2
+ import { lineOf } from './accounts.js';
3
+ import { displayName, senderName } from './format.js';
4
+
5
+ const DEFAULT_LIMIT = 20;
6
+ const MAX_LIMIT = 100;
7
+
8
+ export function clampLimit(limit: unknown): number {
9
+ const n = typeof limit === 'number' ? limit : Number(limit);
10
+ if (!Number.isFinite(n)) return DEFAULT_LIMIT;
11
+ return Math.min(Math.max(1, Math.trunc(n)), MAX_LIMIT);
12
+ }
13
+
14
+ function senderUri(accountId: string, m: Message): string {
15
+ return `metro://telegram-user/${accountId}/user/${m.sender.id}`;
16
+ }
17
+
18
+ function textOf(m: Message): string {
19
+ if (m.text) return m.text;
20
+ if (m.media !== null) return `[${m.media.type}]`;
21
+ return '';
22
+ }
23
+
24
+ export interface ReadMessage {
25
+ id: string;
26
+ ts: string;
27
+ from: string;
28
+ from_name?: string;
29
+ from_display_name?: string;
30
+ text: string;
31
+ }
32
+
33
+ export function shapeHistory(
34
+ accountId: string,
35
+ chatId: number,
36
+ messages: readonly Message[],
37
+ ): { line: string; count: number; messages: ReadMessage[] } {
38
+ const ordered = [...messages].reverse();
39
+ const shaped = ordered.map((m) => ({
40
+ id: String(m.id),
41
+ ts: m.date.toISOString(),
42
+ from: senderUri(accountId, m),
43
+ from_name: senderName(m.sender),
44
+ from_display_name: displayName(m.sender),
45
+ text: textOf(m),
46
+ }));
47
+ return {
48
+ line: lineOf(accountId, chatId),
49
+ count: shaped.length,
50
+ messages: shaped,
51
+ };
52
+ }
@@ -0,0 +1,78 @@
1
+ import type { Message } from '@mtcute/bun';
2
+ import { errMsg, log } from '@metro-labs/mcp/log';
3
+ import { emit } from './wire.js';
4
+ import { envelope, isOwnEcho, attachmentSavedEnvelope } from './format.js';
5
+ import { downloadMedia, isDownloadable } from './attachments.js';
6
+ import { subscribeReactions } from './reactions.js';
7
+ import type { UserClient } from './client.js';
8
+
9
+ async function saveMediaAndEmit(
10
+ client: UserClient,
11
+ m: Message,
12
+ env: Record<string, unknown>,
13
+ ): Promise<void> {
14
+ const { media } = m;
15
+ if (media === null || !isDownloadable(media)) return;
16
+ const accountId = client.account.id;
17
+ try {
18
+ const saved = await downloadMedia(client, media, String(m.id), 0);
19
+ emit(
20
+ attachmentSavedEnvelope(
21
+ accountId,
22
+ env.line as string,
23
+ env.id as string,
24
+ saved,
25
+ ),
26
+ );
27
+ } catch (e) {
28
+ process.stderr.write(
29
+ `telegram-user[${accountId}] media save failed: ${errMsg(e)}\n`,
30
+ );
31
+ }
32
+ }
33
+
34
+ function emitMessage(client: UserClient, m: Message): void {
35
+ log.debug(
36
+ { train: 'telegram-user', account: client.account.id, messageId: m.id },
37
+ 'inbound message received',
38
+ );
39
+ if (isOwnEcho(m)) return;
40
+ const env = envelope(client.account.id, m);
41
+ emit(env);
42
+ if (m.media !== null)
43
+ saveMediaAndEmit(client, m, env).catch((err: unknown) => {
44
+ process.stderr.write(
45
+ `telegram-user[${client.account.id}] media save failed: ${errMsg(err)}\n`,
46
+ );
47
+ });
48
+ }
49
+
50
+ function subscribe(client: UserClient): void {
51
+ const accountId = client.account.id;
52
+ const onMessage = (m: Message): void => {
53
+ try {
54
+ emitMessage(client, m);
55
+ } catch (e) {
56
+ process.stderr.write(
57
+ `telegram-user[${accountId}] normalize failed: ${errMsg(e)}\n`,
58
+ );
59
+ }
60
+ };
61
+ client.tg.onNewMessage.add(onMessage);
62
+ client.tg.onEditMessage.add(onMessage);
63
+ subscribeReactions(client);
64
+ }
65
+
66
+ export async function startInbound(client: UserClient): Promise<void> {
67
+ const accountId = client.account.id;
68
+ try {
69
+ subscribe(client);
70
+ await client.connect();
71
+ await client.startUpdates();
72
+ process.stderr.write(`telegram-user[${accountId}] inbound connected\n`);
73
+ } catch (e) {
74
+ process.stderr.write(
75
+ `telegram-user[${accountId}] connect failed: ${errMsg(e)}\n`,
76
+ );
77
+ }
78
+ }